From 5ef40d957d59417f437c7d62ee66e2ab72a96a12 Mon Sep 17 00:00:00 2001 From: Eric Lacosse <eric.lacosse@tuebingen.mpg.de> Date: Tue, 24 Jan 2017 15:27:11 +0100 Subject: [PATCH] newest pyqtgraph added. Tested with python2 and 3 --- vviewer/ColorMapItem.py | 2 +- vviewer/SliceBox.py | 12 +- .../pyqtgraph/GraphicsScene/GraphicsScene.py | 17 +- .../pyqtgraph/GraphicsScene/exportDialog.py | 9 +- .../exportDialogTemplate_pyqt5.py | 64 ++ .../pyqtgraph/GraphicsScene/mouseEvents.py | 2 - vviewer/pyqtgraph/Qt.py | 156 ++++- vviewer/pyqtgraph/SRTTransform.py | 2 + vviewer/pyqtgraph/WidgetGroup.py | 32 +- vviewer/pyqtgraph/__init__.py | 31 +- vviewer/pyqtgraph/canvas/Canvas.py | 10 +- vviewer/pyqtgraph/canvas/CanvasItem.py | 8 +- vviewer/pyqtgraph/canvas/CanvasTemplate.ui | 2 +- .../pyqtgraph/canvas/CanvasTemplate_pyqt.py | 35 +- .../pyqtgraph/canvas/CanvasTemplate_pyqt5.py | 86 +++ .../pyqtgraph/canvas/CanvasTemplate_pyside.py | 30 +- .../canvas/TransformGuiTemplate_pyqt.py | 7 +- .../canvas/TransformGuiTemplate_pyqt5.py | 55 ++ .../canvas/TransformGuiTemplate_pyside.py | 6 +- vviewer/pyqtgraph/colormap.py | 6 +- vviewer/pyqtgraph/configfile.py | 7 +- vviewer/pyqtgraph/console/Console.py | 18 +- vviewer/pyqtgraph/console/template_pyqt5.py | 107 +++ vviewer/pyqtgraph/debug.py | 101 +-- vviewer/pyqtgraph/dockarea/Dock.py | 18 +- vviewer/pyqtgraph/dockarea/DockArea.py | 52 +- vviewer/pyqtgraph/exporters/Exporter.py | 2 +- vviewer/pyqtgraph/exporters/SVGExporter.py | 35 - vviewer/pyqtgraph/exporters/tests/__init__.py | 0 vviewer/pyqtgraph/exporters/tests/test_csv.py | 18 +- vviewer/pyqtgraph/exporters/tests/test_svg.py | 28 +- vviewer/pyqtgraph/flowchart/Flowchart.py | 32 +- .../flowchart/FlowchartCtrlTemplate_pyqt5.py | 67 ++ .../flowchart/FlowchartGraphicsView.py | 70 +- .../flowchart/FlowchartTemplate_pyqt5.py | 55 ++ vviewer/pyqtgraph/flowchart/Node.py | 3 +- vviewer/pyqtgraph/flowchart/Terminal.py | 85 +-- vviewer/pyqtgraph/flowchart/eq.py | 36 - .../pyqtgraph/flowchart/library/Filters.py | 13 +- .../pyqtgraph/flowchart/library/functions.py | 2 + vviewer/pyqtgraph/functions.py | 496 +++++++++----- vviewer/pyqtgraph/graphicsItems/CurvePoint.py | 5 + .../graphicsItems/FillBetweenItem.py | 34 +- .../graphicsItems/GradientEditorItem.py | 55 +- .../pyqtgraph/graphicsItems/GraphicsItem.py | 4 - .../graphicsItems/HistogramLUTItem.py | 14 +- vviewer/pyqtgraph/graphicsItems/ImageItem.py | 205 ++++-- .../pyqtgraph/graphicsItems/InfiniteLine.py | 357 +++++++--- .../pyqtgraph/graphicsItems/IsocurveItem.py | 36 +- .../pyqtgraph/graphicsItems/PlotCurveItem.py | 8 + .../pyqtgraph/graphicsItems/PlotDataItem.py | 11 +- .../graphicsItems/PlotItem/PlotItem.py | 79 +-- .../PlotItem/plotConfigTemplate_pyqt5.py | 169 +++++ vviewer/pyqtgraph/graphicsItems/ROI.py | 333 +++++---- .../graphicsItems/ScatterPlotItem.py | 286 ++++---- vviewer/pyqtgraph/graphicsItems/TextItem.py | 185 +++-- .../graphicsItems/ViewBox/ViewBox.py | 42 +- .../graphicsItems/ViewBox/ViewBoxMenu.py | 12 +- .../ViewBox/axisCtrlTemplate_pyqt.py | 2 +- .../ViewBox/axisCtrlTemplate_pyqt5.py | 89 +++ .../ViewBox/tests/test_ViewBox.py | 28 +- .../graphicsItems/tests/test_ImageItem.py | 147 ++++ .../graphicsItems/tests/test_InfiniteLine.py | 96 +++ .../graphicsItems/tests/test_PlotCurveItem.py | 34 + .../pyqtgraph/graphicsItems/tests/test_ROI.py | 224 +++++++ .../tests/test_ScatterPlotItem.py | 12 +- vviewer/pyqtgraph/imageview/ImageView.py | 102 ++- .../imageview/ImageViewTemplate_pyqt.py | 2 +- .../imageview/ImageViewTemplate_pyqt5.py | 156 +++++ .../imageview/tests/test_imageview.py | 1 + vviewer/pyqtgraph/metaarray/MetaArray.py | 31 +- .../pyqtgraph/multiprocess/parallelizer.py | 2 + vviewer/pyqtgraph/multiprocess/processes.py | 17 +- vviewer/pyqtgraph/multiprocess/remoteproxy.py | 35 +- vviewer/pyqtgraph/opengl/GLGraphicsItem.py | 6 +- vviewer/pyqtgraph/opengl/GLViewWidget.py | 19 +- vviewer/pyqtgraph/opengl/MeshData.py | 4 +- vviewer/pyqtgraph/parametertree/Parameter.py | 5 +- .../pyqtgraph/parametertree/SystemSolver.py | 2 +- .../pyqtgraph/parametertree/parameterTypes.py | 42 +- .../tests/test_parametertypes.py | 4 +- vviewer/pyqtgraph/pixmaps/__init__.py | 1 + vviewer/pyqtgraph/pixmaps/auto.png | Bin 0 -> 1022 bytes vviewer/pyqtgraph/pixmaps/ctrl.png | Bin 0 -> 934 bytes vviewer/pyqtgraph/pixmaps/default.png | Bin 0 -> 810 bytes vviewer/pyqtgraph/pixmaps/icons.svg | 135 ++++ vviewer/pyqtgraph/pixmaps/lock.png | Bin 0 -> 913 bytes vviewer/pyqtgraph/python2_3.py | 18 +- vviewer/pyqtgraph/tests/__init__.py | 2 + vviewer/pyqtgraph/tests/image_testing.py | 634 ++++++++++++++++++ vviewer/pyqtgraph/tests/test_exit_crash.py | 14 +- vviewer/pyqtgraph/tests/test_functions.py | 249 ++++++- vviewer/pyqtgraph/tests/test_qt.py | 6 +- vviewer/pyqtgraph/tests/test_ref_cycles.py | 11 + vviewer/pyqtgraph/tests/test_stability.py | 12 +- vviewer/pyqtgraph/tests/ui_testing.py | 75 +++ vviewer/pyqtgraph/util/cprint.py | 1 + vviewer/pyqtgraph/util/garbage_collector.py | 2 +- vviewer/pyqtgraph/widgets/ComboBox.py | 5 +- vviewer/pyqtgraph/widgets/GraphicsView.py | 6 +- vviewer/pyqtgraph/widgets/MatplotlibWidget.py | 18 +- vviewer/pyqtgraph/widgets/PlotWidget.py | 2 +- vviewer/pyqtgraph/widgets/RawImageWidget.py | 5 +- .../pyqtgraph/widgets/RemoteGraphicsView.py | 11 + .../pyqtgraph/widgets/ScatterPlotWidget.py | 9 +- vviewer/pyqtgraph/widgets/SpinBox.py | 449 ++++++++----- vviewer/pyqtgraph/widgets/TableWidget.py | 38 +- vviewer/pyqtgraph/widgets/TreeWidget.py | 6 +- .../pyqtgraph/widgets/tests/test_spinbox.py | 28 + 109 files changed, 4827 insertions(+), 1622 deletions(-) create mode 100644 vviewer/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.py create mode 100644 vviewer/pyqtgraph/canvas/CanvasTemplate_pyqt5.py create mode 100644 vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py create mode 100644 vviewer/pyqtgraph/console/template_pyqt5.py create mode 100644 vviewer/pyqtgraph/exporters/tests/__init__.py create mode 100644 vviewer/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py create mode 100644 vviewer/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py delete mode 100644 vviewer/pyqtgraph/flowchart/eq.py create mode 100644 vviewer/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py create mode 100644 vviewer/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py create mode 100644 vviewer/pyqtgraph/graphicsItems/tests/test_ImageItem.py create mode 100644 vviewer/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py create mode 100644 vviewer/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py create mode 100644 vviewer/pyqtgraph/graphicsItems/tests/test_ROI.py create mode 100644 vviewer/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py create mode 100644 vviewer/pyqtgraph/pixmaps/auto.png create mode 100644 vviewer/pyqtgraph/pixmaps/ctrl.png create mode 100644 vviewer/pyqtgraph/pixmaps/default.png create mode 100644 vviewer/pyqtgraph/pixmaps/icons.svg create mode 100644 vviewer/pyqtgraph/pixmaps/lock.png create mode 100644 vviewer/pyqtgraph/tests/__init__.py create mode 100644 vviewer/pyqtgraph/tests/image_testing.py create mode 100644 vviewer/pyqtgraph/tests/ui_testing.py create mode 100644 vviewer/pyqtgraph/widgets/tests/test_spinbox.py diff --git a/vviewer/ColorMapItem.py b/vviewer/ColorMapItem.py index 9dfe69d..476072c 100644 --- a/vviewer/ColorMapItem.py +++ b/vviewer/ColorMapItem.py @@ -10,7 +10,7 @@ from pyqtgraph import GraphicsWidget from pyqtgraph.widgets import SpinBox from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.colormap import ColorMap -#from ..python2_3 import cmp +from pyqtgraph.python2_3 import cmp __all__ = ['TickSliderItem', 'ColorMapItem'] diff --git a/vviewer/SliceBox.py b/vviewer/SliceBox.py index 81ee23a..7087661 100644 --- a/vviewer/SliceBox.py +++ b/vviewer/SliceBox.py @@ -255,7 +255,7 @@ class SliceBox(pg.ViewBox): Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis. If view is None, the axis is left unlinked. """ - if isinstance(view, basestring): + if isinstance(view, str): if view == '': view = None else: @@ -282,7 +282,7 @@ class SliceBox(pg.ViewBox): pass - if view is None or isinstance(view, basestring): + if view is None or isinstance(view, str): self.state['linkedViews'][axis] = view else: self.state['linkedViews'][axis] = weakref.ref(view) @@ -312,7 +312,7 @@ class SliceBox(pg.ViewBox): Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis. If view is None, the axis is left unlinked. """ - if isinstance(view, basestring): + if isinstance(view, str): if view == '': view = None else: @@ -341,7 +341,7 @@ class SliceBox(pg.ViewBox): ## This can occur if the view has been deleted already pass - if view is None or isinstance(view, basestring): + if view is None or isinstance(view, str): self.state['linkedViews'][own_axis] = view else: self.state['linkedViews'][own_axis] = weakref.ref(view) @@ -376,7 +376,7 @@ class SliceBox(pg.ViewBox): ## Return the linked view for axis *ax*. ## this method _always_ returns either a ViewBox or None. v = self.state['linkedViews'][ax] - if v is None or isinstance(v, basestring): + if v is None or isinstance(v, str): return None else: return v() ## dereference weakref pointer. If the reference is dead, this returns None @@ -498,7 +498,7 @@ class SliceBox(pg.ViewBox): for v in state['linkedViews']: if isinstance(v, weakref.ref): v = v() - if v is None or isinstance(v, basestring): + if v is None or isinstance(v, str): views.append(v) else: views.append(v.name) diff --git a/vviewer/pyqtgraph/GraphicsScene/GraphicsScene.py b/vviewer/pyqtgraph/GraphicsScene/GraphicsScene.py index 6f5354d..952a241 100644 --- a/vviewer/pyqtgraph/GraphicsScene/GraphicsScene.py +++ b/vviewer/pyqtgraph/GraphicsScene/GraphicsScene.py @@ -1,12 +1,13 @@ -from ..Qt import QtCore, QtGui -from ..python2_3 import sortList 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 from .mouseEvents import * from .. import debug as debug + if hasattr(QtCore, 'PYQT_VERSION'): try: import sip @@ -97,6 +98,7 @@ class GraphicsScene(QtGui.QGraphicsScene): self.lastDrag = None self.hoverItems = weakref.WeakKeyDictionary() self.lastHoverEvent = None + self.minDragTime = 0.5 # drags shorter than 0.5 sec are interpreted as clicks self.contextMenu = [QtGui.QAction("Export...", self)] self.contextMenu[0].triggered.connect(self.showExportDialog) @@ -133,7 +135,6 @@ class GraphicsScene(QtGui.QGraphicsScene): self._moveDistance = d def mousePressEvent(self, ev): - #print 'scenePress' QtGui.QGraphicsScene.mousePressEvent(self, ev) if self.mouseGrabberItem() is None: ## nobody claimed press; we are free to generate drag/click events if self.lastHoverEvent is not None: @@ -171,8 +172,8 @@ class GraphicsScene(QtGui.QGraphicsScene): 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.screenPos() - cev.screenPos()) - if dist.length() < self._moveDistance and now - cev.time() < 0.5: + 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)) @@ -185,10 +186,8 @@ class GraphicsScene(QtGui.QGraphicsScene): def leaveEvent(self, ev): ## inform items that mouse is gone if len(self.dragButtons) == 0: self.sendHoverEvents(ev, exitOnly=True) - def mouseReleaseEvent(self, ev): - #print 'sceneRelease' if self.mouseGrabberItem() is None: if ev.button() in self.dragButtons: if self.sendDragEvent(ev, final=True): @@ -231,8 +230,6 @@ class GraphicsScene(QtGui.QGraphicsScene): prevItems = list(self.hoverItems.keys()) - #print "hover prev items:", prevItems - #print "hover test items:", items for item in items: if hasattr(item, 'hoverEvent'): event.currentItem = item @@ -247,7 +244,7 @@ class GraphicsScene(QtGui.QGraphicsScene): item.hoverEvent(event) except: debug.printExc("Error sending hover event:") - + event.enter = False event.exit = True #print "hover exit items:", prevItems diff --git a/vviewer/pyqtgraph/GraphicsScene/exportDialog.py b/vviewer/pyqtgraph/GraphicsScene/exportDialog.py index 5efb7c4..2676a3b 100644 --- a/vviewer/pyqtgraph/GraphicsScene/exportDialog.py +++ b/vviewer/pyqtgraph/GraphicsScene/exportDialog.py @@ -1,4 +1,4 @@ -from ..Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, USE_PYSIDE, 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_PYQT5: + from . import exportDialogTemplate_pyqt5 as exportDialogTemplate else: from . import exportDialogTemplate_pyqt as exportDialogTemplate @@ -137,5 +139,6 @@ class ExportDialog(QtGui.QWidget): self.selectBox.setVisible(False) self.setVisible(False) - - + def closeEvent(self, event): + self.close() + QtGui.QWidget.closeEvent(self, event) diff --git a/vviewer/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.py b/vviewer/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.py new file mode 100644 index 0000000..418fd0f --- /dev/null +++ b/vviewer/pyqtgraph/GraphicsScene/exportDialogTemplate_pyqt5.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 PyQt5 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/vviewer/pyqtgraph/GraphicsScene/mouseEvents.py b/vviewer/pyqtgraph/GraphicsScene/mouseEvents.py index 2e472e0..fb9d368 100644 --- a/vviewer/pyqtgraph/GraphicsScene/mouseEvents.py +++ b/vviewer/pyqtgraph/GraphicsScene/mouseEvents.py @@ -276,8 +276,6 @@ class HoverEvent(object): self._modifiers = moveEvent.modifiers() else: self.exit = True - - def isEnter(self): """Returns True if the mouse has just entered the item's shape""" diff --git a/vviewer/pyqtgraph/Qt.py b/vviewer/pyqtgraph/Qt.py index efbe66c..2ed9d6f 100644 --- a/vviewer/pyqtgraph/Qt.py +++ b/vviewer/pyqtgraph/Qt.py @@ -4,37 +4,58 @@ This module exists to smooth out some of the differences between PySide and PyQt * Automatically import either PyQt4 or PySide depending on availability * Allow to import QtCore/QtGui pyqtgraph.Qt without specifying which Qt wrapper you want to use. -* Declare QtCore.Signal, .Slot in PyQt4 +* Declare QtCore.Signal, .Slot in PyQt4 * Declare loadUiType function for Pyside """ -import sys, re +import os, sys, re, time from .python2_3 import asUnicode -## Automatically determine whether to use PyQt or PySide. +PYSIDE = 'PySide' +PYQT4 = 'PyQt4' +PYQT5 = 'PyQt5' + +QT_LIB = os.getenv('PYQTGRAPH_QT_LIB') + +## Automatically determine whether to use PyQt or PySide (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 'PyQt4' in sys.modules: - USE_PYSIDE = False -elif 'PySide' in sys.modules: - USE_PYSIDE = True -else: - try: - import PyQt4 - USE_PYSIDE = False - except ImportError: +if QT_LIB is None: + libOrder = [PYQT4, PYSIDE, PYQT5] + + for lib in libOrder: + if lib in sys.modules: + QT_LIB = lib + break + +if QT_LIB is None: + for lib in libOrder: try: - import PySide - USE_PYSIDE = True + __import__(lib) + QT_LIB = lib + break except ImportError: - raise Exception("PyQtGraph requires either PyQt4 or PySide; neither package could be imported.") + pass + +if QT_LIB is None: + raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") -if USE_PYSIDE: +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: + QtGui.QApplication.processEvents() + QtTest.QTest.qWait = qWait + except ImportError: pass import PySide @@ -59,7 +80,7 @@ if USE_PYSIDE: # Make a loadUiType function like PyQt has - # Credit: + # Credit: # http://stackoverflow.com/questions/4442286/python-code-genration-with-pyside-uic/14195313#14195313 class StringIO(object): @@ -75,7 +96,15 @@ if USE_PYSIDE: 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. + 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 @@ -98,9 +127,9 @@ if USE_PYSIDE: base_class = eval('QtGui.%s'%widget_class) return form_class, base_class - - -else: + +elif QT_LIB == PYQT4: + from PyQt4 import QtGui, QtCore, uic try: from PyQt4 import QtSvg @@ -115,21 +144,98 @@ else: except ImportError: pass + 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 + try: + from PyQt5 import QtSvg + except ImportError: + pass + try: + from PyQt5 import QtOpenGL + except ImportError: + pass + try: + from PyQt5 import QtTest + QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed + except ImportError: + pass + + # 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 __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 = 'PyQt5 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR + +else: + raise ValueError("Invalid Qt lib '%s'" % QT_LIB) + +# Common to PyQt4 and 5 +if QT_LIB.startswith('PyQt'): import sip def isQObjectAlive(obj): return not sip.isdeleted(obj) loadUiType = uic.loadUiType QtCore.Signal = QtCore.pyqtSignal - VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR - + + ## Make sure we have Qt >= 4.7 versionReq = [4, 7] -QtVersion = PySide.QtCore.__version__ if USE_PYSIDE else QtCore.QT_VERSION_STR +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 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)) - diff --git a/vviewer/pyqtgraph/SRTTransform.py b/vviewer/pyqtgraph/SRTTransform.py index 2328134..b1aea29 100644 --- a/vviewer/pyqtgraph/SRTTransform.py +++ b/vviewer/pyqtgraph/SRTTransform.py @@ -3,6 +3,7 @@ from .Qt import QtCore, QtGui from .Point import Point import numpy as np + class SRTTransform(QtGui.QTransform): """Transform that can always be represented as a combination of 3 matrices: scale * rotate * translate This transform has no shear; angles are always preserved. @@ -165,6 +166,7 @@ class SRTTransform(QtGui.QTransform): def matrix(self): return np.array([[self.m11(), self.m12(), self.m13()],[self.m21(), self.m22(), self.m23()],[self.m31(), self.m32(), self.m33()]]) + if __name__ == '__main__': from . import widgets diff --git a/vviewer/pyqtgraph/WidgetGroup.py b/vviewer/pyqtgraph/WidgetGroup.py index 2954145..d7e265c 100644 --- a/vviewer/pyqtgraph/WidgetGroup.py +++ b/vviewer/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 +from .Qt import QtCore, QtGui, USE_PYQT5 import weakref, inspect from .python2_3 import asUnicode @@ -60,9 +60,13 @@ def setComboState(w, v): class WidgetGroup(QtCore.QObject): - """This class takes a list of widgets and keeps an internal record of their state which is always up to date. Allows reading and writing from groups of widgets simultaneously.""" + """This class takes a list of widgets and keeps an internal record of their + state that is always up to date. - ## List of widget types which can be handled by WidgetGroup. + Allows reading and writing from groups of widgets simultaneously. + """ + + ## List of widget types that can be handled by WidgetGroup. ## The value for each type is a tuple (change signal function, get function, set function, [auto-add children]) ## The change signal function that takes an object and returns a signal that is emitted any time the state of the widget changes, not just ## when it is changed by user interaction. (for example, 'clicked' is not a valid signal here) @@ -200,51 +204,35 @@ class WidgetGroup(QtCore.QObject): if hasattr(obj, 'widgetGroupInterface'): return True return False - #return (type(obj) in WidgetGroup.classes) def setScale(self, widget, scale): val = self.readWidget(widget) self.scales[widget] = scale self.setWidget(widget, val) - #print "scaling %f to %f" % (val, self.readWidget(widget)) - def mkChangeCallback(self, w): return lambda *args: self.widgetChanged(w, *args) def widgetChanged(self, w, *args): - #print "widget changed" n = self.widgetList[w] v1 = self.cache[n] v2 = self.readWidget(w) if v1 != v2: - #print "widget", n, " = ", v2 - self.emit(QtCore.SIGNAL('changed'), self.widgetList[w], v2) + if not USE_PYQT5: + # Old signal kept for backward compatibility. + self.emit(QtCore.SIGNAL('changed'), self.widgetList[w], v2) self.sigChanged.emit(self.widgetList[w], v2) def state(self): for w in self.uncachedWidgets: self.readWidget(w) - - #cc = self.cache.copy() - #if 'averageGroup' in cc: - #val = cc['averageGroup'] - #w = self.findWidget('averageGroup') - #self.readWidget(w) - #if val != self.cache['averageGroup']: - #print " AverageGroup did not match cached value!" - #else: - #print " AverageGroup OK" return self.cache.copy() def setState(self, s): - #print "SET STATE", self, s for w in self.widgetList: n = self.widgetList[w] - #print " restore %s?" % n if n not in s: continue - #print " restore state", w, n, s[n] self.setWidget(w, s[n]) def readWidget(self, w): diff --git a/vviewer/pyqtgraph/__init__.py b/vviewer/pyqtgraph/__init__.py index 1c152d4..bc5081f 100644 --- a/vviewer/pyqtgraph/__init__.py +++ b/vviewer/pyqtgraph/__init__.py @@ -4,7 +4,7 @@ PyQtGraph - Scientific Graphics and GUI Library for Python www.pyqtgraph.org """ -__version__ = '0.9.10' +__version__ = '0.10.0' ### import all the goodies and add some helper functions for easy CLI use @@ -41,13 +41,15 @@ elif 'darwin' in sys.platform: ## openGL can have a major impact on mac, but als useOpenGL = False if QtGui.QApplication.instance() is not None: print('Warning: QApplication was created before pyqtgraph was imported; there may be problems (to avoid bugs, call QApplication.setGraphicsSystem("raster") before the QApplication is created).') - QtGui.QApplication.setGraphicsSystem('raster') ## work around a variety of bugs in the native graphics system + if QtGui.QApplication.setGraphicsSystem: + QtGui.QApplication.setGraphicsSystem('raster') ## work around a variety of bugs in the native graphics system else: useOpenGL = False ## on windows there's a more even performance / bugginess tradeoff. CONFIG_OPTIONS = { 'useOpenGL': useOpenGL, ## by default, this is platform-dependent (see widgets/GraphicsView). Set to True or False to explicitly enable/disable opengl. 'leftButtonPan': True, ## if false, left button drags a rubber band for zooming in viewbox + # foreground/background take any arguments to the 'mkColor' in /pyqtgraph/functions.py 'foreground': 'd', ## default foreground color for axes, labels, etc. 'background': 'k', ## default background for GraphicsWidget 'antialias': False, @@ -57,16 +59,32 @@ CONFIG_OPTIONS = { 'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide 'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code) 'crashWarning': False, # If True, print warnings about situations that may result in a crash + 'imageAxisOrder': 'col-major', # For 'row-major', image data is expected in the standard (row, col) order. + # For 'col-major', image data is expected in reversed (col, row) order. + # The default is 'col-major' for backward compatibility, but this may + # change in the future. } 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'): + raise ValueError('imageAxisOrder must be either "row-major" or "col-major"') CONFIG_OPTIONS[opt] = value def setConfigOptions(**opts): - CONFIG_OPTIONS.update(opts) + """Set global configuration options. + + Each keyword argument sets one global option. + """ + for k,v in opts.items(): + setConfigOption(k, v) def getConfigOption(opt): + """Return the value of a single global configuration option. + """ return CONFIG_OPTIONS[opt] @@ -285,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() @@ -345,7 +366,7 @@ def exit(): ## close file handles if sys.platform == 'darwin': - for fd in xrange(3, 4096): + for fd in range(3, 4096): if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac. os.close(fd) else: diff --git a/vviewer/pyqtgraph/canvas/Canvas.py b/vviewer/pyqtgraph/canvas/Canvas.py index 4de891f..5b5ce2f 100644 --- a/vviewer/pyqtgraph/canvas/Canvas.py +++ b/vviewer/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/vviewer/pyqtgraph/canvas/CanvasItem.py b/vviewer/pyqtgraph/canvas/CanvasItem.py index b6ecbb3..a06235b 100644 --- a/vviewer/pyqtgraph/canvas/CanvasItem.py +++ b/vviewer/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/vviewer/pyqtgraph/canvas/CanvasTemplate.ui b/vviewer/pyqtgraph/canvas/CanvasTemplate.ui index 9bea8f8..b05c11c 100644 --- a/vviewer/pyqtgraph/canvas/CanvasTemplate.ui +++ b/vviewer/pyqtgraph/canvas/CanvasTemplate.ui @@ -127,7 +127,7 @@ <customwidget> <class>CanvasCombo</class> <extends>QComboBox</extends> - <header>CanvasManager</header> + <header>.CanvasManager</header> </customwidget> </customwidgets> <resources/> diff --git a/vviewer/pyqtgraph/canvas/CanvasTemplate_pyqt.py b/vviewer/pyqtgraph/canvas/CanvasTemplate_pyqt.py index 557354e..b65ef46 100644 --- a/vviewer/pyqtgraph/canvas/CanvasTemplate_pyqt.py +++ b/vviewer/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/vviewer/pyqtgraph/canvas/CanvasTemplate_pyqt5.py b/vviewer/pyqtgraph/canvas/CanvasTemplate_pyqt5.py new file mode 100644 index 0000000..20f5e33 --- /dev/null +++ b/vviewer/pyqtgraph/canvas/CanvasTemplate_pyqt5.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' +# +# Created by: PyQt5 UI code generator 5.5.1 +# +# 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(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.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): + _translate = QtCore.QCoreApplication.translate + Form.setWindowTitle(_translate("Form", "Form")) + 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/vviewer/pyqtgraph/canvas/CanvasTemplate_pyside.py b/vviewer/pyqtgraph/canvas/CanvasTemplate_pyside.py index 56d1ff4..b0e05a0 100644 --- a/vviewer/pyqtgraph/canvas/CanvasTemplate_pyside.py +++ b/vviewer/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/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py b/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py index 75c694c..c6cf82e 100644 --- a/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyqt.py +++ b/vviewer/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/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py b/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py new file mode 100644 index 0000000..6b1f239 --- /dev/null +++ b/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyqt5.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'pyqtgraph/canvas/TransformGuiTemplate.ui' +# +# Created by: PyQt5 UI code generator 5.5.1 +# +# 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(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.setContentsMargins(0, 0, 0, 0) + self.verticalLayout.setSpacing(1) + 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/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyside.py b/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyside.py index bce7b51..e430b61 100644 --- a/vviewer/pyqtgraph/canvas/TransformGuiTemplate_pyside.py +++ b/vviewer/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/vviewer/pyqtgraph/colormap.py b/vviewer/pyqtgraph/colormap.py index c003370..f943e2f 100644 --- a/vviewer/pyqtgraph/colormap.py +++ b/vviewer/pyqtgraph/colormap.py @@ -1,5 +1,7 @@ import numpy as np from .Qt import QtGui, QtCore +from .python2_3 import basestring + class ColorMap(object): """ @@ -64,7 +66,9 @@ class ColorMap(object): =============== ============================================================== """ self.pos = np.array(pos) - self.color = np.array(color) + order = np.argsort(self.pos) + self.pos = self.pos[order] + self.color = np.array(color)[order] if mode is None: mode = np.ones(len(pos)) self.mode = mode diff --git a/vviewer/pyqtgraph/configfile.py b/vviewer/pyqtgraph/configfile.py index c095bba..7b20db1 100644 --- a/vviewer/pyqtgraph/configfile.py +++ b/vviewer/pyqtgraph/configfile.py @@ -10,14 +10,15 @@ as it can be converted to/from a string using repr and eval. """ import re, os, sys +import numpy from .pgcollections import OrderedDict -GLOBAL_PATH = None # so not thread safe. from . import units -from .python2_3 import asUnicode +from .python2_3 import asUnicode, basestring from .Qt import QtCore from .Point import Point from .colormap import ColorMap -import numpy +GLOBAL_PATH = None # so not thread safe. + class ParseError(Exception): def __init__(self, message, lineNum, line, fileName=None): diff --git a/vviewer/pyqtgraph/console/Console.py b/vviewer/pyqtgraph/console/Console.py index 896de92..ed4b7f0 100644 --- a/vviewer/pyqtgraph/console/Console.py +++ b/vviewer/pyqtgraph/console/Console.py @@ -1,14 +1,17 @@ - -from ..Qt import QtCore, QtGui, USE_PYSIDE import sys, re, os, time, traceback, subprocess +import pickle + +from ..Qt import QtCore, QtGui, USE_PYSIDE, 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_PYQT5: + from . import template_pyqt5 as template else: from . import template_pyqt as template - -from .. import exceptionHandling as exceptionHandling -import pickle -from .. import getConfigOption + class ConsoleWidget(QtGui.QWidget): """ @@ -45,6 +48,7 @@ class ConsoleWidget(QtGui.QWidget): QtGui.QWidget.__init__(self, parent) if namespace is None: namespace = {} + namespace['__console__'] = self self.localNamespace = namespace self.editor = editor self.multiline = None @@ -131,7 +135,7 @@ class ConsoleWidget(QtGui.QWidget): if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): return self.currentFrame().tb_frame.f_globals else: - return globals() + return self.localNamespace def locals(self): frame = self.currentFrame() diff --git a/vviewer/pyqtgraph/console/template_pyqt5.py b/vviewer/pyqtgraph/console/template_pyqt5.py new file mode 100644 index 0000000..1fbc5be --- /dev/null +++ b/vviewer/pyqtgraph/console/template_pyqt5.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 PyQt5 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/vviewer/pyqtgraph/debug.py b/vviewer/pyqtgraph/debug.py index 57c71bc..0da24d7 100644 --- a/vviewer/pyqtgraph/debug.py +++ b/vviewer/pyqtgraph/debug.py @@ -83,8 +83,9 @@ class Tracer(object): funcname = cls.__name__ + "." + funcname return "%s: %s %s: %s" % (callline, filename, lineno, funcname) + def warnOnException(func): - """Decorator which catches/ignores exceptions and prints a stack trace.""" + """Decorator that catches/ignores exceptions and prints a stack trace.""" def w(*args, **kwds): try: func(*args, **kwds) @@ -92,11 +93,9 @@ def warnOnException(func): printExc('Ignored exception:') return w + def getExc(indent=4, prefix='| ', skip=1): - lines = (traceback.format_stack()[:-skip] - + [" ---- exception caught ---->\n"] - + traceback.format_tb(sys.exc_info()[2]) - + traceback.format_exception_only(*sys.exc_info()[:2])) + lines = formatException(*sys.exc_info(), skip=skip) lines2 = [] for l in lines: lines2.extend(l.strip('\n').split('\n')) @@ -112,6 +111,7 @@ def printExc(msg='', indent=4, prefix='|'): print(" "*indent + prefix + '='*30 + '>>') print(exc) print(" "*indent + prefix + '='*30 + '<<') + def printTrace(msg='', indent=4, prefix='|'): """Print an error message followed by an indented stack trace""" @@ -126,7 +126,30 @@ def printTrace(msg='', indent=4, prefix='|'): def backtrace(skip=0): return ''.join(traceback.format_stack()[:-(skip+1)]) + + +def formatException(exctype, value, tb, skip=0): + """Return a list of formatted exception strings. + + Similar to traceback.format_exception, but displays the entire stack trace + rather than just the portion downstream of the point where the exception is + caught. In particular, unhandled exceptions that occur during Qt signal + handling do not usually show the portion of the stack that emitted the + signal. + """ + lines = traceback.format_exception(exctype, value, tb) + lines = [lines[0]] + traceback.format_stack()[:-(skip+1)] + [' --- exception caught here ---\n'] + lines[1:] + return lines + + +def printException(exctype, value, traceback): + """Print an exception with its full traceback. + Set `sys.excepthook = printException` to ensure that exceptions caught + inside Qt signal handlers are printed with their full stack trace. + """ + print(''.join(formatException(exctype, value, traceback, skip=1))) + def listObjs(regex='Q', typ=None): """List all objects managed by python gc with class name matching regex. @@ -723,7 +746,6 @@ class ObjTracker(object): for k in self.startCount: c1[k] = c1.get(k, 0) - self.startCount[k] typs = list(c1.keys()) - #typs.sort(lambda a,b: cmp(c1[a], c1[b])) typs.sort(key=lambda a: c1[a]) for t in typs: if c1[t] == 0: @@ -824,7 +846,6 @@ class ObjTracker(object): c = count.get(typ, [0,0]) count[typ] = [c[0]+1, c[1]+objectSize(obj)] typs = list(count.keys()) - #typs.sort(lambda a,b: cmp(count[a][1], count[b][1])) typs.sort(key=lambda a: count[a][1]) for t in typs: @@ -1097,46 +1118,44 @@ def pretty(data, indent=''): return ret -class PeriodicTrace(object): +class ThreadTrace(object): """ Used to debug freezing by starting a new thread that reports on the - location of the main thread periodically. + location of other threads periodically. """ - class ReportThread(QtCore.QThread): - def __init__(self): - self.frame = None - self.ind = 0 - self.lastInd = None - self.lock = Mutex() - QtCore.QThread.__init__(self) - - def notify(self, frame): - with self.lock: - self.frame = frame - self.ind += 1 - - def run(self): - while True: - time.sleep(1) - with self.lock: - if self.lastInd != self.ind: - print("== Trace %d: ==" % self.ind) - traceback.print_stack(self.frame) - self.lastInd = self.ind + def __init__(self, interval=10.0): + self.interval = interval + self.lock = Mutex() + self._stop = False + self.start() - def __init__(self): - self.mainThread = threading.current_thread() - self.thread = PeriodicTrace.ReportThread() + def stop(self): + with self.lock: + self._stop = True + + def start(self, interval=None): + if interval is not None: + self.interval = interval + self._stop = False + self.thread = threading.Thread(target=self.run) + self.thread.daemon = True self.thread.start() - sys.settrace(self.trace) - - def trace(self, frame, event, arg): - if threading.current_thread() is self.mainThread: # and 'threading' not in frame.f_code.co_filename: - self.thread.notify(frame) - # print("== Trace ==", event, arg) - # traceback.print_stack(frame) - return self.trace + def run(self): + while True: + with self.lock: + if self._stop is True: + return + + print("\n============= THREAD FRAMES: ================") + for id, frame in sys._current_frames().items(): + if id == threading.current_thread().ident: + continue + print("<< thread %d >>" % id) + traceback.print_stack(frame) + print("===============================================\n") + + time.sleep(self.interval) class ThreadColor(object): diff --git a/vviewer/pyqtgraph/dockarea/Dock.py b/vviewer/pyqtgraph/dockarea/Dock.py index 28d4244..4493d07 100644 --- a/vviewer/pyqtgraph/dockarea/Dock.py +++ b/vviewer/pyqtgraph/dockarea/Dock.py @@ -7,10 +7,13 @@ 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) + self._container = None + self._name = name self.area = area self.label = DockLabel(name, self, closable) if closable: @@ -126,6 +129,18 @@ class Dock(QtGui.QWidget, DockDrop): self.labelHidden = False self.allowedAreas.add('center') self.updateStyle() + + def title(self): + """ + Gets the text displayed in the title bar for this dock. + """ + return asUnicode(self.label.text()) + + def setTitle(self, text): + """ + Sets the text displayed in title bar for this Dock. + """ + self.label.setText(text) def setOrientation(self, o='auto', force=False): """ @@ -170,7 +185,7 @@ class Dock(QtGui.QWidget, DockDrop): self.resizeOverlay(self.size()) def name(self): - return asUnicode(self.label.text()) + return self._name def container(self): return self._container @@ -223,6 +238,7 @@ class Dock(QtGui.QWidget, DockDrop): self.label.setParent(None) self._container.apoptose() self._container = None + self.sigClosed.emit(self) def __repr__(self): return "<Dock %s %s>" % (self.name(), self.stretch()) diff --git a/vviewer/pyqtgraph/dockarea/DockArea.py b/vviewer/pyqtgraph/dockarea/DockArea.py index a75d881..ffe75b6 100644 --- a/vviewer/pyqtgraph/dockarea/DockArea.py +++ b/vviewer/pyqtgraph/dockarea/DockArea.py @@ -1,17 +1,11 @@ # -*- coding: utf-8 -*- +import weakref from ..Qt import QtCore, QtGui from .Container import * from .DockDrop import * from .Dock import Dock from .. import debug as debug -import weakref - -## TODO: -# - containers should be drop areas, not docks. (but every slot within a container must have its own drop areas?) -# - drop between tabs -# - nest splitters inside tab boxes, etc. - - +from ..python2_3 import basestring class DockArea(Container, QtGui.QWidget, DockDrop): @@ -102,9 +96,12 @@ class DockArea(Container, QtGui.QWidget, DockDrop): 'below': 'after' }[position] #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() return dock @@ -112,12 +109,10 @@ class DockArea(Container, QtGui.QWidget, DockDrop): """ Move an existing Dock to a new location. """ - old = dock.container() ## Moving to the edge of a tabbed dock causes a drop outside the tab box if position in ['left', 'right', 'top', 'bottom'] and neighbor is not None and neighbor.container() is not None and neighbor.container().type() == 'tab': neighbor = neighbor.container() self.addDock(dock, position, neighbor) - old.apoptose() def getContainer(self, obj): if obj is None: @@ -171,8 +166,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop): if self.home is None: area = DockArea(temporary=True, home=self) self.tempAreas.append(area) - win = QtGui.QMainWindow() - win.setCentralWidget(area) + win = TempAreaWindow(area) area.win = win win.show() else: @@ -196,7 +190,13 @@ class DockArea(Container, QtGui.QWidget, DockDrop): """ Return a serialized (storable) representation of the state of all Docks in this DockArea.""" - state = {'main': self.childState(self.topContainer), 'float': []} + + if self.topContainer is None: + main = None + else: + main = self.childState(self.topContainer) + + state = {'main': main, 'float': []} for a in self.tempAreas: geo = a.win.geometry() geo = (geo.x(), geo.y(), geo.width(), geo.height()) @@ -228,7 +228,8 @@ class DockArea(Container, QtGui.QWidget, DockDrop): #print "found docks:", docks ## 2) create container structure, move docks into new containers - self.buildFromState(state['main'], docks, self) + if state['main'] is not None: + self.buildFromState(state['main'], docks, self) ## 3) create floating areas, populate for s in state['float']: @@ -296,10 +297,16 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def apoptose(self): #print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count() - if self.temporary and self.topContainer.count() == 0: + if self.topContainer.count() == 0: self.topContainer = None - self.home.removeTempArea(self) - #self.close() + if self.temporary: + self.home.removeTempArea(self) + #self.close() + + def clear(self): + docks = self.findAll()[1] + for dock in docks.values(): + dock.close() ## PySide bug: We need to explicitly redefine these methods ## or else drag/drop events will not be delivered. @@ -315,5 +322,12 @@ class DockArea(Container, QtGui.QWidget, DockDrop): def dropEvent(self, *args): DockDrop.dropEvent(self, *args) - - + +class TempAreaWindow(QtGui.QMainWindow): + def __init__(self, area, **kwargs): + QtGui.QMainWindow.__init__(self, **kwargs) + self.setCentralWidget(area) + + def closeEvent(self, *args, **kwargs): + self.centralWidget().clear() + QtGui.QMainWindow.closeEvent(self, *args, **kwargs) diff --git a/vviewer/pyqtgraph/exporters/Exporter.py b/vviewer/pyqtgraph/exporters/Exporter.py index 64a2529..792e36b 100644 --- a/vviewer/pyqtgraph/exporters/Exporter.py +++ b/vviewer/pyqtgraph/exporters/Exporter.py @@ -1,6 +1,6 @@ from ..widgets.FileDialog import FileDialog from ..Qt import QtGui, QtCore, QtSvg -from ..python2_3 import asUnicode +from ..python2_3 import asUnicode, basestring from ..GraphicsScene import GraphicsScene import os, re LastExportDirectory = None diff --git a/vviewer/pyqtgraph/exporters/SVGExporter.py b/vviewer/pyqtgraph/exporters/SVGExporter.py index a91466c..ccf9216 100644 --- a/vviewer/pyqtgraph/exporters/SVGExporter.py +++ b/vviewer/pyqtgraph/exporters/SVGExporter.py @@ -45,41 +45,6 @@ class SVGExporter(Exporter): if toBytes is False and copy is False and fileName is None: self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)") return - #self.svg = QtSvg.QSvgGenerator() - #self.svg.setFileName(fileName) - #dpi = QtGui.QDesktopWidget().physicalDpiX() - ### not really sure why this works, but it seems to be important: - #self.svg.setSize(QtCore.QSize(self.params['width']*dpi/90., self.params['height']*dpi/90.)) - #self.svg.setResolution(dpi) - ##self.svg.setViewBox() - #targetRect = QtCore.QRect(0, 0, self.params['width'], self.params['height']) - #sourceRect = self.getSourceRect() - - #painter = QtGui.QPainter(self.svg) - #try: - #self.setExportMode(True) - #self.render(painter, QtCore.QRectF(targetRect), sourceRect) - #finally: - #self.setExportMode(False) - #painter.end() - - ## Workaround to set pen widths correctly - #data = open(fileName).readlines() - #for i in range(len(data)): - #line = data[i] - #m = re.match(r'(<g .*)stroke-width="1"(.*transform="matrix\(([^\)]+)\)".*)', line) - #if m is not None: - ##print "Matched group:", line - #g = m.groups() - #matrix = list(map(float, g[2].split(','))) - ##print "matrix:", matrix - #scale = max(abs(matrix[0]), abs(matrix[3])) - #if scale == 0 or scale == 1.0: - #continue - #data[i] = g[0] + ' stroke-width="%0.2g" ' % (1.0/scale) + g[1] + '\n' - ##print "old line:", line - ##print "new line:", data[i] - #open(fileName, 'w').write(''.join(data)) ## Qt's SVG generator is not complete. (notably, it lacks clipping) ## Instead, we will use Qt to generate SVG for each item independently, diff --git a/vviewer/pyqtgraph/exporters/tests/__init__.py b/vviewer/pyqtgraph/exporters/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/vviewer/pyqtgraph/exporters/tests/test_csv.py b/vviewer/pyqtgraph/exporters/tests/test_csv.py index a98372e..15c6626 100644 --- a/vviewer/pyqtgraph/exporters/tests/test_csv.py +++ b/vviewer/pyqtgraph/exporters/tests/test_csv.py @@ -1,16 +1,23 @@ """ SVG export test """ +from __future__ import division, print_function, absolute_import import pyqtgraph as pg -import pyqtgraph.exporters import csv +import os +import tempfile app = pg.mkQApp() + def approxeq(a, b): return (a-b) <= ((a + b) * 1e-6) + def test_CSVExporter(): + tempfilename = tempfile.NamedTemporaryFile(suffix='.csv').name + print("using %s as a temporary file" % tempfilename) + plt = pg.plot() y1 = [1,3,2,3,1,6,9,8,4,2] plt.plot(y=y1, name='myPlot') @@ -24,9 +31,9 @@ def test_CSVExporter(): plt.plot(x=x3, y=y3, stepMode=True) ex = pg.exporters.CSVExporter(plt.plotItem) - ex.export(fileName='test.csv') + ex.export(fileName=tempfilename) - r = csv.reader(open('test.csv', 'r')) + r = csv.reader(open(tempfilename, 'r')) lines = [line for line in r] header = lines.pop(0) assert header == ['myPlot_x', 'myPlot_y', 'x0001', 'y0001', 'x0002', 'y0002'] @@ -43,7 +50,8 @@ def test_CSVExporter(): assert (i >= len(x3) and vals[4] == '') or approxeq(float(vals[4]), x3[i]) assert (i >= len(y3) and vals[5] == '') or approxeq(float(vals[5]), y3[i]) i += 1 - + + os.unlink(tempfilename) + if __name__ == '__main__': test_CSVExporter() - \ No newline at end of file diff --git a/vviewer/pyqtgraph/exporters/tests/test_svg.py b/vviewer/pyqtgraph/exporters/tests/test_svg.py index 871f43c..2261f7d 100644 --- a/vviewer/pyqtgraph/exporters/tests/test_svg.py +++ b/vviewer/pyqtgraph/exporters/tests/test_svg.py @@ -1,11 +1,18 @@ """ SVG export test """ +from __future__ import division, print_function, absolute_import import pyqtgraph as pg -import pyqtgraph.exporters +import tempfile +import os + + app = pg.mkQApp() + def test_plotscene(): + tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name + print("using %s as a temporary file" % tempfilename) pg.setConfigOption('foreground', (0,0,0)) w = pg.GraphicsWindow() w.show() @@ -18,10 +25,13 @@ def test_plotscene(): app.processEvents() ex = pg.exporters.SVGExporter(w.scene()) - ex.export(fileName='test.svg') - + ex.export(fileName=tempfilename) + # clean up after the test is done + os.unlink(tempfilename) def test_simple(): + tempfilename = tempfile.NamedTemporaryFile(suffix='.svg').name + print("using %s as a temporary file" % tempfilename) scene = pg.QtGui.QGraphicsScene() #rect = pg.QtGui.QGraphicsRectItem(0, 0, 100, 100) #scene.addItem(rect) @@ -51,17 +61,17 @@ def test_simple(): #el = pg.QtGui.QGraphicsEllipseItem(0, 0, 100, 50) #el.translate(10,-5) #el.scale(0.5,2) + #el.setParentItem(rect2) - + grp2 = pg.ItemGroup() scene.addItem(grp2) grp2.scale(100,100) - + rect3 = pg.QtGui.QGraphicsRectItem(0,0,2,2) rect3.setPen(pg.mkPen(width=1, cosmetic=False)) grp2.addItem(rect3) - - ex = pg.exporters.SVGExporter(scene) - ex.export(fileName='test.svg') - + ex = pg.exporters.SVGExporter(scene) + ex.export(fileName=tempfilename) + os.unlink(tempfilename) diff --git a/vviewer/pyqtgraph/flowchart/Flowchart.py b/vviewer/pyqtgraph/flowchart/Flowchart.py index ab5f4a8..b623f5c 100644 --- a/vviewer/pyqtgraph/flowchart/Flowchart.py +++ b/vviewer/pyqtgraph/flowchart/Flowchart.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from ..Qt import QtCore, QtGui, USE_PYSIDE +from ..Qt import QtCore, QtGui, USE_PYSIDE, 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_PYQT5: + from . import FlowchartTemplate_pyqt5 as FlowchartTemplate + from . import FlowchartCtrlTemplate_pyqt5 as FlowchartCtrlTemplate else: from . import FlowchartTemplate_pyqt as FlowchartTemplate from . import FlowchartCtrlTemplate_pyqt as FlowchartCtrlTemplate @@ -349,7 +352,6 @@ class Flowchart(Node): #tdeps[t] = lastNode if lastInd is not None: dels.append((lastInd+1, t)) - #dels.sort(lambda a,b: cmp(b[0], a[0])) dels.sort(key=lambda a: a[0], reverse=True) for i, t in dels: ops.insert(i, ('d', t)) @@ -379,22 +381,22 @@ class Flowchart(Node): terms = set(startNode.outputs().values()) #print "======= Updating", startNode - #print "Order:", order + # print("Order:", order) for node in order[1:]: - #print "Processing node", node + # print("Processing node", node) + update = False for term in list(node.inputs().values()): - #print " checking terminal", term + # print(" checking terminal", term) deps = list(term.connections().keys()) - update = False for d in deps: if d in terms: - #print " ..input", d, "changed" - update = True + # print(" ..input", d, "changed") + update |= True term.inputChanged(d, process=False) - if update: - #print " processing.." - node.update() - terms |= set(node.outputs().values()) + if update: + # print(" processing..") + node.update() + terms |= set(node.outputs().values()) finally: self.processing = False @@ -464,7 +466,6 @@ class Flowchart(Node): self.clear() Node.restoreState(self, state) nodes = state['nodes'] - #nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0])) nodes.sort(key=lambda a: a['pos'][0]) for n in nodes: if n['name'] in self._nodes: @@ -619,7 +620,10 @@ class FlowchartCtrlWidget(QtGui.QWidget): self.cwWin.resize(1000,800) h = self.ui.ctrlList.header() - h.setResizeMode(0, h.Stretch) + if not USE_PYQT5: + h.setResizeMode(0, h.Stretch) + else: + h.setSectionResizeMode(0, h.Stretch) self.ui.ctrlList.itemChanged.connect(self.itemChanged) self.ui.loadBtn.clicked.connect(self.loadClicked) diff --git a/vviewer/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py b/vviewer/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.py new file mode 100644 index 0000000..b661918 --- /dev/null +++ b/vviewer/pyqtgraph/flowchart/FlowchartCtrlTemplate_pyqt5.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 PyQt5 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/vviewer/pyqtgraph/flowchart/FlowchartGraphicsView.py b/vviewer/pyqtgraph/flowchart/FlowchartGraphicsView.py index ab4b291..9301121 100644 --- a/vviewer/pyqtgraph/flowchart/FlowchartGraphicsView.py +++ b/vviewer/pyqtgraph/flowchart/FlowchartGraphicsView.py @@ -4,72 +4,27 @@ from ..widgets.GraphicsView import GraphicsView from ..GraphicsScene import GraphicsScene from ..graphicsItems.ViewBox import ViewBox -#class FlowchartGraphicsView(QtGui.QGraphicsView): + class FlowchartGraphicsView(GraphicsView): sigHoverOver = QtCore.Signal(object) sigClicked = QtCore.Signal(object) def __init__(self, widget, *args): - #QtGui.QGraphicsView.__init__(self, *args) GraphicsView.__init__(self, *args, useOpenGL=False) - #self.setBackgroundBrush(QtGui.QBrush(QtGui.QColor(255,255,255))) self._vb = FlowchartViewBox(widget, lockAspect=True, invertY=True) self.setCentralItem(self._vb) - #self.scene().addItem(self.vb) - #self.setMouseTracking(True) - #self.lastPos = None - #self.setTransformationAnchor(self.AnchorViewCenter) - #self.setRenderHints(QtGui.QPainter.Antialiasing) self.setRenderHint(QtGui.QPainter.Antialiasing, True) - #self.setDragMode(QtGui.QGraphicsView.RubberBandDrag) - #self.setRubberBandSelectionMode(QtCore.Qt.ContainsItemBoundingRect) def viewBox(self): return self._vb - - #def mousePressEvent(self, ev): - #self.moved = False - #self.lastPos = ev.pos() - #return QtGui.QGraphicsView.mousePressEvent(self, ev) - - #def mouseMoveEvent(self, ev): - #self.moved = True - #callSuper = False - #if ev.buttons() & QtCore.Qt.RightButton: - #if self.lastPos is not None: - #dif = ev.pos() - self.lastPos - #self.scale(1.01**-dif.y(), 1.01**-dif.y()) - #elif ev.buttons() & QtCore.Qt.MidButton: - #if self.lastPos is not None: - #dif = ev.pos() - self.lastPos - #self.translate(dif.x(), -dif.y()) - #else: - ##self.emit(QtCore.SIGNAL('hoverOver'), self.items(ev.pos())) - #self.sigHoverOver.emit(self.items(ev.pos())) - #callSuper = True - #self.lastPos = ev.pos() - - #if callSuper: - #QtGui.QGraphicsView.mouseMoveEvent(self, ev) - - #def mouseReleaseEvent(self, ev): - #if not self.moved: - ##self.emit(QtCore.SIGNAL('clicked'), ev) - #self.sigClicked.emit(ev) - #return QtGui.QGraphicsView.mouseReleaseEvent(self, ev) class FlowchartViewBox(ViewBox): def __init__(self, widget, *args, **kwargs): ViewBox.__init__(self, *args, **kwargs) self.widget = widget - #self.menu = None - #self._subMenus = None ## need a place to store the menus otherwise they dissappear (even though they've been added to other menus) ((yes, it doesn't make sense)) - - - def getMenu(self, ev): ## called by ViewBox to create a new context menu @@ -84,26 +39,3 @@ class FlowchartViewBox(ViewBox): menu = self.widget.buildMenu(ev.scenePos()) menu.setTitle("Add node") return [menu, ViewBox.getMenu(self, ev)] - - - - - - - - - - -##class FlowchartGraphicsScene(QtGui.QGraphicsScene): -#class FlowchartGraphicsScene(GraphicsScene): - - #sigContextMenuEvent = QtCore.Signal(object) - - #def __init__(self, *args): - ##QtGui.QGraphicsScene.__init__(self, *args) - #GraphicsScene.__init__(self, *args) - - #def mouseClickEvent(self, ev): - ##QtGui.QGraphicsScene.contextMenuEvent(self, ev) - #if not ev.button() in [QtCore.Qt.RightButton]: - #self.sigContextMenuEvent.emit(ev) \ No newline at end of file diff --git a/vviewer/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py b/vviewer/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.py new file mode 100644 index 0000000..ba75430 --- /dev/null +++ b/vviewer/pyqtgraph/flowchart/FlowchartTemplate_pyqt5.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 PyQt5 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/vviewer/pyqtgraph/flowchart/Node.py b/vviewer/pyqtgraph/flowchart/Node.py index fc7b04d..c450a9f 100644 --- a/vviewer/pyqtgraph/flowchart/Node.py +++ b/vviewer/pyqtgraph/flowchart/Node.py @@ -6,7 +6,6 @@ from .Terminal import * from ..pgcollections import OrderedDict from ..debug import * import numpy as np -from .eq import * def strDict(d): @@ -261,7 +260,7 @@ class Node(QtCore.QObject): for k, v in args.items(): term = self._inputs[k] oldVal = term.value() - if not eq(oldVal, v): + if not fn.eq(oldVal, v): changed = True term.setValue(v, process=False) if changed and '_updatesHandled_' not in args: diff --git a/vviewer/pyqtgraph/flowchart/Terminal.py b/vviewer/pyqtgraph/flowchart/Terminal.py index 6a6db62..016e2d3 100644 --- a/vviewer/pyqtgraph/flowchart/Terminal.py +++ b/vviewer/pyqtgraph/flowchart/Terminal.py @@ -4,8 +4,7 @@ import weakref from ..graphicsItems.GraphicsObject import GraphicsObject from .. import functions as fn from ..Point import Point -#from PySide import QtCore, QtGui -from .eq import * + class Terminal(object): def __init__(self, node, name, io, optional=False, multi=False, pos=None, renamable=False, removable=False, multiable=False, bypass=None): @@ -29,9 +28,6 @@ class Terminal(object): ============== ================================================================================= """ self._io = io - #self._isOutput = opts[0] in ['out', 'io'] - #self._isInput = opts[0]] in ['in', 'io'] - #self._isIO = opts[0]=='io' self._optional = optional self._multi = multi self._node = weakref.ref(node) @@ -68,7 +64,7 @@ class Terminal(object): """If this is a single-value terminal, val should be a single value. If this is a multi-value terminal, val should be a dict of terminal:value pairs""" if not self.isMultiValue(): - if eq(val, self._value): + if fn.eq(val, self._value): return self._value = val else: @@ -81,11 +77,6 @@ class Terminal(object): if self.isInput() and process: self.node().update() - ## Let the flowchart handle this. - #if self.isOutput(): - #for c in self.connections(): - #if c.isInput(): - #c.inputChanged(self) self.recolor() def setOpts(self, **opts): @@ -94,7 +85,6 @@ class Terminal(object): self._multiable = opts.get('multiable', self._multiable) if 'multi' in opts: self.setMultiValue(opts['multi']) - def connected(self, term): """Called whenever this terminal has been connected to another. (note--this function is called on both terminals)""" @@ -109,12 +99,10 @@ class Terminal(object): if self.isMultiValue() and term in self._value: del self._value[term] self.node().update() - #self.recolor() else: if self.isInput(): self.setValue(None) self.node().disconnected(self, term) - #self.node().update() def inputChanged(self, term, process=True): """Called whenever there is a change to the input value to this terminal. @@ -178,7 +166,6 @@ class Terminal(object): return term in self.connections() def hasInput(self): - #conn = self.extendedConnections() for t in self.connections(): if t.isOutput(): return True @@ -186,17 +173,10 @@ class Terminal(object): def inputTerminals(self): """Return the terminal(s) that give input to this one.""" - #terms = self.extendedConnections() - #for t in terms: - #if t.isOutput(): - #return t return [t for t in self.connections() if t.isOutput()] - def dependentNodes(self): """Return the list of nodes which receive input from this terminal.""" - #conn = self.extendedConnections() - #del conn[self] return set([t.node() for t in self.connections() if t.isInput()]) def connectTo(self, term, connectionItem=None): @@ -210,12 +190,6 @@ class Terminal(object): for t in [self, term]: if t.isInput() and not t._multi and len(t.connections()) > 0: raise Exception("Cannot connect %s <-> %s: Terminal %s is already connected to %s (and does not allow multiple connections)" % (self, term, t, list(t.connections().keys()))) - #if self.hasInput() and term.hasInput(): - #raise Exception('Target terminal already has input') - - #if term in self.node().terminals.values(): - #if self.isOutput() or term.isOutput(): - #raise Exception('Can not connect an output back to the same node.') except: if connectionItem is not None: connectionItem.close() @@ -223,18 +197,12 @@ class Terminal(object): if connectionItem is None: connectionItem = ConnectionItem(self.graphicsItem(), term.graphicsItem()) - #self.graphicsItem().scene().addItem(connectionItem) self.graphicsItem().getViewBox().addItem(connectionItem) - #connectionItem.setParentItem(self.graphicsItem().parent().parent()) self._connections[term] = connectionItem term._connections[self] = connectionItem self.recolor() - #if self.isOutput() and term.isInput(): - #term.inputChanged(self) - #if term.isInput() and term.isOutput(): - #self.inputChanged(term) self.connected(term) term.connected(self) @@ -244,8 +212,6 @@ class Terminal(object): if not self.connectedTo(term): return item = self._connections[term] - #print "removing connection", item - #item.scene().removeItem(item) item.close() del self._connections[term] del term._connections[self] @@ -254,10 +220,6 @@ class Terminal(object): self.disconnected(term) term.disconnected(self) - #if self.isOutput() and term.isInput(): - #term.inputChanged(self) - #if term.isInput() and term.isOutput(): - #self.inputChanged(term) def disconnectAll(self): @@ -270,7 +232,7 @@ class Terminal(object): color = QtGui.QColor(0,0,0) elif self.isInput() and not self.hasInput(): ## input terminal with no connected output terminals color = QtGui.QColor(200,200,0) - elif self._value is None or eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error) + elif self._value is None or fn.eq(self._value, {}): ## terminal is connected but has no data (possibly due to processing error) color = QtGui.QColor(255,255,255) elif self.valueIsAcceptable() is None: ## terminal has data, but it is unknown if the data is ok color = QtGui.QColor(200, 200, 0) @@ -283,7 +245,6 @@ class Terminal(object): if recurse: for t in self.connections(): t.recolor(color, recurse=False) - def rename(self, name): oldName = self._name @@ -294,17 +255,6 @@ class Terminal(object): def __repr__(self): return "<Terminal %s.%s>" % (str(self.node().name()), str(self.name())) - #def extendedConnections(self, terms=None): - #"""Return list of terminals (including this one) that are directly or indirectly wired to this.""" - #if terms is None: - #terms = {} - #terms[self] = None - #for t in self._connections: - #if t in terms: - #continue - #terms.update(t.extendedConnections(terms)) - #return terms - def __hash__(self): return id(self) @@ -318,18 +268,15 @@ class Terminal(object): return {'io': self._io, 'multi': self._multi, 'optional': self._optional, 'renamable': self._renamable, 'removable': self._removable, 'multiable': self._multiable} -#class TerminalGraphicsItem(QtGui.QGraphicsItem): class TerminalGraphicsItem(GraphicsObject): def __init__(self, term, parent=None): self.term = term - #QtGui.QGraphicsItem.__init__(self, parent) GraphicsObject.__init__(self, parent) self.brush = fn.mkBrush(0,0,0) self.box = QtGui.QGraphicsRectItem(0, 0, 10, 10, self) self.label = QtGui.QGraphicsTextItem(self.term.name(), self) self.label.scale(0.7, 0.7) - #self.setAcceptHoverEvents(True) self.newConnection = None self.setFiltersChildEvents(True) ## to pick up mouse events on the rectitem if self.term.isRenamable(): @@ -338,7 +285,6 @@ class TerminalGraphicsItem(GraphicsObject): self.label.keyPressEvent = self.labelKeyPress self.setZValue(1) self.menu = None - def labelFocusOut(self, ev): QtGui.QGraphicsTextItem.focusOutEvent(self.label, ev) @@ -471,8 +417,6 @@ class TerminalGraphicsItem(GraphicsObject): break if not gotTarget: - #print "remove unused connection" - #self.scene().removeItem(self.newConnection) self.newConnection.close() self.newConnection = None else: @@ -488,12 +432,6 @@ class TerminalGraphicsItem(GraphicsObject): self.box.setBrush(self.brush) self.update() - #def hoverEnterEvent(self, ev): - #self.hover = True - - #def hoverLeaveEvent(self, ev): - #self.hover = False - def connectPoint(self): ## return the connect position of this terminal in view coords return self.mapToView(self.mapFromItem(self.box, self.box.boundingRect().center())) @@ -503,11 +441,9 @@ class TerminalGraphicsItem(GraphicsObject): item.updateLine() -#class ConnectionItem(QtGui.QGraphicsItem): class ConnectionItem(GraphicsObject): def __init__(self, source, target=None): - #QtGui.QGraphicsItem.__init__(self) GraphicsObject.__init__(self) self.setFlags( self.ItemIsSelectable | @@ -528,14 +464,12 @@ class ConnectionItem(GraphicsObject): 'selectedColor': (200, 200, 0), 'selectedWidth': 3.0, } - #self.line = QtGui.QGraphicsLineItem(self) self.source.getViewBox().addItem(self) self.updateLine() self.setZValue(0) def close(self): if self.scene() is not None: - #self.scene().removeItem(self.line) self.scene().removeItem(self) def setTarget(self, target): @@ -575,8 +509,11 @@ class ConnectionItem(GraphicsObject): return path def keyPressEvent(self, ev): + if not self.isSelected(): + ev.ignore() + return + if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace: - #if isinstance(self.target, TerminalGraphicsItem): self.source.disconnect(self.target) ev.accept() else: @@ -590,6 +527,7 @@ class ConnectionItem(GraphicsObject): ev.accept() sel = self.isSelected() self.setSelected(True) + self.setFocus() if not sel and self.isSelected(): self.update() @@ -600,12 +538,9 @@ class ConnectionItem(GraphicsObject): self.hovered = False self.update() - def boundingRect(self): return self.shape().boundingRect() - ##return self.line.boundingRect() - #px = self.pixelWidth() - #return QtCore.QRectF(-5*px, 0, 10*px, self.length) + def viewRangeChanged(self): self.shapePath = None self.prepareGeometryChange() @@ -628,7 +563,5 @@ class ConnectionItem(GraphicsObject): p.setPen(fn.mkPen(self.style['hoverColor'], width=self.style['hoverWidth'])) else: p.setPen(fn.mkPen(self.style['color'], width=self.style['width'])) - - #p.drawLine(0, 0, 0, self.length) p.drawPath(self.path) diff --git a/vviewer/pyqtgraph/flowchart/eq.py b/vviewer/pyqtgraph/flowchart/eq.py deleted file mode 100644 index 554989b..0000000 --- a/vviewer/pyqtgraph/flowchart/eq.py +++ /dev/null @@ -1,36 +0,0 @@ -# -*- coding: utf-8 -*- -from numpy import ndarray, bool_ -from ..metaarray import MetaArray - -def eq(a, b): - """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" - if a is b: - return True - - try: - e = a==b - except ValueError: - return False - except AttributeError: - return False - except: - print("a:", str(type(a)), str(a)) - print("b:", str(type(b)), str(b)) - raise - t = type(e) - if t is bool: - return e - elif t is bool_: - return bool(e) - elif isinstance(e, ndarray) or (hasattr(e, 'implements') and e.implements('MetaArray')): - try: ## disaster: if a is an empty array and b is not, then e.all() is True - if a.shape != b.shape: - return False - except: - return False - if (hasattr(e, 'implements') and e.implements('MetaArray')): - return e.asarray().all() - else: - return e.all() - else: - raise Exception("== operator returned type %s" % str(type(e))) diff --git a/vviewer/pyqtgraph/flowchart/library/Filters.py b/vviewer/pyqtgraph/flowchart/library/Filters.py index 88a2f6c..9392b03 100644 --- a/vviewer/pyqtgraph/flowchart/library/Filters.py +++ b/vviewer/pyqtgraph/flowchart/library/Filters.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- +import numpy as np from ...Qt import QtCore, QtGui from ..Node import Node from . import functions from ... import functions as pgfn from .common import * -import numpy as np - +from ...python2_3 import xrange from ... import PolyLineROI from ... import Point from ... import metaarray as metaarray @@ -164,8 +164,15 @@ class Gaussian(CtrlNode): import scipy.ndimage except ImportError: raise Exception("GaussianFilter node requires the package scipy.ndimage.") - return pgfn.gaussianFilter(data, self.ctrls['sigma'].value()) + 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""" diff --git a/vviewer/pyqtgraph/flowchart/library/functions.py b/vviewer/pyqtgraph/flowchart/library/functions.py index 338d25c..cb7fb41 100644 --- a/vviewer/pyqtgraph/flowchart/library/functions.py +++ b/vviewer/pyqtgraph/flowchart/library/functions.py @@ -1,5 +1,7 @@ import numpy as np from ...metaarray import MetaArray +from ...python2_3 import basestring, xrange + def downsample(data, n, axis=0, xvals='subsample'): """Downsample by averaging points together across axis. diff --git a/vviewer/pyqtgraph/functions.py b/vviewer/pyqtgraph/functions.py index 33a0fba..080dbc5 100644 --- a/vviewer/pyqtgraph/functions.py +++ b/vviewer/pyqtgraph/functions.py @@ -6,8 +6,18 @@ Distributed under MIT/X11 license. See license.txt for more infomation. """ from __future__ import division -from .python2_3 import asUnicode +import warnings +import numpy as np +import decimal, re +import ctypes +import sys, struct +from .python2_3 import asUnicode, basestring from .Qt import QtGui, QtCore, USE_PYSIDE +from . import getConfigOption, setConfigOptions +from . import debug + + + Colors = { 'b': QtGui.QColor(0,0,255,255), 'g': QtGui.QColor(0,255,0,255), @@ -24,18 +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<number>[+-]?((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)\s*((?P<siPrefix>[u' + SI_PREFIXES + r']?)(?P<suffix>\w.*))?$') +INT_REGEX = re.compile(r'(?P<number>[+-]?\d+)\s*(?P<siPrefix>[u' + SI_PREFIXES + r']?)(?P<suffix>.*)$') -from .Qt import QtGui, QtCore, USE_PYSIDE -from . import getConfigOption, setConfigOptions -import numpy as np -import decimal, re -import ctypes -import sys, struct - -from . import debug - def siScale(x, minVal=1e-25, allowUnicode=True): """ Return the recommended scale factor and SI prefix string for x. @@ -74,6 +79,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. @@ -103,30 +109,55 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al 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 siEval(s): + +def siParse(s, regex=FLOAT_REGEX): + """Convert a value written in SI notation to a tuple (number, si_prefix, suffix). + + Example:: + + siParse('100 μV") # returns ('100', 'μ', 'V') + """ + s = asUnicode(s) + m = regex.match(s) + if m is None: + raise ValueError('Cannot parse number "%s"' % s) + 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 + + +def siEval(s, typ=float, regex=FLOAT_REGEX): """ - Convert a value written in SI notation to its equivalent prefixless value + 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): @@ -243,6 +274,7 @@ def mkBrush(*args, **kwds): color = args return QtGui.QBrush(mkColor(color)) + def mkPen(*args, **kargs): """ Convenience function for constructing QPen. @@ -292,6 +324,7 @@ def mkPen(*args, **kargs): pen.setDashPattern(dash) return pen + def hsvColor(hue, sat=1.0, val=1.0, alpha=1.0): """Generate a QColor from HSVa values. (all arguments are float 0.0-1.0)""" c = QtGui.QColor() @@ -303,10 +336,12 @@ def colorTuple(c): """Return a tuple (R,G,B,A) from a QColor""" return (c.red(), c.green(), c.blue(), c.alpha()) + def colorStr(c): """Generate a hex string code from a QColor""" 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): """ Creates a QColor from a single index. Useful for stepping through a predefined list of colors. @@ -331,6 +366,7 @@ def intColor(index, hues=9, values=1, maxValue=255, minValue=150, maxHue=360, mi c.setAlpha(alpha) return c + def glColor(*args, **kargs): """ Convert a color to OpenGL color format (r,g,b,a) floats 0.0-1.0 @@ -367,12 +403,80 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0) return path +def eq(a, b): + """The great missing equivalence function: Guaranteed evaluation to a single bool value.""" + 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: + return False + except: + print('failed to evaluate equivalence for:') + print(" a:", str(type(a)), str(a)) + print(" b:", str(type(b)), str(b)) + raise + t = type(e) + if t is bool: + return e + elif t is np.bool_: + return bool(e) + elif isinstance(e, np.ndarray) or (hasattr(e, 'implements') and e.implements('MetaArray')): + try: ## disaster: if a is an empty array and b is not, then e.all() is True + if a.shape != b.shape: + return False + except: + return False + if (hasattr(e, 'implements') and e.implements('MetaArray')): + return e.asarray().all() + else: + return e.all() + 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 <pyqtgraph.ROI.getArrayRegion>` @@ -411,49 +515,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) + 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.") - ## Build array of sample locations. - grid = np.mgrid[tuple([slice(0,x) for x in shape])] ## mesh grid of indexes - #print shape, grid.shape - x = (grid[np.newaxis,...] * vectors.transpose()[(Ellipsis,) + (np.newaxis,)*len(shape)]).sum(axis=1) ## magic - x += origin - #print "X values:" - #print x - ## iterate manually over unused axes since map_coordinates won't do it for us - if have_scipy: + # 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): @@ -462,9 +541,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 = [] @@ -481,18 +559,26 @@ 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 scipy.ndimage.map_coordinates. + 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. - *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. + (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. + ============== =========================================================================================== - Returns array of shape (x.shape[:-1] + data.shape) + Returns array of shape (x.shape[:-1] + data.shape[x.shape[-1]:]) For example, assume we have the following 2D image data:: @@ -535,58 +621,75 @@ 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: + 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,2),) * 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]] - #axisMask = mask.astype(np.ubyte).reshape((1,)*(fields.ndim-1) + mask.shape) - axisIndex[axisIndex < 0] = 0 - axisIndex[axisIndex >= data.shape[ax]] = 0 - fieldInds.append(axisIndex) - prof() + if order == 0: + 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 + 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) - # Get data values surrounding each requested point - # fieldData[..., i] contains all 2**nd values needed to interpolate x[i] - fieldData = data[tuple(fieldInds)] 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 totalMask.ndim > 0: + result[~totalMask] = default + else: + if totalMask is False: + result[:] = default - prof() - totalMask.shape = totalMask.shape + (1,) * (nd - md) - result[~totalMask] = default prof() return result @@ -773,12 +876,11 @@ def solveBilinearTransform(points1, points2): return matrix -def rescaleData(data, scale, offset, dtype=None): +def rescaleData(data, scale, offset, dtype=None, clip=None): """Return data rescaled and optionally cast to a new dtype:: data => (data-offset) * scale - Uses scipy.weave (if available) to improve performance. """ if dtype is None: dtype = data.dtype @@ -823,10 +925,21 @@ def rescaleData(data, scale, offset, dtype=None): setConfigOptions(useWeave=False) #p = np.poly1d([scale, -offset*scale]) - #data = p(data).astype(dtype) - d2 = data-offset - #d2 *= scale - d2 = np.multiply(d2, scale) + #d2 = p(data) + d2 = data - float(offset) + d2 *= scale + + # Clip before converting dtype to avoid overflow + if dtype.kind in 'ui': + lim = np.iinfo(dtype) + if clip is None: + # don't let rescale cause integer overflow + d2 = np.clip(d2, lim.min, lim.max) + else: + d2 = np.clip(d2, max(clip[0], lim.min), min(clip[1], lim.max)) + else: + if clip is not None: + d2 = np.clip(d2, *clip) data = d2.astype(dtype) return data @@ -848,7 +961,6 @@ def makeRGBA(*args, **kwds): kwds['useRGBA'] = True return makeARGB(*args, **kwds) - def myrescaleData(data, scale, maxi, mini): d2 = data-mini d2 = np.multiply(d2, (scale-2)/(maxi-mini)) + 1 @@ -941,7 +1053,7 @@ def mymakeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): if minVal == maxVal: maxVal += 1e-16 #newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=int) - newData[...,i] = myrescaleData(data[...,i], scale, maxVal, minVal) + newData[...,i] = myrescaleData(data[...,i], scale, maxVal, minVal) data = newData else: minVal, maxVal = levels @@ -1003,16 +1115,17 @@ def mymakeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): profile() return imgData, alpha - def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): """ - Convert an array of values into an ARGB array suitable for building QImages, OpenGL textures, etc. + Convert an array of values into an ARGB array suitable for building QImages, + OpenGL textures, etc. - Returns the ARGB array (values 0-255) and a boolean indicating whether there is alpha channel data. - This is a two stage process: + Returns the ARGB array (unsigned byte) and a boolean indicating whether + there is alpha channel data. This is a two stage process: 1) Rescale the data based on the values in the *levels* argument (min, max). - 2) Determine the final output by passing the rescaled values through a lookup table. + 2) Determine the final output by passing the rescaled values through a + lookup table. Both stages are optional. @@ -1031,55 +1144,70 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): channel). The use of this feature requires that levels.shape[0] == data.shape[-1]. scale The maximum value to which data will be rescaled before being passed through the lookup table (or returned if there is no lookup table). By default this will - be set to the length of the lookup table, or 256 is no lookup table is provided. - For OpenGL color specifications (as in GLColor4f) use scale=1.0 + be set to the length of the lookup table, or 255 if no lookup table is provided. lut Optional lookup table (array with dtype=ubyte). Values in data will be converted to color by indexing directly from lut. The output data shape will be input.shape + lut.shape[1:]. - - Note: the output of makeARGB will have the same dtype as the lookup table, so - for conversion to QImage, the dtype must be ubyte. - - Lookup tables can be built using GradientWidget. + Lookup tables can be built using ColorMap or GradientWidget. useRGBA If True, the data is returned in RGBA order (useful for building OpenGL textures). The default is False, which returns in ARGB order for use with QImage - (Note that 'ARGB' is a term used by the Qt documentation; the _actual_ order + (Note that 'ARGB' is a term used by the Qt documentation; the *actual* order is BGRA). ============== ================================================================================== """ 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: + raise TypeError("data.shape[2] must be <= 4") + if lut is not None and not isinstance(lut, np.ndarray): lut = np.array(lut) - if levels is not None and not isinstance(levels, np.ndarray): - levels = np.array(levels) - if levels is not None: - if levels.ndim == 1: - if len(levels) != 2: - raise Exception('levels argument must have length 2') - elif levels.ndim == 2: - if lut is not None and lut.ndim > 1: - raise Exception('Cannot make ARGB data when bot levels and lut have ndim > 2') - if levels.shape != (data.shape[-1], 2): - raise Exception('levels must have shape (data.shape[-1], 2)') + if levels is None: + # automatically decide levels based on data dtype + if data.dtype.kind == 'u': + levels = np.array([0, 2**(data.itemsize*8)-1]) + elif data.dtype.kind == 'i': + s = 2**(data.itemsize*8 - 1) + levels = np.array([-s, s-1]) + elif data.dtype.kind == 'b': + levels = np.array([0,1]) else: - print(levels) - raise Exception("levels argument must be 1D or 2D.") + raise Exception('levels argument is required for float input types') + if not isinstance(levels, np.ndarray): + levels = np.array(levels) + if levels.ndim == 1: + if levels.shape[0] != 2: + raise Exception('levels argument must have length 2') + elif levels.ndim == 2: + if lut is not None and lut.ndim > 1: + raise Exception('Cannot make ARGB data when both levels and lut have ndim > 2') + if levels.shape != (data.shape[-1], 2): + raise Exception('levels must have shape (data.shape[-1], 2)') + else: + raise Exception("levels argument must be 1D or 2D (got shape=%s)." % repr(levels.shape)) profile() + # Decide on maximum scaled value if scale is None: if lut is not None: - scale = lut.shape[0] + scale = lut.shape[0] - 1 else: scale = 255. - ## Apply levels if given - if levels is not None: + # Decide on the dtype we want after scaling + if lut is None: + dtype = np.ubyte + else: + dtype = np.min_scalar_type(lut.shape[0]-1) + # Apply levels if given + if levels is not None: if isinstance(levels, np.ndarray) and levels.ndim == 2: - ## we are going to rescale each channel independently + # we are going to rescale each channel independently if levels.shape[0] != data.shape[-1]: raise Exception("When rescaling multi-channel data, there must be the same number of levels as channels (data.shape[-1] == levels.shape[0])") newData = np.empty(data.shape, dtype=int) @@ -1087,24 +1215,20 @@ 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=int) + newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=dtype) data = newData else: + # Apply level scaling unless it would have no effect on the data minVal, maxVal = levels - if minVal == maxVal: - maxVal += 1e-16 - if maxVal == minVal: - data = rescaleData(data, 1, minVal, dtype=int) - else: - # print(maxVal) - # print(minVal) - # print(scale) - # print(scale/(maxVal-minVal)) - data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=int) + if minVal != 0 or maxVal != scale: + if minVal == maxVal: + maxVal += 1e-16 + data = rescaleData(data, scale/(maxVal-minVal), minVal, dtype=dtype) + profile() - ## apply LUT if given + # apply LUT if given if lut is not None: data = applyLookupTable(data, lut) else: @@ -1113,16 +1237,18 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): profile() - ## copy data into ARGB ordered array + # this will be the final image array imgData = np.empty(data.shape[:2]+(4,), dtype=np.ubyte) profile() + # decide channel order if useRGBA: - order = [0,1,2,3] ## array comes out RGBA + order = [0,1,2,3] # array comes out RGBA else: - order = [2,1,0,3] ## for some reason, the colors line up as BGR in the final image. + order = [2,1,0,3] # for some reason, the colors line up as BGR in the final image. + # copy data into image array if data.ndim == 2: # This is tempting: # imgData[..., :3] = data[..., np.newaxis] @@ -1138,6 +1264,7 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False): profile() + # add opaque alpha channel if needed if data.ndim == 2 or data.shape[2] == 3: alpha = False imgData[..., 3] = 255 @@ -1267,10 +1394,9 @@ def imageToArray(img, copy=False, transpose=True): # If this works on all platforms, then there is no need to use np.asarray.. arr = np.frombuffer(ptr, np.ubyte, img.byteCount()) + arr = arr.reshape(img.height(), img.width(), 4) if fmt == img.Format_RGB32: - arr = arr.reshape(img.height(), img.width(), 3) - elif fmt == img.Format_ARGB32 or fmt == img.Format_ARGB32_Premultiplied: - arr = arr.reshape(img.height(), img.width(), 4) + arr[...,3] = 255 if copy: arr = arr.copy() @@ -1465,22 +1591,17 @@ def arrayToQPath(x, y, connect='all'): arr[1:-1]['y'] = y # decide which points are connected by lines - if connect == 'pairs': - connect = np.empty((n/2,2), dtype=np.int32) - if connect.size != n: - raise Exception("x,y array lengths must be multiple of 2 to use connect='pairs'") - connect[:,0] = 1 - connect[:,1] = 0 - connect = connect.flatten() - if connect == 'finite': - connect = np.isfinite(x) & np.isfinite(y) - arr[1:-1]['c'] = connect - if connect == 'all': + if eq(connect, 'all'): arr[1:-1]['c'] = 1 + elif eq(connect, 'pairs'): + arr[1:-1]['c'][::2] = 1 + arr[1:-1]['c'][1::2] = 0 + elif eq(connect, 'finite'): + arr[1:-1]['c'] = np.isfinite(x) & np.isfinite(y) elif isinstance(connect, np.ndarray): arr[1:-1]['c'] = connect else: - raise Exception('connect argument must be "all", "pairs", or array') + raise Exception('connect argument must be "all", "pairs", "finite", or array') #profiler('fill array') # write last 0 @@ -1671,7 +1792,7 @@ def isocurve(data, level, connected=False, extendToEdge=False, path=False): #vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme vertIndex = i+2*j #print i,j,k," : ", fields[i,j,k], 2**vertIndex - index += fields[i,j] * 2**vertIndex + np.add(index, fields[i,j] * 2**vertIndex, out=index, casting='unsafe') #print index #print index @@ -1821,7 +1942,7 @@ def isosurface(data, level): See Paul Bourke, "Polygonising a Scalar Field" (http://paulbourke.net/geometry/polygonise/) - *data* 3D numpy array of scalar values + *data* 3D numpy array of scalar values. Must be contiguous. *level* The level at which to generate an isosurface Returns an array of vertex coordinates (Nv, 3) and an array of @@ -2173,7 +2294,10 @@ def isosurface(data, level): else: faceShiftTables, edgeShifts, edgeTable, nTableFaces = IsosurfaceDataCache - + # We use strides below, which means we need contiguous array input. + # Ideally we can fix this just by removing the dependency on strides. + if not data.flags['C_CONTIGUOUS']: + raise TypeError("isosurface input data must be c-contiguous.") ## mark everything below the isosurface level mask = data < level @@ -2187,7 +2311,7 @@ def isosurface(data, level): for k in [0,1]: fields[i,j,k] = mask[slices[i], slices[j], slices[k]] vertIndex = i - 2*j*i + 3*j + 4*k ## this is just to match Bourk's vertex numbering scheme - index += fields[i,j,k] * 2**vertIndex + np.add(index, fields[i,j,k] * 2**vertIndex, out=index, casting='unsafe') ### Generate table of edges that have been cut cutEdges = np.zeros([x+1 for x in index.shape]+[3], dtype=np.uint32) @@ -2256,7 +2380,7 @@ def isosurface(data, level): ### expensive: verts = faceShiftTables[i][cellInds] #profiler() - verts[...,:3] += cells[:,np.newaxis,np.newaxis,:] ## we now have indexes into cutEdges + np.add(verts[...,:3], cells[:,np.newaxis,np.newaxis,:], out=verts[...,:3], casting='unsafe') ## we now have indexes into cutEdges verts = verts.reshape((verts.shape[0]*i,)+verts.shape[2:]) #profiler() diff --git a/vviewer/pyqtgraph/graphicsItems/CurvePoint.py b/vviewer/pyqtgraph/graphicsItems/CurvePoint.py index bb6beeb..f7682a4 100644 --- a/vviewer/pyqtgraph/graphicsItems/CurvePoint.py +++ b/vviewer/pyqtgraph/graphicsItems/CurvePoint.py @@ -91,6 +91,11 @@ class CurvePoint(GraphicsObject): pass def makeAnimation(self, prop='position', start=0.0, end=1.0, duration=10000, loop=1): + # In Python 3, a bytes object needs to be used as a property name in + # QPropertyAnimation. PyQt stopped automatically encoding a str when a + # QByteArray was expected in v5.5 (see qbytearray.sip). + if not isinstance(prop, bytes): + prop = prop.encode('latin-1') anim = QtCore.QPropertyAnimation(self, prop) anim.setDuration(duration) anim.setStartValue(start) diff --git a/vviewer/pyqtgraph/graphicsItems/FillBetweenItem.py b/vviewer/pyqtgraph/graphicsItems/FillBetweenItem.py index 15a14f8..0efb11d 100644 --- a/vviewer/pyqtgraph/graphicsItems/FillBetweenItem.py +++ b/vviewer/pyqtgraph/graphicsItems/FillBetweenItem.py @@ -1,4 +1,4 @@ -from ..Qt import QtGui +from ..Qt import QtGui, USE_PYQT5, USE_PYQT4, USE_PYSIDE from .. import functions as fn from .PlotDataItem import PlotDataItem from .PlotCurveItem import PlotCurveItem @@ -7,17 +7,24 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): """ GraphicsItem filling the space between two PlotDataItems. """ - def __init__(self, curve1=None, curve2=None, brush=None): + def __init__(self, curve1=None, curve2=None, brush=None, pen=None): QtGui.QGraphicsPathItem.__init__(self) self.curves = None if curve1 is not None and curve2 is not None: self.setCurves(curve1, curve2) elif curve1 is not None or curve2 is not None: raise Exception("Must specify two curves to fill between.") - + if brush is not None: - self.setBrush(fn.mkBrush(brush)) + self.setBrush(brush) + self.setPen(pen) self.updatePath() + + def setBrush(self, *args, **kwds): + QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds)) + + def setPen(self, *args, **kwds): + QtGui.QGraphicsPathItem.setPen(self, fn.mkPen(*args, **kwds)) def setCurves(self, curve1, curve2): """Set the curves to fill between. @@ -26,14 +33,13 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): Added in version 0.9.9 """ - if self.curves is not None: for c in self.curves: try: c.sigPlotChanged.disconnect(self.curveChanged) except (TypeError, RuntimeError): pass - + curves = [curve1, curve2] for c in curves: if not isinstance(c, PlotDataItem) and not isinstance(c, PlotCurveItem): @@ -43,7 +49,7 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): curve2.sigPlotChanged.connect(self.curveChanged) self.setZValue(min(curve1.zValue(), curve2.zValue())-1) self.curveChanged() - + def setBrush(self, *args, **kwds): """Change the fill brush. Acceps the same arguments as pg.mkBrush()""" QtGui.QGraphicsPathItem.setBrush(self, fn.mkBrush(*args, **kwds)) @@ -61,13 +67,17 @@ class FillBetweenItem(QtGui.QGraphicsPathItem): paths.append(c.curve.getPath()) elif isinstance(c, PlotCurveItem): paths.append(c.getPath()) - + path = QtGui.QPainterPath() - p1 = paths[0].toSubpathPolygons() - p2 = paths[1].toReversed().toSubpathPolygons() - if len(p1) == 0 or len(p2) == 0: + transform = QtGui.QTransform() + ps1 = paths[0].toSubpathPolygons(transform) + ps2 = paths[1].toReversed().toSubpathPolygons(transform) + ps2.reverse() + if len(ps1) == 0 or len(ps2) == 0: self.setPath(QtGui.QPainterPath()) return + - path.addPolygon(p1[0] + p2[0]) + for p1, p2 in zip(ps1, ps2): + path.addPolygon(p1 + p2) self.setPath(path) diff --git a/vviewer/pyqtgraph/graphicsItems/GradientEditorItem.py b/vviewer/pyqtgraph/graphicsItems/GradientEditorItem.py index a151798..f359ff1 100644 --- a/vviewer/pyqtgraph/graphicsItems/GradientEditorItem.py +++ b/vviewer/pyqtgraph/graphicsItems/GradientEditorItem.py @@ -1,18 +1,18 @@ +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 -import weakref from ..pgcollections import OrderedDict from ..colormap import ColorMap +from ..python2_3 import cmp -import numpy as np __all__ = ['TickSliderItem', 'GradientEditorItem'] - Gradients = OrderedDict([ ('thermal', {'ticks': [(0.3333, (185, 0, 0, 255)), (0.6666, (255, 220, 0, 255)), (1, (255, 255, 255, 255)), (0, (0, 0, 0, 255))], 'mode': 'rgb'}), ('flame', {'ticks': [(0.2, (7, 0, 220, 255)), (0.5, (236, 0, 134, 255)), (0.8, (246, 246, 0, 255)), (1.0, (255, 255, 255, 255)), (0.0, (0, 0, 0, 255))], 'mode': 'rgb'}), @@ -24,7 +24,13 @@ Gradients = OrderedDict([ ('grey', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'}), ]) - +def addGradientListToDocstring(): + """Decorator to add list of current pre-defined gradients to the end of a function docstring.""" + def dec(fn): + if fn.__doc__ is not None: + fn.__doc__ = fn.__doc__ + str(Gradients.keys()).strip('[').strip(']') + return fn + return dec @@ -117,16 +123,20 @@ class TickSliderItem(GraphicsWidget): self.resetTransform() ort = orientation if ort == 'top': - self.scale(1, -1) - self.translate(0, -self.height()) + transform = QtGui.QTransform.fromScale(1, -1) + transform.translate(0, -self.height()) + self.setTransform(transform) elif ort == 'left': - self.rotate(270) - self.scale(1, -1) - self.translate(-self.height(), -self.maxDim) + transform = QtGui.QTransform() + transform.rotate(270) + transform.scale(1, -1) + transform.translate(-self.height(), -self.maxDim) + self.setTransform(transform) elif ort == 'right': - self.rotate(270) - self.translate(-self.height(), 0) - #self.setPos(0, -self.height()) + transform = QtGui.QTransform() + transform.rotate(270) + transform.translate(-self.height(), 0) + self.setTransform(transform) elif ort != 'bottom': raise Exception("%s is not a valid orientation. Options are 'left', 'right', 'top', and 'bottom'" %str(ort)) @@ -238,7 +248,7 @@ class TickSliderItem(GraphicsWidget): self.addTick(pos.x()/self.length) elif ev.button() == QtCore.Qt.RightButton: self.showMenu(ev) - + #if ev.button() == QtCore.Qt.RightButton: #if self.moving: #ev.accept() @@ -468,11 +478,12 @@ class GradientEditorItem(TickSliderItem): act = self.sender() self.loadPreset(act.name) + @addGradientListToDocstring() def loadPreset(self, name): """ - Load a predefined gradient. - - """ ## TODO: provide image with names of defined gradients + Load a predefined gradient. Currently defined gradients are: + """## TODO: provide image with names of defined gradients + #global Gradients self.restoreState(Gradients[name]) @@ -783,11 +794,15 @@ class GradientEditorItem(TickSliderItem): self.updateGradient() self.sigGradientChangeFinished.emit(self) - -class Tick(QtGui.QGraphicsObject): ## NOTE: Making this a subclass of GraphicsObject instead results in + +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 ## private class - + + # When making Tick a subclass of QtGui.QGraphicsObject as origin, + # ..GraphicsScene.items(self, *args) will get Tick object as a + # class of QtGui.QMultimediaWidgets.QGraphicsVideoItem in python2.7-PyQt5(5.4.0) + sigMoving = QtCore.Signal(object) sigMoved = QtCore.Signal(object) @@ -805,7 +820,7 @@ class Tick(QtGui.QGraphicsObject): ## NOTE: Making this a subclass of GraphicsO self.pg.lineTo(QtCore.QPointF(scale/3**0.5, scale)) self.pg.closeSubpath() - QtGui.QGraphicsObject.__init__(self) + QtGui.QGraphicsWidget.__init__(self) self.setPos(pos[0], pos[1]) if self.movable: self.setZValue(1) diff --git a/vviewer/pyqtgraph/graphicsItems/GraphicsItem.py b/vviewer/pyqtgraph/graphicsItems/GraphicsItem.py index 2ca3519..d45818d 100644 --- a/vviewer/pyqtgraph/graphicsItems/GraphicsItem.py +++ b/vviewer/pyqtgraph/graphicsItems/GraphicsItem.py @@ -37,9 +37,6 @@ class GraphicsItem(object): if register: GraphicsScene.registerObject(self) ## workaround for pyqt bug in graphicsscene.items() - - - def getViewWidget(self): """ Return the view widget for this item. @@ -95,7 +92,6 @@ class GraphicsItem(object): def forgetViewBox(self): self._viewBox = None - def deviceTransform(self, viewportTransform=None): """ Return the transform that converts local item coordinates to device coordinates (usually pixels). diff --git a/vviewer/pyqtgraph/graphicsItems/HistogramLUTItem.py b/vviewer/pyqtgraph/graphicsItems/HistogramLUTItem.py index 89ebef3..3176425 100644 --- a/vviewer/pyqtgraph/graphicsItems/HistogramLUTItem.py +++ b/vviewer/pyqtgraph/graphicsItems/HistogramLUTItem.py @@ -139,6 +139,9 @@ class HistogramLUTItem(GraphicsWidget): #self.region.setBounds([vr.top(), vr.bottom()]) def setImageItem(self, img): + """Set an ImageItem to have its levels and LUT automatically controlled + by this HistogramLUTItem. + """ self.imageItem = weakref.ref(img) img.sigImageChanged.connect(self.imageChanged) img.setLookupTable(self.getLookupTable) ## send function pointer, not the result @@ -163,6 +166,9 @@ class HistogramLUTItem(GraphicsWidget): 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 n is None: if img.dtype == np.uint8: n = 256 @@ -173,8 +179,8 @@ class HistogramLUTItem(GraphicsWidget): return self.lut def regionChanged(self): - #if self.imageItem is not None: - #self.imageItem.setLevels(self.region.getRegion()) + if self.imageItem() is not None: + self.imageItem().setLevels(self.region.getRegion()) self.sigLevelChangeFinished.emit(self) #self.update() @@ -199,7 +205,11 @@ class HistogramLUTItem(GraphicsWidget): profiler('set region') def getLevels(self): + """Return the min and max levels. + """ return self.region.getRegion() def setLevels(self, mn, mx): + """Set the min and max levels. + """ self.region.setRegion([mn, mx]) diff --git a/vviewer/pyqtgraph/graphicsItems/ImageItem.py b/vviewer/pyqtgraph/graphicsItems/ImageItem.py index 5b04143..3d45ad7 100644 --- a/vviewer/pyqtgraph/graphicsItems/ImageItem.py +++ b/vviewer/pyqtgraph/graphicsItems/ImageItem.py @@ -7,6 +7,8 @@ from .. import functions as fn from .. import debug as debug from .GraphicsObject import GraphicsObject from ..Point import Point +from .. import getConfigOption + __all__ = ['ImageItem'] @@ -28,7 +30,6 @@ class ImageItem(GraphicsObject): 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 @@ -47,6 +48,12 @@ class ImageItem(GraphicsObject): 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 @@ -74,11 +81,6 @@ class ImageItem(GraphicsObject): """ self.paintMode = mode self.update() - - ## use setOpacity instead. - #def setAlpha(self, alpha): - #self.setOpacity(alpha) - #self.updateImage() def setBorder(self, b): self.border = fn.mkPen(b) @@ -87,28 +89,20 @@ class ImageItem(GraphicsObject): def width(self): if self.image is None: return None - return self.image.shape[0] + axis = 0 if self.axisOrder == 'col-major' else 1 + return self.image.shape[axis] def height(self): if self.image is None: return None - return self.image.shape[1] + axis = 1 if self.axisOrder == 'col-major' else 0 + return self.image.shape[axis] def boundingRect(self): if self.image is None: return QtCore.QRectF(0., 0., 0., 0.) return QtCore.QRectF(0., 0., float(self.width()), float(self.height())) - #def setClipLevel(self, level=None): - #self.clipLevel = level - #self.updateImage() - - #def paint(self, p, opt, widget): - #pass - #if self.pixmap is not None: - #p.drawPixmap(0, 0, self.pixmap) - #print "paint" - def setLevels(self, levels, update=True): """ Set image scaling levels. Can be one of: @@ -119,9 +113,13 @@ class ImageItem(GraphicsObject): Only the first format is compatible with lookup tables. See :func:`makeARGB <pyqtgraph.makeARGB>` for more details on how levels are applied. """ - self.levels = levels - if update: - self.updateImage() + if levels is not None: + levels = np.asarray(levels) + if not fn.eq(levels, self.levels): + self.levels = levels + self._effectiveLut = None + if update: + self.updateImage() def getLevels(self): return self.levels @@ -137,9 +135,11 @@ class ImageItem(GraphicsObject): Ordinarily, this table is supplied by a :class:`HistogramLUTItem <pyqtgraph.HistogramLUTItem>` or :class:`GradientEditorItem <pyqtgraph.GradientEditorItem>`. """ - self.lut = lut - if update: - self.updateImage() + if lut is not self.lut: + self.lut = lut + self._effectiveLut = None + if update: + self.updateImage() def setAutoDownsample(self, ads): """ @@ -152,7 +152,11 @@ class ImageItem(GraphicsObject): self.update() def setOpts(self, update=True, **kargs): - + if 'axisOrder' in kargs: + val = kargs['axisOrder'] + if val not in ('row-major', 'col-major'): + raise ValueError('axisOrder must be either "row-major" or "col-major"') + self.axisOrder = val if 'lut' in kargs: self.setLookupTable(kargs['lut'], update=update) if 'levels' in kargs: @@ -195,7 +199,7 @@ class ImageItem(GraphicsObject): 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). + be of length 3 (RGB) or 4 (RGBA). See *notes* below. 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 @@ -206,12 +210,26 @@ class ImageItem(GraphicsObject): 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 <pyqtgraph.ImageItem.setCompositionMode>` + compositionMode See :func:`setCompositionMode <pyqtgraph.ImageItem.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 reduces aliasing. ================= ========================================================================= + + + **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 <apiref_config>`. + + """ profile = debug.Profiler() @@ -222,7 +240,10 @@ class ImageItem(GraphicsObject): else: gotNewData = True shapeChanged = (self.image is None or image.shape != self.image.shape) - self.image = image.view(np.ndarray) + image = image.view(np.ndarray) + if self.image is None or image.dtype != self.image.dtype: + self._effectiveLut = None + self.image = image if self.image.shape[0] > 2**15-1 or self.image.shape[1] > 2**15-1: if 'autoDownsample' not in kargs: kargs['autoDownsample'] = True @@ -261,6 +282,53 @@ class ImageItem(GraphicsObject): if gotNewData: self.sigImageChanged.emit() + 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. + """ + # Might eventually need to account for downsampling / clipping here + tr = QtGui.QTransform() + if self.axisOrder == 'row-major': + # transpose + tr.scale(1, -1) + tr.rotate(-90) + return tr + + 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() + if self.axisOrder == 'row-major': + # transpose + tr.scale(1, -1) + tr.rotate(-90) + return tr + + def mapToData(self, obj): + tr = self.inverseDataTransform() + return tr.map(obj) + + def mapFromData(self, obj): + tr = self.dataTransform() + return tr.map(obj) + + def quickMinMax(self, targetSize=1e6): + """ + Estimate the min/max values of the image data by subsampling. + """ + data = self.image + while data.size > targetSize: + ax = np.argmax(data.shape) + sl = [slice(None)] * data.ndim + sl[ax] = slice(None, None, 2) + data = data[sl] + return nanmin(data), nanmax(data) def updateImage(self, *args, **kargs): ## used for re-rendering qimage from self.image. @@ -291,14 +359,48 @@ class ImageItem(GraphicsObject): y = self.mapToDevice(QtCore.QPointF(0,1)) w = Point(x-o).length() h = Point(y-o).length() - xds = max(1, int(1/w)) - yds = max(1, int(1/h)) - image = fn.downsample(self.image, xds, axis=0) - image = fn.downsample(image, yds, axis=1) + if w == 0 or h == 0: + self.qimage = None + return + xds = max(1, int(1.0 / w)) + yds = max(1, int(1.0 / h)) + axes = [1, 0] if self.axisOrder == 'row-major' else [0, 1] + image = fn.downsample(self.image, xds, axis=axes[0]) + image = fn.downsample(image, yds, axis=axes[1]) + self._lastDownsample = (xds, yds) else: image = self.image + + # if the image data is a small int, then we can combine levels + lut + # into a single lut for better performance + levels = self.levels + if levels is not None and levels.ndim == 1 and image.dtype in (np.ubyte, np.uint16): + if self._effectiveLut is None: + eflsize = 2**(image.itemsize*8) + ind = np.arange(eflsize) + minlev, maxlev = levels + 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, + 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 + + # Assume images are in column-major order for backward compatibility + # (most images are in row-major order) - argb, alpha = fn.makeARGB(image.transpose((1, 0, 2)[:image.ndim]), lut=lut, levels=self.levels) + 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) def paint(self, p, *args): @@ -314,7 +416,8 @@ class ImageItem(GraphicsObject): p.setCompositionMode(self.paintMode) profile('set comp mode') - p.drawImage(QtCore.QRectF(0,0,self.image.shape[0],self.image.shape[1]), self.qimage) + shape = self.image.shape[:2] if self.axisOrder == 'col-major' else self.image.shape[:2][::-1] + p.drawImage(QtCore.QRectF(0,0,*shape), self.qimage) profile('p.drawImage') if self.border is not None: p.setPen(self.border) @@ -347,8 +450,8 @@ class ImageItem(GraphicsObject): if self.image is None: return None,None if step == 'auto': - step = (np.ceil(self.image.shape[0] / targetImageSize), - np.ceil(self.image.shape[1] / targetImageSize)) + step = (int(np.ceil(self.image.shape[0] / targetImageSize)), + int(np.ceil(self.image.shape[1] / targetImageSize))) if np.isscalar(step): step = (step, step) stepData = self.image[::step[0], ::step[1]] @@ -365,6 +468,7 @@ class ImageItem(GraphicsObject): bins = 500 kwds['bins'] = bins + stepData = stepData[np.isfinite(stepData)] hist = np.histogram(stepData, **kwds) return hist[1][:-1], hist[0] @@ -400,21 +504,6 @@ class ImageItem(GraphicsObject): self.qimage = None self.update() - #def mousePressEvent(self, ev): - #if self.drawKernel is not None and ev.button() == QtCore.Qt.LeftButton: - #self.drawAt(ev.pos(), ev) - #ev.accept() - #else: - #ev.ignore() - - #def mouseMoveEvent(self, ev): - ##print "mouse move", ev.pos() - #if self.drawKernel is not None: - #self.drawAt(ev.pos(), ev) - - #def mouseReleaseEvent(self, ev): - #pass - def mouseDragEvent(self, ev): if ev.button() != QtCore.Qt.LeftButton: ev.ignore() @@ -451,24 +540,18 @@ class ImageItem(GraphicsObject): 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. ev.acceptClicks(QtCore.Qt.RightButton) - #self.box.setBrush(fn.mkBrush('w')) elif not ev.isExit() and self.removable: ev.acceptClicks(QtCore.Qt.RightButton) ## accept context menu clicks - #else: - #self.box.setBrush(self.brush) - #self.update() - - def tabletEvent(self, ev): - print(ev.device()) - print(ev.pointerType()) - print(ev.pressure()) + pass + #print(ev.device()) + #print(ev.pointerType()) + #print(ev.pressure()) def drawAt(self, pos, ev=None): pos = [int(pos.x()), int(pos.y())] diff --git a/vviewer/pyqtgraph/graphicsItems/InfiniteLine.py b/vviewer/pyqtgraph/graphicsItems/InfiniteLine.py index 8108c3c..3da8232 100644 --- a/vviewer/pyqtgraph/graphicsItems/InfiniteLine.py +++ b/vviewer/pyqtgraph/graphicsItems/InfiniteLine.py @@ -1,19 +1,23 @@ from ..Qt import QtGui, QtCore from ..Point import Point from .GraphicsObject import GraphicsObject +from .TextItem import TextItem +from .ViewBox import ViewBox from .. import functions as fn import numpy as np import weakref -__all__ = ['InfiniteLine'] +__all__ = ['InfiniteLine', 'InfLineLabel'] + + class InfiniteLine(GraphicsObject): """ **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>` - + Displays a line of infinite length. This line may be dragged to indicate a position in data coordinates. - + =============================== =================================================== **Signals:** sigDragged(self) @@ -21,12 +25,13 @@ class InfiniteLine(GraphicsObject): sigPositionChanged(self) =============================== =================================================== """ - + sigDragged = QtCore.Signal(object) sigPositionChangeFinished = QtCore.Signal(object) sigPositionChanged = QtCore.Signal(object) - - def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None): + + def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, + hoverPen=None, label=None, labelOpts=None, name=None): """ =============== ================================================================== **Arguments:** @@ -37,13 +42,26 @@ class InfiniteLine(GraphicsObject): for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent yellow. movable If True, the line can be dragged to a new position by the user. + hoverPen Pen to use when drawing line when hovering over it. Can be any + arguments that are valid for :func:`mkPen <pyqtgraph.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`. + name Name of the item =============== ================================================================== """ - + self._boundingRect = None + self._line = None + + self._name = name + GraphicsObject.__init__(self) - + if bounds is None: ## allowed value boundaries for orthogonal lines self.maxRange = [None, None] else: @@ -53,64 +71,70 @@ class InfiniteLine(GraphicsObject): self.mouseHovering = False self.p = [0, 0] self.setAngle(angle) + if pos is None: pos = Point(0,0) self.setPos(pos) if pen is None: pen = (200, 200, 100) - self.setPen(pen) - self.setHoverPen(color=(255,0,0), width=self.pen.width()) + if hoverPen is None: + self.setHoverPen(color=(255,0,0), width=self.pen.width()) + else: + self.setHoverPen(hoverPen) self.currentPen = self.pen - #self.setFlag(self.ItemSendsScenePositionChanges) - + + if label is not None: + labelOpts = {} if labelOpts is None else labelOpts + self.label = InfLineLabel(self, text=label, **labelOpts) + def setMovable(self, m): """Set whether the line is movable by the user.""" self.movable = m self.setAcceptHoverEvents(m) - + def setBounds(self, bounds): """Set the (minimum, maximum) allowable values when dragging.""" self.maxRange = bounds self.setValue(self.value()) - + def setPen(self, *args, **kwargs): - """Set the pen for drawing the line. Allowable arguments are any that are valid + """Set the pen for drawing the line. Allowable arguments are any that are valid for :func:`mkPen <pyqtgraph.mkPen>`.""" self.pen = fn.mkPen(*args, **kwargs) if not self.mouseHovering: self.currentPen = self.pen self.update() - + def setHoverPen(self, *args, **kwargs): - """Set the pen for drawing the line while the mouse hovers over it. - Allowable arguments are any that are valid + """Set the pen for drawing the line while the mouse hovers over it. + Allowable arguments are any that are valid for :func:`mkPen <pyqtgraph.mkPen>`. - + If the line is not movable, then hovering is also disabled. - + Added in version 0.9.9.""" self.hoverPen = fn.mkPen(*args, **kwargs) if self.mouseHovering: self.currentPen = self.hoverPen self.update() - + def setAngle(self, angle): """ Takes angle argument in degrees. 0 is horizontal; 90 is vertical. - - Note that the use of value() and setValue() changes if the line is + + 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.resetTransform() self.rotate(self.angle) self.update() - + def setPos(self, pos): - + if type(pos) in [list, tuple]: newPos = pos elif isinstance(pos, QtCore.QPointF): @@ -122,10 +146,10 @@ class InfiniteLine(GraphicsObject): newPos = [0, pos] else: raise Exception("Must specify 2D coordinate for non-orthogonal lines.") - + ## check bounds (only works for orthogonal lines) if self.angle == 90: - if self.maxRange[0] is not None: + if self.maxRange[0] is not None: newPos[0] = max(newPos[0], self.maxRange[0]) if self.maxRange[1] is not None: newPos[0] = min(newPos[0], self.maxRange[1]) @@ -134,24 +158,24 @@ class InfiniteLine(GraphicsObject): newPos[1] = max(newPos[1], self.maxRange[0]) if self.maxRange[1] is not None: newPos[1] = min(newPos[1], self.maxRange[1]) - + if self.p != newPos: self.p = newPos + self._invalidateCache() GraphicsObject.setPos(self, Point(self.p)) - self.update() self.sigPositionChanged.emit(self) def getXPos(self): return self.p[0] - + def getYPos(self): return self.p[1] - + def getPos(self): return self.p def value(self): - """Return the value of the line. Will be a single number for horizontal and + """Return the value of the line. Will be a single number for horizontal and vertical lines, and a list of [x,y] values for diagonal lines.""" if self.angle%180 == 0: return self.getYPos() @@ -159,10 +183,10 @@ class InfiniteLine(GraphicsObject): return self.getXPos() else: return self.getPos() - + def setValue(self, v): - """Set the position of the line. If line is horizontal or vertical, v can be - a single value. Otherwise, a 2D coordinate must be specified (list, tuple and + """Set the position of the line. If line is horizontal or vertical, v can be + a single value. Otherwise, a 2D coordinate must be specified (list, tuple and QPointF are all acceptable).""" self.setPos(v) @@ -175,26 +199,35 @@ class InfiniteLine(GraphicsObject): #else: #print "ignore", change #return GraphicsObject.itemChange(self, change, val) - + + def _invalidateCache(self): + self._line = None + self._boundingRect = None + def boundingRect(self): - #br = UIGraphicsItem.boundingRect(self) - br = self.viewRect() - ## 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) - return br.normalized() - + 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) + return self._boundingRect + def paint(self, p, *args): - br = self.boundingRect() p.setPen(self.currentPen) - p.drawLine(Point(br.right(), 0), Point(br.left(), 0)) - #p.drawRect(self.boundingRect()) - + p.drawLine(self._line) + def dataBounds(self, axis, frac=1.0, orthoRange=None): if axis == 0: return None ## x axis should never be auto-scaled @@ -208,20 +241,16 @@ class InfiniteLine(GraphicsObject): self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos()) self.startPosition = self.pos() ev.accept() - + if not self.moving: return - - #pressDelta = self.mapToParent(ev.buttonDownPos()) - Point(self.p) + self.setPos(self.cursorOffset + self.mapToParent(ev.pos())) self.sigDragged.emit(self) if ev.isFinish(): self.moving = False self.sigPositionChangeFinished.emit(self) - #else: - #print ev - def mouseClickEvent(self, ev): if self.moving and ev.button() == QtCore.Qt.RightButton: ev.accept() @@ -246,30 +275,196 @@ class InfiniteLine(GraphicsObject): else: self.currentPen = self.pen self.update() + + def viewTransformChanged(self): + """ + Called whenever the transformation matrix of the view has changed. + (eg, the view range has changed or the view was resized) + """ + self._invalidateCache() - #def hoverEnterEvent(self, ev): - #print "line hover enter" - #ev.ignore() - #self.updateHoverPen() - - #def hoverMoveEvent(self, ev): - #print "line hover move" - #ev.ignore() - #self.updateHoverPen() - - #def hoverLeaveEvent(self, ev): - #print "line hover leave" - #ev.ignore() - #self.updateHoverPen(False) + def setName(self, name): + self._name = name + + def name(self): + return self._name + + +class InfLineLabel(TextItem): + """ + A TextItem that attaches itself to an InfiniteLine. + + This class extends TextItem with the following features: + + * Automatically positions adjacent to the line at a fixed position along + the line and within the view box. + * Automatically reformats text when the line value has changed. + * Can optionally be dragged to change its location along the line. + * Optionally aligns to its parent line. + + =============== ================================================================== + **Arguments:** + line The InfiniteLine to which this label will be attached. + text String to display in the label. May contain a {value} formatting + string to display the current value of the line. + movable Bool; if True, then the label can be dragged along the line. + position Relative position (0.0-1.0) within the view to position the label + along the line. + anchors List of (x,y) pairs giving the text anchor positions that should + be used when the line is moved to one side of the view or the + other. This allows text to switch to the opposite side of the line + as it approaches the edge of the view. These are automatically + selected for some common cases, but may be specified if the + default values give unexpected results. + =============== ================================================================== + + All extra keyword arguments are passed to TextItem. A particularly useful + option here is to use `rotateAxis=(1, 0)`, which will cause the text to + be automatically rotated parallel to the line. + """ + def __init__(self, line, text="", movable=False, position=0.5, anchors=None, **kwds): + self.line = line + self.movable = movable + self.moving = False + self.orthoPos = position # text will always be placed on the line at a position relative to view bounds + self.format = text + self.line.sigPositionChanged.connect(self.valueChanged) + self._endpoints = (None, None) + if anchors is None: + # automatically pick sensible anchors + rax = kwds.get('rotateAxis', None) + if rax is not None: + if tuple(rax) == (1,0): + anchors = [(0.5, 0), (0.5, 1)] + else: + anchors = [(0, 0.5), (1, 0.5)] + else: + if line.angle % 180 == 0: + anchors = [(0.5, 0), (0.5, 1)] + else: + anchors = [(0, 0.5), (1, 0.5)] + + self.anchors = anchors + TextItem.__init__(self, **kwds) + self.setParentItem(line) + self.valueChanged() + + def valueChanged(self): + if not self.isVisible(): + return + value = self.line.value() + self.setText(self.format.format(value=value)) + self.updatePosition() + + def getEndpoints(self): + # calculate points where line intersects view box + # (in line coordinates) + if self._endpoints[0] is None: + lr = self.line.boundingRect() + pt1 = Point(lr.left(), 0) + pt2 = Point(lr.right(), 0) + + if self.line.angle % 90 != 0: + # more expensive to find text position for oblique lines. + view = self.getViewBox() + if not self.isVisible() or not isinstance(view, ViewBox): + # not in a viewbox, skip update + return (None, None) + p = QtGui.QPainterPath() + p.moveTo(pt1) + p.lineTo(pt2) + p = self.line.itemTransform(view)[0].map(p) + vr = QtGui.QPainterPath() + vr.addRect(view.boundingRect()) + paths = vr.intersected(p).toSubpathPolygons(QtGui.QTransform()) + if len(paths) > 0: + l = list(paths[0]) + pt1 = self.line.mapFromItem(view, l[0]) + pt2 = self.line.mapFromItem(view, l[1]) + self._endpoints = (pt1, pt2) + return self._endpoints + + def updatePosition(self): + # update text position to relative view location along line + self._endpoints = (None, None) + pt1, pt2 = self.getEndpoints() + if pt1 is None: + return + pt = pt2 * self.orthoPos + pt1 * (1-self.orthoPos) + self.setPos(pt) - #def updateHoverPen(self, hover=None): - #if hover is None: - #scene = self.scene() - #hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag) + # update anchor to keep text visible as it nears the view box edge + vr = self.line.viewRect() + if vr is not None: + self.setAnchor(self.anchors[0 if vr.center().y() < 0 else 1]) - #if hover: - #self.currentPen = fn.mkPen(255, 0,0) - #else: - #self.currentPen = self.pen - #self.update() + def setVisible(self, v): + TextItem.setVisible(self, v) + if v: + self.updateText() + self.updatePosition() + + def setMovable(self, m): + """Set whether this label is movable by dragging along the line. + """ + self.movable = m + self.setAcceptHoverEvents(m) + + def setPosition(self, p): + """Set the relative position (0.0-1.0) of this label within the view box + and along the line. + + For horizontal (angle=0) and vertical (angle=90) lines, a value of 0.0 + places the text at the bottom or left of the view, respectively. + """ + self.orthoPos = p + self.updatePosition() + + def setFormat(self, text): + """Set the text format string for this label. + + May optionally contain "{value}" to include the lines current value + (the text will be reformatted whenever the line is moved). + """ + self.format = text + self.valueChanged() + + def mouseDragEvent(self, ev): + if self.movable and ev.button() == QtCore.Qt.LeftButton: + if ev.isStart(): + self._moving = True + self._cursorOffset = self._posToRel(ev.buttonDownPos()) + self._startPosition = self.orthoPos + ev.accept() + + if not self._moving: + return + + rel = self._posToRel(ev.pos()) + self.orthoPos = np.clip(self._startPosition + rel - self._cursorOffset, 0, 1) + self.updatePosition() + if ev.isFinish(): + self._moving = False + + def mouseClickEvent(self, ev): + if self.moving and ev.button() == QtCore.Qt.RightButton: + ev.accept() + self.orthoPos = self._startPosition + self.moving = False + + def hoverEvent(self, ev): + if not ev.isExit() and self.movable: + ev.acceptDrags(QtCore.Qt.LeftButton) + + def viewTransformChanged(self): + self.updatePosition() + TextItem.viewTransformChanged(self) + def _posToRel(self, pos): + # convert local position to relative position along line between view bounds + pt1, pt2 = self.getEndpoints() + if pt1 is None: + return 0 + view = self.getViewBox() + pos = self.mapToParent(pos) + return (pos.x() - pt1.x()) / (pt2.x()-pt1.x()) diff --git a/vviewer/pyqtgraph/graphicsItems/IsocurveItem.py b/vviewer/pyqtgraph/graphicsItems/IsocurveItem.py index 4474e29..03ebc69 100644 --- a/vviewer/pyqtgraph/graphicsItems/IsocurveItem.py +++ b/vviewer/pyqtgraph/graphicsItems/IsocurveItem.py @@ -1,5 +1,4 @@ - - +from .. import getConfigOption from .GraphicsObject import * from .. import functions as fn from ..Qt import QtGui, QtCore @@ -9,12 +8,10 @@ class IsocurveItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>` - Item displaying an isocurve of a 2D array.To align this item correctly with an - ImageItem,call isocurve.setParentItem(image) + Item displaying an isocurve of a 2D array. To align this item correctly with an + ImageItem, call ``isocurve.setParentItem(image)``. """ - - - def __init__(self, data=None, level=0, pen='w'): + def __init__(self, data=None, level=0, pen='w', axisOrder=None): """ Create a new isocurve item. @@ -25,6 +22,9 @@ class IsocurveItem(GraphicsObject): level The cutoff value at which to draw the isocurve. pen The color of the curve item. Can be anything valid for :func:`mkPen <pyqtgraph.mkPen>` + axisOrder May be either 'row-major' or 'col-major'. By default this uses + the ``imageAxisOrder`` + :ref:`global configuration option <apiref_config>`. ============== =============================================================== """ GraphicsObject.__init__(self) @@ -32,9 +32,9 @@ class IsocurveItem(GraphicsObject): self.level = level self.data = None self.path = None + self.axisOrder = getConfigOption('imageAxisOrder') if axisOrder is None else axisOrder self.setPen(pen) self.setData(data, level) - def setData(self, data, level=None): """ @@ -54,7 +54,6 @@ class IsocurveItem(GraphicsObject): self.path = None self.prepareGeometryChange() self.update() - def setLevel(self, level): """Set the level at which the isocurve is drawn.""" @@ -62,7 +61,6 @@ class IsocurveItem(GraphicsObject): self.path = None self.prepareGeometryChange() self.update() - def setPen(self, *args, **kwargs): """Set the pen used to draw the isocurve. Arguments can be any that are valid @@ -75,18 +73,8 @@ class IsocurveItem(GraphicsObject): for :func:`mkBrush <pyqtgraph.mkBrush>`""" self.brush = fn.mkBrush(*args, **kwargs) self.update() - def updateLines(self, data, level): - ##print "data:", data - ##print "level", level - #lines = fn.isocurve(data, level) - ##print len(lines) - #self.path = QtGui.QPainterPath() - #for line in lines: - #self.path.moveTo(*line[0]) - #self.path.lineTo(*line[1]) - #self.update() self.setData(data, level) def boundingRect(self): @@ -100,7 +88,13 @@ class IsocurveItem(GraphicsObject): if self.data is None: self.path = None return - lines = fn.isocurve(self.data, self.level, connected=True, extendToEdge=True) + + if self.axisOrder == 'row-major': + data = self.data.T + else: + data = self.data + + lines = fn.isocurve(data, self.level, connected=True, extendToEdge=True) self.path = QtGui.QPainterPath() for line in lines: self.path.moveTo(*line[0]) diff --git a/vviewer/pyqtgraph/graphicsItems/PlotCurveItem.py b/vviewer/pyqtgraph/graphicsItems/PlotCurveItem.py index 3d3e969..d66a8a9 100644 --- a/vviewer/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/vviewer/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -126,10 +126,18 @@ class PlotCurveItem(GraphicsObject): ## Get min/max (or percentiles) of the requested data range if frac >= 1.0: + # include complete data range + # first try faster nanmin/max function, then cut out infs if needed. b = (np.nanmin(d), np.nanmax(d)) + if any(np.isinf(b)): + mask = np.isfinite(d) + d = d[mask] + b = (d.min(), d.max()) + elif frac <= 0.0: raise Exception("Value for parameter 'frac' must be > 0. (got %s)" % str(frac)) else: + # include a percentile of data range mask = np.isfinite(d) d = d[mask] b = np.percentile(d, [50 * (1 - frac), 50 * (1 + frac)]) diff --git a/vviewer/pyqtgraph/graphicsItems/PlotDataItem.py b/vviewer/pyqtgraph/graphicsItems/PlotDataItem.py index 6148989..37245be 100644 --- a/vviewer/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/vviewer/pyqtgraph/graphicsItems/PlotDataItem.py @@ -1,13 +1,14 @@ +import numpy as np from .. import metaarray as metaarray from ..Qt import QtCore from .GraphicsObject import GraphicsObject from .PlotCurveItem import PlotCurveItem from .ScatterPlotItem import ScatterPlotItem -import numpy as np from .. import functions as fn from .. import debug as debug from .. import getConfigOption + class PlotDataItem(GraphicsObject): """ **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>` @@ -522,6 +523,10 @@ class PlotDataItem(GraphicsObject): #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 + 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]: @@ -569,11 +574,11 @@ class PlotDataItem(GraphicsObject): x = x[::ds] y = y[::ds] elif self.opts['downsampleMethod'] == 'mean': - n = len(x) / ds + n = len(x) // ds x = x[:n*ds:ds] y = y[:n*ds].reshape(n,ds).mean(axis=1) elif self.opts['downsampleMethod'] == 'peak': - n = len(x) / ds + n = len(x) // ds x1 = np.empty((n,2)) x1[:] = x[:n*ds:ds,np.newaxis] x = x1.reshape(n*2) diff --git a/vviewer/pyqtgraph/graphicsItems/PlotItem/PlotItem.py b/vviewer/pyqtgraph/graphicsItems/PlotItem/PlotItem.py index 4f10b0e..41011df 100644 --- a/vviewer/pyqtgraph/graphicsItems/PlotItem/PlotItem.py +++ b/vviewer/pyqtgraph/graphicsItems/PlotItem/PlotItem.py @@ -16,20 +16,14 @@ This class is very heavily featured: - Control panel with a huge feature set including averaging, decimation, display, power spectrum, svg/png export, plot linking, and more. """ -from ...Qt import QtGui, QtCore, QtSvg, USE_PYSIDE -from ... import pixmaps import sys - -if USE_PYSIDE: - from .plotConfigTemplate_pyside import * -else: - from .plotConfigTemplate_pyqt import * - -from ... import functions as fn -from ...widgets.FileDialog import FileDialog import weakref import numpy as np import os +from ...Qt import QtGui, QtCore, QT_LIB +from ... import pixmaps +from ... import functions as fn +from ...widgets.FileDialog import FileDialog from .. PlotDataItem import PlotDataItem from .. ViewBox import ViewBox from .. AxisItem import AxisItem @@ -39,6 +33,14 @@ from .. GraphicsWidget import GraphicsWidget from .. ButtonItem import ButtonItem from .. InfiniteLine import InfiniteLine from ...WidgetGroup import WidgetGroup +from ...python2_3 import basestring + +if QT_LIB == 'PyQt4': + from .plotConfigTemplate_pyqt import * +elif QT_LIB == 'PySide': + from .plotConfigTemplate_pyside import * +elif QT_LIB == 'PyQt5': + from .plotConfigTemplate_pyqt5 import * __all__ = ['PlotItem'] @@ -168,7 +170,10 @@ class PlotItem(GraphicsWidget): axisItems = {} self.axes = {} for k, pos in (('top', (1,1)), ('bottom', (3,1)), ('left', (2,0)), ('right', (2,2))): - axis = axisItems.get(k, AxisItem(orientation=k, parent=self)) + 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) @@ -469,12 +474,13 @@ class PlotItem(GraphicsWidget): ### Average data together (x, y) = curve.getData() + stepMode = curve.opts['stepMode'] if plot.yData is not None and y.shape == plot.yData.shape: # note that if shapes do not match, then the average resets. newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n) - plot.setData(plot.xData, newData) + plot.setData(plot.xData, newData, stepMode=stepMode) else: - plot.setData(x, y) + plot.setData(x, y, stepMode=stepMode) def autoBtnClicked(self): if self.autoBtn.mode == 'auto': @@ -768,14 +774,6 @@ class PlotItem(GraphicsWidget): y = pos.y() * sy fh.write('<circle cx="%f" cy="%f" r="1" fill="#%s" stroke="none" fill-opacity="%f"/>\n' % (x, y, color, opacity)) - #fh.write('<path fill="none" stroke="#%s" stroke-opacity="%f" stroke-width="1" d="M%f,%f ' % (color, opacity, x[0], y[0])) - #for i in xrange(1, len(x)): - #fh.write('L%f,%f ' % (x[i], y[i])) - - #fh.write('"/>') - - ## get list of curves, scatter plots - fh.write("</svg>\n") @@ -787,42 +785,9 @@ class PlotItem(GraphicsWidget): fileName = str(fileName) PlotItem.lastFileDir = os.path.dirname(fileName) - self.svg = QtSvg.QSvgGenerator() - self.svg.setFileName(fileName) - res = 120. - view = self.scene().views()[0] - bounds = view.viewport().rect() - bounds = QtCore.QRectF(0, 0, bounds.width(), bounds.height()) - - self.svg.setResolution(res) - self.svg.setViewBox(bounds) - - self.svg.setSize(QtCore.QSize(bounds.width(), bounds.height())) - - painter = QtGui.QPainter(self.svg) - view.render(painter, bounds) - - painter.end() - - ## Workaround to set pen widths correctly - import re - data = open(fileName).readlines() - for i in range(len(data)): - line = data[i] - m = re.match(r'(<g .*)stroke-width="1"(.*transform="matrix\(([^\)]+)\)".*)', line) - if m is not None: - #print "Matched group:", line - g = m.groups() - matrix = list(map(float, g[2].split(','))) - #print "matrix:", matrix - scale = max(abs(matrix[0]), abs(matrix[3])) - if scale == 0 or scale == 1.0: - continue - data[i] = g[0] + ' stroke-width="%0.2g" ' % (1.0/scale) + g[1] + '\n' - #print "old line:", line - #print "new line:", data[i] - open(fileName, 'w').write(''.join(data)) - + from ...exporters import SVGExporter + ex = SVGExporter(self) + ex.export(fileName) def writeImage(self, fileName=None): if fileName is None: diff --git a/vviewer/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py b/vviewer/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.py new file mode 100644 index 0000000..e9fdff2 --- /dev/null +++ b/vviewer/pyqtgraph/graphicsItems/PlotItem/plotConfigTemplate_pyqt5.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 PyQt5 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/vviewer/pyqtgraph/graphicsItems/ROI.py b/vviewer/pyqtgraph/graphicsItems/ROI.py index 7707466..963ecb0 100644 --- a/vviewer/pyqtgraph/graphicsItems/ROI.py +++ b/vviewer/pyqtgraph/graphicsItems/ROI.py @@ -21,6 +21,7 @@ from math import cos, sin from .. import functions as fn from .GraphicsObject import GraphicsObject from .UIGraphicsItem import UIGraphicsItem +from .. import getConfigOption __all__ = [ 'ROI', @@ -213,20 +214,30 @@ class ROI(GraphicsObject): """Return the angle of the ROI in degrees.""" return self.getState()['angle'] - def setPos(self, pos, update=True, finish=True): + def setPos(self, pos, y=None, update=True, finish=True): """Set the position of the ROI (in the parent's coordinate system). - By default, this will cause both sigRegionChanged and sigRegionChangeFinished to be emitted. - If finish is False, then sigRegionChangeFinished will not be emitted. You can then use - stateChangeFinished() to cause the signal to be emitted after a series of state changes. + Accepts either separate (x, y) arguments or a single :class:`Point` or + ``QPointF`` argument. - If update is False, the state change will be remembered but not processed and no signals + By default, this method causes both ``sigRegionChanged`` and + ``sigRegionChangeFinished`` to be emitted. If *finish* is False, then + ``sigRegionChangeFinished`` will not be emitted. You can then use + stateChangeFinished() to cause the signal to be emitted after a series + of state changes. + + If *update* is False, the state change will be remembered but not processed and no signals will be emitted. You can then use stateChanged() to complete the state change. This allows multiple change functions to be called sequentially while minimizing processing overhead - and repeated signals. Setting update=False also forces finish=False. + and repeated signals. Setting ``update=False`` also forces ``finish=False``. """ - - pos = Point(pos) + if y is None: + pos = Point(pos) + else: + # avoid ambiguity where update is provided as a positional argument + 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: @@ -526,7 +537,7 @@ class ROI(GraphicsObject): if isinstance(handle, Handle): index = [i for i, info in enumerate(self.handles) if info['item'] is handle] if len(index) == 0: - raise Exception("Cannot remove handle; it is not attached to this ROI") + raise Exception("Cannot return handle index; not attached to this ROI") return index[0] else: return handle @@ -636,11 +647,20 @@ class ROI(GraphicsObject): if self.mouseHovering == hover: return self.mouseHovering = hover - if hover: - self.currentPen = fn.mkPen(255, 255, 0) + self._updateHoverColor() + + def _updateHoverColor(self): + pen = self._makePen() + if self.currentPen != pen: + self.currentPen = pen + self.update() + + def _makePen(self): + # Generate the pen color for this ROI based on its current state. + if self.mouseHovering: + return fn.mkPen(255, 255, 0) else: - self.currentPen = self.pen - self.update() + return self.pen def contextMenuEnabled(self): return self.removable @@ -919,8 +939,9 @@ class ROI(GraphicsObject): if self.lastState is None: changed = True else: - for k in list(self.state.keys()): - if self.state[k] != self.lastState[k]: + state = self.getState() + for k in list(state.keys()): + if state[k] != self.lastState[k]: changed = True self.prepareGeometryChange() @@ -940,10 +961,11 @@ class ROI(GraphicsObject): self.sigRegionChanged.emit(self) self.freeHandleMoved = False - self.lastState = self.stateCopy() + self.lastState = self.getState() if finish: self.stateChangeFinished() + self.informViewBoundsChanged() def stateChangeFinished(self): self.sigRegionChangeFinished.emit(self) @@ -988,20 +1010,18 @@ class ROI(GraphicsObject): # 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 from data covered by this ROI. - Also returns the transform which maps the ROI into data coordinates. + """Return a tuple of slice objects that can be used to slice the region + from *data* that is covered by the bounding rectangle of this ROI. + Also returns the transform that maps the ROI into data coordinates. If returnSlice is set to False, the function returns a pair of tuples with the values that would have been used to generate the slice objects. ((ax0Start, ax0Stop), (ax1Start, ax1Stop)) - If the slice can not be computed (usually because the scene/transforms are not properly + If the slice cannot be computed (usually because the scene/transforms are not properly constructed yet), then the method returns None. """ - #print "getArraySlice" - ## Determine shape of array along ROI axes dShape = (data.shape[axes[0]], data.shape[axes[1]]) - #print " dshape", dShape ## Determine transform that maps ROI bounding box to image coordinates try: @@ -1010,25 +1030,28 @@ class ROI(GraphicsObject): return None ## Modify transform to scale from image coords to data coords - #m = QtGui.QTransform() - tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) - #tr = tr * m + axisOrder = img.axisOrder + if axisOrder == 'row-major': + tr.scale(float(dShape[1]) / img.width(), float(dShape[0]) / img.height()) + else: + tr.scale(float(dShape[0]) / img.width(), float(dShape[1]) / img.height()) ## Transform ROI bounds into data bounds dataBounds = tr.mapRect(self.boundingRect()) - #print " boundingRect:", self.boundingRect() - #print " dataBounds:", dataBounds ## Intersect transformed ROI bounds with data bounds - intBounds = dataBounds.intersect(QtCore.QRectF(0, 0, dShape[0], dShape[1])) - #print " intBounds:", intBounds + if axisOrder == 'row-major': + intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[1], dShape[0])) + else: + intBounds = dataBounds.intersected(QtCore.QRectF(0, 0, dShape[0], dShape[1])) ## Determine index values to use when referencing the array. bounds = ( (int(min(intBounds.left(), intBounds.right())), int(1+max(intBounds.left(), intBounds.right()))), (int(min(intBounds.bottom(), intBounds.top())), int(1+max(intBounds.bottom(), intBounds.top()))) ) - #print " bounds:", bounds + if axisOrder == 'row-major': + bounds = bounds[::-1] if returnSlice: ## Create slice objects @@ -1052,7 +1075,10 @@ class ROI(GraphicsObject): Used to determine the relationship between the ROI and the boundaries of *data*. axes (length-2 tuple) Specifies the axes in *data* that - correspond to the x and y axes of *img*. + correspond to the (x, y) axes of *img*. If the + image's axis order is set to + 'row-major', then the axes are instead specified in + (y, x) order. returnMappedCoords (bool) If True, the array slice is returned along with a corresponding array of coordinates that were used to extract data from the original array. @@ -1061,8 +1087,8 @@ class ROI(GraphicsObject): =================== ==================================================== This method uses :func:`affineSlice <pyqtgraph.affineSlice>` to generate - the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` to determine the parameters to - pass to :func:`affineSlice <pyqtgraph.affineSlice>`. + the slice from *data* and uses :func:`getAffineSliceParams <pyqtgraph.ROI.getAffineSliceParams>` + to determine the parameters to pass to :func:`affineSlice <pyqtgraph.affineSlice>`. If *returnMappedCoords* is True, then the method returns a tuple (result, coords) such that coords is the set of coordinates used to interpolate values from the original @@ -1072,59 +1098,91 @@ class ROI(GraphicsObject): All extra keyword arguments are passed to :func:`affineSlice <pyqtgraph.affineSlice>`. """ + # this is a hidden argument for internal use + fromBR = kwds.pop('fromBoundingRect', False) - shape, vectors, origin = self.getAffineSliceParams(data, img, axes) + shape, vectors, origin = self.getAffineSliceParams(data, img, axes, fromBoundingRect=fromBR) if not returnMappedCoords: - return fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + rgn = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) + return rgn else: kwds['returnCoords'] = True result, coords = fn.affineSlice(data, shape=shape, vectors=vectors, origin=origin, axes=axes, **kwds) - #tr = fn.transformToArray(img.transform())[:2] ## remove perspective transform values - - ### separate translation from scale/rotate - #translate = tr[:,2] - #tr = tr[:,:2] - #tr = tr.reshape((2,2) + (1,)*(coords.ndim-1)) - #coords = coords[np.newaxis, ...] ### map coordinates and return - #mapped = (tr*coords).sum(axis=0) ## apply scale/rotate - #mapped += translate.reshape((2,1,1)) mapped = fn.transformCoordinates(img.transform(), coords) return result, mapped - def getAffineSliceParams(self, data, img, axes=(0,1)): + def getAffineSliceParams(self, data, img, axes=(0,1), fromBoundingRect=False): """ - Returns the parameters needed to use :func:`affineSlice <pyqtgraph.affineSlice>` to - extract a subset of *data* using this ROI and *img* to specify the subset. + Returns the parameters needed to use :func:`affineSlice <pyqtgraph.affineSlice>` + (shape, vectors, origin) to extract a subset of *data* using this ROI + and *img* to specify the subset. + + If *fromBoundingRect* is True, then the ROI's bounding rectangle is used + rather than the shape of the ROI. See :func:`getArrayRegion <pyqtgraph.ROI.getArrayRegion>` for more information. """ if self.scene() is not img.scene(): raise Exception("ROI and target item must be members of the same scene.") - shape = self.state['size'] - - origin = self.mapToItem(img, QtCore.QPointF(0, 0)) + origin = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 0))) ## vx and vy point in the directions of the slice axes, but must be scaled properly - vx = self.mapToItem(img, QtCore.QPointF(1, 0)) - origin - vy = self.mapToItem(img, QtCore.QPointF(0, 1)) - origin + vx = img.mapToData(self.mapToItem(img, QtCore.QPointF(1, 0))) - origin + vy = img.mapToData(self.mapToItem(img, QtCore.QPointF(0, 1))) - origin 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 or width of item? - #need pxWidth and pxHeight instead of pxLen ? - sx = pxLen / lvx - sy = pxLen / lvy + #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 vectors = ((vx.x()*sx, vx.y()*sx), (vy.x()*sy, vy.y()*sy)) - shape = self.state['size'] + if fromBoundingRect is True: + shape = self.boundingRect().width(), self.boundingRect().height() + origin = img.mapToData(self.mapToItem(img, self.boundingRect().topLeft())) + origin = (origin.x(), origin.y()) + else: + shape = self.state['size'] + origin = (origin.x(), origin.y()) + shape = [abs(shape[0]/sx), abs(shape[1]/sy)] - origin = (origin.x(), origin.y()) + if img.axisOrder == 'row-major': + # transpose output + vectors = vectors[::-1] + shape = shape[::-1] + return shape, vectors, origin + + def renderShapeMask(self, width, height): + """Return an array of 0.0-1.0 into which the shape of the item has been drawn. + + This can be used to mask array selections. + """ + 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) + p.setPen(fn.mkPen(None)) + p.setBrush(fn.mkBrush('w')) + shape = self.shape() + bounds = shape.boundingRect() + p.scale(im.width() / bounds.width(), im.height() / bounds.height()) + p.translate(-bounds.topLeft()) + p.drawPath(shape) + p.end() + mask = fn.imageToArray(im, transpose=True)[:,:,0].astype(float) / 255. + return mask def getGlobalTransform(self, relativeTo=None): """Return global transformation (rotation angle+translation) required to move @@ -1138,8 +1196,6 @@ class ROI(GraphicsObject): relativeTo['scale'] = relativeTo['size'] st['scale'] = st['size'] - - t1 = SRTTransform(relativeTo) t2 = SRTTransform(st) return t2/t1 @@ -1586,10 +1642,10 @@ class MultiRectROI(QtGui.QGraphicsObject): pos.append(self.mapFromScene(l.getHandles()[1].scenePos())) return pos - def getArrayRegion(self, arr, img=None, axes=(0,1)): + def getArrayRegion(self, arr, img=None, axes=(0,1), **kwds): rgns = [] for l in self.lines: - rgn = l.getArrayRegion(arr, img, axes=axes) + rgn = l.getArrayRegion(arr, img, axes=axes, **kwds) if rgn is None: continue #return None @@ -1598,6 +1654,8 @@ class MultiRectROI(QtGui.QGraphicsObject): ## make sure orthogonal axis is the same size ## (sometimes fp errors cause differences) + if img.axisOrder == 'row-major': + axes = axes[::-1] ms = min([r.shape[axes[1]] for r in rgns]) sl = [slice(None)] * rgns[0].ndim sl[axes[1]] = slice(0,ms) @@ -1659,6 +1717,7 @@ class MultiLineROI(MultiRectROI): def __init__(self, *args, **kwds): MultiRectROI.__init__(self, *args, **kwds) print("Warning: MultiLineROI has been renamed to MultiRectROI. (and MultiLineROI may be redefined in the future)") + class EllipseROI(ROI): """ @@ -1689,19 +1748,27 @@ class EllipseROI(ROI): p.drawEllipse(r) - def getArrayRegion(self, arr, img=None): + def getArrayRegion(self, arr, img=None, axes=(0, 1), **kwds): """ Return the result of ROI.getArrayRegion() masked by the elliptical shape of the ROI. Regions outside the ellipse are set to 0. """ - arr = ROI.getArrayRegion(self, arr, img) - if arr is None or arr.shape[0] == 0 or arr.shape[1] == 0: - return None - w = arr.shape[0] - h = arr.shape[1] + # Note: we could use the same method as used by PolyLineROI, but this + # implementation produces a nicer mask. + arr = ROI.getArrayRegion(self, arr, img, axes, **kwds) + if arr is None or arr.shape[axes[0]] == 0 or arr.shape[axes[1]] == 0: + 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)) - + + # reshape to match array axes + if axes[0] > axes[1]: + mask = mask.T + shape = [(n if i in axes else 1) for i,n in enumerate(arr.shape)] + mask = mask.reshape(shape) + return arr * mask def shape(self): @@ -1782,6 +1849,7 @@ class PolygonROI(ROI): #sc['handles'] = self.handles return sc + class PolyLineROI(ROI): """ Container class for multiple connected LineSegmentROIs. @@ -1811,12 +1879,6 @@ class PolyLineROI(ROI): ROI.__init__(self, pos, size=[1,1], **args) self.setPoints(positions) - #for p in positions: - #self.addFreeHandle(p) - - #start = -1 if self.closed else 0 - #for i in range(start, len(self.handles)-1): - #self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) def setPoints(self, points, closed=None): """ @@ -1834,6 +1896,8 @@ class PolyLineROI(ROI): if closed is not None: self.closed = closed + self.clearPoints() + for p in points: self.addFreeHandle(p) @@ -1841,13 +1905,18 @@ class PolyLineROI(ROI): for i in range(start, len(self.handles)-1): self.addSegment(self.handles[i]['item'], self.handles[i+1]['item']) - def clearPoints(self): """ Remove all handles and segments. """ while len(self.handles) > 0: self.removeHandle(self.handles[0]['item']) + + def getState(self): + state = ROI.getState(self) + state['closed'] = self.closed + state['points'] = [Point(h.pos()) for h in self.getHandles()] + return state def saveState(self): state = ROI.saveState(self) @@ -1857,11 +1926,10 @@ class PolyLineROI(ROI): def setState(self, state): ROI.setState(self, state) - self.clearPoints() self.setPoints(state['points'], closed=state['closed']) def addSegment(self, h1, h2, index=None): - seg = LineSegmentROI(handles=(h1, h2), pen=self.pen, parent=self, movable=False) + seg = _PolyLineSegment(handles=(h1, h2), pen=self.pen, parent=self, movable=False) if index is None: self.segments.append(seg) else: @@ -1877,11 +1945,12 @@ class PolyLineROI(ROI): ## Inform all the ROI's segments that the mouse is(not) hovering over it ROI.setMouseHover(self, hover) for s in self.segments: - s.setMouseHover(hover) + s.setParentHover(hover) def addHandle(self, info, index=None): h = ROI.addHandle(self, info, index=index) h.sigRemoveRequested.connect(self.removeHandle) + self.stateChanged(finish=True) return h def segmentClicked(self, segment, ev=None, pos=None): ## pos should be in this item's coordinate system @@ -1909,11 +1978,12 @@ class PolyLineROI(ROI): if len(segments) == 1: self.removeSegment(segments[0]) - else: + elif len(segments) > 1: handles = [h['item'] for h in segments[1].handles] handles.remove(handle) segments[0].replaceHandle(handle, handles[0]) self.removeSegment(segments[1]) + self.stateChanged(finish=True) def removeSegment(self, seg): for handle in seg.handles[:]: @@ -1930,20 +2000,10 @@ class PolyLineROI(ROI): return len(self.handles) > 2 def paint(self, p, *args): - #for s in self.segments: - #s.update() - #p.setPen(self.currentPen) - #p.setPen(fn.mkPen('w')) - #p.drawRect(self.boundingRect()) - #p.drawPath(self.shape()) pass def boundingRect(self): return self.shape().boundingRect() - #r = QtCore.QRectF() - #for h in self.handles: - #r |= self.mapFromItem(h['item'], h['item'].boundingRect()).boundingRect() ## |= gives the union of the two QRectFs - #return r def shape(self): p = QtGui.QPainterPath() @@ -1953,32 +2013,31 @@ class PolyLineROI(ROI): for i in range(len(self.handles)): p.lineTo(self.handles[i]['item'].pos()) p.lineTo(self.handles[0]['item'].pos()) - return p + return p - def getArrayRegion(self, data, img, axes=(0,1), returnMappedCoords=False, **kwds): + def getArrayRegion(self, data, img, axes=(0,1), **kwds): """ Return the result of ROI.getArrayRegion(), masked by the shape of the ROI. Values outside the ROI shape are set to 0. """ - sl = self.getArraySlice(data, img, axes=(0,1)) - if sl is None: - return None - sliced = data[sl[0]] - im = QtGui.QImage(sliced.shape[axes[0]], sliced.shape[axes[1]], QtGui.QImage.Format_ARGB32) - im.fill(0x0) - p = QtGui.QPainter(im) - p.setPen(fn.mkPen(None)) - p.setBrush(fn.mkBrush('w')) - p.setTransform(self.itemTransform(img)[0]) - bounds = self.mapRectToItem(img, self.boundingRect()) - p.translate(-bounds.left(), -bounds.top()) - p.drawPath(self.shape()) - p.end() - mask = fn.imageToArray(im)[:,:,0].astype(float) / 255. + br = self.boundingRect() + if br.width() > 1000: + raise Exception() + sliced = ROI.getArrayRegion(self, data, img, axes=axes, fromBoundingRect=True, **kwds) + + if img.axisOrder == 'col-major': + mask = self.renderShapeMask(sliced.shape[axes[0]], sliced.shape[axes[1]]) + else: + mask = self.renderShapeMask(sliced.shape[axes[1]], sliced.shape[axes[0]]) + mask = mask.T + + # reshape mask to ensure it is applied to the correct data axes shape = [1] * data.ndim shape[axes[0]] = sliced.shape[axes[0]] shape[axes[1]] = sliced.shape[axes[1]] - return sliced * mask.reshape(shape) + mask = mask.reshape(shape) + + return sliced * mask def setPen(self, *args, **kwds): ROI.setPen(self, *args, **kwds) @@ -2011,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] @@ -2021,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): @@ -2031,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 @@ -2050,7 +2109,7 @@ class LineSegmentROI(ROI): return p - def getArrayRegion(self, data, img, axes=(0,1)): + 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. @@ -2061,16 +2120,42 @@ 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=1) - 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): + # Used internally by PolyLineROI + def __init__(self, *args, **kwds): + self._parentHovering = False + LineSegmentROI.__init__(self, *args, **kwds) + + def setParentHover(self, hover): + # set independently of own hover state + if self._parentHovering != hover: + self._parentHovering = hover + self._updateHoverColor() + + def _makePen(self): + if self.mouseHovering or self._parentHovering: + return fn.mkPen(255, 255, 0) + else: + return self.pen + def hoverEvent(self, ev): + # accept drags even though we discard them to prevent competition with parent ROI + # (unless parent ROI is not movable) + if self.parentItem().translatable: + ev.acceptDrags(QtCore.Qt.LeftButton) + return LineSegmentROI.hoverEvent(self, ev) + class SpiralROI(ROI): def __init__(self, pos=None, size=None, **args): diff --git a/vviewer/pyqtgraph/graphicsItems/ScatterPlotItem.py b/vviewer/pyqtgraph/graphicsItems/ScatterPlotItem.py index faae863..54667b5 100644 --- a/vviewer/pyqtgraph/graphicsItems/ScatterPlotItem.py +++ b/vviewer/pyqtgraph/graphicsItems/ScatterPlotItem.py @@ -1,8 +1,3 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE -from ..Point import Point -from .. import functions as fn -from .GraphicsItem import GraphicsItem -from .GraphicsObject import GraphicsObject from itertools import starmap, repeat try: from itertools import imap @@ -10,26 +5,42 @@ except ImportError: imap = map import numpy as np import weakref +from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYQT5 +from ..Point import Point +from .. import functions as fn +from .GraphicsItem import GraphicsItem +from .GraphicsObject import GraphicsObject from .. import getConfigOption -from .. import debug as debug from ..pgcollections import OrderedDict from .. import debug +from ..python2_3 import basestring __all__ = ['ScatterPlotItem', 'SpotItem'] ## Build all symbol paths -Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 'd', '+', 'x']]) +Symbols = OrderedDict([(name, QtGui.QPainterPath()) for name in ['o', 's', 't', 't1', 't2', 't3','d', '+', 'x', 'p', 'h', 'star']]) Symbols['o'].addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1)) Symbols['s'].addRect(QtCore.QRectF(-0.5, -0.5, 1, 1)) coords = { 't': [(-0.5, -0.5), (0, 0.5), (0.5, -0.5)], + 't1': [(-0.5, 0.5), (0, -0.5), (0.5, 0.5)], + 't2': [(-0.5, -0.5), (-0.5, 0.5), (0.5, 0)], + 't3': [(0.5, 0.5), (0.5, -0.5), (-0.5, 0)], 'd': [(0., -0.5), (-0.4, 0.), (0, 0.5), (0.4, 0)], '+': [ (-0.5, -0.05), (-0.5, 0.05), (-0.05, 0.05), (-0.05, 0.5), - (0.05, 0.5), (0.05, 0.05), (0.5, 0.05), (0.5, -0.05), + (0.05, 0.5), (0.05, 0.05), (0.5, 0.05), (0.5, -0.05), (0.05, -0.05), (0.05, -0.5), (-0.05, -0.5), (-0.05, -0.05) ], + 'p': [(0, -0.5), (-0.4755, -0.1545), (-0.2939, 0.4045), + (0.2939, 0.4045), (0.4755, -0.1545)], + 'h': [(0.433, 0.25), (0., 0.5), (-0.433, 0.25), (-0.433, -0.25), + (0, -0.5), (0.433, -0.25)], + '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)] } for k, c in coords.items(): Symbols[k].moveTo(*c[0]) @@ -40,7 +51,7 @@ tr = QtGui.QTransform() tr.rotate(45) Symbols['x'] = tr.map(Symbols['+']) - + def drawSymbol(painter, symbol, size, pen, brush): if symbol is None: return @@ -53,13 +64,13 @@ def drawSymbol(painter, symbol, size, pen, brush): symbol = list(Symbols.values())[symbol % len(Symbols)] painter.drawPath(symbol) - + def renderSymbol(symbol, size, pen, brush, device=None): """ Render a symbol specification to QImage. Symbol may be either a QPainterPath or one of the keys in the Symbols dict. If *device* is None, a new QPixmap will be returned. Otherwise, - the symbol will be rendered into the device specified (See QPainter documentation + the symbol will be rendered into the device specified (See QPainter documentation for more information). """ ## Render a spot with the given parameters to a pixmap @@ -80,33 +91,33 @@ def makeSymbolPixmap(size, pen, brush, symbol): ## deprecated img = renderSymbol(symbol, size, pen, brush) return QtGui.QPixmap(img) - + class SymbolAtlas(object): """ Used to efficiently construct a single QPixmap containing all rendered symbols for a ScatterPlotItem. This is required for fragment rendering. - + Use example: atlas = SymbolAtlas() sc1 = atlas.getSymbolCoords('o', 5, QPen(..), QBrush(..)) sc2 = atlas.getSymbolCoords('t', 10, QPen(..), QBrush(..)) pm = atlas.getAtlas() - + """ def __init__(self): # symbol key : QRect(...) coordinates where symbol can be found in atlas. - # note that the coordinate list will always be the same list object as + # note that the coordinate list will always be the same list object as # long as the symbol is in the atlas, but the coordinates may # change if the atlas is rebuilt. - # weak value; if all external refs to this list disappear, + # weak value; if all external refs to this list disappear, # the symbol will be forgotten. self.symbolMap = weakref.WeakValueDictionary() - + self.atlasData = None # numpy array of atlas image self.atlas = None # atlas as QPixmap self.atlasValid = False self.max_width=0 - + def getSymbolCoords(self, opts): """ Given a list of spot records, return an object representing the coordinates of that symbol within the atlas @@ -131,7 +142,7 @@ class SymbolAtlas(object): keyi = key sourceRecti = newRectSrc return sourceRect - + def buildAtlas(self): # get rendered array for all symbols, keep track of avg/max width rendered = {} @@ -145,12 +156,12 @@ class SymbolAtlas(object): arr = fn.imageToArray(img, copy=False, transpose=False) else: (y,x,h,w) = sourceRect.getRect() - arr = self.atlasData[x:x+w, y:y+w] + arr = self.atlasData[int(x):int(x+w), int(y):int(y+w)] rendered[key] = arr w = arr.shape[0] avgWidth += w maxWidth = max(maxWidth, w) - + nSymbols = len(rendered) if nSymbols > 0: avgWidth /= nSymbols @@ -158,10 +169,10 @@ class SymbolAtlas(object): else: avgWidth = 0 width = 0 - + # sort symbols by height symbols = sorted(rendered.keys(), key=lambda x: rendered[x].shape[1], reverse=True) - + self.atlasRows = [] x = width @@ -180,14 +191,14 @@ class SymbolAtlas(object): self.atlasRows[-1][2] = x height = y + rowheight - self.atlasData = np.zeros((width, height, 4), dtype=np.ubyte) + self.atlasData = np.zeros((int(width), int(height), 4), dtype=np.ubyte) for key in symbols: y, x, h, w = self.symbolMap[key].getRect() - self.atlasData[x:x+w, y:y+h] = rendered[key] + self.atlasData[int(x):int(x+w), int(y):int(y+h)] = rendered[key] self.atlas = None self.atlasValid = True self.max_width = maxWidth - + def getAtlas(self): if not self.atlasValid: self.buildAtlas() @@ -197,27 +208,27 @@ class SymbolAtlas(object): img = fn.makeQImage(self.atlasData, copy=False, transpose=False) self.atlas = QtGui.QPixmap(img) return self.atlas - - - - + + + + class ScatterPlotItem(GraphicsObject): """ Displays a set of x/y points. Instances of this class are created automatically as part of PlotDataItem; these rarely need to be instantiated directly. - - The size, shape, pen, and fill brush may be set for each point individually - or for all points. - - + + The size, shape, pen, and fill brush may be set for each point individually + or for all points. + + ======================== =============================================== **Signals:** sigPlotChanged(self) Emitted when the data being plotted has changed sigClicked(self, points) Emitted when the curve is clicked. Sends a list of all the points under the mouse pointer. ======================== =============================================== - + """ #sigPointClicked = QtCore.Signal(object, object) sigClicked = QtCore.Signal(object, object) ## self, points @@ -228,17 +239,17 @@ class ScatterPlotItem(GraphicsObject): """ profiler = debug.Profiler() GraphicsObject.__init__(self) - + 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.bounds = [None, None] ## caches data bounds self._maxSpotWidth = 0 ## maximum size of the scale-variant portion of all spots self._maxSpotPxWidth = 0 ## maximum size of the scale-invariant portion of all spots self.opts = { - 'pxMode': True, - 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. + 'pxMode': True, + 'useCache': True, ## If useCache is False, symbols are re-drawn on every paint. 'antialias': getConfigOption('antialias'), 'name': None, } @@ -252,14 +263,14 @@ class ScatterPlotItem(GraphicsObject): profiler('setData') #self.setCacheMode(self.DeviceCoordinateCache) - + def setData(self, *args, **kargs): """ **Ordered Arguments:** - + * If there is only one unnamed argument, it will be interpreted like the 'spots' argument. * If there are two unnamed arguments, they will be interpreted as sequences of x and y values. - + ====================== =============================================================================================== **Keyword Arguments:** *spots* Optional list of dicts. Each dict specifies parameters for a single spot: @@ -285,8 +296,8 @@ class ScatterPlotItem(GraphicsObject): it is in the item's local coordinate system. *data* a list of python objects used to uniquely identify each spot. *identical* *Deprecated*. This functionality is handled automatically now. - *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 + *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) *name* The name of this item. Names are used for automatically generating LegendItem entries and by some exporters. @@ -298,10 +309,10 @@ class ScatterPlotItem(GraphicsObject): def addPoints(self, *args, **kargs): """ - Add new points to the scatter plot. + Add new points to the scatter plot. Arguments are the same as setData() """ - + ## deal with non-keyword arguments if len(args) == 1: kargs['spots'] = args[0] @@ -310,7 +321,7 @@ class ScatterPlotItem(GraphicsObject): kargs['y'] = args[1] elif len(args) > 2: raise Exception('Only accepts up to two non-keyword arguments.') - + ## convert 'pos' argument to 'x' and 'y' if 'pos' in kargs: pos = kargs['pos'] @@ -329,7 +340,7 @@ class ScatterPlotItem(GraphicsObject): y.append(p[1]) kargs['x'] = x kargs['y'] = y - + ## determine how many spots we have if 'spots' in kargs: numPts = len(kargs['spots']) @@ -339,16 +350,16 @@ class ScatterPlotItem(GraphicsObject): kargs['x'] = [] kargs['y'] = [] numPts = 0 - + ## Extend record array oldData = self.data self.data = np.empty(len(oldData)+numPts, dtype=self.data.dtype) ## note that np.empty initializes object fields to None and string fields to '' - + self.data[:len(oldData)] = oldData #for i in range(len(oldData)): #oldData[i]['item']._data = self.data[i] ## Make sure items have proper reference to new array - + newData = self.data[len(oldData):] newData['size'] = -1 ## indicates to use default size @@ -376,12 +387,12 @@ class ScatterPlotItem(GraphicsObject): elif 'y' in kargs: newData['x'] = kargs['x'] newData['y'] = kargs['y'] - + if 'pxMode' in kargs: self.setPxMode(kargs['pxMode']) if 'antialias' in kargs: self.opts['antialias'] = kargs['antialias'] - + ## Set any extra parameters provided in keyword arguments for k in ['pen', 'brush', 'symbol', 'size']: if k in kargs: @@ -397,32 +408,32 @@ class ScatterPlotItem(GraphicsObject): self.invalidate() self.updateSpots(newData) self.sigPlotChanged.emit(self) - + def invalidate(self): ## clear any cached drawing state self.picture = None self.update() - + def getData(self): - return self.data['x'], self.data['y'] - + return self.data['x'], self.data['y'] + def setPoints(self, *args, **kargs): ##Deprecated; use setData return 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 setPen(self, *args, **kargs): - """Set the pen(s) used to draw the outline around each spot. + """Set the pen(s) used to draw the outline around each spot. If a list or array is provided, then the pen for each spot will be set separately. - Otherwise, the arguments are passed to pg.mkPen and used as the default pen for + Otherwise, the arguments are passed to pg.mkPen and used as the default pen for all spots which do not have a pen explicitly set.""" update = kargs.pop('update', True) dataSet = kargs.pop('dataSet', self.data) @@ -436,44 +447,42 @@ class ScatterPlotItem(GraphicsObject): dataSet['pen'] = pens else: self.opts['pen'] = fn.mkPen(*args, **kargs) - + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) - + def setBrush(self, *args, **kargs): - """Set the brush(es) used to fill the interior of each spot. + """Set the brush(es) used to fill the interior of each spot. If a list or array is provided, then the brush for each spot will be set separately. - Otherwise, the arguments are passed to pg.mkBrush and used as the default brush for + Otherwise, the arguments are passed to pg.mkBrush and used as the default brush for all spots which do not have a brush explicitly set.""" update = kargs.pop('update', True) dataSet = kargs.pop('dataSet', self.data) - + if len(args) == 1 and (isinstance(args[0], np.ndarray) or isinstance(args[0], list)): brushes = args[0] if 'mask' in kargs and kargs['mask'] is not None: brushes = brushes[kargs['mask']] if len(brushes) != len(dataSet): raise Exception("Number of brushes does not match number of points (%d != %d)" % (len(brushes), len(dataSet))) - #for i in xrange(len(brushes)): - #self.data[i]['brush'] = fn.mkBrush(brushes[i], **kargs) dataSet['brush'] = brushes else: self.opts['brush'] = fn.mkBrush(*args, **kargs) #self._spotPixmap = None - + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) def setSymbol(self, symbol, update=True, dataSet=None, mask=None): - """Set the symbol(s) used to draw each spot. + """Set the symbol(s) used to draw each spot. If a list or array is provided, then the symbol for each spot will be set separately. - Otherwise, the argument will be used as the default symbol for + Otherwise, the argument will be used as the default symbol for all spots which do not have a symbol explicitly set.""" if dataSet is None: dataSet = self.data - + if isinstance(symbol, np.ndarray) or isinstance(symbol, list): symbols = symbol if mask is not None: @@ -484,19 +493,19 @@ class ScatterPlotItem(GraphicsObject): else: self.opts['symbol'] = symbol self._spotPixmap = None - + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) - + def setSize(self, size, update=True, dataSet=None, mask=None): - """Set the size(s) used to draw each spot. + """Set the size(s) used to draw each spot. If a list or array is provided, then the size for each spot will be set separately. - Otherwise, the argument will be used as the default size for + Otherwise, the argument will be used as the default size for all spots which do not have a size explicitly set.""" if dataSet is None: dataSet = self.data - + if isinstance(size, np.ndarray) or isinstance(size, list): sizes = size if mask is not None: @@ -507,21 +516,21 @@ class ScatterPlotItem(GraphicsObject): else: self.opts['size'] = size self._spotPixmap = None - + dataSet['sourceRect'] = None if update: self.updateSpots(dataSet) - + def setPointData(self, data, dataSet=None, mask=None): if dataSet is None: dataSet = self.data - + if isinstance(data, np.ndarray) or isinstance(data, list): if mask is not None: data = data[mask] if len(data) != len(dataSet): raise Exception("Length of meta data does not match number of points (%d != %d)" % (len(data), len(dataSet))) - + ## Bug: If data is a numpy record array, then items from that array must be copied to dataSet one at a time. ## (otherwise they are converted to tuples and thus lose their field names. if isinstance(data, np.ndarray) and (data.dtype.fields is not None)and len(data.dtype.fields) > 1: @@ -529,14 +538,14 @@ class ScatterPlotItem(GraphicsObject): dataSet['data'][i] = rec else: dataSet['data'] = data - + def setPxMode(self, mode): if self.opts['pxMode'] == mode: return - + self.opts['pxMode'] = mode self.invalidate() - + def updateSpots(self, dataSet=None): if dataSet is None: dataSet = self.data @@ -549,9 +558,9 @@ class ScatterPlotItem(GraphicsObject): opts = self.getSpotOpts(dataSet[mask]) sourceRect = self.fragmentAtlas.getSymbolCoords(opts) dataSet['sourceRect'][mask] = sourceRect - + self.fragmentAtlas.getAtlas() # generate atlas so source widths are available. - + dataSet['width'] = np.array(list(imap(QtCore.QRectF.width, dataSet['sourceRect'])))/2 dataSet['targetRect'] = None self._maxSpotPxWidth = self.fragmentAtlas.max_width @@ -587,9 +596,9 @@ class ScatterPlotItem(GraphicsObject): recs['pen'][np.equal(recs['pen'], None)] = fn.mkPen(self.opts['pen']) recs['brush'][np.equal(recs['brush'], None)] = fn.mkBrush(self.opts['brush']) return recs - - - + + + def measureSpotSizes(self, dataSet): for rec in dataSet: ## keep track of the maximum spot size and pixel size @@ -607,8 +616,8 @@ class ScatterPlotItem(GraphicsObject): self._maxSpotWidth = max(self._maxSpotWidth, width) self._maxSpotPxWidth = max(self._maxSpotPxWidth, pxWidth) self.bounds = [None, None] - - + + def clear(self): """Remove all spots from the scatter plot""" #self.clearItems() @@ -619,23 +628,23 @@ class ScatterPlotItem(GraphicsObject): def dataBounds(self, ax, frac=1.0, orthoRange=None): if frac >= 1.0 and orthoRange is None and self.bounds[ax] is not None: return self.bounds[ax] - + #self.prepareGeometryChange() if self.data is None or len(self.data) == 0: return (None, None) - + if ax == 0: d = self.data['x'] d2 = self.data['y'] elif ax == 1: d = self.data['y'] d2 = self.data['x'] - + if orthoRange is not None: mask = (d2 >= orthoRange[0]) * (d2 <= orthoRange[1]) d = d[mask] d2 = d2[mask] - + 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] @@ -658,11 +667,11 @@ class ScatterPlotItem(GraphicsObject): if ymn is None or ymx is None: ymn = 0 ymx = 0 - + 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() @@ -672,7 +681,7 @@ class ScatterPlotItem(GraphicsObject): py = 0 if py is None else py.length() except OverflowError: py = 0 - + # return bounds expanded by pixel size px *= pxPad py *= pxPad @@ -690,7 +699,7 @@ class ScatterPlotItem(GraphicsObject): def mapPointsToDevice(self, pts): - # Map point locations to device + # Map point locations to device tr = self.deviceTransform() if tr is None: return None @@ -701,7 +710,7 @@ class ScatterPlotItem(GraphicsObject): pts = fn.transformCoordinates(tr, pts) pts -= self.data['width'] pts = np.clip(pts, -2**30, 2**30) ## prevent Qt segmentation fault. - + return pts def getViewMask(self, pts): @@ -715,50 +724,50 @@ class ScatterPlotItem(GraphicsObject): mask = ((pts[0] + w > viewBounds.left()) & (pts[0] - w < viewBounds.right()) & (pts[1] + w > viewBounds.top()) & - (pts[1] - w < viewBounds.bottom())) ## remove out of view points + (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): #p.setPen(fn.mkPen('r')) #p.drawRect(self.boundingRect()) - + if self._exportOpts is not False: aa = self._exportOpts.get('antialias', True) scale = self._exportOpts.get('resolutionScale', 1.0) ## exporting to image; pixel resolution may have changed else: aa = self.opts['antialias'] scale = 1.0 - + if self.opts['pxMode'] is True: p.resetTransform() - + # Map point coordinates to device pts = np.vstack([self.data['x'], self.data['y']]) pts = self.mapPointsToDevice(pts) if pts is None: return - + # 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 atlas = self.fragmentAtlas.getAtlas() - + # Update targetRects if necessary updateMask = viewMask & np.equal(self.data['targetRect'], 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)) - + data = self.data[viewMask] - if USE_PYSIDE: + if USE_PYSIDE or USE_PYQT5: list(imap(p.drawPixmap, data['targetRect'], repeat(atlas), data['sourceRect'])) else: p.drawPixmapFragments(data['targetRect'].tolist(), data['sourceRect'].tolist(), atlas) @@ -784,16 +793,16 @@ class ScatterPlotItem(GraphicsObject): p2.translate(rec['x'], rec['y']) drawSymbol(p2, *self.getSpotOpts(rec, scale)) p2.end() - + p.setRenderHint(p.Antialiasing, aa) self.picture.play(p) - + def points(self): for rec in self.data: if rec['item'] is None: rec['item'] = SpotItem(rec, self) return self.data['item'] - + def pointsAt(self, pos): x = pos.x() y = pos.y() @@ -815,9 +824,8 @@ class ScatterPlotItem(GraphicsObject): #else: #print "No hit:", (x, y), (sx, sy) #print " ", (sx-s2x, sy-s2y), (sx+s2x, sy+s2y) - #pts.sort(lambda a,b: cmp(b.zValue(), a.zValue())) return pts[::-1] - + def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: @@ -836,7 +844,7 @@ class ScatterPlotItem(GraphicsObject): class SpotItem(object): """ Class referring to individual spots in a scatter plot. - These can be retrieved by calling ScatterPlotItem.points() or + These can be retrieved by calling ScatterPlotItem.points() or by connecting to the ScatterPlotItem's click signals. """ @@ -847,34 +855,34 @@ class SpotItem(object): #self.setParentItem(plot) #self.setPos(QtCore.QPointF(data['x'], data['y'])) #self.updateItem() - + def data(self): """Return the user data associated with this spot.""" return self._data['data'] - + def size(self): - """Return the size of this spot. + """Return the size of this spot. If the spot has no explicit size set, then return the ScatterPlotItem's default size instead.""" if self._data['size'] == -1: return self._plot.opts['size'] else: return self._data['size'] - + def pos(self): return Point(self._data['x'], self._data['y']) - + def viewPos(self): return self._plot.mapToView(self.pos()) - + def setSize(self, size): - """Set the size of this spot. - If the size is set to -1, then the ScatterPlotItem's default size + """Set the size of this spot. + If the size is set to -1, then the ScatterPlotItem's default size will be used instead.""" self._data['size'] = size self.updateItem() - + def symbol(self): - """Return the symbol of this spot. + """Return the symbol of this spot. If the spot has no explicit symbol set, then return the ScatterPlotItem's default symbol instead. """ symbol = self._data['symbol'] @@ -886,7 +894,7 @@ class SpotItem(object): except: pass return symbol - + def setSymbol(self, symbol): """Set the symbol for this spot. If the symbol is set to '', then the ScatterPlotItem's default symbol will be used instead.""" @@ -898,35 +906,35 @@ class SpotItem(object): if pen is None: pen = self._plot.opts['pen'] return fn.mkPen(pen) - + def setPen(self, *args, **kargs): """Set the outline pen for this spot""" pen = fn.mkPen(*args, **kargs) self._data['pen'] = pen self.updateItem() - + def resetPen(self): """Remove the pen set for this spot; the scatter plot's default pen will be used instead.""" self._data['pen'] = None ## Note this is NOT the same as calling setPen(None) self.updateItem() - + def brush(self): brush = self._data['brush'] if brush is None: brush = self._plot.opts['brush'] return fn.mkBrush(brush) - + def setBrush(self, *args, **kargs): """Set the fill brush for this spot""" brush = fn.mkBrush(*args, **kargs) self._data['brush'] = brush self.updateItem() - + def resetBrush(self): """Remove the brush set for this spot; the scatter plot's default brush will be used instead.""" self._data['brush'] = None ## Note this is NOT the same as calling setBrush(None) self.updateItem() - + def setData(self, data): """Set the user-data associated with this spot""" self._data['data'] = data @@ -941,14 +949,14 @@ class SpotItem(object): #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() diff --git a/vviewer/pyqtgraph/graphicsItems/TextItem.py b/vviewer/pyqtgraph/graphicsItems/TextItem.py index d3c9800..b2587de 100644 --- a/vviewer/pyqtgraph/graphicsItems/TextItem.py +++ b/vviewer/pyqtgraph/graphicsItems/TextItem.py @@ -1,13 +1,16 @@ +import numpy as np from ..Qt import QtCore, QtGui from ..Point import Point -from .UIGraphicsItem import * from .. import functions as fn +from .GraphicsObject import GraphicsObject -class TextItem(UIGraphicsItem): + +class TextItem(GraphicsObject): """ GraphicsItem displaying unscaled text (the text will always appear normal even inside a scaled ViewBox). """ - def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), border=None, fill=None, angle=0): + def __init__(self, text='', color=(200,200,200), html=None, anchor=(0,0), + border=None, fill=None, angle=0, rotateAxis=None): """ ============== ================================================================================= **Arguments:** @@ -20,46 +23,52 @@ class TextItem(UIGraphicsItem): sets the lower-right corner. *border* A pen to use when drawing the border *fill* A brush to use when filling within the border + *angle* Angle in degrees to rotate text. Default is 0; text will be displayed upright. + *rotateAxis* If None, then a text angle of 0 always points along the +x axis of the scene. + If a QPointF or (x,y) sequence is given, then it represents a vector direction + in the parent's coordinate system that the 0-degree line will be aligned to. This + Allows text to follow both the position and orientation of its parent while still + discarding any scale and shear factors. ============== ================================================================================= + + + The effects of the `rotateAxis` and `angle` arguments are added independently. So for example: + + * rotateAxis=None, angle=0 -> normal horizontal text + * rotateAxis=None, angle=90 -> normal vertical text + * rotateAxis=(1, 0), angle=0 -> text aligned with x axis of its parent + * rotateAxis=(0, 1), angle=0 -> text aligned with y axis of its parent + * rotateAxis=(1, 0), angle=90 -> text orthogonal to x axis of its parent """ - - ## not working yet - #*angle* Angle in degrees to rotate text (note that the rotation assigned in this item's - #transformation will be ignored) self.anchor = Point(anchor) + self.rotateAxis = None if rotateAxis is None else Point(rotateAxis) #self.angle = 0 - UIGraphicsItem.__init__(self) + GraphicsObject.__init__(self) self.textItem = QtGui.QGraphicsTextItem() self.textItem.setParentItem(self) - self.lastTransform = None + self._lastTransform = None + self._lastScene = None self._bounds = QtCore.QRectF() if html is None: - self.setText(text, color) + self.setColor(color) + self.setText(text) else: self.setHtml(html) self.fill = fn.mkBrush(fill) self.border = fn.mkPen(border) - self.rotate(angle) - self.setFlag(self.ItemIgnoresTransformations) ## This is required to keep the text unscaled inside the viewport + self.setAngle(angle) - def setText(self, text, color=(200,200,200)): + def setText(self, text, color=None): """ - Set the text and color of this item. + Set the text of this item. This method sets the plain text of the item; see also setHtml(). """ - color = fn.mkColor(color) - self.textItem.setDefaultTextColor(color) + if color is not None: + self.setColor(color) self.textItem.setPlainText(text) - self.updateText() - #html = '<span style="color: #%s; text-align: center;">%s</span>' % (color, text) - #self.setHtml(html) - - def updateAnchor(self): - pass - #self.resetTransform() - #self.translate(0, 20) + self.updateTextPos() def setPlainText(self, *args): """ @@ -68,7 +77,7 @@ class TextItem(UIGraphicsItem): See QtGui.QGraphicsTextItem.setPlainText(). """ self.textItem.setPlainText(*args) - self.updateText() + self.updateTextPos() def setHtml(self, *args): """ @@ -77,7 +86,7 @@ class TextItem(UIGraphicsItem): See QtGui.QGraphicsTextItem.setHtml(). """ self.textItem.setHtml(*args) - self.updateText() + self.updateTextPos() def setTextWidth(self, *args): """ @@ -89,7 +98,7 @@ class TextItem(UIGraphicsItem): See QtGui.QGraphicsTextItem.setTextWidth(). """ self.textItem.setTextWidth(*args) - self.updateText() + self.updateTextPos() def setFont(self, *args): """ @@ -98,50 +107,61 @@ class TextItem(UIGraphicsItem): See QtGui.QGraphicsTextItem.setFont(). """ self.textItem.setFont(*args) - self.updateText() - - #def setAngle(self, angle): - #self.angle = angle - #self.updateText() - - - def updateText(self): - - ## Needed to maintain font size when rendering to image with increased resolution - self.textItem.resetTransform() - #self.textItem.rotate(self.angle) - if self._exportOpts is not False and 'resolutionScale' in self._exportOpts: - s = self._exportOpts['resolutionScale'] - self.textItem.scale(s, s) - - #br = self.textItem.mapRectToParent(self.textItem.boundingRect()) - self.textItem.setPos(0,0) - br = self.textItem.boundingRect() - apos = self.textItem.mapToParent(Point(br.width()*self.anchor.x(), br.height()*self.anchor.y())) - #print br, apos - self.textItem.setPos(-apos.x(), -apos.y()) - - #def textBoundingRect(self): - ### return the bounds of the text box in device coordinates - #pos = self.mapToDevice(QtCore.QPointF(0,0)) - #if pos is None: - #return None - #tbr = self.textItem.boundingRect() - #return QtCore.QRectF(pos.x() - tbr.width()*self.anchor.x(), pos.y() - tbr.height()*self.anchor.y(), tbr.width(), tbr.height()) - - - def viewRangeChanged(self): - self.updateText() + self.updateTextPos() + + def setAngle(self, angle): + self.angle = angle + self.updateTransform() + + def setAnchor(self, anchor): + self.anchor = Point(anchor) + self.updateTextPos() + def setColor(self, color): + """ + Set the color for this text. + + See QtGui.QGraphicsItem.setDefaultTextColor(). + """ + self.color = fn.mkColor(color) + self.textItem.setDefaultTextColor(self.color) + + def updateTextPos(self): + # update text position to obey anchor + r = self.textItem.boundingRect() + tl = self.textItem.mapToParent(r.topLeft()) + br = self.textItem.mapToParent(r.bottomRight()) + offset = (br - tl) * self.anchor + self.textItem.setPos(-offset) + + ### Needed to maintain font size when rendering to image with increased resolution + #self.textItem.resetTransform() + ##self.textItem.rotate(self.angle) + #if self._exportOpts is not False and 'resolutionScale' in self._exportOpts: + #s = self._exportOpts['resolutionScale'] + #self.textItem.scale(s, s) + def boundingRect(self): return self.textItem.mapToParent(self.textItem.boundingRect()).boundingRect() + + def viewTransformChanged(self): + # called whenever view transform has changed. + # Do this here to avoid double-updates when view changes. + self.updateTransform() def paint(self, p, *args): - tr = p.transform() - if self.lastTransform is not None: - if tr != self.lastTransform: - self.viewRangeChanged() - self.lastTransform = tr + # this is not ideal because it requires the transform to be updated at every draw. + # ideally, we would have a sceneTransformChanged event to react to.. + s = self.scene() + ls = self._lastScene + if s is not ls: + if ls is not None: + ls.sigPrepareForPaint.disconnect(self.updateTransform) + self._lastScene = s + if s is not None: + s.sigPrepareForPaint.connect(self.updateTransform) + self.updateTransform() + p.setTransform(self.sceneTransform()) if self.border.style() != QtCore.Qt.NoPen or self.fill.style() != QtCore.Qt.NoBrush: p.setPen(self.border) @@ -149,4 +169,35 @@ class TextItem(UIGraphicsItem): p.setRenderHint(p.Antialiasing, True) p.drawPolygon(self.textItem.mapToParent(self.textItem.boundingRect())) - \ No newline at end of file + def updateTransform(self): + # update transform such that this item has the correct orientation + # and scaling relative to the scene, but inherits its position from its + # parent. + # This is similar to setting ItemIgnoresTransformations = True, but + # does not break mouse interaction and collision detection. + p = self.parentItem() + if p is None: + pt = QtGui.QTransform() + else: + pt = p.sceneTransform() + + if pt == self._lastTransform: + return + + t = pt.inverted()[0] + # reset translation + t.setMatrix(t.m11(), t.m12(), t.m13(), t.m21(), t.m22(), t.m23(), 0, 0, t.m33()) + + # apply rotation + angle = -self.angle + if self.rotateAxis is not None: + d = pt.map(self.rotateAxis) - pt.map(Point(0, 0)) + a = np.arctan2(d.y(), d.x()) * 180 / np.pi + angle += a + t.rotate(angle) + + self.setTransform(t) + + self._lastTransform = pt + + self.updateTextPos() diff --git a/vviewer/pyqtgraph/graphicsItems/ViewBox/ViewBox.py b/vviewer/pyqtgraph/graphicsItems/ViewBox/ViewBox.py index 022a967..007ff8d 100644 --- a/vviewer/pyqtgraph/graphicsItems/ViewBox/ViewBox.py +++ b/vviewer/pyqtgraph/graphicsItems/ViewBox/ViewBox.py @@ -1,15 +1,15 @@ -from ...Qt import QtGui, QtCore -from ...python2_3 import sortList +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 ...Point import Point from ... import functions as fn from .. ItemGroup import ItemGroup from .. GraphicsWidget import GraphicsWidget -import weakref -from copy import deepcopy from ... import debug as debug from ... import getConfigOption -import sys from ...Qt import isQObjectAlive __all__ = ['ViewBox'] @@ -226,6 +226,7 @@ class ViewBox(GraphicsWidget): else: self.menu = MyViewBoxMenu2(self) + def register(self, name): """ Add this ViewBox to the registered list of views. @@ -1048,7 +1049,6 @@ class ViewBox(GraphicsWidget): finally: view.blockLink(False) - def screenGeometry(self): """return the screen geometry of the viewbox""" v = self.getViewWidget() @@ -1060,8 +1060,6 @@ class ViewBox(GraphicsWidget): wr.adjust(pos.x(), pos.y(), pos.x(), pos.y()) return wr - - def itemsChanged(self): ## called when items are added/removed from self.childGroup self.updateAutoRange() @@ -1073,18 +1071,23 @@ class ViewBox(GraphicsWidget): self.update() #self.updateAutoRange() - def invertY(self, b=True): - """ - By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis. - """ - if self.state['yInverted'] == b: + def _invertAxis(self, ax, inv): + key = 'xy'[ax] + 'Inverted' + if self.state[key] == inv: return - self.state['yInverted'] = b + self.state[key] = inv self._matrixNeedsUpdate = True # updateViewRange won't detect this for us self.updateViewRange() + self.update() self.sigStateChanged.emit(self) - self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) + self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][ax])) + + def invertY(self, b=True): + """ + By default, the positive y-axis points upward on the screen. Use invertY(True) to reverse the y-axis. + """ + self._invertAxis(1, b) def yInverted(self): return self.state['yInverted'] @@ -1093,14 +1096,7 @@ class ViewBox(GraphicsWidget): """ By default, the positive x-axis points rightward on the screen. Use invertX(True) to reverse the x-axis. """ - if self.state['xInverted'] == b: - return - - self.state['xInverted'] = b - #self.updateMatrix(changed=(False, True)) - self.updateViewRange() - self.sigStateChanged.emit(self) - self.sigXRangeChanged.emit(self, tuple(self.state['viewRange'][0])) + self._invertAxis(0, b) def xInverted(self): return self.state['xInverted'] diff --git a/vviewer/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py b/vviewer/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py index 0e7d791..10392d7 100644 --- a/vviewer/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py +++ b/vviewer/pyqtgraph/graphicsItems/ViewBox/ViewBoxMenu.py @@ -1,12 +1,14 @@ -from ...Qt import QtCore, QtGui, USE_PYSIDE +from ...Qt import QtCore, QtGui, QT_LIB from ...python2_3 import asUnicode from ...WidgetGroup import WidgetGroup -if USE_PYSIDE: - from .axisCtrlTemplate_pyside import Ui_Form as AxisCtrlTemplate -else: +if QT_LIB == 'PyQt4': from .axisCtrlTemplate_pyqt import Ui_Form as AxisCtrlTemplate - +elif QT_LIB == 'PySide': + from .axisCtrlTemplate_pyside import Ui_Form as AxisCtrlTemplate +elif QT_LIB == 'PyQt5': + from .axisCtrlTemplate_pyqt5 import Ui_Form as AxisCtrlTemplate + import weakref class ViewBoxMenu(QtGui.QMenu): diff --git a/vviewer/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py b/vviewer/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py index d8ef192..5d95274 100644 --- a/vviewer/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py +++ b/vviewer/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt.py @@ -7,7 +7,7 @@ # # WARNING! All changes made in this file will be lost! -from PyQt4 import QtCore, QtGui +from ...Qt import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 diff --git a/vviewer/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py b/vviewer/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.py new file mode 100644 index 0000000..78da6ee --- /dev/null +++ b/vviewer/pyqtgraph/graphicsItems/ViewBox/axisCtrlTemplate_pyqt5.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 PyQt5 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", "<html><head/><body><p>Links this axis with another view. When linked, both views will display the same data range.</p></body></html>")) + self.autoPercentSpin.setToolTip(_translate("Form", "<html><head/><body><p>Percent of data to be visible when auto-scaling. It may be useful to decrease this value for data with spiky noise.</p></body></html>")) + self.autoPercentSpin.setSuffix(_translate("Form", "%")) + self.autoRadio.setToolTip(_translate("Form", "<html><head/><body><p>Automatically resize this axis whenever the displayed data is changed.</p></body></html>")) + self.autoRadio.setText(_translate("Form", "Auto")) + self.manualRadio.setToolTip(_translate("Form", "<html><head/><body><p>Set the range for this axis manually. This disables automatic scaling. </p></body></html>")) + self.manualRadio.setText(_translate("Form", "Manual")) + self.minText.setToolTip(_translate("Form", "<html><head/><body><p>Minimum value to display for this axis.</p></body></html>")) + self.minText.setText(_translate("Form", "0")) + self.maxText.setToolTip(_translate("Form", "<html><head/><body><p>Maximum value to display for this axis.</p></body></html>")) + self.maxText.setText(_translate("Form", "0")) + self.invertCheck.setToolTip(_translate("Form", "<html><head/><body><p>Inverts the display of this axis. (+y points downward instead of upward)</p></body></html>")) + self.invertCheck.setText(_translate("Form", "Invert Axis")) + self.mouseCheck.setToolTip(_translate("Form", "<html><head/><body><p>Enables mouse interaction (panning, scaling) for this axis.</p></body></html>")) + self.mouseCheck.setText(_translate("Form", "Mouse Enabled")) + self.visibleOnlyCheck.setToolTip(_translate("Form", "<html><head/><body><p>When checked, the axis will only auto-scale to data that is visible along the orthogonal axis.</p></body></html>")) + self.visibleOnlyCheck.setText(_translate("Form", "Visible Data Only")) + self.autoPanCheck.setToolTip(_translate("Form", "<html><head/><body><p>When checked, the axis will automatically pan to center on the current data, but the scale along this axis will not change.</p></body></html>")) + self.autoPanCheck.setText(_translate("Form", "Auto Pan Only")) + diff --git a/vviewer/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py b/vviewer/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py index f1063e7..68f4f49 100644 --- a/vviewer/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py +++ b/vviewer/pyqtgraph/graphicsItems/ViewBox/tests/test_ViewBox.py @@ -1,8 +1,10 @@ #import PySide import pyqtgraph as pg +import pytest app = pg.mkQApp() qtest = pg.Qt.QtTest.QTest +QRectF = pg.QtCore.QRectF def assertMapping(vb, r1, r2): assert vb.mapFromView(r1.topLeft()) == r2.topLeft() @@ -10,9 +12,10 @@ def assertMapping(vb, r1, r2): assert vb.mapFromView(r1.topRight()) == r2.topRight() assert vb.mapFromView(r1.bottomRight()) == r2.bottomRight() -def test_ViewBox(): - global app, win, vb - QRectF = pg.QtCore.QRectF +def init_viewbox(): + """Helper function to init the ViewBox + """ + global win, vb win = pg.GraphicsWindow() win.ci.layout.setContentsMargins(0,0,0,0) @@ -31,6 +34,9 @@ def test_ViewBox(): app.processEvents() +def test_ViewBox(): + init_viewbox() + w = vb.geometry().width() h = vb.geometry().height() view1 = QRectF(0, 0, 10, 10) @@ -65,7 +71,15 @@ def test_ViewBox(): view1 = QRectF(0, -5, 10, 20) size1 = QRectF(0, h, w, -h) assertMapping(vb, view1, size1) - + + +skipreason = "Skipping this test until someone has time to fix it." +@pytest.mark.skipif(True, reason=skipreason) +def test_limits_and_resize(): + init_viewbox() + + # now lock aspect + vb.setAspectLocked() # test limits + resize (aspect ratio constraint has priority over limits win.resize(400, 400) app.processEvents() @@ -77,9 +91,3 @@ def test_ViewBox(): view1 = QRectF(-5, 0, 20, 10) size1 = QRectF(0, h, w, -h) assertMapping(vb, view1, size1) - - -if __name__ == '__main__': - import user,sys - test_ViewBox() - \ No newline at end of file diff --git a/vviewer/pyqtgraph/graphicsItems/tests/test_ImageItem.py b/vviewer/pyqtgraph/graphicsItems/tests/test_ImageItem.py new file mode 100644 index 0000000..4f310bc --- /dev/null +++ b/vviewer/pyqtgraph/graphicsItems/tests/test_ImageItem.py @@ -0,0 +1,147 @@ +import time +import pytest +from pyqtgraph.Qt import QtCore, QtGui, QtTest +import numpy as np +import pyqtgraph as pg +from pyqtgraph.tests import assertImageApproved, TransposedImageItem + +app = pg.mkQApp() + + +def test_ImageItem(transpose=False): + + w = pg.GraphicsWindow() + view = pg.ViewBox() + w.setCentralWidget(view) + w.resize(200, 200) + w.show() + img = TransposedImageItem(border=0.5, transpose=transpose) + + view.addItem(img) + + # test mono float + np.random.seed(0) + data = np.random.normal(size=(20, 20)) + dmax = data.max() + data[:10, 1] = dmax + 10 + data[1, :10] = dmax + 12 + data[3, :10] = dmax + 13 + img.setImage(data) + + QtTest.QTest.qWaitForWindowShown(w) + time.sleep(0.1) + app.processEvents() + assertImageApproved(w, 'imageitem/init', 'Init image item. View is auto-scaled, image axis 0 marked by 1 line, axis 1 is marked by 2 lines. Origin in bottom-left.') + + # ..with colormap + cmap = pg.ColorMap([0, 0.25, 0.75, 1], [[0, 0, 0, 255], [255, 0, 0, 255], [255, 255, 0, 255], [255, 255, 255, 255]]) + img.setLookupTable(cmap.getLookupTable()) + assertImageApproved(w, 'imageitem/lut', 'Set image LUT.') + + # ..and different levels + img.setLevels([dmax+9, dmax+13]) + assertImageApproved(w, 'imageitem/levels1', 'Levels show only axis lines.') + + img.setLookupTable(None) + + # test mono int + data = np.fromfunction(lambda x,y: x+y*10, (129, 128)).astype(np.int16) + img.setImage(data) + assertImageApproved(w, 'imageitem/gradient_mono_int', 'Mono int gradient.') + + img.setLevels([640, 641]) + assertImageApproved(w, 'imageitem/gradient_mono_int_levels', 'Mono int gradient w/ levels to isolate diagonal.') + + # test mono byte + data = np.fromfunction(lambda x,y: x+y, (129, 128)).astype(np.ubyte) + img.setImage(data) + assertImageApproved(w, 'imageitem/gradient_mono_byte', 'Mono byte gradient.') + + img.setLevels([127, 128]) + assertImageApproved(w, 'imageitem/gradient_mono_byte_levels', 'Mono byte gradient w/ levels to isolate diagonal.') + + # test monochrome image + data = np.zeros((10, 10), dtype='uint8') + data[:5,:5] = 1 + data[5:,5:] = 1 + img.setImage(data) + assertImageApproved(w, 'imageitem/monochrome', 'Ubyte image with only 0,1 values.') + + # test bool + data = data.astype(bool) + img.setImage(data) + assertImageApproved(w, 'imageitem/bool', 'Boolean mask.') + + # test RGBA byte + data = np.zeros((100, 100, 4), dtype='ubyte') + data[..., 0] = np.linspace(0, 255, 100).reshape(100, 1) + data[..., 1] = np.linspace(0, 255, 100).reshape(1, 100) + data[..., 3] = 255 + img.setImage(data) + assertImageApproved(w, 'imageitem/gradient_rgba_byte', 'RGBA byte gradient.') + + img.setLevels([[128, 129], [128, 255], [0, 1], [0, 255]]) + assertImageApproved(w, 'imageitem/gradient_rgba_byte_levels', 'RGBA byte gradient. Levels set to show x=128 and y>128.') + + # test RGBA float + data = data.astype(float) + img.setImage(data / 1e9) + assertImageApproved(w, 'imageitem/gradient_rgba_float', 'RGBA float gradient.') + + # checkerboard to test alpha + img2 = TransposedImageItem(transpose=transpose) + img2.setImage(np.fromfunction(lambda x,y: (x+y)%2, (10, 10)), levels=[-1,2]) + view.addItem(img2) + img2.scale(10, 10) + img2.setZValue(-10) + + data[..., 0] *= 1e-9 + data[..., 1] *= 1e9 + data[..., 3] = np.fromfunction(lambda x,y: np.sin(0.1 * (x+y)), (100, 100)) + img.setImage(data, levels=[[0, 128e-9],[0, 128e9],[0, 1],[-1, 1]]) + assertImageApproved(w, 'imageitem/gradient_rgba_float_alpha', 'RGBA float gradient with alpha.') + + # test composition mode + img.setCompositionMode(QtGui.QPainter.CompositionMode_Plus) + assertImageApproved(w, 'imageitem/gradient_rgba_float_additive', 'RGBA float gradient with alpha and additive composition mode.') + + img2.hide() + img.setCompositionMode(QtGui.QPainter.CompositionMode_SourceOver) + + # test downsampling + data = np.fromfunction(lambda x,y: np.cos(0.002 * x**2), (800, 100)) + img.setImage(data, levels=[-1, 1]) + assertImageApproved(w, 'imageitem/resolution_without_downsampling', 'Resolution test without downsampling.') + + img.setAutoDownsample(True) + assertImageApproved(w, 'imageitem/resolution_with_downsampling_x', 'Resolution test with downsampling axross x axis.') + assert img._lastDownsample == (4, 1) + + img.setImage(data.T, levels=[-1, 1]) + assertImageApproved(w, 'imageitem/resolution_with_downsampling_y', 'Resolution test with downsampling across y axis.') + assert img._lastDownsample == (1, 4) + + view.hide() + +def test_ImageItem_axisorder(): + # All image tests pass again using the opposite axis order + origMode = pg.getConfigOption('imageAxisOrder') + altMode = 'row-major' if origMode == 'col-major' else 'col-major' + pg.setConfigOptions(imageAxisOrder=altMode) + try: + test_ImageItem(transpose=True) + finally: + pg.setConfigOptions(imageAxisOrder=origMode) + + +@pytest.mark.skipif(pg.Qt.USE_PYSIDE, reason="pyside does not have qWait") +def test_dividebyzero(): + import pyqtgraph as pg + im = pg.image(pg.np.random.normal(size=(100,100))) + im.imageItem.setAutoDownsample(True) + im.view.setRange(xRange=[-5+25, 5e+25],yRange=[-5e+25, 5e+25]) + app.processEvents() + QtTest.QTest.qWait(1000) + # must manually call im.imageItem.render here or the exception + # will only exist on the Qt event loop + im.imageItem.render() diff --git a/vviewer/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py b/vviewer/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py new file mode 100644 index 0000000..2443886 --- /dev/null +++ b/vviewer/pyqtgraph/graphicsItems/tests/test_InfiniteLine.py @@ -0,0 +1,96 @@ +import pyqtgraph as pg +from pyqtgraph.Qt import QtGui, QtCore, QtTest +from pyqtgraph.tests import mouseDrag, mouseMove +pg.mkQApp() + + +def test_InfiniteLine(): + # Test basic InfiniteLine API + plt = pg.plot() + plt.setXRange(-10, 10) + plt.setYRange(-10, 10) + plt.resize(600, 600) + + # seemingly arbitrary requirements; might need longer wait time for some platforms.. + QtTest.QTest.qWaitForWindowShown(plt) + QtTest.QTest.qWait(100) + + vline = plt.addLine(x=1) + assert vline.angle == 90 + br = vline.mapToView(QtGui.QPolygonF(vline.boundingRect())) + assert br.containsPoint(pg.Point(1, 5), QtCore.Qt.OddEvenFill) + assert not br.containsPoint(pg.Point(5, 0), QtCore.Qt.OddEvenFill) + hline = plt.addLine(y=0) + assert hline.angle == 0 + assert hline.boundingRect().contains(pg.Point(5, 0)) + assert not hline.boundingRect().contains(pg.Point(0, 5)) + + vline.setValue(2) + assert vline.value() == 2 + vline.setPos(pg.Point(4, -5)) + assert vline.value() == 4 + + oline = pg.InfiniteLine(angle=30) + plt.addItem(oline) + oline.setPos(pg.Point(1, -1)) + assert oline.angle == 30 + assert oline.pos() == pg.Point(1, -1) + assert oline.value() == [1, -1] + + # test bounding rect for oblique line + br = oline.mapToScene(oline.boundingRect()) + pos = oline.mapToScene(pg.Point(2, 0)) + assert br.containsPoint(pos, QtCore.Qt.OddEvenFill) + px = pg.Point(-0.5, -1.0 / 3**0.5) + assert br.containsPoint(pos + 5 * px, QtCore.Qt.OddEvenFill) + assert not br.containsPoint(pos + 7 * px, QtCore.Qt.OddEvenFill) + + +def test_mouseInteraction(): + plt = pg.plot() + plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly. + vline = plt.addLine(x=0, movable=True) + plt.addItem(vline) + hline = plt.addLine(y=0, movable=True) + hline2 = plt.addLine(y=-1, movable=False) + plt.setXRange(-10, 10) + plt.setYRange(-10, 10) + + # test horizontal drag + pos = plt.plotItem.vb.mapViewToScene(pg.Point(0,5)).toPoint() + pos2 = pos - QtCore.QPoint(200, 200) + mouseMove(plt, pos) + assert vline.mouseHovering is True and hline.mouseHovering is False + mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) + px = vline.pixelLength(pg.Point(1, 0), ortho=True) + assert abs(vline.value() - plt.plotItem.vb.mapSceneToView(pos2).x()) <= px + + # test missed drag + pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,0)).toPoint() + pos = pos + QtCore.QPoint(0, 6) + pos2 = pos + QtCore.QPoint(-20, -20) + mouseMove(plt, pos) + assert vline.mouseHovering is False and hline.mouseHovering is False + mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) + assert hline.value() == 0 + + # test vertical drag + pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,0)).toPoint() + pos2 = pos - QtCore.QPoint(50, 50) + mouseMove(plt, pos) + assert vline.mouseHovering is False and hline.mouseHovering is True + mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) + px = hline.pixelLength(pg.Point(1, 0), ortho=True) + assert abs(hline.value() - plt.plotItem.vb.mapSceneToView(pos2).y()) <= px + + # test non-interactive line + pos = plt.plotItem.vb.mapViewToScene(pg.Point(5,-1)).toPoint() + pos2 = pos - QtCore.QPoint(50, 50) + mouseMove(plt, pos) + assert hline2.mouseHovering == False + mouseDrag(plt, pos, pos2, QtCore.Qt.LeftButton) + assert hline2.value() == -1 + + +if __name__ == '__main__': + test_mouseInteraction() diff --git a/vviewer/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py b/vviewer/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py new file mode 100644 index 0000000..a3c34b1 --- /dev/null +++ b/vviewer/pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py @@ -0,0 +1,34 @@ +import numpy as np +import pyqtgraph as pg +from pyqtgraph.tests import assertImageApproved + + +def test_PlotCurveItem(): + p = pg.GraphicsWindow() + p.ci.layout.setContentsMargins(4, 4, 4, 4) # default margins vary by platform + v = p.addViewBox() + p.resize(200, 150) + data = np.array([1,4,2,3,np.inf,5,7,6,-np.inf,8,10,9,np.nan,-1,-2,0]) + c = pg.PlotCurveItem(data) + v.addItem(c) + v.autoRange() + + # Check auto-range works. Some platform differences may be expected.. + checkRange = np.array([[-1.1457564053237301, 16.145756405323731], [-3.076811473165955, 11.076811473165955]]) + assert np.allclose(v.viewRange(), checkRange) + + assertImageApproved(p, 'plotcurveitem/connectall', "Plot curve with all points connected.") + + c.setData(data, connect='pairs') + assertImageApproved(p, 'plotcurveitem/connectpairs', "Plot curve with pairs connected.") + + c.setData(data, connect='finite') + assertImageApproved(p, 'plotcurveitem/connectfinite', "Plot curve with finite points connected.") + + 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.") + + + +if __name__ == '__main__': + test_PlotCurveItem() diff --git a/vviewer/pyqtgraph/graphicsItems/tests/test_ROI.py b/vviewer/pyqtgraph/graphicsItems/tests/test_ROI.py new file mode 100644 index 0000000..ddc7f17 --- /dev/null +++ b/vviewer/pyqtgraph/graphicsItems/tests/test_ROI.py @@ -0,0 +1,224 @@ +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 + + +app = pg.mkQApp() + + +def test_getArrayRegion(transpose=False): + pr = pg.PolyLineROI([[0, 0], [27, 0], [0, 28]], closed=True) + pr.setPos(1, 1) + rois = [ + (pg.ROI([1, 1], [27, 28], pen='y'), 'baseroi'), + (pg.RectROI([1, 1], [27, 28], pen='y'), 'rectroi'), + (pg.EllipseROI([1, 1], [27, 28], pen='y'), 'ellipseroi'), + (pr, 'polylineroi'), + ] + for roi, name in rois: + # For some ROIs, resize should not be used. + testResize = not isinstance(roi, pg.PolyLineROI) + + origMode = pg.getConfigOption('imageAxisOrder') + try: + if transpose: + pg.setConfigOptions(imageAxisOrder='row-major') + check_getArrayRegion(roi, 'roi/'+name, testResize, transpose=True) + else: + pg.setConfigOptions(imageAxisOrder='col-major') + check_getArrayRegion(roi, 'roi/'+name, testResize) + finally: + pg.setConfigOptions(imageAxisOrder=origMode) + + +def test_getArrayRegion_axisorder(): + test_getArrayRegion(transpose=True) + + +def check_getArrayRegion(roi, name, testResize=True, transpose=False): + initState = roi.getState() + + #win = pg.GraphicsLayoutWidget() + win = pg.GraphicsView() + win.show() + resizeWindow(win, 200, 400) + # Don't use Qt's layouts for testing--these generate unpredictable results. + #vb1 = win.addViewBox() + #win.nextRow() + #vb2 = win.addViewBox() + + # Instead, place the viewboxes manually + vb1 = pg.ViewBox() + win.scene().addItem(vb1) + vb1.setPos(6, 6) + vb1.resize(188, 191) + + vb2 = pg.ViewBox() + win.scene().addItem(vb2) + vb2.setPos(6, 203) + vb2.resize(188, 191) + + img1 = pg.ImageItem(border='w') + img2 = pg.ImageItem(border='w') + + vb1.addItem(img1) + vb2.addItem(img2) + + np.random.seed(0) + data = np.random.normal(size=(7, 30, 31, 5)) + data[0, :, :, :] += 10 + data[:, 1, :, :] += 10 + data[:, :, 2, :] += 10 + data[:, :, :, 3] += 10 + + if transpose: + data = data.transpose(0, 2, 1, 3) + + img1.setImage(data[0, ..., 0]) + vb1.setAspectLocked() + vb1.enableAutoRange(True, True) + + roi.setZValue(10) + vb1.addItem(roi) + + if isinstance(roi, pg.RectROI): + if transpose: + assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([28.0, 27.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) + else: + assert roi.getAffineSliceParams(data, img1, axes=(1, 2)) == ([27.0, 28.0], ((1.0, 0.0), (0.0, 1.0)), (1.0, 1.0)) + + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + #assert np.all((rgn == data[:, 1:-2, 1:-2, :]) | (rgn == 0)) + img2.setImage(rgn[0, ..., 0]) + vb2.setAspectLocked() + vb2.enableAutoRange(True, True) + + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion', 'Simple ROI region selection.') + + with pytest.raises(TypeError): + roi.setPos(0, False) + + roi.setPos([0.5, 1.5]) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_halfpx', 'Simple ROI region selection, 0.5 pixel shift.') + + roi.setAngle(45) + roi.setPos([3, 0]) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_rotate', 'Simple ROI region selection, rotation.') + + if testResize: + roi.setSize([60, 60]) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_resize', 'Simple ROI region selection, resized.') + + img1.scale(1, -1) + img1.setPos(0, img1.height()) + img1.rotate(20) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_img_trans', 'Simple ROI region selection, image transformed.') + + vb1.invertY() + 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.') + + roi.setState(initState) + img1.resetTransform() + img1.setPos(0, 0) + img1.scale(1, 0.5) + rgn = roi.getArrayRegion(data, img1, axes=(1, 2)) + img2.setImage(rgn[0, ..., 0]) + app.processEvents() + assertImageApproved(win, name+'/roi_getarrayregion_anisotropic', 'Simple ROI region selection, image scaled anisotropically.') + + # allow the roi to be re-used + roi.scene().removeItem(roi) + + +def test_PolyLineROI(): + rois = [ + (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=True, pen=0.3), 'closed'), + (pg.PolyLineROI([[0, 0], [10, 0], [0, 15]], closed=False, pen=0.3), 'open') + ] + + #plt = pg.plot() + plt = pg.GraphicsView() + plt.show() + resizeWindow(plt, 200, 200) + vb = pg.ViewBox() + plt.scene().addItem(vb) + vb.resize(200, 200) + #plt.plotItem = pg.PlotItem() + #plt.scene().addItem(plt.plotItem) + #plt.plotItem.resize(200, 200) + + + plt.scene().minDragTime = 0 # let us simulate mouse drags very quickly. + + # seemingly arbitrary requirements; might need longer wait time for some platforms.. + QtTest.QTest.qWaitForWindowShown(plt) + QtTest.QTest.qWait(100) + + for r, name in rois: + vb.clear() + vb.addItem(r) + vb.autoRange() + app.processEvents() + + assertImageApproved(plt, 'roi/polylineroi/'+name+'_init', 'Init %s polyline.' % name) + initState = r.getState() + assert len(r.getState()['points']) == 3 + + # hover over center + center = r.mapToScene(pg.Point(3, 3)) + mouseMove(plt, center) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_roi', 'Hover mouse over center of ROI.') + + # drag ROI + mouseDrag(plt, center, center + pg.Point(10, -10), QtCore.Qt.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_roi', 'Drag mouse over center of ROI.') + + # hover over handle + pt = r.mapToScene(pg.Point(r.getState()['points'][2])) + mouseMove(plt, pt) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_handle', 'Hover mouse over handle.') + + # drag handle + mouseDrag(plt, pt, pt + pg.Point(5, 20), QtCore.Qt.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_handle', 'Drag mouse over handle.') + + # hover over segment + pt = r.mapToScene((pg.Point(r.getState()['points'][2]) + pg.Point(r.getState()['points'][1])) * 0.5) + mouseMove(plt, pt+pg.Point(0, 2)) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_hover_segment', 'Hover mouse over diagonal segment.') + + # click segment + mouseClick(plt, pt, QtCore.Qt.LeftButton) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_click_segment', 'Click mouse over segment.') + + r.clearPoints() + assertImageApproved(plt, 'roi/polylineroi/'+name+'_clear', 'All points cleared.') + assert len(r.getState()['points']) == 0 + + r.setPoints(initState['points']) + assertImageApproved(plt, 'roi/polylineroi/'+name+'_setpoints', 'Reset points to initial state.') + assert len(r.getState()['points']) == 3 + + 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 diff --git a/vviewer/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py b/vviewer/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py index 8b0ebc8..acf6ad7 100644 --- a/vviewer/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py +++ b/vviewer/pyqtgraph/graphicsItems/tests/test_ScatterPlotItem.py @@ -1,15 +1,15 @@ import pyqtgraph as pg import numpy as np app = pg.mkQApp() -plot = pg.plot() app.processEvents() -# set view range equal to its bounding rect. -# This causes plots to look the same regardless of pxMode. -plot.setRange(rect=plot.boundingRect()) def test_scatterplotitem(): + plot = pg.PlotWidget() + # set view range equal to its bounding rect. + # This causes plots to look the same regardless of pxMode. + plot.setRange(rect=plot.boundingRect()) for i, pxMode in enumerate([True, False]): for j, useCache in enumerate([True, False]): s = pg.ScatterPlotItem() @@ -54,6 +54,10 @@ def test_scatterplotitem(): def test_init_spots(): + plot = pg.PlotWidget() + # set view range equal to its bounding rect. + # This causes plots to look the same regardless of pxMode. + plot.setRange(rect=plot.boundingRect()) spots = [ {'x': 0, 'y': 1}, {'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'}, diff --git a/vviewer/pyqtgraph/imageview/ImageView.py b/vviewer/pyqtgraph/imageview/ImageView.py index 65252cf..5cc00f6 100644 --- a/vviewer/pyqtgraph/imageview/ImageView.py +++ b/vviewer/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, sys +import os import numpy as np from ..Qt import QtCore, QtGui, USE_PYSIDE @@ -26,15 +26,17 @@ from ..graphicsItems.ROI import * from ..graphicsItems.LinearRegionItem import * from ..graphicsItems.InfiniteLine import * from ..graphicsItems.ViewBox import * +from ..graphicsItems.GradientEditorItem import addGradientListToDocstring from .. import ptime as ptime from .. import debug as debug from ..SignalProxy import SignalProxy +from .. import getConfigOption try: from bottleneck import nanmin, nanmax except ImportError: from numpy import nanmin, nanmax - + class PlotROI(ROI): def __init__(self, size): @@ -145,13 +147,13 @@ class ImageView(QtGui.QWidget): self.view.addItem(self.roi) self.roi.hide() self.normRoi = PlotROI(10) - self.normRoi.setPen(QtGui.QPen(QtGui.QColor(255,255,0))) + self.normRoi.setPen('y') 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.timeLine.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0, 200))) + 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]) @@ -202,9 +204,10 @@ class ImageView(QtGui.QWidget): """ Set the image to be displayed in the widget. - ================== ======================================================================= + ================== =========================================================================== **Arguments:** - img (numpy array) the image to be displayed. + 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. autoRange (bool) whether to scale/pan the view to fit the image. @@ -221,7 +224,19 @@ class ImageView(QtGui.QWidget): and *scale*. autoHistogramRange If True, the histogram y-range is automatically scaled to fit the image data. - ================== ======================================================================= + ================== =========================================================================== + + **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():: + + imageview.setImage(imagedata.T) + + This requirement can be changed by the ``imageAxisOrder`` + :ref:`global configuration option <apiref_config>`. + """ profiler = debug.Profiler() @@ -238,28 +253,22 @@ class ImageView(QtGui.QWidget): self.image = img self.imageDisp = None - if xvals is not None: - self.tVals = xvals - elif hasattr(img, 'xvals'): - try: - self.tVals = img.xvals(0) - except: - self.tVals = np.arange(img.shape[0]) - else: - self.tVals = np.arange(img.shape[0]) - profiler() if axes is None: + x,y = (0, 1) if self.imageItem.axisOrder == 'col-major' else (1, 0) + if img.ndim == 2: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': None} + self.axes = {'t': None, 'x': x, 'y': y, 'c': None} elif img.ndim == 3: + # Ambiguous case; make a guess if img.shape[2] <= 4: - self.axes = {'t': None, 'x': 0, 'y': 1, 'c': 2} + self.axes = {'t': None, 'x': x, 'y': y, 'c': 2} else: - self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None} + self.axes = {'t': 0, 'x': x+1, 'y': y+1, 'c': None} elif img.ndim == 4: - self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3} + # Even more ambiguous; just assume the default + self.axes = {'t': 0, 'x': x+1, 'y': y+1, 'c': 3} else: raise Exception("Can not interpret image with dimensions %s" % (str(img.shape))) elif isinstance(axes, dict): @@ -273,6 +282,18 @@ class ImageView(QtGui.QWidget): for x in ['t', 'x', 'y', 'c']: self.axes[x] = self.axes.get(x, None) + axes = self.axes + + if xvals is not None: + self.tVals = xvals + elif axes['t'] is not None: + if hasattr(img, 'xvals'): + try: + self.tVals = img.xvals(axes['t']) + except: + self.tVals = np.arange(img.shape[axes['t']]) + else: + self.tVals = np.arange(img.shape[axes['t']]) profiler() @@ -372,6 +393,7 @@ class ImageView(QtGui.QWidget): self.scene.clear() del self.image del self.imageDisp + super(ImageView, self).close() self.setParent(None) def keyPressEvent(self, ev): @@ -453,7 +475,7 @@ class ImageView(QtGui.QWidget): def setCurrentIndex(self, ind): """Set the currently displayed frame index.""" - self.currentIndex = np.clip(ind, 0, self.getProcessedImage().shape[0]-1) + 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]) @@ -541,6 +563,7 @@ class ImageView(QtGui.QWidget): axes = (1, 2) else: return + data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) if data is not None: while data.ndim > 1: @@ -636,11 +659,21 @@ class ImageView(QtGui.QWidget): if autoHistogramRange: self.ui.histogram.setHistogramRange(self.levelMin, self.levelMax) - if self.axes['t'] is None: - self.imageItem.updateImage(image) + + # Transpose image into order expected by ImageItem + if self.imageItem.axisOrder == 'col-major': + axorder = ['t', 'x', 'y', 'c'] else: + axorder = ['t', 'y', 'x', 'c'] + axorder = [self.axes[ax] for ax in axorder if self.axes[ax] is not None] + image = image.transpose(axorder) + + # Select time index + if self.axes['t'] is not None: self.ui.roiPlot.show() - self.imageItem.updateImage(image[self.currentIndex]) + image = image[self.currentIndex] + + self.imageItem.updateImage(image) def timeIndex(self, slider): @@ -717,4 +750,21 @@ class ImageView(QtGui.QWidget): if self.menu is None: self.buildMenu() self.menu.popup(QtGui.QCursor.pos()) - + + def setColorMap(self, colormap): + """Set the color map. + + ============= ========================================================= + **Arguments** + colormap (A ColorMap() instance) The ColorMap to use for coloring + images. + ============= ========================================================= + """ + self.ui.histogram.gradient.setColorMap(colormap) + + @addGradientListToDocstring() + def setPredefinedGradient(self, name): + """Set one of the gradients defined in :class:`GradientEditorItem <pyqtgraph.graphicsItems.GradientEditorItem>`. + Currently available gradients are: + """ + self.ui.histogram.gradient.loadPreset(name) diff --git a/vviewer/pyqtgraph/imageview/ImageViewTemplate_pyqt.py b/vviewer/pyqtgraph/imageview/ImageViewTemplate_pyqt.py index e728b26..8c9d563 100644 --- a/vviewer/pyqtgraph/imageview/ImageViewTemplate_pyqt.py +++ b/vviewer/pyqtgraph/imageview/ImageViewTemplate_pyqt.py @@ -7,7 +7,7 @@ # # WARNING! All changes made in this file will be lost! -from PyQt4 import QtCore, QtGui +from ..Qt import QtCore, QtGui try: _fromUtf8 = QtCore.QString.fromUtf8 diff --git a/vviewer/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py b/vviewer/pyqtgraph/imageview/ImageViewTemplate_pyqt5.py new file mode 100644 index 0000000..4b4009b --- /dev/null +++ b/vviewer/pyqtgraph/imageview/ImageViewTemplate_pyqt5.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 PyQt5 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.normBtn = 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) + 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.normBtn.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/vviewer/pyqtgraph/imageview/tests/test_imageview.py b/vviewer/pyqtgraph/imageview/tests/test_imageview.py index 2ca1712..3057a8a 100644 --- a/vviewer/pyqtgraph/imageview/tests/test_imageview.py +++ b/vviewer/pyqtgraph/imageview/tests/test_imageview.py @@ -7,5 +7,6 @@ def test_nan_image(): img = np.ones((10,10)) img[0,0] = np.nan v = pg.image(img) + v.imageItem.getHistogram() app.processEvents() v.window().close() diff --git a/vviewer/pyqtgraph/metaarray/MetaArray.py b/vviewer/pyqtgraph/metaarray/MetaArray.py index 9c3f5b8..66ecc46 100644 --- a/vviewer/pyqtgraph/metaarray/MetaArray.py +++ b/vviewer/pyqtgraph/metaarray/MetaArray.py @@ -10,10 +10,11 @@ new methods for slicing and indexing the array based on this meta data. More info at http://www.scipy.org/Cookbook/MetaArray """ -import numpy as np import types, copy, threading, os, re import pickle from functools import reduce +import numpy as np +from ..python2_3 import basestring #import traceback ## By default, the library will use HDF5 when writing files. @@ -151,7 +152,7 @@ class MetaArray(object): if self._data is None: return else: - self._info = [{} for i in range(self.ndim)] + self._info = [{} for i in range(self.ndim + 1)] return else: try: @@ -174,13 +175,16 @@ class MetaArray(object): elif type(info[i]['values']) is not np.ndarray: raise Exception("Axis values must be specified as list or ndarray") if info[i]['values'].ndim != 1 or info[i]['values'].shape[0] != self.shape[i]: - raise Exception("Values array for axis %d has incorrect shape. (given %s, but should be %s)" % (i, str(info[i]['values'].shape), str((self.shape[i],)))) + raise Exception("Values array for axis %d has incorrect shape. (given %s, but should be %s)" % + (i, str(info[i]['values'].shape), str((self.shape[i],)))) if i < self.ndim and 'cols' in info[i]: if not isinstance(info[i]['cols'], list): info[i]['cols'] = list(info[i]['cols']) if len(info[i]['cols']) != self.shape[i]: - raise Exception('Length of column list for axis %d does not match data. (given %d, but should be %d)' % (i, len(info[i]['cols']), self.shape[i])) - + raise Exception('Length of column list for axis %d does not match data. (given %d, but should be %d)' % + (i, len(info[i]['cols']), self.shape[i])) + self._info = info + def implements(self, name=None): ## Rather than isinstance(obj, MetaArray) use object.implements('MetaArray') if name is None: @@ -643,14 +647,21 @@ class MetaArray(object): if len(axs) > maxl: maxl = len(axs) - for i in range(min(self.ndim, len(self._info)-1)): + for i in range(min(self.ndim, len(self._info) - 1)): ax = self._info[i] axs = titles[i] - axs += '%s[%d] :' % (' ' * (maxl + 2 - len(axs)), self.shape[i]) + axs += '%s[%d] :' % (' ' * (maxl - len(axs) + 5 - len(str(self.shape[i]))), self.shape[i]) if 'values' in ax: - v0 = ax['values'][0] - v1 = ax['values'][-1] - axs += " values: [%g ... %g] (step %g)" % (v0, v1, (v1-v0)/(self.shape[i]-1)) + if self.shape[i] > 0: + v0 = ax['values'][0] + axs += " values: [%g" % (v0) + if self.shape[i] > 1: + v1 = ax['values'][-1] + axs += " ... %g] (step %g)" % (v1, (v1 - v0) / (self.shape[i] - 1)) + else: + axs += "]" + else: + axs += " values: []" if 'cols' in ax: axs += " columns: " colstrs = [] diff --git a/vviewer/pyqtgraph/multiprocess/parallelizer.py b/vviewer/pyqtgraph/multiprocess/parallelizer.py index f4ddd95..934bc6d 100644 --- a/vviewer/pyqtgraph/multiprocess/parallelizer.py +++ b/vviewer/pyqtgraph/multiprocess/parallelizer.py @@ -1,6 +1,8 @@ import os, sys, time, multiprocessing, re from .processes import ForkedProcess from .remoteproxy import ClosedError +from ..python2_3 import basestring, xrange + class CanceledError(Exception): """Raised when the progress dialog is canceled during a processing operation.""" diff --git a/vviewer/pyqtgraph/multiprocess/processes.py b/vviewer/pyqtgraph/multiprocess/processes.py index 0dfb80b..c7e4a80 100644 --- a/vviewer/pyqtgraph/multiprocess/processes.py +++ b/vviewer/pyqtgraph/multiprocess/processes.py @@ -156,14 +156,14 @@ class Process(RemoteEventHandler): time.sleep(0.05) self.debugMsg('Child process exited. (%d)' % self.proc.returncode) - def debugMsg(self, msg): + def debugMsg(self, msg, *args): if hasattr(self, '_stdoutForwarder'): ## Lock output from subprocess to make sure we do not get line collisions with self._stdoutForwarder.lock: with self._stderrForwarder.lock: - RemoteEventHandler.debugMsg(self, msg) + RemoteEventHandler.debugMsg(self, msg, *args) else: - RemoteEventHandler.debugMsg(self, msg) + RemoteEventHandler.debugMsg(self, msg, *args) def startEventLoop(name, port, authkey, ppid, debug=False): @@ -267,10 +267,11 @@ class ForkedProcess(RemoteEventHandler): sys.excepthook = excepthook ## Make it harder to access QApplication instance - if 'PyQt4.QtGui' in sys.modules: - sys.modules['PyQt4.QtGui'].QApplication = None - sys.modules.pop('PyQt4.QtGui', None) - sys.modules.pop('PyQt4.QtCore', None) + for qtlib in ('PyQt4', 'PySide', 'PyQt5'): + if qtlib in sys.modules: + sys.modules[qtlib+'.QtGui'].QApplication = None + sys.modules.pop(qtlib+'.QtGui', None) + sys.modules.pop(qtlib+'.QtCore', None) ## sabotage atexit callbacks atexit._exithandlers = [] @@ -420,7 +421,6 @@ def startQtEventLoop(name, port, authkey, ppid, debug=False): if debug: cprint.cout(debug, '[%d] connected; starting remote proxy.\n' % os.getpid(), -1) from ..Qt import QtGui, QtCore - #from PyQt4 import QtGui, QtCore app = QtGui.QApplication.instance() #print app if app is None: @@ -429,7 +429,6 @@ def startQtEventLoop(name, port, authkey, ppid, debug=False): ## until it is explicitly closed by the parent process. global HANDLER - #ppid = 0 if not hasattr(os, 'getppid') else os.getppid() HANDLER = RemoteQtEventHandler(conn, name, ppid, debug=debug) HANDLER.startEventTimer() app.exec_() diff --git a/vviewer/pyqtgraph/multiprocess/remoteproxy.py b/vviewer/pyqtgraph/multiprocess/remoteproxy.py index 4f484b7..208e17f 100644 --- a/vviewer/pyqtgraph/multiprocess/remoteproxy.py +++ b/vviewer/pyqtgraph/multiprocess/remoteproxy.py @@ -69,6 +69,11 @@ class RemoteEventHandler(object): 'deferGetattr': False, ## True, False 'noProxyTypes': [ type(None), str, int, float, tuple, list, dict, LocalObjectProxy, ObjectProxy ], } + if int(sys.version[0]) < 3: + self.proxyOptions['noProxyTypes'].append(unicode) + else: + self.proxyOptions['noProxyTypes'].append(bytes) + self.optsLock = threading.RLock() self.nextRequestId = 0 @@ -88,10 +93,10 @@ class RemoteEventHandler(object): print(pid, cls.handlers) raise - def debugMsg(self, msg): + def debugMsg(self, msg, *args): if not self.debug: return - cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1) + cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)%args), -1) def getProxyOption(self, opt): with self.optsLock: @@ -145,7 +150,7 @@ class RemoteEventHandler(object): sys.excepthook(*sys.exc_info()) if numProcessed > 0: - self.debugMsg('processRequests: finished %d requests' % numProcessed) + self.debugMsg('processRequests: finished %d requests', numProcessed) return numProcessed def handleRequest(self): @@ -166,15 +171,15 @@ class RemoteEventHandler(object): self.debugMsg(' handleRequest: got IOError 4 from recv; try again.') continue else: - self.debugMsg(' handleRequest: got IOError %d from recv (%s); raise ClosedError.' % (err.errno, err.strerror)) + self.debugMsg(' handleRequest: got IOError %d from recv (%s); raise ClosedError.', err.errno, err.strerror) raise ClosedError() - self.debugMsg(" handleRequest: received %s %s" % (str(cmd), str(reqId))) + self.debugMsg(" handleRequest: received %s %s", cmd, reqId) ## read byte messages following the main request byteData = [] if nByteMsgs > 0: - self.debugMsg(" handleRequest: reading %d byte messages" % nByteMsgs) + self.debugMsg(" handleRequest: reading %d byte messages", nByteMsgs) for i in range(nByteMsgs): while True: try: @@ -199,7 +204,7 @@ class RemoteEventHandler(object): ## (this is already a return from a previous request) opts = pickle.loads(optStr) - self.debugMsg(" handleRequest: id=%s opts=%s" % (str(reqId), str(opts))) + self.debugMsg(" handleRequest: id=%s opts=%s", reqId, opts) #print os.getpid(), "received request:", cmd, reqId, opts returnType = opts.get('returnType', 'auto') @@ -279,7 +284,7 @@ class RemoteEventHandler(object): if reqId is not None: if exc is None: - self.debugMsg(" handleRequest: sending return value for %d: %s" % (reqId, str(result))) + self.debugMsg(" handleRequest: sending return value for %d: %s", reqId, result) #print "returnValue:", returnValue, result if returnType == 'auto': with self.optsLock: @@ -294,7 +299,7 @@ class RemoteEventHandler(object): sys.excepthook(*sys.exc_info()) self.replyError(reqId, *sys.exc_info()) else: - self.debugMsg(" handleRequest: returning exception for %d" % reqId) + self.debugMsg(" handleRequest: returning exception for %d", reqId) self.replyError(reqId, *exc) elif exc is not None: @@ -443,16 +448,16 @@ class RemoteEventHandler(object): ## Send primary request request = (request, reqId, nByteMsgs, optStr) - self.debugMsg('send request: cmd=%s nByteMsgs=%d id=%s opts=%s' % (str(request[0]), nByteMsgs, str(reqId), str(opts))) + self.debugMsg('send request: cmd=%s nByteMsgs=%d id=%s opts=%s', request[0], nByteMsgs, reqId, opts) self.conn.send(request) ## 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.debugMsg(' sent %d byte messages' % len(byteData)) + self.debugMsg(' sent %d byte messages', len(byteData)) - self.debugMsg(' call sync: %s' % callSync) + self.debugMsg(' call sync: %s', callSync) if callSync == 'off': return @@ -572,7 +577,7 @@ class RemoteEventHandler(object): try: self.send(request='del', opts=dict(proxyId=proxyId), callSync='off') - except IOError: ## if remote process has closed down, there is no need to send delete requests anymore + except ClosedError: ## if remote process has closed down, there is no need to send delete requests anymore pass def transfer(self, obj, **kwds): @@ -786,6 +791,7 @@ class ObjectProxy(object): 'returnType': None, ## 'proxy', 'value', 'auto', None 'deferGetattr': None, ## True, False, None 'noProxyTypes': None, ## list of types to send by value instead of by proxy + 'autoProxy': None, } self.__dict__['_handler'] = RemoteEventHandler.getHandler(processId) @@ -839,6 +845,9 @@ class ObjectProxy(object): sent to the remote process. ============= ============================================================= """ + for k in kwds: + if k not in self._proxyOptions: + raise KeyError("Unrecognized proxy option '%s'" % k) self._proxyOptions.update(kwds) def _getValue(self): diff --git a/vviewer/pyqtgraph/opengl/GLGraphicsItem.py b/vviewer/pyqtgraph/opengl/GLGraphicsItem.py index 12c5b70..a2c2708 100644 --- a/vviewer/pyqtgraph/opengl/GLGraphicsItem.py +++ b/vviewer/pyqtgraph/opengl/GLGraphicsItem.py @@ -1,7 +1,9 @@ -from ..Qt import QtGui, QtCore -from .. import Transform3D from OpenGL.GL import * from OpenGL import GL +from ..Qt import QtGui, QtCore +from .. import Transform3D +from ..python2_3 import basestring + GLOptions = { 'opaque': { diff --git a/vviewer/pyqtgraph/opengl/GLViewWidget.py b/vviewer/pyqtgraph/opengl/GLViewWidget.py index 992aa73..e0fee04 100644 --- a/vviewer/pyqtgraph/opengl/GLViewWidget.py +++ b/vviewer/pyqtgraph/opengl/GLViewWidget.py @@ -1,4 +1,4 @@ -from ..Qt import QtCore, QtGui, QtOpenGL +from ..Qt import QtCore, QtGui, QtOpenGL, USE_PYQT5 from OpenGL.GL import * import OpenGL.GL.framebufferobjects as glfbo import numpy as np @@ -72,9 +72,9 @@ class GLViewWidget(QtOpenGL.QGLWidget): def setBackgroundColor(self, *args, **kwds): """ Set the background color of the widget. Accepts the same arguments as - pg.mkColor(). + pg.mkColor() and pg.glColor(). """ - self.opts['bgcolor'] = fn.mkColor(*args, **kwds) + self.opts['bgcolor'] = fn.glColor(*args, **kwds) self.update() def getViewport(self): @@ -174,7 +174,7 @@ class GLViewWidget(QtOpenGL.QGLWidget): self.setProjection(region=region) self.setModelview() bgcolor = self.opts['bgcolor'] - glClearColor(bgcolor.red(), bgcolor.green(), bgcolor.blue(), 1.0) + glClearColor(*bgcolor) glClear( GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT ) self.drawItemTree(useItemNames=useItemNames) @@ -324,10 +324,17 @@ class GLViewWidget(QtOpenGL.QGLWidget): def wheelEvent(self, ev): + delta = 0 + if not USE_PYQT5: + delta = ev.delta() + else: + delta = ev.angleDelta().x() + if delta == 0: + delta = ev.angleDelta().y() if (ev.modifiers() & QtCore.Qt.ControlModifier): - self.opts['fov'] *= 0.999**ev.delta() + self.opts['fov'] *= 0.999**delta else: - self.opts['distance'] *= 0.999**ev.delta() + self.opts['distance'] *= 0.999**delta self.update() def keyPressEvent(self, ev): diff --git a/vviewer/pyqtgraph/opengl/MeshData.py b/vviewer/pyqtgraph/opengl/MeshData.py index 5adf4b6..f83fcdf 100644 --- a/vviewer/pyqtgraph/opengl/MeshData.py +++ b/vviewer/pyqtgraph/opengl/MeshData.py @@ -1,6 +1,8 @@ +import numpy as np from ..Qt import QtGui from .. import functions as fn -import numpy as np +from ..python2_3 import xrange + class MeshData(object): """ diff --git a/vviewer/pyqtgraph/parametertree/Parameter.py b/vviewer/pyqtgraph/parametertree/Parameter.py index 5f37ccd..de9a162 100644 --- a/vviewer/pyqtgraph/parametertree/Parameter.py +++ b/vviewer/pyqtgraph/parametertree/Parameter.py @@ -1,7 +1,7 @@ from ..Qt import QtGui, QtCore import os, weakref, re from ..pgcollections import OrderedDict -from ..python2_3 import asUnicode +from ..python2_3 import asUnicode, basestring from .ParameterItem import ParameterItem PARAM_TYPES = {} @@ -312,7 +312,8 @@ class Parameter(QtCore.QObject): If blockSignals is True, no signals will be emitted until the tree has been completely restored. This prevents signal handlers from responding to a partially-rebuilt network. """ - childState = state.get('children', []) + state = state.copy() + childState = state.pop('children', []) ## list of children may be stored either as list or dict. if isinstance(childState, dict): diff --git a/vviewer/pyqtgraph/parametertree/SystemSolver.py b/vviewer/pyqtgraph/parametertree/SystemSolver.py index 0a889df..24e35e9 100644 --- a/vviewer/pyqtgraph/parametertree/SystemSolver.py +++ b/vviewer/pyqtgraph/parametertree/SystemSolver.py @@ -1,4 +1,4 @@ -from collections import OrderedDict +from ..pgcollections import OrderedDict import numpy as np class SystemSolver(object): diff --git a/vviewer/pyqtgraph/parametertree/parameterTypes.py b/vviewer/pyqtgraph/parametertree/parameterTypes.py index 7b1c5ee..2535b13 100644 --- a/vviewer/pyqtgraph/parametertree/parameterTypes.py +++ b/vviewer/pyqtgraph/parametertree/parameterTypes.py @@ -95,28 +95,20 @@ class WidgetParameterItem(ParameterItem): """ opts = self.param.opts t = opts['type'] - if t == 'int': + if t in ('int', 'float'): defs = { - 'value': 0, 'min': None, 'max': None, 'int': True, - 'step': 1.0, 'minStep': 1.0, 'dec': False, - 'siPrefix': False, 'suffix': '' - } - defs.update(opts) - if 'limits' in opts: - defs['bounds'] = opts['limits'] - w = SpinBox() - w.setOpts(**defs) - w.sigChanged = w.sigValueChanged - w.sigChanging = w.sigValueChanging - elif t == 'float': - defs = { - 'value': 0, 'min': None, 'max': None, + 'value': 0, 'min': None, 'max': None, 'step': 1.0, 'dec': False, - 'siPrefix': False, 'suffix': '' + 'siPrefix': False, 'suffix': '', 'decimals': 3, } - defs.update(opts) + if t == 'int': + defs['int'] = True + defs['minStep'] = 1.0 + for k in defs: + 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 @@ -130,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)) @@ -287,13 +280,16 @@ 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() - - class EventProxy(QtCore.QObject): def __init__(self, qobj, callback): @@ -304,8 +300,6 @@ class EventProxy(QtCore.QObject): def eventFilter(self, obj, ev): return self.callback(obj, ev) - - class SimpleParameter(Parameter): itemClass = WidgetParameterItem diff --git a/vviewer/pyqtgraph/parametertree/tests/test_parametertypes.py b/vviewer/pyqtgraph/parametertree/tests/test_parametertypes.py index c7cd2cb..dc58101 100644 --- a/vviewer/pyqtgraph/parametertree/tests/test_parametertypes.py +++ b/vviewer/pyqtgraph/parametertree/tests/test_parametertypes.py @@ -12,7 +12,7 @@ def test_opts(): tree = pt.ParameterTree() tree.setParameters(param) - assert param.param('bool').items.keys()[0].widget.isEnabled() is False - assert param.param('color').items.keys()[0].widget.isEnabled() is False + assert list(param.param('bool').items.keys())[0].widget.isEnabled() is False + assert list(param.param('color').items.keys())[0].widget.isEnabled() is False diff --git a/vviewer/pyqtgraph/pixmaps/__init__.py b/vviewer/pyqtgraph/pixmaps/__init__.py index c26e4a6..7a3411c 100644 --- a/vviewer/pyqtgraph/pixmaps/__init__.py +++ b/vviewer/pyqtgraph/pixmaps/__init__.py @@ -6,6 +6,7 @@ Provides support for frozen environments as well. import os, sys, pickle from ..functions import makeQImage from ..Qt import QtGui +from ..python2_3 import basestring if sys.version_info[0] == 2: from . import pixmapData_2 as pixmapData else: diff --git a/vviewer/pyqtgraph/pixmaps/auto.png b/vviewer/pyqtgraph/pixmaps/auto.png new file mode 100644 index 0000000000000000000000000000000000000000..a27ff4f82b6e31f2819061a53ba72d4b322dc26d GIT binary patch literal 1022 zcmV<a0|ESrP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW zd<bNS00009a7bBm000CB000CB0ZV5*6aWAK8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H11A9qCK~z|U&6mGV<46?8ze8e>^J4|8T_gkr2rEb%1y+EsAi5$F z6^a6Zm<{4b{(-IT$`sImw8eJYQn(>RHV8WC_#q^8EE0#dLYAnABJEZo1<0|z1#9Gq zacovOaj)`x-@JLB@te`i5W_HFnj}eY4a4{wz#jlc0K7tTtODRGfHOkKXF(9Yn+{?E zNRsryFpMt%-ZUwxll>rsd=vzs_y|xe7PEwqg0+{XX{c7KP01Jvh2Zn~D2))36$GJ3 zwzjsqYPH%o0Ivby`uZB8C_+&bsH)l&0ES_}aU62F9QykDO!)^P<Q>`E-26-k`O8e6 z&*!0OTEhY3@i>Nuhbis!^b}`jXAR?gJ|9+BSFI4A{DhEEvn7h6YjOZsTU$dU5}~y7 z^K&GV$%b*7ra=@%tgNh<65j&gz3EgG#Wgw4^S0ywFfuZNXf)~?uPBOjlA|s7wbiQC zs;8RI&(Ayh78e&?<5g9)F78(?t`Qashr>vvQjY%V>1hN4f#(c~X91+sX|OE&*sj%T zHo$Z`{oesBEG$s%?d@&by75wm-vWrmVu;0JlqN|Mip3(O4Gj%paB#5s0QIvkl}b1{ zIe{$8wynFgwA5SxmSt_Vm1P-*VW3<t<LKy!@=Z-mAruNV4<MCFAsh}<nj}e&-}Pk( z27{QHnQ0zC{ocR0xS$r_-rgQ6l?v6z>tb{Th(sdbd7jd&vj>3t`+Mx}?owJb8nvAl zzXedg_*7NJ&d!dlUws(>Z-2qx$l2Q3icBU$X@n5$@9(<~3;>gplUQC}ww1>-z{JD^ zIyyS&0KXqSJw2WPSeAt#2v}cVckI)!1dR?JujW<;$3M{9+lzQSPHDHdx465zYdBya z5I`^(d~6R441ni($IFT%z}(y%2qBb~$z*VOdD##kkw{=;V}sIixtt@wlO^!`{m5ps zly-G><(eD-4i67;b8|y!$z&4k?d{$H#>dCe+1W{H^?TpRbX`ZGKwYrW*4BpE*;z+` z3h;1sW*7#5eedhK4q28x0RTXuP=K!Mlx7}K48vH@iAoD0<X_<7#>#OVy1KfMNF=D> z<KttL%Vp01rBVq82M3ha-`|h1u`zHQXMH1_`wheR3gA!RVX@@%d31ModrszLPi<lI sT5hp_UjAY7;!m^x#pBepmw2B41lP7_Luv|AaR2}S07*qoM6N<$f?UVZiU0rr literal 0 HcmV?d00001 diff --git a/vviewer/pyqtgraph/pixmaps/ctrl.png b/vviewer/pyqtgraph/pixmaps/ctrl.png new file mode 100644 index 0000000000000000000000000000000000000000..c8dc96e439da8ef33abf292bd9b808633646c26b GIT binary patch literal 934 zcmV;X16lluP)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW zd<bNS00009a7bBm000CB000CB0ZV5*6aWAK8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H110zX9K~z|U&6hn$D_IoBe>Wd~F$C0o$RZJh6k)418x_AmtgMxg zXCY%qyWNsP1VI$676i93_`#s^5(Wz!3mXNE#qQf;A$9{Irg9?;At)qHW{d1(<1@;8 z$%y-}X3jnLp5MLq+?jiXX_{D4RrP~un!f-8z)ye&dz1q{1Aho1ehq~}e_IX00#H>o zY?|h8z;`wUeMwm%#LuBnC|LnWCX*k95TB~O48tIw&)X7Fk|dl?=M$R{;$tWjN{WMn zgYO?7AJaewz}3|iilU%t8pUGK767-~jnC&pmSuW-do6QT2=Rk+YHDgp2r*#=Uszb+ z_V$)isbouVfKsVMHk;+-<b*&VKzn;TKpRkU2qA)&qoODnhEa2nfq?<0r>E)a>Z%*W zFbot$sW$aHfFG?!P19cNJU>6r+S(fD=jY7L&D9N|X<GFpgAV>ZYWaNr)jf|yBBax4 zT3T9IUS4K$a<VRfVzF4A+#bi*cGPv9nVA{V=`;X~i;FZjH`fJGtrr1=5H%t8_Vxhq zdc6dL!Dl|U0QC3wb9i`2XJ_ZLw$ahi%5zgw)7t=acX!j**GD3ecmm;axfmN8d*auq zT(yj6XJ_2s-{bfDNhA{N@9#4*GQ#liFdol;zlWuzB{G=|r>CcN1JqoBySqD9R#pJ; z`~8)yuMQ67E!f)HVtjnuUI2jI-Cd&5DA(85uMW^C#H+{f`1qKKi3!5tFtf9>#9}cX z9v*CksF?%RDijJ_TwGM%Z*Feb+1a7BwY4ThG#dS;ocQVwk)>2BMI;ijZ6&@Lz;1}T z0PKXQ8^A7zh5+n<Xc*unh)gC!EEZ#PbCY;HPDe+_+X2*qSYKZ!kw~z*y2|C{CE;-R zZ2`W7@cDc|c^i6qdMFeMHT$?I7Y!asl5o4-4c~+<i0$od0)YTtua~yAHdIvw$~R<5 zk}UU}LkRH~fYa%$P&cYnD#hU7AV)_>Y;0^WG&Dpmmjl4(^HtXfe>zOl{A|6+viz)o zs8_nK6OYG<$K&X_4wNS;%W}2b@9$oiynku-zbH;Ey+?We4}W%#BJQ5X8UO$Q07*qo IM6N<$f(oslRsaA1 literal 0 HcmV?d00001 diff --git a/vviewer/pyqtgraph/pixmaps/default.png b/vviewer/pyqtgraph/pixmaps/default.png new file mode 100644 index 0000000000000000000000000000000000000000..f12394214dadb66bd90e7c671d87a5810c1571a6 GIT binary patch literal 810 zcmV+_1J(SAP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV0008<Nkl<Zc-p0s zNla5w6o%jJdo5K4sbU!v1w}BJumLn0qApy(#%Ka0Mhpwa3dX2WNl<7RA{ChhF%~ez zLdz7!0OA6KLR%RvZ3QZzIDjQIrVN%qkGI4&k{DgM_>z<NpZ~ir?_HuQmDq>z1OUPe zo)1aVX}s1#$L?N9pk7vg&d^@u+F#8>SvWgp*|R{@KeW?!3fxz47$#1qCH6_Y)^y5H zFib~-aq$M`CM?fzU}@|U$SOTo#9WkucPXo&vB2K&eIgE|bw0-T(%|)#nK)Sc{AX5X zIWXIK##ob14q*wyDlklNU<++L7^Y&uFc|~-!AO{IKaDvBOR`X$$6{h|77ub^ps^|y z@hyVorLkzxE2BU^9tFCgFqp3K1eL%6WF<}}X}+szD2o9fg+8#Ph}aT;>%R=;NxN6^ zTbv-8ut4|b3g|{6U|~2MCR^ChTgEaKC)ux7-lsJ^PbRA}<CtqtW9Y_N!NK*#p771Z zo-k<0@;8Z6h+?8K-+p<n>nwbcU4XA6=i!6g4>T`XP|J5%#wT3-^=I~qQWyqB2C1H_ z_Me+?4L0-^9XAQ?kmH0TldgX)VE^c=aNbnZcyEq2A2q2uJxpWGE*p|Gavqm`%@Q#W z38dLJis=%!hEL*?QgvCt%%ga6-E0rSv|=_efIkj~u&`hia8X517NcV-o8oKpY%2!R zm>szsnsM8u7~qjeo<Y%bZnMeXpPMgReI}(J&1EJ|@`>x1WMKkXTaknYw+SY5C^YeS z2csdwiC&Q6hH#OEDXr!rU1uJ%@Lh>*$V47J6bmAsBv4|U6>byYGSUbfk$D~#xucHK z{Zv!lu04uMceeV4v#+L{6@Vo{eGhCx%J0&2Sm19dTTu(y$TBP2jv!^5H#OdTc&DoF zfSX$6?4fzaau?mB-@@gq<{k6mUm5H6LvOo$DP@b7ou<~qR$lC4S9y(2GiMgTKheDM oL#k?5npWtHR9tJvD)=vc0s@|Dx>pGq$N&HU07*qoM6N<$f=L-|8vp<R literal 0 HcmV?d00001 diff --git a/vviewer/pyqtgraph/pixmaps/icons.svg b/vviewer/pyqtgraph/pixmaps/icons.svg new file mode 100644 index 0000000..cfdfeba --- /dev/null +++ b/vviewer/pyqtgraph/pixmaps/icons.svg @@ -0,0 +1,135 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + width="744.09448819" + height="1052.3622047" + id="svg2" + version="1.1" + inkscape:version="0.48.1 r9760" + sodipodi:docname="icons.svg"> + <defs + id="defs4" /> + <sodipodi:namedview + id="base" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:pageopacity="0.0" + inkscape:pageshadow="2" + inkscape:zoom="1.4" + inkscape:cx="290.39972" + inkscape:cy="606.05972" + inkscape:document-units="px" + inkscape:current-layer="layer3" + showgrid="false" + inkscape:window-width="1400" + inkscape:window-height="1030" + inkscape:window-x="-3" + inkscape:window-y="-3" + inkscape:window-maximized="1" /> + <metadata + id="metadata7"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <g + inkscape:label="Layer 1" + inkscape:groupmode="layer" + id="layer1" + style="display:inline" /> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="Layer" + style="display:inline" /> + <g + inkscape:groupmode="layer" + id="layer3" + inkscape:label="Layer#1"> + <g + style="display:inline" + id="g3764" + inkscape:export-filename="/home/luke/work/manis_lab/code/acq4/lib/util/pyqtgraph/graphicsItems/PlotItem/auto.png" + inkscape:export-xdpi="26.181818" + inkscape:export-ydpi="26.181818"> + <rect + ry="18.687822" + y="400.81378" + x="108.08632" + height="100" + width="100" + id="rect2985" + style="fill:#000000;fill-opacity:1;stroke:#a9a9a9;stroke-width:10;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" /> + <text + transform="scale(1.0048002,0.99522278)" + sodipodi:linespacing="125%" + id="text3757" + y="490.5354" + x="118.68684" + style="font-size:108.36511993px;font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;line-height:125%;letter-spacing:0px;word-spacing:0px;fill:#e7e7e7;fill-opacity:1;stroke:none;font-family:Arial;-inkscape-font-specification:Arial Bold" + xml:space="preserve"><tspan + style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-family:Arial;-inkscape-font-specification:Arial Bold" + y="490.5354" + x="118.68684" + id="tspan3759" + sodipodi:role="line">A</tspan></text> + </g> + <g + id="g3777" + style="display:inline" + transform="translate(140,0)" + inkscape:export-filename="/home/luke/work/manis_lab/code/acq4/lib/util/pyqtgraph/graphicsItems/PlotItem/ctrl.png" + inkscape:export-xdpi="26.181818" + inkscape:export-ydpi="26.181818"> + <rect + style="fill:#000000;fill-opacity:1;stroke:#a9a9a9;stroke-width:10;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" + id="rect3779" + width="100" + height="100" + x="108.08632" + y="400.81378" + ry="18.687822" /> + <path + style="fill:#e3e3e3;fill-opacity:1;stroke:none" + d="m 192.1403,484.47301 c 3.45636,-3.45635 3.45636,-9.02124 0,-12.47759 l -36.39994,-36.39995 c 1.29616,-5.58299 -0.2288,-11.68397 -4.57999,-16.03517 -3.03048,-3.03048 -6.90883,-4.69498 -10.87095,-4.98687 l -2.98378,2.98378 9.49382,9.49382 -2.62905,9.80681 -9.79638,2.61862 -9.49382,-9.49382 -2.97334,2.97334 c 0.29168,3.96216 1.94541,7.85036 4.97644,10.88138 4.35119,4.3512 10.45218,5.87615 16.03517,4.57999 l 36.39995,36.39995 c 3.45635,3.45635 9.02124,3.45635 12.47759,0 l 0.34428,-0.34429 z m -3.38021,-3.04636 c -2.09533,2.09533 -5.4893,2.10575 -7.58463,0.0104 -2.09533,-2.09532 -2.09533,-5.49972 -10e-6,-7.59505 2.09534,-2.09534 5.48931,-2.0849 7.58464,0.0104 2.09532,2.09532 2.09533,5.47887 0,7.5742 z" + id="path3793" + inkscape:connector-curvature="0" /> + </g> + <g + transform="translate(280,0)" + style="display:inline" + id="g3785" + inkscape:export-filename="/home/luke/work/manis_lab/code/acq4/lib/util/pyqtgraph/graphicsItems/PlotItem/lock.png" + inkscape:export-xdpi="26.181818" + inkscape:export-ydpi="26.181818"> + <rect + ry="18.687822" + y="400.81378" + x="108.08632" + height="100" + width="100" + id="rect3787" + style="fill:#000000;fill-opacity:1;stroke:#a9a9a9;stroke-width:10;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0" /> + <path + style="font-size:medium;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;text-indent:0;text-align:start;text-decoration:none;line-height:normal;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;text-anchor:start;baseline-shift:baseline;color:#000000;fill:#e6e6e6;fill-opacity:1;stroke:none;stroke-width:20;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate;font-family:Sans;-inkscape-font-specification:Sans" + d="m 158.08632,412.60531 c -13.62786,0 -24.7172,11.19602 -24.7172,24.87236 l 0,10.65958 -0.8689,0 c -3.12596,0 -5.63236,2.52191 -5.63236,5.64787 l 0,29.58927 c 0,3.12596 2.5064,5.64787 5.63236,5.64787 l 51.15669,0 c 3.12596,0 5.64787,-2.52191 5.64787,-5.64787 l 0,-29.58927 c 0,-3.12596 -2.52191,-5.64787 -5.64787,-5.64787 l -0.85339,0 0,-10.65958 c 0,-13.67634 -11.08933,-24.87236 -24.7172,-24.87236 z m 0,9.93032 c 8.25237,0 14.78688,6.54986 14.78688,14.94204 l 0,10.65958 -29.57376,0 0,-10.65958 c 0,-8.39218 6.53451,-14.94204 14.78688,-14.94204 z" + id="rect3830" + inkscape:connector-curvature="0" /> + </g> + </g> +</svg> diff --git a/vviewer/pyqtgraph/pixmaps/lock.png b/vviewer/pyqtgraph/pixmaps/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..3f85dde05d29a80e9cba8d9d339f88d7f2a960e8 GIT binary patch literal 913 zcmV;C18)3@P)<h;3K|Lk000e1NJLTq001BW001Be1^@s6b9#F800004b3#c}2nYxW zd<bNS00009a7bBm000CB000CB0ZV5*6aWAK8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H10}e?<K~z|U?U%i4BS#p<f1}+6BUZ8%l)@l{jJFCZf>w!Ln*0d{ zafdMobt(epI8-eN0v0C530PdEPE`<#8{tZYt^V$cKpKk$7d8?&Dy;LOM!Qmq-0sN` zt$aQuPT{AT_hX)UmYLakUon|X@L-ze*OSTQHSj&415p-(55PO%tq|guTrT&g7Z9ER z(==a9CX+Wnd_jxOLSG2+V=k9#_yQV@#!Df@Z_`<>>tfsXLRM5&6-kor`GgQJbGckY zoSdAjI*#)P@D;%2<t6oc9m}#9jYbO*pePEOrjgI*SzBB4+<hU$H`4a@_D@2HAH2m4 z!{FxThVghjmw0Ju30anL949buJRZ~U_c=d5CzHt#i^TxKz&Ip?$a;zOdL7qw1IZGJ z1P2EPv|24XoerH&hgPdarBY#abv1CV>$=qI^=a34A>dmtXjxVu!S3!ZXJ=>R^Lf(g zG_ous%QES7nnIyKyWM7IXD5)uvaIQk%!c@Q)oj}i-19=Az~SK`s;c_FZnsOf+x6{8 zB*M|r5rslwCeLUznm*joP@vq8NG6jQhT+@IW)of4$!4=;vsrXqr`c@!al<f3CX)}L z=0O5Vr4r$A7y#2WDHe<LdObhh>-8uWi<qVfKsX$xR4Oe_fUfHRIF3WPT%MU%E|>iq z;m!GC0nuobR4N6)#l;1K!C+?YU@)N5=>U*QrHDr7|H<bHNF);e`1<;KF5%VH)j!UC zv;ZN*T#_%D`=B%MZ$b_VZf$L`x3}lpo12@Qo}LC0uCK5A<Nf`8j*pLNx7(jDKv5K` z)hhA$r`LITd6`Tm6DVLB8yg!`t5tMepZSxUxdpLU?B3#!MLZt&9-q$>@L%Csz_Wm7 z0U`cw8c<agMN#~~a5!8@RXiGv7!HTNttiTLRrMhxg!mmmk|Z=u^Al~`rdF%Xt$2gr zI1aU1?OuPOY1(v;@Ln_;jbDLR@Tn}Hu8db!R)|C*j}mZqcgOARt#3<`M5R)h?hSr^ n^2X%pTeJT~bL!zK+Vj5vT1`#lyHYUO00000NkvXXu0mjf+uNW{ literal 0 HcmV?d00001 diff --git a/vviewer/pyqtgraph/python2_3.py b/vviewer/pyqtgraph/python2_3.py index b1c46f2..ae4667e 100644 --- a/vviewer/pyqtgraph/python2_3.py +++ b/vviewer/pyqtgraph/python2_3.py @@ -40,10 +40,6 @@ def sortList(l, cmpFunc): l.sort(key=cmpToKey(cmpFunc)) if sys.version_info[0] == 3: - import builtins - builtins.basestring = str - #builtins.asUnicode = asUnicode - #builtins.sortList = sortList basestring = str def cmp(a,b): if a>b: @@ -52,9 +48,11 @@ if sys.version_info[0] == 3: return -1 else: return 0 - builtins.cmp = cmp - builtins.xrange = range -#else: ## don't use __builtin__ -- this confuses things like pyshell and ActiveState's lazy import recipe - #import __builtin__ - #__builtin__.asUnicode = asUnicode - #__builtin__.sortList = sortList + xrange = range +else: + import __builtin__ + basestring = __builtin__.basestring + cmp = __builtin__.cmp + xrange = __builtin__.xrange + + \ No newline at end of file diff --git a/vviewer/pyqtgraph/tests/__init__.py b/vviewer/pyqtgraph/tests/__init__.py new file mode 100644 index 0000000..393bd3c --- /dev/null +++ b/vviewer/pyqtgraph/tests/__init__.py @@ -0,0 +1,2 @@ +from .image_testing import assertImageApproved, TransposedImageItem +from .ui_testing import resizeWindow, mousePress, mouseMove, mouseRelease, mouseDrag, mouseClick diff --git a/vviewer/pyqtgraph/tests/image_testing.py b/vviewer/pyqtgraph/tests/image_testing.py new file mode 100644 index 0000000..c8a41de --- /dev/null +++ b/vviewer/pyqtgraph/tests/image_testing.py @@ -0,0 +1,634 @@ +# Image-based testing borrowed from vispy + +""" +Procedure for unit-testing with images: + +1. Run unit tests at least once; this initializes a git clone of + pyqtgraph/test-data in ~/.pyqtgraph. + +2. Run individual test scripts with the PYQTGRAPH_AUDIT environment variable set: + + $ 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. + +3. After adding or changing test images, create a new commit: + + $ cd ~/.pyqtgraph/test-data + $ git add ... + $ git commit -a + +4. Look up the most recent tag name from the `testDataTag` global variable + below. Increment the tag name by 1 and create a new tag in the test-data + repository: + + $ git tag test-data-NNN + $ git push --tags origin master + + This tag is used to ensure that each pyqtgraph commit is linked to a specific + commit in the test-data repository. This makes it possible to push new + commits to the test-data repository without interfering with existing + tests, and also allows unit tests to continue working on older pyqtgraph + versions. + +""" + + +# This is the name of a tag in the test-data repository that this version of +# 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' + + +import time +import os +import sys +import inspect +import base64 +import subprocess as sp +import numpy as np + +if sys.version[0] >= '3': + import http.client as httplib + import urllib.parse as urllib +else: + import httplib + import urllib +from ..Qt import QtGui, QtCore, QtTest, QT_LIB +from .. import functions as fn +from .. import GraphicsLayoutWidget +from .. import ImageItem, TextItem + + +tester = None + +# Convenient stamp used for ensuring image orientation is correct +axisImg = [ + " 1 1 1 ", + " 1 1 1 1 1 1 ", + " 1 1 1 1 1 1 1 1 1 1", + " 1 1 1 1 1 ", + " 1 1 1 1 1 1 ", + " 1 1 ", + " 1 1 ", + " 1 ", + " ", + " 1 ", + " 1 ", + " 1 ", + "1 1 1 1 1 ", + "1 1 1 1 1 ", + " 1 1 1 ", + " 1 1 1 ", + " 1 ", + " 1 ", +] +axisImg = np.array([map(int, row[::2].replace(' ', '0')) for row in axisImg]) + + + +def getTester(): + global tester + if tester is None: + tester = ImageTester() + return tester + + +def assertImageApproved(image, standardFile, message=None, **kwargs): + """Check that an image test result matches a pre-approved standard. + + If the result does not match, then the user can optionally invoke a GUI + to compare the images and decide whether to fail the test or save the new + image as the standard. + + This function will automatically clone the test-data repository into + ~/.pyqtgraph/test-data. However, it is up to the user to ensure this repository + is kept up to date and to commit/push new images after they are saved. + + Run the test with the environment variable PYQTGRAPH_AUDIT=1 to bring up + the auditing GUI. + + Parameters + ---------- + image : (h, w, 4) ndarray + standardFile : str + The name of the approved test image to check against. This file name + is relative to the root of the pyqtgraph test-data repository and will + be automatically fetched. + message : str + A string description of the image. It is recommended to describe + specific features that an auditor should look for when deciding whether + to fail a test. + + Extra keyword arguments are used to set the thresholds for automatic image + comparison (see ``assertImageMatch()``). + """ + if isinstance(image, QtGui.QWidget): + w = image + + # just to be sure the widget size is correct (new window may be resized): + QtGui.QApplication.processEvents() + + graphstate = scenegraphState(w, standardFile) + image = np.zeros((w.height(), w.width(), 4), dtype=np.ubyte) + qimg = fn.makeQImage(image, alpha=True, copy=False, transpose=False) + painter = QtGui.QPainter(qimg) + w.render(painter) + painter.end() + + # transpose BGRA to RGBA + image = image[..., [2, 1, 0, 3]] + + if message is None: + code = inspect.currentframe().f_back.f_code + message = "%s::%s" % (code.co_filename, code.co_name) + + # Make sure we have a test data repo available, possibly invoking git + dataPath = getTestDataRepo() + + # Read the standard image if it exists + stdFileName = os.path.join(dataPath, standardFile + '.png') + if not os.path.isfile(stdFileName): + stdImage = None + else: + pxm = QtGui.QPixmap() + pxm.load(stdFileName) + stdImage = fn.imageToArray(pxm.toImage(), copy=True, transpose=False) + + # If the test image does not match, then we go to audit if requested. + try: + 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])) + if image.shape != stdImage.shape: + # Allow im1 to be an integer multiple larger than im2 to account + # for high-resolution displays + ims1 = np.array(image.shape).astype(float) + ims2 = np.array(stdImage.shape).astype(float) + sr = ims1 / ims2 if ims1[0] > ims2[0] else ims2 / ims1 + if (sr[0] != sr[1] or not np.allclose(sr, np.round(sr)) or + sr[0] < 1): + raise TypeError("Test result shape %s is not an integer factor" + " different than standard image shape %s." % + (ims1, ims2)) + sr = np.round(sr).astype(int) + image = fn.downsample(image, sr[0], axis=(0, 1)).astype(image.dtype) + + assertImageMatch(image, stdImage, **kwargs) + + if bool(os.getenv('PYQTGRAPH_PRINT_TEST_STATE', False)): + print(graphstate) + + 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 " + "%s`\n" % (stdFileName, dataPath, standardFile)) + if os.getenv('PYQTGRAPH_AUDIT') == '1' or os.getenv('PYQTGRAPH_AUDIT_ALL') == '1': + sys.excepthook(*sys.exc_info()) + getTester().test(image, stdImage, message) + stdPath = os.path.dirname(stdFileName) + print('Saving new standard image to "%s"' % stdFileName) + if not os.path.isdir(stdPath): + os.makedirs(stdPath) + img = fn.makeQImage(image, alpha=True, transpose=False) + img.save(stdFileName) + else: + if stdImage is None: + raise Exception("Test standard %s does not exist. Set " + "PYQTGRAPH_AUDIT=1 to add this image." % stdFileName) + else: + if os.getenv('TRAVIS') is not None: + saveFailedTest(image, stdImage, standardFile) + print(graphstate) + raise + + +def assertImageMatch(im1, im2, minCorr=None, pxThreshold=50., + pxCount=-1, maxPxDiff=None, avgPxDiff=None, + imgDiff=None): + """Check that two images match. + + Images that differ in shape or dtype will fail unconditionally. + Further tests for similarity depend on the arguments supplied. + + By default, images may have no pixels that gave a value difference greater + than 50. + + Parameters + ---------- + im1 : (h, w, 4) ndarray + Test output image + im2 : (h, w, 4) ndarray + Test standard image + minCorr : float or None + Minimum allowed correlation coefficient between corresponding image + values (see numpy.corrcoef) + pxThreshold : float + Minimum value difference at which two pixels are considered different + pxCount : int or None + Maximum number of pixels that may differ. Default is 0 for Qt4 and + 1% of image size for Qt5. + maxPxDiff : float or None + Maximum allowed difference between pixels + avgPxDiff : float or None + Average allowed difference between pixels + imgDiff : float or None + Maximum allowed summed difference between images + + """ + assert im1.ndim == 3 + assert im1.shape[2] == 4 + assert im1.dtype == im2.dtype + + if pxCount == -1: + if QT_LIB == 'PyQt5': + # Qt5 generates slightly different results; relax the tolerance + # until test images are updated. + pxCount = int(im1.shape[0] * im1.shape[1] * 0.01) + else: + pxCount = 0 + + diff = im1.astype(float) - im2.astype(float) + if imgDiff is not None: + assert np.abs(diff).sum() <= imgDiff + + pxdiff = diff.max(axis=2) # largest value difference per pixel + mask = np.abs(pxdiff) >= pxThreshold + if pxCount is not None: + assert mask.sum() <= pxCount + + maskedDiff = diff[mask] + if maxPxDiff is not None and maskedDiff.size > 0: + assert maskedDiff.max() <= maxPxDiff + if avgPxDiff is not None and maskedDiff.size > 0: + assert maskedDiff.mean() <= avgPxDiff + + if minCorr is not None: + with np.errstate(invalid='ignore'): + corr = np.corrcoef(im1.ravel(), im2.ravel())[0, 1] + assert corr >= minCorr + + +def saveFailedTest(data, expect, filename): + """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 + es = expect.shape + + shape = (max(ds[0], es[0]) + 4, ds[1] + es[1] + 8 + max(ds[1], es[1]), 4) + img = np.empty(shape, dtype=np.ubyte) + img[..., :3] = 100 + img[..., 3] = 255 + + img[2:2+ds[0], 2:2+ds[1], :ds[2]] = data + img[2:2+es[0], ds[1]+4:ds[1]+4+es[1], :es[2]] = expect + + diff = makeDiffImage(data, expect) + img[2:2+diff.shape[0], -diff.shape[1]-2:-2] = diff + + png = makePng(img) + + 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) + print(response) + + +def makePng(img): + """Given an array like (H, W, 4), return a PNG-encoded byte string. + """ + io = QtCore.QBuffer() + qim = fn.makeQImage(img.transpose(1, 0, 2), alpha=False) + qim.save(io, 'PNG') + png = bytes(io.data().data()) + return png + + +def makeDiffImage(im1, im2): + """Return image array showing the differences between im1 and im2. + + Handles images of different shape. Alpha channels are not compared. + """ + ds = im1.shape + es = im2.shape + + diff = np.empty((max(ds[0], es[0]), max(ds[1], es[1]), 4), dtype=int) + diff[..., :3] = 128 + diff[..., 3] = 255 + diff[:ds[0], :ds[1], :min(ds[2], 3)] += im1[..., :3] + diff[:es[0], :es[1], :min(es[2], 3)] -= im2[..., :3] + diff = np.clip(diff, 0, 255).astype(np.ubyte) + return diff + + +class ImageTester(QtGui.QWidget): + """Graphical interface for auditing image comparison tests. + """ + def __init__(self): + self.lastKey = None + + QtGui.QWidget.__init__(self) + self.resize(1200, 800) + #self.showFullScreen() + + self.layout = QtGui.QGridLayout() + self.setLayout(self.layout) + + self.view = GraphicsLayoutWidget() + self.layout.addWidget(self.view, 0, 0, 1, 2) + + self.label = QtGui.QLabel() + self.layout.addWidget(self.label, 1, 0, 1, 2) + self.label.setWordWrap(True) + font = QtGui.QFont("monospace", 14, QtGui.QFont.Bold) + self.label.setFont(font) + + self.passBtn = QtGui.QPushButton('Pass') + self.failBtn = QtGui.QPushButton('Fail') + self.layout.addWidget(self.passBtn, 2, 0) + self.layout.addWidget(self.failBtn, 2, 1) + self.passBtn.clicked.connect(self.passTest) + self.failBtn.clicked.connect(self.failTest) + + self.views = (self.view.addViewBox(row=0, col=0), + self.view.addViewBox(row=0, col=1), + self.view.addViewBox(row=0, col=2)) + labelText = ['test output', 'standard', 'diff'] + for i, v in enumerate(self.views): + v.setAspectLocked(1) + v.invertY() + v.image = ImageItem(axisOrder='row-major') + v.image.setAutoDownsample(True) + v.addItem(v.image) + v.label = TextItem(labelText[i]) + v.setBackgroundColor(0.5) + + self.views[1].setXLink(self.views[0]) + self.views[1].setYLink(self.views[0]) + self.views[2].setXLink(self.views[0]) + self.views[2].setYLink(self.views[0]) + + def test(self, im1, im2, message): + """Ask the user to decide whether an image test passes or fails. + + This method displays the test image, reference image, and the difference + between the two. It then blocks until the user selects the test output + by clicking a pass/fail button or typing p/f. If the user fails the test, + then an exception is raised. + """ + self.show() + if im2 is None: + message += '\nImage1: %s %s Image2: [no standard]' % (im1.shape, im1.dtype) + im2 = np.zeros((1, 1, 3), dtype=np.ubyte) + else: + message += '\nImage1: %s %s Image2: %s %s' % (im1.shape, im1.dtype, im2.shape, im2.dtype) + self.label.setText(message) + + self.views[0].image.setImage(im1) + self.views[1].image.setImage(im2) + diff = makeDiffImage(im1, im2) + + self.views[2].image.setImage(diff) + self.views[0].autoRange() + + while True: + QtGui.QApplication.processEvents() + lastKey = self.lastKey + + self.lastKey = None + if lastKey in ('f', 'esc') or not self.isVisible(): + raise Exception("User rejected test result.") + elif lastKey == 'p': + break + time.sleep(0.03) + + for v in self.views: + v.image.setImage(np.zeros((1, 1, 3), dtype=np.ubyte)) + + def keyPressEvent(self, event): + if event.key() == QtCore.Qt.Key_Escape: + self.lastKey = 'esc' + else: + self.lastKey = str(event.text()).lower() + + def passTest(self): + self.lastKey = 'p' + + def failTest(self): + self.lastKey = 'f' + + +def getTestDataRepo(): + """Return the path to a git repository with the required commit checked + out. + + If the repository does not exist, then it is cloned from + https://github.com/pyqtgraph/test-data. If the repository already exists + then the required commit is checked out. + """ + global testDataTag + + dataPath = os.path.join(os.path.expanduser('~'), '.pyqtgraph', 'test-data') + gitPath = 'https://github.com/pyqtgraph/test-data' + gitbase = gitCmdBase(dataPath) + + if os.path.isdir(dataPath): + # Already have a test-data repository to work with. + + # Get the commit ID of testDataTag. Do a fetch if necessary. + try: + tagCommit = gitCommitId(dataPath, testDataTag) + except NameError: + cmd = gitbase + ['fetch', '--tags', 'origin'] + print(' '.join(cmd)) + sp.check_call(cmd) + try: + tagCommit = gitCommitId(dataPath, testDataTag) + except NameError: + raise Exception("Could not find tag '%s' in test-data repo at" + " %s" % (testDataTag, dataPath)) + except Exception: + if not os.path.exists(os.path.join(dataPath, '.git')): + raise Exception("Directory '%s' does not appear to be a git " + "repository. Please remove this directory." % + dataPath) + else: + raise + + # If HEAD is not the correct commit, then do a checkout + if gitCommitId(dataPath, 'HEAD') != tagCommit: + print("Checking out test-data tag '%s'" % testDataTag) + sp.check_call(gitbase + ['checkout', testDataTag]) + + else: + print("Attempting to create git clone of test data repo in %s.." % + dataPath) + + parentPath = os.path.split(dataPath)[0] + if not os.path.isdir(parentPath): + os.makedirs(parentPath) + + if os.getenv('TRAVIS') is not None: + # Create a shallow clone of the test-data repository (to avoid + # downloading more data than is necessary) + os.makedirs(dataPath) + cmds = [ + gitbase + ['init'], + gitbase + ['remote', 'add', 'origin', gitPath], + gitbase + ['fetch', '--tags', 'origin', testDataTag, + '--depth=1'], + gitbase + ['checkout', '-b', 'master', 'FETCH_HEAD'], + ] + else: + # Create a full clone + cmds = [['git', 'clone', gitPath, dataPath]] + + for cmd in cmds: + print(' '.join(cmd)) + rval = sp.check_call(cmd) + if rval == 0: + continue + raise RuntimeError("Test data path '%s' does not exist and could " + "not be created with git. Please create a git " + "clone of %s at this path." % + (dataPath, gitPath)) + + return dataPath + + +def gitCmdBase(path): + return ['git', '--git-dir=%s/.git' % path, '--work-tree=%s' % path] + + +def gitStatus(path): + """Return a string listing all changes to the working tree in a git + repository. + """ + cmd = gitCmdBase(path) + ['status', '--porcelain'] + return runSubprocess(cmd, stderr=None, universal_newlines=True) + + +def gitCommitId(path, ref): + """Return the commit id of *ref* in the git repository at *path*. + """ + cmd = gitCmdBase(path) + ['show', ref] + try: + output = runSubprocess(cmd, stderr=None, universal_newlines=True) + except sp.CalledProcessError: + print(cmd) + raise NameError("Unknown git reference '%s'" % ref) + commit = output.split('\n')[0] + assert commit[:7] == 'commit ' + return commit[7:] + + +def runSubprocess(command, return_code=False, **kwargs): + """Run command using subprocess.Popen + + Similar to subprocess.check_output(), which is not available in 2.6. + + Run command and wait for command to complete. If the return code was zero + then return, otherwise raise CalledProcessError. + By default, this will also add stdout= and stderr=subproces.PIPE + to the call to Popen to suppress printing to the terminal. + + Parameters + ---------- + command : list of str + Command to run as subprocess (see subprocess.Popen documentation). + **kwargs : dict + Additional kwargs to pass to ``subprocess.Popen``. + + Returns + ------- + stdout : str + Stdout returned by the process. + """ + # code adapted with permission from mne-python + use_kwargs = dict(stderr=None, stdout=sp.PIPE) + use_kwargs.update(kwargs) + + p = sp.Popen(command, **use_kwargs) + output = p.communicate()[0] + + # communicate() may return bytes, str, or None depending on the kwargs + # passed to Popen(). Convert all to unicode str: + output = '' if output is None else output + output = output.decode('utf-8') if isinstance(output, bytes) else output + + if p.returncode != 0: + print(output) + err_fun = sp.CalledProcessError.__init__ + if 'output' in inspect.getargspec(err_fun).args: + raise sp.CalledProcessError(p.returncode, command, output) + else: + raise sp.CalledProcessError(p.returncode, command) + + return output + + +def scenegraphState(view, name): + """Return information about the scenegraph for debugging test failures. + """ + state = "====== Scenegraph state for %s ======\n" % name + state += "view size: %dx%d\n" % (view.width(), view.height()) + state += "view transform:\n" + indent(transformStr(view.transform()), " ") + for item in view.scene().items(): + if item.parentItem() is None: + state += itemState(item) + '\n' + return state + + +def itemState(root): + state = str(root) + '\n' + from .. import ViewBox + state += 'bounding rect: ' + str(root.boundingRect()) + '\n' + if isinstance(root, ViewBox): + state += "view range: " + str(root.viewRange()) + '\n' + state += "transform:\n" + indent(transformStr(root.transform()).strip(), " ") + '\n' + for item in root.childItems(): + state += indent(itemState(item).strip(), " ") + '\n' + return state + + +def transformStr(t): + return ("[%0.2f %0.2f %0.2f]\n"*3) % (t.m11(), t.m12(), t.m13(), t.m21(), t.m22(), t.m23(), t.m31(), t.m32(), t.m33()) + + +def indent(s, pfx): + return '\n'.join([pfx+line for line in s.split('\n')]) + + +class TransposedImageItem(ImageItem): + # used for testing image axis order; we can test row-major and col-major using + # the same test images + def __init__(self, *args, **kwds): + self.__transpose = kwds.pop('transpose', False) + ImageItem.__init__(self, *args, **kwds) + def setImage(self, image=None, **kwds): + if image is not None and self.__transpose is True: + image = np.swapaxes(image, 0, 1) + return ImageItem.setImage(self, image, **kwds) diff --git a/vviewer/pyqtgraph/tests/test_exit_crash.py b/vviewer/pyqtgraph/tests/test_exit_crash.py index 69181f2..de457d5 100644 --- a/vviewer/pyqtgraph/tests/test_exit_crash.py +++ b/vviewer/pyqtgraph/tests/test_exit_crash.py @@ -1,6 +1,7 @@ import os, sys, subprocess, tempfile import pyqtgraph as pg - +import six +import pytest code = """ import sys @@ -10,10 +11,13 @@ app = pg.mkQApp() w = pg.{classname}({args}) """ +skipmessage = ('unclear why this test is failing. skipping until someone has' + ' time to fix it') +@pytest.mark.skipif(True, reason=skipmessage) def test_exit_crash(): - # For each Widget subclass, run a simple python script that creates an - # instance and then shuts down. The intent is to check for segmentation + # For each Widget subclass, run a simple python script that creates an + # instance and then shuts down. The intent is to check for segmentation # faults when each script exits. tmp = tempfile.mktemp(".py") path = os.path.dirname(pg.__file__) @@ -28,8 +32,8 @@ def test_exit_crash(): obj = getattr(pg, name) if not isinstance(obj, type) or not issubclass(obj, pg.QtGui.QWidget): continue - - print name + + print(name) argstr = initArgs.get(name, "") open(tmp, 'w').write(code.format(path=path, classname=name, args=argstr)) proc = subprocess.Popen([sys.executable, tmp]) diff --git a/vviewer/pyqtgraph/tests/test_functions.py b/vviewer/pyqtgraph/tests/test_functions.py index f622dd8..7ad3bf9 100644 --- a/vviewer/pyqtgraph/tests/test_functions.py +++ b/vviewer/pyqtgraph/tests/test_functions.py @@ -1,6 +1,7 @@ import pyqtgraph as pg import numpy as np from numpy.testing import assert_array_almost_equal, assert_almost_equal +import pytest np.random.seed(12345) @@ -21,22 +22,51 @@ 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, order=order) + assert result.shape == x.shape[:-1] + data.shape[x.shape[-1]:] + return result + data = np.array([[ 1., 2., 4. ], [ 10., 20., 40. ], [ 100., 200., 400.]]) + # test various x shapes + interpolateArray(data, np.ones((1,))) + interpolateArray(data, np.ones((2,))) + interpolateArray(data, np.ones((1, 1))) + interpolateArray(data, np.ones((1, 2))) + interpolateArray(data, np.ones((5, 1))) + interpolateArray(data, np.ones((5, 2))) + interpolateArray(data, np.ones((5, 5, 1))) + interpolateArray(data, np.ones((5, 5, 2))) + with pytest.raises(TypeError): + interpolateArray(data, np.ones((3,))) + with pytest.raises(TypeError): + interpolateArray(data, np.ones((1, 3,))) + with pytest.raises(TypeError): + interpolateArray(data, np.ones((5, 5, 3,))) + 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 = pg.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 + result = interpolateArray(data, x) + # 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) @@ -44,23 +74,25 @@ def test_interpolateArray(): x = np.array([[ 0.3, 0], [ 0.3, 1], [ 0.3, 2]]) + r1 = interpolateArray(data, x) + x = np.array([0.3]) # should broadcast across axis 1 + r2 = interpolateArray(data, x) - r1 = pg.interpolateArray(data, x) - r2 = pg.interpolateArray(data, x[0,:1]) assert_array_almost_equal(r1, r2) # 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 = pg.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. ]]) + r1 = interpolateArray(data, x) + 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) + 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)) @@ -76,6 +108,191 @@ def test_subArray(): assert np.all(bb == cc) +def test_rescaleData(): + dtypes = map(np.dtype, ('ubyte', 'uint16', 'byte', 'int16', 'int', 'float')) + for dtype1 in dtypes: + for dtype2 in dtypes: + data = (np.random.random(size=10) * 2**32 - 2**31).astype(dtype1) + for scale, offset in [(10, 0), (10., 0.), (1, -50), (0.2, 0.5), (0.001, 0)]: + if dtype2.kind in 'iu': + lim = np.iinfo(dtype2) + lim = lim.min, lim.max + else: + lim = (-np.inf, np.inf) + s1 = np.clip(float(scale) * (data-float(offset)), *lim).astype(dtype2) + s2 = pg.rescaleData(data, scale, offset, dtype2) + assert s1.dtype == s2.dtype + if dtype2.kind in 'iu': + assert np.all(s1 == s2) + else: + assert np.allclose(s1, s2) + + +def test_makeARGB(): + # Many parameters to test here: + # * data dtype (ubyte, uint16, float, others) + # * data ndim (2 or 3) + # * levels (None, 1D, or 2D) + # * lut dtype + # * lut size + # * lut ndim (1 or 2) + # * useRGBA argument + # Need to check that all input values map to the correct output values, especially + # at and beyond the edges of the level range. + + def checkArrays(a, b): + # because py.test output is difficult to read for arrays + if not np.all(a == b): + comp = [] + for i in range(a.shape[0]): + if a.shape[1] > 1: + comp.append('[') + for j in range(a.shape[1]): + m = a[i,j] == b[i,j] + comp.append('%d,%d %s %s %s%s' % + (i, j, str(a[i,j]).ljust(15), str(b[i,j]).ljust(15), + m, ' ********' if not np.all(m) else '')) + if a.shape[1] > 1: + comp.append(']') + raise Exception("arrays do not match:\n%s" % '\n'.join(comp)) + + def checkImage(img, check, alpha, alphaCheck): + assert img.dtype == np.ubyte + assert alpha is alphaCheck + if alpha is False: + checkArrays(img[..., 3], 255) + + if np.isscalar(check) or check.ndim == 3: + checkArrays(img[..., :3], check) + elif check.ndim == 2: + checkArrays(img[..., :3], check[..., np.newaxis]) + elif check.ndim == 1: + checkArrays(img[..., :3], check[..., np.newaxis, np.newaxis]) + else: + raise Exception('invalid check array ndim') + + # uint8 data tests + + im1 = np.arange(256).astype('ubyte').reshape(256, 1) + im2, alpha = pg.makeARGB(im1, levels=(0, 255)) + checkImage(im2, im1, alpha, False) + + im3, alpha = pg.makeARGB(im1, levels=(0.0, 255.0)) + checkImage(im3, im1, alpha, False) + + im4, alpha = pg.makeARGB(im1, levels=(255, 0)) + checkImage(im4, 255-im1, alpha, False) + + im5, alpha = pg.makeARGB(np.concatenate([im1]*3, axis=1), levels=[(0, 255), (0.0, 255.0), (255, 0)]) + checkImage(im5, np.concatenate([im1, im1, 255-im1], axis=1), alpha, False) + + + im2, alpha = pg.makeARGB(im1, levels=(128,383)) + checkImage(im2[:128], 0, alpha, False) + checkImage(im2[128:], im1[:128], alpha, False) + + + # uint8 data + uint8 LUT + lut = np.arange(256)[::-1].astype(np.uint8) + im2, alpha = pg.makeARGB(im1, lut=lut) + checkImage(im2, lut, alpha, False) + + # lut larger than maxint + lut = np.arange(511).astype(np.uint8) + im2, alpha = pg.makeARGB(im1, lut=lut) + checkImage(im2, lut[::2], alpha, False) + + # 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) + + # 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) + + 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) + + # uint8 data + uint16 LUT + lut = np.arange(4096)[::-1].astype(np.uint16) // 16 + im2, alpha = pg.makeARGB(im1, lut=lut) + checkImage(im2, np.arange(256)[::-1].astype('ubyte'), alpha, False) + + # uint8 data + float LUT + lut = np.linspace(10., 137., 256) + im2, alpha = pg.makeARGB(im1, lut=lut) + checkImage(im2, lut.astype('ubyte'), alpha, False) + + # uint8 data + 2D LUT + lut = np.zeros((256, 3), dtype='ubyte') + lut[:,0] = np.arange(256) + lut[:,1] = np.arange(256)[::-1] + lut[:,2] = 7 + im2, alpha = pg.makeARGB(im1, lut=lut) + checkImage(im2, lut[:,None,::-1], alpha, False) + + # check useRGBA + im2, alpha = pg.makeARGB(im1, lut=lut, useRGBA=True) + checkImage(im2, lut[:,None,:], alpha, False) + + + # uint16 data tests + im1 = np.arange(0, 2**16, 256).astype('uint16')[:, None] + im2, alpha = pg.makeARGB(im1, levels=(512, 2**16)) + checkImage(im2, np.clip(np.linspace(-2, 253, 256), 0, 255).astype('ubyte'), alpha, False) + + lut = (np.arange(512, 2**16)[::-1] // 256).astype('ubyte') + im2, alpha = pg.makeARGB(im1, lut=lut, levels=(512, 2**16-256)) + checkImage(im2, np.clip(np.linspace(257, 2, 256), 0, 255).astype('ubyte'), alpha, False) + + lut = np.zeros(2**16, dtype='ubyte') + lut[1000:1256] = np.arange(256) + lut[1256:] = 255 + im1 = np.arange(1000, 1256).astype('uint16')[:, None] + im2, alpha = pg.makeARGB(im1, lut=lut) + checkImage(im2, np.arange(256).astype('ubyte'), alpha, False) + + + + # float data tests + im1 = np.linspace(1.0, 17.0, 256)[:, None] + im2, alpha = pg.makeARGB(im1, levels=(5.0, 13.0)) + checkImage(im2, np.clip(np.linspace(-128, 383, 256), 0, 255).astype('ubyte'), alpha, False) + + lut = (np.arange(1280)[::-1] // 10).astype('ubyte') + im2, alpha = pg.makeARGB(im1, lut=lut, levels=(1, 17)) + checkImage(im2, np.linspace(127.5, 0, 256).astype('ubyte'), alpha, False) + + + # test sanity checks + class AssertExc(object): + def __init__(self, exc=Exception): + self.exc = exc + def __enter__(self): + return self + def __exit__(self, *args): + assert args[0] is self.exc, "Should have raised %s (got %s)" % (self.exc, args[0]) + return True + + with AssertExc(TypeError): # invalid image shape + pg.makeARGB(np.zeros((2,), dtype='float')) + with AssertExc(TypeError): # invalid image shape + pg.makeARGB(np.zeros((2,2,7), dtype='float')) + with AssertExc(): # float images require levels arg + pg.makeARGB(np.zeros((2,2), dtype='float')) + with AssertExc(): # bad levels arg + pg.makeARGB(np.zeros((2,2), dtype='float'), levels=[1]) + with AssertExc(): # bad levels arg + pg.makeARGB(np.zeros((2,2), dtype='float'), levels=[1,2,3]) + with AssertExc(): # can't mix 3-channel levels and LUT + pg.makeARGB(np.zeros((2,2)), lut=np.zeros((10,3), dtype='ubyte'), levels=[(0,1)]*3) + with AssertExc(): # multichannel levels must have same number of channels as image + pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=[(1,2)]*4) + with AssertExc(): # 3d levels not allowed + pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2])) + if __name__ == '__main__': test_interpolateArray() \ No newline at end of file diff --git a/vviewer/pyqtgraph/tests/test_qt.py b/vviewer/pyqtgraph/tests/test_qt.py index 729bf69..5c8800d 100644 --- a/vviewer/pyqtgraph/tests/test_qt.py +++ b/vviewer/pyqtgraph/tests/test_qt.py @@ -1,5 +1,7 @@ import pyqtgraph as pg import gc, os +import pytest + app = pg.mkQApp() @@ -11,7 +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') def test_loadUiType(): path = os.path.dirname(__file__) formClass, baseClass = pg.Qt.loadUiType(os.path.join(path, 'uictest.ui')) @@ -20,4 +23,3 @@ def test_loadUiType(): ui.setupUi(w) w.show() app.processEvents() - diff --git a/vviewer/pyqtgraph/tests/test_ref_cycles.py b/vviewer/pyqtgraph/tests/test_ref_cycles.py index 0284852..dec95ef 100644 --- a/vviewer/pyqtgraph/tests/test_ref_cycles.py +++ b/vviewer/pyqtgraph/tests/test_ref_cycles.py @@ -5,8 +5,14 @@ Test for unwanted reference cycles import pyqtgraph as pg import numpy as np import gc, weakref +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.') + def assert_alldead(refs): for ref in refs: assert ref() is None @@ -33,6 +39,8 @@ def mkrefs(*objs): return map(weakref.ref, allObjs.values()) + +@pytest.mark.skipif(six.PY3 or pg.Qt.USE_PYSIDE, reason=skipreason) def test_PlotWidget(): def mkobjs(*args, **kwds): w = pg.PlotWidget(*args, **kwds) @@ -50,6 +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) def test_ImageView(): def mkobjs(): iv = pg.ImageView() @@ -61,6 +70,8 @@ def test_ImageView(): for i in range(5): assert_alldead(mkobjs()) + +@pytest.mark.skipif(six.PY3 or pg.Qt.USE_PYSIDE, reason=skipreason) def test_GraphicsWindow(): def mkobjs(): w = pg.GraphicsWindow() diff --git a/vviewer/pyqtgraph/tests/test_stability.py b/vviewer/pyqtgraph/tests/test_stability.py index a64e30e..810b53b 100644 --- a/vviewer/pyqtgraph/tests/test_stability.py +++ b/vviewer/pyqtgraph/tests/test_stability.py @@ -6,7 +6,7 @@ the tear them down repeatedly. The purpose of this is to attempt to generate segmentation faults. """ -from PyQt4.QtTest import QTest +from pyqtgraph.Qt import QtTest import pyqtgraph as pg from random import seed, randint import sys, gc, weakref @@ -34,7 +34,7 @@ itemTypes = [ widgets = [] items = [] -allWidgets = weakref.WeakSet() +allWidgets = weakref.WeakKeyDictionary() def crashtest(): @@ -63,7 +63,7 @@ def crashtest(): print("Caught interrupt; send another to exit.") try: for i in range(100): - QTest.qWait(100) + QtTest.QTest.qWait(100) except KeyboardInterrupt: thread.terminate() break @@ -99,7 +99,7 @@ def createWidget(): widget = randItem(widgetTypes)() widget.setWindowTitle(widget.__class__.__name__) widgets.append(widget) - allWidgets.add(widget) + allWidgets[widget] = 1 p(" %s" % widget) return widget @@ -135,7 +135,7 @@ def showWidget(): def processEvents(): p('process events') - QTest.qWait(25) + QtTest.QTest.qWait(25) class TstException(Exception): pass @@ -157,4 +157,4 @@ def addReference(): if __name__ == '__main__': - test_stability() \ No newline at end of file + test_stability() diff --git a/vviewer/pyqtgraph/tests/ui_testing.py b/vviewer/pyqtgraph/tests/ui_testing.py new file mode 100644 index 0000000..4bcb760 --- /dev/null +++ b/vviewer/pyqtgraph/tests/ui_testing.py @@ -0,0 +1,75 @@ +import time +from ..Qt import QtCore, QtGui, QtTest, QT_LIB + + +def resizeWindow(win, w, h, timeout=2.0): + """Resize a window and wait until it has the correct size. + + This is required for unit testing on some platforms that do not guarantee + immediate response from the windowing system. + """ + QtGui.QApplication.processEvents() + # Sometimes the window size will switch multiple times before settling + # on its final size. Adding qWaitForWindowShown seems to help with this. + QtTest.QTest.qWaitForWindowShown(win) + win.resize(w, h) + start = time.time() + while True: + w1, h1 = win.width(), win.height() + if (w,h) == (w1,h1): + return + QtTest.QTest.qWait(10) + if time.time()-start > timeout: + raise TimeoutError("Window resize failed (requested %dx%d, got %dx%d)" % (w, h, w1, h1)) + + +# Functions for generating user input events. +# We would like to use QTest for this purpose, but it seems to be broken. +# See: http://stackoverflow.com/questions/16299779/qt-qgraphicsview-unit-testing-how-to-keep-the-mouse-in-a-pressed-state + +def mousePress(widget, pos, button, modifier=None): + if isinstance(widget, QtGui.QGraphicsView): + widget = widget.viewport() + if modifier is None: + modifier = QtCore.Qt.NoModifier + if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF): + pos = pos.toPoint() + event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonPress, pos, button, QtCore.Qt.NoButton, modifier) + QtGui.QApplication.sendEvent(widget, event) + + +def mouseRelease(widget, pos, button, modifier=None): + if isinstance(widget, QtGui.QGraphicsView): + widget = widget.viewport() + if modifier is None: + modifier = QtCore.Qt.NoModifier + if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF): + pos = pos.toPoint() + event = QtGui.QMouseEvent(QtCore.QEvent.MouseButtonRelease, pos, button, QtCore.Qt.NoButton, modifier) + QtGui.QApplication.sendEvent(widget, event) + + +def mouseMove(widget, pos, buttons=None, modifier=None): + if isinstance(widget, QtGui.QGraphicsView): + widget = widget.viewport() + if modifier is None: + modifier = QtCore.Qt.NoModifier + if buttons is None: + buttons = QtCore.Qt.NoButton + if QT_LIB != 'PyQt5' and isinstance(pos, QtCore.QPointF): + pos = pos.toPoint() + event = QtGui.QMouseEvent(QtCore.QEvent.MouseMove, pos, QtCore.Qt.NoButton, buttons, modifier) + QtGui.QApplication.sendEvent(widget, event) + + +def mouseDrag(widget, pos1, pos2, button, modifier=None): + mouseMove(widget, pos1) + mousePress(widget, pos1, button, modifier) + mouseMove(widget, pos2, button, modifier) + mouseRelease(widget, pos2, button, modifier) + + +def mouseClick(widget, pos, button, modifier=None): + mouseMove(widget, pos) + mousePress(widget, pos, button, modifier) + mouseRelease(widget, pos, button, modifier) diff --git a/vviewer/pyqtgraph/util/cprint.py b/vviewer/pyqtgraph/util/cprint.py index e88bfd1..8b4fa20 100644 --- a/vviewer/pyqtgraph/util/cprint.py +++ b/vviewer/pyqtgraph/util/cprint.py @@ -7,6 +7,7 @@ import sys, re from .colorama.winterm import WinTerm, WinColor, WinStyle from .colorama.win32 import windll +from ..python2_3 import basestring _WIN = sys.platform.startswith('win') if windll is not None: diff --git a/vviewer/pyqtgraph/util/garbage_collector.py b/vviewer/pyqtgraph/util/garbage_collector.py index 979e66c..0ea42dc 100644 --- a/vviewer/pyqtgraph/util/garbage_collector.py +++ b/vviewer/pyqtgraph/util/garbage_collector.py @@ -47,4 +47,4 @@ class GarbageCollector(object): def debug_cycles(self): gc.collect() for obj in gc.garbage: - print (obj, repr(obj), type(obj)) + print(obj, repr(obj), type(obj)) diff --git a/vviewer/pyqtgraph/widgets/ComboBox.py b/vviewer/pyqtgraph/widgets/ComboBox.py index 5cf6f91..a682895 100644 --- a/vviewer/pyqtgraph/widgets/ComboBox.py +++ b/vviewer/pyqtgraph/widgets/ComboBox.py @@ -1,8 +1,9 @@ +import sys from ..Qt import QtGui, QtCore from ..SignalProxy import SignalProxy -import sys from ..pgcollections import OrderedDict -from ..python2_3 import asUnicode +from ..python2_3 import asUnicode, basestring + class ComboBox(QtGui.QComboBox): """Extends QComboBox to add extra functionality. diff --git a/vviewer/pyqtgraph/widgets/GraphicsView.py b/vviewer/pyqtgraph/widgets/GraphicsView.py index 4062be9..f3f8cbb 100644 --- a/vviewer/pyqtgraph/widgets/GraphicsView.py +++ b/vviewer/pyqtgraph/widgets/GraphicsView.py @@ -63,7 +63,7 @@ class GraphicsView(QtGui.QGraphicsView): :func:`mkColor <pyqtgraph.mkColor>`. By default, the background color is determined using the 'backgroundColor' configuration option (see - :func:`setConfigOption <pyqtgraph.setConfigOption>`. + :func:`setConfigOptions <pyqtgraph.setConfigOptions>`). ============== ============================================================ """ @@ -165,7 +165,8 @@ class GraphicsView(QtGui.QGraphicsView): self.sceneObj = None self.closed = True self.setViewport(None) - + super(GraphicsView, self).close() + def useOpenGL(self, b=True): if b: if not HAVE_OPENGL: @@ -324,6 +325,7 @@ 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 diff --git a/vviewer/pyqtgraph/widgets/MatplotlibWidget.py b/vviewer/pyqtgraph/widgets/MatplotlibWidget.py index 959e188..3049683 100644 --- a/vviewer/pyqtgraph/widgets/MatplotlibWidget.py +++ b/vviewer/pyqtgraph/widgets/MatplotlibWidget.py @@ -1,11 +1,19 @@ -from ..Qt import QtGui, QtCore, USE_PYSIDE +from ..Qt import QtGui, QtCore, USE_PYSIDE, USE_PYQT5 import matplotlib -if USE_PYSIDE: - matplotlib.rcParams['backend.qt4']='PySide' +if not USE_PYQT5: + if USE_PYSIDE: + matplotlib.rcParams['backend.qt4']='PySide' + + from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas + try: + from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar + except ImportError: + from matplotlib.backends.backend_qt4agg import NavigationToolbar2QT as NavigationToolbar +else: + from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas + from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar -from matplotlib.backends.backend_qt4agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.backends.backend_qt4agg import NavigationToolbar2QTAgg as NavigationToolbar from matplotlib.figure import Figure class MatplotlibWidget(QtGui.QWidget): diff --git a/vviewer/pyqtgraph/widgets/PlotWidget.py b/vviewer/pyqtgraph/widgets/PlotWidget.py index e27bce6..964307a 100644 --- a/vviewer/pyqtgraph/widgets/PlotWidget.py +++ b/vviewer/pyqtgraph/widgets/PlotWidget.py @@ -69,7 +69,7 @@ class PlotWidget(GraphicsView): #self.scene().clear() #self.mPlotItem.close() self.setParent(None) - GraphicsView.close(self) + super(PlotWidget, self).close() def __getattr__(self, attr): ## implicitly wrap methods from plotItem if hasattr(self.plotItem, attr): diff --git a/vviewer/pyqtgraph/widgets/RawImageWidget.py b/vviewer/pyqtgraph/widgets/RawImageWidget.py index 970b570..657701f 100644 --- a/vviewer/pyqtgraph/widgets/RawImageWidget.py +++ b/vviewer/pyqtgraph/widgets/RawImageWidget.py @@ -3,7 +3,9 @@ try: from ..Qt import QtOpenGL from OpenGL.GL import * HAVE_OPENGL = True -except ImportError: +except Exception: + # Would prefer `except ImportError` here, but some versions of pyopengl generate + # AttributeError upon import HAVE_OPENGL = False from .. import functions as fn @@ -59,6 +61,7 @@ class RawImageWidget(QtGui.QWidget): #p.drawPixmap(self.rect(), self.pixmap) p.end() + if HAVE_OPENGL: class RawImageGLWidget(QtOpenGL.QGLWidget): """ diff --git a/vviewer/pyqtgraph/widgets/RemoteGraphicsView.py b/vviewer/pyqtgraph/widgets/RemoteGraphicsView.py index 75ce90b..85f5556 100644 --- a/vviewer/pyqtgraph/widgets/RemoteGraphicsView.py +++ b/vviewer/pyqtgraph/widgets/RemoteGraphicsView.py @@ -77,6 +77,10 @@ class RemoteGraphicsView(QtGui.QWidget): if sys.platform.startswith('win'): self.shmtag = newfile ## on windows, we create a new tag for every resize self.shm = mmap.mmap(-1, size, self.shmtag) ## can't use tmpfile on windows because the file can only be opened once. + elif sys.platform == 'darwin': + self.shmFile.close() + self.shmFile = open(self._view.shmFileName(), 'r') + self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) else: self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_READ) self.shm.seek(0) @@ -193,6 +197,13 @@ class Renderer(GraphicsView): ## it also says (sometimes) 'access is denied' if we try to reuse the tag. self.shmtag = "pyqtgraph_shmem_" + ''.join([chr((random.getrandbits(20)%25) + 97) for i in range(20)]) self.shm = mmap.mmap(-1, size, self.shmtag) + elif sys.platform == 'darwin': + self.shm.close() + self.shmFile.close() + self.shmFile = tempfile.NamedTemporaryFile(prefix='pyqtgraph_shmem_') + self.shmFile.write(b'\x00' * (size + 1)) + self.shmFile.flush() + self.shm = mmap.mmap(self.shmFile.fileno(), size, mmap.MAP_SHARED, mmap.PROT_WRITE) else: self.shm.resize(size) diff --git a/vviewer/pyqtgraph/widgets/ScatterPlotWidget.py b/vviewer/pyqtgraph/widgets/ScatterPlotWidget.py index 02f260c..cca40e6 100644 --- a/vviewer/pyqtgraph/widgets/ScatterPlotWidget.py +++ b/vviewer/pyqtgraph/widgets/ScatterPlotWidget.py @@ -13,10 +13,11 @@ __all__ = ['ScatterPlotWidget'] class ScatterPlotWidget(QtGui.QSplitter): """ - Given a record array, display a scatter plot of a specific set of data. - This widget includes controls for selecting the columns to plot, - filtering data, and determining symbol color and shape. This widget allows - the user to explore relationships between columns in a record array. + This is a high-level widget for exploring relationships in tabular data. + + Given a multi-column record array, the widget displays a scatter plot of a + specific subset of the data. Includes controls for selecting the columns to + plot, filtering data, and determining symbol color and shape. The widget consists of four components: diff --git a/vviewer/pyqtgraph/widgets/SpinBox.py b/vviewer/pyqtgraph/widgets/SpinBox.py index 4710140..b8066cd 100644 --- a/vviewer/pyqtgraph/widgets/SpinBox.py +++ b/vviewer/pyqtgraph/widgets/SpinBox.py @@ -1,25 +1,31 @@ # -*- 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 -from decimal import * +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'] + + 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:** @@ -42,67 +48,39 @@ 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): """ ============== ======================================================================== **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 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()) + 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, + 'wrapping': False, ## 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': '', @@ -112,9 +90,13 @@ class SpinBox(QtGui.QAbstractSpinBox): 'delayUntilEditFinished': True, ## do not send signals until text editing has finished - ## for compatibility with QDoubleSpinBox and QSpinBox - 'decimals': 2, + '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 } self.decOpts = ['step', 'minStep'] @@ -125,41 +107,95 @@ 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']) - + def event(self, ev): ret = QtGui.QAbstractSpinBox.event(self, ev) if ev.type() == QtCore.QEvent.KeyPress and ev.key() == QtCore.Qt.Key_Return: 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__ <pyqtgraph.SpinBox.__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 + wrapping (bool) If True and both bounds are not None, spin box has circular behavior. + 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: + + * *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. + 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': - #print opts[k] - self.setMinimum(opts[k][0], update=False) - self.setMaximum(opts[k][1], update=False) - #for i in [0,1]: - #if opts[k][i] is None: - #self.opts[k][i] = None - #else: - #self.opts[k][i] = D(unicode(opts[k][i])) + self.setMinimum(v[0], update=False) + self.setMaximum(v[1], update=False) + elif k == 'min': + self.setMinimum(v, update=False) + elif k == 'max': + 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(v) + elif k == 'regex' and isinstance(v, basestring): + self.opts[k] = re.compile(v) + elif k in self.opts: + self.opts[k] = v else: - self.opts[k] = opts[k] + raise TypeError("Invalid keyword argument '%s'." % k) if 'value' in opts: self.setValue(opts['value']) @@ -192,8 +228,6 @@ class SpinBox(QtGui.QAbstractSpinBox): self.updateText() - - def setMaximum(self, m, update=True): """Set the maximum allowed value (or None for no limit)""" if m is not None: @@ -209,11 +243,25 @@ 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. + """ self.setOpts(prefix=p) def setRange(self, r0, r1): + """Set the upper and lower limits for values in the spinbox. + """ self.setOpts(bounds = [r0,r1]) def setProperty(self, prop, val): @@ -226,12 +274,20 @@ class SpinBox(QtGui.QAbstractSpinBox): print("Warning: SpinBox.setProperty('%s', ..) not supported." % prop) def setSuffix(self, suf): + """Set the string suffix appended to the spinbox text. + """ self.setOpts(suffix=suf) def setSingleStep(self, step): + """Set the step size used when responding to the mouse wheel, arrow + buttons, or arrow keys. + """ self.setOpts(step=step) def setDecimals(self, decimals): + """Set the number of decimals to be displayed when formatting numeric + values. + """ self.setOpts(decimals=decimals) def selectNumber(self): @@ -240,14 +296,15 @@ 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) + self.selectNumber() def value(self): """ @@ -260,29 +317,39 @@ class SpinBox(QtGui.QAbstractSpinBox): return float(self.val) def setValue(self, value=None, update=True, delaySignal=False): - """ - Set the value of this spin. - If the value is out of bounds, it will be clipped to the nearest boundary. + """Set the value of this SpinBox. + + If the value is out of bounds, it will be clipped to the nearest boundary + or wrapped if wrapping is enabled. + If the spin is integer type, the value will be coerced to int. Returns the actual value set. If value is None, then the current value is used (this is for resetting the value after bounds, etc. have changed) """ - if value is None: 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 None not in bounds and self.opts['wrapping'] is True: + # 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) - value = D(asUnicode(value)) + if not isinstance(value, D): + value = D(asUnicode(value)) + if value == self.val: return prev = self.val @@ -296,7 +363,6 @@ class SpinBox(QtGui.QAbstractSpinBox): self.emitChanged() return value - def emitChanged(self): self.lastValEmitted = self.val @@ -316,13 +382,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 @@ -344,7 +406,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']) @@ -356,7 +418,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'] @@ -368,62 +429,64 @@ class SpinBox(QtGui.QAbstractSpinBox): if int(value) != value: return False return True - def updateText(self, prev=None): - #print "Update text." + # temporarily disable validation self.skipValidate = True - if self.opts['siPrefix']: + + 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'] + suffix = self.opts['suffix'] + + # format the string + 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: + # special case: if it's zero use the previous prefix (s, p) = fn.siScale(prev) - txt = "0.0 %s%s" % (p, self.opts['suffix']) else: - txt = fn.siFormat(float(self.val), suffix=self.opts['suffix']) + (s, p) = fn.siScale(val) + parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val} + else: - txt = '%g%s' % (self.val , self.opts['suffix']) - self.lineEdit().setText(txt) - self.lastText = txt - self.skipValidate = False + # 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 ' ' + return self.opts['format'].format(**parts) + def validate(self, strn, pos): if self.skipValidate: - #print "skip validate" - #self.textValid = False 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: - #print '"%s" != "%s"' % (unicode(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: - #print "can't interpret" - #self.setStyleSheet('SpinBox {border: 2px solid #C55;}') - #self.textValid = 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) - #print " OK:", self.val - #self.setStyleSheet('') - #self.textValid = True - - ret = QtGui.QValidator.Acceptable - else: - ret = QtGui.QValidator.Intermediate + ret = QtGui.QValidator.Intermediate except: - #print " BAD" - #import sys - #sys.excepthook(*sys.exc_info()) - #self.textValid = False - #self.setStyleSheet('SpinBox {border: 2px solid #C55;}') + import sys + sys.excepthook(*sys.exc_info()) ret = QtGui.QValidator.Intermediate ## draw / clear border @@ -435,50 +498,48 @@ 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, self.opts['regex']) + except Exception: + return False + + # check suffix + if suffix != self.opts['suffix'] or (suffix == '' and siprefix != ''): return False - #print val + + # generate value + val = self.opts['evalFunc'](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 interpretText(self, strn=None): - #print "Interpret:", strn - #if strn is None: - #strn = self.lineEdit().text() - #self.setValue(siEval(strn), update=False) - ##QtGui.QAbstractSpinBox.interpretText(self) - - + def editingFinishedEvent(self): """Edit has finished; set value.""" #print "Edit finished." @@ -487,7 +548,7 @@ class SpinBox(QtGui.QAbstractSpinBox): return try: val = self.interpret() - except: + except Exception: return if val is False: @@ -497,22 +558,44 @@ 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 + + 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. + (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 textChanged(self): - #print "Text changed." - - -### Drop-in replacement for SpinBox; just for crash-testing -#class SpinBox(QtGui.QDoubleSpinBox): - #valueChanged = QtCore.Signal(object) # (value) for compatibility with QSpinBox - #sigValueChanged = QtCore.Signal(object) # (self) - #sigValueChanging = QtCore.Signal(object) # (value) - #def __init__(self, parent=None, *args, **kargs): - #QtGui.QSpinBox.__init__(self, parent) - - #def __getattr__(self, attr): - #return lambda *args, **kargs: None - - #def widgetGroupInterface(self): - #return (self.valueChanged, SpinBox.value, SpinBox.setValue) - + def paintEvent(self, ev): + p = QtGui.QPainter(self) + p.setPen(fn.mkPen(color='r', width=2)) + p.drawRect(self.rect()) + p.end() diff --git a/vviewer/pyqtgraph/widgets/TableWidget.py b/vviewer/pyqtgraph/widgets/TableWidget.py index 69085a2..5785286 100644 --- a/vviewer/pyqtgraph/widgets/TableWidget.py +++ b/vviewer/pyqtgraph/widgets/TableWidget.py @@ -1,13 +1,8 @@ # -*- coding: utf-8 -*- -from ..Qt import QtGui, QtCore -from ..python2_3 import asUnicode - import numpy as np -try: - import metaarray - HAVE_METAARRAY = True -except ImportError: - HAVE_METAARRAY = False +from ..Qt import QtGui, QtCore +from ..python2_3 import asUnicode, basestring +from .. import metaarray __all__ = ['TableWidget'] @@ -207,7 +202,7 @@ class TableWidget(QtGui.QTableWidget): return lambda d: d.__iter__(), None elif isinstance(data, dict): return lambda d: iter(d.values()), list(map(asUnicode, data.keys())) - elif HAVE_METAARRAY and (hasattr(data, 'implements') and data.implements('MetaArray')): + elif (hasattr(data, 'implements') and data.implements('MetaArray')): if data.axisHasColumns(0): header = [asUnicode(data.columnName(0, i)) for i in range(data.shape[0])] elif data.axisHasValues(0): @@ -358,11 +353,11 @@ class TableWidget(QtGui.QTableWidget): self.contextMenu.popup(ev.globalPos()) def keyPressEvent(self, ev): - if ev.text() == 'c' and ev.modifiers() == QtCore.Qt.ControlModifier: + if ev.key() == QtCore.Qt.Key_C and ev.modifiers() == QtCore.Qt.ControlModifier: ev.accept() - self.copy() + self.copySel() else: - ev.ignore() + QtGui.QTableWidget.keyPressEvent(self, ev) def handleItemChanged(self, item): item.itemChanged() @@ -491,14 +486,13 @@ if __name__ == '__main__': t.setData(ll) - if HAVE_METAARRAY: - ma = metaarray.MetaArray(np.ones((20, 3)), info=[ - {'values': np.linspace(1, 5, 20)}, - {'cols': [ - {'name': 'x'}, - {'name': 'y'}, - {'name': 'z'}, - ]} - ]) - t.setData(ma) + ma = metaarray.MetaArray(np.ones((20, 3)), info=[ + {'values': np.linspace(1, 5, 20)}, + {'cols': [ + {'name': 'x'}, + {'name': 'y'}, + {'name': 'z'}, + ]} + ]) + t.setData(ma) diff --git a/vviewer/pyqtgraph/widgets/TreeWidget.py b/vviewer/pyqtgraph/widgets/TreeWidget.py index ec2c35c..b98da6f 100644 --- a/vviewer/pyqtgraph/widgets/TreeWidget.py +++ b/vviewer/pyqtgraph/widgets/TreeWidget.py @@ -1,8 +1,12 @@ # -*- coding: utf-8 -*- -from ..Qt import QtGui, QtCore from weakref import * +from ..Qt import QtGui, QtCore +from ..python2_3 import xrange + __all__ = ['TreeWidget', 'TreeWidgetItem'] + + class TreeWidget(QtGui.QTreeWidget): """Extends QTreeWidget to allow internal drag/drop with widgets in the tree. Also maintains the expanded state of subtrees as they are moved. diff --git a/vviewer/pyqtgraph/widgets/tests/test_spinbox.py b/vviewer/pyqtgraph/widgets/tests/test_spinbox.py new file mode 100644 index 0000000..1008788 --- /dev/null +++ b/vviewer/pyqtgraph/widgets/tests/test_spinbox.py @@ -0,0 +1,28 @@ +import pyqtgraph as pg +pg.mkQApp() + + +def test_spinbox_formatting(): + sb = pg.SpinBox() + assert sb.opts['decimals'] == 6 + 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, '1e+06', dict(int=True, decimals=6)), + (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: + sb.setOpts(**opts) + sb.setValue(value) + assert sb.value() == value + assert pg.asUnicode(sb.text()) == text -- GitLab