diff --git a/vviewer/ColorMapItem.py b/vviewer/ColorMapItem.py index 9dfe69d5c07e975dd776c51e2cb1eaf303f21b86..476072c89b54b548da0dec895f3bdc3f734ff379 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 81ee23a13ebc80a5cb50253e0c33f385932a661b..7087661267eab92beeead49cac9a96c588d06f91 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 6f5354dca47552b0b6e0e9e225798ae0aecea604..952a241566aaa062871aecd4da16890286bbd4e0 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 5efb7c44c8ca980885a2f21ee82caa3b4c7ab917..2676a3b4054d48ad03d85b90ed36fec83f5067a3 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 0000000000000000000000000000000000000000..418fd0f0c6fb759024aed74c93f404cb9f092b58 --- /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 2e472e04d1e9a832a4be7b014c9255968d46fe63..fb9d368341b14c6c2d204e35fdbdc2ed6afbd240 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 efbe66c4927f14f99ca7a780cb4a4ec20630c2ba..2ed9d6f90aee2cb5e2f5ba1cd38f453845968155 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 23281343f9a4f5a8b88190db2b8e82230a61d061..b1aea2973ee607bed7d88b7afa9fa9a58faced18 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 29541454a13424f0e6b9e9e7b2c2667d8667b992..d7e265c5faa41d4551ecf7ae0db3789fe81e58ce 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 1c152d46216f28e558718ae3981154088a144375..bc5081f70e9281abc42ba0353eba7dc29d27562e 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 4de891f795e26fe44e2cdcd6b191d8504799cf77..5b5ce2f7013f9976d92683c0c4a5f04390e2a354 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 b6ecbb396b53cad80256d04c4fc7fa6cc6ca1aa0..a06235b239cabc1ef11987cf1be4a963ec8fdeb8 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 9bea8f89218b533063b37bee5e18e7fc1404b09a..b05c11cdf8782aa021fe59be6752b5a281359662 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 557354e0a821582486c861d9d36838c626e11e82..b65ef465a4578ea33ff84ebaf145bfb1b98cad7f 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 0000000000000000000000000000000000000000..20f5e3396e43f784f699ca6b3926bd1ccdd57aef --- /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 56d1ff47cb21bc0c9f279e98531aa94c9e085e99..b0e05a07b2a8cbb0345bfc6daa61369b58694d5a 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 75c694c074fde0794806e335b33c29994fe45899..c6cf82e4b1b6b81cf188a3533960ad3bd9ce3855 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 0000000000000000000000000000000000000000..6b1f239b59f6c2e77cfc9dbb6f6b6efd563089ff --- /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 bce7b511df43bf8c4c9ad84018f7b25d21f61236..e430b61a8fd5ba338005c6949625a8bd2f4fd10c 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 c0033708298e3bcc6de2f75ee94cd3d9e54244fd..f943e2fe0690df3ce8665423a660bf5eba9c4360 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 c095bba3059f2c7495bcb5ecabcd3edd38d4ac82..7b20db1da3cc9a1c411d9f1835c397399f90f583 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 896de92467a50937c81b15caa7477443f56a110e..ed4b7f08b929edc05edeb0998c9e14209a1de188 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 0000000000000000000000000000000000000000..1fbc5bed2cad4e1cbfdfd35280da56724137635f --- /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 57c71bc8cbd3bafb9dfda14c1c11614abeadad81..0da24d7c805c6f0db83132639c97f84de8f46246 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 28d4244b0e55aad5cefee882899b84b73154b132..4493d075e1eccc9759b4e914cbd1738f3ab12953 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 a75d881d49db903e1453f2d92c15480aa50e3bca..ffe75b61cd00a09413a14423a07caee45095d4b7 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 64a2529449a489dd129b14111166a517d9b673e9..792e36bdd2a19e6a751a8edec0dca08b72427214 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 a91466c8a9dc0a94236ebab97470bbf9db29b0fc..ccf921654e999f66a36543f10c65103742bdda70 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 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/vviewer/pyqtgraph/exporters/tests/test_csv.py b/vviewer/pyqtgraph/exporters/tests/test_csv.py index a98372ec4f66dedf45296026bab02ac3e5f7e077..15c6626e206fb463a81370e637f0fc163e09eb5a 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 871f43c2b05f4388c0ea85dfd3523086cad86528..2261f7df65cd179b3b5eb642e0edaa1a57685eff 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 ab5f4a820bf75137f9717fc2e0da0c8ca045897b..b623f5c79f262169cf181e5442dd9eea41f812c4 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 0000000000000000000000000000000000000000..b661918d2d15c9662dc503ffbdcca041581ef018 --- /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 ab4b2914d155f34d41d8b77f69fca76f15897e0f..93011218dd489d72c412e73e52bcde2e8f03570a 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 0000000000000000000000000000000000000000..ba754305dfb0d3137e6a4779dd8ee17b884bcf9d --- /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 fc7b04d3553087eb02d07a39865053e1a3f2ec87..c450a9f3238f4ec92603cc1604431686134055f7 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 6a6db62e05b42be580ab8b7b97fc53f6907a5086..016e2d30466a021b7e3e89283e5e201dd6064ede 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 554989b2e33525909e187374b14a45f7975b43ff..0000000000000000000000000000000000000000 --- 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 88a2f6c553c36e2e30254e8cf33bd23e73631bee..9392b037d76f3371c9bdf075af586070285f32da 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 338d25c41693caf6b51e83d7c24815e2fe29c7c1..cb7fb41a80836f494130ce96a2c7799aa5f3e493 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 33a0fba053ce8a5f542afb2764e7d5a54f931604..080dbc5e55769d63b01697b25248488b2477b753 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 bb6beebcd1637e431a39509cb5a796c4ab29a2c0..f7682a4361c937f4a5b3c3dba62a61aa9b26a8c8 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 15a14f86324e42275b5294d5f4ff8d90410d1e15..0efb11dd15d47910bce6cc30ca176d41b7e4791a 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 a151798a5c50456103879d1c326011a43a3fcc44..f359ff119c9e2266a620ca8b26934ea671e15bee 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 2ca3519359cc52ec76bb8ada3c105992d034e67d..d45818dc5588e67f412fa2e7190da86ea48d37b9 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 89ebef3e07ea6abed034ac1d670326a800591c51..31764250c87761c2d26b6ae02d39c13e499b7311 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 5b0414336bad36f0f6f00a1af1ef2174622fdd9f..3d45ad77db83f3f9f0bbd364923fa969f2323885 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 8108c3cf5c229e598a24c7cc9d26949f263387a2..3da8232724656450a8a25aa71951ec8c527bdfc8 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 4474e29aa6bdca88a3fd4b377c5aa360084a47e8..03ebc69feb9072de65233cec9d003f49f5298bd5 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 3d3e969dad3caf7a5decc6368d29119a5aa89143..d66a8a99bf260c29b252dcf57eae436525d5ca31 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 6148989d741affc753683e6be4b9465921ce2645..37245becba5ebd315c956aa093288f5148c9472f 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 4f10b0e388b76cf67207bd3d518f81b141e9f5a1..41011df36c3f826a6f48404591bc2c7799ad48b3 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 0000000000000000000000000000000000000000..e9fdff248397e8fa75d518aca298271cd2504b48 --- /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 7707466ade9313050db2a9e65173fb1acc769b1f..963ecb050b592983c3e64a14e8f95126c774c09c 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 faae863206dfc0c775d18169ab99debb42d9bb59..54667b50217e0ef231d76f79c453c8efec26ee08 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 d3c98006867875b46fc6db4d40427406bfa08beb..b2587ded516c42220b2a36df5abfdf481a1dc39f 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 022a96763df94f152875132d5476c6203a26de41..007ff8da261d9f86760f1ab84ed88b9b9ce05e5a 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 0e7d79126991ea99476f95f2dae2270a152db020..10392d7e003160ceea0e9c29beb2442cea600de4 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 d8ef19259bf3772b59568feda9305e40ccda7f26..5d952741c80e77833c6591a238123a98f1bdf64e 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 0000000000000000000000000000000000000000..78da6eea58e5ac0652bed229a4893386c99cb1ab --- /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 f1063e7f116f80363e028d2fc291f9eac16b828b..68f4f497c124e9510eb68fac29b98a0007ac2c7b 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 0000000000000000000000000000000000000000..4f310bc3724ca22a7c0ca6e49ddcfdd5fbfdaa15 --- /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 0000000000000000000000000000000000000000..24438864881383251d6a7ece43b0a5f6d4e44782 --- /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 0000000000000000000000000000000000000000..a3c34b11702a198d851ddfcfae31523da9f34ea9 --- /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 0000000000000000000000000000000000000000..ddc7f173e4880ff2091b7564e245b2ada63052db --- /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 8b0ebc8fe936d69d1d549ba6ba02c6b960210018..acf6ad721491aeb9cbbafa498f1efe43ed03991f 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 65252cfe4b7d4214c97c4d8ba1b67bd386858184..5cc00f6810f0045270de57cff52fd92b2ff7b421 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 e728b265421de33567729741d7bb2c9fb4c7d549..8c9d5633984f67163d55604c13641cdd89603a2d 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 0000000000000000000000000000000000000000..4b4009b6f0118943464d2c74e0fcf4992b083ad5 --- /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 2ca1712c29129a64f9f0afa2b653ddb5735540c2..3057a8a55789b03ac65f7f9ef35ffbefb34ba4d0 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 9c3f5b8a193fe4f4e31aa54b3e189830b73a7f34..66ecc460414a23ade5c78f07a3975f88fbfb883a 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 f4ddd95c75d000aceb7b21db7de56fa80dede944..934bc6d07a1d433ff0c88aa10b31c45aba62a8d9 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 0dfb80b99d5e943f27428d046c6395888f54b37d..c7e4a80c1d0fc9cea7a2d8b0c5f472b04321f5a2 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 4f484b7444ff81f2de4e9ca248b6e29f868e8661..208e17f424fabe09459257a746bcbe2e4d61ba20 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 12c5b70711cb5b2048bb89a30b4c5c63979a2ad1..a2c2708ab8fe06eead5e7b6bd100d30fd09329cf 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 992aa73e38e4318b593c0d6a342f72ad049be043..e0fee046bdea6364abcf036c9d9e98bfac58fdcb 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 5adf4b648806ecb57d1e079ed97f79794797afc7..f83fcdf650f45f4a74504e8af1981a665b3af0e5 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 5f37ccdc0800d62729bce7c064e2474a98200e04..de9a1624d8beb720acf8b277a46b956edbc0edea 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 0a889dfaf7135163104156b117399cf0bede0024..24e35e9a6375f676828cde3988f48b2dbdb72ba1 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 7b1c5ee6104cc26eac63c847253d0beed9cc53fb..2535b13a8dc721ad63be25001bc1effe98f953f4 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 c7cd2cb335a327d0ed67a2adef8536161d94174d..dc581019b104fbba65b2a4d800d83232bf3f492a 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 c26e4a6b42ad174b5f6849f6671bab186cd32bb4..7a3411ccdcc6418bab20b3cdf199ba1076d60e11 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 Binary files /dev/null and b/vviewer/pyqtgraph/pixmaps/auto.png differ diff --git a/vviewer/pyqtgraph/pixmaps/ctrl.png b/vviewer/pyqtgraph/pixmaps/ctrl.png new file mode 100644 index 0000000000000000000000000000000000000000..c8dc96e439da8ef33abf292bd9b808633646c26b Binary files /dev/null and b/vviewer/pyqtgraph/pixmaps/ctrl.png differ diff --git a/vviewer/pyqtgraph/pixmaps/default.png b/vviewer/pyqtgraph/pixmaps/default.png new file mode 100644 index 0000000000000000000000000000000000000000..f12394214dadb66bd90e7c671d87a5810c1571a6 Binary files /dev/null and b/vviewer/pyqtgraph/pixmaps/default.png differ diff --git a/vviewer/pyqtgraph/pixmaps/icons.svg b/vviewer/pyqtgraph/pixmaps/icons.svg new file mode 100644 index 0000000000000000000000000000000000000000..cfdfeba4b6b0e9deeb455ca5a542357016cce23a --- /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 Binary files /dev/null and b/vviewer/pyqtgraph/pixmaps/lock.png differ diff --git a/vviewer/pyqtgraph/python2_3.py b/vviewer/pyqtgraph/python2_3.py index b1c46f2654dc5b2e42b67b7b5c5c55a88f54b9b3..ae4667ebd472195d8ae8e78e75328bb8e39a572a 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 0000000000000000000000000000000000000000..393bd3c5c05210da6945dd32e65717ccc960a867 --- /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 0000000000000000000000000000000000000000..c8a41dec99794d6eea6d8313bdf6bf0c2b8e6826 --- /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 69181f2167bb05e2d528e0f7bf183d476fa43dbb..de457d544373302c52da1677b2d7898e4d8f5a58 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 f622dd8736d9a8fa43bc23e4d5614e7c9dc6e146..7ad3bf91d67732116337f7bc415f11ac2e445d66 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 729bf695a20b0bfcf2dda285179ec7bc6ea28d2b..5c8800dddd449cab131348e81d0e1e896d75c74c 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 0284852ce0c72d02856b1ab7f3603e19a41ab68e..dec95ef7362cf0655277e25f94ead0cda1247c89 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 a64e30e4b2982f7fb54c1122d41d6f7f00a6d5a7..810b53bf363e349721605446687483b77bfda819 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 0000000000000000000000000000000000000000..4bcb7606801e959cf662e66814983db222845200 --- /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 e88bfd1a54c108099493bece18ae7afc179584a1..8b4fa20814794f95dde8b4766ca32d2592f7a6bc 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 979e66c50ed9f1633d0c4b80bacf53157487b2a1..0ea42dcccc1bf9fe55234eddc3163a29aff34703 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 5cf6f9183f00f374018d62308baaf8841260c2fa..a6828959f67272206659681d7e13f834e6a23076 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 4062be94fa36ae6b9e3361ff6cea09fe43e5854c..f3f8cbb5fe8a5540ad2bef828c6475e71c5e1d8b 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 959e188a334ab927c098b4e872a29c51ee349f57..3049683987aef9b3a4774d5e7d3b5053a2ae8cad 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 e27bce6083ebd923ac02d60ee8966b0e3573454d..964307ae1946cc172123bed96797a7388456ac12 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 970b570b24c4112af8cb1e25b609b378728e5e23..657701f937fb8171678335d05f733eb9acf98d89 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 75ce90b0d93d1685bc9f77c7e20e68474406285d..85f5556abe83e1c336d436b24df5d9a7dfb91f7d 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 02f260ca4459b5ada5c18ac28aa227f36d8a59eb..cca40e65592aa88cdeb616faaa88ea20fa766151 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 4710140511a74571011d43f7e3985c05d4098627..b8066cd792a4c3234b18c67c5d095aae6a2d76e0 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 69085a208993e98bc3a6b40e1538200148141676..57852864e5436f86b1a2faea6fdaac1da042290c 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 ec2c35cf5be9e05f7320600cc280b5a75ef23be6..b98da6fa997b3743a8813c5d12223a020d5cb763 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 0000000000000000000000000000000000000000..100878816d74448233b5fabf8fbb1258d7cb94e1 --- /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