From 8c0064a3230629ab1ff1c5240179922f645b8715 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 11:58:49 -0400 Subject: [PATCH 001/607] DataTreeWidget uses TableWidget to represent arrays --- examples/DataTreeWidget.py | 2 +- pyqtgraph/widgets/DataTreeWidget.py | 34 ++++++++++------------------- 2 files changed, 13 insertions(+), 23 deletions(-) diff --git a/examples/DataTreeWidget.py b/examples/DataTreeWidget.py index 8365db2a..2c2b00f9 100644 --- a/examples/DataTreeWidget.py +++ b/examples/DataTreeWidget.py @@ -19,7 +19,7 @@ d = { 'y': 2, 'z': 'three' }, - 'array1 (20x20)': np.ones((10,10)) + 'array1 (40x10)': np.random.randint(10, size=(40,10)) } tree = pg.DataTreeWidget(data=d) diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index 29e60319..b8215b06 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from ..pgcollections import OrderedDict +from .TableWidget import TableWidget import types, traceback import numpy as np @@ -29,12 +30,8 @@ class DataTreeWidget(QtGui.QTreeWidget): def setData(self, data, hideRoot=False): """data should be a dictionary.""" self.clear() + self.tables = [] self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) - #node = self.mkNode('', data) - #while node.childCount() > 0: - #c = node.child(0) - #node.removeChild(c) - #self.invisibleRootItem().addChild(c) self.expandToDepth(3) self.resizeColumnToContents(0) @@ -62,22 +59,15 @@ class DataTreeWidget(QtGui.QTreeWidget): elif isinstance(data, list) or isinstance(data, tuple): for i in range(len(data)): self.buildTree(data[i], node, str(i)) + elif isinstance(data, np.ndarray): + desc = "<%s shape=%s dtype=%s>" % (data.__class__.__name__, data.shape, data.dtype) + node.setText(2, desc) + subnode = QtGui.QTreeWidgetItem(["", "", ""]) + node.addChild(subnode) + table = TableWidget() + table.setData(data) + table.setMaximumHeight(200) + self.setItemWidget(subnode, 2, table) + self.tables.append(table) else: node.setText(2, str(data)) - - - #def mkNode(self, name, v): - #if type(v) is list and len(v) > 0 and isinstance(v[0], dict): - #inds = map(unicode, range(len(v))) - #v = OrderedDict(zip(inds, v)) - #if isinstance(v, dict): - ##print "\nadd tree", k, v - #node = QtGui.QTreeWidgetItem([name]) - #for k in v: - #newNode = self.mkNode(k, v[k]) - #node.addChild(newNode) - #else: - ##print "\nadd value", k, str(v) - #node = QtGui.QTreeWidgetItem([unicode(name), unicode(v)]) - #return node - From cabd9d6bf2ec61701bdc3ab927d1e9444f09e9df Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 15:21:28 -0400 Subject: [PATCH 002/607] Cleanup, better support for tracebacks in DataTreeWidget --- examples/DataTreeWidget.py | 20 +++++- pyqtgraph/widgets/DataTreeWidget.py | 96 ++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/examples/DataTreeWidget.py b/examples/DataTreeWidget.py index 2c2b00f9..70ac49bd 100644 --- a/examples/DataTreeWidget.py +++ b/examples/DataTreeWidget.py @@ -11,15 +11,29 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np +# for generating a traceback object to display +def some_func1(): + return some_func2() +def some_func2(): + try: + raise Exception() + except: + import sys + return sys.exc_info()[2] + + app = QtGui.QApplication([]) d = { - 'list1': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], - 'dict1': { + 'a list': [1,2,3,4,5,6, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], + 'a dict': { 'x': 1, 'y': 2, 'z': 'three' }, - 'array1 (40x10)': np.random.randint(10, size=(40,10)) + 'an array': np.random.randint(10, size=(40,10)), + 'a traceback': some_func1(), + 'a function': some_func1, + 'a class': pg.DataTreeWidget, } tree = pg.DataTreeWidget(data=d) diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index b8215b06..f960cab9 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -2,6 +2,7 @@ from ..Qt import QtGui, QtCore from ..pgcollections import OrderedDict from .TableWidget import TableWidget +from ..python2_3 import asUnicode import types, traceback import numpy as np @@ -18,19 +19,18 @@ class DataTreeWidget(QtGui.QTreeWidget): Widget for displaying hierarchical python data structures (eg, nested dicts, lists, and arrays) """ - - def __init__(self, parent=None, data=None): QtGui.QTreeWidget.__init__(self, parent) self.setVerticalScrollMode(self.ScrollPerPixel) self.setData(data) self.setColumnCount(3) self.setHeaderLabels(['key / index', 'type', 'value']) + self.setAlternatingRowColors(True) def setData(self, data, hideRoot=False): """data should be a dictionary.""" self.clear() - self.tables = [] + self.widgets = [] self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) self.expandToDepth(3) self.resizeColumnToContents(0) @@ -39,35 +39,75 @@ class DataTreeWidget(QtGui.QTreeWidget): if hideRoot: node = parent else: - typeStr = type(data).__name__ - if typeStr == 'instance': - typeStr += ": " + data.__class__.__name__ - node = QtGui.QTreeWidgetItem([name, typeStr, ""]) + node = QtGui.QTreeWidgetItem([name, "", ""]) parent.addChild(node) - - if isinstance(data, types.TracebackType): ## convert traceback to a list of strings - data = list(map(str.strip, traceback.format_list(traceback.extract_tb(data)))) - elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): - data = { - 'data': data.view(np.ndarray), - 'meta': data.infoCopy() - } - - if isinstance(data, dict): - for k in data.keys(): - self.buildTree(data[k], node, str(k)) - elif isinstance(data, list) or isinstance(data, tuple): - for i in range(len(data)): - self.buildTree(data[i], node, str(i)) - elif isinstance(data, np.ndarray): - desc = "<%s shape=%s dtype=%s>" % (data.__class__.__name__, data.shape, data.dtype) - node.setText(2, desc) + + typeStr, desc, childs, widget = self.parse(data) + node.setText(1, typeStr) + node.setText(2, desc) + if widget is not None: + self.widgets.append(widget) subnode = QtGui.QTreeWidgetItem(["", "", ""]) node.addChild(subnode) + self.setItemWidget(subnode, 0, widget) + self.setFirstItemColumnSpanned(subnode, True) + + for name, data in childs.items(): + self.buildTree(data, node, asUnicode(name)) + + def parse(self, data): + """ + Given any python object, return: + * type + * a short string representation + * a dict of sub-objects to be parsed + * optional widget to display as sub-node + """ + # defaults for all objects + typeStr = type(data).__name__ + if typeStr == 'instance': + typeStr += ": " + data.__class__.__name__ + widget = None + desc = "" + childs = {} + + # type-specific changes + if isinstance(data, dict): + desc = "length=%d" % len(data) + if isinstance(data, OrderedDict): + childs = data + else: + childs = OrderedDict(sorted(data.items())) + elif isinstance(data, (list, tuple)): + desc = "length=%d" % len(data) + childs = OrderedDict(enumerate(data)) + elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): + childs = OrderedDict([ + ('data', data.view(np.ndarray)), + ('meta', data.infoCopy()) + ]) + elif isinstance(data, np.ndarray): + desc = "shape=%s dtype=%s" % (data.shape, data.dtype) table = TableWidget() table.setData(data) table.setMaximumHeight(200) - self.setItemWidget(subnode, 2, table) - self.tables.append(table) + widget = table + elif isinstance(data, types.TracebackType): ## convert traceback to a list of strings + frames = list(map(str.strip, traceback.format_list(traceback.extract_tb(data)))) + #childs = OrderedDict([ + #(i, {'file': child[0], 'line': child[1], 'function': child[2], 'code': child[3]}) + #for i, child in enumerate(frames)]) + #childs = OrderedDict([(i, ch) for i,ch in enumerate(frames)]) + widget = QtGui.QPlainTextEdit(asUnicode('\n'.join(frames))) + widget.setMaximumHeight(200) + widget.setReadOnly(True) else: - node.setText(2, str(data)) + desc = asUnicode(data) + if len(desc) > 100: + desc = desc[:97] + '...' + widget = QtGui.QPlainTextEdit(asUnicode(data)) + widget.setMaximumHeight(200) + widget.setReadOnly(True) + + return typeStr, desc, childs, widget + \ No newline at end of file From 4543031ac59eed15adf6a39c82ffb73197a36800 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 25 Sep 2014 17:26:06 -0400 Subject: [PATCH 003/607] Added DiffTreeWidget --- examples/DiffTreeWidget.py | 52 +++++++++ pyqtgraph/__init__.py | 1 + pyqtgraph/widgets/DataTreeWidget.py | 27 +++-- pyqtgraph/widgets/DiffTreeWidget.py | 163 ++++++++++++++++++++++++++++ pyqtgraph/widgets/TableWidget.py | 3 +- 5 files changed, 237 insertions(+), 9 deletions(-) create mode 100644 examples/DiffTreeWidget.py create mode 100644 pyqtgraph/widgets/DiffTreeWidget.py diff --git a/examples/DiffTreeWidget.py b/examples/DiffTreeWidget.py new file mode 100644 index 00000000..fa57a356 --- /dev/null +++ b/examples/DiffTreeWidget.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +""" +Simple use of DiffTreeWidget to display differences between structures of +nested dicts, lists, and arrays. +""" + +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + + +app = QtGui.QApplication([]) +A = { + 'a list': [1,2,2,4,5,6, {'nested1': 'aaaa', 'nested2': 'bbbbb'}, "seven"], + 'a dict': { + 'x': 1, + 'y': 2, + 'z': 'three' + }, + 'an array': np.random.randint(10, size=(40,10)), + #'a traceback': some_func1(), + #'a function': some_func1, + #'a class': pg.DataTreeWidget, +} + +B = { + 'a list': [1,2,3,4,5,5, {'nested1': 'aaaaa', 'nested2': 'bbbbb'}, "seven"], + 'a dict': { + 'x': 2, + 'y': 2, + 'z': 'three', + 'w': 5 + }, + 'another dict': {1:2, 2:3, 3:4}, + 'an array': np.random.randint(10, size=(40,10)), +} + +tree = pg.DiffTreeWidget() +tree.setData(A, B) +tree.show() +tree.setWindowTitle('pyqtgraph example: DiffTreeWidget') +tree.resize(1000, 800) + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() \ No newline at end of file diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f8983455..de9fb278 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -240,6 +240,7 @@ from .widgets.VerticalLabel import * from .widgets.FeedbackButton import * from .widgets.ColorButton import * from .widgets.DataTreeWidget import * +from .widgets.DiffTreeWidget import * from .widgets.GraphicsView import * from .widgets.LayoutWidget import * from .widgets.TableWidget import * diff --git a/pyqtgraph/widgets/DataTreeWidget.py b/pyqtgraph/widgets/DataTreeWidget.py index f960cab9..39cb0d45 100644 --- a/pyqtgraph/widgets/DataTreeWidget.py +++ b/pyqtgraph/widgets/DataTreeWidget.py @@ -31,20 +31,35 @@ class DataTreeWidget(QtGui.QTreeWidget): """data should be a dictionary.""" self.clear() self.widgets = [] + self.nodes = {} self.buildTree(data, self.invisibleRootItem(), hideRoot=hideRoot) self.expandToDepth(3) self.resizeColumnToContents(0) - def buildTree(self, data, parent, name='', hideRoot=False): + def buildTree(self, data, parent, name='', hideRoot=False, path=()): if hideRoot: node = parent else: node = QtGui.QTreeWidgetItem([name, "", ""]) parent.addChild(node) + + # record the path to the node so it can be retrieved later + # (this is used by DiffTreeWidget) + self.nodes[path] = node typeStr, desc, childs, widget = self.parse(data) node.setText(1, typeStr) node.setText(2, desc) + + # Truncate description and add text box if needed + if len(desc) > 100: + desc = desc[:97] + '...' + if widget is None: + widget = QtGui.QPlainTextEdit(asUnicode(data)) + widget.setMaximumHeight(200) + widget.setReadOnly(True) + + # Add widget to new subnode if widget is not None: self.widgets.append(widget) subnode = QtGui.QTreeWidgetItem(["", "", ""]) @@ -52,8 +67,9 @@ class DataTreeWidget(QtGui.QTreeWidget): self.setItemWidget(subnode, 0, widget) self.setFirstItemColumnSpanned(subnode, True) - for name, data in childs.items(): - self.buildTree(data, node, asUnicode(name)) + # recurse to children + for key, data in childs.items(): + self.buildTree(data, node, asUnicode(key), path=path+(key,)) def parse(self, data): """ @@ -103,11 +119,6 @@ class DataTreeWidget(QtGui.QTreeWidget): widget.setReadOnly(True) else: desc = asUnicode(data) - if len(desc) > 100: - desc = desc[:97] + '...' - widget = QtGui.QPlainTextEdit(asUnicode(data)) - widget.setMaximumHeight(200) - widget.setReadOnly(True) return typeStr, desc, childs, widget \ No newline at end of file diff --git a/pyqtgraph/widgets/DiffTreeWidget.py b/pyqtgraph/widgets/DiffTreeWidget.py new file mode 100644 index 00000000..e3869da8 --- /dev/null +++ b/pyqtgraph/widgets/DiffTreeWidget.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +from ..Qt import QtGui, QtCore +from ..pgcollections import OrderedDict +from .DataTreeWidget import DataTreeWidget +from .. import functions as fn +import types, traceback +import numpy as np + +__all__ = ['DiffTreeWidget'] + + +class DiffTreeWidget(QtGui.QWidget): + """ + Widget for displaying differences between hierarchical python data structures + (eg, nested dicts, lists, and arrays) + """ + def __init__(self, parent=None, a=None, b=None): + QtGui.QWidget.__init__(self, parent) + self.layout = QtGui.QHBoxLayout() + self.setLayout(self.layout) + self.trees = [DataTreeWidget(self), DataTreeWidget(self)] + for t in self.trees: + self.layout.addWidget(t) + if a is not None: + self.setData(a, b) + + def setData(self, a, b): + """ + Set the data to be compared in this widget. + """ + self.data = (a, b) + self.trees[0].setData(a) + self.trees[1].setData(b) + + return self.compare(a, b) + + def compare(self, a, b, path=()): + """ + Compare data structure *a* to structure *b*. + + Return True if the objects match completely. + Otherwise, return a structure that describes the differences: + + { 'type': bool + 'len': bool, + 'str': bool, + 'shape': bool, + 'dtype': bool, + 'mask': array, + } + + + """ + bad = (255, 200, 200) + diff = [] + # generate typestr, desc, childs for each object + typeA, descA, childsA, _ = self.trees[0].parse(a) + typeB, descB, childsB, _ = self.trees[1].parse(b) + + if typeA != typeB: + self.setColor(path, 1, bad) + if descA != descB: + self.setColor(path, 2, bad) + + if isinstance(a, dict) and isinstance(b, dict): + keysA = set(a.keys()) + keysB = set(b.keys()) + for key in keysA - keysB: + self.setColor(path+(key,), 0, bad, tree=0) + for key in keysB - keysA: + self.setColor(path+(key,), 0, bad, tree=1) + for key in keysA & keysB: + self.compare(a[key], b[key], path+(key,)) + + elif isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): + for i in range(max(len(a), len(b))): + if len(a) <= i: + self.setColor(path+(i,), 0, bad, tree=1) + elif len(b) <= i: + self.setColor(path+(i,), 0, bad, tree=0) + else: + self.compare(a[i], b[i], path+(i,)) + + elif isinstance(a, np.ndarray) and isinstance(b, np.ndarray) and a.shape == b.shape: + tableNodes = [tree.nodes[path].child(0) for tree in self.trees] + if a.dtype.fields is None and b.dtype.fields is None: + eq = self.compareArrays(a, b) + if not np.all(eq): + for n in tableNodes: + n.setBackground(0, fn.mkBrush(bad)) + #for i in np.argwhere(~eq): + + else: + for i,k in enumerate(info.dtype.fields.keys()): + eq = self.compareArrays(a[k], b[k]) + if not np.all(eq): + for n in tableNodes: + n.setBackground(0, fn.mkBrush(bad)) + #for j in np.argwhere(~eq): + + # dict: compare keys, then values where keys match + # list: + # array: compare elementwise for same shape + + def compareArrays(self, a, b): + intnan = -9223372036854775808 # happens when np.nan is cast to int + anans = np.isnan(a) | (a == intnan) + bnans = np.isnan(b) | (b == intnan) + eq = anans == bnans + mask = ~anans + eq[mask] = np.allclose(a[mask], b[mask]) + return eq + + def setColor(self, path, column, color, tree=None): + brush = fn.mkBrush(color) + + # Color only one tree if specified. + if tree is None: + trees = self.trees + else: + trees = [self.trees[tree]] + + for tree in trees: + item = tree.nodes[path] + item.setBackground(column, brush) + + def _compare(self, a, b): + """ + Compare data structure *a* to structure *b*. + """ + # Check test structures are the same + assert type(info) is type(expect) + if hasattr(info, '__len__'): + assert len(info) == len(expect) + + if isinstance(info, dict): + for k in info: + assert k in expect + for k in expect: + assert k in info + self.compare_results(info[k], expect[k]) + elif isinstance(info, list): + for i in range(len(info)): + self.compare_results(info[i], expect[i]) + elif isinstance(info, np.ndarray): + assert info.shape == expect.shape + assert info.dtype == expect.dtype + if info.dtype.fields is None: + intnan = -9223372036854775808 # happens when np.nan is cast to int + inans = np.isnan(info) | (info == intnan) + enans = np.isnan(expect) | (expect == intnan) + assert np.all(inans == enans) + mask = ~inans + assert np.allclose(info[mask], expect[mask]) + else: + for k in info.dtype.fields.keys(): + self.compare_results(info[k], expect[k]) + else: + try: + assert info == expect + except Exception: + raise NotImplementedError("Cannot compare objects of type %s" % type(info)) + \ No newline at end of file diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 69085a20..db0b6ae2 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -151,7 +151,8 @@ class TableWidget(QtGui.QTableWidget): i += 1 self.setRow(i, [x for x in fn1(row)]) - if self._sorting and self.horizontalHeader().sortIndicatorSection() >= self.columnCount(): + if (self._sorting and self.horizontalHeadersSet and + self.horizontalHeader().sortIndicatorSection() >= self.columnCount()): self.sortByColumn(0, QtCore.Qt.AscendingOrder) def setEditable(self, editable=True): From 9893fc37353a8f1b02637fb4835a9196df52945b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 26 Sep 2014 15:25:05 -0400 Subject: [PATCH 004/607] corrections for 1d arrays --- pyqtgraph/widgets/DiffTreeWidget.py | 13 +++++++------ pyqtgraph/widgets/TableWidget.py | 5 +++++ 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/widgets/DiffTreeWidget.py b/pyqtgraph/widgets/DiffTreeWidget.py index e3869da8..eac29489 100644 --- a/pyqtgraph/widgets/DiffTreeWidget.py +++ b/pyqtgraph/widgets/DiffTreeWidget.py @@ -91,12 +91,13 @@ class DiffTreeWidget(QtGui.QWidget): #for i in np.argwhere(~eq): else: - for i,k in enumerate(info.dtype.fields.keys()): - eq = self.compareArrays(a[k], b[k]) - if not np.all(eq): - for n in tableNodes: - n.setBackground(0, fn.mkBrush(bad)) - #for j in np.argwhere(~eq): + if a.dtype == b.dtype: + for i,k in enumerate(a.dtype.fields.keys()): + eq = self.compareArrays(a[k], b[k]) + if not np.all(eq): + for n in tableNodes: + n.setBackground(0, fn.mkBrush(bad)) + #for j in np.argwhere(~eq): # dict: compare keys, then values where keys match # list: diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index db0b6ae2..9e9f2144 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -222,6 +222,8 @@ class TableWidget(QtGui.QTableWidget): return self.iterate, list(map(asUnicode, data.dtype.names)) elif data is None: return (None,None) + elif np.isscalar(data): + return self.iterateScalar, None else: msg = "Don't know how to iterate over data type: {!s}".format(type(data)) raise TypeError(msg) @@ -236,6 +238,9 @@ class TableWidget(QtGui.QTableWidget): for x in data: yield x + def iterateScalar(self, data): + yield data + def appendRow(self, data): self.appendData([data]) From ef6cd9be88855b62d07def21efd5cf45fef7d926 Mon Sep 17 00:00:00 2001 From: cjtk Date: Wed, 31 Dec 2014 10:32:36 +1100 Subject: [PATCH 005/607] Fix bug in LayoutWidget.py getWidget tries to get self.row which doesn't exist, get self.rows instead --- pyqtgraph/widgets/LayoutWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/LayoutWidget.py b/pyqtgraph/widgets/LayoutWidget.py index 65d04d3f..91cd1600 100644 --- a/pyqtgraph/widgets/LayoutWidget.py +++ b/pyqtgraph/widgets/LayoutWidget.py @@ -75,7 +75,7 @@ class LayoutWidget(QtGui.QWidget): def getWidget(self, row, col): """Return the widget in (*row*, *col*)""" - return self.row[row][col] + return self.rows[row][col] #def itemIndex(self, item): #for i in range(self.layout.count()): From f470a830d0346c8b0dec1574d81d713e862e8aec Mon Sep 17 00:00:00 2001 From: dlidstrom Date: Sat, 14 Mar 2015 17:30:56 -0600 Subject: [PATCH 006/607] 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 96e06f212a8b553035fa91cbbc017237c700cf6c Mon Sep 17 00:00:00 2001 From: dlidstrom Date: Mon, 16 Mar 2015 14:03:58 -0600 Subject: [PATCH 007/607] Provide widgetGroupInterface to GradientWidget --- pyqtgraph/widgets/GradientWidget.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/widgets/GradientWidget.py b/pyqtgraph/widgets/GradientWidget.py index ce0cbeb9..77881b30 100644 --- a/pyqtgraph/widgets/GradientWidget.py +++ b/pyqtgraph/widgets/GradientWidget.py @@ -71,4 +71,7 @@ class GradientWidget(GraphicsView): ### wrap methods from GradientEditorItem return getattr(self.item, attr) + def widgetGroupInterface(self): + return (self.sigGradientChanged, self.saveState, self.restoreState) + From e98f3582a84a4bbfec1eaed3855d5ca72174be77 Mon Sep 17 00:00:00 2001 From: duguxy Date: Sat, 15 Aug 2015 17:12:00 +0800 Subject: [PATCH 008/607] Fix: flowchart saveFile and loadFile in python3 --- pyqtgraph/flowchart/Flowchart.py | 8 ++++---- pyqtgraph/flowchart/Node.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 17e2bde4..53731df0 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -514,7 +514,6 @@ class Flowchart(Node): return ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") - fileName = unicode(fileName) state = configfile.readConfigFile(fileName) self.restoreState(state, clear=True) self.viewBox.autoRange() @@ -535,7 +534,8 @@ class Flowchart(Node): self.fileDialog.fileSelected.connect(self.saveFile) return #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") - fileName = unicode(fileName) + if not fileName.endswith('.fc'): + fileName += '.fc' configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) @@ -660,7 +660,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def fileSaved(self, fileName): - self.setCurrentFile(unicode(fileName)) + self.setCurrentFile(fileName) self.ui.saveBtn.success("Saved.") def saveClicked(self): @@ -689,7 +689,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def setCurrentFile(self, fileName): - self.currentFileName = unicode(fileName) + self.currentFileName = fileName if fileName is None: self.ui.fileNameLabel.setText("[ new ]") else: diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index fc7b04d3..9399fe2e 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -374,7 +374,7 @@ class Node(QtCore.QObject): pos = self.graphicsItem().pos() state = {'pos': (pos.x(), pos.y()), 'bypass': self.isBypassed()} termsEditable = self._allowAddInput | self._allowAddOutput - for term in self._inputs.values() + self._outputs.values(): + for term in list(self._inputs.values()) + list(self._outputs.values()): termsEditable |= term._renamable | term._removable | term._multiable if termsEditable: state['terminals'] = self.saveTerminals() From eb55e439a350473d250bc0a3180f57b008889d0f Mon Sep 17 00:00:00 2001 From: duguxy Date: Fri, 18 Sep 2015 11:49:04 +0800 Subject: [PATCH 009/607] Fix flowchat save load support --- pyqtgraph/flowchart/Flowchart.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 53731df0..c57503f3 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -24,6 +24,7 @@ from .. import configfile as configfile from .. import dockarea as dockarea from . import FlowchartGraphicsView from .. import functions as fn +from ..python2_3 import asUnicode def strDict(d): return dict([(str(k), v) for k, v in d.items()]) @@ -660,7 +661,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def fileSaved(self, fileName): - self.setCurrentFile(fileName) + self.setCurrentFile(asUnicode(fileName)) self.ui.saveBtn.success("Saved.") def saveClicked(self): @@ -689,7 +690,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): #self.setCurrentFile(newFile) def setCurrentFile(self, fileName): - self.currentFileName = fileName + self.currentFileName = asUnicode(fileName) if fileName is None: self.ui.fileNameLabel.setText("[ new ]") else: From 9fa0d0e7244a4f60685e523bb986befe0c5c876c Mon Sep 17 00:00:00 2001 From: duguxy Date: Fri, 18 Sep 2015 19:53:09 +0800 Subject: [PATCH 010/607] Fix flowchart s&l on python2 --- pyqtgraph/flowchart/Flowchart.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index c57503f3..2149a58a 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -515,6 +515,7 @@ class Flowchart(Node): return ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") + fileName = asUnicode(fileName) state = configfile.readConfigFile(fileName) self.restoreState(state, clear=True) self.viewBox.autoRange() @@ -537,6 +538,7 @@ class Flowchart(Node): #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") if not fileName.endswith('.fc'): fileName += '.fc' + fileName = asUnicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) From d461bf866f3abd53484e6334e7bc69e7c36470aa Mon Sep 17 00:00:00 2001 From: dlidstrom Date: Mon, 16 Mar 2015 15:30:15 -0600 Subject: [PATCH 011/607] Add wrapping option to SpinBox --- pyqtgraph/widgets/SpinBox.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index a863cd60..7c3fe256 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -51,6 +51,7 @@ class SpinBox(QtGui.QAbstractSpinBox): value (float/int) initial value. Default is 0.0. bounds (min,max) Minimum and maximum values allowed in the SpinBox. Either may be None to leave the value unbounded. By default, values are unbounded. + wrapping (bool) If True and both bounds are not None, spin box has circular behavior. suffix (str) suffix (units) to display after the numerical value. By default, suffix is an empty str. siPrefix (bool) If True, then an SI prefix is automatically prepended to the units and the value is scaled accordingly. For example, @@ -81,6 +82,7 @@ class SpinBox(QtGui.QAbstractSpinBox): self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.opts = { 'bounds': [None, None], + 'wrapping': False, ## Log scaling options #### Log mode is no longer supported. #'step': 0.1, @@ -205,6 +207,14 @@ class SpinBox(QtGui.QAbstractSpinBox): self.opts['bounds'][0] = m if update: self.setValue() + + def wrapping(self): + """Return whether or not the spin box is circular.""" + return self.opts['wrapping'] + + def setWrapping(self, s): + """Set whether spin box is circular. Both bounds must be set for this to have an effect.""" + self.opts['wrapping'] = s def setPrefix(self, p): """Set a string prefix. @@ -282,10 +292,17 @@ class SpinBox(QtGui.QAbstractSpinBox): value = self.value() bounds = self.opts['bounds'] - if bounds[0] is not None and value < bounds[0]: - value = bounds[0] - if bounds[1] is not None and value > bounds[1]: - value = bounds[1] + + if bounds[0] is not None and bounds[1] is not None and self.opts['wrapping']: + # Casting of Decimals to floats required to avoid unexpected behavior of remainder operator + value = float(value) + l, u = float(bounds[0]), float(bounds[1]) + value = (value - l) % (u - l) + l + else: + if bounds[0] is not None and value < bounds[0]: + value = bounds[0] + if bounds[1] is not None and value > bounds[1]: + value = bounds[1] if self.opts['int']: value = int(value) From 92d8c2630b096cea2a214c685eb36c2412b3f653 Mon Sep 17 00:00:00 2001 From: lidstrom83 Date: Sun, 25 Oct 2015 03:55:37 -0600 Subject: [PATCH 012/607] Add spin box wrapping example. --- examples/SpinBox.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/SpinBox.py b/examples/SpinBox.py index 2fa9b161..268bfa72 100644 --- a/examples/SpinBox.py +++ b/examples/SpinBox.py @@ -31,6 +31,8 @@ spins = [ pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), ("Float with SI-prefixed units,
dec step=1.0, minStep=0.001", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=1.0, minStep=0.001)), + ("Integer with bounds=[10, 20] and wrapping", + pg.SpinBox(value=10, bounds=[10, 20], int=False, minStep=1, step=1, wrapping=True)), ] From 5322c0233b9e28f3f42d5f3e9d29e1868eea221e Mon Sep 17 00:00:00 2001 From: lidstrom83 Date: Tue, 3 May 2016 12:25:05 -0600 Subject: [PATCH 013/607] Fix bug where int and float parameter limits are not always set. --- pyqtgraph/parametertree/parameterTypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index d8a5f1a6..892a228a 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -108,7 +108,7 @@ class WidgetParameterItem(ParameterItem): if k in opts: defs[k] = opts[k] if 'limits' in opts: - defs['bounds'] = opts['limits'] + defs['min'], defs['max'] = opts['limits'] w = SpinBox() w.setOpts(**defs) w.sigChanged = w.sigValueChanged From bc3acdd5fd5a7da448142c32487fe4d16ca2ab27 Mon Sep 17 00:00:00 2001 From: Soloviev Denis Date: Wed, 4 May 2016 21:53:55 +0500 Subject: [PATCH 014/607] fix legendItem drag --- pyqtgraph/graphicsItems/LegendItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 20d6416e..31bcafb6 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -128,6 +128,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): def mouseDragEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: + ev.accept() dpos = ev.pos() - ev.lastPos() self.autoAnchor(self.pos() + dpos) From 0a25fb087488cae38df42d0a1cdb19af3959642b Mon Sep 17 00:00:00 2001 From: Felix Schill Date: Sat, 21 May 2016 17:50:05 +0200 Subject: [PATCH 015/607] clearing _needUpdate flag in GLImageItem to prevent redundant re-upload of textures --- pyqtgraph/opengl/items/GLImageItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 59ddaf6f..56cfaf99 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -73,6 +73,7 @@ class GLImageItem(GLGraphicsItem): def paint(self): if self._needUpdate: self._updateTexture() + self._needUpdate = False glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) From 0e33ddc28b3148589f8a51d714a6ba342c5b24ce Mon Sep 17 00:00:00 2001 From: Nick Irvine Date: Wed, 20 Jul 2016 16:47:07 -0700 Subject: [PATCH 016/607] Allow MetaArray.__array__ to accept an optional dtype arg Fixes #359 --- pyqtgraph/metaarray/MetaArray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 66ecc460..70300c7f 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -358,7 +358,7 @@ class MetaArray(object): else: return np.array(self._data) - def __array__(self): + def __array__(self, dtype=None): ## supports np.array(metaarray_instance) return self.asarray() From 7f0556b05f98b44e87d0bb14d91a8385ffd8a01f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 13 Sep 2016 18:08:11 -0700 Subject: [PATCH 017/607] Implement order=0 in functions.interpolateArray; use scipy only for order>1. --- pyqtgraph/functions.py | 131 ++++++++++++++++++++++++----------------- 1 file changed, 76 insertions(+), 55 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d79c350f..187a4717 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -409,12 +409,45 @@ def eq(a, b): else: raise Exception("== operator returned type %s" % str(type(e))) + +def affineSliceCoords(shape, origin, vectors, axes): + """Return the array of coordinates used to sample data arrays in affineSlice(). + """ + # sanity check + if len(shape) != len(vectors): + raise Exception("shape and vectors must have same length.") + if len(origin) != len(axes): + raise Exception("origin and axes must have same length.") + for v in vectors: + if len(v) != len(axes): + raise Exception("each vector must be same length as axes.") + + shape = list(map(np.ceil, shape)) + + ## make sure vectors are arrays + if not isinstance(vectors, np.ndarray): + vectors = np.array(vectors) + if not isinstance(origin, np.ndarray): + origin = np.array(origin) + origin.shape = (len(axes),) + (1,)*len(shape) + + ## Build array of sample locations. + grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes + x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic + x += origin + + return x + def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, **kargs): """ - Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays such as MRI images for viewing as 1D or 2D data. + Take a slice of any orientation through an array. This is useful for extracting sections of multi-dimensional arrays + such as MRI images for viewing as 1D or 2D data. - The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger datasets. The original data is interpolated onto a new array of coordinates using scipy.ndimage.map_coordinates if it is available (see the scipy documentation for more information about this). If scipy is not available, then a slower implementation of map_coordinates is used. + The slicing axes are aribtrary; they do not need to be orthogonal to the original data or even to each other. It is + possible to use this function to extract arbitrary linear, rectangular, or parallelepiped shapes from within larger + datasets. The original data is interpolated onto a new array of coordinates using either interpolateArray if order<2 + or scipy.ndimage.map_coordinates otherwise. For a graphical interface to this function, see :func:`ROI.getArrayRegion ` @@ -453,47 +486,24 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, affineSlice(data, shape=(20,20), origin=(40,0,0), vectors=((-1, 1, 0), (-1, 0, 1)), axes=(1,2,3)) """ - try: - import scipy.ndimage - have_scipy = True - except ImportError: - have_scipy = False - have_scipy = False - - # sanity check - if len(shape) != len(vectors): - raise Exception("shape and vectors must have same length.") - if len(origin) != len(axes): - raise Exception("origin and axes must have same length.") - for v in vectors: - if len(v) != len(axes): - raise Exception("each vector must be same length as axes.") - - shape = list(map(np.ceil, shape)) + x = affineSliceCoords(shape, origin, vectors, axes) ## transpose data so slice axes come first trAx = list(range(data.ndim)) - for x in axes: - trAx.remove(x) + for ax in axes: + trAx.remove(ax) tr1 = tuple(axes) + tuple(trAx) data = data.transpose(tr1) #print "tr1:", tr1 ## dims are now [(slice axes), (other axes)] - - ## make sure vectors are arrays - if not isinstance(vectors, np.ndarray): - vectors = np.array(vectors) - if not isinstance(origin, np.ndarray): - origin = np.array(origin) - origin.shape = (len(axes),) + (1,)*len(shape) - - ## Build array of sample locations. - grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes - x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic - x += origin - ## iterate manually over unused axes since map_coordinates won't do it for us - if have_scipy: + if order > 1: + try: + import scipy.ndimage + except ImportError: + raise ImportError("Interpolating with order > 1 requires the scipy.ndimage module, but it could not be imported.") + + # iterate manually over unused axes since map_coordinates won't do it for us extraShape = data.shape[len(axes):] output = np.empty(tuple(shape) + extraShape, dtype=data.dtype) for inds in np.ndindex(*extraShape): @@ -502,8 +512,8 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, else: # map_coordinates expects the indexes as the first axis, whereas # interpolateArray expects indexes at the last axis. - tr = tuple(range(1,x.ndim)) + (0,) - output = interpolateArray(data, x.transpose(tr)) + tr = tuple(range(1, x.ndim)) + (0,) + output = interpolateArray(data, x.transpose(tr), order=order) tr = list(range(output.ndim)) trb = [] @@ -520,16 +530,21 @@ def affineSlice(data, shape, origin, vectors, axes, order=1, returnCoords=False, else: return output -def interpolateArray(data, x, default=0.0): + +def interpolateArray(data, x, default=0.0, order=1): """ N-dimensional interpolation similar to scipy.ndimage.map_coordinates. This function returns linearly-interpolated values sampled from a regular grid of data. - *data* is an array of any shape containing the values to be interpolated. - *x* is an array with (shape[-1] <= data.ndim) containing the locations - within *data* to interpolate. + ============== =========================================================================================== + **Arguments:** + *data* Array of any shape containing the values to be interpolated. + *x* Array with (shape[-1] <= data.ndim) containing the locations within *data* to interpolate. + *default* Value to return for locations in *x* that are outside the bounds of *data*. + *order* Order of interpolation: 0=nearest, 1=linear. + ============== =========================================================================================== Returns array of shape (x.shape[:-1] + data.shape[x.shape[-1]:]) @@ -574,8 +589,11 @@ def interpolateArray(data, x, default=0.0): This is useful for interpolating from arrays of colors, vertexes, etc. """ + if order not in (0, 1): + raise ValueError("interpolateArray requires order=0 or 1 (got %s)" % order) + prof = debug.Profiler() - + nd = data.ndim md = x.shape[-1] if md > nd: @@ -583,7 +601,7 @@ def interpolateArray(data, x, default=0.0): # First we generate arrays of indexes that are needed to # extract the data surrounding each point - fields = np.mgrid[(slice(0,2),) * md] + fields = np.mgrid[(slice(0,order+1),) * md] xmin = np.floor(x).astype(int) xmax = xmin + 1 indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]]) @@ -609,18 +627,21 @@ def interpolateArray(data, x, default=0.0): prof() ## Interpolate - s = np.empty((md,) + fieldData.shape, dtype=float) - dx = x - xmin - # reshape fields for arithmetic against dx - for ax in range(md): - f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1)) - sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax]) - sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim)) - s[ax] = sax - s = np.product(s, axis=0) - result = fieldData * s - for i in range(md): - result = result.sum(axis=0) + if order == 0: + result = fieldData[0,0] + else: + s = np.empty((md,) + fieldData.shape, dtype=float) + dx = x - xmin + # reshape fields for arithmetic against dx + for ax in range(md): + f1 = fields[ax].reshape(fields[ax].shape + (1,)*(dx.ndim-1)) + sax = f1 * dx[...,ax] + (1-f1) * (1-dx[...,ax]) + sax = sax.reshape(sax.shape + (1,) * (s.ndim-1-sax.ndim)) + s[ax] = sax + s = np.product(s, axis=0) + result = fieldData * s + for i in range(md): + result = result.sum(axis=0) prof() From be07979b3904b84e584e992b7b545d654f7c1c58 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 16 Sep 2016 17:16:16 -0700 Subject: [PATCH 018/607] Add returnMappedCoords option to LineSegmentROI.getArrayRegion --- pyqtgraph/graphicsItems/ROI.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 81a4e651..963ecb05 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2070,9 +2070,9 @@ class LineSegmentROI(ROI): if len(positions) > 2: raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.") + self.endpoints = [] for i, p in enumerate(positions): - self.addFreeHandle(p, item=handles[i]) - + self.endpoints.append(self.addFreeHandle(p, item=handles[i])) def listPoints(self): return [p['item'].pos() for p in self.handles] @@ -2080,8 +2080,8 @@ class LineSegmentROI(ROI): def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) p.setPen(self.currentPen) - h1 = self.handles[0]['item'].pos() - h2 = self.handles[1]['item'].pos() + h1 = self.endpoints[0].pos() + h2 = self.endpoints[1].pos() p.drawLine(h1, h2) def boundingRect(self): @@ -2090,8 +2090,8 @@ class LineSegmentROI(ROI): def shape(self): p = QtGui.QPainterPath() - h1 = self.handles[0]['item'].pos() - h2 = self.handles[1]['item'].pos() + h1 = self.endpoints[0].pos() + h2 = self.endpoints[1].pos() dh = h2-h1 if dh.length() == 0: return p @@ -2109,7 +2109,7 @@ class LineSegmentROI(ROI): return p - def getArrayRegion(self, data, img, axes=(0,1), order=1, **kwds): + def getArrayRegion(self, data, img, axes=(0,1), order=1, returnMappedCoords=False, **kwds): """ Use the position of this ROI relative to an imageItem to pull a slice from an array. @@ -2120,15 +2120,15 @@ class LineSegmentROI(ROI): See ROI.getArrayRegion() for a description of the arguments. """ - imgPts = [self.mapToItem(img, h['item'].pos()) for h in self.handles] + imgPts = [self.mapToItem(img, h.pos()) for h in self.endpoints] rgns = [] - for i in range(len(imgPts)-1): - d = Point(imgPts[i+1] - imgPts[i]) - o = Point(imgPts[i]) - r = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=order, **kwds) - rgns.append(r) - - return np.concatenate(rgns, axis=axes[0]) + coords = [] + + d = Point(imgPts[1] - imgPts[0]) + o = Point(imgPts[0]) + rgn = fn.affineSlice(data, shape=(int(d.length()),), vectors=[Point(d.norm())], origin=o, axes=axes, order=order, returnCoords=returnMappedCoords, **kwds) + + return rgn class _PolyLineSegment(LineSegmentROI): From 8bdc19be75a7552cc0043bf8b5f5e0ee796edda0 Mon Sep 17 00:00:00 2001 From: Max Peng Date: Sun, 18 Sep 2016 16:20:24 +0800 Subject: [PATCH 019/607] update to support pyside2 --- examples/ScatterPlotSpeedTest.py | 4 +- examples/VideoSpeedTest.py | 4 +- examples/__main__.py | 4 +- examples/exampleLoaderTemplate_pyqt.py | 3 +- examples/exampleLoaderTemplate_pyside2.py | 93 +++++++++ pyqtgraph/GraphicsScene/exportDialog.py | 4 +- .../exportDialogTemplate_pyside2.py | 64 +++++++ pyqtgraph/Qt.py | 176 ++++++++++++++---- pyqtgraph/canvas/Canvas.py | 6 +- pyqtgraph/canvas/CanvasItem.py | 6 +- pyqtgraph/canvas/CanvasTemplate_pyside2.py | 96 ++++++++++ .../canvas/TransformGuiTemplate_pyside2.py | 56 ++++++ pyqtgraph/console/Console.py | 4 +- pyqtgraph/console/template_pyside2.py | 107 +++++++++++ pyqtgraph/flowchart/Flowchart.py | 5 +- .../FlowchartCtrlTemplate_pyside2.py | 67 +++++++ .../flowchart/FlowchartTemplate_pyside2.py | 55 ++++++ pyqtgraph/functions.py | 6 +- pyqtgraph/graphicsItems/FillBetweenItem.py | 2 +- pyqtgraph/graphicsItems/GraphicsObject.py | 6 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 + .../PlotItem/plotConfigTemplate_pyside2.py | 169 +++++++++++++++++ pyqtgraph/graphicsItems/ScatterPlotItem.py | 4 +- pyqtgraph/graphicsItems/UIGraphicsItem.py | 6 +- .../graphicsItems/ViewBox/ViewBoxMenu.py | 4 +- .../ViewBox/axisCtrlTemplate_pyside2.py | 89 +++++++++ pyqtgraph/imageview/ImageView.py | 6 +- .../imageview/ImageViewTemplate_pyqt5.py | 14 +- .../imageview/ImageViewTemplate_pyside2.py | 156 ++++++++++++++++ pyqtgraph/widgets/RemoteGraphicsView.py | 4 +- 30 files changed, 1155 insertions(+), 67 deletions(-) create mode 100644 examples/exampleLoaderTemplate_pyside2.py create mode 100644 pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py create mode 100644 pyqtgraph/canvas/CanvasTemplate_pyside2.py create mode 100644 pyqtgraph/canvas/TransformGuiTemplate_pyside2.py create mode 100644 pyqtgraph/console/template_pyside2.py create mode 100644 pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py create mode 100644 pyqtgraph/flowchart/FlowchartTemplate_pyside2.py create mode 100644 pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside2.py create mode 100644 pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside2.py create mode 100644 pyqtgraph/imageview/ImageViewTemplate_pyside2.py diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py index 9cbf0c63..cc6818cb 100644 --- a/examples/ScatterPlotSpeedTest.py +++ b/examples/ScatterPlotSpeedTest.py @@ -12,7 +12,7 @@ For testing rapid updates of ScatterPlotItem under various conditions. import initExample -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE, USE_PYQT5 +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 import numpy as np import pyqtgraph as pg from pyqtgraph.ptime import time @@ -22,6 +22,8 @@ app = QtGui.QApplication([]) #mw.resize(800,800) if USE_PYSIDE: from ScatterPlotSpeedTestTemplate_pyside import Ui_Form +elif USE_PYSIDE2: + from ScatterPlotSpeedTestTemplate_pyside2 import Ui_Form elif USE_PYQT5: from ScatterPlotSpeedTestTemplate_pyqt5 import Ui_Form else: diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index 3516472f..e0150f22 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -10,13 +10,15 @@ is used by the view widget import initExample ## Add path to library (just for examples; you do not need this) -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE, USE_PYQT5 +from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 import numpy as np import pyqtgraph as pg import pyqtgraph.ptime as ptime if USE_PYSIDE: import VideoTemplate_pyside as VideoTemplate +elif USE_PYSIDE2: + import VideoTemplate_pyside2 as VideoTemplate elif USE_PYQT5: import VideoTemplate_pyqt5 as VideoTemplate else: diff --git a/examples/__main__.py b/examples/__main__.py index 03c41119..8445a21b 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -7,13 +7,15 @@ if __name__ == "__main__" and (__package__ is None or __package__==''): import pyqtgraph as pg import subprocess from pyqtgraph.python2_3 import basestring -from pyqtgraph.Qt import QtGui, USE_PYSIDE, USE_PYQT5 +from pyqtgraph.Qt import QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 from .utils import buildFileList, testFile, path, examples if USE_PYSIDE: from .exampleLoaderTemplate_pyside import Ui_Form +elif USE_PYSIDE2: + from .exampleLoaderTemplate_pyside2 import Ui_Form elif USE_PYQT5: from .exampleLoaderTemplate_pyqt5 import Ui_Form else: diff --git a/examples/exampleLoaderTemplate_pyqt.py b/examples/exampleLoaderTemplate_pyqt.py index 708839f5..6c791e7f 100644 --- a/examples/exampleLoaderTemplate_pyqt.py +++ b/examples/exampleLoaderTemplate_pyqt.py @@ -7,7 +7,8 @@ # # WARNING! All changes made in this file will be lost! -from PyQt4 import QtCore, QtGui +#from PyQt4 import QtCore, QtGui +from pyqtgraph.Qt import QtGui, QtCore try: _fromUtf8 = QtCore.QString.fromUtf8 diff --git a/examples/exampleLoaderTemplate_pyside2.py b/examples/exampleLoaderTemplate_pyside2.py new file mode 100644 index 00000000..0928e651 --- /dev/null +++ b/examples/exampleLoaderTemplate_pyside2.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' +# +# Created: Sat Feb 28 10:28:50 2015 +# by: PyQt5 UI code generator 5.2.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(846, 552) + self.gridLayout_2 = QtWidgets.QGridLayout(Form) + self.gridLayout_2.setObjectName("gridLayout_2") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.widget = QtWidgets.QWidget(self.splitter) + self.widget.setObjectName("widget") + self.gridLayout = QtWidgets.QGridLayout(self.widget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.exampleTree = QtWidgets.QTreeWidget(self.widget) + self.exampleTree.setObjectName("exampleTree") + self.exampleTree.headerItem().setText(0, "1") + self.exampleTree.header().setVisible(False) + self.gridLayout.addWidget(self.exampleTree, 0, 0, 1, 2) + self.graphicsSystemCombo = QtWidgets.QComboBox(self.widget) + self.graphicsSystemCombo.setObjectName("graphicsSystemCombo") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.gridLayout.addWidget(self.graphicsSystemCombo, 2, 1, 1, 1) + self.qtLibCombo = QtWidgets.QComboBox(self.widget) + self.qtLibCombo.setObjectName("qtLibCombo") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(self.widget) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) + self.label = QtWidgets.QLabel(self.widget) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.loadBtn = QtWidgets.QPushButton(self.widget) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 3, 1, 1, 1) + self.widget1 = QtWidgets.QWidget(self.splitter) + self.widget1.setObjectName("widget1") + self.verticalLayout = QtWidgets.QVBoxLayout(self.widget1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.loadedFileLabel = QtWidgets.QLabel(self.widget1) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.loadedFileLabel.setFont(font) + self.loadedFileLabel.setText("") + self.loadedFileLabel.setAlignment(QtCore.Qt.AlignCenter) + self.loadedFileLabel.setObjectName("loadedFileLabel") + self.verticalLayout.addWidget(self.loadedFileLabel) + self.codeView = QtWidgets.QPlainTextEdit(self.widget1) + font = QtGui.QFont() + font.setFamily("FreeMono") + self.codeView.setFont(font) + self.codeView.setObjectName("codeView") + self.verticalLayout.addWidget(self.codeView) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.graphicsSystemCombo.setItemText(0, _translate("Form", "default")) + self.graphicsSystemCombo.setItemText(1, _translate("Form", "native")) + self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster")) + self.graphicsSystemCombo.setItemText(3, _translate("Form", "opengl")) + self.qtLibCombo.setItemText(0, _translate("Form", "default")) + self.qtLibCombo.setItemText(1, _translate("Form", "PyQt4")) + self.qtLibCombo.setItemText(2, _translate("Form", "PySide")) + self.qtLibCombo.setItemText(3, _translate("Form", "PyQt5")) + self.label_2.setText(_translate("Form", "Graphics System:")) + self.label.setText(_translate("Form", "Qt Library:")) + self.loadBtn.setText(_translate("Form", "Run Example")) + diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 2676a3b4..083cf542 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -1,4 +1,4 @@ -from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5 +from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 from .. import exporters as exporters from .. import functions as fn from ..graphicsItems.ViewBox import ViewBox @@ -6,6 +6,8 @@ from ..graphicsItems.PlotItem import PlotItem if USE_PYSIDE: from . import exportDialogTemplate_pyside as exportDialogTemplate +elif USE_PYSIDE2: + from . import exportDialogTemplate_pyside2 as exportDialogTemplate elif USE_PYQT5: from . import exportDialogTemplate_pyqt5 as exportDialogTemplate else: diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py new file mode 100644 index 00000000..65a1386b --- /dev/null +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/GraphicsScene/exportDialogTemplate.ui' +# +# Created: Wed Mar 26 15:09:29 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(241, 367) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 3) + self.itemTree = QtWidgets.QTreeWidget(Form) + self.itemTree.setObjectName("itemTree") + self.itemTree.headerItem().setText(0, "1") + self.itemTree.header().setVisible(False) + self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) + self.label_2 = QtWidgets.QLabel(Form) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) + self.formatList = QtWidgets.QListWidget(Form) + self.formatList.setObjectName("formatList") + self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) + self.exportBtn = QtWidgets.QPushButton(Form) + self.exportBtn.setObjectName("exportBtn") + self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) + self.closeBtn = QtWidgets.QPushButton(Form) + self.closeBtn.setObjectName("closeBtn") + self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) + self.paramTree = ParameterTree(Form) + self.paramTree.setObjectName("paramTree") + self.paramTree.headerItem().setText(0, "1") + self.paramTree.header().setVisible(False) + self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) + self.label_3 = QtWidgets.QLabel(Form) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + self.copyBtn = QtWidgets.QPushButton(Form) + self.copyBtn.setObjectName("copyBtn") + self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Export")) + self.label.setText(_translate("Form", "Item to export:")) + self.label_2.setText(_translate("Form", "Export format")) + self.exportBtn.setText(_translate("Form", "Export")) + self.closeBtn.setText(_translate("Form", "Close")) + self.label_3.setText(_translate("Form", "Export options")) + self.copyBtn.setText(_translate("Form", "Copy")) + +from ..parametertree import ParameterTree diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 2ed9d6f9..59beb67d 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -14,6 +14,7 @@ import os, sys, re, time from .python2_3 import asUnicode PYSIDE = 'PySide' +PYSIDE2 = 'PySide2' PYQT4 = 'PyQt4' PYQT5 = 'PyQt5' @@ -24,7 +25,7 @@ QT_LIB = os.getenv('PYQTGRAPH_QT_LIB') ## This is done by first checking to see whether one of the libraries ## is already imported. If not, then attempt to import PyQt4, then PySide. if QT_LIB is None: - libOrder = [PYQT4, PYSIDE, PYQT5] + libOrder = [PYQT4, PYSIDE, PYQT5, PYSIDE2] for lib in libOrder: if lib in sys.modules: @@ -43,43 +44,141 @@ if QT_LIB is None: if QT_LIB is None: raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") -if QT_LIB == PYSIDE: - from PySide import QtGui, QtCore, QtOpenGL, QtSvg - try: - from PySide import QtTest - if not hasattr(QtTest.QTest, 'qWait'): - @staticmethod - def qWait(msec): - start = time.time() - QtGui.QApplication.processEvents() - while time.time() < start + msec * 0.001: +if QT_LIB == PYSIDE or QT_LIB == PYSIDE2 : + if QT_LIB == PYSIDE: + from PySide import QtGui, QtCore, QtOpenGL, QtSvg + try: + from PySide import QtTest + if not hasattr(QtTest.QTest, 'qWait'): + @staticmethod + def qWait(msec): + start = time.time() QtGui.QApplication.processEvents() - QtTest.QTest.qWait = qWait - - except ImportError: - pass - import PySide - try: - from PySide import shiboken - isQObjectAlive = shiboken.isValid - except ImportError: - def isQObjectAlive(obj): - try: - if hasattr(obj, 'parent'): - obj.parent() - elif hasattr(obj, 'parentItem'): - obj.parentItem() + while time.time() < start + msec * 0.001: + QtGui.QApplication.processEvents() + QtTest.QTest.qWait = qWait + + except ImportError: + pass + import PySide + try: + from PySide import shiboken + isQObjectAlive = shiboken.isValid + except ImportError: + def isQObjectAlive(obj): + try: + if hasattr(obj, 'parent'): + obj.parent() + elif hasattr(obj, 'parentItem'): + obj.parentItem() + else: + raise Exception("Cannot determine whether Qt object %s is still alive." % obj) + except RuntimeError: + return False else: - raise Exception("Cannot determine whether Qt object %s is still alive." % obj) - except RuntimeError: - return False + return True + + VERSION_INFO = 'PySide ' + PySide.__version__ + + elif QT_LIB == PYSIDE2: + # QtOpenGL, QtSvg is not working for PySide2, see https://wiki.qt.io/PySide2 + #from PySide2 import QtGui, QtCore, QtOpenGL, QtSvg, QtWidgets + from PySide2 import QtGui, QtCore, QtWidgets + + try: + from PySide2 import QtSvg + except ImportError: + pass + + try: + from PySide2 import QtOpenGL + except ImportError: + pass + + try: + from PySide2 import QtTest + if not hasattr(QtTest.QTest, 'qWait'): + @staticmethod + def qWait(msec): + start = time.time() + QtWidgets.QApplication.processEvents() + while time.time() < start + msec * 0.001: + QtWidgets.QApplication.processEvents() + QtTest.QTest.qWait = qWait + except ImportError: + pass + + import PySide2 + try: + from PySide2 import shiboken2 + isQObjectAlive = shiboken2.isValid + except ImportError: + def isQObjectAlive(obj): + try: + if hasattr(obj, 'parent'): + obj.parent() + elif hasattr(obj, 'parentItem'): + obj.parentItem() + else: + raise Exception("Cannot determine whether Qt object %s is still alive." % obj) + except RuntimeError: + return False + else: + return True + + # Re-implement deprecated APIs + __QGraphicsItem_scale = QtWidgets.QGraphicsItem.scale + def scale(self, *args): + if args: + sx, sy = args + tr = self.transform() + tr.scale(sx, sy) + self.setTransform(tr) else: - return True - - VERSION_INFO = 'PySide ' + PySide.__version__ - + return __QGraphicsItem_scale(self) + + QtWidgets.QGraphicsItem.scale = scale + + def rotate(self, angle): + tr = self.transform() + tr.rotate(angle) + self.setTransform(tr) + QtWidgets.QGraphicsItem.rotate = rotate + + def translate(self, dx, dy): + tr = self.transform() + tr.translate(dx, dy) + self.setTransform(tr) + QtWidgets.QGraphicsItem.translate = translate + + def setMargin(self, i): + self.setContentsMargins(i, i, i, i) + QtWidgets.QGridLayout.setMargin = setMargin + + def setResizeMode(self, *args): + self.setSectionResizeMode(*args) + QtWidgets.QHeaderView.setResizeMode = setResizeMode + + + QtGui.QApplication = QtWidgets.QApplication + QtGui.QGraphicsScene = QtWidgets.QGraphicsScene + QtGui.QGraphicsObject = QtWidgets.QGraphicsObject + QtGui.QGraphicsWidget = QtWidgets.QGraphicsWidget + + QtGui.QApplication.setGraphicsSystem = None + + # Import all QtWidgets objects into QtGui + for o in dir(QtWidgets): + if o.startswith('Q'): + setattr(QtGui, o, getattr(QtWidgets,o) ) + + VERSION_INFO = 'PySide2 ' + PySide2.__version__ + + # + # Commons to PySide and PySide2 + # + # Make a loadUiType function like PyQt has - # Credit: # http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 @@ -232,9 +331,16 @@ if QT_LIB.startswith('PyQt'): ## Make sure we have Qt >= 4.7 versionReq = [4, 7] USE_PYSIDE = QT_LIB == PYSIDE +USE_PYSIDE2 = QT_LIB == PYSIDE2 USE_PYQT4 = QT_LIB == PYQT4 USE_PYQT5 = QT_LIB == PYQT5 -QtVersion = PySide.QtCore.__version__ if QT_LIB == PYSIDE else QtCore.QT_VERSION_STR +if QT_LIB == PYSIDE: + QtVersion = PySide.QtCore.__version__ +elif QT_LIB == PYSIDE2: + QtVersion = PySide2.QtCore.__version__ +else: + QtCore.QT_VERSION_STR + m = re.match(r'(\d+)\.(\d+).*', QtVersion) if m is not None and list(map(int, m.groups())) < versionReq: print(list(map(int, m.groups()))) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index 4de891f7..b73382d4 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -4,13 +4,17 @@ if __name__ == '__main__': md = os.path.dirname(os.path.abspath(__file__)) sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path -from ..Qt import QtGui, QtCore, USE_PYSIDE +from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 from ..graphicsItems.ROI import ROI from ..graphicsItems.ViewBox import ViewBox from ..graphicsItems.GridItem import GridItem if USE_PYSIDE: from .CanvasTemplate_pyside import * +elif USE_PYSIDE2: + from .CanvasTemplate_pyside2 import * +elif USE_QT5: + from .CanvasTemplate_pyqt5 import * else: from .CanvasTemplate_pyqt import * diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index b6ecbb39..15f13649 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -1,9 +1,13 @@ # -*- coding: utf-8 -*- -from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 from ..graphicsItems.ROI import ROI from .. import SRTTransform, ItemGroup if USE_PYSIDE: from . import TransformGuiTemplate_pyside as TransformGuiTemplate +elif USE_PYSIDE2: + from . import TransformGuiTemplate_pyside2 as TransformGuiTemplate +elif USE_PYQT5: + from . import TransformGuiTemplate_pyqt5 as TransformGuiTemplate else: from . import TransformGuiTemplate_pyqt as TransformGuiTemplate diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside2.py b/pyqtgraph/canvas/CanvasTemplate_pyside2.py new file mode 100644 index 00000000..e17bc5fe --- /dev/null +++ b/pyqtgraph/canvas/CanvasTemplate_pyside2.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(490, 414) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.view = GraphicsView(self.splitter) + self.view.setObjectName("view") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.storeSvgBtn = QtWidgets.QPushButton(self.layoutWidget) + self.storeSvgBtn.setObjectName("storeSvgBtn") + self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) + self.storePngBtn = QtWidgets.QPushButton(self.layoutWidget) + self.storePngBtn.setObjectName("storePngBtn") + self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) + self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) + self.autoRangeBtn.setSizePolicy(sizePolicy) + self.autoRangeBtn.setObjectName("autoRangeBtn") + self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) + self.redirectCheck.setObjectName("redirectCheck") + self.horizontalLayout.addWidget(self.redirectCheck) + self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo.setObjectName("redirectCombo") + self.horizontalLayout.addWidget(self.redirectCombo) + self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) + self.itemList = TreeWidget(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) + self.itemList.setSizePolicy(sizePolicy) + self.itemList.setHeaderHidden(True) + self.itemList.setObjectName("itemList") + self.itemList.headerItem().setText(0, "1") + self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) + self.ctrlLayout = QtWidgets.QGridLayout() + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setObjectName("ctrlLayout") + self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) + self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) + self.resetTransformsBtn.setObjectName("resetTransformsBtn") + self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) + self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.storeSvgBtn.setText(_translate("Form", "Store SVG")) + self.storePngBtn.setText(_translate("Form", "Store PNG")) + 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")) + self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms")) + self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection")) + self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY")) + +from ..widgets.GraphicsView import GraphicsView +from ..widgets.TreeWidget import TreeWidget +from CanvasManager import CanvasCombo diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py new file mode 100644 index 00000000..253a40d7 --- /dev/null +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(224, 117) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + self.verticalLayout = QtWidgets.QVBoxLayout(Form) + self.verticalLayout.setSpacing(1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.translateLabel = QtWidgets.QLabel(Form) + self.translateLabel.setObjectName("translateLabel") + self.verticalLayout.addWidget(self.translateLabel) + self.rotateLabel = QtWidgets.QLabel(Form) + self.rotateLabel.setObjectName("rotateLabel") + self.verticalLayout.addWidget(self.rotateLabel) + self.scaleLabel = QtWidgets.QLabel(Form) + self.scaleLabel.setObjectName("scaleLabel") + self.verticalLayout.addWidget(self.scaleLabel) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.mirrorImageBtn = QtWidgets.QPushButton(Form) + self.mirrorImageBtn.setToolTip("") + self.mirrorImageBtn.setObjectName("mirrorImageBtn") + self.horizontalLayout.addWidget(self.mirrorImageBtn) + self.reflectImageBtn = QtWidgets.QPushButton(Form) + self.reflectImageBtn.setObjectName("reflectImageBtn") + self.horizontalLayout.addWidget(self.reflectImageBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.translateLabel.setText(_translate("Form", "Translate:")) + self.rotateLabel.setText(_translate("Form", "Rotate:")) + self.scaleLabel.setText(_translate("Form", "Scale:")) + self.mirrorImageBtn.setText(_translate("Form", "Mirror")) + self.reflectImageBtn.setText(_translate("Form", "Reflect")) + diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index ed4b7f08..cfc2904b 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -1,12 +1,14 @@ import sys, re, os, time, traceback, subprocess import pickle -from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5 +from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 from ..python2_3 import basestring from .. import exceptionHandling as exceptionHandling from .. import getConfigOption if USE_PYSIDE: from . import template_pyside as template +elif USE_PYSIDE2: + from . import template_pyside2 as template elif USE_PYQT5: from . import template_pyqt5 as template else: diff --git a/pyqtgraph/console/template_pyside2.py b/pyqtgraph/console/template_pyside2.py new file mode 100644 index 00000000..c82354a2 --- /dev/null +++ b/pyqtgraph/console/template_pyside2.py @@ -0,0 +1,107 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' +# +# Created: Wed Mar 26 15:09:29 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(710, 497) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) + font = QtGui.QFont() + font.setFamily("Monospace") + self.output.setFont(font) + self.output.setReadOnly(True) + self.output.setObjectName("output") + self.verticalLayout.addWidget(self.output) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.input = CmdInput(self.layoutWidget) + self.input.setObjectName("input") + self.horizontalLayout.addWidget(self.input) + self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) + self.historyBtn.setCheckable(True) + self.historyBtn.setObjectName("historyBtn") + self.horizontalLayout.addWidget(self.historyBtn) + self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.exceptionBtn.setCheckable(True) + self.exceptionBtn.setObjectName("exceptionBtn") + self.horizontalLayout.addWidget(self.exceptionBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + self.historyList = QtWidgets.QListWidget(self.splitter) + font = QtGui.QFont() + font.setFamily("Monospace") + self.historyList.setFont(font) + self.historyList.setObjectName("historyList") + self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) + self.exceptionGroup.setObjectName("exceptionGroup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.catchAllExceptionsBtn.setCheckable(True) + self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") + self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) + self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.catchNextExceptionBtn.setCheckable(True) + self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") + self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) + self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) + self.onlyUncaughtCheck.setChecked(True) + self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) + self.exceptionStackList.setAlternatingRowColors(True) + self.exceptionStackList.setObjectName("exceptionStackList") + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) + self.runSelectedFrameCheck.setChecked(True) + self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) + self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Console")) + self.historyBtn.setText(_translate("Form", "History..")) + self.exceptionBtn.setText(_translate("Form", "Exceptions..")) + self.exceptionGroup.setTitle(_translate("Form", "Exception Handling")) + self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions")) + self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception")) + self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions")) + self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame")) + self.exceptionInfoLabel.setText(_translate("Form", "Exception Info")) + self.clearExceptionBtn.setText(_translate("Form", "Clear Exception")) + +from .CmdInput import CmdInput diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index b623f5c7..402f5c60 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYQT5 +from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 from .Node import * from ..pgcollections import OrderedDict from ..widgets.TreeWidget import * @@ -9,6 +9,9 @@ from .. import FileDialog, DataTreeWidget if USE_PYSIDE: from . import FlowchartTemplate_pyside as FlowchartTemplate from . import FlowchartCtrlTemplate_pyside as FlowchartCtrlTemplate +elif USE_PYSIDE2: + from . import FlowchartTemplate_pyside2 as FlowchartTemplate + from . import FlowchartCtrlTemplate_pyside2 as FlowchartCtrlTemplate elif USE_PYQT5: from . import FlowchartTemplate_pyqt5 as FlowchartTemplate from . import FlowchartCtrlTemplate_pyqt5 as FlowchartCtrlTemplate diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py new file mode 100644 index 00000000..84593c8a --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(217, 499) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setVerticalSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.loadBtn = QtWidgets.QPushButton(Form) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) + self.saveBtn = FeedbackButton(Form) + self.saveBtn.setObjectName("saveBtn") + self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) + self.saveAsBtn = FeedbackButton(Form) + self.saveAsBtn.setObjectName("saveAsBtn") + self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) + self.reloadBtn = FeedbackButton(Form) + self.reloadBtn.setCheckable(False) + self.reloadBtn.setFlat(False) + self.reloadBtn.setObjectName("reloadBtn") + self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) + self.showChartBtn = QtWidgets.QPushButton(Form) + self.showChartBtn.setCheckable(True) + self.showChartBtn.setObjectName("showChartBtn") + self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) + self.ctrlList = TreeWidget(Form) + self.ctrlList.setObjectName("ctrlList") + self.ctrlList.headerItem().setText(0, "1") + self.ctrlList.header().setVisible(False) + self.ctrlList.header().setStretchLastSection(False) + self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) + self.fileNameLabel = QtWidgets.QLabel(Form) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.fileNameLabel.setFont(font) + self.fileNameLabel.setText("") + self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fileNameLabel.setObjectName("fileNameLabel") + self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.loadBtn.setText(_translate("Form", "Load..")) + self.saveBtn.setText(_translate("Form", "Save")) + self.saveAsBtn.setText(_translate("Form", "As..")) + self.reloadBtn.setText(_translate("Form", "Reload Libs")) + self.showChartBtn.setText(_translate("Form", "Flowchart")) + +from ..widgets.FeedbackButton import FeedbackButton +from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py new file mode 100644 index 00000000..ae2c9a92 --- /dev/null +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(529, 329) + self.selInfoWidget = QtWidgets.QWidget(Form) + self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) + self.selInfoWidget.setObjectName("selInfoWidget") + self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) + self.selDescLabel.setText("") + self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.selDescLabel.setWordWrap(True) + self.selDescLabel.setObjectName("selDescLabel") + self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) + self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.selNameLabel.setFont(font) + self.selNameLabel.setText("") + self.selNameLabel.setObjectName("selNameLabel") + self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) + self.selectedTree = DataTreeWidget(self.selInfoWidget) + self.selectedTree.setObjectName("selectedTree") + self.selectedTree.headerItem().setText(0, "1") + self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) + self.hoverText = QtWidgets.QTextEdit(Form) + self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) + self.hoverText.setObjectName("hoverText") + self.view = FlowchartGraphicsView(Form) + self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) + self.view.setObjectName("view") + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + +from ..widgets.DataTreeWidget import DataTreeWidget +from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d79c350f..45d48ad7 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -12,7 +12,7 @@ import decimal, re import ctypes import sys, struct from .python2_3 import asUnicode, basestring -from .Qt import QtGui, QtCore, USE_PYSIDE +from .Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2 from . import getConfigOption, setConfigOptions from . import debug @@ -1126,7 +1126,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): if copy is True and copied is False: imgData = imgData.copy() - if USE_PYSIDE: + if USE_PYSIDE or USE_PYSIDE2: ch = ctypes.c_char.from_buffer(imgData, 0) img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) else: @@ -1171,7 +1171,7 @@ def imageToArray(img, copy=False, transpose=True): """ fmt = img.format() ptr = img.bits() - if USE_PYSIDE: + if USE_PYSIDE or USE_PYSIDE2: arr = np.frombuffer(ptr, dtype=np.ubyte) else: ptr.setsize(img.byteCount()) diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py index 0efb11dd..0a73cb73 100644 --- a/pyqtgraph/graphicsItems/FillBetweenItem.py +++ b/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui, USE_PYQT5, USE_PYQT4, USE_PYSIDE +from ..Qt import QtGui, USE_PYQT5, USE_PYQT4, USE_PYSIDE, USE_PYSIDE2 from .. import functions as fn from .PlotDataItem import PlotDataItem from .PlotCurveItem import PlotCurveItem diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index 015a78c6..a6b16372 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -1,5 +1,5 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE -if not USE_PYSIDE: +from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2 +if not USE_PYSIDE and not USE_PYSIDE2: import sip from .GraphicsItem import GraphicsItem @@ -33,7 +33,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): ## workaround for pyqt bug: ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html - if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): + if not USE_PYSIDE and not USE_PYSIDE2 and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): ret = sip.cast(ret, QtGui.QGraphicsItem) return ret diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 41011df3..d1f976b5 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -41,6 +41,8 @@ elif QT_LIB == 'PySide': from .plotConfigTemplate_pyside import * elif QT_LIB == 'PyQt5': from .plotConfigTemplate_pyqt5 import * +elif QT_LIB == 'PySide2': + from .plotConfigTemplate_pyside2 import * __all__ = ['PlotItem'] diff --git a/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside2.py b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside2.py new file mode 100644 index 00000000..d801f298 --- /dev/null +++ b/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyside2.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(481, 840) + self.averageGroup = QtWidgets.QGroupBox(Form) + self.averageGroup.setGeometry(QtCore.QRect(0, 640, 242, 182)) + self.averageGroup.setCheckable(True) + self.averageGroup.setChecked(False) + self.averageGroup.setObjectName("averageGroup") + self.gridLayout_5 = QtWidgets.QGridLayout(self.averageGroup) + self.gridLayout_5.setContentsMargins(0, 0, 0, 0) + self.gridLayout_5.setSpacing(0) + self.gridLayout_5.setObjectName("gridLayout_5") + self.avgParamList = QtWidgets.QListWidget(self.averageGroup) + self.avgParamList.setObjectName("avgParamList") + self.gridLayout_5.addWidget(self.avgParamList, 0, 0, 1, 1) + self.decimateGroup = QtWidgets.QFrame(Form) + self.decimateGroup.setGeometry(QtCore.QRect(10, 140, 191, 171)) + self.decimateGroup.setObjectName("decimateGroup") + self.gridLayout_4 = QtWidgets.QGridLayout(self.decimateGroup) + self.gridLayout_4.setContentsMargins(0, 0, 0, 0) + self.gridLayout_4.setSpacing(0) + self.gridLayout_4.setObjectName("gridLayout_4") + self.clipToViewCheck = QtWidgets.QCheckBox(self.decimateGroup) + self.clipToViewCheck.setObjectName("clipToViewCheck") + self.gridLayout_4.addWidget(self.clipToViewCheck, 7, 0, 1, 3) + self.maxTracesCheck = QtWidgets.QCheckBox(self.decimateGroup) + self.maxTracesCheck.setObjectName("maxTracesCheck") + self.gridLayout_4.addWidget(self.maxTracesCheck, 8, 0, 1, 2) + self.downsampleCheck = QtWidgets.QCheckBox(self.decimateGroup) + self.downsampleCheck.setObjectName("downsampleCheck") + self.gridLayout_4.addWidget(self.downsampleCheck, 0, 0, 1, 3) + self.peakRadio = QtWidgets.QRadioButton(self.decimateGroup) + self.peakRadio.setChecked(True) + self.peakRadio.setObjectName("peakRadio") + self.gridLayout_4.addWidget(self.peakRadio, 6, 1, 1, 2) + self.maxTracesSpin = QtWidgets.QSpinBox(self.decimateGroup) + self.maxTracesSpin.setObjectName("maxTracesSpin") + self.gridLayout_4.addWidget(self.maxTracesSpin, 8, 2, 1, 1) + self.forgetTracesCheck = QtWidgets.QCheckBox(self.decimateGroup) + self.forgetTracesCheck.setObjectName("forgetTracesCheck") + self.gridLayout_4.addWidget(self.forgetTracesCheck, 9, 0, 1, 3) + self.meanRadio = QtWidgets.QRadioButton(self.decimateGroup) + self.meanRadio.setObjectName("meanRadio") + self.gridLayout_4.addWidget(self.meanRadio, 3, 1, 1, 2) + self.subsampleRadio = QtWidgets.QRadioButton(self.decimateGroup) + self.subsampleRadio.setObjectName("subsampleRadio") + self.gridLayout_4.addWidget(self.subsampleRadio, 2, 1, 1, 2) + self.autoDownsampleCheck = QtWidgets.QCheckBox(self.decimateGroup) + self.autoDownsampleCheck.setChecked(True) + self.autoDownsampleCheck.setObjectName("autoDownsampleCheck") + self.gridLayout_4.addWidget(self.autoDownsampleCheck, 1, 2, 1, 1) + spacerItem = QtWidgets.QSpacerItem(30, 20, QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_4.addItem(spacerItem, 2, 0, 1, 1) + self.downsampleSpin = QtWidgets.QSpinBox(self.decimateGroup) + self.downsampleSpin.setMinimum(1) + self.downsampleSpin.setMaximum(100000) + self.downsampleSpin.setProperty("value", 1) + self.downsampleSpin.setObjectName("downsampleSpin") + self.gridLayout_4.addWidget(self.downsampleSpin, 1, 1, 1, 1) + self.transformGroup = QtWidgets.QFrame(Form) + self.transformGroup.setGeometry(QtCore.QRect(0, 0, 154, 79)) + self.transformGroup.setObjectName("transformGroup") + self.gridLayout = QtWidgets.QGridLayout(self.transformGroup) + self.gridLayout.setObjectName("gridLayout") + self.fftCheck = QtWidgets.QCheckBox(self.transformGroup) + self.fftCheck.setObjectName("fftCheck") + self.gridLayout.addWidget(self.fftCheck, 0, 0, 1, 1) + self.logXCheck = QtWidgets.QCheckBox(self.transformGroup) + self.logXCheck.setObjectName("logXCheck") + self.gridLayout.addWidget(self.logXCheck, 1, 0, 1, 1) + self.logYCheck = QtWidgets.QCheckBox(self.transformGroup) + self.logYCheck.setObjectName("logYCheck") + self.gridLayout.addWidget(self.logYCheck, 2, 0, 1, 1) + self.pointsGroup = QtWidgets.QGroupBox(Form) + self.pointsGroup.setGeometry(QtCore.QRect(10, 550, 234, 58)) + self.pointsGroup.setCheckable(True) + self.pointsGroup.setObjectName("pointsGroup") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.pointsGroup) + self.verticalLayout_5.setObjectName("verticalLayout_5") + self.autoPointsCheck = QtWidgets.QCheckBox(self.pointsGroup) + self.autoPointsCheck.setChecked(True) + self.autoPointsCheck.setObjectName("autoPointsCheck") + self.verticalLayout_5.addWidget(self.autoPointsCheck) + self.gridGroup = QtWidgets.QFrame(Form) + self.gridGroup.setGeometry(QtCore.QRect(10, 460, 221, 81)) + self.gridGroup.setObjectName("gridGroup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.gridGroup) + self.gridLayout_2.setObjectName("gridLayout_2") + self.xGridCheck = QtWidgets.QCheckBox(self.gridGroup) + self.xGridCheck.setObjectName("xGridCheck") + self.gridLayout_2.addWidget(self.xGridCheck, 0, 0, 1, 2) + self.yGridCheck = QtWidgets.QCheckBox(self.gridGroup) + self.yGridCheck.setObjectName("yGridCheck") + self.gridLayout_2.addWidget(self.yGridCheck, 1, 0, 1, 2) + self.gridAlphaSlider = QtWidgets.QSlider(self.gridGroup) + self.gridAlphaSlider.setMaximum(255) + self.gridAlphaSlider.setProperty("value", 128) + self.gridAlphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.gridAlphaSlider.setObjectName("gridAlphaSlider") + self.gridLayout_2.addWidget(self.gridAlphaSlider, 2, 1, 1, 1) + self.label = QtWidgets.QLabel(self.gridGroup) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 2, 0, 1, 1) + self.alphaGroup = QtWidgets.QGroupBox(Form) + self.alphaGroup.setGeometry(QtCore.QRect(10, 390, 234, 60)) + self.alphaGroup.setCheckable(True) + self.alphaGroup.setObjectName("alphaGroup") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.alphaGroup) + self.horizontalLayout.setObjectName("horizontalLayout") + self.autoAlphaCheck = QtWidgets.QCheckBox(self.alphaGroup) + self.autoAlphaCheck.setChecked(False) + self.autoAlphaCheck.setObjectName("autoAlphaCheck") + self.horizontalLayout.addWidget(self.autoAlphaCheck) + self.alphaSlider = QtWidgets.QSlider(self.alphaGroup) + self.alphaSlider.setMaximum(1000) + self.alphaSlider.setProperty("value", 1000) + self.alphaSlider.setOrientation(QtCore.Qt.Horizontal) + self.alphaSlider.setObjectName("alphaSlider") + self.horizontalLayout.addWidget(self.alphaSlider) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + 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.")) + self.clipToViewCheck.setText(_translate("Form", "Clip to View")) + self.maxTracesCheck.setToolTip(_translate("Form", "If multiple curves are displayed in this plot, check this box to limit the number of traces that are displayed.")) + self.maxTracesCheck.setText(_translate("Form", "Max Traces:")) + self.downsampleCheck.setText(_translate("Form", "Downsample")) + self.peakRadio.setToolTip(_translate("Form", "Downsample by drawing a saw wave that follows the min and max of the original data. This method produces the best visual representation of the data but is slower.")) + self.peakRadio.setText(_translate("Form", "Peak")) + self.maxTracesSpin.setToolTip(_translate("Form", "If multiple curves are displayed in this plot, check \"Max Traces\" and set this value to limit the number of traces that are displayed.")) + self.forgetTracesCheck.setToolTip(_translate("Form", "If MaxTraces is checked, remove curves from memory after they are hidden (saves memory, but traces can not be un-hidden).")) + self.forgetTracesCheck.setText(_translate("Form", "Forget hidden traces")) + self.meanRadio.setToolTip(_translate("Form", "Downsample by taking the mean of N samples.")) + self.meanRadio.setText(_translate("Form", "Mean")) + self.subsampleRadio.setToolTip(_translate("Form", "Downsample by taking the first of N samples. This method is fastest and least accurate.")) + self.subsampleRadio.setText(_translate("Form", "Subsample")) + self.autoDownsampleCheck.setToolTip(_translate("Form", "Automatically downsample data based on the visible range. This assumes X values are uniformly spaced.")) + self.autoDownsampleCheck.setText(_translate("Form", "Auto")) + self.downsampleSpin.setToolTip(_translate("Form", "Downsample data before plotting. (plot every Nth sample)")) + self.downsampleSpin.setSuffix(_translate("Form", "x")) + self.fftCheck.setText(_translate("Form", "Power Spectrum (FFT)")) + self.logXCheck.setText(_translate("Form", "Log X")) + self.logYCheck.setText(_translate("Form", "Log Y")) + self.pointsGroup.setTitle(_translate("Form", "Points")) + self.autoPointsCheck.setText(_translate("Form", "Auto")) + self.xGridCheck.setText(_translate("Form", "Show X Grid")) + self.yGridCheck.setText(_translate("Form", "Show Y Grid")) + self.label.setText(_translate("Form", "Opacity")) + self.alphaGroup.setTitle(_translate("Form", "Alpha")) + self.autoAlphaCheck.setText(_translate("Form", "Auto")) + diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 54667b50..ff9b1b52 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -5,7 +5,7 @@ except ImportError: imap = map import numpy as np import weakref -from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYQT5 +from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 from ..Point import Point from .. import functions as fn from .GraphicsItem import GraphicsItem @@ -767,7 +767,7 @@ class ScatterPlotItem(GraphicsObject): self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) data = self.data[viewMask] - if USE_PYSIDE or USE_PYQT5: + if USE_PYSIDE or USE_PYSIDE2 or USE_PYQT5: list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) else: p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas) diff --git a/pyqtgraph/graphicsItems/UIGraphicsItem.py b/pyqtgraph/graphicsItems/UIGraphicsItem.py index 6f756334..f1672eeb 100644 --- a/pyqtgraph/graphicsItems/UIGraphicsItem.py +++ b/pyqtgraph/graphicsItems/UIGraphicsItem.py @@ -1,7 +1,7 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE +from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2 import weakref from .GraphicsObject import GraphicsObject -if not USE_PYSIDE: +if not USE_PYSIDE and not USE_PYSIDE2: import sip __all__ = ['UIGraphicsItem'] @@ -49,7 +49,7 @@ class UIGraphicsItem(GraphicsObject): ## workaround for pyqt bug: ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html - if not USE_PYSIDE and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): + if not USE_PYSIDE and not USE_PYSIDE2 and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): ret = sip.cast(ret, QtGui.QGraphicsItem) if change == self.ItemScenePositionHasChanged: diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 10392d7e..74a861d0 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -8,7 +8,9 @@ elif QT_LIB == 'PySide': from .axisCtrlTemplate_pyside import Ui_Form as AxisCtrlTemplate elif QT_LIB == 'PyQt5': from .axisCtrlTemplate_pyqt5 import Ui_Form as AxisCtrlTemplate - +elif QT_LIB == 'PySide2': + from .axisCtrlTemplate_pyside2 import Ui_Form as AxisCtrlTemplate + import weakref class ViewBoxMenu(QtGui.QMenu): diff --git a/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside2.py b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside2.py new file mode 100644 index 00000000..401c52fc --- /dev/null +++ b/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyside2.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(186, 154) + Form.setMaximumSize(QtCore.QSize(200, 16777215)) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 7, 0, 1, 2) + self.linkCombo = QtWidgets.QComboBox(Form) + self.linkCombo.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) + self.linkCombo.setObjectName("linkCombo") + self.gridLayout.addWidget(self.linkCombo, 7, 2, 1, 2) + self.autoPercentSpin = QtWidgets.QSpinBox(Form) + self.autoPercentSpin.setEnabled(True) + self.autoPercentSpin.setMinimum(1) + self.autoPercentSpin.setMaximum(100) + self.autoPercentSpin.setSingleStep(1) + self.autoPercentSpin.setProperty("value", 100) + self.autoPercentSpin.setObjectName("autoPercentSpin") + self.gridLayout.addWidget(self.autoPercentSpin, 2, 2, 1, 2) + self.autoRadio = QtWidgets.QRadioButton(Form) + self.autoRadio.setChecked(True) + self.autoRadio.setObjectName("autoRadio") + self.gridLayout.addWidget(self.autoRadio, 2, 0, 1, 2) + self.manualRadio = QtWidgets.QRadioButton(Form) + self.manualRadio.setObjectName("manualRadio") + self.gridLayout.addWidget(self.manualRadio, 1, 0, 1, 2) + self.minText = QtWidgets.QLineEdit(Form) + self.minText.setObjectName("minText") + self.gridLayout.addWidget(self.minText, 1, 2, 1, 1) + self.maxText = QtWidgets.QLineEdit(Form) + self.maxText.setObjectName("maxText") + self.gridLayout.addWidget(self.maxText, 1, 3, 1, 1) + self.invertCheck = QtWidgets.QCheckBox(Form) + self.invertCheck.setObjectName("invertCheck") + self.gridLayout.addWidget(self.invertCheck, 5, 0, 1, 4) + self.mouseCheck = QtWidgets.QCheckBox(Form) + self.mouseCheck.setChecked(True) + self.mouseCheck.setObjectName("mouseCheck") + self.gridLayout.addWidget(self.mouseCheck, 6, 0, 1, 4) + self.visibleOnlyCheck = QtWidgets.QCheckBox(Form) + self.visibleOnlyCheck.setObjectName("visibleOnlyCheck") + self.gridLayout.addWidget(self.visibleOnlyCheck, 3, 2, 1, 2) + self.autoPanCheck = QtWidgets.QCheckBox(Form) + self.autoPanCheck.setObjectName("autoPanCheck") + self.gridLayout.addWidget(self.autoPanCheck, 4, 2, 1, 2) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + 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.

")) + self.autoPercentSpin.setSuffix(_translate("Form", "%")) + self.autoRadio.setToolTip(_translate("Form", "

Automatically resize this axis whenever the displayed data is changed.

")) + self.autoRadio.setText(_translate("Form", "Auto")) + self.manualRadio.setToolTip(_translate("Form", "

Set the range for this axis manually. This disables automatic scaling.

")) + self.manualRadio.setText(_translate("Form", "Manual")) + self.minText.setToolTip(_translate("Form", "

Minimum value to display for this axis.

")) + self.minText.setText(_translate("Form", "0")) + self.maxText.setToolTip(_translate("Form", "

Maximum value to display for this axis.

")) + self.maxText.setText(_translate("Form", "0")) + self.invertCheck.setToolTip(_translate("Form", "

Inverts the display of this axis. (+y points downward instead of upward)

")) + self.invertCheck.setText(_translate("Form", "Invert Axis")) + self.mouseCheck.setToolTip(_translate("Form", "

Enables mouse interaction (panning, scaling) for this axis.

")) + self.mouseCheck.setText(_translate("Form", "Mouse Enabled")) + self.visibleOnlyCheck.setToolTip(_translate("Form", "

When checked, the axis will only auto-scale to data that is visible along the orthogonal axis.

")) + self.visibleOnlyCheck.setText(_translate("Form", "Visible Data Only")) + self.autoPanCheck.setToolTip(_translate("Form", "

When checked, the axis will automatically pan to center on the current data, but the scale along this axis will not change.

")) + self.autoPanCheck.setText(_translate("Form", "Auto Pan Only")) + diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..c3e01379 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -15,9 +15,13 @@ Widget used for displaying 2D or 3D data. Features: import os import numpy as np -from ..Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 if USE_PYSIDE: from .ImageViewTemplate_pyside import * +elif USE_PYSIDE2: + from .ImageViewTemplate_pyside2 import * +elif USE_PYQT5: + from .ImageViewTemplate_pyqt5 import * else: from .ImageViewTemplate_pyqt import * diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py b/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py index 4b4009b6..1d076a9e 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py @@ -41,15 +41,15 @@ class Ui_Form(object): self.roiBtn.setCheckable(True) self.roiBtn.setObjectName("roiBtn") self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) - self.normBtn = QtWidgets.QPushButton(self.layoutWidget) + self.menuBtn = QtWidgets.QPushButton(self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) - self.normBtn.setSizePolicy(sizePolicy) - self.normBtn.setCheckable(True) - self.normBtn.setObjectName("normBtn") - self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1) + sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth()) + self.menuBtn.setSizePolicy(sizePolicy) + self.menuBtn.setCheckable(True) + self.menuBtn.setObjectName("menuBtn") + self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1) self.roiPlot = PlotWidget(self.splitter) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -136,7 +136,7 @@ class Ui_Form(object): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "Form")) self.roiBtn.setText(_translate("Form", "ROI")) - self.normBtn.setText(_translate("Form", "Norm")) + self.menuBtn.setText(_translate("Form", "Norm")) self.normGroup.setTitle(_translate("Form", "Normalization")) self.normSubtractRadio.setText(_translate("Form", "Subtract")) self.normDivideRadio.setText(_translate("Form", "Divide")) diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside2.py b/pyqtgraph/imageview/ImageViewTemplate_pyside2.py new file mode 100644 index 00000000..70b76635 --- /dev/null +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside2.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui' +# +# Created: Wed Mar 26 15:09:28 2014 +# by: PyQt5 UI code generator 5.0.1 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(726, 588) + self.gridLayout_3 = QtWidgets.QGridLayout(Form) + self.gridLayout_3.setContentsMargins(0, 0, 0, 0) + self.gridLayout_3.setSpacing(0) + self.gridLayout_3.setObjectName("gridLayout_3") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) + self.gridLayout.setSpacing(0) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.graphicsView = GraphicsView(self.layoutWidget) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout.addWidget(self.graphicsView, 0, 0, 2, 1) + self.histogram = HistogramLUTWidget(self.layoutWidget) + self.histogram.setObjectName("histogram") + self.gridLayout.addWidget(self.histogram, 0, 1, 1, 2) + self.roiBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) + self.roiBtn.setSizePolicy(sizePolicy) + self.roiBtn.setCheckable(True) + self.roiBtn.setObjectName("roiBtn") + self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) + self.menuBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth()) + self.menuBtn.setSizePolicy(sizePolicy) + self.menuBtn.setCheckable(True) + self.menuBtn.setObjectName("menuBtn") + self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1) + self.roiPlot = PlotWidget(self.splitter) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) + self.roiPlot.setSizePolicy(sizePolicy) + self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) + self.roiPlot.setObjectName("roiPlot") + self.gridLayout_3.addWidget(self.splitter, 0, 0, 1, 1) + self.normGroup = QtWidgets.QGroupBox(Form) + self.normGroup.setObjectName("normGroup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.normGroup) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.normSubtractRadio = QtWidgets.QRadioButton(self.normGroup) + self.normSubtractRadio.setObjectName("normSubtractRadio") + self.gridLayout_2.addWidget(self.normSubtractRadio, 0, 2, 1, 1) + self.normDivideRadio = QtWidgets.QRadioButton(self.normGroup) + self.normDivideRadio.setChecked(False) + self.normDivideRadio.setObjectName("normDivideRadio") + self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_5.setFont(font) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) + self.label_3 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) + self.label_4 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setBold(True) + font.setWeight(75) + self.label_4.setFont(font) + self.label_4.setObjectName("label_4") + self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) + self.normROICheck = QtWidgets.QCheckBox(self.normGroup) + self.normROICheck.setObjectName("normROICheck") + self.gridLayout_2.addWidget(self.normROICheck, 1, 1, 1, 1) + self.normXBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normXBlurSpin.setObjectName("normXBlurSpin") + self.gridLayout_2.addWidget(self.normXBlurSpin, 2, 2, 1, 1) + self.label_8 = QtWidgets.QLabel(self.normGroup) + self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_8.setObjectName("label_8") + self.gridLayout_2.addWidget(self.label_8, 2, 1, 1, 1) + self.label_9 = QtWidgets.QLabel(self.normGroup) + self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_9.setObjectName("label_9") + self.gridLayout_2.addWidget(self.label_9, 2, 3, 1, 1) + self.normYBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normYBlurSpin.setObjectName("normYBlurSpin") + self.gridLayout_2.addWidget(self.normYBlurSpin, 2, 4, 1, 1) + self.label_10 = QtWidgets.QLabel(self.normGroup) + self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_10.setObjectName("label_10") + self.gridLayout_2.addWidget(self.label_10, 2, 5, 1, 1) + self.normOffRadio = QtWidgets.QRadioButton(self.normGroup) + self.normOffRadio.setChecked(True) + self.normOffRadio.setObjectName("normOffRadio") + self.gridLayout_2.addWidget(self.normOffRadio, 0, 3, 1, 1) + self.normTimeRangeCheck = QtWidgets.QCheckBox(self.normGroup) + self.normTimeRangeCheck.setObjectName("normTimeRangeCheck") + self.gridLayout_2.addWidget(self.normTimeRangeCheck, 1, 3, 1, 1) + self.normFrameCheck = QtWidgets.QCheckBox(self.normGroup) + self.normFrameCheck.setObjectName("normFrameCheck") + self.gridLayout_2.addWidget(self.normFrameCheck, 1, 2, 1, 1) + self.normTBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normTBlurSpin.setObjectName("normTBlurSpin") + self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) + self.gridLayout_3.addWidget(self.normGroup, 1, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + self.roiBtn.setText(_translate("Form", "ROI")) + self.menuBtn.setText(_translate("Form", "Norm")) + self.normGroup.setTitle(_translate("Form", "Normalization")) + self.normSubtractRadio.setText(_translate("Form", "Subtract")) + self.normDivideRadio.setText(_translate("Form", "Divide")) + self.label_5.setText(_translate("Form", "Operation:")) + self.label_3.setText(_translate("Form", "Mean:")) + self.label_4.setText(_translate("Form", "Blur:")) + self.normROICheck.setText(_translate("Form", "ROI")) + self.label_8.setText(_translate("Form", "X")) + self.label_9.setText(_translate("Form", "Y")) + self.label_10.setText(_translate("Form", "T")) + self.normOffRadio.setText(_translate("Form", "Off")) + self.normTimeRangeCheck.setText(_translate("Form", "Time range")) + self.normFrameCheck.setText(_translate("Form", "Frame")) + +from ..widgets.HistogramLUTWidget import HistogramLUTWidget +from ..widgets.PlotWidget import PlotWidget +from ..widgets.GraphicsView import GraphicsView diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index 85f5556a..f6a7888b 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -1,5 +1,5 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE -if not USE_PYSIDE: +from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2 +if not USE_PYSIDE and not USE_PYSIDE2: import sip from .. import multiprocess as mp from .GraphicsView import GraphicsView From 1cbef74e82d471566d50bb358c20e2514b2681f9 Mon Sep 17 00:00:00 2001 From: Max Peng Date: Sun, 18 Sep 2016 19:24:39 +0800 Subject: [PATCH 020/607] update ui files for pyside2 and pyqt5. --- .../ScatterPlotSpeedTestTemplate_pyqt5.py | 44 +++ .../ScatterPlotSpeedTestTemplate_pyside2.py | 44 +++ examples/VideoTemplate_pyqt5.py | 207 ++++++++++++ examples/VideoTemplate_pyside2.py | 207 ++++++++++++ examples/exampleLoaderTemplate_pyside2.py | 185 ++++++----- .../exportDialogTemplate_pyside2.py | 127 ++++--- pyqtgraph/canvas/CanvasTemplate_pyside2.py | 183 +++++------ .../canvas/TransformGuiTemplate_pyside2.py | 111 ++++--- pyqtgraph/console/template_pyside2.py | 220 +++++++------ .../FlowchartCtrlTemplate_pyside2.py | 133 ++++---- .../flowchart/FlowchartTemplate_pyside2.py | 109 +++--- .../imageview/ImageViewTemplate_pyside2.py | 310 +++++++++--------- 12 files changed, 1186 insertions(+), 694 deletions(-) create mode 100644 examples/ScatterPlotSpeedTestTemplate_pyqt5.py create mode 100644 examples/ScatterPlotSpeedTestTemplate_pyside2.py create mode 100644 examples/VideoTemplate_pyqt5.py create mode 100644 examples/VideoTemplate_pyside2.py diff --git a/examples/ScatterPlotSpeedTestTemplate_pyqt5.py b/examples/ScatterPlotSpeedTestTemplate_pyqt5.py new file mode 100644 index 00000000..1c7be1d3 --- /dev/null +++ b/examples/ScatterPlotSpeedTestTemplate_pyqt5.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ScatterPlotSpeedTestTemplate.ui' +# +# Created: Sun Sep 18 19:21:36 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(400, 300) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setObjectName("gridLayout") + self.sizeSpin = QtWidgets.QSpinBox(Form) + self.sizeSpin.setProperty("value", 10) + self.sizeSpin.setObjectName("sizeSpin") + self.gridLayout.addWidget(self.sizeSpin, 1, 1, 1, 1) + self.pixelModeCheck = QtWidgets.QCheckBox(Form) + self.pixelModeCheck.setObjectName("pixelModeCheck") + self.gridLayout.addWidget(self.pixelModeCheck, 1, 3, 1, 1) + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.plot = PlotWidget(Form) + self.plot.setObjectName("plot") + self.gridLayout.addWidget(self.plot, 0, 0, 1, 4) + self.randCheck = QtWidgets.QCheckBox(Form) + self.randCheck.setObjectName("randCheck") + self.gridLayout.addWidget(self.randCheck, 1, 2, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.pixelModeCheck.setText(QtWidgets.QApplication.translate("Form", "pixel mode", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Size", None, -1)) + self.randCheck.setText(QtWidgets.QApplication.translate("Form", "Randomize", None, -1)) + +from pyqtgraph import PlotWidget diff --git a/examples/ScatterPlotSpeedTestTemplate_pyside2.py b/examples/ScatterPlotSpeedTestTemplate_pyside2.py new file mode 100644 index 00000000..7b66ad46 --- /dev/null +++ b/examples/ScatterPlotSpeedTestTemplate_pyside2.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ScatterPlotSpeedTestTemplate.ui' +# +# Created: Sun Sep 18 19:21:36 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(400, 300) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setObjectName("gridLayout") + self.sizeSpin = QtWidgets.QSpinBox(Form) + self.sizeSpin.setProperty("value", 10) + self.sizeSpin.setObjectName("sizeSpin") + self.gridLayout.addWidget(self.sizeSpin, 1, 1, 1, 1) + self.pixelModeCheck = QtWidgets.QCheckBox(Form) + self.pixelModeCheck.setObjectName("pixelModeCheck") + self.gridLayout.addWidget(self.pixelModeCheck, 1, 3, 1, 1) + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.plot = PlotWidget(Form) + self.plot.setObjectName("plot") + self.gridLayout.addWidget(self.plot, 0, 0, 1, 4) + self.randCheck = QtWidgets.QCheckBox(Form) + self.randCheck.setObjectName("randCheck") + self.gridLayout.addWidget(self.randCheck, 1, 2, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.pixelModeCheck.setText(QtWidgets.QApplication.translate("Form", "pixel mode", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Size", None, -1)) + self.randCheck.setText(QtWidgets.QApplication.translate("Form", "Randomize", None, -1)) + +from pyqtgraph import PlotWidget diff --git a/examples/VideoTemplate_pyqt5.py b/examples/VideoTemplate_pyqt5.py new file mode 100644 index 00000000..2e377f44 --- /dev/null +++ b/examples/VideoTemplate_pyqt5.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'VideoTemplate.ui' +# +# Created: Sun Sep 18 19:22:41 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(695, 798) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName("gridLayout_2") + self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget) + self.downsampleCheck.setObjectName("downsampleCheck") + self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2) + self.scaleCheck = QtWidgets.QCheckBox(self.centralwidget) + self.scaleCheck.setObjectName("scaleCheck") + self.gridLayout_2.addWidget(self.scaleCheck, 4, 0, 1, 1) + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.rawRadio = QtWidgets.QRadioButton(self.centralwidget) + self.rawRadio.setObjectName("rawRadio") + self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) + self.gfxRadio = QtWidgets.QRadioButton(self.centralwidget) + self.gfxRadio.setChecked(True) + self.gfxRadio.setObjectName("gfxRadio") + self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) + self.stack = QtWidgets.QStackedWidget(self.centralwidget) + self.stack.setObjectName("stack") + self.page = QtWidgets.QWidget() + self.page.setObjectName("page") + self.gridLayout_3 = QtWidgets.QGridLayout(self.page) + self.gridLayout_3.setObjectName("gridLayout_3") + self.graphicsView = GraphicsView(self.page) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1) + self.stack.addWidget(self.page) + self.page_2 = QtWidgets.QWidget() + self.page_2.setObjectName("page_2") + self.gridLayout_4 = QtWidgets.QGridLayout(self.page_2) + self.gridLayout_4.setObjectName("gridLayout_4") + self.rawImg = RawImageWidget(self.page_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) + self.rawImg.setSizePolicy(sizePolicy) + self.rawImg.setObjectName("rawImg") + self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_2) + self.page_3 = QtWidgets.QWidget() + self.page_3.setObjectName("page_3") + self.gridLayout_5 = QtWidgets.QGridLayout(self.page_3) + self.gridLayout_5.setObjectName("gridLayout_5") + self.rawGLImg = RawImageGLWidget(self.page_3) + self.rawGLImg.setObjectName("rawGLImg") + self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_3) + self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) + self.rawGLRadio = QtWidgets.QRadioButton(self.centralwidget) + self.rawGLRadio.setObjectName("rawGLRadio") + self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1) + self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) + self.dtypeCombo = QtWidgets.QComboBox(self.centralwidget) + self.dtypeCombo.setObjectName("dtypeCombo") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.gridLayout_2.addWidget(self.dtypeCombo, 3, 2, 1, 1) + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 3, 0, 1, 1) + self.rgbLevelsCheck = QtWidgets.QCheckBox(self.centralwidget) + self.rgbLevelsCheck.setObjectName("rgbLevelsCheck") + self.gridLayout_2.addWidget(self.rgbLevelsCheck, 4, 1, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.minSpin2 = SpinBox(self.centralwidget) + self.minSpin2.setEnabled(False) + self.minSpin2.setObjectName("minSpin2") + self.horizontalLayout_2.addWidget(self.minSpin2) + self.label_3 = QtWidgets.QLabel(self.centralwidget) + self.label_3.setAlignment(QtCore.Qt.AlignCenter) + self.label_3.setObjectName("label_3") + self.horizontalLayout_2.addWidget(self.label_3) + self.maxSpin2 = SpinBox(self.centralwidget) + self.maxSpin2.setEnabled(False) + self.maxSpin2.setObjectName("maxSpin2") + self.horizontalLayout_2.addWidget(self.maxSpin2) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 5, 2, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.minSpin1 = SpinBox(self.centralwidget) + self.minSpin1.setObjectName("minSpin1") + self.horizontalLayout.addWidget(self.minSpin1) + self.label_2 = QtWidgets.QLabel(self.centralwidget) + self.label_2.setAlignment(QtCore.Qt.AlignCenter) + self.label_2.setObjectName("label_2") + self.horizontalLayout.addWidget(self.label_2) + self.maxSpin1 = SpinBox(self.centralwidget) + self.maxSpin1.setObjectName("maxSpin1") + self.horizontalLayout.addWidget(self.maxSpin1) + self.gridLayout_2.addLayout(self.horizontalLayout, 4, 2, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.minSpin3 = SpinBox(self.centralwidget) + self.minSpin3.setEnabled(False) + self.minSpin3.setObjectName("minSpin3") + self.horizontalLayout_3.addWidget(self.minSpin3) + self.label_4 = QtWidgets.QLabel(self.centralwidget) + self.label_4.setAlignment(QtCore.Qt.AlignCenter) + self.label_4.setObjectName("label_4") + self.horizontalLayout_3.addWidget(self.label_4) + self.maxSpin3 = SpinBox(self.centralwidget) + self.maxSpin3.setEnabled(False) + self.maxSpin3.setObjectName("maxSpin3") + self.horizontalLayout_3.addWidget(self.maxSpin3) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 6, 2, 1, 1) + self.lutCheck = QtWidgets.QCheckBox(self.centralwidget) + self.lutCheck.setObjectName("lutCheck") + self.gridLayout_2.addWidget(self.lutCheck, 7, 0, 1, 1) + self.alphaCheck = QtWidgets.QCheckBox(self.centralwidget) + self.alphaCheck.setObjectName("alphaCheck") + self.gridLayout_2.addWidget(self.alphaCheck, 7, 1, 1, 1) + self.gradient = GradientWidget(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) + self.gradient.setSizePolicy(sizePolicy) + self.gradient.setObjectName("gradient") + self.gridLayout_2.addWidget(self.gradient, 7, 2, 1, 2) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 3, 3, 1, 1) + self.fpsLabel = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setPointSize(12) + self.fpsLabel.setFont(font) + self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fpsLabel.setObjectName("fpsLabel") + self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + self.rgbCheck = QtWidgets.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName("rgbCheck") + self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(self.centralwidget) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.framesSpin = QtWidgets.QSpinBox(self.centralwidget) + self.framesSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.framesSpin.setProperty("value", 10) + self.framesSpin.setObjectName("framesSpin") + self.horizontalLayout_4.addWidget(self.framesSpin) + self.widthSpin = QtWidgets.QSpinBox(self.centralwidget) + self.widthSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus) + self.widthSpin.setMaximum(10000) + self.widthSpin.setProperty("value", 512) + self.widthSpin.setObjectName("widthSpin") + self.horizontalLayout_4.addWidget(self.widthSpin) + self.heightSpin = QtWidgets.QSpinBox(self.centralwidget) + self.heightSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.heightSpin.setMaximum(10000) + self.heightSpin.setProperty("value", 512) + self.heightSpin.setObjectName("heightSpin") + self.horizontalLayout_4.addWidget(self.heightSpin) + self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2) + self.sizeLabel = QtWidgets.QLabel(self.centralwidget) + self.sizeLabel.setText("") + self.sizeLabel.setObjectName("sizeLabel") + self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + self.stack.setCurrentIndex(2) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1)) + self.downsampleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Auto downsample", None, -1)) + self.scaleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Scale Data", None, -1)) + self.rawRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawImageWidget", None, -1)) + self.gfxRadio.setText(QtWidgets.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, -1)) + self.rawGLRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawGLImageWidget", None, -1)) + self.dtypeCombo.setItemText(0, QtWidgets.QApplication.translate("MainWindow", "uint8", None, -1)) + self.dtypeCombo.setItemText(1, QtWidgets.QApplication.translate("MainWindow", "uint16", None, -1)) + self.dtypeCombo.setItemText(2, QtWidgets.QApplication.translate("MainWindow", "float", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Data type", None, -1)) + self.rgbLevelsCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) + self.label_3.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.label_2.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.label_4.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.lutCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Use Lookup Table", None, -1)) + self.alphaCheck.setText(QtWidgets.QApplication.translate("MainWindow", "alpha", None, -1)) + self.fpsLabel.setText(QtWidgets.QApplication.translate("MainWindow", "FPS", None, -1)) + self.rgbCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) + self.label_5.setText(QtWidgets.QApplication.translate("MainWindow", "Image size", None, -1)) + +from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget +from pyqtgraph import GradientWidget, SpinBox, GraphicsView diff --git a/examples/VideoTemplate_pyside2.py b/examples/VideoTemplate_pyside2.py new file mode 100644 index 00000000..a3596714 --- /dev/null +++ b/examples/VideoTemplate_pyside2.py @@ -0,0 +1,207 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'VideoTemplate.ui' +# +# Created: Sun Sep 18 19:22:41 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(695, 798) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName("gridLayout_2") + self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget) + self.downsampleCheck.setObjectName("downsampleCheck") + self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2) + self.scaleCheck = QtWidgets.QCheckBox(self.centralwidget) + self.scaleCheck.setObjectName("scaleCheck") + self.gridLayout_2.addWidget(self.scaleCheck, 4, 0, 1, 1) + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.rawRadio = QtWidgets.QRadioButton(self.centralwidget) + self.rawRadio.setObjectName("rawRadio") + self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) + self.gfxRadio = QtWidgets.QRadioButton(self.centralwidget) + self.gfxRadio.setChecked(True) + self.gfxRadio.setObjectName("gfxRadio") + self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) + self.stack = QtWidgets.QStackedWidget(self.centralwidget) + self.stack.setObjectName("stack") + self.page = QtWidgets.QWidget() + self.page.setObjectName("page") + self.gridLayout_3 = QtWidgets.QGridLayout(self.page) + self.gridLayout_3.setObjectName("gridLayout_3") + self.graphicsView = GraphicsView(self.page) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1) + self.stack.addWidget(self.page) + self.page_2 = QtWidgets.QWidget() + self.page_2.setObjectName("page_2") + self.gridLayout_4 = QtWidgets.QGridLayout(self.page_2) + self.gridLayout_4.setObjectName("gridLayout_4") + self.rawImg = RawImageWidget(self.page_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) + self.rawImg.setSizePolicy(sizePolicy) + self.rawImg.setObjectName("rawImg") + self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_2) + self.page_3 = QtWidgets.QWidget() + self.page_3.setObjectName("page_3") + self.gridLayout_5 = QtWidgets.QGridLayout(self.page_3) + self.gridLayout_5.setObjectName("gridLayout_5") + self.rawGLImg = RawImageGLWidget(self.page_3) + self.rawGLImg.setObjectName("rawGLImg") + self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_3) + self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) + self.rawGLRadio = QtWidgets.QRadioButton(self.centralwidget) + self.rawGLRadio.setObjectName("rawGLRadio") + self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1) + self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) + self.dtypeCombo = QtWidgets.QComboBox(self.centralwidget) + self.dtypeCombo.setObjectName("dtypeCombo") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.gridLayout_2.addWidget(self.dtypeCombo, 3, 2, 1, 1) + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 3, 0, 1, 1) + self.rgbLevelsCheck = QtWidgets.QCheckBox(self.centralwidget) + self.rgbLevelsCheck.setObjectName("rgbLevelsCheck") + self.gridLayout_2.addWidget(self.rgbLevelsCheck, 4, 1, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.minSpin2 = SpinBox(self.centralwidget) + self.minSpin2.setEnabled(False) + self.minSpin2.setObjectName("minSpin2") + self.horizontalLayout_2.addWidget(self.minSpin2) + self.label_3 = QtWidgets.QLabel(self.centralwidget) + self.label_3.setAlignment(QtCore.Qt.AlignCenter) + self.label_3.setObjectName("label_3") + self.horizontalLayout_2.addWidget(self.label_3) + self.maxSpin2 = SpinBox(self.centralwidget) + self.maxSpin2.setEnabled(False) + self.maxSpin2.setObjectName("maxSpin2") + self.horizontalLayout_2.addWidget(self.maxSpin2) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 5, 2, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.minSpin1 = SpinBox(self.centralwidget) + self.minSpin1.setObjectName("minSpin1") + self.horizontalLayout.addWidget(self.minSpin1) + self.label_2 = QtWidgets.QLabel(self.centralwidget) + self.label_2.setAlignment(QtCore.Qt.AlignCenter) + self.label_2.setObjectName("label_2") + self.horizontalLayout.addWidget(self.label_2) + self.maxSpin1 = SpinBox(self.centralwidget) + self.maxSpin1.setObjectName("maxSpin1") + self.horizontalLayout.addWidget(self.maxSpin1) + self.gridLayout_2.addLayout(self.horizontalLayout, 4, 2, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.minSpin3 = SpinBox(self.centralwidget) + self.minSpin3.setEnabled(False) + self.minSpin3.setObjectName("minSpin3") + self.horizontalLayout_3.addWidget(self.minSpin3) + self.label_4 = QtWidgets.QLabel(self.centralwidget) + self.label_4.setAlignment(QtCore.Qt.AlignCenter) + self.label_4.setObjectName("label_4") + self.horizontalLayout_3.addWidget(self.label_4) + self.maxSpin3 = SpinBox(self.centralwidget) + self.maxSpin3.setEnabled(False) + self.maxSpin3.setObjectName("maxSpin3") + self.horizontalLayout_3.addWidget(self.maxSpin3) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 6, 2, 1, 1) + self.lutCheck = QtWidgets.QCheckBox(self.centralwidget) + self.lutCheck.setObjectName("lutCheck") + self.gridLayout_2.addWidget(self.lutCheck, 7, 0, 1, 1) + self.alphaCheck = QtWidgets.QCheckBox(self.centralwidget) + self.alphaCheck.setObjectName("alphaCheck") + self.gridLayout_2.addWidget(self.alphaCheck, 7, 1, 1, 1) + self.gradient = GradientWidget(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) + self.gradient.setSizePolicy(sizePolicy) + self.gradient.setObjectName("gradient") + self.gridLayout_2.addWidget(self.gradient, 7, 2, 1, 2) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 3, 3, 1, 1) + self.fpsLabel = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setPointSize(12) + self.fpsLabel.setFont(font) + self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fpsLabel.setObjectName("fpsLabel") + self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + self.rgbCheck = QtWidgets.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName("rgbCheck") + self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(self.centralwidget) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.framesSpin = QtWidgets.QSpinBox(self.centralwidget) + self.framesSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.framesSpin.setProperty("value", 10) + self.framesSpin.setObjectName("framesSpin") + self.horizontalLayout_4.addWidget(self.framesSpin) + self.widthSpin = QtWidgets.QSpinBox(self.centralwidget) + self.widthSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus) + self.widthSpin.setMaximum(10000) + self.widthSpin.setProperty("value", 512) + self.widthSpin.setObjectName("widthSpin") + self.horizontalLayout_4.addWidget(self.widthSpin) + self.heightSpin = QtWidgets.QSpinBox(self.centralwidget) + self.heightSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.heightSpin.setMaximum(10000) + self.heightSpin.setProperty("value", 512) + self.heightSpin.setObjectName("heightSpin") + self.horizontalLayout_4.addWidget(self.heightSpin) + self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2) + self.sizeLabel = QtWidgets.QLabel(self.centralwidget) + self.sizeLabel.setText("") + self.sizeLabel.setObjectName("sizeLabel") + self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + self.stack.setCurrentIndex(2) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1)) + self.downsampleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Auto downsample", None, -1)) + self.scaleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Scale Data", None, -1)) + self.rawRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawImageWidget", None, -1)) + self.gfxRadio.setText(QtWidgets.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, -1)) + self.rawGLRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawGLImageWidget", None, -1)) + self.dtypeCombo.setItemText(0, QtWidgets.QApplication.translate("MainWindow", "uint8", None, -1)) + self.dtypeCombo.setItemText(1, QtWidgets.QApplication.translate("MainWindow", "uint16", None, -1)) + self.dtypeCombo.setItemText(2, QtWidgets.QApplication.translate("MainWindow", "float", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Data type", None, -1)) + self.rgbLevelsCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) + self.label_3.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.label_2.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.label_4.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.lutCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Use Lookup Table", None, -1)) + self.alphaCheck.setText(QtWidgets.QApplication.translate("MainWindow", "alpha", None, -1)) + self.fpsLabel.setText(QtWidgets.QApplication.translate("MainWindow", "FPS", None, -1)) + self.rgbCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) + self.label_5.setText(QtWidgets.QApplication.translate("MainWindow", "Image size", None, -1)) + +from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget +from pyqtgraph import GradientWidget, SpinBox, GraphicsView diff --git a/examples/exampleLoaderTemplate_pyside2.py b/examples/exampleLoaderTemplate_pyside2.py index 0928e651..c3e96555 100644 --- a/examples/exampleLoaderTemplate_pyside2.py +++ b/examples/exampleLoaderTemplate_pyside2.py @@ -1,93 +1,92 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' -# -# Created: Sat Feb 28 10:28:50 2015 -# by: PyQt5 UI code generator 5.2.1 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(846, 552) - self.gridLayout_2 = QtWidgets.QGridLayout(Form) - self.gridLayout_2.setObjectName("gridLayout_2") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.widget = QtWidgets.QWidget(self.splitter) - self.widget.setObjectName("widget") - self.gridLayout = QtWidgets.QGridLayout(self.widget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.exampleTree = QtWidgets.QTreeWidget(self.widget) - self.exampleTree.setObjectName("exampleTree") - self.exampleTree.headerItem().setText(0, "1") - self.exampleTree.header().setVisible(False) - self.gridLayout.addWidget(self.exampleTree, 0, 0, 1, 2) - self.graphicsSystemCombo = QtWidgets.QComboBox(self.widget) - self.graphicsSystemCombo.setObjectName("graphicsSystemCombo") - self.graphicsSystemCombo.addItem("") - self.graphicsSystemCombo.addItem("") - self.graphicsSystemCombo.addItem("") - self.graphicsSystemCombo.addItem("") - self.gridLayout.addWidget(self.graphicsSystemCombo, 2, 1, 1, 1) - self.qtLibCombo = QtWidgets.QComboBox(self.widget) - self.qtLibCombo.setObjectName("qtLibCombo") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) - self.label_2 = QtWidgets.QLabel(self.widget) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) - self.label = QtWidgets.QLabel(self.widget) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 1, 0, 1, 1) - self.loadBtn = QtWidgets.QPushButton(self.widget) - self.loadBtn.setObjectName("loadBtn") - self.gridLayout.addWidget(self.loadBtn, 3, 1, 1, 1) - self.widget1 = QtWidgets.QWidget(self.splitter) - self.widget1.setObjectName("widget1") - self.verticalLayout = QtWidgets.QVBoxLayout(self.widget1) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.loadedFileLabel = QtWidgets.QLabel(self.widget1) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.loadedFileLabel.setFont(font) - self.loadedFileLabel.setText("") - self.loadedFileLabel.setAlignment(QtCore.Qt.AlignCenter) - self.loadedFileLabel.setObjectName("loadedFileLabel") - self.verticalLayout.addWidget(self.loadedFileLabel) - self.codeView = QtWidgets.QPlainTextEdit(self.widget1) - font = QtGui.QFont() - font.setFamily("FreeMono") - self.codeView.setFont(font) - self.codeView.setObjectName("codeView") - self.verticalLayout.addWidget(self.codeView) - self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) - self.graphicsSystemCombo.setItemText(0, _translate("Form", "default")) - self.graphicsSystemCombo.setItemText(1, _translate("Form", "native")) - self.graphicsSystemCombo.setItemText(2, _translate("Form", "raster")) - self.graphicsSystemCombo.setItemText(3, _translate("Form", "opengl")) - self.qtLibCombo.setItemText(0, _translate("Form", "default")) - self.qtLibCombo.setItemText(1, _translate("Form", "PyQt4")) - self.qtLibCombo.setItemText(2, _translate("Form", "PySide")) - self.qtLibCombo.setItemText(3, _translate("Form", "PyQt5")) - self.label_2.setText(_translate("Form", "Graphics System:")) - self.label.setText(_translate("Form", "Qt Library:")) - self.loadBtn.setText(_translate("Form", "Run Example")) - +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' +# +# Created: Sun Sep 18 19:20:44 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(846, 552) + self.gridLayout_2 = QtWidgets.QGridLayout(Form) + self.gridLayout_2.setObjectName("gridLayout_2") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.widget = QtWidgets.QWidget(self.splitter) + self.widget.setObjectName("widget") + self.gridLayout = QtWidgets.QGridLayout(self.widget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.exampleTree = QtWidgets.QTreeWidget(self.widget) + self.exampleTree.setObjectName("exampleTree") + self.exampleTree.headerItem().setText(0, "1") + self.exampleTree.header().setVisible(False) + self.gridLayout.addWidget(self.exampleTree, 0, 0, 1, 2) + self.graphicsSystemCombo = QtWidgets.QComboBox(self.widget) + self.graphicsSystemCombo.setObjectName("graphicsSystemCombo") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.gridLayout.addWidget(self.graphicsSystemCombo, 2, 1, 1, 1) + self.qtLibCombo = QtWidgets.QComboBox(self.widget) + self.qtLibCombo.setObjectName("qtLibCombo") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(self.widget) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) + self.label = QtWidgets.QLabel(self.widget) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.loadBtn = QtWidgets.QPushButton(self.widget) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 3, 1, 1, 1) + self.widget1 = QtWidgets.QWidget(self.splitter) + self.widget1.setObjectName("widget1") + self.verticalLayout = QtWidgets.QVBoxLayout(self.widget1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.loadedFileLabel = QtWidgets.QLabel(self.widget1) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.loadedFileLabel.setFont(font) + self.loadedFileLabel.setText("") + self.loadedFileLabel.setAlignment(QtCore.Qt.AlignCenter) + self.loadedFileLabel.setObjectName("loadedFileLabel") + self.verticalLayout.addWidget(self.loadedFileLabel) + self.codeView = QtWidgets.QPlainTextEdit(self.widget1) + font = QtGui.QFont() + font.setFamily("FreeMono") + self.codeView.setFont(font) + self.codeView.setObjectName("codeView") + self.verticalLayout.addWidget(self.codeView) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.graphicsSystemCombo.setItemText(0, QtWidgets.QApplication.translate("Form", "default", None, -1)) + self.graphicsSystemCombo.setItemText(1, QtWidgets.QApplication.translate("Form", "native", None, -1)) + self.graphicsSystemCombo.setItemText(2, QtWidgets.QApplication.translate("Form", "raster", None, -1)) + self.graphicsSystemCombo.setItemText(3, QtWidgets.QApplication.translate("Form", "opengl", None, -1)) + self.qtLibCombo.setItemText(0, QtWidgets.QApplication.translate("Form", "default", None, -1)) + self.qtLibCombo.setItemText(1, QtWidgets.QApplication.translate("Form", "PyQt4", None, -1)) + self.qtLibCombo.setItemText(2, QtWidgets.QApplication.translate("Form", "PySide", None, -1)) + self.qtLibCombo.setItemText(3, QtWidgets.QApplication.translate("Form", "PyQt5", None, -1)) + self.label_2.setText(QtWidgets.QApplication.translate("Form", "Graphics System:", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Qt Library:", None, -1)) + self.loadBtn.setText(QtWidgets.QApplication.translate("Form", "Run Example", None, -1)) + diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py index 65a1386b..24bfd37d 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py @@ -1,64 +1,63 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './pyqtgraph/GraphicsScene/exportDialogTemplate.ui' -# -# Created: Wed Mar 26 15:09:29 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(241, 367) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.label = QtWidgets.QLabel(Form) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 0, 1, 3) - self.itemTree = QtWidgets.QTreeWidget(Form) - self.itemTree.setObjectName("itemTree") - self.itemTree.headerItem().setText(0, "1") - self.itemTree.header().setVisible(False) - self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) - self.label_2 = QtWidgets.QLabel(Form) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) - self.formatList = QtWidgets.QListWidget(Form) - self.formatList.setObjectName("formatList") - self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) - self.exportBtn = QtWidgets.QPushButton(Form) - self.exportBtn.setObjectName("exportBtn") - self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) - self.closeBtn = QtWidgets.QPushButton(Form) - self.closeBtn.setObjectName("closeBtn") - self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) - self.paramTree = ParameterTree(Form) - self.paramTree.setObjectName("paramTree") - self.paramTree.headerItem().setText(0, "1") - self.paramTree.header().setVisible(False) - self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) - self.label_3 = QtWidgets.QLabel(Form) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) - self.copyBtn = QtWidgets.QPushButton(Form) - self.copyBtn.setObjectName("copyBtn") - self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Export")) - self.label.setText(_translate("Form", "Item to export:")) - self.label_2.setText(_translate("Form", "Export format")) - self.exportBtn.setText(_translate("Form", "Export")) - self.closeBtn.setText(_translate("Form", "Close")) - self.label_3.setText(_translate("Form", "Export options")) - self.copyBtn.setText(_translate("Form", "Copy")) - -from ..parametertree import ParameterTree +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'exportDialogTemplate.ui' +# +# Created: Sun Sep 18 19:19:58 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(241, 367) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 3) + self.itemTree = QtWidgets.QTreeWidget(Form) + self.itemTree.setObjectName("itemTree") + self.itemTree.headerItem().setText(0, "1") + self.itemTree.header().setVisible(False) + self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) + self.label_2 = QtWidgets.QLabel(Form) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) + self.formatList = QtWidgets.QListWidget(Form) + self.formatList.setObjectName("formatList") + self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) + self.exportBtn = QtWidgets.QPushButton(Form) + self.exportBtn.setObjectName("exportBtn") + self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) + self.closeBtn = QtWidgets.QPushButton(Form) + self.closeBtn.setObjectName("closeBtn") + self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) + self.paramTree = ParameterTree(Form) + self.paramTree.setObjectName("paramTree") + self.paramTree.headerItem().setText(0, "1") + self.paramTree.header().setVisible(False) + self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) + self.label_3 = QtWidgets.QLabel(Form) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + self.copyBtn = QtWidgets.QPushButton(Form) + self.copyBtn.setObjectName("copyBtn") + self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Export", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Item to export:", None, -1)) + self.label_2.setText(QtWidgets.QApplication.translate("Form", "Export format", None, -1)) + self.exportBtn.setText(QtWidgets.QApplication.translate("Form", "Export", None, -1)) + self.closeBtn.setText(QtWidgets.QApplication.translate("Form", "Close", None, -1)) + self.label_3.setText(QtWidgets.QApplication.translate("Form", "Export options", None, -1)) + self.copyBtn.setText(QtWidgets.QApplication.translate("Form", "Copy", None, -1)) + +from ..parametertree import ParameterTree diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside2.py b/pyqtgraph/canvas/CanvasTemplate_pyside2.py index e17bc5fe..884f0480 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside2.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside2.py @@ -1,96 +1,87 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui' -# -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(490, 414) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.view = GraphicsView(self.splitter) - self.view.setObjectName("view") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.storeSvgBtn = QtWidgets.QPushButton(self.layoutWidget) - self.storeSvgBtn.setObjectName("storeSvgBtn") - self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) - self.storePngBtn = QtWidgets.QPushButton(self.layoutWidget) - self.storePngBtn.setObjectName("storePngBtn") - self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) - self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) - self.autoRangeBtn.setSizePolicy(sizePolicy) - self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) - self.redirectCheck.setObjectName("redirectCheck") - self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) - self.redirectCombo.setObjectName("redirectCombo") - self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(100) - sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) - self.itemList.setSizePolicy(sizePolicy) - self.itemList.setHeaderHidden(True) - self.itemList.setObjectName("itemList") - self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) - self.ctrlLayout = QtWidgets.QGridLayout() - self.ctrlLayout.setSpacing(0) - self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) - self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) - self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) - self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) - self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) - self.storeSvgBtn.setText(_translate("Form", "Store SVG")) - self.storePngBtn.setText(_translate("Form", "Store PNG")) - 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")) - self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms")) - self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection")) - self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY")) - -from ..widgets.GraphicsView import GraphicsView -from ..widgets.TreeWidget import TreeWidget -from CanvasManager import CanvasCombo +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CanvasTemplate.ui' +# +# Created: Sun Sep 18 19:18:22 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(490, 414) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.view = GraphicsView(self.splitter) + self.view.setObjectName("view") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) + self.autoRangeBtn.setSizePolicy(sizePolicy) + self.autoRangeBtn.setObjectName("autoRangeBtn") + self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) + self.redirectCheck.setObjectName("redirectCheck") + self.horizontalLayout.addWidget(self.redirectCheck) + self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo.setObjectName("redirectCombo") + self.horizontalLayout.addWidget(self.redirectCombo) + self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) + self.itemList = TreeWidget(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) + self.itemList.setSizePolicy(sizePolicy) + self.itemList.setHeaderHidden(True) + self.itemList.setObjectName("itemList") + self.itemList.headerItem().setText(0, "1") + self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) + self.ctrlLayout = QtWidgets.QGridLayout() + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setObjectName("ctrlLayout") + self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) + self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) + self.resetTransformsBtn.setObjectName("resetTransformsBtn") + self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) + self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) + self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.autoRangeBtn.setText(QtWidgets.QApplication.translate("Form", "Auto Range", None, -1)) + self.redirectCheck.setToolTip(QtWidgets.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, -1)) + self.redirectCheck.setText(QtWidgets.QApplication.translate("Form", "Redirect", None, -1)) + self.resetTransformsBtn.setText(QtWidgets.QApplication.translate("Form", "Reset Transforms", None, -1)) + self.mirrorSelectionBtn.setText(QtWidgets.QApplication.translate("Form", "Mirror Selection", None, -1)) + self.reflectSelectionBtn.setText(QtWidgets.QApplication.translate("Form", "MirrorXY", None, -1)) + +from ..widgets.TreeWidget import TreeWidget +from CanvasManager import CanvasCombo +from ..widgets.GraphicsView import GraphicsView diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py index 253a40d7..06883270 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py @@ -1,56 +1,55 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' -# -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(224, 117) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) - Form.setSizePolicy(sizePolicy) - self.verticalLayout = QtWidgets.QVBoxLayout(Form) - self.verticalLayout.setSpacing(1) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.translateLabel = QtWidgets.QLabel(Form) - self.translateLabel.setObjectName("translateLabel") - self.verticalLayout.addWidget(self.translateLabel) - self.rotateLabel = QtWidgets.QLabel(Form) - self.rotateLabel.setObjectName("rotateLabel") - self.verticalLayout.addWidget(self.rotateLabel) - self.scaleLabel = QtWidgets.QLabel(Form) - self.scaleLabel.setObjectName("scaleLabel") - self.verticalLayout.addWidget(self.scaleLabel) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.mirrorImageBtn = QtWidgets.QPushButton(Form) - self.mirrorImageBtn.setToolTip("") - self.mirrorImageBtn.setObjectName("mirrorImageBtn") - self.horizontalLayout.addWidget(self.mirrorImageBtn) - self.reflectImageBtn = QtWidgets.QPushButton(Form) - self.reflectImageBtn.setObjectName("reflectImageBtn") - self.horizontalLayout.addWidget(self.reflectImageBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) - self.translateLabel.setText(_translate("Form", "Translate:")) - self.rotateLabel.setText(_translate("Form", "Rotate:")) - self.scaleLabel.setText(_translate("Form", "Scale:")) - self.mirrorImageBtn.setText(_translate("Form", "Mirror")) - self.reflectImageBtn.setText(_translate("Form", "Reflect")) - +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'TransformGuiTemplate.ui' +# +# Created: Sun Sep 18 19:18:41 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(224, 117) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + self.verticalLayout = QtWidgets.QVBoxLayout(Form) + self.verticalLayout.setSpacing(1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.translateLabel = QtWidgets.QLabel(Form) + self.translateLabel.setObjectName("translateLabel") + self.verticalLayout.addWidget(self.translateLabel) + self.rotateLabel = QtWidgets.QLabel(Form) + self.rotateLabel.setObjectName("rotateLabel") + self.verticalLayout.addWidget(self.rotateLabel) + self.scaleLabel = QtWidgets.QLabel(Form) + self.scaleLabel.setObjectName("scaleLabel") + self.verticalLayout.addWidget(self.scaleLabel) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.mirrorImageBtn = QtWidgets.QPushButton(Form) + self.mirrorImageBtn.setToolTip("") + self.mirrorImageBtn.setObjectName("mirrorImageBtn") + self.horizontalLayout.addWidget(self.mirrorImageBtn) + self.reflectImageBtn = QtWidgets.QPushButton(Form) + self.reflectImageBtn.setObjectName("reflectImageBtn") + self.horizontalLayout.addWidget(self.reflectImageBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.translateLabel.setText(QtWidgets.QApplication.translate("Form", "Translate:", None, -1)) + self.rotateLabel.setText(QtWidgets.QApplication.translate("Form", "Rotate:", None, -1)) + self.scaleLabel.setText(QtWidgets.QApplication.translate("Form", "Scale:", None, -1)) + self.mirrorImageBtn.setText(QtWidgets.QApplication.translate("Form", "Mirror", None, -1)) + self.reflectImageBtn.setText(QtWidgets.QApplication.translate("Form", "Reflect", None, -1)) + diff --git a/pyqtgraph/console/template_pyside2.py b/pyqtgraph/console/template_pyside2.py index c82354a2..361e96f2 100644 --- a/pyqtgraph/console/template_pyside2.py +++ b/pyqtgraph/console/template_pyside2.py @@ -1,107 +1,113 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' -# -# Created: Wed Mar 26 15:09:29 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(710, 497) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Vertical) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) - font = QtGui.QFont() - font.setFamily("Monospace") - self.output.setFont(font) - self.output.setReadOnly(True) - self.output.setObjectName("output") - self.verticalLayout.addWidget(self.output) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.input = CmdInput(self.layoutWidget) - self.input.setObjectName("input") - self.horizontalLayout.addWidget(self.input) - self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) - self.historyBtn.setCheckable(True) - self.historyBtn.setObjectName("historyBtn") - self.horizontalLayout.addWidget(self.historyBtn) - self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.exceptionBtn.setCheckable(True) - self.exceptionBtn.setObjectName("exceptionBtn") - self.horizontalLayout.addWidget(self.exceptionBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - self.historyList = QtWidgets.QListWidget(self.splitter) - font = QtGui.QFont() - font.setFamily("Monospace") - self.historyList.setFont(font) - self.historyList.setObjectName("historyList") - self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) - self.exceptionGroup.setObjectName("exceptionGroup") - self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchAllExceptionsBtn.setCheckable(True) - self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") - self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) - self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchNextExceptionBtn.setCheckable(True) - self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") - self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) - self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.onlyUncaughtCheck.setChecked(True) - self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) - self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) - self.exceptionStackList.setAlternatingRowColors(True) - self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) - self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.runSelectedFrameCheck.setChecked(True) - self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) - self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) - self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) - self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Console")) - self.historyBtn.setText(_translate("Form", "History..")) - self.exceptionBtn.setText(_translate("Form", "Exceptions..")) - self.exceptionGroup.setTitle(_translate("Form", "Exception Handling")) - self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions")) - self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception")) - self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions")) - self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame")) - self.exceptionInfoLabel.setText(_translate("Form", "Exception Info")) - self.clearExceptionBtn.setText(_translate("Form", "Clear Exception")) - -from .CmdInput import CmdInput +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'template.ui' +# +# Created: Sun Sep 18 19:19:10 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(694, 497) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) + font = QtGui.QFont() + font.setFamily("Monospace") + self.output.setFont(font) + self.output.setReadOnly(True) + self.output.setObjectName("output") + self.verticalLayout.addWidget(self.output) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.input = CmdInput(self.layoutWidget) + self.input.setObjectName("input") + self.horizontalLayout.addWidget(self.input) + self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) + self.historyBtn.setCheckable(True) + self.historyBtn.setObjectName("historyBtn") + self.horizontalLayout.addWidget(self.historyBtn) + self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.exceptionBtn.setCheckable(True) + self.exceptionBtn.setObjectName("exceptionBtn") + self.horizontalLayout.addWidget(self.exceptionBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + self.historyList = QtWidgets.QListWidget(self.splitter) + font = QtGui.QFont() + font.setFamily("Monospace") + self.historyList.setFont(font) + self.historyList.setObjectName("historyList") + self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) + self.exceptionGroup.setObjectName("exceptionGroup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) + self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.catchAllExceptionsBtn.setCheckable(True) + self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") + self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) + self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.catchNextExceptionBtn.setCheckable(True) + self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") + self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) + self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) + self.onlyUncaughtCheck.setChecked(True) + self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) + self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) + self.exceptionStackList.setAlternatingRowColors(True) + self.exceptionStackList.setObjectName("exceptionStackList") + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) + self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) + self.runSelectedFrameCheck.setChecked(True) + self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) + self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) + self.label = QtWidgets.QLabel(self.exceptionGroup) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) + self.filterText = QtWidgets.QLineEdit(self.exceptionGroup) + self.filterText.setObjectName("filterText") + self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Console", None, -1)) + self.historyBtn.setText(QtWidgets.QApplication.translate("Form", "History..", None, -1)) + self.exceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Exceptions..", None, -1)) + self.exceptionGroup.setTitle(QtWidgets.QApplication.translate("Form", "Exception Handling", None, -1)) + self.clearExceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Clear Exception", None, -1)) + self.catchAllExceptionsBtn.setText(QtWidgets.QApplication.translate("Form", "Show All Exceptions", None, -1)) + self.catchNextExceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Show Next Exception", None, -1)) + self.onlyUncaughtCheck.setText(QtWidgets.QApplication.translate("Form", "Only Uncaught Exceptions", None, -1)) + self.runSelectedFrameCheck.setText(QtWidgets.QApplication.translate("Form", "Run commands in selected stack frame", None, -1)) + self.exceptionInfoLabel.setText(QtWidgets.QApplication.translate("Form", "Exception Info", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Filter (regex):", None, -1)) + +from .CmdInput import CmdInput diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py index 84593c8a..86fbf5d9 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py @@ -1,67 +1,66 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartCtrlTemplate.ui' -# -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(217, 499) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setVerticalSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.loadBtn = QtWidgets.QPushButton(Form) - self.loadBtn.setObjectName("loadBtn") - self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) - self.saveBtn = FeedbackButton(Form) - self.saveBtn.setObjectName("saveBtn") - self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) - self.saveAsBtn = FeedbackButton(Form) - self.saveAsBtn.setObjectName("saveAsBtn") - self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) - self.reloadBtn = FeedbackButton(Form) - self.reloadBtn.setCheckable(False) - self.reloadBtn.setFlat(False) - self.reloadBtn.setObjectName("reloadBtn") - self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) - self.showChartBtn = QtWidgets.QPushButton(Form) - self.showChartBtn.setCheckable(True) - self.showChartBtn.setObjectName("showChartBtn") - self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) - self.ctrlList = TreeWidget(Form) - self.ctrlList.setObjectName("ctrlList") - self.ctrlList.headerItem().setText(0, "1") - self.ctrlList.header().setVisible(False) - self.ctrlList.header().setStretchLastSection(False) - self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) - self.fileNameLabel = QtWidgets.QLabel(Form) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.fileNameLabel.setFont(font) - self.fileNameLabel.setText("") - self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) - self.fileNameLabel.setObjectName("fileNameLabel") - self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) - self.loadBtn.setText(_translate("Form", "Load..")) - self.saveBtn.setText(_translate("Form", "Save")) - self.saveAsBtn.setText(_translate("Form", "As..")) - self.reloadBtn.setText(_translate("Form", "Reload Libs")) - self.showChartBtn.setText(_translate("Form", "Flowchart")) - -from ..widgets.FeedbackButton import FeedbackButton -from ..widgets.TreeWidget import TreeWidget +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'FlowchartCtrlTemplate.ui' +# +# Created: Sun Sep 18 19:16:46 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(217, 499) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setVerticalSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.loadBtn = QtWidgets.QPushButton(Form) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) + self.saveBtn = FeedbackButton(Form) + self.saveBtn.setObjectName("saveBtn") + self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) + self.saveAsBtn = FeedbackButton(Form) + self.saveAsBtn.setObjectName("saveAsBtn") + self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) + self.reloadBtn = FeedbackButton(Form) + self.reloadBtn.setCheckable(False) + self.reloadBtn.setFlat(False) + self.reloadBtn.setObjectName("reloadBtn") + self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) + self.showChartBtn = QtWidgets.QPushButton(Form) + self.showChartBtn.setCheckable(True) + self.showChartBtn.setObjectName("showChartBtn") + self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) + self.ctrlList = TreeWidget(Form) + self.ctrlList.setObjectName("ctrlList") + self.ctrlList.headerItem().setText(0, "1") + self.ctrlList.header().setVisible(False) + self.ctrlList.header().setStretchLastSection(False) + self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) + self.fileNameLabel = QtWidgets.QLabel(Form) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.fileNameLabel.setFont(font) + self.fileNameLabel.setText("") + self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fileNameLabel.setObjectName("fileNameLabel") + self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.loadBtn.setText(QtWidgets.QApplication.translate("Form", "Load..", None, -1)) + self.saveBtn.setText(QtWidgets.QApplication.translate("Form", "Save", None, -1)) + self.saveAsBtn.setText(QtWidgets.QApplication.translate("Form", "As..", None, -1)) + self.reloadBtn.setText(QtWidgets.QApplication.translate("Form", "Reload Libs", None, -1)) + self.showChartBtn.setText(QtWidgets.QApplication.translate("Form", "Flowchart", None, -1)) + +from ..widgets.FeedbackButton import FeedbackButton +from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py index ae2c9a92..58fd6775 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py @@ -1,55 +1,54 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './pyqtgraph/flowchart/FlowchartTemplate.ui' -# -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(529, 329) - self.selInfoWidget = QtWidgets.QWidget(Form) - self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) - self.selInfoWidget.setObjectName("selInfoWidget") - self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) - self.selDescLabel.setText("") - self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.selDescLabel.setWordWrap(True) - self.selDescLabel.setObjectName("selDescLabel") - self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) - self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.selNameLabel.setFont(font) - self.selNameLabel.setText("") - self.selNameLabel.setObjectName("selNameLabel") - self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) - self.selectedTree = DataTreeWidget(self.selInfoWidget) - self.selectedTree.setObjectName("selectedTree") - self.selectedTree.headerItem().setText(0, "1") - self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) - self.hoverText = QtWidgets.QTextEdit(Form) - self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) - self.hoverText.setObjectName("hoverText") - self.view = FlowchartGraphicsView(Form) - self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) - self.view.setObjectName("view") - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) - -from ..widgets.DataTreeWidget import DataTreeWidget -from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'FlowchartTemplate.ui' +# +# Created: Sun Sep 18 19:16:03 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(529, 329) + self.selInfoWidget = QtWidgets.QWidget(Form) + self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) + self.selInfoWidget.setObjectName("selInfoWidget") + self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) + self.selDescLabel.setText("") + self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.selDescLabel.setWordWrap(True) + self.selDescLabel.setObjectName("selDescLabel") + self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) + self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.selNameLabel.setFont(font) + self.selNameLabel.setText("") + self.selNameLabel.setObjectName("selNameLabel") + self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) + self.selectedTree = DataTreeWidget(self.selInfoWidget) + self.selectedTree.setObjectName("selectedTree") + self.selectedTree.headerItem().setText(0, "1") + self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) + self.hoverText = QtWidgets.QTextEdit(Form) + self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) + self.hoverText.setObjectName("hoverText") + self.view = FlowchartGraphicsView(Form) + self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) + self.view.setObjectName("view") + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + +from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView +from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside2.py b/pyqtgraph/imageview/ImageViewTemplate_pyside2.py index 70b76635..74ea308d 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyside2.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside2.py @@ -1,156 +1,154 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui' -# -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(726, 588) - self.gridLayout_3 = QtWidgets.QGridLayout(Form) - self.gridLayout_3.setContentsMargins(0, 0, 0, 0) - self.gridLayout_3.setSpacing(0) - self.gridLayout_3.setObjectName("gridLayout_3") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Vertical) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout.setSpacing(0) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.graphicsView = GraphicsView(self.layoutWidget) - self.graphicsView.setObjectName("graphicsView") - self.gridLayout.addWidget(self.graphicsView, 0, 0, 2, 1) - self.histogram = HistogramLUTWidget(self.layoutWidget) - self.histogram.setObjectName("histogram") - self.gridLayout.addWidget(self.histogram, 0, 1, 1, 2) - self.roiBtn = QtWidgets.QPushButton(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) - self.roiBtn.setSizePolicy(sizePolicy) - self.roiBtn.setCheckable(True) - self.roiBtn.setObjectName("roiBtn") - self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) - self.menuBtn = QtWidgets.QPushButton(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth()) - self.menuBtn.setSizePolicy(sizePolicy) - self.menuBtn.setCheckable(True) - self.menuBtn.setObjectName("menuBtn") - self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1) - self.roiPlot = PlotWidget(self.splitter) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) - self.roiPlot.setSizePolicy(sizePolicy) - self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) - self.roiPlot.setObjectName("roiPlot") - self.gridLayout_3.addWidget(self.splitter, 0, 0, 1, 1) - self.normGroup = QtWidgets.QGroupBox(Form) - self.normGroup.setObjectName("normGroup") - self.gridLayout_2 = QtWidgets.QGridLayout(self.normGroup) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.normSubtractRadio = QtWidgets.QRadioButton(self.normGroup) - self.normSubtractRadio.setObjectName("normSubtractRadio") - self.gridLayout_2.addWidget(self.normSubtractRadio, 0, 2, 1, 1) - self.normDivideRadio = QtWidgets.QRadioButton(self.normGroup) - self.normDivideRadio.setChecked(False) - self.normDivideRadio.setObjectName("normDivideRadio") - self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) - self.label_5 = QtWidgets.QLabel(self.normGroup) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.label_5.setFont(font) - self.label_5.setObjectName("label_5") - self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) - self.label_3 = QtWidgets.QLabel(self.normGroup) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.label_3.setFont(font) - self.label_3.setObjectName("label_3") - self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) - self.label_4 = QtWidgets.QLabel(self.normGroup) - font = QtGui.QFont() - font.setBold(True) - font.setWeight(75) - self.label_4.setFont(font) - self.label_4.setObjectName("label_4") - self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) - self.normROICheck = QtWidgets.QCheckBox(self.normGroup) - self.normROICheck.setObjectName("normROICheck") - self.gridLayout_2.addWidget(self.normROICheck, 1, 1, 1, 1) - self.normXBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) - self.normXBlurSpin.setObjectName("normXBlurSpin") - self.gridLayout_2.addWidget(self.normXBlurSpin, 2, 2, 1, 1) - self.label_8 = QtWidgets.QLabel(self.normGroup) - self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_8.setObjectName("label_8") - self.gridLayout_2.addWidget(self.label_8, 2, 1, 1, 1) - self.label_9 = QtWidgets.QLabel(self.normGroup) - self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_9.setObjectName("label_9") - self.gridLayout_2.addWidget(self.label_9, 2, 3, 1, 1) - self.normYBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) - self.normYBlurSpin.setObjectName("normYBlurSpin") - self.gridLayout_2.addWidget(self.normYBlurSpin, 2, 4, 1, 1) - self.label_10 = QtWidgets.QLabel(self.normGroup) - self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_10.setObjectName("label_10") - self.gridLayout_2.addWidget(self.label_10, 2, 5, 1, 1) - self.normOffRadio = QtWidgets.QRadioButton(self.normGroup) - self.normOffRadio.setChecked(True) - self.normOffRadio.setObjectName("normOffRadio") - self.gridLayout_2.addWidget(self.normOffRadio, 0, 3, 1, 1) - self.normTimeRangeCheck = QtWidgets.QCheckBox(self.normGroup) - self.normTimeRangeCheck.setObjectName("normTimeRangeCheck") - self.gridLayout_2.addWidget(self.normTimeRangeCheck, 1, 3, 1, 1) - self.normFrameCheck = QtWidgets.QCheckBox(self.normGroup) - self.normFrameCheck.setObjectName("normFrameCheck") - self.gridLayout_2.addWidget(self.normFrameCheck, 1, 2, 1, 1) - self.normTBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) - self.normTBlurSpin.setObjectName("normTBlurSpin") - self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) - self.gridLayout_3.addWidget(self.normGroup, 1, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - _translate = QtCore.QCoreApplication.translate - Form.setWindowTitle(_translate("Form", "Form")) - self.roiBtn.setText(_translate("Form", "ROI")) - self.menuBtn.setText(_translate("Form", "Norm")) - self.normGroup.setTitle(_translate("Form", "Normalization")) - self.normSubtractRadio.setText(_translate("Form", "Subtract")) - self.normDivideRadio.setText(_translate("Form", "Divide")) - self.label_5.setText(_translate("Form", "Operation:")) - self.label_3.setText(_translate("Form", "Mean:")) - self.label_4.setText(_translate("Form", "Blur:")) - self.normROICheck.setText(_translate("Form", "ROI")) - self.label_8.setText(_translate("Form", "X")) - self.label_9.setText(_translate("Form", "Y")) - self.label_10.setText(_translate("Form", "T")) - self.normOffRadio.setText(_translate("Form", "Off")) - self.normTimeRangeCheck.setText(_translate("Form", "Time range")) - self.normFrameCheck.setText(_translate("Form", "Frame")) - -from ..widgets.HistogramLUTWidget import HistogramLUTWidget -from ..widgets.PlotWidget import PlotWidget -from ..widgets.GraphicsView import GraphicsView +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ImageViewTemplate.ui' +# +# Created: Sun Sep 18 19:17:41 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(726, 588) + self.gridLayout_3 = QtWidgets.QGridLayout(Form) + self.gridLayout_3.setContentsMargins(0, 0, 0, 0) + self.gridLayout_3.setSpacing(0) + self.gridLayout_3.setObjectName("gridLayout_3") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) + self.gridLayout.setSpacing(0) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.graphicsView = GraphicsView(self.layoutWidget) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout.addWidget(self.graphicsView, 0, 0, 2, 1) + self.histogram = HistogramLUTWidget(self.layoutWidget) + self.histogram.setObjectName("histogram") + self.gridLayout.addWidget(self.histogram, 0, 1, 1, 2) + self.roiBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) + self.roiBtn.setSizePolicy(sizePolicy) + self.roiBtn.setCheckable(True) + self.roiBtn.setObjectName("roiBtn") + self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) + self.menuBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth()) + self.menuBtn.setSizePolicy(sizePolicy) + self.menuBtn.setObjectName("menuBtn") + self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1) + self.roiPlot = PlotWidget(self.splitter) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) + self.roiPlot.setSizePolicy(sizePolicy) + self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) + self.roiPlot.setObjectName("roiPlot") + self.gridLayout_3.addWidget(self.splitter, 0, 0, 1, 1) + self.normGroup = QtWidgets.QGroupBox(Form) + self.normGroup.setObjectName("normGroup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.normGroup) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.normSubtractRadio = QtWidgets.QRadioButton(self.normGroup) + self.normSubtractRadio.setObjectName("normSubtractRadio") + self.gridLayout_2.addWidget(self.normSubtractRadio, 0, 2, 1, 1) + self.normDivideRadio = QtWidgets.QRadioButton(self.normGroup) + self.normDivideRadio.setChecked(False) + self.normDivideRadio.setObjectName("normDivideRadio") + self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_5.setFont(font) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) + self.label_3 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) + self.label_4 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_4.setFont(font) + self.label_4.setObjectName("label_4") + self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) + self.normROICheck = QtWidgets.QCheckBox(self.normGroup) + self.normROICheck.setObjectName("normROICheck") + self.gridLayout_2.addWidget(self.normROICheck, 1, 1, 1, 1) + self.normXBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normXBlurSpin.setObjectName("normXBlurSpin") + self.gridLayout_2.addWidget(self.normXBlurSpin, 2, 2, 1, 1) + self.label_8 = QtWidgets.QLabel(self.normGroup) + self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_8.setObjectName("label_8") + self.gridLayout_2.addWidget(self.label_8, 2, 1, 1, 1) + self.label_9 = QtWidgets.QLabel(self.normGroup) + self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_9.setObjectName("label_9") + self.gridLayout_2.addWidget(self.label_9, 2, 3, 1, 1) + self.normYBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normYBlurSpin.setObjectName("normYBlurSpin") + self.gridLayout_2.addWidget(self.normYBlurSpin, 2, 4, 1, 1) + self.label_10 = QtWidgets.QLabel(self.normGroup) + self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_10.setObjectName("label_10") + self.gridLayout_2.addWidget(self.label_10, 2, 5, 1, 1) + self.normOffRadio = QtWidgets.QRadioButton(self.normGroup) + self.normOffRadio.setChecked(True) + self.normOffRadio.setObjectName("normOffRadio") + self.gridLayout_2.addWidget(self.normOffRadio, 0, 3, 1, 1) + self.normTimeRangeCheck = QtWidgets.QCheckBox(self.normGroup) + self.normTimeRangeCheck.setObjectName("normTimeRangeCheck") + self.gridLayout_2.addWidget(self.normTimeRangeCheck, 1, 3, 1, 1) + self.normFrameCheck = QtWidgets.QCheckBox(self.normGroup) + self.normFrameCheck.setObjectName("normFrameCheck") + self.gridLayout_2.addWidget(self.normFrameCheck, 1, 2, 1, 1) + self.normTBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normTBlurSpin.setObjectName("normTBlurSpin") + self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) + self.gridLayout_3.addWidget(self.normGroup, 1, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.roiBtn.setText(QtWidgets.QApplication.translate("Form", "ROI", None, -1)) + self.menuBtn.setText(QtWidgets.QApplication.translate("Form", "Menu", None, -1)) + self.normGroup.setTitle(QtWidgets.QApplication.translate("Form", "Normalization", None, -1)) + self.normSubtractRadio.setText(QtWidgets.QApplication.translate("Form", "Subtract", None, -1)) + self.normDivideRadio.setText(QtWidgets.QApplication.translate("Form", "Divide", None, -1)) + self.label_5.setText(QtWidgets.QApplication.translate("Form", "Operation:", None, -1)) + self.label_3.setText(QtWidgets.QApplication.translate("Form", "Mean:", None, -1)) + self.label_4.setText(QtWidgets.QApplication.translate("Form", "Blur:", None, -1)) + self.normROICheck.setText(QtWidgets.QApplication.translate("Form", "ROI", None, -1)) + self.label_8.setText(QtWidgets.QApplication.translate("Form", "X", None, -1)) + self.label_9.setText(QtWidgets.QApplication.translate("Form", "Y", None, -1)) + self.label_10.setText(QtWidgets.QApplication.translate("Form", "T", None, -1)) + self.normOffRadio.setText(QtWidgets.QApplication.translate("Form", "Off", None, -1)) + self.normTimeRangeCheck.setText(QtWidgets.QApplication.translate("Form", "Time range", None, -1)) + self.normFrameCheck.setText(QtWidgets.QApplication.translate("Form", "Frame", None, -1)) + +from ..widgets.HistogramLUTWidget import HistogramLUTWidget +from ..widgets.PlotWidget import PlotWidget +from ..widgets.GraphicsView import GraphicsView From 3664115dde20b2c875b69210e258d899b7c47a83 Mon Sep 17 00:00:00 2001 From: Max Peng Date: Sun, 25 Sep 2016 00:32:37 +0800 Subject: [PATCH 021/607] update the messages when none of Qt backends can be found. --- pyqtgraph/Qt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 59beb67d..7e5acb10 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -42,7 +42,7 @@ if QT_LIB is None: pass if QT_LIB is None: - raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") + raise Exception("PyQtGraph requires one of PyQt4, PyQt5, PySide or PySide2; none of these packages could be imported.") if QT_LIB == PYSIDE or QT_LIB == PYSIDE2 : if QT_LIB == PYSIDE: From cf2329b75e7fe63e3e3cb616f6475634d8c2a04b Mon Sep 17 00:00:00 2001 From: Ben Date: Wed, 28 Sep 2016 17:00:10 -0600 Subject: [PATCH 022/607] Fix issue with Python3 and changes in how it handles zip. --- pyqtgraph/colormap.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index f943e2fe..585d7ea1 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -141,7 +141,7 @@ class ColorMap(object): pos, color = self.getStops(mode=self.BYTE) color = [QtGui.QColor(*x) for x in color] - g.setStops(zip(pos, color)) + g.setStops(list(zip(pos, color))) #if self.colorMode == 'rgb': #ticks = self.listTicks() From 92fc9dbe2f0e97f4cac04454877ea65232b1a19b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 12 Oct 2016 09:58:03 -0700 Subject: [PATCH 023/607] Add unit test for interpolateArray with order=0 docstring update --- pyqtgraph/functions.py | 7 ++++-- pyqtgraph/tests/test_functions.py | 37 ++++++++++++++----------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 187a4717..32d9f2bf 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -536,12 +536,15 @@ def interpolateArray(data, x, default=0.0, order=1): N-dimensional interpolation similar to scipy.ndimage.map_coordinates. This function returns linearly-interpolated values sampled from a regular - grid of data. + grid of data. It differs from `ndimage.map_coordinates` by allowing broadcasting + within the input array. ============== =========================================================================================== **Arguments:** *data* Array of any shape containing the values to be interpolated. - *x* Array with (shape[-1] <= data.ndim) containing the locations within *data* to interpolate. + *x* Array with (shape[-1] <= data.ndim) containing the locations within *data* to interpolate. + (note: the axes for this argument are transposed relative to the same argument for + `ndimage.map_coordinates`). *default* Value to return for locations in *x* that are outside the bounds of *data*. *order* Order of interpolation: 0=nearest, 1=linear. ============== =========================================================================================== diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index bfa7e0ea..4c9cabfe 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -22,9 +22,17 @@ def testSolve3D(): assert_array_almost_equal(tr[:3], tr2[:3]) -def test_interpolateArray(): +def test_interpolateArray_order0(): + check_interpolateArray(order=0) + + +def test_interpolateArray_order1(): + check_interpolateArray(order=1) + + +def check_interpolateArray(order): def interpolateArray(data, x): - result = pg.interpolateArray(data, x) + result = pg.interpolateArray(data, x, order=order) assert result.shape == x.shape[:-1] + data.shape[x.shape[-1]:] return result @@ -48,7 +56,6 @@ def test_interpolateArray(): with pytest.raises(TypeError): interpolateArray(data, np.ones((5, 5, 3,))) - x = np.array([[ 0.3, 0.6], [ 1. , 1. ], [ 0.5, 1. ], @@ -56,9 +63,10 @@ def test_interpolateArray(): [ 10. , 10. ]]) result = interpolateArray(data, x) - #import scipy.ndimage - #spresult = scipy.ndimage.map_coordinates(data, x.T, order=1) - spresult = np.array([ 5.92, 20. , 11. , 0. , 0. ]) # generated with the above line + # make sure results match ndimage.map_coordinates + import scipy.ndimage + spresult = scipy.ndimage.map_coordinates(data, x.T, order=order) + #spresult = np.array([ 5.92, 20. , 11. , 0. , 0. ]) # generated with the above line assert_array_almost_equal(result, spresult) @@ -78,24 +86,13 @@ def test_interpolateArray(): [[1.5, 0.5], [1.5, 1.0], [1.5, 1.5]]]) r1 = interpolateArray(data, x) - #r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=1) - r2 = np.array([[ 8.25, 11. , 16.5 ], # generated with the above line - [ 82.5 , 110. , 165. ]]) + r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=order) + #r2 = np.array([[ 8.25, 11. , 16.5 ], # generated with the above line + #[ 82.5 , 110. , 165. ]]) assert_array_almost_equal(r1, r2) - # test interpolate where data.ndim > x.shape[1] - - data = np.array([[[1, 2, 3], [4, 5, 6]], [[7, 8, 9], [10, 11, 12]]]) # 2x2x3 - x = np.array([[1, 1], [0, 0.5], [5, 5]]) - - r1 = interpolateArray(data, x) - assert np.all(r1[0] == data[1, 1]) - assert np.all(r1[1] == 0.5 * (data[0, 0] + data[0, 1])) - assert np.all(r1[2] == 0) - - def test_subArray(): a = np.array([0, 0, 111, 112, 113, 0, 121, 122, 123, 0, 0, 0, 211, 212, 213, 0, 221, 222, 223, 0, 0, 0, 0]) b = pg.subArray(a, offset=2, shape=(2,2,3), stride=(10,4,1)) From e35f59fcb77d50ab493d7c9db847c24c4fbd6006 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 12 Oct 2016 10:26:54 -0700 Subject: [PATCH 024/607] Fix interpolateArray for order=0 --- pyqtgraph/functions.py | 65 +++++++++++++++++-------------- pyqtgraph/tests/test_functions.py | 8 ++-- 2 files changed, 40 insertions(+), 33 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 32d9f2bf..8593241e 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -602,37 +602,44 @@ def interpolateArray(data, x, default=0.0, order=1): if md > nd: raise TypeError("x.shape[-1] must be less than or equal to data.ndim") - # First we generate arrays of indexes that are needed to - # extract the data surrounding each point - fields = np.mgrid[(slice(0,order+1),) * md] - xmin = np.floor(x).astype(int) - xmax = xmin + 1 - indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]]) - fieldInds = [] totalMask = np.ones(x.shape[:-1], dtype=bool) # keep track of out-of-bound indexes - for ax in range(md): - mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1) - # keep track of points that need to be set to default - totalMask &= mask - - # ..and keep track of indexes that are out of bounds - # (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out - # of bounds, but the interpolation will work anyway) - mask &= (xmax[...,ax] < data.shape[ax]) - axisIndex = indexes[...,ax][fields[ax]] - axisIndex[axisIndex < 0] = 0 - axisIndex[axisIndex >= data.shape[ax]] = 0 - fieldInds.append(axisIndex) - prof() - - # Get data values surrounding each requested point - fieldData = data[tuple(fieldInds)] - prof() - - ## Interpolate if order == 0: - result = fieldData[0,0] - else: + xinds = np.round(x).astype(int) # NOTE: for 0.5 this rounds to the nearest *even* number + for ax in range(md): + mask = (xinds[...,ax] >= 0) & (xinds[...,ax] <= data.shape[ax]-1) + xinds[...,ax][~mask] = 0 + # keep track of points that need to be set to default + totalMask &= mask + result = data[tuple([xinds[...,i] for i in range(xinds.shape[-1])])] + + elif order == 1: + # First we generate arrays of indexes that are needed to + # extract the data surrounding each point + fields = np.mgrid[(slice(0,order+1),) * md] + xmin = np.floor(x).astype(int) + xmax = xmin + 1 + indexes = np.concatenate([xmin[np.newaxis, ...], xmax[np.newaxis, ...]]) + fieldInds = [] + for ax in range(md): + mask = (xmin[...,ax] >= 0) & (x[...,ax] <= data.shape[ax]-1) + # keep track of points that need to be set to default + totalMask &= mask + + # ..and keep track of indexes that are out of bounds + # (note that when x[...,ax] == data.shape[ax], then xmax[...,ax] will be out + # of bounds, but the interpolation will work anyway) + mask &= (xmax[...,ax] < data.shape[ax]) + axisIndex = indexes[...,ax][fields[ax]] + axisIndex[axisIndex < 0] = 0 + axisIndex[axisIndex >= data.shape[ax]] = 0 + fieldInds.append(axisIndex) + prof() + + # Get data values surrounding each requested point + fieldData = data[tuple(fieldInds)] + prof() + + ## Interpolate s = np.empty((md,) + fieldData.shape, dtype=float) dx = x - xmin # reshape fields for arithmetic against dx diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 4c9cabfe..7ad3bf91 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -58,8 +58,8 @@ def check_interpolateArray(order): x = np.array([[ 0.3, 0.6], [ 1. , 1. ], - [ 0.5, 1. ], - [ 0.5, 2.5], + [ 0.501, 1. ], # NOTE: testing at exactly 0.5 can yield different results from map_coordinates + [ 0.501, 2.501], # due to differences in rounding [ 10. , 10. ]]) result = interpolateArray(data, x) @@ -82,8 +82,8 @@ def check_interpolateArray(order): # test mapping 2D array of locations - x = np.array([[[0.5, 0.5], [0.5, 1.0], [0.5, 1.5]], - [[1.5, 0.5], [1.5, 1.0], [1.5, 1.5]]]) + x = np.array([[[0.501, 0.501], [0.501, 1.0], [0.501, 1.501]], + [[1.501, 0.501], [1.501, 1.0], [1.501, 1.501]]]) r1 = interpolateArray(data, x) r2 = scipy.ndimage.map_coordinates(data, x.transpose(2,0,1), order=order) From 09725dcb556dfeaf1ae6500ffc02f26351939ce5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 7 Nov 2016 17:57:23 -0800 Subject: [PATCH 025/607] Fixes to --publish option in pg-release script --- tools/pg-release.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tools/pg-release.py b/tools/pg-release.py index ac32b199..bc05f638 100644 --- a/tools/pg-release.py +++ b/tools/pg-release.py @@ -77,7 +77,7 @@ def build(args): mkdir -p {build_dir} cd {build_dir} rm -rf pyqtgraph - git clone --depth 1 -b master {source_repo} pyqtgraph + git clone --depth 1 --branch master --single-branch {source_repo} pyqtgraph cd pyqtgraph git checkout -b release-{version} git pull {source_repo} release-{version} @@ -202,15 +202,19 @@ def publish(args): ### Upload everything to server shell(""" - # Uploading documentation.. cd {build_dir}/pyqtgraph - rsync -rv doc/build/* pyqtgraph.org:/www/code/pyqtgraph/pyqtgraph/documentation/build/ + + # Uploading documentation.. (disabled; now hosted by readthedocs.io) + #rsync -rv doc/build/* pyqtgraph.org:/www/code/pyqtgraph/pyqtgraph/documentation/build/ # Uploading release packages to website - rsync -v {pkg_dir}/{version} pyqtgraph.org:/www/code/pyqtgraph/downloads/ + rsync -v {pkg_dir} pyqtgraph.org:/www/code/pyqtgraph/downloads/ - # Push to github - git push --tags https://github.com/pyqtgraph/pyqtgraph master:master + # Push master to github + git push https://github.com/pyqtgraph/pyqtgraph master:master + + # Push tag to github + git push https://github.com/pyqtgraph/pyqtgraph pyqtgraph-{version} # Upload to pypi.. python setup.py sdist upload From 0bc711b31f2a2cf0f06716885bd61f4ea223c3e6 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 10 Nov 2016 11:22:52 +0100 Subject: [PATCH 026/607] Revert "ignore wheel events in GraphicsView if mouse disabled" This reverts commit f49c179275e86786af70b38b8c5085e38d4e6cce. On Qt 5.7 ignoring the initial `wheelEvent` when `phase() == Qt.ScrollBegin` suppresses all intermediate events (`Qt.ScrollUpdate`) from being delivered to the view. This makes ViewBox zooming unresponsive. --- pyqtgraph/widgets/GraphicsView.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index f3f8cbb5..45cc0254 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -325,7 +325,6 @@ class GraphicsView(QtGui.QGraphicsView): def wheelEvent(self, ev): QtGui.QGraphicsView.wheelEvent(self, ev) if not self.mouseEnabled: - ev.ignore() return sc = 1.001 ** ev.delta() #self.scale *= sc From 2d754672a0c696c0d97da9e7f2148f8ab99fe86e Mon Sep 17 00:00:00 2001 From: Karl Bedrich Date: Thu, 10 Nov 2016 17:01:15 +0000 Subject: [PATCH 027/607] 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 f612d845fcf08601153d1e864240945f1c69a9e1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 10 Nov 2016 18:24:47 -0800 Subject: [PATCH 028/607] Fix canvas classes for PyQt5 and PySide --- CONTRIBUTING.txt | 6 ++-- pyqtgraph/canvas/Canvas.py | 10 +++--- pyqtgraph/canvas/CanvasItem.py | 8 +++-- pyqtgraph/canvas/CanvasTemplate.ui | 2 +- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 35 +++++++++++-------- pyqtgraph/canvas/CanvasTemplate_pyqt5.py | 30 ++++++---------- pyqtgraph/canvas/CanvasTemplate_pyside.py | 30 ++++++---------- pyqtgraph/canvas/TransformGuiTemplate_pyqt.py | 7 ++-- .../canvas/TransformGuiTemplate_pyqt5.py | 7 ++-- .../canvas/TransformGuiTemplate_pyside.py | 6 ++-- tools/rebuildUi.py | 13 +++++-- 11 files changed, 76 insertions(+), 78 deletions(-) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt index 5a904958..5df9703f 100644 --- a/CONTRIBUTING.txt +++ b/CONTRIBUTING.txt @@ -3,12 +3,10 @@ Contributions to pyqtgraph are welcome! Please use the following guidelines when preparing changes: * The preferred method for submitting changes is by github pull request - against the "develop" branch. If this is inconvenient, don't hesitate to - submit by other means. + against the "develop" branch. * Pull requests should include only a focused and related set of changes. - Mixed features and unrelated changes (such as .gitignore) will usually be - rejected. + Mixed features and unrelated changes may be rejected. * For major changes, it is recommended to discuss your plans on the mailing list or in a github issue before putting in too much effort. diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index 4de891f7..5b5ce2f7 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -4,15 +4,17 @@ if __name__ == '__main__': md = os.path.dirname(os.path.abspath(__file__)) sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path -from ..Qt import QtGui, QtCore, USE_PYSIDE +from ..Qt import QtGui, QtCore, QT_LIB from ..graphicsItems.ROI import ROI from ..graphicsItems.ViewBox import ViewBox from ..graphicsItems.GridItem import GridItem -if USE_PYSIDE: +if QT_LIB == 'PySide': from .CanvasTemplate_pyside import * -else: +elif QT_LIB == 'PyQt4': from .CanvasTemplate_pyqt import * +elif QT_LIB == 'PyQt5': + from .CanvasTemplate_pyqt5 import * import numpy as np from .. import debug @@ -378,7 +380,7 @@ class Canvas(QtGui.QWidget): z = citem.zValue() if z is None: zvals = [i.zValue() for i in siblings] - if parent == self.itemList.invisibleRootItem(): + if parent is self.itemList.invisibleRootItem(): if len(zvals) == 0: z = 0 else: diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index b6ecbb39..a06235b2 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -1,11 +1,13 @@ # -*- coding: utf-8 -*- -from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +from ..Qt import QtGui, QtCore, QtSvg, QT_LIB from ..graphicsItems.ROI import ROI from .. import SRTTransform, ItemGroup -if USE_PYSIDE: +if QT_LIB == 'PySide': from . import TransformGuiTemplate_pyside as TransformGuiTemplate -else: +elif QT_LIB == 'PyQt4': from . import TransformGuiTemplate_pyqt as TransformGuiTemplate +elif QT_LIB == 'PyQt5': + from . import TransformGuiTemplate_pyqt5 as TransformGuiTemplate from .. import debug diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index 9bea8f89..b05c11cd 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -127,7 +127,7 @@ CanvasCombo QComboBox -
CanvasManager
+
.CanvasManager
diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index 557354e0..b65ef465 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'acq4/pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Thu Jan 2 11:13:07 2014 -# by: PyQt4 UI code generator 4.9 +# Created by: PyQt4 UI code generator 4.11.4 # # WARNING! All changes made in this file will be lost! @@ -12,7 +11,16 @@ from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 except AttributeError: - _fromUtf8 = lambda s: s + def _fromUtf8(s): + return s + +try: + _encoding = QtGui.QApplication.UnicodeUTF8 + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig, _encoding) +except AttributeError: + def _translate(context, text, disambig): + return QtGui.QApplication.translate(context, text, disambig) class Ui_Form(object): def setupUi(self, Form): @@ -30,7 +38,6 @@ class Ui_Form(object): self.layoutWidget = QtGui.QWidget(self.splitter) self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout_2.setMargin(0) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) @@ -79,14 +86,14 @@ class Ui_Form(object): QtCore.QMetaObject.connectSlotsByName(Form) def retranslateUi(self, Form): - Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", 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)) - self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8)) - self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) - self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) + Form.setWindowTitle(_translate("Form", "Form", 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)) + self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms", None)) + self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection", None)) + self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY", None)) -from ..widgets.TreeWidget import TreeWidget -from CanvasManager import CanvasCombo from ..widgets.GraphicsView import GraphicsView +from ..widgets.TreeWidget import TreeWidget +from .CanvasManager import CanvasCombo diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py index 13b0c83c..20f5e339 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 +# Created by: PyQt5 UI code generator 5.5.1 # # WARNING! All changes made in this file will be lost! @@ -25,14 +24,7 @@ class Ui_Form(object): self.layoutWidget = QtWidgets.QWidget(self.splitter) self.layoutWidget.setObjectName("layoutWidget") self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setObjectName("gridLayout_2") - self.storeSvgBtn = QtWidgets.QPushButton(self.layoutWidget) - self.storeSvgBtn.setObjectName("storeSvgBtn") - self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) - self.storePngBtn = QtWidgets.QPushButton(self.layoutWidget) - self.storePngBtn.setObjectName("storePngBtn") - self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -40,7 +32,7 @@ class Ui_Form(object): sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") @@ -50,7 +42,7 @@ class Ui_Form(object): self.redirectCombo = CanvasCombo(self.layoutWidget) self.redirectCombo.setObjectName("redirectCombo") self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) + self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) self.itemList = TreeWidget(self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -60,20 +52,20 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName("itemList") self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) + self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) self.ctrlLayout = QtWidgets.QGridLayout() self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) + self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) + self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -82,8 +74,6 @@ class Ui_Form(object): def retranslateUi(self, Form): _translate = QtCore.QCoreApplication.translate Form.setWindowTitle(_translate("Form", "Form")) - self.storeSvgBtn.setText(_translate("Form", "Store SVG")) - self.storePngBtn.setText(_translate("Form", "Store PNG")) 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")) @@ -93,4 +83,4 @@ class Ui_Form(object): from ..widgets.GraphicsView import GraphicsView from ..widgets.TreeWidget import TreeWidget -from CanvasManager import CanvasCombo +from .CanvasManager import CanvasCombo diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py index 56d1ff47..b0e05a07 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: pyside-uic 0.2.14 running on PySide 1.1.2 +# Created: Wed Nov 9 18:02:00 2016 +# by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! @@ -27,12 +27,6 @@ class Ui_Form(object): self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) self.gridLayout_2.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setObjectName("gridLayout_2") - self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget) - self.storeSvgBtn.setObjectName("storeSvgBtn") - self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1) - self.storePngBtn = QtGui.QPushButton(self.layoutWidget) - self.storePngBtn.setObjectName("storePngBtn") - self.gridLayout_2.addWidget(self.storePngBtn, 1, 1, 1, 1) self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -40,7 +34,7 @@ class Ui_Form(object): sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 3, 0, 1, 2) + self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") @@ -50,7 +44,7 @@ class Ui_Form(object): self.redirectCombo = CanvasCombo(self.layoutWidget) self.redirectCombo.setObjectName("redirectCombo") self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 6, 0, 1, 2) + self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) self.itemList = TreeWidget(self.layoutWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -60,20 +54,20 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName("itemList") self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 7, 0, 1, 2) + self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) self.ctrlLayout = QtGui.QGridLayout() self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 11, 0, 1, 2) + self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 8, 0, 1, 1) + self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -81,8 +75,6 @@ class Ui_Form(object): def retranslateUi(self, Form): Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) - self.storeSvgBtn.setText(QtGui.QApplication.translate("Form", "Store SVG", None, QtGui.QApplication.UnicodeUTF8)) - self.storePngBtn.setText(QtGui.QApplication.translate("Form", "Store PNG", 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)) @@ -90,6 +82,6 @@ class Ui_Form(object): self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8)) self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8)) +from .CanvasManager import CanvasCombo from ..widgets.TreeWidget import TreeWidget -from CanvasManager import CanvasCombo from ..widgets.GraphicsView import GraphicsView diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py index 75c694c0..c6cf82e4 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/TransformGuiTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: PyQt4 UI code generator 4.10 +# Created by: PyQt4 UI code generator 4.11.4 # # WARNING! All changes made in this file will be lost! @@ -33,8 +32,8 @@ class Ui_Form(object): sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) Form.setSizePolicy(sizePolicy) self.verticalLayout = QtGui.QVBoxLayout(Form) - self.verticalLayout.setSpacing(1) self.verticalLayout.setMargin(0) + self.verticalLayout.setSpacing(1) self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.translateLabel = QtGui.QLabel(Form) self.translateLabel.setObjectName(_fromUtf8("translateLabel")) diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py index 549f3008..6b1f239b 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/TransformGuiTemplate.ui' # -# Created: Wed Mar 26 15:09:28 2014 -# by: PyQt5 UI code generator 5.0.1 +# Created by: PyQt5 UI code generator 5.5.1 # # WARNING! All changes made in this file will be lost! @@ -19,8 +18,8 @@ class Ui_Form(object): sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) Form.setSizePolicy(sizePolicy) self.verticalLayout = QtWidgets.QVBoxLayout(Form) - self.verticalLayout.setSpacing(1) self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(1) self.verticalLayout.setObjectName("verticalLayout") self.translateLabel = QtWidgets.QLabel(Form) self.translateLabel.setObjectName("translateLabel") diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py index bce7b511..e430b61a 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/canvas/TransformGuiTemplate.ui' +# Form implementation generated from reading ui file 'pyqtgraph/canvas/TransformGuiTemplate.ui' # -# Created: Mon Dec 23 10:10:52 2013 -# by: pyside-uic 0.2.14 running on PySide 1.1.2 +# Created: Wed Nov 9 17:57:16 2016 +# by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! diff --git a/tools/rebuildUi.py b/tools/rebuildUi.py index 2ce80d87..bdacda81 100644 --- a/tools/rebuildUi.py +++ b/tools/rebuildUi.py @@ -1,3 +1,4 @@ +#!/usr/bin/python """ Script for compiling Qt Designer .ui files to .py @@ -12,15 +13,23 @@ pyqt5uic = 'pyuic5' usage = """Compile .ui files to .py for all supported pyqt/pyside versions. - Usage: python rebuildUi.py [.ui files|search paths] + Usage: python rebuildUi.py [--force] [.ui files|search paths] May specify a list of .ui files and/or directories to search recursively for .ui files. """ args = sys.argv[1:] + +if '--force' in args: + force = True + args.remove('--force') +else: + force = False + if len(args) == 0: print(usage) sys.exit(-1) + uifiles = [] for arg in args: @@ -42,7 +51,7 @@ for ui in uifiles: base, _ = os.path.splitext(ui) for compiler, ext in [(pyqtuic, '_pyqt.py'), (pysideuic, '_pyside.py'), (pyqt5uic, '_pyqt5.py')]: py = base + ext - if os.path.exists(py) and os.stat(ui).st_mtime <= os.stat(py).st_mtime: + if not force and os.path.exists(py) and os.stat(ui).st_mtime <= os.stat(py).st_mtime: print("Skipping %s; already compiled." % py) else: cmd = '%s %s > %s' % (compiler, ui, py) From c4c31c36502c43a4e660902cec8c550282ba610f Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 28 Nov 2016 15:41:56 +0100 Subject: [PATCH 029/607] Fix `cleanup` when the running qt application is not a QApplication --- pyqtgraph/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 301f9f1e..bc5081f7 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -303,7 +303,10 @@ def cleanup(): ## ALL QGraphicsItems must have a scene before they are deleted. ## This is potentially very expensive, but preferred over crashing. ## Note: this appears to be fixed in PySide as of 2012.12, but it should be left in for a while longer.. - if QtGui.QApplication.instance() is None: + app = QtGui.QApplication.instance() + if app is None or not isinstance(app, QtGui.QApplication): + # app was never constructed is already deleted or is an + # QCoreApplication/QGuiApplication and not a full QApplication return import gc s = QtGui.QGraphicsScene() From e26fb1f9ded0caaec29579f9860d3d319e71de8e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 29 Nov 2016 17:45:42 -0800 Subject: [PATCH 030/607] Add first spinbox tests --- pyqtgraph/widgets/tests/test_spinbox.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 pyqtgraph/widgets/tests/test_spinbox.py diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py new file mode 100644 index 00000000..dcf15cb3 --- /dev/null +++ b/pyqtgraph/widgets/tests/test_spinbox.py @@ -0,0 +1,24 @@ +import pyqtgraph as pg +pg.mkQApp() + +def test_spinbox(): + sb = pg.SpinBox() + assert sb.opts['decimals'] == 3 + assert sb.opts['int'] is False + + # table of test conditions: + # value, text, options + conds = [ + (0, '0', dict(suffix='', siPrefix=False, dec=False, int=False)), + (100, '100', dict()), + (1000000, '1e+06', dict()), + (1000, '1e+03', dict(decimals=2)), + (1000000, '1000000', dict(int=True)), + (12345678955, '12345678955', dict(int=True)), + ] + + for (value, text, opts) in conds: + sb.setOpts(**opts) + sb.setValue(value) + assert sb.value() == value + assert pg.asUnicode(sb.text()) == text From c97c5f51e244ad83691ebf84daf23b842b337462 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 30 Nov 2016 09:16:56 -0800 Subject: [PATCH 031/607] Add spinbox option for custom formatting --- pyqtgraph/functions.py | 3 +- pyqtgraph/widgets/SpinBox.py | 138 +++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 64 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d79c350f..6ec3932f 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -36,8 +36,6 @@ SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY') SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' - - def siScale(x, minVal=1e-25, allowUnicode=True): """ Return the recommended scale factor and SI prefix string for x. @@ -76,6 +74,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True): return (p, pref) + def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): """ Return the number x formatted in engineering notation with SI prefix. diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index a863cd60..50429dee 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -49,28 +49,9 @@ class SpinBox(QtGui.QAbstractSpinBox): **Arguments:** parent Sets the parent widget for this SpinBox (optional). Default is None. value (float/int) initial value. Default is 0.0. - bounds (min,max) Minimum and maximum values allowed in the SpinBox. - Either may be None to leave the value unbounded. By default, values are unbounded. - suffix (str) suffix (units) to display after the numerical value. By default, suffix is an empty str. - siPrefix (bool) If True, then an SI prefix is automatically prepended - to the units and the value is scaled accordingly. For example, - if value=0.003 and suffix='V', then the SpinBox will display - "300 mV" (but a call to SpinBox.value will still return 0.003). Default is False. - step (float) The size of a single step. This is used when clicking the up/ - down arrows, when rolling the mouse wheel, or when pressing - keyboard arrows while the widget has keyboard focus. Note that - the interpretation of this value is different when specifying - the 'dec' argument. Default is 0.01. - dec (bool) If True, then the step value will be adjusted to match - the current size of the variable (for example, a value of 15 - might step in increments of 1 whereas a value of 1500 would - step in increments of 100). In this case, the 'step' argument - is interpreted *relative* to the current value. The most common - 'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is False. - minStep (float) When dec=True, this specifies the minimum allowable step size. - int (bool) if True, the value is forced to integer type. Default is False - decimals (int) Number of decimal values to display. Default is 2. ============== ======================================================================== + + All keyword arguments are passed to :func:`setOpts`. """ QtGui.QAbstractSpinBox.__init__(self, parent) self.lastValEmitted = None @@ -81,28 +62,15 @@ class SpinBox(QtGui.QAbstractSpinBox): self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.opts = { 'bounds': [None, None], - - ## Log scaling options #### Log mode is no longer supported. - #'step': 0.1, - #'minStep': 0.001, - #'log': True, - #'dec': False, - - ## decimal scaling option - example - #'step': 0.1, - #'minStep': .001, - #'log': False, - #'dec': True, ## normal arithmetic step 'step': D('0.01'), ## if 'dec' is false, the spinBox steps by 'step' every time ## if 'dec' is True, the step size is relative to the value ## 'step' needs to be an integral divisor of ten, ie 'step'*n=10 for some integer value of n (but only if dec is True) - 'log': False, + 'log': False, # deprecated 'dec': False, ## if true, does decimal stepping. ie from 1-10 it steps by 'step', from 10 to 100 it steps by 10*'step', etc. ## if true, minStep must be set in order to cross zero. - 'int': False, ## Set True to force value to be integer 'suffix': '', @@ -114,6 +82,8 @@ class SpinBox(QtGui.QAbstractSpinBox): 'decimals': 3, + 'format': asUnicode("{scaledValue:.{decimals}g}{suffixGap}{siPrefix}{suffix}"), + } self.decOpts = ['step', 'minStep'] @@ -134,12 +104,47 @@ class SpinBox(QtGui.QAbstractSpinBox): ret = True ## For some reason, spinbox pretends to ignore return key press return ret - ##lots of config options, just gonna stuff 'em all in here rather than do the get/set crap. def setOpts(self, **opts): - """ - Changes the behavior of the SpinBox. Accepts most of the arguments - allowed in :func:`__init__ `. + """Set options affecting the behavior of the SpinBox. + ============== ======================================================================== + **Arguments:** + bounds (min,max) Minimum and maximum values allowed in the SpinBox. + Either may be None to leave the value unbounded. By default, values are + unbounded. + suffix (str) suffix (units) to display after the numerical value. By default, + suffix is an empty str. + siPrefix (bool) If True, then an SI prefix is automatically prepended + to the units and the value is scaled accordingly. For example, + if value=0.003 and suffix='V', then the SpinBox will display + "300 mV" (but a call to SpinBox.value will still return 0.003). Default + is False. + step (float) The size of a single step. This is used when clicking the up/ + down arrows, when rolling the mouse wheel, or when pressing + keyboard arrows while the widget has keyboard focus. Note that + the interpretation of this value is different when specifying + the 'dec' argument. Default is 0.01. + dec (bool) If True, then the step value will be adjusted to match + the current size of the variable (for example, a value of 15 + might step in increments of 1 whereas a value of 1500 would + step in increments of 100). In this case, the 'step' argument + is interpreted *relative* to the current value. The most common + 'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is + False. + minStep (float) When dec=True, this specifies the minimum allowable step size. + int (bool) if True, the value is forced to integer type. Default is False + decimals (int) Number of decimal values to display. Default is 3. + format (str) Formatting string used to generate the text shown. Formatting is + done with ``str.format()`` and makes use of several arguments: + + * *value* - the unscaled value of the spin box + * *suffix* - the suffix string + * *scaledValue* - the scaled value to use when an SI prefix is present + * *siPrefix* - the SI prefix string (if any), or an empty string if + this feature has been disabled + * *suffixGap* - a single space if a suffix is present, or an empty + string otherwise. + ============== ======================================================================== """ #print opts for k in opts: @@ -154,6 +159,8 @@ class SpinBox(QtGui.QAbstractSpinBox): self.opts[k] = D(asUnicode(opts[k])) elif k == 'value': pass ## don't set value until bounds have been set + elif k == 'format': + self.opts[k] = asUnicode(opts[k]) elif k in self.opts: self.opts[k] = opts[k] else: @@ -378,37 +385,44 @@ class SpinBox(QtGui.QAbstractSpinBox): return True def updateText(self, prev=None): - # get the number of decimal places to print - decimals = self.opts.get('decimals') - # temporarily disable validation self.skipValidate = True - - # add a prefix to the units if requested - if self.opts['siPrefix']: - - # special case: if it's zero use the previous prefix - if self.val == 0 and prev is not None: - (s, p) = fn.siScale(prev) - - # NOTE: insert optional format string here? - txt = ("%."+str(decimals)+"g %s%s") % (0, p, self.opts['suffix']) - else: - # NOTE: insert optional format string here as an argument? - txt = fn.siFormat(float(self.val), precision=decimals, suffix=self.opts['suffix']) - - # otherwise, format the string manually - else: - # NOTE: insert optional format string here? - txt = ('%.'+str(decimals)+'g%s') % (self.val , self.opts['suffix']) - + + txt = self.formatText(prev=prev) + # actually set the text self.lineEdit().setText(txt) self.lastText = txt # re-enable the validation self.skipValidate = False - + + def formatText(self, prev=None): + # get the number of decimal places to print + decimals = self.opts['decimals'] if self.opts['int'] is False else 9 + suffix = self.opts['suffix'] + + # format the string + val = float(self.val) + if self.opts['siPrefix']: + # SI prefix was requested, so scale the value accordingly + + if self.val == 0 and prev is not None: + # special case: if it's zero use the previous prefix + (s, p) = fn.siScale(prev) + else: + (s, p) = fn.siScale(val) + parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val} + + else: + # no SI prefix requested; scale is 1 + parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': '', 'scaledValue': val} + + parts['suffixGap'] = '' if (parts['suffix'] == '' and parts['siPrefix'] == '') else ' ' + + format = self.opts['format'] + return format.format(**parts) + def validate(self, strn, pos): if self.skipValidate: ret = QtGui.QValidator.Acceptable From 5ddbb611d1c3e428c0dfda85e105d8e1b727542b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 30 Nov 2016 09:18:09 -0800 Subject: [PATCH 032/607] spinbox selects only numerical portion of text on focus-in --- pyqtgraph/widgets/SpinBox.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 50429dee..86cbba93 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -264,6 +264,10 @@ class SpinBox(QtGui.QAbstractSpinBox): return le.setSelection(0, index) + def focusInEvent(self, ev): + super(SpinBox, self).focusInEvent(ev) + self.selectNumber() + def value(self): """ Return the value of this SpinBox. From 6b798ffed856d8b2f7d53544ca4c480b2f7911d5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 6 Dec 2016 22:29:22 -0800 Subject: [PATCH 033/607] Fix multiple spinbox problems: - fixed bug with exponents disappearing after edit - fixed parsing of values with junk after suffix - fixed red border - reverted default decimals to 6 - make suffix editable (but show red border if it's wrong) - revert invalid text on focus lost - siPrefix without suffix is no longer allowed - let user set arbitrary format string --- pyqtgraph/functions.py | 56 +++++++++----- pyqtgraph/widgets/SpinBox.py | 143 +++++++++++++++++++++-------------- 2 files changed, 124 insertions(+), 75 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6ec3932f..faa11820 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -34,8 +34,13 @@ Colors = { SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY') SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' +SI_PREFIX_EXPONENTS = dict([(SI_PREFIXES[i], (i-8)*3) for i in range(len(SI_PREFIXES))]) +SI_PREFIX_EXPONENTS['u'] = -6 +FLOAT_REGEX = re.compile(r'(?P[+-]?((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)\s*((?P[u' + SI_PREFIXES + r']?)(?P\w.*))?$') +INT_REGEX = re.compile(r'(?P[+-]?\d+)\s*(?P[u' + SI_PREFIXES + r']?)(?P.*)$') + def siScale(x, minVal=1e-25, allowUnicode=True): """ Return the recommended scale factor and SI prefix string for x. @@ -103,31 +108,48 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al plusminus = " +/- " fmt = "%." + str(precision) + "g%s%s%s%s" return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) + + +def siParse(s, regex=FLOAT_REGEX): + """Convert a value written in SI notation to a tuple (number, si_prefix, suffix). -def siEval(s): + Example:: + + siParse('100 μV") # returns ('100', 'μ', 'V') """ - Convert a value written in SI notation to its equivalent prefixless value - + s = asUnicode(s) + m = regex.match(s) + if m is None: + raise ValueError('Cannot parse number "%s"' % s) + sip = m.group('siprefix') + suf = m.group('suffix') + return m.group('number'), '' if sip is None else sip, '' if suf is None else suf + + +def siEval(s, typ=float, regex=FLOAT_REGEX): + """ + Convert a value written in SI notation to its equivalent prefixless value. + Example:: siEval("100 μV") # returns 0.0001 """ + val, siprefix, suffix = siParse(s, regex) + v = typ(val) + return siApply(val, siprefix) + - s = asUnicode(s) - m = re.match(r'(-?((\d+(\.\d*)?)|(\.\d+))([eE]-?\d+)?)\s*([u' + SI_PREFIXES + r']?).*$', s) - if m is None: - raise Exception("Can't convert string '%s' to number." % s) - v = float(m.groups()[0]) - p = m.groups()[6] - #if p not in SI_PREFIXES: - #raise Exception("Can't convert string '%s' to number--unknown prefix." % s) - if p == '': - n = 0 - elif p == 'u': - n = -2 +def siApply(val, siprefix): + """ + """ + n = SI_PREFIX_EXPONENTS[siprefix] if siprefix != '' else 0 + if n > 0: + return val * 10**n + elif n < 0: + # this case makes it possible to use Decimal objects here + return val / 10**-n else: - n = SI_PREFIXES.index(p) - 8 - return v * 1000**n + return val class Color(QtGui.QColor): diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index 86cbba93..aafdb7d5 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -6,10 +6,13 @@ from ..SignalProxy import SignalProxy from .. import functions as fn from math import log from decimal import Decimal as D ## Use decimal to avoid accumulating floating-point errors -from decimal import * +import decimal import weakref + __all__ = ['SpinBox'] + + class SpinBox(QtGui.QAbstractSpinBox): """ **Bases:** QtGui.QAbstractSpinBox @@ -42,7 +45,7 @@ class SpinBox(QtGui.QAbstractSpinBox): valueChanged = QtCore.Signal(object) # (value) for compatibility with QSpinBox sigValueChanged = QtCore.Signal(object) # (self) sigValueChanging = QtCore.Signal(object, object) # (self, value) sent immediately; no delay. - + def __init__(self, parent=None, value=0.0, **kwargs): """ ============== ======================================================================== @@ -60,6 +63,8 @@ class SpinBox(QtGui.QAbstractSpinBox): self.setMinimumWidth(0) self.setMaximumHeight(20) self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) + self.errorBox = ErrorBox(self.lineEdit()) + self.opts = { 'bounds': [None, None], @@ -80,7 +85,7 @@ class SpinBox(QtGui.QAbstractSpinBox): 'delayUntilEditFinished': True, ## do not send signals until text editing has finished - 'decimals': 3, + 'decimals': 6, 'format': asUnicode("{scaledValue:.{decimals}g}{suffixGap}{siPrefix}{suffix}"), @@ -97,7 +102,7 @@ class SpinBox(QtGui.QAbstractSpinBox): self.editingFinished.connect(self.editingFinishedEvent) self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay']) - + def event(self, ev): ret = QtGui.QAbstractSpinBox.event(self, ev) if ev.type() == QtCore.QEvent.KeyPress and ev.key() == QtCore.Qt.Key_Return: @@ -133,7 +138,7 @@ class SpinBox(QtGui.QAbstractSpinBox): False. minStep (float) When dec=True, this specifies the minimum allowable step size. int (bool) if True, the value is forced to integer type. Default is False - decimals (int) Number of decimal values to display. Default is 3. + decimals (int) Number of decimal values to display. Default is 6. format (str) Formatting string used to generate the text shown. Formatting is done with ``str.format()`` and makes use of several arguments: @@ -301,7 +306,9 @@ class SpinBox(QtGui.QAbstractSpinBox): if self.opts['int']: value = int(value) - value = D(asUnicode(value)) + if not isinstance(value, D): + value = D(asUnicode(value)) + if value == self.val: return prev = self.val @@ -315,7 +322,6 @@ class SpinBox(QtGui.QAbstractSpinBox): self.emitChanged() return value - def emitChanged(self): self.lastValEmitted = self.val @@ -335,13 +341,9 @@ class SpinBox(QtGui.QAbstractSpinBox): def sizeHint(self): return QtCore.QSize(120, 0) - def stepEnabled(self): return self.StepUpEnabled | self.StepDownEnabled - #def fixup(self, *args): - #print "fixup:", args - def stepBy(self, n): n = D(int(n)) ## n must be integral number of steps. s = [D(-1), D(1)][n >= 0] ## determine sign of step @@ -363,7 +365,7 @@ class SpinBox(QtGui.QAbstractSpinBox): vs = [D(-1), D(1)][val >= 0] #exp = D(int(abs(val*(D('1.01')**(s*vs))).log10())) fudge = D('1.01')**(s*vs) ## fudge factor. at some places, the step size depends on the step sign. - exp = abs(val * fudge).log10().quantize(1, ROUND_FLOOR) + exp = abs(val * fudge).log10().quantize(1, decimal.ROUND_FLOOR) step = self.opts['step'] * D(10)**exp if 'minStep' in self.opts: step = max(step, self.opts['minStep']) @@ -375,7 +377,6 @@ class SpinBox(QtGui.QAbstractSpinBox): if 'minStep' in self.opts and abs(val) < self.opts['minStep']: val = D(0) self.setValue(val, delaySignal=True) ## note all steps (arrow buttons, wheel, up/down keys..) emit delayed signals only. - def valueInRange(self, value): bounds = self.opts['bounds'] @@ -403,12 +404,12 @@ class SpinBox(QtGui.QAbstractSpinBox): def formatText(self, prev=None): # get the number of decimal places to print - decimals = self.opts['decimals'] if self.opts['int'] is False else 9 + decimals = self.opts['decimals'] suffix = self.opts['suffix'] # format the string - val = float(self.val) - if self.opts['siPrefix']: + val = self.value() + if self.opts['siPrefix'] is True and len(self.opts['suffix']) > 0: # SI prefix was requested, so scale the value accordingly if self.val == 0 and prev is not None: @@ -419,38 +420,32 @@ class SpinBox(QtGui.QAbstractSpinBox): parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val} else: - # no SI prefix requested; scale is 1 + # no SI prefix /suffix requested; scale is 1 parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': '', 'scaledValue': val} parts['suffixGap'] = '' if (parts['suffix'] == '' and parts['siPrefix'] == '') else ' ' - format = self.opts['format'] - return format.format(**parts) + return self.opts['format'].format(**parts) def validate(self, strn, pos): if self.skipValidate: ret = QtGui.QValidator.Acceptable else: try: - ## first make sure we didn't mess with the suffix - suff = self.opts.get('suffix', '') - if len(suff) > 0 and asUnicode(strn)[-len(suff):] != suff: - ret = QtGui.QValidator.Invalid - - ## next see if we actually have an interpretable value + val = self.interpret() + if val is False: + ret = QtGui.QValidator.Intermediate else: - val = self.interpret() - if val is False: - ret = QtGui.QValidator.Intermediate + if self.valueInRange(val): + if not self.opts['delayUntilEditFinished']: + self.setValue(val, update=False) + ret = QtGui.QValidator.Acceptable else: - if self.valueInRange(val): - if not self.opts['delayUntilEditFinished']: - self.setValue(val, update=False) - ret = QtGui.QValidator.Acceptable - else: - ret = QtGui.QValidator.Intermediate + ret = QtGui.QValidator.Intermediate except: + import sys + sys.excepthook(*sys.exc_info()) ret = QtGui.QValidator.Intermediate ## draw / clear border @@ -462,40 +457,46 @@ class SpinBox(QtGui.QAbstractSpinBox): ## since the text will be forced to its previous state anyway self.update() + self.errorBox.setVisible(not self.textValid) + ## support 2 different pyqt APIs. Bleh. if hasattr(QtCore, 'QString'): return (ret, pos) else: return (ret, strn, pos) - def paintEvent(self, ev): - QtGui.QAbstractSpinBox.paintEvent(self, ev) - - ## draw red border if text is invalid - if not self.textValid: - p = QtGui.QPainter(self) - p.setRenderHint(p.Antialiasing) - p.setPen(fn.mkPen((200,50,50), width=2)) - p.drawRoundedRect(self.rect().adjusted(2, 2, -2, -2), 4, 4) - p.end() - + def fixup(self, strn): + # fixup is called when the spinbox loses focus with an invalid or intermediate string + self.updateText() + strn.clear() + strn.append(self.lineEdit().text()) def interpret(self): - """Return value of text. Return False if text is invalid, raise exception if text is intermediate""" + """Return value of text or False if text is invalid.""" strn = self.lineEdit().text() - suf = self.opts['suffix'] - if len(suf) > 0: - if strn[-len(suf):] != suf: - return False - #raise Exception("Units are invalid.") - strn = strn[:-len(suf)] + + # tokenize into numerical value, si prefix, and suffix try: - val = fn.siEval(strn) - except: - #sys.excepthook(*sys.exc_info()) - #print "invalid" + val, siprefix, suffix = fn.siParse(strn) + except Exception: return False - #print val + + # check suffix + if suffix != self.opts['suffix'] or (suffix == '' and siprefix != ''): + return False + + # generate value + val = D(val) + if self.opts['int']: + val = int(fn.siApply(val, siprefix)) + else: + try: + val = fn.siApply(val, siprefix) + except Exception: + import sys + sys.excepthook(*sys.exc_info()) + return False + return val def editingFinishedEvent(self): @@ -506,7 +507,7 @@ class SpinBox(QtGui.QAbstractSpinBox): return try: val = self.interpret() - except: + except Exception: return if val is False: @@ -516,3 +517,29 @@ class SpinBox(QtGui.QAbstractSpinBox): #print "no value change:", val, self.val return self.setValue(val, delaySignal=False) ## allow text update so that values are reformatted pretty-like + + +class ErrorBox(QtGui.QWidget): + """Red outline to draw around lineedit when value is invalid. + (for some reason, setting border from stylesheet does not work) + """ + def __init__(self, parent): + QtGui.QWidget.__init__(self, parent) + parent.installEventFilter(self) + self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) + self._resize() + self.setVisible(False) + + def eventFilter(self, obj, ev): + if ev.type() == QtCore.QEvent.Resize: + self._resize() + return False + + def _resize(self): + self.setGeometry(0, 0, self.parent().width(), self.parent().height()) + + def paintEvent(self, ev): + p = QtGui.QPainter(self) + p.setPen(fn.mkPen(color='r', width=2)) + p.drawRect(self.rect()) + p.end() From 65e9052580fe93130738549dc374621efdc126fa Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 6 Dec 2016 22:56:55 -0800 Subject: [PATCH 034/607] Fix parametertree sending bad options to spinbox --- pyqtgraph/parametertree/parameterTypes.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 31717481..3c41ffe6 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -279,9 +279,14 @@ class WidgetParameterItem(ParameterItem): ## If widget is a SpinBox, pass options straight through if isinstance(self.widget, SpinBox): + # send only options supported by spinbox + sbOpts = {} if 'units' in opts and 'suffix' not in opts: - opts['suffix'] = opts['units'] - self.widget.setOpts(**opts) + sbOpts['suffix'] = opts['units'] + for k,v in opts.items(): + if k in self.widget.opts: + sbOpts[k] = v + self.widget.setOpts(**sbOpts) self.updateDisplayLabel() From 982343627333b348d883099e34d4f782b7ab8df6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 7 Dec 2016 17:34:12 -0800 Subject: [PATCH 035/607] Add spinbox option to limit height based on font size --- pyqtgraph/widgets/SpinBox.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index aafdb7d5..df7acfcd 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -61,7 +61,8 @@ class SpinBox(QtGui.QAbstractSpinBox): self.lastText = '' self.textValid = True ## If false, we draw a red border self.setMinimumWidth(0) - self.setMaximumHeight(20) + self._lastFontHeight = None + self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred) self.errorBox = ErrorBox(self.lineEdit()) @@ -88,7 +89,8 @@ class SpinBox(QtGui.QAbstractSpinBox): 'decimals': 6, 'format': asUnicode("{scaledValue:.{decimals}g}{suffixGap}{siPrefix}{suffix}"), - + + 'compactHeight': True, # manually remove extra margin outside of text } self.decOpts = ['step', 'minStep'] @@ -99,6 +101,7 @@ class SpinBox(QtGui.QAbstractSpinBox): self.setCorrectionMode(self.CorrectToPreviousValue) self.setKeyboardTracking(False) self.setOpts(**kwargs) + self._updateHeight() self.editingFinished.connect(self.editingFinishedEvent) self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay']) @@ -149,6 +152,9 @@ class SpinBox(QtGui.QAbstractSpinBox): this feature has been disabled * *suffixGap* - a single space if a suffix is present, or an empty string otherwise. + compactHeight (bool) if True, then set the maximum height of the spinbox based on the + height of its font. This allows more compact packing on platforms with + excessive widget decoration. Default is True. ============== ======================================================================== """ #print opts @@ -518,6 +524,21 @@ class SpinBox(QtGui.QAbstractSpinBox): return self.setValue(val, delaySignal=False) ## allow text update so that values are reformatted pretty-like + def _updateHeight(self): + # SpinBox has very large margins on some platforms; this is a hack to remove those + # margins and allow more compact packing of controls. + if not self.opts['compactHeight']: + self.setMaximumHeight(1e6) + return + h = QtGui.QFontMetrics(self.font()).height() + if self._lastFontHeight != h: + self._lastFontHeight = h + self.setMaximumHeight(h) + + def paintEvent(self, ev): + self._updateHeight() + QtGui.QAbstractSpinBox.paintEvent(self, ev) + class ErrorBox(QtGui.QWidget): """Red outline to draw around lineedit when value is invalid. From f0e26d3add3b943a616b3e392023085214b41c19 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 7 Dec 2016 17:34:44 -0800 Subject: [PATCH 036/607] Limit lineedit height in parametertree to match spinbox style --- pyqtgraph/parametertree/parameterTypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 3c41ffe6..4c6a8486 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -122,6 +122,7 @@ class WidgetParameterItem(ParameterItem): self.hideWidget = False elif t == 'str': w = QtGui.QLineEdit() + w.setStyleSheet('border: 0px') w.sigChanged = w.editingFinished w.value = lambda: asUnicode(w.text()) w.setValue = lambda v: w.setText(asUnicode(v)) From e5a17edb4d329e2e0aecd34a93c7d71de3cb768e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 Dec 2016 10:12:45 -0800 Subject: [PATCH 037/607] Add spinbox 'regex' and 'evalFunc' options to complete user-formatting functionality --- examples/SpinBox.py | 9 +++- pyqtgraph/functions.py | 16 +++++-- pyqtgraph/widgets/SpinBox.py | 63 +++++++++++++++---------- pyqtgraph/widgets/tests/test_spinbox.py | 7 ++- 4 files changed, 63 insertions(+), 32 deletions(-) diff --git a/examples/SpinBox.py b/examples/SpinBox.py index 2fa9b161..84c82332 100644 --- a/examples/SpinBox.py +++ b/examples/SpinBox.py @@ -13,7 +13,7 @@ import initExample ## Add path to library (just for examples; you do not need th import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np - +import ast app = QtGui.QApplication([]) @@ -31,6 +31,13 @@ spins = [ pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), ("Float with SI-prefixed units,
dec step=1.0, minStep=0.001", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=1.0, minStep=0.001)), + ("Float with custom formatting", + pg.SpinBox(value=23.07, format='${value:0.02f}', + regex='\$?(?P(-?\d+(\.\d+)?)|(-?\.\d+))$')), + ("Int with custom formatting", + pg.SpinBox(value=4567, step=1, int=True, bounds=[0,None], format='0x{value:X}', + regex='(0x)?(?P[0-9a-fA-F]+)$', + evalFunc=lambda s: ast.literal_eval('0x'+s))), ] diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index faa11820..1fd05946 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -37,8 +37,8 @@ SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY' SI_PREFIX_EXPONENTS = dict([(SI_PREFIXES[i], (i-8)*3) for i in range(len(SI_PREFIXES))]) SI_PREFIX_EXPONENTS['u'] = -6 -FLOAT_REGEX = re.compile(r'(?P[+-]?((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)\s*((?P[u' + SI_PREFIXES + r']?)(?P\w.*))?$') -INT_REGEX = re.compile(r'(?P[+-]?\d+)\s*(?P[u' + SI_PREFIXES + r']?)(?P.*)$') +FLOAT_REGEX = re.compile(r'(?P[+-]?((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)\s*((?P[u' + SI_PREFIXES + r']?)(?P\w.*))?$') +INT_REGEX = re.compile(r'(?P[+-]?\d+)\s*(?P[u' + SI_PREFIXES + r']?)(?P.*)$') def siScale(x, minVal=1e-25, allowUnicode=True): @@ -121,8 +121,16 @@ def siParse(s, regex=FLOAT_REGEX): m = regex.match(s) if m is None: raise ValueError('Cannot parse number "%s"' % s) - sip = m.group('siprefix') - suf = m.group('suffix') + try: + sip = m.group('siPrefix') + except IndexError: + sip = '' + + try: + suf = m.group('suffix') + except IndexError: + suf = '' + return m.group('number'), '' if sip is None else sip, '' if suf is None else suf diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index df7acfcd..8e81f06d 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- -from ..Qt import QtGui, QtCore -from ..python2_3 import asUnicode -from ..SignalProxy import SignalProxy - -from .. import functions as fn from math import log from decimal import Decimal as D ## Use decimal to avoid accumulating floating-point errors import decimal import weakref +import re + +from ..Qt import QtGui, QtCore +from ..python2_3 import asUnicode, basestring +from ..SignalProxy import SignalProxy +from .. import functions as fn __all__ = ['SpinBox'] @@ -89,7 +90,9 @@ class SpinBox(QtGui.QAbstractSpinBox): 'decimals': 6, 'format': asUnicode("{scaledValue:.{decimals}g}{suffixGap}{siPrefix}{suffix}"), - + 'regex': fn.FLOAT_REGEX, + 'evalFunc': D, + 'compactHeight': True, # manually remove extra margin outside of text } @@ -152,28 +155,41 @@ class SpinBox(QtGui.QAbstractSpinBox): this feature has been disabled * *suffixGap* - a single space if a suffix is present, or an empty string otherwise. + regex (str or RegexObject) Regular expression used to parse the spinbox text. + May contain the following group names: + + * *number* - matches the numerical portion of the string (mandatory) + * *siPrefix* - matches the SI prefix string + * *suffix* - matches the suffix string + + Default is defined in ``pyqtgraph.functions.FLOAT_REGEX``. + evalFunc (callable) Fucntion that converts a numerical string to a number, + preferrably a Decimal instance. This function handles only the numerical + of the text; it does not have access to the suffix or SI prefix. compactHeight (bool) if True, then set the maximum height of the spinbox based on the height of its font. This allows more compact packing on platforms with excessive widget decoration. Default is True. ============== ======================================================================== """ #print opts - for k in opts: + for k,v in opts.items(): if k == 'bounds': - self.setMinimum(opts[k][0], update=False) - self.setMaximum(opts[k][1], update=False) + self.setMinimum(v[0], update=False) + self.setMaximum(v[1], update=False) elif k == 'min': - self.setMinimum(opts[k], update=False) + self.setMinimum(v, update=False) elif k == 'max': - self.setMaximum(opts[k], update=False) + self.setMaximum(v, update=False) elif k in ['step', 'minStep']: - self.opts[k] = D(asUnicode(opts[k])) + self.opts[k] = D(asUnicode(v)) elif k == 'value': pass ## don't set value until bounds have been set elif k == 'format': - self.opts[k] = asUnicode(opts[k]) + self.opts[k] = asUnicode(v) + elif k == 'regex' and isinstance(v, basestring): + self.opts[k] = re.compile(v) elif k in self.opts: - self.opts[k] = opts[k] + self.opts[k] = v else: raise TypeError("Invalid keyword argument '%s'." % k) if 'value' in opts: @@ -266,14 +282,11 @@ class SpinBox(QtGui.QAbstractSpinBox): """ le = self.lineEdit() text = asUnicode(le.text()) - if self.opts['suffix'] == '': - le.setSelection(0, len(text)) - else: - try: - index = text.index(' ') - except ValueError: - return - le.setSelection(0, index) + m = self.opts['regex'].match(text) + if m is None: + return + s,e = m.start('number'), m.end('number') + le.setSelection(s, e-s) def focusInEvent(self, ev): super(SpinBox, self).focusInEvent(ev) @@ -483,7 +496,7 @@ class SpinBox(QtGui.QAbstractSpinBox): # tokenize into numerical value, si prefix, and suffix try: - val, siprefix, suffix = fn.siParse(strn) + val, siprefix, suffix = fn.siParse(strn, self.opts['regex']) except Exception: return False @@ -492,7 +505,7 @@ class SpinBox(QtGui.QAbstractSpinBox): return False # generate value - val = D(val) + val = self.opts['evalFunc'](val) if self.opts['int']: val = int(fn.siApply(val, siprefix)) else: @@ -504,7 +517,7 @@ class SpinBox(QtGui.QAbstractSpinBox): return False return val - + def editingFinishedEvent(self): """Edit has finished; set value.""" #print "Edit finished." diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py index dcf15cb3..b9fbaeb2 100644 --- a/pyqtgraph/widgets/tests/test_spinbox.py +++ b/pyqtgraph/widgets/tests/test_spinbox.py @@ -1,6 +1,7 @@ import pyqtgraph as pg pg.mkQApp() + def test_spinbox(): sb = pg.SpinBox() assert sb.opts['decimals'] == 3 @@ -13,8 +14,10 @@ def test_spinbox(): (100, '100', dict()), (1000000, '1e+06', dict()), (1000, '1e+03', dict(decimals=2)), - (1000000, '1000000', dict(int=True)), - (12345678955, '12345678955', dict(int=True)), + (1000000, '1e+06', dict(int=True, decimals=6)), + (12345678955, '12345678955', dict(int=True, decimals=100)), + (1.45e-9, '1.45e-9 A', dict(int=False, decimals=6, suffix='A', siPrefix=False)), + (1.45e-9, '1.45 nA', dict(int=False, decimals=6, suffix='A', siPrefix=True)), ] for (value, text, opts) in conds: From cd7683b61db5171d71487832ab5886509a945ac5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 Dec 2016 10:14:18 -0800 Subject: [PATCH 038/607] Fix unit tests --- pyqtgraph/widgets/tests/test_spinbox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py index b9fbaeb2..b3934d78 100644 --- a/pyqtgraph/widgets/tests/test_spinbox.py +++ b/pyqtgraph/widgets/tests/test_spinbox.py @@ -4,7 +4,7 @@ pg.mkQApp() def test_spinbox(): sb = pg.SpinBox() - assert sb.opts['decimals'] == 3 + assert sb.opts['decimals'] == 6 assert sb.opts['int'] is False # table of test conditions: @@ -16,7 +16,7 @@ def test_spinbox(): (1000, '1e+03', dict(decimals=2)), (1000000, '1e+06', dict(int=True, decimals=6)), (12345678955, '12345678955', dict(int=True, decimals=100)), - (1.45e-9, '1.45e-9 A', dict(int=False, decimals=6, suffix='A', siPrefix=False)), + (1.45e-9, '1.45e-09 A', dict(int=False, decimals=6, suffix='A', siPrefix=False)), (1.45e-9, '1.45 nA', dict(int=False, decimals=6, suffix='A', siPrefix=True)), ] From de0ee32a2062e8bf342d26600a3492c7b6e8b29c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 9 Dec 2016 10:20:19 -0800 Subject: [PATCH 039/607] minor doc / test edits --- pyqtgraph/widgets/SpinBox.py | 12 +++++++----- pyqtgraph/widgets/tests/test_spinbox.py | 3 ++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index a71bf660..b8066cd7 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -18,12 +18,14 @@ class SpinBox(QtGui.QAbstractSpinBox): """ **Bases:** QtGui.QAbstractSpinBox - QSpinBox widget on steroids. Allows selection of numerical value, with extra features: + Extension of QSpinBox widget for selection of a numerical value. + Adds many extra features: - - SI prefix notation (eg, automatically display "300 mV" instead of "0.003 V") - - Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.) - - Option for unbounded values - - Delayed signals (allows multiple rapid changes with only one change signal) + * SI prefix notation (eg, automatically display "300 mV" instead of "0.003 V") + * Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.) + * Option for unbounded values + * Delayed signals (allows multiple rapid changes with only one change signal) + * Customizable text formatting ============================= ============================================== **Signals:** diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py index b3934d78..10087881 100644 --- a/pyqtgraph/widgets/tests/test_spinbox.py +++ b/pyqtgraph/widgets/tests/test_spinbox.py @@ -2,7 +2,7 @@ import pyqtgraph as pg pg.mkQApp() -def test_spinbox(): +def test_spinbox_formatting(): sb = pg.SpinBox() assert sb.opts['decimals'] == 6 assert sb.opts['int'] is False @@ -18,6 +18,7 @@ def test_spinbox(): (12345678955, '12345678955', dict(int=True, decimals=100)), (1.45e-9, '1.45e-09 A', dict(int=False, decimals=6, suffix='A', siPrefix=False)), (1.45e-9, '1.45 nA', dict(int=False, decimals=6, suffix='A', siPrefix=True)), + (-2500.3427, '$-2500.34', dict(int=False, format='${value:0.02f}')), ] for (value, text, opts) in conds: From 8fc98a6a0be80a69a5f7aeb5f9f77019ccae255a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 14 Dec 2016 10:07:16 -0800 Subject: [PATCH 040/607] Add print wrapper to work around interrupted system calls on travis --- examples/test_examples.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/examples/test_examples.py b/examples/test_examples.py index 3e6b8200..65e6f9bb 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -3,6 +3,30 @@ from pyqtgraph import Qt from . import utils import itertools import pytest +import os +import __builtin__ + + +# printing on travis ci frequently leads to "interrupted system call" errors. +# as a workaround, we overwrite the built-in print function (bleh) +if os.getenv('TRAVIS') is not None: + def flaky_print(*args): + """Wrapper for print that retries in case of IOError. + """ + count = 0 + while count < 5: + count += 1 + try: + orig_print(*args) + break + except IOError: + if count >= 5: + raise + pass + orig_print = __builtin__.print + __builtin__.print = flaky_print + print("Installed wrapper for flaky print.") + # apparently importlib does not exist in python 2.6... try: From 8d85b87d71f28c539ba971f07d92a39fc6ed70bc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 14 Dec 2016 10:14:11 -0800 Subject: [PATCH 041/607] py3 fix --- examples/test_examples.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 65e6f9bb..9b3f8eb7 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -3,13 +3,17 @@ from pyqtgraph import Qt from . import utils import itertools import pytest -import os -import __builtin__ +import os, sys # printing on travis ci frequently leads to "interrupted system call" errors. # as a workaround, we overwrite the built-in print function (bleh) if os.getenv('TRAVIS') is not None: + if sys.version_info[0] < 3: + import __builtin__ as builtins + else: + import builtins + def flaky_print(*args): """Wrapper for print that retries in case of IOError. """ @@ -24,7 +28,7 @@ if os.getenv('TRAVIS') is not None: raise pass orig_print = __builtin__.print - __builtin__.print = flaky_print + builtins.print = flaky_print print("Installed wrapper for flaky print.") From 24b288a05aadd33305b1c86b2b829de03f9937ab Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 14 Dec 2016 10:19:01 -0800 Subject: [PATCH 042/607] really actually fix --- examples/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 9b3f8eb7..ae88b087 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -27,7 +27,7 @@ if os.getenv('TRAVIS') is not None: if count >= 5: raise pass - orig_print = __builtin__.print + orig_print = builtins.print builtins.print = flaky_print print("Installed wrapper for flaky print.") From 4e7773fa0bb90e733976093259a7a69eb5631edb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 14 Dec 2016 11:02:40 -0800 Subject: [PATCH 043/607] Add scipy to travis requirements--some unit tests require this --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2c7b7769..c4a67ac3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -51,7 +51,7 @@ install: - conda update conda --yes - conda create -n test_env python=${PYTHON} --yes - source activate test_env - - conda install numpy pyopengl pytest flake8 six coverage --yes + - conda install numpy scipy pyopengl pytest flake8 six coverage --yes - echo ${QT} - echo ${TEST} - echo ${PYTHON} From b420099bd57644643787553ad25c07b27574980e Mon Sep 17 00:00:00 2001 From: Colin Baumgarten Date: Wed, 4 Jan 2017 21:48:00 +0100 Subject: [PATCH 044/607] Fix crash when running pyqtgraph with python -OO Running pyqtgraph with python -OO gives the following crash colin@desktop:~$ python3 -OO -c 'import pyqtgraph' Traceback (most recent call last): File "", line 1, in File "/usr/local/lib/python3.5/dist-packages/pyqtgraph/__init__.py", line 216, in from .graphicsItems.HistogramLUTItem import * File "/usr/local/lib/python3.5/dist-packages/pyqtgraph/graphicsItems/HistogramLUTItem.py", line 10, in from .GradientEditorItem import * File "/usr/local/lib/python3.5/dist-packages/pyqtgraph/graphicsItems/GradientEditorItem.py", line 354, in class GradientEditorItem(TickSliderItem): File "/usr/local/lib/python3.5/dist-packages/pyqtgraph/graphicsItems/GradientEditorItem.py", line 480, in GradientEditorItem @addGradientListToDocstring() File "/usr/local/lib/python3.5/dist-packages/pyqtgraph/graphicsItems/GradientEditorItem.py", line 30, in dec fn.__doc__ = fn.__doc__ + str(Gradients.keys()).strip('[').strip(']') TypeError: unsupported operand type(s) for +: 'NoneType' and 'str' The cause is the @addGradientListToDocstring() annotation in GradientEditorItem.py that cannot handle functions without docstrings as produced when using the python -OO option. Fix this by only adding the gradient list to the docstring if the docstring is not None. --- pyqtgraph/graphicsItems/GradientEditorItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 6ce06b61..f359ff11 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -27,7 +27,8 @@ Gradients = OrderedDict([ def addGradientListToDocstring(): """Decorator to add list of current pre-defined gradients to the end of a function docstring.""" def dec(fn): - fn.__doc__ = fn.__doc__ + str(Gradients.keys()).strip('[').strip(']') + if fn.__doc__ is not None: + fn.__doc__ = fn.__doc__ + str(Gradients.keys()).strip('[').strip(']') return fn return dec From b131785b869ca3912d11f3df7ec7717832befadf Mon Sep 17 00:00:00 2001 From: james1293 Date: Fri, 6 Jan 2017 23:21:31 -0500 Subject: [PATCH 045/607] Removed unnecessary 'curve1' from 'global' --- examples/scrollingPlots.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/scrollingPlots.py b/examples/scrollingPlots.py index 623b9ab1..313d4e8d 100644 --- a/examples/scrollingPlots.py +++ b/examples/scrollingPlots.py @@ -21,7 +21,7 @@ curve1 = p1.plot(data1) curve2 = p2.plot(data1) ptr1 = 0 def update1(): - global data1, curve1, ptr1 + global data1, ptr1 data1[:-1] = data1[1:] # shift data in the array one sample left # (see also: np.roll) data1[-1] = np.random.normal() From 12f6bf916fbe4357305522cc361b522e2482e9f5 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Tue, 17 Jan 2017 22:04:05 +0800 Subject: [PATCH 046/607] fix fft premature slicing away of 0 freq bin also fixes: - use rfft for better efficiency - use rfftfreq to compute coords correctly - works for both odd/even lengths - python3: integer division needed for numpy indexing --- pyqtgraph/graphicsItems/PlotDataItem.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 37245bec..11184ae6 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -679,10 +679,9 @@ class PlotDataItem(GraphicsObject): x2 = np.linspace(x[0], x[-1], len(x)) y = np.interp(x2, x, y) x = x2 - f = np.fft.fft(y) / len(y) - y = abs(f[1:len(f)/2]) - dt = x[-1] - x[0] - x = np.linspace(0, 0.5*len(x)/dt, len(y)) + f = np.fft.rfft(y) / y.size + x = np.fft.rfftfreq(y.size) + y = np.abs(f) return x, y def dataType(obj): From 0a8d5b253aa70f9f03259604b53d51bf77791db8 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Wed, 18 Jan 2017 09:02:53 +0800 Subject: [PATCH 047/607] fix: freq coords need to take into account x-coords spacing --- pyqtgraph/graphicsItems/PlotDataItem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 11184ae6..485576f6 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -679,8 +679,10 @@ class PlotDataItem(GraphicsObject): x2 = np.linspace(x[0], x[-1], len(x)) y = np.interp(x2, x, y) x = x2 - f = np.fft.rfft(y) / y.size - x = np.fft.rfftfreq(y.size) + n = y.size + f = np.fft.rfft(y) / n + d = (x[-1] - x[0]) / (n - 1) + x = np.fft.rfftfreq(n, d) y = np.abs(f) return x, y From 4553b55f736ba142619d9969bbd55bcda6db9824 Mon Sep 17 00:00:00 2001 From: KIU Shueng Chuan Date: Fri, 20 Jan 2017 09:09:18 +0800 Subject: [PATCH 048/607] python2 compat: don't assume true division --- pyqtgraph/graphicsItems/PlotDataItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 485576f6..a26c1c72 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -681,7 +681,7 @@ class PlotDataItem(GraphicsObject): x = x2 n = y.size f = np.fft.rfft(y) / n - d = (x[-1] - x[0]) / (n - 1) + d = float(x[-1]-x[0]) / (len(x)-1) x = np.fft.rfftfreq(n, d) y = np.abs(f) return x, y From f632b02985b87e1fb8e78e14c82e5c5597c2dee5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 5 Feb 2017 23:13:00 -0800 Subject: [PATCH 049/607] Add unit test covering plotdataitem fft --- pyqtgraph/graphicsItems/PlotDataItem.py | 27 ------------------- .../graphicsItems/tests/test_PlotDataItem.py | 25 +++++++++++++++++ 2 files changed, 25 insertions(+), 27 deletions(-) create mode 100644 pyqtgraph/graphicsItems/tests/test_PlotDataItem.py diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index a26c1c72..d7ea5100 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -500,27 +500,10 @@ class PlotDataItem(GraphicsObject): if self.xData is None: return (None, None) - #if self.xClean is None: - #nanMask = np.isnan(self.xData) | np.isnan(self.yData) | np.isinf(self.xData) | np.isinf(self.yData) - #if nanMask.any(): - #self.dataMask = ~nanMask - #self.xClean = self.xData[self.dataMask] - #self.yClean = self.yData[self.dataMask] - #else: - #self.dataMask = None - #self.xClean = self.xData - #self.yClean = self.yData - if self.xDisp is None: x = self.xData y = self.yData - - #ds = self.opts['downsample'] - #if isinstance(ds, int) and ds > 1: - #x = x[::ds] - ##y = resample(y[:len(x)*ds], len(x)) ## scipy.signal.resample causes nasty ringing - #y = y[::ds] if self.opts['fftMode']: x,y = self._fourierTransform(x, y) # Ignore the first bin for fft data if we have a logx scale @@ -531,14 +514,6 @@ class PlotDataItem(GraphicsObject): x = np.log10(x) if self.opts['logMode'][1]: y = np.log10(y) - #if any(self.opts['logMode']): ## re-check for NANs after log - #nanMask = np.isinf(x) | np.isinf(y) | np.isnan(x) | np.isnan(y) - #if any(nanMask): - #self.dataMask = ~nanMask - #x = x[self.dataMask] - #y = y[self.dataMask] - #else: - #self.dataMask = None ds = self.opts['downsample'] if not isinstance(ds, int): @@ -591,8 +566,6 @@ class PlotDataItem(GraphicsObject): self.xDisp = x self.yDisp = y - #print self.yDisp.shape, self.yDisp.min(), self.yDisp.max() - #print self.xDisp.shape, self.xDisp.min(), self.xDisp.max() return self.xDisp, self.yDisp def dataBounds(self, ax, frac=1.0, orthoRange=None): diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py new file mode 100644 index 00000000..dc13bb7a --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -0,0 +1,25 @@ +import numpy as np +import pyqtgraph as pg + +pg.mkQApp() + + +def test_fft(): + f = 20. + x = np.linspace(0, 1, 1000) + y = np.sin(2 * np.pi * f * x) + pd = pg.PlotDataItem(x, y) + pd.setFftMode(True) + x, y = pd.getData() + assert abs(x[np.argmax(y)] - f) < 0.03 + + x = np.linspace(0, 1, 1001) + y = np.sin(2 * np.pi * f * x) + pd.setData(x, y) + x, y = pd.getData() + assert abs(x[np.argmax(y)]- f) < 0.03 + + pd.setLogMode(True, False) + x, y = pd.getData() + assert abs(x[np.argmax(y)] - np.log10(f)) < 0.01 + \ No newline at end of file From b9aea3daf145009f0fb6e03103270f3da20b3fb5 Mon Sep 17 00:00:00 2001 From: Pieter Date: Thu, 16 Feb 2017 12:40:21 +0100 Subject: [PATCH 050/607] add warnings for remote exceptions --- pyqtgraph/multiprocess/remoteproxy.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 208e17f4..6d738f0a 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -1,6 +1,7 @@ import os, time, sys, traceback, weakref import numpy as np import threading +import warnings try: import __builtin__ as builtins import cPickle as pickle @@ -21,6 +22,9 @@ class NoResultError(Exception): because the call has not yet returned.""" pass +class RemoteExceptionWarning(UserWarning): + """Emitted when a request to a remote object results in an Exception """ + pass class RemoteEventHandler(object): """ @@ -502,9 +506,9 @@ class RemoteEventHandler(object): #print ''.join(result) exc, excStr = result if exc is not None: - print("===== Remote process raised exception on request: =====") - print(''.join(excStr)) - print("===== Local Traceback to request follows: =====") + warnings.warn("===== Remote process raised exception on request: =====", RemoteExceptionWarning) + warnings.warn(''.join(excStr), RemoteExceptionWarning) + warnings.warn("===== Local Traceback to request follows: =====", RemoteExceptionWarning) raise exc else: print(''.join(excStr)) From e3827f17f1664068e2cb001b33dfd522a083c14a Mon Sep 17 00:00:00 2001 From: WFrsh Date: Fri, 17 Feb 2017 21:48:15 +0100 Subject: [PATCH 051/607] Included matplotlib 2.0 colormaps Included the four Perceptually Uniform Sequential colormaps from the matplotlib 2.0 release. viridis, inferno, plasma, magma --- pyqtgraph/graphicsItems/GradientEditorItem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index f359ff11..9b0b0f19 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -22,6 +22,10 @@ Gradients = OrderedDict([ ('cyclic', {'ticks': [(0.0, (255, 0, 4, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'hsv'}), ('greyclip', {'ticks': [(0.0, (0, 0, 0, 255)), (0.99, (255, 255, 255, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'rgb'}), ('grey', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'}), + ('viridis', {'ticks': [(0.0, (68, 1, 84, 255)), (0.25, (58, 82, 139, 255)), (0.5, (32, 144, 140, 255)), (0.75, (94, 201, 97, 255)), (1.0, (253, 231, 36, 255))], 'mode': 'rgb'}), + ('inferno', {'ticks': [(0.0, (0, 0, 3, 255)), (0.25, (87, 15, 109, 255)), (0.5, (187, 55, 84, 255)), (0.75, (249, 142, 8, 255)), (1.0, (252, 254, 164, 255))], 'mode': 'rgb'}), + ('plasma', {'ticks': [(0.0, (12, 7, 134, 255)), (0.25, (126, 3, 167, 255)), (0.5, (203, 71, 119, 255)), (0.75, (248, 149, 64, 255)), (1.0, (239, 248, 33, 255))], 'mode': 'rgb'}), + ('magma', {'ticks': [(0.0, (0, 0, 3, 255)), (0.25, (80, 18, 123, 255)), (0.5, (182, 54, 121, 255)), (0.75, (251, 136, 97, 255)), (1.0, (251, 252, 191, 255))], 'mode': 'rgb'}), ]) def addGradientListToDocstring(): From d4bff30f11679af3b9538daaa4e9f523a1fb0dba Mon Sep 17 00:00:00 2001 From: ZacDiggum Date: Tue, 7 Mar 2017 10:34:48 +0100 Subject: [PATCH 052/607] fix image export bug fixes #454 --- pyqtgraph/exporters/ImageExporter.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index 78d93106..4dc07d84 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -23,8 +23,8 @@ class ImageExporter(Exporter): bg.setAlpha(0) self.params = Parameter(name='params', type='group', children=[ - {'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)}, - {'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)}, + {'name': 'width', 'type': 'int', 'value': int(tr.width()), 'limits': (0, None)}, + {'name': 'height', 'type': 'int', 'value': int(tr.height()), 'limits': (0, None)}, {'name': 'antialias', 'type': 'bool', 'value': True}, {'name': 'background', 'type': 'color', 'value': bg}, ]) @@ -34,12 +34,12 @@ class ImageExporter(Exporter): def widthChanged(self): sr = self.getSourceRect() ar = float(sr.height()) / sr.width() - self.params.param('height').setValue(self.params['width'] * ar, blockSignal=self.heightChanged) + self.params.param('height').setValue(int(self.params['width'] * ar), blockSignal=self.heightChanged) def heightChanged(self): sr = self.getSourceRect() ar = float(sr.width()) / sr.height() - self.params.param('width').setValue(self.params['height'] * ar, blockSignal=self.widthChanged) + self.params.param('width').setValue(int(self.params['height'] * ar), blockSignal=self.widthChanged) def parameters(self): return self.params From c247aa3989cc5b375dcf27c7eb0931770a85d134 Mon Sep 17 00:00:00 2001 From: Justin Engel Date: Tue, 11 Apr 2017 10:32:43 -0400 Subject: [PATCH 053/607] Fixed PySide image memory leak PySide has a known memory leak issue when using QImage. It does not handle the reference counter correctly. I manually adjusted the reference counter to the data as suggested in a bug report by Neil Whelchel. This bug report can be found at https://bugreports.qt.io/browse/PYSIDE-140 --- pyqtgraph/functions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 839720d1..73dec8b6 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1188,7 +1188,9 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): if USE_PYSIDE: ch = ctypes.c_char.from_buffer(imgData, 0) + rcount = ctypes.c_long.from_address(id(ch)).value # Get the reference count of self.data. img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) + ctypes.c_long.from_address(id(ch)).value = rcount # This puts the refcount back where it belongs. else: #addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) ## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was) From 9a05b74f250647bb4020c2866532210ff9174104 Mon Sep 17 00:00:00 2001 From: Lorenz Drescher Date: Fri, 21 Apr 2017 17:41:22 +0200 Subject: [PATCH 054/607] Correct wrong function call in LayoutWidget.addLabel and LayoutWidget.addLayout Previously LayoutWidget.addLabel and LayoutWidget.addLayout called a function "addItem", that didn't exist. Corrected to call LayoutWidget.addWidget. This fixes #242 --- pyqtgraph/widgets/LayoutWidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/LayoutWidget.py b/pyqtgraph/widgets/LayoutWidget.py index 65d04d3f..7181778d 100644 --- a/pyqtgraph/widgets/LayoutWidget.py +++ b/pyqtgraph/widgets/LayoutWidget.py @@ -39,7 +39,7 @@ class LayoutWidget(QtGui.QWidget): Returns the created widget. """ text = QtGui.QLabel(text, **kargs) - self.addItem(text, row, col, rowspan, colspan) + self.addWidget(text, row, col, rowspan, colspan) return text def addLayout(self, row=None, col=None, rowspan=1, colspan=1, **kargs): @@ -49,7 +49,7 @@ class LayoutWidget(QtGui.QWidget): Returns the created widget. """ layout = LayoutWidget(**kargs) - self.addItem(layout, row, col, rowspan, colspan) + self.addWidget(layout, row, col, rowspan, colspan) return layout def addWidget(self, item, row=None, col=None, rowspan=1, colspan=1): From d36a161f31813bf68f1a41d224aa8d86dce8e7d1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 May 2017 09:34:37 -0700 Subject: [PATCH 055/607] Fixed unicode error when exporting to SVG with non-ascii symbols --- pyqtgraph/exporters/SVGExporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index ccf92165..fdc65080 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -190,7 +190,7 @@ def _generateItemSvg(item, nodes=None, root=None): xmlStr = str(arr) else: xmlStr = bytes(arr).decode('utf-8') - doc = xml.parseString(xmlStr) + doc = xml.parseString(xmlStr.encode('utf-8')) try: ## Get top-level group for this item From 24116342c28c8528f1ec71da595668cb10f0b1f9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 May 2017 09:43:16 -0700 Subject: [PATCH 056/607] Add option to set composition mode for scatterplotitem --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 54667b50..9a23d965 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -251,6 +251,7 @@ class ScatterPlotItem(GraphicsObject): 'pxMode': True, 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. 'antialias': getConfigOption('antialias'), + 'compositionMode': None, 'name': None, } @@ -730,7 +731,9 @@ class ScatterPlotItem(GraphicsObject): @debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): - + cmode = self.opts.get('compositionMode', None) + if cmode is not None: + p.setCompositionMode(cmode) #p.setPen(fn.mkPen('r')) #p.drawRect(self.boundingRect()) From 3f34ea12d8fc0930efe975fb1dcb1e1bd9478665 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 May 2017 09:45:09 -0700 Subject: [PATCH 057/607] Fix value stepping in intColor --- pyqtgraph/functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d79c350f..5cac27c5 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -313,7 +313,7 @@ def colorStr(c): return ('%02x'*4) % colorTuple(c) -def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255, **kargs): +def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255, alpha=255): """ Creates a QColor from a single index. Useful for stepping through a predefined list of colors. @@ -325,7 +325,7 @@ def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, mi values = int(values) ind = int(index) % (hues * values) indh = ind % hues - indv = ind / hues + indv = ind // hues if values > 1: v = minValue + indv * ((maxValue-minValue) / (values-1)) else: From e0e087716828a63a2c0c4fff2a04e7e2ba860060 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 May 2017 10:22:27 -0700 Subject: [PATCH 058/607] Add documentation --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 9a23d965..597491f3 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -300,6 +300,8 @@ class ScatterPlotItem(GraphicsObject): *antialias* Whether to draw symbols with antialiasing. Note that if pxMode is True, symbols are always rendered with antialiasing (since the rendered symbols can be cached, this incurs very little performance cost) + *compositionMode* If specified, this sets the composition mode used when drawing the + scatter plot (see QPainter::CompositionMode in the Qt documentation). *name* The name of this item. Names are used for automatically generating LegendItem entries and by some exporters. ====================== =============================================================================================== From f6819dda28e9f725215efb5f198257a43b827d38 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 May 2017 11:09:50 -0700 Subject: [PATCH 059/607] Add comments explaining hack --- pyqtgraph/functions.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 73dec8b6..8b2e4684 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1188,9 +1188,20 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): if USE_PYSIDE: ch = ctypes.c_char.from_buffer(imgData, 0) - rcount = ctypes.c_long.from_address(id(ch)).value # Get the reference count of self.data. + + # Bug in PySide + Python 3 causes refcount for image data to be improperly + # incremented, which leads to leaked memory. As a workaround, we manually + # reset the reference count after creating the QImage. + # See: https://bugreports.qt.io/browse/PYSIDE-140 + + # Get initial reference count (PyObject struct has ob_refcnt as first element) + rcount = ctypes.c_long.from_address(id(ch)).value img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) - ctypes.c_long.from_address(id(ch)).value = rcount # This puts the refcount back where it belongs. + if sys.version[0] == '3': + # Reset refcount only on python 3. Technically this would have no effect + # on python 2, but this is a nasty hack, and checking for version here + # helps to mitigate possible unforseen consequences. + ctypes.c_long.from_address(id(ch)).value = rcount else: #addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) ## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was) From 7761b9a23cfe2dcf2e1263ca86e65ff112940eae Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 May 2017 17:09:15 -0700 Subject: [PATCH 060/607] Add basic type checking to parameters --- pyqtgraph/parametertree/Parameter.py | 4 +++ pyqtgraph/parametertree/parameterTypes.py | 36 ++++++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index de9a1624..4ca80ffe 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -255,6 +255,7 @@ class Parameter(QtCore.QObject): try: if blockSignal is not None: self.sigValueChanged.disconnect(blockSignal) + value = self._interpretValue(value) if self.opts['value'] == value: return value self.opts['value'] = value @@ -265,6 +266,9 @@ class Parameter(QtCore.QObject): return value + def _interpretValue(self, v): + return v + def value(self): """ Return the value of this Parameter. diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 2535b13a..e3ed8853 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -4,10 +4,11 @@ from .Parameter import Parameter, registerParameterType from .ParameterItem import ParameterItem from ..widgets.SpinBox import SpinBox from ..widgets.ColorButton import ColorButton +from ..colormap import ColorMap #from ..widgets.GradientWidget import GradientWidget ## creates import loop from .. import pixmaps as pixmaps from .. import functions as fn -import os +import os, sys from ..pgcollections import OrderedDict class WidgetParameterItem(ParameterItem): @@ -320,6 +321,39 @@ class SimpleParameter(Parameter): state['value'] = fn.colorTuple(self.value()) return state + def _interpretValue(self, v): + fn = { + 'int': int, + 'float': float, + 'bool': bool, + 'str': self._interpStr, + 'color': self._interpColor, + 'colormap': self._interpColormap, + }[self.opts['type']] + return fn(v) + + def _interpStr(self, v): + if sys.version[0] == '2': + if isinstance(v, QtCore.QString): + v = unicode(v) + elif not isinstance(v, basestring): + raise TypeError("Cannot set str parmeter from object %r" % v) + else: + if isinstance(v, QtCore.QString): + v = str(v) + elif not isinstance(v, str): + raise TypeError("Cannot set str parmeter from object %r" % v) + return v + + def _interpColor(self, v): + return fn.mkColor(v) + + def _interpColormap(self, v): + if not isinstance(v, ColorMap): + raise TypeError("Cannot set colormap parameter from object %r" % v) + return v + + registerParameterType('int', SimpleParameter, override=True) registerParameterType('float', SimpleParameter, override=True) From b186ecbbd9ab1cf4e303a5863a52601c9cee1b9a Mon Sep 17 00:00:00 2001 From: Chadwick Boulay Date: Wed, 17 May 2017 09:23:41 -0400 Subject: [PATCH 061/607] Fixed incorrect height in VTickGroup. --- pyqtgraph/graphicsItems/VTickGroup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/VTickGroup.py b/pyqtgraph/graphicsItems/VTickGroup.py index 1db4a4a2..2b4f256f 100644 --- a/pyqtgraph/graphicsItems/VTickGroup.py +++ b/pyqtgraph/graphicsItems/VTickGroup.py @@ -90,7 +90,7 @@ class VTickGroup(UIGraphicsItem): br = self.boundingRect() h = br.height() br.setY(br.y() + self.yrange[0] * h) - br.setHeight(h - (1.0-self.yrange[1]) * h) + br.setHeight((self.yrange[1] - self.yrange[0]) * h) p.translate(0, br.y()) p.scale(1.0, br.height()) p.setPen(self.pen) From 8a40c228486c31636db403912c55a600d07eb213 Mon Sep 17 00:00:00 2001 From: kiwi0fruit Date: Thu, 22 Jun 2017 16:00:54 +0700 Subject: [PATCH 062/607] Bug in RawImageWidget.py For example: it prevents integration of this widget to Enaml. --- pyqtgraph/widgets/RawImageWidget.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index 657701f9..ae6448c6 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -21,7 +21,7 @@ class RawImageWidget(QtGui.QWidget): """ Setting scaled=True will cause the entire image to be displayed within the boundaries of the widget. This also greatly reduces the speed at which it will draw frames. """ - QtGui.QWidget.__init__(self, parent=None) + QtGui.QWidget.__init__(self, parent) self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)) self.scaled = scaled self.opts = None @@ -69,7 +69,7 @@ if HAVE_OPENGL: Perfomance varies between platforms; see examples/VideoSpeedTest for benchmarking. """ def __init__(self, parent=None, scaled=False): - QtOpenGL.QGLWidget.__init__(self, parent=None) + QtOpenGL.QGLWidget.__init__(self, parent) self.scaled = scaled self.image = None self.uploaded = False From 54ddb79e89d3ed27639f866bd534ecc9d671b563 Mon Sep 17 00:00:00 2001 From: kiwi0fruit Date: Tue, 27 Jun 2017 20:53:08 +0700 Subject: [PATCH 063/607] Bug-fix and small changes in RawImageWidget.py 1. Bug was in the `def paintGL(self)` method (at least with PySide1): image was mirrored upside down. 2. Added support for `setConfigOptions(imageAxisOrder='row-major')` 3. Small cosmetic changes --- pyqtgraph/widgets/RawImageWidget.py | 87 +++++++++++++++++------------ 1 file changed, 52 insertions(+), 35 deletions(-) diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index ae6448c6..a51bfb1d 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -1,32 +1,43 @@ +# -*- coding: utf-8 -*- +""" +RawImageWidget.py +Copyright 2010-2016 Luke Campagnola +Distributed under MIT/X11 license. See license.txt for more infomation. +""" + from ..Qt import QtCore, QtGui + try: from ..Qt import QtOpenGL from OpenGL.GL import * + HAVE_OPENGL = True -except Exception: +except (ImportError, AttributeError): # Would prefer `except ImportError` here, but some versions of pyopengl generate # AttributeError upon import HAVE_OPENGL = False -from .. import functions as fn -import numpy as np +from .. import getConfigOption, functions as fn + class RawImageWidget(QtGui.QWidget): """ - Widget optimized for very fast video display. + Widget optimized for very fast video display. Generally using an ImageItem inside GraphicsView is fast enough. On some systems this may provide faster video. See the VideoSpeedTest example for benchmarking. """ + def __init__(self, parent=None, scaled=False): """ - Setting scaled=True will cause the entire image to be displayed within the boundaries of the widget. This also greatly reduces the speed at which it will draw frames. + Setting scaled=True will cause the entire image to be displayed within the boundaries of the widget. + This also greatly reduces the speed at which it will draw frames. """ QtGui.QWidget.__init__(self, parent) - self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding)) + self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) self.scaled = scaled self.opts = None self.image = None - + def setImage(self, img, *args, **kargs): """ img must be ndarray of shape (x,y), (x,y,3), or (x,y,4). @@ -43,22 +54,22 @@ class RawImageWidget(QtGui.QWidget): argb, alpha = fn.makeARGB(self.opts[0], *self.opts[1], **self.opts[2]) self.image = fn.makeQImage(argb, alpha) self.opts = () - #if self.pixmap is None: - #self.pixmap = QtGui.QPixmap.fromImage(self.image) + # if self.pixmap is None: + # self.pixmap = QtGui.QPixmap.fromImage(self.image) p = QtGui.QPainter(self) if self.scaled: rect = self.rect() ar = rect.width() / float(rect.height()) imar = self.image.width() / float(self.image.height()) if ar > imar: - rect.setWidth(int(rect.width() * imar/ar)) + rect.setWidth(int(rect.width() * imar / ar)) else: - rect.setHeight(int(rect.height() * ar/imar)) - + rect.setHeight(int(rect.height() * ar / imar)) + p.drawImage(rect, self.image) else: p.drawImage(QtCore.QPointF(), self.image) - #p.drawPixmap(self.rect(), self.pixmap) + # p.drawPixmap(self.rect(), self.pixmap) p.end() @@ -67,7 +78,10 @@ if HAVE_OPENGL: """ Similar to RawImageWidget, but uses a GL widget to do all drawing. Perfomance varies between platforms; see examples/VideoSpeedTest for benchmarking. + + Checks if setConfigOptions(imageAxisOrder='row-major') was set. """ + def __init__(self, parent=None, scaled=False): QtOpenGL.QGLWidget.__init__(self, parent) self.scaled = scaled @@ -75,6 +89,7 @@ if HAVE_OPENGL: self.uploaded = False self.smooth = False self.opts = None + self.row_major = getConfigOption('imageAxisOrder') == 'row-major' def setImage(self, img, *args, **kargs): """ @@ -88,7 +103,7 @@ if HAVE_OPENGL: def initializeGL(self): self.texture = glGenTextures(1) - + def uploadTexture(self): glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) @@ -100,17 +115,22 @@ if HAVE_OPENGL: glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER) glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER) - #glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) - shape = self.image.shape - - ### Test texture dimensions first - #glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, None) - #if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0: - #raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2]) - - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, self.image.transpose((1,0,2))) + # glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_BORDER) + + if self.row_major: + image = self.image + else: + image = self.image.transpose((1, 0, 2)) + + # ## Test texture dimensions first + # shape = self.image.shape + # glTexImage2D(GL_PROXY_TEXTURE_2D, 0, GL_RGBA, shape[0], shape[1], 0, GL_RGBA, GL_UNSIGNED_BYTE, None) + # if glGetTexLevelParameteriv(GL_PROXY_TEXTURE_2D, 0, GL_TEXTURE_WIDTH) == 0: + # raise Exception("OpenGL failed to create 2D texture (%dx%d); too large for this hardware." % shape[:2]) + + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.shape[1], image.shape[0], 0, GL_RGBA, GL_UNSIGNED_BYTE, image) glDisable(GL_TEXTURE_2D) - + def paintGL(self): if self.image is None: if self.opts is None: @@ -118,26 +138,23 @@ if HAVE_OPENGL: img, args, kwds = self.opts kwds['useRGBA'] = True self.image, alpha = fn.makeARGB(img, *args, **kwds) - + if not self.uploaded: self.uploadTexture() - + glViewport(0, 0, self.width(), self.height()) glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) - glColor4f(1,1,1,1) + glColor4f(1, 1, 1, 1) glBegin(GL_QUADS) - glTexCoord2f(0,0) - glVertex3f(-1,-1,0) - glTexCoord2f(1,0) + glTexCoord2f(0, 1) + glVertex3f(-1, -1, 0) + glTexCoord2f(1, 1) glVertex3f(1, -1, 0) - glTexCoord2f(1,1) + glTexCoord2f(1, 0) glVertex3f(1, 1, 0) - glTexCoord2f(0,1) + glTexCoord2f(0, 0) glVertex3f(-1, 1, 0) glEnd() glDisable(GL_TEXTURE_3D) - - - From 2dc31b53dae47cef7eb3d84d1406a7b727cf1e9e Mon Sep 17 00:00:00 2001 From: Justin Engel Date: Thu, 13 Jul 2017 08:25:45 -0400 Subject: [PATCH 064/607] Fixed dataBounds error when all values were inf. If all values are inf. d = d[mask] will create an empty array. You cannot call min or max on an empty array. --- pyqtgraph/graphicsItems/PlotCurveItem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d66a8a99..4b876eb5 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -132,7 +132,11 @@ class PlotCurveItem(GraphicsObject): if any(np.isinf(b)): mask = np.isfinite(d) d = d[mask] - b = (d.min(), d.max()) + try: + b = (d.min(), d.max()) + except ValueError: + # d has no size, because all of d is inf. + b = (-1, 1) # Some default bounds elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) From e87eaa652d5a2959608c4954ed900e5874202c33 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Jul 2017 15:10:04 -0700 Subject: [PATCH 065/607] Docstring correction --- pyqtgraph/graphicsItems/ImageItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 3d45ad77..9588c586 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -214,7 +214,8 @@ class ImageItem(GraphicsObject): border Sets the pen used when drawing the image border. Default is None. autoDownsample (bool) If True, the image is automatically downsampled to match the screen resolution. This improves performance for large images and - reduces aliasing. + reduces aliasing. If autoDownsample is not specified, then ImageItem will + choose whether to downsample the image based on its size. ================= ========================================================================= From d343eb044de8decf2e07ca6b9ed851398ee763e0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 14 Jul 2017 15:10:16 -0700 Subject: [PATCH 066/607] Fix errors getting bounds on nanny data --- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index d66a8a99..fac9ee57 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -132,6 +132,8 @@ class PlotCurveItem(GraphicsObject): if any(np.isinf(b)): mask = np.isfinite(d) d = d[mask] + if len(d) == 0: + return (None, None) b = (d.min(), d.max()) elif frac <= 0.0: @@ -173,7 +175,7 @@ class PlotCurveItem(GraphicsObject): if self._boundingRect is None: (xmn, xmx) = self.dataBounds(ax=0) (ymn, ymx) = self.dataBounds(ax=1) - if xmn is None: + if xmn is None or ymn is None: return QtCore.QRectF() px = py = 0.0 From 5855aa8627fa804283c413a2ffbd0e1b14e36eff Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 27 Jul 2017 22:20:26 -0700 Subject: [PATCH 067/607] Code cleanup; no functional changes --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 130 ++------------------- 1 file changed, 12 insertions(+), 118 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 4cab8662..9af43614 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -85,7 +85,6 @@ class ViewBox(GraphicsWidget): sigXRangeChanged = QtCore.Signal(object, object) sigRangeChangedManually = QtCore.Signal(object) sigRangeChanged = QtCore.Signal(object, object) - #sigActionPositionChanged = QtCore.Signal(object) sigStateChanged = QtCore.Signal(object) sigTransformChanged = QtCore.Signal(object) sigResized = QtCore.Signal(object) @@ -128,8 +127,6 @@ class ViewBox(GraphicsWidget): self.name = None self.linksBlocked = False self.addedItems = [] - #self.gView = view - #self.showGrid = showGrid self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed. @@ -188,9 +185,6 @@ class ViewBox(GraphicsWidget): self.background.setPen(fn.mkPen(None)) self.updateBackground() - #self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan - # this also enables capture of keyPressEvents. - ## Make scale box that is shown when dragging on the view self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) @@ -239,7 +233,6 @@ class ViewBox(GraphicsWidget): ViewBox.updateAllViewLists() sid = id(self) self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None) - #self.destroyed.connect(self.unregister) def unregister(self): """ @@ -288,16 +281,12 @@ class ViewBox(GraphicsWidget): self.prepareForPaint() self._lastScene = scene - - - def prepareForPaint(self): #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) # don't check whether auto range is enabled here--only check when setting dirty flag. if self._autoRangeNeedsUpdate: # and autoRangeEnabled: self.updateAutoRange() - if self._matrixNeedsUpdate: - self.updateMatrix() + self.updateMatrix() def getState(self, copy=True): """Return the current state of the ViewBox. @@ -326,7 +315,6 @@ class ViewBox(GraphicsWidget): del state['linkedViews'] self.state.update(state) - #self.updateMatrix() self.updateViewRange() self.sigStateChanged.emit(self) @@ -353,12 +341,6 @@ class ViewBox(GraphicsWidget): self.state['mouseMode'] = mode self.sigStateChanged.emit(self) - #def toggleLeftAction(self, act): ## for backward compatibility - #if act.text() is 'pan': - #self.setLeftButtonAction('pan') - #elif act.text() is 'zoom': - #self.setLeftButtonAction('rect') - def setLeftButtonAction(self, mode='rect'): ## for backward compatibility if mode.lower() == 'rect': self.setMouseMode(ViewBox.RectMode) @@ -405,7 +387,6 @@ class ViewBox(GraphicsWidget): if not ignoreBounds: self.addedItems.append(item) self.updateAutoRange() - #print "addItem:", item, item.boundingRect() def removeItem(self, item): """Remove an item from this view.""" @@ -562,10 +543,6 @@ class ViewBox(GraphicsWidget): # If nothing has changed, we are done. if any(changed): - #if update and self.matrixNeedsUpdate: - #self.updateMatrix(changed) - #return - self.sigStateChanged.emit(self) # Update target rect for debugging @@ -581,21 +558,6 @@ class ViewBox(GraphicsWidget): self._autoRangeNeedsUpdate = True #self.updateAutoRange() - ## Update view matrix only if requested - #if update: - #self.updateMatrix(changed) - ## Otherwise, indicate that the matrix needs to be updated - #else: - #self.matrixNeedsUpdate = True - - ## Inform linked views that the range has changed <> - #for ax, range in changes.items(): - #link = self.linkedView(ax) - #if link is not None: - #link.linkedViewChanged(self, ax) - - - def setYRange(self, min, max, padding=None, update=True): """ Set the visible Y range of the view to [*min*, *max*]. @@ -675,10 +637,6 @@ class ViewBox(GraphicsWidget): for kwd in kwds: if kwd not in allowed: raise ValueError("Invalid keyword argument '%s'." % kwd) - #for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']: - #if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]: - #self.state['limits'][kwd] = kwds[kwd] - #update = True for axis in [0,1]: for mnmx in [0,1]: kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx] @@ -694,9 +652,6 @@ class ViewBox(GraphicsWidget): if update: self.updateViewRange() - - - def scaleBy(self, s=None, center=None, x=None, y=None): """ @@ -762,8 +717,6 @@ class ViewBox(GraphicsWidget): y = vr.top()+y, vr.bottom()+y if x is not None or y is not None: self.setRange(xRange=x, yRange=y, padding=0) - - def enableAutoRange(self, axis=None, enable=True, x=None, y=None): """ @@ -773,11 +726,6 @@ class ViewBox(GraphicsWidget): The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should be visible (this only works with items implementing a dataRange method, such as PlotDataItem). """ - #print "autorange:", axis, enable - #if not enable: - #import traceback - #traceback.print_stack() - # support simpler interface: if x is not None or y is not None: if x is not None: @@ -813,10 +761,6 @@ class ViewBox(GraphicsWidget): self.state['autoRange'][ax] = enable self._autoRangeNeedsUpdate |= (enable is not False) self.update() - - - #if needAutoRangeUpdate: - # self.updateAutoRange() self.sigStateChanged.emit(self) @@ -828,6 +772,8 @@ class ViewBox(GraphicsWidget): return self.state['autoRange'][:] def setAutoPan(self, x=None, y=None): + """Set whether automatic range will only pan (not scale) the view. + """ if x is not None: self.state['autoPan'][0] = x if y is not None: @@ -836,6 +782,9 @@ class ViewBox(GraphicsWidget): self.updateAutoRange() def setAutoVisible(self, x=None, y=None): + """Set whether automatic range uses only visible data when determining + the range to show. + """ if x is not None: self.state['autoVisibleOnly'][0] = x if x is True: @@ -924,7 +873,6 @@ class ViewBox(GraphicsWidget): """Link this view's Y axis to another view. (see LinkView)""" self.linkView(self.YAxis, view) - def linkView(self, axis, view): """ Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis. @@ -1118,7 +1066,6 @@ class ViewBox(GraphicsWidget): return self.state['aspectLocked'] = ratio if ratio != currentRatio: ## If this would change the current range, do that now - #self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1]) self.updateViewRange() self.updateAutoRange() @@ -1130,12 +1077,9 @@ class ViewBox(GraphicsWidget): Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. (This maps from inside the viewbox to outside) """ - if self._matrixNeedsUpdate: - self.updateMatrix() + self.updateMatrix() m = self.childGroup.transform() - #m1 = QtGui.QTransform() - #m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y()) - return m #*m1 + return m def mapToView(self, obj): """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox""" @@ -1163,7 +1107,6 @@ class ViewBox(GraphicsWidget): def mapFromViewToItem(self, item, obj): """Maps *obj* from view coordinates to the local coordinate system of *item*.""" return self.childGroup.mapToItem(item, obj) - #return item.mapFromScene(self.mapViewToScene(obj)) def mapViewToDevice(self, obj): return self.mapToDevice(self.mapFromView(obj)) @@ -1177,25 +1120,9 @@ class ViewBox(GraphicsWidget): px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()] return (px.length(), py.length()) - def itemBoundingRect(self, item): """Return the bounding rect of the item in view coordinates""" return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() - - #def viewScale(self): - #vr = self.viewRect() - ##print "viewScale:", self.range - #xd = vr.width() - #yd = vr.height() - #if xd == 0 or yd == 0: - #print "Warning: 0 range in view:", xd, yd - #return np.array([1,1]) - - ##cs = self.canvas().size() - #cs = self.boundingRect() - #scale = np.array([cs.width() / xd, cs.height() / yd]) - ##print "view scale:", scale - #return scale def wheelEvent(self, ev, axis=None): mask = np.array(self.state['mouseEnabled'], dtype=np.float) @@ -1206,13 +1133,11 @@ class ViewBox(GraphicsWidget): s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) - #center = ev.pos() self._resetTarget() self.scaleBy(s, center) self.sigRangeChangedManually.emit(self.state['mouseEnabled']) ev.accept() - def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): @@ -1251,7 +1176,6 @@ class ViewBox(GraphicsWidget): if ev.isFinish(): ## This is the final move in the drag; change the view scale now #print "finish" self.rbScaleBox.hide() - #ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos)) ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos)) ax = self.childGroup.mapRectFromParent(ax) self.showAxRect(ax) @@ -1301,12 +1225,6 @@ class ViewBox(GraphicsWidget): ctrl-- : moves backward in the zooming stack (if it exists) """ - #print ev.key() - #print 'I intercepted a key press, but did not accept it' - - ## not implemented yet ? - #self.keypress.sigkeyPressEvent.emit() - ev.accept() if ev.text() == '-': self.scaleHistory(-1) @@ -1324,7 +1242,6 @@ class ViewBox(GraphicsWidget): if ptr != self.axHistoryPointer: self.axHistoryPointer = ptr self.showAxRect(self.axHistory[ptr]) - def updateScaleBox(self, p1, p2): r = QtCore.QRectF(p1, p2) @@ -1338,14 +1255,6 @@ class ViewBox(GraphicsWidget): self.setRange(ax.normalized()) # be sure w, h are correct coordinates self.sigRangeChangedManually.emit(self.state['mouseEnabled']) - #def mouseRect(self): - #vs = self.viewScale() - #vr = self.state['viewRange'] - ## Convert positions from screen (view) pixel coordinates to axis coordinates - #ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]), - #(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1]) - #return(ax) - def allChildren(self, item=None): """Return a list of all children and grandchildren of this ViewBox""" if item is None: @@ -1356,8 +1265,6 @@ class ViewBox(GraphicsWidget): children.extend(self.allChildren(ch)) return children - - def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): """Return the bounding range of all children. [[xmin, xmax], [ymin, ymax]] @@ -1380,8 +1287,6 @@ class ViewBox(GraphicsWidget): useY = True if hasattr(item, 'dataBounds'): - #bounds = self._itemBoundsCache.get(item, None) - #if bounds is None: if frac is None: frac = (1.0, 1.0) xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) @@ -1414,9 +1319,6 @@ class ViewBox(GraphicsWidget): itemBounds.append((bounds, useX, useY, pxPad)) - #self._itemBoundsCache[item] = (bounds, useX, useY) - #else: - #bounds, useX, useY = bounds else: if int(item.flags() & item.ItemHasNoContents) > 0: continue @@ -1425,8 +1327,6 @@ class ViewBox(GraphicsWidget): bounds = self.mapFromItemToView(item, bounds).boundingRect() itemBounds.append((bounds, True, True, 0)) - #print itemBounds - ## determine tentative new range range = [None, None] for bounds, useX, useY, px in itemBounds: @@ -1442,14 +1342,11 @@ class ViewBox(GraphicsWidget): range[0] = [bounds.left(), bounds.right()] profiler() - #print "range", range - ## Now expand any bounds that have a pixel margin ## This must be done _after_ we have a good estimate of the new range ## to ensure that the pixel size is roughly accurate. w = self.width() h = self.height() - #print "w:", w, "h:", h if w > 0 and range[0] is not None: pxSize = (range[0][1] - range[0][0]) / w for bounds, useX, useY, px in itemBounds: @@ -1598,6 +1495,9 @@ class ViewBox(GraphicsWidget): link.linkedViewChanged(self, ax) def updateMatrix(self, changed=None): + if not self._matrixNeedsUpdate: + return + ## Make the childGroup's transform match the requested viewRange. bounds = self.rect() @@ -1648,7 +1548,6 @@ class ViewBox(GraphicsWidget): self.background.show() self.background.setBrush(fn.mkBrush(bg)) - def updateViewLists(self): try: self.window() @@ -1662,7 +1561,6 @@ class ViewBox(GraphicsWidget): ## make a sorted list of all named views nv = list(ViewBox.NamedViews.values()) - #print "new view list:", nv sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList if self in nv: @@ -1676,16 +1574,11 @@ class ViewBox(GraphicsWidget): for v in nv: if link == v.name: self.linkView(ax, v) - #print "New view list:", nv - #print "linked views:", self.state['linkedViews'] @staticmethod def updateAllViewLists(): - #print "Update:", ViewBox.AllViews.keys() - #print "Update:", ViewBox.NamedViews.keys() for v in ViewBox.AllViews: v.updateViewLists() - @staticmethod def forgetView(vid, name): @@ -1766,4 +1659,5 @@ class ViewBox(GraphicsWidget): self.scene().removeItem(self.locateGroup) self.locateGroup = None + from .ViewBoxMenu import ViewBoxMenu From 55d21a436f538c5f2c445398904024189298f071 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 27 Jul 2017 22:21:02 -0700 Subject: [PATCH 068/607] ViewBox: mark matrix dirty _before_ emitting change signal to ensure that slots can access the latest transform. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 9af43614..8ade0c6b 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1482,9 +1482,9 @@ class ViewBox(GraphicsWidget): self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) if any(changed): + self._matrixNeedsUpdate = True self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() - self._matrixNeedsUpdate = True # Inform linked views that the range has changed for ax in [0, 1]: From 82b666e2ee0e5c9ae4bd8aca37f2ae5fda35a236 Mon Sep 17 00:00:00 2001 From: Matthew Shun-Shin Date: Fri, 28 Jul 2017 11:30:19 +0100 Subject: [PATCH 069/607] Fix GL Views being half size on hidpi monitors --- pyqtgraph/opengl/GLViewWidget.py | 4 ++-- pyqtgraph/widgets/RawImageWidget.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index e0fee046..40a7d858 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -80,7 +80,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): def getViewport(self): vp = self.opts['viewport'] if vp is None: - return (0, 0, self.width(), self.height()) + return (0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) else: return vp @@ -99,7 +99,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): def projectionMatrix(self, region=None): # Xw = (Xnd + 1) * width/2 + X if region is None: - region = (0, 0, self.width(), self.height()) + region = (0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) x0, y0, w, h = self.getViewport() dist = self.opts['distance'] diff --git a/pyqtgraph/widgets/RawImageWidget.py b/pyqtgraph/widgets/RawImageWidget.py index 657701f9..ef1d7a38 100644 --- a/pyqtgraph/widgets/RawImageWidget.py +++ b/pyqtgraph/widgets/RawImageWidget.py @@ -122,7 +122,7 @@ if HAVE_OPENGL: if not self.uploaded: self.uploadTexture() - glViewport(0, 0, self.width(), self.height()) + glViewport(0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) glEnable(GL_TEXTURE_2D) glBindTexture(GL_TEXTURE_2D, self.texture) glColor4f(1,1,1,1) From 6c7e0fae8ee13ae9b1e290bcef6be1ca13a40796 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 20 Oct 2016 12:10:53 -0700 Subject: [PATCH 070/607] Add signals for TreeWidget changes in check state, text, and columncount --- pyqtgraph/widgets/TreeWidget.py | 74 +++++++++++++++++++++++++++------ 1 file changed, 61 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index b98da6fa..b20f14fd 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- -from weakref import * from ..Qt import QtGui, QtCore -from ..python2_3 import xrange - +from weakref import * __all__ = ['TreeWidget', 'TreeWidgetItem'] @@ -13,6 +11,9 @@ class TreeWidget(QtGui.QTreeWidget): This class demonstrates the absurd lengths one must go to to make drag/drop work.""" sigItemMoved = QtCore.Signal(object, object, object) # (item, parent, index) + sigItemCheckStateChanged = QtCore.Signal(object, object) + sigItemTextChanged = QtCore.Signal(object, object) + sigColumnCountChanged = QtCore.Signal(object, object) # self, count def __init__(self, parent=None): QtGui.QTreeWidget.__init__(self, parent) @@ -42,7 +43,7 @@ class TreeWidget(QtGui.QTreeWidget): def itemWidget(self, item, col): w = QtGui.QTreeWidget.itemWidget(self, item, col) - if w is not None: + if w is not None and hasattr(w, 'realChild'): w = w.realChild return w @@ -140,7 +141,6 @@ class TreeWidget(QtGui.QTreeWidget): def dropEvent(self, ev): QtGui.QTreeWidget.dropEvent(self, ev) self.updateDropFlags() - def updateDropFlags(self): ### intended to put a limit on how deep nests of children can go. @@ -165,9 +165,8 @@ class TreeWidget(QtGui.QTreeWidget): def informTreeWidgetChange(item): if hasattr(item, 'treeWidgetChanged'): item.treeWidgetChanged() - else: - for i in xrange(item.childCount()): - TreeWidget.informTreeWidgetChange(item.child(i)) + for i in xrange(item.childCount()): + TreeWidget.informTreeWidgetChange(item.child(i)) def addTopLevelItem(self, item): @@ -210,20 +209,51 @@ class TreeWidget(QtGui.QTreeWidget): #for item in items: #self.informTreeWidgetChange(item) - + def itemFromIndex(self, index): + """Return the item and column corresponding to a QModelIndex. + """ + col = index.column() + rows = [] + while index.row() >= 0: + rows.insert(0, index.row()) + index = index.parent() + item = self.topLevelItem(rows[0]) + for row in rows[1:]: + item = item.child(row) + return item, col + + def setColumnCount(self, c): + QtGui.QTreeWidget.setColumnCount(self, c) + self.sigColumnCountChanged.emit(self, c) + + class TreeWidgetItem(QtGui.QTreeWidgetItem): """ - TreeWidgetItem that keeps track of its own widgets. - Widgets may be added to columns before the item is added to a tree. + TreeWidgetItem that keeps track of its own widgets and expansion state. + + * Widgets may be added to columns before the item is added to a tree. + * Expanded state may be set before item is added to a tree. + * Adds setCheked and isChecked methods. + * Adds addChildren, insertChildren, and takeChildren methods. """ def __init__(self, *args): QtGui.QTreeWidgetItem.__init__(self, *args) self._widgets = {} # col: widget self._tree = None - + self._expanded = False def setChecked(self, column, checked): self.setCheckState(column, QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked) + + def isChecked(self, col): + return self.checkState(col) == QtCore.Qt.Checked + + def setExpanded(self, exp): + self._expanded = exp + QtGui.QTreeWidgetItem.setExpanded(self, exp) + + def isExpanded(self): + return self._expanded def setWidget(self, column, widget): if column in self._widgets: @@ -251,7 +281,11 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): return for col, widget in self._widgets.items(): tree.setItemWidget(self, col, widget) - + QtGui.QTreeWidgetItem.setExpanded(self, self._expanded) + + def childItems(self): + return [self.child(i) for i in range(self.childCount())] + def addChild(self, child): QtGui.QTreeWidgetItem.addChild(self, child) TreeWidget.informTreeWidgetChange(child) @@ -285,4 +319,18 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): TreeWidget.informTreeWidgetChange(child) return childs + def setData(self, column, role, value): + # credit: ekhumoro + # http://stackoverflow.com/questions/13662020/how-to-implement-itemchecked-and-itemunchecked-signals-for-qtreewidget-in-pyqt4 + checkstate = self.checkState(column) + text = self.text(column) + QtGui.QTreeWidgetItem.setData(self, column, role, value) + treewidget = self.treeWidget() + if treewidget is None: + return + if (role == QtCore.Qt.CheckStateRole and checkstate != self.checkState(column)): + treewidget.sigItemCheckStateChanged.emit(self, column) + elif (role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) and text != self.text(column)): + treewidget.sigItemTextChanged.emit(self, column) + From 0e06c504020fa1488dbd13538c578706f36b5b36 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Jul 2017 15:57:45 -0700 Subject: [PATCH 071/607] Catch OSError from ForkedProcess that has already exited. --- pyqtgraph/multiprocess/processes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index c7e4a80c..02f259e5 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -321,9 +321,14 @@ class ForkedProcess(RemoteEventHandler): #os.kill(pid, 9) try: self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation. - os.waitpid(self.childPid, 0) except IOError: ## probably remote process has already quit pass + + try: + os.waitpid(self.childPid, 0) + except OSError: ## probably remote process has already quit + pass + self.hasJoined = True def kill(self): From 3fbc3864f2611d385bd88d4f2becf94a9a6f3722 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Jul 2017 16:05:58 -0700 Subject: [PATCH 072/607] Wrap TreeWidget's invisible root item so that child items will receive tree change notifications --- pyqtgraph/widgets/TreeWidget.py | 51 +++++++++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index b20f14fd..09ab8da5 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -141,7 +141,7 @@ class TreeWidget(QtGui.QTreeWidget): def dropEvent(self, ev): QtGui.QTreeWidget.dropEvent(self, ev) self.updateDropFlags() - + def updateDropFlags(self): ### intended to put a limit on how deep nests of children can go. ### self.childNestingLimit is upheld when moving items without children, but if the item being moved has children/grandchildren, the children/grandchildren @@ -168,7 +168,6 @@ class TreeWidget(QtGui.QTreeWidget): for i in xrange(item.childCount()): TreeWidget.informTreeWidgetChange(item.child(i)) - def addTopLevelItem(self, item): QtGui.QTreeWidget.addTopLevelItem(self, item) self.informTreeWidgetChange(item) @@ -208,6 +207,11 @@ class TreeWidget(QtGui.QTreeWidget): ## Why do we want to do this? It causes RuntimeErrors. #for item in items: #self.informTreeWidgetChange(item) + + def invisibleRootItem(self): + # wrap this item so that we can propagate tree change information + # to children. + return InvisibleRootItem(QtGui.QTreeWidget.invisibleRootItem(self)) def itemFromIndex(self, index): """Return the item and column corresponding to a QModelIndex. @@ -333,4 +337,47 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): treewidget.sigItemCheckStateChanged.emit(self, column) elif (role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) and text != self.text(column)): treewidget.sigItemTextChanged.emit(self, column) + +class InvisibleRootItem(QtGui.QTreeWidgetItem): + """Wrapper around a TreeWidget's invisible root item that calls + TreeWidget.informTreeWidgetChange when child items are added/removed. + """ + def __init__(self, item): + self._real_item = item + + def addChild(self, child): + self._real_item.addChild(child) + TreeWidget.informTreeWidgetChange(child) + + def addChildren(self, childs): + self._real_item.addChildren(childs) + for child in childs: + TreeWidget.informTreeWidgetChange(child) + + def insertChild(self, index, child): + self._real_item.insertChild(index, child) + TreeWidget.informTreeWidgetChange(child) + + def insertChildren(self, index, childs): + self._real_item.addChildren(index, childs) + for child in childs: + TreeWidget.informTreeWidgetChange(child) + + def removeChild(self, child): + self._real_item.removeChild(child) + TreeWidget.informTreeWidgetChange(child) + + def takeChild(self, index): + child = self._real_item.takeChild(index) + TreeWidget.informTreeWidgetChange(child) + return child + + def takeChildren(self): + childs = self._real_item.takeChildren() + for child in childs: + TreeWidget.informTreeWidgetChange(child) + return childs + + def __getattr__(self, attr): + return getattr(self._real_item, attr) From f5775422c607195d7cc24ce360eabf48e0fee414 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Jul 2017 16:18:31 -0700 Subject: [PATCH 073/607] py3 fix --- pyqtgraph/widgets/TreeWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index 09ab8da5..ace03b5c 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -165,7 +165,7 @@ class TreeWidget(QtGui.QTreeWidget): def informTreeWidgetChange(item): if hasattr(item, 'treeWidgetChanged'): item.treeWidgetChanged() - for i in xrange(item.childCount()): + for i in range(item.childCount()): TreeWidget.informTreeWidgetChange(item.child(i)) def addTopLevelItem(self, item): From ea51a65dfdbd4add0407f87cecc2922474176cbb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 31 Jul 2017 10:03:13 -0700 Subject: [PATCH 074/607] Send click events to treewidgetitem --- pyqtgraph/widgets/TreeWidget.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index ace03b5c..a37181cf 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -23,6 +23,7 @@ class TreeWidget(QtGui.QTreeWidget): self.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed|QtGui.QAbstractItemView.SelectedClicked) self.placeholders = [] self.childNestingLimit = None + self.itemClicked.connect(self._itemClicked) def setItemWidget(self, item, col, wid): """ @@ -230,7 +231,11 @@ class TreeWidget(QtGui.QTreeWidget): QtGui.QTreeWidget.setColumnCount(self, c) self.sigColumnCountChanged.emit(self, c) - + def _itemClicked(self, item, col): + if hasattr(item, 'itemClicked'): + item.itemClicked(col) + + class TreeWidgetItem(QtGui.QTreeWidgetItem): """ TreeWidgetItem that keeps track of its own widgets and expansion state. @@ -338,6 +343,12 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): elif (role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole) and text != self.text(column)): treewidget.sigItemTextChanged.emit(self, column) + def itemClicked(self, col): + """Called when this item is clicked on. + + Override this method to react to user clicks. + """ + class InvisibleRootItem(QtGui.QTreeWidgetItem): """Wrapper around a TreeWidget's invisible root item that calls From c719ad4355e5fae81c95cf1fb32206ae854dcb4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 31 Jul 2017 17:04:53 -0700 Subject: [PATCH 075/607] Check for existence of QtCore.QString before using it --- pyqtgraph/parametertree/parameterTypes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index e3ed8853..ace0c9a4 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -333,13 +333,14 @@ class SimpleParameter(Parameter): return fn(v) def _interpStr(self, v): + isQString = hasattr(QtCore, 'QString') and isinstance(v, QtCore.QString) if sys.version[0] == '2': - if isinstance(v, QtCore.QString): + if isQString: v = unicode(v) elif not isinstance(v, basestring): raise TypeError("Cannot set str parmeter from object %r" % v) else: - if isinstance(v, QtCore.QString): + if isQString: v = str(v) elif not isinstance(v, str): raise TypeError("Cannot set str parmeter from object %r" % v) From b4e722f07bd09e7adfbcead693c09e3f5ee1a974 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 31 Jul 2017 17:16:46 -0700 Subject: [PATCH 076/607] Loosen string type checking a bit; let asUnicode throw errors if it needs to. --- pyqtgraph/parametertree/parameterTypes.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index ace0c9a4..8c1e587d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -326,26 +326,12 @@ class SimpleParameter(Parameter): 'int': int, 'float': float, 'bool': bool, - 'str': self._interpStr, + 'str': asUnicode, 'color': self._interpColor, 'colormap': self._interpColormap, }[self.opts['type']] return fn(v) - def _interpStr(self, v): - isQString = hasattr(QtCore, 'QString') and isinstance(v, QtCore.QString) - if sys.version[0] == '2': - if isQString: - v = unicode(v) - elif not isinstance(v, basestring): - raise TypeError("Cannot set str parmeter from object %r" % v) - else: - if isQString: - v = str(v) - elif not isinstance(v, str): - raise TypeError("Cannot set str parmeter from object %r" % v) - return v - def _interpColor(self, v): return fn.mkColor(v) From 9094261c542684146a1813470012cd1fbfcabe12 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 2 Aug 2017 15:02:38 -0700 Subject: [PATCH 077/607] Fix eq() bug where calling catch_warnings raised an AttributeError, which would cause eq() to return False Add unit test coverage --- pyqtgraph/functions.py | 37 +++++++++++++----- pyqtgraph/tests/test_functions.py | 63 +++++++++++++++++++++++++++++++ 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index bdbf6d87..1aed6ace 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -200,7 +200,7 @@ def mkColor(*args): try: return Colors[c] except KeyError: - raise Exception('No color named "%s"' % c) + raise ValueError('No color named "%s"' % c) if len(c) == 3: r = int(c[0]*2, 16) g = int(c[1]*2, 16) @@ -235,18 +235,18 @@ def mkColor(*args): elif len(args[0]) == 2: return intColor(*args[0]) else: - raise Exception(err) + raise TypeError(err) elif type(args[0]) == int: return intColor(args[0]) else: - raise Exception(err) + raise TypeError(err) elif len(args) == 3: (r, g, b) = args a = 255 elif len(args) == 4: (r, g, b, a) = args else: - raise Exception(err) + raise TypeError(err) args = [r,g,b,a] args = [0 if np.isnan(a) or np.isinf(a) else a for a in args] @@ -404,22 +404,39 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0) def eq(a, b): - """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" + """The great missing equivalence function: Guaranteed evaluation to a single bool value. + + This function has some important differences from the == operator: + + 1. Returns True if a IS b, even if a==b still evaluates to False, such as with nan values. + 2. Tests for equivalence using ==, but silently ignores some common exceptions that can occur + (AtrtibuteError, ValueError). + 3. When comparing arrays, returns False if the array shapes are not the same. + 4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas + the == operator would return a boolean array). + """ if a is b: return True try: - with warnings.catch_warnings(module=np): # ignore numpy futurewarning (numpy v. 1.10) - e = a==b - except ValueError: - return False - except AttributeError: + try: + # Sometimes running catch_warnings(module=np) generates AttributeError ??? + catcher = warnings.catch_warnings(module=np) # ignore numpy futurewarning (numpy v. 1.10) + catcher.__enter__() + except Exception: + catcher = None + e = a==b + except (ValueError, AttributeError): return False except: print('failed to evaluate equivalence for:') print(" a:", str(type(a)), str(a)) print(" b:", str(type(b)), str(b)) raise + finally: + if catcher is not None: + catcher.__exit__(None, None, None) + t = type(e) if t is bool: return e diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 7ad3bf91..eff56635 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -1,5 +1,6 @@ import pyqtgraph as pg import numpy as np +import sys from numpy.testing import assert_array_almost_equal, assert_almost_equal import pytest @@ -293,6 +294,68 @@ def test_makeARGB(): with AssertExc(): # 3d levels not allowed pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2])) + +def test_eq(): + eq = pg.functions.eq + + zeros = [0, 0.0, np.float(0), np.int(0)] + if sys.version[0] < '3': + zeros.append(long(0)) + for i,x in enumerate(zeros): + for y in zeros[i:]: + assert eq(x, y) + assert eq(y, x) + + assert eq(np.nan, np.nan) + + # test + class NotEq(object): + def __eq__(self, x): + return False + + noteq = NotEq() + assert eq(noteq, noteq) # passes because they are the same object + assert not eq(noteq, NotEq()) + + + # Should be able to test for equivalence even if the test raises certain + # exceptions + class NoEq(object): + def __init__(self, err): + self.err = err + def __eq__(self, x): + raise self.err + + noeq1 = NoEq(AttributeError()) + noeq2 = NoEq(ValueError()) + noeq3 = NoEq(Exception()) + + assert eq(noeq1, noeq1) + assert not eq(noeq1, noeq2) + assert not eq(noeq2, noeq1) + with pytest.raises(Exception): + eq(noeq3, noeq2) + + # test array equivalence + # note that numpy has a weird behavior here--np.all() always returns True + # if one of the arrays has size=0; eq() will only return True if both arrays + # have the same shape. + a1 = np.zeros((10, 20)).astype('float') + a2 = a1 + 1 + a3 = a2.astype('int') + a4 = np.empty((0, 20)) + assert not eq(a1, a2) + assert not eq(a1, a3) + assert not eq(a1, a4) + + assert eq(a2, a3) + assert not eq(a2, a4) + + assert not eq(a3, a4) + + assert eq(a4, a4.copy()) + assert not eq(a4, a4.T) + if __name__ == '__main__': test_interpolateArray() \ No newline at end of file From 16f0e3034c1667776ca008630628fd4f85c3eb0e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 2 Aug 2017 15:03:58 -0700 Subject: [PATCH 078/607] Add tests for inpute/output type on a few parameter types --- .../tests/test_parametertypes.py | 120 +++++++++++++++++- 1 file changed, 118 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/tests/test_parametertypes.py b/pyqtgraph/parametertree/tests/test_parametertypes.py index dc581019..a654a9ad 100644 --- a/pyqtgraph/parametertree/tests/test_parametertypes.py +++ b/pyqtgraph/parametertree/tests/test_parametertypes.py @@ -1,7 +1,19 @@ +# ~*~ coding: utf8 ~*~ +import sys +import pytest +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph.parametertree as pt import pyqtgraph as pg +from pyqtgraph.python2_3 import asUnicode +from pyqtgraph.functions import eq +import numpy as np + app = pg.mkQApp() +def _getWidget(param): + return list(param.items.keys())[0].widget + + def test_opts(): paramSpec = [ dict(name='bool', type='bool', readonly=True), @@ -12,7 +24,111 @@ def test_opts(): tree = pt.ParameterTree() tree.setParameters(param) - assert list(param.param('bool').items.keys())[0].widget.isEnabled() is False - assert list(param.param('color').items.keys())[0].widget.isEnabled() is False + assert _getWidget(param.param('bool')).isEnabled() is False + assert _getWidget(param.param('bool')).isEnabled() is False +def test_types(): + paramSpec = [ + dict(name='float', type='float'), + dict(name='int', type='int'), + dict(name='str', type='str'), + dict(name='list', type='list', values=['x','y','z']), + dict(name='dict', type='list', values={'x':1, 'y':3, 'z':7}), + dict(name='bool', type='bool'), + dict(name='color', type='color'), + ] + + param = pt.Parameter.create(name='params', type='group', children=paramSpec) + tree = pt.ParameterTree() + tree.setParameters(param) + + all_objs = { + 'int0': 0, 'int':7, 'float': -0.35, 'bigfloat': 1e129, 'npfloat': np.float(5), + 'npint': np.int(5),'npinf': np.inf, 'npnan': np.nan, 'bool': True, + 'complex': 5+3j, 'str': 'xxx', 'unicode': asUnicode('µ'), + 'list': [1,2,3], 'dict': {'1': 2}, 'color': pg.mkColor('k'), + 'brush': pg.mkBrush('k'), 'pen': pg.mkPen('k'), 'none': None + } + if hasattr(QtCore, 'QString'): + all_objs['qstring'] = QtCore.QString('xxxµ') + + # float + types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'npinf', 'npnan', 'bool'] + check_param_types(param.child('float'), float, float, 0.0, all_objs, types) + + # int + types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'bool'] + inttyps = int if sys.version[0] >= '3' else (int, long) + check_param_types(param.child('int'), inttyps, int, 0, all_objs, types) + + # str (should be able to make a string out of any type) + types = all_objs.keys() + strtyp = str if sys.version[0] >= '3' else unicode + check_param_types(param.child('str'), strtyp, asUnicode, '', all_objs, types) + + # bool (should be able to make a boolean out of any type?) + types = all_objs.keys() + check_param_types(param.child('bool'), bool, bool, False, all_objs, types) + + # color + types = ['color', 'int0', 'int', 'float', 'npfloat', 'npint', 'list'] + init = QtGui.QColor(128, 128, 128, 255) + check_param_types(param.child('color'), QtGui.QColor, pg.mkColor, init, all_objs, types) + + +def check_param_types(param, types, map_func, init, objs, keys): + """Check that parameter setValue() accepts or rejects the correct types and + that value() returns the correct type. + + Parameters + ---------- + param : Parameter instance + types : type or tuple of types + The allowed types for this parameter to return from value(). + map_func : function + Converts an input value to the expected output value. + init : object + The expected initial value of the parameter + objs : dict + Contains a variety of objects that will be tested as arguments to + param.setValue(). + keys : list + The list of keys indicating the valid objects in *objs*. When + param.setValue() is teasted with each value from *objs*, we expect + an exception to be raised if the associated key is not in *keys*. + """ + val = param.value() + if not isinstance(types, tuple): + types = (types,) + assert val == init and type(val) in types + + # test valid input types + good_inputs = [objs[k] for k in keys if k in objs] + good_outputs = map(map_func, good_inputs) + for x,y in zip(good_inputs, good_outputs): + param.setValue(x) + val = param.value() + if not (eq(val, y) and type(val) in types): + raise Exception("Setting parameter %s with value %r should have resulted in %r (types: %r), " + "but resulted in %r (type: %r) instead." % (param, x, y, types, val, type(val))) + + # test invalid input types + for k,v in objs.items(): + if k in keys: + continue + try: + param.setValue(v) + except (TypeError, ValueError, OverflowError): + continue + except Exception as exc: + raise Exception("Setting %s parameter value to %r raised %r." % (param, v, exc)) + + raise Exception("Setting %s parameter value to %r should have raised an exception." % (param, v)) + + + + + + + \ No newline at end of file From 72a715753853267f3513cae799ccca831f4f9b29 Mon Sep 17 00:00:00 2001 From: Alexander Manakov Date: Fri, 4 Aug 2017 13:56:37 +0300 Subject: [PATCH 079/607] Added essential if. --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 597491f3..f426c91c 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -648,6 +648,9 @@ class ScatterPlotItem(GraphicsObject): d = d[mask] d2 = d2[mask] + if d.size == 0: + return (None, None) + if frac >= 1.0: self.bounds[ax] = (np.nanmin(d) - self._maxSpotWidth*0.7072, np.nanmax(d) + self._maxSpotWidth*0.7072) return self.bounds[ax] From 70831245a838e6471ebe493c4587f208dcb10baa Mon Sep 17 00:00:00 2001 From: HashSplat Date: Tue, 8 Aug 2017 14:58:33 -0400 Subject: [PATCH 080/607] Fixed issue where ImageItem data size would be 0 due to colormap. --- pyqtgraph/graphicsItems/ImageItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 3d45ad77..706735bd 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -401,7 +401,8 @@ class ImageItem(GraphicsObject): image = image.transpose((1, 0, 2)[:image.ndim]) argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) - self.qimage = fn.makeQImage(argb, alpha, transpose=False) + if argb.size > 0: + self.qimage = fn.makeQImage(argb, alpha, transpose=False) def paint(self, p, *args): profile = debug.Profiler() From e7ba0ca272122952dc2bdbb62bde8df51e6e6966 Mon Sep 17 00:00:00 2001 From: HashSplat Date: Tue, 8 Aug 2017 15:35:54 -0400 Subject: [PATCH 081/607] Found that downsample would set the image size to 0 causing errors. --- pyqtgraph/graphicsItems/ImageItem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 706735bd..411eaf5a 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -368,6 +368,10 @@ class ImageItem(GraphicsObject): image = fn.downsample(self.image, xds, axis=axes[0]) image = fn.downsample(image, yds, axis=axes[1]) self._lastDownsample = (xds, yds) + + # Check if downsampling reduced the image size to zero due to inf values. + if image.size == 0: + return else: image = self.image @@ -401,8 +405,7 @@ class ImageItem(GraphicsObject): image = image.transpose((1, 0, 2)[:image.ndim]) argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) - if argb.size > 0: - self.qimage = fn.makeQImage(argb, alpha, transpose=False) + self.qimage = fn.makeQImage(argb, alpha, transpose=False) def paint(self, p, *args): profile = debug.Profiler() From baba93dc604a5aaf27a7c6fd0c008e2ea22073dd Mon Sep 17 00:00:00 2001 From: Justin Engel Date: Tue, 8 Aug 2017 15:51:32 -0400 Subject: [PATCH 082/607] temporarily removed min max fix for pull request --- pyqtgraph/graphicsItems/PlotCurveItem.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 4b876eb5..d66a8a99 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -132,11 +132,7 @@ class PlotCurveItem(GraphicsObject): if any(np.isinf(b)): mask = np.isfinite(d) d = d[mask] - try: - b = (d.min(), d.max()) - except ValueError: - # d has no size, because all of d is inf. - b = (-1, 1) # Some default bounds + b = (d.min(), d.max()) elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) From ae9bce665c9d1a3e91f02cec0a08be7e6c332727 Mon Sep 17 00:00:00 2001 From: HashSplat Date: Wed, 9 Aug 2017 14:36:50 -0400 Subject: [PATCH 083/607] Fixed issue where setData only updated opts if data is given --- pyqtgraph/graphicsItems/PlotDataItem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index d7ea5100..c5b642fb 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -442,6 +442,8 @@ class PlotDataItem(GraphicsObject): if y is None: + self.updateItems() + profiler('update items') return if y is not None and x is None: x = np.arange(len(y)) From 1ea4f5ab6fd41f1dac8239c462250104b623fb34 Mon Sep 17 00:00:00 2001 From: HashSplat Date: Thu, 31 Aug 2017 08:55:58 -0400 Subject: [PATCH 084/607] ImageItem render error if graph is too small. Fixed issue where render would error because 'mapToDevice' would return None if the view size was too small. --- pyqtgraph/graphicsItems/ImageItem.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 9588c586..764caf2d 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -358,6 +358,11 @@ class ImageItem(GraphicsObject): o = self.mapToDevice(QtCore.QPointF(0,0)) x = self.mapToDevice(QtCore.QPointF(1,0)) y = self.mapToDevice(QtCore.QPointF(0,1)) + + # Check if graphics view is too small to render anything + if o is None or x is None or y is None: + return + w = Point(x-o).length() h = Point(y-o).length() if w == 0 or h == 0: From 8398e578b929e90932ba2be9cfe30123a6021320 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 16:48:08 -0700 Subject: [PATCH 085/607] Add a collapsible QGroubBox widget --- pyqtgraph/__init__.py | 1 + pyqtgraph/widgets/GroupBox.py | 91 +++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 pyqtgraph/widgets/GroupBox.py diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index bc5081f7..24653207 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -262,6 +262,7 @@ from .widgets.GraphicsView import * from .widgets.LayoutWidget import * from .widgets.TableWidget import * from .widgets.ProgressDialog import * +from .widgets.GroupBox import GroupBox from .imageview import * from .WidgetGroup import * diff --git a/pyqtgraph/widgets/GroupBox.py b/pyqtgraph/widgets/GroupBox.py new file mode 100644 index 00000000..14a8dab5 --- /dev/null +++ b/pyqtgraph/widgets/GroupBox.py @@ -0,0 +1,91 @@ +from ..Qt import QtGui, QtCore +from .PathButton import PathButton + +class GroupBox(QtGui.QGroupBox): + """Subclass of QGroupBox that implements collapse handle. + """ + sigCollapseChanged = QtCore.Signal(object) + + def __init__(self, *args): + QtGui.QGroupBox.__init__(self, *args) + + self._collapsed = False + # We modify the size policy when the group box is collapsed, so + # keep track of the last requested policy: + self._lastSizePlocy = self.sizePolicy() + + self.closePath = QtGui.QPainterPath() + self.closePath.moveTo(0, -1) + self.closePath.lineTo(0, 1) + self.closePath.lineTo(1, 0) + self.closePath.lineTo(0, -1) + + self.openPath = QtGui.QPainterPath() + self.openPath.moveTo(-1, 0) + self.openPath.lineTo(1, 0) + self.openPath.lineTo(0, 1) + self.openPath.lineTo(-1, 0) + + self.collapseBtn = PathButton(path=self.openPath, size=(12, 12), margin=0) + self.collapseBtn.setStyleSheet(""" + border: none; + """) + self.collapseBtn.setPen('k') + self.collapseBtn.setBrush('w') + self.collapseBtn.setParent(self) + self.collapseBtn.move(3, 3) + self.collapseBtn.setFlat(True) + + self.collapseBtn.clicked.connect(self.toggleCollapsed) + + if len(args) > 0 and isinstance(args[0], basestring): + self.setTitle(args[0]) + + def toggleCollapsed(self): + self.setCollapsed(not self._collapsed) + + def collapsed(self): + return self._collapsed + + def setCollapsed(self, c): + if c == self._collapsed: + return + + if c is True: + self.collapseBtn.setPath(self.closePath) + self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred, closing=True) + elif c is False: + self.collapseBtn.setPath(self.openPath) + self.setSizePolicy(self._lastSizePolicy) + else: + raise TypeError("Invalid argument %r; must be bool." % c) + + for ch in self.children(): + if isinstance(ch, QtGui.QWidget) and ch is not self.collapseBtn: + ch.setVisible(not c) + + self._collapsed = c + self.sigCollapseChanged.emit(c) + + def setSizePolicy(self, *args, **kwds): + QtGui.QGroupBox.setSizePolicy(self, *args) + if kwds.pop('closing', False) is True: + self._lastSizePolicy = self.sizePolicy() + + def setHorizontalPolicy(self, *args): + QtGui.QGroupBox.setHorizontalPolicy(self, *args) + self._lastSizePolicy = self.sizePolicy() + + def setVerticalPolicy(self, *args): + QtGui.QGroupBox.setVerticalPolicy(self, *args) + self._lastSizePolicy = self.sizePolicy() + + def setTitle(self, title): + # Leave room for button + QtGui.QGroupBox.setTitle(self, " " + title) + + def widgetGroupInterface(self): + return (self.sigCollapseChanged, + GroupBox.collapsed, + GroupBox.setCollapsed, + True) From 65fa58c2b10a9ca9dfd8e711f681c23e4de12d87 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 16:51:11 -0700 Subject: [PATCH 086/607] Add targetitem class --- pyqtgraph/graphicsItems/TargetItem.py | 125 ++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 pyqtgraph/graphicsItems/TargetItem.py diff --git a/pyqtgraph/graphicsItems/TargetItem.py b/pyqtgraph/graphicsItems/TargetItem.py new file mode 100644 index 00000000..114c9e6e --- /dev/null +++ b/pyqtgraph/graphicsItems/TargetItem.py @@ -0,0 +1,125 @@ +from ..Qt import QtGui, QtCore +import numpy as np +from ..Point import Point +from .. import functions as fn +from .GraphicsObject import GraphicsObject +from .TextItem import TextItem + + +class TargetItem(GraphicsObject): + """Draws a draggable target symbol (circle plus crosshair). + + The size of TargetItem will remain fixed on screen even as the view is zoomed. + Includes an optional text label. + """ + sigDragged = QtCore.Signal(object) + + def __init__(self, movable=True, radii=(5, 10, 10), pen=(255, 255, 0), brush=(0, 0, 255, 100)): + GraphicsObject.__init__(self) + self._bounds = None + self._radii = radii + self._picture = None + self.movable = movable + self.moving = False + self.label = None + self.labelAngle = 0 + self.pen = fn.mkPen(pen) + self.brush = fn.mkBrush(brush) + + def setLabel(self, label): + if label is None: + if self.label is not None: + self.label.scene().removeItem(self.label) + self.label = None + else: + if self.label is None: + self.label = TextItem() + self.label.setParentItem(self) + self.label.setText(label) + self._updateLabel() + + def setLabelAngle(self, angle): + self.labelAngle = angle + self._updateLabel() + + def boundingRect(self): + if self._picture is None: + self._drawPicture() + return self._bounds + + def dataBounds(self, axis, frac=1.0, orthoRange=None): + return [0, 0] + + def viewTransformChanged(self): + self._picture = None + self.prepareGeometryChange() + self._updateLabel() + + def _updateLabel(self): + if self.label is None: + return + + # find an optimal location for text at the given angle + angle = self.labelAngle * np.pi / 180. + lbr = self.label.boundingRect() + center = lbr.center() + a = abs(np.sin(angle) * lbr.height()*0.5) + b = abs(np.cos(angle) * lbr.width()*0.5) + r = max(self._radii) + 2 + max(a, b) + pos = self.mapFromScene(self.mapToScene(QtCore.QPointF(0, 0)) + r * QtCore.QPointF(np.cos(angle), -np.sin(angle)) - center) + self.label.setPos(pos) + + def paint(self, p, *args): + if self._picture is None: + self._drawPicture() + self._picture.play(p) + + def _drawPicture(self): + self._picture = QtGui.QPicture() + p = QtGui.QPainter(self._picture) + p.setRenderHint(p.Antialiasing) + + # Note: could do this with self.pixelLength, but this is faster. + o = self.mapToScene(QtCore.QPointF(0, 0)) + px = abs(1.0 / (self.mapToScene(QtCore.QPointF(1, 0)) - o).x()) + py = abs(1.0 / (self.mapToScene(QtCore.QPointF(0, 1)) - o).y()) + + r, w, h = self._radii + w = w * px + h = h * py + rx = r * px + ry = r * py + rect = QtCore.QRectF(-rx, -ry, rx*2, ry*2) + p.setPen(self.pen) + p.setBrush(self.brush) + p.drawEllipse(rect) + p.drawLine(Point(-w, 0), Point(w, 0)) + p.drawLine(Point(0, -h), Point(0, h)) + p.end() + + bx = max(w, rx) + by = max(h, ry) + self._bounds = QtCore.QRectF(-bx, -by, bx*2, by*2) + + def mouseDragEvent(self, ev): + if not self.movable: + return + if ev.button() == QtCore.Qt.LeftButton: + if ev.isStart(): + self.moving = True + self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) + self.startPosition = self.pos() + ev.accept() + + if not self.moving: + return + + self.setPos(self.cursorOffset + self.mapToParent(ev.pos())) + if ev.isFinish(): + self.moving = False + self.sigDragged.emit(self) + + def hoverEvent(self, ev): + if self.movable: + ev.acceptDrags(QtCore.Qt.LeftButton) + From b6f9516678e23ad26357a10643dd2f98cf2bafde Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 17:00:33 -0700 Subject: [PATCH 087/607] Make behavior configurable when a reloaded dock is missing. + other bugfixes --- pyqtgraph/dockarea/Container.py | 32 ++-- pyqtgraph/dockarea/Dock.py | 30 ++-- pyqtgraph/dockarea/DockArea.py | 98 ++++++++---- pyqtgraph/dockarea/tests/test_dockarea.py | 184 ++++++++++++++++++++++ 4 files changed, 288 insertions(+), 56 deletions(-) create mode 100644 pyqtgraph/dockarea/tests/test_dockarea.py diff --git a/pyqtgraph/dockarea/Container.py b/pyqtgraph/dockarea/Container.py index c3225edf..bc0b3648 100644 --- a/pyqtgraph/dockarea/Container.py +++ b/pyqtgraph/dockarea/Container.py @@ -17,16 +17,20 @@ class Container(object): def containerChanged(self, c): self._container = c + if c is None: + self.area = None + else: + self.area = c.area def type(self): return None def insert(self, new, pos=None, neighbor=None): - # remove from existing parent first - new.setParent(None) - if not isinstance(new, list): new = [new] + for n in new: + # remove from existing parent first + n.setParent(None) if neighbor is None: if pos == 'before': index = 0 @@ -40,34 +44,37 @@ class Container(object): index += 1 for n in new: - #print "change container", n, " -> ", self - n.containerChanged(self) #print "insert", n, " -> ", self, index self._insertItem(n, index) + #print "change container", n, " -> ", self + n.containerChanged(self) index += 1 n.sigStretchChanged.connect(self.childStretchChanged) #print "child added", self self.updateStretch() def apoptose(self, propagate=True): - ##if there is only one (or zero) item in this container, disappear. + # if there is only one (or zero) item in this container, disappear. + # if propagate is True, then also attempt to apoptose parent containers. cont = self._container c = self.count() if c > 1: return - if self.count() == 1: ## if there is one item, give it to the parent container (unless this is the top) - if self is self.area.topContainer: + if c == 1: ## if there is one item, give it to the parent container (unless this is the top) + ch = self.widget(0) + if (self.area is not None and self is self.area.topContainer and not isinstance(ch, Container)) or self.container() is None: return - self.container().insert(self.widget(0), 'before', self) + self.container().insert(ch, 'before', self) #print "apoptose:", self self.close() if propagate and cont is not None: cont.apoptose() - + def close(self): - self.area = None - self._container = None self.setParent(None) + if self.area is not None and self.area.topContainer is self: + self.area.topContainer = None + self.containerChanged(None) def childEvent(self, ev): ch = ev.child() @@ -92,7 +99,6 @@ class Container(object): ###Set the stretch values for this container to reflect its contents pass - def stretch(self): """Return the stretch factors for this container""" return self._stretch diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 4493d075..1d946062 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -36,6 +36,7 @@ class Dock(QtGui.QWidget, DockDrop): self.widgetArea.setLayout(self.layout) self.widgetArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.widgets = [] + self._container = None self.currentRow = 0 #self.titlePos = 'top' self.raiseOverlay() @@ -187,9 +188,6 @@ class Dock(QtGui.QWidget, DockDrop): def name(self): return self._name - def container(self): - return self._container - def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1): """ Add a new widget to the interior of this Dock. @@ -202,7 +200,6 @@ class Dock(QtGui.QWidget, DockDrop): self.layout.addWidget(widget, row, col, rowspan, colspan) self.raiseOverlay() - def startDrag(self): self.drag = QtGui.QDrag(self) mime = QtCore.QMimeData() @@ -216,21 +213,30 @@ class Dock(QtGui.QWidget, DockDrop): def float(self): self.area.floatDock(self) + def container(self): + return self._container + def containerChanged(self, c): + if self._container is not None: + # ask old container to close itself if it is no longer needed + self._container.apoptose() #print self.name(), "container changed" self._container = c - if c.type() != 'tab': - self.moveLabel = True - self.label.setDim(False) + if c is None: + self.area = None else: - self.moveLabel = False - - self.setOrientation(force=True) - + self.area = c.area + if c.type() != 'tab': + self.moveLabel = True + self.label.setDim(False) + else: + self.moveLabel = False + + self.setOrientation(force=True) + def raiseDock(self): """If this Dock is stacked underneath others, raise it to the top.""" self.container().raiseDock(self) - def close(self): """Remove this dock from the DockArea it lives inside.""" diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index ffe75b61..560495ce 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -61,6 +61,8 @@ class DockArea(Container, QtGui.QWidget, DockDrop): if isinstance(relativeTo, basestring): relativeTo = self.docks[relativeTo] container = self.getContainer(relativeTo) + if container is None: + raise TypeError("Dock %s is not contained in a DockArea; cannot add another dock relative to it." % relativeTo) neighbor = relativeTo ## what container type do we need? @@ -98,7 +100,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop): #print "request insert", dock, insertPos, neighbor old = dock.container() container.insert(dock, insertPos, neighbor) - dock.area = self self.docks[dock.name()] = dock if old is not None: old.apoptose() @@ -142,23 +143,19 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def insert(self, new, pos=None, neighbor=None): if self.topContainer is not None: + # Adding new top-level container; addContainer() should + # take care of giving the old top container a new home. self.topContainer.containerChanged(None) self.layout.addWidget(new) + new.containerChanged(self) self.topContainer = new - #print self, "set top:", new - new._container = self self.raiseOverlay() - #print "Insert top:", new def count(self): if self.topContainer is None: return 0 return 1 - - #def paintEvent(self, ev): - #self.drawDockOverlay() - def resizeEvent(self, ev): self.resizeOverlay(self.size()) @@ -180,7 +177,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop): area.win.resize(dock.size()) area.moveDock(dock, 'top', None) - def removeTempArea(self, area): self.tempAreas.remove(area) #print "close window", area.window() @@ -212,14 +208,16 @@ class DockArea(Container, QtGui.QWidget, DockDrop): childs.append(self.childState(obj.widget(i))) return (obj.type(), childs, obj.saveState()) - - def restoreState(self, state): + def restoreState(self, state, missing='error'): """ Restore Dock configuration as generated by saveState. - Note that this function does not create any Docks--it will only + This function does not create any Docks--it will only restore the arrangement of an existing set of Docks. + By default, docks that are described in *state* but do not exist + in the dock area will cause an exception to be raised. This behavior + can be changed by setting *missing* to 'ignore' or 'create'. """ ## 1) make dict of all docks and list of existing containers @@ -229,17 +227,20 @@ class DockArea(Container, QtGui.QWidget, DockDrop): ## 2) create container structure, move docks into new containers if state['main'] is not None: - self.buildFromState(state['main'], docks, self) + self.buildFromState(state['main'], docks, self, missing=missing) ## 3) create floating areas, populate for s in state['float']: a = self.addTempArea() - a.buildFromState(s[0]['main'], docks, a) + a.buildFromState(s[0]['main'], docks, a, missing=missing) a.win.setGeometry(*s[1]) + a.apoptose() # ask temp area to close itself if it is empty - ## 4) Add any remaining docks to the bottom + ## 4) Add any remaining docks to a float for d in docks.values(): - self.moveDock(d, 'below', None) + a = self.addTempArea() + a.addDock(d, 'below') + # self.moveDock(d, 'below', None) #print "\nKill old containers:" ## 5) kill old containers @@ -248,8 +249,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop): for a in oldTemps: a.apoptose() - - def buildFromState(self, state, docks, root, depth=0): + def buildFromState(self, state, docks, root, depth=0, missing='error'): typ, contents, state = state pfx = " " * depth if typ == 'dock': @@ -257,7 +257,15 @@ class DockArea(Container, QtGui.QWidget, DockDrop): obj = docks[contents] del docks[contents] except KeyError: - raise Exception('Cannot restore dock state; no dock with name "%s"' % contents) + if missing == 'error': + raise Exception('Cannot restore dock state; no dock with name "%s"' % contents) + elif missing == 'create': + obj = Dock(name=contents) + elif missing == 'ignore': + return + else: + raise ValueError('"missing" argument must be one of "error", "create", or "ignore".') + else: obj = self.makeContainer(typ) @@ -266,10 +274,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop): if typ != 'dock': for o in contents: - self.buildFromState(o, docks, obj, depth+1) + self.buildFromState(o, docks, obj, depth+1, missing=missing) + # remove this container if possible. (there are valid situations when a restore will + # generate empty containers, such as when using missing='ignore') obj.apoptose(propagate=False) - obj.restoreState(state) ## this has to be done later? - + obj.restoreState(state) ## this has to be done later? def findAll(self, obj=None, c=None, d=None): if obj is None: @@ -295,14 +304,15 @@ class DockArea(Container, QtGui.QWidget, DockDrop): d.update(d2) return (c, d) - def apoptose(self): + def apoptose(self, propagate=True): + # remove top container if possible, close this area if it is temporary. #print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count() - if self.topContainer.count() == 0: + if self.topContainer is None or self.topContainer.count() == 0: self.topContainer = None if self.temporary: self.home.removeTempArea(self) #self.close() - + def clear(self): docks = self.findAll()[1] for dock in docks.values(): @@ -322,12 +332,38 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def dropEvent(self, *args): DockDrop.dropEvent(self, *args) + def printState(self, state=None, name='Main'): + # for debugging + if state is None: + state = self.saveState() + print("=== %s dock area ===" % name) + if state['main'] is None: + print(" (empty)") + else: + self._printAreaState(state['main']) + for i, float in enumerate(state['float']): + self.printState(float[0], name='float %d' % i) -class TempAreaWindow(QtGui.QMainWindow): + def _printAreaState(self, area, indent=0): + if area[0] == 'dock': + print(" " * indent + area[0] + " " + str(area[1:])) + return + else: + print(" " * indent + area[0]) + for ch in area[1]: + self._printAreaState(ch, indent+1) + + + +class TempAreaWindow(QtGui.QWidget): def __init__(self, area, **kwargs): - QtGui.QMainWindow.__init__(self, **kwargs) - self.setCentralWidget(area) + QtGui.QWidget.__init__(self, **kwargs) + self.layout = QtGui.QGridLayout() + self.setLayout(self.layout) + self.layout.setContentsMargins(0, 0, 0, 0) + self.dockarea = area + self.layout.addWidget(area) - def closeEvent(self, *args, **kwargs): - self.centralWidget().clear() - QtGui.QMainWindow.closeEvent(self, *args, **kwargs) + def closeEvent(self, *args): + self.dockarea.clear() + QtGui.QWidget.closeEvent(self, *args) diff --git a/pyqtgraph/dockarea/tests/test_dockarea.py b/pyqtgraph/dockarea/tests/test_dockarea.py new file mode 100644 index 00000000..e6d14790 --- /dev/null +++ b/pyqtgraph/dockarea/tests/test_dockarea.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- + +import pytest +import pyqtgraph as pg +from collections import OrderedDict +pg.mkQApp() + +import pyqtgraph.dockarea as da + +def test_dockarea(): + a = da.DockArea() + d1 = da.Dock("dock 1") + a.addDock(d1, 'left') + + assert a.topContainer is d1.container() + assert d1.container().container() is a + assert d1.area is a + assert a.topContainer.widget(0) is d1 + + d2 = da.Dock("dock 2") + a.addDock(d2, 'right') + + assert a.topContainer is d1.container() + assert a.topContainer is d2.container() + assert d1.container().container() is a + assert d2.container().container() is a + assert d2.area is a + assert a.topContainer.widget(0) is d1 + assert a.topContainer.widget(1) is d2 + + d3 = da.Dock("dock 3") + a.addDock(d3, 'bottom') + + assert a.topContainer is d3.container() + assert d2.container().container() is d3.container() + assert d1.container().container() is d3.container() + assert d1.container().container().container() is a + assert d2.container().container().container() is a + assert d3.container().container() is a + assert d3.area is a + assert d2.area is a + assert a.topContainer.widget(0) is d1.container() + assert a.topContainer.widget(1) is d3 + + d4 = da.Dock("dock 4") + a.addDock(d4, 'below', d3) + + assert d4.container().type() == 'tab' + assert d4.container() is d3.container() + assert d3.container().container() is d2.container().container() + assert d4.area is a + a.printState() + + # layout now looks like: + # vcontainer + # hcontainer + # dock 1 + # dock 2 + # tcontainer + # dock 3 + # dock 4 + + # test save/restore state + state = a.saveState() + a2 = da.DockArea() + # default behavior is to raise exception if docks are missing + with pytest.raises(Exception): + a2.restoreState(state) + + # test restore with ignore missing + a2.restoreState(state, missing='ignore') + assert a2.topContainer is None + + # test restore with auto-create + a2.restoreState(state, missing='create') + assert a2.saveState() == state + a2.printState() + + # double-check that state actually matches the output of saveState() + c1 = a2.topContainer + assert c1.type() == 'vertical' + c2 = c1.widget(0) + c3 = c1.widget(1) + assert c2.type() == 'horizontal' + assert c2.widget(0).name() == 'dock 1' + assert c2.widget(1).name() == 'dock 2' + assert c3.type() == 'tab' + assert c3.widget(0).name() == 'dock 3' + assert c3.widget(1).name() == 'dock 4' + + # test restore with docks already present + a3 = da.DockArea() + a3docks = [] + for i in range(1, 5): + dock = da.Dock('dock %d' % i) + a3docks.append(dock) + a3.addDock(dock, 'right') + a3.restoreState(state) + assert a3.saveState() == state + + # test restore with extra docks present + a3 = da.DockArea() + a3docks = [] + for i in [1, 2, 5, 4, 3]: + dock = da.Dock('dock %d' % i) + a3docks.append(dock) + a3.addDock(dock, 'left') + a3.restoreState(state) + a3.printState() + + + # test a more complex restore + a4 = da.DockArea() + state1 = {'float': [], 'main': + ('horizontal', [ + ('vertical', [ + ('horizontal', [ + ('tab', [ + ('dock', 'dock1', {}), + ('dock', 'dock2', {}), + ('dock', 'dock3', {}), + ('dock', 'dock4', {}) + ], {'index': 1}), + ('vertical', [ + ('dock', 'dock5', {}), + ('horizontal', [ + ('dock', 'dock6', {}), + ('dock', 'dock7', {}) + ], {'sizes': [184, 363]}) + ], {'sizes': [355, 120]}) + ], {'sizes': [9, 552]}) + ], {'sizes': [480]}), + ('dock', 'dock8', {}) + ], {'sizes': [566, 69]}) + } + + state2 = {'float': [], 'main': + ('horizontal', [ + ('vertical', [ + ('horizontal', [ + ('dock', 'dock2', {}), + ('vertical', [ + ('dock', 'dock5', {}), + ('horizontal', [ + ('dock', 'dock6', {}), + ('dock', 'dock7', {}) + ], {'sizes': [492, 485]}) + ], {'sizes': [936, 0]}) + ], {'sizes': [172, 982]}) + ], {'sizes': [941]}), + ('vertical', [ + ('dock', 'dock8', {}), + ('dock', 'dock4', {}), + ('dock', 'dock1', {}) + ], {'sizes': [681, 225, 25]}) + ], {'sizes': [1159, 116]})} + + a4.restoreState(state1, missing='create') + a4.restoreState(state2, missing='ignore') + a4.printState() + + c, d = a4.findAll() + assert d['dock3'].area is not a4 + assert d['dock1'].container() is d['dock4'].container() is d['dock8'].container() + assert d['dock6'].container() is d['dock7'].container() + assert a4 is d['dock2'].area is d['dock2'].container().container().container() + assert a4 is d['dock5'].area is d['dock5'].container().container().container().container() + + # States should be the same with two exceptions: + # dock3 is in a float because it does not appear in state2 + # a superfluous vertical splitter in state2 has been removed + state4 = a4.saveState() + state4['main'][1][0] = state4['main'][1][0][1][0] + assert clean_state(state4['main']) == clean_state(state2['main']) + + +def clean_state(state): + # return state dict with sizes removed + ch = [clean_state(x) for x in state[1]] if isinstance(state[1], list) else state[1] + state = (state[0], ch, {}) + + +if __name__ == '__main__': + test_dockarea() From e8128fa5e284fe65f0c19e403284c147d46b8206 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 20:14:50 -0700 Subject: [PATCH 088/607] Make dockarea.restoreState behavior for extrra docks be configurable --- pyqtgraph/dockarea/DockArea.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index 560495ce..a55d6bb0 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -208,7 +208,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop): childs.append(self.childState(obj.widget(i))) return (obj.type(), childs, obj.saveState()) - def restoreState(self, state, missing='error'): + def restoreState(self, state, missing='error', extra='bottom'): """ Restore Dock configuration as generated by saveState. @@ -218,6 +218,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop): By default, docks that are described in *state* but do not exist in the dock area will cause an exception to be raised. This behavior can be changed by setting *missing* to 'ignore' or 'create'. + + Extra docks that are in the dockarea but that are not mentioned in + *state* will be added to the bottom of the dockarea, unless otherwise + specified by the *extra* argument. """ ## 1) make dict of all docks and list of existing containers @@ -238,9 +242,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop): ## 4) Add any remaining docks to a float for d in docks.values(): - a = self.addTempArea() - a.addDock(d, 'below') - # self.moveDock(d, 'below', None) + if extra == 'float': + a = self.addTempArea() + a.addDock(d, 'below') + else: + self.moveDock(d, extra, None) #print "\nKill old containers:" ## 5) kill old containers From 715c3a008566650aa2a30b0712b7eff8852ffae9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 20:29:19 -0700 Subject: [PATCH 089/607] Minor changes to Transform3D - allow more types to be passed through map() and add some sanity checks --- pyqtgraph/Transform3D.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Transform3D.py b/pyqtgraph/Transform3D.py index 43b12de3..56283351 100644 --- a/pyqtgraph/Transform3D.py +++ b/pyqtgraph/Transform3D.py @@ -1,13 +1,19 @@ # -*- coding: utf-8 -*- from .Qt import QtCore, QtGui from . import functions as fn +from .Vector import Vector import numpy as np + class Transform3D(QtGui.QMatrix4x4): """ Extension of QMatrix4x4 with some helpful methods added. """ def __init__(self, *args): + if len(args) == 1 and isinstance(args[0], (list, tuple, np.ndarray)): + args = [x for y in args[0] for x in y] + if len(args) != 16: + raise TypeError("Single argument to Transform3D must have 16 elements.") QtGui.QMatrix4x4.__init__(self, *args) def matrix(self, nd=3): @@ -25,8 +31,15 @@ class Transform3D(QtGui.QMatrix4x4): """ Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates """ - if isinstance(obj, np.ndarray) and obj.ndim >= 2 and obj.shape[0] in (2,3): - return fn.transformCoordinates(self, obj) + if isinstance(obj, np.ndarray) and obj.shape[0] in (2,3): + if obj.ndim >= 2: + return fn.transformCoordinates(self, obj) + elif obj.ndim == 1: + v = QtGui.QMatrix4x4.map(self, Vector(obj)) + return np.array([v.x(), v.y(), v.z()])[:obj.shape[0]] + elif isinstance(obj, (list, tuple)): + v = QtGui.QMatrix4x4.map(self, Vector(obj)) + return type(obj)([v.x(), v.y(), v.z()])[:len(obj)] else: return QtGui.QMatrix4x4.map(self, obj) From 73d857750a0e310d17011e5ee116221241d1f498 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 22:04:24 -0700 Subject: [PATCH 090/607] Add check for EINTR during example testing; this should help avoid sporadic test failures on travis --- examples/utils.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/examples/utils.py b/examples/utils.py index cbdf69c6..88adc9c9 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -3,6 +3,7 @@ import subprocess import time import os import sys +import errno from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.python2_3 import basestring @@ -143,7 +144,14 @@ except: output = '' fail = False while True: - c = process.stdout.read(1).decode() + try: + c = process.stdout.read(1).decode() + except IOError as err: + if err.errno == errno.EINTR: + # Interrupted system call; just try again. + c = '' + else: + raise output += c #sys.stdout.write(c) #sys.stdout.flush() From 3dbbc7e53142cd9360c6651ef15acf142b35770f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 23:05:46 -0700 Subject: [PATCH 091/607] Fix unit test following previous commit --- pyqtgraph/dockarea/tests/test_dockarea.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/dockarea/tests/test_dockarea.py b/pyqtgraph/dockarea/tests/test_dockarea.py index e6d14790..a26646bc 100644 --- a/pyqtgraph/dockarea/tests/test_dockarea.py +++ b/pyqtgraph/dockarea/tests/test_dockarea.py @@ -156,10 +156,15 @@ def test_dockarea(): ], {'sizes': [1159, 116]})} a4.restoreState(state1, missing='create') - a4.restoreState(state2, missing='ignore') + # dock3 not mentioned in restored state; stays in dockarea by default + c, d = a4.findAll() + assert d['dock3'].area is a4 + + a4.restoreState(state2, missing='ignore', extra='float') a4.printState() c, d = a4.findAll() + # dock3 not mentioned in restored state; goes to float due to `extra` argument assert d['dock3'].area is not a4 assert d['dock1'].container() is d['dock4'].container() is d['dock8'].container() assert d['dock6'].container() is d['dock7'].container() From 30997d999d5098cf21d7a149e75e97ae4f3e98de Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sun, 3 Sep 2017 23:18:17 -0700 Subject: [PATCH 092/607] Fix unit test for python 2.6 --- pyqtgraph/dockarea/tests/test_dockarea.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/dockarea/tests/test_dockarea.py b/pyqtgraph/dockarea/tests/test_dockarea.py index a26646bc..9575c298 100644 --- a/pyqtgraph/dockarea/tests/test_dockarea.py +++ b/pyqtgraph/dockarea/tests/test_dockarea.py @@ -2,7 +2,7 @@ import pytest import pyqtgraph as pg -from collections import OrderedDict +from pyqtgraph.ordereddict import OrderedDict pg.mkQApp() import pyqtgraph.dockarea as da From 4b188c73b0515d6a391d5b5f2c9ae4bea526aa5b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:09:35 -0700 Subject: [PATCH 093/607] Add disconnect() and SignalBlock - make it possible to retrieve previous versions of reloaded objects (needed by disconnect) --- pyqtgraph/functions.py | 46 ++++++++++++++++++++++++++++++++++++++++-- pyqtgraph/reload.py | 44 +++++++++++++++++++++++++++++++++------- 2 files changed, 81 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 1aed6ace..4b4231c8 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -14,8 +14,8 @@ import sys, struct from .python2_3 import asUnicode, basestring from .Qt import QtGui, QtCore, USE_PYSIDE from . import getConfigOption, setConfigOptions -from . import debug - +from . import debug, reload +from .reload import getPreviousVersion Colors = { @@ -2412,3 +2412,45 @@ def toposort(deps, nodes=None, seen=None, stack=None, depth=0): sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1)) sorted.append(n) return sorted + + +def disconnect(signal, slot): + """Disconnect a Qt signal from a slot. + + This method augments Qt's Signal.disconnect(): + + * Return bool indicating whether disconnection was successful, rather than + raising an exception + * Attempt to disconnect prior versions of the slot when using pg.reload + """ + while True: + try: + signal.disconnect(slot) + return True + except TypeError, RuntimeError: + slot = reload.getPreviousVersion(slot) + if slot is None: + return False + + +class SignalBlock(object): + """Class used to temporarily block a Qt signal connection:: + + with SignalBlock(signal, slot): + # do something that emits a signal; it will + # not be delivered to slot + """ + def __init__(self, signal, slot): + self.signal = signal + self.slot = slot + + def __enter__(self): + self.reconnect = disconnect(self.signal, self.slot) + return self + + def __exit__(self, *args): + if self.reconnect: + self.signal.connect(self.slot) + + + diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index ccf83913..5aa2ed68 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -22,13 +22,14 @@ Does NOT: """ -import inspect, os, sys, gc, traceback +import inspect, os, sys, gc, traceback, types try: import __builtin__ as builtins except ImportError: import builtins from .debug import printExc + def reloadAll(prefix=None, debug=False): """Automatically reload everything whose __file__ begins with prefix. - Skips reload if the file has not been updated (if .pyc is newer than .py) @@ -97,7 +98,9 @@ def reload(module, debug=False, lists=False, dicts=False): if debug: print(" Updating class %s.%s (0x%x -> 0x%x)" % (module.__name__, k, id(old), id(new))) updateClass(old, new, debug) - + # don't put this inside updateClass because it is reentrant. + new.__previous_reload_version__ = old + elif inspect.isfunction(old): depth = updateFunction(old, new, debug) if debug: @@ -152,7 +155,6 @@ def updateFunction(old, new, debug, depth=0, visited=None): ## 1) find all instances of the old class and set instance.__class__ to the new class ## 2) update all old class methods to use code from the new class methods def updateClass(old, new, debug): - ## Track town all instances and subclasses of old refs = gc.get_referrers(old) for ref in refs: @@ -174,13 +176,20 @@ def updateClass(old, new, debug): ## This seems to work. Is there any reason not to? ## Note that every time we reload, the class hierarchy becomes more complex. ## (and I presume this may slow things down?) - ref.__bases__ = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] + newBases = ref.__bases__[:ind] + (new,old) + ref.__bases__[ind+1:] + try: + ref.__bases__ = newBases + except TypeError: + print(" Error setting bases for class %s" % ref) + print(" old bases: %s" % repr(ref.__bases__)) + print(" new bases: %s" % repr(newBases)) + raise if debug: print(" Changed superclass for %s" % safeStr(ref)) #else: #if debug: #print " Ignoring reference", type(ref) - except: + except Exception: print("Error updating reference (%s) for class change (%s -> %s)" % (safeStr(ref), safeStr(old), safeStr(new))) raise @@ -199,6 +208,8 @@ def updateClass(old, new, debug): if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__: depth = updateFunction(oa.__func__, na.__func__, debug) + if not hasattr(na.__func__, '__previous_reload_method__'): + na.__func__.__previous_reload_method__ = oa # important for managing signal connection #oa.im_class = new ## bind old method to new class ## not allowed if debug: extra = "" @@ -208,6 +219,8 @@ def updateClass(old, new, debug): ## And copy in new functions that didn't exist previously for attr in dir(new): + if attr == '__previous_reload_version__': + continue if not hasattr(old, attr): if debug: print(" Adding missing attribute %s" % attr) @@ -223,14 +236,31 @@ def updateClass(old, new, debug): def safeStr(obj): try: s = str(obj) - except: + except Exception: try: s = repr(obj) - except: + except Exception: s = "" % (safeStr(type(obj)), id(obj)) return s +def getPreviousVersion(obj): + """Return the previous version of *obj*, or None if this object has not + been reloaded. + """ + if isinstance(obj, type) or inspect.isfunction(obj): + return getattr(obj, '__previous_reload_version__', None) + elif inspect.ismethod(obj): + if obj.im_self is None: + # unbound method + return getattr(obj.__func__, '__previous_reload_method__', None) + else: + oldmethod = getattr(obj.__func__, '__previous_reload_method__', None) + if oldmethod is None: + return None + self = obj.im_self + cls = oldmethod.im_class + return types.MethodType(oldmethod.__func__, self, cls) From db890b8ed84a7c1c66b4dd17a0969dc1d741636a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:13:56 -0700 Subject: [PATCH 094/607] Add unit test for reload(); make travis use --assert=reinterp (because assert=rewrite does not work with reload) --- .travis.yml | 3 +- pyqtgraph/tests/test_reload.py | 74 ++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 pyqtgraph/tests/test_reload.py diff --git a/.travis.yml b/.travis.yml index c4a67ac3..1c075090 100644 --- a/.travis.yml +++ b/.travis.yml @@ -143,7 +143,8 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. py.test --cov pyqtgraph -sv; + # NOTE: tests/test_reload.py breaks without --assert=reinterp|plain + PYTHONPATH=. py.test --cov --assert=reinterp pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py new file mode 100644 index 00000000..4b670fb6 --- /dev/null +++ b/pyqtgraph/tests/test_reload.py @@ -0,0 +1,74 @@ +import tempfile, os, sys, shutil, atexit +import pyqtgraph as pg +import pyqtgraph.reload +pgpath = os.path.join(os.path.dirname(pg.__file__), '..') + +# make temporary directory to write module code +path = tempfile.mkdtemp() +sys.path.insert(0, path) +def cleanup(): + shutil.rmtree(path) +atexit.register(cleanup) + + +code = """ +import sys +sys.path.append('{path}') + +import pyqtgraph as pg + +class C(pg.QtCore.QObject): + sig = pg.QtCore.Signal() + def fn(self): + print("{msg}") + +""" + +def test_reload(): + # write a module + mod = os.path.join(path, 'reload_test.py') + open(mod, 'w').write(code.format(path=path, msg="C.fn() Version1")) + + # import the new module + import reload_test + + c = reload_test.C() + c.sig.connect(c.fn) + v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + + + + # write again and reload + open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) + os.remove(mod+'c') + pg.reload.reloadAll(path, debug=True) + v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + + assert c.fn.im_class is v2[0] + oldcfn = pg.reload.getPreviousVersion(c.fn) + assert oldcfn.im_class is v1[0] + assert oldcfn.im_func is v1[2].im_func + assert oldcfn.im_self is c + + + + # write again and reload + open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) + os.remove(mod+'c') + pg.reload.reloadAll(path, debug=True) + v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + + #for i in range(len(old)): + #print id(old[i]), id(new1[i]), id(new2[i]), old[i], new1[i] + + cfn1 = pg.reload.getPreviousVersion(c.fn) + cfn2 = pg.reload.getPreviousVersion(cfn1) + + assert cfn1.im_class is v2[0] + assert cfn1.im_func is v2[2].im_func + assert cfn1.im_self is c + assert cfn2.im_class is v1[0] + assert cfn2.im_func is v1[2].im_func + assert cfn2.im_self is c + c.sig.disconnect(cfn2) + From 4bca2ae87916cc21bac659db1a72d5253dd55155 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:39:26 -0700 Subject: [PATCH 095/607] fix reload test pycache removal Ad a warning about using assert=rewrite --- pyqtgraph/tests/test_reload.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 4b670fb6..3f2195e2 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -24,6 +24,14 @@ class C(pg.QtCore.QObject): """ +def remove_cache(mod): + if os.path.isfile(mod+'c'): + os.remove(mod+'c') + cachedir = os.path.join(os.path.dirname(mod), '__pycache__') + if os.path.isdir(cachedir): + shutil.rmtree(cachedir) + + def test_reload(): # write a module mod = os.path.join(path, 'reload_test.py') @@ -40,12 +48,16 @@ def test_reload(): # write again and reload open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) - os.remove(mod+'c') + remove_cache(mod) pg.reload.reloadAll(path, debug=True) v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) assert c.fn.im_class is v2[0] oldcfn = pg.reload.getPreviousVersion(c.fn) + if oldcfn is None: + # Function did not reload; are we using pytest's assertion rewriting? + raise Exception("Function did not reload. (This can happen when using py.test" + " with assertion rewriting; use --assert=reinterp or --assert=plain)") assert oldcfn.im_class is v1[0] assert oldcfn.im_func is v1[2].im_func assert oldcfn.im_self is c @@ -54,7 +66,7 @@ def test_reload(): # write again and reload open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) - os.remove(mod+'c') + remove_cache(mod) pg.reload.reloadAll(path, debug=True) v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) From 1bd97c67e29c1c642bc9a41e0413f7723d638172 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:42:59 -0700 Subject: [PATCH 096/607] Fix travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1c075090..0a6cf699 100644 --- a/.travis.yml +++ b/.travis.yml @@ -143,7 +143,6 @@ script: # Run unit tests - start_test "unit tests"; - # NOTE: tests/test_reload.py breaks without --assert=reinterp|plain PYTHONPATH=. py.test --cov --assert=reinterp pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" From 7e5b40d265f1e8b0fa2c2b9c96a4d91ef9571599 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 6 Sep 2017 09:58:42 -0700 Subject: [PATCH 097/607] Switch to assert=plain; apparently assert=reinterp is no longer supported --- .travis.yml | 2 +- pyqtgraph/tests/test_reload.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a6cf699..4478f0a8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -143,7 +143,7 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. py.test --cov --assert=reinterp pyqtgraph -sv; + PYTHONPATH=. py.test --cov --assert=plain pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 3f2195e2..7ceed546 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -57,7 +57,7 @@ def test_reload(): if oldcfn is None: # Function did not reload; are we using pytest's assertion rewriting? raise Exception("Function did not reload. (This can happen when using py.test" - " with assertion rewriting; use --assert=reinterp or --assert=plain)") + " with assertion rewriting; use --assert=plain for this test.)") assert oldcfn.im_class is v1[0] assert oldcfn.im_func is v1[2].im_func assert oldcfn.im_self is c From f77d7409ba368edaf63bc9e41e47c1683b94c6e5 Mon Sep 17 00:00:00 2001 From: HashSplat Date: Tue, 12 Sep 2017 17:15:26 -0400 Subject: [PATCH 098/607] Fixed legend size after remove item When the legend removed an item with a large label name the legend would not shrink in width. This fix uses the sample and label minimum width to allow the legend to shrink when an item is removed. --- pyqtgraph/graphicsItems/LegendItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 20d6416e..527b814a 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -110,7 +110,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): #print("-------") for sample, label in self.items: height += max(sample.height(), label.height()) + 3 - width = max(width, sample.width()+label.width()) + width = max(width, (sample.sizeHint(QtCore.Qt.MinimumSize, sample.size()).width() + + label.sizeHint(QtCore.Qt.MinimumSize, label.size()).width())) #print(width, height) #print width, height self.setGeometry(0, 0, width+25, height) From 2a70fd99321855d4ac67886a2f55da992b3a0e68 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 09:05:54 -0700 Subject: [PATCH 099/607] Fix some issues with closing subprocesses --- pyqtgraph/multiprocess/__init__.py | 2 +- pyqtgraph/multiprocess/processes.py | 3 ++- pyqtgraph/multiprocess/remoteproxy.py | 6 +++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/multiprocess/__init__.py b/pyqtgraph/multiprocess/__init__.py index 843b42a3..32a250cb 100644 --- a/pyqtgraph/multiprocess/__init__.py +++ b/pyqtgraph/multiprocess/__init__.py @@ -21,4 +21,4 @@ TODO: from .processes import * from .parallelizer import Parallelize, CanceledError -from .remoteproxy import proxy \ No newline at end of file +from .remoteproxy import proxy, ClosedError, NoResultError diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 02f259e5..11348c23 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -182,7 +182,8 @@ def startEventLoop(name, port, authkey, ppid, debug=False): HANDLER.processRequests() # exception raised when the loop should exit time.sleep(0.01) except ClosedError: - break + HANDLER.debugMsg('Exiting server loop.') + sys.exit(0) class ForkedProcess(RemoteEventHandler): diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 208e17f4..805392e2 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -419,7 +419,7 @@ class RemoteEventHandler(object): if opts is None: opts = {} - assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"' + assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async" (got %r)' % callSync if reqId is None: if callSync != 'off': ## requested return value; use the next available request ID reqId = self.nextRequestId @@ -572,6 +572,10 @@ class RemoteEventHandler(object): self.proxies[ref] = proxy._proxyId def deleteProxy(self, ref): + if self.send is None: + # this can happen during shutdown + return + with self.proxyLock: proxyId = self.proxies.pop(ref) From 16781636bfdabb112fd46a6e579718b035dae7fe Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 09:09:05 -0700 Subject: [PATCH 100/607] API: calling remote methods in 'sync' mode no longer returns future on timeout When calling a function with callSync='sync', the assumption is that we either block until the result arrives or raise an exception if no result arrives. Previously, a timeout woud cause the Future object to be returned instead. --- pyqtgraph/multiprocess/remoteproxy.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index 805392e2..bc02da83 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -466,10 +466,7 @@ class RemoteEventHandler(object): return req if callSync == 'sync': - try: - return req.result() - except NoResultError: - return req + return req.result() def close(self, callSync='off', noCleanup=False, **kwds): try: From 5fb5858802dc7181c0ec40be3749f449f0efbb03 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 09:13:03 -0700 Subject: [PATCH 101/607] Allow better control over sys.path in subprocesses Either add path to pyqtgraph, or copy entire path (anything else still requires manual effort) --- pyqtgraph/multiprocess/bootstrap.py | 19 ++++++++++++++----- pyqtgraph/multiprocess/processes.py | 14 +++++++++++--- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index bb71a703..f9cb0b0e 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -13,16 +13,25 @@ if __name__ == '__main__': #print "key:", ' '.join([str(ord(x)) for x in authkey]) path = opts.pop('path', None) if path is not None: - ## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. - while len(sys.path) > 0: - sys.path.pop() - sys.path.extend(path) + if isinstance(path, str): + # if string, just insert this into the path + sys.path.insert(0, path) + else: + # if list, then replace the entire sys.path + ## modify sys.path in place--no idea who already has a reference to the existing list. + while len(sys.path) > 0: + sys.path.pop() + sys.path.extend(path) if opts.pop('pyside', False): import PySide targetStr = opts.pop('targetStr') - target = pickle.loads(targetStr) ## unpickling the target should import everything we need + try: + target = pickle.loads(targetStr) ## unpickling the target should import everything we need + except: + print("Current sys.path:", sys.path) + raise target(**opts) ## Send all other options to the target function sys.exit(0) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 11348c23..c0cd829a 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -1,4 +1,4 @@ -import subprocess, atexit, os, sys, time, random, socket, signal +import subprocess, atexit, os, sys, time, random, socket, signal, inspect import multiprocessing.connection try: import cPickle as pickle @@ -50,7 +50,9 @@ class Process(RemoteEventHandler): process to process requests from the parent process until it is asked to quit. If you wish to specify a different target, it must be picklable (bound methods are not). - copySysPath If True, copy the contents of sys.path to the remote process + copySysPath If True, copy the contents of sys.path to the remote process. + If False, then only the path required to import pyqtgraph is + added. debug If True, print detailed information about communication with the child process. wrapStdout If True (default on windows) then stdout and stderr from the @@ -82,7 +84,13 @@ class Process(RemoteEventHandler): port = l.address[1] ## start remote process, instruct it to run target function - sysPath = sys.path if copySysPath else None + if copySysPath: + sysPath = sys.path + else: + # what path do we need to make target importable? + mod = inspect.getmodule(target) + modroot = sys.modules[mod.__name__.split('.')[0]] + sysPath = os.path.abspath(os.path.join(os.path.dirname(modroot.__file__), '..')) bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap)) From 182e9397854810f2f50022e07292dba2b36d511c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 09:13:31 -0700 Subject: [PATCH 102/607] Fix color output handling --- pyqtgraph/multiprocess/processes.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index c0cd829a..7560ff70 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -471,21 +471,20 @@ class FileForwarder(threading.Thread): self.start() def run(self): - if self.output == 'stdout': + if self.output == 'stdout' and self.color is not False: while True: line = self.input.readline() with self.lock: cprint.cout(self.color, line, -1) - elif self.output == 'stderr': + elif self.output == 'stderr' and self.color is not False: while True: line = self.input.readline() with self.lock: cprint.cerr(self.color, line, -1) else: + if isinstance(self.output, str): + self.output = getattr(sys, self.output) while True: line = self.input.readline() with self.lock: self.output.write(line) - - - From 05176654731684fa84ceb43413bb85fda1a5b3fd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 20:50:31 -0700 Subject: [PATCH 103/607] Allow console to display any frame stack (even without an exception) --- pyqtgraph/console/Console.py | 76 +++++++++++++++++++++------- pyqtgraph/console/template.ui | 9 ++-- pyqtgraph/console/template_pyqt.py | 9 ++-- pyqtgraph/console/template_pyside.py | 4 +- 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index ed4b7f08..23ae93d5 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -53,6 +53,7 @@ class ConsoleWidget(QtGui.QWidget): self.editor = editor self.multiline = None self.inCmd = False + self.frames = [] # stack frames to access when an item in the stack list is selected self.ui = template.Ui_Form() self.ui.setupUi(self) @@ -133,14 +134,14 @@ class ConsoleWidget(QtGui.QWidget): def globals(self): frame = self.currentFrame() if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): - return self.currentFrame().tb_frame.f_globals + return self.currentFrame().f_globals else: return self.localNamespace def locals(self): frame = self.currentFrame() if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): - return self.currentFrame().tb_frame.f_locals + return self.currentFrame().f_locals else: return self.localNamespace @@ -149,10 +150,7 @@ class ConsoleWidget(QtGui.QWidget): if self.currentTraceback is None: return None index = self.ui.exceptionStackList.currentRow() - tb = self.currentTraceback - for i in range(index): - tb = tb.tb_next - return tb + return self.frames[index] def execSingle(self, cmd): try: @@ -171,7 +169,6 @@ class ConsoleWidget(QtGui.QWidget): except: self.displayException() - def execMulti(self, nextLine): #self.stdout.write(nextLine+"\n") if nextLine.strip() != '': @@ -202,6 +199,10 @@ class ConsoleWidget(QtGui.QWidget): self.multiline = None def write(self, strn, html=False): + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if not isGuiThread: + self.stdout.write(strn) + return self.output.moveCursor(QtGui.QTextCursor.End) if html: self.output.textCursor().insertHtml(strn) @@ -293,14 +294,6 @@ class ConsoleWidget(QtGui.QWidget): fileName = tb.tb_frame.f_code.co_filename subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True) - - #def allExceptionsHandler(self, *args): - #self.exceptionHandler(*args) - - #def nextExceptionHandler(self, *args): - #self.ui.catchNextExceptionBtn.setChecked(False) - #self.exceptionHandler(*args) - def updateSysTrace(self): ## Install or uninstall sys.settrace handler @@ -319,7 +312,7 @@ class ConsoleWidget(QtGui.QWidget): else: sys.settrace(self.systrace) - def exceptionHandler(self, excType, exc, tb): + def exceptionHandler(self, excType, exc, tb, systrace=False): if self.ui.catchNextExceptionBtn.isChecked(): self.ui.catchNextExceptionBtn.setChecked(False) elif not self.ui.catchAllExceptionsBtn.isChecked(): @@ -330,13 +323,60 @@ class ConsoleWidget(QtGui.QWidget): excMessage = ''.join(traceback.format_exception_only(excType, exc)) self.ui.exceptionInfoLabel.setText(excMessage) + + if systrace: + # exceptions caught using systrace don't need the usual + # call stack + traceback handling + self.setStack(sys._getframe().f_back.f_back) + else: + self.setStack(frame=sys._getframe().f_back, tb=tb) + + def setStack(self, frame=None, tb=None): + """Display a call stack and exception traceback. + + This allows the user to probe the contents of any frame in the given stack. + + *frame* may either be a Frame instance or None, in which case the current + frame is retrieved from ``sys._getframe()``. + + If *tb* is provided then the frames in the traceback will be appended to + the end of the stack list. If *tb* is None, then sys.exc_info() will + be checked instead. + """ + if frame is None: + frame = sys._getframe().f_back + + if tb is None: + tb = sys.exc_info()[2] + self.ui.exceptionStackList.clear() + self.frames = [] + + # Build stack up to this point + for index, line in enumerate(traceback.extract_stack(frame)): + self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) + while frame is not None: + self.frames.insert(0, frame) + frame = frame.f_back + + if tb is None: + return + + self.ui.exceptionStackList.addItem('-- exception caught here: --') + item = self.ui.exceptionStackList.item(self.ui.exceptionStackList.count()-1) + item.setBackground(QtGui.QBrush(QtGui.QColor(200, 200, 200))) + self.frames.append(None) + + # And finish the rest of the stack up to the exception for index, line in enumerate(traceback.extract_tb(tb)): self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) - + while tb is not None: + self.frames.append(tb.tb_frame) + tb = tb.tb_next + def systrace(self, frame, event, arg): if event == 'exception' and self.checkException(*arg): - self.exceptionHandler(*arg) + self.exceptionHandler(*arg, systrace=True) return self.systrace def checkException(self, excType, exc, tb): diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui index 1a672c5e..1dd752d9 100644 --- a/pyqtgraph/console/template.ui +++ b/pyqtgraph/console/template.ui @@ -86,7 +86,10 @@ 0 - + + 2 + + 0 @@ -95,7 +98,7 @@ false - Clear Exception + Clear Stack @@ -149,7 +152,7 @@ - Exception Info + Stack Trace diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py index 354fb1d6..e5fc4619 100644 --- a/pyqtgraph/console/template_pyqt.py +++ b/pyqtgraph/console/template_pyqt.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'template.ui' # -# Created: Fri May 02 18:55:28 2014 +# Created: Wed Apr 08 16:28:53 2015 # by: PyQt4 UI code generator 4.10.4 # # WARNING! All changes made in this file will be lost! @@ -68,8 +68,9 @@ class Ui_Form(object): self.exceptionGroup = QtGui.QGroupBox(self.splitter) self.exceptionGroup.setObjectName(_fromUtf8("exceptionGroup")) self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setHorizontalSpacing(2) + self.gridLayout_2.setVerticalSpacing(0) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) self.clearExceptionBtn.setEnabled(False) @@ -116,12 +117,12 @@ class Ui_Form(object): self.historyBtn.setText(_translate("Form", "History..", None)) self.exceptionBtn.setText(_translate("Form", "Exceptions..", None)) self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", None)) - self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None)) + self.clearExceptionBtn.setText(_translate("Form", "Clear Stack", None)) self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None)) self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None)) self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None)) self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None)) - self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None)) + self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace", None)) self.label.setText(_translate("Form", "Filter (regex):", None)) from .CmdInput import CmdInput diff --git a/pyqtgraph/console/template_pyside.py b/pyqtgraph/console/template_pyside.py index 2db8ed95..36065afd 100644 --- a/pyqtgraph/console/template_pyside.py +++ b/pyqtgraph/console/template_pyside.py @@ -100,7 +100,7 @@ class Ui_Form(object): self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8)) self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8)) self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) - self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8)) - self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Exception", None, QtGui.QApplication.UnicodeUTF8)) + self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Stack Trace", None, QtGui.QApplication.UnicodeUTF8)) + self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Stack", None, QtGui.QApplication.UnicodeUTF8)) from .CmdInput import CmdInput From 39d4c82d678b12138ac60144468720d1de06f0dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 20:58:10 -0700 Subject: [PATCH 104/607] Fix stack clearing button --- pyqtgraph/console/Console.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 23ae93d5..72164f33 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -318,7 +318,6 @@ class ConsoleWidget(QtGui.QWidget): elif not self.ui.catchAllExceptionsBtn.isChecked(): return - self.ui.clearExceptionBtn.setEnabled(True) self.currentTraceback = tb excMessage = ''.join(traceback.format_exception_only(excType, exc)) @@ -343,6 +342,8 @@ class ConsoleWidget(QtGui.QWidget): the end of the stack list. If *tb* is None, then sys.exc_info() will be checked instead. """ + self.ui.clearExceptionBtn.setEnabled(True) + if frame is None: frame = sys._getframe().f_back From e88e3a4232e4b07ef50e7efa9faae0236f4f8930 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:01:38 -0700 Subject: [PATCH 105/607] code cleanup --- pyqtgraph/canvas/Canvas.py | 135 +-------------------------------- pyqtgraph/canvas/CanvasItem.py | 56 +------------- 2 files changed, 3 insertions(+), 188 deletions(-) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index 5b5ce2f7..a9f1d918 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -32,7 +32,6 @@ class Canvas(QtGui.QWidget): QtGui.QWidget.__init__(self, parent) self.ui = Ui_Form() self.ui.setupUi(self) - #self.view = self.ui.view self.view = ViewBox() self.ui.view.setCentralItem(self.view) self.itemList = self.ui.itemList @@ -49,9 +48,7 @@ class Canvas(QtGui.QWidget): self.redirect = None ## which canvas to redirect items to self.items = [] - #self.view.enableMouse() self.view.setAspectLocked(True) - #self.view.invertY() grid = GridItem() self.grid = CanvasItem(grid, name='Grid', movable=False) @@ -69,8 +66,6 @@ class Canvas(QtGui.QWidget): self.ui.itemList.sigItemMoved.connect(self.treeItemMoved) self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected) self.ui.autoRangeBtn.clicked.connect(self.autoRange) - #self.ui.storeSvgBtn.clicked.connect(self.storeSvg) - #self.ui.storePngBtn.clicked.connect(self.storePng) self.ui.redirectCheck.toggled.connect(self.updateRedirect) self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect) self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged) @@ -88,21 +83,11 @@ class Canvas(QtGui.QWidget): self.ui.redirectCombo.setHostName(self.registeredName) self.menu = QtGui.QMenu() - #self.menu.setTitle("Image") remAct = QtGui.QAction("Remove item", self.menu) remAct.triggered.connect(self.removeClicked) self.menu.addAction(remAct) self.menu.remAct = remAct self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent - - - #def storeSvg(self): - #from pyqtgraph.GraphicsScene.exportDialog import ExportDialog - #ex = ExportDialog(self.ui.view) - #ex.show() - - #def storePng(self): - #self.ui.view.writeImage() def splitterMoved(self): self.resizeEvent() @@ -135,7 +120,6 @@ class Canvas(QtGui.QWidget): s = min(self.width(), max(100, min(200, self.width()*0.25))) s2 = self.width()-s self.ui.splitter.setSizes([s2, s]) - def updateRedirect(self, *args): ### Decide whether/where to redirect items and make it so @@ -154,7 +138,6 @@ class Canvas(QtGui.QWidget): self.reclaimItems() else: self.redirectItems(redirect) - def redirectItems(self, canvas): for i in self.items: @@ -171,12 +154,9 @@ class Canvas(QtGui.QWidget): else: parent.removeChild(li) canvas.addItem(i) - def reclaimItems(self): items = self.items - #self.items = {'Grid': items['Grid']} - #del items['Grid'] self.items = [self.grid] items.remove(self.grid) @@ -185,9 +165,6 @@ class Canvas(QtGui.QWidget): self.addItem(i) def treeItemChanged(self, item, col): - #gi = self.items.get(item.name, None) - #if gi is None: - #return try: citem = item.canvasItem() except AttributeError: @@ -203,25 +180,16 @@ class Canvas(QtGui.QWidget): def treeItemSelected(self): sel = self.selectedItems() - #sel = [] - #for listItem in self.itemList.selectedItems(): - #if hasattr(listItem, 'canvasItem') and listItem.canvasItem is not None: - #sel.append(listItem.canvasItem) - #sel = [self.items[item.name] for item in sel] - + if len(sel) == 0: - #self.selectWidget.hide() return multi = len(sel) > 1 for i in self.items: - #i.ctrlWidget().hide() ## updated the selected state of every item i.selectionChanged(i in sel, multi) if len(sel)==1: - #item = sel[0] - #item.ctrlWidget().show() self.multiSelectBox.hide() self.ui.mirrorSelectionBtn.hide() self.ui.reflectSelectionBtn.hide() @@ -229,14 +197,6 @@ class Canvas(QtGui.QWidget): elif len(sel) > 1: self.showMultiSelectBox() - #if item.isMovable(): - #self.selectBox.setPos(item.item.pos()) - #self.selectBox.setSize(item.item.sceneBoundingRect().size()) - #self.selectBox.show() - #else: - #self.selectBox.hide() - - #self.emit(QtCore.SIGNAL('itemSelected'), self, item) self.sigSelectionChanged.emit(self, sel) def selectedItems(self): @@ -245,19 +205,9 @@ class Canvas(QtGui.QWidget): """ return [item.canvasItem() for item in self.itemList.selectedItems() if item.canvasItem() is not None] - #def selectedItem(self): - #sel = self.itemList.selectedItems() - #if sel is None or len(sel) < 1: - #return - #return self.items.get(sel[0].name, None) - def selectItem(self, item): li = item.listItem - #li = self.getListItem(item.name()) - #print "select", li self.itemList.setCurrentItem(li) - - def showMultiSelectBox(self): ## Get list of selected canvas items @@ -281,7 +231,6 @@ class Canvas(QtGui.QWidget): self.ui.mirrorSelectionBtn.show() self.ui.reflectSelectionBtn.show() self.ui.resetTransformsBtn.show() - #self.multiSelectBoxBase = self.multiSelectBox.getState().copy() def mirrorSelectionClicked(self): for ci in self.selectedItems(): @@ -312,7 +261,6 @@ class Canvas(QtGui.QWidget): ci.setTemporaryTransform(transform) ci.sigTransformChanged.emit(ci) - def addGraphicsItem(self, item, **opts): """Add a new GraphicsItem to the scene at pos. Common options are name, pos, scale, and z @@ -321,13 +269,11 @@ class Canvas(QtGui.QWidget): item._canvasItem = citem self.addItem(citem) return citem - def addGroup(self, name, **kargs): group = GroupCanvasItem(name=name) self.addItem(group, **kargs) return group - def addItem(self, citem): """ @@ -363,7 +309,6 @@ class Canvas(QtGui.QWidget): #name = newname ## find parent and add item to tree - #currentNode = self.itemList.invisibleRootItem() insertLocation = 0 #print "Inserting node:", name @@ -413,11 +358,7 @@ class Canvas(QtGui.QWidget): node.setCheckState(0, QtCore.Qt.Unchecked) node.name = name - #if citem.opts['parent'] != None: - ## insertLocation is incorrect in this case parent.insertChild(insertLocation, node) - #else: - #root.insertChild(insertLocation, node) citem.name = name citem.listItem = node @@ -435,36 +376,6 @@ class Canvas(QtGui.QWidget): if len(self.items) == 2: self.autoRange() - - #for n in name: - #nextnode = None - #for x in range(currentNode.childCount()): - #ch = currentNode.child(x) - #if hasattr(ch, 'name'): ## check Z-value of current item to determine insert location - #zval = ch.canvasItem.zValue() - #if zval > z: - ###print " ->", x - #insertLocation = x+1 - #if n == ch.text(0): - #nextnode = ch - #break - #if nextnode is None: ## If name doesn't exist, create it - #nextnode = QtGui.QTreeWidgetItem([n]) - #nextnode.setFlags((nextnode.flags() | QtCore.Qt.ItemIsUserCheckable) & ~QtCore.Qt.ItemIsDropEnabled) - #nextnode.setCheckState(0, QtCore.Qt.Checked) - ### Add node to correct position in list by Z-value - ###print " ==>", insertLocation - #currentNode.insertChild(insertLocation, nextnode) - - #if n == name[-1]: ## This is the leaf; add some extra properties. - #nextnode.name = name - - #if n == name[0]: ## This is the root; make the item movable - #nextnode.setFlags(nextnode.flags() | QtCore.Qt.ItemIsDragEnabled) - #else: - #nextnode.setFlags(nextnode.flags() & ~QtCore.Qt.ItemIsDragEnabled) - - #currentNode = nextnode return citem def treeItemMoved(self, item, parent, index): @@ -481,31 +392,6 @@ class Canvas(QtGui.QWidget): for i in range(len(siblings)): item = siblings[i] item.setZValue(zvals[i]) - #item = self.itemList.topLevelItem(i) - - ##ci = self.items[item.name] - #ci = item.canvasItem - #if ci is None: - #continue - #if ci.zValue() != zvals[i]: - #ci.setZValue(zvals[i]) - - #if self.itemList.topLevelItemCount() < 2: - #return - #name = item.name - #gi = self.items[name] - #if index == 0: - #next = self.itemList.topLevelItem(1) - #z = self.items[next.name].zValue()+1 - #else: - #prev = self.itemList.topLevelItem(index-1) - #z = self.items[prev.name].zValue()-1 - #gi.setZValue(z) - - - - - def itemVisibilityChanged(self, item): listItem = item.listItem @@ -521,7 +407,6 @@ class Canvas(QtGui.QWidget): if isinstance(item, QtGui.QTreeWidgetItem): item = item.canvasItem() - if isinstance(item, CanvasItem): item.setCanvas(None) listItem = item.listItem @@ -559,15 +444,10 @@ class Canvas(QtGui.QWidget): def getListItem(self, name): return self.items[name] - #def scene(self): - #return self.view.scene() - def itemTransformChanged(self, item): - #self.emit(QtCore.SIGNAL('itemTransformChanged'), self, item) self.sigItemTransformChanged.emit(self, item) def itemTransformChangeFinished(self, item): - #self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item) self.sigItemTransformChangeFinished.emit(self, item) def itemListContextMenuEvent(self, ev): @@ -575,13 +455,13 @@ class Canvas(QtGui.QWidget): self.menu.popup(ev.globalPos()) def removeClicked(self): - #self.removeItem(self.menuItem) for item in self.selectedItems(): self.removeItem(item) self.menuItem = None import gc gc.collect() + class SelectBox(ROI): def __init__(self, scalable=False): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) @@ -593,14 +473,3 @@ class SelectBox(ROI): self.addScaleHandle([0, 0], center, lockAspect=True) self.addRotateHandle([0, 1], center) self.addRotateHandle([1, 0], center) - - - - - - - - - - - diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index a06235b2..bab89e89 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -87,14 +87,12 @@ class CanvasItem(QtCore.QObject): self.alphaSlider.valueChanged.connect(self.alphaChanged) self.alphaSlider.sliderPressed.connect(self.alphaPressed) self.alphaSlider.sliderReleased.connect(self.alphaReleased) - #self.canvas.sigSelectionChanged.connect(self.selectionChanged) self.resetTransformBtn.clicked.connect(self.resetTransformClicked) self.copyBtn.clicked.connect(self.copyClicked) self.pasteBtn.clicked.connect(self.pasteClicked) self.setMovable(self.opts['movable']) ## update gui to reflect this option - if 'transform' in self.opts: self.baseTransform = self.opts['transform'] else: @@ -114,7 +112,6 @@ class CanvasItem(QtCore.QObject): ## every CanvasItem implements its own individual selection box ## so that subclasses are free to make their own. self.selectBox = SelectBox(scalable=self.opts['scalable'], rotatable=self.opts['rotatable']) - #self.canvas.scene().addItem(self.selectBox) self.selectBox.hide() self.selectBox.setZValue(1e6) self.selectBox.sigRegionChanged.connect(self.selectBoxChanged) ## calls selectBoxMoved @@ -129,16 +126,7 @@ class CanvasItem(QtCore.QObject): self.tempTransform = SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. self.userTransform = SRTTransform() ## stores the total transform of the object self.resetUserTransform() - - ## now happens inside resetUserTransform -> selectBoxToItem - # self.selectBoxBase = self.selectBox.getState().copy() - - - #print "Created canvas item", self - #print " base:", self.baseTransform - #print " user:", self.userTransform - #print " temp:", self.tempTransform - #print " bounds:", self.item.sceneBoundingRect() + def setMovable(self, m): self.opts['movable'] = m @@ -239,7 +227,6 @@ class CanvasItem(QtCore.QObject): # s=self.updateTransform() # self.setTranslate(-2*s['pos'][0], -2*s['pos'][1]) # self.selectBoxFromUser() - def hasUserTransform(self): #print self.userRotate, self.userTranslate @@ -255,7 +242,6 @@ class CanvasItem(QtCore.QObject): def isMovable(self): return self.opts['movable'] - def selectBoxMoved(self): """The selection box has moved; get its transformation information and pass to the graphics item""" self.userTransform = self.selectBox.getGlobalTransform(relativeTo=self.selectBoxBase) @@ -290,7 +276,6 @@ class CanvasItem(QtCore.QObject): self.userTransform.setScale(x, y) self.selectBoxFromUser() self.updateTransform() - def setTemporaryTransform(self, transform): self.tempTransform = transform @@ -302,21 +287,6 @@ class CanvasItem(QtCore.QObject): self.resetTemporaryTransform() self.selectBoxFromUser() ## update the selection box to match the new userTransform - #st = self.userTransform.saveState() - - #self.userTransform = self.userTransform * self.tempTransform ## order is important! - - #### matrix multiplication affects the scale factors, need to reset - #if st['scale'][0] < 0 or st['scale'][1] < 0: - #nst = self.userTransform.saveState() - #self.userTransform.setScale([-nst['scale'][0], -nst['scale'][1]]) - - #self.resetTemporaryTransform() - #self.selectBoxFromUser() - #self.selectBoxChangeFinished() - - - def resetTemporaryTransform(self): self.tempTransform = SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere. self.updateTransform() @@ -339,20 +309,13 @@ class CanvasItem(QtCore.QObject): def displayTransform(self, transform): """Updates transform numbers in the ctrl widget.""" - tr = transform.saveState() self.transformGui.translateLabel.setText("Translate: (%f, %f)" %(tr['pos'][0], tr['pos'][1])) self.transformGui.rotateLabel.setText("Rotate: %f degrees" %tr['angle']) self.transformGui.scaleLabel.setText("Scale: (%f, %f)" %(tr['scale'][0], tr['scale'][1])) - #self.transformGui.mirrorImageCheck.setChecked(False) - #if tr['scale'][0] < 0: - # self.transformGui.mirrorImageCheck.setChecked(True) - def resetUserTransform(self): - #self.userRotate = 0 - #self.userTranslate = pg.Point(0,0) self.userTransform.reset() self.updateTransform() @@ -368,8 +331,6 @@ class CanvasItem(QtCore.QObject): def restoreTransform(self, tr): try: - #self.userTranslate = pg.Point(tr['trans']) - #self.userRotate = tr['rot'] self.userTransform = SRTTransform(tr) self.updateTransform() @@ -377,16 +338,11 @@ class CanvasItem(QtCore.QObject): self.sigTransformChanged.emit(self) self.sigTransformChangeFinished.emit(self) except: - #self.userTranslate = pg.Point([0,0]) - #self.userRotate = 0 self.userTransform = SRTTransform() debug.printExc("Failed to load transform:") - #print "set transform", self, self.userTranslate def saveTransform(self): """Return a dict containing the current user transform""" - #print "save transform", self, self.userTranslate - #return {'trans': list(self.userTranslate), 'rot': self.userRotate} return self.userTransform.saveState() def selectBoxFromUser(self): @@ -404,7 +360,6 @@ class CanvasItem(QtCore.QObject): #self.selectBox.setAngle(self.userRotate) #self.selectBox.setPos([x2, y2]) self.selectBox.blockSignals(False) - def selectBoxToItem(self): """Move/scale the selection box so it fits the item's bounding rect. (assumes item is not rotated)""" @@ -424,11 +379,6 @@ class CanvasItem(QtCore.QObject): self.opts['z'] = z if z is not None: self._graphicsItem.setZValue(z) - - #def selectionChanged(self, canvas, items): - #self.selected = len(items) == 1 and (items[0] is self) - #self.showSelectBox() - def selectionChanged(self, sel, multi): """ @@ -456,16 +406,12 @@ class CanvasItem(QtCore.QObject): def hideSelectBox(self): self.selectBox.hide() - def selectBoxChanged(self): self.selectBoxMoved() - #self.updateTransform(self.selectBox) - #self.emit(QtCore.SIGNAL('transformChanged'), self) self.sigTransformChanged.emit(self) def selectBoxChangeFinished(self): - #self.emit(QtCore.SIGNAL('transformChangeFinished'), self) self.sigTransformChangeFinished.emit(self) def alphaPressed(self): From ee117fd957c1f001a089c1dc6733ce35fb4dff69 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:02:58 -0700 Subject: [PATCH 106/607] Give CanvasItem alpha/setAlpha methods --- pyqtgraph/canvas/CanvasItem.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index bab89e89..eb6d0a61 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import numpy as np from ..Qt import QtGui, QtCore, QtSvg, QT_LIB from ..graphicsItems.ROI import ROI from .. import SRTTransform, ItemGroup @@ -239,6 +240,12 @@ class CanvasItem(QtCore.QObject): alpha = val / 1023. self._graphicsItem.setOpacity(alpha) + def setAlpha(self, alpha): + self.alphaSlider.setValue(int(np.clip(alpha * 1023, 0, 1023))) + + def alpha(self): + return self.alphaSlider.value() / 1023. + def isMovable(self): return self.opts['movable'] From 65b5b6a7bc40a88e6eea75cbe488406f5861c1db Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:04:04 -0700 Subject: [PATCH 107/607] Add CanvasItem.saveState/restoreState --- pyqtgraph/canvas/Canvas.py | 9 +++++---- pyqtgraph/canvas/CanvasItem.py | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index a9f1d918..2ebc2ba1 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -19,9 +19,11 @@ elif QT_LIB == 'PyQt5': import numpy as np from .. import debug import weakref +import gc from .CanvasManager import CanvasManager from .CanvasItem import CanvasItem, GroupCanvasItem + class Canvas(QtGui.QWidget): sigSelectionChanged = QtCore.Signal(object, object) @@ -417,25 +419,24 @@ class Canvas(QtGui.QWidget): ctrl = item.ctrlWidget() ctrl.hide() self.ui.ctrlLayout.removeWidget(ctrl) + ctrl.setParent(None) else: if hasattr(item, '_canvasItem'): self.removeItem(item._canvasItem) else: self.view.removeItem(item) - - ## disconnect signals, remove from list, etc.. + + gc.collect() def clear(self): while len(self.items) > 0: self.removeItem(self.items[0]) - def addToScene(self, item): self.view.addItem(item) def removeFromScene(self, item): self.view.removeItem(item) - def listItems(self): """Return a dictionary of name:item pairs""" diff --git a/pyqtgraph/canvas/CanvasItem.py b/pyqtgraph/canvas/CanvasItem.py index eb6d0a61..c406256c 100644 --- a/pyqtgraph/canvas/CanvasItem.py +++ b/pyqtgraph/canvas/CanvasItem.py @@ -453,6 +453,25 @@ class CanvasItem(QtCore.QObject): def isVisible(self): return self.opts['visible'] + def saveState(self): + return { + 'type': self.__class__.__name__, + 'name': self.name, + 'visible': self.isVisible(), + 'alpha': self.alpha(), + 'userTransform': self.saveTransform(), + 'z': self.zValue(), + 'scalable': self.opts['scalable'], + 'rotatable': self.opts['rotatable'], + 'movable': self.opts['movable'], + } + + def restoreState(self, state): + self.setVisible(state['visible']) + self.setAlpha(state['alpha']) + self.restoreTransform(state['userTransform']) + self.setZValue(state['z']) + class GroupCanvasItem(CanvasItem): """ From d8ffc21446d618c4464a10a738b7d4d7762bd58d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:04:43 -0700 Subject: [PATCH 108/607] Refactor canvas ui to make it easier to embed / extend --- pyqtgraph/canvas/CanvasTemplate.ui | 176 +++++++++++----------- pyqtgraph/canvas/CanvasTemplate_pyqt.py | 63 ++++---- pyqtgraph/canvas/CanvasTemplate_pyqt5.py | 66 ++++---- pyqtgraph/canvas/CanvasTemplate_pyside.py | 66 ++++---- 4 files changed, 198 insertions(+), 173 deletions(-) diff --git a/pyqtgraph/canvas/CanvasTemplate.ui b/pyqtgraph/canvas/CanvasTemplate.ui index b05c11cd..bfdacf38 100644 --- a/pyqtgraph/canvas/CanvasTemplate.ui +++ b/pyqtgraph/canvas/CanvasTemplate.ui @@ -6,14 +6,14 @@ 0 0 - 490 - 414 + 821 + 578 Form - + 0 @@ -26,88 +26,96 @@ Qt::Horizontal - - - - - - - 0 - 1 - - - - Auto Range - - - - - - - 0 - - - - - Check to display all local items in a remote canvas. - - - Redirect - - - - - - - - - - - - - 0 - 100 - - - - true - - - - 1 + + + Qt::Vertical + + + + + + + + 0 + 1 + - - - - - - - 0 - - - - - - - Reset Transforms - - - - - - - Mirror Selection - - - - - - - MirrorXY - - - - + + Auto Range + + + + + + + 0 + + + + + Check to display all local items in a remote canvas. + + + Redirect + + + + + + + + + + + + + 0 + 100 + + + + true + + + + 1 + + + + + + + + Reset Transforms + + + + + + + Mirror Selection + + + + + + + MirrorXY + + + + + + + + + 0 + + + 0 + + + diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/pyqtgraph/canvas/CanvasTemplate_pyqt.py index b65ef465..3569c8e7 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'CanvasTemplate.ui' # # Created by: PyQt4 UI code generator 4.11.4 # @@ -25,39 +25,42 @@ except AttributeError: class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(490, 414) - self.gridLayout = QtGui.QGridLayout(Form) - self.gridLayout.setMargin(0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + Form.resize(821, 578) + self.gridLayout_2 = QtGui.QGridLayout(Form) + self.gridLayout_2.setMargin(0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.splitter = QtGui.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName(_fromUtf8("splitter")) self.view = GraphicsView(self.splitter) self.view.setObjectName(_fromUtf8("view")) - self.layoutWidget = QtGui.QWidget(self.splitter) - self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) - self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) - self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) + self.vsplitter = QtGui.QSplitter(self.splitter) + self.vsplitter.setOrientation(QtCore.Qt.Vertical) + self.vsplitter.setObjectName(_fromUtf8("vsplitter")) + self.canvasCtrlWidget = QtGui.QWidget(self.vsplitter) + self.canvasCtrlWidget.setObjectName(_fromUtf8("canvasCtrlWidget")) + self.gridLayout = QtGui.QGridLayout(self.canvasCtrlWidget) + self.gridLayout.setObjectName(_fromUtf8("gridLayout")) + self.autoRangeBtn = QtGui.QPushButton(self.canvasCtrlWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn")) - self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) - self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) + self.redirectCheck = QtGui.QCheckBox(self.canvasCtrlWidget) self.redirectCheck.setObjectName(_fromUtf8("redirectCheck")) self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo = CanvasCombo(self.canvasCtrlWidget) self.redirectCombo.setObjectName(_fromUtf8("redirectCombo")) self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) + self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2) + self.itemList = TreeWidget(self.canvasCtrlWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(100) @@ -66,21 +69,23 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName(_fromUtf8("itemList")) self.itemList.headerItem().setText(0, _fromUtf8("1")) - self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) - self.ctrlLayout = QtGui.QGridLayout() + self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2) + self.resetTransformsBtn = QtGui.QPushButton(self.canvasCtrlWidget) + self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) + self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2) + self.mirrorSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget) + self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) + self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget) + self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn")) + self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.canvasItemCtrl = QtGui.QWidget(self.vsplitter) + self.canvasItemCtrl.setObjectName(_fromUtf8("canvasItemCtrl")) + self.ctrlLayout = QtGui.QGridLayout(self.canvasItemCtrl) + self.ctrlLayout.setMargin(0) self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout")) - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) - self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) - self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) - self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) - self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) - self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) - self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn")) - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) diff --git a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py index 20f5e339..03310d39 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyqt5.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyqt5.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'CanvasTemplate.ui' # -# Created by: PyQt5 UI code generator 5.5.1 +# Created by: PyQt5 UI code generator 5.7.1 # # WARNING! All changes made in this file will be lost! @@ -11,39 +11,43 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(490, 414) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") + Form.resize(821, 578) + self.gridLayout_2 = QtWidgets.QGridLayout(Form) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") self.splitter = QtWidgets.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName("splitter") self.view = GraphicsView(self.splitter) self.view.setObjectName("view") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout_2.setObjectName("gridLayout_2") - self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) + self.vsplitter = QtWidgets.QSplitter(self.splitter) + self.vsplitter.setOrientation(QtCore.Qt.Vertical) + self.vsplitter.setObjectName("vsplitter") + self.canvasCtrlWidget = QtWidgets.QWidget(self.vsplitter) + self.canvasCtrlWidget.setObjectName("canvasCtrlWidget") + self.gridLayout = QtWidgets.QGridLayout(self.canvasCtrlWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.autoRangeBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") - self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) + self.redirectCheck = QtWidgets.QCheckBox(self.canvasCtrlWidget) self.redirectCheck.setObjectName("redirectCheck") self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo = CanvasCombo(self.canvasCtrlWidget) self.redirectCombo.setObjectName("redirectCombo") self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) + self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2) + self.itemList = TreeWidget(self.canvasCtrlWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(100) @@ -52,21 +56,23 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName("itemList") self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) - self.ctrlLayout = QtWidgets.QGridLayout() + self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2) + self.resetTransformsBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) + self.resetTransformsBtn.setObjectName("resetTransformsBtn") + self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2) + self.mirrorSelectionBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) + self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") + self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtWidgets.QPushButton(self.canvasCtrlWidget) + self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") + self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.canvasItemCtrl = QtWidgets.QWidget(self.vsplitter) + self.canvasItemCtrl.setObjectName("canvasItemCtrl") + self.ctrlLayout = QtWidgets.QGridLayout(self.canvasItemCtrl) + self.ctrlLayout.setContentsMargins(0, 0, 0, 0) self.ctrlLayout.setSpacing(0) self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) - self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) - self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) - self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside.py b/pyqtgraph/canvas/CanvasTemplate_pyside.py index b0e05a07..570d5bd1 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' +# Form implementation generated from reading ui file 'CanvasTemplate.ui' # -# Created: Wed Nov 9 18:02:00 2016 +# Created: Fri Mar 24 16:09:39 2017 # by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! @@ -12,40 +12,43 @@ from PySide import QtCore, QtGui class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(490, 414) - self.gridLayout = QtGui.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") + Form.resize(821, 578) + self.gridLayout_2 = QtGui.QGridLayout(Form) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") self.splitter = QtGui.QSplitter(Form) self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setObjectName("splitter") self.view = GraphicsView(self.splitter) self.view.setObjectName("view") - self.layoutWidget = QtGui.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) + self.vsplitter = QtGui.QSplitter(self.splitter) + self.vsplitter.setOrientation(QtCore.Qt.Vertical) + self.vsplitter.setObjectName("vsplitter") + self.canvasCtrlWidget = QtGui.QWidget(self.vsplitter) + self.canvasCtrlWidget.setObjectName("canvasCtrlWidget") + self.gridLayout = QtGui.QGridLayout(self.canvasCtrlWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.autoRangeBtn = QtGui.QPushButton(self.canvasCtrlWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2) self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout.setSpacing(0) self.horizontalLayout.setObjectName("horizontalLayout") - self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) + self.redirectCheck = QtGui.QCheckBox(self.canvasCtrlWidget) self.redirectCheck.setObjectName("redirectCheck") self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo = CanvasCombo(self.canvasCtrlWidget) self.redirectCombo.setObjectName("redirectCombo") self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) + self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2) + self.itemList = TreeWidget(self.canvasCtrlWidget) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(100) @@ -54,21 +57,24 @@ class Ui_Form(object): self.itemList.setHeaderHidden(True) self.itemList.setObjectName("itemList") self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) - self.ctrlLayout = QtGui.QGridLayout() - self.ctrlLayout.setSpacing(0) - self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget) + self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2) + self.resetTransformsBtn = QtGui.QPushButton(self.canvasCtrlWidget) self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) - self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2) + self.mirrorSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget) self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) - self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) + self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1) + self.reflectSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget) self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1) + self.canvasItemCtrl = QtGui.QWidget(self.vsplitter) + self.canvasItemCtrl.setObjectName("canvasItemCtrl") + self.ctrlLayout = QtGui.QGridLayout(self.canvasItemCtrl) + self.ctrlLayout.setContentsMargins(0, 0, 0, 0) + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setContentsMargins(0, 0, 0, 0) + self.ctrlLayout.setObjectName("ctrlLayout") + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) QtCore.QMetaObject.connectSlotsByName(Form) From 5d6be5796b42f33f5ea91d7082cd51a351db4f22 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:36:34 -0700 Subject: [PATCH 109/607] image export: add option to invert pixel values (but not hues) --- pyqtgraph/exporters/ImageExporter.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index 4dc07d84..ffa59091 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -27,6 +27,7 @@ class ImageExporter(Exporter): {'name': 'height', 'type': 'int', 'value': int(tr.height()), 'limits': (0, None)}, {'name': 'antialias', 'type': 'bool', 'value': True}, {'name': 'background', 'type': 'color', 'value': bg}, + {'name': 'invertValue', 'type': 'bool', 'value': False} ]) self.params.param('width').sigValueChanged.connect(self.widthChanged) self.params.param('height').sigValueChanged.connect(self.heightChanged) @@ -67,13 +68,15 @@ class ImageExporter(Exporter): w, h = self.params['width'], self.params['height'] if w == 0 or h == 0: raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h)) - bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte) + bg = np.empty((self.params['height'], self.params['width'], 4), dtype=np.ubyte) color = self.params['background'] bg[:,:,0] = color.blue() bg[:,:,1] = color.green() bg[:,:,2] = color.red() bg[:,:,3] = color.alpha() - self.png = fn.makeQImage(bg, alpha=True) + + self.png = fn.makeQImage(bg, alpha=True, copy=False, transpose=False) + self.bg = bg ## set resolution of image: origTargetRect = self.getTargetRect() @@ -91,6 +94,12 @@ class ImageExporter(Exporter): self.setExportMode(False) painter.end() + if self.params['invertValue']: + mn = bg[...,:3].min(axis=2) + mx = bg[...,:3].max(axis=2) + d = (255 - mx) - mn + bg[...,:3] += d[...,np.newaxis] + if copy: QtGui.QApplication.clipboard().setImage(self.png) elif toBytes: From 6287874b5c3267d40128d637ab08a7957f687c5c Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 21:37:19 -0700 Subject: [PATCH 110/607] Minor fix - check for ragged array length when exporting to hdf5 --- pyqtgraph/exporters/HDF5Exporter.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/exporters/HDF5Exporter.py b/pyqtgraph/exporters/HDF5Exporter.py index cc8b5733..584a9f71 100644 --- a/pyqtgraph/exporters/HDF5Exporter.py +++ b/pyqtgraph/exporters/HDF5Exporter.py @@ -42,14 +42,20 @@ class HDF5Exporter(Exporter): dsname = self.params['Name'] fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite" data = [] - + appendAllX = self.params['columnMode'] == '(x,y) per plot' - for i,c in enumerate(self.item.curves): + #print dir(self.item.curves[0]) + tlen = 0 + for i, c in enumerate(self.item.curves): d = c.getData() + if i > 0 and len(d[0]) != tlen: + raise ValueError ("HDF5 Export requires all curves in plot to have same length") if appendAllX or i == 0: data.append(d[0]) + tlen = len(d[0]) data.append(d[1]) - + + fdata = numpy.array(data).astype('double') dset = fd.create_dataset(dsname, data=fdata) fd.close() From e06fc101f5ba2e1313de0f33bc9f56b91b1a4611 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 22:13:50 -0700 Subject: [PATCH 111/607] Add function to enable faulthandler on all threads --- pyqtgraph/debug.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 0da24d7c..61ae9fd5 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -1186,3 +1186,23 @@ class ThreadColor(object): c = (len(self.colors) % 15) + 1 self.colors[tid] = c return self.colors[tid] + + +def enableFaulthandler(): + """ Enable faulthandler for all threads. + + If the faulthandler package is available, this function disables and then + re-enables fault handling for all threads (this is necessary to ensure any + new threads are handled correctly), and returns True. + + If faulthandler is not available, then returns False. + """ + try: + import faulthandler + # necessary to disable first or else new threads may not be handled. + faulthandler.disable() + faulthandler.enable(all_threads=True) + return True + except ImportError: + return False + From 1911a26f8488689c4844c3a37acbd81eaea4696f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 13 Sep 2017 22:14:29 -0700 Subject: [PATCH 112/607] Allow Mutex to be used as drop-in replacement for python's Lock --- pyqtgraph/util/mutex.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/util/mutex.py b/pyqtgraph/util/mutex.py index 4a193127..c03c65c4 100644 --- a/pyqtgraph/util/mutex.py +++ b/pyqtgraph/util/mutex.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- -from ..Qt import QtCore import traceback +from ..Qt import QtCore + class Mutex(QtCore.QMutex): """ @@ -17,7 +18,7 @@ class Mutex(QtCore.QMutex): QtCore.QMutex.__init__(self, *args) self.l = QtCore.QMutex() ## for serializing access to self.tb self.tb = [] - self.debug = True ## True to enable debugging functions + self.debug = kargs.pop('debug', False) ## True to enable debugging functions def tryLock(self, timeout=None, id=None): if timeout is None: @@ -72,6 +73,16 @@ class Mutex(QtCore.QMutex): finally: self.l.unlock() + def acquire(self, blocking=True): + """Mimics threading.Lock.acquire() to allow this class as a drop-in replacement. + """ + return self.tryLock() + + def release(self): + """Mimics threading.Lock.release() to allow this class as a drop-in replacement. + """ + self.unlock() + def depth(self): self.l.lock() n = len(self.tb) @@ -91,4 +102,13 @@ class Mutex(QtCore.QMutex): def __enter__(self): self.lock() - return self \ No newline at end of file + return self + + +class RecursiveMutex(Mutex): + """Mimics threading.RLock class. + """ + def __init__(self, **kwds): + kwds['recursive'] = True + Mutex.__init__(self, **kwds) + From d081e5495667eb4adc7dd5e6423d8e8278efc37b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:54:50 -0700 Subject: [PATCH 113/607] EvalNode: add method to set code --- pyqtgraph/flowchart/library/Data.py | 33 +++++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 5236de8d..0ad7742b 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -189,31 +189,36 @@ class EvalNode(Node): self.ui = QtGui.QWidget() self.layout = QtGui.QGridLayout() - #self.addInBtn = QtGui.QPushButton('+Input') - #self.addOutBtn = QtGui.QPushButton('+Output') self.text = QtGui.QTextEdit() self.text.setTabStopWidth(30) self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal") - #self.layout.addWidget(self.addInBtn, 0, 0) - #self.layout.addWidget(self.addOutBtn, 0, 1) self.layout.addWidget(self.text, 1, 0, 1, 2) self.ui.setLayout(self.layout) - #QtCore.QObject.connect(self.addInBtn, QtCore.SIGNAL('clicked()'), self.addInput) - #self.addInBtn.clicked.connect(self.addInput) - #QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput) - #self.addOutBtn.clicked.connect(self.addOutput) self.text.focusOutEvent = self.focusOutEvent self.lastText = None def ctrlWidget(self): return self.ui - #def addInput(self): - #Node.addInput(self, 'input', renamable=True) + def setCode(self, code): + # unindent code; this allows nicer inline code specification when + # calling this method. + ind = [] + lines = code.split('\n') + for line in lines: + stripped = line.lstrip() + if len(stripped) > 0: + ind.append(len(line) - len(stripped)) + if len(ind) > 0: + ind = min(ind) + code = '\n'.join([line[ind:] for line in lines]) - #def addOutput(self): - #Node.addOutput(self, 'output', renamable=True) + self.text.clear() + self.text.insertPlainText(code) + + def code(self): + return self.text.toPlainText() def focusOutEvent(self, ev): text = str(self.text.toPlainText()) @@ -247,10 +252,10 @@ class EvalNode(Node): def restoreState(self, state): Node.restoreState(self, state) - self.text.clear() - self.text.insertPlainText(state['text']) + self.setCode(state['text']) self.restoreTerminals(state['terminals']) self.update() + class ColumnJoinNode(Node): """Concatenates record arrays and/or adds new columns""" From 868d9ebf2995007b5ef9558493bec1ee3bfdf055 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:55:17 -0700 Subject: [PATCH 114/607] Add several new data nodes --- pyqtgraph/flowchart/library/Data.py | 114 ++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/pyqtgraph/flowchart/library/Data.py b/pyqtgraph/flowchart/library/Data.py index 0ad7742b..18f1c948 100644 --- a/pyqtgraph/flowchart/library/Data.py +++ b/pyqtgraph/flowchart/library/Data.py @@ -359,3 +359,117 @@ class ColumnJoinNode(Node): self.update() +class Mean(CtrlNode): + """Calculate the mean of an array across an axis. + """ + nodeName = 'Mean' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = None if s['axis'] == -1 else s['axis'] + return data.mean(axis=ax) + + +class Max(CtrlNode): + """Calculate the maximum of an array across an axis. + """ + nodeName = 'Max' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = None if s['axis'] == -1 else s['axis'] + return data.max(axis=ax) + + +class Min(CtrlNode): + """Calculate the minimum of an array across an axis. + """ + nodeName = 'Min' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = None if s['axis'] == -1 else s['axis'] + return data.min(axis=ax) + + +class Stdev(CtrlNode): + """Calculate the standard deviation of an array across an axis. + """ + nodeName = 'Stdev' + uiTemplate = [ + ('axis', 'intSpin', {'value': -0, 'min': -1, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = None if s['axis'] == -1 else s['axis'] + return data.std(axis=ax) + + +class Index(CtrlNode): + """Select an index from an array axis. + """ + nodeName = 'Index' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': 0, 'max': 1000000}), + ('index', 'intSpin', {'value': 0, 'min': 0, 'max': 1000000}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = s['axis'] + ind = s['index'] + if ax == 0: + # allow support for non-ndarray sequence types + return data[ind] + else: + return data.take(ind, axis=ax) + + +class Slice(CtrlNode): + """Select a slice from an array axis. + """ + nodeName = 'Slice' + uiTemplate = [ + ('axis', 'intSpin', {'value': 0, 'min': 0, 'max': 1e6}), + ('start', 'intSpin', {'value': 0, 'min': -1e6, 'max': 1e6}), + ('stop', 'intSpin', {'value': -1, 'min': -1e6, 'max': 1e6}), + ('step', 'intSpin', {'value': 1, 'min': -1e6, 'max': 1e6}), + ] + + def processData(self, data): + s = self.stateGroup.state() + ax = s['axis'] + start = s['start'] + stop = s['stop'] + step = s['step'] + if ax == 0: + # allow support for non-ndarray sequence types + return data[start:stop:step] + else: + sl = [slice(None) for i in range(data.ndim)] + sl[ax] = slice(start, stop, step) + return data[sl] + + +class AsType(CtrlNode): + """Convert an array to a different dtype. + """ + nodeName = 'AsType' + uiTemplate = [ + ('dtype', 'combo', {'values': ['float', 'int', 'float32', 'float64', 'float128', 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'], 'index': 0}), + ] + + def processData(self, data): + s = self.stateGroup.state() + return data.astype(s['dtype']) + From 2016dc0df1b2658c9da9d8606e82a4ae9b5dd724 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:56:45 -0700 Subject: [PATCH 115/607] fix nodes spinbox handling --- pyqtgraph/flowchart/library/Filters.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index 9392b037..ada09dfb 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -38,7 +38,7 @@ class Bessel(CtrlNode): nodeName = 'BesselFilter' uiTemplate = [ ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), - ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('order', 'intSpin', {'value': 4, 'min': 1, 'max': 16}), ('bidir', 'check', {'checked': True}) ] @@ -57,10 +57,10 @@ class Butterworth(CtrlNode): nodeName = 'ButterworthFilter' uiTemplate = [ ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), - ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), - ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('bidir', 'check', {'checked': True}) ] @@ -78,14 +78,14 @@ class ButterworthNotch(CtrlNode): """Butterworth notch filter""" nodeName = 'ButterworthNotchFilter' uiTemplate = [ - ('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), - ('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), - ('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), - ('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), - ('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), + ('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), + ('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('bidir', 'check', {'checked': True}) ] From 19fc846b90955d1f7a9274ba0d10ef7fae1a59c3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:58:29 -0700 Subject: [PATCH 116/607] gaussian node uses internal gaussianFilter function --- pyqtgraph/flowchart/library/Filters.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/flowchart/library/Filters.py b/pyqtgraph/flowchart/library/Filters.py index ada09dfb..9a7fa401 100644 --- a/pyqtgraph/flowchart/library/Filters.py +++ b/pyqtgraph/flowchart/library/Filters.py @@ -160,19 +160,13 @@ class Gaussian(CtrlNode): @metaArrayWrapper def processData(self, data): + sigma = self.ctrls['sigma'].value() try: import scipy.ndimage + return scipy.ndimage.gaussian_filter(data, sigma) except ImportError: - raise Exception("GaussianFilter node requires the package scipy.ndimage.") + return pgfn.gaussianFilter(data, sigma) - if hasattr(data, 'implements') and data.implements('MetaArray'): - info = data.infoCopy() - filt = pgfn.gaussianFilter(data.asarray(), self.ctrls['sigma'].value()) - if 'values' in info[0]: - info[0]['values'] = info[0]['values'][:filt.shape[0]] - return metaarray.MetaArray(filt, info=info) - else: - return pgfn.gaussianFilter(data, self.ctrls['sigma'].value()) class Derivative(CtrlNode): """Returns the pointwise derivative of the input""" From 237b8488371a7b68f224927c8b269ec6bbb2b41e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:59:15 -0700 Subject: [PATCH 117/607] Allow binary operator nodes to select output type --- pyqtgraph/flowchart/library/Operators.py | 29 ++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/flowchart/library/Operators.py b/pyqtgraph/flowchart/library/Operators.py index 579d2cd2..596e8854 100644 --- a/pyqtgraph/flowchart/library/Operators.py +++ b/pyqtgraph/flowchart/library/Operators.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- from ..Node import Node +from .common import CtrlNode + class UniOpNode(Node): """Generic node for performing any operation like Out = In.fn()""" @@ -13,11 +15,22 @@ class UniOpNode(Node): def process(self, **args): return {'Out': getattr(args['In'], self.fn)()} -class BinOpNode(Node): +class BinOpNode(CtrlNode): """Generic node for performing any operation like A.fn(B)""" + + _dtypes = [ + 'float64', 'float32', 'float16', + 'int64', 'int32', 'int16', 'int8', + 'uint64', 'uint32', 'uint16', 'uint8' + ] + + uiTemplate = [ + ('outputType', 'combo', {'values': ['no change', 'input A', 'input B'] + _dtypes , 'index': 0}) + ] + def __init__(self, name, fn): self.fn = fn - Node.__init__(self, name, terminals={ + CtrlNode.__init__(self, name, terminals={ 'A': {'io': 'in'}, 'B': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'A'} @@ -36,6 +49,18 @@ class BinOpNode(Node): out = fn(args['B']) if out is NotImplemented: raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B'])))) + + # Coerce dtype if requested + typ = self.stateGroup.state()['outputType'] + if typ == 'no change': + pass + elif typ == 'input A': + out = out.astype(args['A'].dtype) + elif typ == 'input B': + out = out.astype(args['B'].dtype) + else: + out = out.astype(typ) + #print " ", fn, out return {'Out': out} From d65026f73d4d12913defcb77fb149fb21078c2b5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 08:59:31 -0700 Subject: [PATCH 118/607] add floor division node --- pyqtgraph/flowchart/library/Operators.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyqtgraph/flowchart/library/Operators.py b/pyqtgraph/flowchart/library/Operators.py index 596e8854..d1483c16 100644 --- a/pyqtgraph/flowchart/library/Operators.py +++ b/pyqtgraph/flowchart/library/Operators.py @@ -96,4 +96,10 @@ class DivideNode(BinOpNode): # try truediv first, followed by div BinOpNode.__init__(self, name, ('__truediv__', '__div__')) +class FloorDivideNode(BinOpNode): + """Returns A // B. Does not check input types.""" + nodeName = 'FloorDivide' + def __init__(self, name): + BinOpNode.__init__(self, name, '__floordiv__') + From fedecc5808e41a8c7d10e8803e758cbe13f90572 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:00:50 -0700 Subject: [PATCH 119/607] minor fixes --- pyqtgraph/flowchart/Flowchart.py | 3 ++- pyqtgraph/flowchart/library/common.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index b623f5c7..e31f3999 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -189,7 +189,8 @@ class Flowchart(Node): self.viewBox.addItem(item) item.moveBy(*pos) self._nodes[name] = node - self.widget().addNode(node) + if node is not self.inputNode and node is not self.outputNode: + self.widget().addNode(node) node.sigClosed.connect(self.nodeClosed) node.sigRenamed.connect(self.nodeRenamed) node.sigOutputChanged.connect(self.nodeOutputChanged) diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py index 425fe86c..8b3376c3 100644 --- a/pyqtgraph/flowchart/library/common.py +++ b/pyqtgraph/flowchart/library/common.py @@ -30,6 +30,11 @@ def generateUi(opts): k, t, o = opt else: raise Exception("Widget specification must be (name, type) or (name, type, {opts})") + + ## clean out these options so they don't get sent to SpinBox + hidden = o.pop('hidden', False) + tip = o.pop('tip', None) + if t == 'intSpin': w = QtGui.QSpinBox() if 'max' in o: @@ -63,11 +68,12 @@ def generateUi(opts): w = ColorButton() else: raise Exception("Unknown widget type '%s'" % str(t)) - if 'tip' in o: - w.setToolTip(o['tip']) + + if tip is not None: + w.setToolTip(tip) w.setObjectName(k) l.addRow(k, w) - if o.get('hidden', False): + if hidden: w.hide() label = l.labelForField(w) label.hide() From 698f37bd10a7bd089727404f0edb0f0f9389eace Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:00:59 -0700 Subject: [PATCH 120/607] code cleanup --- pyqtgraph/flowchart/Flowchart.py | 61 ++++++++++++++++---------------- 1 file changed, 30 insertions(+), 31 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index e31f3999..cbfd084e 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -166,6 +166,8 @@ class Flowchart(Node): n[oldName].rename(newName) def createNode(self, nodeType, name=None, pos=None): + """Create a new Node and add it to this flowchart. + """ if name is None: n = 0 while True: @@ -179,6 +181,10 @@ class Flowchart(Node): return node def addNode(self, node, name, pos=None): + """Add an existing Node to this flowchart. + + See also: createNode() + """ if pos is None: pos = [0, 0] if type(pos) in [QtCore.QPoint, QtCore.QPointF]: @@ -197,6 +203,8 @@ class Flowchart(Node): self.sigChartChanged.emit(self, 'add', node) def removeNode(self, node): + """Remove a Node from this flowchart. + """ node.close() def nodeClosed(self, node): @@ -234,7 +242,6 @@ class Flowchart(Node): term2 = self.internalTerminal(term2) term1.connectTo(term2) - def process(self, **args): """ Process data through the flowchart, returning the output. @@ -326,7 +333,6 @@ class Flowchart(Node): #print "DEPS:", deps ## determine correct node-processing order - #deps[self] = [] order = fn.toposort(deps) #print "ORDER1:", order @@ -350,7 +356,6 @@ class Flowchart(Node): if lastNode is None or ind > lastInd: lastNode = n lastInd = ind - #tdeps[t] = lastNode if lastInd is not None: dels.append((lastInd+1, t)) dels.sort(key=lambda a: a[0], reverse=True) @@ -405,27 +410,25 @@ class Flowchart(Node): self.inputWasSet = False else: self.sigStateChanged.emit() - - def chartGraphicsItem(self): - """Return the graphicsItem which displays the internals of this flowchart. - (graphicsItem() still returns the external-view item)""" - #return self._chartGraphicsItem + """Return the graphicsItem that displays the internal nodes and + connections of this flowchart. + + Note that the similar method `graphicsItem()` is inherited from Node + and returns the *external* graphical representation of this flowchart.""" return self.viewBox def widget(self): + """Return the control widget for this flowchart. + + This widget provides GUI access to the parameters for each node and a + graphical representation of the flowchart. + """ if self._widget is None: self._widget = FlowchartCtrlWidget(self) self.scene = self._widget.scene() self.viewBox = self._widget.viewBox() - #self._scene = QtGui.QGraphicsScene() - #self._widget.setScene(self._scene) - #self.scene.addItem(self.chartGraphicsItem()) - - #ci = self.chartGraphicsItem() - #self.viewBox.addItem(ci) - #self.viewBox.autoRange() return self._widget def listConnections(self): @@ -438,10 +441,11 @@ class Flowchart(Node): return conn def saveState(self): + """Return a serializable data structure representing the current state of this flowchart. + """ state = Node.saveState(self) state['nodes'] = [] state['connects'] = [] - #state['terminals'] = self.saveTerminals() for name, node in self._nodes.items(): cls = type(node) @@ -461,6 +465,8 @@ class Flowchart(Node): return state def restoreState(self, state, clear=False): + """Restore the state of this flowchart from a previous call to `saveState()`. + """ self.blockSignals(True) try: if clear: @@ -470,7 +476,6 @@ class Flowchart(Node): nodes.sort(key=lambda a: a['pos'][0]) for n in nodes: if n['name'] in self._nodes: - #self._nodes[n['name']].graphicsItem().moveBy(*n['pos']) self._nodes[n['name']].restoreState(n['state']) continue try: @@ -478,7 +483,6 @@ class Flowchart(Node): node.restoreState(n['state']) except: printExc("Error creating node %s: (continuing anyway)" % n['name']) - #node.graphicsItem().moveBy(*n['pos']) self.inputNode.restoreState(state.get('inputNode', {})) self.outputNode.restoreState(state.get('outputNode', {})) @@ -491,7 +495,6 @@ class Flowchart(Node): print(self._nodes[n1].terminals) print(self._nodes[n2].terminals) printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2)) - finally: self.blockSignals(False) @@ -499,48 +502,46 @@ class Flowchart(Node): self.sigChartLoaded.emit() self.outputChanged() self.sigStateChanged.emit() - #self.sigOutputChanged.emit() def loadFile(self, fileName=None, startDir=None): + """Load a flowchart (*.fc) file. + """ if fileName is None: if startDir is None: startDir = self.filePath if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - #self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.loadFile) return ## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. - #fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") fileName = unicode(fileName) state = configfile.readConfigFile(fileName) self.restoreState(state, clear=True) self.viewBox.autoRange() - #self.emit(QtCore.SIGNAL('fileLoaded'), fileName) self.sigFileLoaded.emit(fileName) def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): + """Save this flowchart to a .fc file + """ if fileName is None: if startDir is None: startDir = self.filePath if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") - #self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - #self.fileDialog.setDirectory(startDir) self.fileDialog.show() self.fileDialog.fileSelected.connect(self.saveFile) return - #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") fileName = unicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) def clear(self): + """Remove all nodes from this flowchart except the original input/output nodes. + """ for n in list(self._nodes.values()): if n is self.inputNode or n is self.outputNode: continue @@ -553,18 +554,15 @@ class Flowchart(Node): self.inputNode.clearTerminals() self.outputNode.clearTerminals() -#class FlowchartGraphicsItem(QtGui.QGraphicsItem): + class FlowchartGraphicsItem(GraphicsObject): def __init__(self, chart): - #print "FlowchartGraphicsItem.__init__" - #QtGui.QGraphicsItem.__init__(self) GraphicsObject.__init__(self) self.chart = chart ## chart is an instance of Flowchart() self.updateTerminals() def updateTerminals(self): - #print "FlowchartGraphicsItem.updateTerminals" self.terminals = {} bounds = self.boundingRect() inp = self.chart.inputs() @@ -760,6 +758,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): item = self.items[node] self.ui.ctrlList.setCurrentItem(item) + class FlowchartWidget(dockarea.DockArea): """Includes the actual graphical flowchart and debugging interface""" def __init__(self, chart, ctrl): From ee0ea5669520346e7c0ae420a6be6f935d276c1e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:05:24 -0700 Subject: [PATCH 121/607] PlotItem.addLegend will not try to add more than once --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 41011df3..7321702c 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -602,6 +602,9 @@ class PlotItem(GraphicsWidget): #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) #item.sigPlotChanged.connect(self.plotChanged) + if self.legend is not None: + self.legend.removeItem(item) + def clear(self): """ Remove all items from the ViewBox. @@ -646,9 +649,13 @@ class PlotItem(GraphicsWidget): Create a new LegendItem and anchor it over the internal ViewBox. Plots will be automatically displayed in the legend if they are created with the 'name' argument. + + If a LegendItem has already been created using this method, that + item will be returned rather than creating a new one. """ - self.legend = LegendItem(size, offset) - self.legend.setParentItem(self.vb) + if self.legend is None: + self.legend = LegendItem(size, offset) + self.legend.setParentItem(self.vb) return self.legend def scatterPlot(self, *args, **kargs): From b88a96c08c6a8d449c4ce315513d28488b765145 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:06:18 -0700 Subject: [PATCH 122/607] ViewBox: make sure transform is up to date in all mapping functions --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8ade0c6b..e7d932d9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -404,6 +404,7 @@ class ViewBox(GraphicsWidget): ch.setParentItem(None) def resizeEvent(self, ev): + self._matrixNeedsUpdate = True self.linkedXChanged() self.linkedYChanged() self.updateAutoRange() @@ -553,11 +554,9 @@ class ViewBox(GraphicsWidget): # Note that aspect ratio constraints and auto-visible probably do not work together.. if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): self._autoRangeNeedsUpdate = True - #self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated? elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): self._autoRangeNeedsUpdate = True - #self.updateAutoRange() - + def setYRange(self, min, max, padding=None, update=True): """ Set the visible Y range of the view to [*min*, *max*]. @@ -1083,35 +1082,43 @@ class ViewBox(GraphicsWidget): def mapToView(self, obj): """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox""" + self.updateMatrix() m = fn.invertQTransform(self.childTransform()) return m.map(obj) def mapFromView(self, obj): """Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox""" + self.updateMatrix() m = self.childTransform() return m.map(obj) def mapSceneToView(self, obj): """Maps from scene coordinates to the coordinate system displayed inside the ViewBox""" + self.updateMatrix() return self.mapToView(self.mapFromScene(obj)) def mapViewToScene(self, obj): """Maps from the coordinate system displayed inside the ViewBox to scene coordinates""" + self.updateMatrix() return self.mapToScene(self.mapFromView(obj)) def mapFromItemToView(self, item, obj): """Maps *obj* from the local coordinate system of *item* to the view coordinates""" + self.updateMatrix() return self.childGroup.mapFromItem(item, obj) #return self.mapSceneToView(item.mapToScene(obj)) def mapFromViewToItem(self, item, obj): """Maps *obj* from view coordinates to the local coordinate system of *item*.""" + self.updateMatrix() return self.childGroup.mapToItem(item, obj) def mapViewToDevice(self, obj): + self.updateMatrix() return self.mapToDevice(self.mapFromView(obj)) def mapDeviceToView(self, obj): + self.updateMatrix() return self.mapToView(self.mapFromDevice(obj)) def viewPixelSize(self): From ea9e8a720b4dd79465fd8c12f6fbf8331a3b1c65 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:09:45 -0700 Subject: [PATCH 123/607] ArrowItem: rotate painterpath instead of the item This makes it easier to attach text to the arrow. --- pyqtgraph/graphicsItems/ArrowItem.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 77e6195f..897cbc50 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -39,7 +39,6 @@ class ArrowItem(QtGui.QGraphicsPathItem): self.setStyle(**defaultOpts) - self.rotate(self.opts['angle']) self.moveBy(*self.opts['pos']) def setStyle(self, **opts): @@ -72,7 +71,10 @@ class ArrowItem(QtGui.QGraphicsPathItem): self.opts.update(opts) opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) - self.path = fn.makeArrowPath(**opt) + tr = QtGui.QTransform() + tr.rotate(self.opts['angle']) + self.path = tr.map(fn.makeArrowPath(**opt)) + self.setPath(self.path) self.setPen(fn.mkPen(self.opts['pen'])) @@ -82,7 +84,8 @@ class ArrowItem(QtGui.QGraphicsPathItem): self.setFlags(self.flags() | self.ItemIgnoresTransformations) else: self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) - + + def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) QtGui.QGraphicsPathItem.paint(self, p, *args) From 653c91a683da40e7d0d72b70c3c6a41fcb68a903 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Sep 2017 09:14:26 -0700 Subject: [PATCH 124/607] InfiniteLine: add markers and ability to limit drawing region --- pyqtgraph/graphicsItems/InfiniteLine.py | 200 +++++++++++++++++++++--- 1 file changed, 174 insertions(+), 26 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 3da82327..7aeb1620 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -31,7 +31,8 @@ class InfiniteLine(GraphicsObject): sigPositionChanged = QtCore.Signal(object) def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, - hoverPen=None, label=None, labelOpts=None, name=None): + hoverPen=None, label=None, labelOpts=None, span=(0, 1), markers=None, + name=None): """ =============== ================================================================== **Arguments:** @@ -41,22 +42,28 @@ class InfiniteLine(GraphicsObject): pen Pen to use when drawing line. Can be any arguments that are valid for :func:`mkPen `. Default pen is transparent yellow. + hoverPen Pen to use when the mouse cursor hovers over the line. + Only used when movable=True. movable If True, the line can be dragged to a new position by the user. + bounds Optional [min, max] bounding values. Bounds are only valid if the + line is vertical or horizontal. hoverPen Pen to use when drawing line when hovering over it. Can be any arguments that are valid for :func:`mkPen `. Default pen is red. - bounds Optional [min, max] bounding values. Bounds are only valid if the - line is vertical or horizontal. label Text to be displayed in a label attached to the line, or None to show no label (default is None). May optionally include formatting strings to display the line value. labelOpts A dict of keyword arguments to use when constructing the text label. See :class:`InfLineLabel`. + span Optional tuple (min, max) giving the range over the view to draw + the line. For example, with a vertical line, use span=(0.5, 1) + to draw only on the top half of the view. + markers List of (marker, position, size) tuples, one per marker to display + on the line. See the addMarker method. name Name of the item =============== ================================================================== """ self._boundingRect = None - self._line = None self._name = name @@ -79,11 +86,25 @@ class InfiniteLine(GraphicsObject): if pen is None: pen = (200, 200, 100) self.setPen(pen) + if hoverPen is None: self.setHoverPen(color=(255,0,0), width=self.pen.width()) else: self.setHoverPen(hoverPen) + + self.span = span self.currentPen = self.pen + + self.markers = [] + self._maxMarkerSize = 0 + if markers is not None: + for m in markers: + self.addMarker(*m) + + # Cache variables for managing bounds + self._endPoints = [0, 1] # + self._bounds = None + self._lastViewSize = None if label is not None: labelOpts = {} if labelOpts is None else labelOpts @@ -98,7 +119,12 @@ class InfiniteLine(GraphicsObject): """Set the (minimum, maximum) allowable values when dragging.""" self.maxRange = bounds self.setValue(self.value()) - + + def bounds(self): + """Return the (minimum, maximum) values allowed when dragging. + """ + return self.maxRange[:] + def setPen(self, *args, **kwargs): """Set the pen for drawing the line. Allowable arguments are any that are valid for :func:`mkPen `.""" @@ -115,11 +141,70 @@ class InfiniteLine(GraphicsObject): If the line is not movable, then hovering is also disabled. Added in version 0.9.9.""" + # If user did not supply a width, then copy it from pen + widthSpecified = ((len(args) == 1 and + (isinstance(args[0], QtGui.QPen) or + (isinstance(args[0], dict) and 'width' in args[0])) + ) or 'width' in kwargs) self.hoverPen = fn.mkPen(*args, **kwargs) + if not widthSpecified: + self.hoverPen.setWidth(self.pen.width()) + if self.mouseHovering: self.currentPen = self.hoverPen self.update() + + def addMarker(self, marker, position=0.5, size=10.0): + """Add a marker to be displayed on the line. + + ============= ========================================================= + **Arguments** + marker String indicating the style of marker to add: + '<|', '|>', '>|', '|<', '<|>', '>|<', '^', 'v', 'o' + position Position (0.0-1.0) along the visible extent of the line + to place the marker. Default is 0.5. + size Size of the marker in pixels. Default is 10.0. + ============= ========================================================= + """ + path = QtGui.QPainterPath() + if marker == 'o': + path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) + if '<|' in marker: + p = QtGui.QPolygonF([Point(0.5, 0), Point(0, -0.5), Point(-0.5, 0)]) + path.addPolygon(p) + path.closeSubpath() + if '|>' in marker: + p = QtGui.QPolygonF([Point(0.5, 0), Point(0, 0.5), Point(-0.5, 0)]) + path.addPolygon(p) + path.closeSubpath() + if '>|' in marker: + p = QtGui.QPolygonF([Point(0.5, -0.5), Point(0, 0), Point(-0.5, -0.5)]) + path.addPolygon(p) + path.closeSubpath() + if '|<' in marker: + p = QtGui.QPolygonF([Point(0.5, 0.5), Point(0, 0), Point(-0.5, 0.5)]) + path.addPolygon(p) + path.closeSubpath() + if '^' in marker: + p = QtGui.QPolygonF([Point(0, -0.5), Point(0.5, 0), Point(0, 0.5)]) + path.addPolygon(p) + path.closeSubpath() + if 'v' in marker: + p = QtGui.QPolygonF([Point(0, -0.5), Point(-0.5, 0), Point(0, 0.5)]) + path.addPolygon(p) + path.closeSubpath() + + self.markers.append((path, position, size)) + self._maxMarkerSize = max([m[2] / 2. for m in self.markers]) + self.update() + def clearMarkers(self): + """ Remove all markers from this line. + """ + self.markers = [] + self._maxMarkerSize = 0 + self.update() + def setAngle(self, angle): """ Takes angle argument in degrees. @@ -128,7 +213,7 @@ class InfiniteLine(GraphicsObject): Note that the use of value() and setValue() changes if the line is not vertical or horizontal. """ - self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 + self.angle = angle #((angle+45) % 180) - 45 ## -45 <= angle < 135 self.resetTransform() self.rotate(self.angle) self.update() @@ -199,35 +284,98 @@ class InfiniteLine(GraphicsObject): #else: #print "ignore", change #return GraphicsObject.itemChange(self, change, val) + + def setSpan(self, mn, mx): + if self.span != (mn, mx): + self.span = (mn, mx) + self.update() def _invalidateCache(self): - self._line = None self._boundingRect = None + def _computeBoundingRect(self): + #br = UIGraphicsItem.boundingRect(self) + vr = self.viewRect() # bounds of containing ViewBox mapped to local coords. + if vr is None: + return QtCore.QRectF() + + ## add a 4-pixel radius around the line for mouse interaction. + + px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line + if px is None: + px = 0 + pw = max(self.pen.width() / 2, self.hoverPen.width() / 2) + w = max(4, self._maxMarkerSize + pw) + 1 + w = w * px + br = QtCore.QRectF(vr) + br.setBottom(-w) + br.setTop(w) + + length = br.width() + left = br.left() + length * self.span[0] + right = br.left() + length * self.span[1] + br.setLeft(left - w) + br.setRight(right + w) + br = br.normalized() + + vs = self.getViewBox().size() + + if self._bounds != br or self._lastViewSize != vs: + self._bounds = br + self._lastViewSize = vs + self.prepareGeometryChange() + + self._endPoints = (left, right) + self._lastViewRect = vr + + return self._bounds + def boundingRect(self): if self._boundingRect is None: - #br = UIGraphicsItem.boundingRect(self) - br = self.viewRect() - if br is None: - return QtCore.QRectF() - - ## add a 4-pixel radius around the line for mouse interaction. - px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line - if px is None: - px = 0 - w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px - br.setBottom(-w) - br.setTop(w) - - br = br.normalized() - self._boundingRect = br - self._line = QtCore.QLineF(br.right(), 0.0, br.left(), 0.0) + self._boundingRect = self._computeBoundingRect() return self._boundingRect def paint(self, p, *args): - p.setPen(self.currentPen) - p.drawLine(self._line) - + p.setRenderHint(p.Antialiasing) + + left, right = self._endPoints + pen = self.currentPen + pen.setJoinStyle(QtCore.Qt.MiterJoin) + p.setPen(pen) + p.drawLine(Point(left, 0), Point(right, 0)) + + + if len(self.markers) == 0: + return + + # paint markers in native coordinate system + tr = p.transform() + p.resetTransform() + + start = tr.map(Point(left, 0)) + end = tr.map(Point(right, 0)) + up = tr.map(Point(left, 1)) + dif = end - start + length = Point(dif).length() + angle = np.arctan2(dif.y(), dif.x()) * 180 / np.pi + + p.translate(start) + p.rotate(angle) + + up = up - start + det = up.x() * dif.y() - dif.x() * up.y() + p.scale(1, 1 if det > 0 else -1) + + p.setBrush(fn.mkBrush(self.currentPen.color())) + #p.setPen(fn.mkPen(None)) + tr = p.transform() + for path, pos, size in self.markers: + p.setTransform(tr) + x = length * pos + p.translate(x, 0) + p.scale(size, size) + p.drawPath(path) + def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == 0: return None ## x axis should never be auto-scaled From 4beea8a153b98a24740132225a62f0ecae6d41d4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 18 Sep 2017 13:31:32 -0700 Subject: [PATCH 125/607] Prevent viewbox auto-scaling to items that are not in the same scene. This can happen when an item that was previously added to the viewbox is then removed using scene.removeItem(). --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 8ade0c6b..c125babf 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1280,7 +1280,7 @@ class ViewBox(GraphicsWidget): ## First collect all boundary information itemBounds = [] for item in items: - if not item.isVisible(): + if not item.isVisible() or not item.scene() is self.scene(): continue useX = True From fe1dff5ad196226a96824e18dfb616f9a75962e8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 19 Sep 2017 09:51:17 -0700 Subject: [PATCH 126/607] Allow console exception label to wrap text This prevents the console window from growing if the exception message contains a very long line --- pyqtgraph/console/template.ui | 5 +++- pyqtgraph/console/template_pyqt.py | 9 +++--- pyqtgraph/console/template_pyqt5.py | 41 ++++++++++++++++------------ pyqtgraph/console/template_pyside.py | 39 ++++++++++++++++---------- 4 files changed, 56 insertions(+), 38 deletions(-) diff --git a/pyqtgraph/console/template.ui b/pyqtgraph/console/template.ui index 1dd752d9..1237b5f3 100644 --- a/pyqtgraph/console/template.ui +++ b/pyqtgraph/console/template.ui @@ -6,7 +6,7 @@ 0 0 - 694 + 739 497 @@ -154,6 +154,9 @@ Stack Trace + + true + diff --git a/pyqtgraph/console/template_pyqt.py b/pyqtgraph/console/template_pyqt.py index e5fc4619..9b39d14a 100644 --- a/pyqtgraph/console/template_pyqt.py +++ b/pyqtgraph/console/template_pyqt.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'template.ui' +# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui' # -# Created: Wed Apr 08 16:28:53 2015 -# by: PyQt4 UI code generator 4.10.4 +# Created by: PyQt4 UI code generator 4.11.4 # # WARNING! All changes made in this file will be lost! @@ -26,7 +25,7 @@ except AttributeError: class Ui_Form(object): def setupUi(self, Form): Form.setObjectName(_fromUtf8("Form")) - Form.resize(694, 497) + Form.resize(739, 497) self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout.setMargin(0) self.gridLayout.setSpacing(0) @@ -37,7 +36,6 @@ class Ui_Form(object): self.layoutWidget = QtGui.QWidget(self.splitter) self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setMargin(0) self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.output = QtGui.QPlainTextEdit(self.layoutWidget) font = QtGui.QFont() @@ -97,6 +95,7 @@ class Ui_Form(object): self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck")) self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setWordWrap(True) self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel")) self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) diff --git a/pyqtgraph/console/template_pyqt5.py b/pyqtgraph/console/template_pyqt5.py index 1fbc5bed..c8c2cbac 100644 --- a/pyqtgraph/console/template_pyqt5.py +++ b/pyqtgraph/console/template_pyqt5.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' +# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui' # -# Created: Wed Mar 26 15:09:29 2014 -# by: PyQt5 UI code generator 5.0.1 +# Created by: PyQt5 UI code generator 5.5.1 # # WARNING! All changes made in this file will be lost! @@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(710, 497) + Form.resize(739, 497) self.gridLayout = QtWidgets.QGridLayout(Form) self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout.setSpacing(0) @@ -23,7 +22,6 @@ class Ui_Form(object): self.layoutWidget = QtWidgets.QWidget(self.splitter) self.layoutWidget.setObjectName("layoutWidget") self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) self.verticalLayout.setObjectName("verticalLayout") self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) font = QtGui.QFont() @@ -54,9 +52,14 @@ class Ui_Form(object): self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) self.exceptionGroup.setObjectName("exceptionGroup") self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setHorizontalSpacing(2) + self.gridLayout_2.setVerticalSpacing(0) self.gridLayout_2.setObjectName("gridLayout_2") + self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) self.catchAllExceptionsBtn.setCheckable(True) self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") @@ -68,24 +71,27 @@ class Ui_Form(object): self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) self.onlyUncaughtCheck.setChecked(True) self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) self.exceptionStackList.setAlternatingRowColors(True) self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) self.runSelectedFrameCheck.setChecked(True) self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setWordWrap(True) self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) - self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) + self.label = QtWidgets.QLabel(self.exceptionGroup) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) + self.filterText = QtWidgets.QLineEdit(self.exceptionGroup) + self.filterText.setObjectName("filterText") + self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -97,11 +103,12 @@ class Ui_Form(object): self.historyBtn.setText(_translate("Form", "History..")) self.exceptionBtn.setText(_translate("Form", "Exceptions..")) self.exceptionGroup.setTitle(_translate("Form", "Exception Handling")) + self.clearExceptionBtn.setText(_translate("Form", "Clear Stack")) self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions")) self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception")) self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions")) self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame")) - self.exceptionInfoLabel.setText(_translate("Form", "Exception Info")) - self.clearExceptionBtn.setText(_translate("Form", "Clear Exception")) + self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace")) + self.label.setText(_translate("Form", "Filter (regex):")) from .CmdInput import CmdInput diff --git a/pyqtgraph/console/template_pyside.py b/pyqtgraph/console/template_pyside.py index 36065afd..1579cb1f 100644 --- a/pyqtgraph/console/template_pyside.py +++ b/pyqtgraph/console/template_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' +# Form implementation generated from reading ui file 'pyqtgraph/console/template.ui' # -# Created: Mon Dec 23 10:10:53 2013 -# by: pyside-uic 0.2.14 running on PySide 1.1.2 +# Created: Tue Sep 19 09:45:18 2017 +# by: pyside-uic 0.2.15 running on PySide 1.2.2 # # WARNING! All changes made in this file will be lost! @@ -12,7 +12,7 @@ from PySide import QtCore, QtGui class Ui_Form(object): def setupUi(self, Form): Form.setObjectName("Form") - Form.resize(710, 497) + Form.resize(739, 497) self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout.setSpacing(0) @@ -54,9 +54,14 @@ class Ui_Form(object): self.exceptionGroup = QtGui.QGroupBox(self.splitter) self.exceptionGroup.setObjectName("exceptionGroup") self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setHorizontalSpacing(2) + self.gridLayout_2.setVerticalSpacing(0) self.gridLayout_2.setObjectName("gridLayout_2") + self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup) self.catchAllExceptionsBtn.setCheckable(True) self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") @@ -68,24 +73,27 @@ class Ui_Form(object): self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup) self.onlyUncaughtCheck.setChecked(True) self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup) self.exceptionStackList.setAlternatingRowColors(True) self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup) self.runSelectedFrameCheck.setChecked(True) self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setWordWrap(True) self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) - self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1) + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) + self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) + self.label = QtGui.QLabel(self.exceptionGroup) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) + self.filterText = QtGui.QLineEdit(self.exceptionGroup) + self.filterText.setObjectName("filterText") + self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.retranslateUi(Form) @@ -96,11 +104,12 @@ class Ui_Form(object): self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8)) + self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Stack", None, QtGui.QApplication.UnicodeUTF8)) self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8)) self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8)) self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8)) self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Stack Trace", None, QtGui.QApplication.UnicodeUTF8)) - self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Stack", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("Form", "Filter (regex):", None, QtGui.QApplication.UnicodeUTF8)) from .CmdInput import CmdInput From 9bfdda06a63bdbcaf66cf022517b32a3f86bbb3f Mon Sep 17 00:00:00 2001 From: HashSplat Date: Wed, 20 Sep 2017 17:15:54 -0400 Subject: [PATCH 127/607] Fixed AxisMouse drag issue Issue where MouseDragEvent would only work for 1 AxisItem. Allowed the MouseDragEvent to propagate to other AxisItems. I had this issue by setting the ViewBox background color and changed the AxisItem ZValue to make the AxisItems visible which made the AxisItem receive all of the MouseDragEvents and only one Axis would actually allow dragging. --- pyqtgraph/graphicsItems/AxisItem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index b125cb7e..9ba3c614 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1066,9 +1066,11 @@ class AxisItem(GraphicsWidget): if self.linkedView() is None: return if self.orientation in ['left', 'right']: - return self.linkedView().mouseDragEvent(event, axis=1) + ret = self.linkedView().mouseDragEvent(event, axis=1) else: - return self.linkedView().mouseDragEvent(event, axis=0) + ret = self.linkedView().mouseDragEvent(event, axis=0) + event.ignore() + return ret def mouseClickEvent(self, event): if self.linkedView() is None: From 98cdc65049a8b61169982bb7add833f923979bf4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 21 Sep 2017 09:04:06 -0700 Subject: [PATCH 128/607] Update LinearRegionItem to support new InfiniteLine features Also add methods for setting hover brush and configurable line swap behavior (block/push/sort) --- pyqtgraph/graphicsItems/LinearRegionItem.py | 222 ++++++++++---------- 1 file changed, 114 insertions(+), 108 deletions(-) diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index e139190b..9903dac5 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -1,14 +1,14 @@ from ..Qt import QtGui, QtCore -from .UIGraphicsItem import UIGraphicsItem +from .GraphicsObject import GraphicsObject from .InfiniteLine import InfiniteLine from .. import functions as fn from .. import debug as debug __all__ = ['LinearRegionItem'] -class LinearRegionItem(UIGraphicsItem): +class LinearRegionItem(GraphicsObject): """ - **Bases:** :class:`UIGraphicsItem ` + **Bases:** :class:`GraphicsObject ` Used for marking a horizontal or vertical region in plots. The region can be dragged and is bounded by lines which can be dragged individually. @@ -26,65 +26,110 @@ class LinearRegionItem(UIGraphicsItem): sigRegionChanged = QtCore.Signal(object) Vertical = 0 Horizontal = 1 + _orientation_axis = { + Vertical: 0, + Horizontal: 1, + 'vertical': 0, + 'horizontal': 1, + } - def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None): + def __init__(self, values=(0, 1), orientation='vertical', brush=None, pen=None, + hoverBrush=None, hoverPen=None, movable=True, bounds=None, + span=(0, 1), swapMode='sort'): """Create a new LinearRegionItem. ============== ===================================================================== **Arguments:** values A list of the positions of the lines in the region. These are not limits; limits can be set by specifying bounds. - orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal. - If not specified it will be vertical. + orientation Options are 'vertical' or 'horizontal', indicating the + The default is 'vertical', indicating that the brush Defines the brush that fills the region. Can be any arguments that are valid for :func:`mkBrush `. Default is transparent blue. + pen The pen to use when drawing the lines that bound the region. + hoverBrush The brush to use when the mouse is hovering over the region. + hoverPen The pen to use when the mouse is hovering over the region. movable If True, the region and individual lines are movable by the user; if False, they are static. bounds Optional [min, max] bounding values for the region + span Optional [min, max] giving the range over the view to draw + the region. For example, with a vertical line, use span=(0.5, 1) + to draw only on the top half of the view. + swapMode Sets the behavior of the region when the lines are moved such that + their order reverses: + * "block" means the user cannot drag one line past the other + * "push" causes both lines to be moved if one would cross the other + * "sort" means that lines may trade places, but the output of + getRegion always gives the line positions in ascending order. + * None means that no attempt is made to handle swapped line + positions. + The default is "sort". ============== ===================================================================== """ - UIGraphicsItem.__init__(self) - if orientation is None: - orientation = LinearRegionItem.Vertical + GraphicsObject.__init__(self) self.orientation = orientation self.bounds = QtCore.QRectF() self.blockLineSignal = False self.moving = False self.mouseHovering = False + self.span = span + self.swapMode = swapMode + self._bounds = None - if orientation == LinearRegionItem.Horizontal: + # note LinearRegionItem.Horizontal and LinearRegionItem.Vertical + # are kept for backward compatibility. + lineKwds = dict( + movable=movable, + bounds=bounds, + span=span, + pen=pen, + hoverPen=hoverPen, + ) + + if orientation in ('horizontal', LinearRegionItem.Horizontal): self.lines = [ - InfiniteLine(QtCore.QPointF(0, values[0]), 0, movable=movable, bounds=bounds), - InfiniteLine(QtCore.QPointF(0, values[1]), 0, movable=movable, bounds=bounds)] - elif orientation == LinearRegionItem.Vertical: + # rotate lines to 180 to preserve expected line orientation + # with respect to region. This ensures that placing a '<|' + # marker on lines[0] causes it to point left in vertical mode + # and down in horizontal mode. + InfiniteLine(QtCore.QPointF(0, values[0]), angle=0, **lineKwds), + InfiniteLine(QtCore.QPointF(0, values[1]), angle=0, **lineKwds)] + self.lines[0].scale(1, -1) + self.lines[1].scale(1, -1) + elif orientation in ('vertical', LinearRegionItem.Vertical): self.lines = [ - InfiniteLine(QtCore.QPointF(values[1], 0), 90, movable=movable, bounds=bounds), - InfiniteLine(QtCore.QPointF(values[0], 0), 90, movable=movable, bounds=bounds)] + InfiniteLine(QtCore.QPointF(values[0], 0), angle=90, **lineKwds), + InfiniteLine(QtCore.QPointF(values[1], 0), angle=90, **lineKwds)] else: - raise Exception('Orientation must be one of LinearRegionItem.Vertical or LinearRegionItem.Horizontal') - + raise Exception("Orientation must be 'vertical' or 'horizontal'.") for l in self.lines: l.setParentItem(self) l.sigPositionChangeFinished.connect(self.lineMoveFinished) - l.sigPositionChanged.connect(self.lineMoved) + self.lines[0].sigPositionChanged.connect(lambda: self.lineMoved(0)) + self.lines[1].sigPositionChanged.connect(lambda: self.lineMoved(1)) if brush is None: brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) self.setBrush(brush) + if hoverBrush is None: + c = self.brush.color() + c.setAlpha(min(c.alpha() * 2, 255)) + hoverBrush = fn.mkBrush(c) + self.setHoverBrush(hoverBrush) + self.setMovable(movable) def getRegion(self): """Return the values at the edges of the region.""" - #if self.orientation[0] == 'h': - #r = (self.bounds.top(), self.bounds.bottom()) - #else: - #r = (self.bounds.left(), self.bounds.right()) - r = [self.lines[0].value(), self.lines[1].value()] - return (min(r), max(r)) + r = (self.lines[0].value(), self.lines[1].value()) + if self.swapMode == 'sort': + return (min(r), max(r)) + else: + return r def setRegion(self, rgn): """Set the values for the edges of the region. @@ -101,7 +146,8 @@ class LinearRegionItem(UIGraphicsItem): self.blockLineSignal = False self.lines[1].setValue(rgn[1]) #self.blockLineSignal = False - self.lineMoved() + self.lineMoved(0) + self.lineMoved(1) self.lineMoveFinished() def setBrush(self, *br, **kargs): @@ -111,6 +157,13 @@ class LinearRegionItem(UIGraphicsItem): self.brush = fn.mkBrush(*br, **kargs) self.currentBrush = self.brush + def setHoverBrush(self, *br, **kargs): + """Set the brush that fills the region when the mouse is hovering over. + Can have any arguments that are valid + for :func:`mkBrush `. + """ + self.hoverBrush = fn.mkBrush(*br, **kargs) + def setBounds(self, bounds): """Optional [min, max] bounding values for the region. To have no bounds on the region use [None, None]. @@ -128,81 +181,67 @@ class LinearRegionItem(UIGraphicsItem): self.movable = m self.setAcceptHoverEvents(m) + def setSpan(self, mn, mx): + if self.span == (mn, mx): + return + self.span = (mn, mx) + self.lines[0].setSpan(mn, mx) + self.lines[1].setSpan(mn, mx) + self.update() + def boundingRect(self): - br = UIGraphicsItem.boundingRect(self) + br = self.viewRect() # bounds of containing ViewBox mapped to local coords. + rng = self.getRegion() - if self.orientation == LinearRegionItem.Vertical: + if self.orientation in ('vertical', LinearRegionItem.Vertical): br.setLeft(rng[0]) br.setRight(rng[1]) + length = br.height() + br.setBottom(br.top() + length * self.span[1]) + br.setTop(br.top() + length * self.span[0]) else: br.setTop(rng[0]) br.setBottom(rng[1]) - return br.normalized() + length = br.width() + br.setRight(br.left() + length * self.span[1]) + br.setLeft(br.left() + length * self.span[0]) + + br = br.normalized() + + if self._bounds != br: + self._bounds = br + self.prepareGeometryChange() + + return br def paint(self, p, *args): profiler = debug.Profiler() - UIGraphicsItem.paint(self, p, *args) p.setBrush(self.currentBrush) p.setPen(fn.mkPen(None)) p.drawRect(self.boundingRect()) def dataBounds(self, axis, frac=1.0, orthoRange=None): - if axis == self.orientation: + if axis == self._orientation_axis[self.orientation]: return self.getRegion() else: return None - def lineMoved(self): + def lineMoved(self, i): if self.blockLineSignal: return + + # lines swapped + if self.lines[0].value() > self.lines[1].value(): + if self.swapMode == 'block': + self.lines[i].setValue(self.lines[1-i].value()) + elif self.swapMode == 'push': + self.lines[1-i].setValue(self.lines[i].value()) + self.prepareGeometryChange() - #self.emit(QtCore.SIGNAL('regionChanged'), self) self.sigRegionChanged.emit(self) def lineMoveFinished(self): - #self.emit(QtCore.SIGNAL('regionChangeFinished'), self) self.sigRegionChangeFinished.emit(self) - - - #def updateBounds(self): - #vb = self.view().viewRect() - #vals = [self.lines[0].value(), self.lines[1].value()] - #if self.orientation[0] == 'h': - #vb.setTop(min(vals)) - #vb.setBottom(max(vals)) - #else: - #vb.setLeft(min(vals)) - #vb.setRight(max(vals)) - #if vb != self.bounds: - #self.bounds = vb - #self.rect.setRect(vb) - - #def mousePressEvent(self, ev): - #if not self.movable: - #ev.ignore() - #return - #for l in self.lines: - #l.mousePressEvent(ev) ## pass event to both lines so they move together - ##if self.movable and ev.button() == QtCore.Qt.LeftButton: - ##ev.accept() - ##self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p) - ##else: - ##ev.ignore() - - #def mouseReleaseEvent(self, ev): - #for l in self.lines: - #l.mouseReleaseEvent(ev) - - #def mouseMoveEvent(self, ev): - ##print "move", ev.pos() - #if not self.movable: - #return - #self.lines[0].blockSignals(True) # only want to update once - #for l in self.lines: - #l.mouseMoveEvent(ev) - #self.lines[0].blockSignals(False) - ##self.setPos(self.mapToParent(ev.pos()) - self.pressDelta) - ##self.emit(QtCore.SIGNAL('dragged'), self) def mouseDragEvent(self, ev): if not self.movable or int(ev.button() & QtCore.Qt.LeftButton) == 0: @@ -218,12 +257,9 @@ class LinearRegionItem(UIGraphicsItem): if not self.moving: return - #delta = ev.pos() - ev.lastPos() self.lines[0].blockSignals(True) # only want to update once for i, l in enumerate(self.lines): l.setPos(self.cursorOffsets[i] + ev.pos()) - #l.setPos(l.pos()+delta) - #l.mouseDragEvent(ev) self.lines[0].blockSignals(False) self.prepareGeometryChange() @@ -242,7 +278,6 @@ class LinearRegionItem(UIGraphicsItem): self.sigRegionChanged.emit(self) self.sigRegionChangeFinished.emit(self) - def hoverEvent(self, ev): if self.movable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): self.setMouseHover(True) @@ -255,36 +290,7 @@ class LinearRegionItem(UIGraphicsItem): return self.mouseHovering = hover if hover: - c = self.brush.color() - c.setAlpha(c.alpha() * 2) - self.currentBrush = fn.mkBrush(c) + self.currentBrush = self.hoverBrush else: self.currentBrush = self.brush self.update() - - #def hoverEnterEvent(self, ev): - #print "rgn hover enter" - #ev.ignore() - #self.updateHoverBrush() - - #def hoverMoveEvent(self, ev): - #print "rgn hover move" - #ev.ignore() - #self.updateHoverBrush() - - #def hoverLeaveEvent(self, ev): - #print "rgn hover leave" - #ev.ignore() - #self.updateHoverBrush(False) - - #def updateHoverBrush(self, hover=None): - #if hover is None: - #scene = self.scene() - #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) - - #if hover: - #self.currentBrush = fn.mkBrush(255, 0,0,100) - #else: - #self.currentBrush = self.brush - #self.update() - From b5e339145306f7ca1de6909db73d54458244c8d2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 22 Sep 2017 16:44:53 -0700 Subject: [PATCH 129/607] Allow calling sip.setapi in subprocess before pyqtgraph is imported --- pyqtgraph/multiprocess/bootstrap.py | 6 ++++++ pyqtgraph/multiprocess/processes.py | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index f9cb0b0e..a8a03d41 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -22,6 +22,12 @@ if __name__ == '__main__': while len(sys.path) > 0: sys.path.pop() sys.path.extend(path) + + pyqtapis = opts.pop('pyqtapis', None) + if pyqtapis is not None: + import sip + for k,v in pyqtapis.items(): + sip.setapi(k, v) if opts.pop('pyside', False): import PySide diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 7560ff70..1be7e50b 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -39,7 +39,7 @@ class Process(RemoteEventHandler): """ _process_count = 1 # just used for assigning colors to each process for debugging - def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None): + def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None, pyqtapis=None): """ ============== ============================================================= **Arguments:** @@ -47,7 +47,7 @@ class Process(RemoteEventHandler): from the remote process. target Optional function to call after starting remote process. By default, this is startEventLoop(), which causes the remote - process to process requests from the parent process until it + process to handle requests from the parent process until it is asked to quit. If you wish to specify a different target, it must be picklable (bound methods are not). copySysPath If True, copy the contents of sys.path to the remote process. @@ -61,6 +61,8 @@ class Process(RemoteEventHandler): for a python bug: http://bugs.python.org/issue3905 but has the side effect that child output is significantly delayed relative to the parent output. + pyqtapis Optional dictionary of PyQt API version numbers to set before + importing pyqtgraph in the remote process. ============== ============================================================= """ if target is None: @@ -130,7 +132,8 @@ class Process(RemoteEventHandler): targetStr=targetStr, path=sysPath, pyside=USE_PYSIDE, - debug=procDebug + debug=procDebug, + pyqtapis=pyqtapis, ) pickle.dump(data, self.proc.stdin) self.proc.stdin.close() From 6962777b9202497fc91d201dc4ec64d7243390dd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:29:04 -0700 Subject: [PATCH 130/607] HistogramLUTItem: add rgb level mode, save/restore methods --- pyqtgraph/graphicsItems/GraphicsItem.py | 3 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 217 +++++++++++++++----- 2 files changed, 163 insertions(+), 57 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index d45818dc..f88069bc 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -146,7 +146,8 @@ class GraphicsItem(object): return parents def viewRect(self): - """Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget""" + """Return the visible bounds of this item's ViewBox or GraphicsWidget, + in the local coordinate system of the item.""" view = self.getViewBox() if view is None: return None diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 31764250..6919cfba 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -36,7 +36,7 @@ class HistogramLUTItem(GraphicsWidget): sigLevelsChanged = QtCore.Signal(object) sigLevelChangeFinished = QtCore.Signal(object) - def __init__(self, image=None, fillHistogram=True): + def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'): """ If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. @@ -44,6 +44,8 @@ class HistogramLUTItem(GraphicsWidget): GraphicsWidget.__init__(self) self.lut = None self.imageItem = lambda: None # fake a dead weakref + self.levelMode = levelMode + self.rgbHistogram = rgbHistogram self.layout = QtGui.QGraphicsGridLayout() self.setLayout(self.layout) @@ -56,9 +58,27 @@ class HistogramLUTItem(GraphicsWidget): self.gradient = GradientEditorItem() self.gradient.setOrientation('right') self.gradient.loadPreset('grey') - self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) - self.region.setZValue(1000) - self.vb.addItem(self.region) + self.regions = [ + LinearRegionItem([0, 1], 'horizontal', swapMode='block'), + LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='r', + brush=fn.mkBrush((255, 50, 50, 50)), span=(0., 1/3.)), + LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='g', + brush=fn.mkBrush((50, 255, 50, 50)), span=(1/3., 2/3.)), + LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='b', + brush=fn.mkBrush((50, 50, 255, 80)), span=(2/3., 1.)), + LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='w', + brush=fn.mkBrush((255, 255, 255, 50)), span=(2/3., 1.))] + for region in self.regions: + region.setZValue(1000) + self.vb.addItem(region) + region.lines[0].addMarker('<|', 0.5) + region.lines[1].addMarker('|>', 0.5) + region.sigRegionChanged.connect(self.regionChanging) + region.sigRegionChangeFinished.connect(self.regionChanged) + + + self.region = self.regions[0] # for backward compatibility. + self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.vb, 0, 1) @@ -71,12 +91,23 @@ class HistogramLUTItem(GraphicsWidget): #self.vb.addItem(self.grid) self.gradient.sigGradientChanged.connect(self.gradientChanged) - self.region.sigRegionChanged.connect(self.regionChanging) - self.region.sigRegionChangeFinished.connect(self.regionChanged) self.vb.sigRangeChanged.connect(self.viewRangeChanged) - self.plot = PlotDataItem() - self.plot.rotate(90) + add = QtGui.QPainter.CompositionMode_Plus + self.plots = [ + PlotCurveItem(pen=(200, 200, 200, 100)), # mono + PlotCurveItem(pen=(255, 0, 0, 100), compositionMode=add), # r + PlotCurveItem(pen=(0, 255, 0, 100), compositionMode=add), # g + PlotCurveItem(pen=(0, 0, 255, 100), compositionMode=add), # b + PlotCurveItem(pen=(200, 200, 200, 100), compositionMode=add), # a + ] + + self.plot = self.plots[0] # for backward compatibility. + for plot in self.plots: + plot.rotate(90) + self.vb.addItem(plot) + self.fillHistogram(fillHistogram) + self._showRegions() self.vb.addItem(self.plot) self.autoHistogramRange() @@ -86,25 +117,30 @@ class HistogramLUTItem(GraphicsWidget): #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): - if fill: - self.plot.setFillLevel(level) - self.plot.setFillBrush(color) - else: - self.plot.setFillLevel(None) + colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)] + for i,plot in enumerate(self.plots): + if fill: + plot.setFillLevel(level) + plot.setBrush(colors[i]) + else: + plot.setFillLevel(None) #def sizeHint(self, *args): #return QtCore.QSizeF(115, 200) def paint(self, p, *args): + if self.levelMode != 'mono': + return + pen = self.region.lines[0].pen rgn = self.getLevels() p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) - for pen in [fn.mkPen('k', width=3), pen]: + for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]: p.setPen(pen) - p.drawLine(p1, gradRect.bottomLeft()) - p.drawLine(p2, gradRect.topLeft()) + p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft()) + p.drawLine(p2 - Point(0, 5), gradRect.topLeft()) p.drawLine(gradRect.topLeft(), gradRect.topRight()) p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) #p.drawRect(self.boundingRect()) @@ -115,28 +151,9 @@ class HistogramLUTItem(GraphicsWidget): self.vb.enableAutoRange(self.vb.YAxis, False) self.vb.setYRange(mn, mx, padding) - #d = mx-mn - #mn -= d*padding - #mx += d*padding - #self.range = [mn,mx] - #self.updateRange() - #self.vb.setMouseEnabled(False, True) - #self.region.setBounds([mn,mx]) - def autoHistogramRange(self): """Enable auto-scaling on the histogram plot.""" self.vb.enableAutoRange(self.vb.XYAxes) - #self.range = None - #self.updateRange() - #self.vb.setMouseEnabled(False, False) - - #def updateRange(self): - #self.vb.autoRange() - #if self.range is not None: - #self.vb.setYRange(*self.range) - #vr = self.vb.viewRect() - - #self.region.setBounds([vr.top(), vr.bottom()]) def setImageItem(self, img): """Set an ImageItem to have its levels and LUT automatically controlled @@ -145,10 +162,8 @@ class HistogramLUTItem(GraphicsWidget): self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result - #self.gradientChanged() self.regionChanged() self.imageChanged(autoLevel=True) - #self.vb.autoRange() def viewRangeChanged(self): self.update() @@ -161,14 +176,14 @@ class HistogramLUTItem(GraphicsWidget): self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.lut = None - #if self.imageItem is not None: - #self.imageItem.setLookupTable(self.gradient.getLookupTable(512)) self.sigLookupTableChanged.emit(self) def getLookupTable(self, img=None, n=None, alpha=None): """Return a lookup table from the color gradient defined by this HistogramLUTItem. """ + if self.levelMode is not 'mono': + return None if n is None: if img.dtype == np.uint8: n = 256 @@ -182,34 +197,124 @@ class HistogramLUTItem(GraphicsWidget): if self.imageItem() is not None: self.imageItem().setLevels(self.region.getRegion()) self.sigLevelChangeFinished.emit(self) - #self.update() def regionChanging(self): if self.imageItem() is not None: - self.imageItem().setLevels(self.region.getRegion()) + self.imageItem().setLevels(self.getLevels()) self.sigLevelsChanged.emit(self) self.update() def imageChanged(self, autoLevel=False, autoRange=False): - profiler = debug.Profiler() - h = self.imageItem().getHistogram() - profiler('get histogram') - if h[0] is None: + if self.imageItem() is None: return - self.plot.setData(*h) - profiler('set plot') - if autoLevel: - mn = h[0][0] - mx = h[0][-1] - self.region.setRegion([mn, mx]) - profiler('set region') + + if self.levelMode == 'mono': + for plt in self.plots[1:]: + plt.setVisible(False) + self.plots[0].setVisible(True) + # plot one histogram for all image data + profiler = debug.Profiler() + h = self.imageItem().getHistogram() + profiler('get histogram') + if h[0] is None: + return + self.plot.setData(*h) + profiler('set plot') + if autoLevel: + mn = h[0][0] + mx = h[0][-1] + self.region.setRegion([mn, mx]) + profiler('set region') + else: + mn, mx = self.imageItem().levels + self.region.setRegion([mn, mx]) + else: + # plot one histogram for each channel + self.plots[0].setVisible(False) + ch = self.imageItem().getHistogram(perChannel=True) + if ch[0] is None: + return + for i in range(1, 5): + if len(ch) >= i: + h = ch[i-1] + self.plots[i].setVisible(True) + self.plots[i].setData(*h) + if autoLevel: + mn = h[0][0] + mx = h[0][-1] + self.region[i].setRegion([mn, mx]) + else: + # hide channels not present in image data + self.plots[i].setVisible(False) + # make sure we are displaying the correct number of channels + self._showRegions() def getLevels(self): """Return the min and max levels. """ - return self.region.getRegion() + if self.levelMode == 'mono': + return self.region.getRegion() + else: + nch = self.imageItem().channels() + if nch is None: + nch = 3 + return [r.getRegion() for r in self.regions[1:nch+1]] - def setLevels(self, mn, mx): - """Set the min and max levels. + def setLevels(self, min=None, max=None, rgba=None): + """Set the min/max (bright and dark) levels. + + Arguments may be *min* and *max* for single-channel data, or + *rgba* = [(rmin, rmax), ...] for multi-channel data. """ - self.region.setRegion([mn, mx]) + if self.levelMode == 'mono': + if min is None: + min, max = rgba[0] + assert None not in (min, max) + self.region.setRegion((min, max)) + else: + if rgba is None: + raise TypeError("Must specify rgba argument when levelMode != 'mono'.") + for i, levels in enumerate(rgba): + self.regions[i+1].setRegion(levels) + + def setLevelMode(self, mode): + """ Set the method of controlling the image levels offered to the user. + Options are 'mono' or 'rgba'. + """ + assert mode in ('mono', 'rgba') + self.levelMode = mode + self._showRegions() + self.imageChanged() + self.update() + + def _showRegions(self): + for i in range(len(self.regions)): + self.regions[i].setVisible(False) + + if self.levelMode == 'rgba': + imax = 4 + if self.imageItem() is not None: + # Only show rgb channels if connected image lacks alpha. + nch = self.imageItem().channels() + if nch is None: + nch = 3 + xdif = 1.0 / nch + for i in range(1, nch+1): + self.regions[i].setVisible(True) + self.regions[i].setSpan((i-1) * xdif, i * xdif) + self.gradient.hide() + elif self.levelMode == 'mono': + self.regions[0].setVisible(True) + self.gradient.show() + else: + raise ValueError("Unknown level mode %r" % self.levelMode) + + def saveState(self): + return { + 'gradient': self.gradient.saveState(), + 'levels': self.getLevels(), + } + + def restoreState(self, state): + self.gradient.restoreState(state['gradient']) + self.setLevels(*state['levels']) From 07d1a62bfc0d26d9242d348dee0dbb16c63a33f7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:31:28 -0700 Subject: [PATCH 131/607] ImageItem: add support for rgb handling by histogramlut --- pyqtgraph/graphicsItems/ImageItem.py | 52 +++++++++++++++++++++------- 1 file changed, 39 insertions(+), 13 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 9588c586..2ae8b812 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -98,6 +98,11 @@ class ImageItem(GraphicsObject): axis = 1 if self.axisOrder == 'col-major' else 0 return self.image.shape[axis] + def channels(self): + if self.image is None: + return None + return self.image.shape[2] if self.image.ndim == 3 else 1 + def boundingRect(self): if self.image is None: return QtCore.QRectF(0., 0., 0., 0.) @@ -348,10 +353,15 @@ class ImageItem(GraphicsObject): profile = debug.Profiler() if self.image is None or self.image.size == 0: return - if isinstance(self.lut, collections.Callable): - lut = self.lut(self.image) + + # Request a lookup table if this image has only one channel + if self.image.ndim == 2 or self.image.shape[2] == 1: + if isinstance(self.lut, collections.Callable): + lut = self.lut(self.image) + else: + lut = self.lut else: - lut = self.lut + lut = None if self.autoDownsample: # reduce dimensions of image based on screen resolution @@ -395,9 +405,12 @@ class ImageItem(GraphicsObject): lut = self._effectiveLut levels = None + # Convert single-channel image to 2D array + if image.ndim == 3 and image.shape[-1] == 1: + image = image[..., 0] + # Assume images are in column-major order for backward compatibility # (most images are in row-major order) - if self.axisOrder == 'col-major': image = image.transpose((1, 0, 2)[:image.ndim]) @@ -430,7 +443,8 @@ class ImageItem(GraphicsObject): self.render() self.qimage.save(fileName, *args) - def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds): + def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200, + targetHistogramSize=500, **kwds): """Returns x and y arrays containing the histogram values for the current image. For an explanation of the return format, see numpy.histogram(). @@ -446,6 +460,9 @@ class ImageItem(GraphicsObject): with each bin having an integer width. * All other types will have *targetHistogramSize* bins. + If *perChannel* is True, then the histogram is computed once per channel + and the output is a list of the results. + This method is also used when automatically computing levels. """ if self.image is None: @@ -458,21 +475,30 @@ class ImageItem(GraphicsObject): stepData = self.image[::step[0], ::step[1]] if bins == 'auto': + mn = stepData.min() + mx = stepData.max() if stepData.dtype.kind in "ui": - mn = stepData.min() - mx = stepData.max() + # For integer data, we select the bins carefully to avoid aliasing step = np.ceil((mx-mn) / 500.) bins = np.arange(mn, mx+1.01*step, step, dtype=np.int) - if len(bins) == 0: - bins = [mn, mx] else: - bins = 500 + # for float data, let numpy select the bins. + bins = np.linspace(mn, mx, 500) + + if len(bins) == 0: + bins = [mn, mx] kwds['bins'] = bins stepData = stepData[np.isfinite(stepData)] - hist = np.histogram(stepData, **kwds) - - return hist[1][:-1], hist[0] + if perChannel: + hist = [] + for i in range(stepData.shape[-1]): + h = np.histogram(stepData[..., i], **kwds) + hist.append((h[1][:-1], h[0])) + return hist + else: + hist = np.histogram(stepData, **kwds) + return hist[1][:-1], hist[0] def setPxMode(self, b): """ From 4a4a7383bc3cf549a7e2fb54c8a7379bd6031168 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:33:34 -0700 Subject: [PATCH 132/607] ImageView: add support for RGB levels mode --- pyqtgraph/imageview/ImageView.py | 132 +++++++++++++++++++++---------- 1 file changed, 91 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..f6cacde0 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -12,7 +12,7 @@ Widget used for displaying 2D or 3D data. Features: - ROI plotting - Image normalization through a variety of methods """ -import os +import os, sys import numpy as np from ..Qt import QtCore, QtGui, USE_PYSIDE @@ -26,6 +26,7 @@ from ..graphicsItems.ROI import * from ..graphicsItems.LinearRegionItem import * from ..graphicsItems.InfiniteLine import * from ..graphicsItems.ViewBox import * +from ..graphicsItems.VTickGroup import VTickGroup from ..graphicsItems.GradientEditorItem import addGradientListToDocstring from .. import ptime as ptime from .. import debug as debug @@ -79,7 +80,8 @@ class ImageView(QtGui.QWidget): sigTimeChanged = QtCore.Signal(object, object) sigProcessingChanged = QtCore.Signal(object) - def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args): + def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, + levelMode='mono', *args): """ By default, this class creates an :class:`ImageItem ` to display image data and a :class:`ViewBox ` to contain the ImageItem. @@ -101,6 +103,9 @@ class ImageView(QtGui.QWidget): imageItem (ImageItem) If specified, this object will be used to display the image. Must be an instance of ImageItem or other compatible object. + levelMode See the *levelMode* argument to + :func:`HistogramLUTItem.__init__() + ` ============= ========================================================= Note: to display axis ticks inside the ImageView, instantiate it @@ -109,8 +114,10 @@ class ImageView(QtGui.QWidget): pg.ImageView(view=pg.PlotItem()) """ QtGui.QWidget.__init__(self, parent, *args) - self.levelMax = 4096 - self.levelMin = 0 + self._imageLevels = None # [(min, max), ...] per channel image metrics + self.levelMin = None # min / max levels across all channels + self.levelMax = None + self.name = name self.image = None self.axes = {} @@ -118,6 +125,7 @@ class ImageView(QtGui.QWidget): self.ui = Ui_Form() self.ui.setupUi(self) self.scene = self.ui.graphicsView.scene() + self.ui.histogram.setLevelMode(levelMode) self.ignoreTimeLine = False @@ -151,13 +159,15 @@ class ImageView(QtGui.QWidget): self.normRoi.setZValue(20) self.view.addItem(self.normRoi) self.normRoi.hide() - self.roiCurve = self.ui.roiPlot.plot() - self.timeLine = InfiniteLine(0, movable=True) + self.roiCurves = [] + self.timeLine = InfiniteLine(0, movable=True, markers=[('^', 0), ('v', 1)]) self.timeLine.setPen((255, 255, 0, 200)) self.timeLine.setZValue(1) self.ui.roiPlot.addItem(self.timeLine) self.ui.splitter.setSizes([self.height()-35, 35]) self.ui.roiPlot.hideAxis('left') + self.frameTicks = VTickGroup(yrange=[0.8, 1], pen=0.4) + self.ui.roiPlot.addItem(self.frameTicks, ignoreBounds=True) self.keysPressed = {} self.playTimer = QtCore.QTimer() @@ -200,7 +210,7 @@ class ImageView(QtGui.QWidget): self.roiClicked() ## initialize roi plot to correct shape / visibility - def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True): + def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True, levelMode=None): """ Set the image to be displayed in the widget. @@ -208,8 +218,9 @@ class ImageView(QtGui.QWidget): **Arguments:** img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and *notes* below. - xvals (numpy array) 1D array of z-axis values corresponding to the third axis - in a 3D image. For video, this array should contain the time of each frame. + xvals (numpy array) 1D array of z-axis values corresponding to the first axis + in a 3D image. For video, this array should contain the time of each + frame. autoRange (bool) whether to scale/pan the view to fit the image. autoLevels (bool) whether to update the white/black levels to fit the image. levels (min, max); the white and black level values to use. @@ -224,7 +235,11 @@ class ImageView(QtGui.QWidget): and *scale*. autoHistogramRange If True, the histogram y-range is automatically scaled to fit the image data. - ================== =========================================================================== + levelMode If specified, this sets the user interaction mode for setting image + levels. Options are 'mono', which provides a single level control for + all image channels, and 'rgb' or 'rgba', which provide individual + controls for each channel. + ================== ======================================================================= **Notes:** @@ -252,6 +267,8 @@ class ImageView(QtGui.QWidget): self.image = img self.imageDisp = None + if levelMode is not None: + self.ui.histogram.setLevelMode(levelMode) profiler() @@ -310,10 +327,9 @@ class ImageView(QtGui.QWidget): profiler() if self.axes['t'] is not None: - #self.ui.roiPlot.show() self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) + self.frameTicks.setXVals(self.tVals) self.timeLine.setValue(0) - #self.ui.roiPlot.setMouseEnabled(False, False) if len(self.tVals) > 1: start = self.tVals.min() stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 @@ -325,8 +341,7 @@ class ImageView(QtGui.QWidget): stop = 1 for s in [self.timeLine, self.normRgn]: s.setBounds([start, stop]) - #else: - #self.ui.roiPlot.hide() + profiler() self.imageItem.resetTransform() @@ -364,11 +379,14 @@ class ImageView(QtGui.QWidget): def autoLevels(self): """Set the min/max intensity levels automatically to match the image data.""" - self.setLevels(self.levelMin, self.levelMax) + self.setLevels(rgba=self._imageLevels) - def setLevels(self, min, max): - """Set the min/max (bright and dark) levels.""" - self.ui.histogram.setLevels(min, max) + def setLevels(self, *args, **kwds): + """Set the min/max (bright and dark) levels. + + See :func:`HistogramLUTItem.setLevels `. + """ + self.ui.histogram.setLevels(*args, **kwds) def autoRange(self): """Auto scale and pan the view around the image such that the image fills the view.""" @@ -377,12 +395,13 @@ class ImageView(QtGui.QWidget): def getProcessedImage(self): """Returns the image data after it has been processed by any normalization options in use. - This method also sets the attributes self.levelMin and self.levelMax - to indicate the range of data in the image.""" + """ if self.imageDisp is None: image = self.normalize(self.image) self.imageDisp = image - self.levelMin, self.levelMax = list(map(float, self.quickMinMax(self.imageDisp))) + self._imageLevels = self.quickMinMax(self.imageDisp) + self.levelMin = min([level[0] for level in self._imageLevels]) + self.levelMax = max([level[1] for level in self._imageLevels]) return self.imageDisp @@ -527,13 +546,15 @@ class ImageView(QtGui.QWidget): #self.ui.roiPlot.show() self.ui.roiPlot.setMouseEnabled(True, True) self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4]) - self.roiCurve.show() + for c in self.roiCurves: + c.show() self.roiChanged() self.ui.roiPlot.showAxis('left') else: self.roi.hide() self.ui.roiPlot.setMouseEnabled(False, False) - self.roiCurve.hide() + for c in self.roiCurves: + c.hide() self.ui.roiPlot.hideAxis('left') if self.hasTimeAxis(): @@ -557,36 +578,65 @@ class ImageView(QtGui.QWidget): return image = self.getProcessedImage() - if image.ndim == 2: - axes = (0, 1) - elif image.ndim == 3: - axes = (1, 2) - else: - return - + + # Extract image data from ROI + axes = (self.axes['x'], self.axes['y']) + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) - if data is not None: - while data.ndim > 1: - data = data.mean(axis=1) - if image.ndim == 3: - self.roiCurve.setData(y=data, x=self.tVals) + if data is None: + return + + # Convert extracted data into 1D plot data + if self.axes['t'] is None: + # Average across y-axis of ROI + data = data.mean(axis=axes[1]) + coords = coords[:,:,0] - coords[:,0:1,0] + xvals = (coords**2).sum(axis=0) ** 0.5 + else: + # Average data within entire ROI for each frame + data = data.mean(axis=max(axes)).mean(axis=min(axes)) + xvals = self.tVals + + # Handle multi-channel data + if data.ndim == 1: + plots = [(xvals, data, 'w')] + if data.ndim == 2: + if data.shape[1] == 1: + colors = 'w' else: - while coords.ndim > 2: - coords = coords[:,:,0] - coords = coords - coords[:,0,np.newaxis] - xvals = (coords**2).sum(axis=0) ** 0.5 - self.roiCurve.setData(y=data, x=xvals) + colors = 'rgbw' + plots = [] + for i in range(data.shape[1]): + d = data[:,i] + plots.append((xvals, d, colors[i])) + + # Update plot line(s) + while len(plots) < len(self.roiCurves): + c = self.roiCurves.pop() + c.scene().removeItem(c) + while len(plots) > len(self.roiCurves): + self.roiCurves.append(self.ui.roiPlot.plot()) + for i in range(len(plots)): + x, y, p = plots[i] + self.roiCurves[i].setData(x, y, pen=p) def quickMinMax(self, data): """ Estimate the min/max values of *data* by subsampling. + Returns [(min, max), ...] with one item per channel """ while data.size > 1e6: ax = np.argmax(data.shape) sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) data = data[sl] - return nanmin(data), nanmax(data) + + cax = self.axes['c'] + if cax is None: + return [(float(nanmin(data)), float(nanmax(data)))] + else: + return [(float(nanmin(data.take(i, axis=cax))), + float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])] def normalize(self, image): """ From bde358ffaf17d6ec401d06a51c9df123a2839a56 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:34:37 -0700 Subject: [PATCH 133/607] Fix colormapwidget.restorestate --- pyqtgraph/widgets/ColorMapWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index f6e28960..bd5668ae 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -152,7 +152,7 @@ class ColorMapParameter(ptree.types.GroupParameter): def restoreState(self, state): if 'fields' in state: self.setFields(state['fields']) - for itemState in state['items']: + for name, itemState in state['items'].items(): item = self.addNew(itemState['field']) item.restoreState(itemState) From 6e22524ac28f484ff74d12853b0a5bf6ba6b0fb2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 26 Sep 2017 08:50:31 -0700 Subject: [PATCH 134/607] Update histogramlut example to allow rgb mode --- examples/HistogramLUT.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/HistogramLUT.py b/examples/HistogramLUT.py index 4d89dd3f..082a963c 100644 --- a/examples/HistogramLUT.py +++ b/examples/HistogramLUT.py @@ -28,19 +28,27 @@ v = pg.GraphicsView() vb = pg.ViewBox() vb.setAspectLocked() v.setCentralItem(vb) -l.addWidget(v, 0, 0) +l.addWidget(v, 0, 0, 3, 1) w = pg.HistogramLUTWidget() l.addWidget(w, 0, 1) -data = pg.gaussianFilter(np.random.normal(size=(256, 256)), (20, 20)) +monoRadio = QtGui.QRadioButton('mono') +rgbaRadio = QtGui.QRadioButton('rgba') +l.addWidget(monoRadio, 1, 1) +l.addWidget(rgbaRadio, 2, 1) +monoRadio.setChecked(True) + +def setLevelMode(): + mode = 'mono' if monoRadio.isChecked() else 'rgba' + w.setLevelMode(mode) +monoRadio.toggled.connect(setLevelMode) + +data = pg.gaussianFilter(np.random.normal(size=(256, 256, 3)), (20, 20, 0)) for i in range(32): for j in range(32): data[i*8, j*8] += .1 img = pg.ImageItem(data) -#data2 = np.zeros((2,) + data.shape + (2,)) -#data2[0,:,:,0] = data ## make non-contiguous array for testing purposes -#img = pg.ImageItem(data2[0,:,:,0]) vb.addItem(img) vb.autoRange() From ef9871885193ebc562169fac38cb7abdeb7f4ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arthur=20Crippa=20B=C3=BArigo?= Date: Wed, 27 Sep 2017 22:13:59 -0300 Subject: [PATCH 135/607] BarGraphItem can plot horizontal bars. Proposed fix to https://github.com/pyqtgraph/pyqtgraph/issues/576 --- pyqtgraph/graphicsItems/BarGraphItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index a1d5d029..657222ba 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -120,7 +120,7 @@ class BarGraphItem(GraphicsObject): p.setPen(fn.mkPen(pen)) p.setBrush(fn.mkBrush(brush)) - for i in range(len(x0)): + for i in range(len(x0 if not np.isscalar(x0) else y0)): if pens is not None: p.setPen(fn.mkPen(pens[i])) if brushes is not None: From 6cd94402992d67e74abc79f5388cf2e3769e2d2e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 08:56:06 -0700 Subject: [PATCH 136/607] LegendItem: make it possible to remove items directly, rather than by name --- pyqtgraph/graphicsItems/LegendItem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 20d6416e..2c0114a7 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -81,19 +81,19 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.layout.addItem(label, row, 1) self.updateSize() - def removeItem(self, name): + def removeItem(self, item): """ Removes one item from the legend. ============== ======================================================== **Arguments:** - title The title displayed for this item. + item The item to remove or its name. ============== ======================================================== """ # Thanks, Ulrich! # cycle for a match for sample, label in self.items: - if label.text == name: # hit + if sample.item is item or label.text == item: self.items.remove( (sample, label) ) # remove from itemlist self.layout.removeItem(sample) # remove from layout sample.close() # remove from drawing @@ -130,7 +130,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): if ev.button() == QtCore.Qt.LeftButton: dpos = ev.pos() - ev.lastPos() self.autoAnchor(self.pos() + dpos) - + + class ItemSample(GraphicsWidget): """ Class responsible for drawing a single item in a LegendItem (sans label). From e885236bd5671b34e2f6fd4ca0fe99bede919e68 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 08:57:42 -0700 Subject: [PATCH 137/607] Add PlotCurveItem composition mode --- pyqtgraph/graphicsItems/PlotCurveItem.py | 32 +++++++++++++++++++++--- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index fac9ee57..9b4e95ef 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -68,6 +68,7 @@ class PlotCurveItem(GraphicsObject): 'antialias': getConfigOption('antialias'), 'connect': 'all', 'mouseWidth': 8, # width of shape responding to mouse click + 'compositionMode': None, } self.setClickable(kargs.get('clickable', False)) self.setData(*args, **kargs) @@ -93,6 +94,24 @@ class PlotCurveItem(GraphicsObject): self._mouseShape = None self._boundingRect = None + def setCompositionMode(self, mode): + """Change the composition mode of the item (see QPainter::CompositionMode + in the Qt documentation). This is useful when overlaying multiple items. + + ============================================ ============================================================ + **Most common arguments:** + QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it + is opaque. Otherwise, it uses the alpha channel to blend + the image with the background. + QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to + reflect the lightness or darkness of the background. + QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels + are added together. + QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. + ============================================ ============================================================ + """ + self.opts['compositionMode'] = mode + self.update() def getData(self): return self.xData, self.yData @@ -274,7 +293,7 @@ class PlotCurveItem(GraphicsObject): def setData(self, *args, **kargs): """ - ============== ======================================================== + =============== ======================================================== **Arguments:** x, y (numpy arrays) Data to show pen Pen to use when drawing. Any single argument accepted by @@ -298,7 +317,9 @@ class PlotCurveItem(GraphicsObject): to be drawn. "finite" causes segments to be omitted if they are attached to nan or inf values. For any other connectivity, specify an array of boolean values. - ============== ======================================================== + compositionMode See :func:`setCompositionMode + `. + =============== ======================================================== If non-keyword arguments are used, they will be interpreted as setData(y) for a single argument and setData(x, y) for two @@ -311,6 +332,9 @@ class PlotCurveItem(GraphicsObject): def updateData(self, *args, **kargs): profiler = debug.Profiler() + if 'compositionMode' in kargs: + self.setCompositionMode(kargs['compositionMode']) + if len(args) == 1: kargs['y'] = args[0] elif len(args) == 2: @@ -430,7 +454,6 @@ class PlotCurveItem(GraphicsObject): x = None y = None path = self.getPath() - profiler('generate path') if self._exportOpts is not False: @@ -440,6 +463,9 @@ class PlotCurveItem(GraphicsObject): p.setRenderHint(p.Antialiasing, aa) + cmode = self.opts['compositionMode'] + if cmode is not None: + p.setCompositionMode(cmode) if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: if self.fillPath is None: From 3c2c970a6b74594b274d27c26b81130840a91ef3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:00:57 -0700 Subject: [PATCH 138/607] Remove spiral ROI --- pyqtgraph/graphicsItems/ROI.py | 76 +--------------------------------- 1 file changed, 2 insertions(+), 74 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 963ecb05..bc77e1c3 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -26,7 +26,8 @@ from .. import getConfigOption __all__ = [ 'ROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', - 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI', + 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', + 'CrosshairROI', ] @@ -2157,79 +2158,6 @@ class _PolyLineSegment(LineSegmentROI): return LineSegmentROI.hoverEvent(self, ev) -class SpiralROI(ROI): - def __init__(self, pos=None, size=None, **args): - if size == None: - size = [100e-6,100e-6] - if pos == None: - pos = [0,0] - ROI.__init__(self, pos, size, **args) - self.translateSnap = False - self.addFreeHandle([0.25,0], name='a') - self.addRotateFreeHandle([1,0], [0,0], name='r') - #self.getRadius() - #QtCore.connect(self, QtCore.SIGNAL('regionChanged'), self. - - - def getRadius(self): - radius = Point(self.handles[1]['item'].pos()).length() - #r2 = radius[1] - #r3 = r2[0] - return radius - - def boundingRect(self): - r = self.getRadius() - return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r) - #return self.bounds - - #def movePoint(self, *args, **kargs): - #ROI.movePoint(self, *args, **kargs) - #self.prepareGeometryChange() - #for h in self.handles: - #h['pos'] = h['item'].pos()/self.state['size'][0] - - def stateChanged(self, finish=True): - ROI.stateChanged(self, finish=finish) - if len(self.handles) > 1: - self.path = QtGui.QPainterPath() - h0 = Point(self.handles[0]['item'].pos()).length() - a = h0/(2.0*np.pi) - theta = 30.0*(2.0*np.pi)/360.0 - self.path.moveTo(QtCore.QPointF(a*theta*cos(theta), a*theta*sin(theta))) - x0 = a*theta*cos(theta) - y0 = a*theta*sin(theta) - radius = self.getRadius() - theta += 20.0*(2.0*np.pi)/360.0 - i = 0 - while Point(x0, y0).length() < radius and i < 1000: - x1 = a*theta*cos(theta) - y1 = a*theta*sin(theta) - self.path.lineTo(QtCore.QPointF(x1,y1)) - theta += 20.0*(2.0*np.pi)/360.0 - x0 = x1 - y0 = y1 - i += 1 - - - return self.path - - - def shape(self): - p = QtGui.QPainterPath() - p.addEllipse(self.boundingRect()) - return p - - def paint(self, p, *args): - p.setRenderHint(QtGui.QPainter.Antialiasing) - #path = self.shape() - p.setPen(self.currentPen) - p.drawPath(self.path) - p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) - p.drawPath(self.shape()) - p.setPen(QtGui.QPen(QtGui.QColor(0,0,255))) - p.drawRect(self.boundingRect()) - - class CrosshairROI(ROI): """A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable.""" From 4d0f3b5821aac9e0ee96cb645973d3f217e65728 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:03:24 -0700 Subject: [PATCH 139/607] Code cleanup --- pyqtgraph/graphicsItems/ROI.py | 68 ++-------------------------------- 1 file changed, 4 insertions(+), 64 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index bc77e1c3..6f0a46a4 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -239,6 +239,7 @@ class ROI(GraphicsObject): if isinstance(y, bool): raise TypeError("Positional arguments to setPos() must be numerical.") pos = Point(pos, y) + self.state['pos'] = pos QtGui.QGraphicsItem.setPos(self, pos) if update: @@ -253,7 +254,7 @@ class ROI(GraphicsObject): self.state['size'] = size if update: self.stateChanged(finish=finish) - + def setAngle(self, angle, update=True, finish=True): """Set the angle of rotation (in degrees) for this ROI. See setPos() for an explanation of the update and finish arguments. @@ -757,11 +758,6 @@ class ROI(GraphicsObject): else: raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.") - - ## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why. - #p0 = self.mapSceneToParent(p0) - #p1 = self.mapSceneToParent(p1) - ## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1) if 'center' in h: c = h['center'] @@ -771,8 +767,6 @@ class ROI(GraphicsObject): if h['type'] == 't': snap = True if (modifiers & QtCore.Qt.ControlModifier) else None - #if self.translateSnap or (): - #snap = Point(self.snapSize, self.snapSize) self.translate(p1-p0, snap=snap, update=False) elif h['type'] == 'f': @@ -780,7 +774,6 @@ class ROI(GraphicsObject): h['item'].setPos(newPos) h['pos'] = newPos self.freeHandleMoved = True - #self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged() elif h['type'] == 's': ## If a handle and its center have the same x or y value, we can't scale across that axis. @@ -922,10 +915,7 @@ class ROI(GraphicsObject): r = self.stateRect(newState) if not self.maxBounds.contains(r): return - #self.setTransform(tr) - #self.setPos(newState['pos'], update=False) - #self.prepareGeometryChange() - #self.state = newState + self.setState(newState, update=False) self.stateChanged(finish=finish) @@ -1761,6 +1751,7 @@ class EllipseROI(ROI): return arr w = arr.shape[axes[0]] h = arr.shape[axes[1]] + ## generate an ellipsoidal mask mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) @@ -2179,16 +2170,8 @@ class CrosshairROI(ROI): self.prepareGeometryChange() def boundingRect(self): - #size = self.size() - #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized() return self.shape().boundingRect() - #def getRect(self): - ### same as boundingRect -- for internal use so that boundingRect can be re-implemented in subclasses - #size = self.size() - #return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized() - - def shape(self): if self._shape is None: radius = self.getState()['size'][1] @@ -2202,56 +2185,13 @@ class CrosshairROI(ROI): stroker.setWidth(10) outline = stroker.createStroke(p) self._shape = self.mapFromDevice(outline) - - - ##h1 = self.handles[0]['item'].pos() - ##h2 = self.handles[1]['item'].pos() - #w1 = Point(-0.5, 0)*self.size() - #w2 = Point(0.5, 0)*self.size() - #h1 = Point(0, -0.5)*self.size() - #h2 = Point(0, 0.5)*self.size() - - #dh = h2-h1 - #dw = w2-w1 - #if dh.length() == 0 or dw.length() == 0: - #return p - #pxv = self.pixelVectors(dh)[1] - #if pxv is None: - #return p - - #pxv *= 4 - - #p.moveTo(h1+pxv) - #p.lineTo(h2+pxv) - #p.lineTo(h2-pxv) - #p.lineTo(h1-pxv) - #p.lineTo(h1+pxv) - - #pxv = self.pixelVectors(dw)[1] - #if pxv is None: - #return p - - #pxv *= 4 - - #p.moveTo(w1+pxv) - #p.lineTo(w2+pxv) - #p.lineTo(w2-pxv) - #p.lineTo(w1-pxv) - #p.lineTo(w1+pxv) return self._shape def paint(self, p, *args): - #p.save() - #r = self.getRect() radius = self.getState()['size'][1] p.setRenderHint(QtGui.QPainter.Antialiasing) p.setPen(self.currentPen) - #p.translate(r.left(), r.top()) - #p.scale(r.width()/10., r.height()/10.) ## need to scale up a little because drawLine has trouble dealing with 0.5 - #p.drawLine(0,5, 10,5) - #p.drawLine(5,0, 5,10) - #p.restore() p.drawLine(Point(0, -radius), Point(0, radius)) p.drawLine(Point(-radius, 0), Point(radius, 0)) From 97b71a2b284664ed8db052fb3799434a3973f09f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:03:47 -0700 Subject: [PATCH 140/607] Add RulerROI --- pyqtgraph/graphicsItems/ROI.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 6f0a46a4..473506cf 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2197,3 +2197,31 @@ class CrosshairROI(ROI): p.drawLine(Point(-radius, 0), Point(radius, 0)) +class RulerROI(LineSegmentROI): + def paint(self, p, *args): + LineSegmentROI.paint(self, p, *args) + h1 = self.handles[0]['item'].pos() + h2 = self.handles[1]['item'].pos() + p1 = p.transform().map(h1) + p2 = p.transform().map(h2) + + vec = Point(h2) - Point(h1) + length = vec.length() + angle = vec.angle(Point(1, 0)) + + pvec = p2 - p1 + pvecT = Point(pvec.y(), -pvec.x()) + pos = 0.5 * (p1 + p2) + pvecT * 40 / pvecT.length() + + p.resetTransform() + + txt = fn.siFormat(length, suffix='m') + '\n%0.1f deg' % angle + p.drawText(QtCore.QRectF(pos.x()-50, pos.y()-50, 100, 100), QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter, txt) + + def boundingRect(self): + r = LineSegmentROI.boundingRect(self) + pxl = self.pixelLength(Point([1, 0])) + if pxl is None: + return r + pxw = 50 * pxl + return r.adjusted(-50, -50, 50, 50) From 0de0bf4c44e5600303c5fdf317bf498d120ac947 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:05:08 -0700 Subject: [PATCH 141/607] Fix: very small ellipse/circle ROIs have bad click areas --- pyqtgraph/graphicsItems/ROI.py | 42 ++++++++++++++++++++++++++++++---- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 473506cf..850104c0 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1725,10 +1725,18 @@ class EllipseROI(ROI): """ def __init__(self, pos, size, **args): #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) + self.path = None ROI.__init__(self, pos, size, **args) + self.sigRegionChanged.connect(self._clearPath) + self._addHandles() + + def _addHandles(self): self.addRotateHandle([1.0, 0.5], [0.5, 0.5]) self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) + def _clearPath(self): + self.path = None + def paint(self, p, opt, widget): r = self.boundingRect() p.setRenderHint(QtGui.QPainter.Antialiasing) @@ -1764,8 +1772,27 @@ class EllipseROI(ROI): return arr * mask def shape(self): - self.path = QtGui.QPainterPath() - self.path.addEllipse(self.boundingRect()) + if self.path is None: + path = QtGui.QPainterPath() + + # Note: Qt has a bug where very small ellipses (radius <0.001) do + # not correctly intersect with mouse position (upper-left and + # lower-right quadrants are not clickable). + #path.addEllipse(self.boundingRect()) + + # Workaround: manually draw the path. + br = self.boundingRect() + center = br.center() + r1 = br.width() / 2. + r2 = br.height() / 2. + theta = np.linspace(0, 2*np.pi, 24) + x = center.x() + r1 * np.cos(theta) + y = center.y() + r2 * np.sin(theta) + path.moveTo(x[0], y[0]) + for i in range(1, len(x)): + path.lineTo(x[i], y[i]) + self.path = path + return self.path @@ -1782,10 +1809,15 @@ class CircleROI(EllipseROI): ============== ============================================================= """ - def __init__(self, pos, size, **args): - ROI.__init__(self, pos, size, **args) + def __init__(self, pos, size=None, radius=None, **args): + if size is None: + if radius is None: + raise TypeError("Must provide either size or radius.") + size = (radius*2, radius*2) + EllipseROI.__init__(self, pos, size, **args) self.aspectLocked = True - #self.addTranslateHandle([0.5, 0.5]) + + def _addHandles(self): self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) From 60ce541df6aa0bda66bc07a3894ab0e34f556a57 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:05:31 -0700 Subject: [PATCH 142/607] minor argument type checking --- pyqtgraph/graphicsItems/ROI.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 850104c0..9f3dae6f 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -232,6 +232,9 @@ class ROI(GraphicsObject): multiple change functions to be called sequentially while minimizing processing overhead and repeated signals. Setting ``update=False`` also forces ``finish=False``. """ + if update not in (True, False): + raise TypeError("update argument must be bool") + if y is None: pos = Point(pos) else: @@ -249,6 +252,8 @@ class ROI(GraphicsObject): """Set the size of the ROI. May be specified as a QPoint, Point, or list of two values. See setPos() for an explanation of the update and finish arguments. """ + if update not in (True, False): + raise TypeError("update argument must be bool") size = Point(size) self.prepareGeometryChange() self.state['size'] = size @@ -259,6 +264,8 @@ class ROI(GraphicsObject): """Set the angle of rotation (in degrees) for this ROI. See setPos() for an explanation of the update and finish arguments. """ + if update not in (True, False): + raise TypeError("update argument must be bool") self.state['angle'] = angle tr = QtGui.QTransform() #tr.rotate(-angle * 180 / np.pi) From 2a56435475a1cd97b69110011e303624a4e7677e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:09:17 -0700 Subject: [PATCH 143/607] MetaArray: make it possible to append multiple axis values Example use case: taking an image stack where each frame has a time value AND a position. Previously we could only append new time values. --- pyqtgraph/metaarray/MetaArray.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 66ecc460..15d374a6 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -748,7 +748,6 @@ class MetaArray(object): else: fd.seek(0) meta = MetaArray._readMeta(fd) - if not kwargs.get("readAllData", True): self._data = np.empty(meta['shape'], dtype=meta['type']) if 'version' in meta: @@ -1031,6 +1030,7 @@ class MetaArray(object): """Write this object to a file. The object can be restored by calling MetaArray(file=fileName) opts: appendAxis: the name (or index) of the appendable axis. Allows the array to grow. + appendKeys: a list of keys (other than "values") for metadata to append to on the appendable axis. compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. chunks: bool or tuple specifying chunk shape """ @@ -1096,7 +1096,6 @@ class MetaArray(object): 'chunks': None, 'compression': None } - ## set maximum shape to allow expansion along appendAxis append = False @@ -1125,14 +1124,19 @@ class MetaArray(object): data[tuple(sl)] = self.view(np.ndarray) ## add axis values if they are present. + axKeys = ["values"] + axKeys.extend(opts.get("appendKeys", [])) axInfo = f['info'][str(ax)] - if 'values' in axInfo: - v = axInfo['values'] - v2 = self._info[ax]['values'] - shape = list(v.shape) - shape[0] += v2.shape[0] - v.resize(shape) - v[-v2.shape[0]:] = v2 + for key in axKeys: + if key in axInfo: + v = axInfo[key] + v2 = self._info[ax][key] + shape = list(v.shape) + shape[0] += v2.shape[0] + v.resize(shape) + v[-v2.shape[0]:] = v2 + else: + raise TypeError('Cannot append to axis info key "%s"; this key is not present in the target file.' % key) f.close() else: f = h5py.File(fileName, 'w') From aad1c737c34b2bf663017493652838a516ee7a01 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 Sep 2017 09:11:07 -0700 Subject: [PATCH 144/607] eq(): better performance by avoiding array comparison when shapes do not match --- pyqtgraph/functions.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 1aed6ace..261727cb 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -417,7 +417,21 @@ def eq(a, b): """ if a is b: return True - + + # Avoid comparing large arrays against scalars; this is expensive and we know it should return False. + aIsArr = isinstance(a, (np.ndarray, MetaArray)) + bIsArr = isinstance(b, (np.ndarray, MetaArray)) + if (aIsArr or bIsArr) and type(a) != type(b): + return False + + # If both inputs are arrays, we can speeed up comparison if shapes / dtypes don't match + # NOTE: arrays of dissimilar type should be considered unequal even if they are numerically + # equal because they may behave differently when computed on. + if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype): + return False + + # Test for equivalence. + # If the test raises a recognized exception, then return Falase try: try: # Sometimes running catch_warnings(module=np) generates AttributeError ??? From 7c9107fa5d9954d2f59bcabf523dc2fdb77eee34 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:45:33 -0700 Subject: [PATCH 145/607] use ndarray() strides argument to construct subarray previously this was done manually (and imperfectly) --- pyqtgraph/functions.py | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 261727cb..b38888fe 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -750,23 +750,15 @@ def subArray(data, offset, shape, stride): #data = data.flatten() data = data[offset:] shape = tuple(shape) - stride = tuple(stride) extraShape = data.shape[1:] - #print data.shape, offset, shape, stride - for i in range(len(shape)): - mask = (slice(None),) * i + (slice(None, shape[i] * stride[i]),) - newShape = shape[:i+1] - if i < len(shape)-1: - newShape += (stride[i],) - newShape += extraShape - #print i, mask, newShape - #print "start:\n", data.shape, data - data = data[mask] - #print "mask:\n", data.shape, data - data = data.reshape(newShape) - #print "reshape:\n", data.shape, data + + strides = list(data.strides[::-1]) + itemsize = strides[-1] + for s in stride[1::-1]: + strides.append(itemsize * s) + strides = tuple(strides[::-1]) - return data + return np.ndarray(buffer=data, shape=shape+extraShape, strides=strides, dtype=data.dtype) def transformToArray(tr): From a2bb944e789bae5cbcbcd262c0286081be8b051f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:48:35 -0700 Subject: [PATCH 146/607] Make PathButton margin customizable --- pyqtgraph/widgets/PathButton.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/widgets/PathButton.py b/pyqtgraph/widgets/PathButton.py index 52c60e20..ee2e0bca 100644 --- a/pyqtgraph/widgets/PathButton.py +++ b/pyqtgraph/widgets/PathButton.py @@ -5,9 +5,11 @@ __all__ = ['PathButton'] class PathButton(QtGui.QPushButton): - """Simple PushButton extension which paints a QPainterPath on its face""" - def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30)): + """Simple PushButton extension that paints a QPainterPath centered on its face. + """ + def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30), margin=7): QtGui.QPushButton.__init__(self, parent) + self.margin = margin self.path = None if pen == 'default': pen = 'k' @@ -19,7 +21,6 @@ class PathButton(QtGui.QPushButton): self.setFixedWidth(size[0]) self.setFixedHeight(size[1]) - def setBrush(self, brush): self.brush = fn.mkBrush(brush) @@ -32,7 +33,7 @@ class PathButton(QtGui.QPushButton): def paintEvent(self, ev): QtGui.QPushButton.paintEvent(self, ev) - margin = 7 + margin = self.margin geom = QtCore.QRectF(0, 0, self.width(), self.height()).adjusted(margin, margin, -margin, -margin) rect = self.path.boundingRect() scale = min(geom.width() / float(rect.width()), geom.height() / float(rect.height())) From e7a92f4720e9ddf542d24e63aa600d537a754790 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:50:12 -0700 Subject: [PATCH 147/607] Add Combobox save/restoreState methods Also allow tuple as input type in addition to list --- pyqtgraph/widgets/ComboBox.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/ComboBox.py b/pyqtgraph/widgets/ComboBox.py index a6828959..6f184c5f 100644 --- a/pyqtgraph/widgets/ComboBox.py +++ b/pyqtgraph/widgets/ComboBox.py @@ -102,7 +102,7 @@ class ComboBox(QtGui.QComboBox): @blockIfUnchanged def setItems(self, items): """ - *items* may be a list or a dict. + *items* may be a list, a tuple, or a dict. If a dict is given, then the keys are used to populate the combo box and the values will be used for both value() and setValue(). """ @@ -191,13 +191,13 @@ class ComboBox(QtGui.QComboBox): @ignoreIndexChange @blockIfUnchanged def addItems(self, items): - if isinstance(items, list): + if isinstance(items, list) or isinstance(items, tuple): texts = items items = dict([(x, x) for x in items]) elif isinstance(items, dict): texts = list(items.keys()) else: - raise TypeError("items argument must be list or dict (got %s)." % type(items)) + raise TypeError("items argument must be list or dict or tuple (got %s)." % type(items)) for t in texts: if t in self._items: @@ -216,3 +216,30 @@ class ComboBox(QtGui.QComboBox): QtGui.QComboBox.clear(self) self.itemsChanged() + def saveState(self): + ind = self.currentIndex() + data = self.itemData(ind) + #if not data.isValid(): + if data is not None: + try: + if not data.isValid(): + data = None + else: + data = data.toInt()[0] + except AttributeError: + pass + if data is None: + return asUnicode(self.itemText(ind)) + else: + return data + + def restoreState(self, v): + if type(v) is int: + ind = self.findData(v) + if ind > -1: + self.setCurrentIndex(ind) + return + self.setCurrentIndex(self.findText(str(v))) + + def widgetGroupInterface(self): + return (self.currentIndexChanged, self.saveState, self.restoreState) From 3609f9df3edf384be7b80c7bcc15a5fb478d877a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:51:26 -0700 Subject: [PATCH 148/607] Fix colormapwidget saveState --- pyqtgraph/widgets/ColorMapWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index f6e28960..bd5668ae 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -152,7 +152,7 @@ class ColorMapParameter(ptree.types.GroupParameter): def restoreState(self, state): if 'fields' in state: self.setFields(state['fields']) - for itemState in state['items']: + for name, itemState in state['items'].items(): item = self.addNew(itemState['field']) item.restoreState(itemState) From 0f910c45d1338f47aaaad021790dc199fafd16f6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:54:33 -0700 Subject: [PATCH 149/607] Make parameter name,value inint args go through setValue and setName --- pyqtgraph/parametertree/Parameter.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index 4ca80ffe..d48fee57 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -162,7 +162,11 @@ class Parameter(QtCore.QObject): 'title': None, #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. } + value = opts.get('value', None) + name = opts.get('name', None) self.opts.update(opts) + self.opts['value'] = None # will be set later. + self.opts['name'] = None self.childs = [] self.names = {} ## map name:child @@ -172,17 +176,19 @@ class Parameter(QtCore.QObject): self.blockTreeChangeEmit = 0 #self.monitoringChildren = False ## prevent calling monitorChildren more than once - if 'value' not in self.opts: - self.opts['value'] = None - - if 'name' not in self.opts or not isinstance(self.opts['name'], basestring): + if not isinstance(name, basestring): raise Exception("Parameter must have a string name specified in opts.") - self.setName(opts['name']) + self.setName(name) self.addChildren(self.opts.get('children', [])) - - if 'value' in self.opts and 'default' not in self.opts: - self.opts['default'] = self.opts['value'] + + self.opts['value'] = None + if value is not None: + self.setValue(value) + + if 'default' not in self.opts: + self.opts['default'] = None + self.setDefault(self.opts['value']) ## Connect all state changed signals to the general sigStateChanged self.sigValueChanged.connect(lambda param, data: self.emitStateChanged('value', data)) From bf31a5ba99ca11cb75c50492bff281bb125aa09a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:55:36 -0700 Subject: [PATCH 150/607] Parameter.child raises KeyError if requested child name does not exist --- pyqtgraph/parametertree/Parameter.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index d48fee57..e28085bf 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -653,18 +653,19 @@ class Parameter(QtCore.QObject): """Return a child parameter. Accepts the name of the child or a tuple (path, to, child) - Added in version 0.9.9. Ealier versions used the 'param' method, which is still - implemented for backward compatibility.""" + Added in version 0.9.9. Earlier versions used the 'param' method, which is still + implemented for backward compatibility. + """ try: param = self.names[names[0]] except KeyError: - raise Exception("Parameter %s has no child named %s" % (self.name(), names[0])) + raise KeyError("Parameter %s has no child named %s" % (self.name(), names[0])) if len(names) > 1: - return param.param(*names[1:]) + return param.child(*names[1:]) else: return param - + def param(self, *names): # for backward compatibility. return self.child(*names) From 09b8e662b17307eee22b969de5cbe23ef134cc04 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:56:28 -0700 Subject: [PATCH 151/607] systemsolver: minor fixes --- pyqtgraph/parametertree/SystemSolver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py index 24e35e9a..c804d50a 100644 --- a/pyqtgraph/parametertree/SystemSolver.py +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -177,7 +177,7 @@ class SystemSolver(object): raise TypeError("constraint must be None, True, 'fixed', or tuple. (got %s)" % constraint) # type checking / massaging - if var[1] is np.ndarray: + if var[1] is np.ndarray and value is not None: value = np.array(value, dtype=float) elif var[1] in (int, float, tuple) and value is not None: value = var[1](value) @@ -185,9 +185,9 @@ class SystemSolver(object): # constraint checks if constraint is True and not self.check_constraint(name, value): raise ValueError("Setting %s = %s violates constraint %s" % (name, value, var[2])) - + # invalidate other dependent values - if var[0] is not None: + if var[0] is not None or value is None: # todo: we can make this more clever..(and might need to) # we just know that a value of None cannot have dependencies # (because if anyone else had asked for this value, it wouldn't be From eb1b7fc8bb07669bc0140678f09c1b934a507bbb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:56:44 -0700 Subject: [PATCH 152/607] add systemsolver copy method --- pyqtgraph/parametertree/SystemSolver.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py index c804d50a..ffdabfae 100644 --- a/pyqtgraph/parametertree/SystemSolver.py +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -1,5 +1,7 @@ from ..pgcollections import OrderedDict import numpy as np +import copy + class SystemSolver(object): """ @@ -73,6 +75,12 @@ class SystemSolver(object): self.__dict__['_currentGets'] = set() self.reset() + def copy(self): + sys = type(self)() + sys.__dict__['_vars'] = copy.deepcopy(self.__dict__['_vars']) + sys.__dict__['_currentGets'] = copy.deepcopy(self.__dict__['_currentGets']) + return sys + def reset(self): """ Reset all variables in the solver to their default state. From 2754427b25d172dff2d73199a470ab4d99b06709 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:58:00 -0700 Subject: [PATCH 153/607] systemsolver: add method for checking constraints / DOF --- pyqtgraph/parametertree/SystemSolver.py | 37 ++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/SystemSolver.py b/pyqtgraph/parametertree/SystemSolver.py index ffdabfae..b1d4256a 100644 --- a/pyqtgraph/parametertree/SystemSolver.py +++ b/pyqtgraph/parametertree/SystemSolver.py @@ -175,6 +175,16 @@ class SystemSolver(object): elif constraint == 'fixed': if 'f' not in var[3]: raise TypeError("Fixed constraints not allowed for '%s'" % name) + # This is nice, but not reliable because sometimes there is 1 DOF but we set 2 + # values simultaneously. + # if var[2] is None: + # try: + # self.get(name) + # # has already been computed by the system; adding a fixed constraint + # # would overspecify the system. + # raise ValueError("Cannot fix parameter '%s'; system would become overconstrained." % name) + # except RuntimeError: + # pass var[2] = constraint elif isinstance(constraint, tuple): if 'r' not in var[3]: @@ -245,6 +255,31 @@ class SystemSolver(object): for k in self._vars: getattr(self, k) + def checkOverconstraint(self): + """Check whether the system is overconstrained. If so, return the name of + the first overconstrained parameter. + + Overconstraints occur when any fixed parameter can be successfully computed by the system. + (Ideally, all parameters are either fixed by the user or constrained by the + system, but never both). + """ + for k,v in self._vars.items(): + if v[2] == 'fixed' and 'n' in v[3]: + oldval = v[:] + self.set(k, None, None) + try: + self.get(k) + return k + except RuntimeError: + pass + finally: + self._vars[k] = oldval + + return False + + + + def __repr__(self): state = OrderedDict() for name, var in self._vars.items(): @@ -386,4 +421,4 @@ if __name__ == '__main__': camera.solve() print(camera.saveState()) - \ No newline at end of file + From ce7594b6972ec6b840829d7e306aad7a2fc13c50 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:59:14 -0700 Subject: [PATCH 154/607] Add GroupParameter.sigAddNew signal --- pyqtgraph/parametertree/parameterTypes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 8c1e587d..d75dbba0 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -462,12 +462,15 @@ class GroupParameter(Parameter): instead of a button. """ itemClass = GroupParameterItem + + sigAddNew = QtCore.Signal(object, object) # self, type def addNew(self, typ=None): """ This method is called when the user has requested to add a new item to the group. + By default, it emits ``sigAddNew(self, typ)``. """ - raise Exception("Must override this function in subclass.") + self.sigAddNew.emit(self, typ) def setAddList(self, vals): """Change the list of options available for the user to add to the group.""" From 812a65461d8e1f7f5b6a2507031530ec31246d2b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 Sep 2017 08:59:37 -0700 Subject: [PATCH 155/607] action parameter minor ui adjustment --- pyqtgraph/parametertree/parameterTypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index d75dbba0..d137410d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -608,6 +608,7 @@ class ActionParameterItem(ParameterItem): ParameterItem.__init__(self, param, depth) self.layoutWidget = QtGui.QWidget() self.layout = QtGui.QHBoxLayout() + self.layout.setContentsMargins(0, 0, 0, 0) self.layoutWidget.setLayout(self.layout) self.button = QtGui.QPushButton(param.name()) #self.layout.addSpacing(100) From fcf45036711c97c51b62f65e3ae5cba930b655bb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 2 Oct 2017 08:58:03 -0700 Subject: [PATCH 156/607] Fix: avoid division by 0 when image is single valued --- pyqtgraph/functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 1aed6ace..3a50eb9e 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1079,7 +1079,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): minVal, maxVal = levels[i] if minVal == maxVal: maxVal += 1e-16 - newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=dtype) + rng = maxVal-minVal + rng = 1 if rng == 0 else rng + newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype) data = newData else: # Apply level scaling unless it would have no effect on the data From c3e52f15b0df0104455922512762887fdfc81e25 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:16:36 -0700 Subject: [PATCH 157/607] Fix ImageItem rgb histogram calculation --- pyqtgraph/graphicsItems/ImageItem.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 2ae8b812..34150282 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -489,14 +489,17 @@ class ImageItem(GraphicsObject): bins = [mn, mx] kwds['bins'] = bins - stepData = stepData[np.isfinite(stepData)] + if perChannel: hist = [] for i in range(stepData.shape[-1]): - h = np.histogram(stepData[..., i], **kwds) + stepChan = stepData[..., i] + stepChan = stepChan[np.isfinite(stepChan)] + h = np.histogram(stepChan, **kwds) hist.append((h[1][:-1], h[0])) return hist else: + stepData = stepData[np.isfinite(stepData)] hist = np.histogram(stepData, **kwds) return hist[1][:-1], hist[0] From faca369a8d38f00b9324051482f51e0bfe269b4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:17:22 -0700 Subject: [PATCH 158/607] code cleanup --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 6919cfba..dc6286e3 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -76,7 +76,6 @@ class HistogramLUTItem(GraphicsWidget): region.sigRegionChanged.connect(self.regionChanging) region.sigRegionChangeFinished.connect(self.regionChanged) - self.region = self.regions[0] # for backward compatibility. self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) @@ -87,9 +86,6 @@ class HistogramLUTItem(GraphicsWidget): self.gradient.setFlag(self.gradient.ItemStacksBehindParent) self.vb.setFlag(self.gradient.ItemStacksBehindParent) - #self.grid = GridItem() - #self.vb.addItem(self.grid) - self.gradient.sigGradientChanged.connect(self.gradientChanged) self.vb.sigRangeChanged.connect(self.viewRangeChanged) add = QtGui.QPainter.CompositionMode_Plus @@ -114,7 +110,6 @@ class HistogramLUTItem(GraphicsWidget): if image is not None: self.setImageItem(image) - #self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)] @@ -125,9 +120,6 @@ class HistogramLUTItem(GraphicsWidget): else: plot.setFillLevel(None) - #def sizeHint(self, *args): - #return QtCore.QSizeF(115, 200) - def paint(self, p, *args): if self.levelMode != 'mono': return @@ -143,8 +135,6 @@ class HistogramLUTItem(GraphicsWidget): p.drawLine(p2 - Point(0, 5), gradRect.topLeft()) p.drawLine(gradRect.topLeft(), gradRect.topRight()) p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) - #p.drawRect(self.boundingRect()) - def setHistogramRange(self, mn, mx, padding=0.1): """Set the Y range on the histogram plot. This disables auto-scaling.""" From 21bda49a294fe133a1a594d6a33dc7d683fd5df0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:27:36 -0700 Subject: [PATCH 159/607] Docstring updates --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 24 +++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index dc6286e3..85cbe9cf 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -25,11 +25,29 @@ __all__ = ['HistogramLUTItem'] class HistogramLUTItem(GraphicsWidget): """ This is a graphicsWidget which provides controls for adjusting the display of an image. + Includes: - Image histogram - Movable region over histogram to select black/white levels - Gradient editor to define color lookup table for single-channel images + + Parameters + ---------- + image : ImageItem or None + If *image* is provided, then the control will be automatically linked to + the image and changes to the control will be immediately reflected in + the image's appearance. + fillHistogram : bool + By default, the histogram is rendered with a fill. + For performance, set *fillHistogram* = False. + rgbHistogram : bool + Sets whether the histogram is computed once over all channels of the + image, or once per channel. + levelMode : 'mono' or 'rgba' + If 'mono', then only a single set of black/whilte level lines is drawn, + and the levels apply to all channels in the image. If 'rgba', then one + set of levels is drawn for each channel. """ sigLookupTableChanged = QtCore.Signal(object) @@ -37,10 +55,6 @@ class HistogramLUTItem(GraphicsWidget): sigLevelChangeFinished = QtCore.Signal(object) def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'): - """ - If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance. - By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False. - """ GraphicsWidget.__init__(self) self.lut = None self.imageItem = lambda: None # fake a dead weakref @@ -241,6 +255,8 @@ class HistogramLUTItem(GraphicsWidget): def getLevels(self): """Return the min and max levels. + + For rgba mode, this returns a list of the levels for each channel. """ if self.levelMode == 'mono': return self.region.getRegion() From a04db637755ceafb20cc784343f2c134157928af Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 08:27:56 -0700 Subject: [PATCH 160/607] Include level mode in save/restore --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 85cbe9cf..68448c11 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -319,8 +319,10 @@ class HistogramLUTItem(GraphicsWidget): return { 'gradient': self.gradient.saveState(), 'levels': self.getLevels(), + 'mode': self.levelMode, } def restoreState(self, state): + self.setLevelMode(state['mode']) self.gradient.restoreState(state['gradient']) self.setLevels(*state['levels']) From f4c3d88251be17f6ad8bab282d4f7cc17cd74b5e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 15:22:31 -0700 Subject: [PATCH 161/607] Add option to join nested progress dialogs into a single window --- examples/ProgressDialog.py | 53 +++++++++++ pyqtgraph/widgets/ProgressDialog.py | 136 +++++++++++++++++++++++++++- 2 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 examples/ProgressDialog.py diff --git a/examples/ProgressDialog.py b/examples/ProgressDialog.py new file mode 100644 index 00000000..08cffa7e --- /dev/null +++ b/examples/ProgressDialog.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +""" +Using ProgressDialog to show progress updates in a nested process. + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import time +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui + +app = QtGui.QApplication([]) + + +def runStage(i): + """Waste time for 3 seconds while incrementing a progress bar. + """ + with pg.ProgressDialog("Running stage %s.." % i, maximum=100, nested=True) as dlg: + for j in range(100): + time.sleep(0.03) + dlg += 1 + if dlg.wasCanceled(): + print("Canceled stage %s" % i) + break + + +def runManyStages(i): + """Iterate over runStage() 3 times while incrementing a progress bar. + """ + with pg.ProgressDialog("Running stage %s.." % i, maximum=3, nested=True, wait=0) as dlg: + for j in range(1,4): + runStage('%d.%d' % (i, j)) + dlg += 1 + if dlg.wasCanceled(): + print("Canceled stage %s" % i) + break + + +with pg.ProgressDialog("Doing a multi-stage process..", maximum=5, nested=True, wait=0) as dlg1: + for i in range(1,6): + if i == 3: + # this stage will have 3 nested progress bars + runManyStages(i) + else: + # this stage will have 2 nested progress bars + runStage(i) + + dlg1 += 1 + if dlg1.wasCanceled(): + print("Canceled process") + break + + diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 8c669be4..7c60004b 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -2,6 +2,8 @@ from ..Qt import QtGui, QtCore __all__ = ['ProgressDialog'] + + class ProgressDialog(QtGui.QProgressDialog): """ Extends QProgressDialog for use in 'with' statements. @@ -14,7 +16,10 @@ class ProgressDialog(QtGui.QProgressDialog): if dlg.wasCanceled(): raise Exception("Processing canceled by user") """ - def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False): + + allDialogs = [] + + def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False, nested=False): """ ============== ================================================================ **Arguments:** @@ -29,6 +34,9 @@ class ProgressDialog(QtGui.QProgressDialog): and calls to wasCanceled() will always return False. If ProgressDialog is entered from a non-gui thread, it will always be disabled. + nested (bool) If True, then this progress bar will be displayed inside + any pre-existing progress dialogs that also allow nesting (if + any). ============== ================================================================ """ isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() @@ -42,20 +50,40 @@ class ProgressDialog(QtGui.QProgressDialog): noCancel = True self.busyCursor = busyCursor - + QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent) - self.setMinimumDuration(wait) + + # If this will be a nested dialog, then we ignore the wait time + if nested is True and len(ProgressDialog.allDialogs) > 0: + self.setMinimumDuration(2**30) + else: + self.setMinimumDuration(wait) + self.setWindowModality(QtCore.Qt.WindowModal) self.setValue(self.minimum()) if noCancel: self.setCancelButton(None) - + # attributes used for nesting dialogs + self.nestedLayout = None + self._nestableWidgets = None + self._nestingReady = False + self._topDialog = None + def __enter__(self): if self.disabled: return self if self.busyCursor: QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + + if len(ProgressDialog.allDialogs) > 0: + topDialog = ProgressDialog.allDialogs[0] + topDialog._addSubDialog(self) + self._topDialog = topDialog + topDialog.canceled.connect(self.cancel) + + ProgressDialog.allDialogs.append(self) + return self def __exit__(self, exType, exValue, exTrace): @@ -63,6 +91,12 @@ class ProgressDialog(QtGui.QProgressDialog): return if self.busyCursor: QtGui.QApplication.restoreOverrideCursor() + + if self._topDialog is not None: + self._topDialog._removeSubDialog(self) + + ProgressDialog.allDialogs.pop(-1) + self.setValue(self.maximum()) def __iadd__(self, val): @@ -72,6 +106,94 @@ class ProgressDialog(QtGui.QProgressDialog): self.setValue(self.value()+val) return self + def _addSubDialog(self, dlg): + # insert widgets from another dialog into this one. + + # set a new layout and arrange children into it (if needed). + self._prepareNesting() + + bar, btn = dlg._extractWidgets() + bar.removed = False + + # where should we insert this widget? Find the first slot with a + # "removed" widget (that was left as a placeholder) + nw = self.nestedLayout.count() + inserted = False + if nw > 1: + for i in range(1, nw): + bar2 = self.nestedLayout.itemAt(i).widget() + if bar2.removed: + self.nestedLayout.removeWidget(bar2) + bar2.hide() + bar2.setParent(None) + self.nestedLayout.insertWidget(i, bar) + inserted = True + break + if not inserted: + self.nestedLayout.addWidget(bar) + + def _removeSubDialog(self, dlg): + # don't remove the widget just yet; instead we hide it and leave it in + # as a placeholder. + bar, btn = dlg._extractWidgets() + bar.layout().setCurrentIndex(1) # causes widgets to be hidden without changing size + bar.removed = True # mark as removed so we know we can insert another bar here later + + def _prepareNesting(self): + # extract all child widgets and place into a new layout that we can add to + if self._nestingReady is False: + # top layout contains progress bars + cancel button at the bottom + self._topLayout = QtGui.QGridLayout() + self.setLayout(self._topLayout) + self._topLayout.setContentsMargins(0, 0, 0, 0) + + # A vbox to contain all progress bars + self.nestedVBox = QtGui.QWidget() + self._topLayout.addWidget(self.nestedVBox, 0, 0, 1, 2) + self.nestedLayout = QtGui.QVBoxLayout() + self.nestedVBox.setLayout(self.nestedLayout) + + # re-insert all widgets + bar, btn = self._extractWidgets() + self.nestedLayout.addWidget(bar) + self._topLayout.addWidget(btn, 1, 1, 1, 1) + self._topLayout.setColumnStretch(0, 100) + self._topLayout.setColumnStretch(1, 1) + self._topLayout.setRowStretch(0, 100) + self._topLayout.setRowStretch(1, 1) + + self._nestingReady = True + + def _extractWidgets(self): + # return a single widget containing all sub-widgets nicely arranged + if self._nestableWidgets is None: + widgets = [ch for ch in self.children() if isinstance(ch, QtGui.QWidget)] + label = [ch for ch in self.children() if isinstance(ch, QtGui.QLabel)][0] + bar = [ch for ch in self.children() if isinstance(ch, QtGui.QProgressBar)][0] + btn = [ch for ch in self.children() if isinstance(ch, QtGui.QPushButton)][0] + + # join label and bar into a stacked layout so they can be hidden + # without changing size + sw = QtGui.QWidget() + sl = QtGui.QStackedLayout() + sw.setLayout(sl) + sl.setContentsMargins(0, 0, 0, 0) + + # inside the stacked layout, the bar and label are in a vbox + w = QtGui.QWidget() + sl.addWidget(w) + l = QtGui.QVBoxLayout() + w.setLayout(l) + l.addWidget(label) + l.addWidget(bar) + + # add a blank page to the stacked layout + blank = QtGui.QWidget() + sl.addWidget(blank) + + self._nestableWidgets = (sw, btn) + + return self._nestableWidgets ## wrap all other functions to make sure they aren't being called from non-gui threads @@ -80,6 +202,11 @@ class ProgressDialog(QtGui.QProgressDialog): return QtGui.QProgressDialog.setValue(self, val) + # Qt docs say this should happen automatically, but that doesn't seem + # to be the case. + if self.windowModality() == QtCore.Qt.WindowModal: + QtGui.QApplication.processEvents() + def setLabelText(self, val): if self.disabled: return @@ -109,4 +236,3 @@ class ProgressDialog(QtGui.QProgressDialog): if self.disabled: return 0 return QtGui.QProgressDialog.minimum(self) - From e6507f860176ad6d3ce5d68e425afb8bc3de610e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Oct 2017 17:14:32 -0700 Subject: [PATCH 162/607] try a different approach to managing nested bars.. --- pyqtgraph/widgets/ProgressDialog.py | 99 +++++++++++++++++------------ 1 file changed, 58 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 7c60004b..4964771d 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -39,6 +39,13 @@ class ProgressDialog(QtGui.QProgressDialog): any). ============== ================================================================ """ + # attributes used for nesting dialogs + self.nestedLayout = None + self._nestableWidgets = None + self._nestingReady = False + self._topDialog = None + self._subBars = [] + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() self.disabled = disable or (not isGuiThread) if self.disabled: @@ -63,12 +70,6 @@ class ProgressDialog(QtGui.QProgressDialog): self.setValue(self.minimum()) if noCancel: self.setCancelButton(None) - - # attributes used for nesting dialogs - self.nestedLayout = None - self._nestableWidgets = None - self._nestingReady = False - self._topDialog = None def __enter__(self): if self.disabled: @@ -113,31 +114,32 @@ class ProgressDialog(QtGui.QProgressDialog): self._prepareNesting() bar, btn = dlg._extractWidgets() - bar.removed = False # where should we insert this widget? Find the first slot with a # "removed" widget (that was left as a placeholder) - nw = self.nestedLayout.count() inserted = False - if nw > 1: - for i in range(1, nw): - bar2 = self.nestedLayout.itemAt(i).widget() - if bar2.removed: - self.nestedLayout.removeWidget(bar2) - bar2.hide() - bar2.setParent(None) - self.nestedLayout.insertWidget(i, bar) - inserted = True - break + for i,bar2 in enumerate(self._subBars): + if bar2.hidden: + self._subBars.pop(i) + bar2.hide() + bar2.setParent(None) + self._subBars.insert(i, bar) + inserted = True + break if not inserted: - self.nestedLayout.addWidget(bar) - + self._subBars.append(bar) + + # reset the layout + while self.nestedLayout.count() > 0: + self.nestedLayout.takeAt(0) + for b in self._subBars: + self.nestedLayout.addWidget(b) + def _removeSubDialog(self, dlg): # don't remove the widget just yet; instead we hide it and leave it in # as a placeholder. bar, btn = dlg._extractWidgets() - bar.layout().setCurrentIndex(1) # causes widgets to be hidden without changing size - bar.removed = True # mark as removed so we know we can insert another bar here later + bar.hide() def _prepareNesting(self): # extract all child widgets and place into a new layout that we can add to @@ -156,6 +158,7 @@ class ProgressDialog(QtGui.QProgressDialog): # re-insert all widgets bar, btn = self._extractWidgets() self.nestedLayout.addWidget(bar) + self._subBars.append(bar) self._topLayout.addWidget(btn, 1, 1, 1, 1) self._topLayout.setColumnStretch(0, 100) self._topLayout.setColumnStretch(1, 1) @@ -165,31 +168,17 @@ class ProgressDialog(QtGui.QProgressDialog): self._nestingReady = True def _extractWidgets(self): - # return a single widget containing all sub-widgets nicely arranged + # return: + # 1. a single widget containing the label and progress bar + # 2. the cancel button + if self._nestableWidgets is None: widgets = [ch for ch in self.children() if isinstance(ch, QtGui.QWidget)] label = [ch for ch in self.children() if isinstance(ch, QtGui.QLabel)][0] bar = [ch for ch in self.children() if isinstance(ch, QtGui.QProgressBar)][0] btn = [ch for ch in self.children() if isinstance(ch, QtGui.QPushButton)][0] - # join label and bar into a stacked layout so they can be hidden - # without changing size - sw = QtGui.QWidget() - sl = QtGui.QStackedLayout() - sw.setLayout(sl) - sl.setContentsMargins(0, 0, 0, 0) - - # inside the stacked layout, the bar and label are in a vbox - w = QtGui.QWidget() - sl.addWidget(w) - l = QtGui.QVBoxLayout() - w.setLayout(l) - l.addWidget(label) - l.addWidget(bar) - - # add a blank page to the stacked layout - blank = QtGui.QWidget() - sl.addWidget(blank) + sw = ProgressWidget(label, bar) self._nestableWidgets = (sw, btn) @@ -202,6 +191,11 @@ class ProgressDialog(QtGui.QProgressDialog): return QtGui.QProgressDialog.setValue(self, val) + if self._topDialog is not None: + tbar = self._topDialog._extractWidgets()[0].bar + tlab = self._topDialog._extractWidgets()[0].label + print(tlab.pos(), tbar.pos()) + # Qt docs say this should happen automatically, but that doesn't seem # to be the case. if self.windowModality() == QtCore.Qt.WindowModal: @@ -236,3 +230,26 @@ class ProgressDialog(QtGui.QProgressDialog): if self.disabled: return 0 return QtGui.QProgressDialog.minimum(self) + + +class ProgressWidget(QtGui.QWidget): + def __init__(self, label, bar): + QtGui.QWidget.__init__(self) + self.hidden = False + self.layout = QtGui.QVBoxLayout() + self.setLayout(self.layout) + + self.label = label + self.bar = bar + self.layout.addWidget(label) + self.layout.addWidget(bar) + + def eventFilter(self, obj, ev): + return ev.type() == QtCore.QEvent.Paint + + def hide(self): + # hide label and bar, but continue occupying the same space in the layout + for widget in (self.label, self.bar): + widget.installEventFilter(self) + widget.update() + self.hidden = True From f1de464c460e9cbdb0987a4cf85b76930e88ad18 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 08:30:38 -0700 Subject: [PATCH 163/607] Preserve levels when switching between mono and rgba modes --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 68448c11..019fa3a7 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -288,9 +288,21 @@ class HistogramLUTItem(GraphicsWidget): Options are 'mono' or 'rgba'. """ assert mode in ('mono', 'rgba') + + oldLevels = self.getLevels() + self.levelMode = mode self._showRegions() self.imageChanged() + + # do our best to preserve old levels + if mode == 'mono': + levels = np.array(oldLevels).mean(axis=0) + self.setLevels(*levels) + else: + levels = [oldLevels] * 4 + self.setLevels(rgba=levels) + self.update() def _showRegions(self): From ce15f4530ac8a343520ccb2923bbe9a8f04b3978 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 08:34:42 -0700 Subject: [PATCH 164/607] Fix: image levels reset to mono after drag release --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 019fa3a7..e6d692e6 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -199,7 +199,7 @@ class HistogramLUTItem(GraphicsWidget): def regionChanged(self): if self.imageItem() is not None: - self.imageItem().setLevels(self.region.getRegion()) + self.imageItem().setLevels(self.getLevels()) self.sigLevelChangeFinished.emit(self) def regionChanging(self): From 7c1a6ecb1afcf4f1012c367380d7195732368380 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:01:51 -0700 Subject: [PATCH 165/607] Prevent dialog from moving label/bar widgets on resize when nested --- examples/ProgressDialog.py | 4 ++-- pyqtgraph/widgets/ProgressDialog.py | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/examples/ProgressDialog.py b/examples/ProgressDialog.py index 08cffa7e..141d2bb4 100644 --- a/examples/ProgressDialog.py +++ b/examples/ProgressDialog.py @@ -13,11 +13,11 @@ app = QtGui.QApplication([]) def runStage(i): - """Waste time for 3 seconds while incrementing a progress bar. + """Waste time for 2 seconds while incrementing a progress bar. """ with pg.ProgressDialog("Running stage %s.." % i, maximum=100, nested=True) as dlg: for j in range(100): - time.sleep(0.03) + time.sleep(0.02) dlg += 1 if dlg.wasCanceled(): print("Canceled stage %s" % i) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 4964771d..de3c6dc4 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -183,6 +183,12 @@ class ProgressDialog(QtGui.QProgressDialog): self._nestableWidgets = (sw, btn) return self._nestableWidgets + + def resizeEvent(self, ev): + if self._nestingReady: + # don't let progress dialog manage widgets anymore. + return + return QtGui.QProgressDialog.resizeEvent(self, ev) ## wrap all other functions to make sure they aren't being called from non-gui threads From 384975dd464b0daa6d93dc26ec8859c24941ca79 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:03:08 -0700 Subject: [PATCH 166/607] Cleanup --- pyqtgraph/widgets/ProgressDialog.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index de3c6dc4..e62a6551 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -200,7 +200,6 @@ class ProgressDialog(QtGui.QProgressDialog): if self._topDialog is not None: tbar = self._topDialog._extractWidgets()[0].bar tlab = self._topDialog._extractWidgets()[0].label - print(tlab.pos(), tbar.pos()) # Qt docs say this should happen automatically, but that doesn't seem # to be the case. From d2942c7acadef3910311c21b5e72cb3cbd854aa3 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:06:05 -0700 Subject: [PATCH 167/607] Fix: obey nested option --- pyqtgraph/widgets/ProgressDialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index e62a6551..6bda4b95 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -45,6 +45,7 @@ class ProgressDialog(QtGui.QProgressDialog): self._nestingReady = False self._topDialog = None self._subBars = [] + self.nested = nested isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() self.disabled = disable or (not isGuiThread) @@ -77,7 +78,7 @@ class ProgressDialog(QtGui.QProgressDialog): if self.busyCursor: QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) - if len(ProgressDialog.allDialogs) > 0: + if self.nested and len(ProgressDialog.allDialogs) > 0: topDialog = ProgressDialog.allDialogs[0] topDialog._addSubDialog(self) self._topDialog = topDialog From e2c991851031653ea53ab0600e1409f4f093663b Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:11:44 -0700 Subject: [PATCH 168/607] docs cleanup --- pyqtgraph/widgets/ProgressDialog.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/widgets/ProgressDialog.py b/pyqtgraph/widgets/ProgressDialog.py index 6bda4b95..ae1826bb 100644 --- a/pyqtgraph/widgets/ProgressDialog.py +++ b/pyqtgraph/widgets/ProgressDialog.py @@ -6,7 +6,10 @@ __all__ = ['ProgressDialog'] class ProgressDialog(QtGui.QProgressDialog): """ - Extends QProgressDialog for use in 'with' statements. + Extends QProgressDialog: + + * Adds context management so the dialog may be used in `with` statements + * Allows nesting multiple progress dialogs Example:: @@ -35,8 +38,7 @@ class ProgressDialog(QtGui.QProgressDialog): If ProgressDialog is entered from a non-gui thread, it will always be disabled. nested (bool) If True, then this progress bar will be displayed inside - any pre-existing progress dialogs that also allow nesting (if - any). + any pre-existing progress dialogs that also allow nesting. ============== ================================================================ """ # attributes used for nesting dialogs @@ -198,10 +200,6 @@ class ProgressDialog(QtGui.QProgressDialog): return QtGui.QProgressDialog.setValue(self, val) - if self._topDialog is not None: - tbar = self._topDialog._extractWidgets()[0].bar - tlab = self._topDialog._extractWidgets()[0].label - # Qt docs say this should happen automatically, but that doesn't seem # to be the case. if self.windowModality() == QtCore.Qt.WindowModal: @@ -239,6 +237,9 @@ class ProgressDialog(QtGui.QProgressDialog): class ProgressWidget(QtGui.QWidget): + """Container for a label + progress bar that also allows its child widgets + to be hidden without changing size. + """ def __init__(self, label, bar): QtGui.QWidget.__init__(self) self.hidden = False From 9ef9f73be5b5cdb639931a4f8e68dbfe98fdbbea Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 09:35:57 -0700 Subject: [PATCH 169/607] minor import and test corrections --- pyqtgraph/functions.py | 2 +- pyqtgraph/tests/test_functions.py | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index b38888fe..8ef57742 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -15,7 +15,7 @@ from .python2_3 import asUnicode, basestring from .Qt import QtGui, QtCore, USE_PYSIDE from . import getConfigOption, setConfigOptions from . import debug - +from .metaarray import MetaArray Colors = { diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index eff56635..68f3dc24 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -344,14 +344,14 @@ def test_eq(): a2 = a1 + 1 a3 = a2.astype('int') a4 = np.empty((0, 20)) - assert not eq(a1, a2) - assert not eq(a1, a3) - assert not eq(a1, a4) + assert not eq(a1, a2) # same shape/dtype, different values + assert not eq(a1, a3) # same shape, different dtype and values + assert not eq(a1, a4) # different shape (note: np.all gives True if one array has size 0) - assert eq(a2, a3) - assert not eq(a2, a4) + assert not eq(a2, a3) # same values, but different dtype + assert not eq(a2, a4) # different shape - assert not eq(a3, a4) + assert not eq(a3, a4) # different shape and dtype assert eq(a4, a4.copy()) assert not eq(a4, a4.T) From 0c28de5fd80f73394664e0eba888825c6714ad25 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 4 Oct 2017 10:24:34 -0700 Subject: [PATCH 170/607] Fix subArray when input data is discontiguous --- pyqtgraph/functions.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8ef57742..7ad603f7 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -747,8 +747,7 @@ def subArray(data, offset, shape, stride): the input in the example above to have shape (10, 7) would cause the output to have shape (2, 3, 7). """ - #data = data.flatten() - data = data[offset:] + data = np.ascontiguousarray(data)[offset:] shape = tuple(shape) extraShape = data.shape[1:] From 8a882b516a6dddbe7ad45b118fb66150b92d56c9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 5 Oct 2017 10:46:39 -0700 Subject: [PATCH 171/607] Fix: InvisibleRootItem is no longer a subclass of QTreeWidgetItem The __getattr__ method is supposed to wrap attributes from the internal TreeWidgetItem, but this was broken because the superclass had already implemented these. --- pyqtgraph/widgets/TreeWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index a37181cf..096227ab 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -350,7 +350,7 @@ class TreeWidgetItem(QtGui.QTreeWidgetItem): """ -class InvisibleRootItem(QtGui.QTreeWidgetItem): +class InvisibleRootItem(object): """Wrapper around a TreeWidget's invisible root item that calls TreeWidget.informTreeWidgetChange when child items are added/removed. """ From 5aa2a1998fac17a1972309d2ee4256a2c80ece54 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 5 Oct 2017 12:42:20 -0700 Subject: [PATCH 172/607] Override qAbort on slot exceptions for PyQt>=5.5 --- pyqtgraph/Qt.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 2ed9d6f9..ad04cd76 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -151,6 +151,17 @@ elif QT_LIB == PYQT5: # We're using PyQt5 which has a different structure so we're going to use a shim to # recreate the Qt4 structure for Qt5 from PyQt5 import QtGui, QtCore, QtWidgets, uic + + # PyQt5, starting in v5.5, calls qAbort when an exception is raised inside + # a slot. To maintain backward compatibility (and sanity for interactive + # users), we install a global exception hook to override this behavior. + ver = QtCore.PYQT_VERSION_STR.split('.') + if int(ver[1]) >= 5: + sys_excepthook = sys.excepthook + def pyqt5_qabort_override(*args, **kwds): + return sys_excepthook(*args, **kwds) + sys.excepthook = pyqt5_qabort_override + try: from PyQt5 import QtSvg except ImportError: From d32454ebb853c7e09153bdf9a256e8bbf48687ec Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 11 Oct 2017 09:05:32 -0700 Subject: [PATCH 173/607] Don't use ORderedDict backport on python 3 --- pyqtgraph/ordereddict.py | 182 ++++++++++++++++++++------------------- 1 file changed, 93 insertions(+), 89 deletions(-) diff --git a/pyqtgraph/ordereddict.py b/pyqtgraph/ordereddict.py index 7242b506..fb37037f 100644 --- a/pyqtgraph/ordereddict.py +++ b/pyqtgraph/ordereddict.py @@ -20,108 +20,112 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # OTHER DEALINGS IN THE SOFTWARE. -from UserDict import DictMixin +import sys +if sys.version[0] > '2': + from collections import OrderedDict +else: + from UserDict import DictMixin -class OrderedDict(dict, DictMixin): + class OrderedDict(dict, DictMixin): - def __init__(self, *args, **kwds): - if len(args) > 1: - raise TypeError('expected at most 1 arguments, got %d' % len(args)) - try: - self.__end - except AttributeError: - self.clear() - self.update(*args, **kwds) + def __init__(self, *args, **kwds): + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__end + except AttributeError: + self.clear() + self.update(*args, **kwds) - def clear(self): - self.__end = end = [] - end += [None, end, end] # sentinel node for doubly linked list - self.__map = {} # key --> [key, prev, next] - dict.clear(self) + def clear(self): + self.__end = end = [] + end += [None, end, end] # sentinel node for doubly linked list + self.__map = {} # key --> [key, prev, next] + dict.clear(self) - def __setitem__(self, key, value): - if key not in self: + def __setitem__(self, key, value): + if key not in self: + end = self.__end + curr = end[1] + curr[2] = end[1] = self.__map[key] = [key, curr, end] + dict.__setitem__(self, key, value) + + def __delitem__(self, key): + dict.__delitem__(self, key) + key, prev, next = self.__map.pop(key) + prev[2] = next + next[1] = prev + + def __iter__(self): + end = self.__end + curr = end[2] + while curr is not end: + yield curr[0] + curr = curr[2] + + def __reversed__(self): end = self.__end curr = end[1] - curr[2] = end[1] = self.__map[key] = [key, curr, end] - dict.__setitem__(self, key, value) + while curr is not end: + yield curr[0] + curr = curr[1] - def __delitem__(self, key): - dict.__delitem__(self, key) - key, prev, next = self.__map.pop(key) - prev[2] = next - next[1] = prev + def popitem(self, last=True): + if not self: + raise KeyError('dictionary is empty') + if last: + key = reversed(self).next() + else: + key = iter(self).next() + value = self.pop(key) + return key, value - def __iter__(self): - end = self.__end - curr = end[2] - while curr is not end: - yield curr[0] - curr = curr[2] + def __reduce__(self): + items = [[k, self[k]] for k in self] + tmp = self.__map, self.__end + del self.__map, self.__end + inst_dict = vars(self).copy() + self.__map, self.__end = tmp + if inst_dict: + return (self.__class__, (items,), inst_dict) + return self.__class__, (items,) - def __reversed__(self): - end = self.__end - curr = end[1] - while curr is not end: - yield curr[0] - curr = curr[1] + def keys(self): + return list(self) - def popitem(self, last=True): - if not self: - raise KeyError('dictionary is empty') - if last: - key = reversed(self).next() - else: - key = iter(self).next() - value = self.pop(key) - return key, value + setdefault = DictMixin.setdefault + update = DictMixin.update + pop = DictMixin.pop + values = DictMixin.values + items = DictMixin.items + iterkeys = DictMixin.iterkeys + itervalues = DictMixin.itervalues + iteritems = DictMixin.iteritems - def __reduce__(self): - items = [[k, self[k]] for k in self] - tmp = self.__map, self.__end - del self.__map, self.__end - inst_dict = vars(self).copy() - self.__map, self.__end = tmp - if inst_dict: - return (self.__class__, (items,), inst_dict) - return self.__class__, (items,) + def __repr__(self): + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, self.items()) - def keys(self): - return list(self) + def copy(self): + return self.__class__(self) - setdefault = DictMixin.setdefault - update = DictMixin.update - pop = DictMixin.pop - values = DictMixin.values - items = DictMixin.items - iterkeys = DictMixin.iterkeys - itervalues = DictMixin.itervalues - iteritems = DictMixin.iteritems + @classmethod + def fromkeys(cls, iterable, value=None): + d = cls() + for key in iterable: + d[key] = value + return d - def __repr__(self): - if not self: - return '%s()' % (self.__class__.__name__,) - return '%s(%r)' % (self.__class__.__name__, self.items()) - - def copy(self): - return self.__class__(self) - - @classmethod - def fromkeys(cls, iterable, value=None): - d = cls() - for key in iterable: - d[key] = value - return d - - def __eq__(self, other): - if isinstance(other, OrderedDict): - if len(self) != len(other): - return False - for p, q in zip(self.items(), other.items()): - if p != q: + def __eq__(self, other): + if isinstance(other, OrderedDict): + if len(self) != len(other): return False - return True - return dict.__eq__(self, other) + for p, q in zip(self.items(), other.items()): + if p != q: + return False + return True + return dict.__eq__(self, other) - def __ne__(self, other): - return not self == other + def __ne__(self, other): + return not self == other From 89993ce700238fcbcc046ee6e69ce98e07f45374 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 11 Oct 2017 09:11:16 -0700 Subject: [PATCH 174/607] Add simple script for invoking pytest --- test.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 test.py diff --git a/test.py b/test.py new file mode 100644 index 00000000..b07fb1cf --- /dev/null +++ b/test.py @@ -0,0 +1,24 @@ +""" +Script for invoking pytest with options to select Qt library +""" + +import sys +import pytest + +args = sys.argv[1:] +if '--pyside' in args: + args.remove('--pyside') + import PySide +elif '--pyqt4' in args: + args.remove('--pyqt4') + import PyQt4 +elif '--pyqt5' in args: + args.remove('--pyqt5') + import PyQt5 + +import pyqtgraph as pg +pg.systemInfo() + +pytest.main(args) + + \ No newline at end of file From c678094f2507302edd448ad08f9eaa6a837eece7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Oct 2017 10:47:41 -0700 Subject: [PATCH 175/607] Make TreeWidget.invisibleRootItem return a singleton --- pyqtgraph/widgets/TreeWidget.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index 096227ab..b0ec54c1 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -17,7 +17,11 @@ class TreeWidget(QtGui.QTreeWidget): def __init__(self, parent=None): QtGui.QTreeWidget.__init__(self, parent) - #self.itemWidgets = WeakKeyDictionary() + + # wrap this item so that we can propagate tree change information + # to children. + self._invRootItem = InvisibleRootItem(QtGui.QTreeWidget.invisibleRootItem(self)) + self.setAcceptDrops(True) self.setDragEnabled(True) self.setEditTriggers(QtGui.QAbstractItemView.EditKeyPressed|QtGui.QAbstractItemView.SelectedClicked) @@ -210,9 +214,7 @@ class TreeWidget(QtGui.QTreeWidget): #self.informTreeWidgetChange(item) def invisibleRootItem(self): - # wrap this item so that we can propagate tree change information - # to children. - return InvisibleRootItem(QtGui.QTreeWidget.invisibleRootItem(self)) + return self._invRootItem def itemFromIndex(self, index): """Return the item and column corresponding to a QModelIndex. From 79eebe1c02f09527afeb61a89c8949611683b953 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Oct 2017 20:42:38 -0700 Subject: [PATCH 176/607] code cleanup --- pyqtgraph/graphicsItems/ROI.py | 133 --------------------------------- 1 file changed, 133 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 9f3dae6f..319524e9 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -113,7 +113,6 @@ class ROI(GraphicsObject): sigRemoveRequested = QtCore.Signal(object) def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False): - #QObjectWorkaround.__init__(self) GraphicsObject.__init__(self, parent) self.setAcceptedMouseButtons(QtCore.Qt.NoButton) pos = Point(pos) @@ -148,7 +147,6 @@ class ROI(GraphicsObject): self.translateSnap = translateSnap self.rotateSnap = rotateSnap self.scaleSnap = scaleSnap - #self.setFlag(self.ItemIsSelectable, True) def getState(self): return self.stateCopy() @@ -268,7 +266,6 @@ class ROI(GraphicsObject): raise TypeError("update argument must be bool") self.state['angle'] = angle tr = QtGui.QTransform() - #tr.rotate(-angle * 180 / np.pi) tr.rotate(angle) self.setTransform(tr) if update: @@ -316,20 +313,14 @@ class ROI(GraphicsObject): newState = self.stateCopy() newState['pos'] = newState['pos'] + pt - ## snap position - #snap = kargs.get('snap', None) - #if (snap is not False) and not (snap is None and self.translateSnap is False): - snap = kargs.get('snap', None) if snap is None: snap = self.translateSnap if snap is not False: newState['pos'] = self.getSnapPosition(newState['pos'], snap=snap) - #d = ev.scenePos() - self.mapToScene(self.pressPos) if self.maxBounds is not None: r = self.stateRect(newState) - #r0 = self.sceneTransform().mapRect(self.boundingRect()) d = Point(0,0) if self.maxBounds.left() > r.left(): d[0] = self.maxBounds.left() - r.left() @@ -341,12 +332,9 @@ class ROI(GraphicsObject): d[1] = self.maxBounds.bottom() - r.bottom() newState['pos'] += d - #self.state['pos'] = newState['pos'] update = kargs.get('update', True) finish = kargs.get('finish', True) self.setPos(newState['pos'], update=update, finish=finish) - #if 'update' not in kargs or kargs['update'] is True: - #self.stateChanged() def rotate(self, angle, update=True, finish=True): """ @@ -629,7 +617,6 @@ class ROI(GraphicsObject): for h in self.handles: h['item'].hide() - def hoverEvent(self, ev): hover = False if not ev.isExit(): @@ -870,10 +857,8 @@ class ROI(GraphicsObject): r = self.stateRect(newState) if not self.maxBounds.contains(r): return - #self.setTransform(tr) self.setPos(newState['pos'], update=False) self.setAngle(ang, update=False) - #self.state = newState ## If this is a free-rotate handle, its distance from the center may change. @@ -898,7 +883,6 @@ class ROI(GraphicsObject): if ang is None: return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - #ang = round(ang / (np.pi/12.)) * (np.pi/12.) ang = round(ang / 15.) * 15. hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) @@ -949,9 +933,6 @@ class ROI(GraphicsObject): if h['item'] in self.childItems(): p = h['pos'] h['item'].setPos(h['pos'] * self.state['size']) - #else: - # trans = self.state['pos']-self.lastState['pos'] - # h['item'].setPos(h['pos'] + h['item'].parentItem().mapFromParent(trans)) self.update() self.sigRegionChanged.emit(self) @@ -971,12 +952,10 @@ class ROI(GraphicsObject): def stateRect(self, state): r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) tr = QtGui.QTransform() - #tr.rotate(-state['angle'] * 180 / np.pi) tr.rotate(-state['angle']) r = tr.mapRect(r) return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) - def getSnapPosition(self, pos, snap=None): ## Given that pos has been requested, return the nearest snap-to position ## optionally, snap may be passed in to specify a rectangular snap grid. @@ -996,7 +975,6 @@ class ROI(GraphicsObject): return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() def paint(self, p, opt, widget): - # p.save() # Note: don't use self.boundingRect here, because subclasses may need to redefine it. r = QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() @@ -1005,7 +983,6 @@ class ROI(GraphicsObject): p.translate(r.left(), r.top()) p.scale(r.width(), r.height()) p.drawRect(0, 0, 1, 1) - # p.restore() def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): """Return a tuple of slice objects that can be used to slice the region @@ -1133,11 +1110,8 @@ class ROI(GraphicsObject): lvx = np.sqrt(vx.x()**2 + vx.y()**2) lvy = np.sqrt(vy.x()**2 + vy.y()**2) - #pxLen = img.width() / float(data.shape[axes[0]]) ##img.width is number of pixels, not width of item. ##need pxWidth and pxHeight instead of pxLen ? - #sx = pxLen / lvx - #sy = pxLen / lvy sx = 1.0 / lvx sy = 1.0 / lvy @@ -1167,7 +1141,6 @@ class ROI(GraphicsObject): if width == 0 or height == 0: return np.empty((width, height), dtype=float) - # QImage(width, height, format) im = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32) im.fill(0x0) p = QtGui.QPainter(im) @@ -1197,27 +1170,6 @@ class ROI(GraphicsObject): t1 = SRTTransform(relativeTo) t2 = SRTTransform(st) return t2/t1 - - - #st = self.getState() - - ### rotation - #ang = (st['angle']-relativeTo['angle']) * 180. / 3.14159265358 - #rot = QtGui.QTransform() - #rot.rotate(-ang) - - ### We need to come up with a universal transformation--one that can be applied to other objects - ### such that all maintain alignment. - ### More specifically, we need to turn the ROI's position and angle into - ### a rotation _around the origin_ and a translation. - - #p0 = Point(relativeTo['pos']) - - ### base position, rotated - #p1 = rot.map(p0) - - #trans = Point(st['pos']) - p1 - #return trans, ang def applyGlobalTransform(self, tr): st = self.getState() @@ -1239,8 +1191,6 @@ class Handle(UIGraphicsItem): Handles may be dragged to change the position, size, orientation, or other properties of the ROI they are attached to. - - """ types = { ## defines number of sides, start angle for each handle type 't': (4, np.pi/4), @@ -1255,9 +1205,6 @@ class Handle(UIGraphicsItem): sigRemoveRequested = QtCore.Signal(object) # self def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False): - #print " create item with parent", parent - #self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10) - #self.setFlags(self.ItemIgnoresTransformations | self.ItemSendsScenePositionChanges) self.rois = [] self.radius = radius self.typ = typ @@ -1276,7 +1223,6 @@ class Handle(UIGraphicsItem): self.deletable = deletable if deletable: self.setAcceptedMouseButtons(QtCore.Qt.RightButton) - #self.updateShape() self.setZValue(11) def connectROI(self, roi): @@ -1285,13 +1231,6 @@ class Handle(UIGraphicsItem): def disconnectROI(self, roi): self.rois.remove(roi) - #for i, r in enumerate(self.roi): - #if r[0] == roi: - #self.roi.pop(i) - - #def close(self): - #for r in self.roi: - #r.removeHandle(self) def setDeletable(self, b): self.deletable = b @@ -1317,21 +1256,12 @@ class Handle(UIGraphicsItem): else: self.currentPen = self.pen self.update() - #if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): - #self.currentPen = fn.mkPen(255, 255,0) - #else: - #self.currentPen = self.pen - #self.update() - - def mouseClickEvent(self, ev): ## right-click cancels drag if ev.button() == QtCore.Qt.RightButton and self.isMoving: self.isMoving = False ## prevents any further motion self.movePoint(self.startPos, finish=True) - #for r in self.roi: - #r[0].cancelMove() ev.accept() elif int(ev.button() & self.acceptedMouseButtons()) > 0: ev.accept() @@ -1340,12 +1270,6 @@ class Handle(UIGraphicsItem): self.sigClicked.emit(self, ev) else: ev.ignore() - - #elif self.deletable: - #ev.accept() - #self.raiseContextMenu(ev) - #else: - #ev.ignore() def buildMenu(self): menu = QtGui.QMenu() @@ -1416,36 +1340,10 @@ class Handle(UIGraphicsItem): self.path.lineTo(x, y) def paint(self, p, opt, widget): - ### determine rotation of transform - #m = self.sceneTransform() - ##mi = m.inverted()[0] - #v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0)) - #va = np.arctan2(v.y(), v.x()) - - ### Determine length of unit vector in painter's coords - ##size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0)) - ##size = (size.x()*size.x() + size.y() * size.y()) ** 0.5 - #size = self.radius - - #bounds = QtCore.QRectF(-size, -size, size*2, size*2) - #if bounds != self.bounds: - #self.bounds = bounds - #self.prepareGeometryChange() p.setRenderHints(p.Antialiasing, True) p.setPen(self.currentPen) - #p.rotate(va * 180. / 3.1415926) - #p.drawPath(self.path) p.drawPath(self.shape()) - #ang = self.startAng + va - #dt = 2*np.pi / self.sides - #for i in range(0, self.sides): - #x1 = size * cos(ang) - #y1 = size * sin(ang) - #x2 = size * cos(ang+dt) - #y2 = size * sin(ang+dt) - #ang += dt - #p.drawLine(Point(x1, y1), Point(x2, y2)) def shape(self): if self._shape is None: @@ -1457,18 +1355,10 @@ class Handle(UIGraphicsItem): return self._shape def boundingRect(self): - #print 'roi:', self.roi s1 = self.shape() - #print " s1:", s1 - #s2 = self.shape() - #print " s2:", s2 - return self.shape().boundingRect() def generateShape(self): - ## determine rotation of transform - #m = self.sceneTransform() ## Qt bug: do not access sceneTransform() until we know this object has a scene. - #mi = m.inverted()[0] dt = self.deviceTransform() if dt is None: @@ -1486,22 +1376,15 @@ class Handle(UIGraphicsItem): return dti.map(tr.map(self.path)) - def viewTransformChanged(self): GraphicsObject.viewTransformChanged(self) self._shape = None ## invalidate shape, recompute later if requested. self.update() - - #def itemChange(self, change, value): - #if change == self.ItemScenePositionHasChanged: - #self.updateShape() class TestROI(ROI): def __init__(self, pos, size, **args): - #QtGui.QGraphicsRectItem.__init__(self, pos[0], pos[1], size[0], size[1]) ROI.__init__(self, pos, size, **args) - #self.addTranslateHandle([0, 0]) self.addTranslateHandle([0.5, 0.5]) self.addScaleHandle([1, 1], [0, 0]) self.addScaleHandle([0, 0], [1, 1]) @@ -1511,7 +1394,6 @@ class TestROI(ROI): self.addRotateHandle([0, 1], [1, 1]) - class RectROI(ROI): """ Rectangular ROI subclass with a single scale handle at the top-right corner. @@ -1530,14 +1412,12 @@ class RectROI(ROI): """ def __init__(self, pos, size, centered=False, sideScalers=False, **args): - #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) ROI.__init__(self, pos, size, **args) if centered: center = [0.5, 0.5] else: center = [0, 0] - #self.addTranslateHandle(center) self.addScaleHandle([1, 1], center) if sideScalers: self.addScaleHandle([1, 0.5], [center[0], 0.5]) @@ -1646,7 +1526,6 @@ class MultiRectROI(QtGui.QGraphicsObject): rgn = l.getArrayRegion(arr, img, axes=axes, **kwds) if rgn is None: continue - #return None rgns.append(rgn) #print l.state['size'] @@ -1731,7 +1610,6 @@ class EllipseROI(ROI): """ def __init__(self, pos, size, **args): - #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) self.path = None ROI.__init__(self, pos, size, **args) self.sigRegionChanged.connect(self._clearPath) @@ -1835,22 +1713,14 @@ class PolygonROI(ROI): if pos is None: pos = [0,0] ROI.__init__(self, pos, [1,1], **args) - #ROI.__init__(self, positions[0]) for p in positions: self.addFreeHandle(p) self.setZValue(1000) print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.") - def listPoints(self): return [p['item'].pos() for p in self.handles] - #def movePoint(self, *args, **kargs): - #ROI.movePoint(self, *args, **kargs) - #self.prepareGeometryChange() - #for h in self.handles: - #h['pos'] = h['item'].pos() - def paint(self, p, *args): p.setRenderHint(QtGui.QPainter.Antialiasing) p.setPen(self.currentPen) @@ -1877,7 +1747,6 @@ class PolygonROI(ROI): sc['pos'] = Point(self.state['pos']) sc['size'] = Point(self.state['size']) sc['angle'] = self.state['angle'] - #sc['handles'] = self.handles return sc @@ -2097,7 +1966,6 @@ class LineSegmentROI(ROI): pos = [0,0] ROI.__init__(self, pos, [1,1], **args) - #ROI.__init__(self, positions[0]) if len(positions) > 2: raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.") @@ -2193,7 +2061,6 @@ class CrosshairROI(ROI): def __init__(self, pos=None, size=None, **kargs): if size == None: - #size = [100e-6,100e-6] size=[1,1] if pos == None: pos = [0,0] From 9b9a72e6bfdddf85287741330f9f208e68327285 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Oct 2017 20:56:19 -0700 Subject: [PATCH 177/607] minor image testing edits --- pyqtgraph/tests/image_testing.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index c8a41dec..2bd6e8d3 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -10,11 +10,13 @@ Procedure for unit-testing with images: $ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py - Any failing tests will - display the test results, standard image, and the differences between the - two. If the test result is bad, then press (f)ail. If the test result is - good, then press (p)ass and the new image will be saved to the test-data - directory. + Any failing tests will display the test results, standard image, and the + differences between the two. If the test result is bad, then press (f)ail. + If the test result is good, then press (p)ass and the new image will be + saved to the test-data directory. + + To check all test results regardless of whether the test failed, set the + environment variable PYQTGRAPH_AUDIT_ALL=1. 3. After adding or changing test images, create a new commit: @@ -162,6 +164,8 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): # If the test image does not match, then we go to audit if requested. try: + if stdImage is None: + raise Exception("No reference image saved for this test.") if image.shape[2] != stdImage.shape[2]: raise Exception("Test result has different channel count than standard image" "(%d vs %d)" % (image.shape[2], stdImage.shape[2])) From c6839b4708f331fdc1a16f859f658846f38b3827 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 17 Oct 2017 21:22:55 -0700 Subject: [PATCH 178/607] fix: polylineroi segment draws to wrong handle after click --- pyqtgraph/graphicsItems/ROI.py | 10 ++++++---- pyqtgraph/graphicsItems/tests/test_ROI.py | 8 ++++++++ pyqtgraph/tests/image_testing.py | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 319524e9..9682b6b3 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -571,7 +571,6 @@ class ROI(GraphicsObject): ## Note: by default, handles are not user-removable even if this method returns True. return True - def getLocalHandlePositions(self, index=None): """Returns the position of handles in the ROI's coordinate system. @@ -1969,9 +1968,13 @@ class LineSegmentROI(ROI): if len(positions) > 2: raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.") - self.endpoints = [] for i, p in enumerate(positions): - self.endpoints.append(self.addFreeHandle(p, item=handles[i])) + self.addFreeHandle(p, item=handles[i]) + + @property + def endpoints(self): + # must not be cached because self.handles may change. + return [h['item'] for h in self.handles] def listPoints(self): return [p['item'].pos() for p in self.handles] @@ -2018,7 +2021,6 @@ class LineSegmentROI(ROI): See ROI.getArrayRegion() for a description of the arguments. """ - imgPts = [self.mapToItem(img, h.pos()) for h in self.endpoints] rgns = [] coords = [] diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index ddc7f173..8cc2efd5 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -208,15 +208,23 @@ def test_PolyLineROI(): # click segment mouseClick(plt, pt, QtCore.Qt.LeftButton) assertImageApproved(plt, 'roi/polylineroi/'+name+'_click_segment', 'Click mouse over segment.') + + # drag new handle + mouseMove(plt, pt+pg.Point(10, -10)) # pg bug: have to move the mouse off/on again to register hover + mouseDrag(plt, pt, pt + pg.Point(10, -10), QtCore.Qt.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_new_handle', 'Drag mouse over created handle.') + # clear all points r.clearPoints() assertImageApproved(plt, 'roi/polylineroi/'+name+'_clear', 'All points cleared.') assert len(r.getState()['points']) == 0 + # call setPoints r.setPoints(initState['points']) assertImageApproved(plt, 'roi/polylineroi/'+name+'_setpoints', 'Reset points to initial state.') assert len(r.getState()['points']) == 3 + # call setState r.setState(initState) assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.') assert len(r.getState()['points']) == 3 diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 2bd6e8d3..a7552631 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -44,7 +44,7 @@ Procedure for unit-testing with images: # pyqtgraph should be tested against. When adding or changing test images, # create and push a new tag and update this variable. To test locally, begin # by creating the tag in your ~/.pyqtgraph/test-data repository. -testDataTag = 'test-data-6' +testDataTag = 'test-data-7' import time From 7be6f1e70cbbbd0839d71dca9557e38afd86f043 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Oct 2017 00:18:07 -0700 Subject: [PATCH 179/607] fix: error when using SpinBox(delay) argument --- pyqtgraph/widgets/SpinBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index b8066cd7..499a3554 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -106,11 +106,11 @@ class SpinBox(QtGui.QAbstractSpinBox): self.skipValidate = False self.setCorrectionMode(self.CorrectToPreviousValue) self.setKeyboardTracking(False) + self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay']) self.setOpts(**kwargs) self._updateHeight() self.editingFinished.connect(self.editingFinishedEvent) - self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay']) def event(self, ev): ret = QtGui.QAbstractSpinBox.event(self, ev) From 60e6591608ee1e9ab26fa86408f7e86d1ef62ba6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Oct 2017 00:18:46 -0700 Subject: [PATCH 180/607] Fix verlet integration demo --- examples/verlet_chain/chain.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/verlet_chain/chain.py b/examples/verlet_chain/chain.py index 6eb3501a..1c4f2403 100644 --- a/examples/verlet_chain/chain.py +++ b/examples/verlet_chain/chain.py @@ -32,8 +32,6 @@ class ChainSim(pg.QtCore.QObject): if self.initialized: return - assert None not in [self.pos, self.mass, self.links, self.lengths] - if self.fixed is None: self.fixed = np.zeros(self.pos.shape[0], dtype=bool) if self.push is None: From 001070d9ff8e2e652e0ad29ca62e35f06e4a805f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 18 Oct 2017 00:26:37 -0700 Subject: [PATCH 181/607] Add new fractal demo --- examples/fractal.py | 122 ++++++++++++++++++++++++++++++++++++++++++++ examples/utils.py | 1 + 2 files changed, 123 insertions(+) create mode 100644 examples/fractal.py diff --git a/examples/fractal.py b/examples/fractal.py new file mode 100644 index 00000000..eeb1bdb0 --- /dev/null +++ b/examples/fractal.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" +Displays an interactive Koch fractal +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import pyqtgraph as pg +from pyqtgraph.Qt import QtCore, QtGui +import numpy as np + +app = QtGui.QApplication([]) + +# Set up UI widgets +win = pg.QtGui.QWidget() +win.setWindowTitle('pyqtgraph example: fractal demo') +layout = pg.QtGui.QGridLayout() +win.setLayout(layout) +layout.setContentsMargins(0, 0, 0, 0) +depthLabel = pg.QtGui.QLabel('fractal depth:') +layout.addWidget(depthLabel, 0, 0) +depthSpin = pg.SpinBox(value=5, step=1, bounds=[1, 10], delay=0, int=True) +depthSpin.resize(100, 20) +layout.addWidget(depthSpin, 0, 1) +w = pg.GraphicsLayoutWidget() +layout.addWidget(w, 1, 0, 1, 2) +win.show() + +# Set up graphics +v = w.addViewBox() +v.setAspectLocked() +baseLine = pg.PolyLineROI([[0, 0], [1, 0], [1.5, 1], [2, 0], [3, 0]], pen=(0, 255, 0, 100), movable=False) +v.addItem(baseLine) +fc = pg.PlotCurveItem(pen=(255, 255, 255, 200), antialias=True) +v.addItem(fc) +v.autoRange() + + +transformMap = [0, 0, None] + + +def update(): + # recalculate and redraw the fractal curve + + depth = depthSpin.value() + pts = baseLine.getState()['points'] + nbseg = len(pts) - 1 + nseg = nbseg**depth + + # Get a transformation matrix for each base segment + trs = [] + v1 = pts[-1] - pts[0] + l1 = v1.length() + for i in range(len(pts)-1): + p1 = pts[i] + p2 = pts[i+1] + v2 = p2 - p1 + t = p1 - pts[0] + r = v2.angle(v1) + s = v2.length() / l1 + trs.append(pg.SRTTransform({'pos': t, 'scale': (s, s), 'angle': r})) + + basePts = [np.array(list(pt) + [1]) for pt in baseLine.getState()['points']] + baseMats = np.dstack([tr.matrix().T for tr in trs]).transpose(2, 0, 1) + + # Generate an array of matrices to transform base points + global transformMap + if transformMap[:2] != [depth, nbseg]: + # we can cache the transform index to save a little time.. + nseg = nbseg**depth + matInds = np.empty((depth, nseg), dtype=int) + for i in range(depth): + matInds[i] = np.tile(np.repeat(np.arange(nbseg), nbseg**(depth-1-i)), nbseg**i) + transformMap = [depth, nbseg, matInds] + + # Each column in matInds contains the indices referring to the base transform + # matrices that must be multiplied together to generate the final transform + # for each segment of the fractal + matInds = transformMap[2] + + # Collect all matrices needed for generating fractal curve + mats = baseMats[matInds] + + # Magic-multiply stacks of matrices together + def matmul(a, b): + return np.sum(np.transpose(a,(0,2,1))[..., None] * b[..., None, :], axis=-3) + mats = reduce(matmul, mats) + + # Transform base points through matrix array + pts = np.empty((nseg * nbseg + 1, 2)) + for l in range(len(trs)): + bp = basePts[l] + pts[l:-1:len(trs)] = np.dot(mats, bp)[:, :2] + + # Finish the curve with the last base point + pts[-1] = basePts[-1][:2] + + # update fractal curve with new points + fc.setData(pts[:,0], pts[:,1]) + + +# Update the fractal whenever the base shape or depth has changed +baseLine.sigRegionChanged.connect(update) +depthSpin.valueChanged.connect(update) + +# Initialize +update() + + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + QtGui.QApplication.instance().exec_() + + + + + + + + + \ No newline at end of file diff --git a/examples/utils.py b/examples/utils.py index 88adc9c9..b004a0d3 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -32,6 +32,7 @@ examples = OrderedDict([ ('Optics', 'optics_demos.py'), ('Special relativity', 'relativity_demo.py'), ('Verlet chain', 'verlet_chain_demo.py'), + ('Koch Fractal', 'fractal.py'), ])), ('GraphicsItems', OrderedDict([ ('Scatter Plot', 'ScatterPlot.py'), From a15057835a78aedcb33e544cf2a94268bd048cf5 Mon Sep 17 00:00:00 2001 From: Petras Jokubauskas Date: Fri, 20 Oct 2017 15:35:53 +0200 Subject: [PATCH 182/607] fix: set foreground color for items which background color is statically set, so that values of those would be readable when OS uses dark theme. --- pyqtgraph/console/Console.py | 5 +++-- pyqtgraph/parametertree/parameterTypes.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 72164f33..c5c2e7b1 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -115,7 +115,7 @@ class ConsoleWidget(QtGui.QWidget): self.write("
%s\n"%encCmd, html=True) self.execMulti(cmd) else: - self.write("
%s\n"%encCmd, html=True) + self.write("
%s\n"%encCmd, html=True) self.inCmd = True self.execSingle(cmd) @@ -209,7 +209,7 @@ class ConsoleWidget(QtGui.QWidget): else: if self.inCmd: self.inCmd = False - self.output.textCursor().insertHtml("

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

") #self.stdout.write("

") self.output.insertPlainText(strn) #self.stdout.write(strn) @@ -366,6 +366,7 @@ class ConsoleWidget(QtGui.QWidget): self.ui.exceptionStackList.addItem('-- exception caught here: --') item = self.ui.exceptionStackList.item(self.ui.exceptionStackList.count()-1) item.setBackground(QtGui.QBrush(QtGui.QColor(200, 200, 200))) + item.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50))) self.frames.append(None) # And finish the rest of the stack up to the exception diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index d137410d..ec14faa1 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -400,6 +400,7 @@ class GroupParameterItem(ParameterItem): else: for c in [0,1]: self.setBackground(c, QtGui.QBrush(QtGui.QColor(220,220,220))) + self.setForeground(c, QtGui.QBrush(QtGui.QColor(100,100,100))) font = self.font(c) font.setBold(True) #font.setPointSize(font.pointSize()+1) From 5d0974673884e24b920847d7a4dd35e57d0e3337 Mon Sep 17 00:00:00 2001 From: miranis <33010847+miranis@users.noreply.github.com> Date: Mon, 23 Oct 2017 01:45:52 +0200 Subject: [PATCH 183/607] Update ImageItem.py Functions nanmin and nanmax are defined in the numpy module and cannot be accessed from the global namespace! --- pyqtgraph/graphicsItems/ImageItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 9588c586..04a5c757 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -329,7 +329,7 @@ class ImageItem(GraphicsObject): sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) data = data[sl] - return nanmin(data), nanmax(data) + return np.nanmin(data), np.nanmax(data) def updateImage(self, *args, **kargs): ## used for re-rendering qimage from self.image. From e16948dbeb702fd46ccca1f20c0a869ca05fb814 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 1 Nov 2017 16:57:45 -0700 Subject: [PATCH 184/607] Add datetime support to configfile --- pyqtgraph/configfile.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 7b20db1d..e7056599 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -9,7 +9,7 @@ file format. Data structures may be nested and contain any data type as long as it can be converted to/from a string using repr and eval. """ -import re, os, sys +import re, os, sys, datetime import numpy from .pgcollections import OrderedDict from . import units @@ -143,6 +143,7 @@ def parseString(lines, start=0): local['Point'] = Point local['QtCore'] = QtCore local['ColorMap'] = ColorMap + local['datetime'] = datetime # Needed for reconstructing numpy arrays local['array'] = numpy.array for dtype in ['int8', 'uint8', From 4752b777921f54b3a23dfc2952697ddf11922112 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 2 Nov 2017 10:25:57 -0700 Subject: [PATCH 185/607] Fix parallelizer's progressdialog usage --- pyqtgraph/multiprocess/parallelizer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 934bc6d0..86298023 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -101,7 +101,10 @@ class Parallelize(object): else: ## parent if self.showProgress: - self.progressDlg.__exit__(None, None, None) + try: + self.progressDlg.__exit__(None, None, None) + except Exception: + pass def runSerial(self): if self.showProgress: From 2db502a9cce1f6ff6605805c12c5d6bb90da91cd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 16 Nov 2017 09:25:27 -0800 Subject: [PATCH 186/607] Fix bug when switching level mode --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index e6d692e6..90e2e790 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -293,7 +293,6 @@ class HistogramLUTItem(GraphicsWidget): self.levelMode = mode self._showRegions() - self.imageChanged() # do our best to preserve old levels if mode == 'mono': @@ -302,7 +301,12 @@ class HistogramLUTItem(GraphicsWidget): else: levels = [oldLevels] * 4 self.setLevels(rgba=levels) + + # force this because calling self.setLevels might not set the imageItem + # levels if there was no change to the region item + self.imageItem().setLevels(self.getLevels()) + self.imageChanged() self.update() def _showRegions(self): From f90a21e9e679952b4e67e66cff025a53019051ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcus=20M=C3=BCller?= Date: Mon, 20 Nov 2017 11:20:17 +0100 Subject: [PATCH 187/607] Information is spelled with an r, even in comments --- pyqtgraph/Point.py | 2 +- pyqtgraph/Vector.py | 2 +- pyqtgraph/WidgetGroup.py | 2 +- pyqtgraph/configfile.py | 2 +- pyqtgraph/debug.py | 2 +- pyqtgraph/functions.py | 2 +- pyqtgraph/graphicsItems/MultiPlotItem.py | 2 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 +- pyqtgraph/graphicsItems/ROI.py | 2 +- pyqtgraph/graphicsWindows.py | 2 +- pyqtgraph/imageview/ImageView.py | 2 +- pyqtgraph/metaarray/MetaArray.py | 2 +- pyqtgraph/pgcollections.py | 2 +- pyqtgraph/ptime.py | 2 +- pyqtgraph/widgets/GraphicsView.py | 2 +- pyqtgraph/widgets/MultiPlotWidget.py | 2 +- pyqtgraph/widgets/PlotWidget.py | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index 4d04f01c..9e6491eb 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -2,7 +2,7 @@ """ Point.py - Extension of QPointF which adds a few missing methods. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtCore diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index f2898e80..c4a31428 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -2,7 +2,7 @@ """ Vector.py - Extension of QVector3D which adds a few missing methods. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtGui, QtCore, USE_PYSIDE diff --git a/pyqtgraph/WidgetGroup.py b/pyqtgraph/WidgetGroup.py index d7e265c5..9371bb97 100644 --- a/pyqtgraph/WidgetGroup.py +++ b/pyqtgraph/WidgetGroup.py @@ -2,7 +2,7 @@ """ WidgetGroup.py - WidgetGroup class for easily managing lots of Qt widgets Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. This class addresses the problem of having to save and restore the state of a large group of widgets. diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 7b20db1d..b8a8c44c 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -2,7 +2,7 @@ """ configfile.py - Human-readable text configuration file library Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Used for reading and writing dictionary objects to a python-like configuration file format. Data structures may be nested and contain any data type as long diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 0da24d7c..d6da1691 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -2,7 +2,7 @@ """ debug.py - Functions to aid in debugging Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from __future__ import print_function diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index d79c350f..5c107052 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2,7 +2,7 @@ """ functions.py - Miscellaneous functions with no other home Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from __future__ import division diff --git a/pyqtgraph/graphicsItems/MultiPlotItem.py b/pyqtgraph/graphicsItems/MultiPlotItem.py index be775d4a..065a605e 100644 --- a/pyqtgraph/graphicsItems/MultiPlotItem.py +++ b/pyqtgraph/graphicsItems/MultiPlotItem.py @@ -2,7 +2,7 @@ """ MultiPlotItem.py - Graphics item used for displaying an array of PlotItems Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from numpy import ndarray diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 41011df3..adc1950a 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -2,7 +2,7 @@ """ PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. This class is one of the workhorses of pyqtgraph. It implements a graphics item with plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 81a4e651..a913aa2e 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2,7 +2,7 @@ """ ROI.py - Interactive graphics items for GraphicsView (ROI widgets) Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Implements a series of graphics items which display movable/scalable/rotatable shapes for use as region-of-interest markers. ROI class automatically handles extraction diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 1aa3f3f4..3a5953cf 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -2,7 +2,7 @@ """ graphicsWindows.py - Convenience classes which create a new window with PlotWidget or ImageView. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtCore, QtGui diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..36ae5c73 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -2,7 +2,7 @@ """ ImageView.py - Widget for basic image dispay and analysis Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Widget used for displaying 2D or 3D data. Features: - float or int (including 16-bit int) image display via ImageItem diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 66ecc460..7984f6a8 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -2,7 +2,7 @@ """ MetaArray.py - Class encapsulating ndarray with meta data Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. MetaArray is an array class based on numpy.ndarray that allows storage of per-axis meta data such as axis values, names, units, column names, etc. It also enables several diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index 76850622..b1fc779f 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -2,7 +2,7 @@ """ advancedTypes.py - Basic data structures not included with python Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Includes: - OrderedDict - Dictionary which preserves the order of its elements diff --git a/pyqtgraph/ptime.py b/pyqtgraph/ptime.py index 1de8282f..4e761afe 100644 --- a/pyqtgraph/ptime.py +++ b/pyqtgraph/ptime.py @@ -2,7 +2,7 @@ """ ptime.py - Precision time function made os-independent (should have been taken care of by python) Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index f3f8cbb5..92547ef0 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -2,7 +2,7 @@ """ GraphicsView.py - Extension of QGraphicsView Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore, QtGui, USE_PYSIDE diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index d1f56034..21258839 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -2,7 +2,7 @@ """ MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiPlotItem Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore from .GraphicsView import GraphicsView diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 964307ae..8711e1f6 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -2,7 +2,7 @@ """ PlotWidget.py - Convenience class--GraphicsView widget displaying a single PlotItem Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore, QtGui From b9c7e379f37d54902f1a6c130fa959c149c5900c Mon Sep 17 00:00:00 2001 From: James Date: Mon, 18 Dec 2017 15:57:31 +0000 Subject: [PATCH 188/607] Prevent ReferenceErrors on cleanup Prevents a ReferenceError being thrown when PyQtGraph tries to put TensorFlow stuff onto a QGraphicsScene() on cleanup - see https://github.com/pyqtgraph/pyqtgraph/issues/603 and https://stackoverflow.com/questions/41542571/pyqtgraph-tries-to-put-tensorflow-stuff-onto-a-qgraphicsscene-on-cleanup --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 24653207..412c8627 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -320,7 +320,7 @@ def cleanup(): 'are properly called before app shutdown (%s)\n' % (o,)) s.addItem(o) - except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object + except (RuntimeError, ReferenceError): ## occurs if a python wrapper no longer has its underlying C++ object continue _cleanupCalled = True From 01d6e4b8e24821582007e85ad0e55f839734dfe0 Mon Sep 17 00:00:00 2001 From: Xinfa Zhu Date: Mon, 18 Dec 2017 16:16:49 -0600 Subject: [PATCH 189/607] Add name label to GradientEditorItem --- pyqtgraph/graphicsItems/GradientEditorItem.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index f359ff11..79d33c45 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -434,8 +434,14 @@ class GradientEditorItem(TickSliderItem): label = QtGui.QLabel() label.setPixmap(px) label.setContentsMargins(1, 1, 1, 1) + labelName = QtGui.QLabel(g) + hbox = QtGui.QHBoxLayout() + hbox.addWidget(labelName) + hbox.addWidget(label) + widget = QtGui.QWidget() + widget.setLayout(hbox) act = QtGui.QWidgetAction(self) - act.setDefaultWidget(label) + act.setDefaultWidget(widget) act.triggered.connect(self.contextMenuClicked) act.name = g self.menu.addAction(act) From 035b5a6c316d561005023af21cc0a82e589764b8 Mon Sep 17 00:00:00 2001 From: Girish Ramlugun Date: Tue, 9 Jan 2018 14:30:59 +1300 Subject: [PATCH 190/607] Play image along 't' axis instead of first axis --- pyqtgraph/imageview/ImageView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 5cc00f68..95ccf12c 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -469,7 +469,7 @@ class ImageView(QtGui.QWidget): n = int(self.playRate * dt) if n != 0: self.lastPlayTime += (float(n)/self.playRate) - if self.currentIndex+n > self.image.shape[0]: + if self.currentIndex+n > self.image.shape[self.axes['t']]: self.play(0) self.jumpFrames(n) From 4867149d83e0b77feb8e77df7cdcceb11357ff23 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:03:44 -0800 Subject: [PATCH 191/607] Be a little more tolerant of missing Qt packages, and defer import errors until we try to use the missing package. --- pyqtgraph/Qt.py | 62 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ad04cd76..749943f2 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -43,8 +43,29 @@ if QT_LIB is None: if QT_LIB is None: raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") + +class FailedImport(object): + """Used to defer ImportErrors until we are sure the module is needed. + """ + def __init__(self, err): + self.err = err + + def __getattr__(self, attr): + raise self.err + + if QT_LIB == PYSIDE: - from PySide import QtGui, QtCore, QtOpenGL, QtSvg + from PySide import QtGui, QtCore + + try: + from PySide import QtOpenGL + except ImportError as err: + QtOpenGL = FailedImport(err) + try: + from PySide import QtSvg + except ImportError as err: + QtSvg = FailedImport(err) + try: from PySide import QtTest if not hasattr(QtTest.QTest, 'qWait'): @@ -55,9 +76,9 @@ if QT_LIB == PYSIDE: while time.time() < start + msec * 0.001: QtGui.QApplication.processEvents() QtTest.QTest.qWait = qWait - - except ImportError: - pass + except ImportError as err: + QtTest = FailedImport(err) + import PySide try: from PySide import shiboken @@ -133,16 +154,16 @@ elif QT_LIB == PYQT4: from PyQt4 import QtGui, QtCore, uic try: from PyQt4 import QtSvg - except ImportError: - pass + except ImportError as err: + QtSvg = FailedImport(err) try: from PyQt4 import QtOpenGL - except ImportError: - pass + except ImportError as err: + QtOpenGL = FailedImport(err) try: from PyQt4 import QtTest - except ImportError: - pass + except ImportError as err: + QtTest = FailedImport(err) VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR @@ -157,24 +178,25 @@ elif QT_LIB == PYQT5: # users), we install a global exception hook to override this behavior. ver = QtCore.PYQT_VERSION_STR.split('.') if int(ver[1]) >= 5: - sys_excepthook = sys.excepthook - def pyqt5_qabort_override(*args, **kwds): - return sys_excepthook(*args, **kwds) - sys.excepthook = pyqt5_qabort_override + if sys.excepthook == sys.__excepthook__: + sys_excepthook = sys.excepthook + def pyqt5_qabort_override(*args, **kwds): + return sys_excepthook(*args, **kwds) + sys.excepthook = pyqt5_qabort_override try: from PyQt5 import QtSvg - except ImportError: - pass + except ImportError as err: + QtSvg = FailedImport(err) try: from PyQt5 import QtOpenGL - except ImportError: - pass + except ImportError as err: + QtOpenGL = FailedImport(err) try: from PyQt5 import QtTest QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed - except ImportError: - pass + except ImportError as err: + QtTest = FailedImport(err) # Re-implement deprecated APIs From 52754d48594f713092a7e6b55a6fdcb046f76be6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:06:05 -0800 Subject: [PATCH 192/607] fix __getattr__ handling in PlotWindow --- pyqtgraph/widgets/PlotWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 964307ae..6e10b13a 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -76,7 +76,7 @@ class PlotWidget(GraphicsView): m = getattr(self.plotItem, attr) if hasattr(m, '__call__'): return m - raise NameError(attr) + raise AttributeError(attr) def viewRangeChanged(self, view, range): #self.emit(QtCore.SIGNAL('viewChanged'), *args) From 09aa19873147d612388131fb15253f9c47bae16e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:06:37 -0800 Subject: [PATCH 193/607] Add top level stack() function for debugging --- pyqtgraph/__init__.py | 19 +++++++++++++++++++ pyqtgraph/console/Console.py | 8 +++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 412c8627..520ea196 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -447,6 +447,25 @@ def dbg(*args, **kwds): except NameError: consoles = [c] return c + + +def stack(*args, **kwds): + """ + Create a console window and show the current stack trace. + + All arguments are passed to :func:`ConsoleWidget.__init__() `. + """ + mkQApp() + from . import console + c = console.ConsoleWidget(*args, **kwds) + c.setStack() + c.show() + global consoles + try: + consoles.append(c) + except NameError: + consoles = [c] + return c def mkQApp(): diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 72164f33..14890e0b 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -147,10 +147,11 @@ class ConsoleWidget(QtGui.QWidget): def currentFrame(self): ## Return the currently selected exception stack frame (or None if there is no exception) - if self.currentTraceback is None: - return None index = self.ui.exceptionStackList.currentRow() - return self.frames[index] + if index >= 0 and index < len(self.frames): + return self.frames[index] + else: + return None def execSingle(self, cmd): try: @@ -276,6 +277,7 @@ class ConsoleWidget(QtGui.QWidget): def clearExceptionClicked(self): self.currentTraceback = None + self.frames = [] self.ui.exceptionInfoLabel.setText("[No current exception]") self.ui.exceptionStackList.clear() self.ui.clearExceptionBtn.setEnabled(False) From 0653c8ec591d0c6c8fbf133ab0baeb535684b03a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:11:42 -0800 Subject: [PATCH 194/607] Add example and test demonstrating spinbox bug --- examples/SpinBox.py | 2 +- pyqtgraph/widgets/tests/test_spinbox.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/examples/SpinBox.py b/examples/SpinBox.py index 2faf10ee..ef1d0fc5 100644 --- a/examples/SpinBox.py +++ b/examples/SpinBox.py @@ -26,7 +26,7 @@ spins = [ ("Float with SI-prefixed units
(n, u, m, k, M, etc)", pg.SpinBox(value=0.9, suffix='V', siPrefix=True)), ("Float with SI-prefixed units,
dec step=0.1, minStep=0.1", - pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.1, minStep=0.1)), + pg.SpinBox(value=1.0, suffix='PSI', siPrefix=True, dec=True, step=0.1, minStep=0.1)), ("Float with SI-prefixed units,
dec step=0.5, minStep=0.01", pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), ("Float with SI-prefixed units,
dec step=1.0, minStep=0.001", diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py index 10087881..cff97da7 100644 --- a/pyqtgraph/widgets/tests/test_spinbox.py +++ b/pyqtgraph/widgets/tests/test_spinbox.py @@ -18,6 +18,8 @@ def test_spinbox_formatting(): (12345678955, '12345678955', dict(int=True, decimals=100)), (1.45e-9, '1.45e-09 A', dict(int=False, decimals=6, suffix='A', siPrefix=False)), (1.45e-9, '1.45 nA', dict(int=False, decimals=6, suffix='A', siPrefix=True)), + (1.45, '1.45 PSI', dict(int=False, decimals=6, suffix='PSI', siPrefix=True)), + (1.45e-3, '1.45 mPSI', dict(int=False, decimals=6, suffix='PSI', siPrefix=True)), (-2500.3427, '$-2500.34', dict(int=False, format='${value:0.02f}')), ] @@ -26,3 +28,14 @@ def test_spinbox_formatting(): sb.setValue(value) assert sb.value() == value assert pg.asUnicode(sb.text()) == text + + # test setting value + if not opts.get('int', False): + suf = sb.opts['suffix'] + sb.lineEdit().setText('0.1' + suf) + sb.editingFinishedEvent() + assert sb.value() == 0.1 + if suf != '': + sb.lineEdit().setText('0.1 m' + suf) + sb.editingFinishedEvent() + assert sb.value() == 0.1e-3 From a812d802da660bdb09885e6921a67fa11deffe4a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 09:12:10 -0800 Subject: [PATCH 195/607] Fix bug when spinbox units begin with an SI prefix (like 'PSI') --- pyqtgraph/functions.py | 25 +++++++++++++++++-------- pyqtgraph/widgets/SpinBox.py | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 7ad603f7..0495a00c 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -110,7 +110,7 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) -def siParse(s, regex=FLOAT_REGEX): +def siParse(s, regex=FLOAT_REGEX, suffix=None): """Convert a value written in SI notation to a tuple (number, si_prefix, suffix). Example:: @@ -118,6 +118,12 @@ def siParse(s, regex=FLOAT_REGEX): siParse('100 μV") # returns ('100', 'μ', 'V') """ s = asUnicode(s) + s = s.strip() + if suffix is not None and len(suffix) > 0: + if s[-len(suffix):] != suffix: + raise ValueError("String '%s' does not have the expected suffix '%s'" % (s, suffix)) + s = s[:-len(suffix)] + 'X' # add a fake suffix so the regex still picks up the si prefix + m = regex.match(s) if m is None: raise ValueError('Cannot parse number "%s"' % s) @@ -126,15 +132,18 @@ def siParse(s, regex=FLOAT_REGEX): except IndexError: sip = '' - try: - suf = m.group('suffix') - except IndexError: - suf = '' + if suffix is None: + try: + suf = m.group('suffix') + except IndexError: + suf = '' + else: + suf = suffix return m.group('number'), '' if sip is None else sip, '' if suf is None else suf -def siEval(s, typ=float, regex=FLOAT_REGEX): +def siEval(s, typ=float, regex=FLOAT_REGEX, suffix=None): """ Convert a value written in SI notation to its equivalent prefixless value. @@ -142,9 +151,9 @@ def siEval(s, typ=float, regex=FLOAT_REGEX): siEval("100 μV") # returns 0.0001 """ - val, siprefix, suffix = siParse(s, regex) + val, siprefix, suffix = siParse(s, regex, suffix=suffix) v = typ(val) - return siApply(val, siprefix) + return siApply(v, siprefix) def siApply(val, siprefix): diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index b8066cd7..17caea32 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -518,7 +518,7 @@ class SpinBox(QtGui.QAbstractSpinBox): # tokenize into numerical value, si prefix, and suffix try: - val, siprefix, suffix = fn.siParse(strn, self.opts['regex']) + val, siprefix, suffix = fn.siParse(strn, self.opts['regex'], suffix=self.opts['suffix']) except Exception: return False From 019c421ca102233ba2df27653c723b90a593e09e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 24 Jan 2018 17:48:03 -0800 Subject: [PATCH 196/607] Don't attempt to set same level mode again --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 90e2e790..f85b64dd 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -289,8 +289,10 @@ class HistogramLUTItem(GraphicsWidget): """ assert mode in ('mono', 'rgba') - oldLevels = self.getLevels() + if mode == self.levelMode: + return + oldLevels = self.getLevels() self.levelMode = mode self._showRegions() From 7d467cb65242d5ceda510290576a1867b9c2c36a Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 26 Jan 2018 08:58:18 -0800 Subject: [PATCH 197/607] Add changelog entry describing new behavior --- CHANGELOG | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 388f51b9..33106c8f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,13 @@ +pyqtgraph-0.11.0 (in development) + + API / behavior changes: + - ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system. + The result is visually the same, but children of ArrowItem are no longer rotated + (this allows screen-aligned text to be attached more easily). + To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument. + + + pyqtgraph-0.10.0 New Features: From b6838eb8c4b60f914d0dcd3e23cba273e1360303 Mon Sep 17 00:00:00 2001 From: Terekhov Date: Sun, 28 Jan 2018 18:10:39 -0500 Subject: [PATCH 198/607] Use symbol id for a key in SymbolAtlas Add test on a custom symbol for ScatterPlotItem. In PyQt5 QPainterPath is not hashable anymore which causes SymbolAtlas to fail accept it as a custom symbol, use id instead. --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 5 ++-- .../tests/test_ScatterPlotItem.py | 26 ++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 597491f3..443cc220 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -126,7 +126,7 @@ class SymbolAtlas(object): keyi = None sourceRecti = None for i, rec in enumerate(opts): - key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? + key = (id(rec[3]), rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? if key == keyi: sourceRect[i] = sourceRecti else: @@ -136,6 +136,7 @@ class SymbolAtlas(object): newRectSrc = QtCore.QRectF() newRectSrc.pen = rec['pen'] newRectSrc.brush = rec['brush'] + newRectSrc.symbol = rec[3] self.symbolMap[key] = newRectSrc self.atlasValid = False sourceRect[i] = newRectSrc @@ -151,7 +152,7 @@ class SymbolAtlas(object): images = [] for key, sourceRect in self.symbolMap.items(): if sourceRect.width() == 0: - img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.brush) + img = renderSymbol(sourceRect.symbol, key[1], sourceRect.pen, sourceRect.brush) images.append(img) ## we only need this to prevent the images being garbage collected immediately arr = fn.imageToArray(img, copy=False, transpose=False) else: diff --git a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py index acf6ad72..ba1fb9d7 100644 --- a/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py @@ -1,3 +1,4 @@ +from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np app = pg.mkQApp() @@ -7,9 +8,16 @@ app.processEvents() def test_scatterplotitem(): plot = pg.PlotWidget() - # set view range equal to its bounding rect. + # set view range equal to its bounding rect. # This causes plots to look the same regardless of pxMode. plot.setRange(rect=plot.boundingRect()) + + # test SymbolAtlas accepts custom symbol + s = pg.ScatterPlotItem() + symbol = QtGui.QPainterPath() + symbol.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) + s.addPoints([{'pos': [0,0], 'data': 1, 'symbol': symbol}]) + for i, pxMode in enumerate([True, False]): for j, useCache in enumerate([True, False]): s = pg.ScatterPlotItem() @@ -17,14 +25,14 @@ def test_scatterplotitem(): plot.addItem(s) s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode) s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30]) - + # Test uniform spot updates s.setSize(10) s.setBrush('r') s.setPen('g') s.setSymbol('+') app.processEvents() - + # Test list spot updates s.setSize([10] * 6) s.setBrush([pg.mkBrush('r')] * 6) @@ -55,7 +63,7 @@ def test_scatterplotitem(): def test_init_spots(): plot = pg.PlotWidget() - # set view range equal to its bounding rect. + # set view range equal to its bounding rect. # This causes plots to look the same regardless of pxMode. plot.setRange(rect=plot.boundingRect()) spots = [ @@ -63,28 +71,28 @@ def test_init_spots(): {'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'}, ] s = pg.ScatterPlotItem(spots=spots) - + # Check we can display without errors plot.addItem(s) app.processEvents() plot.clear() - + # check data is correct spots = s.points() - + defPen = pg.mkPen(pg.getConfigOption('foreground')) assert spots[0].pos().x() == 0 assert spots[0].pos().y() == 1 assert spots[0].pen() == defPen assert spots[0].data() is None - + assert spots[1].pos().x() == 1 assert spots[1].pos().y() == 2 assert spots[1].pen() == pg.mkPen(None) assert spots[1].brush() == pg.mkBrush(None) assert spots[1].data() == 'zzz' - + if __name__ == '__main__': test_scatterplotitem() From 2c30f8a7db7bfb9053b903fb5e582afa21e29eb6 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 29 Jan 2018 08:48:24 -0800 Subject: [PATCH 199/607] end travis testing on python 2.6 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c4a67ac3..acfde8ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,10 +17,10 @@ env: # Enable python 2 and python 3 builds # Note that the 2.6 build doesn't get flake8, and runs old versions of # Pyglet and GLFW to make sure we deal with those correctly - - PYTHON=2.6 QT=pyqt4 TEST=standard + #- PYTHON=2.6 QT=pyqt4 TEST=standard # 2.6 support ended - PYTHON=2.7 QT=pyqt4 TEST=extra - PYTHON=2.7 QT=pyside TEST=standard - - PYTHON=3.4 QT=pyqt5 TEST=standard + - PYTHON=3.5 QT=pyqt5 TEST=standard # - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda #- PYTHON=3.2 QT=pyqt5 TEST=standard From 551ccd105cacbbdbc2420e472e4e861b91658210 Mon Sep 17 00:00:00 2001 From: Terekhov Date: Sun, 28 Jan 2018 18:14:48 -0500 Subject: [PATCH 200/607] Add an example of using text strings as a custom smbol in ScatterPlotItem --- examples/ScatterPlot.py | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 72022acc..93f184f2 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -11,6 +11,7 @@ import initExample from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np +from collections import namedtuple app = QtGui.QApplication([]) mw = QtGui.QMainWindow() @@ -32,8 +33,8 @@ print("Generating data, this takes a few seconds...") ## There are a few different ways we can draw scatter plots; each is optimized for different types of data: -## 1) All spots identical and transform-invariant (top-left plot). -## In this case we can get a huge performance boost by pre-rendering the spot +## 1) All spots identical and transform-invariant (top-left plot). +## In this case we can get a huge performance boost by pre-rendering the spot ## image and just drawing that image repeatedly. n = 300 @@ -57,21 +58,41 @@ s1.sigClicked.connect(clicked) -## 2) Spots are transform-invariant, but not identical (top-right plot). -## In this case, drawing is almsot as fast as 1), but there is more startup -## overhead and memory usage since each spot generates its own pre-rendered +## 2) Spots are transform-invariant, but not identical (top-right plot). +## In this case, drawing is almsot as fast as 1), but there is more startup +## overhead and memory usage since each spot generates its own pre-rendered ## image. +TextSymbol = namedtuple("TextSymbol", "label symbol scale") + +def createLabel(label, angle): + symbol = QtGui.QPainterPath() + #symbol.addText(0, 0, QFont("San Serif", 10), label) + f = QtGui.QFont() + f.setPointSize(10) + symbol.addText(0, 0, f, label) + br = symbol.boundingRect() + scale = min(1. / br.width(), 1. / br.height()) + tr = QtGui.QTransform() + tr.scale(scale, scale) + tr.rotate(angle) + tr.translate(-br.x() - br.width()/2., -br.y() - br.height()/2.) + return TextSymbol(label, tr.map(symbol), 0.1 / scale) + +random_str = lambda : (''.join([chr(np.random.randint(ord('A'),ord('z'))) for i in range(np.random.randint(1,5))]), np.random.randint(0, 360)) + s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) pos = np.random.normal(size=(2,n), scale=1e-5) spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] s2.addPoints(spots) +spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': label[1], 'size': label[2]*(5+i/10.)} for (i, label) in [(i, createLabel(*random_str())) for i in range(n)]] +s2.addPoints(spots) w2.addItem(s2) s2.sigClicked.connect(clicked) -## 3) Spots are not transform-invariant, not identical (bottom-left). -## This is the slowest case, since all spots must be completely re-drawn +## 3) Spots are not transform-invariant, not identical (bottom-left). +## This is the slowest case, since all spots must be completely re-drawn ## every time because their apparent transformation may have changed. s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to transform with the view From 2b0559fd75d04bfcfb7b8344641a9c49f7a99878 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 31 Jan 2018 08:44:09 -0800 Subject: [PATCH 201/607] adjust group parameter fg color --- pyqtgraph/parametertree/parameterTypes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index ec14faa1..42a18fe0 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -400,7 +400,7 @@ class GroupParameterItem(ParameterItem): else: for c in [0,1]: self.setBackground(c, QtGui.QBrush(QtGui.QColor(220,220,220))) - self.setForeground(c, QtGui.QBrush(QtGui.QColor(100,100,100))) + self.setForeground(c, QtGui.QBrush(QtGui.QColor(50,50,50))) font = self.font(c) font.setBold(True) #font.setPointSize(font.pointSize()+1) From 8d9cb79da442b895f6c2a2bdf9c000c076ad64a7 Mon Sep 17 00:00:00 2001 From: Fekete Imre Date: Wed, 31 Jan 2018 23:35:18 +0100 Subject: [PATCH 202/607] Fix issue # 366 Set the right texture for rendering, otherwise a previously set texture is overwritten. --- pyqtgraph/opengl/GLViewWidget.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index e0fee046..71dae9c5 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -450,6 +450,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0) self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region + glBindTexture(GL_TEXTURE_2D, tex) # fixes issue #366 ## read texture back to array data = glGetTexImage(GL_TEXTURE_2D, 0, format, type) From a17d4a6e151ce8492054289efe519e0a19d85164 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 1 Feb 2018 11:42:01 -0800 Subject: [PATCH 203/607] Fix ConsoleWidget to work with changed stack format in python 3 --- pyqtgraph/console/Console.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 9b5b1bf7..634aab4a 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -357,6 +357,11 @@ class ConsoleWidget(QtGui.QWidget): # Build stack up to this point for index, line in enumerate(traceback.extract_stack(frame)): + # extract_stack return value changed in python 3.5 + if 'FrameSummary' in str(type(line)): + print(dir(line)) + line = (line.filename, line.lineno, line.name, line._line) + self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) while frame is not None: self.frames.insert(0, frame) @@ -373,6 +378,11 @@ class ConsoleWidget(QtGui.QWidget): # And finish the rest of the stack up to the exception for index, line in enumerate(traceback.extract_tb(tb)): + # extract_stack return value changed in python 3.5 + if 'FrameSummary' in str(type(line)): + print(dir(line)) + line = (line.filename, line.lineno, line.name, line._line) + self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) while tb is not None: self.frames.append(tb.tb_frame) From 9bff2b23fb85dbca45eef4baed310a8f5b1fcb31 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 1 Feb 2018 11:43:46 -0800 Subject: [PATCH 204/607] Add a signal for detecting scatter plot point clicks in ScatterPlotWidget --- pyqtgraph/widgets/ScatterPlotWidget.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index cca40e65..e0071f24 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -33,6 +33,8 @@ class ScatterPlotWidget(QtGui.QSplitter): specifying multiple criteria. 4) A PlotWidget for displaying the data. """ + sigScatterPlotClicked = QtCore.Signal(object, object) + def __init__(self, parent=None): QtGui.QSplitter.__init__(self, QtCore.Qt.Horizontal) self.ctrlPanel = QtGui.QSplitter(QtCore.Qt.Vertical) @@ -211,8 +213,7 @@ class ScatterPlotWidget(QtGui.QSplitter): self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style) self.scatterPlot.sigPointsClicked.connect(self.plotClicked) - def plotClicked(self, plot, points): - pass + self.sigScatterPlotClicked.emit(self, points) From 8a956bfddb3d31cadc80ab392995ddf6de22d507 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Fri, 2 Feb 2018 22:40:06 +0800 Subject: [PATCH 205/607] Add main window title for the examples --- examples/__main__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/__main__.py b/examples/__main__.py index 03c41119..6b8a07f9 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -26,6 +26,7 @@ class ExampleLoader(QtGui.QMainWindow): self.cw = QtGui.QWidget() self.setCentralWidget(self.cw) self.ui.setupUi(self.cw) + self.setWindowTitle("Examples") self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() From 7e1e7bfdc25427abb5c824cb91ff2d1d6daf8ca9 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Sat, 3 Feb 2018 00:07:49 +0800 Subject: [PATCH 206/607] Add bold style to the list Title --- doc/source/mouse_interaction.rst | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/source/mouse_interaction.rst b/doc/source/mouse_interaction.rst index 0e149f0c..3aea2527 100644 --- a/doc/source/mouse_interaction.rst +++ b/doc/source/mouse_interaction.rst @@ -9,11 +9,11 @@ Most applications that use pyqtgraph's data visualization will generate widgets In pyqtgraph, most 2D visualizations follow the following mouse interaction: -* Left button: Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. -* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. -* Right button click: Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. -* Middle button (or wheel) drag: Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). -* Wheel spin: Zooms the scene in and out. +* **Left button:** Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. +* **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. +* **Right button click:** Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. +* **Middle button (or wheel) drag:** Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). +* **Wheel spin:** Zooms the scene in and out. For machines where dragging with the right or middle buttons is difficult (usually Mac), another mouse interaction mode exists. In this mode, dragging with the left mouse button draws a box over a region of the scene. After the button is released, the scene is scaled and panned to fit the box. This mode can be accessed in the context menu or by calling:: @@ -38,11 +38,11 @@ The exact set of items available in the menu depends on the contents of the scen 3D visualizations use the following mouse interaction: -* Left button drag: Rotates the scene around a central point -* Middle button drag: Pan the scene by moving the central "look-at" point within the x-y plane -* Middle button drag + CTRL: Pan the scene by moving the central "look-at" point along the z axis -* Wheel spin: zoom in/out -* Wheel + CTRL: change field-of-view angle +* **Left button drag:** Rotates the scene around a central point +* **Middle button drag:** Pan the scene by moving the central "look-at" point within the x-y plane +* **Middle button drag + CTRL:** Pan the scene by moving the central "look-at" point along the z axis +* **Wheel spin:** zoom in/out +* **Wheel + CTRL:** change field-of-view angle And keyboard controls: From 708d9d252d480cc6fc5782b77f16a0d6bff5fc1e Mon Sep 17 00:00:00 2001 From: Billy Su Date: Sat, 3 Feb 2018 10:13:22 +0800 Subject: [PATCH 207/607] Add installation method using pip3 --- doc/source/installation.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/doc/source/installation.rst b/doc/source/installation.rst index e2bf0f8d..b53e53ee 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -3,7 +3,10 @@ Installation PyQtGraph does not really require any installation scripts. All that is needed is for the pyqtgraph folder to be placed someplace importable. Most people will prefer to simply place this folder within a larger project folder. If you want to make pyqtgraph available system-wide, use one of the methods listed below: +* **Using pip3(or pip):** Just run "`pip3 install pyqtgraph`" on your command line. * **Debian, Ubuntu, and similar Linux:** Download the .deb file linked at the top of the pyqtgraph web page or install using apt by putting "deb http://luke.campagnola.me/debian dev/" in your /etc/apt/sources.list file and install the python-pyqtgraph package. * **Arch Linux:** Looks like someone has posted unofficial packages for Arch (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) * **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. -* **Everybody (including OSX):** Download the .tar.gz source package linked at the top of the pyqtgraph web page, extract its contents, and run "python setup.py install" from within the extracted directory. +* **From Source(including OSX):** Download the .tar.gz source package linked at the top of the pyqtgraph_ web page, extract its contents, and run "`python setup.py install`" from within the extracted directory. + +.. _pyqtgraph: http://www.pyqtgraph.org/ From 81562b02528072e4bac98152bbbc07704ae9e431 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Sat, 3 Feb 2018 10:16:09 +0800 Subject: [PATCH 208/607] Refactor MultiPlotSpeedTest.py * Change variable to meaningful name, * Remove the outdated commented out code. --- examples/MultiPlotSpeedTest.py | 40 ++++++++++++++-------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/examples/MultiPlotSpeedTest.py b/examples/MultiPlotSpeedTest.py index 0d0d701b..f4295687 100644 --- a/examples/MultiPlotSpeedTest.py +++ b/examples/MultiPlotSpeedTest.py @@ -12,32 +12,27 @@ from pyqtgraph.Qt import QtGui, QtCore import numpy as np import pyqtgraph as pg from pyqtgraph.ptime import time -#QtGui.QApplication.setGraphicsSystem('raster') app = QtGui.QApplication([]) -#mw = QtGui.QMainWindow() -#mw.resize(800,800) -p = pg.plot() -p.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest') -#p.setRange(QtCore.QRectF(0, -10, 5000, 20)) -p.setLabel('bottom', 'Index', units='B') +plot = pg.plot() +plot.setWindowTitle('pyqtgraph example: MultiPlotSpeedTest') +plot.setLabel('bottom', 'Index', units='B') nPlots = 100 nSamples = 500 -#curves = [p.plot(pen=(i,nPlots*1.3)) for i in range(nPlots)] curves = [] -for i in range(nPlots): - c = pg.PlotCurveItem(pen=(i,nPlots*1.3)) - p.addItem(c) - c.setPos(0,i*6) - curves.append(c) +for idx in range(nPlots): + curve = pg.PlotCurveItem(pen=(idx,nPlots*1.3)) + plot.addItem(curve) + curve.setPos(0,idx*6) + curves.append(curve) -p.setYRange(0, nPlots*6) -p.setXRange(0, nSamples) -p.resize(600,900) +plot.setYRange(0, nPlots*6) +plot.setXRange(0, nSamples) +plot.resize(600,900) rgn = pg.LinearRegionItem([nSamples/5.,nSamples/3.]) -p.addItem(rgn) +plot.addItem(rgn) data = np.random.normal(size=(nPlots*23,nSamples)) @@ -46,13 +41,12 @@ lastTime = time() fps = None count = 0 def update(): - global curve, data, ptr, p, lastTime, fps, nPlots, count + global curve, data, ptr, plot, lastTime, fps, nPlots, count count += 1 - #print "---------", count + for i in range(nPlots): curves[i].setData(data[(ptr+i)%data.shape[0]]) - - #print " setData done." + ptr += nPlots now = time() dt = now - lastTime @@ -62,13 +56,11 @@ def update(): else: s = np.clip(dt*3., 0, 1) fps = fps * (1-s) + (1.0/dt) * s - p.setTitle('%0.2f fps' % fps) + plot.setTitle('%0.2f fps' % fps) #app.processEvents() ## force complete redraw for every plot timer = QtCore.QTimer() timer.timeout.connect(update) timer.start(0) - - ## Start Qt event loop unless running in interactive mode. if __name__ == '__main__': From 447001876e6461dc14d2158d98883164b6122f8d Mon Sep 17 00:00:00 2001 From: Billy Su Date: Mon, 5 Feb 2018 22:47:45 +0800 Subject: [PATCH 209/607] API Reference add Graphics Windows --- doc/source/apireference.rst | 1 + .../graphicsWindows/graphicsWindows.rst | 22 +++++++++++++++++++ doc/source/graphicsWindows/index.rst | 11 ++++++++++ 3 files changed, 34 insertions(+) create mode 100644 doc/source/graphicsWindows/graphicsWindows.rst create mode 100644 doc/source/graphicsWindows/index.rst diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index c4dc64aa..bca04bb8 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -9,6 +9,7 @@ Contents: config_options functions graphicsItems/index + graphicsWindows/index widgets/index 3dgraphics/index colormap diff --git a/doc/source/graphicsWindows/graphicsWindows.rst b/doc/source/graphicsWindows/graphicsWindows.rst new file mode 100644 index 00000000..4ce96b36 --- /dev/null +++ b/doc/source/graphicsWindows/graphicsWindows.rst @@ -0,0 +1,22 @@ +Graphics Windows +================ + +.. autoclass:: pyqtgraph.GraphicsWindow + :members: + + .. automethod:: pyqtgraph.GraphicsWindow.__init__ + +.. autoclass:: pyqtgraph.TabWindow + :members: + + .. automethod:: pyqtgraph.TabWindow.__init__ + +.. autoclass:: pyqtgraph.PlotWindow + :members: + + .. automethod:: pyqtgraph.PlotWindow.__init__ + +.. autoclass:: pyqtgraph.ImageWindow + :members: + + .. automethod:: pyqtgraph.ImageWindow.__init__ \ No newline at end of file diff --git a/doc/source/graphicsWindows/index.rst b/doc/source/graphicsWindows/index.rst new file mode 100644 index 00000000..5187cfdc --- /dev/null +++ b/doc/source/graphicsWindows/index.rst @@ -0,0 +1,11 @@ +Graphics Windows +================ + +Convenience classes which create a new window with PlotWidget or ImageView. + +Contents: + +.. toctree:: + :maxdepth: 2 + + graphicsWindows \ No newline at end of file From f3d5273f81ba804871b581e1584175201facae6f Mon Sep 17 00:00:00 2001 From: Fekete Imre Date: Wed, 7 Feb 2018 14:25:15 +0100 Subject: [PATCH 210/607] Cylinder now generates the correct amount of sides Before the first and the last vertex was overlapping. --- pyqtgraph/opengl/MeshData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index f83fcdf6..5bab4626 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -485,7 +485,7 @@ class MeshData(object): if isinstance(radius, int): radius = [radius, radius] # convert to list ## compute vertexes - th = np.linspace(2 * np.pi, 0, cols).reshape(1, cols) + th = np.linspace(2 * np.pi, (2 * np.pi)/cols, cols).reshape(1, cols) r = np.linspace(radius[0],radius[1],num=rows+1, endpoint=True).reshape(rows+1, 1) # radius as a function of z verts[...,2] = np.linspace(0, length, num=rows+1, endpoint=True).reshape(rows+1, 1) # z if offset: From 5e13e89480451c40fa2628ca0d5057b7cc93a573 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 13 Feb 2018 17:24:39 -0800 Subject: [PATCH 211/607] Update installation docs --- doc/source/installation.rst | 45 ++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/doc/source/installation.rst b/doc/source/installation.rst index b53e53ee..bd1594da 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -1,12 +1,45 @@ Installation ============ -PyQtGraph does not really require any installation scripts. All that is needed is for the pyqtgraph folder to be placed someplace importable. Most people will prefer to simply place this folder within a larger project folder. If you want to make pyqtgraph available system-wide, use one of the methods listed below: +There are many different ways to install pyqtgraph, depending on your needs: -* **Using pip3(or pip):** Just run "`pip3 install pyqtgraph`" on your command line. -* **Debian, Ubuntu, and similar Linux:** Download the .deb file linked at the top of the pyqtgraph web page or install using apt by putting "deb http://luke.campagnola.me/debian dev/" in your /etc/apt/sources.list file and install the python-pyqtgraph package. -* **Arch Linux:** Looks like someone has posted unofficial packages for Arch (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) -* **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. -* **From Source(including OSX):** Download the .tar.gz source package linked at the top of the pyqtgraph_ web page, extract its contents, and run "`python setup.py install`" from within the extracted directory. +* The most common way to install pyqtgraph is with pip:: + + $ pip install pyqtgraph + + Some users may need to call ``pip3`` instead. This method should work on + all platforms. +* To get access to the very latest features and bugfixes, clone pyqtgraph from + github:: + + $ git clone https://github.com/pyqtgraph/pyqtgraph + + Now you can install pyqtgraph from the source:: + + $ python setup.py install + + ..or you can simply place the pyqtgraph folder someplace importable, such as + inside the root of another project. PyQtGraph does not need to be "built" or + compiled in any way. +* Packages for pyqtgraph are also available in a few other forms: + + * **Anaconda**: ``conda install pyqtgraph`` + * **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or + download the .deb file linked at the top of the pyqtgraph web page. + * **Arch Linux:** has packages (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) + * **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. + + +Requirements +============ + +PyQtGraph depends on: + +* Python 2.7 or Python 3.x +* A Qt library such as PyQt4, PyQt5, or PySide +* numpy + +The easiest way to meet these dependencies is with ``pip`` or with a scientific python +distribution like Anaconda. .. _pyqtgraph: http://www.pyqtgraph.org/ From 6562dfc892e499c5a0141c4e153aa8133961294d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 13 Feb 2018 17:29:33 -0800 Subject: [PATCH 212/607] minor doc fix --- pyqtgraph/imageview/ImageView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index ece99953..c64953de 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -239,7 +239,7 @@ class ImageView(QtGui.QWidget): levels. Options are 'mono', which provides a single level control for all image channels, and 'rgb' or 'rgba', which provide individual controls for each channel. - ================== ======================================================================= + ================== =========================================================================== **Notes:** From ae2f0c155f4d18c67e4d7cbddf36e912220396e1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 14 Feb 2018 09:06:35 -0800 Subject: [PATCH 213/607] deprecate graphicsWindow classes --- CHANGELOG | 3 +- pyqtgraph/graphicsWindows.py | 18 ++++++++++-- pyqtgraph/widgets/GraphicsLayoutWidget.py | 36 ++++++++++++++++++++++- 3 files changed, 52 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 33106c8f..7b6c916b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,7 +5,8 @@ pyqtgraph-0.11.0 (in development) The result is visually the same, but children of ArrowItem are no longer rotated (this allows screen-aligned text to be attached more easily). To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument. - + - Deprecated graphicsWindow classes; these have been unnecessary for many years because + widgets can be placed into a new window just by calling show(). pyqtgraph-0.10.0 diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 1aa3f3f4..41c8b4d2 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- """ -graphicsWindows.py - Convenience classes which create a new window with PlotWidget or ImageView. -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +DEPRECATED: The classes below are convenience classes that create a new window +containting a single, specific widget. These classes are now unnecessary because +it is possible to place any widget into its own window by simply calling its +show() method. """ from .Qt import QtCore, QtGui @@ -20,6 +21,8 @@ def mkQApp(): class GraphicsWindow(GraphicsLayoutWidget): """ + (deprecated; use GraphicsLayoutWidget instead) + Convenience subclass of :class:`GraphicsLayoutWidget `. This class is intended for use from the interactive python prompt. @@ -34,6 +37,9 @@ class GraphicsWindow(GraphicsLayoutWidget): class TabWindow(QtGui.QMainWindow): + """ + (deprecated) + """ def __init__(self, title=None, size=(800,600)): mkQApp() QtGui.QMainWindow.__init__(self) @@ -52,6 +58,9 @@ class TabWindow(QtGui.QMainWindow): class PlotWindow(PlotWidget): + """ + (deprecated; use PlotWidget instead) + """ def __init__(self, title=None, **kargs): mkQApp() self.win = QtGui.QMainWindow() @@ -65,6 +74,9 @@ class PlotWindow(PlotWidget): class ImageWindow(ImageView): + """ + (deprecated; use ImageView instead) + """ def __init__(self, *args, **kargs): mkQApp() self.win = QtGui.QMainWindow() diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index ec7b9e0d..d42378d5 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -9,6 +9,31 @@ class GraphicsLayoutWidget(GraphicsView): ` with a single :class:`GraphicsLayout ` as its central item. + This widget is an easy starting point for generating multi-panel figures. + Example:: + + w = pg.GraphicsLayoutWidget() + p1 = w.addPlot(row=0, col=0) + p2 = w.addPlot(row=0, col=1) + v = w.addViewBox(row=1, col=0, colspan=2) + + Parameters + ---------- + parent : QWidget or None + The parent widget (see QWidget.__init__) + show : bool + If True, then immediately show the widget after it is created. + If the widget has no parent, then it will be shown inside a new window. + size : (width, height) tuple + Optionally resize the widget. Note: if this widget is placed inside a + layout, then this argument has no effect. + title : str or None + If specified, then set the window title for this widget. + kargs : + All extra arguments are passed to + :func:`GraphicsLayout.__init__() ` + + This class wraps several methods from its internal GraphicsLayout: :func:`nextRow ` :func:`nextColumn ` @@ -22,9 +47,18 @@ class GraphicsLayoutWidget(GraphicsView): :func:`itemIndex ` :func:`clear ` """ - def __init__(self, parent=None, **kargs): + def __init__(self, parent=None, show=False, size=None, title=None, **kargs): GraphicsView.__init__(self, parent) self.ci = GraphicsLayout(**kargs) for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']: setattr(self, n, getattr(self.ci, n)) self.setCentralItem(self.ci) + + if size is not None: + self.resize(*size) + + if title is not None: + self.setWindowTitle(title) + + if show is True: + self.show() From afd8a6f423cb236bdcd9565ed1d893ec427e2fa7 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Fri, 16 Feb 2018 12:15:32 +0800 Subject: [PATCH 214/607] Replace deprecate class in examples Using class GraphicsLayoutWidget to replace the deprecated class GraphicsWindow, cc #631. --- examples/CustomGraphItem.py | 2 +- examples/GraphItem.py | 2 +- examples/InfiniteLine.py | 2 +- examples/LogPlotTest.py | 2 +- examples/PanningPlot.py | 2 +- examples/PlotAutoRange.py | 2 +- examples/Plotting.py | 2 +- examples/ROIExamples.py | 2 +- examples/ROItypes.py | 2 +- examples/ScaleBar.py | 2 +- examples/Symbols.py | 2 +- examples/ViewBoxFeatures.py | 2 +- examples/contextMenu.py | 2 +- examples/crosshair.py | 2 +- examples/histogram.py | 2 +- examples/isocurve.py | 2 +- examples/linkedViews.py | 2 +- examples/logAxis.py | 2 +- examples/optics_demos.py | 2 +- examples/scrollingPlots.py | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/CustomGraphItem.py b/examples/CustomGraphItem.py index 695768e2..8e494c3a 100644 --- a/examples/CustomGraphItem.py +++ b/examples/CustomGraphItem.py @@ -12,7 +12,7 @@ import numpy as np # Enable antialiasing for prettier plots pg.setConfigOptions(antialias=True) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: CustomGraphItem') v = w.addViewBox() v.setAspectLocked() diff --git a/examples/GraphItem.py b/examples/GraphItem.py index c6362295..094b84bd 100644 --- a/examples/GraphItem.py +++ b/examples/GraphItem.py @@ -13,7 +13,7 @@ import numpy as np # Enable antialiasing for prettier plots pg.setConfigOptions(antialias=True) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: GraphItem') v = w.addViewBox() v.setAspectLocked() diff --git a/examples/InfiniteLine.py b/examples/InfiniteLine.py index 50efbd04..55020776 100644 --- a/examples/InfiniteLine.py +++ b/examples/InfiniteLine.py @@ -10,7 +10,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Plotting items examples") +win = pg.GraphicsLayoutWidget(show=True, title="Plotting items examples") win.resize(1000,600) # Enable antialiasing for prettier plots diff --git a/examples/LogPlotTest.py b/examples/LogPlotTest.py index d408a2b4..5ae9d17e 100644 --- a/examples/LogPlotTest.py +++ b/examples/LogPlotTest.py @@ -12,7 +12,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Basic plotting examples") +win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples") win.resize(1000,600) win.setWindowTitle('pyqtgraph example: LogPlotTest') diff --git a/examples/PanningPlot.py b/examples/PanningPlot.py index 165240b2..874bf330 100644 --- a/examples/PanningPlot.py +++ b/examples/PanningPlot.py @@ -9,7 +9,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: PanningPlot') plt = win.addPlot() diff --git a/examples/PlotAutoRange.py b/examples/PlotAutoRange.py index 46aa3a44..0e3cd422 100644 --- a/examples/PlotAutoRange.py +++ b/examples/PlotAutoRange.py @@ -16,7 +16,7 @@ app = QtGui.QApplication([]) #mw = QtGui.QMainWindow() #mw.resize(800,800) -win = pg.GraphicsWindow(title="Plot auto-range examples") +win = pg.GraphicsLayoutWidget(show=True, title="Plot auto-range examples") win.resize(800,600) win.setWindowTitle('pyqtgraph example: PlotAutoRange') diff --git a/examples/Plotting.py b/examples/Plotting.py index 44996ae5..130698a4 100644 --- a/examples/Plotting.py +++ b/examples/Plotting.py @@ -17,7 +17,7 @@ app = QtGui.QApplication([]) #mw = QtGui.QMainWindow() #mw.resize(800,800) -win = pg.GraphicsWindow(title="Basic plotting examples") +win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples") win.resize(1000,600) win.setWindowTitle('pyqtgraph example: Plotting') diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index a48fa7b5..2b922359 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -33,7 +33,7 @@ arr[8:13, 44:46] = 10 ## create GUI app = QtGui.QApplication([]) -w = pg.GraphicsWindow(size=(1000,800), border=True) +w = pg.GraphicsLayoutWidget(show=True, size=(1000,800), border=True) w.setWindowTitle('pyqtgraph example: ROI Examples') text = """Data Selection From Image.
\n diff --git a/examples/ROItypes.py b/examples/ROItypes.py index 9e67ebe1..1a064d33 100644 --- a/examples/ROItypes.py +++ b/examples/ROItypes.py @@ -13,7 +13,7 @@ pg.setConfigOptions(imageAxisOrder='row-major') ## create GUI app = QtGui.QApplication([]) -w = pg.GraphicsWindow(size=(800,800), border=True) +w = pg.GraphicsLayoutWidget(show=True, size=(800,800), border=True) v = w.addViewBox(colspan=2) v.invertY(True) ## Images usually have their Y-axis pointing downward v.setAspectLocked(True) diff --git a/examples/ScaleBar.py b/examples/ScaleBar.py index 5f9675e4..f125eb73 100644 --- a/examples/ScaleBar.py +++ b/examples/ScaleBar.py @@ -9,7 +9,7 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np pg.mkQApp() -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: ScaleBar') vb = win.addViewBox() diff --git a/examples/Symbols.py b/examples/Symbols.py index 3dd28e13..417df35e 100755 --- a/examples/Symbols.py +++ b/examples/Symbols.py @@ -11,7 +11,7 @@ from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg app = QtGui.QApplication([]) -win = pg.GraphicsWindow(title="Scatter Plot Symbols") +win = pg.GraphicsLayoutWidget(show=True, title="Scatter Plot Symbols") win.resize(1000,600) pg.setConfigOptions(antialias=True) diff --git a/examples/ViewBoxFeatures.py b/examples/ViewBoxFeatures.py index 6388e41b..5757924b 100644 --- a/examples/ViewBoxFeatures.py +++ b/examples/ViewBoxFeatures.py @@ -16,7 +16,7 @@ x = np.arange(1000, dtype=float) y = np.random.normal(size=1000) y += 5 * np.sin(x/100) -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: ____') win.resize(1000, 800) win.ci.setBorder((50, 50, 100)) diff --git a/examples/contextMenu.py b/examples/contextMenu.py index c2c5918d..c08008aa 100644 --- a/examples/contextMenu.py +++ b/examples/contextMenu.py @@ -14,7 +14,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: context menu') diff --git a/examples/crosshair.py b/examples/crosshair.py index 076fab49..584eced8 100644 --- a/examples/crosshair.py +++ b/examples/crosshair.py @@ -13,7 +13,7 @@ from pyqtgraph.Point import Point #generate layout app = QtGui.QApplication([]) -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: crosshair') label = pg.LabelItem(justify='right') win.addItem(label) diff --git a/examples/histogram.py b/examples/histogram.py index 2674ba30..a25f0947 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -8,7 +8,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.resize(800,350) win.setWindowTitle('pyqtgraph example: Histogram') plt1 = win.addPlot() diff --git a/examples/isocurve.py b/examples/isocurve.py index b401dfe1..6d89bbec 100644 --- a/examples/isocurve.py +++ b/examples/isocurve.py @@ -20,7 +20,7 @@ data = np.concatenate([data, data], axis=0) data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2] data[:, 15:16, 15:17] += 1 -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: Isocurve') vb = win.addViewBox() img = pg.ImageItem(data[0]) diff --git a/examples/linkedViews.py b/examples/linkedViews.py index e7eb18af..34f2b698 100644 --- a/examples/linkedViews.py +++ b/examples/linkedViews.py @@ -20,7 +20,7 @@ app = QtGui.QApplication([]) x = np.linspace(-50, 50, 1000) y = np.sin(x) / x -win = pg.GraphicsWindow(title="pyqtgraph example: Linked Views") +win = pg.GraphicsLayoutWidget(show=True, title="pyqtgraph example: Linked Views") win.resize(800,600) win.addLabel("Linked Views", colspan=2) diff --git a/examples/logAxis.py b/examples/logAxis.py index a0c7fc53..3b30c50b 100644 --- a/examples/logAxis.py +++ b/examples/logAxis.py @@ -11,7 +11,7 @@ import pyqtgraph as pg app = QtGui.QApplication([]) -w = pg.GraphicsWindow() +w = pg.GraphicsLayoutWidget(show=True) w.setWindowTitle('pyqtgraph example: logAxis') p1 = w.addPlot(0,0, title="X Semilog") p2 = w.addPlot(1,0, title="Y Semilog") diff --git a/examples/optics_demos.py b/examples/optics_demos.py index 36bfc7f9..b2ac5c8a 100644 --- a/examples/optics_demos.py +++ b/examples/optics_demos.py @@ -17,7 +17,7 @@ from pyqtgraph import Point app = pg.QtGui.QApplication([]) -w = pg.GraphicsWindow(border=0.5) +w = pg.GraphicsLayoutWidget(show=True, border=0.5) w.resize(1000, 900) w.show() diff --git a/examples/scrollingPlots.py b/examples/scrollingPlots.py index 313d4e8d..d370aa46 100644 --- a/examples/scrollingPlots.py +++ b/examples/scrollingPlots.py @@ -8,7 +8,7 @@ import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np -win = pg.GraphicsWindow() +win = pg.GraphicsLayoutWidget(show=True) win.setWindowTitle('pyqtgraph example: Scrolling Plots') From 82afad83662b9e58d94d966b83ae3d9c00e7b6cb Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 16 Feb 2018 20:42:34 -0800 Subject: [PATCH 215/607] Fix up Qt.py and deprecate USE_XX variables --- examples/ScatterPlotSpeedTest.py | 8 +- examples/VideoSpeedTest.py | 8 +- examples/__main__.py | 8 +- examples/designerExample_pyside2.py | 32 +++ examples/exampleLoaderTemplate.ui | 5 + examples/exampleLoaderTemplate_pyqt.py | 12 +- examples/exampleLoaderTemplate_pyqt5.py | 7 +- examples/exampleLoaderTemplate_pyside.py | 8 +- examples/exampleLoaderTemplate_pyside2.py | 186 +++++++-------- examples/initExample.py | 2 + pyqtgraph/GraphicsScene/exportDialog.py | 8 +- pyqtgraph/Qt.py | 221 +++++++++++------- pyqtgraph/Vector.py | 4 +- pyqtgraph/WidgetGroup.py | 4 +- pyqtgraph/console/Console.py | 8 +- pyqtgraph/exporters/ImageExporter.py | 4 +- pyqtgraph/exporters/SVGExporter.py | 4 +- pyqtgraph/flowchart/Flowchart.py | 10 +- pyqtgraph/functions.py | 6 +- pyqtgraph/graphicsItems/FillBetweenItem.py | 2 +- pyqtgraph/graphicsItems/GraphicsObject.py | 6 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 8 +- pyqtgraph/graphicsItems/UIGraphicsItem.py | 6 +- .../graphicsItems/tests/test_ImageItem.py | 2 +- pyqtgraph/imageview/ImageView.py | 8 +- pyqtgraph/multiprocess/bootstrap.py | 8 +- pyqtgraph/multiprocess/processes.py | 4 +- pyqtgraph/opengl/GLViewWidget.py | 4 +- pyqtgraph/tests/test_qt.py | 4 +- pyqtgraph/tests/test_ref_cycles.py | 6 +- pyqtgraph/widgets/GraphicsView.py | 4 +- pyqtgraph/widgets/MatplotlibWidget.py | 6 +- pyqtgraph/widgets/RemoteGraphicsView.py | 6 +- tools/rebuildUi.py | 3 +- 34 files changed, 358 insertions(+), 264 deletions(-) create mode 100644 examples/designerExample_pyside2.py diff --git a/examples/ScatterPlotSpeedTest.py b/examples/ScatterPlotSpeedTest.py index cc6818cb..2c64173a 100644 --- a/examples/ScatterPlotSpeedTest.py +++ b/examples/ScatterPlotSpeedTest.py @@ -12,7 +12,7 @@ For testing rapid updates of ScatterPlotItem under various conditions. import initExample -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 +from pyqtgraph.Qt import QtGui, QtCore, QT_LIB import numpy as np import pyqtgraph as pg from pyqtgraph.ptime import time @@ -20,11 +20,11 @@ from pyqtgraph.ptime import time app = QtGui.QApplication([]) #mw = QtGui.QMainWindow() #mw.resize(800,800) -if USE_PYSIDE: +if QT_LIB == 'PySide': from ScatterPlotSpeedTestTemplate_pyside import Ui_Form -elif USE_PYSIDE2: +elif QT_LIB == 'PySide2': from ScatterPlotSpeedTestTemplate_pyside2 import Ui_Form -elif USE_PYQT5: +elif QT_LIB == 'PyQt5': from ScatterPlotSpeedTestTemplate_pyqt5 import Ui_Form else: from ScatterPlotSpeedTestTemplate_pyqt import Ui_Form diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index 0b66df51..f123ccc3 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -10,16 +10,16 @@ is used by the view widget import initExample ## Add path to library (just for examples; you do not need this) -from pyqtgraph.Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 +from pyqtgraph.Qt import QtGui, QtCore, QT_LIB import numpy as np import pyqtgraph as pg import pyqtgraph.ptime as ptime -if USE_PYSIDE: +if QT_LIB == 'PySide': import VideoTemplate_pyside as VideoTemplate -elif USE_PYSIDE2: +elif QT_LIB == 'PySide2': import VideoTemplate_pyside2 as VideoTemplate -elif USE_PYQT5: +elif QT_LIB == 'PyQt5': import VideoTemplate_pyqt5 as VideoTemplate else: import VideoTemplate_pyqt as VideoTemplate diff --git a/examples/__main__.py b/examples/__main__.py index 8445a21b..f2ef175b 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -7,16 +7,16 @@ if __name__ == "__main__" and (__package__ is None or __package__==''): import pyqtgraph as pg import subprocess from pyqtgraph.python2_3 import basestring -from pyqtgraph.Qt import QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 +from pyqtgraph.Qt import QtGui, QT_LIB from .utils import buildFileList, testFile, path, examples -if USE_PYSIDE: +if QT_LIB == 'PySide': from .exampleLoaderTemplate_pyside import Ui_Form -elif USE_PYSIDE2: +elif QT_LIB == 'PySide2': from .exampleLoaderTemplate_pyside2 import Ui_Form -elif USE_PYQT5: +elif QT_LIB == 'PyQt5': from .exampleLoaderTemplate_pyqt5 import Ui_Form else: from .exampleLoaderTemplate_pyqt import Ui_Form diff --git a/examples/designerExample_pyside2.py b/examples/designerExample_pyside2.py new file mode 100644 index 00000000..1e04c1ac --- /dev/null +++ b/examples/designerExample_pyside2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'examples/designerExample.ui' +# +# Created: Fri Feb 16 20:31:04 2018 +# by: pyside2-uic 2.0.0 running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(400, 300) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setObjectName("gridLayout") + self.plotBtn = QtWidgets.QPushButton(Form) + self.plotBtn.setObjectName("plotBtn") + self.gridLayout.addWidget(self.plotBtn, 0, 0, 1, 1) + self.plot = PlotWidget(Form) + self.plot.setObjectName("plot") + self.gridLayout.addWidget(self.plot, 1, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.plotBtn.setText(QtWidgets.QApplication.translate("Form", "Plot!", None, -1)) + +from pyqtgraph import PlotWidget diff --git a/examples/exampleLoaderTemplate.ui b/examples/exampleLoaderTemplate.ui index a1d6bc19..f12459ba 100644 --- a/examples/exampleLoaderTemplate.ui +++ b/examples/exampleLoaderTemplate.ui @@ -79,6 +79,11 @@ PyQt5 + + + PySide2 + + diff --git a/examples/exampleLoaderTemplate_pyqt.py b/examples/exampleLoaderTemplate_pyqt.py index 6c791e7f..732a3ea1 100644 --- a/examples/exampleLoaderTemplate_pyqt.py +++ b/examples/exampleLoaderTemplate_pyqt.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' +# Form implementation generated from reading ui file 'examples/exampleLoaderTemplate.ui' # -# Created: Sat Feb 28 10:30:29 2015 -# by: PyQt4 UI code generator 4.10.4 +# Created by: PyQt4 UI code generator 4.11.4 # # WARNING! All changes made in this file will be lost! -#from PyQt4 import QtCore, QtGui -from pyqtgraph.Qt import QtGui, QtCore +from PyQt4 import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 @@ -36,7 +34,6 @@ class Ui_Form(object): self.widget = QtGui.QWidget(self.splitter) self.widget.setObjectName(_fromUtf8("widget")) self.gridLayout = QtGui.QGridLayout(self.widget) - self.gridLayout.setMargin(0) self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.exampleTree = QtGui.QTreeWidget(self.widget) self.exampleTree.setObjectName(_fromUtf8("exampleTree")) @@ -56,6 +53,7 @@ class Ui_Form(object): self.qtLibCombo.addItem(_fromUtf8("")) self.qtLibCombo.addItem(_fromUtf8("")) self.qtLibCombo.addItem(_fromUtf8("")) + self.qtLibCombo.addItem(_fromUtf8("")) self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) self.label_2 = QtGui.QLabel(self.widget) self.label_2.setObjectName(_fromUtf8("label_2")) @@ -69,7 +67,6 @@ class Ui_Form(object): self.widget1 = QtGui.QWidget(self.splitter) self.widget1.setObjectName(_fromUtf8("widget1")) self.verticalLayout = QtGui.QVBoxLayout(self.widget1) - self.verticalLayout.setMargin(0) self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.loadedFileLabel = QtGui.QLabel(self.widget1) font = QtGui.QFont() @@ -101,6 +98,7 @@ class Ui_Form(object): self.qtLibCombo.setItemText(1, _translate("Form", "PyQt4", None)) self.qtLibCombo.setItemText(2, _translate("Form", "PySide", None)) self.qtLibCombo.setItemText(3, _translate("Form", "PyQt5", None)) + self.qtLibCombo.setItemText(4, _translate("Form", "PySide2", None)) self.label_2.setText(_translate("Form", "Graphics System:", None)) self.label.setText(_translate("Form", "Qt Library:", None)) self.loadBtn.setText(_translate("Form", "Run Example", None)) diff --git a/examples/exampleLoaderTemplate_pyqt5.py b/examples/exampleLoaderTemplate_pyqt5.py index 29c00325..14ded4d9 100644 --- a/examples/exampleLoaderTemplate_pyqt5.py +++ b/examples/exampleLoaderTemplate_pyqt5.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' +# Form implementation generated from reading ui file 'examples/exampleLoaderTemplate.ui' # -# Created: Sat Feb 28 10:28:50 2015 -# by: PyQt5 UI code generator 5.2.1 +# Created by: PyQt5 UI code generator 5.6 # # WARNING! All changes made in this file will be lost! @@ -41,6 +40,7 @@ class Ui_Form(object): self.qtLibCombo.addItem("") self.qtLibCombo.addItem("") self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) self.label_2 = QtWidgets.QLabel(self.widget) self.label_2.setObjectName("label_2") @@ -87,6 +87,7 @@ class Ui_Form(object): self.qtLibCombo.setItemText(1, _translate("Form", "PyQt4")) self.qtLibCombo.setItemText(2, _translate("Form", "PySide")) self.qtLibCombo.setItemText(3, _translate("Form", "PyQt5")) + self.qtLibCombo.setItemText(4, _translate("Form", "PySide2")) self.label_2.setText(_translate("Form", "Graphics System:")) self.label.setText(_translate("Form", "Qt Library:")) self.loadBtn.setText(_translate("Form", "Run Example")) diff --git a/examples/exampleLoaderTemplate_pyside.py b/examples/exampleLoaderTemplate_pyside.py index 61f1d09f..62296827 100644 --- a/examples/exampleLoaderTemplate_pyside.py +++ b/examples/exampleLoaderTemplate_pyside.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' +# Form implementation generated from reading ui file 'examples/exampleLoaderTemplate.ui' # -# Created: Sat Feb 28 10:31:57 2015 -# by: pyside-uic 0.2.15 running on PySide 1.2.1 +# Created: Fri Feb 16 20:29:46 2018 +# by: pyside-uic 0.2.15 running on PySide 1.2.4 # # WARNING! All changes made in this file will be lost! @@ -41,6 +41,7 @@ class Ui_Form(object): self.qtLibCombo.addItem("") self.qtLibCombo.addItem("") self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) self.label_2 = QtGui.QLabel(self.widget) self.label_2.setObjectName("label_2") @@ -86,6 +87,7 @@ class Ui_Form(object): self.qtLibCombo.setItemText(1, QtGui.QApplication.translate("Form", "PyQt4", None, QtGui.QApplication.UnicodeUTF8)) self.qtLibCombo.setItemText(2, QtGui.QApplication.translate("Form", "PySide", None, QtGui.QApplication.UnicodeUTF8)) self.qtLibCombo.setItemText(3, QtGui.QApplication.translate("Form", "PyQt5", None, QtGui.QApplication.UnicodeUTF8)) + self.qtLibCombo.setItemText(4, QtGui.QApplication.translate("Form", "PySide2", None, QtGui.QApplication.UnicodeUTF8)) self.label_2.setText(QtGui.QApplication.translate("Form", "Graphics System:", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("Form", "Qt Library:", None, QtGui.QApplication.UnicodeUTF8)) self.loadBtn.setText(QtGui.QApplication.translate("Form", "Run Example", None, QtGui.QApplication.UnicodeUTF8)) diff --git a/examples/exampleLoaderTemplate_pyside2.py b/examples/exampleLoaderTemplate_pyside2.py index c3e96555..6bef728b 100644 --- a/examples/exampleLoaderTemplate_pyside2.py +++ b/examples/exampleLoaderTemplate_pyside2.py @@ -1,92 +1,94 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'exampleLoaderTemplate.ui' -# -# Created: Sun Sep 18 19:20:44 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(846, 552) - self.gridLayout_2 = QtWidgets.QGridLayout(Form) - self.gridLayout_2.setObjectName("gridLayout_2") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.widget = QtWidgets.QWidget(self.splitter) - self.widget.setObjectName("widget") - self.gridLayout = QtWidgets.QGridLayout(self.widget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.exampleTree = QtWidgets.QTreeWidget(self.widget) - self.exampleTree.setObjectName("exampleTree") - self.exampleTree.headerItem().setText(0, "1") - self.exampleTree.header().setVisible(False) - self.gridLayout.addWidget(self.exampleTree, 0, 0, 1, 2) - self.graphicsSystemCombo = QtWidgets.QComboBox(self.widget) - self.graphicsSystemCombo.setObjectName("graphicsSystemCombo") - self.graphicsSystemCombo.addItem("") - self.graphicsSystemCombo.addItem("") - self.graphicsSystemCombo.addItem("") - self.graphicsSystemCombo.addItem("") - self.gridLayout.addWidget(self.graphicsSystemCombo, 2, 1, 1, 1) - self.qtLibCombo = QtWidgets.QComboBox(self.widget) - self.qtLibCombo.setObjectName("qtLibCombo") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.qtLibCombo.addItem("") - self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) - self.label_2 = QtWidgets.QLabel(self.widget) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) - self.label = QtWidgets.QLabel(self.widget) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 1, 0, 1, 1) - self.loadBtn = QtWidgets.QPushButton(self.widget) - self.loadBtn.setObjectName("loadBtn") - self.gridLayout.addWidget(self.loadBtn, 3, 1, 1, 1) - self.widget1 = QtWidgets.QWidget(self.splitter) - self.widget1.setObjectName("widget1") - self.verticalLayout = QtWidgets.QVBoxLayout(self.widget1) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.loadedFileLabel = QtWidgets.QLabel(self.widget1) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.loadedFileLabel.setFont(font) - self.loadedFileLabel.setText("") - self.loadedFileLabel.setAlignment(QtCore.Qt.AlignCenter) - self.loadedFileLabel.setObjectName("loadedFileLabel") - self.verticalLayout.addWidget(self.loadedFileLabel) - self.codeView = QtWidgets.QPlainTextEdit(self.widget1) - font = QtGui.QFont() - font.setFamily("FreeMono") - self.codeView.setFont(font) - self.codeView.setObjectName("codeView") - self.verticalLayout.addWidget(self.codeView) - self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.graphicsSystemCombo.setItemText(0, QtWidgets.QApplication.translate("Form", "default", None, -1)) - self.graphicsSystemCombo.setItemText(1, QtWidgets.QApplication.translate("Form", "native", None, -1)) - self.graphicsSystemCombo.setItemText(2, QtWidgets.QApplication.translate("Form", "raster", None, -1)) - self.graphicsSystemCombo.setItemText(3, QtWidgets.QApplication.translate("Form", "opengl", None, -1)) - self.qtLibCombo.setItemText(0, QtWidgets.QApplication.translate("Form", "default", None, -1)) - self.qtLibCombo.setItemText(1, QtWidgets.QApplication.translate("Form", "PyQt4", None, -1)) - self.qtLibCombo.setItemText(2, QtWidgets.QApplication.translate("Form", "PySide", None, -1)) - self.qtLibCombo.setItemText(3, QtWidgets.QApplication.translate("Form", "PyQt5", None, -1)) - self.label_2.setText(QtWidgets.QApplication.translate("Form", "Graphics System:", None, -1)) - self.label.setText(QtWidgets.QApplication.translate("Form", "Qt Library:", None, -1)) - self.loadBtn.setText(QtWidgets.QApplication.translate("Form", "Run Example", None, -1)) - +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'examples/exampleLoaderTemplate.ui' +# +# Created: Fri Feb 16 20:30:37 2018 +# by: pyside2-uic 2.0.0 running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(846, 552) + self.gridLayout_2 = QtWidgets.QGridLayout(Form) + self.gridLayout_2.setObjectName("gridLayout_2") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.widget = QtWidgets.QWidget(self.splitter) + self.widget.setObjectName("widget") + self.gridLayout = QtWidgets.QGridLayout(self.widget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.exampleTree = QtWidgets.QTreeWidget(self.widget) + self.exampleTree.setObjectName("exampleTree") + self.exampleTree.headerItem().setText(0, "1") + self.exampleTree.header().setVisible(False) + self.gridLayout.addWidget(self.exampleTree, 0, 0, 1, 2) + self.graphicsSystemCombo = QtWidgets.QComboBox(self.widget) + self.graphicsSystemCombo.setObjectName("graphicsSystemCombo") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.graphicsSystemCombo.addItem("") + self.gridLayout.addWidget(self.graphicsSystemCombo, 2, 1, 1, 1) + self.qtLibCombo = QtWidgets.QComboBox(self.widget) + self.qtLibCombo.setObjectName("qtLibCombo") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.qtLibCombo.addItem("") + self.gridLayout.addWidget(self.qtLibCombo, 1, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(self.widget) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 1) + self.label = QtWidgets.QLabel(self.widget) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.loadBtn = QtWidgets.QPushButton(self.widget) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 3, 1, 1, 1) + self.widget1 = QtWidgets.QWidget(self.splitter) + self.widget1.setObjectName("widget1") + self.verticalLayout = QtWidgets.QVBoxLayout(self.widget1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.loadedFileLabel = QtWidgets.QLabel(self.widget1) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.loadedFileLabel.setFont(font) + self.loadedFileLabel.setText("") + self.loadedFileLabel.setAlignment(QtCore.Qt.AlignCenter) + self.loadedFileLabel.setObjectName("loadedFileLabel") + self.verticalLayout.addWidget(self.loadedFileLabel) + self.codeView = QtWidgets.QPlainTextEdit(self.widget1) + font = QtGui.QFont() + font.setFamily("FreeMono") + self.codeView.setFont(font) + self.codeView.setObjectName("codeView") + self.verticalLayout.addWidget(self.codeView) + self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.graphicsSystemCombo.setItemText(0, QtWidgets.QApplication.translate("Form", "default", None, -1)) + self.graphicsSystemCombo.setItemText(1, QtWidgets.QApplication.translate("Form", "native", None, -1)) + self.graphicsSystemCombo.setItemText(2, QtWidgets.QApplication.translate("Form", "raster", None, -1)) + self.graphicsSystemCombo.setItemText(3, QtWidgets.QApplication.translate("Form", "opengl", None, -1)) + self.qtLibCombo.setItemText(0, QtWidgets.QApplication.translate("Form", "default", None, -1)) + self.qtLibCombo.setItemText(1, QtWidgets.QApplication.translate("Form", "PyQt4", None, -1)) + self.qtLibCombo.setItemText(2, QtWidgets.QApplication.translate("Form", "PySide", None, -1)) + self.qtLibCombo.setItemText(3, QtWidgets.QApplication.translate("Form", "PyQt5", None, -1)) + self.qtLibCombo.setItemText(4, QtWidgets.QApplication.translate("Form", "PySide2", None, -1)) + self.label_2.setText(QtWidgets.QApplication.translate("Form", "Graphics System:", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Qt Library:", None, -1)) + self.loadBtn.setText(QtWidgets.QApplication.translate("Form", "Run Example", None, -1)) + diff --git a/examples/initExample.py b/examples/initExample.py index c10de84e..8bce7441 100644 --- a/examples/initExample.py +++ b/examples/initExample.py @@ -26,6 +26,8 @@ elif 'pyqt' in sys.argv: from PyQt4 import QtGui elif 'pyqt5' in sys.argv: from PyQt5 import QtGui +elif 'pyside2' in sys.argv: + from PySide2 import QtGui else: from pyqtgraph.Qt import QtGui diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 083cf542..8085c5bf 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -1,14 +1,14 @@ -from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 +from ..Qt import QtCore, QtGui, QT_LIB from .. import exporters as exporters from .. import functions as fn from ..graphicsItems.ViewBox import ViewBox from ..graphicsItems.PlotItem import PlotItem -if USE_PYSIDE: +if QT_LIB == 'PySide': from . import exportDialogTemplate_pyside as exportDialogTemplate -elif USE_PYSIDE2: +elif QT_LIB == 'PySide2': from . import exportDialogTemplate_pyside2 as exportDialogTemplate -elif USE_PYQT5: +elif QT_LIB == 'PyQt5': from . import exportDialogTemplate_pyqt5 as exportDialogTemplate else: from . import exportDialogTemplate_pyqt as exportDialogTemplate diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 749943f2..3db5b94b 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -14,17 +14,18 @@ import os, sys, re, time from .python2_3 import asUnicode PYSIDE = 'PySide' +PYSIDE2 = 'PySide2' PYQT4 = 'PyQt4' PYQT5 = 'PyQt5' QT_LIB = os.getenv('PYQTGRAPH_QT_LIB') -## Automatically determine whether to use PyQt or PySide (unless specified by +## Automatically determine which Qt package to use (unless specified by ## environment variable). ## This is done by first checking to see whether one of the libraries ## is already imported. If not, then attempt to import PyQt4, then PySide. if QT_LIB is None: - libOrder = [PYQT4, PYSIDE, PYQT5] + libOrder = [PYQT4, PYSIDE, PYQT5, PYSIDE2] for lib in libOrder: if lib in sys.modules: @@ -41,7 +42,7 @@ if QT_LIB is None: pass if QT_LIB is None: - raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") + raise Exception("PyQtGraph requires one of PyQt4, PyQt5, PySide or PySide2; none of these packages could be imported.") class FailedImport(object): @@ -54,6 +55,74 @@ class FailedImport(object): raise self.err +def _isQObjectAlive(obj): + """An approximation of PyQt's isQObjectAlive(). + """ + try: + if hasattr(obj, 'parent'): + obj.parent() + elif hasattr(obj, 'parentItem'): + obj.parentItem() + else: + raise Exception("Cannot determine whether Qt object %s is still alive." % obj) + except RuntimeError: + return False + else: + return True + + +# Make a loadUiType function like PyQt has + +# Credit: +# http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 + +class _StringIO(object): + """Alternative to built-in StringIO needed to circumvent unicode/ascii issues""" + def __init__(self): + self.data = [] + + def write(self, data): + self.data.append(data) + + def getvalue(self): + return ''.join(map(asUnicode, self.data)).encode('utf8') + + +def _loadUiType(uiFile): + """ + PySide lacks a "loadUiType" command like PyQt4's, so we have to convert + the ui file to py code in-memory first and then execute it in a + special frame to retrieve the form_class. + + from stackoverflow: http://stackoverflow.com/a/14195313/3781327 + + seems like this might also be a legitimate solution, but I'm not sure + how to make PyQt4 and pyside look the same... + http://stackoverflow.com/a/8717832 + """ + import pysideuic + import xml.etree.ElementTree as xml + #from io import StringIO + + parsed = xml.parse(uiFile) + widget_class = parsed.find('widget').get('class') + form_class = parsed.find('class').text + + with open(uiFile, 'r') as f: + o = _StringIO() + frame = {} + + pysideuic.compileUi(f, o, indent=0) + pyc = compile(o.getvalue(), '', 'exec') + exec(pyc, frame) + + #Fetch the base_class and form class based on their type in the xml from designer + form_class = frame['Ui_%s'%form_class] + base_class = eval('QtGui.%s'%widget_class) + + return form_class, base_class + + if QT_LIB == PYSIDE: from PySide import QtGui, QtCore @@ -68,89 +137,20 @@ if QT_LIB == PYSIDE: try: from PySide import QtTest - if not hasattr(QtTest.QTest, 'qWait'): - @staticmethod - def qWait(msec): - start = time.time() - QtGui.QApplication.processEvents() - while time.time() < start + msec * 0.001: - QtGui.QApplication.processEvents() - QtTest.QTest.qWait = qWait except ImportError as err: QtTest = FailedImport(err) - - import PySide + try: from PySide import shiboken isQObjectAlive = shiboken.isValid except ImportError: - def isQObjectAlive(obj): - try: - if hasattr(obj, 'parent'): - obj.parent() - elif hasattr(obj, 'parentItem'): - obj.parentItem() - else: - raise Exception("Cannot determine whether Qt object %s is still alive." % obj) - except RuntimeError: - return False - else: - return True + # use approximate version + isQObjectAlive = _isQObjectAlive - VERSION_INFO = 'PySide ' + PySide.__version__ + import PySide + VERSION_INFO = 'PySide ' + PySide.__version__ + ' Qt ' + QtCore.__version__ - # Make a loadUiType function like PyQt has - - # Credit: - # http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 - - class StringIO(object): - """Alternative to built-in StringIO needed to circumvent unicode/ascii issues""" - def __init__(self): - self.data = [] - - def write(self, data): - self.data.append(data) - - def getvalue(self): - return ''.join(map(asUnicode, self.data)).encode('utf8') - - def loadUiType(uiFile): - """ - Pyside "loadUiType" command like PyQt4 has one, so we have to convert - the ui file to py code in-memory first and then execute it in a - special frame to retrieve the form_class. - - from stackoverflow: http://stackoverflow.com/a/14195313/3781327 - - seems like this might also be a legitimate solution, but I'm not sure - how to make PyQt4 and pyside look the same... - http://stackoverflow.com/a/8717832 - """ - import pysideuic - import xml.etree.ElementTree as xml - #from io import StringIO - - parsed = xml.parse(uiFile) - widget_class = parsed.find('widget').get('class') - form_class = parsed.find('class').text - - with open(uiFile, 'r') as f: - o = StringIO() - frame = {} - - pysideuic.compileUi(f, o, indent=0) - pyc = compile(o.getvalue(), '', 'exec') - exec(pyc, frame) - - #Fetch the base_class and form class based on their type in the xml from designer - form_class = frame['Ui_%s'%form_class] - base_class = eval('QtGui.%s'%widget_class) - - return form_class, base_class - elif QT_LIB == PYQT4: - from PyQt4 import QtGui, QtCore, uic try: from PyQt4 import QtSvg @@ -168,7 +168,6 @@ elif QT_LIB == PYQT4: VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR elif QT_LIB == PYQT5: - # We're using PyQt5 which has a different structure so we're going to use a shim to # recreate the Qt4 structure for Qt5 from PyQt5 import QtGui, QtCore, QtWidgets, uic @@ -198,8 +197,39 @@ elif QT_LIB == PYQT5: except ImportError as err: QtTest = FailedImport(err) - # Re-implement deprecated APIs + VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR +elif QT_LIB == PYSIDE2: + from PySide2 import QtGui, QtCore, QtWidgets + + try: + from PySide2 import QtSvg + except ImportError as err: + QtSvg = FailedImport(err) + try: + from PySide2 import QtOpenGL + except ImportError as err: + QtOpenGL = FailedImport(err) + try: + from PySide2 import QtTest + QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed + except ImportError as err: + QtTest = FailedImport(err) + + isQObjectAlive = _isQObjectAlive + + import PySide2 + VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__ + +else: + raise ValueError("Invalid Qt lib '%s'" % QT_LIB) + + +# common to PyQt5 and PySide2 +if QT_LIB in [PYQT5, PYSIDE2]: + # We're using Qt5 which has a different structure so we're going to use a shim to + # recreate the Qt4 structure + __QGraphicsItem_scale = QtWidgets.QGraphicsItem.scale def scale(self, *args): @@ -246,28 +276,45 @@ elif QT_LIB == PYQT5: if o.startswith('Q'): setattr(QtGui, o, getattr(QtWidgets,o) ) - VERSION_INFO = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR -else: - raise ValueError("Invalid Qt lib '%s'" % QT_LIB) +# Common to PySide and PySide2 +if QT_LIB in [PYSIDE, PYSIDE2]: + QtVersion = QtCore.__version__ + loadUiType = _loadUiType + + # PySide does not implement qWait + if not isinstance(QtTest, FailedImport): + if not hasattr(QtTest.QTest, 'qWait'): + @staticmethod + def qWait(msec): + start = time.time() + QtGui.QApplication.processEvents() + while time.time() < start + msec * 0.001: + QtGui.QApplication.processEvents() + QtTest.QTest.qWait = qWait + # Common to PyQt4 and 5 -if QT_LIB.startswith('PyQt'): +if QT_LIB in [PYQT4, PYQT5]: + QtVersion = QtCore.QT_VERSION_STR + import sip def isQObjectAlive(obj): return not sip.isdeleted(obj) + loadUiType = uic.loadUiType QtCore.Signal = QtCore.pyqtSignal - -## Make sure we have Qt >= 4.7 -versionReq = [4, 7] +# USE_XXX variables are deprecated USE_PYSIDE = QT_LIB == PYSIDE USE_PYQT4 = QT_LIB == PYQT4 USE_PYQT5 = QT_LIB == PYQT5 -QtVersion = PySide.QtCore.__version__ if QT_LIB == PYSIDE else QtCore.QT_VERSION_STR + + +## Make sure we have Qt >= 4.7 +versionReq = [4, 7] m = re.match(r'(\d+)\.(\d+).*', QtVersion) if m is not None and list(map(int, m.groups())) < versionReq: print(list(map(int, m.groups()))) diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index f2898e80..0c980a61 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from .Qt import QtGui, QtCore, USE_PYSIDE +from .Qt import QtGui, QtCore, QT_LIB import numpy as np class Vector(QtGui.QVector3D): @@ -36,7 +36,7 @@ class Vector(QtGui.QVector3D): def __add__(self, b): # workaround for pyside bug. see https://bugs.launchpad.net/pyqtgraph/+bug/1223173 - if USE_PYSIDE and isinstance(b, QtGui.QVector3D): + if QT_LIB == 'PySide' and isinstance(b, QtGui.QVector3D): b = Vector(b) return QtGui.QVector3D.__add__(self, b) diff --git a/pyqtgraph/WidgetGroup.py b/pyqtgraph/WidgetGroup.py index d7e265c5..09c30854 100644 --- a/pyqtgraph/WidgetGroup.py +++ b/pyqtgraph/WidgetGroup.py @@ -8,7 +8,7 @@ This class addresses the problem of having to save and restore the state of a large group of widgets. """ -from .Qt import QtCore, QtGui, USE_PYQT5 +from .Qt import QtCore, QtGui, QT_LIB import weakref, inspect from .python2_3 import asUnicode @@ -218,7 +218,7 @@ class WidgetGroup(QtCore.QObject): v1 = self.cache[n] v2 = self.readWidget(w) if v1 != v2: - if not USE_PYQT5: + if QT_LIB != 'PyQt5': # Old signal kept for backward compatibility. self.emit(QtCore.SIGNAL('changed'), self.widgetList[w], v2) self.sigChanged.emit(self.widgetList[w], v2) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index cb6ee5e7..d64b2193 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -1,15 +1,15 @@ import sys, re, os, time, traceback, subprocess import pickle -from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 +from ..Qt import QtCore, QtGui, QT_LIB from ..python2_3 import basestring from .. import exceptionHandling as exceptionHandling from .. import getConfigOption -if USE_PYSIDE: +if QT_LIB == 'PySide': from . import template_pyside as template -elif USE_PYSIDE2: +elif QT_LIB == 'PySide2': from . import template_pyside2 as template -elif USE_PYQT5: +elif QT_LIB == 'PyQt5': from . import template_pyqt5 as template else: from . import template_pyqt as template diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index ffa59091..a43a3d88 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -1,6 +1,6 @@ from .Exporter import Exporter from ..parametertree import Parameter -from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +from ..Qt import QtGui, QtCore, QtSvg, QT_LIB from .. import functions as fn import numpy as np @@ -47,7 +47,7 @@ class ImageExporter(Exporter): def export(self, fileName=None, toBytes=False, copy=False): if fileName is None and not toBytes and not copy: - if USE_PYSIDE: + if QT_LIB in ['PySide', 'PySide2']: filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()] else: filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index fdc65080..f6c008bf 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -1,7 +1,7 @@ from .Exporter import Exporter from ..python2_3 import asUnicode from ..parametertree import Parameter -from ..Qt import QtGui, QtCore, QtSvg, USE_PYSIDE +from ..Qt import QtGui, QtCore, QtSvg, QT_LIB from .. import debug from .. import functions as fn import re @@ -186,7 +186,7 @@ def _generateItemSvg(item, nodes=None, root=None): #if hasattr(item, 'setExportMode'): #item.setExportMode(False) - if USE_PYSIDE: + if QT_LIB in ['PySide', 'PySide2']: xmlStr = str(arr) else: xmlStr = bytes(arr).decode('utf-8') diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 67dbe2c6..4edea91b 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -1,18 +1,18 @@ # -*- coding: utf-8 -*- -from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 +from ..Qt import QtCore, QtGui, QT_LIB from .Node import * from ..pgcollections import OrderedDict from ..widgets.TreeWidget import * from .. import FileDialog, DataTreeWidget ## pyside and pyqt use incompatible ui files. -if USE_PYSIDE: +if QT_LIB == 'PySide': from . import FlowchartTemplate_pyside as FlowchartTemplate from . import FlowchartCtrlTemplate_pyside as FlowchartCtrlTemplate -elif USE_PYSIDE2: +elif QT_LIB == 'PySide2': from . import FlowchartTemplate_pyside2 as FlowchartTemplate from . import FlowchartCtrlTemplate_pyside2 as FlowchartCtrlTemplate -elif USE_PYQT5: +elif QT_LIB == 'PyQt5': from . import FlowchartTemplate_pyqt5 as FlowchartTemplate from . import FlowchartCtrlTemplate_pyqt5 as FlowchartCtrlTemplate else: @@ -622,7 +622,7 @@ class FlowchartCtrlWidget(QtGui.QWidget): self.cwWin.resize(1000,800) h = self.ui.ctrlList.header() - if not USE_PYQT5: + if QT_LIB in ['PyQt4', 'PySide']: h.setResizeMode(0, h.Stretch) else: h.setSectionResizeMode(0, h.Stretch) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 41acfa36..7473c128 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -12,7 +12,7 @@ import decimal, re import ctypes import sys, struct from .python2_3 import asUnicode, basestring -from .Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2 +from .Qt import QtGui, QtCore, QT_LIB from . import getConfigOption, setConfigOptions from . import debug from .metaarray import MetaArray @@ -1219,7 +1219,7 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): if copy is True and copied is False: imgData = imgData.copy() - if USE_PYSIDE or USE_PYSIDE2: + if QT_LIB in ['PySide', 'PySide2']: ch = ctypes.c_char.from_buffer(imgData, 0) # Bug in PySide + Python 3 causes refcount for image data to be improperly @@ -1277,7 +1277,7 @@ def imageToArray(img, copy=False, transpose=True): """ fmt = img.format() ptr = img.bits() - if USE_PYSIDE or USE_PYSIDE2: + if QT_LIB in ['PySide', 'PySide2']: arr = np.frombuffer(ptr, dtype=np.ubyte) else: ptr.setsize(img.byteCount()) diff --git a/pyqtgraph/graphicsItems/FillBetweenItem.py b/pyqtgraph/graphicsItems/FillBetweenItem.py index 0a73cb73..b16be853 100644 --- a/pyqtgraph/graphicsItems/FillBetweenItem.py +++ b/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui, USE_PYQT5, USE_PYQT4, USE_PYSIDE, USE_PYSIDE2 +from ..Qt import QtGui from .. import functions as fn from .PlotDataItem import PlotDataItem from .PlotCurveItem import PlotCurveItem diff --git a/pyqtgraph/graphicsItems/GraphicsObject.py b/pyqtgraph/graphicsItems/GraphicsObject.py index a6b16372..2493fe76 100644 --- a/pyqtgraph/graphicsItems/GraphicsObject.py +++ b/pyqtgraph/graphicsItems/GraphicsObject.py @@ -1,5 +1,5 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2 -if not USE_PYSIDE and not USE_PYSIDE2: +from ..Qt import QtGui, QtCore, QT_LIB +if QT_LIB in ['PyQt4', 'PyQt5']: import sip from .GraphicsItem import GraphicsItem @@ -33,7 +33,7 @@ class GraphicsObject(GraphicsItem, QtGui.QGraphicsObject): ## workaround for pyqt bug: ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html - if not USE_PYSIDE and not USE_PYSIDE2 and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): + if QT_LIB in ['PyQt4', 'PyQt5'] and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): ret = sip.cast(ret, QtGui.QGraphicsItem) return ret diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 54b43945..d67c1f36 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -5,7 +5,7 @@ except ImportError: imap = map import numpy as np import weakref -from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 +from ..Qt import QtGui, QtCore, QT_LIB from ..Point import Point from .. import functions as fn from .GraphicsItem import GraphicsItem @@ -773,10 +773,10 @@ class ScatterPlotItem(GraphicsObject): self.data['targetRect'][updateMask] = list(imap(QtCore.QRectF, updatePts[0,:], updatePts[1,:], width, width)) data = self.data[viewMask] - if USE_PYSIDE or USE_PYSIDE2 or USE_PYQT5: - list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) - else: + if QT_LIB == 'PyQt4': p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas) + else: + list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) else: # render each symbol individually p.setRenderHint(p.Antialiasing, aa) diff --git a/pyqtgraph/graphicsItems/UIGraphicsItem.py b/pyqtgraph/graphicsItems/UIGraphicsItem.py index f1672eeb..2074a2e9 100644 --- a/pyqtgraph/graphicsItems/UIGraphicsItem.py +++ b/pyqtgraph/graphicsItems/UIGraphicsItem.py @@ -1,7 +1,7 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2 +from ..Qt import QtGui, QtCore, QT_LIB import weakref from .GraphicsObject import GraphicsObject -if not USE_PYSIDE and not USE_PYSIDE2: +if QT_LIB in ['PyQt4', 'PyQt5']: import sip __all__ = ['UIGraphicsItem'] @@ -49,7 +49,7 @@ class UIGraphicsItem(GraphicsObject): ## workaround for pyqt bug: ## http://www.riverbankcomputing.com/pipermail/pyqt/2012-August/031818.html - if not USE_PYSIDE and not USE_PYSIDE2 and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): + if QT_LIB in ['PyQt4', 'PyQt5'] and change == self.ItemParentChange and isinstance(ret, QtGui.QGraphicsItem): ret = sip.cast(ret, QtGui.QGraphicsItem) if change == self.ItemScenePositionHasChanged: diff --git a/pyqtgraph/graphicsItems/tests/test_ImageItem.py b/pyqtgraph/graphicsItems/tests/test_ImageItem.py index 4f310bc3..ca197c6e 100644 --- a/pyqtgraph/graphicsItems/tests/test_ImageItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ImageItem.py @@ -134,7 +134,7 @@ def test_ImageItem_axisorder(): pg.setConfigOptions(imageAxisOrder=origMode) -@pytest.mark.skipif(pg.Qt.USE_PYSIDE, reason="pyside does not have qWait") +@pytest.mark.skipif(pg.Qt.QT_LIB=='PySide', reason="pyside does not have qWait") def test_dividebyzero(): import pyqtgraph as pg im = pg.image(pg.np.random.normal(size=(100,100))) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index aa2bffaf..540d647f 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -15,12 +15,12 @@ Widget used for displaying 2D or 3D data. Features: import os, sys import numpy as np -from ..Qt import QtCore, QtGui, USE_PYSIDE, USE_PYSIDE2, USE_PYQT5 -if USE_PYSIDE: +from ..Qt import QtCore, QtGui, QT_LIB +if QT_LIB == 'PySide': from .ImageViewTemplate_pyside import * -elif USE_PYSIDE2: +elif QT_LIB == 'PySide2': from .ImageViewTemplate_pyside2 import * -elif USE_PYQT5: +elif QT_LIB == 'PyQt5': from .ImageViewTemplate_pyqt5 import * else: from .ImageViewTemplate_pyqt import * diff --git a/pyqtgraph/multiprocess/bootstrap.py b/pyqtgraph/multiprocess/bootstrap.py index a8a03d41..b9868367 100644 --- a/pyqtgraph/multiprocess/bootstrap.py +++ b/pyqtgraph/multiprocess/bootstrap.py @@ -29,9 +29,13 @@ if __name__ == '__main__': for k,v in pyqtapis.items(): sip.setapi(k, v) - if opts.pop('pyside', False): + qt_lib = opts.pop('qt_lib', None) + if qt_lib == 'PySide': import PySide - + elif qt_lib == 'PySide2': + import PySide2 + elif qt_lib == 'PyQt5': + import PyQt5 targetStr = opts.pop('targetStr') try: diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 1be7e50b..2b3065b5 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -6,7 +6,7 @@ except ImportError: import pickle from .remoteproxy import RemoteEventHandler, ClosedError, NoResultError, LocalObjectProxy, ObjectProxy -from ..Qt import USE_PYSIDE +from ..Qt import QT_LIB from ..util import cprint # color printing for debugging @@ -131,7 +131,7 @@ class Process(RemoteEventHandler): ppid=pid, targetStr=targetStr, path=sysPath, - pyside=USE_PYSIDE, + qt_lib=QT_LIB, debug=procDebug, pyqtapis=pyqtapis, ) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 71dae9c5..19db69d8 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -1,4 +1,4 @@ -from ..Qt import QtCore, QtGui, QtOpenGL, USE_PYQT5 +from ..Qt import QtCore, QtGui, QtOpenGL, QT_LIB from OpenGL.GL import * import OpenGL.GL.framebufferobjects as glfbo import numpy as np @@ -325,7 +325,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): def wheelEvent(self, ev): delta = 0 - if not USE_PYQT5: + if QT_LIB in ['PyQt4', 'PySide']: delta = ev.delta() else: delta = ev.angleDelta().x() diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index 5c8800dd..bfb98631 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -13,8 +13,8 @@ def test_isQObjectAlive(): gc.collect() assert not pg.Qt.isQObjectAlive(o2) -@pytest.mark.skipif(pg.Qt.USE_PYSIDE, reason='pysideuic does not appear to be ' - 'packaged with conda') +@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be ' + 'packaged with conda') def test_loadUiType(): path = os.path.dirname(__file__) formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui')) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index dec95ef7..e05c4ef1 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -40,7 +40,7 @@ def mkrefs(*objs): return map(weakref.ref, allObjs.values()) -@pytest.mark.skipif(six.PY3 or pg.Qt.USE_PYSIDE, reason=skipreason) +@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_PlotWidget(): def mkobjs(*args, **kwds): w = pg.PlotWidget(*args, **kwds) @@ -58,7 +58,7 @@ def test_PlotWidget(): for i in range(5): assert_alldead(mkobjs()) -@pytest.mark.skipif(six.PY3 or pg.Qt.USE_PYSIDE, reason=skipreason) +@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_ImageView(): def mkobjs(): iv = pg.ImageView() @@ -71,7 +71,7 @@ def test_ImageView(): assert_alldead(mkobjs()) -@pytest.mark.skipif(six.PY3 or pg.Qt.USE_PYSIDE, reason=skipreason) +@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_GraphicsWindow(): def mkobjs(): w = pg.GraphicsWindow() diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index f3f8cbb5..e1a7327e 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola Distributed under MIT/X11 license. See license.txt for more infomation. """ -from ..Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, QT_LIB try: from ..Qt import QtOpenGL @@ -115,7 +115,7 @@ class GraphicsView(QtGui.QGraphicsView): ## Workaround for PySide crash ## This ensures that the scene will outlive the view. - if USE_PYSIDE: + if QT_LIB == 'PySide': self.sceneObj._view_ref_workaround = self ## by default we set up a central widget with a grid layout. diff --git a/pyqtgraph/widgets/MatplotlibWidget.py b/pyqtgraph/widgets/MatplotlibWidget.py index 30496839..c5b6c980 100644 --- a/pyqtgraph/widgets/MatplotlibWidget.py +++ b/pyqtgraph/widgets/MatplotlibWidget.py @@ -1,8 +1,8 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYQT5 +from ..Qt import QtGui, QtCore, QT_LIB import matplotlib -if not USE_PYQT5: - if USE_PYSIDE: +if QT_LIB != 'PyQt5': + if QT_LIB == 'PySide': matplotlib.rcParams['backend.qt4']='PySide' from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index f6a7888b..a1674cc2 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -1,5 +1,5 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYSIDE2 -if not USE_PYSIDE and not USE_PYSIDE2: +from ..Qt import QtGui, QtCore, QT_LIB +if QT_LIB in ['PyQt4', 'PyQt5']: import sip from .. import multiprocess as mp from .GraphicsView import GraphicsView @@ -208,7 +208,7 @@ class Renderer(GraphicsView): self.shm.resize(size) ## render the scene directly to shared memory - if USE_PYSIDE: + if QT_LIB in ['PySide', 'PySide2']: ch = ctypes.c_char.from_buffer(self.shm, 0) #ch = ctypes.c_char_p(address) self.img = QtGui.QImage(ch, self.width(), self.height(), QtGui.QImage.Format_ARGB32) diff --git a/tools/rebuildUi.py b/tools/rebuildUi.py index bdacda81..18de45d6 100644 --- a/tools/rebuildUi.py +++ b/tools/rebuildUi.py @@ -10,6 +10,7 @@ import os, sys, subprocess, tempfile pyqtuic = 'pyuic4' pysideuic = 'pyside-uic' pyqt5uic = 'pyuic5' +pyside2uic = 'pyside2-uic' usage = """Compile .ui files to .py for all supported pyqt/pyside versions. @@ -49,7 +50,7 @@ for arg in args: # rebuild all requested ui files for ui in uifiles: base, _ = os.path.splitext(ui) - for compiler, ext in [(pyqtuic, '_pyqt.py'), (pysideuic, '_pyside.py'), (pyqt5uic, '_pyqt5.py')]: + for compiler, ext in [(pyqtuic, '_pyqt.py'), (pysideuic, '_pyside.py'), (pyqt5uic, '_pyqt5.py'), (pyside2uic, '_pyside2.py')]: py = base + ext if not force and os.path.exists(py) and os.stat(ui).st_mtime <= os.stat(py).st_mtime: print("Skipping %s; already compiled." % py) From 927fe44ab9d7c7fdb1f0021f669e44183d48e85e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:34:15 -0800 Subject: [PATCH 216/607] move mkQApp to Qt.py to make it easier to import internally GraphicsLayoutWidget now calls mkQApp --- pyqtgraph/Qt.py | 7 +++++++ pyqtgraph/__init__.py | 13 +------------ pyqtgraph/graphicsWindows.py | 8 +------- pyqtgraph/widgets/GraphicsLayoutWidget.py | 3 ++- pyqtgraph/widgets/__init__.py | 21 --------------------- 5 files changed, 11 insertions(+), 41 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 749943f2..ad2a8007 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -272,3 +272,10 @@ m = re.match(r'(\d+)\.(\d+).*', QtVersion) if m is not None and list(map(int, m.groups())) < versionReq: print(list(map(int, m.groups()))) raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) + + +def mkQApp(): + app = QtGui.QApplication.instance() + if app is None: + app = QtGui.QApplication([]) + return app diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 520ea196..583aeaa0 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -10,7 +10,7 @@ __version__ = '0.10.0' ## 'Qt' is a local module; it is intended mainly to cover up the differences ## between PyQt4 and PySide. -from .Qt import QtGui +from .Qt import QtGui, mkQApp ## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause) #if QtGui.QApplication.instance() is None: @@ -466,14 +466,3 @@ def stack(*args, **kwds): except NameError: consoles = [c] return c - - -def mkQApp(): - global QAPP - inst = QtGui.QApplication.instance() - if inst is None: - QAPP = QtGui.QApplication([]) - else: - QAPP = inst - return QAPP - diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index 41c8b4d2..b6598685 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -6,17 +6,11 @@ it is possible to place any widget into its own window by simply calling its show() method. """ -from .Qt import QtCore, QtGui +from .Qt import QtCore, QtGui, mkQApp from .widgets.PlotWidget import * from .imageview import * from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget from .widgets.GraphicsView import GraphicsView -QAPP = None - -def mkQApp(): - if QtGui.QApplication.instance() is None: - global QAPP - QAPP = QtGui.QApplication([]) class GraphicsWindow(GraphicsLayoutWidget): diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index d42378d5..3b41a3ca 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui +from ..Qt import QtGui, mkQApp from ..graphicsItems.GraphicsLayout import GraphicsLayout from .GraphicsView import GraphicsView @@ -48,6 +48,7 @@ class GraphicsLayoutWidget(GraphicsView): :func:`clear ` """ def __init__(self, parent=None, show=False, size=None, title=None, **kargs): + mkQApp() GraphicsView.__init__(self, parent) self.ci = GraphicsLayout(**kargs) for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']: diff --git a/pyqtgraph/widgets/__init__.py b/pyqtgraph/widgets/__init__.py index a81fe391..e69de29b 100644 --- a/pyqtgraph/widgets/__init__.py +++ b/pyqtgraph/widgets/__init__.py @@ -1,21 +0,0 @@ -## just import everything from sub-modules - -#import os - -#d = os.path.split(__file__)[0] -#files = [] -#for f in os.listdir(d): - #if os.path.isdir(os.path.join(d, f)): - #files.append(f) - #elif f[-3:] == '.py' and f != '__init__.py': - #files.append(f[:-3]) - -#for modName in files: - #mod = __import__(modName, globals(), locals(), fromlist=['*']) - #if hasattr(mod, '__all__'): - #names = mod.__all__ - #else: - #names = [n for n in dir(mod) if n[0] != '_'] - #for k in names: - #print modName, k - #globals()[k] = getattr(mod, k) From 3240f7a435e6e744491a8f7a9544bc9d1f4e44f4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:40:36 -0800 Subject: [PATCH 217/607] fix qapp storage bug --- pyqtgraph/Qt.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ad2a8007..8c0041df 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -274,8 +274,9 @@ if m is not None and list(map(int, m.groups())) < versionReq: raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) +QAPP = None def mkQApp(): - app = QtGui.QApplication.instance() - if app is None: - app = QtGui.QApplication([]) - return app + global QAPP + if QtGui.QApplication.instance() is None: + QAPP = QtGui.QApplication([]) + return QAPP From e318bc041b34ac939809dcae8f11799781641e67 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 23 Feb 2018 17:50:44 -0800 Subject: [PATCH 218/607] Fix isosurface error: TypeError('only integer scalar arrays can be converted to a scalar index',) --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index a6c06ad4..e83237c6 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2172,7 +2172,7 @@ def isosurface(data, level): ## compute lookup table of index: vertexes mapping faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte) faceTableInds = np.argwhere(nTableFaces == i) - faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) + faceTableI[faceTableInds[:,0]] = np.array([triTable[j[0]] for j in faceTableInds]) faceTableI = faceTableI.reshape((len(triTable), i, 3)) faceShiftTables.append(edgeShifts[faceTableI]) From b25196067add5989b530cfd0d9a978deb97465a2 Mon Sep 17 00:00:00 2001 From: Tanuj Date: Sun, 25 Feb 2018 10:54:12 +0000 Subject: [PATCH 219/607] Fix grame typo --- pyqtgraph/opengl/items/GLGridItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 4d6bc9d6..63a558ea 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -10,7 +10,7 @@ class GLGridItem(GLGraphicsItem): """ **Bases:** :class:`GLGraphicsItem ` - Displays a wire-grame grid. + Displays a wire-frame grid. """ def __init__(self, size=None, color=None, antialias=True, glOptions='translucent'): From d09fe6bd478770862dffc3157f25e2dfcfa2a4ed Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Thu, 1 Mar 2018 15:34:15 +0100 Subject: [PATCH 220/607] SVGExporter: logicalDpiX instead of physicalDpiX Fixes problems with non-aligning axes on Qt5 svg exports. In the output svg, the axes were (individually) scaled for physical/logical ration. --- pyqtgraph/exporters/SVGExporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index fdc65080..700b63da 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -169,7 +169,7 @@ def _generateItemSvg(item, nodes=None, root=None): buf = QtCore.QBuffer(arr) svg = QtSvg.QSvgGenerator() svg.setOutputDevice(buf) - dpi = QtGui.QDesktopWidget().physicalDpiX() + dpi = QtGui.QDesktopWidget().logicalDpiX() svg.setResolution(dpi) p = QtGui.QPainter() From c7a32d83111dfa6b57fc37d9387e02e747e00bd5 Mon Sep 17 00:00:00 2001 From: Marko Toplak Date: Wed, 28 Feb 2018 11:04:50 +0100 Subject: [PATCH 221/607] SVG export: handle Qt.NoPen on Qt5 --- pyqtgraph/exporters/SVGExporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index fdc65080..97975fcf 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -372,7 +372,7 @@ def correctCoordinates(node, defs, item): ch.setAttribute('font-family', ', '.join([f if ' ' not in f else '"%s"'%f for f in families])) ## correct line widths if needed - if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke': + if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke' and grp.getAttribute('stroke-width'): w = float(grp.getAttribute('stroke-width')) s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True) w = ((s[0]-s[1])**2).sum()**0.5 From a5276c3bd3ccf478662f7dd42ed0d37ac26c847d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 Mar 2018 15:09:36 -0800 Subject: [PATCH 222/607] fix: scatterplotwidget behaves nicely when data contains infs Add methods to make it easier to programatically configure scatterplotwidget --- pyqtgraph/widgets/ColorMapWidget.py | 5 +++++ pyqtgraph/widgets/DataFilterWidget.py | 13 +++++++++---- pyqtgraph/widgets/ScatterPlotWidget.py | 19 +++++++++++++++---- 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index bd5668ae..7e6bfab7 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -42,6 +42,11 @@ class ColorMapWidget(ptree.ParameterTree): def restoreState(self, state): self.params.restoreState(state) + def addColorMap(self, name): + """Add a new color mapping and return the created parameter. + """ + return self.params.addNew(name) + class ColorMapParameter(ptree.types.GroupParameter): sigColorMapChanged = QtCore.Signal(object) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index cae8be86..23cf930f 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -30,7 +30,12 @@ class DataFilterWidget(ptree.ParameterTree): def parameters(self): return self.params - + + def addFilter(self, name): + """Add a new filter and return the created parameter item. + """ + return self.params.addNew(name) + class DataFilterParameter(ptree.types.GroupParameter): @@ -47,10 +52,10 @@ class DataFilterParameter(ptree.types.GroupParameter): def addNew(self, name): mode = self.fields[name].get('mode', 'range') if mode == 'range': - self.addChild(RangeFilterItem(name, self.fields[name])) + child = self.addChild(RangeFilterItem(name, self.fields[name])) elif mode == 'enum': - self.addChild(EnumFilterItem(name, self.fields[name])) - + child = self.addChild(EnumFilterItem(name, self.fields[name])) + return child def fieldNames(self): return self.fields.keys() diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index e0071f24..bd0eb908 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -83,7 +83,19 @@ class ScatterPlotWidget(QtGui.QSplitter): item = self.fieldList.addItem(item) self.filter.setFields(fields) self.colorMap.setFields(fields) - + + def setSelectedFields(self, *fields): + self.fieldList.itemSelectionChanged.disconnect(self.fieldSelectionChanged) + try: + self.fieldList.clearSelection() + for f in fields: + i = self.fields.keys().index(f) + item = self.fieldList.item(i) + item.setSelected(True) + finally: + self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged) + self.fieldSelectionChanged() + def setData(self, data): """ Set the data to be processed and displayed. @@ -114,7 +126,6 @@ class ScatterPlotWidget(QtGui.QSplitter): else: self.filterText.setText('\n'.join(desc)) self.filterText.setVisible(True) - def updatePlot(self): self.plot.clear() @@ -177,9 +188,9 @@ class ScatterPlotWidget(QtGui.QSplitter): ## mask out any nan values mask = np.ones(len(xy[0]), dtype=bool) if xy[0].dtype.kind == 'f': - mask &= ~np.isnan(xy[0]) + mask &= np.isfinite(xy[0]) if xy[1] is not None and xy[1].dtype.kind == 'f': - mask &= ~np.isnan(xy[1]) + mask &= np.isfinite(xy[1]) xy[0] = xy[0][mask] style['symbolBrush'] = colors[mask] From 96a3d216e2e2128e89263a2f84a9ea6f81eeaaec Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 15 Mar 2018 11:59:45 -0700 Subject: [PATCH 223/607] Make it easier to track data in and out of scatterplotwidget --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 56 +++-------------- pyqtgraph/widgets/ScatterPlotWidget.py | 70 +++++++++++++++++++--- 2 files changed, 71 insertions(+), 55 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 443cc220..564c5ef0 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -701,16 +701,12 @@ class ScatterPlotItem(GraphicsObject): GraphicsObject.setExportMode(self, *args, **kwds) self.invalidate() - def mapPointsToDevice(self, pts): # Map point locations to device tr = self.deviceTransform() if tr is None: return None - #pts = np.empty((2,len(self.data['x']))) - #pts[0] = self.data['x'] - #pts[1] = self.data['y'] pts = fn.transformCoordinates(tr, pts) pts -= self.data['width'] pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. @@ -731,7 +727,6 @@ class ScatterPlotItem(GraphicsObject): (pts[1] - w < viewBounds.bottom())) ## remove out of view points return mask - @debug.warnOnException ## raising an exception here causes crash def paint(self, p, *args): cmode = self.opts.get('compositionMode', None) @@ -758,8 +753,6 @@ class ScatterPlotItem(GraphicsObject): # Cull points that are outside view viewMask = self.getViewMask(pts) - #pts = pts[:,mask] - #data = self.data[mask] if self.opts['useCache'] and self._exportOpts is False: # Draw symbols from pre-rendered atlas @@ -804,9 +797,9 @@ class ScatterPlotItem(GraphicsObject): self.picture.play(p) def points(self): - for rec in self.data: + for i,rec in enumerate(self.data): if rec['item'] is None: - rec['item'] = SpotItem(rec, self) + rec['item'] = SpotItem(rec, self, i) return self.data['item'] def pointsAt(self, pos): @@ -854,18 +847,19 @@ class SpotItem(object): by connecting to the ScatterPlotItem's click signals. """ - def __init__(self, data, plot): - #GraphicsItem.__init__(self, register=False) + def __init__(self, data, plot, index): self._data = data self._plot = plot - #self.setParentItem(plot) - #self.setPos(QtCore.QPointF(data['x'], data['y'])) - #self.updateItem() + self._index = index def data(self): """Return the user data associated with this spot.""" return self._data['data'] + def index(self): + """Return the index of this point as given in the scatter plot data.""" + return self._index + def size(self): """Return the size of this spot. If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" @@ -949,37 +943,3 @@ class SpotItem(object): self._data['sourceRect'] = None self._plot.updateSpots(self._data.reshape(1)) self._plot.invalidate() - -#class PixmapSpotItem(SpotItem, QtGui.QGraphicsPixmapItem): - #def __init__(self, data, plot): - #QtGui.QGraphicsPixmapItem.__init__(self) - #self.setFlags(self.flags() | self.ItemIgnoresTransformations) - #SpotItem.__init__(self, data, plot) - - #def setPixmap(self, pixmap): - #QtGui.QGraphicsPixmapItem.setPixmap(self, pixmap) - #self.setOffset(-pixmap.width()/2.+0.5, -pixmap.height()/2.) - - #def updateItem(self): - #symbolOpts = (self._data['pen'], self._data['brush'], self._data['size'], self._data['symbol']) - - ### If all symbol options are default, use default pixmap - #if symbolOpts == (None, None, -1, ''): - #pixmap = self._plot.defaultSpotPixmap() - #else: - #pixmap = makeSymbolPixmap(size=self.size(), pen=self.pen(), brush=self.brush(), symbol=self.symbol()) - #self.setPixmap(pixmap) - - -#class PathSpotItem(SpotItem, QtGui.QGraphicsPathItem): - #def __init__(self, data, plot): - #QtGui.QGraphicsPathItem.__init__(self) - #SpotItem.__init__(self, data, plot) - - #def updateItem(self): - #QtGui.QGraphicsPathItem.setPath(self, Symbols[self.symbol()]) - #QtGui.QGraphicsPathItem.setPen(self, self.pen()) - #QtGui.QGraphicsPathItem.setBrush(self, self.brush()) - #size = self.size() - #self.resetTransform() - #self.scale(size, size) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index bd0eb908..96ad39cb 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -59,9 +59,16 @@ class ScatterPlotWidget(QtGui.QSplitter): self.filterText.setParentItem(self.plot.plotItem) self.data = None + self.indices = None self.mouseOverField = None self.scatterPlot = None + self.selectionScatter = None + self.selectedIndices = [] self.style = dict(pen=None, symbol='o') + self._visibleXY = None # currently plotted points + self._visibleData = None # currently plotted records + self._visibleIndices = None + self._indexMap = None self.fieldList.itemSelectionChanged.connect(self.fieldSelectionChanged) self.filter.sigFilterChanged.connect(self.filterChanged) @@ -102,9 +109,26 @@ class ScatterPlotWidget(QtGui.QSplitter): Argument must be a numpy record array. """ self.data = data + self.indices = np.arange(len(data)) self.filtered = None + self.filteredIndices = None self.updatePlot() + def setSelectedIndices(self, inds): + """Mark the specified indices as selected. + + Must be a sequence of integers that index into the array given in setData(). + """ + self.selectedIndices = inds + self.updateSelected() + + def setSelectedPoints(self, points): + """Mark the specified points as selected. + + Must be a list of points as generated by the sigScatterPlotClicked signal. + """ + self.setSelectedIndices([pt.originalIndex for pt in points]) + def fieldSelectionChanged(self): sel = self.fieldList.selectedItems() if len(sel) > 2: @@ -129,11 +153,13 @@ class ScatterPlotWidget(QtGui.QSplitter): def updatePlot(self): self.plot.clear() - if self.data is None: + if self.data is None or len(self.data) == 0: return if self.filtered is None: - self.filtered = self.filter.filterData(self.data) + mask = self.filter.generateMask(self.data) + self.filtered = self.data[mask] + self.filteredIndices = self.indices[mask] data = self.filtered if len(data) == 0: return @@ -194,6 +220,8 @@ class ScatterPlotWidget(QtGui.QSplitter): xy[0] = xy[0][mask] style['symbolBrush'] = colors[mask] + data = data[mask] + indices = self.filteredIndices[mask] ## Scatter y-values for a histogram-like appearance if xy[1] is None: @@ -215,16 +243,44 @@ class ScatterPlotWidget(QtGui.QSplitter): if smax != 0: scatter *= 0.2 / smax xy[ax][keymask] += scatter - + + if self.scatterPlot is not None: try: self.scatterPlot.sigPointsClicked.disconnect(self.plotClicked) except: pass - self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data[mask], **style) - self.scatterPlot.sigPointsClicked.connect(self.plotClicked) + self._visibleXY = xy + self._visibleData = data + self._visibleIndices = indices + self._indexMap = None + self.scatterPlot = self.plot.plot(xy[0], xy[1], data=data, **style) + self.scatterPlot.sigPointsClicked.connect(self.plotClicked) + self.updateSelected() + + def updateSelected(self): + if self._visibleXY is None: + return + # map from global index to visible index + indMap = self._getIndexMap() + inds = [indMap[i] for i in self.selectedIndices if i in indMap] + x,y = self._visibleXY[0][inds], self._visibleXY[1][inds] + + if self.selectionScatter is not None: + self.plot.plotItem.removeItem(self.selectionScatter) + if len(x) == 0: + return + self.selectionScatter = self.plot.plot(x, y, pen=None, symbol='s', symbolSize=12, symbolBrush=None, symbolPen='y') + + def _getIndexMap(self): + # mapping from original data index to visible point index + if self._indexMap is None: + self._indexMap = {j:i for i,j in enumerate(self._visibleIndices)} + return self._indexMap + def plotClicked(self, plot, points): + # Tag each point with its index into the original dataset + for pt in points: + pt.originalIndex = self._visibleIndices[pt.index()] self.sigScatterPlotClicked.emit(self, points) - - From 27f37d0fd1ae56039936a9af96d8b0688078ee97 Mon Sep 17 00:00:00 2001 From: Vanessa Date: Mon, 19 Mar 2018 12:09:49 +0100 Subject: [PATCH 224/607] fix color ignored in GLGridItem. Issue #283 solved --- pyqtgraph/opengl/items/GLGridItem.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 4d6bc9d6..dece73a5 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -13,7 +13,7 @@ class GLGridItem(GLGraphicsItem): Displays a wire-grame grid. """ - def __init__(self, size=None, color=None, antialias=True, glOptions='translucent'): + def __init__(self, size=None, color=(1, 1, 1, .3), antialias=True, glOptions='translucent'): GLGraphicsItem.__init__(self) self.setGLOptions(glOptions) self.antialias = antialias @@ -21,6 +21,7 @@ class GLGridItem(GLGraphicsItem): size = QtGui.QVector3D(20,20,1) self.setSize(size=size) self.setSpacing(1, 1, 1) + self.color = color def setSize(self, x=None, y=None, z=None, size=None): """ @@ -66,8 +67,8 @@ class GLGridItem(GLGraphicsItem): x,y,z = self.size() xs,ys,zs = self.spacing() xvals = np.arange(-x/2., x/2. + xs*0.001, xs) - yvals = np.arange(-y/2., y/2. + ys*0.001, ys) - glColor4f(1, 1, 1, .3) + yvals = np.arange(-y/2., y/2. + ys*0.001, ys) + glColor4f(*self.color) for x in xvals: glVertex3f(x, yvals[0], 0) glVertex3f(x, yvals[-1], 0) From b3a579fd004f9a841038b6adf6d4ec14dbe8748a Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 29 Mar 2018 12:03:57 +0200 Subject: [PATCH 225/607] ScatterPlotItem: Fix a GC memory leak due to numpy issue 6581 --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 443cc220..30e6cf89 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -857,11 +857,18 @@ class SpotItem(object): def __init__(self, data, plot): #GraphicsItem.__init__(self, register=False) self._data = data - self._plot = plot + # SpotItems are kept in plot.data["items"] numpy object array which + # does not support cyclic garbage collection (numpy issue 6581). + # Keeping a strong ref to plot here would leak the cycle + self.__plot_ref = weakref.ref(plot) #self.setParentItem(plot) #self.setPos(QtCore.QPointF(data['x'], data['y'])) #self.updateItem() + @property + def _plot(self): + return self.__plot_ref() + def data(self): """Return the user data associated with this spot.""" return self._data['data'] From 358b1539eee6d45f75169bf4471604d540c8e8d5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 29 Mar 2018 08:49:42 -0700 Subject: [PATCH 226/607] add reduce import to fractal demo --- examples/fractal.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/examples/fractal.py b/examples/fractal.py index eeb1bdb0..d91133a5 100644 --- a/examples/fractal.py +++ b/examples/fractal.py @@ -4,6 +4,7 @@ Displays an interactive Koch fractal """ import initExample ## Add path to library (just for examples; you do not need this) +from functools import reduce import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui import numpy as np @@ -111,12 +112,4 @@ if __name__ == '__main__': import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): QtGui.QApplication.instance().exec_() - - - - - - - - \ No newline at end of file From f79bd8d4fb6d4b13c4dfdd377d07644968964b37 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 29 Mar 2018 09:06:20 -0700 Subject: [PATCH 227/607] svg export fix: more explicit check --- pyqtgraph/exporters/SVGExporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 97975fcf..0ab9a736 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -372,7 +372,7 @@ def correctCoordinates(node, defs, item): ch.setAttribute('font-family', ', '.join([f if ' ' not in f else '"%s"'%f for f in families])) ## correct line widths if needed - if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke' and grp.getAttribute('stroke-width'): + if removeTransform and ch.getAttribute('vector-effect') != 'non-scaling-stroke' and grp.getAttribute('stroke-width') != '': w = float(grp.getAttribute('stroke-width')) s = fn.transformCoordinates(tr, np.array([[w,0], [0,0]]), transpose=True) w = ((s[0]-s[1])**2).sum()**0.5 From 043a3b4d6d7faf84faa563853e55f319884ebd70 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 29 Mar 2018 18:30:46 -0700 Subject: [PATCH 228/607] Better title --- examples/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/__main__.py b/examples/__main__.py index 6b8a07f9..084fe2c3 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -26,7 +26,7 @@ class ExampleLoader(QtGui.QMainWindow): self.cw = QtGui.QWidget() self.setCentralWidget(self.cw) self.ui.setupUi(self.cw) - self.setWindowTitle("Examples") + self.setWindowTitle("PyQtGraph Examples") self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() From dc95f763328e158b8066ff5440c8325fbb17a957 Mon Sep 17 00:00:00 2001 From: Edward Barnard Date: Fri, 30 Mar 2018 10:29:45 -0700 Subject: [PATCH 229/607] When a floating Dock window is closed, the dock is now returned to its original DockArea --- pyqtgraph/dockarea/DockArea.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index a55d6bb0..b7b0659e 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -46,6 +46,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop): """ if dock is None: dock = Dock(**kwds) + + # store original area that the dock will return to when un-floated + if not self.temporary: + dock.orig_area = self ## Determine the container to insert this dock into. @@ -371,5 +375,11 @@ class TempAreaWindow(QtGui.QWidget): self.layout.addWidget(area) def closeEvent(self, *args): + # restore docks to their original area + docks = self.dockarea.findAll()[1] + for dock in docks.values(): + if hasattr(dock, 'orig_area'): + dock.orig_area.addDock(dock, ) + # clear dock area, and close remaining docks self.dockarea.clear() QtGui.QWidget.closeEvent(self, *args) From 20bcc39eb1039987fa411821a440ee1323dd3a63 Mon Sep 17 00:00:00 2001 From: "Ilya A. Kriveshko" Date: Fri, 30 Mar 2018 14:48:05 -0400 Subject: [PATCH 230/607] examples: use integer division in indexing In python3 / operator produces a float, which is not a valid index. Replace with // integer division. --- examples/GLImageItem.py | 6 +++--- examples/isocurve.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/GLImageItem.py b/examples/GLImageItem.py index 581474fd..70bf5306 100644 --- a/examples/GLImageItem.py +++ b/examples/GLImageItem.py @@ -26,9 +26,9 @@ data += pg.gaussianFilter(np.random.normal(size=shape), (15,15,15))*15 ## slice out three planes, convert to RGBA for OpenGL texture levels = (-0.08, 0.08) -tex1 = pg.makeRGBA(data[shape[0]/2], levels=levels)[0] # yz plane -tex2 = pg.makeRGBA(data[:,shape[1]/2], levels=levels)[0] # xz plane -tex3 = pg.makeRGBA(data[:,:,shape[2]/2], levels=levels)[0] # xy plane +tex1 = pg.makeRGBA(data[shape[0]//2], levels=levels)[0] # yz plane +tex2 = pg.makeRGBA(data[:,shape[1]//2], levels=levels)[0] # xz plane +tex3 = pg.makeRGBA(data[:,:,shape[2]//2], levels=levels)[0] # xy plane #tex1[:,:,3] = 128 #tex2[:,:,3] = 128 #tex3[:,:,3] = 128 diff --git a/examples/isocurve.py b/examples/isocurve.py index 6d89bbec..63b1699e 100644 --- a/examples/isocurve.py +++ b/examples/isocurve.py @@ -17,7 +17,7 @@ app = QtGui.QApplication([]) frames = 200 data = np.random.normal(size=(frames,30,30), loc=0, scale=100) data = np.concatenate([data, data], axis=0) -data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2] +data = pg.gaussianFilter(data, (10, 10, 10))[frames//2:frames + frames//2] data[:, 15:16, 15:17] += 1 win = pg.GraphicsLayoutWidget(show=True) From 42d96d0c21525c35dc5181c7cf1cc9106759363f Mon Sep 17 00:00:00 2001 From: Billy Su Date: Mon, 2 Apr 2018 19:26:01 +0800 Subject: [PATCH 231/607] Remove outdated comments --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 8 -------- pyqtgraph/graphicsItems/tests/test_GraphicsItem.py | 11 ----------- 2 files changed, 19 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 7321702c..65e10439 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -356,14 +356,6 @@ class PlotItem(GraphicsWidget): v = np.clip(alpha, 0, 1)*self.ctrl.gridAlphaSlider.maximum() self.ctrl.gridAlphaSlider.setValue(v) - #def paint(self, *args): - #prof = debug.Profiler() - #QtGui.QGraphicsWidget.paint(self, *args) - - ## bad idea. - #def __getattr__(self, attr): ## wrap ms - #return getattr(self.vb, attr) - def close(self): #print "delete", self ## Most of this crap is needed to avoid PySide trouble. diff --git a/pyqtgraph/graphicsItems/tests/test_GraphicsItem.py b/pyqtgraph/graphicsItems/tests/test_GraphicsItem.py index 112dd4d5..47dfd907 100644 --- a/pyqtgraph/graphicsItems/tests/test_GraphicsItem.py +++ b/pyqtgraph/graphicsItems/tests/test_GraphicsItem.py @@ -34,14 +34,3 @@ def test_getViewWidget_deleted(): assert not pg.Qt.isQObjectAlive(view) assert item.getViewWidget() is None - - -#if __name__ == '__main__': - #view = pg.PlotItem() - #vref = weakref.ref(view) - #item = pg.InfiniteLine() - #view.addItem(item) - #del view - #gc.collect() - - \ No newline at end of file From 90857e7f27fb0a0523754e41646b4b3a0a73e1e4 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Thu, 5 Apr 2018 00:25:36 +0800 Subject: [PATCH 232/607] Extract _choose_filename_dialog() as helper func To reduce the redundancy, extract the same code as a helper function _choose_filename_dialog() in PlotItem class --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 45 ++++++++------------ 1 file changed, 17 insertions(+), 28 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 65e10439..fdf60399 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -694,16 +694,9 @@ class PlotItem(GraphicsWidget): ## Qt's SVG-writing capabilities are pretty terrible. def writeSvgCurves(self, fileName=None): if fileName is None: - self.fileDialog = FileDialog() - if PlotItem.lastFileDir is not None: - self.fileDialog.setDirectory(PlotItem.lastFileDir) - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.writeSvg) + self._choose_filename_dialog(handler=self.writeSvg) return - #if fileName is None: - #fileName = QtGui.QFileDialog.getSaveFileName() + if isinstance(fileName, tuple): raise Exception("Not implemented yet..") fileName = str(fileName) @@ -780,7 +773,9 @@ class PlotItem(GraphicsWidget): def writeSvg(self, fileName=None): if fileName is None: - fileName = QtGui.QFileDialog.getSaveFileName() + self._choose_filename_dialog(handler=self.writeSvg) + return + fileName = str(fileName) PlotItem.lastFileDir = os.path.dirname(fileName) @@ -790,16 +785,9 @@ class PlotItem(GraphicsWidget): def writeImage(self, fileName=None): if fileName is None: - self.fileDialog = FileDialog() - if PlotItem.lastFileDir is not None: - self.fileDialog.setDirectory(PlotItem.lastFileDir) - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.writeImage) + self._choose_filename_dialog(handler=self.writeImage) return - #if fileName is None: - #fileName = QtGui.QFileDialog.getSaveFileName() + if isinstance(fileName, tuple): raise Exception("Not implemented yet..") fileName = str(fileName) @@ -813,16 +801,9 @@ class PlotItem(GraphicsWidget): def writeCsv(self, fileName=None): if fileName is None: - self.fileDialog = FileDialog() - if PlotItem.lastFileDir is not None: - self.fileDialog.setDirectory(PlotItem.lastFileDir) - self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) - self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) - self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.writeCsv) + self._choose_filename_dialog(handler=self.writeCsv) return - #if fileName is None: - #fileName = QtGui.QFileDialog.getSaveFileName() + fileName = str(fileName) PlotItem.lastFileDir = os.path.dirname(fileName) @@ -1227,3 +1208,11 @@ class PlotItem(GraphicsWidget): #else: #self.autoBtn.show() + def _choose_filename_dialog(self, handler): + self.fileDialog = FileDialog() + if PlotItem.lastFileDir is not None: + self.fileDialog.setDirectory(PlotItem.lastFileDir) + self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile) + self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) + self.fileDialog.show() + self.fileDialog.fileSelected.connect(handler) From 7b9ba8634c0a02cb4c82313d9bef3197640c3187 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Thu, 5 Apr 2018 11:13:09 +0800 Subject: [PATCH 233/607] Add test_setData() for PlotDataItem class --- .../graphicsItems/tests/test_PlotDataItem.py | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index dc13bb7a..9a13c0b6 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -22,4 +22,30 @@ def test_fft(): pd.setLogMode(True, False) x, y = pd.getData() assert abs(x[np.argmax(y)] - np.log10(f)) < 0.01 - \ No newline at end of file + +def test_setData(): + pdi = pg.PlotDataItem() + + #test empty data + pdi.setData([]) + + #test y data + y = list(np.random.normal(size=100)) + pdi.setData(y) + assert len(pdi.xData) == 100 + assert len(pdi.yData) == 100 + + #test x, y data + y += list(np.random.normal(size=50)) + x = np.linspace(5, 10, 150) + + pdi.setData(x, y) + assert len(pdi.xData) == 150 + assert len(pdi.yData) == 150 + + #test dict of x, y list + y += list(np.random.normal(size=50)) + x = list(np.linspace(5, 10, 200)) + pdi.setData({'x': x, 'y': y}) + assert len(pdi.xData) == 200 + assert len(pdi.yData) == 200 From e4254172a54db8adbddacf32298bdc1dd41453d5 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Thu, 5 Apr 2018 11:13:58 +0800 Subject: [PATCH 234/607] Add test_clear for PlotDataItem class --- pyqtgraph/graphicsItems/tests/test_PlotDataItem.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index 9a13c0b6..8851a0a2 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -49,3 +49,12 @@ def test_setData(): pdi.setData({'x': x, 'y': y}) assert len(pdi.xData) == 200 assert len(pdi.yData) == 200 + +def test_clear(): + y = list(np.random.normal(size=100)) + x = np.linspace(5, 10, 100) + pdi = pg.PlotDataItem(x, y) + pdi.clear() + + assert pdi.xData == None + assert pdi.yData == None From ce704a1baaf58d4adb0abfd9d5bf7bad83a0023a Mon Sep 17 00:00:00 2001 From: Fekete Imre Date: Fri, 13 Apr 2018 16:23:41 +0200 Subject: [PATCH 235/607] Fix issue #145 Disable textures after painting is finished. --- pyqtgraph/opengl/items/GLScatterPlotItem.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index dc4b298a..fe794d48 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -152,7 +152,9 @@ class GLScatterPlotItem(GLGraphicsItem): glDisableClientState(GL_VERTEX_ARRAY) glDisableClientState(GL_COLOR_ARRAY) #posVBO.unbind() - + ##fixes #145 + glDisable( GL_TEXTURE_2D ) + #for i in range(len(self.pos)): #pos = self.pos[i] From 063e9c91a999853a47675a2befcdfef4c0e30d53 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 24 Apr 2018 08:59:33 -0700 Subject: [PATCH 236/607] Make high-dpi handling conditional on Qt version --- pyqtgraph/opengl/GLViewWidget.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index c6d0c1fa..540fce7d 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -16,9 +16,13 @@ class GLViewWidget(QtOpenGL.QGLWidget): - Axis/grid display - Export options + + High-DPI displays: Qt5 should automatically detect the correct resolution. + For Qt4, specify the ``devicePixelRatio`` argument when initializing the + widget (usually this value is 1-2). """ - def __init__(self, parent=None): + def __init__(self, parent=None, devicePixelRatio=None): global ShareWidget if ShareWidget is None: @@ -37,6 +41,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): 'azimuth': 45, ## camera's azimuthal angle in degrees ## (rotation around z-axis 0 points along x-axis) 'viewport': None, ## glViewport params; None == whole widget + 'devicePixelRatio': devicePixelRatio, } self.setBackgroundColor('k') self.items = [] @@ -79,10 +84,21 @@ class GLViewWidget(QtOpenGL.QGLWidget): def getViewport(self): vp = self.opts['viewport'] + dpr = self.devicePixelRatio() if vp is None: - return (0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) + return (0, 0, int(self.width() * dpr), int(self.height() * dpr)) else: - return vp + return tuple([int(x * dpr) for x in vp]) + + def devicePixelRatio(self): + dpr = self.opts['devicePixelRatio'] + if dpr is not None: + return dpr + + if hasattr(QtOpenGL.QGLWidget, 'devicePixelRatio'): + return QtOpenGL.QGLWidget.devicePixelRatio(self) + else: + return 1.0 def resizeGL(self, w, h): pass @@ -99,7 +115,8 @@ class GLViewWidget(QtOpenGL.QGLWidget): def projectionMatrix(self, region=None): # Xw = (Xnd + 1) * width/2 + X if region is None: - region = (0, 0, self.width() * self.devicePixelRatio(), self.height() * self.devicePixelRatio()) + dpr = self.devicePixelRatio() + region = (0, 0, self.width() * dpr, self.height() * dpr) x0, y0, w, h = self.getViewport() dist = self.opts['distance'] From 80ff4ebfe6196c59c3e5e834cb769f39a8ae93b8 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 08:50:37 -0700 Subject: [PATCH 237/607] Remove prints from console --- pyqtgraph/console/Console.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 634aab4a..02f00c44 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -359,7 +359,6 @@ class ConsoleWidget(QtGui.QWidget): for index, line in enumerate(traceback.extract_stack(frame)): # extract_stack return value changed in python 3.5 if 'FrameSummary' in str(type(line)): - print(dir(line)) line = (line.filename, line.lineno, line.name, line._line) self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) @@ -380,7 +379,6 @@ class ConsoleWidget(QtGui.QWidget): for index, line in enumerate(traceback.extract_tb(tb)): # extract_stack return value changed in python 3.5 if 'FrameSummary' in str(type(line)): - print(dir(line)) line = (line.filename, line.lineno, line.name, line._line) self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) From cbff3a58d78257a70b20507c0c89538171c1c4b2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 11:10:39 -0700 Subject: [PATCH 238/607] Fix scatterplotwidget label color --- pyqtgraph/widgets/ScatterPlotWidget.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/ScatterPlotWidget.py b/pyqtgraph/widgets/ScatterPlotWidget.py index 96ad39cb..bf8a0f42 100644 --- a/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/pyqtgraph/widgets/ScatterPlotWidget.py @@ -52,9 +52,9 @@ class ScatterPlotWidget(QtGui.QSplitter): self.ctrlPanel.addWidget(self.ptree) self.addWidget(self.plot) - bg = fn.mkColor(getConfigOption('background')) - bg.setAlpha(150) - self.filterText = TextItem(border=getConfigOption('foreground'), color=bg) + fg = fn.mkColor(getConfigOption('foreground')) + fg.setAlpha(150) + self.filterText = TextItem(border=getConfigOption('foreground'), color=fg) self.filterText.setPos(60,20) self.filterText.setParentItem(self.plot.plotItem) From 56aae02821c13e080e138247e65615df8bf83586 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 11:54:06 -0700 Subject: [PATCH 239/607] fix svg export failure when first line point is masked --- pyqtgraph/exporters/SVGExporter.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index 3e060e62..ae1ec91c 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -344,6 +344,10 @@ def correctCoordinates(node, defs, item): t = '' nc = fn.transformCoordinates(tr, np.array([[float(x),float(y)]]), transpose=True) newCoords += t+str(nc[0,0])+','+str(nc[0,1])+' ' + # If coords start with L instead of M, then the entire path will not be rendered. + # (This can happen if the first point had nan values in it--Qt will skip it on export) + if newCoords[0] != 'M': + newCoords = 'M' + newCoords[1:] ch.setAttribute('d', newCoords) elif ch.tagName == 'text': removeTransform = False From ee2d00c42ae38460d1071bffcc4ff360570edc69 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 12:15:14 -0700 Subject: [PATCH 240/607] Add option to remove non-scaling-stroke from svg output --- pyqtgraph/exporters/SVGExporter.py | 42 ++++++++++++------------------ 1 file changed, 17 insertions(+), 25 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index ae1ec91c..ce2a90c0 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -23,7 +23,8 @@ class SVGExporter(Exporter): #{'name': 'height', 'type': 'float', 'value': tr.height(), 'limits': (0, None)}, #{'name': 'viewbox clipping', 'type': 'bool', 'value': True}, #{'name': 'normalize coordinates', 'type': 'bool', 'value': True}, - #{'name': 'normalize line width', 'type': 'bool', 'value': True}, + {'name': 'scaling stroke', 'type': 'bool', 'value': False, 'tip': "If False, strokes are non-scaling, " + "which means that they appear the same width on screen regardless of how they are scaled or how the view is zoomed."}, ]) #self.params.param('width').sigValueChanged.connect(self.widthChanged) #self.params.param('height').sigValueChanged.connect(self.heightChanged) @@ -49,7 +50,8 @@ class SVGExporter(Exporter): ## Qt's SVG generator is not complete. (notably, it lacks clipping) ## Instead, we will use Qt to generate SVG for each item independently, ## then manually reconstruct the entire document. - xml = generateSvg(self.item) + options = {ch.name():ch.value() for ch in self.params.children()} + xml = generateSvg(self.item, options) if toBytes: return xml.encode('UTF-8') @@ -69,10 +71,10 @@ xmlHeader = """\ Generated with Qt and pyqtgraph """ -def generateSvg(item): +def generateSvg(item, options={}): global xmlHeader try: - node, defs = _generateItemSvg(item) + node, defs = _generateItemSvg(item, options=options) finally: ## reset export mode for all items in the tree if isinstance(item, QtGui.QGraphicsScene): @@ -94,7 +96,7 @@ def generateSvg(item): return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n\n" -def _generateItemSvg(item, nodes=None, root=None): +def _generateItemSvg(item, nodes=None, root=None, options={}): ## This function is intended to work around some issues with Qt's SVG generator ## and SVG in general. ## 1) Qt SVG does not implement clipping paths. This is absurd. @@ -209,18 +211,8 @@ def _generateItemSvg(item, nodes=None, root=None): ## Get rid of group transformation matrices by applying ## transformation to inner coordinates - correctCoordinates(g1, defs, item) + correctCoordinates(g1, defs, item, options) profiler('correct') - ## make sure g1 has the transformation matrix - #m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) - #g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m) - - #print "=================",item,"=====================" - #print g1.toprettyxml(indent=" ", newl='') - - ## Inkscape does not support non-scaling-stroke (this is SVG 1.2, inkscape supports 1.1) - ## So we need to correct anything attempting to use this. - #correctStroke(g1, item, root) ## decide on a name for this item baseName = item.__class__.__name__ @@ -239,15 +231,10 @@ def _generateItemSvg(item, nodes=None, root=None): ## See if this item clips its children if int(item.flags() & item.ItemClipsChildrenToShape) > 0: ## Generate svg for just the path - #if isinstance(root, QtGui.QGraphicsScene): - #path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) - #else: - #path = QtGui.QGraphicsPathItem(root.mapToParent(item.mapToItem(root, item.shape()))) path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape())) item.scene().addItem(path) try: - #pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0] - pathNode = _generateItemSvg(path, root=root)[0].getElementsByTagName('path')[0] + pathNode = _generateItemSvg(path, root=root, options=options)[0].getElementsByTagName('path')[0] # assume for this path is empty.. possibly problematic. finally: item.scene().removeItem(path) @@ -267,17 +254,18 @@ def _generateItemSvg(item, nodes=None, root=None): ## Add all child items as sub-elements. childs.sort(key=lambda c: c.zValue()) for ch in childs: - csvg = _generateItemSvg(ch, nodes, root) + csvg = _generateItemSvg(ch, nodes, root, options=options) if csvg is None: continue cg, cdefs = csvg childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now) defs.extend(cdefs) - + profiler('children') return g1, defs -def correctCoordinates(node, defs, item): + +def correctCoordinates(node, defs, item, options): # TODO: correct gradient coordinates inside defs ## Remove transformation matrices from tags by applying matrix to coordinates inside. @@ -382,6 +370,10 @@ def correctCoordinates(node, defs, item): w = ((s[0]-s[1])**2).sum()**0.5 ch.setAttribute('stroke-width', str(w)) + # Remove non-scaling-stroke if requested + if options.get('scaling stroke') is True and ch.getAttribute('vector-effect') == 'non-scaling-stroke': + ch.removeAttribute('vector-effect') + if removeTransform: grp.removeAttribute('transform') From d4a4fd7fd5565276dabd7990721210bf9df6dd42 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 13:04:26 -0700 Subject: [PATCH 241/607] fix busy cursor nesting Prevents permenent busy cursor --- pyqtgraph/widgets/BusyCursor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/BusyCursor.py b/pyqtgraph/widgets/BusyCursor.py index d99fe589..5fab3358 100644 --- a/pyqtgraph/widgets/BusyCursor.py +++ b/pyqtgraph/widgets/BusyCursor.py @@ -18,7 +18,7 @@ class BusyCursor(object): BusyCursor.active.append(self) def __exit__(self, *args): - BusyCursor.active.pop(-1) - if len(BusyCursor.active) == 0: + if self._active: + BusyCursor.active.pop(-1) QtGui.QApplication.restoreOverrideCursor() \ No newline at end of file From 7daa5bdff13149f246fb6d13b330ec7fe3d44785 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 13:04:52 -0700 Subject: [PATCH 242/607] Ignore BusyCursor when called from thread --- pyqtgraph/widgets/BusyCursor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/BusyCursor.py b/pyqtgraph/widgets/BusyCursor.py index 5fab3358..e7a26810 100644 --- a/pyqtgraph/widgets/BusyCursor.py +++ b/pyqtgraph/widgets/BusyCursor.py @@ -9,13 +9,19 @@ class BusyCursor(object): with pyqtgraph.BusyCursor(): doLongOperation() - May be nested. + May be nested. If called from a non-gui thread, then the cursor will not be affected. """ active = [] def __enter__(self): - QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) - BusyCursor.active.append(self) + app = QtCore.QCoreApplication.instance() + isGuiThread = (app is not None) and (QtCore.QThread.currentThread() == app.thread()) + if isGuiThread and QtGui.QApplication.instance() is not None: + QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + BusyCursor.active.append(self) + self._active = True + else: + self._active = False def __exit__(self, *args): if self._active: From 58a865e088ba5d047ffab23a6cff645048060ecc Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 13:19:50 -0700 Subject: [PATCH 243/607] Add signal documentation for graphicsscene --- pyqtgraph/GraphicsScene/GraphicsScene.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 952a2415..0fca2684 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -36,6 +36,18 @@ class GraphicsScene(QtGui.QGraphicsScene): This lets us indicate unambiguously to the user which item they are about to click/drag on * Eats mouseMove events that occur too soon after a mouse press. * Reimplements items() and itemAt() to circumvent PyQt bug + + ====================== ================================================================== + **Signals** + sigMouseClicked(event) Emitted when the mouse is clicked over the scene. Use ev.pos() to + get the click position relative to the item that was clicked on, + or ev.scenePos() to get the click position in scene coordinates. + See :class:`pyqtgraph.GraphicsScene.MouseClickEvent`. + sigMouseMoved(pos) Emitted when the mouse cursor moves over the scene. The position + is given in scene coordinates. + sigMouseHover(items) Emitted when the mouse is moved over the scene. Items is a list + of items under the cursor. + ====================== ================================================================== Mouse interaction is as follows: From 465ea2d940e6ef5b9b55b8556506a345265616a4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 13:22:23 -0700 Subject: [PATCH 244/607] Prevent parallelizer leaking file handles --- pyqtgraph/multiprocess/parallelizer.py | 2 ++ pyqtgraph/multiprocess/processes.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 86298023..ef00be7c 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -195,6 +195,8 @@ class Parallelize(object): finally: if self.showProgress: self.progressDlg.__exit__(None, None, None) + for ch in self.childs: + ch.join() if len(self.exitCodes) < len(self.childs): raise Exception("Parallelizer started %d processes but only received exit codes from %d." % (len(self.childs), len(self.exitCodes))) for code in self.exitCodes: diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 1be7e50b..0a372ef0 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -165,6 +165,7 @@ class Process(RemoteEventHandler): if timeout is not None and time.time() - start > timeout: raise Exception('Timed out waiting for remote process to end.') time.sleep(0.05) + self.conn.close() self.debugMsg('Child process exited. (%d)' % self.proc.returncode) def debugMsg(self, msg, *args): @@ -341,6 +342,7 @@ class ForkedProcess(RemoteEventHandler): except OSError: ## probably remote process has already quit pass + self.conn.close() # don't leak file handles! self.hasJoined = True def kill(self): From 51267f569649a6ca668ab1c275ba467b4fa431f0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 14:54:35 -0700 Subject: [PATCH 245/607] int parameters use integer formatting --- pyqtgraph/parametertree/parameterTypes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 42a18fe0..8d65767d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -105,6 +105,7 @@ class WidgetParameterItem(ParameterItem): if t == 'int': defs['int'] = True defs['minStep'] = 1.0 + defs['format'] = '{value:d}' for k in defs: if k in opts: defs[k] = opts[k] From 4ead3f923bcb7e2816c378edbf4ee9de885c5491 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 15:00:44 -0700 Subject: [PATCH 246/607] Add changelog note --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 7b6c916b..921d0616 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,9 @@ pyqtgraph-0.11.0 (in development) To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument. - Deprecated graphicsWindow classes; these have been unnecessary for many years because widgets can be placed into a new window just by calling show(). + - Integer values in ParameterTree are now formatted as integer (%d) by default, rather than + scientific notation (%g). This can be overridden by providing `format={value:g}` when + creating the parameter. pyqtgraph-0.10.0 From 7593d4ea83e9007a27008913291fa10d2983fad0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 15:07:21 -0700 Subject: [PATCH 247/607] fix Profiler when called from top of stack --- pyqtgraph/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 61ae9fd5..dd956620 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -510,7 +510,7 @@ class Profiler(object): try: caller_object_type = type(caller_frame.f_locals["self"]) except KeyError: # we are in a regular function - qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1] + qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1] else: # we are in a method qualifier = caller_object_type.__name__ func_qualname = qualifier + "." + caller_frame.f_code.co_name From 0c1cda49730bdb7da4f9600dd69f55e8fd0cd4d1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 16:11:25 -0700 Subject: [PATCH 248/607] Gracefully handle case where image data has size==0 --- pyqtgraph/graphicsItems/ImageItem.py | 6 +++--- pyqtgraph/imageview/ImageView.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index d1e4aa9e..7d6c2596 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -465,11 +465,11 @@ class ImageItem(GraphicsObject): This method is also used when automatically computing levels. """ - if self.image is None: + if self.image is None or self.image.size == 0: return None,None if step == 'auto': - step = (int(np.ceil(self.image.shape[0] / targetImageSize)), - int(np.ceil(self.image.shape[1] / targetImageSize))) + step = (max(1, int(np.ceil(self.image.shape[0] / targetImageSize))), + max(1, int(np.ceil(self.image.shape[1] / targetImageSize)))) if np.isscalar(step): step = (step, step) stepData = self.image[::step[0], ::step[1]] diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c64953de..1d38a6c3 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -633,8 +633,12 @@ class ImageView(QtGui.QWidget): cax = self.axes['c'] if cax is None: + if data.size == 0: + return [(0, 0)] return [(float(nanmin(data)), float(nanmax(data)))] else: + if data.size == 0: + return [(0, 0)] * data.shape[-1] return [(float(nanmin(data.take(i, axis=cax))), float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])] From 5f778f905c784c788f17d240412721336b00e921 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Apr 2018 16:55:10 -0700 Subject: [PATCH 249/607] Clean up README Removed contributor list; the actual list of contributors is way too long to hold here :D --- README.md | 64 +++++++++++++------------------------------------------ 1 file changed, 15 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 30268796..85f4f9e7 100644 --- a/README.md +++ b/README.md @@ -6,80 +6,46 @@ PyQtGraph A pure-Python graphics library for PyQt/PySide -Copyright 2012 Luke Campagnola, University of North Carolina at Chapel Hill +Copyright 2017 Luke Campagnola, University of North Carolina at Chapel Hill -Maintainer ----------- +PyQtGraph is intended for use in mathematics / scientific / engineering applications. +Despite being written entirely in python, the library is fast due to its +heavy leverage of numpy for number crunching, Qt's GraphicsView framework for +2D display, and OpenGL for 3D display. - * Luke Campagnola - -Contributors ------------- - - * Megan Kratz - * Paul Manis - * Ingo Breßler - * Christian Gavin - * Michael Cristopher Hogg - * Ulrich Leutner - * Felix Schill - * Guillaume Poulin - * Antony Lee - * Mattias Põldaru - * Thomas S. - * Fabio Zadrozny - * Mikhail Terekhov - * Pietro Zambelli - * Stefan Holzmann - * Nicholas TJ - * John David Reaver - * David Kaplan - * Martin Fitzpatrick - * Daniel Lidstrom - * Eric Dill - * Vincent LeSaux Requirements ------------ * PyQt 4.7+, PySide, or PyQt5 - * python 2.6, 2.7, or 3.x + * python 2.7, or 3.x * NumPy * For 3D graphics: pyopengl and qt-opengl * Known to run on Windows, Linux, and Mac. Support ------- - - Post at the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) + + * Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues) + * Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph) Installation Methods -------------------- + * From pypi: + `pip install pyqtgraph` * To use with a specific project, simply copy the pyqtgraph subdirectory - anywhere that is importable from your project. PyQtGraph may also be - used as a git subtree by cloning the git-core repository from github. + anywhere that is importable from your project. * To install system-wide from source distribution: `$ python setup.py install` * For installation packages, see the website (pyqtgraph.org) - * On debian-like systems, pyqtgraph requires the following packages: - python-numpy, python-qt4 | python-pyside - For 3D support: python-opengl, python-qt4-gl | python-pyside.qtopengl Documentation ------------- -There are many examples; run `python -m pyqtgraph.examples` for a menu. +The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu. + +The official documentation lives at http://pyqtgraph.org/documentation -Some (incomplete) documentation exists at this time. - * Easiest place to get documentation is at - * If you acquired this code as a .tar.gz file from the website, then you can also look in - doc/html. - * If you acquired this code via GitHub, then you can build the documentation using sphinx. - From the documentation directory, run: - `$ make html` - -Please feel free to pester Luke or post to the forum if you need a specific - section of documentation to be expanded. From 972f6205b56438b734041d22fc18a9c3945d39cd Mon Sep 17 00:00:00 2001 From: Doug Johnston Date: Wed, 25 Apr 2018 20:06:40 -0700 Subject: [PATCH 250/607] Allow for panning in the plane of the camera Accessed via -[left click] --- pyqtgraph/opengl/GLViewWidget.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 540fce7d..f89671b4 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -309,37 +309,51 @@ class GLViewWidget(QtOpenGL.QGLWidget): dist = (pos-cam).length() xDist = dist * 2. * np.tan(0.5 * self.opts['fov'] * np.pi / 180.) return xDist / self.width() - + def mousePressEvent(self, ev): self.mousePos = ev.pos() - + def mouseMoveEvent(self, ev): diff = ev.pos() - self.mousePos self.mousePos = ev.pos() - + if ev.buttons() == QtCore.Qt.LeftButton: - self.orbit(-diff.x(), diff.y()) - #print self.opts['azimuth'], self.opts['elevation'] + if (ev.modifiers() & QtCore.Qt.ControlModifier): + # pan in plane of camera + elev = np.radians(self.opts['elevation']) + azim = np.radians(self.opts['azimuth']) + fov = np.radians(self.opts['fov']) + dist = (self.opts['center'] - self.cameraPosition()).length() + fov_factor = np.tan(fov / 2) * 2 + scale_factor = dist * fov_factor / self.width() + dx = diff.x() + dy = diff.y() + z = scale_factor * np.cos(elev) * dy + x = scale_factor * (np.sin(azim) * dx - np.sin(elev) * np.cos(azim) * dy) + y = scale_factor * (np.cos(azim) * dx + np.sin(elev) * np.sin(azim) * dy) + self.pan(x, -y, z, relative=False) + else: + self.orbit(-diff.x(), diff.y()) elif ev.buttons() == QtCore.Qt.MidButton: if (ev.modifiers() & QtCore.Qt.ControlModifier): self.pan(diff.x(), 0, diff.y(), relative=True) else: self.pan(diff.x(), diff.y(), 0, relative=True) - + def mouseReleaseEvent(self, ev): pass # Example item selection code: #region = (ev.pos().x()-5, ev.pos().y()-5, 10, 10) #print(self.itemsAt(region)) - + ## debugging code: draw the picking region #glViewport(*self.getViewport()) #glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) #region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3]) #self.paintGL(region=region) #self.swapBuffers() - - + + def wheelEvent(self, ev): delta = 0 if not USE_PYQT5: From 87efc2418a36e3041ad78b54538845638e7a4d90 Mon Sep 17 00:00:00 2001 From: Doug Johnston Date: Thu, 26 Apr 2018 09:17:00 -0700 Subject: [PATCH 251/607] fix whitespace changes --- pyqtgraph/opengl/GLViewWidget.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index f89671b4..2906d1eb 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -309,14 +309,14 @@ class GLViewWidget(QtOpenGL.QGLWidget): dist = (pos-cam).length() xDist = dist * 2. * np.tan(0.5 * self.opts['fov'] * np.pi / 180.) return xDist / self.width() - + def mousePressEvent(self, ev): self.mousePos = ev.pos() - + def mouseMoveEvent(self, ev): diff = ev.pos() - self.mousePos self.mousePos = ev.pos() - + if ev.buttons() == QtCore.Qt.LeftButton: if (ev.modifiers() & QtCore.Qt.ControlModifier): # pan in plane of camera @@ -339,21 +339,21 @@ class GLViewWidget(QtOpenGL.QGLWidget): self.pan(diff.x(), 0, diff.y(), relative=True) else: self.pan(diff.x(), diff.y(), 0, relative=True) - + def mouseReleaseEvent(self, ev): pass # Example item selection code: #region = (ev.pos().x()-5, ev.pos().y()-5, 10, 10) #print(self.itemsAt(region)) - + ## debugging code: draw the picking region #glViewport(*self.getViewport()) #glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) #region = (region[0], self.height()-(region[1]+region[3]), region[2], region[3]) #self.paintGL(region=region) #self.swapBuffers() - - + + def wheelEvent(self, ev): delta = 0 if not USE_PYQT5: From b64294064ff6f75437038d2805b69ac9e8cf798c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc=20Poulhi=C3=A8s?= Date: Thu, 26 Apr 2018 21:57:51 +0200 Subject: [PATCH 252/607] typo fix in comment trivial typo fix --- pyqtgraph/SRTTransform3D.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/SRTTransform3D.py b/pyqtgraph/SRTTransform3D.py index 9b54843b..3c4edcc8 100644 --- a/pyqtgraph/SRTTransform3D.py +++ b/pyqtgraph/SRTTransform3D.py @@ -113,7 +113,7 @@ class SRTTransform3D(Transform3D): def setFromMatrix(self, m): """ - Set this transform mased on the elements of *m* + Set this transform based on the elements of *m* The input matrix must be affine AND have no shear, otherwise the conversion will most likely fail. """ From 60a48ed2e4ef9fcecaffca33e009f4e0a35bd972 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 13:22:47 -0700 Subject: [PATCH 253/607] reload tests pass in python 3 --- pyqtgraph/functions.py | 2 +- pyqtgraph/reload.py | 46 ++++++++++++++++-------- pyqtgraph/tests/test_reload.py | 65 ++++++++++++++++++++++++---------- 3 files changed, 79 insertions(+), 34 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 21e6c006..8857c052 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2444,7 +2444,7 @@ def disconnect(signal, slot): try: signal.disconnect(slot) return True - except TypeError, RuntimeError: + except (TypeError, RuntimeError): slot = reload.getPreviousVersion(slot) if slot is None: return False diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index 5aa2ed68..f682a025 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -21,15 +21,18 @@ Does NOT: print module.someObject """ - +from __future__ import print_function import inspect, os, sys, gc, traceback, types try: - import __builtin__ as builtins + from builtins import reload as orig_reload except ImportError: - import builtins + from importlib import reload as orig_reload from .debug import printExc +py3 = sys.version_info >= (3,) + + def reloadAll(prefix=None, debug=False): """Automatically reload everything whose __file__ begins with prefix. - Skips reload if the file has not been updated (if .pyc is newer than .py) @@ -80,7 +83,7 @@ def reload(module, debug=False, lists=False, dicts=False): ## make a copy of the old module dictionary, reload, then grab the new module dictionary for comparison oldDict = module.__dict__.copy() - builtins.reload(module) + orig_reload(module) newDict = module.__dict__ ## Allow modules access to the old dictionary after they reload @@ -130,6 +133,9 @@ def updateFunction(old, new, debug, depth=0, visited=None): old.__code__ = new.__code__ old.__defaults__ = new.__defaults__ + if hasattr(old, '__kwdefaults'): + old.__kwdefaults__ = new.__kwdefaults__ + old.__doc__ = new.__doc__ if visited is None: visited = [] @@ -154,6 +160,8 @@ def updateFunction(old, new, debug, depth=0, visited=None): ## For classes: ## 1) find all instances of the old class and set instance.__class__ to the new class ## 2) update all old class methods to use code from the new class methods + + def updateClass(old, new, debug): ## Track town all instances and subclasses of old refs = gc.get_referrers(old) @@ -198,7 +206,8 @@ def updateClass(old, new, debug): ## but it fixes a few specific cases (pyqt signals, for one) for attr in dir(old): oa = getattr(old, attr) - if inspect.ismethod(oa): + if (py3 and inspect.isfunction(oa)) or inspect.ismethod(oa): + # note python2 has unbound methods, whereas python3 just uses plain functions try: na = getattr(new, attr) except AttributeError: @@ -206,11 +215,14 @@ def updateClass(old, new, debug): print(" Skipping method update for %s; new class does not have this attribute" % attr) continue - if hasattr(oa, 'im_func') and hasattr(na, 'im_func') and oa.__func__ is not na.__func__: - depth = updateFunction(oa.__func__, na.__func__, debug) - if not hasattr(na.__func__, '__previous_reload_method__'): - na.__func__.__previous_reload_method__ = oa # important for managing signal connection - #oa.im_class = new ## bind old method to new class ## not allowed + ofunc = getattr(oa, '__func__', oa) # in py2 we have to get the __func__ from unbound method, + nfunc = getattr(na, '__func__', na) # in py3 the attribute IS the function + + if ofunc is not nfunc: + depth = updateFunction(ofunc, nfunc, debug) + if not hasattr(nfunc, '__previous_reload_method__'): + nfunc.__previous_reload_method__ = oa # important for managing signal connection + #oa.__class__ = new ## bind old method to new class ## not allowed if debug: extra = "" if depth > 0: @@ -251,16 +263,22 @@ def getPreviousVersion(obj): if isinstance(obj, type) or inspect.isfunction(obj): return getattr(obj, '__previous_reload_version__', None) elif inspect.ismethod(obj): - if obj.im_self is None: + if obj.__self__ is None: # unbound method return getattr(obj.__func__, '__previous_reload_method__', None) else: oldmethod = getattr(obj.__func__, '__previous_reload_method__', None) if oldmethod is None: return None - self = obj.im_self - cls = oldmethod.im_class - return types.MethodType(oldmethod.__func__, self, cls) + self = obj.__self__ + oldfunc = getattr(oldmethod, '__func__', oldmethod) + if hasattr(oldmethod, 'im_class'): + # python 2 + cls = oldmethod.im_class + return types.MethodType(oldfunc, self, cls) + else: + # python 3 + return types.MethodType(oldfunc, self) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 7ceed546..57924d60 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -1,14 +1,23 @@ -import tempfile, os, sys, shutil, atexit +import tempfile, os, sys, shutil import pyqtgraph as pg import pyqtgraph.reload + + pgpath = os.path.join(os.path.dirname(pg.__file__), '..') # make temporary directory to write module code -path = tempfile.mkdtemp() -sys.path.insert(0, path) -def cleanup(): +path = None + +def setup_module(): + # make temporary directory to write module code + global path + path = tempfile.mkdtemp() + sys.path.insert(0, path) + +def teardown_module(): + global path shutil.rmtree(path) -atexit.register(cleanup) + sys.path.remove(path) code = """ @@ -33,6 +42,8 @@ def remove_cache(mod): def test_reload(): + py3 = sys.version_info >= (3,) + # write a module mod = os.path.join(path, 'reload_test.py') open(mod, 'w').write(code.format(path=path, msg="C.fn() Version1")) @@ -42,7 +53,10 @@ def test_reload(): c = reload_test.C() c.sig.connect(c.fn) - v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) @@ -50,25 +64,34 @@ def test_reload(): open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) - v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) - assert c.fn.im_class is v2[0] + if not py3: + assert c.fn.im_class is v2[0] oldcfn = pg.reload.getPreviousVersion(c.fn) if oldcfn is None: # Function did not reload; are we using pytest's assertion rewriting? raise Exception("Function did not reload. (This can happen when using py.test" " with assertion rewriting; use --assert=plain for this test.)") - assert oldcfn.im_class is v1[0] - assert oldcfn.im_func is v1[2].im_func - assert oldcfn.im_self is c - + if py3: + assert oldcfn.__func__ is v1[2] + else: + assert oldcfn.im_class is v1[0] + assert oldcfn.__func__ is v1[2].__func__ + assert oldcfn.__self__ is c # write again and reload open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) - v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + if py3: + v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + else: + v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) #for i in range(len(old)): #print id(old[i]), id(new1[i]), id(new2[i]), old[i], new1[i] @@ -76,11 +99,15 @@ def test_reload(): cfn1 = pg.reload.getPreviousVersion(c.fn) cfn2 = pg.reload.getPreviousVersion(cfn1) - assert cfn1.im_class is v2[0] - assert cfn1.im_func is v2[2].im_func - assert cfn1.im_self is c - assert cfn2.im_class is v1[0] - assert cfn2.im_func is v1[2].im_func - assert cfn2.im_self is c + if py3: + assert cfn1.__func__ is v2[2] + assert cfn2.__func__ is v1[2] + else: + assert cfn1.__func__ is v2[2].__func__ + assert cfn2.__func__ is v1[2].__func__ + assert cfn1.im_class is v2[0] + assert cfn2.im_class is v1[0] + assert cfn1.__self__ is c + assert cfn2.__self__ is c c.sig.disconnect(cfn2) From 986ccb81f489e791b442f1296e0efe27bbfda601 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 13:28:51 -0700 Subject: [PATCH 254/607] fix reload import --- pyqtgraph/reload.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index f682a025..766ec9d0 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -23,11 +23,11 @@ Does NOT: from __future__ import print_function import inspect, os, sys, gc, traceback, types -try: - from builtins import reload as orig_reload -except ImportError: - from importlib import reload as orig_reload from .debug import printExc +try: + from importlib import reload as orig_reload +except ImportError: + orig_reload = reload py3 = sys.version_info >= (3,) From 6e392c2e0eae4adbd353e01c45b2134f8e585d8d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 13:49:34 -0700 Subject: [PATCH 255/607] Fix disconnect test for pyside --- pyqtgraph/tests/test_reload.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 57924d60..b90072f1 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -109,5 +109,6 @@ def test_reload(): assert cfn2.im_class is v1[0] assert cfn1.__self__ is c assert cfn2.__self__ is c - c.sig.disconnect(cfn2) + + pg.functions.disconnect(c.sig, c.fn) From a1145b5cbfc1db32d8dffb7ffaa1b1d18b843923 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 26 Apr 2018 14:43:20 -0700 Subject: [PATCH 256/607] Fix pytest assertion rewrite conflicting with reload by giving temporary module a different name --- .travis.yml | 2 +- pyqtgraph/tests/test_reload.py | 26 ++++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1c422f19..acfde8ec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -143,7 +143,7 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. py.test --cov --assert=plain pyqtgraph -sv; + PYTHONPATH=. py.test --cov pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index b90072f1..6adbeeb6 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -45,29 +45,31 @@ def test_reload(): py3 = sys.version_info >= (3,) # write a module - mod = os.path.join(path, 'reload_test.py') - open(mod, 'w').write(code.format(path=path, msg="C.fn() Version1")) + mod = os.path.join(path, 'reload_test_mod.py') + print("\nRELOAD FILE:", mod) + open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version1")) # import the new module - import reload_test + import reload_test_mod + print("RELOAD MOD:", reload_test_mod.__file__) - c = reload_test.C() + c = reload_test_mod.C() c.sig.connect(c.fn) if py3: - v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + v1 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__) else: - v1 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + v1 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__) # write again and reload - open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) + open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) if py3: - v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + v2 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__) else: - v2 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + v2 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__) if not py3: assert c.fn.im_class is v2[0] @@ -85,13 +87,13 @@ def test_reload(): # write again and reload - open(mod, 'w').write(code.format(path=path, msg="C.fn() Version2")) + open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) if py3: - v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, c.sig, c.fn, c.fn.__func__) + v3 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, c.sig, c.fn, c.fn.__func__) else: - v3 = (reload_test.C, reload_test.C.sig, reload_test.C.fn, reload_test.C.fn.__func__, c.sig, c.fn, c.fn.__func__) + v3 = (reload_test_mod.C, reload_test_mod.C.sig, reload_test_mod.C.fn, reload_test_mod.C.fn.__func__, c.sig, c.fn, c.fn.__func__) #for i in range(len(old)): #print id(old[i]), id(new1[i]), id(new2[i]), old[i], new1[i] From 436fcccf8273c463e06862cbeae8c221ee57da9d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 27 Apr 2018 19:07:22 -0700 Subject: [PATCH 257/607] Avoid overflow in Point.length by using trig functions or returning inf --- pyqtgraph/Point.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index 4d04f01c..3fb43cac 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -105,7 +105,13 @@ class Point(QtCore.QPointF): def length(self): """Returns the vector length of this Point.""" - return (self[0]**2 + self[1]**2) ** 0.5 + try: + return (self[0]**2 + self[1]**2) ** 0.5 + except OverflowError: + try: + return self[1] / np.sin(np.arctan2(self[1], self[0])) + except OverflowError: + return np.inf def norm(self): """Returns a vector in the same direction with unit length.""" From 0fd0f08384343ceee73a3f205169372d98229873 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 3 May 2018 15:30:19 -0700 Subject: [PATCH 258/607] Doc updates for datafilterwidget --- pyqtgraph/widgets/DataFilterWidget.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index 23cf930f..a26bf0bf 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -10,6 +10,9 @@ class DataFilterWidget(ptree.ParameterTree): """ This class allows the user to filter multi-column data sets by specifying multiple criteria + + Wraps methods from DataFilterParameter: setFields, generateMask, + filterData, and describe. """ sigFilterChanged = QtCore.Signal(object) @@ -22,6 +25,7 @@ class DataFilterWidget(ptree.ParameterTree): self.params.sigTreeStateChanged.connect(self.filterChanged) self.setFields = self.params.setFields + self.generateMask = self.params.generateMask self.filterData = self.params.filterData self.describe = self.params.describe @@ -38,7 +42,8 @@ class DataFilterWidget(ptree.ParameterTree): class DataFilterParameter(ptree.types.GroupParameter): - + """A parameter group that specifies a set of filters to apply to tabular data. + """ sigFilterChanged = QtCore.Signal(object) def __init__(self): @@ -61,6 +66,17 @@ class DataFilterParameter(ptree.types.GroupParameter): return self.fields.keys() def setFields(self, fields): + """Set the list of fields that are available to be filtered. + + *fields* must be a dict or list of tuples that maps field names + to a specification describing the field. Each specification is + itself a dict with either ``'mode':'range'`` or ``'mode':'enum'``:: + + filter.setFields([ + ('field1', {'mode': 'range'}), + ('field2', {'mode': 'enum', 'values': ['val1', 'val2', 'val3']}), + ]) + """ self.fields = OrderedDict(fields) names = self.fieldNames() self.setAddList(names) @@ -71,6 +87,9 @@ class DataFilterParameter(ptree.types.GroupParameter): return data[self.generateMask(data)] def generateMask(self, data): + """Return a boolean mask indicating whether each item in *data* passes + the filter critera. + """ mask = np.ones(len(data), dtype=bool) if len(data) == 0: return mask @@ -94,6 +113,7 @@ class DataFilterParameter(ptree.types.GroupParameter): desc.append(fp.describe()) return desc + class RangeFilterItem(ptree.types.SimpleParameter): def __init__(self, name, opts): self.fieldName = name From 11463c5a0b21d91783e24844d068115d6689e827 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 4 May 2018 11:42:37 -0700 Subject: [PATCH 259/607] Fix: mkQApp returned None if a QApplication was already created elsewhere --- pyqtgraph/Qt.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 8c0041df..4ce571cc 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -276,7 +276,8 @@ if m is not None and list(map(int, m.groups())) < versionReq: QAPP = None def mkQApp(): - global QAPP - if QtGui.QApplication.instance() is None: + global QAPP + QAPP = QtGui.QApplication.instance() + if QAPP is None: QAPP = QtGui.QApplication([]) return QAPP From 9460b746fa95c6682637b8870e70f9c2c76961c5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 4 May 2018 14:17:14 -0700 Subject: [PATCH 260/607] Allow data filter entries to be updated after they are created --- pyqtgraph/widgets/DataFilterWidget.py | 49 +++++++++++++++++++-------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index a26bf0bf..bd92a765 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -6,6 +6,7 @@ from .. import functions as fn __all__ = ['DataFilterWidget'] + class DataFilterWidget(ptree.ParameterTree): """ This class allows the user to filter multi-column data sets by specifying @@ -80,6 +81,12 @@ class DataFilterParameter(ptree.types.GroupParameter): self.fields = OrderedDict(fields) names = self.fieldNames() self.setAddList(names) + + # update any existing filters + for ch in self.children(): + name = ch.fieldName + if name in fields: + ch.updateFilter(fields[name]) def filterData(self, data): if len(data) == 0: @@ -134,25 +141,17 @@ class RangeFilterItem(ptree.types.SimpleParameter): def describe(self): return "%s < %s < %s" % (fn.siFormat(self['Min'], suffix=self.units), self.fieldName, fn.siFormat(self['Max'], suffix=self.units)) + + def updateFilter(self, opts): + pass + class EnumFilterItem(ptree.types.SimpleParameter): def __init__(self, name, opts): self.fieldName = name - vals = opts.get('values', []) - childs = [] - if isinstance(vals, list): - vals = OrderedDict([(v,str(v)) for v in vals]) - for val,vname in vals.items(): - ch = ptree.Parameter.create(name=vname, type='bool', value=True) - ch.maskValue = val - childs.append(ch) - ch = ptree.Parameter.create(name='(other)', type='bool', value=True) - ch.maskValue = '__other__' - childs.append(ch) - ptree.types.SimpleParameter.__init__(self, - name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True, - children=childs) + name=name, autoIncrementName=True, type='bool', value=True, removable=True, renamable=True) + self.setEnumVals(opts) def generateMask(self, data, startMask): vals = data[self.fieldName][startMask] @@ -172,4 +171,24 @@ class EnumFilterItem(ptree.types.SimpleParameter): def describe(self): vals = [ch.name() for ch in self if ch.value() is True] - return "%s: %s" % (self.fieldName, ', '.join(vals)) \ No newline at end of file + return "%s: %s" % (self.fieldName, ', '.join(vals)) + + def updateFilter(self, opts): + self.setEnumVals(opts) + + def setEnumVals(self, opts): + vals = opts.get('values', []) + prevState = {} + for ch in self.children(): + prevState[ch.name()] = ch.value() + self.removeChild(ch) + + if isinstance(vals, list): + vals = OrderedDict([(v,str(v)) for v in vals]) + for val,vname in vals.items(): + ch = ptree.Parameter.create(name=vname, type='bool', value=prevState.get(vname, True)) + ch.maskValue = val + self.addChild(ch) + ch = ptree.Parameter.create(name='(other)', type='bool', value=prevState.get('(other)', True)) + ch.maskValue = '__other__' + self.addChild(ch) From 1a3a0a70930b6e96e9767367aaeeaae04d2781ba Mon Sep 17 00:00:00 2001 From: danielhrisca Date: Tue, 8 May 2018 10:01:49 +0300 Subject: [PATCH 261/607] update version for new development --- pyqtgraph/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 4ac5e646..f590aa79 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python www.pyqtgraph.org """ -__version__ = '0.10.0' +__version__ = '0.11.dev' ### import all the goodies and add some helper functions for easy CLI use From 4d26b4bb20013eb5b90b5c5f5b058b26d0eb8906 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 9 May 2018 10:56:01 -0700 Subject: [PATCH 262/607] Fix py3 string handling in scatterplotwidget example --- examples/ScatterPlotWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ScatterPlotWidget.py b/examples/ScatterPlotWidget.py index 33503cab..f3766d56 100644 --- a/examples/ScatterPlotWidget.py +++ b/examples/ScatterPlotWidget.py @@ -28,7 +28,7 @@ pg.mkQApp() # Make up some tabular data with structure data = np.empty(1000, dtype=[('x_pos', float), ('y_pos', float), ('count', int), ('amplitude', float), - ('decay', float), ('type', 'S10')]) + ('decay', float), ('type', 'U10')]) strings = ['Type-A', 'Type-B', 'Type-C', 'Type-D', 'Type-E'] typeInds = np.random.randint(5, size=1000) data['type'] = np.array(strings)[typeInds] From a8610bfb4390766a080829efda322aa109554775 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 9 May 2018 10:57:42 -0700 Subject: [PATCH 263/607] Add option to set enum default values in DataFilterWidget --- pyqtgraph/widgets/DataFilterWidget.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index bd92a765..7b03725c 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -3,6 +3,7 @@ from .. import parametertree as ptree import numpy as np from ..pgcollections import OrderedDict from .. import functions as fn +from ..python2_3 import basestring __all__ = ['DataFilterWidget'] @@ -76,6 +77,7 @@ class DataFilterParameter(ptree.types.GroupParameter): filter.setFields([ ('field1', {'mode': 'range'}), ('field2', {'mode': 'enum', 'values': ['val1', 'val2', 'val3']}), + ('field3', {'mode': 'enum', 'values': {'val1':True, 'val2':False, 'val3':True}}), ]) """ self.fields = OrderedDict(fields) @@ -177,16 +179,30 @@ class EnumFilterItem(ptree.types.SimpleParameter): self.setEnumVals(opts) def setEnumVals(self, opts): - vals = opts.get('values', []) + vals = opts.get('values', {}) + prevState = {} for ch in self.children(): prevState[ch.name()] = ch.value() self.removeChild(ch) - if isinstance(vals, list): - vals = OrderedDict([(v,str(v)) for v in vals]) - for val,vname in vals.items(): - ch = ptree.Parameter.create(name=vname, type='bool', value=prevState.get(vname, True)) + if not isinstance(vals, dict): + vals = OrderedDict([(v,(str(v), True)) for v in vals]) + + # Each filterable value can come with either (1) a string name, (2) a bool + # indicating whether the value is enabled by default, or (3) a tuple providing + # both. + for val,valopts in vals.items(): + if isinstance(valopts, bool): + enabled = valopts + vname = str(val) + elif isinstance(valopts, basestring): + enabled = True + vname = valopts + elif isinstance(valopts, tuple): + vname, enabled = valopts + + ch = ptree.Parameter.create(name=vname, type='bool', value=prevState.get(vname, enabled)) ch.maskValue = val self.addChild(ch) ch = ptree.Parameter.create(name='(other)', type='bool', value=prevState.get('(other)', True)) From cd2125cccbe9627cff4439181bb3a91249b136bf Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 15 May 2018 14:24:52 -0700 Subject: [PATCH 264/607] fixes #687 --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index bd2a4c45..02a8ddad 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -847,10 +847,6 @@ class ViewBox(GraphicsWidget): childRange[ax][1] += wp targetRect[ax] = childRange[ax] args['xRange' if ax == 0 else 'yRange'] = targetRect[ax] - if len(args) == 0: - return - args['padding'] = 0 - args['disableAutoRange'] = False # check for and ignore bad ranges for k in ['xRange', 'yRange']: @@ -858,6 +854,11 @@ class ViewBox(GraphicsWidget): if not np.all(np.isfinite(args[k])): r = args.pop(k) #print("Warning: %s is invalid: %s" % (k, str(r)) + + if len(args) == 0: + return + args['padding'] = 0 + args['disableAutoRange'] = False self.setRange(**args) finally: From a86ab305786048d58407ce79ebb7e7d19ef55dce Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 15 May 2018 16:37:20 -0700 Subject: [PATCH 265/607] fix iteritems --- pyqtgraph/debug.py | 2 +- pyqtgraph/exporters/Matplotlib.py | 2 +- pyqtgraph/graphicsItems/PlotDataItem.py | 2 +- pyqtgraph/multiprocess/parallelizer.py | 2 +- pyqtgraph/multiprocess/processes.py | 4 ++-- pyqtgraph/multiprocess/remoteproxy.py | 2 +- pyqtgraph/pgcollections.py | 4 ++-- pyqtgraph/util/lru_cache.py | 2 +- pyqtgraph/util/tests/test_lru_cache.py | 16 ++++++++-------- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index dd956620..3ddcae37 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -1097,7 +1097,7 @@ def pretty(data, indent=''): ind2 = indent + " " if isinstance(data, dict): ret = indent+"{\n" - for k, v in data.iteritems(): + for k, v in data.items(): ret += ind2 + repr(k) + ": " + pretty(v, ind2).strip() + "\n" ret += indent+"}\n" elif isinstance(data, list) or isinstance(data, tuple): diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index 8cec1cef..2da979b1 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -43,7 +43,7 @@ class MatplotlibExporter(Exporter): for ax in axl: if ax is None: continue - for loc, spine in ax.spines.iteritems(): + for loc, spine in ax.spines.items(): if loc in ['left', 'bottom']: pass elif loc in ['right', 'top']: diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index d7ea5100..c85c3e5b 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -771,7 +771,7 @@ def isSequence(obj): #if isinstance(arg, basestring): #return self.data[arg] #elif isinstance(arg, int): - #return dict([(k, v[arg]) for k, v in self.data.iteritems()]) + #return dict([(k, v[arg]) for k, v in self.data.items()]) #elif isinstance(arg, tuple): #arg = self._orderArgs(arg) #return self.data[arg[1]][arg[0]] diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index ef00be7c..989bd4f8 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -245,7 +245,7 @@ class Tasker(object): self.proc = process self.par = parallelizer self.tasks = tasks - for k, v in kwds.iteritems(): + for k, v in kwds.items(): setattr(self, k, v) def __iter__(self): diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index 0a372ef0..4cf2ab70 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -251,7 +251,7 @@ class ForkedProcess(RemoteEventHandler): proxyIDs = {} if preProxy is not None: - for k, v in preProxy.iteritems(): + for k, v in preProxy.items(): proxyId = LocalObjectProxy.registerObject(v) proxyIDs[k] = proxyId @@ -300,7 +300,7 @@ class ForkedProcess(RemoteEventHandler): RemoteEventHandler.__init__(self, remoteConn, name+'_child', pid=ppid) self.forkedProxies = {} - for name, proxyId in proxyIDs.iteritems(): + for name, proxyId in proxyIDs.items(): self.forkedProxies[name] = ObjectProxy(ppid, proxyId=proxyId, typeStr=repr(preProxy[name])) if target is not None: diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index bc02da83..f73d4309 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -545,7 +545,7 @@ class RemoteEventHandler(object): if autoProxy is True: args = [self.autoProxy(v, noProxyTypes) for v in args] - for k, v in kwds.iteritems(): + for k, v in kwds.items(): opts[k] = self.autoProxy(v, noProxyTypes) byteMsgs = [] diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index 76850622..db0fb74e 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -239,7 +239,7 @@ class CaselessDict(OrderedDict): return key.lower() in self.keyMap def update(self, d): - for k, v in d.iteritems(): + for k, v in d.items(): self[k] = v def copy(self): @@ -315,7 +315,7 @@ class ProtectedDict(dict): yield protect(v) def iteritems(self): - for k, v in self._data_.iteritems(): + for k, v in self._data_.items(): yield (k, protect(v)) def deepcopy(self): diff --git a/pyqtgraph/util/lru_cache.py b/pyqtgraph/util/lru_cache.py index 9c04abf3..69c1c20a 100644 --- a/pyqtgraph/util/lru_cache.py +++ b/pyqtgraph/util/lru_cache.py @@ -117,5 +117,5 @@ class LRUCache(object): for x in sorted(self._dict.itervalues(), key=operator.itemgetter(2)): yield x[0], x[1] else: - for x in self._dict.iteritems(): + for x in self._dict.items(): yield x[0], x[1] diff --git a/pyqtgraph/util/tests/test_lru_cache.py b/pyqtgraph/util/tests/test_lru_cache.py index c0cf9f8a..94451d97 100644 --- a/pyqtgraph/util/tests/test_lru_cache.py +++ b/pyqtgraph/util/tests/test_lru_cache.py @@ -22,28 +22,28 @@ def checkLru(lru): set([2, 1]) == set(lru.values()) #Iterates from the used in the last access to others based on access time. - assert [(2, 2), (1, 1)] == list(lru.iteritems(accessTime=True)) + assert [(2, 2), (1, 1)] == list(lru.items(accessTime=True)) lru[2] = 2 - assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True)) + assert [(1, 1), (2, 2)] == list(lru.items(accessTime=True)) del lru[2] - assert [(1, 1), ] == list(lru.iteritems(accessTime=True)) + assert [(1, 1), ] == list(lru.items(accessTime=True)) lru[2] = 2 - assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True)) + assert [(1, 1), (2, 2)] == list(lru.items(accessTime=True)) _a = lru[1] - assert [(2, 2), (1, 1)] == list(lru.iteritems(accessTime=True)) + assert [(2, 2), (1, 1)] == list(lru.items(accessTime=True)) _a = lru[2] - assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True)) + assert [(1, 1), (2, 2)] == list(lru.items(accessTime=True)) assert lru.get(2) == 2 assert lru.get(3) == None - assert [(1, 1), (2, 2)] == list(lru.iteritems(accessTime=True)) + assert [(1, 1), (2, 2)] == list(lru.items(accessTime=True)) lru.clear() - assert [] == list(lru.iteritems()) + assert [] == list(lru.items()) if __name__ == '__main__': From 1f380c93c167f36130763438aeb52adfccbe3547 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 15 May 2018 16:37:58 -0700 Subject: [PATCH 266/607] fix itervalues --- pyqtgraph/flowchart/Node.py | 4 ++-- pyqtgraph/pgcollections.py | 2 +- pyqtgraph/util/lru_cache.py | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/flowchart/Node.py b/pyqtgraph/flowchart/Node.py index c450a9f3..e7ce9360 100644 --- a/pyqtgraph/flowchart/Node.py +++ b/pyqtgraph/flowchart/Node.py @@ -222,7 +222,7 @@ class Node(QtCore.QObject): for t in self.inputs().values(): nodes |= set([i.node() for i in t.inputTerminals()]) return nodes - #return set([t.inputTerminals().node() for t in self.listInputs().itervalues()]) + #return set([t.inputTerminals().node() for t in self.listInputs().values()]) def __repr__(self): return "" % (self.name(), id(self)) @@ -477,7 +477,7 @@ class NodeGraphicsItem(GraphicsObject): #self.node.sigTerminalRenamed.connect(self.updateActionMenu) #def setZValue(self, z): - #for t, item in self.terminals.itervalues(): + #for t, item in self.terminals.values(): #item.setZValue(z+1) #GraphicsObject.setZValue(self, z) diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index db0fb74e..ac7f68fe 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -311,7 +311,7 @@ class ProtectedDict(dict): raise Exception("It is not safe to copy protected dicts! (instead try deepcopy, but be careful.)") def itervalues(self): - for v in self._data_.itervalues(): + for v in self._data_.values(): yield protect(v) def iteritems(self): diff --git a/pyqtgraph/util/lru_cache.py b/pyqtgraph/util/lru_cache.py index 69c1c20a..d0b160dd 100644 --- a/pyqtgraph/util/lru_cache.py +++ b/pyqtgraph/util/lru_cache.py @@ -94,14 +94,14 @@ class LRUCache(object): else: def values(self): - return [i[1] for i in self._dict.itervalues()] + return [i[1] for i in self._dict.values()] def keys(self): - return [x[0] for x in self._dict.itervalues()] + return [x[0] for x in self._dict.values()] def _resizeTo(self): - ordered = sorted(self._dict.itervalues(), key=operator.itemgetter(2))[:self.resizeTo] + ordered = sorted(self._dict.values(), key=operator.itemgetter(2))[:self.resizeTo] for i in ordered: del self._dict[i[0]] @@ -114,7 +114,7 @@ class LRUCache(object): ============= ====================================================== ''' if accessTime: - for x in sorted(self._dict.itervalues(), key=operator.itemgetter(2)): + for x in sorted(self._dict.values(), key=operator.itemgetter(2)): yield x[0], x[1] else: for x in self._dict.items(): From c390c5cd9802708cd444557f89c6c18cbaa357cd Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 16 May 2018 08:33:51 -0700 Subject: [PATCH 267/607] fix lru_cache --- pyqtgraph/util/lru_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/util/lru_cache.py b/pyqtgraph/util/lru_cache.py index d0b160dd..5300e0ff 100644 --- a/pyqtgraph/util/lru_cache.py +++ b/pyqtgraph/util/lru_cache.py @@ -80,7 +80,7 @@ class LRUCache(object): for i in ordered: del self._dict[i[0]] - def iteritems(self, accessTime=False): + def items(self, accessTime=False): ''' :param bool accessTime: If True sorts the returned items by the internal access time. @@ -105,7 +105,7 @@ class LRUCache(object): for i in ordered: del self._dict[i[0]] - def iteritems(self, accessTime=False): + def items(self, accessTime=False): ''' ============= ====================================================== **Arguments** From 8da208b914e99914684b407524db419ea8217d4f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 17 May 2018 09:02:41 -0700 Subject: [PATCH 268/607] flip pyside2 template line endings --- .../ScatterPlotSpeedTestTemplate_pyside2.py | 88 ++-- examples/VideoTemplate_pyside2.py | 414 +++++++++--------- .../exportDialogTemplate_pyside2.py | 126 +++--- pyqtgraph/Transform3D.py | 3 +- pyqtgraph/canvas/CanvasTemplate_pyside2.py | 174 ++++---- .../canvas/TransformGuiTemplate_pyside2.py | 110 ++--- pyqtgraph/console/template_pyside2.py | 226 +++++----- .../FlowchartCtrlTemplate_pyside2.py | 132 +++--- .../flowchart/FlowchartTemplate_pyside2.py | 108 ++--- .../imageview/ImageViewTemplate_pyside2.py | 308 ++++++------- 10 files changed, 845 insertions(+), 844 deletions(-) diff --git a/examples/ScatterPlotSpeedTestTemplate_pyside2.py b/examples/ScatterPlotSpeedTestTemplate_pyside2.py index 7b66ad46..b0e62814 100644 --- a/examples/ScatterPlotSpeedTestTemplate_pyside2.py +++ b/examples/ScatterPlotSpeedTestTemplate_pyside2.py @@ -1,44 +1,44 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'ScatterPlotSpeedTestTemplate.ui' -# -# Created: Sun Sep 18 19:21:36 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(400, 300) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setObjectName("gridLayout") - self.sizeSpin = QtWidgets.QSpinBox(Form) - self.sizeSpin.setProperty("value", 10) - self.sizeSpin.setObjectName("sizeSpin") - self.gridLayout.addWidget(self.sizeSpin, 1, 1, 1, 1) - self.pixelModeCheck = QtWidgets.QCheckBox(Form) - self.pixelModeCheck.setObjectName("pixelModeCheck") - self.gridLayout.addWidget(self.pixelModeCheck, 1, 3, 1, 1) - self.label = QtWidgets.QLabel(Form) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 1, 0, 1, 1) - self.plot = PlotWidget(Form) - self.plot.setObjectName("plot") - self.gridLayout.addWidget(self.plot, 0, 0, 1, 4) - self.randCheck = QtWidgets.QCheckBox(Form) - self.randCheck.setObjectName("randCheck") - self.gridLayout.addWidget(self.randCheck, 1, 2, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.pixelModeCheck.setText(QtWidgets.QApplication.translate("Form", "pixel mode", None, -1)) - self.label.setText(QtWidgets.QApplication.translate("Form", "Size", None, -1)) - self.randCheck.setText(QtWidgets.QApplication.translate("Form", "Randomize", None, -1)) - -from pyqtgraph import PlotWidget +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ScatterPlotSpeedTestTemplate.ui' +# +# Created: Sun Sep 18 19:21:36 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(400, 300) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setObjectName("gridLayout") + self.sizeSpin = QtWidgets.QSpinBox(Form) + self.sizeSpin.setProperty("value", 10) + self.sizeSpin.setObjectName("sizeSpin") + self.gridLayout.addWidget(self.sizeSpin, 1, 1, 1, 1) + self.pixelModeCheck = QtWidgets.QCheckBox(Form) + self.pixelModeCheck.setObjectName("pixelModeCheck") + self.gridLayout.addWidget(self.pixelModeCheck, 1, 3, 1, 1) + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.plot = PlotWidget(Form) + self.plot.setObjectName("plot") + self.gridLayout.addWidget(self.plot, 0, 0, 1, 4) + self.randCheck = QtWidgets.QCheckBox(Form) + self.randCheck.setObjectName("randCheck") + self.gridLayout.addWidget(self.randCheck, 1, 2, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.pixelModeCheck.setText(QtWidgets.QApplication.translate("Form", "pixel mode", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Size", None, -1)) + self.randCheck.setText(QtWidgets.QApplication.translate("Form", "Randomize", None, -1)) + +from pyqtgraph import PlotWidget diff --git a/examples/VideoTemplate_pyside2.py b/examples/VideoTemplate_pyside2.py index a3596714..37b7d2e8 100644 --- a/examples/VideoTemplate_pyside2.py +++ b/examples/VideoTemplate_pyside2.py @@ -1,207 +1,207 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'VideoTemplate.ui' -# -# Created: Sun Sep 18 19:22:41 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_MainWindow(object): - def setupUi(self, MainWindow): - MainWindow.setObjectName("MainWindow") - MainWindow.resize(695, 798) - self.centralwidget = QtWidgets.QWidget(MainWindow) - self.centralwidget.setObjectName("centralwidget") - self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) - self.gridLayout_2.setObjectName("gridLayout_2") - self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget) - self.downsampleCheck.setObjectName("downsampleCheck") - self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2) - self.scaleCheck = QtWidgets.QCheckBox(self.centralwidget) - self.scaleCheck.setObjectName("scaleCheck") - self.gridLayout_2.addWidget(self.scaleCheck, 4, 0, 1, 1) - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setObjectName("gridLayout") - self.rawRadio = QtWidgets.QRadioButton(self.centralwidget) - self.rawRadio.setObjectName("rawRadio") - self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) - self.gfxRadio = QtWidgets.QRadioButton(self.centralwidget) - self.gfxRadio.setChecked(True) - self.gfxRadio.setObjectName("gfxRadio") - self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) - self.stack = QtWidgets.QStackedWidget(self.centralwidget) - self.stack.setObjectName("stack") - self.page = QtWidgets.QWidget() - self.page.setObjectName("page") - self.gridLayout_3 = QtWidgets.QGridLayout(self.page) - self.gridLayout_3.setObjectName("gridLayout_3") - self.graphicsView = GraphicsView(self.page) - self.graphicsView.setObjectName("graphicsView") - self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1) - self.stack.addWidget(self.page) - self.page_2 = QtWidgets.QWidget() - self.page_2.setObjectName("page_2") - self.gridLayout_4 = QtWidgets.QGridLayout(self.page_2) - self.gridLayout_4.setObjectName("gridLayout_4") - self.rawImg = RawImageWidget(self.page_2) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) - self.rawImg.setSizePolicy(sizePolicy) - self.rawImg.setObjectName("rawImg") - self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) - self.stack.addWidget(self.page_2) - self.page_3 = QtWidgets.QWidget() - self.page_3.setObjectName("page_3") - self.gridLayout_5 = QtWidgets.QGridLayout(self.page_3) - self.gridLayout_5.setObjectName("gridLayout_5") - self.rawGLImg = RawImageGLWidget(self.page_3) - self.rawGLImg.setObjectName("rawGLImg") - self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1) - self.stack.addWidget(self.page_3) - self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) - self.rawGLRadio = QtWidgets.QRadioButton(self.centralwidget) - self.rawGLRadio.setObjectName("rawGLRadio") - self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1) - self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) - self.dtypeCombo = QtWidgets.QComboBox(self.centralwidget) - self.dtypeCombo.setObjectName("dtypeCombo") - self.dtypeCombo.addItem("") - self.dtypeCombo.addItem("") - self.dtypeCombo.addItem("") - self.gridLayout_2.addWidget(self.dtypeCombo, 3, 2, 1, 1) - self.label = QtWidgets.QLabel(self.centralwidget) - self.label.setObjectName("label") - self.gridLayout_2.addWidget(self.label, 3, 0, 1, 1) - self.rgbLevelsCheck = QtWidgets.QCheckBox(self.centralwidget) - self.rgbLevelsCheck.setObjectName("rgbLevelsCheck") - self.gridLayout_2.addWidget(self.rgbLevelsCheck, 4, 1, 1, 1) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.minSpin2 = SpinBox(self.centralwidget) - self.minSpin2.setEnabled(False) - self.minSpin2.setObjectName("minSpin2") - self.horizontalLayout_2.addWidget(self.minSpin2) - self.label_3 = QtWidgets.QLabel(self.centralwidget) - self.label_3.setAlignment(QtCore.Qt.AlignCenter) - self.label_3.setObjectName("label_3") - self.horizontalLayout_2.addWidget(self.label_3) - self.maxSpin2 = SpinBox(self.centralwidget) - self.maxSpin2.setEnabled(False) - self.maxSpin2.setObjectName("maxSpin2") - self.horizontalLayout_2.addWidget(self.maxSpin2) - self.gridLayout_2.addLayout(self.horizontalLayout_2, 5, 2, 1, 1) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.minSpin1 = SpinBox(self.centralwidget) - self.minSpin1.setObjectName("minSpin1") - self.horizontalLayout.addWidget(self.minSpin1) - self.label_2 = QtWidgets.QLabel(self.centralwidget) - self.label_2.setAlignment(QtCore.Qt.AlignCenter) - self.label_2.setObjectName("label_2") - self.horizontalLayout.addWidget(self.label_2) - self.maxSpin1 = SpinBox(self.centralwidget) - self.maxSpin1.setObjectName("maxSpin1") - self.horizontalLayout.addWidget(self.maxSpin1) - self.gridLayout_2.addLayout(self.horizontalLayout, 4, 2, 1, 1) - self.horizontalLayout_3 = QtWidgets.QHBoxLayout() - self.horizontalLayout_3.setObjectName("horizontalLayout_3") - self.minSpin3 = SpinBox(self.centralwidget) - self.minSpin3.setEnabled(False) - self.minSpin3.setObjectName("minSpin3") - self.horizontalLayout_3.addWidget(self.minSpin3) - self.label_4 = QtWidgets.QLabel(self.centralwidget) - self.label_4.setAlignment(QtCore.Qt.AlignCenter) - self.label_4.setObjectName("label_4") - self.horizontalLayout_3.addWidget(self.label_4) - self.maxSpin3 = SpinBox(self.centralwidget) - self.maxSpin3.setEnabled(False) - self.maxSpin3.setObjectName("maxSpin3") - self.horizontalLayout_3.addWidget(self.maxSpin3) - self.gridLayout_2.addLayout(self.horizontalLayout_3, 6, 2, 1, 1) - self.lutCheck = QtWidgets.QCheckBox(self.centralwidget) - self.lutCheck.setObjectName("lutCheck") - self.gridLayout_2.addWidget(self.lutCheck, 7, 0, 1, 1) - self.alphaCheck = QtWidgets.QCheckBox(self.centralwidget) - self.alphaCheck.setObjectName("alphaCheck") - self.gridLayout_2.addWidget(self.alphaCheck, 7, 1, 1, 1) - self.gradient = GradientWidget(self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) - self.gradient.setSizePolicy(sizePolicy) - self.gradient.setObjectName("gradient") - self.gridLayout_2.addWidget(self.gradient, 7, 2, 1, 2) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 3, 3, 1, 1) - self.fpsLabel = QtWidgets.QLabel(self.centralwidget) - font = QtGui.QFont() - font.setPointSize(12) - self.fpsLabel.setFont(font) - self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) - self.fpsLabel.setObjectName("fpsLabel") - self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) - self.rgbCheck = QtWidgets.QCheckBox(self.centralwidget) - self.rgbCheck.setObjectName("rgbCheck") - self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) - self.label_5 = QtWidgets.QLabel(self.centralwidget) - self.label_5.setObjectName("label_5") - self.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1) - self.horizontalLayout_4 = QtWidgets.QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.framesSpin = QtWidgets.QSpinBox(self.centralwidget) - self.framesSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) - self.framesSpin.setProperty("value", 10) - self.framesSpin.setObjectName("framesSpin") - self.horizontalLayout_4.addWidget(self.framesSpin) - self.widthSpin = QtWidgets.QSpinBox(self.centralwidget) - self.widthSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus) - self.widthSpin.setMaximum(10000) - self.widthSpin.setProperty("value", 512) - self.widthSpin.setObjectName("widthSpin") - self.horizontalLayout_4.addWidget(self.widthSpin) - self.heightSpin = QtWidgets.QSpinBox(self.centralwidget) - self.heightSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) - self.heightSpin.setMaximum(10000) - self.heightSpin.setProperty("value", 512) - self.heightSpin.setObjectName("heightSpin") - self.horizontalLayout_4.addWidget(self.heightSpin) - self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2) - self.sizeLabel = QtWidgets.QLabel(self.centralwidget) - self.sizeLabel.setText("") - self.sizeLabel.setObjectName("sizeLabel") - self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1) - MainWindow.setCentralWidget(self.centralwidget) - - self.retranslateUi(MainWindow) - self.stack.setCurrentIndex(2) - QtCore.QMetaObject.connectSlotsByName(MainWindow) - - def retranslateUi(self, MainWindow): - MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1)) - self.downsampleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Auto downsample", None, -1)) - self.scaleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Scale Data", None, -1)) - self.rawRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawImageWidget", None, -1)) - self.gfxRadio.setText(QtWidgets.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, -1)) - self.rawGLRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawGLImageWidget", None, -1)) - self.dtypeCombo.setItemText(0, QtWidgets.QApplication.translate("MainWindow", "uint8", None, -1)) - self.dtypeCombo.setItemText(1, QtWidgets.QApplication.translate("MainWindow", "uint16", None, -1)) - self.dtypeCombo.setItemText(2, QtWidgets.QApplication.translate("MainWindow", "float", None, -1)) - self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Data type", None, -1)) - self.rgbLevelsCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) - self.label_3.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) - self.label_2.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) - self.label_4.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) - self.lutCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Use Lookup Table", None, -1)) - self.alphaCheck.setText(QtWidgets.QApplication.translate("MainWindow", "alpha", None, -1)) - self.fpsLabel.setText(QtWidgets.QApplication.translate("MainWindow", "FPS", None, -1)) - self.rgbCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) - self.label_5.setText(QtWidgets.QApplication.translate("MainWindow", "Image size", None, -1)) - -from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget -from pyqtgraph import GradientWidget, SpinBox, GraphicsView +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'VideoTemplate.ui' +# +# Created: Sun Sep 18 19:22:41 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(695, 798) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.centralwidget) + self.gridLayout_2.setObjectName("gridLayout_2") + self.downsampleCheck = QtWidgets.QCheckBox(self.centralwidget) + self.downsampleCheck.setObjectName("downsampleCheck") + self.gridLayout_2.addWidget(self.downsampleCheck, 8, 0, 1, 2) + self.scaleCheck = QtWidgets.QCheckBox(self.centralwidget) + self.scaleCheck.setObjectName("scaleCheck") + self.gridLayout_2.addWidget(self.scaleCheck, 4, 0, 1, 1) + self.gridLayout = QtWidgets.QGridLayout() + self.gridLayout.setObjectName("gridLayout") + self.rawRadio = QtWidgets.QRadioButton(self.centralwidget) + self.rawRadio.setObjectName("rawRadio") + self.gridLayout.addWidget(self.rawRadio, 3, 0, 1, 1) + self.gfxRadio = QtWidgets.QRadioButton(self.centralwidget) + self.gfxRadio.setChecked(True) + self.gfxRadio.setObjectName("gfxRadio") + self.gridLayout.addWidget(self.gfxRadio, 2, 0, 1, 1) + self.stack = QtWidgets.QStackedWidget(self.centralwidget) + self.stack.setObjectName("stack") + self.page = QtWidgets.QWidget() + self.page.setObjectName("page") + self.gridLayout_3 = QtWidgets.QGridLayout(self.page) + self.gridLayout_3.setObjectName("gridLayout_3") + self.graphicsView = GraphicsView(self.page) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 1) + self.stack.addWidget(self.page) + self.page_2 = QtWidgets.QWidget() + self.page_2.setObjectName("page_2") + self.gridLayout_4 = QtWidgets.QGridLayout(self.page_2) + self.gridLayout_4.setObjectName("gridLayout_4") + self.rawImg = RawImageWidget(self.page_2) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.rawImg.sizePolicy().hasHeightForWidth()) + self.rawImg.setSizePolicy(sizePolicy) + self.rawImg.setObjectName("rawImg") + self.gridLayout_4.addWidget(self.rawImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_2) + self.page_3 = QtWidgets.QWidget() + self.page_3.setObjectName("page_3") + self.gridLayout_5 = QtWidgets.QGridLayout(self.page_3) + self.gridLayout_5.setObjectName("gridLayout_5") + self.rawGLImg = RawImageGLWidget(self.page_3) + self.rawGLImg.setObjectName("rawGLImg") + self.gridLayout_5.addWidget(self.rawGLImg, 0, 0, 1, 1) + self.stack.addWidget(self.page_3) + self.gridLayout.addWidget(self.stack, 0, 0, 1, 1) + self.rawGLRadio = QtWidgets.QRadioButton(self.centralwidget) + self.rawGLRadio.setObjectName("rawGLRadio") + self.gridLayout.addWidget(self.rawGLRadio, 4, 0, 1, 1) + self.gridLayout_2.addLayout(self.gridLayout, 1, 0, 1, 4) + self.dtypeCombo = QtWidgets.QComboBox(self.centralwidget) + self.dtypeCombo.setObjectName("dtypeCombo") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.dtypeCombo.addItem("") + self.gridLayout_2.addWidget(self.dtypeCombo, 3, 2, 1, 1) + self.label = QtWidgets.QLabel(self.centralwidget) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 3, 0, 1, 1) + self.rgbLevelsCheck = QtWidgets.QCheckBox(self.centralwidget) + self.rgbLevelsCheck.setObjectName("rgbLevelsCheck") + self.gridLayout_2.addWidget(self.rgbLevelsCheck, 4, 1, 1, 1) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.minSpin2 = SpinBox(self.centralwidget) + self.minSpin2.setEnabled(False) + self.minSpin2.setObjectName("minSpin2") + self.horizontalLayout_2.addWidget(self.minSpin2) + self.label_3 = QtWidgets.QLabel(self.centralwidget) + self.label_3.setAlignment(QtCore.Qt.AlignCenter) + self.label_3.setObjectName("label_3") + self.horizontalLayout_2.addWidget(self.label_3) + self.maxSpin2 = SpinBox(self.centralwidget) + self.maxSpin2.setEnabled(False) + self.maxSpin2.setObjectName("maxSpin2") + self.horizontalLayout_2.addWidget(self.maxSpin2) + self.gridLayout_2.addLayout(self.horizontalLayout_2, 5, 2, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.minSpin1 = SpinBox(self.centralwidget) + self.minSpin1.setObjectName("minSpin1") + self.horizontalLayout.addWidget(self.minSpin1) + self.label_2 = QtWidgets.QLabel(self.centralwidget) + self.label_2.setAlignment(QtCore.Qt.AlignCenter) + self.label_2.setObjectName("label_2") + self.horizontalLayout.addWidget(self.label_2) + self.maxSpin1 = SpinBox(self.centralwidget) + self.maxSpin1.setObjectName("maxSpin1") + self.horizontalLayout.addWidget(self.maxSpin1) + self.gridLayout_2.addLayout(self.horizontalLayout, 4, 2, 1, 1) + self.horizontalLayout_3 = QtWidgets.QHBoxLayout() + self.horizontalLayout_3.setObjectName("horizontalLayout_3") + self.minSpin3 = SpinBox(self.centralwidget) + self.minSpin3.setEnabled(False) + self.minSpin3.setObjectName("minSpin3") + self.horizontalLayout_3.addWidget(self.minSpin3) + self.label_4 = QtWidgets.QLabel(self.centralwidget) + self.label_4.setAlignment(QtCore.Qt.AlignCenter) + self.label_4.setObjectName("label_4") + self.horizontalLayout_3.addWidget(self.label_4) + self.maxSpin3 = SpinBox(self.centralwidget) + self.maxSpin3.setEnabled(False) + self.maxSpin3.setObjectName("maxSpin3") + self.horizontalLayout_3.addWidget(self.maxSpin3) + self.gridLayout_2.addLayout(self.horizontalLayout_3, 6, 2, 1, 1) + self.lutCheck = QtWidgets.QCheckBox(self.centralwidget) + self.lutCheck.setObjectName("lutCheck") + self.gridLayout_2.addWidget(self.lutCheck, 7, 0, 1, 1) + self.alphaCheck = QtWidgets.QCheckBox(self.centralwidget) + self.alphaCheck.setObjectName("alphaCheck") + self.gridLayout_2.addWidget(self.alphaCheck, 7, 1, 1, 1) + self.gradient = GradientWidget(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.gradient.sizePolicy().hasHeightForWidth()) + self.gradient.setSizePolicy(sizePolicy) + self.gradient.setObjectName("gradient") + self.gridLayout_2.addWidget(self.gradient, 7, 2, 1, 2) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 3, 3, 1, 1) + self.fpsLabel = QtWidgets.QLabel(self.centralwidget) + font = QtGui.QFont() + font.setPointSize(12) + self.fpsLabel.setFont(font) + self.fpsLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fpsLabel.setObjectName("fpsLabel") + self.gridLayout_2.addWidget(self.fpsLabel, 0, 0, 1, 4) + self.rgbCheck = QtWidgets.QCheckBox(self.centralwidget) + self.rgbCheck.setObjectName("rgbCheck") + self.gridLayout_2.addWidget(self.rgbCheck, 3, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(self.centralwidget) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 2, 0, 1, 1) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.framesSpin = QtWidgets.QSpinBox(self.centralwidget) + self.framesSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.framesSpin.setProperty("value", 10) + self.framesSpin.setObjectName("framesSpin") + self.horizontalLayout_4.addWidget(self.framesSpin) + self.widthSpin = QtWidgets.QSpinBox(self.centralwidget) + self.widthSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.PlusMinus) + self.widthSpin.setMaximum(10000) + self.widthSpin.setProperty("value", 512) + self.widthSpin.setObjectName("widthSpin") + self.horizontalLayout_4.addWidget(self.widthSpin) + self.heightSpin = QtWidgets.QSpinBox(self.centralwidget) + self.heightSpin.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons) + self.heightSpin.setMaximum(10000) + self.heightSpin.setProperty("value", 512) + self.heightSpin.setObjectName("heightSpin") + self.horizontalLayout_4.addWidget(self.heightSpin) + self.gridLayout_2.addLayout(self.horizontalLayout_4, 2, 1, 1, 2) + self.sizeLabel = QtWidgets.QLabel(self.centralwidget) + self.sizeLabel.setText("") + self.sizeLabel.setObjectName("sizeLabel") + self.gridLayout_2.addWidget(self.sizeLabel, 2, 3, 1, 1) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + self.stack.setCurrentIndex(2) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + MainWindow.setWindowTitle(QtWidgets.QApplication.translate("MainWindow", "MainWindow", None, -1)) + self.downsampleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Auto downsample", None, -1)) + self.scaleCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Scale Data", None, -1)) + self.rawRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawImageWidget", None, -1)) + self.gfxRadio.setText(QtWidgets.QApplication.translate("MainWindow", "GraphicsView + ImageItem", None, -1)) + self.rawGLRadio.setText(QtWidgets.QApplication.translate("MainWindow", "RawGLImageWidget", None, -1)) + self.dtypeCombo.setItemText(0, QtWidgets.QApplication.translate("MainWindow", "uint8", None, -1)) + self.dtypeCombo.setItemText(1, QtWidgets.QApplication.translate("MainWindow", "uint16", None, -1)) + self.dtypeCombo.setItemText(2, QtWidgets.QApplication.translate("MainWindow", "float", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("MainWindow", "Data type", None, -1)) + self.rgbLevelsCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) + self.label_3.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.label_2.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.label_4.setText(QtWidgets.QApplication.translate("MainWindow", "<--->", None, -1)) + self.lutCheck.setText(QtWidgets.QApplication.translate("MainWindow", "Use Lookup Table", None, -1)) + self.alphaCheck.setText(QtWidgets.QApplication.translate("MainWindow", "alpha", None, -1)) + self.fpsLabel.setText(QtWidgets.QApplication.translate("MainWindow", "FPS", None, -1)) + self.rgbCheck.setText(QtWidgets.QApplication.translate("MainWindow", "RGB", None, -1)) + self.label_5.setText(QtWidgets.QApplication.translate("MainWindow", "Image size", None, -1)) + +from pyqtgraph.widgets.RawImageWidget import RawImageGLWidget, RawImageWidget +from pyqtgraph import GradientWidget, SpinBox, GraphicsView diff --git a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py index 24bfd37d..6c0fec47 100644 --- a/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py +++ b/pyqtgraph/GraphicsScene/exportDialogTemplate_pyside2.py @@ -1,63 +1,63 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'exportDialogTemplate.ui' -# -# Created: Sun Sep 18 19:19:58 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(241, 367) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.label = QtWidgets.QLabel(Form) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 0, 1, 3) - self.itemTree = QtWidgets.QTreeWidget(Form) - self.itemTree.setObjectName("itemTree") - self.itemTree.headerItem().setText(0, "1") - self.itemTree.header().setVisible(False) - self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) - self.label_2 = QtWidgets.QLabel(Form) - self.label_2.setObjectName("label_2") - self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) - self.formatList = QtWidgets.QListWidget(Form) - self.formatList.setObjectName("formatList") - self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) - self.exportBtn = QtWidgets.QPushButton(Form) - self.exportBtn.setObjectName("exportBtn") - self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) - self.closeBtn = QtWidgets.QPushButton(Form) - self.closeBtn.setObjectName("closeBtn") - self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) - self.paramTree = ParameterTree(Form) - self.paramTree.setObjectName("paramTree") - self.paramTree.headerItem().setText(0, "1") - self.paramTree.header().setVisible(False) - self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) - self.label_3 = QtWidgets.QLabel(Form) - self.label_3.setObjectName("label_3") - self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) - self.copyBtn = QtWidgets.QPushButton(Form) - self.copyBtn.setObjectName("copyBtn") - self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Export", None, -1)) - self.label.setText(QtWidgets.QApplication.translate("Form", "Item to export:", None, -1)) - self.label_2.setText(QtWidgets.QApplication.translate("Form", "Export format", None, -1)) - self.exportBtn.setText(QtWidgets.QApplication.translate("Form", "Export", None, -1)) - self.closeBtn.setText(QtWidgets.QApplication.translate("Form", "Close", None, -1)) - self.label_3.setText(QtWidgets.QApplication.translate("Form", "Export options", None, -1)) - self.copyBtn.setText(QtWidgets.QApplication.translate("Form", "Copy", None, -1)) - -from ..parametertree import ParameterTree +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'exportDialogTemplate.ui' +# +# Created: Sun Sep 18 19:19:58 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(241, 367) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 0, 0, 1, 3) + self.itemTree = QtWidgets.QTreeWidget(Form) + self.itemTree.setObjectName("itemTree") + self.itemTree.headerItem().setText(0, "1") + self.itemTree.header().setVisible(False) + self.gridLayout.addWidget(self.itemTree, 1, 0, 1, 3) + self.label_2 = QtWidgets.QLabel(Form) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 2, 0, 1, 3) + self.formatList = QtWidgets.QListWidget(Form) + self.formatList.setObjectName("formatList") + self.gridLayout.addWidget(self.formatList, 3, 0, 1, 3) + self.exportBtn = QtWidgets.QPushButton(Form) + self.exportBtn.setObjectName("exportBtn") + self.gridLayout.addWidget(self.exportBtn, 6, 1, 1, 1) + self.closeBtn = QtWidgets.QPushButton(Form) + self.closeBtn.setObjectName("closeBtn") + self.gridLayout.addWidget(self.closeBtn, 6, 2, 1, 1) + self.paramTree = ParameterTree(Form) + self.paramTree.setObjectName("paramTree") + self.paramTree.headerItem().setText(0, "1") + self.paramTree.header().setVisible(False) + self.gridLayout.addWidget(self.paramTree, 5, 0, 1, 3) + self.label_3 = QtWidgets.QLabel(Form) + self.label_3.setObjectName("label_3") + self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3) + self.copyBtn = QtWidgets.QPushButton(Form) + self.copyBtn.setObjectName("copyBtn") + self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Export", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Item to export:", None, -1)) + self.label_2.setText(QtWidgets.QApplication.translate("Form", "Export format", None, -1)) + self.exportBtn.setText(QtWidgets.QApplication.translate("Form", "Export", None, -1)) + self.closeBtn.setText(QtWidgets.QApplication.translate("Form", "Close", None, -1)) + self.label_3.setText(QtWidgets.QApplication.translate("Form", "Export options", None, -1)) + self.copyBtn.setText(QtWidgets.QApplication.translate("Form", "Copy", None, -1)) + +from ..parametertree import ParameterTree diff --git a/pyqtgraph/Transform3D.py b/pyqtgraph/Transform3D.py index 56283351..326ccef2 100644 --- a/pyqtgraph/Transform3D.py +++ b/pyqtgraph/Transform3D.py @@ -14,6 +14,7 @@ class Transform3D(QtGui.QMatrix4x4): args = [x for y in args[0] for x in y] if len(args) != 16: raise TypeError("Single argument to Transform3D must have 16 elements.") + print(args) QtGui.QMatrix4x4.__init__(self, *args) def matrix(self, nd=3): @@ -45,4 +46,4 @@ class Transform3D(QtGui.QMatrix4x4): def inverted(self): inv, b = QtGui.QMatrix4x4.inverted(self) - return Transform3D(inv), b \ No newline at end of file + return Transform3D(inv), b diff --git a/pyqtgraph/canvas/CanvasTemplate_pyside2.py b/pyqtgraph/canvas/CanvasTemplate_pyside2.py index 884f0480..de9c6322 100644 --- a/pyqtgraph/canvas/CanvasTemplate_pyside2.py +++ b/pyqtgraph/canvas/CanvasTemplate_pyside2.py @@ -1,87 +1,87 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'CanvasTemplate.ui' -# -# Created: Sun Sep 18 19:18:22 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(490, 414) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Horizontal) - self.splitter.setObjectName("splitter") - self.view = GraphicsView(self.splitter) - self.view.setObjectName("view") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) - self.autoRangeBtn.setSizePolicy(sizePolicy) - self.autoRangeBtn.setObjectName("autoRangeBtn") - self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) - self.redirectCheck.setObjectName("redirectCheck") - self.horizontalLayout.addWidget(self.redirectCheck) - self.redirectCombo = CanvasCombo(self.layoutWidget) - self.redirectCombo.setObjectName("redirectCombo") - self.horizontalLayout.addWidget(self.redirectCombo) - self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) - self.itemList = TreeWidget(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(100) - sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) - self.itemList.setSizePolicy(sizePolicy) - self.itemList.setHeaderHidden(True) - self.itemList.setObjectName("itemList") - self.itemList.headerItem().setText(0, "1") - self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) - self.ctrlLayout = QtWidgets.QGridLayout() - self.ctrlLayout.setSpacing(0) - self.ctrlLayout.setObjectName("ctrlLayout") - self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) - self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) - self.resetTransformsBtn.setObjectName("resetTransformsBtn") - self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) - self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") - self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) - self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") - self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.autoRangeBtn.setText(QtWidgets.QApplication.translate("Form", "Auto Range", None, -1)) - self.redirectCheck.setToolTip(QtWidgets.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, -1)) - self.redirectCheck.setText(QtWidgets.QApplication.translate("Form", "Redirect", None, -1)) - self.resetTransformsBtn.setText(QtWidgets.QApplication.translate("Form", "Reset Transforms", None, -1)) - self.mirrorSelectionBtn.setText(QtWidgets.QApplication.translate("Form", "Mirror Selection", None, -1)) - self.reflectSelectionBtn.setText(QtWidgets.QApplication.translate("Form", "MirrorXY", None, -1)) - -from ..widgets.TreeWidget import TreeWidget -from CanvasManager import CanvasCombo -from ..widgets.GraphicsView import GraphicsView +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'CanvasTemplate.ui' +# +# Created: Sun Sep 18 19:18:22 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(490, 414) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Horizontal) + self.splitter.setObjectName("splitter") + self.view = GraphicsView(self.splitter) + self.view.setObjectName("view") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) + self.autoRangeBtn.setSizePolicy(sizePolicy) + self.autoRangeBtn.setObjectName("autoRangeBtn") + self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) + self.redirectCheck.setObjectName("redirectCheck") + self.horizontalLayout.addWidget(self.redirectCheck) + self.redirectCombo = CanvasCombo(self.layoutWidget) + self.redirectCombo.setObjectName("redirectCombo") + self.horizontalLayout.addWidget(self.redirectCombo) + self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) + self.itemList = TreeWidget(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(100) + sizePolicy.setHeightForWidth(self.itemList.sizePolicy().hasHeightForWidth()) + self.itemList.setSizePolicy(sizePolicy) + self.itemList.setHeaderHidden(True) + self.itemList.setObjectName("itemList") + self.itemList.headerItem().setText(0, "1") + self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) + self.ctrlLayout = QtWidgets.QGridLayout() + self.ctrlLayout.setSpacing(0) + self.ctrlLayout.setObjectName("ctrlLayout") + self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) + self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget) + self.resetTransformsBtn.setObjectName("resetTransformsBtn") + self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) + self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") + self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) + self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") + self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.autoRangeBtn.setText(QtWidgets.QApplication.translate("Form", "Auto Range", None, -1)) + self.redirectCheck.setToolTip(QtWidgets.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, -1)) + self.redirectCheck.setText(QtWidgets.QApplication.translate("Form", "Redirect", None, -1)) + self.resetTransformsBtn.setText(QtWidgets.QApplication.translate("Form", "Reset Transforms", None, -1)) + self.mirrorSelectionBtn.setText(QtWidgets.QApplication.translate("Form", "Mirror Selection", None, -1)) + self.reflectSelectionBtn.setText(QtWidgets.QApplication.translate("Form", "MirrorXY", None, -1)) + +from ..widgets.TreeWidget import TreeWidget +from CanvasManager import CanvasCombo +from ..widgets.GraphicsView import GraphicsView diff --git a/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py b/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py index 06883270..e05ceb14 100644 --- a/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py +++ b/pyqtgraph/canvas/TransformGuiTemplate_pyside2.py @@ -1,55 +1,55 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'TransformGuiTemplate.ui' -# -# Created: Sun Sep 18 19:18:41 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(224, 117) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) - Form.setSizePolicy(sizePolicy) - self.verticalLayout = QtWidgets.QVBoxLayout(Form) - self.verticalLayout.setSpacing(1) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.translateLabel = QtWidgets.QLabel(Form) - self.translateLabel.setObjectName("translateLabel") - self.verticalLayout.addWidget(self.translateLabel) - self.rotateLabel = QtWidgets.QLabel(Form) - self.rotateLabel.setObjectName("rotateLabel") - self.verticalLayout.addWidget(self.rotateLabel) - self.scaleLabel = QtWidgets.QLabel(Form) - self.scaleLabel.setObjectName("scaleLabel") - self.verticalLayout.addWidget(self.scaleLabel) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.mirrorImageBtn = QtWidgets.QPushButton(Form) - self.mirrorImageBtn.setToolTip("") - self.mirrorImageBtn.setObjectName("mirrorImageBtn") - self.horizontalLayout.addWidget(self.mirrorImageBtn) - self.reflectImageBtn = QtWidgets.QPushButton(Form) - self.reflectImageBtn.setObjectName("reflectImageBtn") - self.horizontalLayout.addWidget(self.reflectImageBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.translateLabel.setText(QtWidgets.QApplication.translate("Form", "Translate:", None, -1)) - self.rotateLabel.setText(QtWidgets.QApplication.translate("Form", "Rotate:", None, -1)) - self.scaleLabel.setText(QtWidgets.QApplication.translate("Form", "Scale:", None, -1)) - self.mirrorImageBtn.setText(QtWidgets.QApplication.translate("Form", "Mirror", None, -1)) - self.reflectImageBtn.setText(QtWidgets.QApplication.translate("Form", "Reflect", None, -1)) - +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'TransformGuiTemplate.ui' +# +# Created: Sun Sep 18 19:18:41 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(224, 117) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(Form.sizePolicy().hasHeightForWidth()) + Form.setSizePolicy(sizePolicy) + self.verticalLayout = QtWidgets.QVBoxLayout(Form) + self.verticalLayout.setSpacing(1) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.translateLabel = QtWidgets.QLabel(Form) + self.translateLabel.setObjectName("translateLabel") + self.verticalLayout.addWidget(self.translateLabel) + self.rotateLabel = QtWidgets.QLabel(Form) + self.rotateLabel.setObjectName("rotateLabel") + self.verticalLayout.addWidget(self.rotateLabel) + self.scaleLabel = QtWidgets.QLabel(Form) + self.scaleLabel.setObjectName("scaleLabel") + self.verticalLayout.addWidget(self.scaleLabel) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.mirrorImageBtn = QtWidgets.QPushButton(Form) + self.mirrorImageBtn.setToolTip("") + self.mirrorImageBtn.setObjectName("mirrorImageBtn") + self.horizontalLayout.addWidget(self.mirrorImageBtn) + self.reflectImageBtn = QtWidgets.QPushButton(Form) + self.reflectImageBtn.setObjectName("reflectImageBtn") + self.horizontalLayout.addWidget(self.reflectImageBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.translateLabel.setText(QtWidgets.QApplication.translate("Form", "Translate:", None, -1)) + self.rotateLabel.setText(QtWidgets.QApplication.translate("Form", "Rotate:", None, -1)) + self.scaleLabel.setText(QtWidgets.QApplication.translate("Form", "Scale:", None, -1)) + self.mirrorImageBtn.setText(QtWidgets.QApplication.translate("Form", "Mirror", None, -1)) + self.reflectImageBtn.setText(QtWidgets.QApplication.translate("Form", "Reflect", None, -1)) + diff --git a/pyqtgraph/console/template_pyside2.py b/pyqtgraph/console/template_pyside2.py index 361e96f2..c8662c74 100644 --- a/pyqtgraph/console/template_pyside2.py +++ b/pyqtgraph/console/template_pyside2.py @@ -1,113 +1,113 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'template.ui' -# -# Created: Sun Sep 18 19:19:10 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(694, 497) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Vertical) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) - self.verticalLayout.setContentsMargins(0, 0, 0, 0) - self.verticalLayout.setObjectName("verticalLayout") - self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) - font = QtGui.QFont() - font.setFamily("Monospace") - self.output.setFont(font) - self.output.setReadOnly(True) - self.output.setObjectName("output") - self.verticalLayout.addWidget(self.output) - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.input = CmdInput(self.layoutWidget) - self.input.setObjectName("input") - self.horizontalLayout.addWidget(self.input) - self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) - self.historyBtn.setCheckable(True) - self.historyBtn.setObjectName("historyBtn") - self.horizontalLayout.addWidget(self.historyBtn) - self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) - self.exceptionBtn.setCheckable(True) - self.exceptionBtn.setObjectName("exceptionBtn") - self.horizontalLayout.addWidget(self.exceptionBtn) - self.verticalLayout.addLayout(self.horizontalLayout) - self.historyList = QtWidgets.QListWidget(self.splitter) - font = QtGui.QFont() - font.setFamily("Monospace") - self.historyList.setFont(font) - self.historyList.setObjectName("historyList") - self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) - self.exceptionGroup.setObjectName("exceptionGroup") - self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.clearExceptionBtn.setEnabled(False) - self.clearExceptionBtn.setObjectName("clearExceptionBtn") - self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) - self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchAllExceptionsBtn.setCheckable(True) - self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") - self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) - self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) - self.catchNextExceptionBtn.setCheckable(True) - self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") - self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) - self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.onlyUncaughtCheck.setChecked(True) - self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") - self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) - self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) - self.exceptionStackList.setAlternatingRowColors(True) - self.exceptionStackList.setObjectName("exceptionStackList") - self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) - self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) - self.runSelectedFrameCheck.setChecked(True) - self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") - self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) - self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) - self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") - self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) - self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) - self.label = QtWidgets.QLabel(self.exceptionGroup) - self.label.setObjectName("label") - self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) - self.filterText = QtWidgets.QLineEdit(self.exceptionGroup) - self.filterText.setObjectName("filterText") - self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) - self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Console", None, -1)) - self.historyBtn.setText(QtWidgets.QApplication.translate("Form", "History..", None, -1)) - self.exceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Exceptions..", None, -1)) - self.exceptionGroup.setTitle(QtWidgets.QApplication.translate("Form", "Exception Handling", None, -1)) - self.clearExceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Clear Exception", None, -1)) - self.catchAllExceptionsBtn.setText(QtWidgets.QApplication.translate("Form", "Show All Exceptions", None, -1)) - self.catchNextExceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Show Next Exception", None, -1)) - self.onlyUncaughtCheck.setText(QtWidgets.QApplication.translate("Form", "Only Uncaught Exceptions", None, -1)) - self.runSelectedFrameCheck.setText(QtWidgets.QApplication.translate("Form", "Run commands in selected stack frame", None, -1)) - self.exceptionInfoLabel.setText(QtWidgets.QApplication.translate("Form", "Exception Info", None, -1)) - self.label.setText(QtWidgets.QApplication.translate("Form", "Filter (regex):", None, -1)) - -from .CmdInput import CmdInput +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'template.ui' +# +# Created: Sun Sep 18 19:19:10 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(694, 497) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) + self.verticalLayout.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setObjectName("verticalLayout") + self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) + font = QtGui.QFont() + font.setFamily("Monospace") + self.output.setFont(font) + self.output.setReadOnly(True) + self.output.setObjectName("output") + self.verticalLayout.addWidget(self.output) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.input = CmdInput(self.layoutWidget) + self.input.setObjectName("input") + self.horizontalLayout.addWidget(self.input) + self.historyBtn = QtWidgets.QPushButton(self.layoutWidget) + self.historyBtn.setCheckable(True) + self.historyBtn.setObjectName("historyBtn") + self.horizontalLayout.addWidget(self.historyBtn) + self.exceptionBtn = QtWidgets.QPushButton(self.layoutWidget) + self.exceptionBtn.setCheckable(True) + self.exceptionBtn.setObjectName("exceptionBtn") + self.horizontalLayout.addWidget(self.exceptionBtn) + self.verticalLayout.addLayout(self.horizontalLayout) + self.historyList = QtWidgets.QListWidget(self.splitter) + font = QtGui.QFont() + font.setFamily("Monospace") + self.historyList.setFont(font) + self.historyList.setObjectName("historyList") + self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) + self.exceptionGroup.setObjectName("exceptionGroup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.clearExceptionBtn.setEnabled(False) + self.clearExceptionBtn.setObjectName("clearExceptionBtn") + self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1) + self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.catchAllExceptionsBtn.setCheckable(True) + self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") + self.gridLayout_2.addWidget(self.catchAllExceptionsBtn, 0, 1, 1, 1) + self.catchNextExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup) + self.catchNextExceptionBtn.setCheckable(True) + self.catchNextExceptionBtn.setObjectName("catchNextExceptionBtn") + self.gridLayout_2.addWidget(self.catchNextExceptionBtn, 0, 0, 1, 1) + self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) + self.onlyUncaughtCheck.setChecked(True) + self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") + self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1) + self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) + self.exceptionStackList.setAlternatingRowColors(True) + self.exceptionStackList.setObjectName("exceptionStackList") + self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7) + self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) + self.runSelectedFrameCheck.setChecked(True) + self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") + self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) + self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) + self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") + self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) + spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) + self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1) + self.label = QtWidgets.QLabel(self.exceptionGroup) + self.label.setObjectName("label") + self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1) + self.filterText = QtWidgets.QLineEdit(self.exceptionGroup) + self.filterText.setObjectName("filterText") + self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1) + self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Console", None, -1)) + self.historyBtn.setText(QtWidgets.QApplication.translate("Form", "History..", None, -1)) + self.exceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Exceptions..", None, -1)) + self.exceptionGroup.setTitle(QtWidgets.QApplication.translate("Form", "Exception Handling", None, -1)) + self.clearExceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Clear Exception", None, -1)) + self.catchAllExceptionsBtn.setText(QtWidgets.QApplication.translate("Form", "Show All Exceptions", None, -1)) + self.catchNextExceptionBtn.setText(QtWidgets.QApplication.translate("Form", "Show Next Exception", None, -1)) + self.onlyUncaughtCheck.setText(QtWidgets.QApplication.translate("Form", "Only Uncaught Exceptions", None, -1)) + self.runSelectedFrameCheck.setText(QtWidgets.QApplication.translate("Form", "Run commands in selected stack frame", None, -1)) + self.exceptionInfoLabel.setText(QtWidgets.QApplication.translate("Form", "Exception Info", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Filter (regex):", None, -1)) + +from .CmdInput import CmdInput diff --git a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py index 86fbf5d9..2e7a7a0b 100644 --- a/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py +++ b/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyside2.py @@ -1,66 +1,66 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'FlowchartCtrlTemplate.ui' -# -# Created: Sun Sep 18 19:16:46 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(217, 499) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setVerticalSpacing(0) - self.gridLayout.setObjectName("gridLayout") - self.loadBtn = QtWidgets.QPushButton(Form) - self.loadBtn.setObjectName("loadBtn") - self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) - self.saveBtn = FeedbackButton(Form) - self.saveBtn.setObjectName("saveBtn") - self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) - self.saveAsBtn = FeedbackButton(Form) - self.saveAsBtn.setObjectName("saveAsBtn") - self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) - self.reloadBtn = FeedbackButton(Form) - self.reloadBtn.setCheckable(False) - self.reloadBtn.setFlat(False) - self.reloadBtn.setObjectName("reloadBtn") - self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) - self.showChartBtn = QtWidgets.QPushButton(Form) - self.showChartBtn.setCheckable(True) - self.showChartBtn.setObjectName("showChartBtn") - self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) - self.ctrlList = TreeWidget(Form) - self.ctrlList.setObjectName("ctrlList") - self.ctrlList.headerItem().setText(0, "1") - self.ctrlList.header().setVisible(False) - self.ctrlList.header().setStretchLastSection(False) - self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) - self.fileNameLabel = QtWidgets.QLabel(Form) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.fileNameLabel.setFont(font) - self.fileNameLabel.setText("") - self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) - self.fileNameLabel.setObjectName("fileNameLabel") - self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.loadBtn.setText(QtWidgets.QApplication.translate("Form", "Load..", None, -1)) - self.saveBtn.setText(QtWidgets.QApplication.translate("Form", "Save", None, -1)) - self.saveAsBtn.setText(QtWidgets.QApplication.translate("Form", "As..", None, -1)) - self.reloadBtn.setText(QtWidgets.QApplication.translate("Form", "Reload Libs", None, -1)) - self.showChartBtn.setText(QtWidgets.QApplication.translate("Form", "Flowchart", None, -1)) - -from ..widgets.FeedbackButton import FeedbackButton -from ..widgets.TreeWidget import TreeWidget +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'FlowchartCtrlTemplate.ui' +# +# Created: Sun Sep 18 19:16:46 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(217, 499) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setVerticalSpacing(0) + self.gridLayout.setObjectName("gridLayout") + self.loadBtn = QtWidgets.QPushButton(Form) + self.loadBtn.setObjectName("loadBtn") + self.gridLayout.addWidget(self.loadBtn, 1, 0, 1, 1) + self.saveBtn = FeedbackButton(Form) + self.saveBtn.setObjectName("saveBtn") + self.gridLayout.addWidget(self.saveBtn, 1, 1, 1, 2) + self.saveAsBtn = FeedbackButton(Form) + self.saveAsBtn.setObjectName("saveAsBtn") + self.gridLayout.addWidget(self.saveAsBtn, 1, 3, 1, 1) + self.reloadBtn = FeedbackButton(Form) + self.reloadBtn.setCheckable(False) + self.reloadBtn.setFlat(False) + self.reloadBtn.setObjectName("reloadBtn") + self.gridLayout.addWidget(self.reloadBtn, 4, 0, 1, 2) + self.showChartBtn = QtWidgets.QPushButton(Form) + self.showChartBtn.setCheckable(True) + self.showChartBtn.setObjectName("showChartBtn") + self.gridLayout.addWidget(self.showChartBtn, 4, 2, 1, 2) + self.ctrlList = TreeWidget(Form) + self.ctrlList.setObjectName("ctrlList") + self.ctrlList.headerItem().setText(0, "1") + self.ctrlList.header().setVisible(False) + self.ctrlList.header().setStretchLastSection(False) + self.gridLayout.addWidget(self.ctrlList, 3, 0, 1, 4) + self.fileNameLabel = QtWidgets.QLabel(Form) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.fileNameLabel.setFont(font) + self.fileNameLabel.setText("") + self.fileNameLabel.setAlignment(QtCore.Qt.AlignCenter) + self.fileNameLabel.setObjectName("fileNameLabel") + self.gridLayout.addWidget(self.fileNameLabel, 0, 1, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.loadBtn.setText(QtWidgets.QApplication.translate("Form", "Load..", None, -1)) + self.saveBtn.setText(QtWidgets.QApplication.translate("Form", "Save", None, -1)) + self.saveAsBtn.setText(QtWidgets.QApplication.translate("Form", "As..", None, -1)) + self.reloadBtn.setText(QtWidgets.QApplication.translate("Form", "Reload Libs", None, -1)) + self.showChartBtn.setText(QtWidgets.QApplication.translate("Form", "Flowchart", None, -1)) + +from ..widgets.FeedbackButton import FeedbackButton +from ..widgets.TreeWidget import TreeWidget diff --git a/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py b/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py index 58fd6775..2bca5f82 100644 --- a/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py +++ b/pyqtgraph/flowchart/FlowchartTemplate_pyside2.py @@ -1,54 +1,54 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'FlowchartTemplate.ui' -# -# Created: Sun Sep 18 19:16:03 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(529, 329) - self.selInfoWidget = QtWidgets.QWidget(Form) - self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) - self.selInfoWidget.setObjectName("selInfoWidget") - self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) - self.selDescLabel.setText("") - self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) - self.selDescLabel.setWordWrap(True) - self.selDescLabel.setObjectName("selDescLabel") - self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) - self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.selNameLabel.setFont(font) - self.selNameLabel.setText("") - self.selNameLabel.setObjectName("selNameLabel") - self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) - self.selectedTree = DataTreeWidget(self.selInfoWidget) - self.selectedTree.setObjectName("selectedTree") - self.selectedTree.headerItem().setText(0, "1") - self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) - self.hoverText = QtWidgets.QTextEdit(Form) - self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) - self.hoverText.setObjectName("hoverText") - self.view = FlowchartGraphicsView(Form) - self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) - self.view.setObjectName("view") - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - -from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView -from ..widgets.DataTreeWidget import DataTreeWidget +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'FlowchartTemplate.ui' +# +# Created: Sun Sep 18 19:16:03 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(529, 329) + self.selInfoWidget = QtWidgets.QWidget(Form) + self.selInfoWidget.setGeometry(QtCore.QRect(260, 10, 264, 222)) + self.selInfoWidget.setObjectName("selInfoWidget") + self.gridLayout = QtWidgets.QGridLayout(self.selInfoWidget) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.selDescLabel = QtWidgets.QLabel(self.selInfoWidget) + self.selDescLabel.setText("") + self.selDescLabel.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) + self.selDescLabel.setWordWrap(True) + self.selDescLabel.setObjectName("selDescLabel") + self.gridLayout.addWidget(self.selDescLabel, 0, 0, 1, 1) + self.selNameLabel = QtWidgets.QLabel(self.selInfoWidget) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.selNameLabel.setFont(font) + self.selNameLabel.setText("") + self.selNameLabel.setObjectName("selNameLabel") + self.gridLayout.addWidget(self.selNameLabel, 0, 1, 1, 1) + self.selectedTree = DataTreeWidget(self.selInfoWidget) + self.selectedTree.setObjectName("selectedTree") + self.selectedTree.headerItem().setText(0, "1") + self.gridLayout.addWidget(self.selectedTree, 1, 0, 1, 2) + self.hoverText = QtWidgets.QTextEdit(Form) + self.hoverText.setGeometry(QtCore.QRect(0, 240, 521, 81)) + self.hoverText.setObjectName("hoverText") + self.view = FlowchartGraphicsView(Form) + self.view.setGeometry(QtCore.QRect(0, 0, 256, 192)) + self.view.setObjectName("view") + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + +from ..flowchart.FlowchartGraphicsView import FlowchartGraphicsView +from ..widgets.DataTreeWidget import DataTreeWidget diff --git a/pyqtgraph/imageview/ImageViewTemplate_pyside2.py b/pyqtgraph/imageview/ImageViewTemplate_pyside2.py index 74ea308d..cfe400c1 100644 --- a/pyqtgraph/imageview/ImageViewTemplate_pyside2.py +++ b/pyqtgraph/imageview/ImageViewTemplate_pyside2.py @@ -1,154 +1,154 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'ImageViewTemplate.ui' -# -# Created: Sun Sep 18 19:17:41 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PySide2 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(726, 588) - self.gridLayout_3 = QtWidgets.QGridLayout(Form) - self.gridLayout_3.setContentsMargins(0, 0, 0, 0) - self.gridLayout_3.setSpacing(0) - self.gridLayout_3.setObjectName("gridLayout_3") - self.splitter = QtWidgets.QSplitter(Form) - self.splitter.setOrientation(QtCore.Qt.Vertical) - self.splitter.setObjectName("splitter") - self.layoutWidget = QtWidgets.QWidget(self.splitter) - self.layoutWidget.setObjectName("layoutWidget") - self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) - self.gridLayout.setSpacing(0) - self.gridLayout.setContentsMargins(0, 0, 0, 0) - self.gridLayout.setObjectName("gridLayout") - self.graphicsView = GraphicsView(self.layoutWidget) - self.graphicsView.setObjectName("graphicsView") - self.gridLayout.addWidget(self.graphicsView, 0, 0, 2, 1) - self.histogram = HistogramLUTWidget(self.layoutWidget) - self.histogram.setObjectName("histogram") - self.gridLayout.addWidget(self.histogram, 0, 1, 1, 2) - self.roiBtn = QtWidgets.QPushButton(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) - self.roiBtn.setSizePolicy(sizePolicy) - self.roiBtn.setCheckable(True) - self.roiBtn.setObjectName("roiBtn") - self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) - self.menuBtn = QtWidgets.QPushButton(self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth()) - self.menuBtn.setSizePolicy(sizePolicy) - self.menuBtn.setObjectName("menuBtn") - self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1) - self.roiPlot = PlotWidget(self.splitter) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) - self.roiPlot.setSizePolicy(sizePolicy) - self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) - self.roiPlot.setObjectName("roiPlot") - self.gridLayout_3.addWidget(self.splitter, 0, 0, 1, 1) - self.normGroup = QtWidgets.QGroupBox(Form) - self.normGroup.setObjectName("normGroup") - self.gridLayout_2 = QtWidgets.QGridLayout(self.normGroup) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setSpacing(0) - self.gridLayout_2.setObjectName("gridLayout_2") - self.normSubtractRadio = QtWidgets.QRadioButton(self.normGroup) - self.normSubtractRadio.setObjectName("normSubtractRadio") - self.gridLayout_2.addWidget(self.normSubtractRadio, 0, 2, 1, 1) - self.normDivideRadio = QtWidgets.QRadioButton(self.normGroup) - self.normDivideRadio.setChecked(False) - self.normDivideRadio.setObjectName("normDivideRadio") - self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) - self.label_5 = QtWidgets.QLabel(self.normGroup) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.label_5.setFont(font) - self.label_5.setObjectName("label_5") - self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) - self.label_3 = QtWidgets.QLabel(self.normGroup) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.label_3.setFont(font) - self.label_3.setObjectName("label_3") - self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) - self.label_4 = QtWidgets.QLabel(self.normGroup) - font = QtGui.QFont() - font.setWeight(75) - font.setBold(True) - self.label_4.setFont(font) - self.label_4.setObjectName("label_4") - self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) - self.normROICheck = QtWidgets.QCheckBox(self.normGroup) - self.normROICheck.setObjectName("normROICheck") - self.gridLayout_2.addWidget(self.normROICheck, 1, 1, 1, 1) - self.normXBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) - self.normXBlurSpin.setObjectName("normXBlurSpin") - self.gridLayout_2.addWidget(self.normXBlurSpin, 2, 2, 1, 1) - self.label_8 = QtWidgets.QLabel(self.normGroup) - self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_8.setObjectName("label_8") - self.gridLayout_2.addWidget(self.label_8, 2, 1, 1, 1) - self.label_9 = QtWidgets.QLabel(self.normGroup) - self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_9.setObjectName("label_9") - self.gridLayout_2.addWidget(self.label_9, 2, 3, 1, 1) - self.normYBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) - self.normYBlurSpin.setObjectName("normYBlurSpin") - self.gridLayout_2.addWidget(self.normYBlurSpin, 2, 4, 1, 1) - self.label_10 = QtWidgets.QLabel(self.normGroup) - self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) - self.label_10.setObjectName("label_10") - self.gridLayout_2.addWidget(self.label_10, 2, 5, 1, 1) - self.normOffRadio = QtWidgets.QRadioButton(self.normGroup) - self.normOffRadio.setChecked(True) - self.normOffRadio.setObjectName("normOffRadio") - self.gridLayout_2.addWidget(self.normOffRadio, 0, 3, 1, 1) - self.normTimeRangeCheck = QtWidgets.QCheckBox(self.normGroup) - self.normTimeRangeCheck.setObjectName("normTimeRangeCheck") - self.gridLayout_2.addWidget(self.normTimeRangeCheck, 1, 3, 1, 1) - self.normFrameCheck = QtWidgets.QCheckBox(self.normGroup) - self.normFrameCheck.setObjectName("normFrameCheck") - self.gridLayout_2.addWidget(self.normFrameCheck, 1, 2, 1, 1) - self.normTBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) - self.normTBlurSpin.setObjectName("normTBlurSpin") - self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) - self.gridLayout_3.addWidget(self.normGroup, 1, 0, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.roiBtn.setText(QtWidgets.QApplication.translate("Form", "ROI", None, -1)) - self.menuBtn.setText(QtWidgets.QApplication.translate("Form", "Menu", None, -1)) - self.normGroup.setTitle(QtWidgets.QApplication.translate("Form", "Normalization", None, -1)) - self.normSubtractRadio.setText(QtWidgets.QApplication.translate("Form", "Subtract", None, -1)) - self.normDivideRadio.setText(QtWidgets.QApplication.translate("Form", "Divide", None, -1)) - self.label_5.setText(QtWidgets.QApplication.translate("Form", "Operation:", None, -1)) - self.label_3.setText(QtWidgets.QApplication.translate("Form", "Mean:", None, -1)) - self.label_4.setText(QtWidgets.QApplication.translate("Form", "Blur:", None, -1)) - self.normROICheck.setText(QtWidgets.QApplication.translate("Form", "ROI", None, -1)) - self.label_8.setText(QtWidgets.QApplication.translate("Form", "X", None, -1)) - self.label_9.setText(QtWidgets.QApplication.translate("Form", "Y", None, -1)) - self.label_10.setText(QtWidgets.QApplication.translate("Form", "T", None, -1)) - self.normOffRadio.setText(QtWidgets.QApplication.translate("Form", "Off", None, -1)) - self.normTimeRangeCheck.setText(QtWidgets.QApplication.translate("Form", "Time range", None, -1)) - self.normFrameCheck.setText(QtWidgets.QApplication.translate("Form", "Frame", None, -1)) - -from ..widgets.HistogramLUTWidget import HistogramLUTWidget -from ..widgets.PlotWidget import PlotWidget -from ..widgets.GraphicsView import GraphicsView +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ImageViewTemplate.ui' +# +# Created: Sun Sep 18 19:17:41 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PySide2 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(726, 588) + self.gridLayout_3 = QtWidgets.QGridLayout(Form) + self.gridLayout_3.setContentsMargins(0, 0, 0, 0) + self.gridLayout_3.setSpacing(0) + self.gridLayout_3.setObjectName("gridLayout_3") + self.splitter = QtWidgets.QSplitter(Form) + self.splitter.setOrientation(QtCore.Qt.Vertical) + self.splitter.setObjectName("splitter") + self.layoutWidget = QtWidgets.QWidget(self.splitter) + self.layoutWidget.setObjectName("layoutWidget") + self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) + self.gridLayout.setSpacing(0) + self.gridLayout.setContentsMargins(0, 0, 0, 0) + self.gridLayout.setObjectName("gridLayout") + self.graphicsView = GraphicsView(self.layoutWidget) + self.graphicsView.setObjectName("graphicsView") + self.gridLayout.addWidget(self.graphicsView, 0, 0, 2, 1) + self.histogram = HistogramLUTWidget(self.layoutWidget) + self.histogram.setObjectName("histogram") + self.gridLayout.addWidget(self.histogram, 0, 1, 1, 2) + self.roiBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.roiBtn.sizePolicy().hasHeightForWidth()) + self.roiBtn.setSizePolicy(sizePolicy) + self.roiBtn.setCheckable(True) + self.roiBtn.setObjectName("roiBtn") + self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) + self.menuBtn = QtWidgets.QPushButton(self.layoutWidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth()) + self.menuBtn.setSizePolicy(sizePolicy) + self.menuBtn.setObjectName("menuBtn") + self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1) + self.roiPlot = PlotWidget(self.splitter) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.roiPlot.sizePolicy().hasHeightForWidth()) + self.roiPlot.setSizePolicy(sizePolicy) + self.roiPlot.setMinimumSize(QtCore.QSize(0, 40)) + self.roiPlot.setObjectName("roiPlot") + self.gridLayout_3.addWidget(self.splitter, 0, 0, 1, 1) + self.normGroup = QtWidgets.QGroupBox(Form) + self.normGroup.setObjectName("normGroup") + self.gridLayout_2 = QtWidgets.QGridLayout(self.normGroup) + self.gridLayout_2.setContentsMargins(0, 0, 0, 0) + self.gridLayout_2.setSpacing(0) + self.gridLayout_2.setObjectName("gridLayout_2") + self.normSubtractRadio = QtWidgets.QRadioButton(self.normGroup) + self.normSubtractRadio.setObjectName("normSubtractRadio") + self.gridLayout_2.addWidget(self.normSubtractRadio, 0, 2, 1, 1) + self.normDivideRadio = QtWidgets.QRadioButton(self.normGroup) + self.normDivideRadio.setChecked(False) + self.normDivideRadio.setObjectName("normDivideRadio") + self.gridLayout_2.addWidget(self.normDivideRadio, 0, 1, 1, 1) + self.label_5 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_5.setFont(font) + self.label_5.setObjectName("label_5") + self.gridLayout_2.addWidget(self.label_5, 0, 0, 1, 1) + self.label_3 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_3.setFont(font) + self.label_3.setObjectName("label_3") + self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) + self.label_4 = QtWidgets.QLabel(self.normGroup) + font = QtGui.QFont() + font.setWeight(75) + font.setBold(True) + self.label_4.setFont(font) + self.label_4.setObjectName("label_4") + self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) + self.normROICheck = QtWidgets.QCheckBox(self.normGroup) + self.normROICheck.setObjectName("normROICheck") + self.gridLayout_2.addWidget(self.normROICheck, 1, 1, 1, 1) + self.normXBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normXBlurSpin.setObjectName("normXBlurSpin") + self.gridLayout_2.addWidget(self.normXBlurSpin, 2, 2, 1, 1) + self.label_8 = QtWidgets.QLabel(self.normGroup) + self.label_8.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_8.setObjectName("label_8") + self.gridLayout_2.addWidget(self.label_8, 2, 1, 1, 1) + self.label_9 = QtWidgets.QLabel(self.normGroup) + self.label_9.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_9.setObjectName("label_9") + self.gridLayout_2.addWidget(self.label_9, 2, 3, 1, 1) + self.normYBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normYBlurSpin.setObjectName("normYBlurSpin") + self.gridLayout_2.addWidget(self.normYBlurSpin, 2, 4, 1, 1) + self.label_10 = QtWidgets.QLabel(self.normGroup) + self.label_10.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.label_10.setObjectName("label_10") + self.gridLayout_2.addWidget(self.label_10, 2, 5, 1, 1) + self.normOffRadio = QtWidgets.QRadioButton(self.normGroup) + self.normOffRadio.setChecked(True) + self.normOffRadio.setObjectName("normOffRadio") + self.gridLayout_2.addWidget(self.normOffRadio, 0, 3, 1, 1) + self.normTimeRangeCheck = QtWidgets.QCheckBox(self.normGroup) + self.normTimeRangeCheck.setObjectName("normTimeRangeCheck") + self.gridLayout_2.addWidget(self.normTimeRangeCheck, 1, 3, 1, 1) + self.normFrameCheck = QtWidgets.QCheckBox(self.normGroup) + self.normFrameCheck.setObjectName("normFrameCheck") + self.gridLayout_2.addWidget(self.normFrameCheck, 1, 2, 1, 1) + self.normTBlurSpin = QtWidgets.QDoubleSpinBox(self.normGroup) + self.normTBlurSpin.setObjectName("normTBlurSpin") + self.gridLayout_2.addWidget(self.normTBlurSpin, 2, 6, 1, 1) + self.gridLayout_3.addWidget(self.normGroup, 1, 0, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.roiBtn.setText(QtWidgets.QApplication.translate("Form", "ROI", None, -1)) + self.menuBtn.setText(QtWidgets.QApplication.translate("Form", "Menu", None, -1)) + self.normGroup.setTitle(QtWidgets.QApplication.translate("Form", "Normalization", None, -1)) + self.normSubtractRadio.setText(QtWidgets.QApplication.translate("Form", "Subtract", None, -1)) + self.normDivideRadio.setText(QtWidgets.QApplication.translate("Form", "Divide", None, -1)) + self.label_5.setText(QtWidgets.QApplication.translate("Form", "Operation:", None, -1)) + self.label_3.setText(QtWidgets.QApplication.translate("Form", "Mean:", None, -1)) + self.label_4.setText(QtWidgets.QApplication.translate("Form", "Blur:", None, -1)) + self.normROICheck.setText(QtWidgets.QApplication.translate("Form", "ROI", None, -1)) + self.label_8.setText(QtWidgets.QApplication.translate("Form", "X", None, -1)) + self.label_9.setText(QtWidgets.QApplication.translate("Form", "Y", None, -1)) + self.label_10.setText(QtWidgets.QApplication.translate("Form", "T", None, -1)) + self.normOffRadio.setText(QtWidgets.QApplication.translate("Form", "Off", None, -1)) + self.normTimeRangeCheck.setText(QtWidgets.QApplication.translate("Form", "Time range", None, -1)) + self.normFrameCheck.setText(QtWidgets.QApplication.translate("Form", "Frame", None, -1)) + +from ..widgets.HistogramLUTWidget import HistogramLUTWidget +from ..widgets.PlotWidget import PlotWidget +from ..widgets.GraphicsView import GraphicsView From ebc805a45f9041573f70a6820caa3635cfb62045 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 23 May 2018 21:12:16 -0700 Subject: [PATCH 269/607] workaround pyside bug #671 --- examples/optics/pyoptic.py | 32 ++++++-------------------------- 1 file changed, 6 insertions(+), 26 deletions(-) diff --git a/examples/optics/pyoptic.py b/examples/optics/pyoptic.py index c2cb2ba2..f59f3bac 100644 --- a/examples/optics/pyoptic.py +++ b/examples/optics/pyoptic.py @@ -110,7 +110,11 @@ class ParamObj(object): def __getitem__(self, item): # bug in pyside 1.2.2 causes getitem to be called inside QGraphicsObject.parentItem: - return self.getParam(item) # PySide bug: https://bugreports.qt.io/browse/PYSIDE-441 + return self.getParam(item) # PySide bug: https://bugreports.qt.io/browse/PYSIDE-671 + + def __len__(self): + # Workaround for PySide bug: https://bugreports.qt.io/browse/PYSIDE-671 + return 0 def getParam(self, param): return self.__params[param] @@ -235,35 +239,21 @@ class Lens(Optic): N must already be normalized in order to achieve the desired result. """ - - - iors = [self.ior(ray['wl']), 1.0] for i in [0,1]: surface = self.surfaces[i] ior = iors[i] p1, ai = surface.intersectRay(ray) - #print "surface intersection:", p1, ai*180/3.14159 - #trans = self.sceneTransform().inverted()[0] * surface.sceneTransform() - #p1 = trans.map(p1) if p1 is None: ray.setEnd(None) break p1 = surface.mapToItem(ray, p1) - #print "adjusted position:", p1 - #ior = self.ior(ray['wl']) rd = ray['dir'] a1 = np.arctan2(rd[1], rd[0]) ar = a1 - ai + np.arcsin((np.sin(ai) * ray['ior'] / ior)) - #print [x for x in [a1, ai, (np.sin(ai) * ray['ior'] / ior), ar]] - #print ai, np.sin(ai), ray['ior'], ior ray.setEnd(p1) dp = Point(np.cos(ar), np.sin(ar)) - #p2 = p1+dp - #p1p = self.mapToScene(p1) - #p2p = self.mapToScene(p2) - #dpp = Point(p2p-p1p) ray = Ray(parent=ray, ior=ior, dir=dp) return [ray] @@ -384,20 +374,12 @@ class CircleSurface(pg.GraphicsObject): else: ## half-height of surface can't be larger than radius h2 = min(h2, abs(r)) - - #dx = abs(r) - (abs(r)**2 - abs(h2)**2)**0.5 - #p.moveTo(-d*w/2.+ d*dx, d*h2) arc = QtCore.QRectF(0, -r, r*2, r*2) - #self.surfaces.append((arc.center(), r, h2)) a1 = np.arcsin(h2/r) * 180. / np.pi a2 = -2*a1 a1 += 180. self.path.arcMoveTo(arc, a1) self.path.arcTo(arc, a1, a2) - #if d == -1: - #p1 = QtGui.QPainterPath() - #p1.addRect(arc) - #self.paths.append(p1) self.h2 = h2 def boundingRect(self): @@ -405,8 +387,6 @@ class CircleSurface(pg.GraphicsObject): def paint(self, p, *args): return ## usually we let the optic draw. - #p.setPen(pg.mkPen('r')) - #p.drawPath(self.path) def intersectRay(self, ray): ## return the point of intersection and the angle of incidence @@ -527,7 +507,6 @@ class Ray(pg.GraphicsObject, ParamObj): p2 = trans.map(pos + dir) return Point(p1), Point(p2-p1) - def setEnd(self, end): self['end'] = end self.mkPath() @@ -561,6 +540,7 @@ def trace(rays, optics): r2 = o.propagateRay(r) trace(r2, optics[1:]) + class Tracer(QtCore.QObject): """ Simple ray tracer. From 86b2006d46f31a7020ebcb7f6a95d76ea1dba674 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 23 May 2018 21:30:02 -0700 Subject: [PATCH 270/607] Fix transform3D for pyside2 --- pyqtgraph/Transform3D.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/Transform3D.py b/pyqtgraph/Transform3D.py index 326ccef2..b5a41bc2 100644 --- a/pyqtgraph/Transform3D.py +++ b/pyqtgraph/Transform3D.py @@ -10,11 +10,14 @@ class Transform3D(QtGui.QMatrix4x4): Extension of QMatrix4x4 with some helpful methods added. """ def __init__(self, *args): - if len(args) == 1 and isinstance(args[0], (list, tuple, np.ndarray)): - args = [x for y in args[0] for x in y] - if len(args) != 16: - raise TypeError("Single argument to Transform3D must have 16 elements.") - print(args) + if len(args) == 1: + if isinstance(args[0], (list, tuple, np.ndarray)): + args = [x for y in args[0] for x in y] + if len(args) != 16: + raise TypeError("Single argument to Transform3D must have 16 elements.") + elif isinstance(args[0], QtGui.QMatrix4x4): + args = list(args[0].copyDataTo()) + QtGui.QMatrix4x4.__init__(self, *args) def matrix(self, nd=3): From cc8149619796329f4e941c64c11047f7eb653cf2 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 24 May 2018 08:48:50 -0700 Subject: [PATCH 271/607] update changelog --- CHANGELOG | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 921d0616..afba9d4a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ pyqtgraph-0.11.0 (in development) + New Features: + - PySide2 support + API / behavior changes: - ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system. The result is visually the same, but children of ArrowItem are no longer rotated From 69645895c94dbb7a6b03f4363b841fb969a2576f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 24 May 2018 08:54:31 -0700 Subject: [PATCH 272/607] flip template line endings --- .../ScatterPlotSpeedTestTemplate_pyqt5.py | 88 +++++++++---------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/examples/ScatterPlotSpeedTestTemplate_pyqt5.py b/examples/ScatterPlotSpeedTestTemplate_pyqt5.py index 1c7be1d3..66254a5a 100644 --- a/examples/ScatterPlotSpeedTestTemplate_pyqt5.py +++ b/examples/ScatterPlotSpeedTestTemplate_pyqt5.py @@ -1,44 +1,44 @@ -# -*- coding: utf-8 -*- - -# Form implementation generated from reading ui file 'ScatterPlotSpeedTestTemplate.ui' -# -# Created: Sun Sep 18 19:21:36 2016 -# by: pyside2-uic running on PySide2 2.0.0~alpha0 -# -# WARNING! All changes made in this file will be lost! - -from PyQt5 import QtCore, QtGui, QtWidgets - -class Ui_Form(object): - def setupUi(self, Form): - Form.setObjectName("Form") - Form.resize(400, 300) - self.gridLayout = QtWidgets.QGridLayout(Form) - self.gridLayout.setObjectName("gridLayout") - self.sizeSpin = QtWidgets.QSpinBox(Form) - self.sizeSpin.setProperty("value", 10) - self.sizeSpin.setObjectName("sizeSpin") - self.gridLayout.addWidget(self.sizeSpin, 1, 1, 1, 1) - self.pixelModeCheck = QtWidgets.QCheckBox(Form) - self.pixelModeCheck.setObjectName("pixelModeCheck") - self.gridLayout.addWidget(self.pixelModeCheck, 1, 3, 1, 1) - self.label = QtWidgets.QLabel(Form) - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 1, 0, 1, 1) - self.plot = PlotWidget(Form) - self.plot.setObjectName("plot") - self.gridLayout.addWidget(self.plot, 0, 0, 1, 4) - self.randCheck = QtWidgets.QCheckBox(Form) - self.randCheck.setObjectName("randCheck") - self.gridLayout.addWidget(self.randCheck, 1, 2, 1, 1) - - self.retranslateUi(Form) - QtCore.QMetaObject.connectSlotsByName(Form) - - def retranslateUi(self, Form): - Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) - self.pixelModeCheck.setText(QtWidgets.QApplication.translate("Form", "pixel mode", None, -1)) - self.label.setText(QtWidgets.QApplication.translate("Form", "Size", None, -1)) - self.randCheck.setText(QtWidgets.QApplication.translate("Form", "Randomize", None, -1)) - -from pyqtgraph import PlotWidget +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ScatterPlotSpeedTestTemplate.ui' +# +# Created: Sun Sep 18 19:21:36 2016 +# by: pyside2-uic running on PySide2 2.0.0~alpha0 +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore, QtGui, QtWidgets + +class Ui_Form(object): + def setupUi(self, Form): + Form.setObjectName("Form") + Form.resize(400, 300) + self.gridLayout = QtWidgets.QGridLayout(Form) + self.gridLayout.setObjectName("gridLayout") + self.sizeSpin = QtWidgets.QSpinBox(Form) + self.sizeSpin.setProperty("value", 10) + self.sizeSpin.setObjectName("sizeSpin") + self.gridLayout.addWidget(self.sizeSpin, 1, 1, 1, 1) + self.pixelModeCheck = QtWidgets.QCheckBox(Form) + self.pixelModeCheck.setObjectName("pixelModeCheck") + self.gridLayout.addWidget(self.pixelModeCheck, 1, 3, 1, 1) + self.label = QtWidgets.QLabel(Form) + self.label.setObjectName("label") + self.gridLayout.addWidget(self.label, 1, 0, 1, 1) + self.plot = PlotWidget(Form) + self.plot.setObjectName("plot") + self.gridLayout.addWidget(self.plot, 0, 0, 1, 4) + self.randCheck = QtWidgets.QCheckBox(Form) + self.randCheck.setObjectName("randCheck") + self.gridLayout.addWidget(self.randCheck, 1, 2, 1, 1) + + self.retranslateUi(Form) + QtCore.QMetaObject.connectSlotsByName(Form) + + def retranslateUi(self, Form): + Form.setWindowTitle(QtWidgets.QApplication.translate("Form", "Form", None, -1)) + self.pixelModeCheck.setText(QtWidgets.QApplication.translate("Form", "pixel mode", None, -1)) + self.label.setText(QtWidgets.QApplication.translate("Form", "Size", None, -1)) + self.randCheck.setText(QtWidgets.QApplication.translate("Form", "Randomize", None, -1)) + +from pyqtgraph import PlotWidget From cfc9759502701e75cf0c707fb80105b8ca80a346 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 24 May 2018 17:16:38 -0700 Subject: [PATCH 273/607] update readme / install docs to mention PySide2 --- README.md | 2 +- doc/source/installation.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 85f4f9e7..a0048103 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ heavy leverage of numpy for number crunching, Qt's GraphicsView framework for Requirements ------------ - * PyQt 4.7+, PySide, or PyQt5 + * PyQt 4.7+, PySide, PyQt5, or PySide2 * python 2.7, or 3.x * NumPy * For 3D graphics: pyopengl and qt-opengl diff --git a/doc/source/installation.rst b/doc/source/installation.rst index bd1594da..37c0ae0e 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -36,7 +36,7 @@ Requirements PyQtGraph depends on: * Python 2.7 or Python 3.x -* A Qt library such as PyQt4, PyQt5, or PySide +* A Qt library such as PyQt4, PyQt5, PySide, or PySide2 * numpy The easiest way to meet these dependencies is with ``pip`` or with a scientific python From e5079bca32acd2d7498e39f29e88fe8fc38b2d35 Mon Sep 17 00:00:00 2001 From: kolt mcbride Date: Thu, 24 May 2018 22:06:27 -0700 Subject: [PATCH 274/607] this import seems to be missing, I couldn't set color with out it for some reason --- pyqtgraph/opengl/items/GLLinePlotItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index f5cb7545..737ac755 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -3,6 +3,7 @@ from OpenGL.arrays import vbo from .. GLGraphicsItem import GLGraphicsItem from .. import shaders from ... import QtGui +from ... import functions as fn import numpy as np __all__ = ['GLLinePlotItem'] From de9007d9c47ecec1059bc0b26a3553ad93e2a0f7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 30 May 2018 18:54:31 -0700 Subject: [PATCH 275/607] Fix error when using PlotDataItem with both stepMode and symbol --- pyqtgraph/graphicsItems/PlotDataItem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index c85c3e5b..2faa9ac1 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -490,6 +490,9 @@ class PlotDataItem(GraphicsObject): self.curve.hide() if scatterArgs['symbol'] is not None: + + if self.opts.get('stepMode', False) is True: + x = 0.5 * (x[:-1] + x[1:]) self.scatter.setData(x=x, y=y, **scatterArgs) self.scatter.show() else: From 8a5c66f25e5f8ce2d13704bff02a60d58cb9bbef Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 31 May 2018 10:36:04 -0700 Subject: [PATCH 276/607] Allow strings to specify color of GLLinePlot --- pyqtgraph/opengl/items/GLLinePlotItem.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/pyqtgraph/opengl/items/GLLinePlotItem.py b/pyqtgraph/opengl/items/GLLinePlotItem.py index 737ac755..2daf78ba 100644 --- a/pyqtgraph/opengl/items/GLLinePlotItem.py +++ b/pyqtgraph/opengl/items/GLLinePlotItem.py @@ -57,21 +57,6 @@ class GLLinePlotItem(GLGraphicsItem): def initializeGL(self): pass - #def setupGLState(self): - #"""Prepare OpenGL state for drawing. This function is called immediately before painting.""" - ##glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) ## requires z-sorting to render properly. - #glBlendFunc(GL_SRC_ALPHA, GL_ONE) - #glEnable( GL_BLEND ) - #glEnable( GL_ALPHA_TEST ) - #glDisable( GL_DEPTH_TEST ) - - ##glEnable( GL_POINT_SMOOTH ) - - ##glHint(GL_POINT_SMOOTH_HINT, GL_NICEST) - ##glPointParameterfv(GL_POINT_DISTANCE_ATTENUATION, (0, 0, -1e-3)) - ##glPointParameterfv(GL_POINT_SIZE_MAX, (65500,)) - ##glPointParameterfv(GL_POINT_SIZE_MIN, (0,)) - def paint(self): if self.pos is None: return @@ -86,12 +71,11 @@ class GLLinePlotItem(GLGraphicsItem): glEnableClientState(GL_COLOR_ARRAY) glColorPointerf(self.color) else: - if isinstance(self.color, QtGui.QColor): + if isinstance(self.color, (str, QtGui.QColor)): glColor4f(*fn.glColor(self.color)) else: glColor4f(*self.color) glLineWidth(self.width) - #glPointSize(self.width) if self.antialias: glEnable(GL_LINE_SMOOTH) From a48a3776be264b39b723bae1e7c4f47669f3135b Mon Sep 17 00:00:00 2001 From: Pol Welter Date: Fri, 1 Jun 2018 10:31:47 +0200 Subject: [PATCH 277/607] Fix issue #697 --- pyqtgraph/widgets/SpinBox.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index ea59bf31..496ea37a 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -509,8 +509,14 @@ class SpinBox(QtGui.QAbstractSpinBox): def fixup(self, strn): # fixup is called when the spinbox loses focus with an invalid or intermediate string self.updateText() - strn.clear() - strn.append(self.lineEdit().text()) + + # support both PyQt APIs (for Python 2 and 3 respectively) + # http://pyqt.sourceforge.net/Docs/PyQt4/python_v3.html#qvalidator + try: + strn.clear() + strn.append(self.lineEdit().text()) + except AttributeError: + return self.lineEdit().text() def interpret(self): """Return value of text or False if text is invalid.""" From d35991ec37df96bbec8ccf91faf6e3b6dc862c43 Mon Sep 17 00:00:00 2001 From: Pol Welter Date: Fri, 1 Jun 2018 12:30:33 +0200 Subject: [PATCH 278/607] Fix issue #699 Nan in image data not handled properly --- pyqtgraph/graphicsItems/ImageItem.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 6017fabc..7722925e 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -268,8 +268,9 @@ class ImageItem(GraphicsObject): img = self.image while img.size > 2**16: img = img[::2, ::2] - mn, mx = img.min(), img.max() - if mn == mx: + mn, mx = np.nanmin(img), np.nanmax(img) + # mn and mx can still be NaN if the data is all-NaN + if mn == mx or np.isnan(mn) or np.isnan(mx): mn = 0 mx = 255 kargs['levels'] = [mn,mx] @@ -470,7 +471,7 @@ class ImageItem(GraphicsObject): This method is also used when automatically computing levels. """ if self.image is None or self.image.size == 0: - return None,None + return None, None if step == 'auto': step = (max(1, int(np.ceil(self.image.shape[0] / targetImageSize))), max(1, int(np.ceil(self.image.shape[1] / targetImageSize)))) @@ -479,8 +480,11 @@ class ImageItem(GraphicsObject): stepData = self.image[::step[0], ::step[1]] if bins == 'auto': - mn = stepData.min() - mx = stepData.max() + mn = np.nanmin(stepData) + mx = np.nanmax(stepData) + if np.isnan(mn) or np.isnan(mx): + # the data are all-nan + return None, None if stepData.dtype.kind in "ui": # For integer data, we select the bins carefully to avoid aliasing step = np.ceil((mx-mn) / 500.) From 937e3420e2be2a87582a6d509731c297b40bdfa4 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 5 Jun 2018 15:20:15 -0700 Subject: [PATCH 279/607] Don't store `children` key in Parameter.opts (closes #494, fixes #493) --- pyqtgraph/parametertree/Parameter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index e28085bf..be77c9ff 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -180,7 +180,7 @@ class Parameter(QtCore.QObject): raise Exception("Parameter must have a string name specified in opts.") self.setName(name) - self.addChildren(self.opts.get('children', [])) + self.addChildren(self.opts.pop('children', [])) self.opts['value'] = None if value is not None: From c20022580de93880783138e98df87c695b39cc01 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 7 Jun 2018 17:49:42 -0700 Subject: [PATCH 280/607] move panning code from mouse handler to pan() --- pyqtgraph/opengl/GLViewWidget.py | 74 +++++++++++++++++++------------- 1 file changed, 43 insertions(+), 31 deletions(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 60a829ab..92332cf5 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -113,7 +113,6 @@ class GLViewWidget(QtOpenGL.QGLWidget): glMultMatrixf(a.transpose()) def projectionMatrix(self, region=None): - # Xw = (Xnd + 1) * width/2 + X if region is None: dpr = self.devicePixelRatio() region = (0, 0, self.width() * dpr, self.height() * dpr) @@ -127,8 +126,6 @@ class GLViewWidget(QtOpenGL.QGLWidget): r = nearClip * np.tan(fov * 0.5 * np.pi / 180.) t = r * h / w - # convert screen coordinates (region) to normalized device coordinates - # Xnd = (Xw - X0) * 2/width - 1 ## Note that X0 and width in these equations must be the values used in viewport left = r * ((region[0]-x0) * (2.0/w) - 1) right = r * ((region[0]+region[2]-x0) * (2.0/w) - 1) @@ -247,8 +244,6 @@ class GLViewWidget(QtOpenGL.QGLWidget): self.opts['azimuth'] = azimuth self.update() - - def cameraPosition(self): """Return current position of camera based on center, dist, elevation, and azimuth""" center = self.opts['center'] @@ -267,24 +262,41 @@ class GLViewWidget(QtOpenGL.QGLWidget): def orbit(self, azim, elev): """Orbits the camera around the center position. *azim* and *elev* are given in degrees.""" self.opts['azimuth'] += azim - #self.opts['elevation'] += elev self.opts['elevation'] = np.clip(self.opts['elevation'] + elev, -90, 90) self.update() - def pan(self, dx, dy, dz, relative=False): + def pan(self, dx, dy, dz, relative='global'): """ Moves the center (look-at) position while holding the camera in place. - If relative=True, then the coordinates are interpreted such that x - if in the global xy plane and points to the right side of the view, y is - in the global xy plane and orthogonal to x, and z points in the global z - direction. Distances are scaled roughly such that a value of 1.0 moves + ============== ======================================================= + **Arguments:** + *dx* Distance to pan in x direction + *dy* Distance to pan in y direction + *dx* Distance to pan in z direction + *relative* String that determines the direction of dx,dy,dz. + If "global", then the global coordinate system is used. + If "view", then the z axis is aligned with the view + direction, and x and y axes are inthe plane of the + view: +x points right, +y points up. + If "view-upright", then x is in the global xy plane and + points to the right side of the view, y is in the + global xy plane and orthogonal to x, and z points in + the global z direction. + ============== ======================================================= + + Distances are scaled roughly such that a value of 1.0 moves by one pixel on screen. + Prior to version 0.11, *relative* was expected to be either True (x-aligned) or + False (global). These values are deprecated but still recognized. """ - if not relative: + # for backward compatibility: + relative = {True: "view-upright", False: "global"}.get(relative, relative) + + if relative == 'global': self.opts['center'] += QtGui.QVector3D(dx, dy, dz) - else: + elif relative == 'view-upright': cPos = self.cameraPosition() cVec = self.opts['center'] - cPos dist = cVec.length() ## distance from camera to center @@ -294,6 +306,21 @@ class GLViewWidget(QtOpenGL.QGLWidget): xVec = QtGui.QVector3D.crossProduct(zVec, cVec).normalized() yVec = QtGui.QVector3D.crossProduct(xVec, zVec).normalized() self.opts['center'] = self.opts['center'] + xVec * xScale * dx + yVec * xScale * dy + zVec * xScale * dz + elif relative == 'view': + # pan in plane of camera + elev = np.radians(self.opts['elevation']) + azim = np.radians(self.opts['azimuth']) + fov = np.radians(self.opts['fov']) + dist = (self.opts['center'] - self.cameraPosition()).length() + fov_factor = np.tan(fov / 2) * 2 + scale_factor = dist * fov_factor / self.width() + z = scale_factor * np.cos(elev) * dy + x = scale_factor * (np.sin(azim) * dx - np.sin(elev) * np.cos(azim) * dy) + y = scale_factor * (np.cos(azim) * dx + np.sin(elev) * np.sin(azim) * dy) + self.opts['center'] += QtGui.QVector3D(x, -y, z) + else: + raise ValueError("relative argument must be global, view, or view-upright") + self.update() def pixelSize(self, pos): @@ -319,26 +346,14 @@ class GLViewWidget(QtOpenGL.QGLWidget): if ev.buttons() == QtCore.Qt.LeftButton: if (ev.modifiers() & QtCore.Qt.ControlModifier): - # pan in plane of camera - elev = np.radians(self.opts['elevation']) - azim = np.radians(self.opts['azimuth']) - fov = np.radians(self.opts['fov']) - dist = (self.opts['center'] - self.cameraPosition()).length() - fov_factor = np.tan(fov / 2) * 2 - scale_factor = dist * fov_factor / self.width() - dx = diff.x() - dy = diff.y() - z = scale_factor * np.cos(elev) * dy - x = scale_factor * (np.sin(azim) * dx - np.sin(elev) * np.cos(azim) * dy) - y = scale_factor * (np.cos(azim) * dx + np.sin(elev) * np.sin(azim) * dy) - self.pan(x, -y, z, relative=False) + self.pan(diff.x(), diff.y(), 0, relative='view') else: self.orbit(-diff.x(), diff.y()) elif ev.buttons() == QtCore.Qt.MidButton: if (ev.modifiers() & QtCore.Qt.ControlModifier): - self.pan(diff.x(), 0, diff.y(), relative=True) + self.pan(diff.x(), 0, diff.y(), relative='view-upright') else: - self.pan(diff.x(), diff.y(), 0, relative=True) + self.pan(diff.x(), diff.y(), 0, relative='view-upright') def mouseReleaseEvent(self, ev): pass @@ -417,8 +432,6 @@ class GLViewWidget(QtOpenGL.QGLWidget): else: raise - - def readQImage(self): """ Read the current buffer pixels out as a QImage. @@ -442,7 +455,6 @@ class GLViewWidget(QtOpenGL.QGLWidget): img = fn.makeQImage(pixels, transpose=False) return img - def renderToArray(self, size, format=GL_BGRA, type=GL_UNSIGNED_BYTE, textureSize=1024, padding=256): w,h = map(int, size) From 7be7c3f459775dd4aea6a869d9967015ef86e596 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 8 Jun 2018 08:43:46 -0700 Subject: [PATCH 281/607] More PlotItem cleanup --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 123 +++++-------------- 1 file changed, 32 insertions(+), 91 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 3e5624b9..b8face5e 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -1,21 +1,4 @@ # -*- coding: utf-8 -*- -""" -PlotItem.py - Graphics item implementing a scalable ViewBox with plotting powers. -Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. - -This class is one of the workhorses of pyqtgraph. It implements a graphics item with -plots, labels, and scales which can be viewed inside a QGraphicsScene. If you want -a widget that can be added to your GUI, see PlotWidget instead. - -This class is very heavily featured: - - Automatically creates and manages PlotCurveItems - - Fast display and update of plots - - Manages zoom/pan ViewBox, scale, and label elements - - Automatic scaling when data changes - - Control panel with a huge feature set including averaging, decimation, - display, power spectrum, svg/png export, plot linking, and more. -""" import sys import weakref import numpy as np @@ -53,17 +36,24 @@ except: HAVE_METAARRAY = False - - class PlotItem(GraphicsWidget): - - """ + """GraphicsWidget implementing a standard 2D plotting area with axes. + **Bases:** :class:`GraphicsWidget ` - Plot graphics item that can be added to any graphics scene. Implements axes, titles, and interactive viewbox. - PlotItem also provides some basic analysis functionality that may be accessed from the context menu. - Use :func:`plot() ` to create a new PlotDataItem and add it to the view. - Use :func:`addItem() ` to add any QGraphicsItem to the view. + This class provides the ViewBox-plus-axes that appear when using + :func:`pg.plot() `, :class:`PlotWidget `, + and :func:`GraphicsLayoutWidget.addPlot() `. + + It's main functionality is: + + - Manage placement of ViewBox, AxisItems, and LabelItems + - Create and manage a list of PlotDataItems displayed inside the ViewBox + - Implement a context menu with commonly used display and analysis options + + Use :func:`plot() ` to create a new PlotDataItem and + add it to the view. Use :func:`addItem() ` to + add any QGraphicsItem to the view. This class wraps several methods from its internal ViewBox: :func:`setXRange `, @@ -99,8 +89,7 @@ class PlotItem(GraphicsWidget): sigRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox range has changed sigYRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox Y range has changed sigXRangeChanged = QtCore.Signal(object, object) ## Emitted when the ViewBox X range has changed - - + lastFileDir = None def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): @@ -133,12 +122,9 @@ class PlotItem(GraphicsWidget): ## Set up control buttons path = os.path.dirname(__file__) - #self.autoImageFile = os.path.join(path, 'auto.png') - #self.lockImageFile = os.path.join(path, 'lock.png') self.autoBtn = ButtonItem(pixmaps.getPixmap('auto'), 14, self) self.autoBtn.mode = 'auto' self.autoBtn.clicked.connect(self.autoBtnClicked) - #self.autoBtn.hide() self.buttonsHidden = False ## whether the user has requested buttons to be hidden self.mouseHovering = False @@ -186,7 +172,6 @@ class PlotItem(GraphicsWidget): self.layout.addItem(self.titleLabel, 0, 1) self.setTitle(None) ## hide - for i in range(4): self.layout.setRowPreferredHeight(i, 0) self.layout.setRowMinimumHeight(i, 0) @@ -289,8 +274,7 @@ class PlotItem(GraphicsWidget): self.setTitle(title) if len(kargs) > 0: - self.plot(**kargs) - + self.plot(**kargs) def implements(self, interface=None): return interface in ['ViewBoxWrapper'] @@ -298,12 +282,10 @@ class PlotItem(GraphicsWidget): def getViewBox(self): """Return the :class:`ViewBox ` contained within.""" return self.vb - ## Wrap a few methods from viewBox. #Important: don't use a settattr(m, getattr(self.vb, m)) as we'd be leaving the viebox alive #because we had a reference to an instance method (creating wrapper methods at runtime instead). - for m in ['setXRange', 'setYRange', 'setXLink', 'setYLink', 'setAutoPan', # NOTE: 'setAutoVisible', 'setRange', 'autoRange', 'viewRect', 'viewRange', # If you update this list, please 'setMouseEnabled', 'setLimits', 'enableAutoRange', 'disableAutoRange', # update the class docstring @@ -318,8 +300,7 @@ class PlotItem(GraphicsWidget): locals()[m] = _create_method(m) del _create_method - - + def setLogMode(self, x=None, y=None): """ Set log scaling for x and/or y axes. @@ -359,7 +340,6 @@ class PlotItem(GraphicsWidget): self.ctrl.gridAlphaSlider.setValue(v) def close(self): - #print "delete", self ## Most of this crap is needed to avoid PySide trouble. ## The problem seems to be whenever scene.clear() leads to deletion of widgets (either through proxies or qgraphicswidgets) ## the solution is to manually remove all widgets before scene.clear() is called @@ -400,7 +380,6 @@ class PlotItem(GraphicsWidget): wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) return wr - def avgToggled(self, b): if b: self.recomputeAverages() @@ -541,8 +520,7 @@ class PlotItem(GraphicsWidget): #self.plotChanged() #name = kargs.get('name', getattr(item, 'opts', {}).get('name', None)) if name is not None and hasattr(self, 'legend') and self.legend is not None: - self.legend.addItem(item, name=name) - + self.legend.addItem(item, name=name) def addDataItem(self, item, *args): print("PlotItem.addDataItem is deprecated. Use addItem instead.") @@ -573,9 +551,7 @@ class PlotItem(GraphicsWidget): self.addItem(line) if z is not None: line.setZValue(z) - return line - - + return line def removeItem(self, item): """ @@ -593,8 +569,6 @@ class PlotItem(GraphicsWidget): self.curves.remove(item) self.updateDecimation() self.updateParamList() - #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) - #item.sigPlotChanged.connect(self.plotChanged) if self.legend is not None: self.legend.removeItem(item) @@ -610,8 +584,7 @@ class PlotItem(GraphicsWidget): def clearPlots(self): for i in self.curves[:]: self.removeItem(i) - self.avgCurves = {} - + self.avgCurves = {} def plot(self, *args, **kargs): """ @@ -622,8 +595,6 @@ class PlotItem(GraphicsWidget): clear - clear all plots before displaying new data params - meta-parameters to associate with this data """ - - clear = kargs.get('clear', False) params = kargs.get('params', None) @@ -692,11 +663,9 @@ class PlotItem(GraphicsWidget): self.paramList[p] = (i.checkState() == QtCore.Qt.Checked) - - ## Qt's SVG-writing capabilities are pretty terrible. def writeSvgCurves(self, fileName=None): if fileName is None: - self._choose_filename_dialog(handler=self.writeSvg) + self._chooseFilenameDialog(handler=self.writeSvg) return if isinstance(fileName, tuple): @@ -722,12 +691,10 @@ class PlotItem(GraphicsWidget): sy *= 1000 sy *= -1 - #fh.write('\n' % (rect.left()*sx, rect.top()*sx, rect.width()*sy, rect.height()*sy)) fh.write('\n') fh.write('\n' % (rect.left()*sx, rect.right()*sx)) fh.write('\n' % (rect.top()*sy, rect.bottom()*sy)) - for item in self.curves: if isinstance(item, PlotCurveItem): color = fn.colorStr(item.pen.color()) @@ -744,13 +711,12 @@ class PlotItem(GraphicsWidget): x *= sx y *= sy - #fh.write('\n' % color) fh.write('') - #fh.write("") + for item in self.dataItems: if isinstance(item, ScatterPlotItem): @@ -770,12 +736,10 @@ class PlotItem(GraphicsWidget): fh.write('\n' % (x, y, color, opacity)) fh.write("\n") - - def writeSvg(self, fileName=None): if fileName is None: - self._choose_filename_dialog(handler=self.writeSvg) + self._chooseFilenameDialog(handler=self.writeSvg) return fileName = str(fileName) @@ -787,23 +751,16 @@ class PlotItem(GraphicsWidget): def writeImage(self, fileName=None): if fileName is None: - self._choose_filename_dialog(handler=self.writeImage) + self._chooseFilenameDialog(handler=self.writeImage) return - if isinstance(fileName, tuple): - raise Exception("Not implemented yet..") - fileName = str(fileName) - PlotItem.lastFileDir = os.path.dirname(fileName) - self.png = QtGui.QImage(int(self.size().width()), int(self.size().height()), QtGui.QImage.Format_ARGB32) - painter = QtGui.QPainter(self.png) - painter.setRenderHints(painter.Antialiasing | painter.TextAntialiasing) - self.scene().render(painter, QtCore.QRectF(), self.mapRectToScene(self.boundingRect())) - painter.end() - self.png.save(fileName) + from ...exporters import ImageExporter + ex = ImageExporter(self) + ex.export(fileName) def writeCsv(self, fileName=None): if fileName is None: - self._choose_filename_dialog(handler=self.writeCsv) + self._chooseFilenameDialog(handler=self.writeCsv) return fileName = str(fileName) @@ -826,7 +783,6 @@ class PlotItem(GraphicsWidget): i += 1 fd.close() - def saveState(self): state = self.stateGroup.state() state['paramList'] = self.paramList.copy() @@ -861,7 +817,6 @@ class PlotItem(GraphicsWidget): 'viewRange': r, } self.vb.setState(state['view']) - def widgetGroupInterface(self): return (None, PlotItem.saveState, PlotItem.restoreState) @@ -960,9 +915,7 @@ class PlotItem(GraphicsWidget): def clipToViewMode(self): return self.ctrl.clipToViewCheck.isChecked() - - - + def updateDecimation(self): if self.ctrl.maxTracesCheck.isChecked(): numCurves = self.ctrl.maxTracesSpin.value() @@ -979,8 +932,7 @@ class PlotItem(GraphicsWidget): curves[i].clear() self.removeItem(curves[i]) else: - curves[i].hide() - + curves[i].hide() def updateAlpha(self, *args): (alpha, auto) = self.alphaState() @@ -1007,7 +959,6 @@ class PlotItem(GraphicsWidget): else: mode = False return mode - def resizeEvent(self, ev): if self.autoBtn is None: ## already closed down @@ -1016,7 +967,6 @@ class PlotItem(GraphicsWidget): y = self.size().height() - btnRect.height() self.autoBtn.setPos(0, y) - def getMenu(self): return self.ctrlMenu @@ -1050,7 +1000,6 @@ class PlotItem(GraphicsWidget): self.mouseHovering = False self.updateButtons() - def getLabel(self, key): pass @@ -1099,7 +1048,6 @@ class PlotItem(GraphicsWidget): v = (v,) self.setLabel(k, *v) - def showLabel(self, axis, show=True): """ Show or hide one of the plot's axis labels (the axis itself will be unaffected). @@ -1172,8 +1120,6 @@ class PlotItem(GraphicsWidget): raise Exception("X array must be 1D to plot (shape is %s)" % x.shape) c = PlotCurveItem(arr, x=x, **kargs) return c - - def _plotMetaArray(self, arr, x=None, autoLabel=True, **kargs): inf = arr.infoCopy() @@ -1200,17 +1146,12 @@ class PlotItem(GraphicsWidget): self.setLabel('left', text=name, units=units) return c - def setExportMode(self, export, opts=None): GraphicsWidget.setExportMode(self, export, opts) self.updateButtons() - #if export: - #self.autoBtn.hide() - #else: - #self.autoBtn.show() - def _choose_filename_dialog(self, handler): + def _chooseFilenameDialog(self, handler): self.fileDialog = FileDialog() if PlotItem.lastFileDir is not None: self.fileDialog.setDirectory(PlotItem.lastFileDir) From 2d2e548f8e7142014d6031f508c667e109b95b77 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Jun 2018 11:21:17 -0700 Subject: [PATCH 282/607] delegate ROI mouse drag handling to a separate class --- pyqtgraph/graphicsItems/ROI.py | 68 ++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 27 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 9682b6b3..8af04b0d 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -147,6 +147,9 @@ class ROI(GraphicsObject): self.translateSnap = translateSnap self.rotateSnap = rotateSnap self.scaleSnap = scaleSnap + + # Implement mouse handling in a separate class to allow easier customization + self.mouseDragHandler = MouseDragHandler(self) def getState(self): return self.stateCopy() @@ -683,34 +686,8 @@ class ROI(GraphicsObject): QtCore.QTimer.singleShot(0, lambda: self.sigRemoveRequested.emit(self)) def mouseDragEvent(self, ev): - if ev.isStart(): - #p = ev.pos() - #if not self.isMoving and not self.shape().contains(p): - #ev.ignore() - #return - if ev.button() == QtCore.Qt.LeftButton: - self.setSelected(True) - if self.translatable: - self.isMoving = True - self.preMoveState = self.getState() - self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) - self.sigRegionChangeStarted.emit(self) - ev.accept() - else: - ev.ignore() + self.mouseDragHandler.mouseDragEvent(ev) - elif ev.isFinish(): - if self.translatable: - if self.isMoving: - self.stateChangeFinished() - self.isMoving = False - return - - if self.translatable and self.isMoving and ev.buttons() == QtCore.Qt.LeftButton: - snap = True if (ev.modifiers() & QtCore.Qt.ControlModifier) else None - newPos = self.mapToParent(ev.pos()) + self.cursorOffset - self.translate(newPos - self.pos(), snap=snap, finish=False) - def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.isMoving: ev.accept() @@ -1381,6 +1358,43 @@ class Handle(UIGraphicsItem): self.update() +class MouseDragHandler(object): + """Implements default mouse drag behavior for ROI (not for ROI handles). + """ + def __init__(self, roi): + self.roi = roi + + def mouseDragEvent(self, ev): + roi = self.roi + + if ev.isStart(): + if ev.button() == QtCore.Qt.LeftButton: + roi.setSelected(True) + if roi.translatable: + roi.isMoving = True + roi.preMoveState = roi.getState() + roi.cursorOffset = roi.pos() - roi.mapToParent(ev.buttonDownPos()) + roi.sigRegionChangeStarted.emit(roi) + ev.accept() + else: + ev.ignore() + + elif ev.isFinish(): + if roi.translatable: + if roi.isMoving: + roi.stateChangeFinished() + roi.isMoving = False + return + + if roi.translatable and roi.isMoving and ev.buttons() == QtCore.Qt.LeftButton: + snap = True if (ev.modifiers() & QtCore.Qt.ControlModifier) else None + newPos = roi.mapToParent(ev.pos()) + roi.cursorOffset + roi.translate(newPos - roi.pos(), snap=snap, finish=False) + # elif self.rotatable and self.isMoving and (ev.modifiers() & QtCore.Qt.AltModifier): + + + + class TestROI(ROI): def __init__(self, pos, size, **args): ROI.__init__(self, pos, size, **args) From e78693631bd1c2a59fa5cdef321e834819745856 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Jun 2018 13:36:33 -0700 Subject: [PATCH 283/607] Implement handle-free scale/rotation mouse interaction for ROI --- pyqtgraph/graphicsItems/ROI.py | 181 +++++++++++++++++++++++++-------- 1 file changed, 139 insertions(+), 42 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 8af04b0d..26de969c 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -68,13 +68,18 @@ class ROI(GraphicsObject): to be integer multiples of *snapSize* when being resized by the user. Default is False. rotateSnap (bool) If True, the ROI angle is forced to a multiple of - 15 degrees when rotated by the user. Default is False. + the ROI's snap angle (default is 15 degrees) when rotated + by the user. Default is False. parent (QGraphicsItem) The graphics item parent of this ROI. It is generally not necessary to specify the parent. pen (QPen or argument to pg.mkPen) The pen to use when drawing the shape of the ROI. movable (bool) If True, the ROI can be moved by dragging anywhere inside the ROI. Default is True. + rotatable (bool) If True, the ROI can be rotated by mouse drag + ALT + resizable (bool) If True, the ROI can be resized by mouse drag + SHIFT + lockAspect (bool) If True, the aspect ratio of the ROI is locked during + mouse interaction. removable (bool) If True, the ROI will be given a context menu with an option to remove the ROI. The ROI emits sigRemoveRequested when this menu action is selected. @@ -112,14 +117,19 @@ class ROI(GraphicsObject): sigClicked = QtCore.Signal(object, object) sigRemoveRequested = QtCore.Signal(object) - def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False): + def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, + snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, + parent=None, pen=None, movable=True, rotatable=True, resizable=True, + lockAspect=False, removable=False): GraphicsObject.__init__(self, parent) self.setAcceptedMouseButtons(QtCore.Qt.NoButton) pos = Point(pos) size = Point(size) self.aspectLocked = False self.translatable = movable - self.rotateAllowed = True + self.rotatable = rotatable + self.resizable = resizable + self.lockAspect = lockAspect self.removable = removable self.menu = None @@ -146,7 +156,9 @@ class ROI(GraphicsObject): self.snapSize = snapSize self.translateSnap = translateSnap self.rotateSnap = rotateSnap + self.rotateSnapAngle = 15.0 self.scaleSnap = scaleSnap + self.scaleSnapSize = snapSize # Implement mouse handling in a separate class to allow easier customization self.mouseDragHandler = MouseDragHandler(self) @@ -249,45 +261,87 @@ class ROI(GraphicsObject): if update: self.stateChanged(finish=finish) - def setSize(self, size, update=True, finish=True): - """Set the size of the ROI. May be specified as a QPoint, Point, or list of two values. - See setPos() for an explanation of the update and finish arguments. + def setSize(self, size, center=None, centerLocal=None, snap=False, update=True, finish=True): + """ + Set the ROI's size. + + =============== ========================================================================== + **Arguments** + size (Point | QPointF | sequence) The final size of the ROI + center (None | Point) Optional center point around which the ROI is scaled, + expressed as [0-1, 0-1] over the size of the ROI. + centerLocal (None | Point) Same as *center*, but the position is expressed in the + local coordinate system of the ROI + snap (bool) If True, the final size is snapped to the nearest increment (see + ROI.scaleSnapSize) + update (bool) See setPos() + finish (bool) See setPos() + =============== ========================================================================== """ if update not in (True, False): raise TypeError("update argument must be bool") size = Point(size) + + if centerLocal is not None: + center = Point(centerLocal) / self.state['size'] + + if center is not None: + center = Point(center) + c = self.mapToParent(Point(center) * self.state['size']) + c1 = self.mapToParent(Point(center) * size) + newPos = self.state['pos'] + c - c1 + self.setPos(newPos, update=False, finish=False) + self.prepareGeometryChange() self.state['size'] = size if update: self.stateChanged(finish=finish) - def setAngle(self, angle, update=True, finish=True): - """Set the angle of rotation (in degrees) for this ROI. - See setPos() for an explanation of the update and finish arguments. + def setAngle(self, angle, center=None, centerLocal=None, snap=False, update=True, finish=True): + """ + Set the ROI's rotation angle. + + =============== ========================================================================== + **Arguments** + angle (float) The final ROI angle in degrees + center (None | Point) Optional center point around which the ROI is rotated, + expressed as [0-1, 0-1] over the size of the ROI. + centerLocal (None | Point) Same as *center*, but the position is expressed in the + local coordinate system of the ROI + snap (bool) If True, the final ROI angle is snapped to the nearest increment + (default is 15 degrees; see ROI.rotateSnapAngle) + update (bool) See setPos() + finish (bool) See setPos() + =============== ========================================================================== """ if update not in (True, False): raise TypeError("update argument must be bool") + + if snap is True: + angle = round(angle / self.rotateSnapAngle) * self.rotateSnapAngle + self.state['angle'] = angle - tr = QtGui.QTransform() + tr = QtGui.QTransform() # note: only rotation is contained in the transform tr.rotate(angle) + if center is not None: + centerLocal = Point(center) * self.state['size'] + if centerLocal is not None: + centerLocal = Point(centerLocal) + # rotate to new angle, keeping a specific point anchored as the center of rotation + cc = self.mapToParent(centerLocal) - (tr.map(centerLocal) + self.state['pos']) + self.translate(cc, update=False) + self.setTransform(tr) if update: self.stateChanged(finish=finish) - def scale(self, s, center=[0,0], update=True, finish=True): + def scale(self, s, center=None, centerLocal=None, snap=False, update=True, finish=True): """ Resize the ROI by scaling relative to *center*. See setPos() for an explanation of the *update* and *finish* arguments. """ - c = self.mapToParent(Point(center) * self.state['size']) - self.prepareGeometryChange() newSize = self.state['size'] * s - c1 = self.mapToParent(Point(center) * newSize) - newPos = self.state['pos'] + c - c1 - - self.setSize(newSize, update=False) - self.setPos(newPos, update=update, finish=finish) - + self.setSize(newSize, center=center, centerLocal=centerLocal, snap=snap, update=update, finish=finish) def translate(self, *args, **kargs): """ @@ -339,14 +393,22 @@ class ROI(GraphicsObject): finish = kargs.get('finish', True) self.setPos(newState['pos'], update=update, finish=finish) - def rotate(self, angle, update=True, finish=True): + def rotate(self, angle, center=None, snap=False, update=True, finish=True): """ Rotate the ROI by *angle* degrees. - Also accepts *update* and *finish* arguments (see setPos() for a - description of these). + =============== ========================================================================== + **Arguments** + angle (float) The angle in degrees to rotate + center (None | Point) Optional center point around which the ROI is rotated, in + the local coordinate system of the ROI + snap (bool) If True, the final ROI angle is snapped to the nearest increment + (default is 15 degrees; see ROI.rotateSnapAngle) + update (bool) See setPos() + finish (bool) See setPos() + =============== ========================================================================== """ - self.setAngle(self.angle()+angle, update=update, finish=finish) + self.setAngle(self.angle()+angle, center=center, snap=snap, update=update, finish=finish) def handleMoveStarted(self): self.preMoveState = self.getState() @@ -701,6 +763,16 @@ class ROI(GraphicsObject): else: ev.ignore() + def _moveStarted(self): + self.isMoving = True + self.preMoveState = self.getState() + self.sigRegionChangeStarted.emit(self) + + def _moveFinished(self): + if self.isMoving: + self.stateChangeFinished() + self.isMoving = False + def cancelMove(self): self.isMoving = False self.setState(self.preMoveState) @@ -803,7 +875,7 @@ class ROI(GraphicsObject): if h['type'] == 'rf': self.freeHandleMoved = True - if not self.rotateAllowed: + if not self.rotatable: return ## If the handle is directly over its center point, we can't compute an angle. try: @@ -817,7 +889,7 @@ class ROI(GraphicsObject): if ang is None: ## this should never happen.. return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - ang = round(ang / 15.) * 15. ## 180/12 = 15 + ang = round(ang / self.rotateSnapAngle) * self.rotateSnapAngle ## create rotation transform tr = QtGui.QTransform() @@ -859,7 +931,7 @@ class ROI(GraphicsObject): if ang is None: return if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): - ang = round(ang / 15.) * 15. + ang = round(ang / self.rotateSnapAngle) * self.rotateSnapAngle hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) newState['size'][scaleAxis] = lp1.length() / hs @@ -1363,6 +1435,12 @@ class MouseDragHandler(object): """ def __init__(self, roi): self.roi = roi + self.dragMode = None + self.startState = None + self.snapModifier = QtCore.Qt.ControlModifier + self.translateModifier = QtCore.Qt.NoModifier + self.rotateModifier = QtCore.Qt.AltModifier + self.scaleModifier = QtCore.Qt.ShiftModifier def mouseDragEvent(self, ev): roi = self.roi @@ -1370,29 +1448,48 @@ class MouseDragHandler(object): if ev.isStart(): if ev.button() == QtCore.Qt.LeftButton: roi.setSelected(True) - if roi.translatable: - roi.isMoving = True - roi.preMoveState = roi.getState() - roi.cursorOffset = roi.pos() - roi.mapToParent(ev.buttonDownPos()) - roi.sigRegionChangeStarted.emit(roi) + mods = ev.modifiers() & ~self.snapModifier + if roi.translatable and mods == self.translateModifier: + self.dragMode = 'translate' + elif roi.rotatable and mods == self.rotateModifier: + self.dragMode = 'rotate' + elif roi.resizable and mods == self.scaleModifier: + self.dragMode = 'scale' + else: + self.dragMode = None + + if self.dragMode is not None: + roi._moveStarted() + self.startPos = roi.mapToParent(ev.buttonDownPos()) + self.startState = roi.saveState() + self.cursorOffset = roi.pos() - self.startPos ev.accept() else: ev.ignore() + else: + self.dragMode = None + ev.ignore() - elif ev.isFinish(): - if roi.translatable: - if roi.isMoving: - roi.stateChangeFinished() - roi.isMoving = False + + if ev.isFinish() and self.dragMode is not None: + roi._moveFinished() return - if roi.translatable and roi.isMoving and ev.buttons() == QtCore.Qt.LeftButton: - snap = True if (ev.modifiers() & QtCore.Qt.ControlModifier) else None - newPos = roi.mapToParent(ev.pos()) + roi.cursorOffset - roi.translate(newPos - roi.pos(), snap=snap, finish=False) - # elif self.rotatable and self.isMoving and (ev.modifiers() & QtCore.Qt.AltModifier): - + # roi.isMoving becomes False if the move was cancelled by right-click + if not roi.isMoving or self.dragMode is None: + return + snap = True if (ev.modifiers() & self.snapModifier) else None + pos = roi.mapToParent(ev.pos()) + if self.dragMode == 'translate': + newPos = pos + self.cursorOffset + roi.translate(newPos - roi.pos(), snap=snap, finish=False) + elif self.dragMode == 'rotate': + diff = (ev.scenePos() - ev.buttonDownScenePos()).y() + roi.setAngle(self.startState['angle'] + diff, centerLocal=ev.buttonDownPos(), snap=snap, finish=False) + elif self.dragMode == 'scale': + diff = 1.01 ** -(ev.scenePos() - ev.buttonDownScenePos()).y() + roi.setSize(Point(self.startState['size']) * diff, centerLocal=ev.buttonDownPos(), snap=snap, finish=False) class TestROI(ROI): From 060d2479f922fcc5631f50bdb4f1a1cef9dce48e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 15 Jun 2018 14:18:38 -0700 Subject: [PATCH 284/607] minor fixes --- examples/ROIExamples.py | 2 +- pyqtgraph/graphicsItems/ROI.py | 30 +++++++++++++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/examples/ROIExamples.py b/examples/ROIExamples.py index 2b922359..fe3e4db8 100644 --- a/examples/ROIExamples.py +++ b/examples/ROIExamples.py @@ -138,7 +138,7 @@ label4 = w4.addLabel(text, row=0, col=0) v4 = w4.addViewBox(row=1, col=0, lockAspect=True) g = pg.GridItem() v4.addItem(g) -r4 = pg.ROI([0,0], [100,100], removable=True) +r4 = pg.ROI([0,0], [100,100], resizable=False, removable=True) r4.addRotateHandle([1,0], [0.5, 0.5]) r4.addRotateHandle([0,1], [0.5, 0.5]) img4 = pg.ImageItem(arr) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 26de969c..7568d000 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -42,9 +42,7 @@ class ROI(GraphicsObject): rotate/translate/scale handles. ROIs can be customized to have a variety of shapes (by subclassing or using any of the built-in subclasses) and any combination of draggable handles - that allow the user to manipulate the ROI. - - + that allow the user to manipulate the ROI. ================ =========================================================== **Arguments** @@ -78,8 +76,6 @@ class ROI(GraphicsObject): inside the ROI. Default is True. rotatable (bool) If True, the ROI can be rotated by mouse drag + ALT resizable (bool) If True, the ROI can be resized by mouse drag + SHIFT - lockAspect (bool) If True, the aspect ratio of the ROI is locked during - mouse interaction. removable (bool) If True, the ROI will be given a context menu with an option to remove the ROI. The ROI emits sigRemoveRequested when this menu action is selected. @@ -120,7 +116,7 @@ class ROI(GraphicsObject): def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, rotatable=True, resizable=True, - lockAspect=False, removable=False): + removable=False): GraphicsObject.__init__(self, parent) self.setAcceptedMouseButtons(QtCore.Qt.NoButton) pos = Point(pos) @@ -129,7 +125,6 @@ class ROI(GraphicsObject): self.translatable = movable self.rotatable = rotatable self.resizable = resizable - self.lockAspect = lockAspect self.removable = removable self.menu = None @@ -281,9 +276,15 @@ class ROI(GraphicsObject): if update not in (True, False): raise TypeError("update argument must be bool") size = Point(size) + if snap: + size[0] = round(size[0] / self.scaleSnapSize) * self.scaleSnapSize + size[1] = round(size[1] / self.scaleSnapSize) * self.scaleSnapSize if centerLocal is not None: - center = Point(centerLocal) / self.state['size'] + oldSize = Point(self.state['size']) + oldSize[0] = 1 if oldSize[0] == 0 else oldSize[0] + oldSize[1] = 1 if oldSize[1] == 0 else oldSize[1] + center = Point(centerLocal) / oldSize if center is not None: center = Point(center) @@ -826,8 +827,8 @@ class ROI(GraphicsObject): ## snap if self.scaleSnap or (modifiers & QtCore.Qt.ControlModifier): - lp1[0] = round(lp1[0] / self.snapSize) * self.snapSize - lp1[1] = round(lp1[1] / self.snapSize) * self.snapSize + lp1[0] = round(lp1[0] / self.scaleSnapSize) * self.scaleSnapSize + lp1[1] = round(lp1[1] / self.scaleSnapSize) * self.scaleSnapSize ## preserve aspect ratio (this can override snapping) if h['lockAspect'] or (modifiers & QtCore.Qt.AltModifier): @@ -1441,6 +1442,8 @@ class MouseDragHandler(object): self.translateModifier = QtCore.Qt.NoModifier self.rotateModifier = QtCore.Qt.AltModifier self.scaleModifier = QtCore.Qt.ShiftModifier + self.rotateSpeed = 0.7 + self.scaleSpeed = 1.01 def mouseDragEvent(self, ev): roi = self.roi @@ -1485,10 +1488,11 @@ class MouseDragHandler(object): newPos = pos + self.cursorOffset roi.translate(newPos - roi.pos(), snap=snap, finish=False) elif self.dragMode == 'rotate': - diff = (ev.scenePos() - ev.buttonDownScenePos()).y() - roi.setAngle(self.startState['angle'] + diff, centerLocal=ev.buttonDownPos(), snap=snap, finish=False) + diff = self.rotateSpeed * (ev.scenePos() - ev.buttonDownScenePos()).x() + angle = self.startState['angle'] - diff + roi.setAngle(angle, centerLocal=ev.buttonDownPos(), snap=snap, finish=False) elif self.dragMode == 'scale': - diff = 1.01 ** -(ev.scenePos() - ev.buttonDownScenePos()).y() + diff = self.scaleSpeed ** -(ev.scenePos() - ev.buttonDownScenePos()).y() roi.setSize(Point(self.startState['size']) * diff, centerLocal=ev.buttonDownPos(), snap=snap, finish=False) From 6a170519bf273be026264c611c45ca9a79bafcbe Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 18 Jun 2018 13:52:16 -0700 Subject: [PATCH 285/607] minor edits --- pyqtgraph/graphicsItems/ROI.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 7568d000..84a8d0bd 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -42,8 +42,22 @@ class ROI(GraphicsObject): rotate/translate/scale handles. ROIs can be customized to have a variety of shapes (by subclassing or using any of the built-in subclasses) and any combination of draggable handles - that allow the user to manipulate the ROI. - + that allow the user to manipulate the ROI. + + Default mouse interaction: + + * Left drag moves the ROI + * Left drag + Ctrl moves the ROI with position snapping + * Left drag + Alt rotates the ROI + * Left drag + Alt + Ctrl rotates the ROI with angle snapping + * Left drag + Shift scales the ROI + * Left drag + Shift + Ctrl scales the ROI with size snapping + + In addition to the above interaction modes, it is possible to attach any + number of handles to the ROI that can be dragged to change the ROI in + various ways (see the ROI.add____Handle methods). + + ================ =========================================================== **Arguments** pos (length-2 sequence) Indicates the position of the ROI's @@ -75,7 +89,8 @@ class ROI(GraphicsObject): movable (bool) If True, the ROI can be moved by dragging anywhere inside the ROI. Default is True. rotatable (bool) If True, the ROI can be rotated by mouse drag + ALT - resizable (bool) If True, the ROI can be resized by mouse drag + SHIFT + resizable (bool) If True, the ROI can be resized by mouse drag + + SHIFT removable (bool) If True, the ROI will be given a context menu with an option to remove the ROI. The ROI emits sigRemoveRequested when this menu action is selected. @@ -1442,7 +1457,7 @@ class MouseDragHandler(object): self.translateModifier = QtCore.Qt.NoModifier self.rotateModifier = QtCore.Qt.AltModifier self.scaleModifier = QtCore.Qt.ShiftModifier - self.rotateSpeed = 0.7 + self.rotateSpeed = 0.5 self.scaleSpeed = 1.01 def mouseDragEvent(self, ev): From 929123ff465a2e6c1ad167eacaea1ba4b2302b90 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 18 Jun 2018 13:54:43 -0700 Subject: [PATCH 286/607] avoid repeated signal emission in filterwidget --- pyqtgraph/widgets/DataFilterWidget.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/widgets/DataFilterWidget.py b/pyqtgraph/widgets/DataFilterWidget.py index 7b03725c..6421d71b 100644 --- a/pyqtgraph/widgets/DataFilterWidget.py +++ b/pyqtgraph/widgets/DataFilterWidget.py @@ -24,16 +24,13 @@ class DataFilterWidget(ptree.ParameterTree): self.params = DataFilterParameter() self.setParameters(self.params) - self.params.sigTreeStateChanged.connect(self.filterChanged) + self.params.sigFilterChanged.connect(self.sigFilterChanged) self.setFields = self.params.setFields self.generateMask = self.params.generateMask self.filterData = self.params.filterData self.describe = self.params.describe - def filterChanged(self): - self.sigFilterChanged.emit(self) - def parameters(self): return self.params @@ -80,15 +77,17 @@ class DataFilterParameter(ptree.types.GroupParameter): ('field3', {'mode': 'enum', 'values': {'val1':True, 'val2':False, 'val3':True}}), ]) """ - self.fields = OrderedDict(fields) - names = self.fieldNames() - self.setAddList(names) + with fn.SignalBlock(self.sigTreeStateChanged, self.filterChanged): + self.fields = OrderedDict(fields) + names = self.fieldNames() + self.setAddList(names) - # update any existing filters - for ch in self.children(): - name = ch.fieldName - if name in fields: - ch.updateFilter(fields[name]) + # update any existing filters + for ch in self.children(): + name = ch.fieldName + if name in fields: + ch.updateFilter(fields[name]) + self.sigFilterChanged.emit(self) def filterData(self, data): if len(data) == 0: From f2baf31d5159acd66eb13a3a7dd0cc0ea4d2c116 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 20 Jun 2018 13:01:52 -0700 Subject: [PATCH 287/607] Fix console input up/down arrows In some cases, pressing down past the end of the command history would not clear the input --- pyqtgraph/console/CmdInput.py | 42 +++++++++-------------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/pyqtgraph/console/CmdInput.py b/pyqtgraph/console/CmdInput.py index 24a01e89..21d25382 100644 --- a/pyqtgraph/console/CmdInput.py +++ b/pyqtgraph/console/CmdInput.py @@ -9,19 +9,18 @@ class CmdInput(QtGui.QLineEdit): QtGui.QLineEdit.__init__(self, parent) self.history = [""] self.ptr = 0 - #self.lastCmd = None - #self.setMultiline(False) def keyPressEvent(self, ev): - #print "press:", ev.key(), QtCore.Qt.Key_Up, QtCore.Qt.Key_Down, QtCore.Qt.Key_Enter - if ev.key() == QtCore.Qt.Key_Up and self.ptr < len(self.history) - 1: - self.setHistory(self.ptr+1) - ev.accept() - return - elif ev.key() == QtCore.Qt.Key_Down and self.ptr > 0: - self.setHistory(self.ptr-1) - ev.accept() - return + if ev.key() == QtCore.Qt.Key_Up: + if self.ptr < len(self.history) - 1: + self.setHistory(self.ptr+1) + ev.accept() + return + elif ev.key() == QtCore.Qt.Key_Down: + if self.ptr > 0: + self.setHistory(self.ptr-1) + ev.accept() + return elif ev.key() == QtCore.Qt.Key_Return: self.execCmd() else: @@ -32,7 +31,6 @@ class CmdInput(QtGui.QLineEdit): cmd = asUnicode(self.text()) if len(self.history) == 1 or cmd != self.history[1]: self.history.insert(1, cmd) - #self.lastCmd = cmd self.history[0] = "" self.setHistory(0) self.sigExecuteCmd.emit(cmd) @@ -40,23 +38,3 @@ class CmdInput(QtGui.QLineEdit): def setHistory(self, num): self.ptr = num self.setText(self.history[self.ptr]) - - #def setMultiline(self, m): - #height = QtGui.QFontMetrics(self.font()).lineSpacing() - #if m: - #self.setFixedHeight(height*5) - #else: - #self.setFixedHeight(height+15) - #self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - #self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - - - #def sizeHint(self): - #hint = QtGui.QPlainTextEdit.sizeHint(self) - #height = QtGui.QFontMetrics(self.font()).lineSpacing() - #hint.setHeight(height) - #return hint - - - - \ No newline at end of file From b9f6a2888472f8c1f3699bcc50c357cf4156a183 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 20 Jun 2018 13:02:39 -0700 Subject: [PATCH 288/607] Add ConsoleWidget.write(scrollToBottom) argument --- pyqtgraph/console/Console.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index c30a392c..430ab237 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -101,7 +101,6 @@ class ConsoleWidget(QtGui.QWidget): pickle.dump(open(self.historyFile, 'wb'), history) def runCmd(self, cmd): - #cmd = str(self.input.lastCmd) self.stdout = sys.stdout self.stderr = sys.stderr encCmd = re.sub(r'>', '>', re.sub(r'<', '<', cmd)) @@ -114,22 +113,20 @@ class ConsoleWidget(QtGui.QWidget): sys.stdout = self sys.stderr = self if self.multiline is not None: - self.write("
%s\n"%encCmd, html=True) + self.write("
%s\n"%encCmd, html=True, scrollToBottom=True) self.execMulti(cmd) else: - self.write("
%s\n"%encCmd, html=True) + self.write("
%s\n"%encCmd, html=True, scrollToBottom=True) self.inCmd = True self.execSingle(cmd) if not self.inCmd: - self.write("
\n", html=True) + self.write("
\n", html=True, scrollToBottom=True) finally: sys.stdout = self.stdout sys.stderr = self.stderr - sb = self.output.verticalScrollBar() - sb.setValue(sb.maximum()) sb = self.ui.historyList.verticalScrollBar() sb.setValue(sb.maximum()) @@ -201,11 +198,23 @@ class ConsoleWidget(QtGui.QWidget): self.displayException() self.multiline = None - def write(self, strn, html=False): + def write(self, strn, html=False, scrollToBottom='auto'): + """Write a string into the console. + + If scrollToBottom is 'auto', then the console is automatically scrolled + to fit the new text only if it was already at the bottom. + """ isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() if not isGuiThread: self.stdout.write(strn) return + + sb = self.output.verticalScrollBar() + scroll = sb.value() + if scrollToBottom == 'auto': + atBottom = scroll == sb.maximum() + scrollToBottom = atBottom + self.output.moveCursor(QtGui.QTextCursor.End) if html: self.output.textCursor().insertHtml(strn) @@ -213,10 +222,13 @@ class ConsoleWidget(QtGui.QWidget): if self.inCmd: self.inCmd = False self.output.textCursor().insertHtml("

") - #self.stdout.write("

") self.output.insertPlainText(strn) - #self.stdout.write(strn) - + + if scrollToBottom: + sb.setValue(sb.maximum()) + else: + sb.setValue(scroll) + def displayException(self): """ Display the current exception and stack. From 3fd56eb195b10e209c3c00376b37408328fce58f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 22 Jun 2018 17:26:31 -0700 Subject: [PATCH 289/607] Handle Qt4/5 API difference in filedialog --- examples/relativity/relativity.py | 16 ++++++++++------ pyqtgraph/imageview/ImageView.py | 2 ++ pyqtgraph/widgets/TableWidget.py | 2 ++ 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/examples/relativity/relativity.py b/examples/relativity/relativity.py index e3f2c435..726b49b4 100644 --- a/examples/relativity/relativity.py +++ b/examples/relativity/relativity.py @@ -159,17 +159,21 @@ class RelativityGUI(QtGui.QWidget): self.setAnimation(self.params['Animate']) def save(self): - fn = str(pg.QtGui.QFileDialog.getSaveFileName(self, "Save State..", "untitled.cfg", "Config Files (*.cfg)")) - if fn == '': + filename = str(pg.QtGui.QFileDialog.getSaveFileName(self, "Save State..", "untitled.cfg", "Config Files (*.cfg)")) + if isinstance(filename, tuple): + filename = filename[0] # Qt4/5 API difference + if filename == '': return state = self.params.saveState() - pg.configfile.writeConfigFile(state, fn) + pg.configfile.writeConfigFile(state, filename) def load(self): - fn = str(pg.QtGui.QFileDialog.getOpenFileName(self, "Save State..", "", "Config Files (*.cfg)")) - if fn == '': + filename = str(pg.QtGui.QFileDialog.getOpenFileName(self, "Save State..", "", "Config Files (*.cfg)")) + if isinstance(filename, tuple): + filename = filename[0] # Qt4/5 API difference + if filename == '': return - state = pg.configfile.readConfigFile(fn) + state = pg.configfile.readConfigFile(filename) self.loadState(state) def loadPreset(self, param, preset): diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 40a3987a..ca3ac416 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -790,6 +790,8 @@ class ImageView(QtGui.QWidget): def exportClicked(self): fileName = QtGui.QFileDialog.getSaveFileName() + if isinstance(fileName, tuple): + fileName = fileName[0] # Qt4/5 API difference if fileName == '': return self.export(fileName) diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index d1bec16b..66fdea8e 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -351,6 +351,8 @@ class TableWidget(QtGui.QTableWidget): def save(self, data): fileName = QtGui.QFileDialog.getSaveFileName(self, "Save As..", "", "Tab-separated values (*.tsv)") + if isinstance(fileName, tuple): + fileName = fileName[0] # Qt4/5 API difference if fileName == '': return open(fileName, 'w').write(data) From 08349351da13c7108b43ad6eddace08d3eab0269 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 22 Jun 2018 17:31:58 -0700 Subject: [PATCH 290/607] Fix QString handling --- examples/relativity/relativity.py | 8 ++++---- pyqtgraph/imageview/ImageView.py | 2 +- pyqtgraph/widgets/TableWidget.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/relativity/relativity.py b/examples/relativity/relativity.py index 726b49b4..98ef524e 100644 --- a/examples/relativity/relativity.py +++ b/examples/relativity/relativity.py @@ -159,21 +159,21 @@ class RelativityGUI(QtGui.QWidget): self.setAnimation(self.params['Animate']) def save(self): - filename = str(pg.QtGui.QFileDialog.getSaveFileName(self, "Save State..", "untitled.cfg", "Config Files (*.cfg)")) + filename = pg.QtGui.QFileDialog.getSaveFileName(self, "Save State..", "untitled.cfg", "Config Files (*.cfg)") if isinstance(filename, tuple): filename = filename[0] # Qt4/5 API difference if filename == '': return state = self.params.saveState() - pg.configfile.writeConfigFile(state, filename) + pg.configfile.writeConfigFile(state, str(filename)) def load(self): - filename = str(pg.QtGui.QFileDialog.getOpenFileName(self, "Save State..", "", "Config Files (*.cfg)")) + filename = pg.QtGui.QFileDialog.getOpenFileName(self, "Save State..", "", "Config Files (*.cfg)") if isinstance(filename, tuple): filename = filename[0] # Qt4/5 API difference if filename == '': return - state = pg.configfile.readConfigFile(filename) + state = pg.configfile.readConfigFile(str(filename)) self.loadState(state) def loadPreset(self, param, preset): diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index ca3ac416..2b43b940 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -794,7 +794,7 @@ class ImageView(QtGui.QWidget): fileName = fileName[0] # Qt4/5 API difference if fileName == '': return - self.export(fileName) + self.export(str(fileName)) def buildMenu(self): self.menu = QtGui.QMenu() diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 66fdea8e..90b56139 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -355,7 +355,7 @@ class TableWidget(QtGui.QTableWidget): fileName = fileName[0] # Qt4/5 API difference if fileName == '': return - open(fileName, 'w').write(data) + open(str(fileName), 'w').write(data) def contextMenuEvent(self, ev): self.contextMenu.popup(ev.globalPos()) From f3583ed33829ffadec7b8f931aa916134433959f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 3 Jul 2018 13:43:28 -0700 Subject: [PATCH 291/607] Make console handle exceptions from non-gui threads a little more carefully --- pyqtgraph/console/Console.py | 38 +++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 430ab237..477beb77 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -5,6 +5,7 @@ from ..Qt import QtCore, QtGui, QT_LIB from ..python2_3 import basestring from .. import exceptionHandling as exceptionHandling from .. import getConfigOption +from ..functions import SignalBlock if QT_LIB == 'PySide': from . import template_pyside as template elif QT_LIB == 'PySide2': @@ -33,6 +34,7 @@ class ConsoleWidget(QtGui.QWidget): - ability to add extra features like exception stack introspection - ability to have multiple interactive prompts, including for spawned sub-processes """ + _threadException = QtCore.Signal(object) def __init__(self, parent=None, namespace=None, historyFile=None, text=None, editor=None): """ @@ -89,6 +91,9 @@ class ConsoleWidget(QtGui.QWidget): self.ui.onlyUncaughtCheck.toggled.connect(self.updateSysTrace) self.currentTraceback = None + + # send exceptions raised in non-gui threads back to the main thread by signal. + self._threadException.connect(self._threadExceptionHandler) def loadHistory(self): """Return the list of previously-invoked command strings (or None).""" @@ -260,9 +265,12 @@ class ConsoleWidget(QtGui.QWidget): If True, the console will catch all unhandled exceptions and display the stack trace. Each exception caught clears the last. """ - self.ui.catchAllExceptionsBtn.setChecked(catch) + with SignalBlock(self.ui.catchAllExceptionsBtn.toggled, self.catchAllExceptions): + self.ui.catchAllExceptionsBtn.setChecked(catch) + if catch: - self.ui.catchNextExceptionBtn.setChecked(False) + with SignalBlock(self.ui.catchNextExceptionBtn.toggled, self.catchNextException): + self.ui.catchNextExceptionBtn.setChecked(False) self.enableExceptionHandling() self.ui.exceptionBtn.setChecked(True) else: @@ -273,9 +281,11 @@ class ConsoleWidget(QtGui.QWidget): If True, the console will catch the next unhandled exception and display the stack trace. """ - self.ui.catchNextExceptionBtn.setChecked(catch) + with SignalBlock(self.ui.catchNextExceptionBtn.toggled, self.catchNextException): + self.ui.catchNextExceptionBtn.setChecked(catch) if catch: - self.ui.catchAllExceptionsBtn.setChecked(False) + with SignalBlock(self.ui.catchAllExceptionsBtn.toggled, self.catchAllExceptions): + self.ui.catchAllExceptionsBtn.setChecked(False) self.enableExceptionHandling() self.ui.exceptionBtn.setChecked(True) else: @@ -328,7 +338,18 @@ class ConsoleWidget(QtGui.QWidget): else: sys.settrace(self.systrace) - def exceptionHandler(self, excType, exc, tb, systrace=False): + def exceptionHandler(self, excType, exc, tb, systrace=False, frame=None): + if frame is None: + frame = sys._getframe() + + # exceptions raised in non-gui threads must be handled separately + isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() + if not isGuiThread: + # sending a frame from one thread to another.. probably not safe, but better than just + # dropping the exception? + self._threadException.emit((excType, exc, tb, systrace, frame.f_back)) + return + if self.ui.catchNextExceptionBtn.isChecked(): self.ui.catchNextExceptionBtn.setChecked(False) elif not self.ui.catchAllExceptionsBtn.isChecked(): @@ -342,10 +363,13 @@ class ConsoleWidget(QtGui.QWidget): if systrace: # exceptions caught using systrace don't need the usual # call stack + traceback handling - self.setStack(sys._getframe().f_back.f_back) + self.setStack(frame.f_back.f_back) else: - self.setStack(frame=sys._getframe().f_back, tb=tb) + self.setStack(frame=frame.f_back, tb=tb) + def _threadExceptionHandler(self, args): + self.exceptionHandler(*args) + def setStack(self, frame=None, tb=None): """Display a call stack and exception traceback. From 56950479f9eee3c9685cfb565b34c34225bd6ae8 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Fri, 6 Jul 2018 15:53:28 +0200 Subject: [PATCH 292/607] SVGExport: Initialize option.exposedRect for items that use it --- pyqtgraph/exporters/SVGExporter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index b1569b74..dcd95c2b 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -180,7 +180,10 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): item.setExportMode(True, {'painter': p}) try: p.setTransform(tr) - item.paint(p, QtGui.QStyleOptionGraphicsItem(), None) + opt = QtGui.QStyleOptionGraphicsItem() + if item.flags() & QtGui.QGraphicsItem.ItemUsesExtendedStyleOption: + opt.exposedRect = item.boundingRect() + item.paint(p, opt, None) finally: p.end() ## Can't do this here--we need to wait until all children have painted as well. From f8efbec53f10ec0dd5240528616037cc3aa40a31 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Sat, 7 Jul 2018 00:10:15 -0700 Subject: [PATCH 293/607] Add image hover callback to imageAnalysis example --- examples/imageAnalysis.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/examples/imageAnalysis.py b/examples/imageAnalysis.py index 13adf5ac..da753e34 100644 --- a/examples/imageAnalysis.py +++ b/examples/imageAnalysis.py @@ -21,7 +21,7 @@ win = pg.GraphicsLayoutWidget() win.setWindowTitle('pyqtgraph example: Image Analysis') # A plot area (ViewBox + axes) for displaying the image -p1 = win.addPlot() +p1 = win.addPlot(title="") # Item for displaying image data img = pg.ImageItem() @@ -93,6 +93,26 @@ def updateIsocurve(): isoLine.sigDragged.connect(updateIsocurve) +def imageHoverEvent(event): + """Show the position, pixel, and value under the mouse cursor. + """ + if event.isExit(): + p1.setTitle("") + return + pos = event.pos() + i, j = pos.y(), pos.x() + i = int(np.clip(i, 0, data.shape[0] - 1)) + j = int(np.clip(j, 0, data.shape[1] - 1)) + val = data[i, j] + ppos = img.mapToParent(pos) + x, y = ppos.x(), ppos.y() + p1.setTitle("pos: (%0.1f, %0.1f) pixel: (%d, %d) value: %g" % (x, y, i, j, val)) + +# Monkey-patch the image to use our custom hover function. +# This is generally discouraged (you should subclass ImageItem instead), +# but it works for a very simple use like this. +img.hoverEvent = imageHoverEvent + ## Start Qt event loop unless running in interactive mode or using pyside. if __name__ == '__main__': From 585390e16ebbc7eb993c78216cc88e439c5bfe60 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 10 Jul 2018 05:23:53 -0700 Subject: [PATCH 294/607] fix: mouse wheel on viewbox leaves autorange unaffected if mouse is disabled for one axis --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 34 +++++++++------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 02a8ddad..b1cd3c10 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -662,21 +662,13 @@ class ViewBox(GraphicsWidget): cause slight changes due to floating-point error). """ if s is not None: - scale = Point(s) - else: - scale = [x, y] + x, y = s[0], s[1] - affect = [True, True] - if scale[0] is None and scale[1] is None: + affect = [x is not None, y is not None] + if not any(affect): return - elif scale[0] is None: - affect[0] = False - scale[0] = 1.0 - elif scale[1] is None: - affect[1] = False - scale[1] = 1.0 - - scale = Point(scale) + + scale = Point([1.0 if x is None else x, 1.0 if y is None else y]) if self.state['aspectLocked'] is not False: scale[0] = scale[1] @@ -1132,19 +1124,19 @@ class ViewBox(GraphicsWidget): """Return the bounding rect of the item in view coordinates""" return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() - def wheelEvent(self, ev, axis=None): - mask = np.array(self.state['mouseEnabled'], dtype=np.float) + def wheelEvent(self, ev, axis=None): if axis is not None and axis >= 0 and axis < len(mask): - mv = mask[axis] - mask[:] = 0 - mask[axis] = mv - s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor - + mask = [False, False] + mask[axis] = self.state['mouseEnabled'][axis] + else: + mask = self.state['mouseEnabled'][:] + s = 1.02 ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor + s = [(None if m is False else s) for m in mask] center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) self._resetTarget() self.scaleBy(s, center) - self.sigRangeChangedManually.emit(self.state['mouseEnabled']) + self.sigRangeChangedManually.emit(mask) ev.accept() def mouseClickEvent(self, ev): From 2884eef0796351555b4cc80abcb070b00f187be0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 10 Jul 2018 05:56:35 -0700 Subject: [PATCH 295/607] Add ViewBoxFeatures to example menu --- examples/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/utils.py b/examples/utils.py index b004a0d3..f7786dba 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -19,6 +19,7 @@ examples = OrderedDict([ ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), ('Image Analysis', 'imageAnalysis.py'), + ('ViewBox Features', 'ViewBoxFeatures.py'), ('Dock widgets', 'dockarea.py'), ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), @@ -50,7 +51,7 @@ examples = OrderedDict([ ('Text Item', 'text.py'), ('Linked Views', 'linkedViews.py'), ('Arrow', 'Arrow.py'), - ('ViewBox', 'ViewBox.py'), + ('ViewBox', 'ViewBoxFeatures.py'), ('Custom Graphics', 'customGraphicsItem.py'), ('Labeled Graph', 'CustomGraphItem.py'), ])), From a240766607f63c55de18462df7817814f01ba1b0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 10 Jul 2018 16:14:59 -0700 Subject: [PATCH 296/607] add pip/git install method to README --- README.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index a0048103..a8742066 100644 --- a/README.md +++ b/README.md @@ -34,13 +34,14 @@ Support Installation Methods -------------------- - * From pypi: - `pip install pyqtgraph` - * To use with a specific project, simply copy the pyqtgraph subdirectory - anywhere that is importable from your project. - * To install system-wide from source distribution: - `$ python setup.py install` - * For installation packages, see the website (pyqtgraph.org) +* From pypi: + - Last released version: `pip install pyqtgraph` + - Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph` +* To install system-wide from source distribution: `python setup.py install` +* Many linux package repositories have release versions. +* To use with a specific project, simply copy the pyqtgraph subdirectory + anywhere that is importable from your project. +* For installation packages, see the website (pyqtgraph.org) Documentation ------------- From fe7e1775da21e2af7c172f6a3c00509ddd861351 Mon Sep 17 00:00:00 2001 From: David Nadlinger Date: Wed, 11 Jul 2018 15:31:29 +0100 Subject: [PATCH 297/607] AxisItem: Fix tick elision with self.scale set Previously, only the highest level ticks would be displayed for e.g. scale = 1e3. --- pyqtgraph/graphicsItems/AxisItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index b125cb7e..19c5e1f0 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -693,7 +693,7 @@ class AxisItem(GraphicsWidget): ## remove any ticks that were present in higher levels ## we assume here that if the difference between a tick value and a previously seen tick value ## is less than spacing/100, then they are 'equal' and we can ignore the new tick. - values = list(filter(lambda x: all(np.abs(allValues-x) > spacing*0.01), values) ) + values = list(filter(lambda x: all(np.abs(allValues-x) > spacing/self.scale*0.01), values)) allValues = np.concatenate([allValues, values]) ticks.append((spacing/self.scale, values)) From 6826e2615ce13a1a73cf3f5c3448e2b29ed319f7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 25 Jul 2018 10:07:24 -0700 Subject: [PATCH 298/607] Fix exit error caused by callback invoked during Qt teardown --- pyqtgraph/graphicsItems/GraphicsItem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index f88069bc..628b495b 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -445,6 +445,10 @@ class GraphicsItem(object): ## called to see whether this item has a new view to connect to ## NOTE: This is called from GraphicsObject.itemChange or GraphicsWidget.itemChange. + if not hasattr(self, '_connectedView'): + # Happens when Python is shutting down. + return + ## It is possible this item has moved to a different ViewBox or widget; ## clear out previously determined references to these. self.forgetViewBox() From c238be004ebb4a16917dc53cf5f53a6b52a4be1e Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Thu, 15 Dec 2016 09:26:19 -0500 Subject: [PATCH 299/607] add test case for the PlotDataItem.clear() in stepMode --- pyqtgraph/graphicsItems/tests/test_PlotDataItem.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index 8851a0a2..b506a654 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -9,16 +9,16 @@ def test_fft(): x = np.linspace(0, 1, 1000) y = np.sin(2 * np.pi * f * x) pd = pg.PlotDataItem(x, y) - pd.setFftMode(True) + pd.setFftMode(True) x, y = pd.getData() assert abs(x[np.argmax(y)] - f) < 0.03 - + x = np.linspace(0, 1, 1001) y = np.sin(2 * np.pi * f * x) pd.setData(x, y) x, y = pd.getData() assert abs(x[np.argmax(y)]- f) < 0.03 - + pd.setLogMode(True, False) x, y = pd.getData() assert abs(x[np.argmax(y)] - np.log10(f)) < 0.01 @@ -58,3 +58,9 @@ def test_clear(): assert pdi.xData == None assert pdi.yData == None + +def test_clear_in_step_mode(): + w = pg.PlotWidget() + c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode=True) + w.addItem(c) + c.clear() From b13062f081464d6950990d7e3add969e43e7d934 Mon Sep 17 00:00:00 2001 From: Mikhail Terekhov Date: Wed, 14 Dec 2016 22:39:39 -0500 Subject: [PATCH 300/607] In PlotDataItem.clear() use corresponding curve.clear() and scatter.clear() Otherwise when stepMode is True curve.setData([]) causes exception: "len(X) must be len(Y)+1 ..." --- pyqtgraph/graphicsItems/PlotDataItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 2faa9ac1..1bf48f5d 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -627,9 +627,9 @@ class PlotDataItem(GraphicsObject): #self.yClean = None self.xDisp = None self.yDisp = None - self.curve.setData([]) - self.scatter.setData([]) - + self.curve.clear() + self.scatter.clear() + def appendData(self, *args, **kargs): pass From f1b51027cc44377133d9c2a739e007c1e047948f Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 30 Jul 2018 11:36:08 -0700 Subject: [PATCH 301/607] Fix error in ViewBox wheelEvent when mouse has 2-axis wheel events --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index b1cd3c10..0982cb37 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1124,8 +1124,8 @@ class ViewBox(GraphicsWidget): """Return the bounding rect of the item in view coordinates""" return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() - def wheelEvent(self, ev, axis=None): - if axis is not None and axis >= 0 and axis < len(mask): + def wheelEvent(self, ev, axis=None): + if axis in (0, 1): mask = [False, False] mask[axis] = self.state['mouseEnabled'][axis] else: From 839ad486f854c5c4aba2205a1fcf41c4e7524c19 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 11 Sep 2018 07:14:28 -0700 Subject: [PATCH 302/607] Flush shared memory file before attempting to map This avoids the error "ValueError: mmap length is greater than file size" on OSX. (see #730) --- pyqtgraph/widgets/RemoteGraphicsView.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index a1674cc2..edf4db3c 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -152,6 +152,7 @@ class Renderer(GraphicsView): else: self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') self.shmFile.write(b'\x00' * (mmap.PAGESIZE+1)) + self.shmFile.flush() fd = self.shmFile.fileno() self.shm = mmap.mmap(fd, mmap.PAGESIZE, mmap.MAP_SHARED, mmap.PROT_WRITE) atexit.register(self.close) From 6b26245e506e9c7140f1acad204a5490ef9dff26 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 28 Sep 2018 16:02:14 -0700 Subject: [PATCH 303/607] Add explanatory comment --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 9ba3c614..a50c75a1 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1069,6 +1069,10 @@ class AxisItem(GraphicsWidget): ret = self.linkedView().mouseDragEvent(event, axis=1) else: ret = self.linkedView().mouseDragEvent(event, axis=0) + # Ignore event because if grid lines are enabled, we don't want the + # AxisItem to eat events meant for the ViewBox (see PR #565). A better + # solution here is to have grid lines drawn by a separate item inside the + # viewbox. event.ignore() return ret From 984eb5ed29f5ba942ff24adc1581265c66225a43 Mon Sep 17 00:00:00 2001 From: Hugo Slepicka Date: Fri, 5 Oct 2018 14:18:09 -0700 Subject: [PATCH 304/607] FIX: Protection over downsampling calculation. --- pyqtgraph/graphicsItems/PlotDataItem.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index ff0c785b..6797af64 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -527,14 +527,15 @@ class PlotDataItem(GraphicsObject): if self.opts['autoDownsample']: # this option presumes that x-values have uniform spacing range = self.viewRect() - if range is not None: + if range is not None and len(x) > 1: dx = float(x[-1]-x[0]) / (len(x)-1) - x0 = (range.left()-x[0]) / dx - x1 = (range.right()-x[0]) / dx - width = self.getViewBox().width() - if width != 0.0: - ds = int(max(1, int((x1-x0) / (width*self.opts['autoDownsampleFactor'])))) - ## downsampling is expensive; delay until after clipping. + if dx != 0.0: + x0 = (range.left()-x[0]) / dx + x1 = (range.right()-x[0]) / dx + width = self.getViewBox().width() + if width != 0.0: + ds = int(max(1, int((x1-x0) / (width*self.opts['autoDownsampleFactor'])))) + ## downsampling is expensive; delay until after clipping. if self.opts['clipToView']: view = self.getViewBox() From 266b0d0b4761d608f83b235cb597a11c14b635a3 Mon Sep 17 00:00:00 2001 From: Billy Su Date: Mon, 8 Oct 2018 10:51:18 +0800 Subject: [PATCH 305/607] Update the installation document * Add the method to directly install the latest commit or any branch on the GitHub. --- doc/source/installation.rst | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/doc/source/installation.rst b/doc/source/installation.rst index 37c0ae0e..e3e1f1fc 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -9,18 +9,28 @@ There are many different ways to install pyqtgraph, depending on your needs: Some users may need to call ``pip3`` instead. This method should work on all platforms. -* To get access to the very latest features and bugfixes, clone pyqtgraph from - github:: +* To get access to the very latest features and bugfixes you have three choice:: + + 1. Clone pyqtgraph from github:: $ git clone https://github.com/pyqtgraph/pyqtgraph - - Now you can install pyqtgraph from the source:: - + + Now you can install pyqtgraph from the source:: + $ python setup.py install - ..or you can simply place the pyqtgraph folder someplace importable, such as - inside the root of another project. PyQtGraph does not need to be "built" or - compiled in any way. + 2. Directly install from GitHub repo:: + + $ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop + + You can change to ``develop`` of the above command to the branch + name or the commit you prefer. + + 3. + You can simply place the pyqtgraph folder someplace importable, such as + inside the root of another project. PyQtGraph does not need to be "built" or + compiled in any way. + * Packages for pyqtgraph are also available in a few other forms: * **Anaconda**: ``conda install pyqtgraph`` From d550473ba75206e3a1eca004623cc0001d479723 Mon Sep 17 00:00:00 2001 From: "Ezequiel (gac-esbmx) Panepucci" Date: Tue, 9 Oct 2018 09:37:04 +0200 Subject: [PATCH 306/607] import python2_3 for basestring --- pyqtgraph/widgets/GroupBox.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/widgets/GroupBox.py b/pyqtgraph/widgets/GroupBox.py index 14a8dab5..43250115 100644 --- a/pyqtgraph/widgets/GroupBox.py +++ b/pyqtgraph/widgets/GroupBox.py @@ -1,5 +1,7 @@ from ..Qt import QtGui, QtCore from .PathButton import PathButton +from ..python2_3 import basestring + class GroupBox(QtGui.QGroupBox): """Subclass of QGroupBox that implements collapse handle. From d261c2f0f2fb317ecc7e4a580147dc95db8f8524 Mon Sep 17 00:00:00 2001 From: Jim Crowell Date: Wed, 10 Oct 2018 10:29:16 -0400 Subject: [PATCH 307/607] fixed bug in graphicsItems/ImageItem.py: degenerate images (max==min) would raise exception in getHistogram() --- pyqtgraph/graphicsItems/ImageItem.py | 139 ++++++++++++++------------- 1 file changed, 71 insertions(+), 68 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index a5a761bb..2ebce2c7 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -16,23 +16,23 @@ __all__ = ['ImageItem'] class ImageItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject ` - + GraphicsObject displaying an image. Optimized for rapid update (ie video display). This item displays either a 2D numpy array (height, width) or - a 3D array (height, width, RGBa). This array is optionally scaled (see + a 3D array (height, width, RGBa). This array is optionally scaled (see :func:`setLevels `) and/or colored with a lookup table (see :func:`setLookupTable `) before being displayed. - - ImageItem is frequently used in conjunction with - :class:`HistogramLUTItem ` or + + ImageItem is frequently used in conjunction with + :class:`HistogramLUTItem ` or :class:`HistogramLUTWidget ` to provide a GUI for controlling the levels and lookup table used to display the image. """ - + sigImageChanged = QtCore.Signal() sigRemoveRequested = QtCore.Signal(object) # self; emitted when 'remove' is selected from context menu - + def __init__(self, image=None, **kargs): """ See :func:`setImage ` for all allowed initialization arguments. @@ -41,23 +41,23 @@ class ImageItem(GraphicsObject): self.menu = None self.image = None ## original image data self.qimage = None ## rendered image for display - + self.paintMode = None - + self.levels = None ## [min, max] or [[redMin, redMax], ...] self.lut = None self.autoDownsample = False - + self.axisOrder = getConfigOption('imageAxisOrder') - + # In some cases, we use a modified lookup table to handle both rescaling # and LUT more efficiently self._effectiveLut = None - + self.drawKernel = None self.border = None self.removable = False - + if image is not None: self.setImage(image, **kargs) else: @@ -66,32 +66,32 @@ class ImageItem(GraphicsObject): def setCompositionMode(self, mode): """Change the composition mode of the item (see QPainter::CompositionMode in the Qt documentation). This is useful when overlaying multiple ImageItems. - + ============================================ ============================================================ **Most common arguments:** QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it is opaque. Otherwise, it uses the alpha channel to blend the image with the background. - QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to + QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to reflect the lightness or darkness of the background. - QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels + QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels are added together. QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. ============================================ ============================================================ """ self.paintMode = mode self.update() - + def setBorder(self, b): self.border = fn.mkPen(b) self.update() - + def width(self): if self.image is None: return None axis = 0 if self.axisOrder == 'col-major' else 1 return self.image.shape[axis] - + def height(self): if self.image is None: return None @@ -111,10 +111,10 @@ class ImageItem(GraphicsObject): def setLevels(self, levels, update=True): """ Set image scaling levels. Can be one of: - + * [blackLevel, whiteLevel] * [[minRed, maxRed], [minGreen, maxGreen], [minBlue, maxBlue]] - + Only the first format is compatible with lookup tables. See :func:`makeARGB ` for more details on how levels are applied. """ @@ -125,18 +125,18 @@ class ImageItem(GraphicsObject): self._effectiveLut = None if update: self.updateImage() - + def getLevels(self): return self.levels #return self.whiteLevel, self.blackLevel def setLookupTable(self, lut, update=True): """ - Set the lookup table (numpy array) to use for this image. (see + Set the lookup table (numpy array) to use for this image. (see :func:`makeARGB ` for more information on how this is used). - Optionally, lut can be a callable that accepts the current image as an + Optionally, lut can be a callable that accepts the current image as an argument and returns the lookup table to use. - + Ordinarily, this table is supplied by a :class:`HistogramLUTItem ` or :class:`GradientEditorItem `. """ @@ -149,7 +149,7 @@ class ImageItem(GraphicsObject): def setAutoDownsample(self, ads): """ Set the automatic downsampling mode for this ImageItem. - + Added in version 0.9.9 """ self.autoDownsample = ads @@ -198,44 +198,44 @@ class ImageItem(GraphicsObject): """ Update the image displayed by this item. For more information on how the image is processed before displaying, see :func:`makeARGB ` - + ================= ========================================================================= **Arguments:** - image (numpy array) Specifies the image data. May be 2D (width, height) or + image (numpy array) Specifies the image data. May be 2D (width, height) or 3D (width, height, RGBa). The array dtype must be integer or floating point of any bit depth. For 3D arrays, the third dimension must be of length 3 (RGB) or 4 (RGBA). See *notes* below. - autoLevels (bool) If True, this forces the image to automatically select + autoLevels (bool) If True, this forces the image to automatically select levels based on the maximum and minimum values in the data. By default, this argument is true unless the levels argument is given. lut (numpy array) The color lookup table to use when displaying the image. See :func:`setLookupTable `. levels (min, max) The minimum and maximum values to use when rescaling the image - data. By default, this will be set to the minimum and maximum values + data. By default, this will be set to the minimum and maximum values in the image. If the image array has dtype uint8, no rescaling is necessary. opacity (float 0.0-1.0) compositionMode See :func:`setCompositionMode ` border Sets the pen used when drawing the image border. Default is None. autoDownsample (bool) If True, the image is automatically downsampled to match the - screen resolution. This improves performance for large images and + screen resolution. This improves performance for large images and reduces aliasing. If autoDownsample is not specified, then ImageItem will choose whether to downsample the image based on its size. ================= ========================================================================= - - - **Notes:** - + + + **Notes:** + For backward compatibility, image data is assumed to be in column-major order (column, row). However, most image data is stored in row-major order (row, column) and will need to be transposed before calling setImage():: - + imageitem.setImage(imagedata.T) - + This requirement can be changed by calling ``image.setOpts(axisOrder='row-major')`` or by changing the ``imageAxisOrder`` :ref:`global configuration option `. - - + + """ profile = debug.Profiler() @@ -292,7 +292,7 @@ class ImageItem(GraphicsObject): def dataTransform(self): """Return the transform that maps from this image's input array to its local coordinate system. - + This transform corrects for the transposition that occurs when image data is interpreted in row-major order. """ @@ -307,7 +307,7 @@ class ImageItem(GraphicsObject): def inverseDataTransform(self): """Return the transform that maps from this image's local coordinate system to its input array. - + See dataTransform() for more information. """ tr = QtGui.QTransform() @@ -339,7 +339,7 @@ class ImageItem(GraphicsObject): def updateImage(self, *args, **kargs): ## used for re-rendering qimage from self.image. - + ## can we make any assumptions here that speed things up? ## dtype, range, size are all the same? defaults = { @@ -350,11 +350,11 @@ class ImageItem(GraphicsObject): def render(self): # Convert data to QImage for display. - + profile = debug.Profiler() if self.image is None or self.image.size == 0: return - + # Request a lookup table if this image has only one channel if self.image.ndim == 2 or self.image.shape[2] == 1: if isinstance(self.lut, collections.Callable): @@ -385,7 +385,7 @@ class ImageItem(GraphicsObject): image = fn.downsample(self.image, xds, axis=axes[0]) image = fn.downsample(image, yds, axis=axes[1]) self._lastDownsample = (xds, yds) - + # Check if downsampling reduced the image size to zero due to inf values. if image.size == 0: return @@ -403,27 +403,27 @@ class ImageItem(GraphicsObject): levdiff = maxlev - minlev levdiff = 1 if levdiff == 0 else levdiff # don't allow division by 0 if lut is None: - efflut = fn.rescaleData(ind, scale=255./levdiff, + efflut = fn.rescaleData(ind, scale=255./levdiff, offset=minlev, dtype=np.ubyte) else: lutdtype = np.min_scalar_type(lut.shape[0]-1) efflut = fn.rescaleData(ind, scale=(lut.shape[0]-1)/levdiff, offset=minlev, dtype=lutdtype, clip=(0, lut.shape[0]-1)) efflut = lut[efflut] - + self._effectiveLut = efflut lut = self._effectiveLut levels = None - + # Convert single-channel image to 2D array if image.ndim == 3 and image.shape[-1] == 1: image = image[..., 0] - + # Assume images are in column-major order for backward compatibility # (most images are in row-major order) if self.axisOrder == 'col-major': image = image.transpose((1, 0, 2)[:image.ndim]) - + argb, alpha = fn.makeARGB(image, lut=lut, levels=levels) self.qimage = fn.makeQImage(argb, alpha, transpose=False) @@ -453,26 +453,26 @@ class ImageItem(GraphicsObject): self.render() self.qimage.save(fileName, *args) - def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200, + def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200, targetHistogramSize=500, **kwds): """Returns x and y arrays containing the histogram values for the current image. For an explanation of the return format, see numpy.histogram(). - + The *step* argument causes pixels to be skipped when computing the histogram to save time. If *step* is 'auto', then a step is chosen such that the analyzed data has dimensions roughly *targetImageSize* for each axis. - - The *bins* argument and any extra keyword arguments are passed to + + The *bins* argument and any extra keyword arguments are passed to np.histogram(). If *bins* is 'auto', then a bin number is automatically chosen based on the image characteristics: - - * Integer images will have approximately *targetHistogramSize* bins, + + * Integer images will have approximately *targetHistogramSize* bins, with each bin having an integer width. * All other types will have *targetHistogramSize* bins. - + If *perChannel* is True, then the histogram is computed once per channel and the output is a list of the results. - + This method is also used when automatically computing levels. """ if self.image is None or self.image.size == 0: @@ -483,10 +483,13 @@ class ImageItem(GraphicsObject): if np.isscalar(step): step = (step, step) stepData = self.image[::step[0], ::step[1]] - + if bins == 'auto': mn = np.nanmin(stepData) mx = np.nanmax(stepData) + if mx == mn: + # degenerate image, arange will fail + mx += 1 if np.isnan(mn) or np.isnan(mx): # the data are all-nan return None, None @@ -497,7 +500,7 @@ class ImageItem(GraphicsObject): else: # for float data, let numpy select the bins. bins = np.linspace(mn, mx, 500) - + if len(bins) == 0: bins = [mn, mx] @@ -524,7 +527,7 @@ class ImageItem(GraphicsObject): (see GraphicsItem::ItemIgnoresTransformations in the Qt documentation) """ self.setFlag(self.ItemIgnoresTransformations, b) - + def setScaledMode(self): self.setPxMode(False) @@ -534,14 +537,14 @@ class ImageItem(GraphicsObject): if self.qimage is None: return None return QtGui.QPixmap.fromImage(self.qimage) - + def pixelSize(self): """return scene-size of a single pixel in the image""" br = self.sceneBoundingRect() if self.image is None: return 1,1 return br.width()/self.width(), br.height()/self.height() - + def viewTransformChanged(self): if self.autoDownsample: self.qimage = None @@ -582,7 +585,7 @@ class ImageItem(GraphicsObject): self.menu.addAction(remAct) self.menu.remAct = remAct return self.menu - + def hoverEvent(self, ev): if not ev.isExit() and self.drawKernel is not None and ev.acceptDrags(QtCore.Qt.LeftButton): ev.acceptClicks(QtCore.Qt.LeftButton) ## we don't use the click, but we also don't want anyone else to use it. @@ -595,7 +598,7 @@ class ImageItem(GraphicsObject): #print(ev.device()) #print(ev.pointerType()) #print(ev.pressure()) - + def drawAt(self, pos, ev=None): pos = [int(pos.x()), int(pos.y())] dk = self.drawKernel @@ -604,7 +607,7 @@ class ImageItem(GraphicsObject): sy = [0,dk.shape[1]] tx = [pos[0] - kc[0], pos[0] - kc[0]+ dk.shape[0]] ty = [pos[1] - kc[1], pos[1] - kc[1]+ dk.shape[1]] - + for i in [0,1]: dx1 = -min(0, tx[i]) dx2 = min(0, self.image.shape[0]-tx[i]) @@ -620,7 +623,7 @@ class ImageItem(GraphicsObject): ss = (slice(sx[0],sx[1]), slice(sy[0],sy[1])) mask = self.drawMask src = dk - + if isinstance(self.drawMode, collections.Callable): self.drawMode(dk, self.image, mask, ss, ts, ev) else: @@ -636,7 +639,7 @@ class ImageItem(GraphicsObject): else: raise Exception("Unknown draw mode '%s'" % self.drawMode) self.updateImage() - + def setDrawKernel(self, kernel=None, mask=None, center=(0,0), mode='set'): self.drawKernel = kernel self.drawKernelCenter = center From 56ed52b71848fb3ecfbb68cb8bfc21abcce5e8cf Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 11 Oct 2018 13:36:02 +0100 Subject: [PATCH 308/607] changelog all caught up --- CHANGELOG | 157 ++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 152 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index afba9d4a..dcdf01b1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,18 +1,165 @@ pyqtgraph-0.11.0 (in development) New Features: - - PySide2 support + - #410: SpinBox custom formatting options + - #415: ROI.getArrayRegion supports nearest-neighbor interpolation (especially handy for label images) + - #428: DataTreeWidget: + - Add DiffTreeWidget, which highlights differences between two DataTreeWidgets + - Improved support for displaying tracebacks + - Use TableWidget to represent arrays rather than plain text + - #476: Add option to set composition mode for scatterplotitem + - #518: TreeWidget: + - Add new signals: sigItemCheckStateChanged, sigItemTextChanged, sigColumnCountChanged + - Allow setting expansion state of items before they are added to a treewidget + - Support for using TreeWidget.invisibleRootItem() (see also #592, #595) + - #542: Add collapsible QGroupBox widgets + - #543: Add TargetItem: simple graphicsitem that draws a scale-invariant circle + crosshair + - #544: Make DockArea.restoreState behavior configurable in cases where either a dock to be restored is + missing, or an extra dock exists that is not mentioned in the restore state. + - #545: Allow more types to be mapped through Transform3D + - #548: Adds a disconnect() function that allows to conditionally disconnect signals, + including after reload. + Also, a SignalBlock class used to temporarily block a signal-slot pair + - #557: Allow console stack to be set outside of exceptions (see also: pg.stack) + - #558: CanvasItem save/restore, make Canvas ui easier to embed + - #559: Image exporter gets option to invert value while leaving hue fixed + - #560: Add function to enable faulthandler on all threads, also allow Mutex to be used as + drop-in replacement for python's Lock + - #567: Flowchart + - Add several new data nodes + - Add floordiv node + - Add EvalNode.setCode + - Binary operator nodes can select output array type + - #568: LinearRegionItem + - InfiniteLine can draw markers attached to the line + - InfiniteLine can limit the region of the viewbox over which it is drawn + - LinearRegionItem gets customizable line swap behavior (lines can block or push each other) + - Added LinearRegionItem.setHoverBrush + - #580: Allow calling sip.setapi in subprocess before pyqtgraph is imported + - #582: Add ComboBox save/restoreState methods + - #586: ParameterTree + - Add GroupParameter.sigAddNew signal + - systemsolver: add method for checking constraints / DOF + - add systemsolver copy method + - Parameter.child raises KeyError if requested child name does not exist + - #587: Make PathButton margin customizable + - #588: Add PlotCurveItem composition mode + - #589: Add RulerROI + - #591: Add nested progress dialogs + - #597: Fancy new interactive fractal demo + - #621: RGB mode for HistogramLUTWidget + - #628,670: Add point selection in ScatterPlotWidget + - #635: PySide2 support + - #671: Add SVG export option to force non-scaling stroke + - #676: OpenGL allow for panning in the plane of the camera + - #683: Allow data filter entries to be updated after they are created + - #685: Add option to set enum default values in DataFilterWidget + - #710: Adds ability to rotate/scale ROIs by mouse drag on the ROI itself (using alt/shift modifiers) API / behavior changes: - - ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system. + - Deprecated graphicsWindow classes; these have been unnecessary for many years because + widgets can be placed into a new window just by calling show(). + - #566: ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system. The result is visually the same, but children of ArrowItem are no longer rotated (this allows screen-aligned text to be attached more easily). To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument. - - Deprecated graphicsWindow classes; these have been unnecessary for many years because - widgets can be placed into a new window just by calling show(). - - Integer values in ParameterTree are now formatted as integer (%d) by default, rather than + - #673: Integer values in ParameterTree are now formatted as integer (%d) by default, rather than scientific notation (%g). This can be overridden by providing `format={value:g}` when creating the parameter. + - #374: ConsoleWidget uses the console's namespace as both global and local scope, which + - #410: SpinBox siPrefix without suffix is not longer allowed, select only numerical portion of text on focus-in + allows functions defined in the console to access the global namespace. + - #479,521: ParameterTree simple parameters check types before setting value + - #555: multiprocess using callSync='sync' no longer returns a future in case of timeout + - #583: eq() no longer compares array values if they have different shape + - #589: Remove SpiralROI (this was unintentionally added in the first case) + - #593: Override qAbort on slot exceptions for PyQt>=5.5 + - #657: When a floating Dock window is closed, the dock is now returned home + + Bugfixes: + - #408: Fix `cleanup` when the running qt application is not a QApplication + - #410: SpinBox fixes + - fixed bug with exponents disappearing after edit + - fixed parsing of values with junk after suffix + - fixed red border + - reverted default decimals to 6 + - make suffix editable (but show red border if it's wrong) + - revert invalid text on focus lost + - siPrefix without suffix is no longer allowed + - fixed parametree sending invalid options to spinbox + - fix spinbox wrapping (merged #159 from @lidstrom83) + - fixed parametertree ignoring spinbox bounds (merged #329 from @lidstrom83) + - fixed spinbox height too small for font size + - ROI subclass getArrayRegion methods are a bit more consistent (still need work) + - #424: Fix crash when running pyqtgraph with python -OO + - #429: fix fft premature slicing away of 0 freq bin + - #458: Fixed image export problems with new numpy API + - #478: Fixed PySide image memory leak + - #475: Fixed unicode error when exporting to SVG with non-ascii symbols + - #477: Fix handling of the value argument to functions.intColor + - #485: Fixed incorrect height in VTickGroup + - #514: Fixes bug where ViewBox emits sigRangeChanged before it has marked its transform dirty. + - #516,668: Fix GL Views being half size on hidpi monitors + - #526: Fix autorange exception with empty scatterplot + - #528: Prevent image downsampling causing exception in makeQImage + - #530: Fixed issue where setData only updated opts if data is given + - #541: Fixed issue where render would error because 'mapToDevice' would return None if the view size was too small. + - #553: Fixed legend size after remove item + - #555: Fixed console color issues, problems with subprocess closing + - #559: HDF5 exporter: check for ragged array length + - #563: Prevent viewbox auto-scaling to items that are not in the same scene. (This could + happen if an item that was previously added to the viewbox is then removed using scene.removeItem(). + - #564: Allow console exception label to wrap text (prevents console + growing too large for long exception messages) + - #565: Fixed AxisItem preventing mouse events reaching the ViewBox if it is displaying grid lines + and has its Z value set higher than the ViewBox. + - #567: fix flowchart spinbox bounds + - #569: PlotItem.addLegend will not try to add more than once + - #570: ViewBox: make sure transform is up to date in all mapping functions + - #577: Fix bargraphitem plotting horizontal bars + - #581: Fix colormapwidget saveState + - #586: ParameterTree + - Make parameter name,value inint args go through setValue and setName + - Fix colormapwidget saveState + - #589: Fix click area for small ellipse/circle ROIs + - #592,595: Fix InvisibleRootItem issues introduced in #518 + - #596: Fix polyline click causing lines to bedrawn to the wrong node + - #598: Better ParameterTree support for dark themes + - #623: Fix PyQt5 / ScatterPlot issue with custom symbols + - #626: Fix OpenGL texture state leaking to wrong items + - #627: Fix ConsoleWidget stack handling on python 3.5 + - #633: Fix OpenGL cylinder geometry + - #637: Fix TypeError in isosurface + - #641,642: Fix SVG export on Qt5 / high-DPI displays + - #645: scatterplotwidget behaves nicely when data contains infs + - #653: ScatterPlotItem: Fix a GC memory leak due to numpy issue 6581 + - #648: fix color ignored in GLGridItem + - #671: fixed SVG export failing if the first value of a plot is nan + - #674: fixed parallelizer leaking file handles + - #675: Gracefully handle case where image data has size==0 + - #679: Fix overflow in Point.length() + - #682: Fix: mkQApp returned None if a QApplication was already created elsewhere + - #689: ViewBox fix: don't call setRange with empty args + - #693: Fix GLLinePlotItem setting color + - #696: Fix error when using PlotDataItem with both stepMode and symbol + - #697: Fix SpinBox validation on python 3 + - #699: Fix nan handling in ImageItem.setData + - #713: ConsoleWidget: Fixed up/down arrows sometimes unable to get back to the original + (usually blank) input state + - #715: Fix file dialog handling in Qt 5 + - #718: Fix SVG export with items that require option.exposedRect + - #721: Fixes mouse wheel ignoring disabled mouse axes -- although the scaling was correct, + it was causing auto range to be disabled. + - #723: Fix axis ticks when using self.scale + - #739: Fix handling of 2-axis mouse wheel events + - #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX. + + Maintenance: + - Lots of new unit tests + - Lots of code cleanup + - #546: Add check for EINTR during example testing to avoid sporadic test failures on travis + - #624: TravisCI no longer running python 2.6 tests + - #695: "dev0" added to version string pyqtgraph-0.10.0 From fc5e0cd9f41ae64042fafefd1055c234c4b84162 Mon Sep 17 00:00:00 2001 From: Stefan Ecklebe Date: Fri, 12 Oct 2018 15:32:23 +0200 Subject: [PATCH 309/607] Fixed issue #481 --- pyqtgraph/opengl/GLViewWidget.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 92332cf5..65b381a1 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -236,6 +236,8 @@ class GLViewWidget(QtOpenGL.QGLWidget): glPopMatrix() def setCameraPosition(self, pos=None, distance=None, elevation=None, azimuth=None): + if pos is not None: + self.opts['center'] = pos if distance is not None: self.opts['distance'] = distance if elevation is not None: From 0f149f38c2707c0877c19c5be09349344cbd3379 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 14 Oct 2018 18:49:03 +0200 Subject: [PATCH 310/607] No warning for arrays with zeros in logscale NumPy evaluates log10(0) to -inf, so there is no reason to show the user a RuntimeWarning. Before, if visualizing data arrays containing zeros in logscale, a RuntimeWarning was shown. --- pyqtgraph/graphicsItems/PlotDataItem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 6797af64..d8a7aed5 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -514,11 +514,13 @@ class PlotDataItem(GraphicsObject): # Ignore the first bin for fft data if we have a logx scale if self.opts['logMode'][0]: x=x[1:] - y=y[1:] - if self.opts['logMode'][0]: - x = np.log10(x) - if self.opts['logMode'][1]: - y = np.log10(y) + y=y[1:] + + with np.errstate(divide='ignore'): + if self.opts['logMode'][0]: + x = np.log10(x) + if self.opts['logMode'][1]: + y = np.log10(y) ds = self.opts['downsample'] if not isinstance(ds, int): From 16616c77b7aa637712c949245d9d4945d88592a0 Mon Sep 17 00:00:00 2001 From: Tran Duy Hoa Date: Fri, 26 Oct 2018 23:06:23 +0200 Subject: [PATCH 311/607] Fix bug in GLViewWidget.py call debug.printExc() instead of pyqtgraph.debug.printExc() --- pyqtgraph/opengl/GLViewWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 92332cf5..bbdf9df4 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -427,7 +427,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): ver = glGetString(GL_VERSION).split()[0] if int(ver.split('.')[0]) < 2: from .. import debug - pyqtgraph.debug.printExc() + debug.printExc() raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) else: raise From 4285785985531c7b4bcfcab8c3009f3726fdbf55 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 29 Oct 2018 10:59:11 -0700 Subject: [PATCH 312/607] Revert "Fixed AxisMouse drag issue" The original change breaks mouse dragging directly on the axis This reverts commit 9bfdda06a63bdbcaf66cf022517b32a3f86bbb3f. --- pyqtgraph/graphicsItems/AxisItem.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index b1e8c2cc..19c5e1f0 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1066,15 +1066,9 @@ class AxisItem(GraphicsWidget): if self.linkedView() is None: return if self.orientation in ['left', 'right']: - ret = self.linkedView().mouseDragEvent(event, axis=1) + return self.linkedView().mouseDragEvent(event, axis=1) else: - ret = self.linkedView().mouseDragEvent(event, axis=0) - # Ignore event because if grid lines are enabled, we don't want the - # AxisItem to eat events meant for the ViewBox (see PR #565). A better - # solution here is to have grid lines drawn by a separate item inside the - # viewbox. - event.ignore() - return ret + return self.linkedView().mouseDragEvent(event, axis=0) def mouseClickEvent(self, event): if self.linkedView() is None: From 482dd2ee335d6dd3146417f7d520d7a0b413340f Mon Sep 17 00:00:00 2001 From: Sebastian Pauka Date: Tue, 30 Oct 2018 10:22:49 +1100 Subject: [PATCH 313/607] Terminate FileForwarder thread on process end Previously on windows the FileForwarder threads would continue to run and eat up a lot of CPU once the child process they were forwarding dies. This commit shuts down those threads when the child process is killed. --- pyqtgraph/multiprocess/processes.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/multiprocess/processes.py b/pyqtgraph/multiprocess/processes.py index d841ea40..6e815edc 100644 --- a/pyqtgraph/multiprocess/processes.py +++ b/pyqtgraph/multiprocess/processes.py @@ -166,6 +166,14 @@ class Process(RemoteEventHandler): raise Exception('Timed out waiting for remote process to end.') time.sleep(0.05) self.conn.close() + + # Close remote polling threads, otherwise they will spin continuously + if hasattr(self, "_stdoutForwarder"): + self._stdoutForwarder.finish.set() + self._stderrForwarder.finish.set() + self._stdoutForwarder.join() + self._stderrForwarder.join() + self.debugMsg('Child process exited. (%d)' % self.proc.returncode) def debugMsg(self, msg, *args): @@ -473,23 +481,24 @@ class FileForwarder(threading.Thread): self.lock = threading.Lock() self.daemon = True self.color = color + self.finish = threading.Event() self.start() def run(self): if self.output == 'stdout' and self.color is not False: - while True: + while not self.finish.is_set(): line = self.input.readline() with self.lock: cprint.cout(self.color, line, -1) elif self.output == 'stderr' and self.color is not False: - while True: + while not self.finish.is_set(): line = self.input.readline() with self.lock: cprint.cerr(self.color, line, -1) else: if isinstance(self.output, str): self.output = getattr(sys, self.output) - while True: + while not self.finish.is_set(): line = self.input.readline() with self.lock: - self.output.write(line) + self.output.write(line.decode('utf8')) From 2e69b9c5e6bd8008c2d7e9f384b9c9b32ff2f5a7 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 9 Nov 2018 07:18:36 -0800 Subject: [PATCH 314/607] Add changelog entry and credit for previous PR --- CHANGELOG | 1 + pyqtgraph/graphicsItems/GradientEditorItem.py | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index dcdf01b1..2cb16918 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ pyqtgraph-0.11.0 (in development) - Add DiffTreeWidget, which highlights differences between two DataTreeWidgets - Improved support for displaying tracebacks - Use TableWidget to represent arrays rather than plain text + - #446: Added Perceptually Uniform Sequential colormaps from the matplotlib 2.0 release - #476: Add option to set composition mode for scatterplotitem - #518: TreeWidget: - Add new signals: sigItemCheckStateChanged, sigItemTextChanged, sigColumnCountChanged diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index 9b0b0f19..fc1d638c 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -22,6 +22,7 @@ Gradients = OrderedDict([ ('cyclic', {'ticks': [(0.0, (255, 0, 4, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'hsv'}), ('greyclip', {'ticks': [(0.0, (0, 0, 0, 255)), (0.99, (255, 255, 255, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'rgb'}), ('grey', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'}), + # Perceptually uniform sequential colormaps from Matplotlib 2.0 ('viridis', {'ticks': [(0.0, (68, 1, 84, 255)), (0.25, (58, 82, 139, 255)), (0.5, (32, 144, 140, 255)), (0.75, (94, 201, 97, 255)), (1.0, (253, 231, 36, 255))], 'mode': 'rgb'}), ('inferno', {'ticks': [(0.0, (0, 0, 3, 255)), (0.25, (87, 15, 109, 255)), (0.5, (187, 55, 84, 255)), (0.75, (249, 142, 8, 255)), (1.0, (252, 254, 164, 255))], 'mode': 'rgb'}), ('plasma', {'ticks': [(0.0, (12, 7, 134, 255)), (0.25, (126, 3, 167, 255)), (0.5, (203, 71, 119, 255)), (0.75, (248, 149, 64, 255)), (1.0, (239, 248, 33, 255))], 'mode': 'rgb'}), From 7e09022e67caaa350466ed51dba825516e0ef06d Mon Sep 17 00:00:00 2001 From: MingZZZZZZZZ <32181145+ReehcQ@users.noreply.github.com> Date: Fri, 21 Dec 2018 01:45:00 -0500 Subject: [PATCH 315/607] add legend for bar graph items --- pyqtgraph/graphicsItems/LegendItem.py | 29 +++++++++++++-------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 200820fc..91b6e9a3 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -147,29 +147,28 @@ class ItemSample(GraphicsWidget): return QtCore.QRectF(0, 0, 20, 20) def paint(self, p, *args): - #p.setRenderHint(p.Antialiasing) # only if the data is antialiased. + # p.setRenderHint(p.Antialiasing) # only if the data is antialiased. opts = self.item.opts - - if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None: - p.setBrush(fn.mkBrush(opts['fillBrush'])) - p.setPen(fn.mkPen(None)) - p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)])) - + if not isinstance(self.item, ScatterPlotItem): p.setPen(fn.mkPen(opts['pen'])) p.drawLine(2, 18, 18, 2) - + + if opts.get('fillLevel', None) is not None and opts.get('fillBrush', None) is not None: + p.setBrush(fn.mkBrush(opts['fillBrush'])) + p.setPen(fn.mkPen(opts['fillBrush'])) + p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2, 18), QtCore.QPointF(18, 2), QtCore.QPointF(18, 18)])) + symbol = opts.get('symbol', None) if symbol is not None: if isinstance(self.item, PlotDataItem): opts = self.item.scatter.opts - - pen = fn.mkPen(opts['pen']) - brush = fn.mkBrush(opts['brush']) - size = opts['size'] - - p.translate(10,10) - path = drawSymbol(p, symbol, size, pen, brush) + p.translate(10, 10) + drawSymbol(p, symbol, opts['size'], fn.mkPen(opts['pen']), fn.mkBrush(opts['brush'])) + + if isinstance(self.item, BarGraphItem): + p.setBrush(fn.mkBrush(opts['brush'])) + p.drawRect(QtCore.QRectF(2, 2, 18, 18)) From 3f93e30b312c8966b695fde07054c62f34f9896c Mon Sep 17 00:00:00 2001 From: MingZZZZZZZZ <32181145+ReehcQ@users.noreply.github.com> Date: Fri, 21 Dec 2018 01:47:23 -0500 Subject: [PATCH 316/607] Update Legend.py add legend examples for line plot, bar graph and scatter plot. --- examples/Legend.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/examples/Legend.py b/examples/Legend.py index f7841151..3759c2e9 100644 --- a/examples/Legend.py +++ b/examples/Legend.py @@ -7,17 +7,37 @@ import initExample ## Add path to library (just for examples; you do not need th import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtGui +import numpy as np -plt = pg.plot() -plt.setWindowTitle('pyqtgraph example: Legend') -plt.addLegend() -#l = pg.LegendItem((100,60), offset=(70,30)) # args are (size, offset) -#l.setParentItem(plt.graphicsItem()) # Note we do NOT call plt.addItem in this case +win = pg.plot() +win.setWindowTitle('pyqtgraph example: BarGraphItem') -c1 = plt.plot([1,3,2,4], pen='r', symbol='o', symbolPen='r', symbolBrush=0.5, name='red plot') -c2 = plt.plot([2,1,4,3], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='green plot') -#l.addItem(c1, 'red plot') -#l.addItem(c2, 'green plot') +# # option1: only for .plot(), following c1,c2 for example----------------------- +# win.addLegend() + +# bar graph +x = np.arange(10) +y = np.sin(x+2) * 3 +bg1 = pg.BarGraphItem(x=x, height=y, width=0.3, brush='b', pen='w', name='bar') +win.addItem(bg1) + +# curve +c1 = win.plot([np.random.randint(0,8) for i in range(10)], pen='r', symbol='t', symbolPen='r', symbolBrush='g', name='curve1') +c2 = win.plot([2,1,4,3,1,3,2,4,3,2], pen='g', fillLevel=0, fillBrush=(255,255,255,30), name='curve2') + +# scatter plot +s1 = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush(255, 255, 255, 120), name='scatter') +spots = [{'pos': [i, np.random.randint(-3, 3)], 'data': 1} for i in range(10)] +s1.addPoints(spots) +win.addItem(s1) + +# # option2: generic method------------------------------------------------ +legend = pg.LegendItem((80,60), offset=(70,20)) +legend.setParentItem(win.graphicsItem()) +legend.addItem(bg1, 'bar') +legend.addItem(c1, 'curve1') +legend.addItem(c2, 'curve2') +legend.addItem(s1, 'scatter') ## Start Qt event loop unless running in interactive mode or using pyside. From b575b56edfee7afec66683bc9d06a2675c409745 Mon Sep 17 00:00:00 2001 From: danielhrisca Date: Wed, 16 Jan 2019 16:43:04 +0200 Subject: [PATCH 317/607] avoid calling setLabel repeatedly for AxisItem --- pyqtgraph/graphicsItems/AxisItem.py | 393 ++++++++++++++-------------- 1 file changed, 202 insertions(+), 191 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 19c5e1f0..a0d0bcbd 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -14,10 +14,10 @@ class AxisItem(GraphicsWidget): GraphicsItem showing a single plot axis with ticks, values, and label. Can be configured to fit on any side of a plot, and can automatically synchronize its displayed scale with ViewBox items. Ticks can be extended to draw a grid. - If maxTickLength is negative, ticks point into the plot. + If maxTickLength is negative, ticks point into the plot. """ - - def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True): + + def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args): """ ============== =============================================================== **Arguments:** @@ -26,11 +26,19 @@ class AxisItem(GraphicsWidget): into the plot, positive values draw outward. linkView (ViewBox) causes the range of values displayed in the axis to be linked to the visible range of a ViewBox. - showValues (bool) Whether to display values adjacent to ticks + showValues (bool) Whether to display values adjacent to ticks pen (QPen) Pen used when drawing ticks. + text The text (excluding units) to display on the label for this + axis. + units The units for this axis. Units should generally be given + without any scaling prefix (eg, 'V' instead of 'mV'). The + scaling prefix will be automatically prepended based on the + range of data displayed. + **args All extra keyword arguments become CSS style options for + the tag which will surround the axis label and units. ============== =============================================================== """ - + GraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) self.picture = None @@ -39,15 +47,15 @@ class AxisItem(GraphicsWidget): raise Exception("Orientation argument must be one of 'left', 'right', 'top', or 'bottom'.") if orientation in ['left', 'right']: self.label.rotate(-90) - + self.style = { - 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis + 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis 'tickTextWidth': 30, ## space reserved for tick text - 'tickTextHeight': 18, + 'tickTextHeight': 18, 'autoExpandTextSpace': True, ## automatically expand text space if needed 'tickFont': None, - 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick - 'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally. + 'stopAxisAtTick': (False, False), ## whether axis is drawn to edge of box or to last tick + 'textFillLimits': [ ## how much of the axis to fill up with tick text, maximally. (0, 0.8), ## never fill more than 80% of the axis (2, 0.6), ## If we already have 2 ticks with text, fill no more than 60% of the axis (4, 0.4), ## If we already have 4 ticks with text, fill no more than 40% of the axis @@ -58,93 +66,93 @@ class AxisItem(GraphicsWidget): 'maxTickLevel': 2, 'maxTextLevel': 2, } - - self.textWidth = 30 ## Keeps track of maximum width / height of tick text + + self.textWidth = 30 ## Keeps track of maximum width / height of tick text self.textHeight = 18 - + # If the user specifies a width / height, remember that setting # indefinitely. self.fixedWidth = None self.fixedHeight = None - - self.labelText = '' - self.labelUnits = '' - self.labelUnitPrefix='' - self.labelStyle = {} + + self.labelText = text + self.labelUnits = units + self.labelUnitPrefix = unitPrefix + self.labelStyle = args self.logMode = False self.tickFont = None - + self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self._tickSpacing = None # used to override default tickSpacing method self.scale = 1.0 self.autoSIPrefix = True self.autoSIPrefixScale = 1.0 - + self.setRange(0, 1) - + if pen is None: self.setPen() else: self.setPen(pen) - + self._linkedView = None if linkView is not None: self.linkToView(linkView) - + self.showLabel(False) - + self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) def setStyle(self, **kwds): """ Set various style options. - + =================== ======================================================= Keyword Arguments: - tickLength (int) The maximum length of ticks in pixels. - Positive values point toward the text; negative + tickLength (int) The maximum length of ticks in pixels. + Positive values point toward the text; negative values point away. tickTextOffset (int) reserved spacing between text and axis in px tickTextWidth (int) Horizontal space reserved for tick text in px tickTextHeight (int) Vertical space reserved for tick text in px autoExpandTextSpace (bool) Automatically expand text space if the tick strings become too long. - tickFont (QFont or None) Determines the font used for tick + tickFont (QFont or None) Determines the font used for tick values. Use None for the default font. - stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis - line is drawn only as far as the last tick. - Otherwise, the line is drawn to the edge of the + stopAxisAtTick (tuple: (bool min, bool max)) If True, the axis + line is drawn only as far as the last tick. + Otherwise, the line is drawn to the edge of the AxisItem boundary. textFillLimits (list of (tick #, % fill) tuples). This structure - determines how the AxisItem decides how many ticks + determines how the AxisItem decides how many ticks should have text appear next to them. Each tuple in the list specifies what fraction of the axis length may be occupied by text, given the number of ticks that already have text displayed. For example:: - + [(0, 0.8), # Never fill more than 80% of the axis - (2, 0.6), # If we already have 2 ticks with text, + (2, 0.6), # If we already have 2 ticks with text, # fill no more than 60% of the axis - (4, 0.4), # If we already have 4 ticks with text, + (4, 0.4), # If we already have 4 ticks with text, # fill no more than 40% of the axis - (6, 0.2)] # If we already have 6 ticks with text, + (6, 0.2)] # If we already have 6 ticks with text, # fill no more than 20% of the axis - + showValues (bool) indicates whether text is displayed adjacent to ticks. =================== ======================================================= - + Added in version 0.9.9 """ for kwd,value in kwds.items(): if kwd not in self.style: raise NameError("%s is not a valid style argument." % kwd) - + if kwd in ('tickLength', 'tickTextOffset', 'tickTextWidth', 'tickTextHeight'): if not isinstance(value, int): raise ValueError("Argument '%s' must be int" % kwd) - + if kwd == 'tickTextOffset': if self.orientation in ('left', 'right'): self.style['tickTextOffset'][0] = value @@ -158,19 +166,19 @@ class AxisItem(GraphicsWidget): self.style[kwd] = value else: self.style[kwd] = value - + self.picture = None self._adjustSize() self.update() - + def close(self): self.scene().removeItem(self.label) self.label = None self.scene().removeItem(self) - + def setGrid(self, grid): """Set the alpha value (0-255) for the grid, or False to disable. - + When grid lines are enabled, the axis tick lines are extended to cover the extent of the linked ViewBox, if any. """ @@ -178,28 +186,28 @@ class AxisItem(GraphicsWidget): self.picture = None self.prepareGeometryChange() self.update() - + def setLogMode(self, log): """ If *log* is True, then ticks are displayed on a logarithmic scale and values - are adjusted accordingly. (This is usually accessed by changing the log mode + are adjusted accordingly. (This is usually accessed by changing the log mode of a :func:`PlotItem `) """ self.logMode = log self.picture = None self.update() - + def setTickFont(self, font): self.tickFont = font self.picture = None self.prepareGeometryChange() ## Need to re-allocate space depending on font size? - + self.update() - + def resizeEvent(self, ev=None): #s = self.size() - + ## Set the position of the label nudge = 5 br = self.label.boundingRect() @@ -218,7 +226,7 @@ class AxisItem(GraphicsWidget): p.setY(int(self.size().height()-br.height()+nudge)) self.label.setPos(p) self.picture = None - + def showLabel(self, show=True): """Show/hide the label text for this axis.""" #self.drawLabel = show @@ -229,10 +237,10 @@ class AxisItem(GraphicsWidget): self._updateHeight() if self.autoSIPrefix: self.updateAutoSIPrefix() - + def setLabel(self, text=None, units=None, unitPrefix=None, **args): """Set the text displayed adjacent to the axis. - + ============== ============================================================= **Arguments:** text The text (excluding units) to display on the label for this @@ -244,23 +252,26 @@ class AxisItem(GraphicsWidget): **args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== ============================================================= - + The final text generated for the label will look like:: - + {text} (prefix{units}) - - Each extra keyword argument will become a CSS option in the above template. + + Each extra keyword argument will become a CSS option in the above template. For example, you can set the font size and color of the label:: - + labelStyle = {'color': '#FFF', 'font-size': '14pt'} axis.setLabel('label text', units='V', **labelStyle) - + """ + show_label = False if text is not None: self.labelText = text - self.showLabel() + show_label = True if units is not None: self.labelUnits = units + show_label = True + if show_label: self.showLabel() if unitPrefix is not None: self.labelUnitPrefix = unitPrefix @@ -270,7 +281,7 @@ class AxisItem(GraphicsWidget): self._adjustSize() self.picture = None self.update() - + def labelString(self): if self.labelUnits == '': if not self.autoSIPrefix or self.autoSIPrefixScale == 1.0: @@ -280,13 +291,13 @@ class AxisItem(GraphicsWidget): else: #print repr(self.labelUnitPrefix), repr(self.labelUnits) units = asUnicode('(%s%s)') % (asUnicode(self.labelUnitPrefix), asUnicode(self.labelUnits)) - + s = asUnicode('%s %s') % (asUnicode(self.labelText), asUnicode(units)) - + style = ';'.join(['%s: %s' % (k, self.labelStyle[k]) for k in self.labelStyle]) - + return asUnicode("%s") % (style, asUnicode(s)) - + def _updateMaxTextSize(self, x): ## Informs that the maximum tick size orthogonal to the axis has ## changed; we use this to decide whether the item needs to be resized @@ -305,22 +316,22 @@ class AxisItem(GraphicsWidget): if self.style['autoExpandTextSpace'] is True: self._updateHeight() #return True ## size has changed - + def _adjustSize(self): if self.orientation in ['left', 'right']: self._updateWidth() else: self._updateHeight() - + def setHeight(self, h=None): """Set the height of this axis reserved for ticks and tick labels. The height of the axis label is automatically added. - + If *height* is None, then the value will be determined automatically based on the size of the tick text.""" self.fixedHeight = h self._updateHeight() - + def _updateHeight(self): if not self.isVisible(): h = 0 @@ -338,20 +349,20 @@ class AxisItem(GraphicsWidget): h += self.label.boundingRect().height() * 0.8 else: h = self.fixedHeight - + self.setMaximumHeight(h) self.setMinimumHeight(h) self.picture = None - + def setWidth(self, w=None): """Set the width of this axis reserved for ticks and tick labels. The width of the axis label is automatically added. - + If *width* is None, then the value will be determined automatically based on the size of the tick text.""" self.fixedWidth = w self._updateWidth() - + def _updateWidth(self): if not self.isVisible(): w = 0 @@ -369,20 +380,20 @@ class AxisItem(GraphicsWidget): w += self.label.boundingRect().height() * 0.8 ## bounding rect is usually an overestimate else: w = self.fixedWidth - + self.setMaximumWidth(w) self.setMinimumWidth(w) self.picture = None - + def pen(self): if self._pen is None: return fn.mkPen(getConfigOption('foreground')) return fn.mkPen(self._pen) - + def setPen(self, *args, **kwargs): """ Set the pen used for drawing text, axes, ticks, and grid lines. - If no arguments are given, the default foreground color will be used + If no arguments are given, the default foreground color will be used (see :func:`setConfigOption `). """ self.picture = None @@ -393,44 +404,44 @@ class AxisItem(GraphicsWidget): self.labelStyle['color'] = '#' + fn.colorStr(self._pen.color())[:6] self.setLabel() self.update() - + def setScale(self, scale=None): """ - Set the value scaling for this axis. - + Set the value scaling for this axis. + Setting this value causes the axis to draw ticks and tick labels as if - the view coordinate system were scaled. By default, the axis scaling is + the view coordinate system were scaled. By default, the axis scaling is 1.0. """ # Deprecated usage, kept for backward compatibility - if scale is None: + if scale is None: scale = 1.0 self.enableAutoSIPrefix(True) - + if scale != self.scale: self.scale = scale self.setLabel() self.picture = None self.update() - + def enableAutoSIPrefix(self, enable=True): """ - Enable (or disable) automatic SI prefix scaling on this axis. - - When enabled, this feature automatically determines the best SI prefix + Enable (or disable) automatic SI prefix scaling on this axis. + + When enabled, this feature automatically determines the best SI prefix to prepend to the label units, while ensuring that axis values are scaled - accordingly. - - For example, if the axis spans values from -0.1 to 0.1 and has units set + accordingly. + + For example, if the axis spans values from -0.1 to 0.1 and has units set to 'V' then the axis would display values -100 to 100 and the units would appear as 'mV' - + This feature is enabled by default, and is only available when a suffix (unit string) is provided to display on the label. """ self.autoSIPrefix = enable self.updateAutoSIPrefix() - + def updateAutoSIPrefix(self): if self.label.isVisible(): (scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale))) @@ -440,12 +451,12 @@ class AxisItem(GraphicsWidget): self.setLabel(unitPrefix=prefix) else: scale = 1.0 - + self.autoSIPrefixScale = scale self.picture = None self.update() - - + + def setRange(self, mn, mx): """Set the range of values displayed by the axis. Usually this is handled automatically by linking the axis to a ViewBox with :func:`linkToView `""" @@ -456,14 +467,14 @@ class AxisItem(GraphicsWidget): self.updateAutoSIPrefix() self.picture = None self.update() - + def linkedView(self): """Return the ViewBox this axis is linked to""" if self._linkedView is None: return None else: return self._linkedView() - + def linkToView(self, view): """Link this axis to a ViewBox, causing its displayed range to match the visible range of the view.""" oldView = self.linkedView() @@ -476,11 +487,11 @@ class AxisItem(GraphicsWidget): if oldView is not None: oldView.sigXRangeChanged.disconnect(self.linkedViewChanged) view.sigXRangeChanged.connect(self.linkedViewChanged) - + if oldView is not None: oldView.sigResized.disconnect(self.linkedViewChanged) view.sigResized.connect(self.linkedViewChanged) - + def linkedViewChanged(self, view, newRange=None): if self.orientation in ['right', 'left']: if newRange is None: @@ -496,7 +507,7 @@ class AxisItem(GraphicsWidget): self.setRange(*newRange[::-1]) else: self.setRange(*newRange) - + def boundingRect(self): linkedView = self.linkedView() if linkedView is None or self.grid is False: @@ -515,7 +526,7 @@ class AxisItem(GraphicsWidget): return rect else: return self.mapRectFromParent(self.geometry()) | linkedView.mapRectToItem(self, linkedView.boundingRect()) - + def paint(self, p, opt, widget): profiler = debug.Profiler() if self.picture is None: @@ -544,26 +555,26 @@ class AxisItem(GraphicsWidget): [ (minorTickValue1, minorTickString1), (minorTickValue2, minorTickString2), ... ], ... ] - + If *ticks* is None, then the default tick system will be used instead. """ self._tickLevels = ticks self.picture = None self.update() - + def setTickSpacing(self, major=None, minor=None, levels=None): """ - Explicitly determine the spacing of major and minor ticks. This + Explicitly determine the spacing of major and minor ticks. This overrides the default behavior of the tickSpacing method, and disables - the effect of setTicks(). Arguments may be either *major* and *minor*, - or *levels* which is a list of (spacing, offset) tuples for each + the effect of setTicks(). Arguments may be either *major* and *minor*, + or *levels* which is a list of (spacing, offset) tuples for each tick level desired. - + If no arguments are given, then the default behavior of tickSpacing is enabled. - + Examples:: - + # two levels, all offsets = 0 axis.setTickSpacing(5, 1) # three levels, all offsets = 0 @@ -571,7 +582,7 @@ class AxisItem(GraphicsWidget): # reset to default axis.setTickSpacing() """ - + if levels is None: if major is None: levels = None @@ -580,16 +591,16 @@ class AxisItem(GraphicsWidget): self._tickSpacing = levels self.picture = None self.update() - + def tickSpacing(self, minVal, maxVal, size): """Return values describing the desired spacing and offset of ticks. - - This method is called whenever the axis needs to be redrawn and is a + + This method is called whenever the axis needs to be redrawn and is a good method to override in subclasses that require control over tick locations. - + The return value must be a list of tuples, one for each set of ticks:: - + [ (major tick spacing, offset), (minor tick spacing, offset), @@ -600,32 +611,32 @@ class AxisItem(GraphicsWidget): # First check for override tick spacing if self._tickSpacing is not None: return self._tickSpacing - + dif = abs(maxVal - minVal) if dif == 0: return [] - + ## decide optimal minor tick spacing in pixels (this is just aesthetics) optimalTickCount = max(2., np.log(size)) - - ## optimal minor tick spacing + + ## optimal minor tick spacing optimalSpacing = dif / optimalTickCount - + ## the largest power-of-10 spacing which is smaller than optimal p10unit = 10 ** np.floor(np.log10(optimalSpacing)) - + ## Determine major/minor tick spacings which flank the optimal spacing. intervals = np.array([1., 2., 10., 20., 100.]) * p10unit minorIndex = 0 while intervals[minorIndex+1] <= optimalSpacing: minorIndex += 1 - + levels = [ (intervals[minorIndex+2], 0), (intervals[minorIndex+1], 0), #(intervals[minorIndex], 0) ## Pretty, but eats up CPU ] - + if self.style['maxTickLevel'] >= 2: ## decide whether to include the last level of ticks minSpacing = min(size / 20., 30.) @@ -633,16 +644,16 @@ class AxisItem(GraphicsWidget): if dif / intervals[minorIndex] <= maxTickCount: levels.append((intervals[minorIndex], 0)) return levels - - - + + + ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection ### Determine major/minor tick spacings which flank the optimal spacing. #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit #minorIndex = 0 #while intervals[minorIndex+1] <= optimalSpacing: #minorIndex += 1 - + ### make sure we never see 5 and 2 at the same time #intIndexes = [ #[0,1,3], @@ -651,42 +662,42 @@ class AxisItem(GraphicsWidget): #[3,4,6], #[3,5,6], #][minorIndex] - + #return [ #(intervals[intIndexes[2]], 0), #(intervals[intIndexes[1]], 0), #(intervals[intIndexes[0]], 0) #] - + def tickValues(self, minVal, maxVal, size): """ Return the values and spacing of ticks to draw:: - - [ - (spacing, [major ticks]), - (spacing, [minor ticks]), - ... + + [ + (spacing, [major ticks]), + (spacing, [minor ticks]), + ... ] - + By default, this method calls tickSpacing to determine the correct tick locations. This is a good method to override in subclasses. """ minVal, maxVal = sorted((minVal, maxVal)) - - minVal *= self.scale + + minVal *= self.scale maxVal *= self.scale #size *= self.scale - + ticks = [] tickLevels = self.tickSpacing(minVal, maxVal, size) allValues = np.array([]) for i in range(len(tickLevels)): spacing, offset = tickLevels[i] - + ## determine starting tick start = (np.ceil((minVal-offset) / spacing) * spacing) + offset - + ## determine number of ticks num = int((maxVal-start) / spacing) + 1 values = (np.arange(num) * spacing + start) / self.scale @@ -696,11 +707,11 @@ class AxisItem(GraphicsWidget): values = list(filter(lambda x: all(np.abs(allValues-x) > spacing/self.scale*0.01), values)) allValues = np.concatenate([allValues, values]) ticks.append((spacing/self.scale, values)) - + if self.logMode: return self.logTickValues(minVal, maxVal, size, ticks) - - + + #nticks = [] #for t in ticks: #nvals = [] @@ -708,24 +719,24 @@ class AxisItem(GraphicsWidget): #nvals.append(v/self.scale) #nticks.append((t[0]/self.scale,nvals)) #ticks = nticks - + return ticks - + def logTickValues(self, minVal, maxVal, size, stdTicks): - + ## start with the tick spacing given by tickValues(). ## Any level whose spacing is < 1 needs to be converted to log scale - + ticks = [] for (spacing, t) in stdTicks: if spacing >= 1.0: ticks.append((spacing, t)) - + if len(ticks) < 3: v1 = int(np.floor(minVal)) v2 = int(np.ceil(maxVal)) #major = list(range(v1+1, v2)) - + minor = [] for v in range(v1, v2): minor.extend(v + np.log10(np.arange(1, 10))) @@ -734,21 +745,21 @@ class AxisItem(GraphicsWidget): return ticks def tickStrings(self, values, scale, spacing): - """Return the strings that should be placed next to ticks. This method is called + """Return the strings that should be placed next to ticks. This method is called when redrawing the axis and is a good method to override in subclasses. - The method is called with a list of tick values, a scaling factor (see below), and the - spacing between ticks (this is required since, in some instances, there may be only + The method is called with a list of tick values, a scaling factor (see below), and the + spacing between ticks (this is required since, in some instances, there may be only one tick and thus no other way to determine the tick spacing) - + The scale argument is used when the axis label is displaying units which may have an SI scaling prefix. When determining the text to display, use value*scale to correctly account for this prefix. For example, if the axis label's units are set to 'V', then a tick value of 0.001 might - be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and + be accompanied by a scale value of 1000. This indicates that the label is displaying 'mV', and thus the tick should display 0.001 * 1000 = 1. """ if self.logMode: return self.logTickStrings(values, scale, spacing) - + places = max(0, np.ceil(-np.log10(spacing*scale))) strings = [] for v in values: @@ -759,27 +770,27 @@ class AxisItem(GraphicsWidget): vstr = ("%%0.%df" % places) % vs strings.append(vstr) return strings - + def logTickStrings(self, values, scale, spacing): return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)] - + def generateDrawSpecs(self, p): """ Calls tickValues() and tickStrings() to determine where and how ticks should - be drawn, then generates from this a set of drawing commands to be + be drawn, then generates from this a set of drawing commands to be interpreted by drawPicture(). """ profiler = debug.Profiler() #bounds = self.boundingRect() bounds = self.mapRectFromParent(self.geometry()) - + linkedView = self.linkedView() if linkedView is None or self.grid is False: tickBounds = bounds else: tickBounds = linkedView.mapRectToItem(self, linkedView.boundingRect()) - + if self.orientation == 'left': span = (bounds.topRight(), bounds.bottomRight()) tickStart = tickBounds.right() @@ -805,7 +816,7 @@ class AxisItem(GraphicsWidget): tickDir = 1 axis = 1 #print tickStart, tickStop, span - + ## determine size of this item in pixels points = list(map(self.mapToDevice, span)) if None in points: @@ -830,7 +841,7 @@ class AxisItem(GraphicsWidget): for val, strn in level: values.append(val) strings.append(strn) - + ## determine mapping between tick values and local coordinates dif = self.range[1] - self.range[0] if dif == 0: @@ -843,29 +854,29 @@ class AxisItem(GraphicsWidget): else: xScale = bounds.width() / dif offset = self.range[0] * xScale - + xRange = [x * xScale - offset for x in self.range] xMin = min(xRange) xMax = max(xRange) - + profiler('init') - + tickPositions = [] # remembers positions of previously drawn ticks - + ## compute coordinates to draw ticks ## draw three different intervals, long ticks first tickSpecs = [] for i in range(len(tickLevels)): tickPositions.append([]) ticks = tickLevels[i][1] - + ## length of tick tickLength = self.style['tickLength'] / ((i*0.5)+1.0) - + lineAlpha = 255 / (i+1) if self.grid is not False: lineAlpha *= self.grid/255. * np.clip((0.05 * lengthInPixels / (len(ticks)+1)), 0., 1.) - + for v in ticks: ## determine actual position to draw this tick x = (v * xScale) - offset @@ -873,7 +884,7 @@ class AxisItem(GraphicsWidget): tickPositions[i].append(None) continue tickPositions[i].append(x) - + p1 = [x, x] p2 = [x, x] p1[axis] = tickStart @@ -887,7 +898,7 @@ class AxisItem(GraphicsWidget): tickSpecs.append((tickPen, Point(p1), Point(p2))) profiler('compute ticks') - + if self.style['stopAxisAtTick'][0] is True: stop = max(span[0].y(), min(map(min, tickPositions))) if axis == 0: @@ -902,7 +913,7 @@ class AxisItem(GraphicsWidget): span[1].setX(stop) axisSpec = (self.pen(), span[0], span[1]) - + textOffset = self.style['tickTextOffset'][axis] ## spacing between axis and text #if self.style['autoExpandTextSpace'] is True: #textWidth = self.textWidth @@ -910,15 +921,15 @@ class AxisItem(GraphicsWidget): #else: #textWidth = self.style['tickTextWidth'] ## space allocated for horizontal text #textHeight = self.style['tickTextHeight'] ## space allocated for horizontal text - + textSize2 = 0 textRects = [] textSpecs = [] ## list of draw - + # If values are hidden, return early if not self.style['showValues']: return (axisSpec, tickSpecs, textSpecs) - + for i in range(min(len(tickLevels), self.style['maxTextLevel']+1)): ## Get the list of strings to display for this level if tickStrings is None: @@ -926,10 +937,10 @@ class AxisItem(GraphicsWidget): strings = self.tickStrings(values, self.autoSIPrefixScale * self.scale, spacing) else: strings = tickStrings[i] - + if len(strings) == 0: continue - + ## ignore strings belonging to ticks that were previously ignored for j in range(len(strings)): if tickPositions[i][j] is None: @@ -945,10 +956,10 @@ class AxisItem(GraphicsWidget): ## boundingRect is usually just a bit too large ## (but this probably depends on per-font metrics?) br.setHeight(br.height() * 0.8) - + rects.append(br) textRects.append(rects[-1]) - + if len(textRects) > 0: ## measure all text, make sure there's enough room if axis == 0: @@ -973,7 +984,7 @@ class AxisItem(GraphicsWidget): break if finished: break - + #spacing, values = tickLevels[best] #strings = self.tickStrings(values, self.scale, spacing) # Determine exactly where tick text should be drawn @@ -1006,24 +1017,24 @@ class AxisItem(GraphicsWidget): #p.drawText(rect, textFlags, vstr) textSpecs.append((rect, textFlags, vstr)) profiler('compute text') - + ## update max text size if needed. self._updateMaxTextSize(textSize2) - + return (axisSpec, tickSpecs, textSpecs) - + def drawPicture(self, p, axisSpec, tickSpecs, textSpecs): profiler = debug.Profiler() p.setRenderHint(p.Antialiasing, False) p.setRenderHint(p.TextAntialiasing, True) - + ## draw long line along axis pen, p1, p2 = axisSpec p.setPen(pen) p.drawLine(p1, p2) p.translate(0.5,0) ## resolves some damn pixel ambiguity - + ## draw ticks for pen, p1, p2 in tickSpecs: p.setPen(pen) @@ -1045,7 +1056,7 @@ class AxisItem(GraphicsWidget): self._updateWidth() else: self._updateHeight() - + def hide(self): GraphicsWidget.hide(self) if self.orientation in ['left', 'right']: @@ -1054,23 +1065,23 @@ class AxisItem(GraphicsWidget): self._updateHeight() def wheelEvent(self, ev): - if self.linkedView() is None: + if self.linkedView() is None: return if self.orientation in ['left', 'right']: self.linkedView().wheelEvent(ev, axis=1) else: self.linkedView().wheelEvent(ev, axis=0) ev.accept() - + def mouseDragEvent(self, event): - if self.linkedView() is None: + if self.linkedView() is None: return if self.orientation in ['left', 'right']: return self.linkedView().mouseDragEvent(event, axis=1) else: return self.linkedView().mouseDragEvent(event, axis=0) - + def mouseClickEvent(self, event): - if self.linkedView() is None: + if self.linkedView() is None: return return self.linkedView().mouseClickEvent(event) From 5a53539be0d05e2d04ac8191f3700e5eb9d1481d Mon Sep 17 00:00:00 2001 From: danielhrisca Date: Wed, 16 Jan 2019 17:04:06 +0200 Subject: [PATCH 318/607] enforce enableMenu in ViewBox --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 493 +++++++++++---------- 1 file changed, 256 insertions(+), 237 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0982cb37..434a3980 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -35,21 +35,21 @@ class WeakList(object): i -= 1 class ChildGroup(ItemGroup): - + def __init__(self, parent): ItemGroup.__init__(self, parent) - - # Used as callback to inform ViewBox when items are added/removed from - # the group. - # Note 1: We would prefer to override itemChange directly on the + + # Used as callback to inform ViewBox when items are added/removed from + # the group. + # Note 1: We would prefer to override itemChange directly on the # ViewBox, but this causes crashes on PySide. # Note 2: We might also like to use a signal rather than this callback - # mechanism, but this causes a different PySide crash. + # mechanism, but this causes a different PySide crash. self.itemsChangedListeners = WeakList() - + # excempt from telling view when transform changes self._GraphicsObject__inform_view_on_change = False - + def itemChange(self, change, value): ret = ItemGroup.itemChange(self, change, value) if change == self.ItemChildAddedChange or change == self.ItemChildRemovedChange: @@ -68,19 +68,19 @@ class ChildGroup(ItemGroup): class ViewBox(GraphicsWidget): """ **Bases:** :class:`GraphicsWidget ` - - Box that allows internal scaling/panning of children by mouse drag. + + Box that allows internal scaling/panning of children by mouse drag. This class is usually created automatically as part of a :class:`PlotItem ` or :class:`Canvas ` or with :func:`GraphicsLayout.addViewBox() `. - + Features: - + * Scaling contents by mouse or auto-scale when contents change * View linking--multiple views display the same data ranges * Configurable by context menu * Item coordinate mapping methods - + """ - + sigYRangeChanged = QtCore.Signal(object, object) sigXRangeChanged = QtCore.Signal(object, object) sigRangeChangedManually = QtCore.Signal(object) @@ -88,20 +88,20 @@ class ViewBox(GraphicsWidget): sigStateChanged = QtCore.Signal(object) sigTransformChanged = QtCore.Signal(object) sigResized = QtCore.Signal(object) - + ## mouse modes PanMode = 3 RectMode = 1 - + ## axes XAxis = 0 YAxis = 1 XYAxes = 2 - + ## for linking views together NamedViews = weakref.WeakValueDictionary() # name: ViewBox AllViews = weakref.WeakKeyDictionary() # ViewBox: None - + def __init__(self, parent=None, border=None, lockAspect=False, enableMouse=True, invertY=False, enableMenu=True, name=None, invertX=False): """ ============== ============================================================= @@ -114,15 +114,15 @@ class ViewBox(GraphicsWidget): *enableMouse* (bool) Whether mouse can be used to scale/pan the view *invertY* (bool) See :func:`invertY ` *invertX* (bool) See :func:`invertX ` - *enableMenu* (bool) Whether to display a context menu when + *enableMenu* (bool) Whether to display a context menu when right-clicking on the ViewBox background. *name* (str) Used to register this ViewBox so that it appears in the "Link axis" dropdown inside other ViewBox context menus. This allows the user to manually link - the axes of any other view to this one. + the axes of any other view to this one. ============== ============================================================= """ - + GraphicsWidget.__init__(self, parent) self.name = None self.linksBlocked = False @@ -131,60 +131,60 @@ class ViewBox(GraphicsWidget): self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed. self._lastScene = None ## stores reference to the last known scene this view was a part of. - + self.state = { - + ## separating targetRange and viewRange allows the view to be resized ## while keeping all previously viewed contents visible 'targetRange': [[0,1], [0,1]], ## child coord. range visible [[xmin, xmax], [ymin, ymax]] 'viewRange': [[0,1], [0,1]], ## actual range viewed - + 'yInverted': invertY, 'xInverted': invertX, 'aspectLocked': False, ## False if aspect is unlocked, otherwise float specifies the locked ratio. - 'autoRange': [True, True], ## False if auto range is disabled, + 'autoRange': [True, True], ## False if auto range is disabled, ## otherwise float gives the fraction of data that is visible 'autoPan': [False, False], ## whether to only pan (do not change scaling) when auto-range is enabled - 'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot + 'autoVisibleOnly': [False, False], ## whether to auto-range only to the visible portion of a plot 'linkedViews': [None, None], ## may be None, "viewName", or weakref.ref(view) ## a name string indicates that the view *should* link to another, but no view with that name exists yet. - + 'mouseEnabled': [enableMouse, enableMouse], - 'mouseMode': ViewBox.PanMode if getConfigOption('leftButtonPan') else ViewBox.RectMode, + 'mouseMode': ViewBox.PanMode if getConfigOption('leftButtonPan') else ViewBox.RectMode, 'enableMenu': enableMenu, 'wheelScaleFactor': -1.0 / 8.0, 'background': None, - + # Limits 'limits': { - 'xLimits': [None, None], # Maximum and minimum visible X values - 'yLimits': [None, None], # Maximum and minimum visible Y values + 'xLimits': [None, None], # Maximum and minimum visible X values + 'yLimits': [None, None], # Maximum and minimum visible Y values 'xRange': [None, None], # Maximum and minimum X range - 'yRange': [None, None], # Maximum and minimum Y range + 'yRange': [None, None], # Maximum and minimum Y range } - + } self._updatingRange = False ## Used to break recursive loops. See updateAutoRange. self._itemBoundsCache = weakref.WeakKeyDictionary() - + self.locateGroup = None ## items displayed when using ViewBox.locate(item) - + self.setFlag(self.ItemClipsChildrenToShape) self.setFlag(self.ItemIsFocusable, True) ## so we can receive key presses - + ## childGroup is required so that ViewBox has local coordinates similar to device coordinates. ## this is a workaround for a Qt + OpenGL bug that causes improper clipping ## https://bugreports.qt.nokia.com/browse/QTBUG-23723 self.childGroup = ChildGroup(self) self.childGroup.itemsChangedListeners.append(self) - + self.background = QtGui.QGraphicsRectItem(self.rect()) self.background.setParentItem(self) self.background.setZValue(-1e6) self.background.setPen(fn.mkPen(None)) self.updateBackground() - + ## Make scale box that is shown when dragging on the view self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) @@ -192,36 +192,39 @@ class ViewBox(GraphicsWidget): self.rbScaleBox.setZValue(1e9) self.rbScaleBox.hide() self.addItem(self.rbScaleBox, ignoreBounds=True) - + ## show target rect for debugging self.target = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.target.setPen(fn.mkPen('r')) self.target.setParentItem(self) self.target.hide() - + self.axHistory = [] # maintain a history of zoom locations self.axHistoryPointer = -1 # pointer into the history. Allows forward/backward movement, not just "undo" - + self.setZValue(-100) self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) - + self.setAspectLocked(lockAspect) - + self.border = fn.mkPen(border) - self.menu = ViewBoxMenu(self) - + if enableMenu: + self.menu = ViewBoxMenu(self) + else: + self.menu = None + self.register(name) if name is None: self.updateViewLists() - + def register(self, name): """ - Add this ViewBox to the registered list of views. - + Add this ViewBox to the registered list of views. + This allows users to manually link the axes of any other ViewBox to - this one. The specified *name* will appear in the drop-down lists for + this one. The specified *name* will appear in the drop-down lists for axis linking in the context menus of all other views. - + The same can be accomplished by initializing the ViewBox with the *name* attribute. """ ViewBox.AllViews[self] = None @@ -248,7 +251,7 @@ class ViewBox(GraphicsWidget): def implements(self, interface): return interface == 'ViewBox' - + # removed due to https://bugreports.qt-project.org/browse/PYSIDE-86 #def itemChange(self, change, value): ## Note: Calling QWidget.itemChange causes segv in python 3 + PyQt @@ -263,9 +266,9 @@ class ViewBox(GraphicsWidget): #if scene is not None and hasattr(scene, 'sigPrepareForPaint'): #scene.sigPrepareForPaint.connect(self.prepareForPaint) #return ret - + def checkSceneChange(self): - # ViewBox needs to receive sigPrepareForPaint from its scene before + # ViewBox needs to receive sigPrepareForPaint from its scene before # being painted. However, we have no way of being informed when the # scene has changed in order to make this connection. The usual way # to do this is via itemChange(), but bugs prevent this approach @@ -280,16 +283,16 @@ class ViewBox(GraphicsWidget): scene.sigPrepareForPaint.connect(self.prepareForPaint) self.prepareForPaint() self._lastScene = scene - + def prepareForPaint(self): #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) # don't check whether auto range is enabled here--only check when setting dirty flag. - if self._autoRangeNeedsUpdate: # and autoRangeEnabled: + if self._autoRangeNeedsUpdate: # and autoRangeEnabled: self.updateAutoRange() self.updateMatrix() - + def getState(self, copy=True): - """Return the current state of the ViewBox. + """Return the current state of the ViewBox. Linked views are always converted to view names in the returned state.""" state = self.state.copy() views = [] @@ -305,7 +308,7 @@ class ViewBox(GraphicsWidget): return deepcopy(state) else: return state - + def setState(self, state): """Restore the state of this ViewBox. (see also getState)""" @@ -313,17 +316,24 @@ class ViewBox(GraphicsWidget): self.setXLink(state['linkedViews'][0]) self.setYLink(state['linkedViews'][1]) del state['linkedViews'] - + self.state.update(state) + + if self.state['enableMenu'] and self.menu is None: + self.menu = ViewBoxMenu(self) + self.updateViewLists() + else: + self.menu = None + self.updateViewRange() self.sigStateChanged.emit(self) def setBackgroundColor(self, color): """ Set the background color of the ViewBox. - + If color is None, then no background will be drawn. - + Added in version 0.9.9 """ self.background.setVisible(color is not None) @@ -348,10 +358,10 @@ class ViewBox(GraphicsWidget): self.setMouseMode(ViewBox.PanMode) else: raise Exception('graphicsItems:ViewBox:setLeftButtonAction: unknown mode = %s (Options are "pan" and "rect")' % mode) - + def innerSceneItem(self): return self.childGroup - + def setMouseEnabled(self, x=None, y=None): """ Set whether each axis is enabled for mouse interaction. *x*, *y* arguments must be True or False. @@ -362,17 +372,24 @@ class ViewBox(GraphicsWidget): if y is not None: self.state['mouseEnabled'][1] = y self.sigStateChanged.emit(self) - + def mouseEnabled(self): return self.state['mouseEnabled'][:] - + def setMenuEnabled(self, enableMenu=True): self.state['enableMenu'] = enableMenu + if enableMenu: + if self.menu is None: + self.menu = ViewBoxMenu(self) + self.updateViewLists() + else: + self.menu.setParent(None) + self.menu = None self.sigStateChanged.emit(self) def menuEnabled(self): - return self.state.get('enableMenu', True) - + return self.state.get('enableMenu', True) + def addItem(self, item, ignoreBounds=False): """ Add a QGraphicsItem to this view. The view will include this item when determining how to set its range @@ -387,7 +404,7 @@ class ViewBox(GraphicsWidget): if not ignoreBounds: self.addedItems.append(item) self.updateAutoRange() - + def removeItem(self, item): """Remove an item from this view.""" try: @@ -402,7 +419,7 @@ class ViewBox(GraphicsWidget): self.removeItem(i) for ch in self.childGroup.childItems(): ch.setParentItem(None) - + def resizeEvent(self, ev): self._matrixNeedsUpdate = True self.linkedXChanged() @@ -413,7 +430,7 @@ class ViewBox(GraphicsWidget): self.sigStateChanged.emit(self) self.background.setRect(self.rect()) self.sigResized.emit(self) - + def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" return [x[:] for x in self.state['viewRange']] ## return copy @@ -427,13 +444,13 @@ class ViewBox(GraphicsWidget): except: print("make qrectf failed:", self.state['viewRange']) raise - + def targetRange(self): return [x[:] for x in self.state['targetRange']] ## return copy - - def targetRect(self): + + def targetRect(self): """ - Return the region which has been requested to be visible. + Return the region which has been requested to be visible. (this is not necessarily the same as the region that is *actually* visible-- resizing and aspect ratio constraints can cause targetRect() and viewRect() to differ) """ @@ -455,30 +472,30 @@ class ViewBox(GraphicsWidget): def setRange(self, rect=None, xRange=None, yRange=None, padding=None, update=True, disableAutoRange=True): """ Set the visible range of the ViewBox. - Must specify at least one of *rect*, *xRange*, or *yRange*. - + Must specify at least one of *rect*, *xRange*, or *yRange*. + ================== ===================================================================== **Arguments:** *rect* (QRectF) The full range that should be visible in the view box. *xRange* (min,max) The range that should be visible along the x-axis. *yRange* (min,max) The range that should be visible along the y-axis. - *padding* (float) Expand the view by a fraction of the requested range. + *padding* (float) Expand the view by a fraction of the requested range. By default, this value is set between 0.02 and 0.1 depending on the size of the ViewBox. - *update* (bool) If True, update the range of the ViewBox immediately. + *update* (bool) If True, update the range of the ViewBox immediately. Otherwise, the update is deferred until before the next render. *disableAutoRange* (bool) If True, auto-ranging is diabled. Otherwise, it is left unchanged. ================== ===================================================================== - + """ #print self.name, "ViewBox.setRange", rect, xRange, yRange, padding #import traceback #traceback.print_stack() - + changes = {} # axes setRequested = [False, False] - + if rect is not None: changes = {0: [rect.left(), rect.right()], 1: [rect.top(), rect.bottom()]} setRequested = [True, True] @@ -492,27 +509,27 @@ class ViewBox(GraphicsWidget): if len(changes) == 0: print(rect) raise Exception("Must specify at least one of rect, xRange, or yRange. (gave rect=%s)" % str(type(rect))) - + # Update axes one at a time changed = [False, False] for ax, range in changes.items(): mn = min(range) mx = max(range) - - # If we requested 0 range, try to preserve previous scale. + + # If we requested 0 range, try to preserve previous scale. # Otherwise just pick an arbitrary scale. - if mn == mx: + if mn == mx: dy = self.state['viewRange'][ax][1] - self.state['viewRange'][ax][0] if dy == 0: dy = 1 mn -= dy*0.5 mx += dy*0.5 xpad = 0.0 - + # Make sure no nan/inf get through if not all(np.isfinite([mn, mx])): raise Exception("Cannot set range [%s, %s]" % (str(mn), str(mx))) - + # Apply padding if padding is None: xpad = self.suggestPadding(ax) @@ -521,20 +538,20 @@ class ViewBox(GraphicsWidget): p = (mx-mn) * xpad mn -= p mx += p - + # Set target range if self.state['targetRange'][ax] != [mn, mx]: self.state['targetRange'][ax] = [mn, mx] changed[ax] = True - - # Update viewRange to match targetRange as closely as possible while + + # Update viewRange to match targetRange as closely as possible while # accounting for aspect ratio constraint lockX, lockY = setRequested if lockX and lockY: lockX = False lockY = False self.updateViewRange(lockX, lockY) - + # Disable auto-range for each axis that was requested to be set if disableAutoRange: xOff = False if setRequested[0] else None @@ -545,11 +562,11 @@ class ViewBox(GraphicsWidget): # If nothing has changed, we are done. if any(changed): self.sigStateChanged.emit(self) - + # Update target rect for debugging if self.target.isVisible(): self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) - + # If ortho axes have auto-visible-only, update them now # Note that aspect ratio constraints and auto-visible probably do not work together.. if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): @@ -559,15 +576,15 @@ class ViewBox(GraphicsWidget): def setYRange(self, min, max, padding=None, update=True): """ - Set the visible Y range of the view to [*min*, *max*]. + Set the visible Y range of the view to [*min*, *max*]. The *padding* argument causes the range to be set larger by the fraction specified. (by default, this value is between 0.02 and 0.1 depending on the size of the ViewBox) """ self.setRange(yRange=[min, max], update=update, padding=padding) - + def setXRange(self, min, max, padding=None, update=True): """ - Set the visible X range of the view to [*min*, *max*]. + Set the visible X range of the view to [*min*, *max*]. The *padding* argument causes the range to be set larger by the fraction specified. (by default, this value is between 0.02 and 0.1 depending on the size of the ViewBox) """ @@ -576,9 +593,9 @@ class ViewBox(GraphicsWidget): def autoRange(self, padding=None, items=None, item=None): """ Set the range of the view box to make all children visible. - Note that this is not the same as enableAutoRange, which causes the view to + Note that this is not the same as enableAutoRange, which causes the view to automatically auto-range whenever its contents are changed. - + ============== ============================================================ **Arguments:** padding The fraction of the total data range to add on to the final @@ -593,10 +610,10 @@ class ViewBox(GraphicsWidget): else: print("Warning: ViewBox.autoRange(item=__) is deprecated. Use 'items' argument instead.") bounds = self.mapFromItemToView(item, item.boundingRect()).boundingRect() - + if bounds is not None: self.setRange(bounds, padding=padding) - + def suggestPadding(self, axis): l = self.width() if axis==0 else self.height() if l > 0: @@ -604,31 +621,31 @@ class ViewBox(GraphicsWidget): else: padding = 0.02 return padding - + def setLimits(self, **kwds): """ Set limits that constrain the possible view ranges. - - **Panning limits**. The following arguments define the region within the + + **Panning limits**. The following arguments define the region within the viewbox coordinate system that may be accessed by panning the view. - + =========== ============================================================ xMin Minimum allowed x-axis value xMax Maximum allowed x-axis value yMin Minimum allowed y-axis value yMax Maximum allowed y-axis value - =========== ============================================================ - + =========== ============================================================ + **Scaling limits**. These arguments prevent the view being zoomed in or out too far. - + =========== ============================================================ minXRange Minimum allowed left-to-right span across the view. maxXRange Maximum allowed left-to-right span across the view. minYRange Minimum allowed top-to-bottom span across the view. maxYRange Maximum allowed top-to-bottom span across the view. =========== ============================================================ - + Added in version 0.9.9 """ update = False @@ -648,28 +665,28 @@ class ViewBox(GraphicsWidget): if kwd in kwds and self.state['limits'][lname][mnmx] != kwds[kwd]: self.state['limits'][lname][mnmx] = kwds[kwd] update = True - + if update: self.updateViewRange() - + def scaleBy(self, s=None, center=None, x=None, y=None): """ Scale by *s* around given center point (or center of view). *s* may be a Point or tuple (x, y). - - Optionally, x or y may be specified individually. This allows the other + + Optionally, x or y may be specified individually. This allows the other axis to be left unaffected (note that using a scale factor of 1.0 may cause slight changes due to floating-point error). """ if s is not None: x, y = s[0], s[1] - + affect = [x is not None, y is not None] if not any(affect): return - + scale = Point([1.0 if x is None else x, 1.0 if y is None else y]) - + if self.state['aspectLocked'] is not False: scale[0] = scale[1] @@ -678,21 +695,21 @@ class ViewBox(GraphicsWidget): center = Point(vr.center()) else: center = Point(center) - + tl = center + (vr.topLeft()-center) * scale br = center + (vr.bottomRight()-center) * scale - + if not affect[0]: self.setYRange(tl.y(), br.y(), padding=0) elif not affect[1]: self.setXRange(tl.x(), br.x(), padding=0) else: self.setRange(QtCore.QRectF(tl, br), padding=0) - + def translateBy(self, t=None, x=None, y=None): """ Translate the view by *t*, which may be a Point or tuple (x, y). - + Alternately, x or y may be specified independently, leaving the other axis unchanged (note that using a translation of 0 may still cause small changes due to floating-point error). @@ -708,7 +725,7 @@ class ViewBox(GraphicsWidget): y = vr.top()+y, vr.bottom()+y if x is not None or y is not None: self.setRange(xRange=x, yRange=y, padding=0) - + def enableAutoRange(self, axis=None, enable=True, x=None, y=None): """ Enable (or disable) auto-range for *axis*, which may be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes for both @@ -724,15 +741,15 @@ class ViewBox(GraphicsWidget): if y is not None: self.enableAutoRange(ViewBox.YAxis, y) return - + if enable is True: enable = 1.0 - + if axis is None: axis = ViewBox.XYAxes - + needAutoRangeUpdate = False - + if axis == ViewBox.XYAxes or axis == 'xy': axes = [0, 1] elif axis == ViewBox.XAxis or axis == 'x': @@ -741,18 +758,18 @@ class ViewBox(GraphicsWidget): axes = [1] else: raise Exception('axis argument must be ViewBox.XAxis, ViewBox.YAxis, or ViewBox.XYAxes.') - + for ax in axes: if self.state['autoRange'][ax] != enable: # If we are disabling, do one last auto-range to make sure that # previously scheduled auto-range changes are enacted if enable is False and self._autoRangeNeedsUpdate: self.updateAutoRange() - + self.state['autoRange'][ax] = enable self._autoRangeNeedsUpdate |= (enable is not False) self.update() - + self.sigStateChanged.emit(self) def disableAutoRange(self, axis=None): @@ -784,30 +801,30 @@ class ViewBox(GraphicsWidget): self.state['autoVisibleOnly'][1] = y if y is True: self.state['autoVisibleOnly'][0] = False - + if x is not None or y is not None: self.updateAutoRange() def updateAutoRange(self): ## Break recursive loops when auto-ranging. - ## This is needed because some items change their size in response + ## This is needed because some items change their size in response ## to a view change. if self._updatingRange: return - + self._updatingRange = True try: targetRect = self.viewRange() if not any(self.state['autoRange']): return - + fractionVisible = self.state['autoRange'][:] for i in [0,1]: if type(fractionVisible[i]) is bool: fractionVisible[i] = 1.0 childRange = None - + order = [0,1] if self.state['autoVisibleOnly'][0] is True: order = [1,0] @@ -820,11 +837,11 @@ class ViewBox(GraphicsWidget): oRange = [None, None] oRange[ax] = targetRect[1-ax] childRange = self.childrenBounds(frac=fractionVisible, orthoRange=oRange) - + else: if childRange is None: childRange = self.childrenBounds(frac=fractionVisible) - + ## Make corrections to range xr = childRange[ax] if xr is not None: @@ -839,32 +856,32 @@ class ViewBox(GraphicsWidget): childRange[ax][1] += wp targetRect[ax] = childRange[ax] args['xRange' if ax == 0 else 'yRange'] = targetRect[ax] - + # check for and ignore bad ranges for k in ['xRange', 'yRange']: if k in args: if not np.all(np.isfinite(args[k])): r = args.pop(k) #print("Warning: %s is invalid: %s" % (k, str(r)) - + if len(args) == 0: return args['padding'] = 0 args['disableAutoRange'] = False - + self.setRange(**args) finally: self._autoRangeNeedsUpdate = False self._updatingRange = False - + def setXLink(self, view): """Link this view's X axis to another view. (see LinkView)""" self.linkView(self.XAxis, view) - + def setYLink(self, view): """Link this view's Y axis to another view. (see LinkView)""" self.linkView(self.YAxis, view) - + def linkView(self, axis, view): """ Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis. @@ -896,8 +913,8 @@ class ViewBox(GraphicsWidget): except (TypeError, RuntimeError): ## This can occur if the view has been deleted already pass - - + + if view is None or isinstance(view, basestring): self.state['linkedViews'][axis] = view else: @@ -910,10 +927,10 @@ class ViewBox(GraphicsWidget): else: if self.autoRangeEnabled()[axis] is False: slot() - - + + self.sigStateChanged.emit(self) - + def blockLink(self, b): self.linksBlocked = b ## prevents recursive plot-change propagation @@ -926,7 +943,7 @@ class ViewBox(GraphicsWidget): ## called when y range of linked view has changed view = self.linkedView(1) self.linkedViewChanged(view, ViewBox.YAxis) - + def linkedView(self, ax): ## Return the linked view for axis *ax*. ## this method _always_ returns either a ViewBox or None. @@ -939,19 +956,19 @@ class ViewBox(GraphicsWidget): def linkedViewChanged(self, view, axis): if self.linksBlocked or view is None: return - + #print self.name, "ViewBox.linkedViewChanged", axis, view.viewRange()[axis] vr = view.viewRect() vg = view.screenGeometry() sg = self.screenGeometry() if vg is None or sg is None: return - + view.blockLink(True) try: if axis == ViewBox.XAxis: overlap = min(sg.right(), vg.right()) - max(sg.left(), vg.left()) - if overlap < min(vg.width()/3, sg.width()/3): ## if less than 1/3 of views overlap, + if overlap < min(vg.width()/3, sg.width()/3): ## if less than 1/3 of views overlap, ## then just replicate the view x1 = vr.left() x2 = vr.right() @@ -966,7 +983,7 @@ class ViewBox(GraphicsWidget): self.setXRange(x1, x2, padding=0) else: overlap = min(sg.bottom(), vg.bottom()) - max(sg.top(), vg.top()) - if overlap < min(vg.height()/3, sg.height()/3): ## if less than 1/3 of views overlap, + if overlap < min(vg.height()/3, sg.height()/3): ## if less than 1/3 of views overlap, ## then just replicate the view y1 = vr.top() y2 = vr.bottom() @@ -981,7 +998,7 @@ class ViewBox(GraphicsWidget): self.setYRange(y1, y2, padding=0) finally: view.blockLink(False) - + def screenGeometry(self): """return the screen geometry of the viewbox""" v = self.getViewWidget() @@ -996,7 +1013,7 @@ class ViewBox(GraphicsWidget): def itemsChanged(self): ## called when items are added/removed from self.childGroup self.updateAutoRange() - + def itemBoundsChanged(self, item): self._itemBoundsCache.pop(item, None) if (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False): @@ -1008,7 +1025,7 @@ class ViewBox(GraphicsWidget): key = 'xy'[ax] + 'Inverted' if self.state[key] == inv: return - + self.state[key] = inv self._matrixNeedsUpdate = True # updateViewRange won't detect this for us self.updateViewRange() @@ -1024,7 +1041,7 @@ class ViewBox(GraphicsWidget): def yInverted(self): return self.state['yInverted'] - + def invertX(self, b=True): """ By default, the positive x-axis points rightward on the screen. Use invertX(True) to reverse the x-axis. @@ -1033,14 +1050,14 @@ class ViewBox(GraphicsWidget): def xInverted(self): return self.state['xInverted'] - + def setAspectLocked(self, lock=True, ratio=1): """ If the aspect ratio is locked, view scaling must always preserve the aspect ratio. By default, the ratio is set to 1; x and y both have the same scaling. This ratio can be overridden (xScale/yScale), or use None to lock in the current ratio. """ - + if not lock: if self.state['aspectLocked'] == False: return @@ -1059,16 +1076,16 @@ class ViewBox(GraphicsWidget): self.state['aspectLocked'] = ratio if ratio != currentRatio: ## If this would change the current range, do that now self.updateViewRange() - + self.updateAutoRange() self.updateViewRange() self.sigStateChanged.emit(self) - + def childTransform(self): """ Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. (This maps from inside the viewbox to outside) - """ + """ self.updateMatrix() m = self.childGroup.transform() return m @@ -1094,7 +1111,7 @@ class ViewBox(GraphicsWidget): """Maps from the coordinate system displayed inside the ViewBox to scene coordinates""" self.updateMatrix() return self.mapToScene(self.mapFromView(obj)) - + def mapFromItemToView(self, item, obj): """Maps *obj* from the local coordinate system of *item* to the view coordinates""" self.updateMatrix() @@ -1109,17 +1126,17 @@ class ViewBox(GraphicsWidget): def mapViewToDevice(self, obj): self.updateMatrix() return self.mapToDevice(self.mapFromView(obj)) - + def mapDeviceToView(self, obj): self.updateMatrix() return self.mapToView(self.mapFromDevice(obj)) - + def viewPixelSize(self): """Return the (width, height) of a screen pixel in view coordinates.""" o = self.mapToView(Point(0,0)) px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()] return (px.length(), py.length()) - + def itemBoundingRect(self, item): """Return the bounding rect of the item in view coordinates""" return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() @@ -1133,12 +1150,12 @@ class ViewBox(GraphicsWidget): s = 1.02 ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor s = [(None if m is False else s) for m in mask] center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) - + self._resetTarget() self.scaleBy(s, center) self.sigRangeChangedManually.emit(mask) ev.accept() - + def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): ev.accept() @@ -1146,8 +1163,9 @@ class ViewBox(GraphicsWidget): def raiseContextMenu(self, ev): menu = self.getMenu(ev) - self.scene().addParentContextMenus(self, menu, ev) - menu.popup(ev.screenPos().toPoint()) + if menu is not None: + self.scene().addParentContextMenus(self, menu, ev) + menu.popup(ev.screenPos().toPoint()) def getMenu(self, ev): return self.menu @@ -1158,7 +1176,7 @@ class ViewBox(GraphicsWidget): def mouseDragEvent(self, ev, axis=None): ## if axis is specified, event will only affect that axis. ev.accept() ## we accept all buttons - + pos = ev.pos() lastPos = ev.lastPos() dif = pos - lastPos @@ -1189,7 +1207,7 @@ class ViewBox(GraphicsWidget): tr = self.mapToView(tr) - self.mapToView(Point(0,0)) x = tr.x() if mask[0] == 1 else None y = tr.y() if mask[1] == 1 else None - + self._resetTarget() if x is not None or y is not None: self.translateBy(x=x, y=y) @@ -1198,18 +1216,18 @@ class ViewBox(GraphicsWidget): #print "vb.rightDrag" if self.state['aspectLocked'] is not False: mask[0] = 0 - + dif = ev.screenPos() - ev.lastScreenPos() dif = np.array([dif.x(), dif.y()]) dif[0] *= -1 s = ((mask * 0.02) + 1) ** dif - + tr = self.childGroup.transform() tr = fn.invertQTransform(tr) - + x = s[0] if mouseEnabled[0] == 1 else None y = s[1] if mouseEnabled[1] == 1 else None - + center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton))) self._resetTarget() self.scaleBy(x=x, y=y, center=center) @@ -1223,7 +1241,7 @@ class ViewBox(GraphicsWidget): ctrl-A : zooms out to the default "full" view of the plot ctrl-+ : moves forward in the zooming stack (if it exists) ctrl-- : moves backward in the zooming stack (if it exists) - + """ ev.accept() if ev.text() == '-': @@ -1259,12 +1277,12 @@ class ViewBox(GraphicsWidget): """Return a list of all children and grandchildren of this ViewBox""" if item is None: item = self.childGroup - + children = [item] for ch in item.childItems(): children.extend(self.allChildren(ch)) return children - + def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): """Return the bounding range of all children. [[xmin, xmax], [ymin, ymax]] @@ -1273,19 +1291,19 @@ class ViewBox(GraphicsWidget): profiler = debug.Profiler() if items is None: items = self.addedItems - + ## measure pixel dimensions in view box px, py = [v.length() if v is not None else 0 for v in self.childGroup.pixelVectors()] - + ## First collect all boundary information itemBounds = [] for item in items: if not item.isVisible() or not item.scene() is self.scene(): continue - + useX = True useY = True - + if hasattr(item, 'dataBounds'): if frac is None: frac = (1.0, 1.0) @@ -1301,23 +1319,23 @@ class ViewBox(GraphicsWidget): bounds = QtCore.QRectF(xr[0], yr[0], xr[1]-xr[0], yr[1]-yr[0]) bounds = self.mapFromItemToView(item, bounds).boundingRect() - + if not any([useX, useY]): continue - + ## If we are ignoring only one axis, we need to check for rotations if useX != useY: ## != means xor ang = round(item.transformAngle()) if ang == 0 or ang == 180: pass elif ang == 90 or ang == 270: - useX, useY = useY, useX + useX, useY = useY, useX else: ## Item is rotated at non-orthogonal angle, ignore bounds entirely. ## Not really sure what is the expected behavior in this case. - continue ## need to check for item rotations and decide how best to apply this boundary. - - + continue ## need to check for item rotations and decide how best to apply this boundary. + + itemBounds.append((bounds, useX, useY, pxPad)) else: if int(item.flags() & item.ItemHasNoContents) > 0: @@ -1326,7 +1344,7 @@ class ViewBox(GraphicsWidget): bounds = item.boundingRect() bounds = self.mapFromItemToView(item, bounds).boundingRect() itemBounds.append((bounds, True, True, 0)) - + ## determine tentative new range range = [None, None] for bounds, useX, useY, px in itemBounds: @@ -1341,7 +1359,7 @@ class ViewBox(GraphicsWidget): else: range[0] = [bounds.left(), bounds.right()] profiler() - + ## Now expand any bounds that have a pixel margin ## This must be done _after_ we have a good estimate of the new range ## to ensure that the pixel size is roughly accurate. @@ -1363,7 +1381,7 @@ class ViewBox(GraphicsWidget): range[1][1] = max(range[1][1], bounds.bottom() + px*pxSize) return range - + def childrenBoundingRect(self, *args, **kwds): range = self.childrenBounds(*args, **kwds) tr = self.targetRange() @@ -1371,31 +1389,31 @@ class ViewBox(GraphicsWidget): range[0] = tr[0] if range[1] is None: range[1] = tr[1] - + bounds = QtCore.QRectF(range[0][0], range[1][0], range[0][1]-range[0][0], range[1][1]-range[1][0]) return bounds - + def updateViewRange(self, forceX=False, forceY=False): - ## Update viewRange to match targetRange as closely as possible, given - ## aspect ratio constraints. The *force* arguments are used to indicate + ## Update viewRange to match targetRange as closely as possible, given + ## aspect ratio constraints. The *force* arguments are used to indicate ## which axis (if any) should be unchanged when applying constraints. viewRange = [self.state['targetRange'][0][:], self.state['targetRange'][1][:]] changed = [False, False] - + #-------- Make correction for aspect ratio constraint ---------- - + # aspect is (widget w/h) / (view range w/h) 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: @@ -1403,11 +1421,11 @@ class ViewBox(GraphicsWidget): elif forceY: ax = 1 else: - # if we are not required to keep a particular axis unchanged, + # 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: + + if ax == 0: ## view range needs to be taller than target dy = 0.5 * (tr.width() / viewRatio - tr.height()) if dy != 0: @@ -1420,27 +1438,27 @@ class ViewBox(GraphicsWidget): 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]] maxRng = [self.state['limits']['xRange'][1], self.state['limits']['yRange'][1]] - + for axis in [0, 1]: if limits[axis][0] is None and limits[axis][1] is None and minRng[axis] is None and maxRng[axis] is None: continue - + # 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]) else: 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] - + # Apply xRange, yRange diff = viewRange[axis][1] - viewRange[axis][0] if maxRng[axis] is not None and diff > maxRng[axis]: @@ -1451,12 +1469,12 @@ class ViewBox(GraphicsWidget): changed[axis] = True else: delta = 0 - + viewRange[axis][0] -= delta/2. viewRange[axis][1] += delta/2. - + #print "after applying min/max:", viewRange[axis] - + # Apply xLimits, yLimits mn, mx = limits[axis] if mn is not None and viewRange[axis][0] < mn: @@ -1469,23 +1487,23 @@ class ViewBox(GraphicsWidget): 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 - + # emit range change signals if changed[0]: self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) if changed[1]: self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) - + if any(changed): self._matrixNeedsUpdate = True self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() - + # Inform linked views that the range has changed for ax in [0, 1]: if not changed[ax]: @@ -1493,14 +1511,14 @@ class ViewBox(GraphicsWidget): link = self.linkedView(ax) if link is not None: link.linkedViewChanged(self, ax) - + def updateMatrix(self, changed=None): if not self._matrixNeedsUpdate: return ## Make the childGroup's transform match the requested viewRange. bounds = self.rect() - + vr = self.viewRect() if vr.height() == 0 or vr.width() == 0: return @@ -1510,30 +1528,30 @@ class ViewBox(GraphicsWidget): if self.state['xInverted']: scale = scale * Point(-1, 1) m = QtGui.QTransform() - + ## First center the viewport at 0 center = bounds.center() m.translate(center.x(), center.y()) - + ## Now scale and translate properly m.scale(scale[0], scale[1]) st = Point(vr.center()) m.translate(-st[0], -st[1]) - + self.childGroup.setTransform(m) - + self.sigTransformChanged.emit(self) ## segfaults here: 1 self._matrixNeedsUpdate = False def paint(self, p, opt, widget): self.checkSceneChange() - + if self.border is not None: bounds = self.shape() p.setPen(self.border) #p.fillRect(bounds, QtGui.QColor(0, 0, 0)) p.drawPath(bounds) - + #p.setPen(fn.mkPen('r')) #path = QtGui.QPainterPath() #path.addRect(self.targetRect()) @@ -1547,27 +1565,28 @@ class ViewBox(GraphicsWidget): else: self.background.show() self.background.setBrush(fn.mkBrush(bg)) - + def updateViewLists(self): try: self.window() except RuntimeError: ## this view has already been deleted; it will probably be collected shortly. return - + def cmpViews(a, b): wins = 100 * cmp(a.window() is self.window(), b.window() is self.window()) alpha = cmp(a.name, b.name) return wins + alpha - + ## make a sorted list of all named views nv = list(ViewBox.NamedViews.values()) sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList - + if self in nv: nv.remove(self) - - self.menu.setViewList(nv) - + + if self.menu is not None: + self.menu.setViewList(nv) + for ax in [0,1]: link = self.state['linkedViews'][ax] if isinstance(link, basestring): ## axis has not been linked yet; see if it's possible now @@ -1601,7 +1620,7 @@ class ViewBox(GraphicsWidget): for k in ViewBox.AllViews: if isQObjectAlive(k) and getConfigOption('crashWarning'): sys.stderr.write('Warning: ViewBox should be closed before application exit.\n') - + try: k.destroyed.disconnect() except RuntimeError: ## signal is already disconnected. @@ -1610,7 +1629,7 @@ class ViewBox(GraphicsWidget): pass except AttributeError: # PySide has deleted signal pass - + def locate(self, item, timeout=3.0, children=False): """ Temporarily display the bounding rect of an item and lines connecting to the center of the view. @@ -1618,16 +1637,16 @@ class ViewBox(GraphicsWidget): if allChildren is True, then the bounding rect of all item's children will be shown instead. """ self.clearLocate() - + if item.scene() is not self.scene(): raise Exception("Item does not share a scene with this ViewBox.") - + c = self.viewRect().center() if children: br = self.mapFromItemToView(item, item.childrenBoundingRect()).boundingRect() else: br = self.mapFromItemToView(item, item.boundingRect()).boundingRect() - + g = ItemGroup() g.setParentItem(self.childGroup) self.locateGroup = g @@ -1638,11 +1657,11 @@ class ViewBox(GraphicsWidget): line = QtGui.QGraphicsLineItem(c.x(), c.y(), p.x(), p.y()) line.setParentItem(g) g.lines.append(line) - + for item in g.childItems(): item.setPen(fn.mkPen(color='y', width=3)) g.setZValue(1000000) - + if children: g.path = QtGui.QGraphicsPathItem(g.childrenShape()) else: @@ -1650,9 +1669,9 @@ class ViewBox(GraphicsWidget): g.path.setParentItem(g) g.path.setPen(fn.mkPen('g')) g.path.setZValue(100) - + QtCore.QTimer.singleShot(timeout*1000, self.clearLocate) - + def clearLocate(self): if self.locateGroup is None: return From e4681b720108e375486ea8e22d0b9bf41ad7a53a Mon Sep 17 00:00:00 2001 From: Sara Zanzottera Date: Thu, 17 Jan 2019 11:19:16 +0100 Subject: [PATCH 319/607] Fix issue #811 --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 0982cb37..610746b2 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1014,7 +1014,10 @@ class ViewBox(GraphicsWidget): self.updateViewRange() self.update() self.sigStateChanged.emit(self) - self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][ax])) + if ax: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][ax])) + else: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][ax])) def invertY(self, b=True): """ From 82d2b757e47ae5effe15cab9d7acd67987c78c22 Mon Sep 17 00:00:00 2001 From: danielhrisca Date: Fri, 18 Jan 2019 10:31:37 +0200 Subject: [PATCH 320/607] speed up AxisItem __init__ --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index a0d0bcbd..3e358870 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -88,6 +88,8 @@ class AxisItem(GraphicsWidget): self.autoSIPrefix = True self.autoSIPrefixScale = 1.0 + self.showLabel(False) + self.setRange(0, 1) if pen is None: @@ -99,8 +101,6 @@ class AxisItem(GraphicsWidget): if linkView is not None: self.linkToView(linkView) - self.showLabel(False) - self.grid = False #self.setCacheMode(self.DeviceCoordinateCache) From e2ca71a65ca3459d145717f3fdf537709a09c6c2 Mon Sep 17 00:00:00 2001 From: ronpandolfi Date: Fri, 1 Feb 2019 19:44:54 -0800 Subject: [PATCH 321/607] Fix for PySide2; QtCore.QPoint.__sub__ no longer works with tuples --- pyqtgraph/widgets/GraphicsView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index e1a7327e..b81eab9d 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -362,7 +362,7 @@ class GraphicsView(QtGui.QGraphicsView): def mouseMoveEvent(self, ev): if self.lastMousePos is None: self.lastMousePos = Point(ev.pos()) - delta = Point(ev.pos() - self.lastMousePos) + delta = Point(ev.pos() - QtCore.QPoint(*self.lastMousePos)) self.lastMousePos = Point(ev.pos()) QtGui.QGraphicsView.mouseMoveEvent(self, ev) From 691da09eb0897ddc1361b72a122f20e4c94b5851 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 14 Feb 2019 16:36:02 -0500 Subject: [PATCH 322/607] MNT: do not use 'is' with literals, use == py3.8 gives a syntax warning on this --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index b8face5e..ae74c5b6 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -986,8 +986,8 @@ class PlotItem(GraphicsWidget): self._menuEnabled = enableMenu if enableViewBoxMenu is None: return - if enableViewBoxMenu is 'same': - enableViewBoxMenu = enableMenu + if enableViewBoxMenu == 'same': + enableViewBoxMenu = enableMenu self.vb.setMenuEnabled(enableViewBoxMenu) def menuEnabled(self): From 4fe90bb21514a729d76a877c394b970234b066ef Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 14 Feb 2019 16:39:45 -0500 Subject: [PATCH 323/607] MNT: escape docstrings that have rst escaping in them --- pyqtgraph/graphicsItems/ROI.py | 41 ++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 84a8d0bd..a710f808 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1102,9 +1102,9 @@ class ROI(GraphicsObject): return bounds, tr def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): - """Use the position and orientation of this ROI relative to an imageItem + r"""Use the position and orientation of this ROI relative to an imageItem to pull a slice from an array. - + =================== ==================================================== **Arguments** data The array to slice from. Note that this array does @@ -1524,9 +1524,9 @@ class TestROI(ROI): class RectROI(ROI): - """ + r""" Rectangular ROI subclass with a single scale handle at the top-right corner. - + ============== ============================================================= **Arguments** pos (length-2 sequence) The position of the ROI origin. @@ -1586,11 +1586,13 @@ class LineROI(ROI): + + class MultiRectROI(QtGui.QGraphicsObject): - """ - Chain of rectangular ROIs connected by handles. - - This is generally used to mark a curved path through + r""" + Chain of rectangular ROIs connected by handles. + + This is generally used to mark a curved path through an image similarly to PolyLineROI. It differs in that each segment of the chain is rectangular instead of linear and thus has width. @@ -1724,12 +1726,12 @@ class MultiLineROI(MultiRectROI): MultiRectROI.__init__(self, *args, **kwds) print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") - + class EllipseROI(ROI): - """ + r""" Elliptical ROI subclass with one scale handle and one rotation handle. - - + + ============== ============================================================= **Arguments** pos (length-2 sequence) The position of the ROI's origin. @@ -1810,8 +1812,9 @@ class EllipseROI(ROI): return self.path + class CircleROI(EllipseROI): - """ + r""" Circular ROI subclass. Behaves exactly as EllipseROI, but may only be scaled proportionally to maintain its aspect ratio. @@ -1878,13 +1881,13 @@ class PolygonROI(ROI): sc['angle'] = self.state['angle'] return sc - + class PolyLineROI(ROI): - """ + r""" Container class for multiple connected LineSegmentROIs. - + This class allows the user to draw paths of multiple line segments. - + ============== ============================================================= **Arguments** positions (list of length-2 sequences) The list of points in the path. @@ -2076,9 +2079,9 @@ class PolyLineROI(ROI): class LineSegmentROI(ROI): - """ + r""" ROI subclass with two freely-moving handles defining a line. - + ============== ============================================================= **Arguments** positions (list of two length-2 sequences) The endpoints of the line From 0649ff8f3cb4605484f1bc1fd13bc55e9207ba0a Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 14 Feb 2019 16:41:06 -0500 Subject: [PATCH 324/607] MNT: do not use 'is not' on literal py38 raises a SyntaxWarning on this --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index f85b64dd..7272aef3 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -186,7 +186,7 @@ class HistogramLUTItem(GraphicsWidget): """Return a lookup table from the color gradient defined by this HistogramLUTItem. """ - if self.levelMode is not 'mono': + if self.levelMode != 'mono': return None if n is None: if img.dtype == np.uint8: From da1bf54ec86de69b2ae884e5bdb896cd224010b8 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Thu, 14 Feb 2019 16:41:54 -0500 Subject: [PATCH 325/607] MNT: use raw for regular expression --- pyqtgraph/parametertree/Parameter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index be77c9ff..df6b1492 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -612,7 +612,7 @@ class Parameter(QtCore.QObject): def incrementName(self, name): ## return an unused name by adding a number to the name given - base, num = re.match('(.*)(\d*)', name).groups() + base, num = re.match(r'(.*)(\d*)', name).groups() numLen = len(num) if numLen == 0: num = 2 From fb098e9cdc0cdf75bdcb4c01874b05c2f6503151 Mon Sep 17 00:00:00 2001 From: "ALLENINST\\stephanies" Date: Wed, 6 Mar 2019 12:42:22 -0800 Subject: [PATCH 326/607] add option to set other field variables in ColorMapWidget --- pyqtgraph/widgets/ColorMapWidget.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 7e6bfab7..8939d632 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -90,6 +90,10 @@ class ColorMapParameter(ptree.types.GroupParameter): values List of unique values for which the user may assign a color when mode=='enum'. Optionally may specify a dict instead {value: name}. + colormap assign a colormap to the field + min/max assign a min/max to the field, default is 0, 1 + operation assign operation for merging multiple maps from ['Overlay', + 'Add', 'Multiply', 'Set'], default is 'Overlay' ============== ============================================================ """ self.fields = OrderedDict(fields) @@ -169,12 +173,12 @@ class RangeColorMapItem(ptree.types.SimpleParameter): self.fieldName = name units = opts.get('units', '') ptree.types.SimpleParameter.__init__(self, - name=name, autoIncrementName=True, type='colormap', removable=True, renamable=True, + name=name, autoIncrementName=True, type='colormap', removable=True, renamable=True, value=opts.get('colormap', None), children=[ #dict(name="Field", type='list', value=name, values=fields), - dict(name='Min', type='float', value=0.0, suffix=units, siPrefix=True), - dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True), - dict(name='Operation', type='list', value='Overlay', values=['Overlay', 'Add', 'Multiply', 'Set']), + dict(name='Min', type='float', value=opts.get('min', 0.0), suffix=units, siPrefix=True), + dict(name='Max', type='float', value=opts.get('max', 1.0), suffix=units, siPrefix=True), + dict(name='Operation', type='list', value=opts.get('operation', 'Overlay'), values=['Overlay', 'Add', 'Multiply', 'Set']), dict(name='Channels..', type='group', expanded=False, children=[ dict(name='Red', type='bool', value=True), dict(name='Green', type='bool', value=True), @@ -219,7 +223,7 @@ class EnumColorMapItem(ptree.types.GroupParameter): name=name, autoIncrementName=True, removable=True, renamable=True, children=[ dict(name='Values', type='group', children=childs), - dict(name='Operation', type='list', value='Overlay', values=['Overlay', 'Add', 'Multiply', 'Set']), + dict(name='Operation', type='list', value=opts.get('operation', 'Overlay'), values=['Overlay', 'Add', 'Multiply', 'Set']), dict(name='Channels..', type='group', expanded=False, children=[ dict(name='Red', type='bool', value=True), dict(name='Green', type='bool', value=True), From bafe1b4b7422fff6bfdde60117b95d7905def66c Mon Sep 17 00:00:00 2001 From: "ALLENINST\\stephanies" Date: Fri, 8 Mar 2019 12:00:21 -0800 Subject: [PATCH 327/607] group all defaults into one argument --- pyqtgraph/widgets/ColorMapWidget.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index 8939d632..e06f250e 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -90,10 +90,7 @@ class ColorMapParameter(ptree.types.GroupParameter): values List of unique values for which the user may assign a color when mode=='enum'. Optionally may specify a dict instead {value: name}. - colormap assign a colormap to the field - min/max assign a min/max to the field, default is 0, 1 - operation assign operation for merging multiple maps from ['Overlay', - 'Add', 'Multiply', 'Set'], default is 'Overlay' + defaults Dict of ColorMapParameter children and it's default value ============== ============================================================ """ self.fields = OrderedDict(fields) @@ -173,12 +170,12 @@ class RangeColorMapItem(ptree.types.SimpleParameter): self.fieldName = name units = opts.get('units', '') ptree.types.SimpleParameter.__init__(self, - name=name, autoIncrementName=True, type='colormap', removable=True, renamable=True, value=opts.get('colormap', None), + name=name, autoIncrementName=True, type='colormap', removable=True, renamable=True, children=[ #dict(name="Field", type='list', value=name, values=fields), - dict(name='Min', type='float', value=opts.get('min', 0.0), suffix=units, siPrefix=True), - dict(name='Max', type='float', value=opts.get('max', 1.0), suffix=units, siPrefix=True), - dict(name='Operation', type='list', value=opts.get('operation', 'Overlay'), values=['Overlay', 'Add', 'Multiply', 'Set']), + dict(name='Min', type='float', value=0.0, suffix=units, siPrefix=True), + dict(name='Max', type='float', value=1.0, suffix=units, siPrefix=True), + dict(name='Operation', type='list', value='Overlay', values=['Overlay', 'Add', 'Multiply', 'Set']), dict(name='Channels..', type='group', expanded=False, children=[ dict(name='Red', type='bool', value=True), dict(name='Green', type='bool', value=True), @@ -188,7 +185,14 @@ class RangeColorMapItem(ptree.types.SimpleParameter): dict(name='Enabled', type='bool', value=True), dict(name='NaN', type='color'), ]) - + if 'defaults' in opts: + defaults = opts['defaults'] + for k, v in defaults.items(): + if k == 'colormap': + self.setValue(v) + else: + self[k] = v + def map(self, data): data = data[self.fieldName] @@ -223,7 +227,7 @@ class EnumColorMapItem(ptree.types.GroupParameter): name=name, autoIncrementName=True, removable=True, renamable=True, children=[ dict(name='Values', type='group', children=childs), - dict(name='Operation', type='list', value=opts.get('operation', 'Overlay'), values=['Overlay', 'Add', 'Multiply', 'Set']), + dict(name='Operation', type='list', value='Overlay', values=['Overlay', 'Add', 'Multiply', 'Set']), dict(name='Channels..', type='group', expanded=False, children=[ dict(name='Red', type='bool', value=True), dict(name='Green', type='bool', value=True), From 894c9a43621f509b7252949b0908ee327d3ec035 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 8 Mar 2019 12:33:14 -0800 Subject: [PATCH 328/607] move default handling up to colormapparameter --- pyqtgraph/widgets/ColorMapWidget.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index e06f250e..b1235571 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -60,11 +60,21 @@ class ColorMapParameter(ptree.types.GroupParameter): self.sigColorMapChanged.emit(self) def addNew(self, name): - mode = self.fields[name].get('mode', 'range') + fieldSpec = self.fields[name] + + mode = fieldSpec.get('mode', 'range') if mode == 'range': item = RangeColorMapItem(name, self.fields[name]) elif mode == 'enum': item = EnumColorMapItem(name, self.fields[name]) + + defaults = fieldSpec.get('defaults', {}) + for k, v in defaults.items(): + if k == 'colormap': + item.setValue(v) + else: + item[k] = v + self.addChild(item) return item @@ -90,7 +100,7 @@ class ColorMapParameter(ptree.types.GroupParameter): values List of unique values for which the user may assign a color when mode=='enum'. Optionally may specify a dict instead {value: name}. - defaults Dict of ColorMapParameter children and it's default value + defaults Dict of ColorMapParameter children and its default value ============== ============================================================ """ self.fields = OrderedDict(fields) @@ -137,8 +147,7 @@ class ColorMapParameter(ptree.types.GroupParameter): c3[:,3:4] = colors[:,3:4] + (1-colors[:,3:4]) * a colors = c3 elif op == 'Set': - colors[mask] = colors2[mask] - + colors[mask] = colors2[mask] colors = np.clip(colors, 0, 1) if mode == 'byte': @@ -185,13 +194,6 @@ class RangeColorMapItem(ptree.types.SimpleParameter): dict(name='Enabled', type='bool', value=True), dict(name='NaN', type='color'), ]) - if 'defaults' in opts: - defaults = opts['defaults'] - for k, v in defaults.items(): - if k == 'colormap': - self.setValue(v) - else: - self[k] = v def map(self, data): data = data[self.fieldName] From 41107e4caa41737e9dfc3da6e88137b8d4045f7d Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 8 Mar 2019 12:46:10 -0800 Subject: [PATCH 329/607] expand docstring --- pyqtgraph/widgets/ColorMapWidget.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/ColorMapWidget.py b/pyqtgraph/widgets/ColorMapWidget.py index b1235571..b5e25d94 100644 --- a/pyqtgraph/widgets/ColorMapWidget.py +++ b/pyqtgraph/widgets/ColorMapWidget.py @@ -100,7 +100,11 @@ class ColorMapParameter(ptree.types.GroupParameter): values List of unique values for which the user may assign a color when mode=='enum'. Optionally may specify a dict instead {value: name}. - defaults Dict of ColorMapParameter children and its default value + defaults Dict of default values to apply to color map items when + they are created. Valid keys are 'colormap' to provide + a default color map, or otherwise they a string or tuple + indicating the parameter to be set, such as 'Operation' or + ('Channels..', 'Red'). ============== ============================================================ """ self.fields = OrderedDict(fields) From 365c13fedc26f316149f9d8f114598b29d65703f Mon Sep 17 00:00:00 2001 From: SamSchott Date: Thu, 14 Mar 2019 22:41:10 +0000 Subject: [PATCH 330/607] Clipping: don't assume that x-values have uniform spacing Do not assume that x-values have uniform spacing -- this can cause problems especially with large datasets and non-uniform spacing (e.g., time-dependent readings from an instrument). Use `np.searchsorted` instead to find the first and last data index in the view range. This only assumes that x-values are in ascending order. This prevents potentially too strong clipping. --- pyqtgraph/graphicsItems/PlotDataItem.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 6797af64..69d7dc6e 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -540,15 +540,16 @@ class PlotDataItem(GraphicsObject): if self.opts['clipToView']: view = self.getViewBox() if view is None or not view.autoRangeEnabled()[0]: - # this option presumes that x-values have uniform spacing + # this option presumes that x-values are in increasing order range = self.viewRect() if range is not None and len(x) > 1: - dx = float(x[-1]-x[0]) / (len(x)-1) # clip to visible region extended by downsampling value - x0 = np.clip(int((range.left()-x[0])/dx)-1*ds , 0, len(x)-1) - x1 = np.clip(int((range.right()-x[0])/dx)+2*ds , 0, len(x)-1) - x = x[x0:x1] - y = y[x0:x1] + idx = np.searchsorted(x, [range.left(), range.right()]) + idx = idx + np.array([-2*ds, 2*ds]) + idx = np.clip(idx, a_min=0, a_max=len(x)) + + x = x[idx[0]:idx[1]] + y = y[idx[0]:idx[1]] if ds > 1: if self.opts['downsampleMethod'] == 'subsample': From b773b020a51e720ab07cedc654fb642e23b9011a Mon Sep 17 00:00:00 2001 From: MingZZZZZZZZ <32181145+ReehcQ@users.noreply.github.com> Date: Mon, 25 Mar 2019 18:02:04 -0400 Subject: [PATCH 331/607] Update LegendItem.py --- pyqtgraph/graphicsItems/LegendItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 91b6e9a3..418c428f 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -6,6 +6,7 @@ from ..Point import Point from .ScatterPlotItem import ScatterPlotItem, drawSymbol from .PlotDataItem import PlotDataItem from .GraphicsWidgetAnchor import GraphicsWidgetAnchor +from .BarGraphItem import BarGraphItem __all__ = ['LegendItem'] class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): From 2817b95c93c3d953c4b7c9cdeec928f21f7f9b8c Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 3 May 2019 18:45:15 -0700 Subject: [PATCH 332/607] Set path attr in case ErrorBarItem initialized without data --- pyqtgraph/graphicsItems/ErrorBarItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 986c5140..09fa97da 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -59,6 +59,7 @@ class ErrorBarItem(GraphicsObject): x, y = self.opts['x'], self.opts['y'] if x is None or y is None: + self.path = p return beam = self.opts['beam'] @@ -146,4 +147,4 @@ class ErrorBarItem(GraphicsObject): self.drawPath() return self.path.boundingRect() - \ No newline at end of file + From aa50296b9fe44da3e8131ed04c101ac4f32ad380 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sun, 12 May 2019 17:30:40 -0700 Subject: [PATCH 333/607] gc.collect() causes segfault on pyside2, test will pass on pyqt5 bindings (did not test pyqt4 or pyside1) --- pyqtgraph/tests/test_qt.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index bfb98631..c86cd500 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -10,7 +10,6 @@ def test_isQObjectAlive(): o2 = pg.QtCore.QObject() o2.setParent(o1) del o1 - gc.collect() assert not pg.Qt.isQObjectAlive(o2) @pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be ' From d873ee6b264bcdc9897ce3206599877637dc78cf Mon Sep 17 00:00:00 2001 From: Ogi Date: Sun, 12 May 2019 17:31:12 -0700 Subject: [PATCH 334/607] fixes ImportError on importing pysideuic --- pyqtgraph/Qt.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 88c27e27..696d65f5 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -100,10 +100,13 @@ def _loadUiType(uiFile): how to make PyQt4 and pyside look the same... http://stackoverflow.com/a/8717832 """ - import pysideuic + + if QT_LIB == "PYSIDE": + import pysideuic + else: + import pyside2uic as pysideuic import xml.etree.ElementTree as xml - #from io import StringIO - + parsed = xml.parse(uiFile) widget_class = parsed.find('widget').get('class') form_class = parsed.find('class').text From afb665ec992264f134607fb02cd81e6c311d67f2 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sun, 12 May 2019 17:35:26 -0700 Subject: [PATCH 335/607] make use of shiboken2 directly for isValid method --- pyqtgraph/Qt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 696d65f5..0941c3c7 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -219,8 +219,12 @@ elif QT_LIB == PYSIDE2: except ImportError as err: QtTest = FailedImport(err) - isQObjectAlive = _isQObjectAlive - + try: + import shiboken2 + isQObjectAlive = shiboken2.isValid + except ImportError: + # use approximate version + isQObjectAlive = _isQObjectAlive import PySide2 VERSION_INFO = 'PySide2 ' + PySide2.__version__ + ' Qt ' + QtCore.__version__ From af57d16bb51e24bd6623aeac3dd41404eedc149f Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 19 May 2019 00:28:18 +0200 Subject: [PATCH 336/607] Removed system_site_packages from Travis CI system_site_packages are opposed to the used conda installation. They are both unnecessary and lead to Travis build errors. --- .travis.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c7b7769..a4d85e47 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,10 +9,6 @@ sudo: false notifications: email: false -virtualenv: - system_site_packages: true - - env: # Enable python 2 and python 3 builds # Note that the 2.6 build doesn't get flake8, and runs old versions of From 42fd5614d0350474093ce616dfec79bbc070ce30 Mon Sep 17 00:00:00 2001 From: dschoni Date: Tue, 21 May 2019 13:38:34 +0200 Subject: [PATCH 337/607] Fix deprecation warning of multi-dimension tuples --- pyqtgraph/imageview/ImageView.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 2b43b940..81463b7a 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -633,7 +633,7 @@ class ImageView(QtGui.QWidget): ax = np.argmax(data.shape) sl = [slice(None)] * data.ndim sl[ax] = slice(None, None, 2) - data = data[sl] + data = data[tuple(sl)] cax = self.axes['c'] if cax is None: From 7f93e8205f8cbe291847019c06e43e52bc2385b6 Mon Sep 17 00:00:00 2001 From: dschoni Date: Thu, 2 May 2019 17:33:00 +0200 Subject: [PATCH 338/607] Found one more instance of the same warning in functions.py --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index fe3f9910..062986c7 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1380,7 +1380,7 @@ def gaussianFilter(data, sigma): # clip off extra data sl = [slice(None)] * data.ndim sl[ax] = slice(filtered.shape[ax]-data.shape[ax],None,None) - filtered = filtered[sl] + filtered = filtered[tuple(sl)] return filtered + baseline From bac8080b0c130b85cfe7d875cb73c993cd31fef8 Mon Sep 17 00:00:00 2001 From: dschoni Date: Tue, 21 May 2019 14:14:10 +0200 Subject: [PATCH 339/607] Typecast Levels to be float This circumvents cases in which "levels" is a boolean array and therefore the substraction fails due to deprecation. --- pyqtgraph/functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index fe3f9910..1fa10f5c 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1057,6 +1057,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): raise Exception('levels argument is required for float input types') if not isinstance(levels, np.ndarray): levels = np.array(levels) + levels = levels.astype(np.float) if levels.ndim == 1: if levels.shape[0] != 2: raise Exception('levels argument must have length 2') From 9cb351feee9524610baaf529a6558af6d754e0b6 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 22 May 2019 15:24:21 -0700 Subject: [PATCH 340/607] Implement azure ci (#865) * [skip-ci] Initial Azure-Pipelines configuration. The following configurations are tested * macOS 10.13 * ubuntu 16.04 * Windows Server 2016 Under each operating system, the following Qt bindings are tested * conda based pyqt4 * conda based pyside * conda based pyside2 (5.6) * conda based PyQt5 (5.9) * pip basedd PyQt5 (5.12) * pip based PySide2 (5.12) For each configuration, it runs `python -m pytest --cov pyqtgraph -sv` The only configuration that actually passes all tests is Ubuntu-pip-PyQt5 --- azure-pipelines.yml | 38 +++++++++ azure-test-template.yml | 183 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 azure-pipelines.yml create mode 100644 azure-test-template.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..b91f515a --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,38 @@ +############################################################################################ +# This config was rectrieved in no small part from https://github.com/slaclab/pydm +############################################################################################ + +trigger: + branches: + include: + - '*' # Build for all branches if they have a azure-pipelines.yml file. + tags: + include: + - 'v*' # Ensure that we are building for tags starting with 'v' (Official Versions) + +# Build only for PRs for master branch +pr: + autoCancel: true + branches: + include: + - master + - develop + +variables: + OFFICIAL_REPO: 'pyqtgraph/pyqtgraph' + +jobs: + - template: azure-test-template.yml + parameters: + name: Linux + vmImage: 'Ubuntu 16.04' + + - template: azure-test-template.yml + parameters: + name: Windows + vmImage: 'vs2017-win2016' + + - template: azure-test-template.yml + parameters: + name: MacOS + vmImage: 'macOS-10.13' diff --git a/azure-test-template.yml b/azure-test-template.yml new file mode 100644 index 00000000..2f1a7ae3 --- /dev/null +++ b/azure-test-template.yml @@ -0,0 +1,183 @@ +# Azure Pipelines CI job template for PyDM Tests +# https://docs.microsoft.com/en-us/azure/devops/pipelines/languages/anaconda?view=azure-devops +parameters: + name: '' + vmImage: '' + +jobs: +- job: ${{ parameters.name }} + pool: + vmImage: ${{ parameters.vmImage }} + strategy: + matrix: + Python27-Qt4: + python.version: '2.7' + install.method: "conda" + qt.bindings: "pyqt=4" + Python27-PySide: + python.version: '2.7' + qt.bindings: "pyside" + install.method: "conda" + Python37-PyQt-5.9: + python.version: "3.7" + qt.bindings: "pyqt" + install.method: "conda" + Python37-PySide2-5.6: + python.version: "3.7" + qt.bindings: "pyside2" + install.method: "conda" + Python35-PyQt-5.12: + python.version: '3.5' + qt.bindings: "PyQt5" + install.method: "pip" + Python35-PySide2-5.12: + python.version: "3.5" + qt.bindings: "PySide2" + install.method: "pip" + + steps: + - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" + displayName: 'Windows - Add conda to PATH' + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: | + echo "##vso[task.prependpath]$CONDA/bin" + sudo chown -R $USER $CONDA + displayName: 'MacOS - Add conda to PATH' + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Darwin' )) + + - bash: | + brew update && brew install azure-cli + brew update && brew install python3 && brew upgrade python3 + brew link --overwrite python3 + displayName: "MacOS - Intall Python3" + condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Darwin' )) + + - bash: | + echo "##vso[task.prependpath]/usr/share/miniconda/bin" + displayName: 'Linux - Add conda to PATH' + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Linux' )) + + - bash: | + # Install & Start Windows Manager for Linux + sudo apt-get install -y xvfb libxkbcommon-x11-0 # herbstluftwm + displayName: 'Linux - Prepare OS' + condition: eq(variables['agent.os'], 'Linux' ) + + - bash: | + source $HOME/miniconda/etc/profile.d/conda.sh + hash -r + conda config --set always_yes yes --set auto_update_conda no + conda config --add channels conda-forge + conda create -n test_env --quiet python=$(python.version) + displayName: 'Conda Setup Test Environment' + condition: eq(variables['install.method'], 'conda' ) + + - script: | + call activate test_env + conda install --quiet $(qt.bindings) + conda install --quiet numpy scipy pyopengl pytest flake8 six coverage + pip install pytest-azurepipelines pytest-xdist pytest-cov + displayName: Conda Install Dependencies - Windows + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: | + source activate test_env + conda install --quiet $(qt.bindings) + conda install --quiet numpy scipy pyopengl pytest flake8 six coverage + pip install pytest-azurepipelines pytest-xdist pytest-cov pytest-xvfb + displayName: Conda Install Dependencies - MacOS+Linux + condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) + + - bash: | + pip3 install setuptools wheel + pip3 install $(qt.bindings) + pip3 install numpy scipy pyopengl pytest flake8 six coverage + pip3 install pytest-azurepipelines pytest-xdist pytest-cov pytest-xvfb + displayName: "Pip - Install Dependencies" + condition: eq(variables['install.method'], 'pip' ) + + - bash: | + source activate test_env + echo python location: `which python3` + echo python version: `python3 --version` + echo pytest location: `which pytest` + echo installed packages + conda list + echo pyqtgraph system info + python -c "import pyqtgraph as pg; pg.systemInfo()" + displayName: 'Debug - Conda/MacOS+Linux' + continueOnError: false + condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) + + - script: | + call activate test_env + echo python location + where python + echo python version + python --version + echo pytest location + where pytest + echo installed packages + conda list + echo pyqtgraph system info + python -c "import pyqtgraph as pg; pg.systemInfo()" + displayName: 'Debug - Conda/Windows' + continueOnError: false + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: | + echo python location: `which python3` + echo python version: `python3 --version` + echo pytest location: `which pytest` + echo installed packages + pip3 list + echo pyqtgraph system info + python3 -c "import pyqtgraph as pg; pg.systemInfo()" + displayName: 'Debug - System/MacOS+Linux' + continueOnError: false + condition: and(eq(variables['install.method'], 'pip' ), ne(variables['agent.os'], 'Windows_NT' )) + + - bash: | + echo python location: `where python` + echo python version: `python --version` + echo pytest location: `where pytest` + echo installed packages + python -m pip list + echo pyqtgraph system info + python -c "import pyqtgraph as pg; pg.systemInfo()" + displayName: 'Debug - System/Windows' + continueOnError: false + condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: python3 -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings + displayName: 'Tests - Run - Pip/MacOS+Linux' + continueOnError: false + env: + DISPLAY: :99.0 + condition: and(eq(variables['install.method'], 'pip' ), ne(variables['agent.os'], 'Windows_NT' )) + + - bash: python -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings + displayName: 'Tests - Run - Pip/Windows' + continueOnError: false + env: + DISPLAY: :99.0 + condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Windows_NT' )) + + - bash: | + source activate test_env + pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings + displayName: 'Tests - Run - Conda/MacOS+Linux' + continueOnError: false + env: + DISPLAY: :99.0 + condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) + + - script: | + call activate test_env + python -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings + displayName: 'Tests - Run - Conda/Windows' + continueOnError: false + env: + DISPLAY: :99.0 + condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) From cf3c2948993afe05ba42a1d5de90d2dbd62a812c Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Thu, 23 May 2019 00:44:54 +0200 Subject: [PATCH 341/607] Fix Travis CI on 'develop' branch (#877) * Removed unused code There is no reason to keep old, unused code in a git repository * Removed system_site_packages from Travis CI system_site_packages are opposed to the used conda installation. They are both unnecessary and lead to Travis build errors. --- .travis.yml | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index acfde8ec..5a8dcf5f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,15 +9,9 @@ sudo: false notifications: email: false -virtualenv: - system_site_packages: true - - env: # Enable python 2 and python 3 builds - # Note that the 2.6 build doesn't get flake8, and runs old versions of - # Pyglet and GLFW to make sure we deal with those correctly - #- PYTHON=2.6 QT=pyqt4 TEST=standard # 2.6 support ended + # Note that the python 2.6 support ended. - PYTHON=2.7 QT=pyqt4 TEST=extra - PYTHON=2.7 QT=pyside TEST=standard - PYTHON=3.5 QT=pyqt5 TEST=standard @@ -68,11 +62,6 @@ install: - pip install pytest-xdist # multi-thread py.test - pip install pytest-cov # add coverage stats - # required for example testing on python 2.6 - - if [ "${PYTHON}" == "2.6" ]; then - pip install importlib; - fi; - # Debugging helpers - uname -a - cat /etc/issue From 309f89d413f05ed0fe457947503dc023f26106c8 Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 22 May 2019 22:07:30 -0700 Subject: [PATCH 342/607] Create tox configuration, update README accordingly. --- README.md | 34 +++++++++++++++++++++++----------- tox.ini | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 11 deletions(-) create mode 100644 tox.ini diff --git a/README.md b/README.md index a8742066..123949d5 100644 --- a/README.md +++ b/README.md @@ -19,28 +19,28 @@ heavy leverage of numpy for number crunching, Qt's GraphicsView framework for Requirements ------------ - * PyQt 4.7+, PySide, PyQt5, or PySide2 - * python 2.7, or 3.x - * NumPy - * For 3D graphics: pyopengl and qt-opengl - * Known to run on Windows, Linux, and Mac. +* PyQt 4.7+, PySide, PyQt5, or PySide2 +* python 2.7, or 3.x +* NumPy +* For 3D graphics: pyopengl and qt-opengl +* Known to run on Windows, Linux, and Mac. Support ------- - * Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues) - * Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph) +* Report issues on the [GitHub issue tracker](https://github.com/pyqtgraph/pyqtgraph/issues) +* Post questions to the [mailing list / forum](https://groups.google.com/forum/?fromgroups#!forum/pyqtgraph) or [StackOverflow](https://stackoverflow.com/questions/tagged/pyqtgraph) Installation Methods -------------------- -* From pypi: - - Last released version: `pip install pyqtgraph` - - Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph` +* From PyPI: + * Last released version: `pip install pyqtgraph` + * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph` * To install system-wide from source distribution: `python setup.py install` * Many linux package repositories have release versions. * To use with a specific project, simply copy the pyqtgraph subdirectory - anywhere that is importable from your project. + anywhere that is importable from your project. * For installation packages, see the website (pyqtgraph.org) Documentation @@ -50,3 +50,15 @@ The easiest way to learn pyqtgraph is to browse through the examples; run `pytho The official documentation lives at http://pyqtgraph.org/documentation +Testing +------- + +To test the pyqtgraph library, clone the repository, and run `pytest pyqtgraph`. For more thurough testing, you can use `tox`, however the [tox-conda](https://github.com/tox-dev/tox-conda) plugin is required. Running `tox` on its own will run `pytest pyqtgraph -vv` on it's own, however if you want to run a specific test, you can run `tox -- pyqtgraph/exporters/tests/test_svg::test_plotscene` for example. + +Dependencies include: + +* pytest +* pytest-cov +* pytest-xdist +* tox +* tox-conda \ No newline at end of file diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..5a86b387 --- /dev/null +++ b/tox.ini @@ -0,0 +1,46 @@ +[tox] +envlist = + ; qt 5.12.x + py{27,37}-pyside2-pip + ; qt 5.12.x + py{35,37}-pyqt5-pip + ; qt 5.9.7 + py{27,37}-pyqt5-conda + ; qt 5.6.2 + py35-pyqt5-conda + ; qt 5.6.2 + py{27,35,37}-pyside2-conda + ; pyqt 4.11.4 / qt 4.8.7 + py{27,36}-pyqt4-conda + ; pyside 1.2.4 / qt 4.8.7 + py{27,36}-pyside-conda + +[base] +deps = + pytest + numpy + scipy + pyopengl + flake8 + six + coverage + +[testenv] +deps= + {[base]deps} + pytest-cov + pytest-xdist + pyside2-pip: pyside2 + pyqt5-pip: pyqt5 + +conda_deps= + pyside2-conda: pyside2 + pyside-conda: pyside + pyqt5-conda: pyqt + pyqt4-conda: pyqt=4 + +conda_channels= + conda-forge +commands= + python -c "import pyqtgraph as pg; pg.systemInfo()" + python -m pytest {posargs:pyqtgraph -svv} From fd134f77c6fde72df39a14820482f182e899fdc0 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 23 May 2019 17:53:42 -0700 Subject: [PATCH 343/607] Only append .fc file extension if not added in the file dialog. --- pyqtgraph/flowchart/Flowchart.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 6a486232..f28ebc3b 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -525,7 +525,12 @@ class Flowchart(Node): self.restoreState(state, clear=True) self.viewBox.autoRange() self.sigFileLoaded.emit(fileName) - + + def saveFileSelected(self, fileName): + if not fileName.endswith('.fc'): + fileName += '.fc' + self.saveFile(fileName=fileName) + def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): """Save this flowchart to a .fc file """ @@ -537,11 +542,8 @@ class Flowchart(Node): self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.saveFile) + self.fileDialog.fileSelected.connect(self.saveFileSelected) return - fileName = unicode(fileName) - if not fileName.endswith('.fc'): - fileName += '.fc' fileName = asUnicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) self.sigFileSaved.emit(fileName) From ffd1624cb9b980abcced959e9c722e23e7ff04e8 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 23 May 2019 19:02:56 -0700 Subject: [PATCH 344/607] Use defaultSuffix for smarter file extension handling. --- pyqtgraph/flowchart/Flowchart.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index f28ebc3b..ae03d4c2 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -526,11 +526,6 @@ class Flowchart(Node): self.viewBox.autoRange() self.sigFileLoaded.emit(fileName) - def saveFileSelected(self, fileName): - if not fileName.endswith('.fc'): - fileName += '.fc' - self.saveFile(fileName=fileName) - def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): """Save this flowchart to a .fc file """ @@ -540,9 +535,10 @@ class Flowchart(Node): if startDir is None: startDir = '.' self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") + self.fileDialog.setDefaultSuffix("fc") self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.show() - self.fileDialog.fileSelected.connect(self.saveFileSelected) + self.fileDialog.fileSelected.connect(self.saveFile) return fileName = asUnicode(fileName) configfile.writeConfigFile(self.saveState(), fileName) From 8420fe984acb7b23e8feb4e5d6a0e4d3da5e4eb1 Mon Sep 17 00:00:00 2001 From: HappyTreeBeard <34220817+HappyTreeBeard@users.noreply.github.com> Date: Thu, 23 May 2019 21:33:23 -0700 Subject: [PATCH 345/607] Fixed bug in unit test where temp file remained open when os.unlink was called (#832) --- pyqtgraph/exporters/tests/test_csv.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/exporters/tests/test_csv.py b/pyqtgraph/exporters/tests/test_csv.py index 15c6626e..d6da033b 100644 --- a/pyqtgraph/exporters/tests/test_csv.py +++ b/pyqtgraph/exporters/tests/test_csv.py @@ -1,5 +1,5 @@ """ -SVG export test +CSV export test """ from __future__ import division, print_function, absolute_import import pyqtgraph as pg @@ -33,8 +33,9 @@ def test_CSVExporter(): ex = pg.exporters.CSVExporter(plt.plotItem) ex.export(fileName=tempfilename) - r = csv.reader(open(tempfilename, 'r')) - lines = [line for line in r] + with open(tempfilename, 'r') as csv_file: + r = csv.reader(csv_file) + lines = [line for line in r] header = lines.pop(0) assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002'] From e2b01ccf749ef8a46e2f5a3185ca9007587501b9 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 24 May 2019 06:35:01 +0200 Subject: [PATCH 346/607] FIX: Correct deletion of matplotlib exporter window object (#868) E.g. when opening the Matplotlib exporter multiple times, and one closes one instance, Python crashes. This is caused by the Matplotlib QMainWindow listening to the closeEvent and deleting the only reference of the window before it is closed properly. --- pyqtgraph/exporters/Matplotlib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/Matplotlib.py b/pyqtgraph/exporters/Matplotlib.py index 2da979b1..dedc2b87 100644 --- a/pyqtgraph/exporters/Matplotlib.py +++ b/pyqtgraph/exporters/Matplotlib.py @@ -124,5 +124,4 @@ class MatplotlibWindow(QtGui.QMainWindow): def closeEvent(self, ev): MatplotlibExporter.windows.remove(self) - - + self.deleteLater() From 95f4b00e1463cbfd72d8d45568977e192499d5fc Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 23 May 2019 23:56:53 -0700 Subject: [PATCH 347/607] TreeWidget.topLevelItems Python 3 fix --- pyqtgraph/widgets/TreeWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/widgets/TreeWidget.py b/pyqtgraph/widgets/TreeWidget.py index b0ec54c1..8c55ae2f 100644 --- a/pyqtgraph/widgets/TreeWidget.py +++ b/pyqtgraph/widgets/TreeWidget.py @@ -201,7 +201,7 @@ class TreeWidget(QtGui.QTreeWidget): return item def topLevelItems(self): - return map(self.topLevelItem, xrange(self.topLevelItemCount())) + return [self.topLevelItem(i) for i in range(self.topLevelItemCount())] def clear(self): items = self.topLevelItems() From 849c7cab55f7956068a64f1333a7fe1745343557 Mon Sep 17 00:00:00 2001 From: Ogi Date: Fri, 24 May 2019 23:28:48 -0700 Subject: [PATCH 348/607] PySide2 is also a Qt5 binding --- pyqtgraph/tests/image_testing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index a7552631..2f9e98f9 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -253,7 +253,7 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., assert im1.dtype == im2.dtype if pxCount == -1: - if QT_LIB == 'PyQt5': + if QT_LIB in {'PyQt5', 'PySide2'}: # Qt5 generates slightly different results; relax the tolerance # until test images are updated. pxCount = int(im1.shape[0] * im1.shape[1] * 0.01) From c69e04db2df7485b823338e482bdbc1cf6323e99 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 25 May 2019 00:21:37 -0700 Subject: [PATCH 349/607] Simpler way of extracting types from QByteArray Simpler way of extracting bytes from QByteArray --- pyqtgraph/exporters/SVGExporter.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index dcd95c2b..b0e9b1c0 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -190,12 +190,7 @@ def _generateItemSvg(item, nodes=None, root=None, options={}): ## this is taken care of in generateSvg instead. #if hasattr(item, 'setExportMode'): #item.setExportMode(False) - - if QT_LIB in ['PySide', 'PySide2']: - xmlStr = str(arr) - else: - xmlStr = bytes(arr).decode('utf-8') - doc = xml.parseString(xmlStr.encode('utf-8')) + doc = xml.parseString(arr.data()) try: ## Get top-level group for this item From deab37d533975f4eccc318dc0b5ede2dfafd2181 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 25 May 2019 16:25:49 -0700 Subject: [PATCH 350/607] Try to import from collections.abc for Python 3.3+ (#887) * Try to import from collections.abc for Python 3.3+ --- pyqtgraph/graphicsItems/ImageItem.py | 10 +++++++--- pyqtgraph/pgcollections.py | 17 ++++++++++++----- 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 2ebce2c7..65e87eec 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -2,13 +2,17 @@ from __future__ import division from ..Qt import QtGui, QtCore import numpy as np -import collections from .. import functions as fn from .. import debug as debug from .GraphicsObject import GraphicsObject from ..Point import Point from .. import getConfigOption +try: + from collections.abc import Callable +except ImportError: + # fallback for python < 3.3 + from collections import Callable __all__ = ['ImageItem'] @@ -357,7 +361,7 @@ class ImageItem(GraphicsObject): # Request a lookup table if this image has only one channel if self.image.ndim == 2 or self.image.shape[2] == 1: - if isinstance(self.lut, collections.Callable): + if isinstance(self.lut, Callable): lut = self.lut(self.image) else: lut = self.lut @@ -624,7 +628,7 @@ class ImageItem(GraphicsObject): mask = self.drawMask src = dk - if isinstance(self.drawMode, collections.Callable): + if isinstance(self.drawMode, Callable): self.drawMode(dk, self.image, mask, ss, ts, ev) else: src = src[ss] diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index ac7f68fe..ef3db258 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -10,15 +10,22 @@ Includes: - ThreadsafeDict, ThreadsafeList - Self-mutexed data structures """ -import threading, sys, copy, collections -#from debug import * +import threading +import sys +import copy try: from collections import OrderedDict except ImportError: # fallback: try to use the ordereddict backport when using python 2.6 from ordereddict import OrderedDict - + +try: + from collections.abc import Sequence +except ImportError: + # fallback for python < 3.3 + from collections import Sequence + class ReverseDict(dict): """extends dict so that reverse lookups are possible by requesting the key as a list of length 1: @@ -326,7 +333,7 @@ class ProtectedDict(dict): -class ProtectedList(collections.Sequence): +class ProtectedList(Sequence): """ A class allowing read-only 'view' of a list or dict. The object can be treated like a normal list, but will never modify the original list it points to. @@ -408,7 +415,7 @@ class ProtectedList(collections.Sequence): raise Exception("This is a list. It does not poop.") -class ProtectedTuple(collections.Sequence): +class ProtectedTuple(Sequence): """ A class allowing read-only 'view' of a tuple. The object can be treated like a normal tuple, but its contents will be returned as protected objects. From 6a4e0a106f83bed0856cd7aa7732b43be099f5c9 Mon Sep 17 00:00:00 2001 From: ksunden Date: Mon, 27 May 2019 16:49:08 -0500 Subject: [PATCH 351/607] Add condition for namespace packages --- pyqtgraph/reload.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index 766ec9d0..f6c630b9 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -47,7 +47,7 @@ def reloadAll(prefix=None, debug=False): continue ## Ignore if the file name does not start with prefix - if not hasattr(mod, '__file__') or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']: + if not hasattr(mod, '__file__') or mod.__file__ is None or os.path.splitext(mod.__file__)[1] not in ['.py', '.pyc']: continue if prefix is not None and mod.__file__[:len(prefix)] != prefix: continue From a37e8776312946e19de34bf34aed049ffae5eea3 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Tue, 28 May 2019 06:07:25 +0200 Subject: [PATCH 352/607] Add PyQt5 and PySide2 to test_example.py (#897) * Add PySide2 to test_example.py Before, example tests are skipped because no PyQt version was found --- examples/test_examples.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index ae88b087..d8de370f 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -42,7 +42,7 @@ except ImportError: pass files = utils.buildFileList(utils.examples) -frontends = {Qt.PYQT4: False, Qt.PYSIDE: False} +frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} # sort out which of the front ends are available for frontend in frontends.keys(): try: From e8854d69bba513a98932efd6e3e74d0d6d1e5ad7 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:41:44 -0700 Subject: [PATCH 353/607] Capture Screenshots --- pyqtgraph/tests/image_testing.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 2f9e98f9..c1a14c4d 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -191,6 +191,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.") except Exception: + if stdFileName in gitStatus(dataPath): print("\n\nWARNING: unit test failed against modified standard " "image %s.\nTo revert this file, run `cd %s; git checkout " @@ -210,6 +211,9 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): "PYQTGRAPH_AUDIT=1 to add this image." % stdFileName) else: if os.getenv('TRAVIS') is not None: + saveFailedTest(image, stdImage, standardFile, upload=True) + elif os.getenv('AZURE') is not None: + standardFile = r"artifacts/" + standardFile saveFailedTest(image, stdImage, standardFile) print(graphstate) raise @@ -281,14 +285,13 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., assert corr >= minCorr -def saveFailedTest(data, expect, filename): +def saveFailedTest(data, expect, filename, upload=False): """Upload failed test images to web server to allow CI test debugging. """ commit = runSubprocess(['git', 'rev-parse', 'HEAD']) name = filename.split('/') name.insert(-1, commit.strip()) filename = '/'.join(name) - host = 'data.pyqtgraph.org' # concatenate data, expect, and diff into a single image ds = data.shape @@ -306,15 +309,25 @@ def saveFailedTest(data, expect, filename): img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = makePng(img) + directory, _, _ = filename.rpartition("/") + if not os.path.isdir(directory): + os.makedirs(directory) + with open(filename + ".png", "wb") as png_file: + png_file.write(png) + print("\nImage comparison failed. Test result: %s %s Expected result: " + "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + if upload: + uploadFailedTest(filename) +def uploadFailedTest(filename): + host = 'data.pyqtgraph.org' conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, 'data': base64.b64encode(png)}) conn.request('POST', '/upload.py', req) response = conn.getresponse().read() conn.close() - print("\nImage comparison failed. Test result: %s %s Expected result: " - "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + print("Uploaded to: \nhttp://%s/data/%s" % (host, filename)) if not response.startswith(b'OK'): print("WARNING: Error uploading data to %s" % host) @@ -495,7 +508,7 @@ def getTestDataRepo(): if not os.path.isdir(parentPath): os.makedirs(parentPath) - if os.getenv('TRAVIS') is not None: + if os.getenv('TRAVIS') is not None or os.getenv('AZURE') is not None: # Create a shallow clone of the test-data repository (to avoid # downloading more data than is necessary) os.makedirs(dataPath) From f2aeea8964992ddedf0f5b1d92882d9c8629dc0a Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:42:01 -0700 Subject: [PATCH 354/607] We support pyside2 don't we? --- examples/__main__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/examples/__main__.py b/examples/__main__.py index 9c49bb3b..0251974a 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -135,6 +135,8 @@ if __name__ == '__main__': lib = 'PyQt4' elif '--pyqt5' in args: lib = 'PyQt5' + elif '--pyside2' in args: + lib = 'PySide2' else: lib = '' From aa63c07523dbb1f1ad9e38ef80e06a5e8eb3893d Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:42:25 -0700 Subject: [PATCH 355/607] Show available desktop resolution --- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 68f4f497..a42fb5f9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -31,7 +31,8 @@ def init_viewbox(): g = pg.GridItem() vb.addItem(g) - + desktop = app.desktop().screenGeometry() + print("\n\nDesktop Resolution: {} x {}\n\n".format(desktop.width(), desktop.height())) app.processEvents() def test_ViewBox(): From 560993e8c5b4bc7091b0b18c69bfe504279cf030 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Wed, 29 May 2019 10:58:33 +0200 Subject: [PATCH 356/607] Exclude selected examples from tests (such as HDF5) --- examples/test_examples.py | 2 +- examples/utils.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index d8de370f..c5997348 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -41,7 +41,7 @@ except ImportError: "pypi\n\npip install importlib\n\n") pass -files = utils.buildFileList(utils.examples) +files = utils.buildFileList(utils.tested_examples) frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} # sort out which of the front ends are available for frontend in frontends.keys(): diff --git a/examples/utils.py b/examples/utils.py index f7786dba..82270f4c 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -4,6 +4,7 @@ import time import os import sys import errno +import copy from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.python2_3 import basestring @@ -91,6 +92,11 @@ examples = OrderedDict([ ('Custom Flowchart Nodes', 'FlowchartCustomNode.py'), ]) +not_tested = ['HDF5 big data'] + +tested_examples = copy.deepcopy(examples) +all(map(tested_examples.pop, not_tested)) + def buildFileList(examples, files=None): if files == None: From c4e295ceae9e783b23c54fe24b13b00ef45c8125 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 29 May 2019 10:53:04 -0700 Subject: [PATCH 357/607] Use correct path seperators, pass png to upload function --- pyqtgraph/tests/image_testing.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index c1a14c4d..564e6d46 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -289,9 +289,9 @@ def saveFailedTest(data, expect, filename, upload=False): """Upload failed test images to web server to allow CI test debugging. """ commit = runSubprocess(['git', 'rev-parse', 'HEAD']) - name = filename.split('/') + name = filename.split(os.path.sep) name.insert(-1, commit.strip()) - filename = '/'.join(name) + filename = os.path.sep.join(name) # concatenate data, expect, and diff into a single image ds = data.shape @@ -309,7 +309,7 @@ def saveFailedTest(data, expect, filename, upload=False): img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = makePng(img) - directory, _, _ = filename.rpartition("/") + directory = os.path.dirname(filename) if not os.path.isdir(directory): os.makedirs(directory) with open(filename + ".png", "wb") as png_file: @@ -317,9 +317,9 @@ def saveFailedTest(data, expect, filename, upload=False): print("\nImage comparison failed. Test result: %s %s Expected result: " "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) if upload: - uploadFailedTest(filename) + uploadFailedTest(filename, png) -def uploadFailedTest(filename): +def uploadFailedTest(filename, png): host = 'data.pyqtgraph.org' conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, From 4b26519feffe03954e6ca45ace108b05c514aafa Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 29 May 2019 13:07:08 -0700 Subject: [PATCH 358/607] Move Desktop Resolution info print statement to test.py --- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 2 -- test.py | 7 ++++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index a42fb5f9..bb705c18 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -31,8 +31,6 @@ def init_viewbox(): g = pg.GridItem() vb.addItem(g) - desktop = app.desktop().screenGeometry() - print("\n\nDesktop Resolution: {} x {}\n\n".format(desktop.width(), desktop.height())) app.processEvents() def test_ViewBox(): diff --git a/test.py b/test.py index b07fb1cf..63656d68 100644 --- a/test.py +++ b/test.py @@ -15,10 +15,15 @@ elif '--pyqt4' in args: elif '--pyqt5' in args: args.remove('--pyqt5') import PyQt5 +elif '--pyside2' in args: + args.remove('--pyside2') + import PySide2 import pyqtgraph as pg pg.systemInfo() - +qApp = pg.mkQApp() +desktop = qApp.desktop().screenGeometry() +print("\n\nDesktop Resolution: {} x {}\n\n".format(desktop.width(), desktop.height())) pytest.main(args) \ No newline at end of file From b3c0bf635d2babffa53596e95a269d6ca91f7f55 Mon Sep 17 00:00:00 2001 From: Matt Liberty Date: Wed, 29 May 2019 20:02:00 -0400 Subject: [PATCH 359/607] Fixed ViewBox.updateViewRange so that transformation is updated for sigXRangeChanged and sigYRangeChanged in PySide2. --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 5002fa35..e85796c9 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1496,14 +1496,13 @@ class ViewBox(GraphicsWidget): 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 - # emit range change signals - if changed[0]: - self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) - if changed[1]: - self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) - if any(changed): self._matrixNeedsUpdate = True + # emit range change signals + if changed[0]: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + if changed[1]: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() From f2426e9dd2155ffe164911bf775e8629df26394d Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:41:44 -0700 Subject: [PATCH 360/607] Capture Screenshots --- pyqtgraph/tests/image_testing.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index 2f9e98f9..c1a14c4d 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -191,6 +191,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): if os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': raise Exception("Image test passed, but auditing due to PYQTGRAPH_AUDIT_ALL evnironment variable.") except Exception: + if stdFileName in gitStatus(dataPath): print("\n\nWARNING: unit test failed against modified standard " "image %s.\nTo revert this file, run `cd %s; git checkout " @@ -210,6 +211,9 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): "PYQTGRAPH_AUDIT=1 to add this image." % stdFileName) else: if os.getenv('TRAVIS') is not None: + saveFailedTest(image, stdImage, standardFile, upload=True) + elif os.getenv('AZURE') is not None: + standardFile = r"artifacts/" + standardFile saveFailedTest(image, stdImage, standardFile) print(graphstate) raise @@ -281,14 +285,13 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., assert corr >= minCorr -def saveFailedTest(data, expect, filename): +def saveFailedTest(data, expect, filename, upload=False): """Upload failed test images to web server to allow CI test debugging. """ commit = runSubprocess(['git', 'rev-parse', 'HEAD']) name = filename.split('/') name.insert(-1, commit.strip()) filename = '/'.join(name) - host = 'data.pyqtgraph.org' # concatenate data, expect, and diff into a single image ds = data.shape @@ -306,15 +309,25 @@ def saveFailedTest(data, expect, filename): img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = makePng(img) + directory, _, _ = filename.rpartition("/") + if not os.path.isdir(directory): + os.makedirs(directory) + with open(filename + ".png", "wb") as png_file: + png_file.write(png) + print("\nImage comparison failed. Test result: %s %s Expected result: " + "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + if upload: + uploadFailedTest(filename) +def uploadFailedTest(filename): + host = 'data.pyqtgraph.org' conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, 'data': base64.b64encode(png)}) conn.request('POST', '/upload.py', req) response = conn.getresponse().read() conn.close() - print("\nImage comparison failed. Test result: %s %s Expected result: " - "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) + print("Uploaded to: \nhttp://%s/data/%s" % (host, filename)) if not response.startswith(b'OK'): print("WARNING: Error uploading data to %s" % host) @@ -495,7 +508,7 @@ def getTestDataRepo(): if not os.path.isdir(parentPath): os.makedirs(parentPath) - if os.getenv('TRAVIS') is not None: + if os.getenv('TRAVIS') is not None or os.getenv('AZURE') is not None: # Create a shallow clone of the test-data repository (to avoid # downloading more data than is necessary) os.makedirs(dataPath) From 2df71abfec301b8ffd3026a064e7c95cbac07302 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 28 May 2019 22:42:01 -0700 Subject: [PATCH 361/607] We support pyside2 don't we? --- azure-test-template.yml | 259 +++++++++--------- examples/__main__.py | 2 + .../ViewBox/tests/test_ViewBox.py | 1 - pyqtgraph/tests/image_testing.py | 21 +- pyqtgraph/tests/test_display.py | 10 + pyqtgraph/util/get_resolution.py | 7 + test.py | 6 +- 7 files changed, 156 insertions(+), 150 deletions(-) create mode 100644 pyqtgraph/tests/test_display.py create mode 100644 pyqtgraph/util/get_resolution.py diff --git a/azure-test-template.yml b/azure-test-template.yml index 2f1a7ae3..f3eaac40 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -12,8 +12,8 @@ jobs: matrix: Python27-Qt4: python.version: '2.7' - install.method: "conda" qt.bindings: "pyqt=4" + install.method: "conda" Python27-PySide: python.version: '2.7' qt.bindings: "pyside" @@ -26,158 +26,145 @@ jobs: python.version: "3.7" qt.bindings: "pyside2" install.method: "conda" - Python35-PyQt-5.12: - python.version: '3.5' + Python37-PyQt-5.12: + python.version: '3.7' qt.bindings: "PyQt5" install.method: "pip" - Python35-PySide2-5.12: - python.version: "3.5" + Python37-PySide2-5.12: + python.version: "3.7" qt.bindings: "PySide2" install.method: "pip" steps: - - powershell: Write-Host "##vso[task.prependpath]$env:CONDA\Scripts" - displayName: 'Windows - Add conda to PATH' - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + - task: ScreenResolutionUtility@1 + inputs: + displaySettings: 'specific' + width: '1920' + height: '1080' + condition: eq(variables['agent.os'], 'Windows_NT' ) + + - task: UsePythonVersion@0 + inputs: + versionSpec: $(python.version) + condition: eq(variables['install.method'], 'pip') - bash: | - echo "##vso[task.prependpath]$CONDA/bin" - sudo chown -R $USER $CONDA - displayName: 'MacOS - Add conda to PATH' - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Darwin' )) - - - bash: | - brew update && brew install azure-cli - brew update && brew install python3 && brew upgrade python3 - brew link --overwrite python3 - displayName: "MacOS - Intall Python3" - condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Darwin' )) - - - bash: | - echo "##vso[task.prependpath]/usr/share/miniconda/bin" - displayName: 'Linux - Add conda to PATH' - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Linux' )) - - - bash: | - # Install & Start Windows Manager for Linux - sudo apt-get install -y xvfb libxkbcommon-x11-0 # herbstluftwm - displayName: 'Linux - Prepare OS' - condition: eq(variables['agent.os'], 'Linux' ) - - - bash: | - source $HOME/miniconda/etc/profile.d/conda.sh - hash -r - conda config --set always_yes yes --set auto_update_conda no - conda config --add channels conda-forge - conda create -n test_env --quiet python=$(python.version) - displayName: 'Conda Setup Test Environment' + if [ $(agent.os) == 'Linux' ] + then + echo '##vso[task.prependpath]/usr/share/miniconda/bin' + elif [ $(agent.os) == 'Darwin' ] + then + echo '##vso[task.prependpath]$CONDA/bin' + sudo install -d -m 0777 /usr/local/miniconda/envs + elif [ $(agent.os) == 'Windows_NT' ] + then + echo "##vso[task.prependpath]$env:CONDA\Scripts" + else + echo 'Just what OS are you using?' + fi + displayName: 'Add Conda to $PATH' condition: eq(variables['install.method'], 'conda' ) + + - task: CondaEnvironment@0 + displayName: 'Create Conda Environment' + condition: eq(variables['install.method'], 'conda') + inputs: + environmentName: 'test-environment-$(python.version)' + packageSpecs: 'python=$(python.version)' + + - bash: | + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes + else + pip install $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage + fi + pip install pytest-xdist pytest-cov pytest-faulthandler + displayName: "Install Dependencies" - - script: | - call activate test_env - conda install --quiet $(qt.bindings) - conda install --quiet numpy scipy pyopengl pytest flake8 six coverage - pip install pytest-azurepipelines pytest-xdist pytest-cov - displayName: Conda Install Dependencies - Windows - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + - bash: | + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + fi + + pip install setuptools wheel + python setup.py bdist_wheel + pip install dist/*.whl + displayName: 'Build Wheel and Install' + + - task: CopyFiles@2 + inputs: + contents: 'dist/**' + targetFolder: $(Build.ArtifactStagingDirectory) + cleanTargetFolder: true # Optional + displayName: "Copy Distributions To Artifacts" + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Distributions' + condition: always() + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory)/dist + artifactName: Distributions - bash: | - source activate test_env - conda install --quiet $(qt.bindings) - conda install --quiet numpy scipy pyopengl pytest flake8 six coverage - pip install pytest-azurepipelines pytest-xdist pytest-cov pytest-xvfb - displayName: Conda Install Dependencies - MacOS+Linux - condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) - + sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + fi + pip install pytest-xvfb + displayName: "Linux Virtual Display Setup" + condition: eq(variables['agent.os'], 'Linux' ) + - bash: | - pip3 install setuptools wheel - pip3 install $(qt.bindings) - pip3 install numpy scipy pyopengl pytest flake8 six coverage - pip3 install pytest-azurepipelines pytest-xdist pytest-cov pytest-xvfb - displayName: "Pip - Install Dependencies" - condition: eq(variables['install.method'], 'pip' ) - - - bash: | - source activate test_env - echo python location: `which python3` - echo python version: `python3 --version` - echo pytest location: `which pytest` - echo installed packages - conda list - echo pyqtgraph system info - python -c "import pyqtgraph as pg; pg.systemInfo()" - displayName: 'Debug - Conda/MacOS+Linux' - continueOnError: false - condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) - - - script: | - call activate test_env - echo python location - where python - echo python version - python --version - echo pytest location - where pytest - echo installed packages - conda list - echo pyqtgraph system info - python -c "import pyqtgraph as pg; pg.systemInfo()" - displayName: 'Debug - Conda/Windows' - continueOnError: false - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) - - - bash: | - echo python location: `which python3` - echo python version: `python3 --version` - echo pytest location: `which pytest` - echo installed packages - pip3 list - echo pyqtgraph system info - python3 -c "import pyqtgraph as pg; pg.systemInfo()" - displayName: 'Debug - System/MacOS+Linux' - continueOnError: false - condition: and(eq(variables['install.method'], 'pip' ), ne(variables['agent.os'], 'Windows_NT' )) - - - bash: | - echo python location: `where python` + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + fi + echo python location: `which python` echo python version: `python --version` - echo pytest location: `where pytest` + echo pytest location: `which pytest` echo installed packages - python -m pip list + pip list echo pyqtgraph system info python -c "import pyqtgraph as pg; pg.systemInfo()" - displayName: 'Debug - System/Windows' + displayName: 'Debug Info' continueOnError: false - condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Windows_NT' )) - - bash: python3 -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings - displayName: 'Tests - Run - Pip/MacOS+Linux' - continueOnError: false + - bash: | + if [ $(install.method) == "conda" ] + then + source activate test-environment-$(python.version) + fi + mkdir -p "$SCREENSHOT_DIR" + # echo "If Screenshots are generated, they may be downloaded from:" + # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" + python -m pytest -sv \ + --junitxml=junit/test-results.xml \ + --cov pyqtgraph --cov-report=xml --cov-report=html + displayName: 'Unit tests' env: - DISPLAY: :99.0 - condition: and(eq(variables['install.method'], 'pip' ), ne(variables['agent.os'], 'Windows_NT' )) - - - bash: python -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings - displayName: 'Tests - Run - Pip/Windows' - continueOnError: false - env: - DISPLAY: :99.0 - condition: and(eq(variables['install.method'], 'pip' ), eq(variables['agent.os'], 'Windows_NT' )) - - - bash: | - source activate test_env - pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings - displayName: 'Tests - Run - Conda/MacOS+Linux' - continueOnError: false - env: - DISPLAY: :99.0 - condition: and(eq(variables['install.method'], 'conda' ), ne(variables['agent.os'], 'Windows_NT' )) - - - script: | - call activate test_env - python -m pytest --cov pyqtgraph -sv --test-run-title="Tests for $(Agent.OS) - Python $(python.version) - Install Method $(install.method)- Bindings $(qt.bindings)" --napoleon-docstrings - displayName: 'Tests - Run - Conda/Windows' - continueOnError: false - env: - DISPLAY: :99.0 - condition: and(eq(variables['install.method'], 'conda' ), eq(variables['agent.os'], 'Windows_NT' )) + AZURE: 1 + SCREENSHOT_DIR: $(Build.ArtifactStagingDirectory)/screenshots + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Screenshots' + condition: failed() + inputs: + pathtoPublish: $(Build.ArtifactStagingDirectory)/screenshots + artifactName: Screenshots + + - task: PublishTestResults@2 + condition: succeededOrFailed() + inputs: + testResultsFiles: '**/test-*.xml' + testRunTitle: 'Test Results for $(agent.os) - $(python.version) - $(qt.bindings) - $(install.method)' + publishRunAttachments: true + + - task: PublishCodeCoverageResults@1 + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' + reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' \ No newline at end of file diff --git a/examples/__main__.py b/examples/__main__.py index 9c49bb3b..0251974a 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -135,6 +135,8 @@ if __name__ == '__main__': lib = 'PyQt4' elif '--pyqt5' in args: lib = 'PyQt5' + elif '--pyside2' in args: + lib = 'PySide2' else: lib = '' diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 68f4f497..bb705c18 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -31,7 +31,6 @@ def init_viewbox(): g = pg.GridItem() vb.addItem(g) - app.processEvents() def test_ViewBox(): diff --git a/pyqtgraph/tests/image_testing.py b/pyqtgraph/tests/image_testing.py index c1a14c4d..cfb62bb9 100644 --- a/pyqtgraph/tests/image_testing.py +++ b/pyqtgraph/tests/image_testing.py @@ -213,7 +213,7 @@ def assertImageApproved(image, standardFile, message=None, **kwargs): if os.getenv('TRAVIS') is not None: saveFailedTest(image, stdImage, standardFile, upload=True) elif os.getenv('AZURE') is not None: - standardFile = r"artifacts/" + standardFile + standardFile = os.path.join(os.getenv("SCREENSHOT_DIR", "screenshots"), standardFile) saveFailedTest(image, stdImage, standardFile) print(graphstate) raise @@ -288,11 +288,6 @@ def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., def saveFailedTest(data, expect, filename, upload=False): """Upload failed test images to web server to allow CI test debugging. """ - commit = runSubprocess(['git', 'rev-parse', 'HEAD']) - name = filename.split('/') - name.insert(-1, commit.strip()) - filename = '/'.join(name) - # concatenate data, expect, and diff into a single image ds = data.shape es = expect.shape @@ -309,7 +304,7 @@ def saveFailedTest(data, expect, filename, upload=False): img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff png = makePng(img) - directory, _, _ = filename.rpartition("/") + directory = os.path.dirname(filename) if not os.path.isdir(directory): os.makedirs(directory) with open(filename + ".png", "wb") as png_file: @@ -317,9 +312,15 @@ def saveFailedTest(data, expect, filename, upload=False): print("\nImage comparison failed. Test result: %s %s Expected result: " "%s %s" % (data.shape, data.dtype, expect.shape, expect.dtype)) if upload: - uploadFailedTest(filename) - -def uploadFailedTest(filename): + uploadFailedTest(filename, png) + + +def uploadFailedTest(filename, png): + commit = runSubprocess(['git', 'rev-parse', 'HEAD']) + name = filename.split(os.path.sep) + name.insert(-1, commit.strip()) + filename = os.path.sep.join(name) + host = 'data.pyqtgraph.org' conn = httplib.HTTPConnection(host) req = urllib.urlencode({'name': filename, diff --git a/pyqtgraph/tests/test_display.py b/pyqtgraph/tests/test_display.py new file mode 100644 index 00000000..951a10f9 --- /dev/null +++ b/pyqtgraph/tests/test_display.py @@ -0,0 +1,10 @@ +from .. import mkQApp + +qApp = mkQApp() + + +def test_displayResolution(): + desktop = qApp.desktop().screenGeometry() + width, height = desktop.width(), desktop.height() + print("\n\nDisplay Resolution Logged as {}x{}\n\n".format(width, height)) + assert height > 0 and width > 0 diff --git a/pyqtgraph/util/get_resolution.py b/pyqtgraph/util/get_resolution.py new file mode 100644 index 00000000..3558a81c --- /dev/null +++ b/pyqtgraph/util/get_resolution.py @@ -0,0 +1,7 @@ +from .. import mkQApp + + +def getResolution(): + qApp = mkQApp() + desktop = qApp.desktop().screenGeometry() + return (desktop.width(), desktop.height()) diff --git a/test.py b/test.py index b07fb1cf..d2aeff5c 100644 --- a/test.py +++ b/test.py @@ -15,10 +15,10 @@ elif '--pyqt4' in args: elif '--pyqt5' in args: args.remove('--pyqt5') import PyQt5 +elif '--pyside2' in args: + args.remove('--pyside2') + import PySide2 import pyqtgraph as pg pg.systemInfo() - pytest.main(args) - - \ No newline at end of file From 1616e99b3a5fc2686eadaf9f1b9ad16527555dd2 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 29 May 2019 16:20:32 -0700 Subject: [PATCH 362/607] Fix docstring warning --- pyqtgraph/graphicsItems/ROI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index a710f808..fa2bcf5f 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1553,7 +1553,7 @@ class RectROI(ROI): self.addScaleHandle([0.5, 1], [0.5, center[1]]) class LineROI(ROI): - """ + r""" Rectangular ROI subclass with scale-rotate handles on either side. This allows the ROI to be positioned as if moving the ends of a line segment. A third handle controls the width of the ROI orthogonal to its "line" axis. From 153d78711bbbaa662f460687f19338bb232954ea Mon Sep 17 00:00:00 2001 From: Ogi Date: Thu, 30 May 2019 14:13:58 -0700 Subject: [PATCH 363/607] Implement Fault Handler when test takes 60 seconds --- azure-test-template.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index f3eaac40..09ba4757 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -143,7 +143,8 @@ jobs: # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" python -m pytest -sv \ --junitxml=junit/test-results.xml \ - --cov pyqtgraph --cov-report=xml --cov-report=html + --cov pyqtgraph --cov-report=xml --cov-report=html \ + --faulthandler-timeout=60 displayName: 'Unit tests' env: AZURE: 1 From 191ce16e8d9e9b8b0d9d5076b9a09557e4315ba0 Mon Sep 17 00:00:00 2001 From: Ogi Date: Fri, 31 May 2019 17:11:22 -0700 Subject: [PATCH 364/607] Add pytest config file specifying colordepth when using pytest-xvfb --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 pytest.ini diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..1f133c35 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] + +xvfb_colordepth = 24 From c52382c3b982205588cd903f72c2fddd45658701 Mon Sep 17 00:00:00 2001 From: Matt Liberty Date: Sat, 1 Jun 2019 16:28:23 -0400 Subject: [PATCH 365/607] Moved emits after all method state updates since PySide2 immediately executes signals. Pull request #907 addressed a specific case where a signal was emitted before a state update. If an application's slot then calls back into the instance, the instance was in an inconsistent state. This commit audits and fixes similar issues throughout the pyqtgraph library. This commit fixes several latent issues: * SignalProxy: flush -> sigDelayed -> signalReceived would have incorrectly resulted in timer.stop(). * ViewBox: resizeEvent -> sigStateChange -> background state * ViewBox: setRange -> sigStateChange -> autoranging not updated correctly * ViewBox: updateMatrix -> sigTransformChanged -> any _matrixNeedsUpdate = True -> ignored * Parameter: Child may have missed state tree messages on insert or received extra on remove * GraphicsView: updateMatrix -> sigDeviceRangeChanged/sigDeviceTransformChange -> before propagated to locked viewports. --- pyqtgraph/SignalProxy.py | 6 ++-- pyqtgraph/dockarea/Dock.py | 2 +- pyqtgraph/flowchart/Flowchart.py | 2 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 2 +- pyqtgraph/graphicsItems/ROI.py | 2 +- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 37 +++++++++++---------- pyqtgraph/parametertree/Parameter.py | 4 +-- pyqtgraph/widgets/ColorButton.py | 2 +- pyqtgraph/widgets/GraphicsView.py | 7 ++-- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index d36282fa..7463dfc3 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -67,11 +67,11 @@ class SignalProxy(QtCore.QObject): """If there is a signal queued up, send it now.""" if self.args is None or self.block: return False - #self.emit(self.signal, *self.args) - self.sigDelayed.emit(self.args) - self.args = None + args, self.args = self.args, None self.timer.stop() self.lastFlushTime = time() + #self.emit(self.signal, *self.args) + self.sigDelayed.emit(args) return True def disconnect(self): diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index 1d946062..ddeb0c4a 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -346,9 +346,9 @@ class DockLabel(VerticalLabel): ev.accept() def mouseReleaseEvent(self, ev): + ev.accept() if not self.startedDrag: self.sigClicked.emit(self, ev) - ev.accept() def mouseDoubleClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index ae03d4c2..5aeeac38 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -503,8 +503,8 @@ class Flowchart(Node): finally: self.blockSignals(False) - self.sigChartLoaded.emit() self.outputChanged() + self.sigChartLoaded.emit() self.sigStateChanged.emit() def loadFile(self, fileName=None, startDir=None): diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 7272aef3..687c2e3f 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -205,8 +205,8 @@ class HistogramLUTItem(GraphicsWidget): def regionChanging(self): if self.imageItem() is not None: self.imageItem().setLevels(self.getLevels()) - self.sigLevelsChanged.emit(self) self.update() + self.sigLevelsChanged.emit(self) def imageChanged(self, autoLevel=False, autoRange=False): if self.imageItem() is None: diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index fa2bcf5f..48f30880 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -711,10 +711,10 @@ class ROI(GraphicsObject): if hover: self.setMouseHover(True) - self.sigHoverEvent.emit(self) ev.acceptClicks(QtCore.Qt.LeftButton) ## If the ROI is hilighted, we should accept all clicks to avoid confusion. ev.acceptClicks(QtCore.Qt.RightButton) ev.acceptClicks(QtCore.Qt.MidButton) + self.sigHoverEvent.emit(self) else: self.setMouseHover(False) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 89bb5b98..67fafd83 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -834,8 +834,8 @@ class ScatterPlotItem(GraphicsObject): pts = self.pointsAt(ev.pos()) if len(pts) > 0: self.ptsClicked = pts - self.sigClicked.emit(self, self.ptsClicked) ev.accept() + self.sigClicked.emit(self, self.ptsClicked) else: #print "no spots" ev.ignore() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index e85796c9..b874a3c4 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -427,8 +427,8 @@ class ViewBox(GraphicsWidget): self.updateAutoRange() self.updateViewRange() self._matrixNeedsUpdate = True - self.sigStateChanged.emit(self) self.background.setRect(self.rect()) + self.sigStateChanged.emit(self) self.sigResized.emit(self) def viewRange(self): @@ -561,18 +561,18 @@ class ViewBox(GraphicsWidget): # If nothing has changed, we are done. if any(changed): - self.sigStateChanged.emit(self) - # Update target rect for debugging if self.target.isVisible(): self.target.setRect(self.mapRectFromItem(self.childGroup, self.targetRect())) - # If ortho axes have auto-visible-only, update them now - # Note that aspect ratio constraints and auto-visible probably do not work together.. - if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): - self._autoRangeNeedsUpdate = True - elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): - self._autoRangeNeedsUpdate = True + # If ortho axes have auto-visible-only, update them now + # Note that aspect ratio constraints and auto-visible probably do not work together.. + if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): + self._autoRangeNeedsUpdate = True + elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): + self._autoRangeNeedsUpdate = True + + self.sigStateChanged.emit(self) def setYRange(self, min, max, padding=None, update=True): """ @@ -1156,8 +1156,8 @@ class ViewBox(GraphicsWidget): self._resetTarget() self.scaleBy(s, center) - self.sigRangeChangedManually.emit(mask) ev.accept() + self.sigRangeChangedManually.emit(mask) def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): @@ -1498,14 +1498,8 @@ class ViewBox(GraphicsWidget): if any(changed): self._matrixNeedsUpdate = True - # emit range change signals - if changed[0]: - self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) - if changed[1]: - self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) - self.sigRangeChanged.emit(self, self.state['viewRange']) self.update() - + # Inform linked views that the range has changed for ax in [0, 1]: if not changed[ax]: @@ -1514,6 +1508,13 @@ class ViewBox(GraphicsWidget): if link is not None: link.linkedViewChanged(self, ax) + # emit range change signals + if changed[0]: + self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + if changed[1]: + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) + self.sigRangeChanged.emit(self, self.state['viewRange']) + def updateMatrix(self, changed=None): if not self._matrixNeedsUpdate: return @@ -1541,9 +1542,9 @@ class ViewBox(GraphicsWidget): m.translate(-st[0], -st[1]) self.childGroup.setTransform(m) + self._matrixNeedsUpdate = False self.sigTransformChanged.emit(self) ## segfaults here: 1 - self._matrixNeedsUpdate = False def paint(self, p, opt, widget): self.checkSceneChange() diff --git a/pyqtgraph/parametertree/Parameter.py b/pyqtgraph/parametertree/Parameter.py index df6b1492..654a33db 100644 --- a/pyqtgraph/parametertree/Parameter.py +++ b/pyqtgraph/parametertree/Parameter.py @@ -559,8 +559,8 @@ class Parameter(QtCore.QObject): self.childs.insert(pos, child) child.parentChanged(self) - self.sigChildAdded.emit(self, child, pos) child.sigTreeStateChanged.connect(self.treeStateChanged) + self.sigChildAdded.emit(self, child, pos) return child def removeChild(self, child): @@ -571,11 +571,11 @@ class Parameter(QtCore.QObject): del self.names[name] self.childs.pop(self.childs.index(child)) child.parentChanged(None) - self.sigChildRemoved.emit(self, child) try: child.sigTreeStateChanged.disconnect(self.treeStateChanged) except (TypeError, RuntimeError): ## already disconnected pass + self.sigChildRemoved.emit(self, child) def clearChildren(self): """Remove all child parameters.""" diff --git a/pyqtgraph/widgets/ColorButton.py b/pyqtgraph/widgets/ColorButton.py index a0bb0c8e..43dd16f6 100644 --- a/pyqtgraph/widgets/ColorButton.py +++ b/pyqtgraph/widgets/ColorButton.py @@ -50,11 +50,11 @@ class ColorButton(QtGui.QPushButton): def setColor(self, color, finished=True): """Sets the button's color and emits both sigColorChanged and sigColorChanging.""" self._color = functions.mkColor(color) + self.update() if finished: self.sigColorChanged.emit(self) else: self.sigColorChanging.emit(self) - self.update() def selectColor(self): self.origColor = self.color() diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index b81eab9d..7b8c5986 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -227,12 +227,12 @@ class GraphicsView(QtGui.QGraphicsView): else: self.fitInView(self.range, QtCore.Qt.IgnoreAspectRatio) - self.sigDeviceRangeChanged.emit(self, self.range) - self.sigDeviceTransformChanged.emit(self) - if propagate: for v in self.lockedViewports: v.setXRange(self.range, padding=0) + + self.sigDeviceRangeChanged.emit(self, self.range) + self.sigDeviceTransformChanged.emit(self) def viewRect(self): """Return the boundaries of the view in scene coordinates""" @@ -262,7 +262,6 @@ class GraphicsView(QtGui.QGraphicsView): h = self.range.height() / scale[1] self.range = QtCore.QRectF(center.x() - (center.x()-self.range.left()) / scale[0], center.y() - (center.y()-self.range.top()) /scale[1], w, h) - self.updateMatrix() self.sigScaleChanged.emit(self) From 5ff409ba4b2dacc041ea7f9a7a0a006f52e25ee4 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 1 Jun 2019 22:18:39 -0700 Subject: [PATCH 366/607] Move example test code such that pytest is required --- examples/__main__.py | 29 +--------- examples/test_examples.py | 119 ++++++++++++++++++++++++++++++++------ examples/utils.py | 81 -------------------------- 3 files changed, 104 insertions(+), 125 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 0251974a..ffc38ff7 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -9,8 +9,8 @@ import subprocess from pyqtgraph.python2_3 import basestring from pyqtgraph.Qt import QtGui, QT_LIB +from .utils import buildFileList, path, examples -from .utils import buildFileList, testFile, path, examples if QT_LIB == 'PySide': from .exampleLoaderTemplate_pyside import Ui_Form @@ -117,32 +117,7 @@ class ExampleLoader(QtGui.QMainWindow): def run(): app = QtGui.QApplication([]) loader = ExampleLoader() - app.exec_() if __name__ == '__main__': - - args = sys.argv[1:] - - if '--test' in args: - # get rid of orphaned cache files first - pg.renamePyc(path) - - files = buildFileList(examples) - if '--pyside' in args: - lib = 'PySide' - elif '--pyqt' in args or '--pyqt4' in args: - lib = 'PyQt4' - elif '--pyqt5' in args: - lib = 'PyQt5' - elif '--pyside2' in args: - lib = 'PySide2' - else: - lib = '' - - exe = sys.executable - print("Running tests:", lib, sys.executable) - for f in files: - testFile(f[0], f[1], exe, lib) - else: - run() + run() diff --git a/examples/test_examples.py b/examples/test_examples.py index c5997348..81de8235 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,9 +1,87 @@ from __future__ import print_function, division, absolute_import from pyqtgraph import Qt from . import utils +import errno +import importlib import itertools +import pkgutil import pytest import os, sys +import subprocess +import time + + +path = os.path.abspath(os.path.dirname(__file__)) + + +def runExampleFile(name, f, exe, lib, graphicsSystem=None): + global path + fn = os.path.join(path,f) + os.chdir(path) + sys.stdout.write("{} ".format(name)) + sys.stdout.flush() + import1 = "import %s" % lib if lib != '' else '' + import2 = os.path.splitext(os.path.split(fn)[1])[0] + graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + code = """ +try: + %s + import initExample + import pyqtgraph as pg + %s + import %s + import sys + print("test complete") + sys.stdout.flush() + import time + while True: ## run a little event loop + pg.QtGui.QApplication.processEvents() + time.sleep(0.01) +except: + print("test failed") + raise + +""" % (import1, graphicsSystem, import2) + if sys.platform.startswith('win'): + process = subprocess.Popen([exe], + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + else: + process = subprocess.Popen(['exec %s -i' % (exe)], + shell=True, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + process.stdin.write(code.encode('UTF-8')) + process.stdin.close() ##? + output = '' + fail = False + while True: + try: + c = process.stdout.read(1).decode() + except IOError as err: + if err.errno == errno.EINTR: + # Interrupted system call; just try again. + c = '' + else: + raise + output += c + + if output.endswith('test complete'): + break + if output.endswith('test failed'): + fail = True + break + time.sleep(1) + process.kill() + #res = process.communicate() + res = (process.stdout.read(), process.stderr.read()) + if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): + print(res[0].decode()) + print(res[1].decode()) + return False + return True # printing on travis ci frequently leads to "interrupted system call" errors. @@ -32,16 +110,7 @@ if os.getenv('TRAVIS') is not None: print("Installed wrapper for flaky print.") -# apparently importlib does not exist in python 2.6... -try: - import importlib -except ImportError: - # we are on python 2.6 - print("If you want to test the examples, please install importlib from " - "pypi\n\npip install importlib\n\n") - pass - -files = utils.buildFileList(utils.tested_examples) +files = utils.buildFileList(utils.examples) frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} # sort out which of the front ends are available for frontend in frontends.keys(): @@ -50,16 +119,32 @@ for frontend in frontends.keys(): frontends[frontend] = True except ImportError: pass + except ModuleNotFoundError: + pass + +installed = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) + +# keep a dictionary of example files and their non-standard dependencies +specialExamples = { + "hdf5.py": ["h5py"] +} @pytest.mark.parametrize( - "frontend, f", itertools.product(sorted(list(frontends.keys())), files)) -def test_examples(frontend, f): - # Test the examples with all available front-ends - print('frontend = %s. f = %s' % (frontend, f)) - if not frontends[frontend]: - pytest.skip('%s is not installed. Skipping tests' % frontend) - utils.testFile(f[0], f[1], utils.sys.executable, frontend) + "frontend, f", + [ + pytest.param( + frontend, + f, + marks=pytest.mark.skipif(any(pkgutil.find_loader(pkg) is None for pkg in specialExamples[f[1]]), + reason="Skipping Example for Missing Dependencies") if f[1] in specialExamples.keys() else (), + ) + for frontend, f, in itertools.product(installed, files) + ], + ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installed, files)] +) +def testExamples(frontend, f): + assert runExampleFile(f[0], f[1], sys.executable, frontend) if __name__ == "__main__": pytest.cmdline.main() diff --git a/examples/utils.py b/examples/utils.py index 82270f4c..494b686b 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -1,10 +1,5 @@ from __future__ import division, print_function, absolute_import -import subprocess -import time import os -import sys -import errno -import copy from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.python2_3 import basestring @@ -87,16 +82,10 @@ examples = OrderedDict([ #('VerticalLabel', '../widgets/VerticalLabel.py'), ('JoystickButton', 'JoystickButton.py'), ])), - ('Flowcharts', 'Flowchart.py'), ('Custom Flowchart Nodes', 'FlowchartCustomNode.py'), ]) -not_tested = ['HDF5 big data'] - -tested_examples = copy.deepcopy(examples) -all(map(tested_examples.pop, not_tested)) - def buildFileList(examples, files=None): if files == None: @@ -109,73 +98,3 @@ def buildFileList(examples, files=None): else: buildFileList(val, files) return files - -def testFile(name, f, exe, lib, graphicsSystem=None): - global path - fn = os.path.join(path,f) - #print "starting process: ", fn - os.chdir(path) - sys.stdout.write(name) - sys.stdout.flush() - - import1 = "import %s" % lib if lib != '' else '' - import2 = os.path.splitext(os.path.split(fn)[1])[0] - graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem - code = """ -try: - %s - import initExample - import pyqtgraph as pg - %s - import %s - import sys - print("test complete") - sys.stdout.flush() - import time - while True: ## run a little event loop - pg.QtGui.QApplication.processEvents() - time.sleep(0.01) -except: - print("test failed") - raise - -""" % (import1, graphicsSystem, import2) - - if sys.platform.startswith('win'): - process = subprocess.Popen([exe], stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - process.stdin.write(code.encode('UTF-8')) - process.stdin.close() - else: - process = subprocess.Popen(['exec %s -i' % (exe)], shell=True, stdin=subprocess.PIPE, stderr=subprocess.PIPE, stdout=subprocess.PIPE) - process.stdin.write(code.encode('UTF-8')) - process.stdin.close() ##? - output = '' - fail = False - while True: - try: - c = process.stdout.read(1).decode() - except IOError as err: - if err.errno == errno.EINTR: - # Interrupted system call; just try again. - c = '' - else: - raise - output += c - #sys.stdout.write(c) - #sys.stdout.flush() - if output.endswith('test complete'): - break - if output.endswith('test failed'): - fail = True - break - time.sleep(1) - process.kill() - #res = process.communicate() - res = (process.stdout.read(), process.stderr.read()) - - if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): - print('.' * (50-len(name)) + 'FAILED') - print(res[0].decode()) - print(res[1].decode()) - else: - print('.' * (50-len(name)) + 'passed') From d2331bde7f5e7080cee8dc3edb2f8d3a5f7f916f Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 1 Jun 2019 22:36:29 -0700 Subject: [PATCH 367/607] Removing duplicate entries --- examples/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/utils.py b/examples/utils.py index 494b686b..cf147fb5 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -15,9 +15,7 @@ examples = OrderedDict([ ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), ('Image Analysis', 'imageAnalysis.py'), - ('ViewBox Features', 'ViewBoxFeatures.py'), ('Dock widgets', 'dockarea.py'), - ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), ('Beeswarm plot', 'beeswarm.py'), ('Symbols', 'Symbols.py'), From 9f66b7dc6ec285885a492e18e7890cc2e2edcdb7 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sun, 2 Jun 2019 22:06:07 -0700 Subject: [PATCH 368/607] Much better error reporting/tracepacks on examples --- examples/test_examples.py | 155 +++++++++++++++++++------------------- 1 file changed, 77 insertions(+), 78 deletions(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 81de8235..979bbfb5 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,6 +1,7 @@ from __future__ import print_function, division, absolute_import from pyqtgraph import Qt from . import utils +from collections import namedtuple import errno import importlib import itertools @@ -13,77 +14,6 @@ import time path = os.path.abspath(os.path.dirname(__file__)) - -def runExampleFile(name, f, exe, lib, graphicsSystem=None): - global path - fn = os.path.join(path,f) - os.chdir(path) - sys.stdout.write("{} ".format(name)) - sys.stdout.flush() - import1 = "import %s" % lib if lib != '' else '' - import2 = os.path.splitext(os.path.split(fn)[1])[0] - graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem - code = """ -try: - %s - import initExample - import pyqtgraph as pg - %s - import %s - import sys - print("test complete") - sys.stdout.flush() - import time - while True: ## run a little event loop - pg.QtGui.QApplication.processEvents() - time.sleep(0.01) -except: - print("test failed") - raise - -""" % (import1, graphicsSystem, import2) - if sys.platform.startswith('win'): - process = subprocess.Popen([exe], - stdin=subprocess.PIPE, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE) - else: - process = subprocess.Popen(['exec %s -i' % (exe)], - shell=True, - stdin=subprocess.PIPE, - stderr=subprocess.PIPE, - stdout=subprocess.PIPE) - process.stdin.write(code.encode('UTF-8')) - process.stdin.close() ##? - output = '' - fail = False - while True: - try: - c = process.stdout.read(1).decode() - except IOError as err: - if err.errno == errno.EINTR: - # Interrupted system call; just try again. - c = '' - else: - raise - output += c - - if output.endswith('test complete'): - break - if output.endswith('test failed'): - fail = True - break - time.sleep(1) - process.kill() - #res = process.communicate() - res = (process.stdout.read(), process.stderr.read()) - if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): - print(res[0].decode()) - print(res[1].decode()) - return False - return True - - # printing on travis ci frequently leads to "interrupted system call" errors. # as a workaround, we overwrite the built-in print function (bleh) if os.getenv('TRAVIS') is not None: @@ -124,9 +54,9 @@ for frontend in frontends.keys(): installed = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) -# keep a dictionary of example files and their non-standard dependencies -specialExamples = { - "hdf5.py": ["h5py"] +exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) +conditionalExampleTests = { + "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing") } @@ -136,15 +66,84 @@ specialExamples = { pytest.param( frontend, f, - marks=pytest.mark.skipif(any(pkgutil.find_loader(pkg) is None for pkg in specialExamples[f[1]]), - reason="Skipping Example for Missing Dependencies") if f[1] in specialExamples.keys() else (), + marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False, + reason=conditionalExampleTests[f[1]].reason) if f[1] in conditionalExampleTests.keys() else (), ) for frontend, f, in itertools.product(installed, files) ], ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installed, files)] ) -def testExamples(frontend, f): - assert runExampleFile(f[0], f[1], sys.executable, frontend) +def testExamples(frontend, f, graphicsSystem=None): + # runExampleFile(f[0], f[1], sys.executable, frontend) + + name, file = f + global path + fn = os.path.join(path,file) + os.chdir(path) + sys.stdout.write("{} ".format(name)) + sys.stdout.flush() + import1 = "import %s" % frontend if frontend != '' else '' + import2 = os.path.splitext(os.path.split(fn)[1])[0] + graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + code = """ +try: + %s + import initExample + import pyqtgraph as pg + %s + import %s + import sys + print("test complete") + sys.stdout.flush() + import time + while True: ## run a little event loop + pg.QtGui.QApplication.processEvents() + time.sleep(0.01) +except: + print("test failed") + raise + +""" % (import1, graphicsSystem, import2) + if sys.platform.startswith('win'): + process = subprocess.Popen([sys.executable], + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + else: + process = subprocess.Popen(['exec %s -i' % (sys.executable)], + shell=True, + stdin=subprocess.PIPE, + stderr=subprocess.PIPE, + stdout=subprocess.PIPE) + process.stdin.write(code.encode('UTF-8')) + process.stdin.close() ##? + output = '' + fail = False + while True: + try: + c = process.stdout.read(1).decode() + except IOError as err: + if err.errno == errno.EINTR: + # Interrupted system call; just try again. + c = '' + else: + raise + output += c + + if output.endswith('test complete'): + break + if output.endswith('test failed'): + fail = True + break + time.sleep(1) + process.kill() + #res = process.communicate() + res = (process.stdout.read(), process.stderr.read()) + if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): + print(res[0].decode()) + print(res[1].decode()) + pytest.fail("{}\n{}\nFailed {} Example Test Located in {} ".format(res[0].decode(), res[1].decode(), name, file), pytrace=False) + assert True if __name__ == "__main__": pytest.cmdline.main() From be0e95ace7e05430e6b454de7c6ef035bacd15b4 Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 3 Jun 2019 20:49:31 -0700 Subject: [PATCH 369/607] Incorporating requested changes --- examples/test_examples.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 979bbfb5..61d60d88 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -49,8 +49,6 @@ for frontend in frontends.keys(): frontends[frontend] = True except ImportError: pass - except ModuleNotFoundError: - pass installed = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) @@ -143,7 +141,6 @@ except: print(res[0].decode()) print(res[1].decode()) pytest.fail("{}\n{}\nFailed {} Example Test Located in {} ".format(res[0].decode(), res[1].decode(), name, file), pytrace=False) - assert True if __name__ == "__main__": pytest.cmdline.main() From 501ad4f08238b53e999c5f8e321fbd77680edcec Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 6 Jun 2019 23:45:28 -0700 Subject: [PATCH 370/607] Only set visible when ErrorBarItem has something to draw. --- pyqtgraph/graphicsItems/ErrorBarItem.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 09fa97da..4dc93a56 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -23,6 +23,7 @@ class ErrorBarItem(GraphicsObject): beam=None, pen=None ) + self.setVisible(False) self.setData(**opts) def setData(self, **opts): @@ -44,6 +45,8 @@ class ErrorBarItem(GraphicsObject): This method was added in version 0.9.9. For prior versions, use setOpts. """ + if 'x' in opts and 'y' in opts: + self.setVisible(True) self.opts.update(opts) self.path = None self.update() From 654b76e6a360dd62c21082be8820c2020a90c049 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 6 Jun 2019 23:57:34 -0700 Subject: [PATCH 371/607] Handle setting/clearing data a little more robustly. --- pyqtgraph/graphicsItems/ErrorBarItem.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 4dc93a56..5e399e34 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -45,9 +45,11 @@ class ErrorBarItem(GraphicsObject): This method was added in version 0.9.9. For prior versions, use setOpts. """ - if 'x' in opts and 'y' in opts: - self.setVisible(True) self.opts.update(opts) + if self.opts['x'] is not None and self.opts['y'] is not None: + self.setVisible(True) + else: + self.setVisible(False) self.path = None self.update() self.prepareGeometryChange() From a2fb00633aa1ae64952eb61e97df2763fdbb966b Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 7 Jun 2019 00:00:30 -0700 Subject: [PATCH 372/607] DeMorgans the logic for better readability. --- pyqtgraph/graphicsItems/ErrorBarItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 5e399e34..5d57e3db 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -46,10 +46,10 @@ class ErrorBarItem(GraphicsObject): This method was added in version 0.9.9. For prior versions, use setOpts. """ self.opts.update(opts) - if self.opts['x'] is not None and self.opts['y'] is not None: - self.setVisible(True) - else: + if self.opts['x'] is None or self.opts['y'] is None: self.setVisible(False) + else: + self.setVisible(True) self.path = None self.update() self.prepareGeometryChange() From 1839c5ef59fd7d2f1a3671cda7c4b7e478d6a5a0 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 7 Jun 2019 13:32:25 -0700 Subject: [PATCH 373/607] More concise visibility setting logic --- pyqtgraph/graphicsItems/ErrorBarItem.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ErrorBarItem.py b/pyqtgraph/graphicsItems/ErrorBarItem.py index 5d57e3db..b79da6f7 100644 --- a/pyqtgraph/graphicsItems/ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/ErrorBarItem.py @@ -46,10 +46,7 @@ class ErrorBarItem(GraphicsObject): This method was added in version 0.9.9. For prior versions, use setOpts. """ self.opts.update(opts) - if self.opts['x'] is None or self.opts['y'] is None: - self.setVisible(False) - else: - self.setVisible(True) + self.setVisible(all(self.opts[ax] is not None for ax in ['x', 'y'])) self.path = None self.update() self.prepareGeometryChange() From 9f7a4423af2bfc74c9cfcb79f4646d89d9f484a1 Mon Sep 17 00:00:00 2001 From: Ogi Date: Fri, 7 Jun 2019 15:06:59 -0700 Subject: [PATCH 374/607] Fix attribute lookup reference --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index b874a3c4..27fb8268 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -277,7 +277,7 @@ class ViewBox(GraphicsWidget): scene = self.scene() if scene == self._lastScene: return - if self._lastScene is not None and hasattr(self.lastScene, 'sigPrepareForPaint'): + if self._lastScene is not None and hasattr(self._lastScene, 'sigPrepareForPaint'): self._lastScene.sigPrepareForPaint.disconnect(self.prepareForPaint) if scene is not None and hasattr(scene, 'sigPrepareForPaint'): scene.sigPrepareForPaint.connect(self.prepareForPaint) From 0c8423461274eb5567171ebf16cec93f9c617706 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 8 Jun 2019 19:57:53 -0700 Subject: [PATCH 375/607] Add a test for ErrorBarItem --- .../graphicsItems/tests/test_ErrorBarItem.py | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py diff --git a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py new file mode 100644 index 00000000..8fa38153 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py @@ -0,0 +1,39 @@ +import pytest +from pyqtgraph.Qt import QtGui, QtCore +import pyqtgraph as pg +import numpy as np + +app = pg.mkQApp() + + +def test_errorbaritem_defer_data(): + plot = pg.PlotWidget() + plot.show() + + # plot some data away from the origin to set the view rect + x = np.arange(5) + 10 + curve = pg.PlotCurveItem(x=x, y=x) + plot.addItem(curve) + app.processEvents() + r_no_ebi = plot.viewRect() + + # ErrorBarItem with no data shouldn't affect the view rect + err = pg.ErrorBarItem() + plot.addItem(err) + app.processEvents() + r_empty_ebi = plot.viewRect() + + assert r_no_ebi == r_empty_ebi + + err.setData(x=x, y=x, bottom=x, top=x) + app.processEvents() + r_ebi = plot.viewRect() + + assert r_empty_ebi != r_ebi + + # unset data, ErrorBarItem disappears and view rect goes back to original + err.setData(x=None, y=None) + app.processEvents() + r_clear_ebi = plot.viewRect() + + assert r_clear_ebi == r_no_ebi From 24621959914e08c9d9dff0baffa00daa9b8bddbb Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 8 Jun 2019 18:42:37 -0700 Subject: [PATCH 376/607] Call pytest directly, ignore specific warnings, fix azure template labeling --- azure-test-template.yml | 13 ++++++------- pytest.ini | 10 +++++++++- tox.ini | 17 +++++++++++------ 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 09ba4757..cfdb98dc 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -10,7 +10,7 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python27-Qt4: + Python27-PyQt4: python.version: '2.7' qt.bindings: "pyqt=4" install.method: "conda" @@ -19,11 +19,11 @@ jobs: qt.bindings: "pyside" install.method: "conda" Python37-PyQt-5.9: - python.version: "3.7" + python.version: "3.6" qt.bindings: "pyqt" install.method: "conda" - Python37-PySide2-5.6: - python.version: "3.7" + Python37-PySide2-5.9: + python.version: "3.6" qt.bindings: "pyside2" install.method: "conda" Python37-PyQt-5.12: @@ -141,10 +141,9 @@ jobs: mkdir -p "$SCREENSHOT_DIR" # echo "If Screenshots are generated, they may be downloaded from:" # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" - python -m pytest -sv \ + pytest . -sv \ --junitxml=junit/test-results.xml \ - --cov pyqtgraph --cov-report=xml --cov-report=html \ - --faulthandler-timeout=60 + --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' env: AZURE: 1 diff --git a/pytest.ini b/pytest.ini index 1f133c35..c2f39a6f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,11 @@ [pytest] - +# use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 +addopts = --faulthandler-timeout=15 +filterwarnings = + # comfortable skipping these warnings runtime warnings + # https://stackoverflow.com/questions/40845304/runtimewarning-numpy-dtype-size-changed-may-indicate-binary-incompatibility + ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning + # Warnings generated from PyQt5.9 + ignore:*U.*mode is deprecated:DeprecationWarning + ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning \ No newline at end of file diff --git a/tox.ini b/tox.ini index 5a86b387..6bbb5566 100644 --- a/tox.ini +++ b/tox.ini @@ -2,19 +2,22 @@ envlist = ; qt 5.12.x py{27,37}-pyside2-pip - ; qt 5.12.x py{35,37}-pyqt5-pip + ; qt 5.9.7 py{27,37}-pyqt5-conda + py{27,37}-pyside2-conda + ; qt 5.6.2 py35-pyqt5-conda - ; qt 5.6.2 - py{27,35,37}-pyside2-conda - ; pyqt 4.11.4 / qt 4.8.7 + ; consider dropping support... + ; py35-pyside2-conda + + ; qt 4.8.7 py{27,36}-pyqt4-conda - ; pyside 1.2.4 / qt 4.8.7 py{27,36}-pyside-conda + [base] deps = pytest @@ -26,10 +29,12 @@ deps = coverage [testenv] +passenv = DISPLAY XAUTHORITY deps= {[base]deps} pytest-cov pytest-xdist + pytest-faulthandler pyside2-pip: pyside2 pyqt5-pip: pyqt5 @@ -43,4 +48,4 @@ conda_channels= conda-forge commands= python -c "import pyqtgraph as pg; pg.systemInfo()" - python -m pytest {posargs:pyqtgraph -svv} + pytest {posargs:.} From c5126dc26f786e0249b757901bc9fdea7d5de2e0 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 9 Jun 2019 09:12:01 -0700 Subject: [PATCH 377/607] Update test name. Cleanup unused imports. --- pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py index 8fa38153..4ee25e45 100644 --- a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py @@ -1,12 +1,10 @@ -import pytest -from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg import numpy as np app = pg.mkQApp() -def test_errorbaritem_defer_data(): +def test_ErrorBarItem_defer_data(): plot = pg.PlotWidget() plot.show() From 5c44d51d6c2b0e4ae2872af2ea35deb493880754 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 8 Jun 2019 21:55:32 -0700 Subject: [PATCH 378/607] remove resolution test, have display information printed during debug step --- azure-test-template.yml | 25 ++++++++++++++++--------- pyqtgraph/tests/test_display.py | 10 ---------- pyqtgraph/util/get_resolution.py | 16 ++++++++++++---- pytest.ini | 7 +++++-- 4 files changed, 33 insertions(+), 25 deletions(-) delete mode 100644 pyqtgraph/tests/test_display.py diff --git a/azure-test-template.yml b/azure-test-template.yml index cfdb98dc..6a237e99 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -10,19 +10,19 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python27-PyQt4: + Python27-PyQt4-4.8: python.version: '2.7' qt.bindings: "pyqt=4" install.method: "conda" - Python27-PySide: + Python27-PySide-4.8: python.version: '2.7' qt.bindings: "pyside" install.method: "conda" - Python37-PyQt-5.9: + Python36-PyQt-5.9: python.version: "3.6" qt.bindings: "pyqt" install.method: "conda" - Python37-PySide2-5.9: + Python36-PySide2-5.9: python.version: "3.6" qt.bindings: "pyside2" install.method: "conda" @@ -88,7 +88,6 @@ jobs: then source activate test-environment-$(python.version) fi - pip install setuptools wheel python setup.py bdist_wheel pip install dist/*.whl @@ -98,11 +97,11 @@ jobs: inputs: contents: 'dist/**' targetFolder: $(Build.ArtifactStagingDirectory) - cleanTargetFolder: true # Optional - displayName: "Copy Distributions To Artifacts" + cleanTargetFolder: true + displayName: "Copy Binary Wheel Distribution To Artifacts" - task: PublishBuildArtifacts@1 - displayName: 'Publish Distributions' + displayName: 'Publish Binary Wheel' condition: always() inputs: pathtoPublish: $(Build.ArtifactStagingDirectory)/dist @@ -130,10 +129,18 @@ jobs: pip list echo pyqtgraph system info python -c "import pyqtgraph as pg; pg.systemInfo()" + echo display information + if [ $(agent.os) == 'Linux' ] + then + export DISPLAY=:99.0 + Xvfb :99 -screen 0 1920x1080x24 & + sleep 3 + fi + python -m pyqtgraph.util.get_resolution displayName: 'Debug Info' continueOnError: false - - bash: | + - bash: | if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) diff --git a/pyqtgraph/tests/test_display.py b/pyqtgraph/tests/test_display.py deleted file mode 100644 index 951a10f9..00000000 --- a/pyqtgraph/tests/test_display.py +++ /dev/null @@ -1,10 +0,0 @@ -from .. import mkQApp - -qApp = mkQApp() - - -def test_displayResolution(): - desktop = qApp.desktop().screenGeometry() - width, height = desktop.width(), desktop.height() - print("\n\nDisplay Resolution Logged as {}x{}\n\n".format(width, height)) - assert height > 0 and width > 0 diff --git a/pyqtgraph/util/get_resolution.py b/pyqtgraph/util/get_resolution.py index 3558a81c..79e17170 100644 --- a/pyqtgraph/util/get_resolution.py +++ b/pyqtgraph/util/get_resolution.py @@ -1,7 +1,15 @@ from .. import mkQApp - -def getResolution(): +def test_screenInformation(): qApp = mkQApp() - desktop = qApp.desktop().screenGeometry() - return (desktop.width(), desktop.height()) + desktop = qApp.desktop() + resolution = desktop.screenGeometry() + availableResolution = desktop.availableGeometry() + print("Screen resolution: {}x{}".format(resolution.width(), resolution.height())) + print("Available geometry: {}x{}".format(availableResolution.width(), availableResolution.height())) + print("Number of Screens: {}".format(desktop.screenCount())) + return None + + +if __name__ == "__main__": + test_screenInformation() \ No newline at end of file diff --git a/pytest.ini b/pytest.ini index c2f39a6f..7d27b7a2 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,11 +1,14 @@ [pytest] +xvfb_width = 1920 +xvfb_height = 1080 # use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 addopts = --faulthandler-timeout=15 + filterwarnings = # comfortable skipping these warnings runtime warnings # https://stackoverflow.com/questions/40845304/runtimewarning-numpy-dtype-size-changed-may-indicate-binary-incompatibility ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning # Warnings generated from PyQt5.9 - ignore:*U.*mode is deprecated:DeprecationWarning - ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning \ No newline at end of file + ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning + ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning \ No newline at end of file From f05ff6fbf9331cfabb29d2048143c4ff16045dcb Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 8 Jun 2019 08:07:47 -0700 Subject: [PATCH 379/607] Restore duplicate entries in examples app, but test_examples does not duplicate tests --- examples/test_examples.py | 8 ++++---- examples/utils.py | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 61d60d88..97809653 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -40,7 +40,7 @@ if os.getenv('TRAVIS') is not None: print("Installed wrapper for flaky print.") -files = utils.buildFileList(utils.examples) +files = sorted(set(utils.buildFileList(utils.examples))) frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} # sort out which of the front ends are available for frontend in frontends.keys(): @@ -50,7 +50,7 @@ for frontend in frontends.keys(): except ImportError: pass -installed = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) +installedFrontends = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) conditionalExampleTests = { @@ -67,9 +67,9 @@ conditionalExampleTests = { marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False, reason=conditionalExampleTests[f[1]].reason) if f[1] in conditionalExampleTests.keys() else (), ) - for frontend, f, in itertools.product(installed, files) + for frontend, f, in itertools.product(installedFrontends, files) ], - ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installed, files)] + ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installedFrontends, files)] ) def testExamples(frontend, f, graphicsSystem=None): # runExampleFile(f[0], f[1], sys.executable, frontend) diff --git a/examples/utils.py b/examples/utils.py index cf147fb5..494b686b 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -15,7 +15,9 @@ examples = OrderedDict([ ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), ('Image Analysis', 'imageAnalysis.py'), + ('ViewBox Features', 'ViewBoxFeatures.py'), ('Dock widgets', 'dockarea.py'), + ('Console', 'ConsoleWidget.py'), ('Histograms', 'histogram.py'), ('Beeswarm plot', 'beeswarm.py'), ('Symbols', 'Symbols.py'), From f359449715bba89bf39c9877bc916cb4c528b8fe Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 10 Jun 2019 22:24:53 -0700 Subject: [PATCH 380/607] README and CONTRIBUTING update --- CONTRIBUTING.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.txt | 58 ---------------------------------------- README.md | 53 +++++++++++++++++++++---------------- 3 files changed, 99 insertions(+), 81 deletions(-) create mode 100644 CONTRIBUTING.md delete mode 100644 CONTRIBUTING.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..3ca5e0bf --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,69 @@ +# Contributing to PyQtGraph + +Contributions to pyqtgraph are welcome! + +Please use the following guidelines when preparing changes: + +## Submitting Code Changes + +* The preferred method for submitting changes is by github pull request against the "develop" branch. +* Pull requests should include only a focused and related set of changes. Mixed features and unrelated changes may be rejected. +* For major changes, it is recommended to discuss your plans on the mailing list or in a github issue before putting in too much effort. + * Along these lines, please note that `pyqtgraph.opengl` will be deprecated soon and replaced with VisPy. + +## Documentation + +* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. +* Documentation is generated with sphinx; please check that docstring changes compile correctly + +## Style guidelines + +* PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable. +* Use `python setup.py style` to see whether your code follows the mandatory style guidelines checked by flake8. +* Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt +* Exception 2: Function docstrings use ReStructuredText tables for describing arguments: + + ```text + ============== ======================================================== + **Arguments:** + argName1 (type) Description of argument + argName2 (type) Description of argument. Longer descriptions must + be wrapped within the column guidelines defined by the + "====" header and footer. + ============== ======================================================== + ``` + + QObject subclasses that implement new signals should also describe + these in a similar table. + +## Testing Setting up a test environment + +### Dependencies + +* tox +* tox-conda +* pytest +* pytest-cov +* pytest-xdist +* pytest-faulthandler +* Optional: pytest-xvfb + +### Tox + +As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make use of `tox` to test against most of the configurations in our test matrix. As some of the qt-bindings are only installable via `conda`, `conda` needs to be in your `PATH`, and we utilize the `tox-conda` plugin. + +* Tests for a module should ideally cover all code in that module, i.e., statement coverage should be at 100%. +* To measure the test coverage, un `pytest --cov -n 4` to run the test suite with coverage on 4 cores. + +### Continous Integration + +For our Continuous Integration, we utilize Azure Pipelines. On each OS, we test the following 6 configurations + +* Python2.7 with PyQt4 +* Python2.7 with PySide +* Python3.6 with PyQt5-5.9 +* Python3.6 with PySide2-5.9 +* Python3.7 with PyQt5-5.12 +* Python3.7 with PySide2-5.12 + +More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1) diff --git a/CONTRIBUTING.txt b/CONTRIBUTING.txt deleted file mode 100644 index 5df9703f..00000000 --- a/CONTRIBUTING.txt +++ /dev/null @@ -1,58 +0,0 @@ -Contributions to pyqtgraph are welcome! - -Please use the following guidelines when preparing changes: - -* The preferred method for submitting changes is by github pull request - against the "develop" branch. - -* Pull requests should include only a focused and related set of changes. - Mixed features and unrelated changes may be rejected. - -* For major changes, it is recommended to discuss your plans on the mailing - list or in a github issue before putting in too much effort. - - * Along these lines, please note that pyqtgraph.opengl will be deprecated - soon and replaced with VisPy. - -* Writing proper documentation and unit tests is highly encouraged. PyQtGraph - uses nose / py.test style testing, so tests should usually be included in a - tests/ directory adjacent to the relevant code. - -* Documentation is generated with sphinx; please check that docstring changes - compile correctly. - -* Style guidelines: - - * PyQtGraph prefers PEP8 for most style issues, but this is not enforced - rigorously as long as the code is clean and readable. - - * Use `python setup.py style` to see whether your code follows - the mandatory style guidelines checked by flake8. - - * Exception 1: All variable names should use camelCase rather than - underscore_separation. This is done for consistency with Qt - - * Exception 2: Function docstrings use ReStructuredText tables for - describing arguments: - - ``` - ============== ======================================================== - **Arguments:** - argName1 (type) Description of argument - argName2 (type) Description of argument. Longer descriptions must - be wrapped within the column guidelines defined by the - "====" header and footer. - ============== ======================================================== - ``` - - QObject subclasses that implement new signals should also describe - these in a similar table. - -* Setting up a test environment. - - Tests for a module should ideally cover all code in that module, - i.e., statement coverage should be at 100%. - - To measure the test coverage, install py.test, pytest-cov and pytest-xdist. - Then run 'py.test --cov -n 4' to run the test suite with coverage on 4 cores. - diff --git a/README.md b/README.md index 123949d5..e5b3a9c7 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -[![Build Status](https://travis-ci.org/pyqtgraph/pyqtgraph.svg?branch=develop)](https://travis-ci.org/pyqtgraph/pyqtgraph) -[![codecov.io](http://codecov.io/github/pyqtgraph/pyqtgraph/coverage.svg?branch=develop)](http://codecov.io/github/pyqtgraph/pyqtgraph?branch=develop) + +[![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop) + PyQtGraph ========= -A pure-Python graphics library for PyQt/PySide +A pure-Python graphics library for PyQt/PySide/PyQt5/PySide2 -Copyright 2017 Luke Campagnola, University of North Carolina at Chapel Hill +Copyright 2019 Luke Campagnola, University of North Carolina at Chapel Hill @@ -15,15 +16,32 @@ Despite being written entirely in python, the library is fast due to its heavy leverage of numpy for number crunching, Qt's GraphicsView framework for 2D display, and OpenGL for 3D display. - Requirements ------------ -* PyQt 4.7+, PySide, PyQt5, or PySide2 +* PyQt 4.8+, PySide, PyQt5, or PySide2 * python 2.7, or 3.x -* NumPy -* For 3D graphics: pyopengl and qt-opengl -* Known to run on Windows, Linux, and Mac. +* Required + * `numpy`, `scipy` +* Optional + * `pyopengl` for 3D graphics + * `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy` + * `hdf5` for large hdf5 binary format support +* Known to run on Windows, Linux, and macOS. + +Qt Bindings Test Matrix +----------------------- + +Below is a table of the configurations we test and have confidence pyqtgraph will work with. All current operating major operating systems (Windows, macOS, Linux) are tested against this configuration. We recommend using the Qt 5.12 or 5.9 (either PyQt5 or PySide2) bindings. + +| Python Version | PyQt4 | PySide | PyQt5-5.6 | PySide2-5.6 | PyQt5-5.9 | PySide2-5.9 | PyQt5-5.12 | PySide2 5.12 | +| :-------------- | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | +| 2.7 | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | +| 3.5 | :x: | :x: | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | +| 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | + +* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible. Support ------- @@ -36,7 +54,9 @@ Installation Methods * From PyPI: * Last released version: `pip install pyqtgraph` - * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph` + * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@develop` +* From conda + * Last released version: `conda install pyqtgraph` * To install system-wide from source distribution: `python setup.py install` * Many linux package repositories have release versions. * To use with a specific project, simply copy the pyqtgraph subdirectory @@ -49,16 +69,3 @@ Documentation The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu. The official documentation lives at http://pyqtgraph.org/documentation - -Testing -------- - -To test the pyqtgraph library, clone the repository, and run `pytest pyqtgraph`. For more thurough testing, you can use `tox`, however the [tox-conda](https://github.com/tox-dev/tox-conda) plugin is required. Running `tox` on its own will run `pytest pyqtgraph -vv` on it's own, however if you want to run a specific test, you can run `tox -- pyqtgraph/exporters/tests/test_svg::test_plotscene` for example. - -Dependencies include: - -* pytest -* pytest-cov -* pytest-xdist -* tox -* tox-conda \ No newline at end of file From ed3a039d236cd0893cfb7365d32c09f22f0c7a1b Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 11 Jun 2019 23:01:24 -0700 Subject: [PATCH 381/607] Testing segfault potential fix --- 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 6a237e99..8a68317b 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -150,7 +150,7 @@ jobs: # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" pytest . -sv \ --junitxml=junit/test-results.xml \ - --cov pyqtgraph --cov-report=xml --cov-report=html + -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' env: AZURE: 1 From 4a592ef10e9cad8fe469d3d5b8a29d9b0ad88a37 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Mon, 17 Jun 2019 19:10:32 +0200 Subject: [PATCH 382/607] Prevent element-wise string comparison Issue #835 shows that comparing `bins`, which may be a numpy array, with a string `'auto'` leads to element-wise comparison, because the `==` operator for numpy arrays is used. With this commit, potential array and string are switched, so the `==` operator for strings is used, which does no element-wise comparison. --- pyqtgraph/graphicsItems/ImageItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 65e87eec..1758bb4d 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -488,7 +488,7 @@ class ImageItem(GraphicsObject): step = (step, step) stepData = self.image[::step[0], ::step[1]] - if bins == 'auto': + if 'auto' == bins: mn = np.nanmin(stepData) mx = np.nanmax(stepData) if mx == mn: From fa2a03b8ecb9216cd0833d853e4cf64e803c3f83 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Tue, 18 Jun 2019 20:14:51 +0200 Subject: [PATCH 383/607] Write Python representation of path to Python file Before, if the path contained escaped sequences, they would be parsed before being written to `reload_test_mod.py`, therefore when the file was parsed by the Python interpreter, the escape signs would be missing. With this commit, the Python representation is written to the file, so escaped sequences stay escaped. --- pyqtgraph/tests/test_reload.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/tests/test_reload.py b/pyqtgraph/tests/test_reload.py index 6adbeeb6..007e90d2 100644 --- a/pyqtgraph/tests/test_reload.py +++ b/pyqtgraph/tests/test_reload.py @@ -4,6 +4,7 @@ import pyqtgraph.reload pgpath = os.path.join(os.path.dirname(pg.__file__), '..') +pgpath_repr = repr(pgpath) # make temporary directory to write module code path = None @@ -22,7 +23,7 @@ def teardown_module(): code = """ import sys -sys.path.append('{path}') +sys.path.append({path_repr}) import pyqtgraph as pg @@ -47,7 +48,7 @@ def test_reload(): # write a module mod = os.path.join(path, 'reload_test_mod.py') print("\nRELOAD FILE:", mod) - open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version1")) + open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version1")) # import the new module import reload_test_mod @@ -63,7 +64,7 @@ def test_reload(): # write again and reload - open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2")) + open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) if py3: @@ -87,7 +88,7 @@ def test_reload(): # write again and reload - open(mod, 'w').write(code.format(path=pgpath, msg="C.fn() Version2")) + open(mod, 'w').write(code.format(path_repr=pgpath_repr, msg="C.fn() Version2")) remove_cache(mod) pg.reload.reloadAll(path, debug=True) if py3: From 04baa6eef7571ef1565674bb226603b87725763b Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Thu, 20 Jun 2019 04:37:09 +0200 Subject: [PATCH 384/607] addLine now accepts 'pos' and 'angle' parameters The issue and this solution are discussed in issue https://github.com/pyqtgraph/pyqtgraph/issues/70. --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index ae74c5b6..9703f286 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -545,9 +545,9 @@ class PlotItem(GraphicsWidget): :func:`InfiniteLine.__init__() `. Returns the item created. """ - pos = kwds.get('pos', x if x is not None else y) - angle = kwds.get('angle', 0 if x is None else 90) - line = InfiniteLine(pos, angle, **kwds) + kwds['pos'] = kwds.get('pos', x if x is not None else y) + kwds['angle'] = kwds.get('angle', 0 if x is None else 90) + line = InfiniteLine(**kwds) self.addItem(line) if z is not None: line.setZValue(z) From 9b8ef188a573f4c1f248c2040df2973e56cbce0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Janez=20Dem=C5=A1ar?= Date: Thu, 20 Jun 2019 07:07:57 +0200 Subject: [PATCH 385/607] Fix incorrect clipping of horizontal axis when stopAxisAtTick is set (#932) Horizontal axis are clipeed incorrectly because the code always takes the vertical coordinate of the span even if the axis is horizontal. --- pyqtgraph/graphicsItems/AxisItem.py | 8 +++-- .../graphicsItems/tests/test_AxisItem.py | 30 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 pyqtgraph/graphicsItems/tests/test_AxisItem.py diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 3e358870..cc94f318 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -900,16 +900,20 @@ class AxisItem(GraphicsWidget): if self.style['stopAxisAtTick'][0] is True: - stop = max(span[0].y(), min(map(min, tickPositions))) + minTickPosition = min(map(min, tickPositions)) if axis == 0: + stop = max(span[0].y(), minTickPosition) span[0].setY(stop) else: + stop = max(span[0].x(), minTickPosition) span[0].setX(stop) if self.style['stopAxisAtTick'][1] is True: - stop = min(span[1].y(), max(map(max, tickPositions))) + maxTickPosition = max(map(max, tickPositions)) if axis == 0: + stop = min(span[1].y(), maxTickPosition) span[1].setY(stop) else: + stop = min(span[1].x(), maxTickPosition) span[1].setX(stop) axisSpec = (self.pen(), span[0], span[1]) diff --git a/pyqtgraph/graphicsItems/tests/test_AxisItem.py b/pyqtgraph/graphicsItems/tests/test_AxisItem.py new file mode 100644 index 00000000..f076890d --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_AxisItem.py @@ -0,0 +1,30 @@ +import pyqtgraph as pg + +app = pg.mkQApp() + +def test_AxisItem_stopAxisAtTick(monkeypatch): + def test_bottom(p, axisSpec, tickSpecs, textSpecs): + assert view.mapToView(axisSpec[1]).x() == 0.25 + assert view.mapToView(axisSpec[2]).x() == 0.75 + + def test_left(p, axisSpec, tickSpecs, textSpecs): + assert view.mapToView(axisSpec[1]).y() == 0.875 + assert view.mapToView(axisSpec[2]).y() == 0.125 + + plot = pg.PlotWidget() + view = plot.plotItem.getViewBox() + bottom = plot.getAxis("bottom") + bottom.setRange(0, 1) + bticks = [(0.25, "a"), (0.6, "b"), (0.75, "c")] + bottom.setTicks([bticks, bticks]) + bottom.setStyle(stopAxisAtTick=(True, True)) + monkeypatch.setattr(bottom, "drawPicture", test_bottom) + + left = plot.getAxis("left") + lticks = [(0.125, "a"), (0.55, "b"), (0.875, "c")] + left.setTicks([lticks, lticks]) + left.setRange(0, 1) + left.setStyle(stopAxisAtTick=(True, True)) + monkeypatch.setattr(left, "drawPicture", test_left) + + plot.show() From 2f4ac51a118a532b0650645259380e7166f53cd0 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 19 Jun 2019 22:08:54 -0700 Subject: [PATCH 386/607] Check if items having events sent to are still in the scene (#919) Check if items having events sent to are still in the scene --- pyqtgraph/GraphicsScene/GraphicsScene.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 0fca2684..01b6b808 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -263,7 +263,8 @@ class GraphicsScene(QtGui.QGraphicsScene): for item in prevItems: event.currentItem = item try: - item.hoverEvent(event) + if item.scene() is self: + item.hoverEvent(event) except: debug.printExc("Error sending hover exit event:") finally: @@ -288,7 +289,7 @@ class GraphicsScene(QtGui.QGraphicsScene): else: acceptedItem = None - if acceptedItem is not None: + if acceptedItem is not None and acceptedItem.scene() is self: #print "Drag -> pre-selected item:", acceptedItem self.dragItem = acceptedItem event.currentItem = self.dragItem @@ -435,6 +436,8 @@ class GraphicsScene(QtGui.QGraphicsScene): for item in items: if hoverable and not hasattr(item, 'hoverEvent'): continue + if item.scene() is not self: + continue shape = item.shape() # Note: default shape() returns boundingRect() if shape is None: continue From 0cc4900d7aedd853c2b1b10fe5bdf8d886751a61 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 21 Jun 2019 08:36:42 -0700 Subject: [PATCH 387/607] Skip some test examples (#937) * Skip RemoteSpeedTest.py during testing * Skip `optics_demos.py` test on PySide 1. 2.4 due to documented pyside bug --- examples/test_examples.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index 97809653..0856b4ff 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -54,7 +54,9 @@ installedFrontends = sorted([frontend for frontend, isPresent in frontends.items exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) conditionalExampleTests = { - "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing") + "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing"), + "RemoteSpeedTest.py": exceptionCondition(False, reason="Test is being problematic on CI machines"), + "optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671") } From 5238c097d59fd57f7e819484ce0ba80af4fc7972 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 21 Jun 2019 21:54:04 +0200 Subject: [PATCH 388/607] Update Travis according to new xvfb syntax (#944) --- .travis.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5a8dcf5f..4ce5f228 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,6 +18,8 @@ env: # - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda #- PYTHON=3.2 QT=pyqt5 TEST=standard +services: + - xvfb before_install: - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi @@ -74,7 +76,6 @@ install: before_script: # We need to create a (fake) display on Travis, let's use a funny resolution - export DISPLAY=:99.0 - - "sh -e /etc/init.d/xvfb start" - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render # Make sure everyone uses the correct python (this is handled by conda) From 0264dd40cd286c5350138769ac0b88481b7588bb Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 22 Jun 2019 01:52:11 +0200 Subject: [PATCH 389/607] Added pytest-faulthandler to Travis (#945) --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4ce5f228..0da455d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,6 +63,7 @@ install: fi; - pip install pytest-xdist # multi-thread py.test - pip install pytest-cov # add coverage stats + - pip install pytest-faulthandler # activate faulthandler # Debugging helpers - uname -a From 781e129725ab66dd155162896d6967a3af52dcf0 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 22 Jun 2019 06:18:12 +0200 Subject: [PATCH 390/607] Fix deprecation warning of multi-dimensional tuples (#947) --- pyqtgraph/graphicsItems/ROI.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 48f30880..9ce62bd9 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -1667,7 +1667,7 @@ class MultiRectROI(QtGui.QGraphicsObject): ms = min([r.shape[axes[1]] for r in rgns]) sl = [slice(None)] * rgns[0].ndim sl[axes[1]] = slice(0,ms) - rgns = [r[sl] for r in rgns] + rgns = [r[tuple(sl)] for r in rgns] #print [r.shape for r in rgns], axes return np.concatenate(rgns, axis=axes[0]) From edf2942010e8b9651ae0aab2ce351f674e02e737 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 22 Jun 2019 06:19:02 +0200 Subject: [PATCH 391/607] Replaced usage of deprecated ROI classes in example (#946) --- examples/ROItypes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ROItypes.py b/examples/ROItypes.py index 1a064d33..4352f888 100644 --- a/examples/ROItypes.py +++ b/examples/ROItypes.py @@ -92,10 +92,10 @@ def updateRoiPlot(roi, data=None): rois = [] rois.append(pg.TestROI([0, 0], [20, 20], maxBounds=QtCore.QRectF(-10, -10, 230, 140), pen=(0,9))) rois.append(pg.LineROI([0, 0], [20, 20], width=5, pen=(1,9))) -rois.append(pg.MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) +rois.append(pg.MultiRectROI([[0, 50], [50, 60], [60, 30]], width=5, pen=(2,9))) rois.append(pg.EllipseROI([110, 10], [30, 20], pen=(3,9))) rois.append(pg.CircleROI([110, 50], [20, 20], pen=(4,9))) -rois.append(pg.PolygonROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) +rois.append(pg.PolyLineROI([[2,0], [2.1,0], [2,.1]], pen=(5,9))) #rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0))) ## Add each ROI to the scene and link its data to a plot curve with the same color From 9500f4db0194a37521f94fde50af9fcd39e1e980 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 23 Jun 2019 07:17:14 +0200 Subject: [PATCH 392/607] Allow multiline parameters in configparser (#949) * FIX: Exception.message does not exist in Python3 * FIX: Allow multiline configfile parameters * Added configparser tests * Reasonable file ending for test files --- pyqtgraph/configfile.py | 6 ++--- pyqtgraph/tests/test_configparser.py | 36 ++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 pyqtgraph/tests/test_configparser.py diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index e7056599..275a4fdb 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -33,9 +33,8 @@ class ParseError(Exception): msg = "Error parsing string at line %d:\n" % self.lineNum else: msg = "Error parsing config file '%s' at line %d:\n" % (self.fileName, self.lineNum) - msg += "%s\n%s" % (self.line, self.message) + msg += "%s\n%s" % (self.line, Exception.__str__(self)) return msg - #raise Exception() def writeConfigFile(data, fname): @@ -93,13 +92,14 @@ def genString(data, indent=''): s += indent + sk + ':\n' s += genString(data[k], indent + ' ') else: - s += indent + sk + ': ' + repr(data[k]) + '\n' + s += indent + sk + ': ' + repr(data[k]).replace("\n", "\\\n") + '\n' return s def parseString(lines, start=0): data = OrderedDict() if isinstance(lines, basestring): + lines = lines.replace("\\\n", "") lines = lines.split('\n') lines = [l for l in lines if re.search(r'\S', l) and not re.match(r'\s*#', l)] ## remove empty lines diff --git a/pyqtgraph/tests/test_configparser.py b/pyqtgraph/tests/test_configparser.py new file mode 100644 index 00000000..27af9ec7 --- /dev/null +++ b/pyqtgraph/tests/test_configparser.py @@ -0,0 +1,36 @@ +from pyqtgraph import configfile +import numpy as np +import tempfile, os + +def test_longArrays(): + """ + Test config saving and loading of long arrays. + """ + tmp = tempfile.mktemp(".cfg") + + arr = np.arange(20) + configfile.writeConfigFile({'arr':arr}, tmp) + config = configfile.readConfigFile(tmp) + + assert all(config['arr'] == arr) + + os.remove(tmp) + +def test_multipleParameters(): + """ + Test config saving and loading of multiple parameters. + """ + tmp = tempfile.mktemp(".cfg") + + par1 = [1,2,3] + par2 = "Test" + par3 = {'a':3,'b':'c'} + + configfile.writeConfigFile({'par1':par1, 'par2':par2, 'par3':par3}, tmp) + config = configfile.readConfigFile(tmp) + + assert config['par1'] == par1 + assert config['par2'] == par2 + assert config['par3'] == par3 + + os.remove(tmp) From 96532540943faf1d53c36a5921b8b1622853b3cb Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 23 Jun 2019 09:38:48 -0700 Subject: [PATCH 393/607] Fix infinite scale in makeARGB (#955) --- pyqtgraph/functions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index a08c995e..6f67cfff 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1094,7 +1094,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): for i in range(data.shape[-1]): minVal, maxVal = levels[i] if minVal == maxVal: - maxVal += 1e-16 + maxVal = np.nextafter(maxVal, 2*maxVal) rng = maxVal-minVal rng = 1 if rng == 0 else rng newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype) @@ -1104,7 +1104,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): minVal, maxVal = levels if minVal != 0 or maxVal != scale: if minVal == maxVal: - maxVal += 1e-16 + maxVal = np.nextafter(maxVal, 2*maxVal) data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype) From 563083cf866fb3473ac10364658c82a6925844e6 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 23 Jun 2019 12:14:52 -0700 Subject: [PATCH 394/607] Handle invalid file descriptor in exit --- pyqtgraph/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 2db79985..b1aa98aa 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -369,8 +369,12 @@ def exit(): ## close file handles if sys.platform == 'darwin': for fd in range(3, 4096): - if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac. + if fd in [7]: # trying to close 7 produces an illegal instruction on the Mac. + continue + try: os.close(fd) + except OSError: + pass else: os.closerange(3, 4096) ## just guessing on the maximum descriptor count.. From e510971d71be5d8aeec5860df6ecd2f0e719b09f Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Mon, 24 Jun 2019 02:01:32 +0200 Subject: [PATCH 395/607] RotateFree handle now rotates freely (Code by alguryanow) (#952) --- pyqtgraph/graphicsItems/ROI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 9ce62bd9..bb0523cf 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -928,6 +928,7 @@ class ROI(GraphicsObject): if h['type'] == 'rf': h['item'].setPos(self.mapFromScene(p1)) ## changes ROI coordinates of handle + h['pos'] = self.mapFromParent(p1) elif h['type'] == 'sr': if h['center'][0] == h['pos'][0]: From dea8a86dfd7e5df4f94f088201c968632564470f Mon Sep 17 00:00:00 2001 From: Ben Mathews Date: Sun, 23 Jun 2019 18:05:11 -0600 Subject: [PATCH 396/607] Fixes https://github.com/pyqtgraph/pyqtgraph/issues/950 (#951) Moving a scale handle on a ROI object does not fire a sigRegionChangeStarted signal. This patch adds the signal emit to handleMoveStarted(). --- pyqtgraph/graphicsItems/ROI.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index bb0523cf..fafb5592 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -428,6 +428,7 @@ class ROI(GraphicsObject): def handleMoveStarted(self): self.preMoveState = self.getState() + self.sigRegionChangeStarted.emit(self) def addTranslateHandle(self, pos, axes=None, item=None, name=None, index=None): """ From 1b6537b241ba54eed286417dcf7cea703f19051f Mon Sep 17 00:00:00 2001 From: SamSchott Date: Mon, 24 Jun 2019 01:07:55 +0100 Subject: [PATCH 397/607] Curve fill: draw line around patch (#922) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 9b4e95ef..b864c61b 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -502,7 +502,10 @@ class PlotCurveItem(GraphicsObject): p.setPen(sp) p.drawPath(path) p.setPen(cp) - p.drawPath(path) + if self.fillPath is not None: + p.drawPath(self.fillPath) + else: + p.drawPath(path) profiler('drawPath') #print "Render hints:", int(p.renderHints()) From 3e7cace746cd11ce02144028c84921b52045ad4d Mon Sep 17 00:00:00 2001 From: SamSchott Date: Mon, 24 Jun 2019 01:27:16 +0100 Subject: [PATCH 398/607] tickSpacing bug fix (#836) Fixed a bug where `tickSpacing()` would return `None` if `style['maxTickLevel'] < 2`, resulting in the axis not being drawn. --- pyqtgraph/graphicsItems/AxisItem.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index cc94f318..b34052ae 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -643,10 +643,9 @@ class AxisItem(GraphicsWidget): maxTickCount = size / minSpacing if dif / intervals[minorIndex] <= maxTickCount: levels.append((intervals[minorIndex], 0)) - return levels - - - + + return levels + ##### This does not work -- switching between 2/5 confuses the automatic text-level-selection ### Determine major/minor tick spacings which flank the optimal spacing. #intervals = np.array([1., 2., 5., 10., 20., 50., 100.]) * p10unit From 297e1d95a56ad84dd44dd9f02a7a575bffa04b31 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Mon, 24 Jun 2019 03:30:40 +0300 Subject: [PATCH 399/607] avoid double call to mkPen when creating PlotCurveItem objects (#817) * avoid double call to mkPen when creating PlotCurveItem objects * avoid unnecessary calls to mkPen in paint --- pyqtgraph/graphicsItems/PlotCurveItem.py | 205 +++++++++++------------ 1 file changed, 102 insertions(+), 103 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index b864c61b..673d8334 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -4,7 +4,7 @@ try: HAVE_OPENGL = True except: HAVE_OPENGL = False - + import numpy as np from .GraphicsObject import GraphicsObject from .. import functions as fn @@ -15,51 +15,50 @@ from .. import debug __all__ = ['PlotCurveItem'] class PlotCurveItem(GraphicsObject): - - + + """ Class representing a single plot curve. Instances of this class are created automatically as part of PlotDataItem; these rarely need to be instantiated directly. - + Features: - + - Fast data update - Fill under curve - Mouse interaction - + ==================== =============================================== **Signals:** sigPlotChanged(self) Emitted when the data being plotted has changed sigClicked(self) Emitted when the curve is clicked ==================== =============================================== """ - + sigPlotChanged = QtCore.Signal(object) sigClicked = QtCore.Signal(object) - + def __init__(self, *args, **kargs): """ Forwards all arguments to :func:`setData `. - + Some extra arguments are accepted as well: - + ============== ======================================================= **Arguments:** parent The parent GraphicsObject (optional) - clickable If True, the item will emit sigClicked when it is + clickable If True, the item will emit sigClicked when it is clicked on. Defaults to False. ============== ======================================================= """ GraphicsObject.__init__(self, kargs.get('parent', None)) self.clear() - + ## this is disastrous for performance. #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - + self.metaData = {} self.opts = { - 'pen': fn.mkPen('w'), 'shadowPen': None, 'fillLevel': None, 'brush': None, @@ -70,21 +69,23 @@ class PlotCurveItem(GraphicsObject): 'mouseWidth': 8, # width of shape responding to mouse click 'compositionMode': None, } + if 'pen' not in kargs: + self.opts['pen'] = fn.mkPen('w') self.setClickable(kargs.get('clickable', False)) self.setData(*args, **kargs) - + def implements(self, interface=None): ints = ['plotData'] if interface is None: return ints return interface in ints - + def name(self): return self.opts.get('name', None) - + def setClickable(self, s, width=None): """Sets whether the item responds to mouse clicks. - + The *width* argument specifies the width in pixels orthogonal to the curve that will respond to a mouse click. """ @@ -92,41 +93,41 @@ class PlotCurveItem(GraphicsObject): if width is not None: self.opts['mouseWidth'] = width self._mouseShape = None - self._boundingRect = None - + self._boundingRect = None + def setCompositionMode(self, mode): """Change the composition mode of the item (see QPainter::CompositionMode in the Qt documentation). This is useful when overlaying multiple items. - + ============================================ ============================================================ **Most common arguments:** QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it is opaque. Otherwise, it uses the alpha channel to blend the image with the background. - QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to + QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to reflect the lightness or darkness of the background. - QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels + QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels are added together. QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background. ============================================ ============================================================ """ self.opts['compositionMode'] = mode self.update() - + def getData(self): return self.xData, self.yData - + def dataBounds(self, ax, frac=1.0, orthoRange=None): ## Need this to run as fast as possible. ## check cache first: cache = self._boundsCache[ax] if cache is not None and cache[0] == (frac, orthoRange): return cache[1] - + (x, y) = self.getData() if x is None or len(x) == 0: return (None, None) - + if ax == 0: d = x d2 = y @@ -139,7 +140,7 @@ class PlotCurveItem(GraphicsObject): mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) d = d[mask] #d2 = d2[mask] - + if len(d) == 0: return (None, None) @@ -154,7 +155,7 @@ class PlotCurveItem(GraphicsObject): if len(d) == 0: return (None, None) b = (d.min(), d.max()) - + elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: @@ -166,7 +167,7 @@ class PlotCurveItem(GraphicsObject): ## adjust for fill level if ax == 1 and self.opts['fillLevel'] is not None: b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel'])) - + ## Add pen width only if it is non-cosmetic. pen = self.opts['pen'] spen = self.opts['shadowPen'] @@ -174,10 +175,10 @@ class PlotCurveItem(GraphicsObject): b = (b[0] - pen.widthF()*0.7072, b[1] + pen.widthF()*0.7072) if spen is not None and not spen.isCosmetic() and spen.style() != QtCore.Qt.NoPen: b = (b[0] - spen.widthF()*0.7072, b[1] + spen.widthF()*0.7072) - + self._boundsCache[ax] = [(frac, orthoRange), b] return b - + def pixelPadding(self): pen = self.opts['pen'] spen = self.opts['shadowPen'] @@ -196,11 +197,11 @@ class PlotCurveItem(GraphicsObject): (ymn, ymx) = self.dataBounds(ax=1) if xmn is None or ymn is None: return QtCore.QRectF() - + px = py = 0.0 pxPad = self.pixelPadding() if pxPad > 0: - # determine length of pixel in local x, y directions + # determine length of pixel in local x, y directions px, py = self.pixelVectors() try: px = 0 if px is None else px.length() @@ -210,68 +211,68 @@ class PlotCurveItem(GraphicsObject): py = 0 if py is None else py.length() except OverflowError: py = 0 - + # return bounds expanded by pixel size px *= pxPad py *= pxPad #px += self._maxSpotWidth * 0.5 #py += self._maxSpotWidth * 0.5 self._boundingRect = QtCore.QRectF(xmn-px, ymn-py, (2*px)+xmx-xmn, (2*py)+ymx-ymn) - + return self._boundingRect - + def viewTransformChanged(self): self.invalidateBounds() self.prepareGeometryChange() - + #def boundingRect(self): #if self._boundingRect is None: #(x, y) = self.getData() #if x is None or y is None or len(x) == 0 or len(y) == 0: #return QtCore.QRectF() - - + + #if self.opts['shadowPen'] is not None: #lineWidth = (max(self.opts['pen'].width(), self.opts['shadowPen'].width()) + 1) #else: #lineWidth = (self.opts['pen'].width()+1) - - + + #pixels = self.pixelVectors() #if pixels == (None, None): #pixels = [Point(0,0), Point(0,0)] - + #xmin = x.min() #xmax = x.max() #ymin = y.min() #ymax = y.max() - + #if self.opts['fillLevel'] is not None: #ymin = min(ymin, self.opts['fillLevel']) #ymax = max(ymax, self.opts['fillLevel']) - + #xmin -= pixels[0].x() * lineWidth #xmax += pixels[0].x() * lineWidth #ymin -= abs(pixels[1].y()) * lineWidth #ymax += abs(pixels[1].y()) * lineWidth - + #self._boundingRect = QtCore.QRectF(xmin, ymin, xmax-xmin, ymax-ymin) #return self._boundingRect - + def invalidateBounds(self): self._boundingRect = None self._boundsCache = [None, None] - + def setPen(self, *args, **kargs): """Set the pen used to draw the curve.""" self.opts['pen'] = fn.mkPen(*args, **kargs) self.invalidateBounds() self.update() - + def setShadowPen(self, *args, **kargs): """Set the shadow pen used to draw behind tyhe primary pen. - This pen must have a larger width than the primary + This pen must have a larger width than the primary pen to be visible. """ self.opts['shadowPen'] = fn.mkPen(*args, **kargs) @@ -283,7 +284,7 @@ class PlotCurveItem(GraphicsObject): self.opts['brush'] = fn.mkBrush(*args, **kargs) self.invalidateBounds() self.update() - + def setFillLevel(self, level): """Set the level filled to when filling under the curve""" self.opts['fillLevel'] = level @@ -295,11 +296,11 @@ class PlotCurveItem(GraphicsObject): """ =============== ======================================================== **Arguments:** - x, y (numpy arrays) Data to show + x, y (numpy arrays) Data to show pen Pen to use when drawing. Any single argument accepted by :func:`mkPen ` is allowed. shadowPen Pen for drawing behind the primary pen. Usually this - is used to emphasize the curve by providing a + is used to emphasize the curve by providing a high-contrast border. Any single argument accepted by :func:`mkPen ` is allowed. fillLevel (float or None) Fill the area 'under' the curve to @@ -317,18 +318,18 @@ class PlotCurveItem(GraphicsObject): to be drawn. "finite" causes segments to be omitted if they are attached to nan or inf values. For any other connectivity, specify an array of boolean values. - compositionMode See :func:`setCompositionMode + compositionMode See :func:`setCompositionMode `. =============== ======================================================== - + If non-keyword arguments are used, they will be interpreted as setData(y) for a single argument and setData(x, y) for two arguments. - - + + """ self.updateData(*args, **kargs) - + def updateData(self, *args, **kargs): profiler = debug.Profiler() @@ -340,12 +341,12 @@ class PlotCurveItem(GraphicsObject): elif len(args) == 2: kargs['x'] = args[0] kargs['y'] = args[1] - + if 'y' not in kargs or kargs['y'] is None: kargs['y'] = np.array([]) if 'x' not in kargs or kargs['x'] is None: kargs['x'] = np.arange(len(kargs['y'])) - + for k in ['x', 'y']: data = kargs[k] if isinstance(data, list): @@ -355,9 +356,9 @@ class PlotCurveItem(GraphicsObject): raise Exception("Plot data must be 1D ndarray.") if 'complex' in str(data.dtype): raise Exception("Can not plot complex data types.") - + profiler("data checks") - + #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly ## Test this bug with test_PlotWidget and zoom in on the animated plot self.invalidateBounds() @@ -365,24 +366,24 @@ class PlotCurveItem(GraphicsObject): self.informViewBoundsChanged() self.yData = kargs['y'].view(np.ndarray) self.xData = kargs['x'].view(np.ndarray) - + profiler('copy') - + if 'stepMode' in kargs: self.opts['stepMode'] = kargs['stepMode'] - + if self.opts['stepMode'] is True: if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape)) else: if self.xData.shape != self.yData.shape: ## allow difference of 1 for step mode plots raise Exception("X and Y arrays must be the same shape--got %s and %s." % (self.xData.shape, self.yData.shape)) - + self.path = None self.fillPath = None self._mouseShape = None #self.xDisp = self.yDisp = None - + if 'name' in kargs: self.opts['name'] = kargs['name'] if 'connect' in kargs: @@ -397,14 +398,14 @@ class PlotCurveItem(GraphicsObject): self.setBrush(kargs['brush']) if 'antialias' in kargs: self.opts['antialias'] = kargs['antialias'] - - + + profiler('set') self.update() profiler('update') self.sigPlotChanged.emit(self) profiler('emit') - + def generatePath(self, x, y): if self.opts['stepMode']: ## each value in the x/y arrays generates 2 points. @@ -423,9 +424,9 @@ class PlotCurveItem(GraphicsObject): y = y2.reshape(y2.size)[1:-1] y[0] = self.opts['fillLevel'] y[-1] = self.opts['fillLevel'] - + path = fn.arrayToQPath(x, y, connect=self.opts['connect']) - + return path @@ -438,7 +439,7 @@ class PlotCurveItem(GraphicsObject): self.path = self.generatePath(*self.getData()) self.fillPath = None self._mouseShape = None - + return self.path @debug.warnOnException ## raising an exception here causes crash @@ -446,27 +447,27 @@ class PlotCurveItem(GraphicsObject): profiler = debug.Profiler() if self.xData is None or len(self.xData) == 0: return - + if HAVE_OPENGL and getConfigOption('enableExperimental') and isinstance(widget, QtOpenGL.QGLWidget): self.paintGL(p, opt, widget) return - + x = None y = None path = self.getPath() profiler('generate path') - + if self._exportOpts is not False: aa = self._exportOpts.get('antialias', True) else: aa = self.opts['antialias'] - + p.setRenderHint(p.Antialiasing, aa) - + cmode = self.opts['compositionMode'] if cmode is not None: p.setCompositionMode(cmode) - + if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: if self.fillPath is None: if x is None: @@ -477,14 +478,14 @@ class PlotCurveItem(GraphicsObject): p2.lineTo(x[0], y[0]) p2.closeSubpath() self.fillPath = p2 - + profiler('generate fill path') p.fillPath(self.fillPath, self.opts['brush']) profiler('draw fill path') - - sp = fn.mkPen(self.opts['shadowPen']) - cp = fn.mkPen(self.opts['pen']) - + + sp = self.opts['shadowPen'] + cp = self.opts['pen'] + ## Copy pens and apply alpha adjustment #sp = QtGui.QPen(self.opts['shadowPen']) #cp = QtGui.QPen(self.opts['pen']) @@ -495,9 +496,7 @@ class PlotCurveItem(GraphicsObject): #c.setAlpha(c.alpha() * self.opts['alphaHint']) #pen.setColor(c) ##pen.setCosmetic(True) - - - + if sp is not None and sp.style() != QtCore.Qt.NoPen: p.setPen(sp) p.drawPath(path) @@ -507,29 +506,29 @@ class PlotCurveItem(GraphicsObject): else: p.drawPath(path) profiler('drawPath') - + #print "Render hints:", int(p.renderHints()) #p.setPen(QtGui.QPen(QtGui.QColor(255,0,0))) #p.drawRect(self.boundingRect()) - + def paintGL(self, p, opt, widget): p.beginNativePainting() import OpenGL.GL as gl - + ## set clipping viewport view = self.getViewBox() if view is not None: rect = view.mapRectToItem(self, view.boundingRect()) #gl.glViewport(int(rect.x()), int(rect.y()), int(rect.width()), int(rect.height())) - + #gl.glTranslate(-rect.x(), -rect.y(), 0) - + gl.glEnable(gl.GL_STENCIL_TEST) gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE) # disable drawing to frame buffer gl.glDepthMask(gl.GL_FALSE) # disable drawing to depth buffer - gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF) - gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP) - + gl.glStencilFunc(gl.GL_NEVER, 1, 0xFF) + gl.glStencilOp(gl.GL_REPLACE, gl.GL_KEEP, gl.GL_KEEP) + ## draw stencil pattern gl.glStencilMask(0xFF) gl.glClear(gl.GL_STENCIL_BUFFER_BIT) @@ -541,12 +540,12 @@ class PlotCurveItem(GraphicsObject): gl.glVertex2f(rect.x()+rect.width(), rect.y()) gl.glVertex2f(rect.x(), rect.y()+rect.height()) gl.glEnd() - + gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE) gl.glDepthMask(gl.GL_TRUE) gl.glStencilMask(0x00) gl.glStencilFunc(gl.GL_EQUAL, 1, 0xFF) - + try: x, y = self.getData() pos = np.empty((len(x), 2)) @@ -571,7 +570,7 @@ class PlotCurveItem(GraphicsObject): gl.glDisableClientState(gl.GL_VERTEX_ARRAY) finally: p.endNativePainting() - + def clear(self): self.xData = None ## raw values self.yData = None @@ -587,7 +586,7 @@ class PlotCurveItem(GraphicsObject): def mouseShape(self): """ Return a QPainterPath representing the clickable shape of the curve - + """ if self._mouseShape is None: view = self.getViewBox() @@ -600,14 +599,14 @@ class PlotCurveItem(GraphicsObject): mousePath = stroker.createStroke(path) self._mouseShape = self.mapFromItem(view, mousePath) return self._mouseShape - + def mouseClickEvent(self, ev): if not self.clickable or ev.button() != QtCore.Qt.LeftButton: return if self.mouseShape().contains(ev.pos()): ev.accept() self.sigClicked.emit(self) - + class ROIPlotItem(PlotCurveItem): @@ -622,7 +621,7 @@ class ROIPlotItem(PlotCurveItem): #roi.connect(roi, QtCore.SIGNAL('regionChanged'), self.roiChangedEvent) roi.sigRegionChanged.connect(self.roiChangedEvent) #self.roiChangedEvent() - + def getRoiData(self): d = self.roi.getArrayRegion(self.roiData, self.roiImg, axes=self.axes) if d is None: @@ -630,7 +629,7 @@ class ROIPlotItem(PlotCurveItem): while d.ndim > 1: d = d.mean(axis=1) return d - + def roiChangedEvent(self): d = self.getRoiData() self.updateData(d, self.xVals) From 0ba07300e125d3ead890aad3528c3244397fcca6 Mon Sep 17 00:00:00 2001 From: SamSchott Date: Mon, 24 Jun 2019 02:10:35 +0100 Subject: [PATCH 400/607] `_updateMaxTextSize` to reduce text size when no longer needed (#838) Currently `_updateMaxTextSize ` will increase the current space required for axis labels, if necessary, but not decrease it when the extra space is no longer needed. The proposed change will release no longer needed space again. --- pyqtgraph/graphicsItems/AxisItem.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index b34052ae..4bd77a65 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -301,18 +301,16 @@ class AxisItem(GraphicsWidget): def _updateMaxTextSize(self, x): ## Informs that the maximum tick size orthogonal to the axis has ## changed; we use this to decide whether the item needs to be resized - ## to accomodate. + ## to accommodate. if self.orientation in ['left', 'right']: - mx = max(self.textWidth, x) - if mx > self.textWidth or mx < self.textWidth-10: - self.textWidth = mx + if x > self.textWidth or x < self.textWidth-10: + self.textWidth = x if self.style['autoExpandTextSpace'] is True: self._updateWidth() #return True ## size has changed else: - mx = max(self.textHeight, x) - if mx > self.textHeight or mx < self.textHeight-10: - self.textHeight = mx + if x > self.textHeight or x < self.textHeight-10: + self.textHeight = x if self.style['autoExpandTextSpace'] is True: self._updateHeight() #return True ## size has changed From 053fca6e831de8afc4d69b516719738b80b82a35 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sun, 23 Jun 2019 21:41:20 -0700 Subject: [PATCH 401/607] Revert "`_updateMaxTextSize` to reduce text size when no longer needed (#838)" (#957) This reverts commit 0ba07300e125d3ead890aad3528c3244397fcca6. --- pyqtgraph/graphicsItems/AxisItem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 4bd77a65..b34052ae 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -301,16 +301,18 @@ class AxisItem(GraphicsWidget): def _updateMaxTextSize(self, x): ## Informs that the maximum tick size orthogonal to the axis has ## changed; we use this to decide whether the item needs to be resized - ## to accommodate. + ## to accomodate. if self.orientation in ['left', 'right']: - if x > self.textWidth or x < self.textWidth-10: - self.textWidth = x + mx = max(self.textWidth, x) + if mx > self.textWidth or mx < self.textWidth-10: + self.textWidth = mx if self.style['autoExpandTextSpace'] is True: self._updateWidth() #return True ## size has changed else: - if x > self.textHeight or x < self.textHeight-10: - self.textHeight = x + mx = max(self.textHeight, x) + if mx > self.textHeight or mx < self.textHeight-10: + self.textHeight = mx if self.style['autoExpandTextSpace'] is True: self._updateHeight() #return True ## size has changed From 7506ee3d3f8156f6c77184cceffd774dea2d216f Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sun, 23 Jun 2019 23:03:51 -0700 Subject: [PATCH 402/607] Add mesa drivers to windows CI images and show openGL info during debug stage (#954) Add mesa drivers to Windows CI Image --- azure-test-template.yml | 27 +++++++++++++++++++++++---- pyqtgraph/opengl/glInfo.py | 4 ++-- pytest.ini | 1 + 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 8a68317b..496ec10b 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -43,6 +43,23 @@ jobs: height: '1080' condition: eq(variables['agent.os'], 'Windows_NT' ) + - script: | + curl -LJO https://github.com/pal1000/mesa-dist-win/releases/download/19.1.0/mesa3d-19.1.0-release-msvc.exe + 7z x mesa3d-19.1.0-release-msvc.exe + cd x64 + xcopy opengl32.dll C:\windows\system32\mesadrv.dll* + xcopy opengl32.dll C:\windows\syswow64\mesadrv.dll* + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DLL /t REG_SZ /d "mesadrv.dll" /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v DriverVersion /t REG_DWORD /d 1 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Flags /t REG_DWORD /d 1 /f + REG ADD "HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\OpenGLDrivers\MSOGL" /v Version /t REG_DWORD /d 2 /f + displayName: "Install Windows-Mesa OpenGL DLL" + condition: eq(variables['agent.os'], 'Windows_NT') + - task: UsePythonVersion@0 inputs: versionSpec: $(python.version) @@ -76,7 +93,7 @@ jobs: if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) - conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes + conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes --quiet else pip install $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage fi @@ -114,9 +131,9 @@ jobs: source activate test-environment-$(python.version) fi pip install pytest-xvfb - displayName: "Linux Virtual Display Setup" + displayName: "Virtual Display Setup" condition: eq(variables['agent.os'], 'Linux' ) - + - bash: | if [ $(install.method) == "conda" ] then @@ -133,10 +150,12 @@ jobs: if [ $(agent.os) == 'Linux' ] then export DISPLAY=:99.0 - Xvfb :99 -screen 0 1920x1080x24 & + Xvfb :99 -screen 0 1920x1200x24 -ac +extension GLX +render -noreset & sleep 3 fi python -m pyqtgraph.util.get_resolution + echo openGL information + python -c "from pyqtgraph.opengl.glInfo import GLTest" displayName: 'Debug Info' continueOnError: false diff --git a/pyqtgraph/opengl/glInfo.py b/pyqtgraph/opengl/glInfo.py index 84346d81..0c3e758a 100644 --- a/pyqtgraph/opengl/glInfo.py +++ b/pyqtgraph/opengl/glInfo.py @@ -6,10 +6,10 @@ class GLTest(QtOpenGL.QGLWidget): def __init__(self): QtOpenGL.QGLWidget.__init__(self) self.makeCurrent() - print("GL version:" + glGetString(GL_VERSION)) + print("GL version:" + glGetString(GL_VERSION).decode("utf-8")) print("MAX_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_TEXTURE_SIZE)) print("MAX_3D_TEXTURE_SIZE: %d" % glGetIntegerv(GL_MAX_3D_TEXTURE_SIZE)) - print("Extensions: " + glGetString(GL_EXTENSIONS)) + print("Extensions: " + glGetString(GL_EXTENSIONS).decode("utf-8").replace(" ", "\n")) GLTest() diff --git a/pytest.ini b/pytest.ini index 7d27b7a2..fa664793 100644 --- a/pytest.ini +++ b/pytest.ini @@ -3,6 +3,7 @@ xvfb_width = 1920 xvfb_height = 1080 # use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 +xvfb_args=-ac +extension GLX +render addopts = --faulthandler-timeout=15 filterwarnings = From f23889d5940b5b9be6c76be8c3e78e80689e223d Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 24 Jun 2019 13:53:00 +0100 Subject: [PATCH 403/607] only use `np.searchsorted` quicker method fails --- pyqtgraph/graphicsItems/PlotDataItem.py | 26 ++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 69d7dc6e..7c7c2fba 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -543,13 +543,25 @@ class PlotDataItem(GraphicsObject): # this option presumes that x-values are in increasing order range = self.viewRect() if range is not None and len(x) > 1: - # clip to visible region extended by downsampling value - idx = np.searchsorted(x, [range.left(), range.right()]) - idx = idx + np.array([-2*ds, 2*ds]) - idx = np.clip(idx, a_min=0, a_max=len(x)) - - x = x[idx[0]:idx[1]] - y = y[idx[0]:idx[1]] + # clip to visible region extended by downsampling value, assuming + # uniform spacing of x-values, has O(1) performance + dx = float(x[-1]-x[0]) / (len(x)-1) + x0 = np.clip(int((range.left()-x[0])/dx) - 1*ds, 0, len(x)-1) + x1 = np.clip(int((range.right()-x[0])/dx) + 2*ds, 0, len(x)-1) + + # if data has been clipped too strongly (in case of non-uniform + # spacing of x-values), refine the clipping region as required + # worst case performance: O(log(n)) + # best case performance: O(1) + if x[x0] > range.left(): + x0 = np.searchsorted(x, range.left()) - 1*ds + x0 = np.clip(x0, a_min=0, a_max=len(x)) + if x[x1] < range.right(): + x1 = np.searchsorted(x, range.right()) + 2*ds + x1 = np.clip(x1, a_min=0, a_max=len(x)) + + x = x[x0:x1] + y = y[x0:x1] if ds > 1: if self.opts['downsampleMethod'] == 'subsample': From b491f82006ddfb44551327da9308ad15cb86674f Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Mon, 25 Apr 2016 09:14:12 +0300 Subject: [PATCH 404/607] Bugfix: ViewBox border drawing - Fixed border overlapping (issue #316) - Added new method ViewBox.setBorder to complete API --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 33 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 27fb8268..45722e79 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -14,6 +14,7 @@ from ...Qt import isQObjectAlive __all__ = ['ViewBox'] + class WeakList(object): def __init__(self): @@ -34,10 +35,12 @@ class WeakList(object): yield d i -= 1 + class ChildGroup(ItemGroup): def __init__(self, parent): ItemGroup.__init__(self, parent) + self.setFlag(self.ItemClipsChildrenToShape) # Used as callback to inform ViewBox when items are added/removed from # the group. @@ -64,6 +67,12 @@ class ChildGroup(ItemGroup): listener.itemsChanged() return ret + def shape(self): + return self.mapFromParent(self.parentItem().shape()) + + def boundingRect(self): + return self.mapRectFromParent(self.parentItem().boundingRect()) + class ViewBox(GraphicsWidget): """ @@ -185,6 +194,11 @@ class ViewBox(GraphicsWidget): self.background.setPen(fn.mkPen(None)) self.updateBackground() + self.borderRect = QtGui.QGraphicsRectItem(self.rect()) + self.borderRect.setParentItem(self) + self.borderRect.setZValue(1e3) + self.borderRect.setPen(self.border) + ## Make scale box that is shown when dragging on the view self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) @@ -428,8 +442,10 @@ class ViewBox(GraphicsWidget): self.updateViewRange() self._matrixNeedsUpdate = True self.background.setRect(self.rect()) + self.borderRect.setRect(self.rect()) self.sigStateChanged.emit(self) self.sigResized.emit(self) + self.childGroup.prepareGeometryChange() def viewRange(self): """Return a the view's visible range as a list: [[xmin, xmax], [ymin, ymax]]""" @@ -571,7 +587,7 @@ class ViewBox(GraphicsWidget): self._autoRangeNeedsUpdate = True elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): self._autoRangeNeedsUpdate = True - + self.sigStateChanged.emit(self) def setYRange(self, min, max, padding=None, update=True): @@ -1054,6 +1070,19 @@ class ViewBox(GraphicsWidget): def xInverted(self): return self.state['xInverted'] + def setBorder(self, *args, **kwds): + """ + Set the pen used to draw border around the view + + If border is None, then no border will be drawn. + + Added in version 0.9.10 + + See :func:`mkPen ` for arguments. + """ + self.border = fn.mkPen(*args, **kwds) + self.borderRect.setPen(self.border) + def setAspectLocked(self, lock=True, ratio=1): """ If the aspect ratio is locked, view scaling must always preserve the aspect ratio. @@ -1499,7 +1528,7 @@ class ViewBox(GraphicsWidget): if any(changed): self._matrixNeedsUpdate = True self.update() - + # Inform linked views that the range has changed for ax in [0, 1]: if not changed[ax]: From 001390c160a71ad0d4f98752c057c506010bfc92 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Mon, 25 Apr 2016 11:35:24 +0300 Subject: [PATCH 405/607] Bugfix: GraphicsLayout border drawing - Fixed border overlapping (issue #316) --- pyqtgraph/graphicsItems/GraphicsLayout.py | 48 ++++++++++++++++------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index 6ec38fb5..c0db5890 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -23,6 +23,7 @@ class GraphicsLayout(GraphicsWidget): self.setLayout(self.layout) self.items = {} ## item: [(row, col), (row, col), ...] lists all cells occupied by the item self.rows = {} ## row: {col1: item1, col2: item2, ...} maps cell location to item + self.itemBorders = {} ## {item1: QtGui.QGraphicsRectItem, ...} border rects self.currentRow = 0 self.currentCol = 0 self.setSizePolicy(QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)) @@ -39,8 +40,10 @@ class GraphicsLayout(GraphicsWidget): See :func:`mkPen ` for arguments. """ self.border = fn.mkPen(*args, **kwds) - self.update() - + + for borderRect in self.itemBorders.values(): + borderRect.setPen(self.border) + def nextRow(self): """Advance to next row for automatic item placement""" self.currentRow += 1 @@ -119,7 +122,17 @@ class GraphicsLayout(GraphicsWidget): self.rows[row2] = {} self.rows[row2][col2] = item self.items[item].append((row2, col2)) - + + borderRect = QtGui.QGraphicsRectItem() + + borderRect.setParentItem(self) + borderRect.setZValue(1e3) + borderRect.setPen(fn.mkPen(self.border)) + + self.itemBorders[item] = borderRect + + item.geometryChanged.connect(self._updateItemBorder) + self.layout.addItem(item, row, col, rowspan, colspan) self.nextColumn() @@ -129,15 +142,7 @@ class GraphicsLayout(GraphicsWidget): def boundingRect(self): return self.rect() - - def paint(self, p, *args): - if self.border is None: - return - p.setPen(fn.mkPen(self.border)) - for i in self.items: - r = i.mapRectToParent(i.boundingRect()) - p.drawRect(r) - + def itemIndex(self, item): for i in range(self.layout.count()): if self.layout.itemAt(i).graphicsItem() is item: @@ -150,13 +155,16 @@ class GraphicsLayout(GraphicsWidget): self.layout.removeAt(ind) self.scene().removeItem(item) - for r,c in self.items[item]: + for r, c in self.items[item]: del self.rows[r][c] del self.items[item] + + item.geometryChanged.disconnect(self._updateItemBorder) + del self.itemBorders[item] + self.update() def clear(self): - items = [] for i in list(self.items.keys()): self.removeItem(i) @@ -168,4 +176,14 @@ class GraphicsLayout(GraphicsWidget): def setSpacing(self, *args): self.layout.setSpacing(*args) - \ No newline at end of file + + def _updateItemBorder(self): + if self.border is None: + return + + item = self.sender() + if item is None: + return + + r = item.mapRectToParent(item.boundingRect()) + self.itemBorders[item].setRect(r) From 1955ae024c66c5ac3407bd3eb94812a945fa0b83 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Mon, 24 Jun 2019 16:36:23 +0300 Subject: [PATCH 406/607] Fix: object has no attribute 'border' --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 45722e79..a542e916 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -194,6 +194,8 @@ class ViewBox(GraphicsWidget): self.background.setPen(fn.mkPen(None)) self.updateBackground() + self.border = fn.mkPen(border) + self.borderRect = QtGui.QGraphicsRectItem(self.rect()) self.borderRect.setParentItem(self) self.borderRect.setZValue(1e3) @@ -221,7 +223,6 @@ class ViewBox(GraphicsWidget): self.setAspectLocked(lockAspect) - self.border = fn.mkPen(border) if enableMenu: self.menu = ViewBoxMenu(self) else: From d870a34359682251dc22079161b003b5ad060c77 Mon Sep 17 00:00:00 2001 From: Sam Schott Date: Mon, 24 Jun 2019 14:49:32 +0100 Subject: [PATCH 407/607] add a test for clipping --- .../graphicsItems/tests/test_PlotDataItem.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index b506a654..adc525d9 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -64,3 +64,25 @@ def test_clear_in_step_mode(): c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode=True) w.addItem(c) c.clear() + +def test_clipping(): + y = np.random.normal(size=150) + x = np.exp2(np.linspace(5, 10, 150)) # non-uniform spacing + + w = pg.PlotWidget(autoRange=True, downsample=5) + c = pg.PlotDataItem(x, y) + w.addItem(c) + w.show() + + c.setClipToView(True) + + w.setXRange(200, 600) + + for x_min in range(100, 2**10 - 100, 100): + w.setXRange(x_min, x_min + 100) + + xDisp, _ = c.getData() + vr = c.viewRect() + + assert xDisp[0] <= vr.left() + assert xDisp[-1] >= vr.right() From c210795cac7efbac661c958ddef6e7d177dfa6d1 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 23 Jun 2019 15:24:36 -0700 Subject: [PATCH 408/607] Add test for exit function --- pyqtgraph/tests/test_exit_crash.py | 38 +++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py index de457d54..7c472104 100644 --- a/pyqtgraph/tests/test_exit_crash.py +++ b/pyqtgraph/tests/test_exit_crash.py @@ -1,7 +1,12 @@ -import os, sys, subprocess, tempfile +import os +import sys +import subprocess +import tempfile import pyqtgraph as pg import six import pytest +import textwrap +import time code = """ import sys @@ -14,6 +19,25 @@ w = pg.{classname}({args}) skipmessage = ('unclear why this test is failing. skipping until someone has' ' time to fix it') + +def call_with_timeout(*args, **kwargs): + """Mimic subprocess.call with timeout for python < 3.3""" + wait_per_poll = 0.1 + try: + timeout = kwargs.pop('timeout') + except KeyError: + timeout = 10 + + rc = None + p = subprocess.Popen(*args, **kwargs) + for i in range(int(timeout/wait_per_poll)): + rc = p.poll() + if rc is not None: + break + time.sleep(wait_per_poll) + return rc + + @pytest.mark.skipif(True, reason=skipmessage) def test_exit_crash(): # For each Widget subclass, run a simple python script that creates an @@ -40,3 +64,15 @@ def test_exit_crash(): assert proc.wait() == 0 os.remove(tmp) + + +def test_pg_exit(): + # test the pg.exit() function + code = textwrap.dedent(""" + import pyqtgraph as pg + app = pg.mkQApp() + pg.plot() + pg.exit() + """) + rc = call_with_timeout([sys.executable, '-c', code], timeout=5) + assert rc == 0 From 8067ee25d5b97ed0a093e2d8e4ff1d2cc01ec2d4 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 24 Jun 2019 15:39:12 -0700 Subject: [PATCH 409/607] Work around PySide setOverrideCursor bug in BusyCursor --- pyqtgraph/widgets/BusyCursor.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/BusyCursor.py b/pyqtgraph/widgets/BusyCursor.py index e7a26810..f6bbc84c 100644 --- a/pyqtgraph/widgets/BusyCursor.py +++ b/pyqtgraph/widgets/BusyCursor.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui, QtCore +from ..Qt import QtGui, QtCore, QT_LIB __all__ = ['BusyCursor'] @@ -17,7 +17,12 @@ class BusyCursor(object): app = QtCore.QCoreApplication.instance() isGuiThread = (app is not None) and (QtCore.QThread.currentThread() == app.thread()) if isGuiThread and QtGui.QApplication.instance() is not None: - QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) + if QT_LIB == 'PySide': + # pass CursorShape rather than QCursor for PySide + # see https://bugreports.qt.io/browse/PYSIDE-243 + QtGui.QApplication.setOverrideCursor(QtCore.Qt.WaitCursor) + else: + QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) BusyCursor.active.append(self) self._active = True else: @@ -27,4 +32,3 @@ class BusyCursor(object): if self._active: BusyCursor.active.pop(-1) QtGui.QApplication.restoreOverrideCursor() - \ No newline at end of file From 55d9e8888b77c50a1ba568c9dd3153f5f288976f Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 25 Jun 2019 16:08:18 -0700 Subject: [PATCH 410/607] Allow last image in stack to be selected by slider in ImageView --- pyqtgraph/imageview/ImageView.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 81463b7a..512d503b 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -740,7 +740,7 @@ class ImageView(QtGui.QWidget): return (0,0) t = slider.value() - + xv = self.tVals if xv is None: ind = int(t) @@ -748,7 +748,7 @@ class ImageView(QtGui.QWidget): if len(xv) < 2: return (0,0) totTime = xv[-1] + (xv[-1]-xv[-2]) - inds = np.argwhere(xv < t) + inds = np.argwhere(xv <= t) if len(inds) < 1: return (0,t) ind = inds[-1,0] From a04d31731d65556d6e8372f374118822dbd04d0f Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 25 Jun 2019 22:06:36 -0700 Subject: [PATCH 411/607] Preventing conda from updating --- azure-test-template.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 496ec10b..c2b6e58c 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -88,14 +88,15 @@ jobs: inputs: environmentName: 'test-environment-$(python.version)' packageSpecs: 'python=$(python.version)' + updateConda: false - bash: | if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) - conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage --yes --quiet + conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest six coverage --yes --quiet else - pip install $(qt.bindings) numpy scipy pyopengl pytest flake8 six coverage + pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage fi pip install pytest-xdist pytest-cov pytest-faulthandler displayName: "Install Dependencies" From fe637512b52ec8b2f1849302c471de7c42d099c8 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 25 Jun 2019 23:05:38 -0700 Subject: [PATCH 412/607] Skip GL* examples if on macOS with python2 and qt4 bindings, and update readme --- README.md | 4 +++- examples/test_examples.py | 11 +++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e5b3a9c7..28143078 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Requirements * `numpy`, `scipy` * Optional * `pyopengl` for 3D graphics + * macOS with Python2 and Qt4 bindings (PyQt4 or PySide) do not work with 3D OpenGL graphics * `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy` * `hdf5` for large hdf5 binary format support * Known to run on Windows, Linux, and macOS. @@ -41,7 +42,8 @@ Below is a table of the configurations we test and have confidence pyqtgraph wil | 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | | 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible. +* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible +* on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work Support ------- diff --git a/examples/test_examples.py b/examples/test_examples.py index 0856b4ff..bb4682f1 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -56,10 +56,17 @@ exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) conditionalExampleTests = { "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing"), "RemoteSpeedTest.py": exceptionCondition(False, reason="Test is being problematic on CI machines"), - "optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671") + "optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671"), + 'GLVolumeItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLIsosurface.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLSurfacePlot.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLScatterPlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLshaders.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLLinePlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLMeshItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), + 'GLImageItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939") } - @pytest.mark.parametrize( "frontend, f", [ From 26963ffbc4744559e409385ce922e46f4261fd04 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 24 Jun 2019 16:17:48 -0700 Subject: [PATCH 413/607] Fix pg.exit test in case pyqtgraph is not installed --- .travis.yml | 15 ++++++++------- pyqtgraph/tests/test_exit_crash.py | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0da455d8..e44739c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,7 +61,7 @@ install: - if [ "${QT}" == "pyside" ]; then conda install pyside --yes; fi; - - pip install pytest-xdist # multi-thread py.test + - pip install pytest-xdist # multi-thread pytest - pip install pytest-cov # add coverage stats - pip install pytest-faulthandler # activate faulthandler @@ -132,9 +132,15 @@ script: # Check system info - python -c "import pyqtgraph as pg; pg.systemInfo()" + + # Check install works + - start_test "install test"; + python setup.py --quiet install; + check_output "install test"; + # Run unit tests - start_test "unit tests"; - PYTHONPATH=. py.test --cov pyqtgraph -sv; + PYTHONPATH=. pytest --cov pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd @@ -164,11 +170,6 @@ script: check_output "style check"; fi; - # Check install works - - start_test "install test"; - python setup.py --quiet install; - check_output "install test"; - # Check double-install fails # Note the bash -c is because travis strips off the ! otherwise. - start_test "double install test"; diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py index 7c472104..50924908 100644 --- a/pyqtgraph/tests/test_exit_crash.py +++ b/pyqtgraph/tests/test_exit_crash.py @@ -74,5 +74,5 @@ def test_pg_exit(): pg.plot() pg.exit() """) - rc = call_with_timeout([sys.executable, '-c', code], timeout=5) + rc = call_with_timeout([sys.executable, '-c', code], timeout=5, shell=False) assert rc == 0 From d9f0da5a7c8679095f2716fdc9cd6880855fc159 Mon Sep 17 00:00:00 2001 From: Jeffrey Nichols Date: Thu, 27 Jun 2019 14:45:37 -0400 Subject: [PATCH 414/607] Fix for AxisItem using old scale to create label --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index b34052ae..3a92e2b1 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -448,11 +448,11 @@ class AxisItem(GraphicsWidget): if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. scale = 1.0 prefix = '' + self.autoSIPrefixScale = scale self.setLabel(unitPrefix=prefix) else: - scale = 1.0 + self.autoSIPrefixScale = 1.0 - self.autoSIPrefixScale = scale self.picture = None self.update() From c37956b29a728e3b8552eb2fe4b229614f78c2f0 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Fri, 28 Jun 2019 01:51:20 +0200 Subject: [PATCH 415/607] Corrected documentation for heightColor shader --- pyqtgraph/opengl/shaders.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/shaders.py b/pyqtgraph/opengl/shaders.py index 8922cd21..7ada939c 100644 --- a/pyqtgraph/opengl/shaders.py +++ b/pyqtgraph/opengl/shaders.py @@ -140,9 +140,9 @@ def initShaders(): ## colors fragments by z-value. ## This is useful for coloring surface plots by height. ## This shader uses a uniform called "colorMap" to determine how to map the colors: - ## red = pow(z * colorMap[0] + colorMap[1], colorMap[2]) - ## green = pow(z * colorMap[3] + colorMap[4], colorMap[5]) - ## blue = pow(z * colorMap[6] + colorMap[7], colorMap[8]) + ## red = pow(colorMap[0]*(z + colorMap[1]), colorMap[2]) + ## green = pow(colorMap[3]*(z + colorMap[4]), colorMap[5]) + ## blue = pow(colorMap[6]*(z + colorMap[7]), colorMap[8]) ## (set the values like this: shader['uniformMap'] = array([...]) ShaderProgram('heightColor', [ VertexShader(""" From 23b4e174f073bb1d444ff8390641dc2c7c52e0a0 Mon Sep 17 00:00:00 2001 From: Billy SU Date: Fri, 28 Jun 2019 12:51:54 +0800 Subject: [PATCH 416/607] Add Dock test and remove outdated comments (#659) * Add test for Dock closable arg * Remove outdated and debug comments * Add test for hideTitle dock --- pyqtgraph/dockarea/Dock.py | 18 ------------------ pyqtgraph/dockarea/tests/test_dock.py | 12 ++++++++++++ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/pyqtgraph/dockarea/Dock.py b/pyqtgraph/dockarea/Dock.py index ddeb0c4a..a7234073 100644 --- a/pyqtgraph/dockarea/Dock.py +++ b/pyqtgraph/dockarea/Dock.py @@ -89,27 +89,15 @@ class Dock(QtGui.QWidget, DockDrop): The actual size will be determined by comparing this Dock's stretch value to the rest of the docks it shares space with. """ - #print "setStretch", self, x, y - #self._stretch = (x, y) if x is None: x = 0 if y is None: y = 0 - #policy = self.sizePolicy() - #policy.setHorizontalStretch(x) - #policy.setVerticalStretch(y) - #self.setSizePolicy(policy) self._stretch = (x, y) self.sigStretchChanged.emit() - #print "setStretch", self, x, y, self.stretch() def stretch(self): - #policy = self.sizePolicy() - #return policy.horizontalStretch(), policy.verticalStretch() return self._stretch - - #def stretch(self): - #return self._stretch def hideTitleBar(self): """ @@ -150,7 +138,6 @@ class Dock(QtGui.QWidget, DockDrop): By default ('auto'), the orientation is determined based on the aspect ratio of the Dock. """ - #print self.name(), "setOrientation", o, force if o == 'auto' and self.autoOrient: if self.container().type() == 'tab': o = 'horizontal' @@ -165,19 +152,16 @@ class Dock(QtGui.QWidget, DockDrop): def updateStyle(self): ## updates orientation and appearance of title bar - #print self.name(), "update style:", self.orientation, self.moveLabel, self.label.isVisible() if self.labelHidden: self.widgetArea.setStyleSheet(self.nStyle) elif self.orientation == 'vertical': self.label.setOrientation('vertical') if self.moveLabel: - #print self.name(), "reclaim label" self.topLayout.addWidget(self.label, 1, 0) self.widgetArea.setStyleSheet(self.vStyle) else: self.label.setOrientation('horizontal') if self.moveLabel: - #print self.name(), "reclaim label" self.topLayout.addWidget(self.label, 0, 1) self.widgetArea.setStyleSheet(self.hStyle) @@ -203,7 +187,6 @@ class Dock(QtGui.QWidget, DockDrop): def startDrag(self): self.drag = QtGui.QDrag(self) mime = QtCore.QMimeData() - #mime.setPlainText("asd") self.drag.setMimeData(mime) self.widgetArea.setStyleSheet(self.dragStyle) self.update() @@ -220,7 +203,6 @@ class Dock(QtGui.QWidget, DockDrop): if self._container is not None: # ask old container to close itself if it is no longer needed self._container.apoptose() - #print self.name(), "container changed" self._container = c if c is None: self.area = None diff --git a/pyqtgraph/dockarea/tests/test_dock.py b/pyqtgraph/dockarea/tests/test_dock.py index 949f3f0e..3fb47075 100644 --- a/pyqtgraph/dockarea/tests/test_dock.py +++ b/pyqtgraph/dockarea/tests/test_dock.py @@ -14,3 +14,15 @@ def test_dock(): assert dock.name() == name # no surprises in return type. assert type(dock.name()) == type(name) + +def test_closable_dock(): + name = "Test close dock" + dock = da.Dock(name=name, closable=True) + + assert dock.label.closeButton != None + +def test_hide_title_dock(): + name = "Test hide title dock" + dock = da.Dock(name=name, hideTitle=True) + + assert dock.labelHidden == True From caf7378f3860968fb7c8b6680aa299cb32cacfde Mon Sep 17 00:00:00 2001 From: Fernando Date: Fri, 28 Jun 2019 10:59:03 -0300 Subject: [PATCH 417/607] FIX: fix disconnect method of SignalProxy class. --- pyqtgraph/SignalProxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/SignalProxy.py b/pyqtgraph/SignalProxy.py index 7463dfc3..46b44887 100644 --- a/pyqtgraph/SignalProxy.py +++ b/pyqtgraph/SignalProxy.py @@ -81,7 +81,7 @@ class SignalProxy(QtCore.QObject): except: pass try: - self.sigDelayed.disconnect(self.slot()) + self.sigDelayed.disconnect(self.slot) except: pass From a4cecf4a222927aa9b989fb6bc7059a6ce9b28c8 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sun, 30 Jun 2019 17:50:11 +0200 Subject: [PATCH 418/607] Call parent __init__ as soon as possible for CtrlNode --- pyqtgraph/flowchart/library/common.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/flowchart/library/common.py b/pyqtgraph/flowchart/library/common.py index 8b3376c3..1f5613c9 100644 --- a/pyqtgraph/flowchart/library/common.py +++ b/pyqtgraph/flowchart/library/common.py @@ -91,14 +91,15 @@ class CtrlNode(Node): sigStateChanged = QtCore.Signal(object) def __init__(self, name, ui=None, terminals=None): + if terminals is None: + terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}} + Node.__init__(self, name=name, terminals=terminals) + if ui is None: if hasattr(self, 'uiTemplate'): ui = self.uiTemplate else: ui = [] - if terminals is None: - terminals = {'In': {'io': 'in'}, 'Out': {'io': 'out', 'bypass': 'In'}} - Node.__init__(self, name=name, terminals=terminals) self.ui, self.stateGroup, self.ctrls = generateUi(ui) self.stateGroup.sigChanged.connect(self.changed) From 98e66a855e88df9002de04c17ea99b990ef86bad Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 30 Jun 2019 12:05:57 -0700 Subject: [PATCH 419/607] Remove pytest-faulthandler from test dependencies --- .travis.yml | 1 - CONTRIBUTING.md | 1 - azure-test-template.yml | 4 ++-- pytest.ini | 3 +-- tox.ini | 1 - 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index e44739c4..c75f4523 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,7 +63,6 @@ install: fi; - pip install pytest-xdist # multi-thread pytest - pip install pytest-cov # add coverage stats - - pip install pytest-faulthandler # activate faulthandler # Debugging helpers - uname -a diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3ca5e0bf..3d27ad10 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -45,7 +45,6 @@ Please use the following guidelines when preparing changes: * pytest * pytest-cov * pytest-xdist -* pytest-faulthandler * Optional: pytest-xvfb ### Tox diff --git a/azure-test-template.yml b/azure-test-template.yml index c2b6e58c..d66c304c 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -98,7 +98,7 @@ jobs: else pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage fi - pip install pytest-xdist pytest-cov pytest-faulthandler + pip install pytest-xdist pytest-cov displayName: "Install Dependencies" - bash: | @@ -194,4 +194,4 @@ jobs: inputs: codeCoverageTool: Cobertura summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' - reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' \ No newline at end of file + reportDirectory: '$(System.DefaultWorkingDirectory)/**/htmlcov' diff --git a/pytest.ini b/pytest.ini index fa664793..1814592b 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,7 +4,6 @@ xvfb_height = 1080 # use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 xvfb_args=-ac +extension GLX +render -addopts = --faulthandler-timeout=15 filterwarnings = # comfortable skipping these warnings runtime warnings @@ -12,4 +11,4 @@ filterwarnings = ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning # Warnings generated from PyQt5.9 ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning - ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning \ No newline at end of file + ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning diff --git a/tox.ini b/tox.ini index 6bbb5566..9091c8cb 100644 --- a/tox.ini +++ b/tox.ini @@ -34,7 +34,6 @@ deps= {[base]deps} pytest-cov pytest-xdist - pytest-faulthandler pyside2-pip: pyside2 pyqt5-pip: pyqt5 From df0467961e2141f91bdead670a53cce1a398bde4 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 30 Jun 2019 13:30:58 -0700 Subject: [PATCH 420/607] Install pytest-faulthandler for py27 and add timeout --- .travis.yml | 7 ++++++- azure-test-template.yml | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index c75f4523..642181f7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -64,6 +64,11 @@ install: - pip install pytest-xdist # multi-thread pytest - pip install pytest-cov # add coverage stats + # faulthandler support not built in to pytest for python 2.7 + - if [ "${PYTHON}" == "2.7" ]; then + pip install pytest-faulthandler; + fi; + # Debugging helpers - uname -a - cat /etc/issue @@ -139,7 +144,7 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. pytest --cov pyqtgraph -sv; + PYTHONPATH=. pytest --cov pyqtgraph -sv -o faulthandler_timeout=15; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/azure-test-template.yml b/azure-test-template.yml index d66c304c..38b7d8b2 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -99,6 +99,10 @@ jobs: pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage fi pip install pytest-xdist pytest-cov + if [ $(python.version) == "2.7" ] + then + pip install pytest-faulthandler + fi displayName: "Install Dependencies" - bash: | @@ -170,7 +174,8 @@ jobs: # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" pytest . -sv \ --junitxml=junit/test-results.xml \ - -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html + -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html \ + -o faulthandler_timeout=15 displayName: 'Unit tests' env: AZURE: 1 From 2a0f866f7cffeaedb2a4120bb02285bbde1931c5 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 1 Jul 2019 09:21:40 -0700 Subject: [PATCH 421/607] Add timeout option back to ini and remove command line option --- .travis.yml | 2 +- CONTRIBUTING.md | 4 ++++ azure-test-template.yml | 3 +-- pytest.ini | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 642181f7..bec90488 100644 --- a/.travis.yml +++ b/.travis.yml @@ -144,7 +144,7 @@ script: # Run unit tests - start_test "unit tests"; - PYTHONPATH=. pytest --cov pyqtgraph -sv -o faulthandler_timeout=15; + PYTHONPATH=. pytest --cov pyqtgraph -sv; check_output "unit tests"; - echo "test script finished. Current directory:" - pwd diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d27ad10..d602b89b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,6 +47,10 @@ Please use the following guidelines when preparing changes: * pytest-xdist * Optional: pytest-xvfb +If you have pytest < 5, you may also want to install the pytest-faulthandler +plugin to output extra debugging information in case of test failures. This +isn't necessary with pytest 5+ as the plugin was merged into core pytest. + ### Tox As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make use of `tox` to test against most of the configurations in our test matrix. As some of the qt-bindings are only installable via `conda`, `conda` needs to be in your `PATH`, and we utilize the `tox-conda` plugin. diff --git a/azure-test-template.yml b/azure-test-template.yml index 38b7d8b2..f9d6e7ef 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -174,8 +174,7 @@ jobs: # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" pytest . -sv \ --junitxml=junit/test-results.xml \ - -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html \ - -o faulthandler_timeout=15 + -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' env: AZURE: 1 diff --git a/pytest.ini b/pytest.ini index 1814592b..f53aea00 100644 --- a/pytest.ini +++ b/pytest.ini @@ -4,6 +4,7 @@ xvfb_height = 1080 # use this due to some issues with ndarray reshape errors on CI systems xvfb_colordepth = 24 xvfb_args=-ac +extension GLX +render +faulthandler_timeout = 15 filterwarnings = # comfortable skipping these warnings runtime warnings From ac0e9dc99df33da1ac436ef602775df62086be96 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 1 Jul 2019 11:25:29 -0700 Subject: [PATCH 422/607] Add timeout for pytest<5 --- .travis.yml | 1 + azure-test-template.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index bec90488..173fa668 100644 --- a/.travis.yml +++ b/.travis.yml @@ -67,6 +67,7 @@ install: # faulthandler support not built in to pytest for python 2.7 - if [ "${PYTHON}" == "2.7" ]; then pip install pytest-faulthandler; + export PYTEST_ADDOPTS="--faulthandler-timeout=15"; fi; # Debugging helpers diff --git a/azure-test-template.yml b/azure-test-template.yml index f9d6e7ef..c1e6c1b0 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -102,6 +102,7 @@ jobs: if [ $(python.version) == "2.7" ] then pip install pytest-faulthandler + export PYTEST_ADDOPTS="--faulthandler-timeout=15" fi displayName: "Install Dependencies" From cff9cfa98d174aab906d60120a8d8c50602d7423 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 1 Jul 2019 19:00:18 -0700 Subject: [PATCH 423/607] Reduce test window size for OSX compatibility --- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index bb705c18..1d831b02 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -63,7 +63,7 @@ def test_ViewBox(): assertMapping(vb, view1, size1) # test tall resize - win.resize(400, 800) + win.resize(200, 400) app.processEvents() w = vb.geometry().width() h = vb.geometry().height() From 138fdd0af2362a5dfdbdb77db083b16b2a22765e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Tue, 2 Jul 2019 03:57:45 -0700 Subject: [PATCH 424/607] relax image comparison for failing windows test (#979) relax image comparison for failing windows test --- pyqtgraph/graphicsItems/tests/test_ROI.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 8cc2efd5..b22ad530 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -1,3 +1,4 @@ +import sys import numpy as np import pytest import pyqtgraph as pg @@ -133,7 +134,12 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) img2.setImage(rgn[0, ..., 0]) app.processEvents() - assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.') + # on windows, one edge of one ROI handle is shifted slightly; letting this slide with pxCount=10 + if sys.platform == 'win32' and pg.Qt.QT_LIB in ('PyQt4', 'PySide'): + pxCount = 10 + else: + pxCount=-1 + assertImageApproved(win, name+'/roi_getarrayregion_inverty', 'Simple ROI region selection, view inverted.', pxCount=pxCount) roi.setState(initState) img1.resetTransform() From 73c440a4db6cef93fa47e8aefd7d0f502e281e7e Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Tue, 2 Jul 2019 14:01:32 +0300 Subject: [PATCH 425/607] remove deprecated call to time.clock (#980) remove deprecated call to time.clock for python3 --- pyqtgraph/ptime.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/ptime.py b/pyqtgraph/ptime.py index 1de8282f..f86bdd88 100644 --- a/pyqtgraph/ptime.py +++ b/pyqtgraph/ptime.py @@ -7,22 +7,29 @@ Distributed under MIT/X11 license. See license.txt for more infomation. import sys -import time as systime + +if sys.version_info[0] < 3: + from time import clock + from time import time as system_time +else: + from time import perf_counter as clock + from time import time as system_time + START_TIME = None time = None def winTime(): """Return the current time in seconds with high precision (windows version, use Manager.time() to stay platform independent).""" - return systime.clock() + START_TIME + return clock() - START_TIME #return systime.time() def unixTime(): """Return the current time in seconds with high precision (unix version, use Manager.time() to stay platform independent).""" - return systime.time() + return system_time() if sys.platform.startswith('win'): - cstart = systime.clock() ### Required to start the clock in windows - START_TIME = systime.time() - cstart + cstart = clock() ### Required to start the clock in windows + START_TIME = system_time() - cstart time = winTime else: From b0bc5b8931b8430b82e7fab4114811176272a994 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Tue, 2 Jul 2019 16:26:51 +0200 Subject: [PATCH 426/607] Add issue template (#976) * Create issue template * Refined issue template Following suggestions by ixjlyons --- .github/ISSUE_TEMPLATE/bug_report.md | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..85ea5b79 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,44 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + + + + +### Short description + + +### Code to reproduce + +```python +import pyqtgraph as pg +import numpy as np +``` + +### Expected behavior + + +### Real behavior + + +``` +An error occurred? +Post the full traceback inside these 'code fences'! +``` + +### Tested environment(s) + + * PyQtGraph version: + * Qt Python binding: + * Python version: + * NumPy version: + * Operating system: + * Installation method: + +### Additional context From a38bdc06ded32ebd8c0581fe69bd609b3e575be3 Mon Sep 17 00:00:00 2001 From: "Daryl.Xu" Date: Wed, 10 Jul 2019 10:38:04 +0800 Subject: [PATCH 427/607] Fix typo --- doc/source/mouse_interaction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/mouse_interaction.rst b/doc/source/mouse_interaction.rst index 3aea2527..c1bec45d 100644 --- a/doc/source/mouse_interaction.rst +++ b/doc/source/mouse_interaction.rst @@ -10,7 +10,7 @@ Most applications that use pyqtgraph's data visualization will generate widgets In pyqtgraph, most 2D visualizations follow the following mouse interaction: * **Left button:** Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. -* **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. +* **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes visible in the scene, then right-dragging over the axis will _only_ affect that axis. * **Right button click:** Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. * **Middle button (or wheel) drag:** Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). * **Wheel spin:** Zooms the scene in and out. From e24d23af5c1d2a85b95f7f2c72aeb4a1a8035f70 Mon Sep 17 00:00:00 2001 From: "Daryl.Xu" Date: Wed, 10 Jul 2019 11:00:33 +0800 Subject: [PATCH 428/607] Fix typo --- doc/source/mouse_interaction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/mouse_interaction.rst b/doc/source/mouse_interaction.rst index 0e149f0c..71a76415 100644 --- a/doc/source/mouse_interaction.rst +++ b/doc/source/mouse_interaction.rst @@ -10,7 +10,7 @@ Most applications that use pyqtgraph's data visualization will generate widgets In pyqtgraph, most 2D visualizations follow the following mouse interaction: * Left button: Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. -* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. +* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes visible in the scene, then right-dragging over the axis will _only_ affect that axis. * Right button click: Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. * Middle button (or wheel) drag: Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). * Wheel spin: Zooms the scene in and out. From 6648db031e2d578fe40dcd60de14123d0f92b7e5 Mon Sep 17 00:00:00 2001 From: Kevin Newman <47572615+kevinanewman@users.noreply.github.com> Date: Wed, 17 Jul 2019 08:37:16 -0400 Subject: [PATCH 429/607] Update LegendItem.py Propose adding a clear() method (or equivalent) for easier legend re-use with dynamically updated plots... --- pyqtgraph/graphicsItems/LegendItem.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index efb700a1..ce5bd883 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -101,6 +101,15 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): label.close() self.updateSize() # redraq box + def clear(self): + """ + Removes all items from the legend. + + Useful for reusing and dynamically updating charts and their legends. + """ + while self.items != []: + self.removeItem(self.items[0][1].text) + def updateSize(self): if self.size is not None: return From a655e974ff13f6993ffbf21104af9604d342d8f6 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sat, 20 Jul 2019 20:33:11 +0200 Subject: [PATCH 430/607] Call multiprocess.connection.Connection.send_bytes with bytes --- pyqtgraph/multiprocess/remoteproxy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/multiprocess/remoteproxy.py b/pyqtgraph/multiprocess/remoteproxy.py index b1077674..f0d993cb 100644 --- a/pyqtgraph/multiprocess/remoteproxy.py +++ b/pyqtgraph/multiprocess/remoteproxy.py @@ -458,7 +458,7 @@ class RemoteEventHandler(object): ## follow up by sending byte messages if byteData is not None: for obj in byteData: ## Remote process _must_ be prepared to read the same number of byte messages! - self.conn.send_bytes(obj) + self.conn.send_bytes(bytes(obj)) self.debugMsg(' sent %d byte messages', len(byteData)) self.debugMsg(' call sync: %s', callSync) From 2312c5fbfc9ca48c882b3a86299eeb10f5ef3eae Mon Sep 17 00:00:00 2001 From: dschoni Date: Mon, 29 Jul 2019 17:50:38 +0200 Subject: [PATCH 431/607] Add all important and accepted PRs to changelog --- CHANGELOG | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2cb16918..4f2d4ff5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -56,6 +56,10 @@ pyqtgraph-0.11.0 (in development) - #683: Allow data filter entries to be updated after they are created - #685: Add option to set enum default values in DataFilterWidget - #710: Adds ability to rotate/scale ROIs by mouse drag on the ROI itself (using alt/shift modifiers) + - #813,814,817: Performance improvements + - #837: Added options for field variables in ColorMapWidget + - #840, 932: Improve clipping behavior + - #922: Curve fill for fill-patches API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because @@ -76,6 +80,8 @@ pyqtgraph-0.11.0 (in development) - #589: Remove SpiralROI (this was unintentionally added in the first case) - #593: Override qAbort on slot exceptions for PyQt>=5.5 - #657: When a floating Dock window is closed, the dock is now returned home + - #771: Suppress RuntimeWarning for arrays containing zeros in logscale + - #963: Last image in image-stack can now be selected with the z-slider Bugfixes: - #408: Fix `cleanup` when the running qt application is not a QApplication @@ -154,13 +160,34 @@ pyqtgraph-0.11.0 (in development) - #723: Fix axis ticks when using self.scale - #739: Fix handling of 2-axis mouse wheel events - #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX. + - #763: Fix OverflowError when using Auto Downsampling. + - #767: Fix Image display for images with the same value everywhere. + - #770: Fix GLVieWidget.setCameraPosition ignoring first parameter. + - #782: Fix missing FileForwarder thread termination. + - #815: Fixed mirroring of x-axis with "invert Axis" submenu. + - #824: Fix several issues related with mouse movement and GraphicsView. + - #832: Fix Permission error in tests due to unclosed filehandle. + - #836: Fix tickSpacing bug that lead to axis not being drawn. + - #861: Fix crash of PlotWidget if empty ErrorBarItem is added. + - #868: Fix segfault on repeated closing of matplotlib exporter. + - #875,876,887,934,947,980: Fix deprecation warnings. + - #886: Fix flowchart saving on python3. + - #888: Fix TreeWidget.topLevelItems in python3. + - #935: Fix PlotItem.addLine with 'pos' and 'angle' parameter. + - #949: Fix multiline parameters (such as arrays) reading from config files. + - #951: Fix event firing from scale handler. + - #952: Fix RotateFree handle dragging + - #968: Fix Si units in AxisItem leading to an incorrect unit. + - #971: Fix a segfault stemming from incorrect signal disconnection Maintenance: - Lots of new unit tests - Lots of code cleanup + - A lot of work on CI pipelines, test coverage and test passing (see e.g. #903,911) - #546: Add check for EINTR during example testing to avoid sporadic test failures on travis - #624: TravisCI no longer running python 2.6 tests - #695: "dev0" added to version string + - #865,873,877 (and more): Implement Azure CI pipelines, fix Travis CI pyqtgraph-0.10.0 From 18661cab0dfeba857a96a6eaf404f0caaddc0615 Mon Sep 17 00:00:00 2001 From: Axel Jacobsen Date: Tue, 6 Aug 2019 09:32:43 -0700 Subject: [PATCH 432/607] BarGraphItem CSV export and documentation --- doc/source/graphicsItems/bargraphitem.rst | 8 ++++++++ pyqtgraph/graphicsItems/BarGraphItem.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) create mode 100644 doc/source/graphicsItems/bargraphitem.rst diff --git a/doc/source/graphicsItems/bargraphitem.rst b/doc/source/graphicsItems/bargraphitem.rst new file mode 100644 index 00000000..4959385b --- /dev/null +++ b/doc/source/graphicsItems/bargraphitem.rst @@ -0,0 +1,8 @@ +BarGraphItem +============ + +.. autoclass:: pyqtgraph.BarGraphItem + :members: + + .. automethod:: pyqtgraph.BarGraphItem.__init__ + diff --git a/pyqtgraph/graphicsItems/BarGraphItem.py b/pyqtgraph/graphicsItems/BarGraphItem.py index 657222ba..4e820cb8 100644 --- a/pyqtgraph/graphicsItems/BarGraphItem.py +++ b/pyqtgraph/graphicsItems/BarGraphItem.py @@ -40,6 +40,7 @@ class BarGraphItem(GraphicsObject): y0=None, x1=None, y1=None, + name=None, height=None, width=None, pen=None, @@ -166,3 +167,15 @@ class BarGraphItem(GraphicsObject): if self.picture is None: self.drawPicture() return self._shape + + def implements(self, interface=None): + ints = ['plotData'] + if interface is None: + return ints + return interface in ints + + def name(self): + return self.opts.get('name', None) + + def getData(self): + return self.opts.get('x'), self.opts.get('height') From 27d94cae923d1371736a4b6ea650559cbf317d1f Mon Sep 17 00:00:00 2001 From: Axel Jacobsen Date: Tue, 6 Aug 2019 11:55:14 -0700 Subject: [PATCH 433/607] enforce utf-8 encoding for casting QByteArray to str remove print statements --- pyqtgraph/exporters/ImageExporter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index a43a3d88..69c02508 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -48,7 +48,7 @@ class ImageExporter(Exporter): def export(self, fileName=None, toBytes=False, copy=False): if fileName is None and not toBytes and not copy: if QT_LIB in ['PySide', 'PySide2']: - filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()] + filter = ["*."+str(f, encoding='utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] else: filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] preferred = ['*.png', '*.tif', '*.jpg'] @@ -105,7 +105,7 @@ class ImageExporter(Exporter): elif toBytes: return self.png else: - self.png.save(fileName) + return self.png.save(fileName) ImageExporter.register() From abd028436ae7b1c9d63f2c23366b26cb6bd0a55d Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Thu, 8 Aug 2019 16:50:29 +0200 Subject: [PATCH 434/607] Always convert PlotDataItem data to NumPy array --- pyqtgraph/graphicsItems/PlotDataItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 4bc9540f..bf119879 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -448,9 +448,9 @@ class PlotDataItem(GraphicsObject): if y is not None and x is None: x = np.arange(len(y)) - if isinstance(x, list): + if not isinstance(x, np.ndarray): x = np.array(x) - if isinstance(y, list): + if not isinstance(y, np.ndarray): y = np.array(y) self.xData = x.view(np.ndarray) ## one last check to make sure there are no MetaArrays getting by From 4e6629f3524ed80b8ead8f7adc307cf9fb95fc16 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Thu, 8 Aug 2019 17:39:11 +0200 Subject: [PATCH 435/607] Fix: ImageView sigTimeChanged was only emitted on mouse interaction --- pyqtgraph/imageview/ImageView.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 512d503b..2495d898 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -131,7 +131,7 @@ class ImageView(QtGui.QWidget): self.scene = self.ui.graphicsView.scene() self.ui.histogram.setLevelMode(levelMode) - self.ignoreTimeLine = False + self.ignorePlaying = False if view is None: self.view = ViewBox() @@ -498,11 +498,11 @@ class ImageView(QtGui.QWidget): def setCurrentIndex(self, ind): """Set the currently displayed frame index.""" - self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[self.axes['t']]-1) - self.updateImage() - self.ignoreTimeLine = True - self.timeLine.setValue(self.tVals[self.currentIndex]) - self.ignoreTimeLine = False + index = np.clip(ind, 0, self.getProcessedImage().shape[self.axes['t']]-1) + self.ignorePlaying = True + # Implicitly call timeLineChanged + self.timeLine.setValue(self.tVals[index]) + self.ignorePlaying = False def jumpFrames(self, n): """Move video frame ahead n frames (may be negative)""" @@ -696,16 +696,13 @@ class ImageView(QtGui.QWidget): return norm def timeLineChanged(self): - #(ind, time) = self.timeIndex(self.ui.timeSlider) - if self.ignoreTimeLine: - return - self.play(0) + if not self.ignorePlaying: + self.play(0) + (ind, time) = self.timeIndex(self.timeLine) if ind != self.currentIndex: self.currentIndex = ind self.updateImage() - #self.timeLine.setPos(time) - #self.emit(QtCore.SIGNAL('timeChanged'), ind, time) self.sigTimeChanged.emit(ind, time) def updateImage(self, autoHistogramRange=True): From 652ae9e64a5375b7d4e989bb379a6aeb686a299c Mon Sep 17 00:00:00 2001 From: 2xB <2xb@users.noreply.github.com> Date: Thu, 15 Aug 2019 02:41:51 +0200 Subject: [PATCH 436/607] Fix: GLScatterPlotItem and GLImageItem initializeGL only executed once --- pyqtgraph/opengl/items/GLImageItem.py | 3 +++ pyqtgraph/opengl/items/GLScatterPlotItem.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/pyqtgraph/opengl/items/GLImageItem.py b/pyqtgraph/opengl/items/GLImageItem.py index 59ddaf6f..d72448a1 100644 --- a/pyqtgraph/opengl/items/GLImageItem.py +++ b/pyqtgraph/opengl/items/GLImageItem.py @@ -29,8 +29,11 @@ class GLImageItem(GLGraphicsItem): GLGraphicsItem.__init__(self) self.setData(data) self.setGLOptions(glOptions) + self.texture = None def initializeGL(self): + if self.texture is not None: + return glEnable(GL_TEXTURE_2D) self.texture = glGenTextures(1) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index fe794d48..828b0f0c 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -20,6 +20,7 @@ class GLScatterPlotItem(GLGraphicsItem): self.pxMode = True #self.vbo = {} ## VBO does not appear to improve performance very much. self.setData(**kwds) + self.shader = None def setData(self, **kwds): """ @@ -54,6 +55,8 @@ class GLScatterPlotItem(GLGraphicsItem): self.update() def initializeGL(self): + if self.shader is not None: + return ## Generate texture for rendering points w = 64 From 8d2c16901b92813f8bebcd6d6cc988fd37bff9f3 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 05:16:01 +0200 Subject: [PATCH 437/607] Merge master into develop (#981) * Information is spelled with an r, even in comments --- pyqtgraph/Point.py | 2 +- pyqtgraph/Vector.py | 2 +- pyqtgraph/WidgetGroup.py | 2 +- pyqtgraph/configfile.py | 2 +- pyqtgraph/debug.py | 2 +- pyqtgraph/functions.py | 2 +- pyqtgraph/graphicsItems/MultiPlotItem.py | 2 +- pyqtgraph/graphicsItems/ROI.py | 2 +- pyqtgraph/imageview/ImageView.py | 2 +- pyqtgraph/metaarray/MetaArray.py | 2 +- pyqtgraph/pgcollections.py | 2 +- pyqtgraph/ptime.py | 2 +- pyqtgraph/widgets/GraphicsView.py | 2 +- pyqtgraph/widgets/MultiPlotWidget.py | 2 +- pyqtgraph/widgets/PlotWidget.py | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/pyqtgraph/Point.py b/pyqtgraph/Point.py index 3fb43cac..3b4dacf3 100644 --- a/pyqtgraph/Point.py +++ b/pyqtgraph/Point.py @@ -2,7 +2,7 @@ """ Point.py - Extension of QPointF which adds a few missing methods. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtCore diff --git a/pyqtgraph/Vector.py b/pyqtgraph/Vector.py index 0c980a61..f2166c45 100644 --- a/pyqtgraph/Vector.py +++ b/pyqtgraph/Vector.py @@ -2,7 +2,7 @@ """ Vector.py - Extension of QVector3D which adds a few missing methods. Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from .Qt import QtGui, QtCore, QT_LIB diff --git a/pyqtgraph/WidgetGroup.py b/pyqtgraph/WidgetGroup.py index 09c30854..2792aa98 100644 --- a/pyqtgraph/WidgetGroup.py +++ b/pyqtgraph/WidgetGroup.py @@ -2,7 +2,7 @@ """ WidgetGroup.py - WidgetGroup class for easily managing lots of Qt widgets Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. This class addresses the problem of having to save and restore the state of a large group of widgets. diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 275a4fdb..0cc8f030 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -2,7 +2,7 @@ """ configfile.py - Human-readable text configuration file library Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Used for reading and writing dictionary objects to a python-like configuration file format. Data structures may be nested and contain any data type as long diff --git a/pyqtgraph/debug.py b/pyqtgraph/debug.py index 3ddcae37..bc6d6895 100644 --- a/pyqtgraph/debug.py +++ b/pyqtgraph/debug.py @@ -2,7 +2,7 @@ """ debug.py - Functions to aid in debugging Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from __future__ import print_function diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6f67cfff..8ce2c404 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -2,7 +2,7 @@ """ functions.py - Miscellaneous functions with no other home Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from __future__ import division diff --git a/pyqtgraph/graphicsItems/MultiPlotItem.py b/pyqtgraph/graphicsItems/MultiPlotItem.py index be775d4a..065a605e 100644 --- a/pyqtgraph/graphicsItems/MultiPlotItem.py +++ b/pyqtgraph/graphicsItems/MultiPlotItem.py @@ -2,7 +2,7 @@ """ MultiPlotItem.py - Graphics item used for displaying an array of PlotItems Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from numpy import ndarray diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index fafb5592..7863dfef 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -2,7 +2,7 @@ """ ROI.py - Interactive graphics items for GraphicsView (ROI widgets) Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Implements a series of graphics items which display movable/scalable/rotatable shapes for use as region-of-interest markers. ROI class automatically handles extraction diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 512d503b..6affc990 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -2,7 +2,7 @@ """ ImageView.py - Widget for basic image dispay and analysis Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Widget used for displaying 2D or 3D data. Features: - float or int (including 16-bit int) image display via ImageItem diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 15d374a6..f157c588 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -2,7 +2,7 @@ """ MetaArray.py - Class encapsulating ndarray with meta data Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. MetaArray is an array class based on numpy.ndarray that allows storage of per-axis meta data such as axis values, names, units, column names, etc. It also enables several diff --git a/pyqtgraph/pgcollections.py b/pyqtgraph/pgcollections.py index ef3db258..49ed4ed6 100644 --- a/pyqtgraph/pgcollections.py +++ b/pyqtgraph/pgcollections.py @@ -2,7 +2,7 @@ """ advancedTypes.py - Basic data structures not included with python Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. Includes: - OrderedDict - Dictionary which preserves the order of its elements diff --git a/pyqtgraph/ptime.py b/pyqtgraph/ptime.py index f86bdd88..eb012934 100644 --- a/pyqtgraph/ptime.py +++ b/pyqtgraph/ptime.py @@ -2,7 +2,7 @@ """ ptime.py - Precision time function made os-independent (should have been taken care of by python) Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 7b8c5986..4aa2e90a 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -2,7 +2,7 @@ """ GraphicsView.py - Extension of QGraphicsView Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore, QtGui, QT_LIB diff --git a/pyqtgraph/widgets/MultiPlotWidget.py b/pyqtgraph/widgets/MultiPlotWidget.py index d1f56034..21258839 100644 --- a/pyqtgraph/widgets/MultiPlotWidget.py +++ b/pyqtgraph/widgets/MultiPlotWidget.py @@ -2,7 +2,7 @@ """ MultiPlotWidget.py - Convenience class--GraphicsView widget displaying a MultiPlotItem Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore from .GraphicsView import GraphicsView diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 6e10b13a..5208e3b3 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -2,7 +2,7 @@ """ PlotWidget.py - Convenience class--GraphicsView widget displaying a single PlotItem Copyright 2010 Luke Campagnola -Distributed under MIT/X11 license. See license.txt for more infomation. +Distributed under MIT/X11 license. See license.txt for more information. """ from ..Qt import QtCore, QtGui From 08c0de768bc205a720eeebf171cbe4919b346015 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 07:32:06 +0200 Subject: [PATCH 438/607] Warn if visible GraphicsView is garbage collected (#942) * Warn if visible window is garbage collected * (Py)Qt does not rely on Python GC * Only warn if deleted widget has no parents (if it is a standalone window) * Hide windows when closing * Only implement GraphicsView.__del__ if it does not prevent circular reference garbage collection --- pyqtgraph/graphicsItems/tests/test_ImageItem.py | 2 +- pyqtgraph/graphicsItems/tests/test_ROI.py | 6 ++++-- pyqtgraph/widgets/GraphicsView.py | 9 ++++++++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/tests/test_ImageItem.py b/pyqtgraph/graphicsItems/tests/test_ImageItem.py index ca197c6e..91926fe4 100644 --- a/pyqtgraph/graphicsItems/tests/test_ImageItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ImageItem.py @@ -121,7 +121,7 @@ def test_ImageItem(transpose=False): assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.') assert img._lastDownsample == (1, 4) - view.hide() + w.hide() def test_ImageItem_axisorder(): # All image tests pass again using the opposite axis order diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index b22ad530..33a18217 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -153,6 +153,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): # allow the roi to be re-used roi.scene().removeItem(roi) + win.hide() def test_PolyLineROI(): rois = [ @@ -234,5 +235,6 @@ def test_PolyLineROI(): r.setState(initState) assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.') assert len(r.getState()['points']) == 3 - - \ No newline at end of file + + plt.hide() + diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 4aa2e90a..b3b921cd 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -15,6 +15,7 @@ except ImportError: from ..Point import Point import sys, os +import warnings from .FileDialog import FileDialog from ..GraphicsScene import GraphicsScene import numpy as np @@ -396,5 +397,11 @@ class GraphicsView(QtGui.QGraphicsView): def dragEnterEvent(self, ev): ev.ignore() ## not sure why, but for some reason this class likes to consume drag events - + def _del(self): + if self.parentWidget() is None and self.isVisible(): + msg = "Visible window deleted. To prevent this, store a reference to the window object." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + +if sys.version_info[0] == 3 and sys.version_info[1] >= 4: + GraphicsView.__del__ = GraphicsView._del From cd3f7fd68e97d0d9466135fca446be5cb50734d7 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 18:55:55 +0200 Subject: [PATCH 439/607] Adding setter for GLGridItem.color (#992) * adding option to set grid color on demand * Update after setColor * Made GLGridItem color attribute private * Init GLGridItem color with fn.Color * Added docstring --- pyqtgraph/opengl/items/GLGridItem.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/opengl/items/GLGridItem.py b/pyqtgraph/opengl/items/GLGridItem.py index 0da9f61e..9dcff070 100644 --- a/pyqtgraph/opengl/items/GLGridItem.py +++ b/pyqtgraph/opengl/items/GLGridItem.py @@ -3,6 +3,7 @@ import numpy as np from OpenGL.GL import * from .. GLGraphicsItem import GLGraphicsItem from ... import QtGui +from ... import functions as fn __all__ = ['GLGridItem'] @@ -13,7 +14,7 @@ class GLGridItem(GLGraphicsItem): Displays a wire-frame grid. """ - def __init__(self, size=None, color=(1, 1, 1, .3), antialias=True, glOptions='translucent'): + def __init__(self, size=None, color=(255, 255, 255, 76.5), antialias=True, glOptions='translucent'): GLGraphicsItem.__init__(self) self.setGLOptions(glOptions) self.antialias = antialias @@ -21,7 +22,7 @@ class GLGridItem(GLGraphicsItem): size = QtGui.QVector3D(20,20,1) self.setSize(size=size) self.setSpacing(1, 1, 1) - self.color = color + self.setColor(color) def setSize(self, x=None, y=None, z=None, size=None): """ @@ -53,6 +54,14 @@ class GLGridItem(GLGraphicsItem): def spacing(self): return self.__spacing[:] + def setColor(self, color): + """Set the color of the grid. Arguments are the same as those accepted by functions.mkColor()""" + self.__color = fn.Color(color) + self.update() + + def color(self): + return self.__color + def paint(self): self.setupGLState() @@ -68,7 +77,7 @@ class GLGridItem(GLGraphicsItem): xs,ys,zs = self.spacing() xvals = np.arange(-x/2., x/2. + xs*0.001, xs) yvals = np.arange(-y/2., y/2. + ys*0.001, ys) - glColor4f(*self.color) + glColor4f(*self.color().glColor()) for x in xvals: glVertex3f(x, yvals[0], 0) glVertex3f(x, yvals[-1], 0) From 05aa3e93934887da5f5abc9ba6c79619761e47b1 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 18:57:40 +0200 Subject: [PATCH 440/607] Add 'fillOutline' option to draw an outline around a filled area (#999) --- examples/histogram.py | 2 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 9 +++++++-- pyqtgraph/graphicsItems/PlotDataItem.py | 5 ++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/examples/histogram.py b/examples/histogram.py index a25f0947..85fbe3f0 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -22,7 +22,7 @@ y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40)) ## Using stepMode=True causes the plot to draw two lines for each sample. ## notice that len(x) == len(y)+1 -plt1.plot(x, y, stepMode=True, fillLevel=0, brush=(0,0,255,150)) +plt1.plot(x, y, stepMode=True, fillLevel=0, fillOutline=True, brush=(0,0,255,150)) ## Now draw all points as a nicely-spaced scatter plot y = pg.pseudoScatter(vals, spacing=0.15) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 673d8334..fb3f6ea6 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -61,6 +61,7 @@ class PlotCurveItem(GraphicsObject): self.opts = { 'shadowPen': None, 'fillLevel': None, + 'fillOutline': False, 'brush': None, 'stepMode': False, 'name': None, @@ -291,7 +292,7 @@ class PlotCurveItem(GraphicsObject): self.fillPath = None self.invalidateBounds() self.update() - + def setData(self, *args, **kargs): """ =============== ======================================================== @@ -305,6 +306,8 @@ class PlotCurveItem(GraphicsObject): :func:`mkPen ` is allowed. fillLevel (float or None) Fill the area 'under' the curve to *fillLevel* + fillOutline (bool) If True, an outline surrounding the *fillLevel* + area is drawn. brush QBrush to use when filling. Any single argument accepted by :func:`mkBrush ` is allowed. antialias (bool) Whether to use antialiasing when drawing. This @@ -394,6 +397,8 @@ class PlotCurveItem(GraphicsObject): self.setShadowPen(kargs['shadowPen']) if 'fillLevel' in kargs: self.setFillLevel(kargs['fillLevel']) + if 'fillOutline' in kargs: + self.opts['fillOutline'] = kargs['fillOutline'] if 'brush' in kargs: self.setBrush(kargs['brush']) if 'antialias' in kargs: @@ -501,7 +506,7 @@ class PlotCurveItem(GraphicsObject): p.setPen(sp) p.drawPath(path) p.setPen(cp) - if self.fillPath is not None: + if self.opts['fillOutline'] and self.fillPath is not None: p.drawPath(self.fillPath) else: p.drawPath(path) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index bf119879..172e3beb 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -67,6 +67,8 @@ class PlotDataItem(GraphicsObject): shadowPen Pen for secondary line to draw behind the primary line. disabled by default. May be any single argument accepted by :func:`mkPen() ` fillLevel Fill the area between the curve and fillLevel + fillOutline (bool) If True, an outline surrounding the *fillLevel* + area is drawn. fillBrush Fill to use when fillLevel is specified. May be any single argument accepted by :func:`mkBrush() ` stepMode If True, two orthogonal lines are drawn for each sample @@ -154,6 +156,7 @@ class PlotDataItem(GraphicsObject): 'pen': (200,200,200), 'shadowPen': None, 'fillLevel': None, + 'fillOutline': False, 'fillBrush': None, 'stepMode': None, @@ -474,7 +477,7 @@ class PlotDataItem(GraphicsObject): def updateItems(self): curveArgs = {} - for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect'), ('stepMode', 'stepMode')]: + for k,v in [('pen','pen'), ('shadowPen','shadowPen'), ('fillLevel','fillLevel'), ('fillOutline', 'fillOutline'), ('fillBrush', 'brush'), ('antialias', 'antialias'), ('connect', 'connect'), ('stepMode', 'stepMode')]: curveArgs[v] = self.opts[k] scatterArgs = {} From d77ad273c71870881c3fda3d2b49d3488f100425 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sat, 17 Aug 2019 20:52:45 +0200 Subject: [PATCH 441/607] Fix: Item on ViewBox causes duplicate paint calls (#1017) * Fix: Item on ViewBox causes duplicate paint calls * Assure call of ViewBox.updateMatrix on resizeEvent * Fix: Disable autorange on "ViewBox.setRange" before updateAutoRange is called (Called via updateViewRange -> update -> prepareForPaint) --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 44 +++++++++------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index a542e916..6c6e3718 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -281,23 +281,10 @@ class ViewBox(GraphicsWidget): #if scene is not None and hasattr(scene, 'sigPrepareForPaint'): #scene.sigPrepareForPaint.connect(self.prepareForPaint) #return ret - - def checkSceneChange(self): - # ViewBox needs to receive sigPrepareForPaint from its scene before - # being painted. However, we have no way of being informed when the - # scene has changed in order to make this connection. The usual way - # to do this is via itemChange(), but bugs prevent this approach - # (see above). Instead, we simply check at every paint to see whether - # (the scene has changed. - scene = self.scene() - if scene == self._lastScene: - return - if self._lastScene is not None and hasattr(self._lastScene, 'sigPrepareForPaint'): - self._lastScene.sigPrepareForPaint.disconnect(self.prepareForPaint) - if scene is not None and hasattr(scene, 'sigPrepareForPaint'): - scene.sigPrepareForPaint.connect(self.prepareForPaint) + + def update(self, *args, **kwargs): self.prepareForPaint() - self._lastScene = scene + GraphicsWidget.update(self, *args, **kwargs) def prepareForPaint(self): #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) @@ -437,13 +424,20 @@ 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() @@ -529,6 +523,14 @@ class ViewBox(GraphicsWidget): # Update axes one at a time changed = [False, False] + + # Disable auto-range for each axis that was requested to be set + if disableAutoRange: + xOff = False if setRequested[0] else None + yOff = False if setRequested[1] else None + self.enableAutoRange(x=xOff, y=yOff) + changed.append(True) + for ax, range in changes.items(): mn = min(range) mx = max(range) @@ -569,13 +571,6 @@ class ViewBox(GraphicsWidget): lockY = False self.updateViewRange(lockX, lockY) - # Disable auto-range for each axis that was requested to be set - if disableAutoRange: - xOff = False if setRequested[0] else None - yOff = False if setRequested[1] else None - self.enableAutoRange(x=xOff, y=yOff) - changed.append(True) - # If nothing has changed, we are done. if any(changed): # Update target rect for debugging @@ -1548,7 +1543,6 @@ class ViewBox(GraphicsWidget): def updateMatrix(self, changed=None): if not self._matrixNeedsUpdate: return - ## Make the childGroup's transform match the requested viewRange. bounds = self.rect() @@ -1577,8 +1571,6 @@ class ViewBox(GraphicsWidget): self.sigTransformChanged.emit(self) ## segfaults here: 1 def paint(self, p, opt, widget): - self.checkSceneChange() - if self.border is not None: bounds = self.shape() p.setPen(self.border) From b7b431de8d31c1d894ffc368ff5cd562fcc13678 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 18 Aug 2019 05:36:34 +0200 Subject: [PATCH 442/607] FIX: Curves are automatically set visible when one is deleted (#987) * Do not automatically set all curves visible * Improved array iteration in PlotItem.updateDecimation --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 9703f286..2158b1a1 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -924,15 +924,13 @@ class PlotItem(GraphicsWidget): curves = self.curves[:] split = len(curves) - numCurves - for i in range(len(curves)): - if numCurves == -1 or i >= split: - curves[i].show() - else: + for curve in curves[split:]: + if numCurves != -1: if self.ctrl.forgetTracesCheck.isChecked(): - curves[i].clear() + curve.clear() self.removeItem(curves[i]) else: - curves[i].hide() + curve.hide() def updateAlpha(self, *args): (alpha, auto) = self.alphaState() From ff30a82298a842e823a5373f80192df4097d7e3d Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 18 Aug 2019 05:38:05 +0200 Subject: [PATCH 443/607] Fix: colormap: Support various arguments as color (#1014) * colormap: Support various arguments as color * Using mapping for speed and consistency (suggested by @j9ac9k) --- pyqtgraph/colormap.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/colormap.py b/pyqtgraph/colormap.py index 585d7ea1..eb423634 100644 --- a/pyqtgraph/colormap.py +++ b/pyqtgraph/colormap.py @@ -1,6 +1,7 @@ import numpy as np from .Qt import QtGui, QtCore from .python2_3 import basestring +from .functions import mkColor class ColorMap(object): @@ -56,9 +57,9 @@ class ColorMap(object): =============== ============================================================== **Arguments:** pos Array of positions where each color is defined - color Array of RGBA colors. - Integer data types are interpreted as 0-255; float data types - are interpreted as 0.0-1.0 + color Array of colors. + Values are interpreted via + :func:`mkColor() `. mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG) indicating the color space that should be used when interpolating between stops. Note that the last mode value is @@ -68,7 +69,11 @@ class ColorMap(object): self.pos = np.array(pos) order = np.argsort(self.pos) self.pos = self.pos[order] - self.color = np.array(color)[order] + self.color = np.apply_along_axis( + func1d = lambda x: mkColor(x).getRgb(), + axis = -1, + arr = color, + )[order] if mode is None: mode = np.ones(len(pos)) self.mode = mode @@ -225,7 +230,7 @@ class ColorMap(object): x = np.linspace(start, stop, nPts) table = self.map(x, mode) - if not alpha: + if not alpha and mode != self.QCOLOR: return table[:,:3] else: return table From 31f1ae586bb788b2a927356c78d9bef96afc6f3e Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 18 Aug 2019 05:43:21 +0200 Subject: [PATCH 444/607] Fix: Allow wrapped GraphicsLayoutWidget to be deleted before its wrapping Python object (#1022) --- pyqtgraph/widgets/GraphicsView.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index b3b921cd..3c553feb 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -399,9 +399,12 @@ class GraphicsView(QtGui.QGraphicsView): ev.ignore() ## not sure why, but for some reason this class likes to consume drag events def _del(self): - if self.parentWidget() is None and self.isVisible(): - msg = "Visible window deleted. To prevent this, store a reference to the window object." - warnings.warn(msg, RuntimeWarning, stacklevel=2) + try: + if self.parentWidget() is None and self.isVisible(): + msg = "Visible window deleted. To prevent this, store a reference to the window object." + warnings.warn(msg, RuntimeWarning, stacklevel=2) + except RuntimeError: + pass if sys.version_info[0] == 3 and sys.version_info[1] >= 4: GraphicsView.__del__ = GraphicsView._del From 3a863fff9ab711ba7db67e9ef83fc9dce77e1448 Mon Sep 17 00:00:00 2001 From: Pepijn Kenter Date: Sun, 18 Aug 2019 07:19:11 +0200 Subject: [PATCH 445/607] Fixes incorrect default value for scale parameter in makeARGB. (#793) * Fix incorrect default value for scale paremter in makeARGB. * update tests to pass with codebase change --- pyqtgraph/functions.py | 2 +- pyqtgraph/tests/test_functions.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8ce2c404..2c11b647 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1074,7 +1074,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): # Decide on maximum scaled value if scale is None: if lut is not None: - scale = lut.shape[0] - 1 + scale = lut.shape[0] else: scale = 255. diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 68f3dc24..e013fe42 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -206,15 +206,15 @@ def test_makeARGB(): # lut smaller than maxint lut = np.arange(128).astype(np.uint8) im2, alpha = pg.makeARGB(im1, lut=lut) - checkImage(im2, np.linspace(0, 127, 256).astype('ubyte'), alpha, False) + checkImage(im2, np.linspace(0, 127.5, 256, dtype='ubyte'), alpha, False) # lut + levels lut = np.arange(256)[::-1].astype(np.uint8) im2, alpha = pg.makeARGB(im1, lut=lut, levels=[-128, 384]) - checkImage(im2, np.linspace(192, 65.5, 256).astype('ubyte'), alpha, False) + checkImage(im2, np.linspace(191.5, 64.5, 256, dtype='ubyte'), alpha, False) im2, alpha = pg.makeARGB(im1, lut=lut, levels=[64, 192]) - checkImage(im2, np.clip(np.linspace(385.5, -126.5, 256), 0, 255).astype('ubyte'), alpha, False) + checkImage(im2, np.clip(np.linspace(384.5, -127.5, 256), 0, 255).astype('ubyte'), alpha, False) # uint8 data + uint16 LUT lut = np.arange(4096)[::-1].astype(np.uint16) // 16 From 80f8af2432efb5a20437cb91ff448713aeb0d474 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Sun, 18 Aug 2019 19:17:34 +0200 Subject: [PATCH 446/607] Use Qt5 QWheelEvent functions if necessary (#924) * Make QWheelEvent code consistently compatible with Qt5 * Add documentation * Removed old TODO message * Init remote QWheelEvent only with relative position, minor code simplifications * RemoteGraphicsView Renderer assumes to be at (0,0) * Orientation serialized as boolean --- pyqtgraph/widgets/GraphicsView.py | 11 +++- pyqtgraph/widgets/RemoteGraphicsView.py | 79 +++++++++++++++++++++---- 2 files changed, 79 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 3c553feb..1be1a274 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -327,7 +327,16 @@ class GraphicsView(QtGui.QGraphicsView): if not self.mouseEnabled: ev.ignore() return - sc = 1.001 ** ev.delta() + + delta = 0 + if QT_LIB in ['PyQt4', 'PySide']: + delta = ev.delta() + else: + delta = ev.angleDelta().x() + if delta == 0: + delta = ev.angleDelta().y() + + sc = 1.001 ** delta #self.scale *= sc #self.updateMatrix() self.scale(sc, sc) diff --git a/pyqtgraph/widgets/RemoteGraphicsView.py b/pyqtgraph/widgets/RemoteGraphicsView.py index edf4db3c..9be1b531 100644 --- a/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/pyqtgraph/widgets/RemoteGraphicsView.py @@ -8,6 +8,56 @@ import numpy as np import mmap, tempfile, ctypes, atexit, sys, random __all__ = ['RemoteGraphicsView'] + +class SerializableWheelEvent: + """ + Contains all information of a QWheelEvent, is serializable and can generate QWheelEvents. + + Methods have the functionality of their QWheelEvent equivalent. + """ + def __init__(self, _pos, _globalPos, _delta, _buttons, _modifiers, _orientation): + self._pos = _pos + self._globalPos = _globalPos + self._delta = _delta + self._buttons = _buttons + self._modifiers = _modifiers + self._orientation_vertical = _orientation == QtCore.Qt.Vertical + + def pos(self): + return self._pos + + def globalPos(self): + return self._globalPos + + def delta(self): + return self._delta + + def orientation(self): + if self._orientation_vertical: + return QtCore.Qt.Vertical + else: + return QtCore.Qt.Horizontal + + def angleDelta(self): + if self._orientation_vertical: + return QtCore.QPoint(0, self._delta) + else: + return QtCore.QPoint(self._delta, 0) + + def buttons(self): + return QtCore.Qt.MouseButtons(self._buttons) + + def modifiers(self): + return QtCore.Qt.KeyboardModifiers(self._modifiers) + + def toQWheelEvent(self): + """ + Generate QWheelEvent from SerializableWheelEvent. + """ + if QT_LIB in ['PyQt4', 'PySide']: + return QtGui.QWheelEvent(self.pos(), self.globalPos(), self.delta(), self.buttons(), self.modifiers(), self.orientation()) + else: + return QtGui.QWheelEvent(self.pos(), self.globalPos(), QtCore.QPoint(), self.angleDelta(), self.delta(), self.orientation(), self.buttons(), self.modifiers()) class RemoteGraphicsView(QtGui.QWidget): """ @@ -97,22 +147,34 @@ class RemoteGraphicsView(QtGui.QWidget): p.end() def mousePressEvent(self, ev): - self._view.mousePressEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mousePressEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mousePressEvent(self, ev) def mouseReleaseEvent(self, ev): - self._view.mouseReleaseEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mouseReleaseEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mouseReleaseEvent(self, ev) def mouseMoveEvent(self, ev): - self._view.mouseMoveEvent(int(ev.type()), ev.pos(), ev.globalPos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') + self._view.mouseMoveEvent(int(ev.type()), ev.pos(), ev.pos(), int(ev.button()), int(ev.buttons()), int(ev.modifiers()), _callSync='off') ev.accept() return QtGui.QWidget.mouseMoveEvent(self, ev) def wheelEvent(self, ev): - self._view.wheelEvent(ev.pos(), ev.globalPos(), ev.delta(), int(ev.buttons()), int(ev.modifiers()), int(ev.orientation()), _callSync='off') + delta = 0 + orientation = QtCore.Qt.Horizontal + if QT_LIB in ['PyQt4', 'PySide']: + delta = ev.delta() + orientation = ev.orientation() + else: + delta = ev.angleDelta().x() + if delta == 0: + orientation = QtCore.Qt.Vertical + delta = ev.angleDelta().y() + + serializableEvent = SerializableWheelEvent(ev.pos(), ev.pos(), delta, int(ev.buttons()), int(ev.modifiers()), orientation) + self._view.wheelEvent(serializableEvent, _callSync='off') ev.accept() return QtGui.QWidget.wheelEvent(self, ev) @@ -251,12 +313,9 @@ class Renderer(GraphicsView): btns = QtCore.Qt.MouseButtons(btns) mods = QtCore.Qt.KeyboardModifiers(mods) return GraphicsView.mouseReleaseEvent(self, QtGui.QMouseEvent(typ, pos, gpos, btn, btns, mods)) - - def wheelEvent(self, pos, gpos, d, btns, mods, ori): - btns = QtCore.Qt.MouseButtons(btns) - mods = QtCore.Qt.KeyboardModifiers(mods) - ori = (None, QtCore.Qt.Horizontal, QtCore.Qt.Vertical)[ori] - return GraphicsView.wheelEvent(self, QtGui.QWheelEvent(pos, gpos, d, btns, mods, ori)) + + def wheelEvent(self, ev): + return GraphicsView.wheelEvent(self, ev.toQWheelEvent()) def keyEvent(self, typ, mods, text, autorep, count): typ = QtCore.QEvent.Type(typ) From fd11e1352d9e0b15d67520354bbbf40c615bf996 Mon Sep 17 00:00:00 2001 From: Paul Debus Date: Sun, 18 Aug 2019 21:16:31 +0200 Subject: [PATCH 447/607] fix encoding error in checkOpenGLVersion (#787) --- pyqtgraph/opengl/GLViewWidget.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/GLViewWidget.py b/pyqtgraph/opengl/GLViewWidget.py index 1a70d735..f123b151 100644 --- a/pyqtgraph/opengl/GLViewWidget.py +++ b/pyqtgraph/opengl/GLViewWidget.py @@ -427,7 +427,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): def checkOpenGLVersion(self, msg): ## Only to be called from within exception handler. ver = glGetString(GL_VERSION).split()[0] - if int(ver.split('.')[0]) < 2: + if int(ver.split(b'.')[0]) < 2: from .. import debug debug.printExc() raise Exception(msg + " The original exception is printed above; however, pyqtgraph requires OpenGL version 2.0 or greater for many of its 3D features and your OpenGL version is %s. Installing updated display drivers may resolve this issue." % ver) From 982fb3427a68e9aaf0021a3fb60fa36c680a7209 Mon Sep 17 00:00:00 2001 From: dschoni Date: Mon, 29 Jul 2019 17:50:38 +0200 Subject: [PATCH 448/607] Add all important and accepted PRs to changelog --- CHANGELOG | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 2cb16918..4f2d4ff5 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -56,6 +56,10 @@ pyqtgraph-0.11.0 (in development) - #683: Allow data filter entries to be updated after they are created - #685: Add option to set enum default values in DataFilterWidget - #710: Adds ability to rotate/scale ROIs by mouse drag on the ROI itself (using alt/shift modifiers) + - #813,814,817: Performance improvements + - #837: Added options for field variables in ColorMapWidget + - #840, 932: Improve clipping behavior + - #922: Curve fill for fill-patches API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because @@ -76,6 +80,8 @@ pyqtgraph-0.11.0 (in development) - #589: Remove SpiralROI (this was unintentionally added in the first case) - #593: Override qAbort on slot exceptions for PyQt>=5.5 - #657: When a floating Dock window is closed, the dock is now returned home + - #771: Suppress RuntimeWarning for arrays containing zeros in logscale + - #963: Last image in image-stack can now be selected with the z-slider Bugfixes: - #408: Fix `cleanup` when the running qt application is not a QApplication @@ -154,13 +160,34 @@ pyqtgraph-0.11.0 (in development) - #723: Fix axis ticks when using self.scale - #739: Fix handling of 2-axis mouse wheel events - #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX. + - #763: Fix OverflowError when using Auto Downsampling. + - #767: Fix Image display for images with the same value everywhere. + - #770: Fix GLVieWidget.setCameraPosition ignoring first parameter. + - #782: Fix missing FileForwarder thread termination. + - #815: Fixed mirroring of x-axis with "invert Axis" submenu. + - #824: Fix several issues related with mouse movement and GraphicsView. + - #832: Fix Permission error in tests due to unclosed filehandle. + - #836: Fix tickSpacing bug that lead to axis not being drawn. + - #861: Fix crash of PlotWidget if empty ErrorBarItem is added. + - #868: Fix segfault on repeated closing of matplotlib exporter. + - #875,876,887,934,947,980: Fix deprecation warnings. + - #886: Fix flowchart saving on python3. + - #888: Fix TreeWidget.topLevelItems in python3. + - #935: Fix PlotItem.addLine with 'pos' and 'angle' parameter. + - #949: Fix multiline parameters (such as arrays) reading from config files. + - #951: Fix event firing from scale handler. + - #952: Fix RotateFree handle dragging + - #968: Fix Si units in AxisItem leading to an incorrect unit. + - #971: Fix a segfault stemming from incorrect signal disconnection Maintenance: - Lots of new unit tests - Lots of code cleanup + - A lot of work on CI pipelines, test coverage and test passing (see e.g. #903,911) - #546: Add check for EINTR during example testing to avoid sporadic test failures on travis - #624: TravisCI no longer running python 2.6 tests - #695: "dev0" added to version string + - #865,873,877 (and more): Implement Azure CI pipelines, fix Travis CI pyqtgraph-0.10.0 From 614c4d05293d91f71e6107df197941c7b7c04049 Mon Sep 17 00:00:00 2001 From: dschoni Date: Mon, 19 Aug 2019 13:27:16 +0200 Subject: [PATCH 449/607] Added newly merged PRs from the last 3 weeks. --- CHANGELOG | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4f2d4ff5..89c8eb13 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -60,6 +60,7 @@ pyqtgraph-0.11.0 (in development) - #837: Added options for field variables in ColorMapWidget - #840, 932: Improve clipping behavior - #922: Curve fill for fill-patches + - #996: Allow the update of LegendItem API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because @@ -81,7 +82,11 @@ pyqtgraph-0.11.0 (in development) - #593: Override qAbort on slot exceptions for PyQt>=5.5 - #657: When a floating Dock window is closed, the dock is now returned home - #771: Suppress RuntimeWarning for arrays containing zeros in logscale + - #942: If the visible GraphicsView is garbage collected, a warning is issued. - #963: Last image in image-stack can now be selected with the z-slider + - #992: Added a setter for GlGridItem.color. + - #999: Make outline around fillLevel optional. + - #1014: Enable various arguments as color in colormap. Bugfixes: - #408: Fix `cleanup` when the running qt application is not a QApplication @@ -164,6 +169,8 @@ pyqtgraph-0.11.0 (in development) - #767: Fix Image display for images with the same value everywhere. - #770: Fix GLVieWidget.setCameraPosition ignoring first parameter. - #782: Fix missing FileForwarder thread termination. + - #787: Fix encoding errors in checkOpenGLVersion. + - #793: Fix wrong default scaling in makeARGB - #815: Fixed mirroring of x-axis with "invert Axis" submenu. - #824: Fix several issues related with mouse movement and GraphicsView. - #832: Fix Permission error in tests due to unclosed filehandle. @@ -173,12 +180,21 @@ pyqtgraph-0.11.0 (in development) - #875,876,887,934,947,980: Fix deprecation warnings. - #886: Fix flowchart saving on python3. - #888: Fix TreeWidget.topLevelItems in python3. + - #924: Fix QWheelEvent in RemoteGraphicsView with pyqt5. - #935: Fix PlotItem.addLine with 'pos' and 'angle' parameter. - #949: Fix multiline parameters (such as arrays) reading from config files. - #951: Fix event firing from scale handler. - #952: Fix RotateFree handle dragging - #968: Fix Si units in AxisItem leading to an incorrect unit. - - #971: Fix a segfault stemming from incorrect signal disconnection + - #971: Fix a segfault stemming from incorrect signal disconnection. + - #974: Fix recursion error when instancing CtrlNode. + - #987: Fix visibility reset when PlotItems are removed. + - #998: Fix QtProcess proxy being unable to handle numpy arrays with dtype uint8. + - #1010: Fix matplotlib/CSV export. + - #1015: Iterators are now converted to NumPy arrays. + - #1016: Fix synchronisation of multiple ImageViews with time axis. + - #1017: Fix duplicate paint calls emitted by Items on ViewBox. + - #1019: Fix disappearing GLGridItems when PlotItems are removed and readded. Maintenance: - Lots of new unit tests From 584c4516f0c4386ba01edf6fee2b1f392f88cfaf Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Thu, 29 Aug 2019 13:56:25 -0700 Subject: [PATCH 450/607] Expand CI + pre-commit (#991) * Initial attempt at extra checks in CI land * Adding flake8 config * Adding pre-commit configuration and explanation in CONTRIBUTING.md --- .flake8 | 49 ++++++++ .pre-commit-config.yaml | 11 ++ CONTRIBUTING.md | 16 ++- azure-pipelines.yml | 82 ++++++++++-- azure-test-template.yml | 93 +++++++------- examples/test_examples.py | 157 ++++++++++++++++++----- setup.py | 7 +- tools/setupHelpers.py | 256 ++++++++++++++++++++++---------------- 8 files changed, 469 insertions(+), 202 deletions(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..0556c925 --- /dev/null +++ b/.flake8 @@ -0,0 +1,49 @@ +[flake8] +exclude = .git,.tox,__pycache__,doc,old,build,dist +show_source = True +statistics = True +verbose = 2 +select = + E101, + E112, + E122, + E125, + E133, + E223, + E224, + E242, + E273, + E274, + E901, + E902, + W191, + W601, + W602, + W603, + W604, + E124, + E231, + E211, + E261, + E271, + E272, + E304, + F401, + F402, + F403, + F404, + E501, + E502, + E702, + E703, + E711, + E712, + E721, + F811, + F812, + F821, + F822, + F823, + F831, + F841, + W292 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..c2f8f9a8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,11 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + sha: master + hooks: + - id: check-added-large-files + args: ['--maxkb=100'] + - id: check-case-conflict + - id: end-of-file-fixer + - id: fix-encoding-pragma + - id: mixed-line-ending + args: [--fix=lf] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d602b89b..9af2e508 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to PyQtGraph -Contributions to pyqtgraph are welcome! +Contributions to pyqtgraph are welcome! Please use the following guidelines when preparing changes: @@ -13,11 +13,13 @@ Please use the following guidelines when preparing changes: ## Documentation -* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. +* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. * Documentation is generated with sphinx; please check that docstring changes compile correctly ## Style guidelines +### Rules + * PyQtGraph prefers PEP8 for most style issues, but this is not enforced rigorously as long as the code is clean and readable. * Use `python setup.py style` to see whether your code follows the mandatory style guidelines checked by flake8. * Exception 1: All variable names should use camelCase rather than underscore_separation. This is done for consistency with Qt @@ -33,9 +35,15 @@ Please use the following guidelines when preparing changes: ============== ======================================================== ``` - QObject subclasses that implement new signals should also describe + QObject subclasses that implement new signals should also describe these in a similar table. - + +### Pre-Commit + +PyQtGraph developers are highly encouraged to (but not required) to use [`pre-commit`](https://pre-commit.com/). `pre-commit` does a number of checks when attempting to commit the code to ensure it conforms to various standards, such as `flake8`, utf-8 encoding pragma, line-ending fixers, and so on. If any of the checks fail, the commit will be rejected, and you will have the opportunity to make the necessary fixes before adding and committing a file again. This ensures that every commit made conforms to (most) of the styling standards that the library enforces; and you will most likely pass the code style checks by the CI. + +To make use of `pre-commit`, have it available in your `$PATH` and run `pre-commit install` from the root directory of PyQtGraph. + ## Testing Setting up a test environment ### Dependencies diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b91f515a..657189f8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,7 +1,3 @@ -############################################################################################ -# This config was rectrieved in no small part from https://github.com/slaclab/pydm -############################################################################################ - trigger: branches: include: @@ -20,19 +16,83 @@ pr: variables: OFFICIAL_REPO: 'pyqtgraph/pyqtgraph' + DEFAULT_MERGE_BRANCH: 'develop' -jobs: - - template: azure-test-template.yml - parameters: - name: Linux +stages: +- stage: "pre_test" + jobs: + - job: check_diff_size + pool: vmImage: 'Ubuntu 16.04' + steps: + - bash: | + git config --global advice.detachedHead false + mkdir ~/repo-clone && cd ~/repo-clone + git init + git remote add -t $(Build.SourceBranchName) origin $(Build.Repository.Uri) + git remote add -t ${DEFAULT_MERGE_BRANCH} upstream https://github.com/${OFFICIAL_REPO}.git + + git fetch origin $(Build.SourceBranchName) + git fetch upstream ${DEFAULT_MERGE_BRANCH} + + git checkout $(Build.SourceBranchName) + MERGE_SIZE=`du -s . | sed -e "s/\t.*//"` + echo -e "Merge Size ${MERGE_SIZE}" + + git checkout ${DEFAULT_MERGE_BRANCH} + TARGET_SIZE=`du -s . | sed -e "s/\t.*//"` + echo -e "Target Size ${TARGET_SIZE}" + + if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then + SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`; + else + SIZE_DIFF=0; + fi; + echo -e "Estimated content size difference = ${SIZE_DIFF} kB" && + test ${SIZE_DIFF} -lt 100; + displayName: 'Diff Size Check' + continueOnError: true + + - job: "style_check" + pool: + vmImage: "Ubuntu 16.04" + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: 3.7 + - bash: | + pip install flake8 + python setup.py style + displayName: 'flake8 check' + continueOnError: true + + - job: "build_wheel" + pool: + vmImage: 'Ubuntu 16.04' + steps: + - task: UsePythonVersion@0 + inputs: + versionSpec: 3.7 + - script: | + python -m pip install setuptools wheel + python setup.py bdist_wheel --universal + displayName: "Build Python Wheel" + continueOnError: false + - publish: dist + artifact: wheel + +- stage: "test" + jobs: - template: azure-test-template.yml parameters: - name: Windows + name: linux + vmImage: 'Ubuntu 16.04' + - template: azure-test-template.yml + parameters: + name: windows vmImage: 'vs2017-win2016' - - template: azure-test-template.yml parameters: - name: MacOS + name: macOS vmImage: 'macOS-10.13' diff --git a/azure-test-template.yml b/azure-test-template.yml index c1e6c1b0..81e4399c 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -26,16 +26,22 @@ jobs: python.version: "3.6" qt.bindings: "pyside2" install.method: "conda" - Python37-PyQt-5.12: + Python37-PyQt-5.13: python.version: '3.7' qt.bindings: "PyQt5" install.method: "pip" - Python37-PySide2-5.12: + Python37-PySide2-5.13: python.version: "3.7" qt.bindings: "PySide2" install.method: "pip" steps: + - task: DownloadPipelineArtifact@2 + inputs: + source: 'current' + artifact: wheel + path: 'dist' + - task: ScreenResolutionUtility@1 inputs: displaySettings: 'specific' @@ -43,6 +49,11 @@ jobs: height: '1080' condition: eq(variables['agent.os'], 'Windows_NT' ) + - task: UsePythonVersion@0 + inputs: + versionSpec: $(python.version) + condition: eq(variables['install.method'], 'pip') + - script: | curl -LJO https://github.com/pal1000/mesa-dist-win/releases/download/19.1.0/mesa3d-19.1.0-release-msvc.exe 7z x mesa3d-19.1.0-release-msvc.exe @@ -60,75 +71,71 @@ jobs: displayName: "Install Windows-Mesa OpenGL DLL" condition: eq(variables['agent.os'], 'Windows_NT') - - task: UsePythonVersion@0 - inputs: - versionSpec: $(python.version) - condition: eq(variables['install.method'], 'pip') - - bash: | if [ $(agent.os) == 'Linux' ] then - echo '##vso[task.prependpath]/usr/share/miniconda/bin' + echo "##vso[task.prependpath]$CONDA/bin" + if [ $(python.version) == '2.7' ] + then + echo "Grabbing Older Miniconda" + wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-Linux-x86_64.sh -O Miniconda.sh + bash Miniconda.sh -b -p $CONDA -f + fi elif [ $(agent.os) == 'Darwin' ] then - echo '##vso[task.prependpath]$CONDA/bin' - sudo install -d -m 0777 /usr/local/miniconda/envs + sudo chown -R $USER $CONDA + echo "##vso[task.prependpath]$CONDA/bin" + if [ $(python.version) == '2.7' ] + then + echo "Grabbing Older Miniconda" + wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-MacOSX-x86_64.sh -O Miniconda.sh + bash Miniconda.sh -b -p $CONDA -f + fi elif [ $(agent.os) == 'Windows_NT' ] then - echo "##vso[task.prependpath]$env:CONDA\Scripts" + echo "##vso[task.prependpath]$CONDA/Scripts" else echo 'Just what OS are you using?' fi - displayName: 'Add Conda to $PATH' + displayName: 'Add Conda To $PATH' condition: eq(variables['install.method'], 'conda' ) - - task: CondaEnvironment@0 - displayName: 'Create Conda Environment' - condition: eq(variables['install.method'], 'conda') - inputs: - environmentName: 'test-environment-$(python.version)' - packageSpecs: 'python=$(python.version)' - updateConda: false - - bash: | if [ $(install.method) == "conda" ] then + conda create --name test-environment-$(python.version) python=$(python.version) --yes + echo "Conda Info:" + conda info + echo "Installing qt-bindings" source activate test-environment-$(python.version) - conda install -c conda-forge $(qt.bindings) numpy scipy pyopengl pytest six coverage --yes --quiet + + if [ $(agent.os) == "Linux" ] && [ $(python.version) == "2.7" ] + then + conda install $(qt.bindings) --yes + else + conda install -c conda-forge $(qt.bindings) --yes + fi + echo "Installing remainder of dependencies" + conda install -c conda-forge numpy scipy six pyopengl --yes else - pip install $(qt.bindings) numpy scipy pyopengl pytest six coverage + pip install $(qt.bindings) numpy scipy pyopengl six fi - pip install pytest-xdist pytest-cov + echo "" + pip install pytest pytest-xdist pytest-cov coverage if [ $(python.version) == "2.7" ] then - pip install pytest-faulthandler + pip install pytest-faulthandler==1.6.0 export PYTEST_ADDOPTS="--faulthandler-timeout=15" fi displayName: "Install Dependencies" - + - bash: | if [ $(install.method) == "conda" ] then source activate test-environment-$(python.version) fi - pip install setuptools wheel - python setup.py bdist_wheel - pip install dist/*.whl - displayName: 'Build Wheel and Install' - - - task: CopyFiles@2 - inputs: - contents: 'dist/**' - targetFolder: $(Build.ArtifactStagingDirectory) - cleanTargetFolder: true - displayName: "Copy Binary Wheel Distribution To Artifacts" - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Binary Wheel' - condition: always() - inputs: - pathtoPublish: $(Build.ArtifactStagingDirectory)/dist - artifactName: Distributions + python -m pip install --no-index --find-links=dist pyqtgraph + displayName: 'Install Wheel' - bash: | sudo apt-get install -y libxkbcommon-x11-0 # herbstluftwm diff --git a/examples/test_examples.py b/examples/test_examples.py index bb4682f1..c6fef377 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import from pyqtgraph import Qt from . import utils @@ -5,7 +6,6 @@ from collections import namedtuple import errno import importlib import itertools -import pkgutil import pytest import os, sys import subprocess @@ -41,7 +41,12 @@ if os.getenv('TRAVIS') is not None: files = sorted(set(utils.buildFileList(utils.examples))) -frontends = {Qt.PYQT4: False, Qt.PYQT5: False, Qt.PYSIDE: False, Qt.PYSIDE2: False} +frontends = { + Qt.PYQT4: False, + Qt.PYQT5: False, + Qt.PYSIDE: False, + Qt.PYSIDE2: False +} # sort out which of the front ends are available for frontend in frontends.keys(): try: @@ -50,48 +55,136 @@ for frontend in frontends.keys(): except ImportError: pass -installedFrontends = sorted([frontend for frontend, isPresent in frontends.items() if isPresent]) +installedFrontends = sorted([ + frontend for frontend, isPresent in frontends.items() if isPresent +]) exceptionCondition = namedtuple("exceptionCondition", ["condition", "reason"]) -conditionalExampleTests = { - "hdf5.py": exceptionCondition(False, reason="Example requires user interaction and is not suitable for testing"), - "RemoteSpeedTest.py": exceptionCondition(False, reason="Test is being problematic on CI machines"), - "optics_demos.py": exceptionCondition(not frontends[Qt.PYSIDE], reason="Test fails due to PySide bug: https://bugreports.qt.io/browse/PYSIDE-671"), - 'GLVolumeItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLIsosurface.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLSurfacePlot.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLScatterPlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLshaders.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLLinePlotItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLMeshItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939"), - 'GLImageItem.py': exceptionCondition(not(sys.platform == "darwin" and sys.version_info[0] == 2 and (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), reason="glClear does not work on macOS + Python2.7 + Qt4: https://github.com/pyqtgraph/pyqtgraph/issues/939") +conditionalExamples = { + "hdf5.py": exceptionCondition( + False, + reason="Example requires user interaction" + ), + "RemoteSpeedTest.py": exceptionCondition( + False, + reason="Test is being problematic on CI machines" + ), + "optics_demos.py": exceptionCondition( + not frontends[Qt.PYSIDE], + reason=( + "Test fails due to PySide bug: ", + "https://bugreports.qt.io/browse/PYSIDE-671" + ) + ), + 'GLVolumeItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLIsosurface.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLSurfacePlot.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLScatterPlotItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLshaders.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLLinePlotItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLMeshItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ), + 'GLImageItem.py': exceptionCondition( + not(sys.platform == "darwin" and + sys.version_info[0] == 2 and + (frontends[Qt.PYQT4] or frontends[Qt.PYSIDE])), + reason=( + "glClear does not work on macOS + Python2.7 + Qt4: ", + "https://github.com/pyqtgraph/pyqtgraph/issues/939" + ) + ) } @pytest.mark.parametrize( - "frontend, f", - [ - pytest.param( - frontend, + "frontend, f", + [ + pytest.param( + frontend, f, - marks=pytest.mark.skipif(conditionalExampleTests[f[1]].condition is False, - reason=conditionalExampleTests[f[1]].reason) if f[1] in conditionalExampleTests.keys() else (), - ) - for frontend, f, in itertools.product(installedFrontends, files) - ], - ids = [" {} - {} ".format(f[1], frontend) for frontend, f in itertools.product(installedFrontends, files)] + marks=pytest.mark.skipif( + conditionalExamples[f[1]].condition is False, + reason=conditionalExamples[f[1]].reason + ) if f[1] in conditionalExamples.keys() else (), + ) + for frontend, f, in itertools.product(installedFrontends, files) + ], + ids = [ + " {} - {} ".format(f[1], frontend) + for frontend, f in itertools.product( + installedFrontends, + files + ) + ] ) def testExamples(frontend, f, graphicsSystem=None): # runExampleFile(f[0], f[1], sys.executable, frontend) name, file = f global path - fn = os.path.join(path,file) + fn = os.path.join(path, file) os.chdir(path) sys.stdout.write("{} ".format(name)) sys.stdout.flush() import1 = "import %s" % frontend if frontend != '' else '' import2 = os.path.splitext(os.path.split(fn)[1])[0] - graphicsSystem = '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + graphicsSystem = ( + '' if graphicsSystem is None else "pg.QtGui.QApplication.setGraphicsSystem('%s')" % graphicsSystem + ) code = """ try: %s @@ -123,7 +216,7 @@ except: stderr=subprocess.PIPE, stdout=subprocess.PIPE) process.stdin.write(code.encode('UTF-8')) - process.stdin.close() ##? + process.stdin.close() output = '' fail = False while True: @@ -146,10 +239,14 @@ except: process.kill() #res = process.communicate() res = (process.stdout.read(), process.stderr.read()) - if fail or 'exception' in res[1].decode().lower() or 'error' in res[1].decode().lower(): + if (fail or + 'exception' in res[1].decode().lower() or + 'error' in res[1].decode().lower()): print(res[0].decode()) print(res[1].decode()) - pytest.fail("{}\n{}\nFailed {} Example Test Located in {} ".format(res[0].decode(), res[1].decode(), name, file), pytrace=False) + pytest.fail("{}\n{}\nFailed {} Example Test Located in {} " + .format(res[0].decode(), res[1].decode(), name, file), + pytrace=False) if __name__ == "__main__": pytest.cmdline.main() diff --git a/setup.py b/setup.py index a59f7dd5..38ee477a 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ +# -*- coding: utf-8 -*- DESCRIPTION = """\ -PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PySide and +PyQtGraph is a pure-python graphics and GUI library built on PyQt4/PyQt5/PySide/PySide2 and numpy. It is intended for use in mathematics / scientific / engineering applications. @@ -12,14 +13,13 @@ setupOpts = dict( name='pyqtgraph', description='Scientific Graphics and GUI Library for Python', long_description=DESCRIPTION, - license='MIT', + license = 'MIT', url='http://www.pyqtgraph.org', author='Luke Campagnola', author_email='luke.campagnola@gmail.com', classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", "Development Status :: 4 - Beta", @@ -145,4 +145,3 @@ setup( ], **setupOpts ) - diff --git a/tools/setupHelpers.py b/tools/setupHelpers.py index 4afec66b..6ebbfa46 100644 --- a/tools/setupHelpers.py +++ b/tools/setupHelpers.py @@ -10,14 +10,15 @@ except ImportError: output = proc.stdout.read() proc.wait() if proc.returncode != 0: - ex = Exception("Process had nonzero return value %d" % proc.returncode) + ex = Exception("Process had nonzero return value " + + "%d " % proc.returncode) ex.returncode = proc.returncode ex.output = output raise ex return output # Maximum allowed repository size difference (in kB) following merge. -# This is used to prevent large files from being inappropriately added to +# This is used to prevent large files from being inappropriately added to # the repository history. MERGE_SIZE_LIMIT = 100 @@ -42,19 +43,19 @@ FLAKE_MANDATORY = set([ 'E901', # SyntaxError or IndentationError 'E902', # IOError - + 'W191', # indentation contains tabs - + 'W601', # .has_key() is deprecated, use ‘in’ 'W602', # deprecated form of raising exception 'W603', # ‘<>’ is deprecated, use ‘!=’ - 'W604', # backticks are deprecated, use ‘repr()’ + 'W604', # backticks are deprecated, use ‘repr()’ ]) FLAKE_RECOMMENDED = set([ 'E124', # closing bracket does not match visual indentation 'E231', # missing whitespace after ‘,’ - + 'E211', # whitespace before ‘(‘ 'E261', # at least two spaces before inline comment 'E271', # multiple spaces after keyword @@ -65,10 +66,10 @@ FLAKE_RECOMMENDED = set([ 'F402', # import module from line N shadowed by loop variable 'F403', # ‘from module import *’ used; unable to detect undefined names 'F404', # future import(s) name after other statements - + 'E501', # line too long (82 > 79 characters) 'E502', # the backslash is redundant between brackets - + 'E702', # multiple statements on one line (semicolon) 'E703', # statement ends with a semicolon 'E711', # comparison to None should be ‘if cond is None:’ @@ -82,7 +83,7 @@ FLAKE_RECOMMENDED = set([ 'F823', # local variable name ... referenced before assignment 'F831', # duplicate argument name in function definition 'F841', # local variable name is assigned to but never used - + 'W292', # no newline at end of file ]) @@ -93,7 +94,7 @@ FLAKE_OPTIONAL = set([ 'E126', # continuation line over-indented for hanging indent 'E127', # continuation line over-indented for visual indent 'E128', # continuation line under-indented for visual indent - + 'E201', # whitespace after ‘(‘ 'E202', # whitespace before ‘)’ 'E203', # whitespace before ‘:’ @@ -105,19 +106,19 @@ FLAKE_OPTIONAL = set([ 'E228', # missing whitespace around modulo operator 'E241', # multiple spaces after ‘,’ 'E251', # unexpected spaces around keyword / parameter equals - 'E262', # inline comment should start with ‘# ‘ - + 'E262', # inline comment should start with ‘# ‘ + 'E301', # expected 1 blank line, found 0 'E302', # expected 2 blank lines, found 0 'E303', # too many blank lines (3) - + 'E401', # multiple imports on one line 'E701', # multiple statements on one line (colon) - + 'W291', # trailing whitespace 'W293', # blank line contains whitespace - + 'W391', # blank line at end of file ]) @@ -128,23 +129,10 @@ FLAKE_IGNORE = set([ ]) -#def checkStyle(): - #try: - #out = check_output(['flake8', '--select=%s' % FLAKE_TESTS, '--statistics', 'pyqtgraph/']) - #ret = 0 - #print("All style checks OK.") - #except Exception as e: - #out = e.output - #ret = e.returncode - #print(out.decode('utf-8')) - #return ret - - def checkStyle(): """ Run flake8, checking only lines that are modified since the last git commit. """ - test = [ 1,2,3 ] - + # First check _all_ code against mandatory error codes print('flake8: check all code against mandatory error set...') errors = ','.join(FLAKE_MANDATORY) @@ -154,39 +142,47 @@ def checkStyle(): output = proc.stdout.read().decode('utf-8') ret = proc.wait() printFlakeOutput(output) - + # Check for DOS newlines print('check line endings in all files...') count = 0 allowedEndings = set([None, '\n']) for path, dirs, files in os.walk('.'): + if path.startswith("." + os.path.sep + ".tox"): + continue for f in files: if os.path.splitext(f)[1] not in ('.py', '.rst'): continue filename = os.path.join(path, f) fh = open(filename, 'U') - x = fh.readlines() - endings = set(fh.newlines if isinstance(fh.newlines, tuple) else (fh.newlines,)) + _ = fh.readlines() + endings = set( + fh.newlines + if isinstance(fh.newlines, tuple) + else (fh.newlines,) + ) endings -= allowedEndings if len(endings) > 0: - print("\033[0;31m" + "File has invalid line endings: %s" % filename + "\033[0m") + print("\033[0;31m" + + "File has invalid line endings: " + + "%s" % filename + "\033[0m") ret = ret | 2 count += 1 print('checked line endings in %d files' % count) - - + + # Next check new code with optional error codes print('flake8: check new code against recommended error set...') diff = subprocess.check_output(['git', 'diff']) - proc = subprocess.Popen(['flake8', '--diff', #'--show-source', + proc = subprocess.Popen(['flake8', '--diff', # '--show-source', '--ignore=' + errors], - stdin=subprocess.PIPE, + stdin=subprocess.PIPE, stdout=subprocess.PIPE) proc.stdin.write(diff) proc.stdin.close() output = proc.stdout.read().decode('utf-8') ret |= printFlakeOutput(output) - + if ret == 0: print('style test passed.') else: @@ -244,14 +240,20 @@ def unitTests(): return ret -def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, targetRepo=None): +def checkMergeSize( + sourceBranch=None, + targetBranch=None, + sourceRepo=None, + targetRepo=None +): """ - Check that a git merge would not increase the repository size by MERGE_SIZE_LIMIT. + Check that a git merge would not increase the repository size by + MERGE_SIZE_LIMIT. """ if sourceBranch is None: sourceBranch = getGitBranch() sourceRepo = '..' - + if targetBranch is None: if sourceBranch == 'develop': targetBranch = 'develop' @@ -259,38 +261,38 @@ def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, target else: targetBranch = 'develop' targetRepo = '..' - + workingDir = '__merge-test-clone' - env = dict(TARGET_BRANCH=targetBranch, - SOURCE_BRANCH=sourceBranch, - TARGET_REPO=targetRepo, + env = dict(TARGET_BRANCH=targetBranch, + SOURCE_BRANCH=sourceBranch, + TARGET_REPO=targetRepo, SOURCE_REPO=sourceRepo, WORKING_DIR=workingDir, ) - + print("Testing merge size difference:\n" " SOURCE: {SOURCE_REPO} {SOURCE_BRANCH}\n" " TARGET: {TARGET_BRANCH} {TARGET_REPO}".format(**env)) - + setup = """ mkdir {WORKING_DIR} && cd {WORKING_DIR} && git init && git remote add -t {TARGET_BRANCH} target {TARGET_REPO} && - git fetch target {TARGET_BRANCH} && - git checkout -qf target/{TARGET_BRANCH} && + git fetch target {TARGET_BRANCH} && + git checkout -qf target/{TARGET_BRANCH} && git gc -q --aggressive """.format(**env) - + checkSize = """ - cd {WORKING_DIR} && + cd {WORKING_DIR} && du -s . | sed -e "s/\t.*//" """.format(**env) - + merge = """ cd {WORKING_DIR} && - git pull -q {SOURCE_REPO} {SOURCE_BRANCH} && + git pull -q {SOURCE_REPO} {SOURCE_BRANCH} && git gc -q --aggressive """.format(**env) - + try: print("Check out target branch:\n" + setup) check_call(setup, shell=True) @@ -300,13 +302,17 @@ def checkMergeSize(sourceBranch=None, targetBranch=None, sourceRepo=None, target check_call(merge, shell=True) mergeSize = int(check_output(checkSize, shell=True)) print("MERGE SIZE: %d kB" % mergeSize) - + diff = mergeSize - targetSize if diff <= MERGE_SIZE_LIMIT: print("DIFFERENCE: %d kB [OK]" % diff) return 0 else: - print("\033[0;31m" + "DIFFERENCE: %d kB [exceeds %d kB]" % (diff, MERGE_SIZE_LIMIT) + "\033[0m") + print("\033[0;31m" + + "DIFFERENCE: %d kB [exceeds %d kB]" % ( + diff, + MERGE_SIZE_LIMIT) + + "\033[0m") return 2 finally: if os.path.isdir(workingDir): @@ -327,7 +333,11 @@ def mergeTests(): def listAllPackages(pkgroot): path = os.getcwd() n = len(path.split(os.path.sep)) - subdirs = [i[0].split(os.path.sep)[n:] for i in os.walk(os.path.join(path, pkgroot)) if '__init__.py' in i[2]] + subdirs = [ + i[0].split(os.path.sep)[n:] + for i in os.walk(os.path.join(path, pkgroot)) + if '__init__.py' in i[2] + ] return ['.'.join(p) for p in subdirs] @@ -338,48 +348,61 @@ def getInitVersion(pkgroot): init = open(initfile).read() m = re.search(r'__version__ = (\S+)\n', init) if m is None or len(m.groups()) != 1: - raise Exception("Cannot determine __version__ from init file: '%s'!" % initfile) + raise Exception("Cannot determine __version__ from init file: " + + "'%s'!" % initfile) version = m.group(1).strip('\'\"') return version def gitCommit(name): """Return the commit ID for the given name.""" - commit = check_output(['git', 'show', name], universal_newlines=True).split('\n')[0] + commit = check_output( + ['git', 'show', name], + universal_newlines=True).split('\n')[0] assert commit[:7] == 'commit ' return commit[7:] def getGitVersion(tagPrefix): """Return a version string with information about this git checkout. - If the checkout is an unmodified, tagged commit, then return the tag version. - If this is not a tagged commit, return the output of ``git describe --tags``. + If the checkout is an unmodified, tagged commit, then return the tag + version + + If this is not a tagged commit, return the output of + ``git describe --tags`` + If this checkout has been modified, append "+" to the version. """ path = os.getcwd() if not os.path.isdir(os.path.join(path, '.git')): return None - - v = check_output(['git', 'describe', '--tags', '--dirty', '--match=%s*'%tagPrefix]).strip().decode('utf-8') - + + v = check_output(['git', + 'describe', + '--tags', + '--dirty', + '--match=%s*'%tagPrefix]).strip().decode('utf-8') + # chop off prefix assert v.startswith(tagPrefix) v = v[len(tagPrefix):] # split up version parts parts = v.split('-') - + # has working tree been modified? modified = False if parts[-1] == 'dirty': modified = True parts = parts[:-1] - + # have commits been added on top of last tagged version? # (git describe adds -NNN-gXXXXXXX if this is the case) local = None - if len(parts) > 2 and re.match(r'\d+', parts[-2]) and re.match(r'g[0-9a-f]{7}', parts[-1]): + if (len(parts) > 2 and + re.match(r'\d+', parts[-2]) and + re.match(r'g[0-9a-f]{7}', parts[-1])): local = parts[-1] parts = parts[:-2] - + gitVersion = '-'.join(parts) if local is not None: gitVersion += '+' + local @@ -389,7 +412,10 @@ def getGitVersion(tagPrefix): return gitVersion def getGitBranch(): - m = re.search(r'\* (.*)', check_output(['git', 'branch'], universal_newlines=True)) + m = re.search( + r'\* (.*)', + check_output(['git', 'branch'], + universal_newlines=True)) if m is None: return '' else: @@ -397,32 +423,33 @@ def getGitBranch(): def getVersionStrings(pkg): """ - Returns 4 version strings: - + Returns 4 version strings: + * the version string to use for this build, * version string requested with --force-version (or None) * version string that describes the current git checkout (or None). - * version string in the pkg/__init__.py, - + * version string in the pkg/__init__.py, + The first return value is (forceVersion or gitVersion or initVersion). """ - + ## Determine current version string from __init__.py initVersion = getInitVersion(pkgroot=pkg) - ## If this is a git checkout, try to generate a more descriptive version string + # If this is a git checkout + # try to generate a more descriptive version string try: gitVersion = getGitVersion(tagPrefix=pkg+'-') except: gitVersion = None - sys.stderr.write("This appears to be a git checkout, but an error occurred " - "while attempting to determine a version string for the " - "current commit.\n") + sys.stderr.write("This appears to be a git checkout, but an error " + "occurred while attempting to determine a version " + "string for the current commit.\n") sys.excepthook(*sys.exc_info()) # See whether a --force-version flag was given forcedVersion = None - for i,arg in enumerate(sys.argv): + for i, arg in enumerate(sys.argv): if arg.startswith('--force-version'): if arg == '--force-version': forcedVersion = sys.argv[i+1] @@ -431,8 +458,8 @@ def getVersionStrings(pkg): elif arg.startswith('--force-version='): forcedVersion = sys.argv[i].replace('--force-version=', '') sys.argv.pop(i) - - + + ## Finally decide on a version string to use: if forcedVersion is not None: version = forcedVersion @@ -443,7 +470,8 @@ def getVersionStrings(pkg): _, local = gitVersion.split('+') if local != '': version = version + '+' + local - sys.stderr.write("Detected git commit; will use version string: '%s'\n" % version) + sys.stderr.write("Detected git commit; " + + "will use version string: '%s'\n" % version) return version, forcedVersion, gitVersion, initVersion @@ -457,29 +485,31 @@ class DebCommand(Command): maintainer = "Luke Campagnola " debTemplate = "debian" debDir = "deb_build" - + user_options = [] - + def initialize_options(self): self.cwd = None - + def finalize_options(self): self.cwd = os.getcwd() - + def run(self): version = self.distribution.get_version() pkgName = self.distribution.get_name() debName = "python-" + pkgName debDir = self.debDir - - assert os.getcwd() == self.cwd, 'Must be in package root: %s' % self.cwd - + + assert os.getcwd() == self.cwd, 'Must be in package root: ' + + '%s' % self.cwd + if os.path.isdir(debDir): raise Exception('DEB build dir already exists: "%s"' % debDir) sdist = "dist/%s-%s.tar.gz" % (pkgName, version) if not os.path.isfile(sdist): - raise Exception("No source distribution; run `setup.py sdist` first.") - + raise Exception("No source distribution; " + + "run `setup.py sdist` first.") + # copy sdist to build directory and extract os.mkdir(debDir) renamedSdist = '%s_%s.orig.tar.gz' % (debName, version) @@ -489,16 +519,20 @@ class DebCommand(Command): if os.system("cd %s; tar -xzf %s" % (debDir, renamedSdist)) != 0: raise Exception("Error extracting source distribution.") buildDir = '%s/%s-%s' % (debDir, pkgName, version) - + # copy debian control structure print("copytree %s => %s" % (self.debTemplate, buildDir+'/debian')) shutil.copytree(self.debTemplate, buildDir+'/debian') - + # Write new changelog - chlog = generateDebianChangelog(pkgName, 'CHANGELOG', version, self.maintainer) + chlog = generateDebianChangelog( + pkgName, + 'CHANGELOG', + version, + self.maintainer) print("write changelog %s" % buildDir+'/debian/changelog') open(buildDir+'/debian/changelog', 'w').write(chlog) - + # build package print('cd %s; debuild -us -uc' % buildDir) if os.system('cd %s; debuild -us -uc' % buildDir) != 0: @@ -521,43 +555,45 @@ class DebugCommand(Command): class TestCommand(Command): - description = "Run all package tests and exit immediately with informative return code." + description = "Run all package tests and exit immediately with ", \ + "informative return code." user_options = [] - + def run(self): sys.exit(unitTests()) - + def initialize_options(self): pass - + def finalize_options(self): pass - + class StyleCommand(Command): - description = "Check all code for style, exit immediately with informative return code." + description = "Check all code for style, exit immediately with ", \ + "informative return code." user_options = [] - + def run(self): sys.exit(checkStyle()) - + def initialize_options(self): pass - + def finalize_options(self): pass - + class MergeTestCommand(Command): - description = "Run all tests needed to determine whether the current code is suitable for merge." + description = "Run all tests needed to determine whether the current ",\ + "code is suitable for merge." user_options = [] - + def run(self): sys.exit(mergeTests()) - + def initialize_options(self): pass - + def finalize_options(self): pass - From bbc11b96a9b3089ec23366a4afde5b804bc804cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jind=C5=99ich=20Makovi=C4=8Dka?= Date: Thu, 12 Sep 2019 23:50:43 +0200 Subject: [PATCH 451/607] Configurable GridItem tick spacing and pen color (#101) --- pyqtgraph/graphicsItems/GridItem.py | 123 ++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 17 deletions(-) diff --git a/pyqtgraph/graphicsItems/GridItem.py b/pyqtgraph/graphicsItems/GridItem.py index 87f90a62..0b1eb525 100644 --- a/pyqtgraph/graphicsItems/GridItem.py +++ b/pyqtgraph/graphicsItems/GridItem.py @@ -3,6 +3,7 @@ from .UIGraphicsItem import * import numpy as np from ..Point import Point from .. import functions as fn +from .. import getConfigOption __all__ = ['GridItem'] class GridItem(UIGraphicsItem): @@ -12,16 +13,75 @@ class GridItem(UIGraphicsItem): Displays a rectangular grid of lines indicating major divisions within a coordinate system. Automatically determines what divisions to use. """ - - def __init__(self): + + def __init__(self, pen='default', textPen='default'): UIGraphicsItem.__init__(self) #QtGui.QGraphicsItem.__init__(self, *args) #self.setFlag(QtGui.QGraphicsItem.ItemClipsToShape) #self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) - + + self.opts = {} + + self.setPen(pen) + self.setTextPen(textPen) + self.setTickSpacing(x=[None, None, None], y=[None, None, None]) + + + def setPen(self, *args, **kwargs): + """Set the pen used to draw the grid.""" + if kwargs == {} and (args == () or args == ('default',)): + self.opts['pen'] = fn.mkPen(getConfigOption('foreground')) + else: + self.opts['pen'] = fn.mkPen(*args, **kwargs) + self.picture = None - - + self.update() + + + def setTextPen(self, *args, **kwargs): + """Set the pen used to draw the texts.""" + if kwargs == {} and (args == () or args == ('default',)): + self.opts['textPen'] = fn.mkPen(getConfigOption('foreground')) + else: + if args == (None,): + self.opts['textPen'] = None + else: + self.opts['textPen'] = fn.mkPen(*args, **kargs) + + self.picture = None + self.update() + + + def setTickSpacing(self, x=None, y=None): + """ + Set the grid tick spacing to use. + + Tick spacing for each axis shall be specified as an array of + descending values, one for each tick scale. When the value + is set to None, grid line distance is chosen automatically + for this particular level. + + Example: + Default setting of 3 scales for each axis: + setTickSpacing(x=[None, None, None], y=[None, None, None]) + + Single scale with distance of 1.0 for X axis, Two automatic + scales for Y axis: + setTickSpacing(x=[1.0], y=[None, None]) + + Single scale with distance of 1.0 for X axis, Two scales + for Y axis, one with spacing of 1.0, other one automatic: + setTickSpacing(x=[1.0], y=[1.0, None]) + """ + self.opts['tickSpacing'] = (x or self.opts['tickSpacing'][0], + y or self.opts['tickSpacing'][1]) + + self.grid_depth = max([len(s) for s in self.opts['tickSpacing']]) + + self.picture = None + self.update() + + def viewRangeChanged(self): UIGraphicsItem.viewRangeChanged(self) self.picture = None @@ -48,7 +108,6 @@ class GridItem(UIGraphicsItem): p = QtGui.QPainter() p.begin(self.picture) - dt = fn.invertQTransform(self.viewTransform()) vr = self.getViewWidget().rect() unit = self.pixelWidth(), self.pixelHeight() dim = [vr.width(), vr.height()] @@ -62,10 +121,22 @@ class GridItem(UIGraphicsItem): x = ul[1] ul[1] = br[1] br[1] = x - for i in [2,1,0]: ## Draw three different scales of grid + + lastd = [None, None] + for i in range(self.grid_depth - 1, -1, -1): dist = br-ul nlTarget = 10.**i + d = 10. ** np.floor(np.log10(abs(dist/nlTarget))+0.5) + for ax in range(0,2): + ts = self.opts['tickSpacing'][ax] + try: + if ts[i] is not None: + d[ax] = ts[i] + except IndexError: + pass + lastd[ax] = d[ax] + ul1 = np.floor(ul / d) * d br1 = np.ceil(br / d) * d dist = br1-ul1 @@ -76,12 +147,25 @@ class GridItem(UIGraphicsItem): #print " d", d #print " nl", nl for ax in range(0,2): ## Draw grid for both axes + if i >= len(self.opts['tickSpacing'][ax]): + continue + if d[ax] < lastd[ax]: + continue + ppl = dim[ax] / nl[ax] - c = np.clip(3.*(ppl-3), 0., 30.) - linePen = QtGui.QPen(QtGui.QColor(255, 255, 255, c)) - textPen = QtGui.QPen(QtGui.QColor(255, 255, 255, c*2)) - #linePen.setCosmetic(True) - #linePen.setWidth(1) + c = np.clip(5.*(ppl-3), 0., 50.) + + linePen = self.opts['pen'] + lineColor = self.opts['pen'].color() + lineColor.setAlpha(c) + linePen.setColor(lineColor) + + textPen = self.opts['textPen'] + if textPen is not None: + textColor = self.opts['textPen'].color() + textColor.setAlpha(c * 2) + textPen.setColor(textColor) + bx = (ax+1) % 2 for x in range(0, int(nl[ax])): linePen.setCosmetic(False) @@ -102,8 +186,7 @@ class GridItem(UIGraphicsItem): if p1[ax] < min(ul[ax], br[ax]) or p1[ax] > max(ul[ax], br[ax]): continue p.drawLine(QtCore.QPointF(p1[0], p1[1]), QtCore.QPointF(p2[0], p2[1])) - if i < 2: - p.setPen(textPen) + if i < 2 and textPen is not None: if ax == 0: x = p1[0] + unit[0] y = ul[1] + unit[1] * 8. @@ -114,7 +197,13 @@ class GridItem(UIGraphicsItem): tr = self.deviceTransform() #tr.scale(1.5, 1.5) p.setWorldTransform(fn.invertQTransform(tr)) - for t in texts: - x = tr.map(t[0]) + Point(0.5, 0.5) - p.drawText(x, t[1]) + + if textPen is not None and len(texts) > 0: + # if there is at least one text, then c is set + textColor.setAlpha(c * 2) + p.setPen(QtGui.QPen(textColor)) + for t in texts: + x = tr.map(t[0]) + Point(0.5, 0.5) + p.drawText(x, t[1]) + p.end() From e3884ebd20fd6580633e01ba141a6af200abc90e Mon Sep 17 00:00:00 2001 From: miranis <33010847+miranis@users.noreply.github.com> Date: Fri, 13 Sep 2019 05:24:48 +0200 Subject: [PATCH 452/607] Update GraphicsScene.py (#599) In lines 174 and 191 cev[0] is being accessed when cev is an empty list. I get this error when inheriting from GraphicsLayoutWidget and overloading mouseDoubleClickEvent. --- pyqtgraph/GraphicsScene/GraphicsScene.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 01b6b808..785031d5 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -183,12 +183,14 @@ class GraphicsScene(QtGui.QGraphicsScene): if int(ev.buttons() & btn) == 0: continue if int(btn) not in self.dragButtons: ## see if we've dragged far enough yet - cev = [e for e in self.clickEvents if int(e.button()) == int(btn)][0] - dist = Point(ev.scenePos() - cev.scenePos()).length() - if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime): - continue - init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True - self.dragButtons.append(int(btn)) + cev = [e for e in self.clickEvents if int(e.button()) == int(btn)] + if cev: + cev = cev[0] + dist = Point(ev.scenePos() - cev.scenePos()).length() + if dist == 0 or (dist < self._moveDistance and now - cev.time() < self.minDragTime): + continue + init = init or (len(self.dragButtons) == 0) ## If this is the first button to be dragged, then init=True + self.dragButtons.append(int(btn)) ## If we have dragged buttons, deliver a drag event if len(self.dragButtons) > 0: @@ -208,10 +210,11 @@ class GraphicsScene(QtGui.QGraphicsScene): self.dragButtons.remove(ev.button()) else: cev = [e for e in self.clickEvents if int(e.button()) == int(ev.button())] - if self.sendClickEvent(cev[0]): - #print "sent click event" - ev.accept() - self.clickEvents.remove(cev[0]) + if cev: + if self.sendClickEvent(cev[0]): + #print "sent click event" + ev.accept() + self.clickEvents.remove(cev[0]) if int(ev.buttons()) == 0: self.dragItem = None From 8309b5301480a623c7bf4c70fba12a47979b1bf0 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 13 Sep 2019 06:00:38 +0200 Subject: [PATCH 453/607] Fix: Reset ParentItem to None on removing from PlotItem/ViewBox (#1031) --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 ++-- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 2158b1a1..f3849b99 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -563,8 +563,8 @@ class PlotItem(GraphicsWidget): if item in self.dataItems: self.dataItems.remove(item) - if item.scene() is not None: - self.vb.removeItem(item) + self.vb.removeItem(item) + if item in self.curves: self.curves.remove(item) self.updateDecimation() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 6c6e3718..9c71d7db 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -399,10 +399,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() @@ -413,7 +415,12 @@ class ViewBox(GraphicsWidget): self.addedItems.remove(item) except: pass - self.scene().removeItem(item) + + scene = self.scene() + if scene is not None: + scene.removeItem(item) + item.setParentItem(None) + self.updateAutoRange() def clear(self): From bfd36dc2038d77cf33ec5bef0c6a5fcb6948c9bf Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 13 Sep 2019 06:30:39 +0200 Subject: [PATCH 454/607] Prevent element-wise string comparison (code by @flutefreak7) (#1024) --- pyqtgraph/graphicsItems/ImageItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ImageItem.py b/pyqtgraph/graphicsItems/ImageItem.py index 1758bb4d..b05c2f70 100644 --- a/pyqtgraph/graphicsItems/ImageItem.py +++ b/pyqtgraph/graphicsItems/ImageItem.py @@ -488,7 +488,7 @@ class ImageItem(GraphicsObject): step = (step, step) stepData = self.image[::step[0], ::step[1]] - if 'auto' == bins: + if isinstance(bins, str) and bins == 'auto': mn = np.nanmin(stepData) mx = np.nanmax(stepData) if mx == mn: From 061a30e827a85fa20dbf94a141e67bf48d19aff7 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 13 Sep 2019 06:58:49 +0200 Subject: [PATCH 455/607] Correctly include SI units for log AxisItems (#972) --- pyqtgraph/graphicsItems/AxisItem.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 3a92e2b1..088ba6b8 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -444,7 +444,11 @@ class AxisItem(GraphicsWidget): def updateAutoSIPrefix(self): if self.label.isVisible(): - (scale, prefix) = fn.siScale(max(abs(self.range[0]*self.scale), abs(self.range[1]*self.scale))) + if self.logMode: + _range = 10**np.array(self.range) + else: + _range = self.range + (scale, prefix) = fn.siScale(max(abs(_range[0]*self.scale), abs(_range[1]*self.scale))) if self.labelUnits == '' and prefix in ['k', 'm']: ## If we are not showing units, wait until 1e6 before scaling. scale = 1.0 prefix = '' @@ -771,7 +775,7 @@ class AxisItem(GraphicsWidget): return strings def logTickStrings(self, values, scale, spacing): - return ["%0.1g"%x for x in 10 ** np.array(values).astype(float)] + return ["%0.1g"%x for x in 10 ** np.array(values).astype(float) * np.array(scale)] def generateDrawSpecs(self, p): """ From 3edbef6c57702fc77acf2d60980c74a58ab84fc5 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sat, 9 Jul 2016 20:25:52 -0400 Subject: [PATCH 456/607] Ensure exported images use integer dimensions. It seems that the parameter tree doesn't enforce the int type very strongly. Also, use some local variables more often. --- pyqtgraph/exporters/ImageExporter.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index 69c02508..e600afc9 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -58,17 +58,17 @@ class ImageExporter(Exporter): filter.insert(0, p) self.fileSaveDialog(filter=filter) return - - targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) - sourceRect = self.getSourceRect() - - - #self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32) - #self.png.fill(pyqtgraph.mkColor(self.params['background'])) - w, h = self.params['width'], self.params['height'] + + w = int(self.params['width']) + h = int(self.params['height']) if w == 0 or h == 0: - raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h)) - bg = np.empty((self.params['height'], self.params['width'], 4), dtype=np.ubyte) + raise Exception("Cannot export image with size=0 (requested " + "export size is %dx%d)" % (w, h)) + + targetRect = QtCore.QRect(0, 0, w, h) + sourceRect = self.getSourceRect() + + bg = np.empty((h, w, 4), dtype=np.ubyte) color = self.params['background'] bg[:,:,0] = color.blue() bg[:,:,1] = color.green() From d726a9693ec73c863b761fa9cadaac6ba32de971 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 9 Sep 2019 00:27:29 -0400 Subject: [PATCH 457/607] Fix UnboundLocalError in VideoSpeedTest. --- examples/VideoSpeedTest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/VideoSpeedTest.py b/examples/VideoSpeedTest.py index f123ccc3..7131f9d1 100644 --- a/examples/VideoSpeedTest.py +++ b/examples/VideoSpeedTest.py @@ -103,6 +103,7 @@ def mkData(): dt = np.float loc = 1.0 scale = 0.1 + mx = 1.0 if ui.rgbCheck.isChecked(): data = np.random.normal(size=(frames,width,height,3), loc=loc, scale=scale) From 2e900898904ba1ec903d83c4d24114f4dc3b34e4 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Mon, 9 Sep 2019 00:28:19 -0400 Subject: [PATCH 458/607] Fix undefined reduce call. --- pyqtgraph/opengl/items/GLBarGraphItem.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/opengl/items/GLBarGraphItem.py b/pyqtgraph/opengl/items/GLBarGraphItem.py index b3060dc9..42d05fb7 100644 --- a/pyqtgraph/opengl/items/GLBarGraphItem.py +++ b/pyqtgraph/opengl/items/GLBarGraphItem.py @@ -8,7 +8,7 @@ class GLBarGraphItem(GLMeshItem): pos is (...,3) array of the bar positions (the corner of each bar) size is (...,3) array of the sizes of each bar """ - nCubes = reduce(lambda a,b: a*b, pos.shape[:-1]) + nCubes = np.prod(pos.shape[:-1]) cubeVerts = np.mgrid[0:2,0:2,0:2].reshape(3,8).transpose().reshape(1,8,3) cubeFaces = np.array([ [0,1,2], [3,2,1], @@ -22,8 +22,5 @@ class GLBarGraphItem(GLMeshItem): verts = cubeVerts * size + pos faces = cubeFaces + (np.arange(nCubes) * 8).reshape(nCubes,1,1) md = MeshData(verts.reshape(nCubes*8,3), faces.reshape(nCubes*12,3)) - - GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False) - - \ No newline at end of file + GLMeshItem.__init__(self, meshdata=md, shader='shaded', smooth=False) From c94b1cb99eeb49475ce759f7485727c5f2165c2f Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 13 Sep 2019 22:04:48 -0700 Subject: [PATCH 459/607] Always update transform when setting angle of a TextItem (#970) * Always update transform when setting angle of a TextItem * Add test to check TextItem.setAngle * Relax test a bit but still check that setAngle has an effect * Add docstring to setAngle * Remove unneeded numpy testing function imports --- pyqtgraph/graphicsItems/TextItem.py | 15 ++++++++---- .../graphicsItems/tests/test_TextItem.py | 23 +++++++++++++++++++ 2 files changed, 34 insertions(+), 4 deletions(-) create mode 100644 pyqtgraph/graphicsItems/tests/test_TextItem.py diff --git a/pyqtgraph/graphicsItems/TextItem.py b/pyqtgraph/graphicsItems/TextItem.py index b2587ded..9dc17960 100644 --- a/pyqtgraph/graphicsItems/TextItem.py +++ b/pyqtgraph/graphicsItems/TextItem.py @@ -110,9 +110,16 @@ class TextItem(GraphicsObject): self.updateTextPos() def setAngle(self, angle): + """ + Set the angle of the text in degrees. + + This sets the rotation angle of the text as a whole, measured + counter-clockwise from the x axis of the parent. Note that this rotation + angle does not depend on horizontal/vertical scaling of the parent. + """ self.angle = angle - self.updateTransform() - + self.updateTransform(force=True) + def setAnchor(self, anchor): self.anchor = Point(anchor) self.updateTextPos() @@ -169,7 +176,7 @@ class TextItem(GraphicsObject): p.setRenderHint(p.Antialiasing, True) p.drawPolygon(self.textItem.mapToParent(self.textItem.boundingRect())) - def updateTransform(self): + def updateTransform(self, force=False): # update transform such that this item has the correct orientation # and scaling relative to the scene, but inherits its position from its # parent. @@ -181,7 +188,7 @@ class TextItem(GraphicsObject): else: pt = p.sceneTransform() - if pt == self._lastTransform: + if not force and pt == self._lastTransform: return t = pt.inverted()[0] diff --git a/pyqtgraph/graphicsItems/tests/test_TextItem.py b/pyqtgraph/graphicsItems/tests/test_TextItem.py new file mode 100644 index 00000000..6667dfc5 --- /dev/null +++ b/pyqtgraph/graphicsItems/tests/test_TextItem.py @@ -0,0 +1,23 @@ +import pytest +import pyqtgraph as pg + +app = pg.mkQApp() + + +def test_TextItem_setAngle(): + plt = pg.plot() + plt.setXRange(-10, 10) + plt.setYRange(-20, 20) + item = pg.TextItem(text="test") + plt.addItem(item) + + t1 = item.transform() + + item.setAngle(30) + app.processEvents() + + t2 = item.transform() + + assert t1 != t2 + assert not t1.isRotating() + assert t2.isRotating() From 8c137a1caf30bdf8f25ece4859a19bcfcceb88be Mon Sep 17 00:00:00 2001 From: Aikhjarto Date: Sat, 14 Sep 2019 07:12:23 +0200 Subject: [PATCH 460/607] fix: circular texture was slightly off-center (#1012) --- pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index 828b0f0c..636c1621 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -61,7 +61,7 @@ class GLScatterPlotItem(GLGraphicsItem): ## Generate texture for rendering points w = 64 def fn(x,y): - r = ((x-w/2.)**2 + (y-w/2.)**2) ** 0.5 + r = ((x-(w-1)/2.)**2 + (y-(w-1)/2.)**2) ** 0.5 return 255 * (w/2. - np.clip(r, w/2.-1.0, w/2.)) pData = np.empty((w, w, 4)) pData[:] = 255 From df28c41d4b92f868cab1ae021ac1fd4e146182f5 Mon Sep 17 00:00:00 2001 From: lidstrom83 Date: Fri, 13 Sep 2019 23:08:28 -0700 Subject: [PATCH 461/607] Make DockArea compatible with Qt Designer (#158) --- pyqtgraph/dockarea/DockArea.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/dockarea/DockArea.py b/pyqtgraph/dockarea/DockArea.py index b7b0659e..ff3f22ab 100644 --- a/pyqtgraph/dockarea/DockArea.py +++ b/pyqtgraph/dockarea/DockArea.py @@ -9,9 +9,9 @@ from ..python2_3 import basestring class DockArea(Container, QtGui.QWidget, DockDrop): - def __init__(self, temporary=False, home=None): + def __init__(self, parent=None, temporary=False, home=None): Container.__init__(self, self) - QtGui.QWidget.__init__(self) + QtGui.QWidget.__init__(self, parent=parent) DockDrop.__init__(self, allowedAreas=['left', 'right', 'top', 'bottom']) self.layout = QtGui.QVBoxLayout() self.layout.setContentsMargins(0,0,0,0) From b7f1aa88cd69dcf6685171efc48e0f1abfb863eb Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 22 Sep 2019 17:03:30 -0700 Subject: [PATCH 462/607] Cast to int after division --- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index fb3f6ea6..20db0abe 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore try: from ..Qt import QtOpenGL @@ -570,7 +571,7 @@ class PlotCurveItem(GraphicsObject): gl.glEnable(gl.GL_BLEND) gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA) gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST) - gl.glDrawArrays(gl.GL_LINE_STRIP, 0, pos.size / pos.shape[-1]) + gl.glDrawArrays(gl.GL_LINE_STRIP, 0, int(pos.size / pos.shape[-1])) finally: gl.glDisableClientState(gl.GL_VERTEX_ARRAY) finally: @@ -638,4 +639,3 @@ class ROIPlotItem(PlotCurveItem): def roiChangedEvent(self): d = self.getRoiData() self.updateData(d, self.xVals) - From ed264802a2d9e7cab0846e75a7a028e332e99def Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 23 Sep 2019 21:19:01 -0700 Subject: [PATCH 463/607] Raise AttributeError in __getattr__ --- pyqtgraph/graphicsWindows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index b6598685..f1315005 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -48,7 +48,7 @@ class TabWindow(QtGui.QMainWindow): if hasattr(self.cw, attr): return getattr(self.cw, attr) else: - raise NameError(attr) + raise AttributeError(attr) class PlotWindow(PlotWidget): From b57d45df6de3eaafcb0b50017d3d8badb6d2f05e Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 23 Sep 2019 21:25:52 -0700 Subject: [PATCH 464/607] The lazier way --- pyqtgraph/graphicsWindows.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index f1315005..b6a321ee 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -45,10 +45,7 @@ class TabWindow(QtGui.QMainWindow): self.show() def __getattr__(self, attr): - if hasattr(self.cw, attr): - return getattr(self.cw, attr) - else: - raise AttributeError(attr) + return getattr(self.cw, attr) class PlotWindow(PlotWidget): From aa3a5d39958291d13f69f5fa78ffd9b698780937 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Thu, 26 Sep 2019 20:08:43 +0300 Subject: [PATCH 465/607] remote legacy work-around for old numpy errors (#1046) * remote legacy work-around for old numpy errors * forgot to remove the numpy_fix import * require numyp >= 1.8.0 --- pyqtgraph/__init__.py | 3 --- pyqtgraph/numpy_fix.py | 22 ---------------------- setup.py | 2 +- 3 files changed, 1 insertion(+), 26 deletions(-) delete mode 100644 pyqtgraph/numpy_fix.py diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index b1aa98aa..bdb4fe15 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -29,9 +29,6 @@ if sys.version_info[0] < 2 or (sys.version_info[0] == 2 and sys.version_info[1] ## helpers for 2/3 compatibility from . import python2_3 -## install workarounds for numpy bugs -from . import numpy_fix - ## in general openGL is poorly supported with Qt+GraphicsView. ## we only enable it where the performance benefit is critical. ## Note this only applies to 2D graphics; 3D graphics always use OpenGL. diff --git a/pyqtgraph/numpy_fix.py b/pyqtgraph/numpy_fix.py deleted file mode 100644 index 2fa8ef1f..00000000 --- a/pyqtgraph/numpy_fix.py +++ /dev/null @@ -1,22 +0,0 @@ -try: - import numpy as np - - ## Wrap np.concatenate to catch and avoid a segmentation fault bug - ## (numpy trac issue #2084) - if not hasattr(np, 'concatenate_orig'): - np.concatenate_orig = np.concatenate - def concatenate(vals, *args, **kwds): - """Wrapper around numpy.concatenate (see pyqtgraph/numpy_fix.py)""" - dtypes = [getattr(v, 'dtype', None) for v in vals] - names = [getattr(dt, 'names', None) for dt in dtypes] - if len(dtypes) < 2 or all([n is None for n in names]): - return np.concatenate_orig(vals, *args, **kwds) - if any([dt != dtypes[0] for dt in dtypes[1:]]): - raise TypeError("Cannot concatenate structured arrays of different dtype.") - return np.concatenate_orig(vals, *args, **kwds) - - np.concatenate = concatenate - -except ImportError: - pass - diff --git a/setup.py b/setup.py index 38ee477a..aa1bb787 100644 --- a/setup.py +++ b/setup.py @@ -141,7 +141,7 @@ setup( package_dir={'pyqtgraph.examples': 'examples'}, ## install examples along with the rest of the source package_data={'pyqtgraph.examples': ['optics/*.gz', 'relativity/presets/*.cfg']}, install_requires = [ - 'numpy', + 'numpy>=1.8.0', ], **setupOpts ) From 96a4270a30b0fb273b1ae916a7ab9373ced77690 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Fri, 27 Sep 2019 22:02:54 +0200 Subject: [PATCH 466/607] Fix HistogramLUTWidget with background parameter (#953) * Fix HistogramLUTWidget with background parameter HistogramLUTWidget cannot be initialized with the `background` parameter, because all parameters are also passed to the constructor of HistogramLUTItem which does not have a `background` parameter. This pull request fixes that issue by defining `background` explicitly as parameter in the function header. Closes #175 * Added test for HistogramLUTWidget initialization with background * Fixed Python2 compatibility * Do not pg.exit() after test * Moved test_histogramlutwidget to widget tests --- pyqtgraph/widgets/HistogramLUTWidget.py | 2 +- .../widgets/tests/test_histogramlutwidget.py | 44 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) create mode 100644 pyqtgraph/widgets/tests/test_histogramlutwidget.py diff --git a/pyqtgraph/widgets/HistogramLUTWidget.py b/pyqtgraph/widgets/HistogramLUTWidget.py index 9aec837c..5259900c 100644 --- a/pyqtgraph/widgets/HistogramLUTWidget.py +++ b/pyqtgraph/widgets/HistogramLUTWidget.py @@ -13,7 +13,7 @@ __all__ = ['HistogramLUTWidget'] class HistogramLUTWidget(GraphicsView): def __init__(self, parent=None, *args, **kargs): - background = kargs.get('background', 'default') + background = kargs.pop('background', 'default') GraphicsView.__init__(self, parent, useOpenGL=False, background=background) self.item = HistogramLUTItem(*args, **kargs) self.setCentralItem(self.item) diff --git a/pyqtgraph/widgets/tests/test_histogramlutwidget.py b/pyqtgraph/widgets/tests/test_histogramlutwidget.py new file mode 100644 index 00000000..f8a381a7 --- /dev/null +++ b/pyqtgraph/widgets/tests/test_histogramlutwidget.py @@ -0,0 +1,44 @@ +""" +HistogramLUTWidget test: + +Tests the creation of a HistogramLUTWidget. +""" + +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui +import numpy as np + +def testHistogramLUTWidget(): + pg.mkQApp() + + win = QtGui.QMainWindow() + win.show() + + cw = QtGui.QWidget() + win.setCentralWidget(cw) + + l = QtGui.QGridLayout() + cw.setLayout(l) + l.setSpacing(0) + + v = pg.GraphicsView() + vb = pg.ViewBox() + vb.setAspectLocked() + v.setCentralItem(vb) + l.addWidget(v, 0, 0, 3, 1) + + w = pg.HistogramLUTWidget(background='w') + l.addWidget(w, 0, 1) + + data = pg.gaussianFilter(np.random.normal(size=(256, 256, 3)), (20, 20, 0)) + for i in range(32): + for j in range(32): + data[i*8, j*8] += .1 + img = pg.ImageItem(data) + vb.addItem(img) + vb.autoRange() + + w.setImageItem(img) + + QtGui.QApplication.processEvents() + From 071e4295357d6cb0cd7c2c2a1cc50a2ba8547c16 Mon Sep 17 00:00:00 2001 From: Mi! Date: Fri, 27 Sep 2019 22:31:47 +0200 Subject: [PATCH 467/607] makeRGBA/ImageItem: Applying alpha mask on numpy.nan data values (#406) * Applying alpha mask on numpy.nan data values * Typesafe, checking for `data.dtype.kind` --- pyqtgraph/functions.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 2c11b647..5cbb177e 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -78,7 +78,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True): pref = SI_PREFIXES_ASCII[m+8] p = .001**m - return (p, pref) + return (p, pref) def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True): @@ -1035,7 +1035,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): ============== ================================================================================== """ profile = debug.Profiler() - if data.ndim not in (2, 3): raise TypeError("data must be 2D or 3D") if data.ndim == 3 and data.shape[2] > 4: @@ -1083,7 +1082,12 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): dtype = np.ubyte else: dtype = np.min_scalar_type(lut.shape[0]-1) - + + # awkward, but fastest numpy native nan evaluation + # + nanMask = None + if data.dtype.kind == 'f' and np.isnan(data.min()): + nanMask = np.isnan(data) # Apply levels if given if levels is not None: if isinstance(levels, np.ndarray) and levels.ndim == 2: @@ -1106,10 +1110,8 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if minVal == maxVal: maxVal = np.nextafter(maxVal, 2*maxVal) data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype) - profile() - # apply LUT if given if lut is not None: data = applyLookupTable(data, lut) @@ -1152,7 +1154,12 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): imgData[..., 3] = 255 else: alpha = True - + + # apply nan mask through alpha channel + if nanMask is not None: + alpha = True + imgData[nanMask, 3] = 0 + profile() return imgData, alpha From 61ec73a741ebee50c1ba8cf55bbb4ea9e7b685bb Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 27 Sep 2019 13:37:40 -0700 Subject: [PATCH 468/607] Close windows at the end of test functions (#1042) * Close windows at the end of test functions * Can't show window deletion warning during interpreter shutdown starting --- pyqtgraph/exporters/tests/test_svg.py | 1 + pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 2 ++ pyqtgraph/graphicsItems/tests/test_AxisItem.py | 2 ++ pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py | 2 ++ pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py | 3 ++- pyqtgraph/graphicsItems/tests/test_PlotDataItem.py | 2 ++ pyqtgraph/widgets/GraphicsView.py | 6 +++++- 7 files changed, 16 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/exporters/tests/test_svg.py b/pyqtgraph/exporters/tests/test_svg.py index 2261f7df..62946368 100644 --- a/pyqtgraph/exporters/tests/test_svg.py +++ b/pyqtgraph/exporters/tests/test_svg.py @@ -28,6 +28,7 @@ def test_plotscene(): ex.export(fileName=tempfilename) # clean up after the test is done os.unlink(tempfilename) + w.close() def test_simple(): tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 1d831b02..5a8ca141 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -71,6 +71,8 @@ def test_ViewBox(): size1 = QRectF(0, h, w, -h) assertMapping(vb, view1, size1) + win.close() + skipreason = "Skipping this test until someone has time to fix it." @pytest.mark.skipif(True, reason=skipreason) diff --git a/pyqtgraph/graphicsItems/tests/test_AxisItem.py b/pyqtgraph/graphicsItems/tests/test_AxisItem.py index f076890d..22dccdb4 100644 --- a/pyqtgraph/graphicsItems/tests/test_AxisItem.py +++ b/pyqtgraph/graphicsItems/tests/test_AxisItem.py @@ -28,3 +28,5 @@ def test_AxisItem_stopAxisAtTick(monkeypatch): monkeypatch.setattr(left, "drawPicture", test_left) plot.show() + app.processEvents() + plot.close() diff --git a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py index 4ee25e45..2b922c1e 100644 --- a/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py +++ b/pyqtgraph/graphicsItems/tests/test_ErrorBarItem.py @@ -35,3 +35,5 @@ def test_ErrorBarItem_defer_data(): r_clear_ebi = plot.viewRect() assert r_clear_ebi == r_no_ebi + + plot.close() diff --git a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py index a3c34b11..6d60d3e1 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py @@ -27,7 +27,8 @@ def test_PlotCurveItem(): c.setData(data, connect=np.array([1,1,1,0,1,1,0,0,1,0,0,0,1,1,0,0])) assertImageApproved(p, 'plotcurveitem/connectarray', "Plot curve with connection array.") - + + p.close() if __name__ == '__main__': diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index adc525d9..894afc74 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -86,3 +86,5 @@ def test_clipping(): assert xDisp[0] <= vr.left() assert xDisp[-1] >= vr.right() + + w.close() diff --git a/pyqtgraph/widgets/GraphicsView.py b/pyqtgraph/widgets/GraphicsView.py index 1be1a274..86b43222 100644 --- a/pyqtgraph/widgets/GraphicsView.py +++ b/pyqtgraph/widgets/GraphicsView.py @@ -411,7 +411,11 @@ class GraphicsView(QtGui.QGraphicsView): try: if self.parentWidget() is None and self.isVisible(): msg = "Visible window deleted. To prevent this, store a reference to the window object." - warnings.warn(msg, RuntimeWarning, stacklevel=2) + try: + warnings.warn(msg, RuntimeWarning, stacklevel=2) + except TypeError: + # warnings module not available during interpreter shutdown + pass except RuntimeError: pass From ed6586c7ddd3ad95eadd40cb8fda7c4c1d4c746b Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Mon, 30 Sep 2019 18:15:03 +0200 Subject: [PATCH 469/607] Removed unnecessary enlargement of bounding box (#1048) --- pyqtgraph/graphicsItems/InfiniteLine.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 7aeb1620..36505026 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -314,8 +314,8 @@ class InfiniteLine(GraphicsObject): length = br.width() left = br.left() + length * self.span[0] right = br.left() + length * self.span[1] - br.setLeft(left - w) - br.setRight(right + w) + br.setLeft(left) + br.setRight(right) br = br.normalized() vs = self.getViewBox().size() From f2740f7e69087f462b457336b081217ad82cd29e Mon Sep 17 00:00:00 2001 From: Agamemnon Krasoulis Date: Tue, 22 Oct 2019 17:45:45 +0100 Subject: [PATCH 470/607] Fix typo in documentation (#1062) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 20db0abe..05b11b4d 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -273,7 +273,7 @@ class PlotCurveItem(GraphicsObject): self.update() def setShadowPen(self, *args, **kargs): - """Set the shadow pen used to draw behind tyhe primary pen. + """Set the shadow pen used to draw behind the primary pen. This pen must have a larger width than the primary pen to be visible. """ From a84953530f2090917c77442709887acfaa9730f0 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Mon, 28 Oct 2019 13:59:20 +0100 Subject: [PATCH 471/607] Fix: setEnableMenu in ViewBox --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 9c71d7db..a12eb519 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -324,7 +324,8 @@ class ViewBox(GraphicsWidget): if self.state['enableMenu'] and self.menu is None: self.menu = ViewBoxMenu(self) self.updateViewLists() - else: + elif not self.state['enableMenu'] and self.menu is not None: + self.menu.setParent(None) self.menu = None self.updateViewRange() @@ -380,11 +381,10 @@ class ViewBox(GraphicsWidget): def setMenuEnabled(self, enableMenu=True): self.state['enableMenu'] = enableMenu - if enableMenu: - if self.menu is None: - self.menu = ViewBoxMenu(self) - self.updateViewLists() - else: + if enableMenu and self.menu is None: + self.menu = ViewBoxMenu(self) + self.updateViewLists() + elif not enableMenu and self.menu is not None: self.menu.setParent(None) self.menu = None self.sigStateChanged.emit(self) From cb4d9b23b2144e6b23b8022c973f249c7c855586 Mon Sep 17 00:00:00 2001 From: wuyuanyi135 Date: Sun, 3 Nov 2019 00:36:58 -0400 Subject: [PATCH 472/607] fix flowchart context menu redundant menu (#1060) --- pyqtgraph/flowchart/Flowchart.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 5aeeac38..2e7ed0eb 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -834,9 +834,9 @@ class FlowchartWidget(dockarea.DockArea): def buildMenu(self, pos=None): def buildSubMenu(node, rootMenu, subMenus, pos=None): for section, node in node.items(): - menu = QtGui.QMenu(section) - rootMenu.addMenu(menu) - if isinstance(node, OrderedDict): + if isinstance(node, OrderedDict): + menu = QtGui.QMenu(section) + rootMenu.addMenu(menu) buildSubMenu(node, menu, subMenus, pos=pos) subMenus.append(menu) else: From b1df230964ce5b9bdc7140b888939f0c12b40376 Mon Sep 17 00:00:00 2001 From: Eugene Prilepin Date: Sun, 3 Nov 2019 07:51:20 +0300 Subject: [PATCH 473/607] Remove 'global' for CONFIG_OPTIONS because it is redundant for dict (#1055) --- pyqtgraph/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index bdb4fe15..aad5c3c8 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -64,7 +64,6 @@ CONFIG_OPTIONS = { def setConfigOption(opt, value): - global CONFIG_OPTIONS if opt not in CONFIG_OPTIONS: raise KeyError('Unknown configuration option "%s"' % opt) if opt == 'imageAxisOrder' and value not in ('row-major', 'col-major'): From 684882455773f410e07c0dd16977e5696edaf6ce Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Sun, 3 Nov 2019 06:00:06 +0100 Subject: [PATCH 474/607] add bookkeeping exporter parameters (#1023) --- pyqtgraph/GraphicsScene/exportDialog.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/GraphicsScene/exportDialog.py b/pyqtgraph/GraphicsScene/exportDialog.py index 8085c5bf..045698fe 100644 --- a/pyqtgraph/GraphicsScene/exportDialog.py +++ b/pyqtgraph/GraphicsScene/exportDialog.py @@ -23,6 +23,8 @@ class ExportDialog(QtGui.QWidget): self.currentExporter = None self.scene = scene + self.exporterParameters = {} + self.selectBox = QtGui.QGraphicsRectItem() self.selectBox.setPen(fn.mkPen('y', width=3, style=QtCore.Qt.DashLine)) self.selectBox.hide() @@ -121,7 +123,18 @@ class ExportDialog(QtGui.QWidget): return expClass = self.exporterClasses[str(item.text())] exp = expClass(item=self.ui.itemTree.currentItem().gitem) - params = exp.parameters() + + 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 + if params is None: self.ui.paramTree.clear() else: From 50cf2f561f10e0ff88d767a4f03c4d48ec530115 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Wed, 6 Nov 2019 10:58:00 +0100 Subject: [PATCH 475/607] Move common code to _applyEnableMenu --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index a12eb519..27e64b56 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -321,13 +321,7 @@ class ViewBox(GraphicsWidget): self.state.update(state) - if self.state['enableMenu'] and self.menu is None: - self.menu = ViewBoxMenu(self) - self.updateViewLists() - elif not self.state['enableMenu'] and self.menu is not None: - self.menu.setParent(None) - self.menu = None - + self._applyMenuEnabled() self.updateViewRange() self.sigStateChanged.emit(self) @@ -381,16 +375,20 @@ class ViewBox(GraphicsWidget): def setMenuEnabled(self, enableMenu=True): self.state['enableMenu'] = enableMenu + self._applyMenuEnabled() + self.sigStateChanged.emit(self) + + def menuEnabled(self): + return self.state.get('enableMenu', True) + + def _applyMenuEnabled(self): + enableMenu = self.state.get("enableMenu", True) if enableMenu and self.menu is None: self.menu = ViewBoxMenu(self) self.updateViewLists() elif not enableMenu and self.menu is not None: self.menu.setParent(None) self.menu = None - self.sigStateChanged.emit(self) - - def menuEnabled(self): - return self.state.get('enableMenu', True) def addItem(self, item, ignoreBounds=False): """ From a65b8c91f7fc207b17466e77ab814848b186b077 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Wed, 6 Nov 2019 10:59:51 +0100 Subject: [PATCH 476/607] Add simple test for setEnableMenu --- pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index 5a8ca141..9495bfc3 100644 --- a/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -74,6 +74,14 @@ def test_ViewBox(): win.close() +def test_ViewBox_setMenuEnabled(): + init_viewbox() + vb.setMenuEnabled(True) + assert vb.menu is not None + vb.setMenuEnabled(False) + assert vb.menu is None + + skipreason = "Skipping this test until someone has time to fix it." @pytest.mark.skipif(True, reason=skipreason) def test_limits_and_resize(): From f5e25622a788d931a6cf4ff59d35d23f24e703a9 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 12 Nov 2019 08:36:16 -0800 Subject: [PATCH 477/607] Validate min/max text inputs in ViewBoxMenu (#1074) --- .../graphicsItems/ViewBox/ViewBoxMenu.py | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 74a861d0..1f44bdd6 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ...Qt import QtCore, QtGui, QT_LIB from ...python2_3 import asUnicode from ...WidgetGroup import WidgetGroup @@ -48,8 +49,8 @@ class ViewBoxMenu(QtGui.QMenu): connects = [ (ui.mouseCheck.toggled, 'MouseToggled'), (ui.manualRadio.clicked, 'ManualClicked'), - (ui.minText.editingFinished, 'MinTextChanged'), - (ui.maxText.editingFinished, 'MaxTextChanged'), + (ui.minText.editingFinished, 'RangeTextChanged'), + (ui.maxText.editingFinished, 'RangeTextChanged'), (ui.autoRadio.clicked, 'AutoClicked'), (ui.autoPercentSpin.valueChanged, 'AutoSpinChanged'), (ui.linkCombo.currentIndexChanged, 'LinkComboChanged'), @@ -162,14 +163,10 @@ class ViewBoxMenu(QtGui.QMenu): def xManualClicked(self): self.view().enableAutoRange(ViewBox.XAxis, False) - def xMinTextChanged(self): + def xRangeTextChanged(self): self.ctrl[0].manualRadio.setChecked(True) - self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0) + self.view().setXRange(*self._validateRangeText(0), padding=0) - def xMaxTextChanged(self): - self.ctrl[0].manualRadio.setChecked(True) - self.view().setXRange(float(self.ctrl[0].minText.text()), float(self.ctrl[0].maxText.text()), padding=0) - def xAutoClicked(self): val = self.ctrl[0].autoPercentSpin.value() * 0.01 self.view().enableAutoRange(ViewBox.XAxis, val) @@ -194,13 +191,9 @@ class ViewBoxMenu(QtGui.QMenu): def yManualClicked(self): self.view().enableAutoRange(ViewBox.YAxis, False) - def yMinTextChanged(self): + def yRangeTextChanged(self): self.ctrl[1].manualRadio.setChecked(True) - self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0) - - def yMaxTextChanged(self): - self.ctrl[1].manualRadio.setChecked(True) - self.view().setYRange(float(self.ctrl[1].minText.text()), float(self.ctrl[1].maxText.text()), padding=0) + self.view().setYRange(*self._validateRangeText(1), padding=0) def yAutoClicked(self): val = self.ctrl[1].autoPercentSpin.value() * 0.01 @@ -265,6 +258,20 @@ class ViewBoxMenu(QtGui.QMenu): if changed: c.setCurrentIndex(0) c.currentIndexChanged.emit(c.currentIndex()) + + def _validateRangeText(self, axis): + """Validate range text inputs. Return current value(s) if invalid.""" + inputs = (self.ctrl[axis].minText.text(), + self.ctrl[axis].maxText.text()) + vals = self.view().viewRange()[axis] + for i, text in enumerate(inputs): + try: + vals[i] = float(text) + except ValueError: + # could not convert string to float + pass + return vals + from .ViewBox import ViewBox From faef56c3e7801b80bfb75a8bb9c8fc5c7dfdb955 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 12 Nov 2019 08:45:42 -0800 Subject: [PATCH 478/607] Qulogic py3 fixes (#1073) * py3k: Remove reduce calls. * py3k: Remove compatibility sortList function. Sorting by key has existed since Python 2.4. * Remove unnecessary sys.path manipulation. This file doesn't have any __main__ code to run anyway. * Use context manager --- pyqtgraph/GraphicsScene/GraphicsScene.py | 7 +- pyqtgraph/__init__.py | 3 +- pyqtgraph/canvas/Canvas.py | 4 - pyqtgraph/configfile.py | 25 ++-- pyqtgraph/console/Console.py | 7 +- pyqtgraph/exporters/CSVExporter.py | 43 +++--- pyqtgraph/graphicsItems/GradientEditorItem.py | 8 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 134 ++++++++++-------- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 18 ++- pyqtgraph/metaarray/MetaArray.py | 5 +- pyqtgraph/multiprocess/parallelizer.py | 17 +-- pyqtgraph/opengl/items/GLScatterPlotItem.py | 2 +- pyqtgraph/pixmaps/compile.py | 6 +- pyqtgraph/python2_3.py | 35 +---- pyqtgraph/reload.py | 21 ++- pyqtgraph/tests/test_exit_crash.py | 4 +- pyqtgraph/widgets/TableWidget.py | 3 +- pyqtgraph/widgets/ValueLabel.py | 3 +- 18 files changed, 163 insertions(+), 182 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index 785031d5..b61f3a1b 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,6 +1,6 @@ +# -*- coding: utf-8 -*- import weakref from ..Qt import QtCore, QtGui -from ..python2_3 import sortList, cmp from ..Point import Point from .. import functions as fn from .. import ptime as ptime @@ -454,7 +454,7 @@ class GraphicsScene(QtGui.QGraphicsScene): return 0 return item.zValue() + absZValue(item.parentItem()) - sortList(items2, lambda a,b: cmp(absZValue(b), absZValue(a))) + items2.sort(key=absZValue, reverse=True) return items2 @@ -563,6 +563,3 @@ class GraphicsScene(QtGui.QGraphicsScene): @staticmethod def translateGraphicsItems(items): return list(map(GraphicsScene.translateGraphicsItem, items)) - - - diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index aad5c3c8..5f816245 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -95,7 +95,8 @@ def systemInfo(): if __version__ is None: ## this code was probably checked out from bzr; look up the last-revision file lastRevFile = os.path.join(os.path.dirname(__file__), '..', '.bzr', 'branch', 'last-revision') if os.path.exists(lastRevFile): - rev = open(lastRevFile, 'r').read().strip() + with open(lastRevFile, 'r') as fd: + rev = fd.read().strip() print("pyqtgraph: %s; %s" % (__version__, rev)) print("config:") diff --git a/pyqtgraph/canvas/Canvas.py b/pyqtgraph/canvas/Canvas.py index 72d70d7e..2ec13b19 100644 --- a/pyqtgraph/canvas/Canvas.py +++ b/pyqtgraph/canvas/Canvas.py @@ -1,8 +1,4 @@ # -*- coding: utf-8 -*- -if __name__ == '__main__': - import sys, os - md = os.path.dirname(os.path.abspath(__file__)) - sys.path = [os.path.dirname(md), os.path.join(md, '..', '..', '..')] + sys.path from ..Qt import QtGui, QtCore, QT_LIB from ..graphicsItems.ROI import ROI diff --git a/pyqtgraph/configfile.py b/pyqtgraph/configfile.py index 0cc8f030..6ae8a0c5 100644 --- a/pyqtgraph/configfile.py +++ b/pyqtgraph/configfile.py @@ -39,10 +39,10 @@ class ParseError(Exception): def writeConfigFile(data, fname): s = genString(data) - fd = open(fname, 'w') - fd.write(s) - fd.close() - + with open(fname, 'w') as fd: + fd.write(s) + + def readConfigFile(fname): #cwd = os.getcwd() global GLOBAL_PATH @@ -55,9 +55,8 @@ def readConfigFile(fname): try: #os.chdir(newDir) ## bad. - fd = open(fname) - s = asUnicode(fd.read()) - fd.close() + with open(fname) as fd: + s = asUnicode(fd.read()) s = s.replace("\r\n", "\n") s = s.replace("\r", "\n") data = parseString(s)[1] @@ -73,9 +72,8 @@ def readConfigFile(fname): def appendConfigFile(data, fname): s = genString(data) - fd = open(fname, 'a') - fd.write(s) - fd.close() + with open(fname, 'a') as fd: + fd.write(s) def genString(data, indent=''): @@ -194,8 +192,6 @@ def measureIndent(s): if __name__ == '__main__': import tempfile - fn = tempfile.mktemp() - tf = open(fn, 'w') cf = """ key: 'value' key2: ##comment @@ -205,8 +201,9 @@ key2: ##comment key22: [1,2,3] key23: 234 #comment """ - tf.write(cf) - tf.close() + fn = tempfile.mktemp() + with open(fn, 'w') as tf: + tf.write(cf) print("=== Test:===") num = 1 for line in cf.split('\n'): diff --git a/pyqtgraph/console/Console.py b/pyqtgraph/console/Console.py index 477beb77..aac32d63 100644 --- a/pyqtgraph/console/Console.py +++ b/pyqtgraph/console/Console.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import sys, re, os, time, traceback, subprocess import pickle @@ -98,12 +99,14 @@ class ConsoleWidget(QtGui.QWidget): def loadHistory(self): """Return the list of previously-invoked command strings (or None).""" if self.historyFile is not None: - return pickle.load(open(self.historyFile, 'rb')) + with open(self.historyFile, 'rb') as pf: + return pickle.load(pf) def saveHistory(self, history): """Store the list of previously-invoked command strings.""" if self.historyFile is not None: - pickle.dump(open(self.historyFile, 'wb'), history) + with open(self.historyFile, 'wb') as pf: + pickle.dump(pf, history) def runCmd(self, cmd): self.stdout = sys.stdout diff --git a/pyqtgraph/exporters/CSVExporter.py b/pyqtgraph/exporters/CSVExporter.py index b87f0182..c7591932 100644 --- a/pyqtgraph/exporters/CSVExporter.py +++ b/pyqtgraph/exporters/CSVExporter.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from .Exporter import Exporter from ..parametertree import Parameter @@ -29,7 +30,6 @@ class CSVExporter(Exporter): self.fileSaveDialog(filter=["*.csv", "*.tsv"]) return - fd = open(fileName, 'w') data = [] header = [] @@ -55,28 +55,29 @@ class CSVExporter(Exporter): sep = ',' else: sep = '\t' - - fd.write(sep.join(header) + '\n') - i = 0 - numFormat = '%%0.%dg' % self.params['precision'] - numRows = max([len(d[0]) for d in data]) - for i in range(numRows): - for j, d in enumerate(data): - # write x value if this is the first column, or if we want x - # for all rows - if appendAllX or j == 0: - if d is not None and i < len(d[0]): - fd.write(numFormat % d[0][i] + sep) + + with open(fileName, 'w') as fd: + fd.write(sep.join(header) + '\n') + i = 0 + numFormat = '%%0.%dg' % self.params['precision'] + numRows = max([len(d[0]) for d in data]) + for i in range(numRows): + for j, d in enumerate(data): + # write x value if this is the first column, or if we want + # x for all rows + if appendAllX or j == 0: + if d is not None and i < len(d[0]): + fd.write(numFormat % d[0][i] + sep) + else: + fd.write(' %s' % sep) + + # write y value + if d is not None and i < len(d[1]): + fd.write(numFormat % d[1][i] + sep) else: fd.write(' %s' % sep) - - # write y value - if d is not None and i < len(d[1]): - fd.write(numFormat % d[1][i] + sep) - else: - fd.write(' %s' % sep) - fd.write('\n') - fd.close() + fd.write('\n') + CSVExporter.register() diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index fc1d638c..b360b2f7 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -1,14 +1,14 @@ +# -*- coding: utf-8 -*- +import operator import weakref import numpy as np from ..Qt import QtGui, QtCore -from ..python2_3 import sortList from .. import functions as fn from .GraphicsObject import GraphicsObject from .GraphicsWidget import GraphicsWidget from ..widgets.SpinBox import SpinBox from ..pgcollections import OrderedDict from ..colormap import ColorMap -from ..python2_3 import cmp __all__ = ['TickSliderItem', 'GradientEditorItem'] @@ -352,8 +352,7 @@ class TickSliderItem(GraphicsWidget): def listTicks(self): """Return a sorted list of all the Tick objects on the slider.""" ## public - ticks = list(self.ticks.items()) - sortList(ticks, lambda a,b: cmp(a[1], b[1])) ## see pyqtgraph.python2_3.sortList + ticks = sorted(self.ticks.items(), key=operator.itemgetter(1)) return ticks @@ -944,4 +943,3 @@ class TickMenu(QtGui.QMenu): # self.fracPosSpin.blockSignals(True) # self.fracPosSpin.setValue(self.sliderItem().tickValue(self.tick())) # self.fracPosSpin.blockSignals(False) - diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index f3849b99..cf588912 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -677,7 +677,6 @@ class PlotItem(GraphicsWidget): xRange = rect.left(), rect.right() svg = "" - fh = open(fileName, 'w') dx = max(rect.right(),0) - min(rect.left(),0) ymn = min(rect.top(), rect.bottom()) @@ -691,52 +690,68 @@ class PlotItem(GraphicsWidget): sy *= 1000 sy *= -1 - fh.write('\n') - fh.write('\n' % (rect.left()*sx, rect.right()*sx)) - fh.write('\n' % (rect.top()*sy, rect.bottom()*sy)) + with open(fileName, 'w') as fh: + # fh.write('\n' % (rect.left() * sx, + # rect.top() * sx, + # rect.width() * sy, + # rect.height()*sy)) + fh.write('\n') + fh.write('\n' % ( + rect.left() * sx, rect.right() * sx)) + fh.write('\n' % ( + rect.top() * sy, rect.bottom() * sy)) - for item in self.curves: - if isinstance(item, PlotCurveItem): - color = fn.colorStr(item.pen.color()) - opacity = item.pen.color().alpha() / 255. - color = color[:6] - x, y = item.getData() - mask = (x > xRange[0]) * (x < xRange[1]) - mask[:-1] += mask[1:] - m2 = mask.copy() - mask[1:] += m2[:-1] - x = x[mask] - y = y[mask] - - x *= sx - y *= sy - - fh.write('') - - for item in self.dataItems: - if isinstance(item, ScatterPlotItem): - - pRect = item.boundingRect() - vRect = pRect.intersected(rect) - - for point in item.points(): - pos = point.pos() - if not rect.contains(pos): - continue - color = fn.colorStr(point.brush.color()) - opacity = point.brush.color().alpha() / 255. + for item in self.curves: + if isinstance(item, PlotCurveItem): + color = fn.colorStr(item.pen.color()) + opacity = item.pen.color().alpha() / 255. color = color[:6] - x = pos.x() * sx - y = pos.y() * sy - - fh.write('\n' % (x, y, color, opacity)) - - fh.write("\n") - + x, y = item.getData() + mask = (x > xRange[0]) * (x < xRange[1]) + mask[:-1] += mask[1:] + m2 = mask.copy() + mask[1:] += m2[:-1] + x = x[mask] + y = y[mask] + + x *= sx + y *= sy + + # fh.write('\n' % ( + # color, )) + fh.write('') + # fh.write("") + + for item in self.dataItems: + if isinstance(item, ScatterPlotItem): + pRect = item.boundingRect() + vRect = pRect.intersected(rect) + + for point in item.points(): + pos = point.pos() + if not rect.contains(pos): + continue + color = fn.colorStr(point.brush.color()) + opacity = point.brush.color().alpha() / 255. + color = color[:6] + x = pos.x() * sx + y = pos.y() * sy + + fh.write('\n' % ( + x, y, color, opacity)) + + fh.write("\n") + def writeSvg(self, fileName=None): if fileName is None: self._chooseFilenameDialog(handler=self.writeSvg) @@ -766,22 +781,21 @@ class PlotItem(GraphicsWidget): fileName = str(fileName) PlotItem.lastFileDir = os.path.dirname(fileName) - fd = open(fileName, 'w') data = [c.getData() for c in self.curves] - i = 0 - while True: - done = True - for d in data: - if i < len(d[0]): - fd.write('%g,%g,'%(d[0][i], d[1][i])) - done = False - else: - fd.write(' , ,') - fd.write('\n') - if done: - break - i += 1 - fd.close() + with open(fileName, 'w') as fd: + i = 0 + while True: + done = True + for d in data: + if i < len(d[0]): + fd.write('%g,%g,' % (d[0][i], d[1][i])) + done = False + else: + fd.write(' , ,') + fd.write('\n') + if done: + break + i += 1 def saveState(self): state = self.stateGroup.state() diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 9c71d7db..2cd6c28f 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1,9 +1,10 @@ +# -*- coding: utf-8 -*- import weakref import sys from copy import deepcopy import numpy as np from ...Qt import QtGui, QtCore -from ...python2_3 import sortList, basestring, cmp +from ...python2_3 import basestring from ...Point import Point from ... import functions as fn from .. ItemGroup import ItemGroup @@ -1603,16 +1604,13 @@ class ViewBox(GraphicsWidget): self.window() except RuntimeError: ## this view has already been deleted; it will probably be collected shortly. return - - def cmpViews(a, b): - wins = 100 * cmp(a.window() is self.window(), b.window() is self.window()) - alpha = cmp(a.name, b.name) - return wins + alpha - + + def view_key(view): + return (view.window() is self.window(), view.name) + ## make a sorted list of all named views - nv = list(ViewBox.NamedViews.values()) - sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList - + nv = sorted(ViewBox.NamedViews.values(), key=view_key) + if self in nv: nv.remove(self) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index f157c588..690ff49d 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -12,7 +12,6 @@ More info at http://www.scipy.org/Cookbook/MetaArray import types, copy, threading, os, re import pickle -from functools import reduce import numpy as np from ..python2_3 import basestring #import traceback @@ -844,7 +843,7 @@ class MetaArray(object): frames = [] frameShape = list(meta['shape']) frameShape[dynAxis] = 1 - frameSize = reduce(lambda a,b: a*b, frameShape) + frameSize = np.prod(frameShape) n = 0 while True: ## Extract one non-blank line @@ -1298,7 +1297,7 @@ class MetaArray(object): #frames = [] #frameShape = list(meta['shape']) #frameShape[dynAxis] = 1 - #frameSize = reduce(lambda a,b: a*b, frameShape) + #frameSize = np.prod(frameShape) #n = 0 #while True: ### Extract one non-blank line diff --git a/pyqtgraph/multiprocess/parallelizer.py b/pyqtgraph/multiprocess/parallelizer.py index 989bd4f8..b0f064bd 100644 --- a/pyqtgraph/multiprocess/parallelizer.py +++ b/pyqtgraph/multiprocess/parallelizer.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os, sys, time, multiprocessing, re from .processes import ForkedProcess from .remoteproxy import ClosedError @@ -213,14 +214,14 @@ class Parallelize(object): try: cores = {} pid = None - - for line in open('/proc/cpuinfo'): - m = re.match(r'physical id\s+:\s+(\d+)', line) - if m is not None: - pid = m.groups()[0] - m = re.match(r'cpu cores\s+:\s+(\d+)', line) - if m is not None: - cores[pid] = int(m.groups()[0]) + with open('/proc/cpuinfo') as fd: + for line in fd: + m = re.match(r'physical id\s+:\s+(\d+)', line) + if m is not None: + pid = m.groups()[0] + m = re.match(r'cpu cores\s+:\s+(\d+)', line) + if m is not None: + cores[pid] = int(m.groups()[0]) return sum(cores.values()) except: return multiprocessing.cpu_count() diff --git a/pyqtgraph/opengl/items/GLScatterPlotItem.py b/pyqtgraph/opengl/items/GLScatterPlotItem.py index 636c1621..463ad742 100644 --- a/pyqtgraph/opengl/items/GLScatterPlotItem.py +++ b/pyqtgraph/opengl/items/GLScatterPlotItem.py @@ -123,7 +123,7 @@ class GLScatterPlotItem(GLGraphicsItem): try: pos = self.pos #if pos.ndim > 2: - #pos = pos.reshape((reduce(lambda a,b: a*b, pos.shape[:-1]), pos.shape[-1])) + #pos = pos.reshape((-1, pos.shape[-1])) glVertexPointerf(pos) if isinstance(self.color, np.ndarray): diff --git a/pyqtgraph/pixmaps/compile.py b/pyqtgraph/pixmaps/compile.py index fa0d2408..68fd2da1 100644 --- a/pyqtgraph/pixmaps/compile.py +++ b/pyqtgraph/pixmaps/compile.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import numpy as np from PyQt4 import QtGui import os, pickle, sys @@ -14,6 +15,5 @@ for f in os.listdir(path): arr = np.asarray(ptr).reshape(img.height(), img.width(), 4).transpose(1,0,2) pixmaps[f] = pickle.dumps(arr) ver = sys.version_info[0] -fh = open(os.path.join(path, 'pixmapData_%d.py' %ver), 'w') -fh.write("import numpy as np; pixmapData=%s" % repr(pixmaps)) - +with open(os.path.join(path, 'pixmapData_%d.py' % (ver, )), 'w') as fh: + fh.write("import numpy as np; pixmapData=%s" % (repr(pixmaps), )) diff --git a/pyqtgraph/python2_3.py b/pyqtgraph/python2_3.py index ae4667eb..952b49b1 100644 --- a/pyqtgraph/python2_3.py +++ b/pyqtgraph/python2_3.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Helper functions that smooth out the differences between python 2 and 3. """ @@ -13,46 +14,12 @@ def asUnicode(x): return unicode(x) else: return str(x) - -def cmpToKey(mycmp): - 'Convert a cmp= function into a key= function' - class K(object): - def __init__(self, obj, *args): - self.obj = obj - def __lt__(self, other): - return mycmp(self.obj, other.obj) < 0 - def __gt__(self, other): - return mycmp(self.obj, other.obj) > 0 - def __eq__(self, other): - return mycmp(self.obj, other.obj) == 0 - def __le__(self, other): - return mycmp(self.obj, other.obj) <= 0 - def __ge__(self, other): - return mycmp(self.obj, other.obj) >= 0 - def __ne__(self, other): - return mycmp(self.obj, other.obj) != 0 - return K -def sortList(l, cmpFunc): - if sys.version_info[0] == 2: - l.sort(cmpFunc) - else: - l.sort(key=cmpToKey(cmpFunc)) if sys.version_info[0] == 3: basestring = str - def cmp(a,b): - if a>b: - return 1 - elif b > a: - return -1 - else: - return 0 xrange = range else: import __builtin__ basestring = __builtin__.basestring - cmp = __builtin__.cmp xrange = __builtin__.xrange - - \ No newline at end of file diff --git a/pyqtgraph/reload.py b/pyqtgraph/reload.py index f6c630b9..b0c875f1 100644 --- a/pyqtgraph/reload.py +++ b/pyqtgraph/reload.py @@ -306,7 +306,8 @@ if __name__ == '__main__': import os if not os.path.isdir('test1'): os.mkdir('test1') - open('test1/__init__.py', 'w') + with open('test1/__init__.py', 'w'): + pass modFile1 = "test1/test1.py" modCode1 = """ import sys @@ -345,8 +346,10 @@ def fn(): print("fn: %s") """ - open(modFile1, 'w').write(modCode1%(1,1)) - open(modFile2, 'w').write(modCode2%"message 1") + with open(modFile1, 'w') as f: + f.write(modCode1 % (1, 1)) + with open(modFile2, 'w') as f: + f.write(modCode2 % ("message 1", )) import test1.test1 as test1 import test2 print("Test 1 originals:") @@ -382,7 +385,8 @@ def fn(): c1.fn() os.remove(modFile1+'c') - open(modFile1, 'w').write(modCode1%(2,2)) + with open(modFile1, 'w') as f: + f.write(modCode1 %(2, 2)) print("\n----RELOAD test1-----\n") reloadAll(os.path.abspath(__file__)[:10], debug=True) @@ -393,7 +397,8 @@ def fn(): os.remove(modFile2+'c') - open(modFile2, 'w').write(modCode2%"message 2") + with open(modFile2, 'w') as f: + f.write(modCode2 % ("message 2", )) print("\n----RELOAD test2-----\n") reloadAll(os.path.abspath(__file__)[:10], debug=True) @@ -429,8 +434,10 @@ def fn(): os.remove(modFile1+'c') os.remove(modFile2+'c') - open(modFile1, 'w').write(modCode1%(3,3)) - open(modFile2, 'w').write(modCode2%"message 3") + with open(modFile1, 'w') as f: + f.write(modCode1 % (3, 3)) + with open(modFile2, 'w') as f: + f.write(modCode2 % ("message 3", )) print("\n----RELOAD-----\n") reloadAll(os.path.abspath(__file__)[:10], debug=True) diff --git a/pyqtgraph/tests/test_exit_crash.py b/pyqtgraph/tests/test_exit_crash.py index 50924908..5a10a0a3 100644 --- a/pyqtgraph/tests/test_exit_crash.py +++ b/pyqtgraph/tests/test_exit_crash.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import os import sys import subprocess @@ -59,7 +60,8 @@ def test_exit_crash(): print(name) argstr = initArgs.get(name, "") - open(tmp, 'w').write(code.format(path=path, classname=name, args=argstr)) + with open(tmp, 'w') as f: + f.write(code.format(path=path, classname=name, args=argstr)) proc = subprocess.Popen([sys.executable, tmp]) assert proc.wait() == 0 diff --git a/pyqtgraph/widgets/TableWidget.py b/pyqtgraph/widgets/TableWidget.py index 90b56139..0378b5fc 100644 --- a/pyqtgraph/widgets/TableWidget.py +++ b/pyqtgraph/widgets/TableWidget.py @@ -355,7 +355,8 @@ class TableWidget(QtGui.QTableWidget): fileName = fileName[0] # Qt4/5 API difference if fileName == '': return - open(str(fileName), 'w').write(data) + with open(fileName, 'w') as fd: + fd.write(data) def contextMenuEvent(self, ev): self.contextMenu.popup(ev.globalPos()) diff --git a/pyqtgraph/widgets/ValueLabel.py b/pyqtgraph/widgets/ValueLabel.py index 4e5b3011..b24fb16c 100644 --- a/pyqtgraph/widgets/ValueLabel.py +++ b/pyqtgraph/widgets/ValueLabel.py @@ -1,7 +1,6 @@ from ..Qt import QtCore, QtGui from ..ptime import time from .. import functions as fn -from functools import reduce __all__ = ['ValueLabel'] @@ -54,7 +53,7 @@ class ValueLabel(QtGui.QLabel): self.averageTime = t def averageValue(self): - return reduce(lambda a,b: a+b, [v[1] for v in self.values]) / float(len(self.values)) + return sum(v[1] for v in self.values) / float(len(self.values)) def paintEvent(self, ev): From ec445e76015143b49642722d7c3bd9746eaf8f45 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 12 Nov 2019 09:01:49 -0800 Subject: [PATCH 479/607] HDF5Exporter handling of ragged curves with tests (#1072) * HDF5Exporter handles ragged curves by saving them into different datasets based on their names. * Add HDF5Exporter tests * Document HDF5Exporter * Fix tmp file path --- azure-test-template.yml | 4 +- doc/source/exporting.rst | 7 ++- pyqtgraph/exporters/HDF5Exporter.py | 31 ++++++----- pyqtgraph/exporters/tests/test_hdf5.py | 71 ++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 pyqtgraph/exporters/tests/test_hdf5.py diff --git a/azure-test-template.yml b/azure-test-template.yml index 81e4399c..5d204009 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -116,9 +116,9 @@ jobs: conda install -c conda-forge $(qt.bindings) --yes fi echo "Installing remainder of dependencies" - conda install -c conda-forge numpy scipy six pyopengl --yes + conda install -c conda-forge numpy scipy six pyopengl h5py --yes else - pip install $(qt.bindings) numpy scipy pyopengl six + pip install $(qt.bindings) numpy scipy pyopengl six h5py fi echo "" pip install pytest pytest-xdist pytest-cov coverage diff --git a/doc/source/exporting.rst b/doc/source/exporting.rst index ccd017d7..0bb1c82a 100644 --- a/doc/source/exporting.rst +++ b/doc/source/exporting.rst @@ -30,8 +30,13 @@ Export Formats for export. * Printer - Exports to the operating system's printing service. This exporter is provided for completeness, but is not well supported due to problems with Qt's printing system. +* HDF5 - Exports data from a :class:`~pyqtgraph.PlotItem` to a HDF5 file if + h5py_ is installed. This exporter supports :class:`~pyqtgraph.PlotItem` + objects containing multiple curves, stacking the data into a single HDF5 + dataset based on the ``columnMode`` parameter. If data items aren't the same + size, each one is given its own dataset. - +.. _h5py: https://www.h5py.org/ Exporting from the API ---------------------- diff --git a/pyqtgraph/exporters/HDF5Exporter.py b/pyqtgraph/exporters/HDF5Exporter.py index 584a9f71..2a2ac19c 100644 --- a/pyqtgraph/exporters/HDF5Exporter.py +++ b/pyqtgraph/exporters/HDF5Exporter.py @@ -44,20 +44,27 @@ class HDF5Exporter(Exporter): data = [] appendAllX = self.params['columnMode'] == '(x,y) per plot' - #print dir(self.item.curves[0]) - tlen = 0 - for i, c in enumerate(self.item.curves): - d = c.getData() - if i > 0 and len(d[0]) != tlen: - raise ValueError ("HDF5 Export requires all curves in plot to have same length") - if appendAllX or i == 0: - data.append(d[0]) - tlen = len(d[0]) - data.append(d[1]) + # Check if the arrays are ragged + len_first = len(self.item.curves[0].getData()[0]) if self.item.curves[0] else None + ragged = any(len(i.getData()[0]) != len_first for i in self.item.curves) + if ragged: + dgroup = fd.create_group(dsname) + for i, c in enumerate(self.item.curves): + d = c.getData() + fdata = numpy.array([d[0], d[1]]).astype('double') + cname = c.name() if c.name() is not None else str(i) + dset = dgroup.create_dataset(cname, data=fdata) + else: + for i, c in enumerate(self.item.curves): + d = c.getData() + if appendAllX or i == 0: + data.append(d[0]) + data.append(d[1]) + + fdata = numpy.array(data).astype('double') + dset = fd.create_dataset(dsname, data=fdata) - fdata = numpy.array(data).astype('double') - dset = fd.create_dataset(dsname, data=fdata) fd.close() if HAVE_HDF5: diff --git a/pyqtgraph/exporters/tests/test_hdf5.py b/pyqtgraph/exporters/tests/test_hdf5.py new file mode 100644 index 00000000..69bb8ae7 --- /dev/null +++ b/pyqtgraph/exporters/tests/test_hdf5.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +import pytest +import pyqtgraph as pg +from pyqtgraph.exporters import HDF5Exporter +import numpy as np +from numpy.testing import assert_equal +import h5py +import os + + +@pytest.fixture +def tmp_h5(tmp_path): + yield tmp_path / "data.h5" + + +@pytest.mark.parametrize("combine", [False, True]) +def test_HDF5Exporter(tmp_h5, combine): + # Basic test of functionality: multiple curves with shared x array. Tests + # both options for stacking the data (columnMode). + x = np.linspace(0, 1, 100) + y1 = np.sin(x) + y2 = np.cos(x) + + plt = pg.plot() + plt.plot(x=x, y=y1) + plt.plot(x=x, y=y2) + + ex = HDF5Exporter(plt.plotItem) + + if combine: + ex.parameters()['columnMode'] = '(x,y,y,y) for all plots' + + ex.export(fileName=tmp_h5) + + with h5py.File(tmp_h5, 'r') as f: + # should be a single dataset with the name of the exporter + dset = f[ex.parameters()['Name']] + assert isinstance(dset, h5py.Dataset) + + if combine: + assert_equal(np.array([x, y1, y2]), dset) + else: + assert_equal(np.array([x, y1, x, y2]), dset) + + +def test_HDF5Exporter_unequal_lengths(tmp_h5): + # Test export with multiple curves of different size. The exporter should + # detect this and create multiple hdf5 datasets under a group. + x1 = np.linspace(0, 1, 10) + y1 = np.sin(x1) + x2 = np.linspace(0, 1, 100) + y2 = np.cos(x2) + + plt = pg.plot() + plt.plot(x=x1, y=y1, name='plot0') + plt.plot(x=x2, y=y2) + + ex = HDF5Exporter(plt.plotItem) + ex.export(fileName=tmp_h5) + + with h5py.File(tmp_h5, 'r') as f: + # should be a group with the name of the exporter + group = f[ex.parameters()['Name']] + assert isinstance(group, h5py.Group) + + # should be a dataset under the group with the name of the PlotItem + assert_equal(np.array([x1, y1]), group['plot0']) + + # should be a dataset under the group with a default name that's the + # index of the curve in the PlotItem + assert_equal(np.array([x2, y2]), group['1']) From 220393339323ff9d23e5f3070c3ebd00f93e46a9 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Tue, 12 Nov 2019 09:02:08 -0800 Subject: [PATCH 480/607] Declare scipy optional (#1067) * Replace use of scipy.random with numpy.random * Update README to reflect scipy being an optional depenency --- README.md | 3 ++- examples/MultiPlotWidget.py | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 28143078..914523fd 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,9 @@ Requirements * PyQt 4.8+, PySide, PyQt5, or PySide2 * python 2.7, or 3.x * Required - * `numpy`, `scipy` + * `numpy` * Optional + * `scipy` for image processing * `pyopengl` for 3D graphics * macOS with Python2 and Qt4 bindings (PyQt4 or PySide) do not work with 3D OpenGL graphics * `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy` diff --git a/examples/MultiPlotWidget.py b/examples/MultiPlotWidget.py index 5ab4b21d..67cb83ee 100644 --- a/examples/MultiPlotWidget.py +++ b/examples/MultiPlotWidget.py @@ -3,8 +3,7 @@ ## Add path to library (just for examples; you do not need this) import initExample - -from scipy import random +import numpy as np from numpy import linspace from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph as pg @@ -22,7 +21,7 @@ pw = MultiPlotWidget() mw.setCentralWidget(pw) mw.show() -data = random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]]) +data = np.random.normal(size=(3, 1000)) * np.array([[0.1], [1e-5], [1]]) ma = MetaArray(data, info=[ {'name': 'Signal', 'cols': [ {'name': 'Col1', 'units': 'V'}, From 15a1f5af94872347ef8eb61aba2beca271157b18 Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Tue, 19 Nov 2019 18:14:53 +0200 Subject: [PATCH 481/607] improve performance of updateData PlotCurveItem (saves about 2us per call) (#1079) * improve performance of updateData PlotCurveItem (saves about 2us per call) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 05b11b4d..fea3834f 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -293,7 +293,7 @@ class PlotCurveItem(GraphicsObject): self.fillPath = None self.invalidateBounds() self.update() - + def setData(self, *args, **kargs): """ =============== ======================================================== @@ -358,7 +358,7 @@ class PlotCurveItem(GraphicsObject): kargs[k] = data if not isinstance(data, np.ndarray) or data.ndim > 1: raise Exception("Plot data must be 1D ndarray.") - if 'complex' in str(data.dtype): + if data.dtype.kind == 'c': raise Exception("Can not plot complex data types.") profiler("data checks") From 60c760a2e0515e042a8d307fe30348b58ad86b6a Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Tue, 19 Nov 2019 08:15:27 -0800 Subject: [PATCH 482/607] Add RemoteGraphicsView to __init__.py (#1066) --- pyqtgraph/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index 5f816245..da14a83b 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -261,6 +261,7 @@ from .widgets.LayoutWidget import * from .widgets.TableWidget import * from .widgets.ProgressDialog import * from .widgets.GroupBox import GroupBox +from .widgets.RemoteGraphicsView import RemoteGraphicsView from .imageview import * from .WidgetGroup import * From 267a0af8e76dc751d60c7e2fd863a7e64a9ab6a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Tue, 19 Nov 2019 17:21:36 +0100 Subject: [PATCH 483/607] Reset currentRow and currentCol on clear (#1076) --- pyqtgraph/graphicsItems/GraphicsLayout.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyqtgraph/graphicsItems/GraphicsLayout.py b/pyqtgraph/graphicsItems/GraphicsLayout.py index c0db5890..c3722ec0 100644 --- a/pyqtgraph/graphicsItems/GraphicsLayout.py +++ b/pyqtgraph/graphicsItems/GraphicsLayout.py @@ -167,6 +167,8 @@ class GraphicsLayout(GraphicsWidget): def clear(self): for i in list(self.items.keys()): self.removeItem(i) + self.currentRow = 0 + self.currentCol = 0 def setContentsMargins(self, *args): # Wrap calls to layout. This should happen automatically, but there From b1b2f4662b611321b3f07b3d2d1a5b032a6d4274 Mon Sep 17 00:00:00 2001 From: boylea Date: Tue, 19 Nov 2019 20:03:15 -0800 Subject: [PATCH 484/607] Fixed image scatter plot export bug (#88) --- pyqtgraph/graphicsItems/GraphicsItem.py | 5 +++-- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 628b495b..439d94ad 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -98,8 +98,9 @@ class GraphicsItem(object): Extends deviceTransform to automatically determine the viewportTransform. """ if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different. - return self._exportOpts['painter'].deviceTransform() * self.sceneTransform() - + scaler = self._exportOpts['resolutionScale'] + return self.sceneTransform() * QtGui.QTransform(scaler, 0, 0, scaler, 1, 1) + if viewportTransform is None: view = self.getViewWidget() if view is None: diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 67fafd83..bfc41969 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -781,7 +781,7 @@ class ScatterPlotItem(GraphicsObject): pts = pts[:,viewMask] for i, rec in enumerate(data): p.resetTransform() - p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']) + p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']/2) drawSymbol(p, *self.getSpotOpts(rec, scale)) else: if self.picture is None: From 455fdc2a2a9fa192eddd0a00f5f09d54c0dfe2b5 Mon Sep 17 00:00:00 2001 From: rwalroth <31414518+rwalroth@users.noreply.github.com> Date: Tue, 19 Nov 2019 20:05:45 -0800 Subject: [PATCH 485/607] Allowed actions to diplay title instead of name (#1069) ActionParameterItem changed so that if there is a title it will be displayed, otherwise displays name. --- pyqtgraph/parametertree/parameterTypes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index 8d65767d..b728fb8e 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -612,7 +612,10 @@ class ActionParameterItem(ParameterItem): self.layout = QtGui.QHBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layoutWidget.setLayout(self.layout) - self.button = QtGui.QPushButton(param.name()) + title = param.opts.get('title', None) + if title is None: + title = param.name() + self.button = QtGui.QPushButton(title) #self.layout.addSpacing(100) self.layout.addWidget(self.button) self.layout.addStretch() From c95ab570b11cd749bc54d6ae5fa3991d91f6c252 Mon Sep 17 00:00:00 2001 From: SamSchott Date: Wed, 20 Nov 2019 04:43:27 +0000 Subject: [PATCH 486/607] set color of tick-labels separately (#841) --- pyqtgraph/graphicsItems/AxisItem.py | 33 +++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 088ba6b8..da57403f 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -17,7 +17,7 @@ class AxisItem(GraphicsWidget): If maxTickLength is negative, ticks point into the plot. """ - def __init__(self, orientation, pen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args): + def __init__(self, orientation, pen=None, textPen=None, linkView=None, parent=None, maxTickLength=-5, showValues=True, text='', units='', unitPrefix='', **args): """ ============== =============================================================== **Arguments:** @@ -28,6 +28,7 @@ class AxisItem(GraphicsWidget): to be linked to the visible range of a ViewBox. showValues (bool) Whether to display values adjacent to ticks pen (QPen) Pen used when drawing ticks. + textPen (QPen) Pen used when drawing tick labels. text The text (excluding units) to display on the label for this axis. units The units for this axis. Units should generally be given @@ -97,6 +98,11 @@ class AxisItem(GraphicsWidget): else: self.setPen(pen) + if textPen is None: + self.setTextPen() + else: + self.setTextPen(pen) + self._linkedView = None if linkView is not None: self.linkToView(linkView) @@ -405,6 +411,25 @@ class AxisItem(GraphicsWidget): self.setLabel() self.update() + def textPen(self): + if self._textPen is None: + return fn.mkPen(getConfigOption('foreground')) + return fn.mkPen(self._textPen) + + def setTextPen(self, *args, **kwargs): + """ + Set the pen used for drawing text. + If no arguments are given, the default foreground color will be used. + """ + self.picture = None + if args or kwargs: + self._textPen = fn.mkPen(*args, **kwargs) + else: + self._textPen = fn.mkPen(getConfigOption('foreground')) + self.labelStyle['color'] = '#' + fn.colorStr(self._textPen.color())[:6] + self.setLabel() + self.update() + def setScale(self, scale=None): """ Set the value scaling for this axis. @@ -1048,13 +1073,13 @@ class AxisItem(GraphicsWidget): p.drawLine(p1, p2) profiler('draw ticks') - ## Draw all text + # Draw all text if self.tickFont is not None: p.setFont(self.tickFont) - p.setPen(self.pen()) + p.setPen(self.textPen()) for rect, flags, text in textSpecs: p.drawText(rect, flags, text) - #p.drawRect(rect) + profiler('draw text') def show(self): From c0ae44bc2d8f719dc267dc32e1496d5a38d8fd4a Mon Sep 17 00:00:00 2001 From: SamSchott Date: Wed, 20 Nov 2019 05:42:31 +0000 Subject: [PATCH 487/607] Nicer legend (#958) * More customizable and nicer legend. - Give kwargs for legend frame and background colors instead of hard-coded values. - Reduce spacing for more compact legend - Give separate kwarg `labelTextColor`. - New method to clear all legend items. - New methods to get and change `offset` relative to the legend's parent. - Horizontal instead of tilted lines for legend pictures. --- pyqtgraph/graphicsItems/LegendItem.py | 175 ++++++++++++++++++-------- 1 file changed, 123 insertions(+), 52 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index ce5bd883..d8986011 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -8,6 +8,7 @@ from .PlotDataItem import PlotDataItem from .GraphicsWidgetAnchor import GraphicsWidgetAnchor __all__ = ['LegendItem'] + class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ Displays a legend used for describing the contents of a plot. @@ -19,47 +20,120 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): legend.setParentItem(plotItem) """ - def __init__(self, size=None, offset=None): + def __init__(self, size=None, offset=None, horSpacing=25, verSpacing=0, pen=None, + brush=None, labelTextColor=None, **kwargs): """ ============== =============================================================== **Arguments:** size Specifies the fixed size (width, height) of the legend. If - this argument is omitted, the legend will autimatically resize + this argument is omitted, the legend will automatically resize to fit its contents. offset Specifies the offset position relative to the legend's parent. Positive values offset from the left or top; negative values offset from the right or bottom. If offset is None, the legend must be anchored manually by calling anchor() or positioned by calling setPos(). + horSpacing Specifies the spacing between the line symbol and the label. + verSpacing Specifies the spacing between individual entries of the legend + vertically. (Can also be negative to have them really close) + pen Pen to use when drawing legend border. Any single argument + accepted by :func:`mkPen ` is allowed. + brush QBrush to use as legend background filling. Any single argument + accepted by :func:`mkBrush ` is allowed. + labelTextColor Pen to use when drawing legend text. Any single argument + accepted by :func:`mkPen ` is allowed. ============== =============================================================== - + """ - - + + GraphicsWidget.__init__(self) GraphicsWidgetAnchor.__init__(self) self.setFlag(self.ItemIgnoresTransformations) self.layout = QtGui.QGraphicsGridLayout() + self.layout.setVerticalSpacing(verSpacing) + self.layout.setHorizontalSpacing(horSpacing) + self.setLayout(self.layout) self.items = [] self.size = size - self.offset = offset if size is not None: self.setGeometry(QtCore.QRectF(0, 0, self.size[0], self.size[1])) - + + self.opts = { + 'pen': fn.mkPen(pen), + 'brush': fn.mkBrush(brush), + 'labelTextColor': labelTextColor, + 'offset': offset, + } + + self.opts.update(kwargs) + + def offset(self): + return self.opts['offset'] + + def setOffset(self, offset): + self.opts['offset'] = offset + + offset = Point(self.opts['offset']) + anchorx = 1 if offset[0] <= 0 else 0 + anchory = 1 if offset[1] <= 0 else 0 + anchor = (anchorx, anchory) + self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) + + def pen(self): + return self.opts['pen'] + + def setPen(self, *args, **kargs): + """ + Sets the pen used to draw lines between points. + *pen* can be a QPen or any argument accepted by + :func:`pyqtgraph.mkPen() ` + """ + pen = fn.mkPen(*args, **kargs) + self.opts['pen'] = pen + + self.paint() + + def brush(self): + return self.opts['brush'] + + def setBrush(self, *args, **kargs): + brush = fn.mkBrush(*args, **kargs) + if self.opts['brush'] == brush: + return + self.opts['brush'] = brush + + self.paint() + + def labelTextColor(self): + return self.opts['labelTextColor'] + + def setLabelTextColor(self, *args, **kargs): + """ + Sets the color of the label text. + *pen* can be a QPen or any argument accepted by + :func:`pyqtgraph.mkColor() ` + """ + self.opts['labelTextColor'] = fn.mkColor(*args, **kargs) + for sample, label in self.items: + label.setAttr('color', self.opts['labelTextColor']) + + self.paint() + def setParentItem(self, p): ret = GraphicsWidget.setParentItem(self, p) if self.offset is not None: - offset = Point(self.offset) + offset = Point(self.opts['offset']) anchorx = 1 if offset[0] <= 0 else 0 anchory = 1 if offset[1] <= 0 else 0 anchor = (anchorx, anchory) self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) return ret - + def addItem(self, item, name): """ - Add a new entry to the legend. + Add a new entry to the legend. ============== ======================================================== **Arguments:** @@ -70,36 +144,45 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): title The title to display for this item. Simple HTML allowed. ============== ======================================================== """ - label = LabelItem(name) + label = LabelItem(name, color=self.opts['labelTextColor'], justify='left') if isinstance(item, ItemSample): sample = item else: - sample = ItemSample(item) + sample = ItemSample(item) + row = self.layout.rowCount() self.items.append((sample, label)) self.layout.addItem(sample, row, 0) self.layout.addItem(label, row, 1) self.updateSize() - + def removeItem(self, item): """ - Removes one item from the legend. + Removes one item from the legend. ============== ======================================================== **Arguments:** item The item to remove or its name. ============== ======================================================== """ - # Thanks, Ulrich! - # cycle for a match for sample, label in self.items: if sample.item is item or label.text == item: - self.items.remove( (sample, label) ) # remove from itemlist + self.items.remove((sample, label)) # remove from itemlist self.layout.removeItem(sample) # remove from layout sample.close() # remove from drawing self.layout.removeItem(label) label.close() self.updateSize() # redraq box + return # return after first match + + def clear(self): + """Removes all items from legend.""" + for sample, label in self.items: + self.layout.removeItem(sample) + self.layout.removeItem(label) + + self.items = [] + self.updateSize() def clear(self): """ @@ -113,29 +196,20 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): def updateSize(self): if self.size is not None: return - - height = 0 - width = 0 - #print("-------") - for sample, label in self.items: - height += max(sample.height(), label.height()) + 3 - width = max(width, (sample.sizeHint(QtCore.Qt.MinimumSize, sample.size()).width() + - label.sizeHint(QtCore.Qt.MinimumSize, label.size()).width())) - #print(width, height) - #print width, height - self.setGeometry(0, 0, width+25, height) - + + self.setGeometry(0, 0, 0, 0) + def boundingRect(self): return QtCore.QRectF(0, 0, self.width(), self.height()) - + def paint(self, p, *args): - p.setPen(fn.mkPen(255,255,255,100)) - p.setBrush(fn.mkBrush(100,100,100,50)) + p.setPen(self.opts['pen']) + p.setBrush(self.opts['brush']) p.drawRect(self.boundingRect()) def hoverEvent(self, ev): ev.acceptDrags(QtCore.Qt.LeftButton) - + def mouseDragEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: ev.accept() @@ -145,42 +219,39 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): class ItemSample(GraphicsWidget): """ Class responsible for drawing a single item in a LegendItem (sans label). - + This may be subclassed to draw custom graphics in a Legend. """ ## Todo: make this more generic; let each item decide how it should be represented. def __init__(self, item): GraphicsWidget.__init__(self) self.item = item - + def boundingRect(self): return QtCore.QRectF(0, 0, 20, 20) - + def paint(self, p, *args): - #p.setRenderHint(p.Antialiasing) # only if the data is antialiased. opts = self.item.opts - - if opts.get('fillLevel',None) is not None and opts.get('fillBrush',None) is not None: - p.setBrush(fn.mkBrush(opts['fillBrush'])) - p.setPen(fn.mkPen(None)) - p.drawPolygon(QtGui.QPolygonF([QtCore.QPointF(2,18), QtCore.QPointF(18,2), QtCore.QPointF(18,18)])) - + + if opts['antialias']: + p.setRenderHint(p.Antialiasing) + if not isinstance(self.item, ScatterPlotItem): p.setPen(fn.mkPen(opts['pen'])) - p.drawLine(2, 18, 18, 2) - + p.drawLine(0, 11, 20, 11) + symbol = opts.get('symbol', None) if symbol is not None: if isinstance(self.item, PlotDataItem): opts = self.item.scatter.opts - + pen = fn.mkPen(opts['pen']) brush = fn.mkBrush(opts['brush']) size = opts['size'] - - p.translate(10,10) + + p.translate(10, 10) path = drawSymbol(p, symbol, size, pen, brush) - - - - + + + + From ae61d3582e8b80eb0ec4d446f3e03767a29b21a6 Mon Sep 17 00:00:00 2001 From: "Paul B. Manis" Date: Tue, 24 Jul 2018 20:01:27 -0400 Subject: [PATCH 488/607] Py2/3 MetaArray adjustments, first pass --- pyqtgraph/metaarray/MetaArray.py | 42 +++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 690ff49d..cecea39f 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -26,7 +26,9 @@ except: USE_HDF5 = False HAVE_HDF5 = False - +if HAVE_HDF5: + import h5py.highlevel + def axis(name=None, cols=None, values=None, units=None): """Convenience function for generating axis descriptions when defining MetaArrays""" ax = {} @@ -102,7 +104,7 @@ class MetaArray(object): since the actual values are described (name and units) in the column info for the first axis. """ - version = '2' + version = u'2' # Default hdf5 compression to use when writing # 'gzip' is widely available and somewhat slow @@ -740,7 +742,7 @@ class MetaArray(object): ## decide which read function to use with open(filename, 'rb') as fd: magic = fd.read(8) - if magic == '\x89HDF\r\n\x1a\n': + if magic == b'\x89HDF\r\n\x1a\n': fd.close() self._readHDF5(filename, **kwargs) self._isHDF = True @@ -765,7 +767,7 @@ class MetaArray(object): """Read meta array from the top of a file. Read lines until a blank line is reached. This function should ideally work for ALL versions of MetaArray. """ - meta = '' + meta = u'' ## Read meta information until the first blank line while True: line = fd.readline().strip() @@ -776,6 +778,20 @@ class MetaArray(object): #print ret return ret + def fix_info(self, info): + """ + Recursive version + """ + if isinstance(info, list): + for i in range(len(info)): + info[i] = self.fix_info(info[i]) + elif isinstance(info, dict): + for k in info.keys(): + info[k] = self.fix_info(info[k]) + elif isinstance(info, bytes): # change all bytestrings to string and remove internal quotes + info = info.decode('utf-8').replace("\'", '') + return info + def _readData1(self, fd, meta, mmap=False, **kwds): ## Read array data from the file descriptor for MetaArray v1 files ## read in axis values for any axis that specifies a length @@ -786,7 +802,7 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - self._info = meta['info'] + self._info = self.fix_info(meta['info']) if not kwds.get("readAllData", True): return ## the remaining data is the actual array @@ -814,7 +830,7 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - self._info = meta['info'] + self._info = self.fix_info(meta['info']) if not kwds.get("readAllData", True): return @@ -901,7 +917,7 @@ class MetaArray(object): del ax['values_type'] #subarr = subarr.view(subtype) #subarr._info = meta['info'] - self._info = meta['info'] + self._info = self.fix_info(meta['info']) self._data = subarr #raise Exception() ## stress-testing #return subarr @@ -934,10 +950,14 @@ class MetaArray(object): f = h5py.File(fileName, mode) ver = f.attrs['MetaArray'] + try: + ver = ver.decode('utf-8') + except: + pass if ver > MetaArray.version: print("Warning: This file was written with MetaArray version %s, but you are using version %s. (Will attempt to read anyway)" % (str(ver), str(MetaArray.version))) meta = MetaArray.readHDF5Meta(f['info']) - self._info = meta + self._info = self.fix_info(meta) if writable or not readAllData: ## read all data, convert to ndarray, close file self._data = f['data'] @@ -962,7 +982,7 @@ class MetaArray(object): MetaArray._h5py_metaarray = proc._import('pyqtgraph.metaarray') ma = MetaArray._h5py_metaarray.MetaArray(file=fileName) self._data = ma.asarray()._getValue() - self._info = ma._info._getValue() + self._info = self.fix_info(ma._info._getValue()) #print MetaArray._hdf5Process #import inspect #print MetaArray, id(MetaArray), inspect.getmodule(MetaArray) @@ -1010,6 +1030,10 @@ class MetaArray(object): data[k] = val typ = root.attrs['_metaType_'] + try: + typ = typ.decode('utf-8') + except: + pass del data['_metaType_'] if typ == 'dict': From c484c8641710b82acfdb255f124f9534407108c0 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 1 Aug 2018 08:57:47 -0700 Subject: [PATCH 489/607] don't modify info from v1 files, move info correction to hdf reading --- pyqtgraph/metaarray/MetaArray.py | 67 ++++++++++---------------------- 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index cecea39f..6ce9b05b 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -14,21 +14,19 @@ import types, copy, threading, os, re import pickle import numpy as np from ..python2_3 import basestring -#import traceback + ## By default, the library will use HDF5 when writing files. ## This can be overridden by setting USE_HDF5 = False USE_HDF5 = True try: - import h5py + import h5py.highlevel HAVE_HDF5 = True except: USE_HDF5 = False HAVE_HDF5 = False -if HAVE_HDF5: - import h5py.highlevel - + def axis(name=None, cols=None, values=None, units=None): """Convenience function for generating axis descriptions when defining MetaArrays""" ax = {} @@ -777,20 +775,6 @@ class MetaArray(object): ret = eval(meta) #print ret return ret - - def fix_info(self, info): - """ - Recursive version - """ - if isinstance(info, list): - for i in range(len(info)): - info[i] = self.fix_info(info[i]) - elif isinstance(info, dict): - for k in info.keys(): - info[k] = self.fix_info(info[k]) - elif isinstance(info, bytes): # change all bytestrings to string and remove internal quotes - info = info.decode('utf-8').replace("\'", '') - return info def _readData1(self, fd, meta, mmap=False, **kwds): ## Read array data from the file descriptor for MetaArray v1 files @@ -802,7 +786,7 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - self._info = self.fix_info(meta['info']) + self._info = meta['info'] if not kwds.get("readAllData", True): return ## the remaining data is the actual array @@ -830,7 +814,7 @@ class MetaArray(object): frameSize *= ax['values_len'] del ax['values_len'] del ax['values_type'] - self._info = self.fix_info(meta['info']) + self._info = meta['info'] if not kwds.get("readAllData", True): return @@ -901,10 +885,8 @@ class MetaArray(object): newSubset = list(subset[:]) newSubset[dynAxis] = slice(dStart, dStop) if dStop > dStart: - #print n, data.shape, " => ", newSubset, data[tuple(newSubset)].shape frames.append(data[tuple(newSubset)].copy()) else: - #data = data[subset].copy() ## what's this for?? frames.append(data) n += inf['numFrames'] @@ -915,12 +897,8 @@ class MetaArray(object): ax['values'] = np.array(xVals, dtype=ax['values_type']) del ax['values_len'] del ax['values_type'] - #subarr = subarr.view(subtype) - #subarr._info = meta['info'] - self._info = self.fix_info(meta['info']) + self._info = meta['info'] self._data = subarr - #raise Exception() ## stress-testing - #return subarr def _readHDF5(self, fileName, readAllData=None, writable=False, **kargs): if 'close' in kargs and readAllData is None: ## for backward compatibility @@ -957,7 +935,7 @@ class MetaArray(object): if ver > MetaArray.version: print("Warning: This file was written with MetaArray version %s, but you are using version %s. (Will attempt to read anyway)" % (str(ver), str(MetaArray.version))) meta = MetaArray.readHDF5Meta(f['info']) - self._info = self.fix_info(meta) + self._info = meta if writable or not readAllData: ## read all data, convert to ndarray, close file self._data = f['data'] @@ -982,12 +960,7 @@ class MetaArray(object): MetaArray._h5py_metaarray = proc._import('pyqtgraph.metaarray') ma = MetaArray._h5py_metaarray.MetaArray(file=fileName) self._data = ma.asarray()._getValue() - self._info = self.fix_info(ma._info._getValue()) - #print MetaArray._hdf5Process - #import inspect - #print MetaArray, id(MetaArray), inspect.getmodule(MetaArray) - - + self._info = ma._info._getValue() @staticmethod def mapHDF5Array(data, writable=False): @@ -999,9 +972,6 @@ class MetaArray(object): if off is None: raise Exception("This dataset uses chunked storage; it can not be memory-mapped. (store using mappable=True)") return np.memmap(filename=data.file.filename, offset=off, dtype=data.dtype, shape=data.shape, mode=mode) - - - @staticmethod def readHDF5Meta(root, mmap=False): @@ -1010,6 +980,8 @@ class MetaArray(object): ## Pull list of values from attributes and child objects for k in root.attrs: val = root.attrs[k] + if isinstance(val, bytes): + val = val.decode() if isinstance(val, basestring): ## strings need to be re-evaluated to their original types try: val = eval(val) @@ -1047,21 +1019,24 @@ class MetaArray(object): return d2 else: raise Exception("Don't understand metaType '%s'" % typ) - - def write(self, fileName, **opts): + def write(self, fileName, version=2, **opts): """Write this object to a file. The object can be restored by calling MetaArray(file=fileName) opts: appendAxis: the name (or index) of the appendable axis. Allows the array to grow. appendKeys: a list of keys (other than "values") for metadata to append to on the appendable axis. compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. chunks: bool or tuple specifying chunk shape - """ - - if USE_HDF5 and HAVE_HDF5: + """ + if version == 1: + return self.writeMa(fileName, **opts) + elif USE_HDF5 and HAVE_HDF5: return self.writeHDF5(fileName, **opts) else: - return self.writeMa(fileName, **opts) + if not HAVE_HDF5: + raise Exception("h5py is required for writing .ma version 2 files") + else: + raise Exception("HDF5 is required for writing .ma version 2 files, but it has been disabled.") def writeMeta(self, fileName): """Used to re-write meta info to the given file. @@ -1074,7 +1049,6 @@ class MetaArray(object): self.writeHDF5Meta(f, 'info', self._info) f.close() - def writeHDF5(self, fileName, **opts): ## default options for writing datasets comp = self.defaultCompression @@ -1110,8 +1084,7 @@ class MetaArray(object): ## update options if they were passed in for k in dsOpts: if k in opts: - dsOpts[k] = opts[k] - + dsOpts[k] = opts[k] ## If mappable is in options, it disables chunking/compression if opts.get('mappable', False): From e58b7d4708290ae9af242dc1fd3d879677ebe2d5 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Wed, 1 Aug 2018 09:02:48 -0700 Subject: [PATCH 490/607] minor correction --- pyqtgraph/metaarray/MetaArray.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 6ce9b05b..63aee2ec 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -1020,7 +1020,7 @@ class MetaArray(object): else: raise Exception("Don't understand metaType '%s'" % typ) - def write(self, fileName, version=2, **opts): + def write(self, fileName, **opts): """Write this object to a file. The object can be restored by calling MetaArray(file=fileName) opts: appendAxis: the name (or index) of the appendable axis. Allows the array to grow. @@ -1028,15 +1028,12 @@ class MetaArray(object): compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. chunks: bool or tuple specifying chunk shape """ - if version == 1: + if USE_HDF5 is False: return self.writeMa(fileName, **opts) - elif USE_HDF5 and HAVE_HDF5: + elif HAVE_HDF5 is True: return self.writeHDF5(fileName, **opts) else: - if not HAVE_HDF5: - raise Exception("h5py is required for writing .ma version 2 files") - else: - raise Exception("HDF5 is required for writing .ma version 2 files, but it has been disabled.") + raise Exception("h5py is required for writing .ma hdf5 files, but it could not be imported.") def writeMeta(self, fileName): """Used to re-write meta info to the given file. From 542f4b446b98e26a08d1a815e0534578a06ed0d1 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 3 Aug 2018 18:18:28 -0700 Subject: [PATCH 491/607] Add eq() support for comparing dict, list, tuple --- pyqtgraph/functions.py | 20 ++++++++++++++++++++ pyqtgraph/tests/test_functions.py | 27 +++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 5cbb177e..ef2d7449 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -424,6 +424,8 @@ def eq(a, b): 3. When comparing arrays, returns False if the array shapes are not the same. 4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas the == operator would return a boolean array). + 5. Collections (dict, list, etc.) must have the same type to be considered equal. One + consequence is that comparing a dict to an OrderedDict will always return False. """ if a is b: return True @@ -440,6 +442,24 @@ def eq(a, b): if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype): return False + # Recursively handle common containers + if isinstance(a, dict) and isinstance(b, dict): + if type(a) != type(b) or len(a) != len(b): + return False + if a.keys() != b.keys(): + return False + for k,v in a.items(): + if not eq(v, b[k]): + return False + return True + if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): + if type(a) != type(b) or len(a) != len(b): + return False + for v1,v2 in zip(a, b): + if not eq(v1, v2): + return False + return True + # Test for equivalence. # If the test raises a recognized exception, then return Falase try: diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index e013fe42..fcd16254 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -1,11 +1,15 @@ import pyqtgraph as pg import numpy as np import sys +from copy import deepcopy +from collections import OrderedDict from numpy.testing import assert_array_almost_equal, assert_almost_equal import pytest + np.random.seed(12345) + def testSolve3D(): p1 = np.array([[0,0,0,1], [1,0,0,1], @@ -356,6 +360,29 @@ def test_eq(): assert eq(a4, a4.copy()) assert not eq(a4, a4.T) + # test containers + + assert not eq({'a': 1}, {'a': 1, 'b': 2}) + assert not eq({'a': 1}, {'a': 2}) + d1 = {'x': 1, 'y': np.nan, 3: ['a', np.nan, a3, 7, 2.3], 4: a4} + d2 = deepcopy(d1) + assert eq(d1, d2) + assert eq(OrderedDict(d1), OrderedDict(d2)) + assert not eq(OrderedDict(d1), d2) + items = list(d1.items()) + assert not eq(OrderedDict(items), OrderedDict(reversed(items))) + + assert not eq([1,2,3], [1,2,3,4]) + l1 = [d1, np.inf, -np.inf, np.nan] + l2 = deepcopy(l1) + t1 = tuple(l1) + t2 = tuple(l2) + assert eq(l1, l2) + assert eq(t1, t2) + + assert eq(set(range(10)), set(range(10))) + assert not eq(set(range(10)), set(range(9))) + if __name__ == '__main__': test_interpolateArray() \ No newline at end of file From 7cb27594a5ec54c23b3eca2b6127975af757eac9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 6 Aug 2018 09:05:50 -0700 Subject: [PATCH 492/607] fix dict keys comparison --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index ef2d7449..8a29d107 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -446,7 +446,7 @@ def eq(a, b): if isinstance(a, dict) and isinstance(b, dict): if type(a) != type(b) or len(a) != len(b): return False - if a.keys() != b.keys(): + if set(a.keys()) != set(b.keys()): return False for k,v in a.items(): if not eq(v, b[k]): From a8529e48f35a6cc55ec68b53bcd6a7b37deb0b52 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Mon, 6 Aug 2018 09:17:23 -0700 Subject: [PATCH 493/607] faster keys comparison --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8a29d107..ef959466 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -446,7 +446,7 @@ def eq(a, b): if isinstance(a, dict) and isinstance(b, dict): if type(a) != type(b) or len(a) != len(b): return False - if set(a.keys()) != set(b.keys()): + if sorted(a.keys()) != sorted(b.keys()): return False for k,v in a.items(): if not eq(v, b[k]): From 477feb777bfa31c6427351208df7adeced489b1c Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 20 Nov 2019 21:22:31 -0800 Subject: [PATCH 494/607] import h5py.highlevel is deprecated, use import h5py instead --- pyqtgraph/metaarray/MetaArray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 63aee2ec..1410e40c 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -20,7 +20,7 @@ from ..python2_3 import basestring ## This can be overridden by setting USE_HDF5 = False USE_HDF5 = True try: - import h5py.highlevel + import h5py HAVE_HDF5 = True except: USE_HDF5 = False From 71c4807559b3a129ee360062257ad50406ca1271 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 20 Nov 2019 21:22:46 -0800 Subject: [PATCH 495/607] fix dict eq() checks --- pyqtgraph/functions.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index ef959466..6cf5f98d 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -11,6 +11,7 @@ import numpy as np import decimal, re import ctypes import sys, struct +from .pgcollections import OrderedDict from .python2_3 import asUnicode, basestring from .Qt import QtGui, QtCore, QT_LIB from . import getConfigOption, setConfigOptions @@ -446,11 +447,15 @@ def eq(a, b): if isinstance(a, dict) and isinstance(b, dict): if type(a) != type(b) or len(a) != len(b): return False - if sorted(a.keys()) != sorted(b.keys()): + if set(a.keys()) != set(b.keys()): return False - for k,v in a.items(): + for k, v in a.items(): if not eq(v, b[k]): return False + if isinstance(a, OrderedDict) or sys.version_info >= (3, 7): + for a_item, b_item in zip(a.items(), b.items()): + if not eq(a_item, b_item): + return False return True if isinstance(a, (list, tuple)) and isinstance(b, (list, tuple)): if type(a) != type(b) or len(a) != len(b): From 770ce06dc1b4d721bd7afb1a815384901f5bc0d1 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Thu, 21 Nov 2019 08:42:44 -0800 Subject: [PATCH 496/607] Revert "Allow MetaArray.__array__ to accept an optional dtype arg" --- pyqtgraph/metaarray/MetaArray.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 18f7250a..1410e40c 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -357,7 +357,7 @@ class MetaArray(object): else: return np.array(self._data) - def __array__(self, dtype=None): + def __array__(self): ## supports np.array(metaarray_instance) return self.asarray() From ef4ca9e9ea94c65390cf10206ec73fc9cbb186d7 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Thu, 21 Nov 2019 08:46:25 -0800 Subject: [PATCH 497/607] Incorporated correction luke suggested --- pyqtgraph/metaarray/MetaArray.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/metaarray/MetaArray.py b/pyqtgraph/metaarray/MetaArray.py index 1410e40c..374c9acf 100644 --- a/pyqtgraph/metaarray/MetaArray.py +++ b/pyqtgraph/metaarray/MetaArray.py @@ -357,9 +357,12 @@ class MetaArray(object): else: return np.array(self._data) - def __array__(self): + def __array__(self, dtype=None): ## supports np.array(metaarray_instance) - return self.asarray() + if dtype is None: + return self.asarray() + else: + return self.asarray().astype(dtype) def view(self, typ): ## deprecated; kept for backward compatibility From b02ada024b45de93eeab2218029094223b0ed9e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Sat, 23 Nov 2019 13:10:49 +0100 Subject: [PATCH 498/607] fix error on SVG export of scatter plots: KeyError: 'resolutionScale' --- pyqtgraph/graphicsItems/GraphicsItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 439d94ad..541ab13b 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -98,7 +98,7 @@ class GraphicsItem(object): Extends deviceTransform to automatically determine the viewportTransform. """ if self._exportOpts is not False and 'painter' in self._exportOpts: ## currently exporting; device transform may be different. - scaler = self._exportOpts['resolutionScale'] + scaler = self._exportOpts.get('resolutionScale', 1.0) return self.sceneTransform() * QtGui.QTransform(scaler, 0, 0, scaler, 1, 1) if viewportTransform is None: From 2a01c3848a55357ef6c04c09427511c4c70a0a9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paul=20M=C3=BCller?= Date: Mon, 25 Nov 2019 14:14:15 +0100 Subject: [PATCH 499/607] fix wrong offset when drawing symbol --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index bfc41969..aa2cabba 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -781,7 +781,7 @@ class ScatterPlotItem(GraphicsObject): pts = pts[:,viewMask] for i, rec in enumerate(data): p.resetTransform() - p.translate(pts[0,i] + rec['width'], pts[1,i] + rec['width']/2) + p.translate(pts[0,i] + rec['width']/2, pts[1,i] + rec['width']/2) drawSymbol(p, *self.getSpotOpts(rec, scale)) else: if self.picture is None: 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 500/607] 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 9b6102900dea5d9a1e62cec90f0f12968a0b4c99 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Sun, 15 Dec 2019 21:54:36 -0600 Subject: [PATCH 501/607] Update Changelog with PRs merged since August --- CHANGELOG | 44 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 89c8eb13..9a3bcf4e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ pyqtgraph-0.11.0 (in development) New Features: + - #101: GridItem formatting options - #410: SpinBox custom formatting options - #415: ROI.getArrayRegion supports nearest-neighbor interpolation (especially handy for label images) - #428: DataTreeWidget: @@ -59,12 +60,17 @@ pyqtgraph-0.11.0 (in development) - #813,814,817: Performance improvements - #837: Added options for field variables in ColorMapWidget - #840, 932: Improve clipping behavior + - #841: Set color of tick-labels separately - #922: Curve fill for fill-patches - #996: Allow the update of LegendItem + - #1023: Add bookkeeping exporter parameters + - #1072: HDF5Exporter handling of ragged curves with tests API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because widgets can be placed into a new window just by calling show(). + - #158: Make DockArea compatible with Qt Designer + - #406: Applying alpha mask on numpy.nan data values - #566: ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system. The result is visually the same, but children of ArrowItem are no longer rotated (this allows screen-aligned text to be attached more easily). @@ -83,12 +89,23 @@ pyqtgraph-0.11.0 (in development) - #657: When a floating Dock window is closed, the dock is now returned home - #771: Suppress RuntimeWarning for arrays containing zeros in logscale - #942: If the visible GraphicsView is garbage collected, a warning is issued. + - #958: Nicer Legend - #963: Last image in image-stack can now be selected with the z-slider - #992: Added a setter for GlGridItem.color. - #999: Make outline around fillLevel optional. - #1014: Enable various arguments as color in colormap. + - #1044: Raise AttributeError in __getattr__ in graphicsWindows (deprecated) + - #1055: Remove global for CONFIG_OPTIONS in setConfigOption + - #1066: Add RemoteGraphicsView to __init__.py + - #1069: Allow actions to display title instead of name + - #1074: Validate min/max text inputs in ViewBoxMenu + - #1076: Reset currentRow and currentCol on GraphicsLayout.clear() + - #1079: Improve performance of updateData PlotCurveItem + - #1082: Allow MetaArray.__array__ to accept an optional dtype arg Bugfixes: + - #88: Fixed image scatterplot export + - #356: Fix some NumPy warnings - #408: Fix `cleanup` when the running qt application is not a QApplication - #410: SpinBox fixes - fixed bug with exponents disappearing after edit @@ -104,7 +121,7 @@ pyqtgraph-0.11.0 (in development) - fixed spinbox height too small for font size - ROI subclass getArrayRegion methods are a bit more consistent (still need work) - #424: Fix crash when running pyqtgraph with python -OO - - #429: fix fft premature slicing away of 0 freq bin + - #429: Fix fft premature slicing away of 0 freq bin - #458: Fixed image export problems with new numpy API - #478: Fixed PySide image memory leak - #475: Fixed unicode error when exporting to SVG with non-ascii symbols @@ -137,17 +154,18 @@ pyqtgraph-0.11.0 (in development) - #592,595: Fix InvisibleRootItem issues introduced in #518 - #596: Fix polyline click causing lines to bedrawn to the wrong node - #598: Better ParameterTree support for dark themes + - #599: Prevent invalid list access in GraphicsScene - #623: Fix PyQt5 / ScatterPlot issue with custom symbols - #626: Fix OpenGL texture state leaking to wrong items - #627: Fix ConsoleWidget stack handling on python 3.5 - #633: Fix OpenGL cylinder geometry - #637: Fix TypeError in isosurface - #641,642: Fix SVG export on Qt5 / high-DPI displays - - #645: scatterplotwidget behaves nicely when data contains infs + - #645: ScatterPlotWidget behaves nicely when data contains infs - #653: ScatterPlotItem: Fix a GC memory leak due to numpy issue 6581 - #648: fix color ignored in GLGridItem - - #671: fixed SVG export failing if the first value of a plot is nan - - #674: fixed parallelizer leaking file handles + - #671: Fixed SVG export failing if the first value of a plot is nan + - #674: Fixed parallelizer leaking file handles - #675: Gracefully handle case where image data has size==0 - #679: Fix overflow in Point.length() - #682: Fix: mkQApp returned None if a QApplication was already created elsewhere @@ -164,6 +182,7 @@ pyqtgraph-0.11.0 (in development) it was causing auto range to be disabled. - #723: Fix axis ticks when using self.scale - #739: Fix handling of 2-axis mouse wheel events + - #742: Fix Metaarray in python 3 - #758: Fix remote graphicsview "ValueError: mmap length is greater than file size" on OSX. - #763: Fix OverflowError when using Auto Downsampling. - #767: Fix Image display for images with the same value everywhere. @@ -185,16 +204,29 @@ pyqtgraph-0.11.0 (in development) - #949: Fix multiline parameters (such as arrays) reading from config files. - #951: Fix event firing from scale handler. - #952: Fix RotateFree handle dragging + - #953: Fix HistogramLUTWidget with background parameter - #968: Fix Si units in AxisItem leading to an incorrect unit. + - #970: Always update transform when setting angle of a TextItem - #971: Fix a segfault stemming from incorrect signal disconnection. + - #972: Correctly include SI units for log AxisItems - #974: Fix recursion error when instancing CtrlNode. - #987: Fix visibility reset when PlotItems are removed. - #998: Fix QtProcess proxy being unable to handle numpy arrays with dtype uint8. - #1010: Fix matplotlib/CSV export. + - #1012: Fix circular texture centering - #1015: Iterators are now converted to NumPy arrays. - #1016: Fix synchronisation of multiple ImageViews with time axis. - #1017: Fix duplicate paint calls emitted by Items on ViewBox. - #1019: Fix disappearing GLGridItems when PlotItems are removed and readded. + - #1024: Prevent element-wise string comparison + - #1031: Reset ParentItem to None on removing from PlotItem/ViewBox + - #1044: Fix PlotCurveItem.paintGL + - #1048: Fix bounding box for InfiniteLine + - #1062: Fix flowchart context menu redundant menu + - #1062: Fix a typo + - #1073: Fix Python3 compatibility + - #1083: Fix SVG export of scatter plots + - #1085: Fix ofset when drawing symbol Maintenance: - Lots of new unit tests @@ -204,6 +236,10 @@ pyqtgraph-0.11.0 (in development) - #624: TravisCI no longer running python 2.6 tests - #695: "dev0" added to version string - #865,873,877 (and more): Implement Azure CI pipelines, fix Travis CI + - #991: Use Azure Pipelines to do style checks, Add .pre-commit-config.yaml + - #1042: Close windows at the end of test functions + - #1046: Establish minimum numpy version, remove legacy workarounds + - #1067: Make scipy dependency optional pyqtgraph-0.10.0 From 61104cd43c91feadb51f28ea16c2a33e6de74756 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Tue, 24 Dec 2019 10:04:31 -0800 Subject: [PATCH 502/607] Fix small oversight in LegendItem --- pyqtgraph/graphicsItems/LegendItem.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index d8986011..5c3083a2 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from .GraphicsWidget import GraphicsWidget from .LabelItem import LabelItem from ..Qt import QtGui, QtCore @@ -123,7 +124,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): def setParentItem(self, p): ret = GraphicsWidget.setParentItem(self, p) - if self.offset is not None: + if self.opts['offset'] is not None: offset = Point(self.opts['offset']) anchorx = 1 if offset[0] <= 0 else 0 anchory = 1 if offset[1] <= 0 else 0 @@ -251,7 +252,3 @@ class ItemSample(GraphicsWidget): p.translate(10, 10) path = drawSymbol(p, symbol, size, pen, brush) - - - - From 65d2ac58e035083ba480aef4ef112f24ff6e40ac Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Wed, 8 Jan 2020 21:19:09 +0100 Subject: [PATCH 503/607] fix for nextafter --- pyqtgraph/functions.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 6cf5f98d..45e9aad6 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1134,7 +1134,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if minVal != 0 or maxVal != scale: if minVal == maxVal: maxVal = np.nextafter(maxVal, 2*maxVal) - data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype) + rng = maxVal-minVal + rng = 1 if rng == 0 else rng + data = rescaleData(data, scale/rng, minVal, dtype=dtype) profile() # apply LUT if given From 660ac675f117918cf590c71e341941f468c9738c Mon Sep 17 00:00:00 2001 From: Xinfa Joseph Zhu Date: Thu, 9 Jan 2020 15:23:49 -0600 Subject: [PATCH 504/607] Fix typo bug --- pyqtgraph/opengl/MeshData.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/opengl/MeshData.py b/pyqtgraph/opengl/MeshData.py index 5bab4626..95ba88af 100644 --- a/pyqtgraph/opengl/MeshData.py +++ b/pyqtgraph/opengl/MeshData.py @@ -371,7 +371,7 @@ class MeshData(object): #pass def _computeEdges(self): - if not self.hasFaceIndexedData: + if not self.hasFaceIndexedData(): ## generate self._edges from self._faces nf = len(self._faces) edges = np.empty(nf*3, dtype=[('i', np.uint, 2)]) From 74294502bd2adead205af9576049a79233c14b12 Mon Sep 17 00:00:00 2001 From: Julian Hofer Date: Fri, 10 Jan 2020 11:36:06 +0100 Subject: [PATCH 505/607] doc: Fix small mistake in introduction --- doc/source/introduction.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/introduction.rst b/doc/source/introduction.rst index 70161173..741acd30 100644 --- a/doc/source/introduction.rst +++ b/doc/source/introduction.rst @@ -73,7 +73,7 @@ How does it compare to... such as image interaction, volumetric rendering, parameter trees, flowcharts, etc. -* pyqwt5: About as fast as pyqwt5, but not quite as complete for plotting +* pyqwt5: About as fast as pyqtgraph, but not quite as complete for plotting functionality. Image handling in pyqtgraph is much more complete (again, no ROI widgets in qwt). Also, pyqtgraph is written in pure python, so it is more portable than pyqwt, which often lags behind pyqt in development (I From adba81a8d86c362817727eda0b0da3738068cf92 Mon Sep 17 00:00:00 2001 From: Gabriel Linder Date: Sun, 9 Feb 2020 23:43:58 +0100 Subject: [PATCH 506/607] Syntax highlighting for examples. --- examples/__main__.py | 2 + examples/syntax.py | 186 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 examples/syntax.py diff --git a/examples/__main__.py b/examples/__main__.py index ffc38ff7..3867fbd3 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -10,6 +10,7 @@ from pyqtgraph.python2_3 import basestring from pyqtgraph.Qt import QtGui, QT_LIB from .utils import buildFileList, path, examples +from .syntax import PythonHighlighter if QT_LIB == 'PySide': @@ -33,6 +34,7 @@ class ExampleLoader(QtGui.QMainWindow): self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() self.ui.codeView.setLayout(self.codeLayout) + self.hl = PythonHighlighter(self.ui.codeView.document()) self.codeLayout.addItem(QtGui.QSpacerItem(100,100,QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding), 0, 0) self.codeLayout.addWidget(self.codeBtn, 1, 1) self.codeBtn.hide() diff --git a/examples/syntax.py b/examples/syntax.py new file mode 100644 index 00000000..cd2cccf1 --- /dev/null +++ b/examples/syntax.py @@ -0,0 +1,186 @@ +# based on https://github.com/art1415926535/PyQt5-syntax-highlighting + +from pyqtgraph.Qt import QtCore, QtGui + +QRegExp = QtCore.QRegExp + +QFont = QtGui.QFont +QColor = QtGui.QColor +QTextCharFormat = QtGui.QTextCharFormat +QSyntaxHighlighter = QtGui.QSyntaxHighlighter + + +def format(color, style=''): + """ + Return a QTextCharFormat with the given attributes. + """ + _color = QColor() + if type(color) is not str: + _color.setRgb(color[0], color[1], color[2]) + else: + _color.setNamedColor(color) + + _format = QTextCharFormat() + _format.setForeground(_color) + if 'bold' in style: + _format.setFontWeight(QFont.Bold) + if 'italic' in style: + _format.setFontItalic(True) + + return _format + + +# Syntax styles that can be shared by all languages +STYLES = { + 'keyword': format('blue'), + 'operator': format('red'), + 'brace': format('darkGray'), + 'defclass': format('black', 'bold'), + 'string': format('magenta'), + 'string2': format('darkMagenta'), + 'comment': format('darkGreen', 'italic'), + 'self': format('black', 'italic'), + 'numbers': format('brown'), +} + + +class PythonHighlighter(QSyntaxHighlighter): + """Syntax highlighter for the Python language. + """ + # Python keywords + keywords = [ + 'and', 'assert', 'break', 'class', 'continue', 'def', + 'del', 'elif', 'else', 'except', 'exec', 'finally', + 'for', 'from', 'global', 'if', 'import', 'in', + 'is', 'lambda', 'not', 'or', 'pass', 'print', + 'raise', 'return', 'try', 'while', 'yield', + 'None', 'True', 'False', + ] + + # Python operators + operators = [ + '=', + # Comparison + '==', '!=', '<', '<=', '>', '>=', + # Arithmetic + '\+', '-', '\*', '/', '//', '\%', '\*\*', + # In-place + '\+=', '-=', '\*=', '/=', '\%=', + # Bitwise + '\^', '\|', '\&', '\~', '>>', '<<', + ] + + # Python braces + braces = [ + '\{', '\}', '\(', '\)', '\[', '\]', + ] + + def __init__(self, document): + QSyntaxHighlighter.__init__(self, document) + + # Multi-line strings (expression, flag, style) + # FIXME: The triple-quotes in these two lines will mess up the + # syntax highlighting from this point onward + self.tri_single = (QRegExp("'''"), 1, STYLES['string2']) + self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) + + rules = [] + + # Keyword, operator, and brace rules + rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) + for w in PythonHighlighter.keywords] + rules += [(r'%s' % o, 0, STYLES['operator']) + for o in PythonHighlighter.operators] + rules += [(r'%s' % b, 0, STYLES['brace']) + for b in PythonHighlighter.braces] + + # All other rules + rules += [ + + # 'self' + (r'\bself\b', 0, STYLES['self']), + + # 'def' followed by an identifier + (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']), + # 'class' followed by an identifier + (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']), + + # Numeric literals + (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), + (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), + (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), + + # Double-quoted string, possibly containing escape sequences + (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']), + # Single-quoted string, possibly containing escape sequences + (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']), + + # From '#' until a newline + (r'#[^\n]*', 0, STYLES['comment']), + + ] + + # Build a QRegExp for each pattern + self.rules = [(QRegExp(pat), index, fmt) + for (pat, index, fmt) in rules] + + def highlightBlock(self, text): + """Apply syntax highlighting to the given block of text. + """ + # Do other syntax formatting + for expression, nth, format in self.rules: + index = expression.indexIn(text, 0) + + while index >= 0: + # We actually want the index of the nth match + index = expression.pos(nth) + length = len(expression.cap(nth)) + self.setFormat(index, length, format) + index = expression.indexIn(text, index + length) + + self.setCurrentBlockState(0) + + # Do multi-line strings + in_multiline = self.match_multiline(text, *self.tri_single) + if not in_multiline: + in_multiline = self.match_multiline(text, *self.tri_double) + + def match_multiline(self, text, delimiter, in_state, style): + """Do highlighting of multi-line strings. ``delimiter`` should be a + ``QRegExp`` for triple-single-quotes or triple-double-quotes, and + ``in_state`` should be a unique integer to represent the corresponding + state changes when inside those strings. Returns True if we're still + inside a multi-line string when this function is finished. + """ + # If inside triple-single quotes, start at 0 + if self.previousBlockState() == in_state: + start = 0 + add = 0 + # Otherwise, look for the delimiter on this line + else: + start = delimiter.indexIn(text) + # Move past this match + add = delimiter.matchedLength() + + # As long as there's a delimiter match on this line... + while start >= 0: + # Look for the ending delimiter + end = delimiter.indexIn(text, start + add) + # Ending delimiter on this line? + if end >= add: + length = end - start + add + delimiter.matchedLength() + self.setCurrentBlockState(0) + # No; multi-line string + else: + self.setCurrentBlockState(in_state) + length = len(text) - start + add + # Apply formatting + self.setFormat(start, length, style) + # Look for the next match + start = delimiter.indexIn(text, start + length) + + # Return True if still inside a multi-line string, False otherwise + if self.currentBlockState() == in_state: + return True + else: + return False From 07af12d489ec82b5bbc4bfa14176a4b63dc7947c Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 21 Feb 2020 09:28:48 -0800 Subject: [PATCH 507/607] Update CI Config --- azure-pipelines.yml | 14 +++++----- azure-test-template.yml | 62 +++++++++++++++++------------------------ 2 files changed, 33 insertions(+), 43 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 657189f8..b9abcf86 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -23,7 +23,7 @@ stages: jobs: - job: check_diff_size pool: - vmImage: 'Ubuntu 16.04' + vmImage: 'Ubuntu 18.04' steps: - bash: | git config --global advice.detachedHead false @@ -56,7 +56,7 @@ stages: - job: "style_check" pool: - vmImage: "Ubuntu 16.04" + vmImage: "Ubuntu 18.04" steps: - task: UsePythonVersion@0 inputs: @@ -69,11 +69,11 @@ stages: - job: "build_wheel" pool: - vmImage: 'Ubuntu 16.04' + vmImage: 'Ubuntu 18.04' steps: - task: UsePythonVersion@0 inputs: - versionSpec: 3.7 + versionSpec: 3.8 - script: | python -m pip install setuptools wheel python setup.py bdist_wheel --universal @@ -87,12 +87,12 @@ stages: - template: azure-test-template.yml parameters: name: linux - vmImage: 'Ubuntu 16.04' + vmImage: 'Ubuntu 18.04' - template: azure-test-template.yml parameters: name: windows - vmImage: 'vs2017-win2016' + vmImage: 'windows-2019' - template: azure-test-template.yml parameters: name: macOS - vmImage: 'macOS-10.13' + vmImage: 'macOS-10.15' diff --git a/azure-test-template.yml b/azure-test-template.yml index 5d204009..bc1a16df 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -22,17 +22,13 @@ jobs: python.version: "3.6" qt.bindings: "pyqt" install.method: "conda" - Python36-PySide2-5.9: - python.version: "3.6" - qt.bindings: "pyside2" - install.method: "conda" - Python37-PyQt-5.13: - python.version: '3.7' - qt.bindings: "PyQt5" - install.method: "pip" Python37-PySide2-5.13: python.version: "3.7" - qt.bindings: "PySide2" + qt.bindings: "pyside2" + install.method: "conda" + Python38-PyQt-5.14: + python.version: '3.8' + qt.bindings: "PyQt5" install.method: "pip" steps: @@ -75,22 +71,10 @@ jobs: if [ $(agent.os) == 'Linux' ] then echo "##vso[task.prependpath]$CONDA/bin" - if [ $(python.version) == '2.7' ] - then - echo "Grabbing Older Miniconda" - wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-Linux-x86_64.sh -O Miniconda.sh - bash Miniconda.sh -b -p $CONDA -f - fi elif [ $(agent.os) == 'Darwin' ] then sudo chown -R $USER $CONDA echo "##vso[task.prependpath]$CONDA/bin" - if [ $(python.version) == '2.7' ] - then - echo "Grabbing Older Miniconda" - wget https://repo.anaconda.com/miniconda/Miniconda2-4.6.14-MacOSX-x86_64.sh -O Miniconda.sh - bash Miniconda.sh -b -p $CONDA -f - fi elif [ $(agent.os) == 'Windows_NT' ] then echo "##vso[task.prependpath]$CONDA/Scripts" @@ -103,25 +87,31 @@ jobs: - bash: | if [ $(install.method) == "conda" ] then - conda create --name test-environment-$(python.version) python=$(python.version) --yes - echo "Conda Info:" - conda info - echo "Installing qt-bindings" + conda create --name test-environment-$(python.version) python=$(python.version) --yes --quiet source activate test-environment-$(python.version) - - if [ $(agent.os) == "Linux" ] && [ $(python.version) == "2.7" ] + conda config --env --set always_yes true + if [ $(python.version) == '2.7' ] && [ $(agent.os) != 'Windows_NT' ] then - conda install $(qt.bindings) --yes - else - conda install -c conda-forge $(qt.bindings) --yes + conda config --set restore_free_channel true fi - echo "Installing remainder of dependencies" - conda install -c conda-forge numpy scipy six pyopengl h5py --yes + + if [ $(qt.bindings) == "pyside2" ] || ([ $(qt.bindings) == 'pyside' ] && [ $(agent.os) == 'Darwin' ]) + then + conda config --prepend channels conda-forge + fi + conda info + if [ $(python.version) == '2.7' ] && [ $(agent.os) == 'Linux' ] + then + pip install --upgrade pip==19.3.1 + conda install setuptools=44.0.0 --yes --quiet + conda install nomkl + fi + conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet else - pip install $(qt.bindings) numpy scipy pyopengl six h5py + pip install $(qt.bindings) numpy scipy pyopengl h5py six fi echo "" - pip install pytest pytest-xdist pytest-cov coverage + pip install pytest pytest-cov coverage if [ $(python.version) == "2.7" ] then pip install pytest-faulthandler==1.6.0 @@ -180,9 +170,9 @@ jobs: mkdir -p "$SCREENSHOT_DIR" # echo "If Screenshots are generated, they may be downloaded from:" # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" - pytest . -sv \ + pytest . -v \ --junitxml=junit/test-results.xml \ - -n 1 --cov pyqtgraph --cov-report=xml --cov-report=html + --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' env: AZURE: 1 From 1549959902a2f7fadc0a01722c7e86d4fbd8762f Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 21 Feb 2020 16:24:33 -0800 Subject: [PATCH 508/607] Skipping problematic test on py2/qt4/linux --- azure-test-template.yml | 6 ------ pyqtgraph/graphicsItems/tests/test_ROI.py | 8 ++++++-- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index bc1a16df..7ca6ba45 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -100,12 +100,6 @@ jobs: conda config --prepend channels conda-forge fi conda info - if [ $(python.version) == '2.7' ] && [ $(agent.os) == 'Linux' ] - then - pip install --upgrade pip==19.3.1 - conda install setuptools=44.0.0 --yes --quiet - conda install nomkl - fi conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet else pip install $(qt.bindings) numpy scipy pyopengl h5py six diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 33a18217..2fc61d11 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -4,11 +4,15 @@ import pytest import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtTest from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem, resizeWindow - +import pytest +import platform +import six app = pg.mkQApp() +reason = ("Test fails intermittently, will be deprecating py2/qt4 support soon anyway") +@pytest.mark.skipif(six.PY2 and pg.Qt.QT_LIB in {"PySide", "PyQt4"} and platform.system() == "Linux", reason=reason) def test_getArrayRegion(transpose=False): pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) pr.setPos(1, 1) @@ -33,7 +37,7 @@ def test_getArrayRegion(transpose=False): finally: pg.setConfigOptions(imageAxisOrder=origMode) - +@pytest.mark.skipif(six.PY2 and pg.Qt.QT_LIB in {"PySide", "PyQt4"} and platform.system() == "Linux", reason=reason) def test_getArrayRegion_axisorder(): test_getArrayRegion(transpose=True) From f0d1c4eda1f776bb472489202ad5831666c34604 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 21 Feb 2020 16:24:33 -0800 Subject: [PATCH 509/607] Skipping problematic test on py2/qt4/linux --- azure-pipelines.yml | 1 + azure-test-template.yml | 17 +++++++---------- pyqtgraph/graphicsItems/tests/test_ROI.py | 7 +++---- 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b9abcf86..eb379119 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,6 +17,7 @@ pr: variables: OFFICIAL_REPO: 'pyqtgraph/pyqtgraph' DEFAULT_MERGE_BRANCH: 'develop' + disable.coverage.autogenerate: 'true' stages: - stage: "pre_test" diff --git a/azure-test-template.yml b/azure-test-template.yml index bc1a16df..04fd7e42 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -83,39 +83,35 @@ jobs: fi displayName: 'Add Conda To $PATH' condition: eq(variables['install.method'], 'conda' ) + continueOnError: false - bash: | if [ $(install.method) == "conda" ] then + conda update --all --yes --quiet conda create --name test-environment-$(python.version) python=$(python.version) --yes --quiet source activate test-environment-$(python.version) conda config --env --set always_yes true - if [ $(python.version) == '2.7' ] && [ $(agent.os) != 'Windows_NT' ] + if [ $(python.version) == '2.7' ] then conda config --set restore_free_channel true fi - if [ $(qt.bindings) == "pyside2" ] || ([ $(qt.bindings) == 'pyside' ] && [ $(agent.os) == 'Darwin' ]) then conda config --prepend channels conda-forge fi conda info - if [ $(python.version) == '2.7' ] && [ $(agent.os) == 'Linux' ] - then - pip install --upgrade pip==19.3.1 - conda install setuptools=44.0.0 --yes --quiet - conda install nomkl - fi conda install $(qt.bindings) numpy scipy pyopengl h5py six --yes --quiet else pip install $(qt.bindings) numpy scipy pyopengl h5py six fi - echo "" - pip install pytest pytest-cov coverage + pip install pytest pytest-cov coverage pytest-xdist if [ $(python.version) == "2.7" ] then pip install pytest-faulthandler==1.6.0 export PYTEST_ADDOPTS="--faulthandler-timeout=15" + else + pip install pytest pytest-cov coverage fi displayName: "Install Dependencies" @@ -171,6 +167,7 @@ jobs: # echo "If Screenshots are generated, they may be downloaded from:" # echo "https://dev.azure.com/pyqtgraph/pyqtgraph/_apis/build/builds/$(Build.BuildId)/artifacts?artifactName=Screenshots&api-version=5.0" pytest . -v \ + -n 1 \ --junitxml=junit/test-results.xml \ --cov pyqtgraph --cov-report=xml --cov-report=html displayName: 'Unit tests' diff --git a/pyqtgraph/graphicsItems/tests/test_ROI.py b/pyqtgraph/graphicsItems/tests/test_ROI.py index 33a18217..10c6009b 100644 --- a/pyqtgraph/graphicsItems/tests/test_ROI.py +++ b/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -1,14 +1,14 @@ +# -*- coding: utf-8 -*- import sys import numpy as np import pytest import pyqtgraph as pg from pyqtgraph.Qt import QtCore, QtTest from pyqtgraph.tests import assertImageApproved, mouseMove, mouseDrag, mouseClick, TransposedImageItem, resizeWindow - +import pytest app = pg.mkQApp() - def test_getArrayRegion(transpose=False): pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) pr.setPos(1, 1) @@ -33,7 +33,6 @@ def test_getArrayRegion(transpose=False): finally: pg.setConfigOptions(imageAxisOrder=origMode) - def test_getArrayRegion_axisorder(): test_getArrayRegion(transpose=True) @@ -135,7 +134,7 @@ def check_getArrayRegion(roi, name, testResize=True, transpose=False): img2.setImage(rgn[0, ..., 0]) app.processEvents() # on windows, one edge of one ROI handle is shifted slightly; letting this slide with pxCount=10 - if sys.platform == 'win32' and pg.Qt.QT_LIB in ('PyQt4', 'PySide'): + if pg.Qt.QT_LIB in {'PyQt4', 'PySide'}: pxCount = 10 else: pxCount=-1 From 428af4950d9d942ceb12dfd276abc0bad734f148 Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 24 Feb 2020 23:00:09 -0800 Subject: [PATCH 510/607] unskip py3 tests, weakref works fine in a list --- pyqtgraph/tests/test_ref_cycles.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index e05c4ef1..b04390ca 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ Test for unwanted reference cycles @@ -9,9 +10,7 @@ import six import pytest app = pg.mkQApp() -skipreason = ('unclear why test is failing on python 3. skipping until someone ' - 'has time to fix it. Or pyside is being used. This test is ' - 'failing on pyside for an unknown reason too.') +skipreason = ('This test is failing on pyside for an unknown reason.') def assert_alldead(refs): for ref in refs: @@ -37,10 +36,10 @@ def mkrefs(*objs): for o in obj: allObjs[id(o)] = o - return map(weakref.ref, allObjs.values()) + return list(map(weakref.ref, allObjs.values())) -@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_PlotWidget(): def mkobjs(*args, **kwds): w = pg.PlotWidget(*args, **kwds) @@ -58,7 +57,7 @@ def test_PlotWidget(): for i in range(5): assert_alldead(mkobjs()) -@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_ImageView(): def mkobjs(): iv = pg.ImageView() @@ -71,7 +70,7 @@ def test_ImageView(): assert_alldead(mkobjs()) -@pytest.mark.skipif(six.PY3 or pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) def test_GraphicsWindow(): def mkobjs(): w = pg.GraphicsWindow() From 19ae94765fd783885397da8dbbae4c59891f6027 Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 24 Feb 2020 23:00:42 -0800 Subject: [PATCH 511/607] Skip tests involving loadUi with pyside2 5.14 --- examples/test_examples.py | 3 ++- pyqtgraph/tests/test_qt.py | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index c6fef377..f10fe358 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- from __future__ import print_function, division, absolute_import -from pyqtgraph import Qt from . import utils from collections import namedtuple +from pyqtgraph import Qt import errno import importlib import itertools @@ -150,6 +150,7 @@ conditionalExamples = { ) } +@pytest.mark.skipif(Qt.QT_LIB == "PySide2" and "Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality") @pytest.mark.parametrize( "frontend, f", [ diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index c86cd500..9a4f373b 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pyqtgraph as pg import gc, os import pytest @@ -14,6 +15,7 @@ def test_isQObjectAlive(): @pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be ' 'packaged with conda') +@pytest.mark.skipif(pg.Qt.QT_LIB == "PySide2" and "pg.Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality") def test_loadUiType(): path = os.path.dirname(__file__) formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui')) From 3195ed4c8faacf621de2c93f581d9218f4eb9006 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 26 Feb 2020 10:06:02 -0800 Subject: [PATCH 512/607] Skip some tests on pyside2 --- pyqtgraph/tests/test_ref_cycles.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/pyqtgraph/tests/test_ref_cycles.py b/pyqtgraph/tests/test_ref_cycles.py index b04390ca..121a09e4 100644 --- a/pyqtgraph/tests/test_ref_cycles.py +++ b/pyqtgraph/tests/test_ref_cycles.py @@ -10,7 +10,7 @@ import six import pytest app = pg.mkQApp() -skipreason = ('This test is failing on pyside for an unknown reason.') +skipreason = ('This test is failing on pyside and pyside2 for an unknown reason.') def assert_alldead(refs): for ref in refs: @@ -35,11 +35,10 @@ def mkrefs(*objs): obj = [obj] for o in obj: allObjs[id(o)] = o - - return list(map(weakref.ref, allObjs.values())) + return [weakref.ref(obj) for obj in allObjs.values()] -@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB in {'PySide', 'PySide2'}, reason=skipreason) def test_PlotWidget(): def mkobjs(*args, **kwds): w = pg.PlotWidget(*args, **kwds) @@ -57,7 +56,7 @@ def test_PlotWidget(): for i in range(5): assert_alldead(mkobjs()) -@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB in {'PySide', 'PySide2'}, reason=skipreason) def test_ImageView(): def mkobjs(): iv = pg.ImageView() @@ -65,12 +64,12 @@ def test_ImageView(): iv.setImage(data) return mkrefs(iv, iv.imageItem, iv.view, iv.ui.histogram, data) - for i in range(5): + gc.collect() assert_alldead(mkobjs()) -@pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason=skipreason) +@pytest.mark.skipif(pg.Qt.QT_LIB in {'PySide', 'PySide2'}, reason=skipreason) def test_GraphicsWindow(): def mkobjs(): w = pg.GraphicsWindow() From 8930adc27e233995efc527043503d3cf25bde8e6 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Wed, 26 Feb 2020 10:06:15 -0800 Subject: [PATCH 513/607] Update tox config --- tox.ini | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/tox.ini b/tox.ini index 9091c8cb..f5c8e7a6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,16 @@ [tox] envlist = - ; qt 5.12.x - py{27,37}-pyside2-pip - py{35,37}-pyqt5-pip + ; qt latest + py{37,38}-{pyqt5,pyside2}_latest - ; qt 5.9.7 - py{27,37}-pyqt5-conda - py{27,37}-pyside2-conda + ; qt 5.12.x (LTS) + py{36,37}-{pyqt5,pyside2}_512 - ; qt 5.6.2 - py35-pyqt5-conda - ; consider dropping support... - ; py35-pyside2-conda + ; qt 5.9.7 (LTS) + py36-{pyqt5,pyside2}_59_conda ; qt 4.8.7 - py{27,36}-pyqt4-conda - py{27,36}-pyside-conda - + py27-{pyqt4,pyside}_conda [base] deps = @@ -34,17 +28,22 @@ deps= {[base]deps} pytest-cov pytest-xdist - pyside2-pip: pyside2 - pyqt5-pip: pyqt5 + h5py + pyside2_512: pyside2>=5.12,<5.13 + pyqt5_512: pyqt5>=5.12,<5.13 + pyside2_latest: pyside2 + pyqt5_latest: pyqt5 conda_deps= - pyside2-conda: pyside2 - pyside-conda: pyside - pyqt5-conda: pyqt - pyqt4-conda: pyqt=4 - + pyside2_59_conda: pyside2=5.9 + pyqt5_59_conda: pyqt=5.9 + pyqt4_conda: pyqt=4 + pyside_conda: pyside + conda_channels= conda-forge + free + commands= python -c "import pyqtgraph as pg; pg.systemInfo()" pytest {posargs:.} From 7199a4f4ce32ab31566426100f0bc6cb31efd3a9 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:25:34 -0800 Subject: [PATCH 514/607] deepcopy(dict) does not necessarily preserve insertion order --- pyqtgraph/tests/test_functions.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index fcd16254..6a6aaa33 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -367,8 +367,10 @@ def test_eq(): d1 = {'x': 1, 'y': np.nan, 3: ['a', np.nan, a3, 7, 2.3], 4: a4} d2 = deepcopy(d1) assert eq(d1, d2) - assert eq(OrderedDict(d1), OrderedDict(d2)) - assert not eq(OrderedDict(d1), d2) + d1_ordered = OrderedDict(d1) + d2_ordered = deepcopy(d1_ordered) + assert eq(d1_ordered, d2_ordered) + assert not eq(d1_ordered, d2) items = list(d1.items()) assert not eq(OrderedDict(items), OrderedDict(reversed(items))) From 6ed8a405feb25dba8511c1f281ae52e0be134f2a Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:27:10 -0800 Subject: [PATCH 515/607] Address FutureWarning about implicit float to int conversions --- pyqtgraph/graphicsItems/AxisItem.py | 4 ++-- pyqtgraph/graphicsItems/GradientEditorItem.py | 2 +- pyqtgraph/graphicsItems/GridItem.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index da57403f..e97f66d3 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -921,7 +921,7 @@ class AxisItem(GraphicsWidget): p2[axis] += tickLength*tickDir tickPen = self.pen() color = tickPen.color() - color.setAlpha(lineAlpha) + color.setAlpha(int(lineAlpha)) tickPen.setColor(color) tickSpecs.append((tickPen, Point(p1), Point(p2))) profiler('compute ticks') @@ -1078,7 +1078,7 @@ class AxisItem(GraphicsWidget): p.setFont(self.tickFont) p.setPen(self.textPen()) for rect, flags, text in textSpecs: - p.drawText(rect, flags, text) + p.drawText(rect, flags.__int__(), text) profiler('draw text') diff --git a/pyqtgraph/graphicsItems/GradientEditorItem.py b/pyqtgraph/graphicsItems/GradientEditorItem.py index b360b2f7..1cb11d1c 100644 --- a/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -654,7 +654,7 @@ class GradientEditorItem(TickSliderItem): s = s1 * (1.-f) + s2 * f v = v1 * (1.-f) + v2 * f c = QtGui.QColor() - c.setHsv(h,s,v) + c.setHsv(*map(int, [h,s,v])) if toQColor: return c else: diff --git a/pyqtgraph/graphicsItems/GridItem.py b/pyqtgraph/graphicsItems/GridItem.py index 0b1eb525..db64cbbf 100644 --- a/pyqtgraph/graphicsItems/GridItem.py +++ b/pyqtgraph/graphicsItems/GridItem.py @@ -153,7 +153,7 @@ class GridItem(UIGraphicsItem): continue ppl = dim[ax] / nl[ax] - c = np.clip(5.*(ppl-3), 0., 50.) + c = np.clip(5 * (ppl-3), 0., 50.).astype(int) linePen = self.opts['pen'] lineColor = self.opts['pen'].color() From ae776a807d6dcf5e1ab2124ab6bc7af9cb84af27 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:28:36 -0800 Subject: [PATCH 516/607] Filter out expected warnings --- pytest.ini | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pytest.ini b/pytest.ini index f53aea00..355e9dfd 100644 --- a/pytest.ini +++ b/pytest.ini @@ -12,4 +12,8 @@ filterwarnings = ignore:numpy.ufunc size changed, may indicate binary incompatibility.*:RuntimeWarning # Warnings generated from PyQt5.9 ignore:This method will be removed in future versions. Use 'tree.iter\(\)' or 'list\(tree.iter\(\)\)' instead.:PendingDeprecationWarning - ignore:'U' mode is deprecated\nplugin = open\(filename, 'rU'\):DeprecationWarning + ignore:.*'U' mode is deprecated.*:DeprecationWarning + # py36/pyside2_512 specific issue + ignore:split\(\) requires a non-empty pattern match\.:FutureWarning + # pyqtgraph specific warning we want to ignore during testing + ignore:Visible window deleted. To prevent this, store a reference to the window object. \ No newline at end of file From 87d6eae84d917aeb32ad5ffea3f0f36baaccd09c Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:29:16 -0800 Subject: [PATCH 517/607] Remove py2 pip warning message --- tox.ini | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index f5c8e7a6..130085ba 100644 --- a/tox.ini +++ b/tox.ini @@ -24,10 +24,10 @@ deps = [testenv] passenv = DISPLAY XAUTHORITY +setenv = PYTHONWARNINGS=ignore:DEPRECATION::pip._internal.cli.base_command deps= {[base]deps} pytest-cov - pytest-xdist h5py pyside2_512: pyside2>=5.12,<5.13 pyqt5_512: pyqt5>=5.12,<5.13 @@ -46,4 +46,4 @@ conda_channels= commands= python -c "import pyqtgraph as pg; pg.systemInfo()" - pytest {posargs:.} + pytest {posargs:} From 1d552feaf08dc7e25912b415a21d2ee126e246cb Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 28 Feb 2020 14:48:24 -0800 Subject: [PATCH 518/607] Update readme and contributing files --- CONTRIBUTING.md | 22 +++++++--------------- README.md | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9af2e508..461e9b14 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,11 +9,14 @@ Please use the following guidelines when preparing changes: * The preferred method for submitting changes is by github pull request against the "develop" branch. * Pull requests should include only a focused and related set of changes. Mixed features and unrelated changes may be rejected. * For major changes, it is recommended to discuss your plans on the mailing list or in a github issue before putting in too much effort. - * Along these lines, please note that `pyqtgraph.opengl` will be deprecated soon and replaced with VisPy. +* The following deprecations are being considered by the maintainers + * `pyqtgraph.opengl` may be deprecated and replaced with `VisPy` functionality + * After v0.11, pyqtgraph will adopt [NEP-29](https://numpy.org/neps/nep-0029-deprecation_policy.html) which will effectively mean that python2 support will be deprecated + * Qt4 will be deprecated shortly, as well as Qt5<5.9 (and potentially <5.12) ## Documentation -* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses nose / pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. +* Writing proper documentation and unit tests is highly encouraged. PyQtGraph uses pytest style testing, so tests should usually be included in a tests/ directory adjacent to the relevant code. * Documentation is generated with sphinx; please check that docstring changes compile correctly ## Style guidelines @@ -55,9 +58,7 @@ To make use of `pre-commit`, have it available in your `$PATH` and run `pre-comm * pytest-xdist * Optional: pytest-xvfb -If you have pytest < 5, you may also want to install the pytest-faulthandler -plugin to output extra debugging information in case of test failures. This -isn't necessary with pytest 5+ as the plugin was merged into core pytest. +If you have `pytest<5` (used in python2), you may also want to install `pytest-faulthandler==1.6` plugin to output extra debugging information in case of test failures. This isn't necessary with `pytest>=5` ### Tox @@ -68,13 +69,4 @@ As PyQtGraph supports a wide array of Qt-bindings, and python versions, we make ### Continous Integration -For our Continuous Integration, we utilize Azure Pipelines. On each OS, we test the following 6 configurations - -* Python2.7 with PyQt4 -* Python2.7 with PySide -* Python3.6 with PyQt5-5.9 -* Python3.6 with PySide2-5.9 -* Python3.7 with PyQt5-5.12 -* Python3.7 with PySide2-5.12 - -More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1) +For our Continuous Integration, we utilize Azure Pipelines. Tested configurations are visible on [README](README.md). More information on coverage and test failures can be found on the respective tabs of the [build results page](https://dev.azure.com/pyqtgraph/pyqtgraph/_build?definitionId=1) diff --git a/README.md b/README.md index 914523fd..b461f4f6 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ [![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop) - PyQtGraph ========= @@ -20,7 +19,8 @@ Requirements ------------ * PyQt 4.8+, PySide, PyQt5, or PySide2 -* python 2.7, or 3.x + * PySide2 5.14 does not have loadUiType functionality, and thus the example application will not work. You can follow along with restoring that functionality [here](https://bugreports.qt.io/browse/PYSIDE-1223). +* Python 2.7, or 3.x * Required * `numpy` * Optional @@ -34,14 +34,15 @@ Requirements Qt Bindings Test Matrix ----------------------- -Below is a table of the configurations we test and have confidence pyqtgraph will work with. All current operating major operating systems (Windows, macOS, Linux) are tested against this configuration. We recommend using the Qt 5.12 or 5.9 (either PyQt5 or PySide2) bindings. +The following table represents the python environments we test in our CI system. Our CI system uses Ubuntu 18.04, Windows Server 2019, and macOS 10.15 base images. -| Python Version | PyQt4 | PySide | PyQt5-5.6 | PySide2-5.6 | PyQt5-5.9 | PySide2-5.9 | PyQt5-5.12 | PySide2 5.12 | -| :-------------- | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | :----------------: | -| 2.7 | :white_check_mark: | :white_check_mark: | :x: | :x: | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: | -| 3.5 | :x: | :x: | :white_check_mark: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | -| 3.6 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | -| 3.7 | :x: | :x: | :x: | :x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | +| Qt-Bindings | Python 2.7 | Python 3.6 | Python 3.7 | Python 3.8 | +| :----------- | :----------------: | :----------------: | :----------------: | :----------------: | +| PyQt-4 | :white_check_mark: | :x: | :x: | :x: | +| PySide1 | :white_check_mark: | :x: | :x: | :x: | +| PyQt-5.9 | :x: | :white_check_mark: | :x: | :x: | +| PySide2-5.13 | :x: | :x: | :white_check_mark: | :x: | +| PyQt-5.14 | :x: | :x: | :x: | :white_check_mark: | * pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible * on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work From 3158c5b4db320853bfad6028d617184f0de74323 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 29 Feb 2020 14:38:19 -0800 Subject: [PATCH 519/607] Use int() instead of .__int__() --- pyqtgraph/graphicsItems/AxisItem.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index e97f66d3..2601ecae 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from ..python2_3 import asUnicode import numpy as np @@ -1078,7 +1079,7 @@ class AxisItem(GraphicsWidget): p.setFont(self.tickFont) p.setPen(self.textPen()) for rect, flags, text in textSpecs: - p.drawText(rect, flags.__int__(), text) + p.drawText(rect, int(flags), text) profiler('draw text') From 6985be2a6fe2133c9229bafc439f013bc5825ec5 Mon Sep 17 00:00:00 2001 From: Unknown Date: Sun, 1 Mar 2020 17:46:01 +0100 Subject: [PATCH 520/607] replaced incompatible string construction --- pyqtgraph/exporters/ImageExporter.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pyqtgraph/exporters/ImageExporter.py b/pyqtgraph/exporters/ImageExporter.py index e600afc9..a8d235a8 100644 --- a/pyqtgraph/exporters/ImageExporter.py +++ b/pyqtgraph/exporters/ImageExporter.py @@ -47,10 +47,7 @@ class ImageExporter(Exporter): def export(self, fileName=None, toBytes=False, copy=False): if fileName is None and not toBytes and not copy: - if QT_LIB in ['PySide', 'PySide2']: - filter = ["*."+str(f, encoding='utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] - else: - filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()] + 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: From 3509d79c0f9fc9cfd9b96caa542dea955b7d2d09 Mon Sep 17 00:00:00 2001 From: SamSchott Date: Fri, 6 Mar 2020 15:02:39 +0000 Subject: [PATCH 521/607] bug fix for `setPen`, `setBrush`, ... Fixes a bug where `setPen`, `setBrush` and `setLabelTextColor` would fail because they call `LegendItem.paint` without a pen. They should instead call `LegendItem.update`. --- pyqtgraph/graphicsItems/LegendItem.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 5c3083a2..7d60f37a 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -94,7 +94,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): pen = fn.mkPen(*args, **kargs) self.opts['pen'] = pen - self.paint() + self.update() def brush(self): return self.opts['brush'] @@ -105,7 +105,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): return self.opts['brush'] = brush - self.paint() + self.update() def labelTextColor(self): return self.opts['labelTextColor'] @@ -120,7 +120,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): for sample, label in self.items: label.setAttr('color', self.opts['labelTextColor']) - self.paint() + self.update() def setParentItem(self, p): ret = GraphicsWidget.setParentItem(self, p) From db6341de129d3249ca72140e0d7ef15b6bb5988b Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 6 Mar 2020 10:35:19 -0800 Subject: [PATCH 522/607] Removing use of travis CI --- .travis.yml | 195 ---------------------------------------------------- 1 file changed, 195 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 173fa668..00000000 --- a/.travis.yml +++ /dev/null @@ -1,195 +0,0 @@ -language: python -sudo: false -# Credit: Original .travis.yml lifted from VisPy - -# Here we use anaconda for 2.6 and 3.3, since it provides the simplest -# interface for running different versions of Python. We could also use -# it for 2.7, but the Ubuntu system has installable 2.7 Qt4-GL, which -# allows for more complete testing. -notifications: - email: false - -env: - # Enable python 2 and python 3 builds - # Note that the python 2.6 support ended. - - PYTHON=2.7 QT=pyqt4 TEST=extra - - PYTHON=2.7 QT=pyside TEST=standard - - PYTHON=3.5 QT=pyqt5 TEST=standard - # - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda - #- PYTHON=3.2 QT=pyqt5 TEST=standard - -services: - - xvfb - -before_install: - - if [ ${TRAVIS_PYTHON_VERSION:0:1} == "2" ]; then wget http://repo.continuum.io/miniconda/Miniconda-3.5.5-Linux-x86_64.sh -O miniconda.sh; else wget http://repo.continuum.io/miniconda/Miniconda3-3.5.5-Linux-x86_64.sh -O miniconda.sh; fi - - chmod +x miniconda.sh - - ./miniconda.sh -b -p /home/travis/mc - - export PATH=/home/travis/mc/bin:$PATH - - # not sure what is if block is for - - if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then - GIT_TARGET_EXTRA="+refs/heads/${TRAVIS_BRANCH}"; - GIT_SOURCE_EXTRA="+refs/pull/${TRAVIS_PULL_REQUEST}/merge"; - else - GIT_TARGET_EXTRA=""; - GIT_SOURCE_EXTRA=""; - fi; - - # to aid in debugging - - echo ${TRAVIS_BRANCH} - - echo ${TRAVIS_REPO_SLUG} - - echo ${GIT_TARGET_EXTRA} - - echo ${GIT_SOURCE_EXTRA} - -install: - - export GIT_FULL_HASH=`git rev-parse HEAD` - - conda update conda --yes - - conda create -n test_env python=${PYTHON} --yes - - source activate test_env - - conda install numpy scipy pyopengl pytest flake8 six coverage --yes - - echo ${QT} - - echo ${TEST} - - echo ${PYTHON} - - - if [ "${QT}" == "pyqt5" ]; then - conda install pyqt --yes; - fi; - - if [ "${QT}" == "pyqt4" ]; then - conda install pyqt=4 --yes; - fi; - - if [ "${QT}" == "pyside" ]; then - conda install pyside --yes; - fi; - - pip install pytest-xdist # multi-thread pytest - - pip install pytest-cov # add coverage stats - - # faulthandler support not built in to pytest for python 2.7 - - if [ "${PYTHON}" == "2.7" ]; then - pip install pytest-faulthandler; - export PYTEST_ADDOPTS="--faulthandler-timeout=15"; - fi; - - # Debugging helpers - - uname -a - - cat /etc/issue - - if [ "${PYTHON}" == "2.7" ]; then - python --version; - else - python3 --version; - fi; - -before_script: - # We need to create a (fake) display on Travis, let's use a funny resolution - - export DISPLAY=:99.0 - - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/custom_xvfb_99.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :99 -screen 0 1400x900x24 -ac +extension GLX +render - - # Make sure everyone uses the correct python (this is handled by conda) - - which python - - python --version - - pwd - - ls - # Help color output from each test - - RESET='\033[0m'; - RED='\033[00;31m'; - GREEN='\033[00;32m'; - YELLOW='\033[00;33m'; - BLUE='\033[00;34m'; - PURPLE='\033[00;35m'; - CYAN='\033[00;36m'; - WHITE='\033[00;37m'; - start_test() { - echo -e "${BLUE}======== Starting $1 ========${RESET}"; - }; - check_output() { - ret=$?; - if [ $ret == 0 ]; then - echo -e "${GREEN}>>>>>> $1 passed <<<<<<${RESET}"; - else - echo -e "${RED}>>>>>> $1 FAILED <<<<<<${RESET}"; - fi; - return $ret; - }; - - - if [ "${TEST}" == "extra" ]; then - start_test "repo size check"; - mkdir ~/repo-clone && cd ~/repo-clone && - git init && git remote add -t ${TRAVIS_BRANCH} origin git://github.com/${TRAVIS_REPO_SLUG}.git && - git fetch origin ${GIT_TARGET_EXTRA} && - git checkout -qf FETCH_HEAD && - git tag travis-merge-target && - git gc --aggressive && - TARGET_SIZE=`du -s . | sed -e "s/\t.*//"` && - git pull origin ${GIT_SOURCE_EXTRA} && - git gc --aggressive && - MERGE_SIZE=`du -s . | sed -e "s/\t.*//"` && - if [ "${MERGE_SIZE}" != "${TARGET_SIZE}" ]; then - SIZE_DIFF=`expr \( ${MERGE_SIZE} - ${TARGET_SIZE} \)`; - else - SIZE_DIFF=0; - fi; - fi; - -script: - - - source activate test_env - - # Check system info - - python -c "import pyqtgraph as pg; pg.systemInfo()" - - - # Check install works - - start_test "install test"; - python setup.py --quiet install; - check_output "install test"; - - # Run unit tests - - start_test "unit tests"; - PYTHONPATH=. pytest --cov pyqtgraph -sv; - check_output "unit tests"; - - echo "test script finished. Current directory:" - - pwd - - # check line endings - - if [ "${TEST}" == "extra" ]; then - start_test "line ending check"; - ! find ./ -name "*.py" | xargs file | grep CRLF && - ! find ./ -name "*.rst" | xargs file | grep CRLF; - check_output "line ending check"; - fi; - - # Check repo size does not expand too much - - if [ "${TEST}" == "extra" ]; then - start_test "repo size check"; - echo -e "Estimated content size difference = ${SIZE_DIFF} kB" && - test ${SIZE_DIFF} -lt 100; - check_output "repo size check"; - fi; - - # Check for style issues - - if [ "${TEST}" == "extra" ]; then - start_test "style check"; - cd ~/repo-clone && - git reset -q travis-merge-target && - python setup.py style && - check_output "style check"; - fi; - - # Check double-install fails - # Note the bash -c is because travis strips off the ! otherwise. - - start_test "double install test"; - bash -c "! python setup.py --quiet install"; - check_output "double install test"; - - # Check we can import pg - - start_test "import test"; - echo "import sys; print(sys.path)" | python && - cd /; echo "import pyqtgraph.examples" | python; - check_output "import test"; - -after_success: - - cd /home/travis/build/pyqtgraph/pyqtgraph - - pip install codecov --upgrade # add coverage integration service - - codecov - - pip install coveralls --upgrade # add another coverage integration service - - coveralls From 221d5d88305e42dc59557138d3f2a3d640058420 Mon Sep 17 00:00:00 2001 From: Ognyan Moore Date: Fri, 6 Mar 2020 10:35:31 -0800 Subject: [PATCH 523/607] No longer usign mailmap --- .mailmap | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 .mailmap diff --git a/.mailmap b/.mailmap deleted file mode 100644 index 025cf940..00000000 --- a/.mailmap +++ /dev/null @@ -1,12 +0,0 @@ -Luke Campagnola Luke Campagnola <> -Luke Campagnola Luke Campagnola -Megan Kratz meganbkratz@gmail.com <> -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Megan Kratz Megan Kratz -Ingo Breßler Ingo Breßler -Ingo Breßler Ingo B. - From 412698c8bb0358d182c1e4480a1647e7a0aaf2ba Mon Sep 17 00:00:00 2001 From: Gabriel Linder Date: Sat, 7 Mar 2020 22:42:01 +0100 Subject: [PATCH 524/607] Dark mode support. --- examples/__main__.py | 32 +++++++++++- examples/syntax.py | 118 +++++++++++++++++++++++++++++++++---------- 2 files changed, 122 insertions(+), 28 deletions(-) diff --git a/examples/__main__.py b/examples/__main__.py index 3867fbd3..22dd7ef0 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -22,6 +22,16 @@ elif QT_LIB == 'PyQt5': else: from .exampleLoaderTemplate_pyqt import Ui_Form +class App(QtGui.QApplication): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.paletteChanged.connect(self.onPaletteChange) + self.onPaletteChange(self.palette()) + + def onPaletteChange(self, palette): + self.dark_mode = palette.base().color().name().lower() != "#ffffff" + class ExampleLoader(QtGui.QMainWindow): def __init__(self): QtGui.QMainWindow.__init__(self) @@ -34,6 +44,7 @@ class ExampleLoader(QtGui.QMainWindow): self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() self.ui.codeView.setLayout(self.codeLayout) + #self.simulate_black_mode() self.hl = PythonHighlighter(self.ui.codeView.document()) self.codeLayout.addItem(QtGui.QSpacerItem(100,100,QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding), 0, 0) self.codeLayout.addWidget(self.codeBtn, 1, 1) @@ -53,6 +64,25 @@ class ExampleLoader(QtGui.QMainWindow): self.ui.codeView.textChanged.connect(self.codeEdited) self.codeBtn.clicked.connect(self.runEditedCode) + def simulate_black_mode(self): + """ + used to simulate MacOS "black mode" on other platforms + intended for debug only, as it manage only the QPlainTextEdit + """ + # first, a dark background + c = QtGui.QColor('#171717') + p = self.ui.codeView.palette() + p.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, c) + p.setColor(QtGui.QPalette.Inactive, QtGui.QPalette.Base, c) + self.ui.codeView.setPalette(p) + # then, a light font + f = QtGui.QTextCharFormat() + f.setForeground(QtGui.QColor('white')) + self.ui.codeView.setCurrentCharFormat(f) + # finally, override application automatic detection + app = QtGui.QApplication.instance() + app.dark_mode = True + def populateTree(self, root, examples): for key, val in examples.items(): item = QtGui.QTreeWidgetItem([key]) @@ -117,7 +147,7 @@ class ExampleLoader(QtGui.QMainWindow): self.loadFile(edited=True) def run(): - app = QtGui.QApplication([]) + app = App([]) loader = ExampleLoader() app.exec_() diff --git a/examples/syntax.py b/examples/syntax.py index cd2cccf1..95417827 100644 --- a/examples/syntax.py +++ b/examples/syntax.py @@ -30,17 +30,75 @@ def format(color, style=''): return _format -# Syntax styles that can be shared by all languages -STYLES = { - 'keyword': format('blue'), - 'operator': format('red'), - 'brace': format('darkGray'), - 'defclass': format('black', 'bold'), - 'string': format('magenta'), - 'string2': format('darkMagenta'), - 'comment': format('darkGreen', 'italic'), - 'self': format('black', 'italic'), - 'numbers': format('brown'), +class LightThemeColors: + + Red = "#B71C1C" + Pink = "#FCE4EC" + Purple = "#4A148C" + DeepPurple = "#311B92" + Indigo = "#1A237E" + Blue = "#0D47A1" + LightBlue = "#01579B" + Cyan = "#006064" + Teal = "#004D40" + Green = "#1B5E20" + LightGreen = "#33691E" + Lime = "#827717" + Yellow = "#F57F17" + Amber = "#FF6F00" + Orange = "#E65100" + DeepOrange = "#BF360C" + Brown = "#3E2723" + Grey = "#212121" + BlueGrey = "#263238" + + +class DarkThemeColors: + + Red = "#F44336" + Pink = "#F48FB1" + Purple = "#CE93D8" + DeepPurple = "#B39DDB" + Indigo = "#9FA8DA" + Blue = "#90CAF9" + LightBlue = "#81D4FA" + Cyan = "#80DEEA" + Teal = "#80CBC4" + Green = "#A5D6A7" + LightGreen = "#C5E1A5" + Lime = "#E6EE9C" + Yellow = "#FFF59D" + Amber = "#FFE082" + Orange = "#FFCC80" + DeepOrange = "#FFAB91" + Brown = "#BCAAA4" + Grey = "#EEEEEE" + BlueGrey = "#B0BEC5" + + +LIGHT_STYLES = { + 'keyword': format(LightThemeColors.Blue, 'bold'), + 'operator': format(LightThemeColors.Red, 'bold'), + 'brace': format(LightThemeColors.Purple), + 'defclass': format(LightThemeColors.Indigo, 'bold'), + 'string': format(LightThemeColors.Amber), + 'string2': format(LightThemeColors.DeepPurple), + 'comment': format(LightThemeColors.Green, 'italic'), + 'self': format(LightThemeColors.Blue, 'bold'), + 'numbers': format(LightThemeColors.Teal), +} + + +DARK_STYLES = { + 'keyword': format(DarkThemeColors.Blue, 'bold'), + 'operator': format(DarkThemeColors.Red, 'bold'), + 'brace': format(DarkThemeColors.Purple), + 'defclass': format(DarkThemeColors.Indigo, 'bold'), + 'string': format(DarkThemeColors.Amber), + 'string2': format(DarkThemeColors.DeepPurple), + 'comment': format(DarkThemeColors.Green, 'italic'), + 'self': format(DarkThemeColors.Blue, 'bold'), + 'numbers': format(DarkThemeColors.Teal), } @@ -54,7 +112,7 @@ class PythonHighlighter(QSyntaxHighlighter): 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'not', 'or', 'pass', 'print', 'raise', 'return', 'try', 'while', 'yield', - 'None', 'True', 'False', + 'None', 'True', 'False', 'async', 'await', ] # Python operators @@ -81,42 +139,42 @@ class PythonHighlighter(QSyntaxHighlighter): # Multi-line strings (expression, flag, style) # FIXME: The triple-quotes in these two lines will mess up the # syntax highlighting from this point onward - self.tri_single = (QRegExp("'''"), 1, STYLES['string2']) - self.tri_double = (QRegExp('"""'), 2, STYLES['string2']) + self.tri_single = (QRegExp("'''"), 1, 'string2') + self.tri_double = (QRegExp('"""'), 2, 'string2') rules = [] # Keyword, operator, and brace rules - rules += [(r'\b%s\b' % w, 0, STYLES['keyword']) + rules += [(r'\b%s\b' % w, 0, 'keyword') for w in PythonHighlighter.keywords] - rules += [(r'%s' % o, 0, STYLES['operator']) + rules += [(r'%s' % o, 0, 'operator') for o in PythonHighlighter.operators] - rules += [(r'%s' % b, 0, STYLES['brace']) + rules += [(r'%s' % b, 0, 'brace') for b in PythonHighlighter.braces] # All other rules rules += [ # 'self' - (r'\bself\b', 0, STYLES['self']), + (r'\bself\b', 0, 'self'), # 'def' followed by an identifier - (r'\bdef\b\s*(\w+)', 1, STYLES['defclass']), + (r'\bdef\b\s*(\w+)', 1, 'defclass'), # 'class' followed by an identifier - (r'\bclass\b\s*(\w+)', 1, STYLES['defclass']), + (r'\bclass\b\s*(\w+)', 1, 'defclass'), # Numeric literals - (r'\b[+-]?[0-9]+[lL]?\b', 0, STYLES['numbers']), - (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, STYLES['numbers']), - (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, STYLES['numbers']), + (r'\b[+-]?[0-9]+[lL]?\b', 0, 'numbers'), + (r'\b[+-]?0[xX][0-9A-Fa-f]+[lL]?\b', 0, 'numbers'), + (r'\b[+-]?[0-9]+(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?\b', 0, 'numbers'), # Double-quoted string, possibly containing escape sequences - (r'"[^"\\]*(\\.[^"\\]*)*"', 0, STYLES['string']), + (r'"[^"\\]*(\\.[^"\\]*)*"', 0, 'string'), # Single-quoted string, possibly containing escape sequences - (r"'[^'\\]*(\\.[^'\\]*)*'", 0, STYLES['string']), + (r"'[^'\\]*(\\.[^'\\]*)*'", 0, 'string'), # From '#' until a newline - (r'#[^\n]*', 0, STYLES['comment']), + (r'#[^\n]*', 0, 'comment'), ] @@ -124,12 +182,18 @@ class PythonHighlighter(QSyntaxHighlighter): self.rules = [(QRegExp(pat), index, fmt) for (pat, index, fmt) in rules] + @property + def styles(self): + app = QtGui.QApplication.instance() + return DARK_STYLES if app.dark_mode else LIGHT_STYLES + def highlightBlock(self, text): """Apply syntax highlighting to the given block of text. """ # Do other syntax formatting for expression, nth, format in self.rules: index = expression.indexIn(text, 0) + format = self.styles[format] while index >= 0: # We actually want the index of the nth match @@ -175,7 +239,7 @@ class PythonHighlighter(QSyntaxHighlighter): self.setCurrentBlockState(in_state) length = len(text) - start + add # Apply formatting - self.setFormat(start, length, style) + self.setFormat(start, length, self.styles[style]) # Look for the next match start = delimiter.indexIn(text, start + length) From d0b92349ddfde42f5261c78d610cc655216eea62 Mon Sep 17 00:00:00 2001 From: Gabriel Linder Date: Sun, 8 Mar 2020 10:34:54 +0100 Subject: [PATCH 525/607] Intercept light/dark modes transitions on MacOS. --- examples/__main__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/examples/__main__.py b/examples/__main__.py index 22dd7ef0..df390cb9 100644 --- a/examples/__main__.py +++ b/examples/__main__.py @@ -44,8 +44,9 @@ class ExampleLoader(QtGui.QMainWindow): self.codeBtn = QtGui.QPushButton('Run Edited Code') self.codeLayout = QtGui.QGridLayout() self.ui.codeView.setLayout(self.codeLayout) - #self.simulate_black_mode() self.hl = PythonHighlighter(self.ui.codeView.document()) + app = QtGui.QApplication.instance() + app.paletteChanged.connect(self.updateTheme) self.codeLayout.addItem(QtGui.QSpacerItem(100,100,QtGui.QSizePolicy.Expanding,QtGui.QSizePolicy.Expanding), 0, 0) self.codeLayout.addWidget(self.codeBtn, 1, 1) self.codeBtn.hide() @@ -83,6 +84,9 @@ class ExampleLoader(QtGui.QMainWindow): app = QtGui.QApplication.instance() app.dark_mode = True + def updateTheme(self): + self.hl = PythonHighlighter(self.ui.codeView.document()) + def populateTree(self, root, examples): for key, val in examples.items(): item = QtGui.QTreeWidgetItem([key]) From 3ba76475d484bd79af070f8354205fadf4f87437 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Thu, 2 Apr 2020 22:55:33 -0700 Subject: [PATCH 526/607] Added ImageExporter test for py2-pyside fix --- pyqtgraph/exporters/tests/test_image.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 pyqtgraph/exporters/tests/test_image.py diff --git a/pyqtgraph/exporters/tests/test_image.py b/pyqtgraph/exporters/tests/test_image.py new file mode 100644 index 00000000..6f52eceb --- /dev/null +++ b/pyqtgraph/exporters/tests/test_image.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- +import pyqtgraph as pg +from pyqtgraph.exporters import ImageExporter + +app = pg.mkQApp() + + +def test_ImageExporter_filename_dialog(): + """Tests ImageExporter code path that opens a file dialog. Regression test + for pull request 1133.""" + p = pg.plot() + exp = ImageExporter(p.getPlotItem()) + exp.export() From db67a256a925c5a2811b74d537d39bce201b4251 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 3 Apr 2020 10:06:25 -0700 Subject: [PATCH 527/607] Miscellaneous doc fixups (#1142) * Miscellaneous doc cleanup * Moved dockarea up a level (like flowchart, parametertree). Removed extraneous parametertree doc --- doc/source/apireference.rst | 2 + doc/source/conf.py | 3 +- doc/source/dockarea.rst | 11 ++ doc/source/graphicsItems/index.rst | 2 +- doc/source/graphicswindow.rst | 20 ++-- doc/source/installation.rst | 107 +++++++++++--------- doc/source/plotting.rst | 4 +- doc/source/widgets/dockarea.rst | 5 - doc/source/widgets/index.rst | 2 - doc/source/widgets/parametertree.rst | 5 - pyqtgraph/flowchart/Flowchart.py | 3 +- pyqtgraph/functions.py | 11 +- pyqtgraph/graphicsItems/AxisItem.py | 4 +- pyqtgraph/graphicsItems/HistogramLUTItem.py | 31 +++--- pyqtgraph/graphicsItems/InfiniteLine.py | 4 +- pyqtgraph/graphicsItems/LinearRegionItem.py | 21 ++-- pyqtgraph/graphicsItems/PlotDataItem.py | 37 ++++--- pyqtgraph/graphicsWindows.py | 11 +- pyqtgraph/widgets/GraphicsLayoutWidget.py | 30 +++--- 19 files changed, 169 insertions(+), 144 deletions(-) create mode 100644 doc/source/dockarea.rst delete mode 100644 doc/source/widgets/dockarea.rst delete mode 100644 doc/source/widgets/parametertree.rst diff --git a/doc/source/apireference.rst b/doc/source/apireference.rst index c4dc64aa..c52c8df1 100644 --- a/doc/source/apireference.rst +++ b/doc/source/apireference.rst @@ -13,5 +13,7 @@ Contents: 3dgraphics/index colormap parametertree/index + dockarea graphicsscene/index flowchart/index + graphicswindow diff --git a/doc/source/conf.py b/doc/source/conf.py index 3ec48f75..dd5e0718 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -93,7 +93,7 @@ pygments_style = 'sphinx' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the @@ -216,4 +216,3 @@ man_pages = [ ('index', 'pyqtgraph', 'pyqtgraph Documentation', ['Luke Campagnola'], 1) ] - diff --git a/doc/source/dockarea.rst b/doc/source/dockarea.rst new file mode 100644 index 00000000..384581d3 --- /dev/null +++ b/doc/source/dockarea.rst @@ -0,0 +1,11 @@ +Dock Area Module +================ + +.. automodule:: pyqtgraph.dockarea + :members: + +.. autoclass:: pyqtgraph.dockarea.DockArea + :members: + +.. autoclass:: pyqtgraph.dockarea.Dock + :members: diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst index 7042d27e..eec86610 100644 --- a/doc/source/graphicsItems/index.rst +++ b/doc/source/graphicsItems/index.rst @@ -24,6 +24,7 @@ Contents: axisitem textitem errorbaritem + bargraphitem arrowitem fillbetweenitem curvepoint @@ -42,4 +43,3 @@ Contents: graphicsitem uigraphicsitem graphicswidgetanchor - diff --git a/doc/source/graphicswindow.rst b/doc/source/graphicswindow.rst index 3d5641c3..0602ae7e 100644 --- a/doc/source/graphicswindow.rst +++ b/doc/source/graphicswindow.rst @@ -1,8 +1,16 @@ -Basic display widgets -===================== +Deprecated Window Classes +========================= - - GraphicsWindow - - GraphicsView - - GraphicsLayoutItem - - ViewBox +.. automodule:: pyqtgraph.graphicsWindows +.. autoclass:: pyqtgraph.GraphicsWindow + :members: + +.. autoclass:: pyqtgraph.TabWindow + :members: + +.. autoclass:: pyqtgraph.PlotWindow + :members: + +.. autoclass:: pyqtgraph.ImageWindow + :members: diff --git a/doc/source/installation.rst b/doc/source/installation.rst index e3e1f1fc..fd9f5288 100644 --- a/doc/source/installation.rst +++ b/doc/source/installation.rst @@ -1,55 +1,70 @@ Installation ============ -There are many different ways to install pyqtgraph, depending on your needs: - -* The most common way to install pyqtgraph is with pip:: - - $ pip install pyqtgraph - - Some users may need to call ``pip3`` instead. This method should work on - all platforms. -* To get access to the very latest features and bugfixes you have three choice:: - - 1. Clone pyqtgraph from github:: - - $ git clone https://github.com/pyqtgraph/pyqtgraph - - Now you can install pyqtgraph from the source:: - - $ python setup.py install - - 2. Directly install from GitHub repo:: - - $ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop - - You can change to ``develop`` of the above command to the branch - name or the commit you prefer. - - 3. - You can simply place the pyqtgraph folder someplace importable, such as - inside the root of another project. PyQtGraph does not need to be "built" or - compiled in any way. - -* Packages for pyqtgraph are also available in a few other forms: - - * **Anaconda**: ``conda install pyqtgraph`` - * **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or - download the .deb file linked at the top of the pyqtgraph web page. - * **Arch Linux:** has packages (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577) - * **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. - - -Requirements -============ - PyQtGraph depends on: - + * Python 2.7 or Python 3.x * A Qt library such as PyQt4, PyQt5, PySide, or PySide2 * numpy -The easiest way to meet these dependencies is with ``pip`` or with a scientific python -distribution like Anaconda. +The easiest way to meet these dependencies is with ``pip`` or with a scientific +python distribution like Anaconda. -.. _pyqtgraph: http://www.pyqtgraph.org/ +There are many different ways to install pyqtgraph, depending on your needs: + +pip +--- + +The most common way to install pyqtgraph is with pip:: + + $ pip install pyqtgraph + +Some users may need to call ``pip3`` instead. This method should work on all +platforms. + +conda +----- + +pyqtgraph is on the default Anaconda channel:: + + $ conda install pyqtgraph + +It is also available in the conda-forge channel:: + + $ conda install -c conda-forge pyqtgraph + +From Source +----------- + +To get access to the very latest features and bugfixes you have three choices: + +1. Clone pyqtgraph from github:: + + $ git clone https://github.com/pyqtgraph/pyqtgraph + $ cd pyqtgraph + + Now you can install pyqtgraph from the source:: + + $ pip install . + +2. Directly install from GitHub repo:: + + $ pip install git+git://github.com/pyqtgraph/pyqtgraph.git@develop + + You can change ``develop`` of the above command to the branch name or the + commit you prefer. + +3. You can simply place the pyqtgraph folder someplace importable, such as + inside the root of another project. PyQtGraph does not need to be "built" or + compiled in any way. + +Other Packages +-------------- + +Packages for pyqtgraph are also available in a few other forms: + +* **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or + download the .deb file linked at the top of the pyqtgraph web page. +* **Arch Linux:** https://www.archlinux.org/packages/community/any/python-pyqtgraph/ +* **Windows:** Download and run the .exe installer file linked at the top of the + pyqtgraph web page: http://pyqtgraph.org diff --git a/doc/source/plotting.rst b/doc/source/plotting.rst index 8a99663a..956f5b97 100644 --- a/doc/source/plotting.rst +++ b/doc/source/plotting.rst @@ -41,7 +41,7 @@ There are several classes invloved in displaying plot data. Most of these classe * :class:`AxisItem ` - Displays axis values, ticks, and labels. Most commonly used with PlotItem. * Container Classes (subclasses of QWidget; may be embedded in PyQt GUIs) * :class:`PlotWidget ` - A subclass of GraphicsView with a single PlotItem displayed. Most of the methods provided by PlotItem are also available through PlotWidget. - * :class:`GraphicsLayoutWidget ` - QWidget subclass displaying a single GraphicsLayoutItem. Most of the methods provided by GraphicsLayoutItem are also available through GraphicsLayoutWidget. + * :class:`GraphicsLayoutWidget ` - QWidget subclass displaying a single :class:`~pyqtgraph.GraphicsLayout`. Most of the methods provided by :class:`~pyqtgraph.GraphicsLayout` are also available through GraphicsLayoutWidget. .. image:: images/plottingClasses.png @@ -69,5 +69,3 @@ Create/show a plot widget, display three data curves:: for i in range(3): plotWidget.plot(x, y[i], pen=(i,3)) ## setting pen=(i,3) automaticaly creates three different-colored pens - - diff --git a/doc/source/widgets/dockarea.rst b/doc/source/widgets/dockarea.rst deleted file mode 100644 index 09a6acca..00000000 --- a/doc/source/widgets/dockarea.rst +++ /dev/null @@ -1,5 +0,0 @@ -dockarea module -=============== - -.. automodule:: pyqtgraph.dockarea - :members: diff --git a/doc/source/widgets/index.rst b/doc/source/widgets/index.rst index 9cfbc0c4..e5acb7f0 100644 --- a/doc/source/widgets/index.rst +++ b/doc/source/widgets/index.rst @@ -12,11 +12,9 @@ Contents: plotwidget imageview - dockarea spinbox gradientwidget histogramlutwidget - parametertree consolewidget colormapwidget scatterplotwidget diff --git a/doc/source/widgets/parametertree.rst b/doc/source/widgets/parametertree.rst deleted file mode 100644 index 565b930b..00000000 --- a/doc/source/widgets/parametertree.rst +++ /dev/null @@ -1,5 +0,0 @@ -parametertree module -==================== - -.. automodule:: pyqtgraph.parametertree - :members: diff --git a/pyqtgraph/flowchart/Flowchart.py b/pyqtgraph/flowchart/Flowchart.py index 2e7ed0eb..e269c62f 100644 --- a/pyqtgraph/flowchart/Flowchart.py +++ b/pyqtgraph/flowchart/Flowchart.py @@ -508,7 +508,7 @@ class Flowchart(Node): self.sigStateChanged.emit() def loadFile(self, fileName=None, startDir=None): - """Load a flowchart (*.fc) file. + """Load a flowchart (``*.fc``) file. """ if fileName is None: if startDir is None: @@ -938,4 +938,3 @@ class FlowchartWidget(dockarea.DockArea): class FlowchartNode(Node): pass - diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 45e9aad6..8e1de665 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -934,10 +934,12 @@ def solveBilinearTransform(points1, points2): return matrix def rescaleData(data, scale, offset, dtype=None, clip=None): - """Return data rescaled and optionally cast to a new dtype:: - + """Return data rescaled and optionally cast to a new dtype. + + The scaling operation is:: + data => (data-offset) * scale - + """ if dtype is None: dtype = data.dtype @@ -2503,6 +2505,3 @@ class SignalBlock(object): def __exit__(self, *args): if self.reconnect: self.signal.connect(self.slot) - - - diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 2601ecae..6b2c63df 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -36,7 +36,7 @@ class AxisItem(GraphicsWidget): without any scaling prefix (eg, 'V' instead of 'mV'). The scaling prefix will be automatically prepended based on the range of data displayed. - **args All extra keyword arguments become CSS style options for + args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== =============================================================== """ @@ -256,7 +256,7 @@ class AxisItem(GraphicsWidget): without any scaling prefix (eg, 'V' instead of 'mV'). The scaling prefix will be automatically prepended based on the range of data displayed. - **args All extra keyword arguments become CSS style options for + args All extra keyword arguments become CSS style options for the tag which will surround the axis label and units. ============== ============================================================= diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index 687c2e3f..ad39b60e 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ GraphicsWidget displaying an image histogram along with gradient editor. Can be used to adjust the appearance of images. """ @@ -32,22 +33,20 @@ class HistogramLUTItem(GraphicsWidget): - Movable region over histogram to select black/white levels - Gradient editor to define color lookup table for single-channel images - Parameters - ---------- - image : ImageItem or None - If *image* is provided, then the control will be automatically linked to - the image and changes to the control will be immediately reflected in - the image's appearance. - fillHistogram : bool - By default, the histogram is rendered with a fill. - For performance, set *fillHistogram* = False. - rgbHistogram : bool - Sets whether the histogram is computed once over all channels of the - image, or once per channel. - levelMode : 'mono' or 'rgba' - If 'mono', then only a single set of black/whilte level lines is drawn, - and the levels apply to all channels in the image. If 'rgba', then one - set of levels is drawn for each channel. + ================ =========================================================== + image (:class:`~pyqtgraph.ImageItem` or ``None``) If *image* is + provided, then the control will be automatically linked to + the image and changes to the control will be immediately + reflected in the image's appearance. + fillHistogram (bool) By default, the histogram is rendered with a fill. + For performance, set ``fillHistogram=False`` + rgbHistogram (bool) Sets whether the histogram is computed once over all + channels of the image, or once per channel. + levelMode 'mono' or 'rgba'. If 'mono', then only a single set of + black/white level lines is drawn, and the levels apply to + all channels in the image. If 'rgba', then one set of + levels is drawn for each channel. + ================ =========================================================== """ sigLookupTableChanged = QtCore.Signal(object) diff --git a/pyqtgraph/graphicsItems/InfiniteLine.py b/pyqtgraph/graphicsItems/InfiniteLine.py index 36505026..37d84c7e 100644 --- a/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/pyqtgraph/graphicsItems/InfiniteLine.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from ..Point import Point from .GraphicsObject import GraphicsObject @@ -160,7 +161,8 @@ class InfiniteLine(GraphicsObject): ============= ========================================================= **Arguments** marker String indicating the style of marker to add: - '<|', '|>', '>|', '|<', '<|>', '>|<', '^', 'v', 'o' + ``'<|'``, ``'|>'``, ``'>|'``, ``'|<'``, ``'<|>'``, + ``'>|<'``, ``'^'``, ``'v'``, ``'o'`` position Position (0.0-1.0) along the visible extent of the line to place the marker. Default is 0.5. size Size of the marker in pixels. Default is 10.0. diff --git a/pyqtgraph/graphicsItems/LinearRegionItem.py b/pyqtgraph/graphicsItems/LinearRegionItem.py index 9903dac5..56ff5748 100644 --- a/pyqtgraph/graphicsItems/LinearRegionItem.py +++ b/pyqtgraph/graphicsItems/LinearRegionItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, QtCore from .GraphicsObject import GraphicsObject from .InfiniteLine import InfiniteLine @@ -54,17 +55,17 @@ class LinearRegionItem(GraphicsObject): False, they are static. bounds Optional [min, max] bounding values for the region span Optional [min, max] giving the range over the view to draw - the region. For example, with a vertical line, use span=(0.5, 1) - to draw only on the top half of the view. + the region. For example, with a vertical line, use + ``span=(0.5, 1)`` to draw only on the top half of the + view. swapMode Sets the behavior of the region when the lines are moved such that - their order reverses: - * "block" means the user cannot drag one line past the other - * "push" causes both lines to be moved if one would cross the other - * "sort" means that lines may trade places, but the output of - getRegion always gives the line positions in ascending order. - * None means that no attempt is made to handle swapped line - positions. - The default is "sort". + their order reverses. "block" means the user cannot drag + one line past the other. "push" causes both lines to be + moved if one would cross the other. "sort" means that + lines may trade places, but the output of getRegion + always gives the line positions in ascending order. None + means that no attempt is made to handle swapped line + positions. The default is "sort". ============== ===================================================================== """ diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 172e3beb..58a218c7 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import numpy as np from .. import metaarray as metaarray from ..Qt import QtCore @@ -40,25 +41,30 @@ class PlotDataItem(GraphicsObject): **Data initialization arguments:** (x,y data only) =================================== ====================================== - PlotDataItem(xValues, yValues) x and y values may be any sequence (including ndarray) of real numbers - PlotDataItem(yValues) y values only -- x will be automatically set to range(len(y)) + PlotDataItem(xValues, yValues) x and y values may be any sequence + (including ndarray) of real numbers + PlotDataItem(yValues) y values only -- x will be + automatically set to range(len(y)) PlotDataItem(x=xValues, y=yValues) x and y given by keyword arguments - PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where x=data[:,0] and y=data[:,1] + PlotDataItem(ndarray(Nx2)) numpy array with shape (N, 2) where + ``x=data[:,0]`` and ``y=data[:,1]`` =================================== ====================================== **Data initialization arguments:** (x,y data AND may include spot style) - =========================== ========================================= - PlotDataItem(recarray) numpy array with dtype=[('x', float), ('y', float), ...] - PlotDataItem(list-of-dicts) [{'x': x, 'y': y, ...}, ...] - PlotDataItem(dict-of-lists) {'x': [...], 'y': [...], ...} - PlotDataItem(MetaArray) 1D array of Y values with X sepecified as axis values - OR 2D array with a column 'y' and extra columns as needed. - =========================== ========================================= + ============================ ========================================= + PlotDataItem(recarray) numpy array with ``dtype=[('x', float), + ('y', float), ...]`` + PlotDataItem(list-of-dicts) ``[{'x': x, 'y': y, ...}, ...]`` + PlotDataItem(dict-of-lists) ``{'x': [...], 'y': [...], ...}`` + PlotDataItem(MetaArray) 1D array of Y values with X sepecified as + axis values OR 2D array with a column 'y' + and extra columns as needed. + ============================ ========================================= **Line style keyword arguments:** - ========== ============================================================================== + ============ ============================================================================== connect Specifies how / whether vertexes should be connected. See :func:`arrayToQPath() ` pen Pen to use for drawing line between points. @@ -67,15 +73,14 @@ class PlotDataItem(GraphicsObject): shadowPen Pen for secondary line to draw behind the primary line. disabled by default. May be any single argument accepted by :func:`mkPen() ` fillLevel Fill the area between the curve and fillLevel - fillOutline (bool) If True, an outline surrounding the *fillLevel* - area is drawn. - fillBrush Fill to use when fillLevel is specified. + fillOutline (bool) If True, an outline surrounding the *fillLevel* area is drawn. + fillBrush Fill to use when fillLevel is specified. May be any single argument accepted by :func:`mkBrush() ` stepMode If True, two orthogonal lines are drawn for each sample as steps. This is commonly used when drawing histograms. - Note that in this case, `len(x) == len(y) + 1` + Note that in this case, ``len(x) == len(y) + 1`` (added in version 0.9.9) - ========== ============================================================================== + ============ ============================================================================== **Point style keyword arguments:** (see :func:`ScatterPlotItem.setData() ` for more information) diff --git a/pyqtgraph/graphicsWindows.py b/pyqtgraph/graphicsWindows.py index b6a321ee..4033baf3 100644 --- a/pyqtgraph/graphicsWindows.py +++ b/pyqtgraph/graphicsWindows.py @@ -15,11 +15,10 @@ from .widgets.GraphicsView import GraphicsView class GraphicsWindow(GraphicsLayoutWidget): """ - (deprecated; use GraphicsLayoutWidget instead) + (deprecated; use :class:`~pyqtgraph.GraphicsLayoutWidget` instead) - Convenience subclass of :class:`GraphicsLayoutWidget - `. This class is intended for use from - the interactive python prompt. + Convenience subclass of :class:`~pyqtgraph.GraphicsLayoutWidget`. This class + is intended for use from the interactive python prompt. """ def __init__(self, title=None, size=(800,600), **kargs): mkQApp() @@ -50,7 +49,7 @@ class TabWindow(QtGui.QMainWindow): class PlotWindow(PlotWidget): """ - (deprecated; use PlotWidget instead) + (deprecated; use :class:`~pyqtgraph.PlotWidget` instead) """ def __init__(self, title=None, **kargs): mkQApp() @@ -66,7 +65,7 @@ class PlotWindow(PlotWidget): class ImageWindow(ImageView): """ - (deprecated; use ImageView instead) + (deprecated; use :class:`~pyqtgraph.ImageView` instead) """ def __init__(self, *args, **kargs): mkQApp() diff --git a/pyqtgraph/widgets/GraphicsLayoutWidget.py b/pyqtgraph/widgets/GraphicsLayoutWidget.py index 3b41a3ca..6249ba26 100644 --- a/pyqtgraph/widgets/GraphicsLayoutWidget.py +++ b/pyqtgraph/widgets/GraphicsLayoutWidget.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from ..Qt import QtGui, mkQApp from ..graphicsItems.GraphicsLayout import GraphicsLayout from .GraphicsView import GraphicsView @@ -17,21 +18,20 @@ class GraphicsLayoutWidget(GraphicsView): p2 = w.addPlot(row=0, col=1) v = w.addViewBox(row=1, col=0, colspan=2) - Parameters - ---------- - parent : QWidget or None - The parent widget (see QWidget.__init__) - show : bool - If True, then immediately show the widget after it is created. - If the widget has no parent, then it will be shown inside a new window. - size : (width, height) tuple - Optionally resize the widget. Note: if this widget is placed inside a - layout, then this argument has no effect. - title : str or None - If specified, then set the window title for this widget. - kargs : - All extra arguments are passed to - :func:`GraphicsLayout.__init__() ` + ========= ================================================================= + parent (QWidget or None) The parent widget. + show (bool) If True, then immediately show the widget after it is + created. If the widget has no parent, then it will be shown + inside a new window. + size (width, height) tuple. Optionally resize the widget. Note: if + this widget is placed inside a layout, then this argument has no + effect. + title (str or None) If specified, then set the window title for this + widget. + kargs All extra arguments are passed to + :meth:`GraphicsLayout.__init__ + ` + ========= ================================================================= This class wraps several methods from its internal GraphicsLayout: From a5dd549be1e120adce676d0f327b36db4e6777df Mon Sep 17 00:00:00 2001 From: lcmcninch Date: Fri, 3 Apr 2020 18:33:21 -0400 Subject: [PATCH 528/607] Pass showAxRect keyword arguments to setRange to allow caller to set padding, etc. (#1145) Co-authored-by: Luke McNinch --- pyqtgraph/graphicsItems/ViewBox/ViewBox.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index e504be56..bf2bb5b5 100644 --- a/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1303,8 +1303,11 @@ class ViewBox(GraphicsWidget): self.rbScaleBox.scale(r.width(), r.height()) self.rbScaleBox.show() - def showAxRect(self, ax): - self.setRange(ax.normalized()) # be sure w, h are correct coordinates + def showAxRect(self, ax, **kwargs): + """Set the visible range to the given rectangle + Passes keyword arguments to setRange + """ + self.setRange(ax.normalized(), **kwargs) # be sure w, h are correct coordinates self.sigRangeChangedManually.emit(self.state['mouseEnabled']) def allChildren(self, item=None): From 61967bd7f7b2f8db482f52669e443878f39151a3 Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Thu, 19 Dec 2019 21:35:07 +0100 Subject: [PATCH 529/607] add nanfix --- pyqtgraph/functions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 8e1de665..b82e482b 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1113,7 +1113,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): # awkward, but fastest numpy native nan evaluation # nanMask = None - if data.dtype.kind == 'f' and np.isnan(data.min()): + if data.ndim == 2 and data.dtype.kind == 'f' and np.isnan(data.min()): nanMask = np.isnan(data) # Apply levels if given if levels is not None: From daeacad71ff680658d4071d2823378377fff035f Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Sat, 7 Mar 2020 10:16:49 +0100 Subject: [PATCH 530/607] Make nanMask compatible with 3D data --- pyqtgraph/functions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index b82e482b..3863b51a 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1113,7 +1113,10 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): # awkward, but fastest numpy native nan evaluation # nanMask = None - if data.ndim == 2 and data.dtype.kind == 'f' and np.isnan(data.min()): + if data.dtype.kind == 'f' and np.isnan(data.min()): + nanMask = np.isnan(data) + if data.ndim > 2: + nanMask = np.any(nanMask, axis=-1) nanMask = np.isnan(data) # Apply levels if given if levels is not None: From 6f34da586dfac635bea3ceb0f207574843b1004c Mon Sep 17 00:00:00 2001 From: Jan Kotanski Date: Fri, 13 Mar 2020 11:01:47 +0100 Subject: [PATCH 531/607] remove second nanMask = np.isnan(data) --- pyqtgraph/functions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 3863b51a..e788afa7 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1117,7 +1117,6 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): nanMask = np.isnan(data) if data.ndim > 2: nanMask = np.any(nanMask, axis=-1) - nanMask = np.isnan(data) # Apply levels if given if levels is not None: if isinstance(levels, np.ndarray) and levels.ndim == 2: From 988e5c12223b708b334b561feacd97913a4854dc Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Fri, 3 Apr 2020 10:21:55 -0700 Subject: [PATCH 532/607] Test makeARGB with nans --- pyqtgraph/tests/test_functions.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_functions.py b/pyqtgraph/tests/test_functions.py index 6a6aaa33..f9320ef2 100644 --- a/pyqtgraph/tests/test_functions.py +++ b/pyqtgraph/tests/test_functions.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- import pyqtgraph as pg import numpy as np import sys @@ -270,6 +271,30 @@ def test_makeARGB(): im2, alpha = pg.makeARGB(im1, lut=lut, levels=(1, 17)) checkImage(im2, np.linspace(127.5, 0, 256).astype('ubyte'), alpha, False) + # nans in image + + # 2d input image, one pixel is nan + im1 = np.ones((10, 12)) + im1[3, 5] = np.nan + im2, alpha = pg.makeARGB(im1, levels=(0, 1)) + assert alpha + assert im2[3, 5, 3] == 0 # nan pixel is transparent + assert im2[0, 0, 3] == 255 # doesn't affect other pixels + + # 3d RGB input image, any color channel of a pixel is nan + im1 = np.ones((10, 12, 3)) + im1[3, 5, 1] = np.nan + im2, alpha = pg.makeARGB(im1, levels=(0, 1)) + assert alpha + assert im2[3, 5, 3] == 0 # nan pixel is transparent + assert im2[0, 0, 3] == 255 # doesn't affect other pixels + + # 3d RGBA input image, any color channel of a pixel is nan + im1 = np.ones((10, 12, 4)) + im1[3, 5, 1] = np.nan + im2, alpha = pg.makeARGB(im1, levels=(0, 1), useRGBA=True) + assert alpha + assert im2[3, 5, 3] == 0 # nan pixel is transparent # test sanity checks class AssertExc(object): @@ -387,4 +412,4 @@ def test_eq(): if __name__ == '__main__': - test_interpolateArray() \ No newline at end of file + test_interpolateArray() From 1e81f3dad08cf13832d20da4b2bbcf544dbbe6b9 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Wed, 8 Apr 2020 01:14:36 +0200 Subject: [PATCH 533/607] SVGExporter: Correct image pixelation. --- pyqtgraph/exporters/SVGExporter.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pyqtgraph/exporters/SVGExporter.py b/pyqtgraph/exporters/SVGExporter.py index b0e9b1c0..6f0035bb 100644 --- a/pyqtgraph/exporters/SVGExporter.py +++ b/pyqtgraph/exporters/SVGExporter.py @@ -69,6 +69,13 @@ xmlHeader = """\ pyqtgraph SVG export Generated with Qt and pyqtgraph + """ def generateSvg(item, options={}): From 71636e351868ec9d83fb1c2fb1628d4a37939911 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Wed, 8 Apr 2020 17:10:32 +0200 Subject: [PATCH 534/607] Fix: Update axes after data is set --- pyqtgraph/graphicsItems/PlotCurveItem.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index fea3834f..e0af8bed 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -365,11 +365,12 @@ class PlotCurveItem(GraphicsObject): #self.setCacheMode(QtGui.QGraphicsItem.NoCache) ## Disabling and re-enabling the cache works around a bug in Qt 4.6 causing the cached results to display incorrectly ## Test this bug with test_PlotWidget and zoom in on the animated plot + self.yData = kargs['y'].view(np.ndarray) + self.xData = kargs['x'].view(np.ndarray) + self.invalidateBounds() self.prepareGeometryChange() self.informViewBoundsChanged() - self.yData = kargs['y'].view(np.ndarray) - self.xData = kargs['x'].view(np.ndarray) profiler('copy') From be1ed14bd0c2641cfb357cdc61a9418dd07267b7 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sun, 12 Apr 2020 00:43:16 +0200 Subject: [PATCH 535/607] pg.mkQApp: Pass default application name to Qt, added documentation --- pyqtgraph/Qt.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 0941c3c7..ec5a79cc 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -329,9 +329,18 @@ if m is not None and list(map(int, m.groups())) < versionReq: QAPP = None -def mkQApp(): - global QAPP +def mkQApp(name="pyqtgraph", qt_args=[]): + """ + Creates new QApplication or returns current instance if existing. + + ============== ================================================================================= + **Arguments:** + name Application name, passed to Qt + qt_args Array of command line arguments passed to Qt + ============== ================================================================================= + """ + global QAPP QAPP = QtGui.QApplication.instance() if QAPP is None: - QAPP = QtGui.QApplication([]) + QAPP = QtGui.QApplication([name] + qt_args) return QAPP From 4f1bf8bb18b3f5f994155d5f0b03a60ed58b8040 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sun, 12 Apr 2020 02:47:23 +0200 Subject: [PATCH 536/607] GroupParameterItem: Did not pass changed options to ParameterItem `ParameterItem` handles visibility changes in `optsChanged`. `GroupParameterItem` overrides this function, but never calls the super function, leading in visibility changes not being applied. This PR fixes this by calling said function. Fixes #788 --- pyqtgraph/parametertree/parameterTypes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/parametertree/parameterTypes.py b/pyqtgraph/parametertree/parameterTypes.py index b728fb8e..a8e3781d 100644 --- a/pyqtgraph/parametertree/parameterTypes.py +++ b/pyqtgraph/parametertree/parameterTypes.py @@ -437,8 +437,10 @@ class GroupParameterItem(ParameterItem): else: ParameterItem.addChild(self, child) - def optsChanged(self, param, changed): - if 'addList' in changed: + def optsChanged(self, param, opts): + ParameterItem.optsChanged(self, param, opts) + + if 'addList' in opts: self.updateAddList() def updateAddList(self): From a703155a21f455469839324745aa4e565a3f29d4 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 11 Apr 2020 20:15:00 -0700 Subject: [PATCH 537/607] Replace default list arg with None --- pyqtgraph/Qt.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ec5a79cc..cc8b3d0a 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ This module exists to smooth out some of the differences between PySide and PyQt4: @@ -329,7 +330,7 @@ if m is not None and list(map(int, m.groups())) < versionReq: QAPP = None -def mkQApp(name="pyqtgraph", qt_args=[]): +def mkQApp(name="pyqtgraph", qt_args=None): """ Creates new QApplication or returns current instance if existing. @@ -342,5 +343,8 @@ def mkQApp(name="pyqtgraph", qt_args=[]): global QAPP QAPP = QtGui.QApplication.instance() if QAPP is None: - QAPP = QtGui.QApplication([name] + qt_args) + args = [name] + if qt_args is not None: + args.extend(qt_args) + QAPP = QtGui.QApplication(args) return QAPP 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 538/607] 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 539/607] 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 a76d9daec25724c8bf22c61a2ebb3c9b6bff6a4d Mon Sep 17 00:00:00 2001 From: Lev Maximov Date: Tue, 28 Apr 2020 01:43:22 +0700 Subject: [PATCH 540/607] Date axis item (#1154) * Add DateAxisItem * Change style to camelCase * Fix missing first tick for negative timestamps * Add ms precision, auto skipping Auto skipping allows a zoom level to skip ticks automatically if the maximum number of ticks/pt is exceeded * fixes suggested by @goetzc * workaround for negative argument to utcfromtimestamp on windows * attachToPlotItem method * default date axis orientation * Use new DateAxisItem in Plot Customization example * attachToPlotItem bugfix * examples of DateAxisItem * modified description of customPlot example * added descriptions to the new examples, reformatted their code, included the first one into utils.py * typo * Refactored code for setting axis items into new function Replaces "DateAxisItem.attachToPlotItem" * Fix string comparison with == * Doc: Slightly more text for DateAxisItem, small improvement for PlotItem * Make PlotWidget.setAxisItems official * Fix typo in docstring * renamed an example * merge bug fix * Revert "merge bug fix" This reverts commit 876b5a7cdb50cd824b4a5218427081b3ce5c2fe4. * Real bug fix * support for dates upto -1e13..1e13 * Automatically limit DateAxisItem to a range from -1e12 to 1e12 years Very large years (|y|>1e13) cause infinite loop, and since nobody needs time 100 times larger than the age of the universe anyways, this constrains it to 1e12. Following suggestion by @axil: https://github.com/pyqtgraph/pyqtgraph/pull/1154#issuecomment-612662168 * Also catch ValueErrors occuring on Linux before OverfloeErrors While zooming out, before hitting OverflowErrors, utctimestamp produces ValueErrors (at least on my Linux machine), so they are also catched. * Fix: Timestamp 0 corresponds to year 1970 For large years, x axis labels jump by 1970 years if it is not accounted for timestamp 0 to be equal to year 1970. * Fix: When zooming into extreme dates, OSError occurs This commit catches the OSError like the other observed errors * Disable stepping below years for dates outside *_REGULAR_TIMESTAMP 2 reasons: First: At least on my Linux machine, zooming into those dates creates infinite loops. Second: Nobody needs sub-year-precision for those extreme years anyways. * Adapt zoom level sizes based on current font size and screen resolution This is somewhat experimental. With this commit, no longer 60 px are assumed as width for all zoom levels, but the current font and display resolution are considered to calculate the width of ticks in each zoom level. See the new function `updateZoomLevels` for details. Before calling this function, overridden functions `paint` and `generateDrawSpecs` provide information over the current display and font via `self.fontScaleFactor` and `self.fontMetrics`. * Meaningful error meassage when adding axis to multiple PlotItems As @axil noted in the DateAxisItem PR, currently users get a segmentation fault when one tries to add an axis to multiple PlotItems. This commit adds a meaningful RuntimeError message for that case. * setZoomLevelForDensity: Refactoring and calculating optimal spacing on the fly * DateTimeAxis Fix: 1970 shows when zooming far out * Refactoring: Make zoomLevels a customizable dict again * updated the dateaxisitem example * Fix: Get screen resolution in a way that also works for Qt 4 This is both a simplification in code and an improvement in backwards compatibility with Qt 4. * DateAxisItem Fix: Also resolve time below 0.5 seconds * unix line endings in examples * DateTimeAxis Fix: For years < 1 and > 9999, stepping broke Stepping was off by 1970 years for years < 1 and > 9999, resulting in a gap in ticks visible when zooming out. Fixed by subtracting the usual 1970 years. * DateTimeAxis Fix: Zooming out too far causes infinite loop Fixed by setting default limits to +/- 1e10 years. Should still be enough. * improved second dateaxisitem example * 1..9999 years limit * DateTimeAxis: Use OrderedDict to stay compatible with Python < 3-6 * DateAxisItem: Use font height to determine spacing for vertical axes * window title * added dateaxisitem.rst * updated index.rst Co-authored-by: Lukas Heiniger Co-authored-by: Lev Maximov Co-authored-by: 2xB <2xB@users.noreply.github.com> --- doc/source/graphicsItems/dateaxisitem.rst | 8 + doc/source/graphicsItems/index.rst | 1 + doc/source/graphicsItems/make | 1 + examples/DateAxisItem.py | 33 ++ examples/DateAxisItem_QtDesigner.py | 48 +++ examples/DateAxisItem_QtDesigner.ui | 44 +++ examples/customPlot.py | 42 +-- examples/utils.py | 1 + pyqtgraph/__init__.py | 1 + pyqtgraph/graphicsItems/AxisItem.py | 17 +- pyqtgraph/graphicsItems/DateAxisItem.py | 319 +++++++++++++++++++ pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 74 +++-- pyqtgraph/widgets/PlotWidget.py | 5 +- 13 files changed, 531 insertions(+), 63 deletions(-) create mode 100644 doc/source/graphicsItems/dateaxisitem.rst create mode 100644 examples/DateAxisItem.py create mode 100644 examples/DateAxisItem_QtDesigner.py create mode 100644 examples/DateAxisItem_QtDesigner.ui create mode 100644 pyqtgraph/graphicsItems/DateAxisItem.py diff --git a/doc/source/graphicsItems/dateaxisitem.rst b/doc/source/graphicsItems/dateaxisitem.rst new file mode 100644 index 00000000..9da36c6f --- /dev/null +++ b/doc/source/graphicsItems/dateaxisitem.rst @@ -0,0 +1,8 @@ +DateAxisItem +============ + +.. autoclass:: pyqtgraph.DateAxisItem + :members: + + .. automethod:: pyqtgraph.DateAxisItem.__init__ + diff --git a/doc/source/graphicsItems/index.rst b/doc/source/graphicsItems/index.rst index eec86610..390d8f17 100644 --- a/doc/source/graphicsItems/index.rst +++ b/doc/source/graphicsItems/index.rst @@ -43,3 +43,4 @@ Contents: graphicsitem uigraphicsitem graphicswidgetanchor + dateaxisitem diff --git a/doc/source/graphicsItems/make b/doc/source/graphicsItems/make index 293db0d6..9d9f9954 100644 --- a/doc/source/graphicsItems/make +++ b/doc/source/graphicsItems/make @@ -2,6 +2,7 @@ files = """ArrowItem AxisItem ButtonItem CurvePoint +DateAxisItem GradientEditorItem GradientLegend GraphicsLayout diff --git a/examples/DateAxisItem.py b/examples/DateAxisItem.py new file mode 100644 index 00000000..7bbaafff --- /dev/null +++ b/examples/DateAxisItem.py @@ -0,0 +1,33 @@ +""" +Demonstrates the usage of DateAxisItem to display properly-formatted +timestamps on x-axis which automatically adapt to current zoom level. + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import time +from datetime import datetime, timedelta + +import numpy as np +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui + +app = QtGui.QApplication([]) + +# Create a plot with a date-time axis +w = pg.PlotWidget(axisItems = {'bottom': pg.DateAxisItem()}) +w.showGrid(x=True, y=True) + +# Plot sin(1/x^2) with timestamps in the last 100 years +now = time.time() +x = np.linspace(2*np.pi, 1000*2*np.pi, 8301) +w.plot(now-(2*np.pi/x)**2*100*np.pi*1e7, np.sin(x), symbol='o') + +w.setWindowTitle('pyqtgraph example: DateAxisItem') +w.show() + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() diff --git a/examples/DateAxisItem_QtDesigner.py b/examples/DateAxisItem_QtDesigner.py new file mode 100644 index 00000000..f6f17489 --- /dev/null +++ b/examples/DateAxisItem_QtDesigner.py @@ -0,0 +1,48 @@ +""" +Demonstrates the usage of DateAxisItem in a layout created with Qt Designer. + +The spotlight here is on the 'setAxisItems' method, without which +one would have to subclass plotWidget in order to attach a dateaxis to it. + +""" +import initExample ## Add path to library (just for examples; you do not need this) + +import sys +import time + +import numpy as np +from PyQt5 import QtWidgets, QtCore, uic +import pyqtgraph as pg + +pg.setConfigOption('background', 'w') +pg.setConfigOption('foreground', 'k') + +BLUE = pg.mkPen('#1f77b4') + +Design, _ = uic.loadUiType('DateAxisItem_QtDesigner.ui') + +class ExampleApp(QtWidgets.QMainWindow, Design): + def __init__(self): + super().__init__() + self.setupUi(self) + now = time.time() + # Plot random values with timestamps in the last 6 months + timestamps = np.linspace(now - 6*30*24*3600, now, 100) + self.curve = self.plotWidget.plot(x=timestamps, y=np.random.rand(100), + symbol='o', symbolSize=5, pen=BLUE) + # 'o' circle 't' triangle 'd' diamond '+' plus 's' square + self.plotWidget.setAxisItems({'bottom': pg.DateAxisItem()}) + self.plotWidget.showGrid(x=True, y=True) + +app = QtWidgets.QApplication(sys.argv) +app.setStyle(QtWidgets.QStyleFactory.create('Fusion')) +app.setPalette(QtWidgets.QApplication.style().standardPalette()) +window = ExampleApp() +window.setWindowTitle('pyqtgraph example: DateAxisItem_QtDesigner') +window.show() + +## Start Qt event loop unless running in interactive mode or using pyside. +if __name__ == '__main__': + import sys + if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): + app.exec_() diff --git a/examples/DateAxisItem_QtDesigner.ui b/examples/DateAxisItem_QtDesigner.ui new file mode 100644 index 00000000..91f77ba9 --- /dev/null +++ b/examples/DateAxisItem_QtDesigner.ui @@ -0,0 +1,44 @@ + + + MainWindow + + + + 0 + 0 + 536 + 381 + + + + MainWindow + + + + + + + + + + + + 0 + 0 + 536 + 18 + + + + + + + + PlotWidget + QGraphicsView +
pyqtgraph
+
+
+ + +
diff --git a/examples/customPlot.py b/examples/customPlot.py index b523fd17..c5e05f91 100644 --- a/examples/customPlot.py +++ b/examples/customPlot.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ -This example demonstrates the creation of a plot with a customized -AxisItem and ViewBox. +This example demonstrates the creation of a plot with +DateAxisItem and a customized ViewBox. """ @@ -12,40 +12,6 @@ from pyqtgraph.Qt import QtCore, QtGui import numpy as np import time -class DateAxis(pg.AxisItem): - def tickStrings(self, values, scale, spacing): - strns = [] - rng = max(values)-min(values) - #if rng < 120: - # return pg.AxisItem.tickStrings(self, values, scale, spacing) - if rng < 3600*24: - string = '%H:%M:%S' - label1 = '%b %d -' - label2 = ' %b %d, %Y' - elif rng >= 3600*24 and rng < 3600*24*30: - string = '%d' - label1 = '%b - ' - label2 = '%b, %Y' - elif rng >= 3600*24*30 and rng < 3600*24*30*24: - string = '%b' - label1 = '%Y -' - label2 = ' %Y' - elif rng >=3600*24*30*24: - string = '%Y' - label1 = '' - label2 = '' - for x in values: - try: - strns.append(time.strftime(string, time.localtime(x))) - except ValueError: ## Windows can't handle dates before 1970 - strns.append('') - try: - label = time.strftime(label1, time.localtime(min(values)))+time.strftime(label2, time.localtime(max(values))) - except ValueError: - label = '' - #self.setLabel(text=label) - return strns - class CustomViewBox(pg.ViewBox): def __init__(self, *args, **kwds): pg.ViewBox.__init__(self, *args, **kwds) @@ -65,10 +31,10 @@ class CustomViewBox(pg.ViewBox): app = pg.mkQApp() -axis = DateAxis(orientation='bottom') +axis = pg.DateAxisItem(orientation='bottom') vb = CustomViewBox() -pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with custom axis and ViewBox

Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom") +pw = pg.PlotWidget(viewBox=vb, axisItems={'bottom': axis}, enableMenu=False, title="PlotItem with DateAxisItem and custom ViewBox
Menu disabled, mouse behavior changed: left-drag to zoom, right-click to reset zoom") dates = np.arange(8) * (3600*24*356) pw.plot(x=dates, y=[1,6,2,4,3,5,6,8], symbol='o') pw.show() diff --git a/examples/utils.py b/examples/utils.py index 494b686b..041d17d7 100644 --- a/examples/utils.py +++ b/examples/utils.py @@ -14,6 +14,7 @@ examples = OrderedDict([ ('Crosshair / Mouse interaction', 'crosshair.py'), ('Data Slicing', 'DataSlicing.py'), ('Plot Customization', 'customPlot.py'), + ('Timestamps on x axis', 'DateAxisItem.py'), ('Image Analysis', 'imageAnalysis.py'), ('ViewBox Features', 'ViewBoxFeatures.py'), ('Dock widgets', 'dockarea.py'), diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index da14a83b..45e00c83 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -219,6 +219,7 @@ from .graphicsItems.ViewBox import * from .graphicsItems.ArrowItem import * from .graphicsItems.ImageItem import * from .graphicsItems.AxisItem import * +from .graphicsItems.DateAxisItem import * from .graphicsItems.LabelItem import * from .graphicsItems.CurvePoint import * from .graphicsItems.GraphicsWidgetAnchor import * diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 6b2c63df..3faf83a4 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -507,20 +507,29 @@ class AxisItem(GraphicsWidget): def linkToView(self, view): """Link this axis to a ViewBox, causing its displayed range to match the visible range of the view.""" - oldView = self.linkedView() + self.unlinkFromView() + self._linkedView = weakref.ref(view) + if self.orientation in ['right', 'left']: + view.sigYRangeChanged.connect(self.linkedViewChanged) + else: + view.sigXRangeChanged.connect(self.linkedViewChanged) + + view.sigResized.connect(self.linkedViewChanged) + + def unlinkFromView(self): + """Unlink this axis from a ViewBox.""" + oldView = self.linkedView() + self._linkedView = None if self.orientation in ['right', 'left']: if oldView is not None: oldView.sigYRangeChanged.disconnect(self.linkedViewChanged) - view.sigYRangeChanged.connect(self.linkedViewChanged) else: if oldView is not None: oldView.sigXRangeChanged.disconnect(self.linkedViewChanged) - view.sigXRangeChanged.connect(self.linkedViewChanged) if oldView is not None: oldView.sigResized.disconnect(self.linkedViewChanged) - view.sigResized.connect(self.linkedViewChanged) def linkedViewChanged(self, view, newRange=None): if self.orientation in ['right', 'left']: diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py new file mode 100644 index 00000000..9d692dac --- /dev/null +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -0,0 +1,319 @@ +import sys +import numpy as np +import time +from datetime import datetime, timedelta + +from .AxisItem import AxisItem +from ..pgcollections import OrderedDict + +__all__ = ['DateAxisItem', 'ZoomLevel'] + +MS_SPACING = 1/1000.0 +SECOND_SPACING = 1 +MINUTE_SPACING = 60 +HOUR_SPACING = 3600 +DAY_SPACING = 24 * HOUR_SPACING +WEEK_SPACING = 7 * DAY_SPACING +MONTH_SPACING = 30 * DAY_SPACING +YEAR_SPACING = 365 * DAY_SPACING + +if sys.platform == 'win32': + _epoch = datetime.utcfromtimestamp(0) + def utcfromtimestamp(timestamp): + return _epoch + timedelta(seconds=timestamp) +else: + utcfromtimestamp = datetime.utcfromtimestamp + +MIN_REGULAR_TIMESTAMP = (datetime(1, 1, 1) - datetime(1970,1,1)).total_seconds() +MAX_REGULAR_TIMESTAMP = (datetime(9999, 1, 1) - datetime(1970,1,1)).total_seconds() +SEC_PER_YEAR = 365.25*24*3600 + +def makeMSStepper(stepSize): + def stepper(val, n): + if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP: + return np.inf + + val *= 1000 + f = stepSize * 1000 + return (val // (n*f) + 1) * (n*f) / 1000.0 + return stepper + +def makeSStepper(stepSize): + def stepper(val, n): + if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP: + return np.inf + + return (val // (n*stepSize) + 1) * (n*stepSize) + return stepper + +def makeMStepper(stepSize): + def stepper(val, n): + if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP: + return np.inf + + d = utcfromtimestamp(val) + base0m = (d.month + n*stepSize - 1) + d = datetime(d.year + base0m // 12, base0m % 12 + 1, 1) + return (d - datetime(1970, 1, 1)).total_seconds() + return stepper + +def makeYStepper(stepSize): + def stepper(val, n): + if val < MIN_REGULAR_TIMESTAMP or val > MAX_REGULAR_TIMESTAMP: + return np.inf + + d = utcfromtimestamp(val) + next_year = (d.year // (n*stepSize) + 1) * (n*stepSize) + if next_year > 9999: + return np.inf + next_date = datetime(next_year, 1, 1) + return (next_date - datetime(1970, 1, 1)).total_seconds() + return stepper + +class TickSpec: + """ Specifies the properties for a set of date ticks and computes ticks + within a given utc timestamp range """ + def __init__(self, spacing, stepper, format, autoSkip=None): + """ + ============= ========================================================== + Arguments + spacing approximate (average) tick spacing + stepper a stepper function that takes a utc time stamp and a step + steps number n to compute the start of the next unit. You + can use the make_X_stepper functions to create common + steppers. + format a strftime compatible format string which will be used to + convert tick locations to date/time strings + autoSkip list of step size multipliers to be applied when the tick + density becomes too high. The tick spec automatically + applies additional powers of 10 (10, 100, ...) to the list + if necessary. Set to None to switch autoSkip off + ============= ========================================================== + + """ + self.spacing = spacing + self.step = stepper + self.format = format + self.autoSkip = autoSkip + + def makeTicks(self, minVal, maxVal, minSpc): + ticks = [] + n = self.skipFactor(minSpc) + x = self.step(minVal, n) + while x <= maxVal: + ticks.append(x) + x = self.step(x, n) + return (np.array(ticks), n) + + def skipFactor(self, minSpc): + if self.autoSkip is None or minSpc < self.spacing: + return 1 + factors = np.array(self.autoSkip, dtype=np.float) + while True: + for f in factors: + spc = self.spacing * f + if spc > minSpc: + return int(f) + factors *= 10 + + +class ZoomLevel: + """ Generates the ticks which appear in a specific zoom level """ + def __init__(self, tickSpecs, exampleText): + """ + ============= ========================================================== + tickSpecs a list of one or more TickSpec objects with decreasing + coarseness + ============= ========================================================== + + """ + self.tickSpecs = tickSpecs + self.utcOffset = 0 + self.exampleText = exampleText + + def tickValues(self, minVal, maxVal, minSpc): + # return tick values for this format in the range minVal, maxVal + # the return value is a list of tuples (, [tick positions]) + # minSpc indicates the minimum spacing (in seconds) between two ticks + # to fullfill the maxTicksPerPt constraint of the DateAxisItem at the + # current zoom level. This is used for auto skipping ticks. + allTicks = [] + valueSpecs = [] + # back-project (minVal maxVal) to UTC, compute ticks then offset to + # back to local time again + utcMin = minVal - self.utcOffset + utcMax = maxVal - self.utcOffset + for spec in self.tickSpecs: + ticks, skipFactor = spec.makeTicks(utcMin, utcMax, minSpc) + # reposition tick labels to local time coordinates + ticks += self.utcOffset + # remove any ticks that were present in higher levels + tick_list = [x for x in ticks.tolist() if x not in allTicks] + allTicks.extend(tick_list) + valueSpecs.append((spec.spacing, tick_list)) + # if we're skipping ticks on the current level there's no point in + # producing lower level ticks + if skipFactor > 1: + break + return valueSpecs + + +YEAR_MONTH_ZOOM_LEVEL = ZoomLevel([ + TickSpec(YEAR_SPACING, makeYStepper(1), '%Y', autoSkip=[1, 5, 10, 25]), + TickSpec(MONTH_SPACING, makeMStepper(1), '%b') +], "YYYY") +MONTH_DAY_ZOOM_LEVEL = ZoomLevel([ + TickSpec(MONTH_SPACING, makeMStepper(1), '%b'), + TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%d', autoSkip=[1, 5]) +], "MMM") +DAY_HOUR_ZOOM_LEVEL = ZoomLevel([ + TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%a %d'), + TickSpec(HOUR_SPACING, makeSStepper(HOUR_SPACING), '%H:%M', autoSkip=[1, 6]) +], "MMM 00") +HOUR_MINUTE_ZOOM_LEVEL = ZoomLevel([ + TickSpec(DAY_SPACING, makeSStepper(DAY_SPACING), '%a %d'), + TickSpec(MINUTE_SPACING, makeSStepper(MINUTE_SPACING), '%H:%M', + autoSkip=[1, 5, 15]) +], "MMM 00") +HMS_ZOOM_LEVEL = ZoomLevel([ + TickSpec(SECOND_SPACING, makeSStepper(SECOND_SPACING), '%H:%M:%S', + autoSkip=[1, 5, 15, 30]) +], "99:99:99") +MS_ZOOM_LEVEL = ZoomLevel([ + TickSpec(MINUTE_SPACING, makeSStepper(MINUTE_SPACING), '%H:%M:%S'), + TickSpec(MS_SPACING, makeMSStepper(MS_SPACING), '%S.%f', + autoSkip=[1, 5, 10, 25]) +], "99:99:99") + +class DateAxisItem(AxisItem): + """ + **Bases:** :class:`AxisItem ` + + An AxisItem that displays dates from unix timestamps. + + The display format is adjusted automatically depending on the current time + density (seconds/point) on the axis. For more details on changing this + behaviour, see :func:`setZoomLevelForDensity() `. + + Can be added to an existing plot e.g. via + :func:`setAxisItems({'bottom':axis}) `. + + """ + + def __init__(self, orientation='bottom', **kwargs): + """ + Create a new DateAxisItem. + + For `orientation` and `**kwargs`, see + :func:`AxisItem.__init__ `. + + """ + + super(DateAxisItem, self).__init__(orientation, **kwargs) + # Set the zoom level to use depending on the time density on the axis + self.utcOffset = time.timezone + + self.zoomLevels = OrderedDict([ + (np.inf, YEAR_MONTH_ZOOM_LEVEL), + (5 * 3600*24, MONTH_DAY_ZOOM_LEVEL), + (6 * 3600, DAY_HOUR_ZOOM_LEVEL), + (15 * 60, HOUR_MINUTE_ZOOM_LEVEL), + (30, HMS_ZOOM_LEVEL), + (1, MS_ZOOM_LEVEL), + ]) + + def tickStrings(self, values, scale, spacing): + tickSpecs = self.zoomLevel.tickSpecs + tickSpec = next((s for s in tickSpecs if s.spacing == spacing), None) + try: + dates = [utcfromtimestamp(v - self.utcOffset) for v in values] + except (OverflowError, ValueError, OSError): + # should not normally happen + return ['%g' % ((v-self.utcOffset)//SEC_PER_YEAR + 1970) for v in values] + + formatStrings = [] + for x in dates: + try: + s = x.strftime(tickSpec.format) + if '%f' in tickSpec.format: + # we only support ms precision + s = s[:-3] + elif '%Y' in tickSpec.format: + s = s.lstrip('0') + formatStrings.append(s) + except ValueError: # Windows can't handle dates before 1970 + formatStrings.append('') + return formatStrings + + def tickValues(self, minVal, maxVal, size): + density = (maxVal - minVal) / size + self.setZoomLevelForDensity(density) + values = self.zoomLevel.tickValues(minVal, maxVal, minSpc=self.minSpacing) + return values + + def setZoomLevelForDensity(self, density): + """ + Setting `zoomLevel` and `minSpacing` based on given density of seconds per pixel + + The display format is adjusted automatically depending on the current time + density (seconds/point) on the axis. You can customize the behaviour by + overriding this function or setting a different set of zoom levels + than the default one. The `zoomLevels` variable is a dictionary with the + maximal distance of ticks in seconds which are allowed for each zoom level + before the axis switches to the next coarser level. To create custom + zoom levels, override this function and provide custom `zoomLevelWidths` and + `zoomLevels`. + """ + padding = 10 + + # Size in pixels a specific tick label will take + if self.orientation in ['bottom', 'top']: + def sizeOf(text): + return self.fontMetrics.boundingRect(text).width() + padding*self.fontScaleFactor + else: + def sizeOf(text): + return self.fontMetrics.boundingRect(text).height() + padding*self.fontScaleFactor + + # Fallback zoom level: Years/Months + self.zoomLevel = YEAR_MONTH_ZOOM_LEVEL + for maximalSpacing, zoomLevel in self.zoomLevels.items(): + size = sizeOf(zoomLevel.exampleText) + + # Test if zoom level is too fine grained + if maximalSpacing/size < density: + break + + self.zoomLevel = zoomLevel + + # Set up zoomLevel + self.zoomLevel.utcOffset = self.utcOffset + + # Calculate minimal spacing of items on the axis + size = sizeOf(self.zoomLevel.exampleText) + self.minSpacing = density*size + + def linkToView(self, view): + super(DateAxisItem, self).linkToView(view) + + # Set default limits + _min = MIN_REGULAR_TIMESTAMP + _max = MAX_REGULAR_TIMESTAMP + + if self.orientation in ['right', 'left']: + view.setLimits(yMin=_min, yMax=_max) + else: + view.setLimits(xMin=_min, xMax=_max) + + def generateDrawSpecs(self, p): + # Get font metrics from QPainter + # Not happening in "paint", as the QPainter p there is a different one from the one here, + # so changing that font could cause unwanted side effects + if self.tickFont is not None: + p.setFont(self.tickFont) + + self.fontMetrics = p.fontMetrics() + + # Get font scale factor by current window resolution + self.fontScaleFactor = p.device().logicalDpiX() / 96 + + return super(DateAxisItem, self).generateDrawSpecs(p) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index cf588912..dd864c49 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -95,7 +95,7 @@ class PlotItem(GraphicsWidget): def __init__(self, parent=None, name=None, labels=None, title=None, viewBox=None, axisItems=None, enableMenu=True, **kargs): """ Create a new PlotItem. All arguments are optional. - Any extra keyword arguments are passed to PlotItem.plot(). + Any extra keyword arguments are passed to :func:`PlotItem.plot() `. ============== ========================================================================================== **Arguments:** @@ -153,20 +153,9 @@ class PlotItem(GraphicsWidget): self.legend = None - ## Create and place axis items - if axisItems is None: - axisItems = {} + # Initialize axis items self.axes = {} - for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): - if k in axisItems: - axis = axisItems[k] - else: - axis = AxisItem(orientation=k, parent=self) - axis.linkToView(self.vb) - self.axes[k] = {'item': axis, 'pos': pos} - self.layout.addItem(axis, *pos) - axis.setZValue(-1000) - axis.setFlag(axis.ItemNegativeZStacksBehindParent) + self.setAxisItems(axisItems) self.titleLabel = LabelItem('', size='11pt', parent=self) self.layout.addItem(self.titleLabel, 0, 1) @@ -254,11 +243,6 @@ class PlotItem(GraphicsWidget): self.ctrl.maxTracesCheck.toggled.connect(self.updateDecimation) self.ctrl.maxTracesSpin.valueChanged.connect(self.updateDecimation) - self.hideAxis('right') - self.hideAxis('top') - self.showAxis('left') - self.showAxis('bottom') - if labels is None: labels = {} for label in list(self.axes.keys()): @@ -300,6 +284,58 @@ class PlotItem(GraphicsWidget): locals()[m] = _create_method(m) del _create_method + + def setAxisItems(self, axisItems=None): + """ + Place axis items as given by `axisItems`. Initializes non-existing axis items. + + ============== ========================================================================================== + **Arguments:**< + *axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items + for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top') + and the values must be instances of AxisItem (or at least compatible with AxisItem). + ============== ========================================================================================== + """ + + + if axisItems is None: + axisItems = {} + + # Array containing visible axis items + # Also containing potentially hidden axes, but they are not touched so it does not matter + visibleAxes = ['left', 'bottom'] + visibleAxes.append(axisItems.keys()) # Note that it does not matter that this adds + # some values to visibleAxes a second time + + for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): + if k in self.axes: + if k not in axisItems: + continue # Nothing to do here + + # Remove old axis + oldAxis = self.axes[k]['item'] + self.layout.removeItem(oldAxis) + oldAxis.scene().removeItem(oldAxis) + oldAxis.unlinkFromView() + + # Create new axis + if k in axisItems: + axis = axisItems[k] + if axis.scene() is not None: + if axis != self.axes[k]["item"]: + raise RuntimeError("Can't add an axis to multiple plots.") + else: + axis = AxisItem(orientation=k, parent=self) + + # Set up new axis + axis.linkToView(self.vb) + self.axes[k] = {'item': axis, 'pos': pos} + self.layout.addItem(axis, *pos) + axis.setZValue(-1000) + axis.setFlag(axis.ItemNegativeZStacksBehindParent) + + axisVisible = k in visibleAxes + self.showAxis(k, axisVisible) def setLogMode(self, x=None, y=None): """ diff --git a/pyqtgraph/widgets/PlotWidget.py b/pyqtgraph/widgets/PlotWidget.py index 5208e3b3..d00ee704 100644 --- a/pyqtgraph/widgets/PlotWidget.py +++ b/pyqtgraph/widgets/PlotWidget.py @@ -24,6 +24,7 @@ class PlotWidget(GraphicsView): :func:`addItem `, :func:`removeItem `, :func:`clear `, + :func:`setAxisItems `, :func:`setXRange `, :func:`setYRange `, :func:`setRange `, @@ -55,7 +56,7 @@ class PlotWidget(GraphicsView): self.setCentralItem(self.plotItem) ## Explicitly wrap methods from plotItem ## NOTE: If you change this list, update the documentation above as well. - for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setXRange', + for m in ['addItem', 'removeItem', 'autoRange', 'clear', 'setAxisItems', 'setXRange', 'setYRange', 'setRange', 'setAspectLocked', 'setMouseEnabled', 'setXLink', 'setYLink', 'enableAutoRange', 'disableAutoRange', 'setLimits', 'register', 'unregister', 'viewRect']: @@ -96,4 +97,4 @@ class PlotWidget(GraphicsView): return self.plotItem - \ No newline at end of file + From 02b7532706ea36a811ad64a85063e6469fcacc11 Mon Sep 17 00:00:00 2001 From: Ales Erjavec Date: Thu, 20 Dec 2018 18:40:36 +0100 Subject: [PATCH 541/607] Remove use of GraphicsScene._addressCache in translateGraphicsItem Use QGraphicsItem.toQGrapicsObject on the item instead. This probably is not even needed since PyQt 4.9 --- pyqtgraph/GraphicsScene/GraphicsScene.py | 61 +++++++----------------- pyqtgraph/graphicsItems/GraphicsItem.py | 15 +++--- 2 files changed, 25 insertions(+), 51 deletions(-) diff --git a/pyqtgraph/GraphicsScene/GraphicsScene.py b/pyqtgraph/GraphicsScene/GraphicsScene.py index b61f3a1b..b67e44ef 100644 --- a/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- import weakref +import warnings + from ..Qt import QtCore, QtGui from ..Point import Point from .. import functions as fn @@ -88,15 +90,11 @@ class GraphicsScene(QtGui.QGraphicsScene): @classmethod def registerObject(cls, obj): - """ - Workaround for PyQt bug in qgraphicsscene.items() - All subclasses of QGraphicsObject must register themselves with this function. - (otherwise, mouse interaction with those objects will likely fail) - """ - if HAVE_SIP and isinstance(obj, sip.wrapper): - cls._addressCache[sip.unwrapinstance(sip.cast(obj, QtGui.QGraphicsItem))] = obj - - + warnings.warn( + "'registerObject' is deprecated and does nothing.", + DeprecationWarning, stacklevel=2 + ) + def __init__(self, clickRadius=2, moveDistance=5, parent=None): QtGui.QGraphicsScene.__init__(self, parent) self.setClickRadius(clickRadius) @@ -368,46 +366,15 @@ class GraphicsScene(QtGui.QGraphicsScene): return ev.isAccepted() def items(self, *args): - #print 'args:', args items = QtGui.QGraphicsScene.items(self, *args) - ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, - ## then the object returned will be different than the actual item that was originally added to the scene - items2 = list(map(self.translateGraphicsItem, items)) - #if HAVE_SIP and isinstance(self, sip.wrapper): - #items2 = [] - #for i in items: - #addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem)) - #i2 = GraphicsScene._addressCache.get(addr, i) - ##print i, "==>", i2 - #items2.append(i2) - #print 'items:', items - return items2 + return self.translateGraphicsItems(items) def selectedItems(self, *args): items = QtGui.QGraphicsScene.selectedItems(self, *args) - ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, - ## then the object returned will be different than the actual item that was originally added to the scene - #if HAVE_SIP and isinstance(self, sip.wrapper): - #items2 = [] - #for i in items: - #addr = sip.unwrapinstance(sip.cast(i, QtGui.QGraphicsItem)) - #i2 = GraphicsScene._addressCache.get(addr, i) - ##print i, "==>", i2 - #items2.append(i2) - items2 = list(map(self.translateGraphicsItem, items)) - - #print 'items:', items - return items2 + return self.translateGraphicsItems(items) def itemAt(self, *args): item = QtGui.QGraphicsScene.itemAt(self, *args) - - ## PyQt bug: items() returns a list of QGraphicsItem instances. If the item is subclassed from QGraphicsObject, - ## then the object returned will be different than the actual item that was originally added to the scene - #if HAVE_SIP and isinstance(self, sip.wrapper): - #addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem)) - #item = GraphicsScene._addressCache.get(addr, item) - #return item return self.translateGraphicsItem(item) def itemsNearEvent(self, event, selMode=QtCore.Qt.IntersectsItemShape, sortOrder=QtCore.Qt.DescendingOrder, hoverable=False): @@ -554,10 +521,14 @@ class GraphicsScene(QtGui.QGraphicsScene): @staticmethod def translateGraphicsItem(item): - ## for fixing pyqt bugs where the wrong item is returned + # This function is intended as a workaround for a problem with older + # versions of PyQt (< 4.9?), where methods returning 'QGraphicsItem *' + # lose the type of the QGraphicsObject subclasses and instead return + # generic QGraphicsItem wrappers. if HAVE_SIP and isinstance(item, sip.wrapper): - addr = sip.unwrapinstance(sip.cast(item, QtGui.QGraphicsItem)) - item = GraphicsScene._addressCache.get(addr, item) + obj = item.toGraphicsObject() + if obj is not None: + item = obj return item @staticmethod diff --git a/pyqtgraph/graphicsItems/GraphicsItem.py b/pyqtgraph/graphicsItems/GraphicsItem.py index 541ab13b..3337a367 100644 --- a/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/pyqtgraph/graphicsItems/GraphicsItem.py @@ -19,8 +19,8 @@ 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) - - def __init__(self, register=True): + + def __init__(self, register=None): if not hasattr(self, '_qtBaseClass'): for b in self.__class__.__bases__: if issubclass(b, QtGui.QGraphicsItem): @@ -28,15 +28,18 @@ class GraphicsItem(object): break if not hasattr(self, '_qtBaseClass'): raise Exception('Could not determine Qt base class for GraphicsItem: %s' % str(self)) - + self._pixelVectorCache = [None, None] self._viewWidget = None self._viewBox = None self._connectedView = None self._exportOpts = False ## If False, not currently exporting. Otherwise, contains dict of export options. - if register: - GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() - + if register is not None and register: + warnings.warn( + "'register' argument is deprecated and does nothing", + DeprecationWarning, stacklevel=2 + ) + def getViewWidget(self): """ Return the view widget for this item. From f7364f52b3218a3ea85d3d6ad179ec28e600b11b Mon Sep 17 00:00:00 2001 From: Daniel Hrisca Date: Mon, 4 May 2020 23:42:03 +0300 Subject: [PATCH 542/607] improve SymbolAtlas.getSymbolCoords performance (#1184) * remote legacy work-around for old numpy errors * forgot to remove the numpy_fix import * require numyp >= 1.8.0 * improve performance of updateData PlotCurveItem (saves about 2us per call) * improve ScatterPlotItem performance --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 29 ++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index aa2cabba..a774a30f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -122,26 +122,35 @@ class SymbolAtlas(object): """ Given a list of spot records, return an object representing the coordinates of that symbol within the atlas """ - sourceRect = np.empty(len(opts), dtype=object) + + sourceRect = [] keyi = None sourceRecti = None - for i, rec in enumerate(opts): - key = (id(rec[3]), rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? + symbol_map = self.symbolMap + + for i, rec in enumerate(opts.tolist()): + size, symbol, pen, brush = rec[2: 6] + + key = id(symbol), size, id(pen), id(brush) if key == keyi: - sourceRect[i] = sourceRecti + sourceRect.append(sourceRecti) else: try: - sourceRect[i] = self.symbolMap[key] + sourceRect.append(symbol_map[key]) except KeyError: newRectSrc = QtCore.QRectF() - newRectSrc.pen = rec['pen'] - newRectSrc.brush = rec['brush'] - newRectSrc.symbol = rec[3] - self.symbolMap[key] = newRectSrc + newRectSrc.pen = pen + newRectSrc.brush = brush + newRectSrc.symbol = symbol + + symbol_map[key] = newRectSrc self.atlasValid = False - sourceRect[i] = newRectSrc + sourceRect.append(newRectSrc) + keyi = key sourceRecti = newRectSrc + + sourceRect = np.array(sourceRect, dtype=object) return sourceRect def buildAtlas(self): From 96be1bd23ffac47d21b6884688f8089d41a181b5 Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Mon, 4 May 2020 23:58:29 +0200 Subject: [PATCH 543/607] Fix: AxisItem tickFont is defined in two places while only one is used (#1180) To set the tick font of `AxisItem`s, there are two options: `setStyle({"tickFont":...})` and `setTickFont(...)`. The first option sets `AxisItem.style['tickFont']`, the second sets `self.tickFont`. Only `self.tickFont` is actually used. This PR replaces all occurrences of the second variable with the first variable, so both options work again. Also, documentation from `setStyle` is copied to `setTickFont`. Co-authored-by: 2xB <2xB@users.noreply.github.com> --- pyqtgraph/graphicsItems/AxisItem.py | 11 +++++++---- pyqtgraph/graphicsItems/DateAxisItem.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index 3faf83a4..2c6a15e3 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -82,7 +82,6 @@ class AxisItem(GraphicsWidget): self.labelUnitPrefix = unitPrefix self.labelStyle = args self.logMode = False - self.tickFont = None self._tickLevels = None ## used to override the automatic ticking system with explicit ticks self._tickSpacing = None # used to override default tickSpacing method @@ -205,7 +204,11 @@ class AxisItem(GraphicsWidget): self.update() def setTickFont(self, font): - self.tickFont = font + """ + (QFont or None) Determines the font used for tick values. + Use None for the default font. + """ + self.style['tickFont'] = font self.picture = None self.prepareGeometryChange() ## Need to re-allocate space depending on font size? @@ -1084,8 +1087,8 @@ class AxisItem(GraphicsWidget): profiler('draw ticks') # Draw all text - if self.tickFont is not None: - p.setFont(self.tickFont) + if self.style['tickFont'] is not None: + p.setFont(self.style['tickFont']) p.setPen(self.textPen()) for rect, flags, text in textSpecs: p.drawText(rect, int(flags), text) diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py index 9d692dac..7cf9be2c 100644 --- a/pyqtgraph/graphicsItems/DateAxisItem.py +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -308,8 +308,8 @@ class DateAxisItem(AxisItem): # Get font metrics from QPainter # Not happening in "paint", as the QPainter p there is a different one from the one here, # so changing that font could cause unwanted side effects - if self.tickFont is not None: - p.setFont(self.tickFont) + if self.style['tickFont'] is not None: + p.setFont(self.style['tickFont']) self.fontMetrics = p.fontMetrics() From 720fa5f3c2e77dd1b5d18af26f65de41188b282e Mon Sep 17 00:00:00 2001 From: 2xB <31772910+2xB@users.noreply.github.com> Date: Tue, 5 May 2020 18:16:07 +0200 Subject: [PATCH 544/607] DateAxisItem: AxisItem unlinking tests and doc fixed (#1179) * Added test_AxisItem by @mliberty1 As found in https://github.com/pyqtgraph/pyqtgraph/pull/917 * test_AxisItem: Fit to current implementation * DateAxisItem: Fix documentation to zoomLevels zoomLevels is not intended to be set by the user (see discussion in converstation from https://github.com/pyqtgraph/pyqtgraph/pull/1154/files#diff-aefdb23660d0963df0dff3a116baded8 ). Also, `zoomLevelWidths` does currently not exist. This commit adapts the documentation to reflect that. * DateAxisItem: Do not publish ZoomLevel * DateAxisItem testing: Removed unnecessary monkeypatch fixture Co-authored-by: 2xB <2xB@users.noreply.github.com> --- pyqtgraph/graphicsItems/DateAxisItem.py | 7 +-- .../graphicsItems/tests/test_AxisItem.py | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/DateAxisItem.py b/pyqtgraph/graphicsItems/DateAxisItem.py index 7cf9be2c..a5132fd9 100644 --- a/pyqtgraph/graphicsItems/DateAxisItem.py +++ b/pyqtgraph/graphicsItems/DateAxisItem.py @@ -6,7 +6,7 @@ from datetime import datetime, timedelta from .AxisItem import AxisItem from ..pgcollections import OrderedDict -__all__ = ['DateAxisItem', 'ZoomLevel'] +__all__ = ['DateAxisItem'] MS_SPACING = 1/1000.0 SECOND_SPACING = 1 @@ -260,9 +260,8 @@ class DateAxisItem(AxisItem): overriding this function or setting a different set of zoom levels than the default one. The `zoomLevels` variable is a dictionary with the maximal distance of ticks in seconds which are allowed for each zoom level - before the axis switches to the next coarser level. To create custom - zoom levels, override this function and provide custom `zoomLevelWidths` and - `zoomLevels`. + before the axis switches to the next coarser level. To customize the zoom level + selection, override this function. """ padding = 10 diff --git a/pyqtgraph/graphicsItems/tests/test_AxisItem.py b/pyqtgraph/graphicsItems/tests/test_AxisItem.py index 22dccdb4..8d89259a 100644 --- a/pyqtgraph/graphicsItems/tests/test_AxisItem.py +++ b/pyqtgraph/graphicsItems/tests/test_AxisItem.py @@ -30,3 +30,60 @@ def test_AxisItem_stopAxisAtTick(monkeypatch): plot.show() app.processEvents() plot.close() + + +def test_AxisItem_viewUnlink(): + plot = pg.PlotWidget() + view = plot.plotItem.getViewBox() + axis = plot.getAxis("bottom") + assert axis.linkedView() == view + axis.unlinkFromView() + assert axis.linkedView() is None + + +class FakeSignal: + + def __init__(self): + self.calls = [] + + def connect(self, *args, **kwargs): + self.calls.append('connect') + + def disconnect(self, *args, **kwargs): + self.calls.append('disconnect') + + +class FakeView: + + def __init__(self): + self.sigYRangeChanged = FakeSignal() + self.sigXRangeChanged = FakeSignal() + self.sigResized = FakeSignal() + + +def test_AxisItem_bottomRelink(): + axis = pg.AxisItem('bottom') + fake_view = FakeView() + axis.linkToView(fake_view) + assert axis.linkedView() == fake_view + assert fake_view.sigYRangeChanged.calls == [] + assert fake_view.sigXRangeChanged.calls == ['connect'] + assert fake_view.sigResized.calls == ['connect'] + axis.unlinkFromView() + assert fake_view.sigYRangeChanged.calls == [] + assert fake_view.sigXRangeChanged.calls == ['connect', 'disconnect'] + assert fake_view.sigResized.calls == ['connect', 'disconnect'] + + +def test_AxisItem_leftRelink(): + axis = pg.AxisItem('left') + fake_view = FakeView() + axis.linkToView(fake_view) + assert axis.linkedView() == fake_view + assert fake_view.sigYRangeChanged.calls == ['connect'] + assert fake_view.sigXRangeChanged.calls == [] + assert fake_view.sigResized.calls == ['connect'] + axis.unlinkFromView() + assert fake_view.sigYRangeChanged.calls == ['connect', 'disconnect'] + assert fake_view.sigXRangeChanged.calls == [] + assert fake_view.sigResized.calls == ['connect', 'disconnect'] From 5bebf697b0805520e2dc0410cd45b76cf312d111 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 10 May 2020 08:39:17 -0700 Subject: [PATCH 545/607] Disable remove ROI menu action in handle context menu (#1197) --- pyqtgraph/graphicsItems/ROI.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 7863dfef..43bb921d 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -758,6 +758,9 @@ class ROI(GraphicsObject): remAct.triggered.connect(self.removeClicked) self.menu.addAction(remAct) self.menu.remAct = remAct + # ROI menu may be requested when showing the handle context menu, so + # return the menu but disable it if the ROI isn't removable + self.menu.setEnabled(self.contextMenuEnabled()) return self.menu def removeClicked(self): From 14075e6223ae6064c1edef21e9c721744e45dc32 Mon Sep 17 00:00:00 2001 From: Maxim Millen Date: Mon, 11 May 2020 03:42:04 +1200 Subject: [PATCH 546/607] Added support for plot curve to handle both fill and connect args. (#1188) --- pyqtgraph/graphicsItems/PlotCurveItem.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index e0af8bed..c3a58da2 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -167,7 +167,7 @@ class PlotCurveItem(GraphicsObject): b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) ## adjust for fill level - if ax == 1 and self.opts['fillLevel'] is not None: + if ax == 1 and self.opts['fillLevel'] not in [None, 'enclosed']: b = (min(b[0], self.opts['fillLevel']), max(b[1], self.opts['fillLevel'])) ## Add pen width only if it is non-cosmetic. @@ -480,9 +480,10 @@ class PlotCurveItem(GraphicsObject): if x is None: x,y = self.getData() p2 = QtGui.QPainterPath(self.path) - p2.lineTo(x[-1], self.opts['fillLevel']) - p2.lineTo(x[0], self.opts['fillLevel']) - p2.lineTo(x[0], y[0]) + if self.opts['fillLevel'] != 'enclosed': + p2.lineTo(x[-1], self.opts['fillLevel']) + p2.lineTo(x[0], self.opts['fillLevel']) + p2.lineTo(x[0], y[0]) p2.closeSubpath() self.fillPath = p2 From 10cb80a2ae89c6d092150b1b1529225b15c236b7 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 11 May 2020 20:23:10 -0700 Subject: [PATCH 547/607] Add dependencies for docs build --- doc/requirements.txt | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 doc/requirements.txt diff --git a/doc/requirements.txt b/doc/requirements.txt new file mode 100644 index 00000000..d9335fc8 --- /dev/null +++ b/doc/requirements.txt @@ -0,0 +1,6 @@ +pyside2 +numpy +scipy +h5py +sphinx +sphinx_rtd_theme From f2e91d1b9a2390b8001e19ce84a0aa5cc4937eab Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 11 May 2020 20:45:25 -0700 Subject: [PATCH 548/607] Add matplotlib and pyopengl to docs dependencies --- doc/requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/requirements.txt b/doc/requirements.txt index d9335fc8..a98d86f2 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -2,5 +2,7 @@ pyside2 numpy scipy h5py +matplotlib +pyopengl sphinx sphinx_rtd_theme From 9a9be68d9092ab08d6a6cf85d76ba69b7da09288 Mon Sep 17 00:00:00 2001 From: Chris Billington Date: Fri, 15 May 2020 14:31:42 -0400 Subject: [PATCH 549/607] mkQApp: Use sys.argv if non-empty and always set given name (#1199) Always pass `sys.argv`, if non-empty, to `QApplication` constructor. This allows code to continue to rely on the fact that the application name is by default set from `sys.argv[0]`, which is important for example on Linux where this determines the WM_CLASS X attribute used by desktop environments to match applications to their launchers. If `sys.argv` is empty, as it is in an interactive Python session, pass `["pyqtgraph"]` in its place as a sensible default for the application name, which causes issues if not set (issue #1165). If a `name` is given, set it using `setApplicationName()` instead of via the argument list. This ensures it will be set even if the singleton `QApplication` already existed prior to calling `mkQApp()`. --- pyqtgraph/Qt.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index cc8b3d0a..702bc2bd 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -330,21 +330,19 @@ if m is not None and list(map(int, m.groups())) < versionReq: QAPP = None -def mkQApp(name="pyqtgraph", qt_args=None): +def mkQApp(name=None): """ Creates new QApplication or returns current instance if existing. - ============== ================================================================================= + ============== ======================================================== **Arguments:** - name Application name, passed to Qt - qt_args Array of command line arguments passed to Qt - ============== ================================================================================= + name (str) Application name, passed to Qt + ============== ======================================================== """ global QAPP QAPP = QtGui.QApplication.instance() if QAPP is None: - args = [name] - if qt_args is not None: - args.extend(qt_args) - QAPP = QtGui.QApplication(args) + QAPP = QtGui.QApplication(sys.argv or ["pyqtgraph"]) + if name is not None: + QAPP.setApplicationName(name) return QAPP From 5353acdb1c988d7a67baad46ae2fa16a276e0ee0 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 11 May 2020 21:17:57 -0700 Subject: [PATCH 550/607] Static paths not used for docs. Fix malformed table in docstring --- doc/source/conf.py | 2 +- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/source/conf.py b/doc/source/conf.py index dd5e0718..e59e5efd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -122,7 +122,7 @@ html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +#html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index dd864c49..73aa29cb 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -290,7 +290,7 @@ class PlotItem(GraphicsWidget): Place axis items as given by `axisItems`. Initializes non-existing axis items. ============== ========================================================================================== - **Arguments:**< + **Arguments:** *axisItems* Optional dictionary instructing the PlotItem to use pre-constructed items for its axes. The dict keys must be axis names ('left', 'bottom', 'right', 'top') and the values must be instances of AxisItem (or at least compatible with AxisItem). From ae8fc195da919adcc7466746d8daf1040deee849 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 16 May 2020 10:14:52 -0700 Subject: [PATCH 551/607] Disable inherited docstrings --- doc/source/conf.py | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/conf.py b/doc/source/conf.py index e59e5efd..a979488a 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -88,6 +88,7 @@ pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] +autodoc_inherit_docstrings = False # -- Options for HTML output --------------------------------------------------- From 54ade7dfb8a341b40c9f2d099216321145e12159 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 17 May 2020 16:06:00 -0700 Subject: [PATCH 552/607] Add readthedocs config file as recommended --- .readthedocs.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000..795d359a --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,12 @@ +# Read the Docs configuration file +# https://docs.readthedocs.io/en/stable/config-file/v2.html + +version: 2 + +python: + version: 3 + install: + - requirements: doc/requirements.txt + +sphinx: + fail_on_warning: true From 9d844f3a423d8846fa9742eb837d41848030ec09 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sun, 17 May 2020 20:10:47 -0700 Subject: [PATCH 553/607] Mock dependencies that aren't strictly needed for docs build --- doc/requirements.txt | 3 --- doc/source/conf.py | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/doc/requirements.txt b/doc/requirements.txt index a98d86f2..60d1d1e7 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,8 +1,5 @@ pyside2 numpy -scipy -h5py -matplotlib pyopengl sphinx sphinx_rtd_theme diff --git a/doc/source/conf.py b/doc/source/conf.py index a979488a..3da573eb 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -89,6 +89,11 @@ pygments_style = 'sphinx' #modindex_common_prefix = [] autodoc_inherit_docstrings = False +autodoc_mock_imports = [ + "scipy", + "h5py", + "matplotlib", +] # -- Options for HTML output --------------------------------------------------- From 8b66d0e20f58655c1350f327effe6f6ac6ccca85 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Mon, 18 May 2020 14:53:41 -0700 Subject: [PATCH 554/607] Updated README with readthedocs link and badge --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b461f4f6..d082d7ee 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,6 @@ [![Build Status](https://pyqtgraph.visualstudio.com/pyqtgraph/_apis/build/status/pyqtgraph.pyqtgraph?branchName=develop)](https://pyqtgraph.visualstudio.com/pyqtgraph/_build/latest?definitionId=17&branchName=develop) +[![Documentation Status](https://readthedocs.org/projects/pyqtgraph/badge/?version=latest)](https://pyqtgraph.readthedocs.io/en/latest/?badge=latest) PyQtGraph ========= @@ -72,4 +73,4 @@ Documentation The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu. -The official documentation lives at http://pyqtgraph.org/documentation +The official documentation lives at https://pyqtgraph.readthedocs.io From c349c3665bdd0aecd8bfc02a6b55e31dc6e54d4f Mon Sep 17 00:00:00 2001 From: Megan Kratz Date: Tue, 19 May 2020 15:11:11 -0600 Subject: [PATCH 555/607] fix for roi getting wrong data when imageAxisOrder='row-major' --- pyqtgraph/imageview/ImageView.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index daa9b06d..c3878afd 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -586,7 +586,8 @@ class ImageView(QtGui.QWidget): # Extract image data from ROI axes = (self.axes['x'], self.axes['y']) - data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) + #data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, returnMappedCoords=True) if data is None: return From 360bcad47b2c25ae2c7cffb8958bb97953b016db Mon Sep 17 00:00:00 2001 From: Megan Kratz Date: Tue, 19 May 2020 15:12:09 -0600 Subject: [PATCH 556/607] fix for mismatched axis exception when imageAxisOrder='row-major' --- pyqtgraph/imageview/ImageView.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index c3878afd..8809def9 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -595,7 +595,13 @@ class ImageView(QtGui.QWidget): if self.axes['t'] is None: # Average across y-axis of ROI data = data.mean(axis=axes[1]) - coords = coords[:,:,0] - coords[:,0:1,0] + + if axes == (0,1): ## there's probably a better way to do this slicing dynamically, but I'm not sure what it is. + coords = coords[:,:,0] - coords[:,0:1,0] + elif axes == (1,0): ## we're in row-major order mode + coords = coords[:,0,:] - coords[:,0,0:1] + else: + raise Exception("Need to implement a better way to handle these axes: %s" %str(self.axes)) xvals = (coords**2).sum(axis=0) ** 0.5 else: # Average data within entire ROI for each frame From ca2e5849c21bf99f29c32606fb5ed7866a1b0861 Mon Sep 17 00:00:00 2001 From: Megan Kratz Date: Tue, 19 May 2020 15:25:15 -0600 Subject: [PATCH 557/607] better conditional handling so as not to break something that was working before --- pyqtgraph/imageview/ImageView.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index 8809def9..f93c1fea 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -595,13 +595,10 @@ class ImageView(QtGui.QWidget): if self.axes['t'] is None: # Average across y-axis of ROI data = data.mean(axis=axes[1]) - - if axes == (0,1): ## there's probably a better way to do this slicing dynamically, but I'm not sure what it is. - coords = coords[:,:,0] - coords[:,0:1,0] - elif axes == (1,0): ## we're in row-major order mode + if axes == (1,0): ## we're in row-major order mode -- there's probably a better way to do this slicing dynamically, but I've not figured it out yet. coords = coords[:,0,:] - coords[:,0,0:1] - else: - raise Exception("Need to implement a better way to handle these axes: %s" %str(self.axes)) + else: #default to old way + coords = coords[:,:,0] - coords[:,0:1,0] xvals = (coords**2).sum(axis=0) ** 0.5 else: # Average data within entire ROI for each frame From 4052f0dd11da29e35fd08c80492fafa600100278 Mon Sep 17 00:00:00 2001 From: Marko Bausch Date: Fri, 22 May 2020 15:17:33 +0200 Subject: [PATCH 558/607] 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 6a76f40869a02d9561d008cf5fa1db0abc67b5b9 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 May 2020 11:59:31 -0700 Subject: [PATCH 559/607] Add support for running pyside2-uic binary to dynamically compile ui files --- pyqtgraph/Qt.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 702bc2bd..693ac46e 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -10,7 +10,7 @@ This module exists to smooth out some of the differences between PySide and PyQt """ -import os, sys, re, time +import os, sys, re, time, subprocess from .python2_3 import asUnicode @@ -105,28 +105,45 @@ def _loadUiType(uiFile): if QT_LIB == "PYSIDE": import pysideuic else: - import pyside2uic as pysideuic - import xml.etree.ElementTree as xml + try: + import pyside2uic as pysideuic + except ImportError: + # later vserions of pyside2 have dropped pysideuic; use the uic binary instead. + pysideuic = None + # get class names from ui file + import xml.etree.ElementTree as xml parsed = xml.parse(uiFile) widget_class = parsed.find('widget').get('class') form_class = parsed.find('class').text - - with open(uiFile, 'r') as f: + + # convert ui file to python code + if pysideuic is None: + uipy = subprocess.check_output(['pyside2-uic', uiFile]) + else: o = _StringIO() - frame = {} + with open(uiFile, 'r') as f: + pysideuic.compileUi(f, o, indent=0) + uipy = o.getvalue() - pysideuic.compileUi(f, o, indent=0) - pyc = compile(o.getvalue(), '', 'exec') - exec(pyc, frame) + # exceute python code + pyc = compile(uipy, '', 'exec') + frame = {} + exec(pyc, frame) - #Fetch the base_class and form class based on their type in the xml from designer - form_class = frame['Ui_%s'%form_class] - base_class = eval('QtGui.%s'%widget_class) + # fetch the base_class and form class based on their type in the xml from designer + form_class = frame['Ui_%s'%form_class] + base_class = eval('QtGui.%s'%widget_class) return form_class, base_class +def _pyside2uic(uiFile): + glob = {} + mod = exec(uipy, globals=glob) + + + if QT_LIB == PYSIDE: from PySide import QtGui, QtCore From d1c384876cb40cac291feb700cb5fb5b6437b137 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 28 May 2020 13:32:24 -0700 Subject: [PATCH 560/607] Remove junk code --- pyqtgraph/Qt.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 693ac46e..ef19fc63 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -138,12 +138,6 @@ def _loadUiType(uiFile): return form_class, base_class -def _pyside2uic(uiFile): - glob = {} - mod = exec(uipy, globals=glob) - - - if QT_LIB == PYSIDE: from PySide import QtGui, QtCore 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 561/607] 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 562/607] 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 563/607] 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 564/607] 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 565/607] 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 9d1fbb6a3e2a7a31a567500c23f3e3115bc9b538 Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Fri, 29 May 2020 23:42:35 -0700 Subject: [PATCH 566/607] Add warning about PySide 5.14, avoid a confusing error message that would appear with 5.14 --- examples/PlotWidget.py | 2 +- pyqtgraph/Qt.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/PlotWidget.py b/examples/PlotWidget.py index e52a893d..38bbc73c 100644 --- a/examples/PlotWidget.py +++ b/examples/PlotWidget.py @@ -13,7 +13,7 @@ import numpy as np import pyqtgraph as pg #QtGui.QApplication.setGraphicsSystem('raster') -app = QtGui.QApplication([]) +app = pg.mkQApp() mw = QtGui.QMainWindow() mw.setWindowTitle('pyqtgraph example: PlotWidget') mw.resize(800,800) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index ef19fc63..25cb488f 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -10,7 +10,7 @@ This module exists to smooth out some of the differences between PySide and PyQt """ -import os, sys, re, time, subprocess +import os, sys, re, time, subprocess, warnings from .python2_3 import asUnicode @@ -119,6 +119,8 @@ def _loadUiType(uiFile): # convert ui file to python code if pysideuic is None: + if PySide2.__version__[:5].split('.')[:2] == ['5', '14']: + warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15') uipy = subprocess.check_output(['pyside2-uic', uiFile]) else: o = _StringIO() From dfe83dc1c819477523453adb106a0ea83d9518f1 Mon Sep 17 00:00:00 2001 From: Ogi Date: Sat, 30 May 2020 07:15:46 -0700 Subject: [PATCH 567/607] 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 568/607] 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 569/607] 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 570/607] 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 571/607] 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 572/607] 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 573/607] 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 574/607] 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 575/607] 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 576/607] 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 577/607] 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 578/607] 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 579/607] 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 580/607] 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 581/607] 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 582/607] 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 583/607] 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 584/607] 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: From ab96ca1d30cb4619e723f2c45c1995e3123e0a8f Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 2 Jun 2020 22:44:17 -0700 Subject: [PATCH 585/607] Examples Should Be Tested on PySide2 5.14.2.2 --- examples/test_examples.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/test_examples.py b/examples/test_examples.py index f10fe358..a9fecca2 100644 --- a/examples/test_examples.py +++ b/examples/test_examples.py @@ -150,7 +150,12 @@ conditionalExamples = { ) } -@pytest.mark.skipif(Qt.QT_LIB == "PySide2" and "Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality") +@pytest.mark.skipif( + Qt.QT_LIB == "PySide2" + and tuple(map(int, Qt.PySide2.__version__.split("."))) >= (5, 14) + and tuple(map(int, Qt.PySide2.__version__.split("."))) < (5, 14, 2, 2), + reason="new PySide2 doesn't have loadUi functionality" +) @pytest.mark.parametrize( "frontend, f", [ From f8c107e7b268b1429ac10390bfad14433c867a2e Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 3 Jun 2020 20:18:17 -0700 Subject: [PATCH 586/607] Do not emit loadUiType warning for pyside2 5.14.2.2 --- pyqtgraph/Qt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/Qt.py b/pyqtgraph/Qt.py index 25cb488f..6035d7ac 100644 --- a/pyqtgraph/Qt.py +++ b/pyqtgraph/Qt.py @@ -119,7 +119,8 @@ def _loadUiType(uiFile): # convert ui file to python code if pysideuic is None: - if PySide2.__version__[:5].split('.')[:2] == ['5', '14']: + pyside2version = tuple(map(int, PySide2.__version__.split("."))) + if pyside2version >= (5, 14) and pyside2version < (5, 14, 2, 2): warnings.warn('For UI compilation, it is recommended to upgrade to PySide >= 5.15') uipy = subprocess.check_output(['pyside2-uic', uiFile]) else: From 3ed8c495990d24f75712297e7b884cc77352723b Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 3 Jun 2020 21:22:01 -0700 Subject: [PATCH 587/607] test_loadUiType should run on 5.14.2.2 --- pyqtgraph/tests/test_qt.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/tests/test_qt.py b/pyqtgraph/tests/test_qt.py index 9a4f373b..3ecf9db8 100644 --- a/pyqtgraph/tests/test_qt.py +++ b/pyqtgraph/tests/test_qt.py @@ -15,7 +15,12 @@ def test_isQObjectAlive(): @pytest.mark.skipif(pg.Qt.QT_LIB == 'PySide', reason='pysideuic does not appear to be ' 'packaged with conda') -@pytest.mark.skipif(pg.Qt.QT_LIB == "PySide2" and "pg.Qt.QtVersion.startswith('5.14')", reason="new PySide2 doesn't have loadUi functionality") +@pytest.mark.skipif( + pg.Qt.QT_LIB == "PySide2" + and tuple(map(int, pg.Qt.PySide2.__version__.split("."))) >= (5, 14) + and tuple(map(int, pg.Qt.PySide2.__version__.split("."))) < (5, 14, 2, 2), + reason="new PySide2 doesn't have loadUi functionality" +) def test_loadUiType(): path = os.path.dirname(__file__) formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui')) From a171a098ad16b42cdcb2dc27dc1380ece0b12062 Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 3 Jun 2020 21:27:49 -0700 Subject: [PATCH 588/607] Expand CI to test latest PySide2 --- README.md | 17 +++++++++-------- azure-test-template.yml | 8 ++++++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index d082d7ee..5ab066e2 100644 --- a/README.md +++ b/README.md @@ -37,15 +37,16 @@ Qt Bindings Test Matrix The following table represents the python environments we test in our CI system. Our CI system uses Ubuntu 18.04, Windows Server 2019, and macOS 10.15 base images. -| Qt-Bindings | Python 2.7 | Python 3.6 | Python 3.7 | Python 3.8 | -| :----------- | :----------------: | :----------------: | :----------------: | :----------------: | -| PyQt-4 | :white_check_mark: | :x: | :x: | :x: | -| PySide1 | :white_check_mark: | :x: | :x: | :x: | -| PyQt-5.9 | :x: | :white_check_mark: | :x: | :x: | -| PySide2-5.13 | :x: | :x: | :white_check_mark: | :x: | -| PyQt-5.14 | :x: | :x: | :x: | :white_check_mark: | +| Qt-Bindings | Python 2.7 | Python 3.6 | Python 3.7 | Python 3.8 | +| :------------- | :----------------: | :----------------: | :----------------: | :----------------: | +| PyQt-4 | :white_check_mark: | :x: | :x: | :x: | +| PySide1 | :white_check_mark: | :x: | :x: | :x: | +| PyQt5-5.9 | :x: | :white_check_mark: | :x: | :x: | +| PySide2-5.13 | :x: | :x: | :white_check_mark: | :x: | +| PyQt5-Latest | :x: | :x: | :x: | :white_check_mark: | +| PySide2-Latest | :x: | :x: | :x: | :white_check_mark: | -* pyqtgraph has had some incompatabilities with PySide2-5.6, and we recommend you avoid those bindings if possible +* pyqtgraph has had some incompatibilities with PySide2-5.6, and we recommend you avoid those bindings if possible * on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work Support diff --git a/azure-test-template.yml b/azure-test-template.yml index 04fd7e42..d3966499 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -18,7 +18,7 @@ jobs: python.version: '2.7' qt.bindings: "pyside" install.method: "conda" - Python36-PyQt-5.9: + Python36-PyQt5-5.9: python.version: "3.6" qt.bindings: "pyqt" install.method: "conda" @@ -26,10 +26,14 @@ jobs: python.version: "3.7" qt.bindings: "pyside2" install.method: "conda" - Python38-PyQt-5.14: + Python38-PyQt5-Latest: python.version: '3.8' qt.bindings: "PyQt5" install.method: "pip" + Python38-PySide2-Latest: + python.version: '3.8' + qt.bindings: "PySide2" + install.method: "pip" steps: - task: DownloadPipelineArtifact@2 From d282f8aba8e09c70b44c4b5ad5c0ab9a1183c32b Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Fri, 5 Jun 2020 20:57:20 -0700 Subject: [PATCH 589/607] Remove workaround for memory leak in QImage (#1223) Co-authored-by: Ognyan Moore --- pyqtgraph/functions.py | 30 ------------------------------ 1 file changed, 30 deletions(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index e788afa7..193cce6a 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -1261,30 +1261,10 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): if QT_LIB in ['PySide', 'PySide2']: ch = ctypes.c_char.from_buffer(imgData, 0) - - # Bug in PySide + Python 3 causes refcount for image data to be improperly - # incremented, which leads to leaked memory. As a workaround, we manually - # reset the reference count after creating the QImage. - # See: https://bugreports.qt.io/browse/PYSIDE-140 - - # Get initial reference count (PyObject struct has ob_refcnt as first element) - rcount = ctypes.c_long.from_address(id(ch)).value img = QtGui.QImage(ch, imgData.shape[1], imgData.shape[0], imgFormat) - if sys.version[0] == '3': - # Reset refcount only on python 3. Technically this would have no effect - # on python 2, but this is a nasty hack, and checking for version here - # helps to mitigate possible unforseen consequences. - ctypes.c_long.from_address(id(ch)).value = rcount else: - #addr = ctypes.addressof(ctypes.c_char.from_buffer(imgData, 0)) ## PyQt API for QImage changed between 4.9.3 and 4.9.6 (I don't know exactly which version it was) ## So we first attempt the 4.9.6 API, then fall back to 4.9.3 - #addr = ctypes.c_char.from_buffer(imgData, 0) - #try: - #img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) - #except TypeError: - #addr = ctypes.addressof(addr) - #img = QtGui.QImage(addr, imgData.shape[1], imgData.shape[0], imgFormat) try: img = QtGui.QImage(imgData.ctypes.data, imgData.shape[1], imgData.shape[0], imgFormat) except: @@ -1297,16 +1277,6 @@ def makeQImage(imgData, alpha=None, copy=True, transpose=True): img.data = imgData return img - #try: - #buf = imgData.data - #except AttributeError: ## happens when image data is non-contiguous - #buf = imgData.data - - #profiler() - #qimage = QtGui.QImage(buf, imgData.shape[1], imgData.shape[0], imgFormat) - #profiler() - #qimage.data = imgData - #return qimage def imageToArray(img, copy=False, transpose=True): """ From c0b9bfa040575ea6bd0580a87aa3e3efdbd99d7f Mon Sep 17 00:00:00 2001 From: Ogi Date: Fri, 5 Jun 2020 21:00:18 -0700 Subject: [PATCH 590/607] Remove commented out line --- pyqtgraph/imageview/ImageView.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyqtgraph/imageview/ImageView.py b/pyqtgraph/imageview/ImageView.py index f93c1fea..8324cbbc 100644 --- a/pyqtgraph/imageview/ImageView.py +++ b/pyqtgraph/imageview/ImageView.py @@ -586,7 +586,6 @@ class ImageView(QtGui.QWidget): # Extract image data from ROI axes = (self.axes['x'], self.axes['y']) - #data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, returnMappedCoords=True) if data is None: return From d86bb65520f92061814668cab937df36c2e0bfe5 Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sat, 6 Jun 2020 15:52:55 +0200 Subject: [PATCH 591/607] ParameterTree: Fix custom context menu This issue was introduced in merging develop into #1175. While refactoring for the merge, the change in namespace was not correctly attributed, leading to the parameter `opts` to be assumed in local namespace when it isn't. --- pyqtgraph/parametertree/ParameterItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index ecafd577..ab5fad96 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -117,7 +117,7 @@ class ParameterItem(QtGui.QTreeWidgetItem): self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) # context menu - context = opts.get('context', None) + context = self.param.opts.get('context', None) if isinstance(context, list): for name in context: self.contextMenu.addAction(name).triggered.connect( From 78929adbea2944d661cfdd3981ec582e1ba5fe4f Mon Sep 17 00:00:00 2001 From: 2xB <2xB@users.noreply.github.com> Date: Sat, 6 Jun 2020 16:04:05 +0200 Subject: [PATCH 592/607] ParameterItem: self.param.opts -> opts Using `opts` as alias for `self.param.opts`, following the style of `updateFlags`. --- pyqtgraph/parametertree/ParameterItem.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyqtgraph/parametertree/ParameterItem.py b/pyqtgraph/parametertree/ParameterItem.py index ab5fad96..b697b956 100644 --- a/pyqtgraph/parametertree/ParameterItem.py +++ b/pyqtgraph/parametertree/ParameterItem.py @@ -104,20 +104,22 @@ 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)\ - and "context" not in self.param.opts: + opts = self.param.opts + + if not opts.get('removable', False) and not opts.get('renamable', False)\ + and "context" not in 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): + if opts.get('renamable', False): self.contextMenu.addAction('Rename').triggered.connect(self.editName) - if self.param.opts.get('removable', False): + if opts.get('removable', False): self.contextMenu.addAction("Remove").triggered.connect(self.requestRemove) # context menu - context = self.param.opts.get('context', None) + context = opts.get('context', None) if isinstance(context, list): for name in context: self.contextMenu.addAction(name).triggered.connect( From 120d251a25c95034d304605b6ed941e64b8fabda Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 6 Jun 2020 15:56:01 -0700 Subject: [PATCH 593/607] Minor improvements to LegendItem. - Adds doc strings for user-facing methods so they appear in the documentation. - Allows PlotItem.addLegend to accept the same arguments as LegendItem constructor for convenience. - Fixes a bug for adding a BarGraphItem (which doesn't have an antialias option) to LegendItem --- pyqtgraph/graphicsItems/LegendItem.py | 56 ++++++++++---------- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 12 +++-- 2 files changed, 35 insertions(+), 33 deletions(-) diff --git a/pyqtgraph/graphicsItems/LegendItem.py b/pyqtgraph/graphicsItems/LegendItem.py index 7d60f37a..da830782 100644 --- a/pyqtgraph/graphicsItems/LegendItem.py +++ b/pyqtgraph/graphicsItems/LegendItem.py @@ -13,10 +13,13 @@ __all__ = ['LegendItem'] class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): """ Displays a legend used for describing the contents of a plot. - LegendItems are most commonly created by calling PlotItem.addLegend(). - Note that this item should not be added directly to a PlotItem. Instead, - Make it a direct descendant of the PlotItem:: + LegendItems are most commonly created by calling :meth:`PlotItem.addLegend + `. + + Note that this item should *not* be added directly to a PlotItem (via + :meth:`PlotItem.addItem `). Instead, make it a + direct descendant of the PlotItem:: legend.setParentItem(plotItem) @@ -46,8 +49,6 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): ============== =============================================================== """ - - GraphicsWidget.__init__(self) GraphicsWidgetAnchor.__init__(self) self.setFlag(self.ItemIgnoresTransformations) @@ -71,9 +72,11 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.opts.update(kwargs) def offset(self): + """Get the offset position relative to the parent.""" return self.opts['offset'] def setOffset(self, offset): + """Set the offset position relative to the parent.""" self.opts['offset'] = offset offset = Point(self.opts['offset']) @@ -83,13 +86,13 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.anchor(itemPos=anchor, parentPos=anchor, offset=offset) def pen(self): + """Get the QPen used to draw the border around the legend.""" return self.opts['pen'] def setPen(self, *args, **kargs): - """ - Sets the pen used to draw lines between points. - *pen* can be a QPen or any argument accepted by - :func:`pyqtgraph.mkPen() ` + """Set the pen used to draw a border around the legend. + + Accepts the same arguments as :func:`~pyqtgraph.mkPen`. """ pen = fn.mkPen(*args, **kargs) self.opts['pen'] = pen @@ -97,9 +100,14 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.update() def brush(self): + """Get the QBrush used to draw the legend background.""" return self.opts['brush'] def setBrush(self, *args, **kargs): + """Set the brush used to draw the legend background. + + Accepts the same arguments as :func:`~pyqtgraph.mkBrush`. + """ brush = fn.mkBrush(*args, **kargs) if self.opts['brush'] == brush: return @@ -108,13 +116,13 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.update() def labelTextColor(self): + """Get the QColor used for the item labels.""" return self.opts['labelTextColor'] def setLabelTextColor(self, *args, **kargs): - """ - Sets the color of the label text. - *pen* can be a QPen or any argument accepted by - :func:`pyqtgraph.mkColor() ` + """Set the color of the item labels. + + Accepts the same arguments as :func:`~pyqtgraph.mkColor`. """ self.opts['labelTextColor'] = fn.mkColor(*args, **kargs) for sample, label in self.items: @@ -123,6 +131,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.update() def setParentItem(self, p): + """Set the parent.""" ret = GraphicsWidget.setParentItem(self, p) if self.opts['offset'] is not None: offset = Point(self.opts['offset']) @@ -138,10 +147,10 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): ============== ======================================================== **Arguments:** - item A PlotDataItem from which the line and point style - of the item will be determined or an instance of - ItemSample (or a subclass), allowing the item display - to be customized. + item A :class:`~pyqtgraph.PlotDataItem` from which the line + and point style of the item will be determined or an + instance of ItemSample (or a subclass), allowing the + item display to be customized. title The title to display for this item. Simple HTML allowed. ============== ======================================================== """ @@ -177,7 +186,7 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): return # return after first match def clear(self): - """Removes all items from legend.""" + """Remove all items from the legend.""" for sample, label in self.items: self.layout.removeItem(sample) self.layout.removeItem(label) @@ -185,15 +194,6 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor): self.items = [] self.updateSize() - def clear(self): - """ - Removes all items from the legend. - - Useful for reusing and dynamically updating charts and their legends. - """ - while self.items != []: - self.removeItem(self.items[0][1].text) - def updateSize(self): if self.size is not None: return @@ -234,7 +234,7 @@ class ItemSample(GraphicsWidget): def paint(self, p, *args): opts = self.item.opts - if opts['antialias']: + if opts.get('antialias'): p.setRenderHint(p.Antialiasing) if not isinstance(self.item, ScatterPlotItem): diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 4142fa3f..77f4cc15 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -682,17 +682,19 @@ class PlotItem(GraphicsWidget): return item - def addLegend(self, size=None, offset=(30, 30)): + def addLegend(self, offset=(30, 30), **kwargs): """ - Create a new LegendItem and anchor it over the internal ViewBox. - Plots will be automatically displayed in the legend if they - are created with the 'name' argument. + Create a new :class:`~pyqtgraph.LegendItem` and anchor it over the + internal ViewBox. Plots will be automatically displayed in the legend + if they are created with the 'name' argument. If a LegendItem has already been created using this method, that item will be returned rather than creating a new one. + + Accepts the same arguments as :meth:`~pyqtgraph.LegendItem`. """ if self.legend is None: - self.legend = LegendItem(size, offset) + self.legend = LegendItem(offset=offset, **kwargs) self.legend.setParentItem(self.vb) return self.legend From 3cad91b5f1d50bddbea6d2c945d9041c3b3d0f19 Mon Sep 17 00:00:00 2001 From: Kenneth Lyons Date: Sat, 6 Jun 2020 16:22:16 -0700 Subject: [PATCH 594/607] Wrap text in tables in docs. --- doc/source/_static/custom.css | 14 ++++++++++++++ doc/source/conf.py | 6 +++++- 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 doc/source/_static/custom.css diff --git a/doc/source/_static/custom.css b/doc/source/_static/custom.css new file mode 100644 index 00000000..2ad3413b --- /dev/null +++ b/doc/source/_static/custom.css @@ -0,0 +1,14 @@ +/* Customizations to the theme */ + +/* override table width restrictions */ +/* https://github.com/readthedocs/sphinx_rtd_theme/issues/117 */ +@media screen and (min-width: 768px) { + .wy-table-responsive table td, .wy-table-responsive table th { + white-space: normal !important; + } + + .wy-table-responsive { + overflow: visible !important; + max-width: 100%; + } +} diff --git a/doc/source/conf.py b/doc/source/conf.py index a6e2cf8c..04a95afd 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -131,7 +131,11 @@ html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -#html_static_path = ['_static'] +html_static_path = ['_static'] + +# add the theme customizations +def setup(app): + app.add_stylesheet("custom.css") # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. From f43b2973125a4510fbd1dda300cce2cee249e422 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Sat, 6 Jun 2020 22:06:14 -0500 Subject: [PATCH 595/607] Update changelog with changes since v0.11.0rc0 (#1230) * Update changelog with changes since v0.11.0rc0 * tab to spaces --- CHANGELOG | 68 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 9a3bcf4e..65b423dd 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -65,6 +65,10 @@ pyqtgraph-0.11.0 (in development) - #996: Allow the update of LegendItem - #1023: Add bookkeeping exporter parameters - #1072: HDF5Exporter handling of ragged curves with tests + - #1124: Syntax highlighting for examples. + - #1154: Date axis item + - #393: NEW show/hide gradient ticks NEW link gradientEditor to others + - #1211: Add support for running pyside2-uic binary to dynamically compile ui files API / behavior changes: - Deprecated graphicsWindow classes; these have been unnecessary for many years because @@ -102,6 +106,32 @@ pyqtgraph-0.11.0 (in development) - #1076: Reset currentRow and currentCol on GraphicsLayout.clear() - #1079: Improve performance of updateData PlotCurveItem - #1082: Allow MetaArray.__array__ to accept an optional dtype arg + - #841: set color of tick-labels separately + - #1111: Add name label to GradientEditorItem + - #1145: Pass showAxRect keyword arguments to setRange + - #1184: improve SymbolAtlas.getSymbolCoords performance + - #1198: improve SymbolAtlas.getSymbolCoords and ScatterPlotItem.plot performance + - #1197: Disable remove ROI menu action in handle context menu + - #1188: Added support for plot curve to handle both fill and connect args + - #801: Remove use of GraphicsScene._addressCache in translateGraphicsItem + - Deprecates registerObject meethod of GraphicsScene + - Deprecates regstar argument to GraphicsScene.__init__ + - #1166: pg.mkQApp: Pass non-empty string array to QApplication() as default + - #1199: Pass non-empty sys.argv to QApplication + - #1090: dump ExportDialog.exporterParameters + - #1173: GraphicsLayout: Always call layout.activate() after adding items + - #1097: pretty-print log-scale axes labels + - #755: Check lastDownsample in viewTransformChanged + - #1216: Add cache for mapRectFromView + - #444: Fix duplicate menus in GradientEditorItem + - #151: Optionally provide custom PlotItem to PlotWidget + - #1093: Fix aspectRatio and zoom range issues when zooming + - #390: moved some functionality from method 'export' to new method + - #391: changed structure to redefine axis via plotitem.setAxes + - #468: Patch/window handling + - #392: new method 'getAxpectRatio' with code taken from 'setAspectLocked' + - #1206: Added context menu option to parametertree + - #1228: Minor improvements to LegendItem Bugfixes: - #88: Fixed image scatterplot export @@ -227,6 +257,33 @@ pyqtgraph-0.11.0 (in development) - #1073: Fix Python3 compatibility - #1083: Fix SVG export of scatter plots - #1085: Fix ofset when drawing symbol + - #1101: Fix small oversight in LegendItem + - #1113: Correctly call hasFaceIndexedData function + - #1139: Bug fix in LegendItem for `setPen`, `setBrush` etc (Call update instead of paint) + - #1110: fix for makeARGB error after #955 + - #1063: Fix: AttributeError in ViewBox.setEnableMenu + - #1151: ImageExporter py2-pyside fix with test + - #1133: compatibility-fix for py2/pyside + - #1152: Nanmask fix in makeARGB + - #1159: Fix: Update axes after data is set + - #1156: SVGExporter: Correct image pixelation + - #1169: Replace default list arg with None + - #770: Do not ignore pos argument of setCameraPosition + - #1180: Fix: AxisItem tickFont is defined in two places while only one is used + - #1168: GroupParameterItem: Did not pass changed options to ParameterItem + - #1174: Fixed a possible race condition with linked views + - #809: Fix selection of FlowchartWidget input/output nodes + - #1071: Fix py3 execution in flowchart + - #1212: Fix PixelVectors cache + - #1161: Correctly import numpy where needed + - #1218: Fix ParameterTree.clear() + - #1175: Fix: Parameter tree ignores user-set 'expanded' state + - #1219: Encode csv export header as unicode + - #507: Fix Dock close event QLabel still running with no parent + - #1222: py3 fix for ScatterPlotWidget.setSelectedFields + - #1203: Image axis order bugfix + - #1225: ParameterTree: Fix custom context menu + Maintenance: - Lots of new unit tests @@ -240,6 +297,17 @@ pyqtgraph-0.11.0 (in development) - #1042: Close windows at the end of test functions - #1046: Establish minimum numpy version, remove legacy workarounds - #1067: Make scipy dependency optional + - #1114: doc: Fix small mistake in introduction + - #1131: Update CI/tox and Enable More Tests + - #1142: Miscellaneous doc fixups + - #1179: DateAxisItem: AxisItem unlinking tests and doc fixed + - #1201: Get readthedocs working + - #1214: Pin PyVirtualDisplay Version + - #1215: Skip test when on qt 5.9 + - #1221: Identify pyqt5 5.15 ci issue + - #1223: Remove workaround for memory leak in QImage + - #1217: Get docs version and copyright year dynamically + - #1229: Wrap text in tables in docs pyqtgraph-0.10.0 From 4a5af52fca8c3f2223a8358a1851f9a0cc3d4d39 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sat, 6 Jun 2020 20:34:21 -0700 Subject: [PATCH 596/607] Update README for 0.11 release --- README.md | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 5ab066e2..07787663 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ PyQtGraph A pure-Python graphics library for PyQt/PySide/PyQt5/PySide2 -Copyright 2019 Luke Campagnola, University of North Carolina at Chapel Hill +Copyright 2020 Luke Campagnola, University of North Carolina at Chapel Hill @@ -19,18 +19,14 @@ heavy leverage of numpy for number crunching, Qt's GraphicsView framework for Requirements ------------ -* PyQt 4.8+, PySide, PyQt5, or PySide2 - * PySide2 5.14 does not have loadUiType functionality, and thus the example application will not work. You can follow along with restoring that functionality [here](https://bugreports.qt.io/browse/PYSIDE-1223). * Python 2.7, or 3.x * Required + * PyQt 4.8+, PySide, PyQt5, or PySide2 * `numpy` * Optional * `scipy` for image processing * `pyopengl` for 3D graphics - * macOS with Python2 and Qt4 bindings (PyQt4 or PySide) do not work with 3D OpenGL graphics - * `pyqtgraph.opengl` will be depreciated in a future version and replaced with `VisPy` * `hdf5` for large hdf5 binary format support -* Known to run on Windows, Linux, and macOS. Qt Bindings Test Matrix ----------------------- @@ -46,8 +42,8 @@ The following table represents the python environments we test in our CI system. | PyQt5-Latest | :x: | :x: | :x: | :white_check_mark: | | PySide2-Latest | :x: | :x: | :x: | :white_check_mark: | -* pyqtgraph has had some incompatibilities with PySide2-5.6, and we recommend you avoid those bindings if possible -* on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work +* pyqtgraph has had some incompatibilities with PySide2 versions 5.6-5.11, and we recommend you avoid those versions if possible +* on macOS with Python 2.7 and Qt4 bindings (PyQt4 or PySide) the openGL related visualizations do not work reliably Support ------- @@ -60,18 +56,17 @@ Installation Methods * From PyPI: * Last released version: `pip install pyqtgraph` - * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@develop` + * Latest development version: `pip install git+https://github.com/pyqtgraph/pyqtgraph@master` * From conda - * Last released version: `conda install pyqtgraph` + * Last released version: `conda install -c conda-forge pyqtgraph` * To install system-wide from source distribution: `python setup.py install` * Many linux package repositories have release versions. * To use with a specific project, simply copy the pyqtgraph subdirectory anywhere that is importable from your project. -* For installation packages, see the website (pyqtgraph.org) Documentation ------------- -The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` for a menu. - The official documentation lives at https://pyqtgraph.readthedocs.io + +The easiest way to learn pyqtgraph is to browse through the examples; run `python -m pyqtgraph.examples` to launch the examples application. From 5b5749aa0b1535a044f2e9b8c09cd1e75e988ea8 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Sun, 7 Jun 2020 20:29:28 -0700 Subject: [PATCH 597/607] Revert "changed structure to redefine axis via plotitem.setAxes (#391)" This reverts commit bb21791c710ccd11c889a6641672adc7fbbdcf3e. --- pyqtgraph/graphicsItems/AxisItem.py | 29 ++--------- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 51 +++----------------- 2 files changed, 12 insertions(+), 68 deletions(-) diff --git a/pyqtgraph/graphicsItems/AxisItem.py b/pyqtgraph/graphicsItems/AxisItem.py index c02e6e0c..29f3ad62 100644 --- a/pyqtgraph/graphicsItems/AxisItem.py +++ b/pyqtgraph/graphicsItems/AxisItem.py @@ -45,8 +45,11 @@ class AxisItem(GraphicsWidget): GraphicsWidget.__init__(self, parent) self.label = QtGui.QGraphicsTextItem(self) self.picture = None - self.orientation = None - self.setOrientation(orientation) + 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.style = { 'tickTextOffset': [5, 2], ## (horizontal, vertical) spacing between text and axis @@ -108,27 +111,6 @@ 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. @@ -532,7 +514,6 @@ 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 77f4cc15..c61a35b2 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -153,10 +153,10 @@ class PlotItem(GraphicsWidget): self.legend = None - ## Create and place axis items + # Initialize axis items self.axes = {} - self.setAxes(axisItems) - + self.setAxisItems(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,45 +258,8 @@ 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'] @@ -1162,8 +1125,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.getAxis(axis) - #p = self.axes[axis]['pos'] + s = self.getScale(axis) + p = self.axes[axis]['pos'] if show: s.show() else: From e1f2cdce7441f0351afc72a9f36ab007da8b25c9 Mon Sep 17 00:00:00 2001 From: Kyle Sunden Date: Mon, 8 Jun 2020 18:20:41 -0500 Subject: [PATCH 598/607] Final preparations for 0.11.0 release Intend to tag and upload after this is merged --- CHANGELOG | 6 ++++-- pyqtgraph/__init__.py | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 65b423dd..efc3ee3b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,6 @@ -pyqtgraph-0.11.0 (in development) +pyqtgraph-0.11.0 + + NOTICE: This is the _last_ feature release to support Python 2 and Qt 4 (PyQt4 or pyside 1) New Features: - #101: GridItem formatting options @@ -127,7 +129,6 @@ pyqtgraph-0.11.0 (in development) - #151: Optionally provide custom PlotItem to PlotWidget - #1093: Fix aspectRatio and zoom range issues when zooming - #390: moved some functionality from method 'export' to new method - - #391: changed structure to redefine axis via plotitem.setAxes - #468: Patch/window handling - #392: new method 'getAxpectRatio' with code taken from 'setAspectLocked' - #1206: Added context menu option to parametertree @@ -308,6 +309,7 @@ pyqtgraph-0.11.0 (in development) - #1223: Remove workaround for memory leak in QImage - #1217: Get docs version and copyright year dynamically - #1229: Wrap text in tables in docs + - #1231: Update readme for 0.11 release pyqtgraph-0.10.0 diff --git a/pyqtgraph/__init__.py b/pyqtgraph/__init__.py index f834e637..bc36e891 100644 --- a/pyqtgraph/__init__.py +++ b/pyqtgraph/__init__.py @@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python www.pyqtgraph.org """ -__version__ = '0.11.0.dev0' +__version__ = '0.11.0' ### import all the goodies and add some helper functions for easy CLI use From 66d89433170ada9ed5a9ac0b14afacf2d7dbefca Mon Sep 17 00:00:00 2001 From: Ogi Date: Mon, 8 Jun 2020 21:36:09 -0700 Subject: [PATCH 599/607] PlotItem doesn't add item if already there --- pyqtgraph/graphicsItems/PlotItem/PlotItem.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index c61a35b2..79d59235 100644 --- a/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import sys +import warnings import weakref import numpy as np import os @@ -514,6 +515,9 @@ class PlotItem(GraphicsWidget): If the item has plot data (PlotDataItem, PlotCurveItem, ScatterPlotItem), it may be included in analysis performed by the PlotItem. """ + if item in self.items: + warnings.warn('Item already added to PlotItem, ignoring.') + return self.items.append(item) vbargs = {} if 'ignoreBounds' in kargs: From fc7921100e6bb66b0367f2c57e7122ace832b918 Mon Sep 17 00:00:00 2001 From: alfon_news Date: Tue, 9 Jun 2020 07:51:14 +0200 Subject: [PATCH 600/607] Fix siScale imprecision errors (#508) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix siScale imprecision errors * Implement 2xB suggested change Co-authored-by: Alberto Fontán Correa Co-authored-by: Ogi Moore --- pyqtgraph/functions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index 193cce6a..b202e86a 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -77,7 +77,8 @@ def siScale(x, minVal=1e-25, allowUnicode=True): pref = SI_PREFIXES[m+8] else: pref = SI_PREFIXES_ASCII[m+8] - p = .001**m + m1 = -3*m + p = 10.**m1 return (p, pref) From 2848d451f67695ab34e7b2832f24c1e948784cc7 Mon Sep 17 00:00:00 2001 From: Karl Georg Bedrich Date: Tue, 9 Jun 2020 08:33:12 +0200 Subject: [PATCH 601/607] draw connector lines between gradient and region with anti-aliasing (#496) Co-authored-by: serkgb Co-authored-by: Ogi Moore --- pyqtgraph/graphicsItems/HistogramLUTItem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyqtgraph/graphicsItems/HistogramLUTItem.py b/pyqtgraph/graphicsItems/HistogramLUTItem.py index ad39b60e..38f1e5b4 100644 --- a/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -142,6 +142,7 @@ class HistogramLUTItem(GraphicsWidget): p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) + p.setRenderHint(QtGui.QPainter.Antialiasing) for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]: p.setPen(pen) p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft()) From 258da198867516ba6959e5274a79fa924462e919 Mon Sep 17 00:00:00 2001 From: Ogi Date: Tue, 9 Jun 2020 20:26:21 -0700 Subject: [PATCH 602/607] Use older pytest-xvfb for py2 configs --- azure-test-template.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/azure-test-template.yml b/azure-test-template.yml index 902077a6..e1d4e177 100644 --- a/azure-test-template.yml +++ b/azure-test-template.yml @@ -135,7 +135,12 @@ jobs: then source activate test-environment-$(python.version) fi - pip install PyVirtualDisplay==0.2.5 pytest-xvfb + if [ $(python.version) == "2.7" ] + then + pip install PyVirtualDisplay==0.2.5 pytest-xvfb==1.2.0 + else + pip install pytest-xvfb + fi displayName: "Virtual Display Setup" condition: eq(variables['agent.os'], 'Linux' ) From e18af48b8dfd67801ad22596127fd0f355d64feb Mon Sep 17 00:00:00 2001 From: Maurice van der Pot Date: Wed, 10 Jun 2020 07:04:29 +0200 Subject: [PATCH 603/607] Implement headWidth parameter for arrows (#385) Although the documentation used to say that specifying tipAngle would override headWidth, headWidth was never used. The new behaviour is that tipAngle will be used, with a default value of 25, unless headWidth is specified. Co-authored-by: Ogi Moore --- examples/Arrow.py | 2 +- pyqtgraph/functions.py | 7 ++++--- pyqtgraph/graphicsItems/ArrowItem.py | 9 +++++---- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/Arrow.py b/examples/Arrow.py index d5ea2a74..2a707fec 100644 --- a/examples/Arrow.py +++ b/examples/Arrow.py @@ -30,7 +30,7 @@ p2 = cw.addPlot(row=1, col=0) ## variety of arrow shapes a1 = pg.ArrowItem(angle=-160, tipAngle=60, headLen=40, tailLen=40, tailWidth=20, pen={'color': 'w', 'width': 3}) a2 = pg.ArrowItem(angle=-120, tipAngle=30, baseAngle=20, headLen=40, tailLen=40, tailWidth=8, pen=None, brush='y') -a3 = pg.ArrowItem(angle=-60, tipAngle=30, baseAngle=20, headLen=40, tailLen=None, brush=None) +a3 = pg.ArrowItem(angle=-60, baseAngle=20, headLen=40, headWidth=20, tailLen=None, brush=None) a4 = pg.ArrowItem(angle=-20, tipAngle=30, baseAngle=-30, headLen=40, tailLen=None) a2.setPos(10,0) a3.setPos(20,0) diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index b202e86a..e47aa411 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -389,14 +389,15 @@ def glColor(*args, **kargs): -def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0): +def makeArrowPath(headLen=20, headWidth=None, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0): """ Construct a path outlining an arrow with the given dimensions. The arrow points in the -x direction with tip positioned at 0,0. - If *tipAngle* is supplied (in degrees), it overrides *headWidth*. + If *headWidth* is supplied, it overrides *tipAngle* (in degrees). If *tailLen* is None, no tail will be drawn. """ - headWidth = headLen * np.tan(tipAngle * 0.5 * np.pi/180.) + if headWidth is None: + headWidth = headLen * np.tan(tipAngle * 0.5 * np.pi/180.) path = QtGui.QPainterPath() path.moveTo(0,0) path.lineTo(headLen, -headWidth) diff --git a/pyqtgraph/graphicsItems/ArrowItem.py b/pyqtgraph/graphicsItems/ArrowItem.py index 897cbc50..b272b7fc 100644 --- a/pyqtgraph/graphicsItems/ArrowItem.py +++ b/pyqtgraph/graphicsItems/ArrowItem.py @@ -28,6 +28,7 @@ class ArrowItem(QtGui.QGraphicsPathItem): 'angle': -150, ## If the angle is 0, the arrow points left 'pos': (0,0), 'headLen': 20, + 'headWidth': None, 'tipAngle': 25, 'baseAngle': 0, 'tailLen': None, @@ -52,10 +53,10 @@ class ArrowItem(QtGui.QGraphicsPathItem): 0; arrow pointing to the left. headLen Length of the arrow head, from tip to base. default=20 - headWidth Width of the arrow head at its base. + headWidth Width of the arrow head at its base. If + headWidth is specified, it overrides tipAngle. tipAngle Angle of the tip of the arrow in degrees. Smaller - values make a 'sharper' arrow. If tipAngle is - specified, ot overrides headWidth. default=25 + values make a 'sharper' arrow. default=25 baseAngle Angle of the base of the arrow head. Default is 0, which means that the base of the arrow head is perpendicular to the arrow tail. @@ -70,7 +71,7 @@ class ArrowItem(QtGui.QGraphicsPathItem): """ self.opts.update(opts) - opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) + opt = dict([(k,self.opts[k]) for k in ['headLen', 'headWidth', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) tr = QtGui.QTransform() tr.rotate(self.opts['angle']) self.path = tr.map(fn.makeArrowPath(**opt)) From 2e8dce2fc2b7b25d6360e6bafd3933cafa85fa54 Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 10 Jun 2020 20:08:34 -0700 Subject: [PATCH 604/607] Emit the event with sigClicked in PlotCurveItem --- pyqtgraph/graphicsItems/PlotCurveItem.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index c3a58da2..b6c6d216 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -613,7 +613,7 @@ class PlotCurveItem(GraphicsObject): return if self.mouseShape().contains(ev.pos()): ev.accept() - self.sigClicked.emit(self) + self.sigClicked.emit(self, ev) From 05f8921555f8957205b900b9ea9213df0d17cad0 Mon Sep 17 00:00:00 2001 From: Ogi Date: Wed, 10 Jun 2020 20:50:04 -0700 Subject: [PATCH 605/607] Implement suggested changes in PR 143 --- pyqtgraph/graphicsItems/ROI.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/pyqtgraph/graphicsItems/ROI.py b/pyqtgraph/graphicsItems/ROI.py index 43bb921d..fdcada14 100644 --- a/pyqtgraph/graphicsItems/ROI.py +++ b/pyqtgraph/graphicsItems/ROI.py @@ -590,13 +590,13 @@ class ROI(GraphicsObject): ## If a Handle was not supplied, create it now if 'item' not in info or info['item'] is None: h = Handle(self.handleSize, typ=info['type'], pen=self.handlePen, parent=self) - h.setPos(info['pos'] * self.state['size']) info['item'] = h else: h = info['item'] if info['pos'] is None: info['pos'] = h.pos() - + h.setPos(info['pos'] * self.state['size']) + ## connect the handle to this ROI #iid = len(self.handles) h.connectROI(self) @@ -1273,11 +1273,12 @@ class Handle(UIGraphicsItem): sigClicked = QtCore.Signal(object, object) # self, event sigRemoveRequested = QtCore.Signal(object) # self - def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False): + def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False, activePen=(255, 255, 0)): self.rois = [] self.radius = radius self.typ = typ self.pen = fn.mkPen(pen) + self.activePen = fn.mkPen(activePen) self.currentPen = self.pen self.pen.setWidth(0) self.pen.setCosmetic(True) @@ -1321,7 +1322,7 @@ class Handle(UIGraphicsItem): hover=True if hover: - self.currentPen = fn.mkPen(255, 255,0) + self.currentPen = self.activePen else: self.currentPen = self.pen self.update() @@ -1374,15 +1375,19 @@ class Handle(UIGraphicsItem): for r in self.rois: r.stateChangeFinished() self.isMoving = False + self.currentPen = self.pen + self.update() elif ev.isStart(): for r in self.rois: r.handleMoveStarted() self.isMoving = True self.startPos = self.scenePos() self.cursorOffset = self.scenePos() - ev.buttonDownScenePos() + self.currentPen = self.activePen if self.isMoving: ## note: isMoving may become False in mid-drag due to right-click. pos = ev.scenePos() + self.cursorOffset + self.currentPen = self.activePen self.movePoint(pos, ev.modifiers(), finish=False) def movePoint(self, pos, modifiers=QtCore.Qt.KeyboardModifier(), finish=True): From dbdd5d9a395befc6b589b55f5c625efa7c77812e Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 10 Jun 2020 23:03:43 -0700 Subject: [PATCH 606/607] Peque scatter symbols (#1244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added arrow symbols for the ScatterPlotItem * Fixed arrows rotation in scatter plots * Added new symbols to example Co-authored-by: Miguel Sánchez de León Peque --- examples/ScatterPlot.py | 3 +-- examples/Symbols.py | 4 ++++ pyqtgraph/graphicsItems/ScatterPlotItem.py | 16 +++++++++++++--- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/examples/ScatterPlot.py b/examples/ScatterPlot.py index 93f184f2..ea86bd19 100644 --- a/examples/ScatterPlot.py +++ b/examples/ScatterPlot.py @@ -83,7 +83,7 @@ random_str = lambda : (''.join([chr(np.random.randint(ord('A'),ord('z'))) for i s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) pos = np.random.normal(size=(2,n), scale=1e-5) -spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] +spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%10, 'size': 5+i/10.} for i in range(n)] s2.addPoints(spots) spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': label[1], 'size': label[2]*(5+i/10.)} for (i, label) in [(i, createLabel(*random_str())) for i in range(n)]] s2.addPoints(spots) @@ -120,4 +120,3 @@ if __name__ == '__main__': import sys if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'): QtGui.QApplication.instance().exec_() - diff --git a/examples/Symbols.py b/examples/Symbols.py index 417df35e..a0c57f75 100755 --- a/examples/Symbols.py +++ b/examples/Symbols.py @@ -29,6 +29,10 @@ plot.plot([7, 8, 9, 10, 11], pen=(217,83,25), symbolBrush=(217,83,25), symbolPen plot.plot([8, 9, 10, 11, 12], pen=(237,177,32), symbolBrush=(237,177,32), symbolPen='w', symbol='star', symbolSize=14, name="symbol='star'") plot.plot([9, 10, 11, 12, 13], pen=(126,47,142), symbolBrush=(126,47,142), symbolPen='w', symbol='+', symbolSize=14, name="symbol='+'") plot.plot([10, 11, 12, 13, 14], pen=(119,172,48), symbolBrush=(119,172,48), symbolPen='w', symbol='d', symbolSize=14, name="symbol='d'") +plot.plot([11, 12, 13, 14, 15], pen=(253, 216, 53), symbolBrush=(253, 216, 53), symbolPen='w', symbol='arrow_down', symbolSize=22, name="symbol='arrow_down'") +plot.plot([12, 13, 14, 15, 16], pen=(189, 189, 189), symbolBrush=(189, 189, 189), symbolPen='w', symbol='arrow_left', symbolSize=22, name="symbol='arrow_left'") +plot.plot([13, 14, 15, 16, 17], pen=(187, 26, 95), symbolBrush=(187, 26, 95), symbolPen='w', symbol='arrow_up', symbolSize=22, name="symbol='arrow_up'") +plot.plot([14, 15, 16, 17, 18], pen=(248, 187, 208), symbolBrush=(248, 187, 208), symbolPen='w', symbol='arrow_right', symbolSize=22, name="symbol='arrow_right'") plot.setXRange(-2, 4) ## Start Qt event loop unless running in interactive mode or using pyside. diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index af6efcc8..1140c36f 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- from itertools import starmap, repeat try: from itertools import imap @@ -20,7 +21,9 @@ __all__ = ['ScatterPlotItem', 'SpotItem'] ## Build all symbol paths -Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 't1', 't2', 't3','d', '+', 'x', 'p', 'h', 'star']]) +name_list = ['o', 's', 't', 't1', 't2', 't3', 'd', '+', 'x', 'p', 'h', 'star', + 'arrow_up', 'arrow_right', 'arrow_down', 'arrow_left'] +Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in name_list]) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) coords = { @@ -41,7 +44,11 @@ coords = { 'star': [(0, -0.5), (-0.1123, -0.1545), (-0.4755, -0.1545), (-0.1816, 0.059), (-0.2939, 0.4045), (0, 0.1910), (0.2939, 0.4045), (0.1816, 0.059), (0.4755, -0.1545), - (0.1123, -0.1545)] + (0.1123, -0.1545)], + 'arrow_down': [ + (-0.125, 0.125), (0, 0), (0.125, 0.125), + (0.05, 0.125), (0.05, 0.5), (-0.05, 0.5), (-0.05, 0.125) + ] } for k, c in coords.items(): Symbols[k].moveTo(*c[0]) @@ -51,7 +58,10 @@ for k, c in coords.items(): tr = QtGui.QTransform() tr.rotate(45) Symbols['x'] = tr.map(Symbols['+']) - +tr.rotate(45) +Symbols['arrow_right'] = tr.map(Symbols['arrow_down']) +Symbols['arrow_up'] = tr.map(Symbols['arrow_right']) +Symbols['arrow_left'] = tr.map(Symbols['arrow_up']) def drawSymbol(painter, symbol, size, pen, brush): if symbol is None: From 12a7c449f10b2c4fb07a3df2ddcc40d678e8fe99 Mon Sep 17 00:00:00 2001 From: Ogi Moore Date: Wed, 10 Jun 2020 23:31:39 -0700 Subject: [PATCH 607/607] Give ability to hide/show individual spots (#1245) Co-authored-by: dlidstrom --- pyqtgraph/graphicsItems/ScatterPlotItem.py | 38 ++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/pyqtgraph/graphicsItems/ScatterPlotItem.py b/pyqtgraph/graphicsItems/ScatterPlotItem.py index 1140c36f..5bbdffe7 100644 --- a/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -267,8 +267,8 @@ class ScatterPlotItem(GraphicsObject): self.picture = None # QPicture used for rendering when pxmode==False self.fragmentAtlas = SymbolAtlas() - - self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float)]) + + self.data = np.empty(0, dtype=[('x', float), ('y', float), ('size', float), ('symbol', object), ('pen', object), ('brush', object), ('data', object), ('item', object), ('sourceRect', object), ('targetRect', object), ('width', float), ('visible', bool)]) self.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots @@ -390,6 +390,7 @@ class ScatterPlotItem(GraphicsObject): newData = self.data[len(oldData):] newData['size'] = -1 ## indicates to use default size + newData['visible'] = True if 'spots' in kargs: spots = kargs['spots'] @@ -549,6 +550,28 @@ class ScatterPlotItem(GraphicsObject): if update: self.updateSpots(dataSet) + + def setPointsVisible(self, visible, update=True, dataSet=None, mask=None): + """Set whether or not each spot is visible. + If a list or array is provided, then the visibility for each spot will be set separately. + Otherwise, the argument will be used for all spots.""" + if dataSet is None: + dataSet = self.data + + if isinstance(visible, np.ndarray) or isinstance(visible, list): + visibilities = visible + if mask is not None: + visibilities = visibilities[mask] + if len(visibilities) != len(dataSet): + raise Exception("Number of visibilities does not match number of points (%d != %d)" % (len(visibilities), len(dataSet))) + dataSet['visible'] = visibilities + else: + dataSet['visible'] = visible + + dataSet['sourceRect'] = None + if update: + self.updateSpots(dataSet) + def setPointData(self, data, dataSet=None, mask=None): if dataSet is None: dataSet = self.data @@ -750,6 +773,8 @@ class ScatterPlotItem(GraphicsObject): (pts[0] - w < viewBounds.right()) & (pts[1] + w > viewBounds.top()) & (pts[1] - w < viewBounds.bottom())) ## remove out of view points + + mask &= self.data['visible'] return mask @debug.warnOnException ## raising an exception here causes crash @@ -975,6 +1000,15 @@ class SpotItem(object): self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) self.updateItem() + + def isVisible(self): + return self._data['visible'] + + def setVisible(self, visible): + """Set whether or not this spot is visible.""" + self._data['visible'] = visible + self.updateItem() + def setData(self, data): """Set the user-data associated with this spot""" self._data['data'] = data