Squashed commit of the following:
commit ca3fbe2ff9
Author: Luke Campagnola <luke.campagnola@gmail.com>
Date: Thu Aug 7 08:41:30 2014 -0400
Merged numerous updates from acq4:
* Added HDF5 exporter
* CSV exporter gets (x,y,y,y) export mode
* Updates to SVG, Matplotlib exporter
* Console can filter exceptions by string
* Added tick context menu to GradientEditorItem
* Added export feature to imageview
* Parameter trees:
- Option to save only user-editable values
- Option to set visible title of parameters separately from name
- Added experimental ParameterSystem for handling large systems of
interdependent parameters
- Auto-select editable portion of spinbox when editing
* Added Vector.__abs__
* Added replacement garbage collector for avoiding crashes on multithreaded Qt
* Fixed "illegal instruction" caused by closing file handle 7 on OSX
* configfile now reloads QtCore objects, Point, ColorMap, numpy arrays
* Avoid triggering recursion issues in exception handler
* Various bugfies and performance enhancements
This commit is contained in:
parent
55a07b0bec
commit
753ac9b4c4
@ -81,5 +81,7 @@ class Vector(QtGui.QVector3D):
|
||||
# ang *= -1.
|
||||
return ang * 180. / np.pi
|
||||
|
||||
def __abs__(self):
|
||||
return Vector(abs(self.x()), abs(self.y()), abs(self.z()))
|
||||
|
||||
|
@ -325,8 +325,13 @@ def exit():
|
||||
atexit._run_exitfuncs()
|
||||
|
||||
## close file handles
|
||||
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
|
||||
|
||||
if sys.platform == 'darwin':
|
||||
for fd in xrange(3, 4096):
|
||||
if fd not in [7]: # trying to close 7 produces an illegal instruction on the Mac.
|
||||
os.close(fd)
|
||||
else:
|
||||
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
|
||||
|
||||
os._exit(0)
|
||||
|
||||
|
||||
|
@ -67,8 +67,8 @@ class Canvas(QtGui.QWidget):
|
||||
self.ui.itemList.sigItemMoved.connect(self.treeItemMoved)
|
||||
self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected)
|
||||
self.ui.autoRangeBtn.clicked.connect(self.autoRange)
|
||||
self.ui.storeSvgBtn.clicked.connect(self.storeSvg)
|
||||
self.ui.storePngBtn.clicked.connect(self.storePng)
|
||||
#self.ui.storeSvgBtn.clicked.connect(self.storeSvg)
|
||||
#self.ui.storePngBtn.clicked.connect(self.storePng)
|
||||
self.ui.redirectCheck.toggled.connect(self.updateRedirect)
|
||||
self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect)
|
||||
self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged)
|
||||
@ -94,11 +94,13 @@ class Canvas(QtGui.QWidget):
|
||||
self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent
|
||||
|
||||
|
||||
def storeSvg(self):
|
||||
self.ui.view.writeSvg()
|
||||
#def storeSvg(self):
|
||||
#from pyqtgraph.GraphicsScene.exportDialog import ExportDialog
|
||||
#ex = ExportDialog(self.ui.view)
|
||||
#ex.show()
|
||||
|
||||
def storePng(self):
|
||||
self.ui.view.writeImage()
|
||||
#def storePng(self):
|
||||
#self.ui.view.writeImage()
|
||||
|
||||
def splitterMoved(self):
|
||||
self.resizeEvent()
|
||||
@ -571,7 +573,9 @@ class Canvas(QtGui.QWidget):
|
||||
self.menu.popup(ev.globalPos())
|
||||
|
||||
def removeClicked(self):
|
||||
self.removeItem(self.menuItem)
|
||||
#self.removeItem(self.menuItem)
|
||||
for item in self.selectedItems():
|
||||
self.removeItem(item)
|
||||
self.menuItem = None
|
||||
import gc
|
||||
gc.collect()
|
||||
|
@ -28,21 +28,7 @@
|
||||
<widget class="GraphicsView" name="view"/>
|
||||
<widget class="QWidget" name="layoutWidget">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="1" column="0">
|
||||
<widget class="QPushButton" name="storeSvgBtn">
|
||||
<property name="text">
|
||||
<string>Store SVG</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QPushButton" name="storePngBtn">
|
||||
<property name="text">
|
||||
<string>Store PNG</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QPushButton" name="autoRangeBtn">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
@ -55,7 +41,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0" colspan="2">
|
||||
<item row="5" column="0" colspan="2">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
@ -75,7 +61,7 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="7" column="0" colspan="2">
|
||||
<item row="6" column="0" colspan="2">
|
||||
<widget class="TreeWidget" name="itemList">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
@ -93,28 +79,28 @@
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="11" column="0" colspan="2">
|
||||
<item row="10" column="0" colspan="2">
|
||||
<layout class="QGridLayout" name="ctrlLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="8" column="0">
|
||||
<item row="7" column="0">
|
||||
<widget class="QPushButton" name="resetTransformsBtn">
|
||||
<property name="text">
|
||||
<string>Reset Transforms</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<item row="3" column="0">
|
||||
<widget class="QPushButton" name="mirrorSelectionBtn">
|
||||
<property name="text">
|
||||
<string>Mirror Selection</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<item row="3" column="1">
|
||||
<widget class="QPushButton" name="reflectSelectionBtn">
|
||||
<property name="text">
|
||||
<string>MirrorXY</string>
|
||||
|
@ -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 'acq4/pyqtgraph/canvas/CanvasTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:52 2013
|
||||
# by: PyQt4 UI code generator 4.10
|
||||
# Created: Thu Jan 2 11:13:07 2014
|
||||
# by: PyQt4 UI code generator 4.9
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@ -12,16 +12,7 @@ from PyQt4 import QtCore, QtGui
|
||||
try:
|
||||
_fromUtf8 = QtCore.QString.fromUtf8
|
||||
except AttributeError:
|
||||
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)
|
||||
_fromUtf8 = lambda s: s
|
||||
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
@ -41,12 +32,6 @@ class Ui_Form(object):
|
||||
self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget)
|
||||
self.gridLayout_2.setMargin(0)
|
||||
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
|
||||
self.storeSvgBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.storeSvgBtn.setObjectName(_fromUtf8("storeSvgBtn"))
|
||||
self.gridLayout_2.addWidget(self.storeSvgBtn, 1, 0, 1, 1)
|
||||
self.storePngBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.storePngBtn.setObjectName(_fromUtf8("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)
|
||||
@ -54,7 +39,7 @@ class Ui_Form(object):
|
||||
sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth())
|
||||
self.autoRangeBtn.setSizePolicy(sizePolicy)
|
||||
self.autoRangeBtn.setObjectName(_fromUtf8("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(_fromUtf8("horizontalLayout"))
|
||||
@ -64,7 +49,7 @@ class Ui_Form(object):
|
||||
self.redirectCombo = CanvasCombo(self.layoutWidget)
|
||||
self.redirectCombo.setObjectName(_fromUtf8("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)
|
||||
@ -74,35 +59,33 @@ class Ui_Form(object):
|
||||
self.itemList.setHeaderHidden(True)
|
||||
self.itemList.setObjectName(_fromUtf8("itemList"))
|
||||
self.itemList.headerItem().setText(0, _fromUtf8("1"))
|
||||
self.gridLayout_2.addWidget(self.itemList, 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(_fromUtf8("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(_fromUtf8("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(_fromUtf8("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(_fromUtf8("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)
|
||||
QtCore.QMetaObject.connectSlotsByName(Form)
|
||||
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(_translate("Form", "Form", None))
|
||||
self.storeSvgBtn.setText(_translate("Form", "Store SVG", None))
|
||||
self.storePngBtn.setText(_translate("Form", "Store PNG", 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))
|
||||
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))
|
||||
|
||||
from ..widgets.TreeWidget import TreeWidget
|
||||
from CanvasManager import CanvasCombo
|
||||
|
@ -244,4 +244,7 @@ class ColorMap(object):
|
||||
else:
|
||||
return np.all(self.color == np.array([[0,0,0,255], [255,255,255,255]]))
|
||||
|
||||
|
||||
def __repr__(self):
|
||||
pos = repr(self.pos).replace('\n', '')
|
||||
color = repr(self.color).replace('\n', '')
|
||||
return "ColorMap(%s, %s)" % (pos, color)
|
||||
|
@ -14,6 +14,10 @@ from .pgcollections import OrderedDict
|
||||
GLOBAL_PATH = None # so not thread safe.
|
||||
from . import units
|
||||
from .python2_3 import asUnicode
|
||||
from .Qt import QtCore
|
||||
from .Point import Point
|
||||
from .colormap import ColorMap
|
||||
import numpy
|
||||
|
||||
class ParseError(Exception):
|
||||
def __init__(self, message, lineNum, line, fileName=None):
|
||||
@ -46,7 +50,7 @@ def readConfigFile(fname):
|
||||
fname2 = os.path.join(GLOBAL_PATH, fname)
|
||||
if os.path.exists(fname2):
|
||||
fname = fname2
|
||||
|
||||
|
||||
GLOBAL_PATH = os.path.dirname(os.path.abspath(fname))
|
||||
|
||||
try:
|
||||
@ -135,6 +139,17 @@ def parseString(lines, start=0):
|
||||
local = units.allUnits.copy()
|
||||
local['OrderedDict'] = OrderedDict
|
||||
local['readConfigFile'] = readConfigFile
|
||||
local['Point'] = Point
|
||||
local['QtCore'] = QtCore
|
||||
local['ColorMap'] = ColorMap
|
||||
# Needed for reconstructing numpy arrays
|
||||
local['array'] = numpy.array
|
||||
for dtype in ['int8', 'uint8',
|
||||
'int16', 'uint16', 'float16',
|
||||
'int32', 'uint32', 'float32',
|
||||
'int64', 'uint64', 'float64']:
|
||||
local[dtype] = getattr(numpy, dtype)
|
||||
|
||||
if len(k) < 1:
|
||||
raise ParseError('Missing name preceding colon', ln+1, l)
|
||||
if k[0] == '(' and k[-1] == ')': ## If the key looks like a tuple, try evaluating it.
|
||||
|
@ -341,6 +341,17 @@ class ConsoleWidget(QtGui.QWidget):
|
||||
filename = tb.tb_frame.f_code.co_filename
|
||||
function = tb.tb_frame.f_code.co_name
|
||||
|
||||
filterStr = str(self.ui.filterText.text())
|
||||
if filterStr != '':
|
||||
if isinstance(exc, Exception):
|
||||
msg = exc.message
|
||||
elif isinstance(exc, basestring):
|
||||
msg = exc
|
||||
else:
|
||||
msg = repr(exc)
|
||||
match = re.search(filterStr, "%s:%s:%s" % (filename, function, msg))
|
||||
return match is not None
|
||||
|
||||
## Go through a list of common exception points we like to ignore:
|
||||
if excType is GeneratorExit or excType is StopIteration:
|
||||
return False
|
||||
|
@ -6,7 +6,7 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>710</width>
|
||||
<width>694</width>
|
||||
<height>497</height>
|
||||
</rect>
|
||||
</property>
|
||||
@ -89,6 +89,16 @@
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item row="0" column="6">
|
||||
<widget class="QPushButton" name="clearExceptionBtn">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Clear Exception</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QPushButton" name="catchAllExceptionsBtn">
|
||||
<property name="text">
|
||||
@ -109,7 +119,7 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<item row="0" column="4">
|
||||
<widget class="QCheckBox" name="onlyUncaughtCheck">
|
||||
<property name="text">
|
||||
<string>Only Uncaught Exceptions</string>
|
||||
@ -119,14 +129,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0" colspan="5">
|
||||
<item row="2" column="0" colspan="7">
|
||||
<widget class="QListWidget" name="exceptionStackList">
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="5">
|
||||
<item row="3" column="0" colspan="7">
|
||||
<widget class="QCheckBox" name="runSelectedFrameCheck">
|
||||
<property name="text">
|
||||
<string>Run commands in selected stack frame</string>
|
||||
@ -136,24 +146,14 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0" colspan="5">
|
||||
<item row="1" column="0" colspan="7">
|
||||
<widget class="QLabel" name="exceptionInfoLabel">
|
||||
<property name="text">
|
||||
<string>Exception Info</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QPushButton" name="clearExceptionBtn">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Clear Exception</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<item row="0" column="5">
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@ -166,6 +166,16 @@
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Filter (regex):</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="3">
|
||||
<widget class="QLineEdit" name="filterText"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
|
@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/console/template.ui'
|
||||
# Form implementation generated from reading ui file 'template.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:53 2013
|
||||
# by: PyQt4 UI code generator 4.10
|
||||
# Created: Fri May 02 18:55:28 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@ -26,7 +26,7 @@ except AttributeError:
|
||||
class Ui_Form(object):
|
||||
def setupUi(self, Form):
|
||||
Form.setObjectName(_fromUtf8("Form"))
|
||||
Form.resize(710, 497)
|
||||
Form.resize(694, 497)
|
||||
self.gridLayout = QtGui.QGridLayout(Form)
|
||||
self.gridLayout.setMargin(0)
|
||||
self.gridLayout.setSpacing(0)
|
||||
@ -71,6 +71,10 @@ class Ui_Form(object):
|
||||
self.gridLayout_2.setSpacing(0)
|
||||
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
|
||||
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
|
||||
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.clearExceptionBtn.setEnabled(False)
|
||||
self.clearExceptionBtn.setObjectName(_fromUtf8("clearExceptionBtn"))
|
||||
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1)
|
||||
self.catchAllExceptionsBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.catchAllExceptionsBtn.setCheckable(True)
|
||||
self.catchAllExceptionsBtn.setObjectName(_fromUtf8("catchAllExceptionsBtn"))
|
||||
@ -82,24 +86,26 @@ class Ui_Form(object):
|
||||
self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup)
|
||||
self.onlyUncaughtCheck.setChecked(True)
|
||||
self.onlyUncaughtCheck.setObjectName(_fromUtf8("onlyUncaughtCheck"))
|
||||
self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1)
|
||||
self.exceptionStackList = QtGui.QListWidget(self.exceptionGroup)
|
||||
self.exceptionStackList.setAlternatingRowColors(True)
|
||||
self.exceptionStackList.setObjectName(_fromUtf8("exceptionStackList"))
|
||||
self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5)
|
||||
self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7)
|
||||
self.runSelectedFrameCheck = QtGui.QCheckBox(self.exceptionGroup)
|
||||
self.runSelectedFrameCheck.setChecked(True)
|
||||
self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck"))
|
||||
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5)
|
||||
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7)
|
||||
self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup)
|
||||
self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel"))
|
||||
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5)
|
||||
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
|
||||
self.clearExceptionBtn.setEnabled(False)
|
||||
self.clearExceptionBtn.setObjectName(_fromUtf8("clearExceptionBtn"))
|
||||
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1)
|
||||
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
|
||||
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
|
||||
self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1)
|
||||
self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1)
|
||||
self.label = QtGui.QLabel(self.exceptionGroup)
|
||||
self.label.setObjectName(_fromUtf8("label"))
|
||||
self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1)
|
||||
self.filterText = QtGui.QLineEdit(self.exceptionGroup)
|
||||
self.filterText.setObjectName(_fromUtf8("filterText"))
|
||||
self.gridLayout_2.addWidget(self.filterText, 0, 3, 1, 1)
|
||||
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
|
||||
|
||||
self.retranslateUi(Form)
|
||||
@ -110,11 +116,12 @@ class Ui_Form(object):
|
||||
self.historyBtn.setText(_translate("Form", "History..", None))
|
||||
self.exceptionBtn.setText(_translate("Form", "Exceptions..", None))
|
||||
self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", None))
|
||||
self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None))
|
||||
self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None))
|
||||
self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None))
|
||||
self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None))
|
||||
self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None))
|
||||
self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None))
|
||||
self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None))
|
||||
self.label.setText(_translate("Form", "Filter (regex):", None))
|
||||
|
||||
from .CmdInput import CmdInput
|
||||
|
@ -32,6 +32,57 @@ def ftrace(func):
|
||||
return rv
|
||||
return w
|
||||
|
||||
|
||||
class Tracer(object):
|
||||
"""
|
||||
Prints every function enter/exit. Useful for debugging crashes / lockups.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
self.stack = []
|
||||
|
||||
def trace(self, frame, event, arg):
|
||||
self.count += 1
|
||||
# If it has been a long time since we saw the top of the stack,
|
||||
# print a reminder
|
||||
if self.count % 1000 == 0:
|
||||
print("----- current stack: -----")
|
||||
for line in self.stack:
|
||||
print(line)
|
||||
if event == 'call':
|
||||
line = " " * len(self.stack) + ">> " + self.frameInfo(frame)
|
||||
print(line)
|
||||
self.stack.append(line)
|
||||
elif event == 'return':
|
||||
self.stack.pop()
|
||||
line = " " * len(self.stack) + "<< " + self.frameInfo(frame)
|
||||
print(line)
|
||||
if len(self.stack) == 0:
|
||||
self.count = 0
|
||||
|
||||
return self.trace
|
||||
|
||||
def stop(self):
|
||||
sys.settrace(None)
|
||||
|
||||
def start(self):
|
||||
sys.settrace(self.trace)
|
||||
|
||||
def frameInfo(self, fr):
|
||||
filename = fr.f_code.co_filename
|
||||
funcname = fr.f_code.co_name
|
||||
lineno = fr.f_lineno
|
||||
callfr = sys._getframe(3)
|
||||
callline = "%s %d" % (callfr.f_code.co_name, callfr.f_lineno)
|
||||
args, _, _, value_dict = inspect.getargvalues(fr)
|
||||
if len(args) and args[0] == 'self':
|
||||
instance = value_dict.get('self', None)
|
||||
if instance is not None:
|
||||
cls = getattr(instance, '__class__', None)
|
||||
if cls is not None:
|
||||
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."""
|
||||
def w(*args, **kwds):
|
||||
@ -41,17 +92,22 @@ def warnOnException(func):
|
||||
printExc('Ignored exception:')
|
||||
return w
|
||||
|
||||
def getExc(indent=4, prefix='| '):
|
||||
tb = traceback.format_exc()
|
||||
lines = []
|
||||
for l in tb.split('\n'):
|
||||
lines.append(" "*indent + prefix + l)
|
||||
return '\n'.join(lines)
|
||||
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]))
|
||||
lines2 = []
|
||||
for l in lines:
|
||||
lines2.extend(l.strip('\n').split('\n'))
|
||||
lines3 = [" "*indent + prefix + l for l in lines2]
|
||||
return '\n'.join(lines3)
|
||||
|
||||
|
||||
def printExc(msg='', indent=4, prefix='|'):
|
||||
"""Print an error message followed by an indented exception backtrace
|
||||
(This function is intended to be called within except: blocks)"""
|
||||
exc = getExc(indent, prefix + ' ')
|
||||
exc = getExc(indent, prefix + ' ', skip=2)
|
||||
print("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg))
|
||||
print(" "*indent + prefix + '='*30 + '>>')
|
||||
print(exc)
|
||||
@ -407,6 +463,7 @@ class Profiler(object):
|
||||
|
||||
_depth = 0
|
||||
_msgs = []
|
||||
disable = False # set this flag to disable all or individual profilers at runtime
|
||||
|
||||
class DisabledProfiler(object):
|
||||
def __init__(self, *args, **kwds):
|
||||
@ -418,12 +475,11 @@ class Profiler(object):
|
||||
def mark(self, msg=None):
|
||||
pass
|
||||
_disabledProfiler = DisabledProfiler()
|
||||
|
||||
|
||||
|
||||
def __new__(cls, msg=None, disabled='env', delayed=True):
|
||||
"""Optionally create a new profiler based on caller's qualname.
|
||||
"""
|
||||
if disabled is True or (disabled=='env' and len(cls._profilers) == 0):
|
||||
if disabled is True or (disabled == 'env' and len(cls._profilers) == 0):
|
||||
return cls._disabledProfiler
|
||||
|
||||
# determine the qualified name of the caller function
|
||||
@ -431,11 +487,11 @@ class Profiler(object):
|
||||
try:
|
||||
caller_object_type = type(caller_frame.f_locals["self"])
|
||||
except KeyError: # we are in a regular function
|
||||
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1]
|
||||
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[1]
|
||||
else: # we are in a method
|
||||
qualifier = caller_object_type.__name__
|
||||
func_qualname = qualifier + "." + caller_frame.f_code.co_name
|
||||
if disabled=='env' and func_qualname not in cls._profilers: # don't do anything
|
||||
if disabled == 'env' and func_qualname not in cls._profilers: # don't do anything
|
||||
return cls._disabledProfiler
|
||||
# create an actual profiling object
|
||||
cls._depth += 1
|
||||
@ -447,13 +503,12 @@ class Profiler(object):
|
||||
obj._firstTime = obj._lastTime = ptime.time()
|
||||
obj._newMsg("> Entering " + obj._name)
|
||||
return obj
|
||||
#else:
|
||||
#def __new__(cls, delayed=True):
|
||||
#return lambda msg=None: None
|
||||
|
||||
def __call__(self, msg=None):
|
||||
"""Register or print a new message with timing information.
|
||||
"""
|
||||
if self.disable:
|
||||
return
|
||||
if msg is None:
|
||||
msg = str(self._markCount)
|
||||
self._markCount += 1
|
||||
@ -479,7 +534,7 @@ class Profiler(object):
|
||||
def finish(self, msg=None):
|
||||
"""Add a final message; flush the message list if no parent profiler.
|
||||
"""
|
||||
if self._finished:
|
||||
if self._finished or self.disable:
|
||||
return
|
||||
self._finished = True
|
||||
if msg is not None:
|
||||
@ -984,6 +1039,7 @@ def qObjectReport(verbose=False):
|
||||
|
||||
|
||||
class PrintDetector(object):
|
||||
"""Find code locations that print to stdout."""
|
||||
def __init__(self):
|
||||
self.stdout = sys.stdout
|
||||
sys.stdout = self
|
||||
@ -1002,6 +1058,45 @@ class PrintDetector(object):
|
||||
self.stdout.flush()
|
||||
|
||||
|
||||
def listQThreads():
|
||||
"""Prints Thread IDs (Qt's, not OS's) for all QThreads."""
|
||||
thr = findObj('[Tt]hread')
|
||||
thr = [t for t in thr if isinstance(t, QtCore.QThread)]
|
||||
import sip
|
||||
for t in thr:
|
||||
print("--> ", t)
|
||||
print(" Qt ID: 0x%x" % sip.unwrapinstance(t))
|
||||
|
||||
|
||||
def pretty(data, indent=''):
|
||||
"""Format nested dict/list/tuple structures into a more human-readable string
|
||||
This function is a bit better than pprint for displaying OrderedDicts.
|
||||
"""
|
||||
ret = ""
|
||||
ind2 = indent + " "
|
||||
if isinstance(data, dict):
|
||||
ret = indent+"{\n"
|
||||
for k, v in data.iteritems():
|
||||
ret += ind2 + repr(k) + ": " + pretty(v, ind2).strip() + "\n"
|
||||
ret += indent+"}\n"
|
||||
elif isinstance(data, list) or isinstance(data, tuple):
|
||||
s = repr(data)
|
||||
if len(s) < 40:
|
||||
ret += indent + s
|
||||
else:
|
||||
if isinstance(data, list):
|
||||
d = '[]'
|
||||
else:
|
||||
d = '()'
|
||||
ret = indent+d[0]+"\n"
|
||||
for i, v in enumerate(data):
|
||||
ret += ind2 + str(i) + ": " + pretty(v, ind2).strip() + "\n"
|
||||
ret += indent+d[1]+"\n"
|
||||
else:
|
||||
ret += indent + repr(data)
|
||||
return ret
|
||||
|
||||
|
||||
class PeriodicTrace(object):
|
||||
"""
|
||||
Used to debug freezing by starting a new thread that reports on the
|
||||
|
@ -49,29 +49,45 @@ def setTracebackClearing(clear=True):
|
||||
|
||||
class ExceptionHandler(object):
|
||||
def __call__(self, *args):
|
||||
## call original exception handler first (prints exception)
|
||||
global original_excepthook, callbacks, clear_tracebacks
|
||||
print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time()))))
|
||||
ret = original_excepthook(*args)
|
||||
## Start by extending recursion depth just a bit.
|
||||
## If the error we are catching is due to recursion, we don't want to generate another one here.
|
||||
recursionLimit = sys.getrecursionlimit()
|
||||
try:
|
||||
sys.setrecursionlimit(recursionLimit+100)
|
||||
|
||||
for cb in callbacks:
|
||||
|
||||
## call original exception handler first (prints exception)
|
||||
global original_excepthook, callbacks, clear_tracebacks
|
||||
try:
|
||||
cb(*args)
|
||||
except:
|
||||
print(" --------------------------------------------------------------")
|
||||
print(" Error occurred during exception callback %s" % str(cb))
|
||||
print(" --------------------------------------------------------------")
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
|
||||
|
||||
## Clear long-term storage of last traceback to prevent memory-hogging.
|
||||
## (If an exception occurs while a lot of data is present on the stack,
|
||||
## such as when loading large files, the data would ordinarily be kept
|
||||
## until the next exception occurs. We would rather release this memory
|
||||
## as soon as possible.)
|
||||
if clear_tracebacks is True:
|
||||
sys.last_traceback = None
|
||||
print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time()))))
|
||||
except Exception:
|
||||
sys.stderr.write("Warning: stdout is broken! Falling back to stderr.\n")
|
||||
sys.stdout = sys.stderr
|
||||
|
||||
ret = original_excepthook(*args)
|
||||
|
||||
for cb in callbacks:
|
||||
try:
|
||||
cb(*args)
|
||||
except Exception:
|
||||
print(" --------------------------------------------------------------")
|
||||
print(" Error occurred during exception callback %s" % str(cb))
|
||||
print(" --------------------------------------------------------------")
|
||||
traceback.print_exception(*sys.exc_info())
|
||||
|
||||
|
||||
## Clear long-term storage of last traceback to prevent memory-hogging.
|
||||
## (If an exception occurs while a lot of data is present on the stack,
|
||||
## such as when loading large files, the data would ordinarily be kept
|
||||
## until the next exception occurs. We would rather release this memory
|
||||
## as soon as possible.)
|
||||
if clear_tracebacks is True:
|
||||
sys.last_traceback = None
|
||||
|
||||
finally:
|
||||
sys.setrecursionlimit(recursionLimit)
|
||||
|
||||
|
||||
def implements(self, interface=None):
|
||||
## this just makes it easy for us to detect whether an ExceptionHook is already installed.
|
||||
if interface is None:
|
||||
|
@ -14,6 +14,7 @@ class CSVExporter(Exporter):
|
||||
self.params = Parameter(name='params', type='group', children=[
|
||||
{'name': 'separator', 'type': 'list', 'value': 'comma', 'values': ['comma', 'tab']},
|
||||
{'name': 'precision', 'type': 'int', 'value': 10, 'limits': [0, None]},
|
||||
{'name': 'columnMode', 'type': 'list', 'values': ['(x,y) per plot', '(x,y,y,y) for all plots']}
|
||||
])
|
||||
|
||||
def parameters(self):
|
||||
@ -31,15 +32,24 @@ class CSVExporter(Exporter):
|
||||
fd = open(fileName, 'w')
|
||||
data = []
|
||||
header = []
|
||||
for c in self.item.curves:
|
||||
|
||||
appendAllX = self.params['columnMode'] == '(x,y) per plot'
|
||||
|
||||
for i, c in enumerate(self.item.curves):
|
||||
cd = c.getData()
|
||||
if cd[0] is None:
|
||||
continue
|
||||
data.append(cd)
|
||||
name = ''
|
||||
if hasattr(c, 'implements') and c.implements('plotData') and c.name() is not None:
|
||||
name = c.name().replace('"', '""') + '_'
|
||||
header.extend(['"'+name+'x"', '"'+name+'y"'])
|
||||
xName, yName = '"'+name+'x"', '"'+name+'y"'
|
||||
else:
|
||||
xName = 'x%04d' % i
|
||||
yName = 'y%04d' % i
|
||||
if appendAllX or i == 0:
|
||||
header.extend([xName, yName])
|
||||
else:
|
||||
header.extend([yName])
|
||||
|
||||
if self.params['separator'] == 'comma':
|
||||
sep = ','
|
||||
@ -51,12 +61,20 @@ class CSVExporter(Exporter):
|
||||
numFormat = '%%0.%dg' % self.params['precision']
|
||||
numRows = max([len(d[0]) for d in data])
|
||||
for i in range(numRows):
|
||||
for d in data:
|
||||
for j in [0, 1]:
|
||||
if i < len(d[j]):
|
||||
fd.write(numFormat % d[j][i] + sep)
|
||||
for j, d in enumerate(data):
|
||||
# write x value if this is the first column, or if we want x
|
||||
# for all rows
|
||||
if appendAllX or j == 0:
|
||||
if d is not None and i < len(d[0]):
|
||||
fd.write(numFormat % d[0][i] + sep)
|
||||
else:
|
||||
fd.write(' %s' % sep)
|
||||
|
||||
# write y value
|
||||
if d is not None and i < len(d[1]):
|
||||
fd.write(numFormat % d[1][i] + sep)
|
||||
else:
|
||||
fd.write(' %s' % sep)
|
||||
fd.write('\n')
|
||||
fd.close()
|
||||
|
||||
|
58
pyqtgraph/exporters/HDF5Exporter.py
Normal file
58
pyqtgraph/exporters/HDF5Exporter.py
Normal file
@ -0,0 +1,58 @@
|
||||
from ..Qt import QtGui, QtCore
|
||||
from .Exporter import Exporter
|
||||
from ..parametertree import Parameter
|
||||
from .. import PlotItem
|
||||
|
||||
import numpy
|
||||
try:
|
||||
import h5py
|
||||
HAVE_HDF5 = True
|
||||
except ImportError:
|
||||
HAVE_HDF5 = False
|
||||
|
||||
__all__ = ['HDF5Exporter']
|
||||
|
||||
|
||||
class HDF5Exporter(Exporter):
|
||||
Name = "HDF5 Export: plot (x,y)"
|
||||
windows = []
|
||||
allowCopy = False
|
||||
|
||||
def __init__(self, item):
|
||||
Exporter.__init__(self, item)
|
||||
self.params = Parameter(name='params', type='group', children=[
|
||||
{'name': 'Name', 'type': 'str', 'value': 'Export',},
|
||||
{'name': 'columnMode', 'type': 'list', 'values': ['(x,y) per plot', '(x,y,y,y) for all plots']},
|
||||
])
|
||||
|
||||
def parameters(self):
|
||||
return self.params
|
||||
|
||||
def export(self, fileName=None):
|
||||
if not HAVE_HDF5:
|
||||
raise RuntimeError("This exporter requires the h5py package, "
|
||||
"but it was not importable.")
|
||||
|
||||
if not isinstance(self.item, PlotItem):
|
||||
raise Exception("Must have a PlotItem selected for HDF5 export.")
|
||||
|
||||
if fileName is None:
|
||||
self.fileSaveDialog(filter=["*.h5", "*.hdf", "*.hd5"])
|
||||
return
|
||||
dsname = self.params['Name']
|
||||
fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite"
|
||||
data = []
|
||||
|
||||
appendAllX = self.params['columnMode'] == '(x,y) per plot'
|
||||
for i,c in enumerate(self.item.curves):
|
||||
d = c.getData()
|
||||
if appendAllX or i == 0:
|
||||
data.append(d[0])
|
||||
data.append(d[1])
|
||||
|
||||
fdata = numpy.array(data).astype('double')
|
||||
dset = fd.create_dataset(dsname, data=fdata)
|
||||
fd.close()
|
||||
|
||||
if HAVE_HDF5:
|
||||
HDF5Exporter.register()
|
@ -4,7 +4,29 @@ from .. import PlotItem
|
||||
from .. import functions as fn
|
||||
|
||||
__all__ = ['MatplotlibExporter']
|
||||
|
||||
|
||||
"""
|
||||
It is helpful when using the matplotlib Exporter if your
|
||||
.matplotlib/matplotlibrc file is configured appropriately.
|
||||
The following are suggested for getting usable PDF output that
|
||||
can be edited in Illustrator, etc.
|
||||
|
||||
backend : Qt4Agg
|
||||
text.usetex : True # Assumes you have a findable LaTeX installation
|
||||
interactive : False
|
||||
font.family : sans-serif
|
||||
font.sans-serif : 'Arial' # (make first in list)
|
||||
mathtext.default : sf
|
||||
figure.facecolor : white # personal preference
|
||||
# next setting allows pdf font to be readable in Adobe Illustrator
|
||||
pdf.fonttype : 42 # set fonts to TrueType (otherwise it will be 3
|
||||
# and the text will be vectorized.
|
||||
text.dvipnghack : True # primarily to clean up font appearance on Mac
|
||||
|
||||
The advantage is that there is less to do to get an exported file cleaned and ready for
|
||||
publication. Fonts are not vectorized (outlined), and window colors are white.
|
||||
|
||||
"""
|
||||
|
||||
class MatplotlibExporter(Exporter):
|
||||
Name = "Matplotlib Window"
|
||||
@ -14,18 +36,43 @@ class MatplotlibExporter(Exporter):
|
||||
|
||||
def parameters(self):
|
||||
return None
|
||||
|
||||
def cleanAxes(self, axl):
|
||||
if type(axl) is not list:
|
||||
axl = [axl]
|
||||
for ax in axl:
|
||||
if ax is None:
|
||||
continue
|
||||
for loc, spine in ax.spines.iteritems():
|
||||
if loc in ['left', 'bottom']:
|
||||
pass
|
||||
elif loc in ['right', 'top']:
|
||||
spine.set_color('none')
|
||||
# do not draw the spine
|
||||
else:
|
||||
raise ValueError('Unknown spine location: %s' % loc)
|
||||
# turn off ticks when there is no spine
|
||||
ax.xaxis.set_ticks_position('bottom')
|
||||
|
||||
def export(self, fileName=None):
|
||||
|
||||
if isinstance(self.item, PlotItem):
|
||||
mpw = MatplotlibWindow()
|
||||
MatplotlibExporter.windows.append(mpw)
|
||||
|
||||
stdFont = 'Arial'
|
||||
|
||||
fig = mpw.getFigure()
|
||||
|
||||
ax = fig.add_subplot(111)
|
||||
# get labels from the graphic item
|
||||
xlabel = self.item.axes['bottom']['item'].label.toPlainText()
|
||||
ylabel = self.item.axes['left']['item'].label.toPlainText()
|
||||
title = self.item.titleLabel.text
|
||||
|
||||
ax = fig.add_subplot(111, title=title)
|
||||
ax.clear()
|
||||
self.cleanAxes(ax)
|
||||
#ax.grid(True)
|
||||
|
||||
for item in self.item.curves:
|
||||
x, y = item.getData()
|
||||
opts = item.opts
|
||||
@ -42,17 +89,21 @@ class MatplotlibExporter(Exporter):
|
||||
symbolBrush = fn.mkBrush(opts['symbolBrush'])
|
||||
markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())])
|
||||
markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.color())])
|
||||
markersize = opts['symbolSize']
|
||||
|
||||
if opts['fillLevel'] is not None and opts['fillBrush'] is not None:
|
||||
fillBrush = fn.mkBrush(opts['fillBrush'])
|
||||
fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())])
|
||||
ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor)
|
||||
|
||||
ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(), linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor)
|
||||
|
||||
pl = ax.plot(x, y, marker=symbol, color=color, linewidth=pen.width(),
|
||||
linestyle=linestyle, markeredgecolor=markeredgecolor, markerfacecolor=markerfacecolor,
|
||||
markersize=markersize)
|
||||
xr, yr = self.item.viewRange()
|
||||
ax.set_xbound(*xr)
|
||||
ax.set_ybound(*yr)
|
||||
ax.set_xlabel(xlabel) # place the labels.
|
||||
ax.set_ylabel(ylabel)
|
||||
mpw.draw()
|
||||
else:
|
||||
raise Exception("Matplotlib export currently only works with plot items")
|
||||
|
@ -102,14 +102,12 @@ xmlHeader = """\
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny">
|
||||
<title>pyqtgraph SVG export</title>
|
||||
<desc>Generated with Qt and pyqtgraph</desc>
|
||||
<defs>
|
||||
</defs>
|
||||
"""
|
||||
|
||||
def generateSvg(item):
|
||||
global xmlHeader
|
||||
try:
|
||||
node = _generateItemSvg(item)
|
||||
node, defs = _generateItemSvg(item)
|
||||
finally:
|
||||
## reset export mode for all items in the tree
|
||||
if isinstance(item, QtGui.QGraphicsScene):
|
||||
@ -124,7 +122,11 @@ def generateSvg(item):
|
||||
|
||||
cleanXml(node)
|
||||
|
||||
return xmlHeader + node.toprettyxml(indent=' ') + "\n</svg>\n"
|
||||
defsXml = "<defs>\n"
|
||||
for d in defs:
|
||||
defsXml += d.toprettyxml(indent=' ')
|
||||
defsXml += "</defs>\n"
|
||||
return xmlHeader + defsXml + node.toprettyxml(indent=' ') + "\n</svg>\n"
|
||||
|
||||
|
||||
def _generateItemSvg(item, nodes=None, root=None):
|
||||
@ -230,6 +232,10 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
g1 = doc.getElementsByTagName('g')[0]
|
||||
## get list of sub-groups
|
||||
g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g']
|
||||
|
||||
defs = doc.getElementsByTagName('defs')
|
||||
if len(defs) > 0:
|
||||
defs = [n for n in defs[0].childNodes if isinstance(n, xml.Element)]
|
||||
except:
|
||||
print(doc.toxml())
|
||||
raise
|
||||
@ -238,7 +244,7 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
|
||||
## Get rid of group transformation matrices by applying
|
||||
## transformation to inner coordinates
|
||||
correctCoordinates(g1, item)
|
||||
correctCoordinates(g1, defs, item)
|
||||
profiler('correct')
|
||||
## make sure g1 has the transformation matrix
|
||||
#m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
|
||||
@ -275,7 +281,9 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape()))
|
||||
item.scene().addItem(path)
|
||||
try:
|
||||
pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0]
|
||||
#pathNode = _generateItemSvg(path, root=root).getElementsByTagName('path')[0]
|
||||
pathNode = _generateItemSvg(path, root=root)[0].getElementsByTagName('path')[0]
|
||||
# assume <defs> for this path is empty.. possibly problematic.
|
||||
finally:
|
||||
item.scene().removeItem(path)
|
||||
|
||||
@ -294,14 +302,19 @@ def _generateItemSvg(item, nodes=None, root=None):
|
||||
## Add all child items as sub-elements.
|
||||
childs.sort(key=lambda c: c.zValue())
|
||||
for ch in childs:
|
||||
cg = _generateItemSvg(ch, nodes, root)
|
||||
if cg is None:
|
||||
csvg = _generateItemSvg(ch, nodes, root)
|
||||
if csvg is None:
|
||||
continue
|
||||
cg, cdefs = csvg
|
||||
childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now)
|
||||
defs.extend(cdefs)
|
||||
|
||||
profiler('children')
|
||||
return g1
|
||||
return g1, defs
|
||||
|
||||
def correctCoordinates(node, item):
|
||||
def correctCoordinates(node, defs, item):
|
||||
# TODO: correct gradient coordinates inside defs
|
||||
|
||||
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
|
||||
## Each item is represented by a single top-level group with one or more groups inside.
|
||||
## Each inner group contains one or more drawing primitives, possibly of different types.
|
||||
|
@ -4,7 +4,7 @@ from .SVGExporter import *
|
||||
from .Matplotlib import *
|
||||
from .CSVExporter import *
|
||||
from .PrintExporter import *
|
||||
|
||||
from .HDF5Exporter import *
|
||||
|
||||
def listExporters():
|
||||
return Exporter.Exporters[:]
|
||||
|
@ -20,41 +20,12 @@ from ..debug import printExc
|
||||
from .. import configfile as configfile
|
||||
from .. import dockarea as dockarea
|
||||
from . import FlowchartGraphicsView
|
||||
from .. import functions as fn
|
||||
|
||||
def strDict(d):
|
||||
return dict([(str(k), v) for k, v in d.items()])
|
||||
|
||||
|
||||
def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
|
||||
"""Topological sort. Arguments are:
|
||||
deps dictionary describing dependencies where a:[b,c] means "a depends on b and c"
|
||||
nodes optional, specifies list of starting nodes (these should be the nodes
|
||||
which are not depended on by any other nodes)
|
||||
"""
|
||||
|
||||
if nodes is None:
|
||||
## run through deps to find nodes that are not depended upon
|
||||
rem = set()
|
||||
for dep in deps.values():
|
||||
rem |= set(dep)
|
||||
nodes = set(deps.keys()) - rem
|
||||
if seen is None:
|
||||
seen = set()
|
||||
stack = []
|
||||
sorted = []
|
||||
#print " "*depth, "Starting from", nodes
|
||||
for n in nodes:
|
||||
if n in stack:
|
||||
raise Exception("Cyclic dependency detected", stack + [n])
|
||||
if n in seen:
|
||||
continue
|
||||
seen.add(n)
|
||||
#print " "*depth, " descending into", n, deps[n]
|
||||
sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1))
|
||||
#print " "*depth, " Added", n
|
||||
sorted.append(n)
|
||||
#print " "*depth, " ", sorted
|
||||
return sorted
|
||||
|
||||
|
||||
class Flowchart(Node):
|
||||
@ -278,9 +249,10 @@ class Flowchart(Node):
|
||||
|
||||
## Record inputs given to process()
|
||||
for n, t in self.inputNode.outputs().items():
|
||||
if n not in args:
|
||||
raise Exception("Parameter %s required to process this chart." % n)
|
||||
data[t] = args[n]
|
||||
# if n not in args:
|
||||
# raise Exception("Parameter %s required to process this chart." % n)
|
||||
if n in args:
|
||||
data[t] = args[n]
|
||||
|
||||
ret = {}
|
||||
|
||||
@ -305,7 +277,7 @@ class Flowchart(Node):
|
||||
if len(inputs) == 0:
|
||||
continue
|
||||
if inp.isMultiValue(): ## multi-input terminals require a dict of all inputs
|
||||
args[inp.name()] = dict([(i, data[i]) for i in inputs])
|
||||
args[inp.name()] = dict([(i, data[i]) for i in inputs if i in data])
|
||||
else: ## single-inputs terminals only need the single input value available
|
||||
args[inp.name()] = data[inputs[0]]
|
||||
|
||||
@ -325,9 +297,8 @@ class Flowchart(Node):
|
||||
#print out.name()
|
||||
try:
|
||||
data[out] = result[out.name()]
|
||||
except:
|
||||
print(out, out.name())
|
||||
raise
|
||||
except KeyError:
|
||||
pass
|
||||
elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory)
|
||||
#print "===> delete", arg
|
||||
if arg in data:
|
||||
@ -352,7 +323,7 @@ class Flowchart(Node):
|
||||
#print "DEPS:", deps
|
||||
## determine correct node-processing order
|
||||
#deps[self] = []
|
||||
order = toposort(deps)
|
||||
order = fn.toposort(deps)
|
||||
#print "ORDER1:", order
|
||||
|
||||
## construct list of operations
|
||||
@ -401,7 +372,7 @@ class Flowchart(Node):
|
||||
deps[node].extend(t.dependentNodes())
|
||||
|
||||
## determine order of updates
|
||||
order = toposort(deps, nodes=[startNode])
|
||||
order = fn.toposort(deps, nodes=[startNode])
|
||||
order.reverse()
|
||||
|
||||
## keep track of terminals that have been updated
|
||||
@ -542,7 +513,7 @@ class Flowchart(Node):
|
||||
return
|
||||
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs..
|
||||
#fileName = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
|
||||
fileName = str(fileName)
|
||||
fileName = unicode(fileName)
|
||||
state = configfile.readConfigFile(fileName)
|
||||
self.restoreState(state, clear=True)
|
||||
self.viewBox.autoRange()
|
||||
@ -563,7 +534,7 @@ class Flowchart(Node):
|
||||
self.fileDialog.fileSelected.connect(self.saveFile)
|
||||
return
|
||||
#fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
|
||||
fileName = str(fileName)
|
||||
fileName = unicode(fileName)
|
||||
configfile.writeConfigFile(self.saveState(), fileName)
|
||||
self.sigFileSaved.emit(fileName)
|
||||
|
||||
@ -685,7 +656,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
||||
#self.setCurrentFile(newFile)
|
||||
|
||||
def fileSaved(self, fileName):
|
||||
self.setCurrentFile(str(fileName))
|
||||
self.setCurrentFile(unicode(fileName))
|
||||
self.ui.saveBtn.success("Saved.")
|
||||
|
||||
def saveClicked(self):
|
||||
@ -714,7 +685,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
|
||||
#self.setCurrentFile(newFile)
|
||||
|
||||
def setCurrentFile(self, fileName):
|
||||
self.currentFileName = str(fileName)
|
||||
self.currentFileName = unicode(fileName)
|
||||
if fileName is None:
|
||||
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
|
||||
else:
|
||||
|
@ -182,8 +182,8 @@ class EvalNode(Node):
|
||||
def __init__(self, name):
|
||||
Node.__init__(self, name,
|
||||
terminals = {
|
||||
'input': {'io': 'in', 'renamable': True},
|
||||
'output': {'io': 'out', 'renamable': True},
|
||||
'input': {'io': 'in', 'renamable': True, 'multiable': True},
|
||||
'output': {'io': 'out', 'renamable': True, 'multiable': True},
|
||||
},
|
||||
allowAddInput=True, allowAddOutput=True)
|
||||
|
||||
|
@ -6,6 +6,8 @@ from ... import functions as pgfn
|
||||
from .common import *
|
||||
import numpy as np
|
||||
|
||||
from ... import PolyLineROI
|
||||
from ... import Point
|
||||
from ... import metaarray as metaarray
|
||||
|
||||
|
||||
@ -201,6 +203,72 @@ class Detrend(CtrlNode):
|
||||
raise Exception("DetrendFilter node requires the package scipy.signal.")
|
||||
return detrend(data)
|
||||
|
||||
class RemoveBaseline(PlottingCtrlNode):
|
||||
"""Remove an arbitrary, graphically defined baseline from the data."""
|
||||
nodeName = 'RemoveBaseline'
|
||||
|
||||
def __init__(self, name):
|
||||
## define inputs and outputs (one output needs to be a plot)
|
||||
PlottingCtrlNode.__init__(self, name)
|
||||
self.line = PolyLineROI([[0,0],[1,0]])
|
||||
self.line.sigRegionChanged.connect(self.changed)
|
||||
|
||||
## create a PolyLineROI, add it to a plot -- actually, I think we want to do this after the node is connected to a plot (look at EventDetection.ThresholdEvents node for ideas), and possible after there is data. We will need to update the end positions of the line each time the input data changes
|
||||
#self.line = None ## will become a PolyLineROI
|
||||
|
||||
def connectToPlot(self, node):
|
||||
"""Define what happens when the node is connected to a plot"""
|
||||
|
||||
if node.plot is None:
|
||||
return
|
||||
node.getPlot().addItem(self.line)
|
||||
|
||||
def disconnectFromPlot(self, plot):
|
||||
"""Define what happens when the node is disconnected from a plot"""
|
||||
plot.removeItem(self.line)
|
||||
|
||||
def processData(self, data):
|
||||
## get array of baseline (from PolyLineROI)
|
||||
h0 = self.line.getHandles()[0]
|
||||
h1 = self.line.getHandles()[-1]
|
||||
|
||||
timeVals = data.xvals(0)
|
||||
h0.setPos(timeVals[0], h0.pos()[1])
|
||||
h1.setPos(timeVals[-1], h1.pos()[1])
|
||||
|
||||
pts = self.line.listPoints() ## lists line handles in same coordinates as data
|
||||
pts, indices = self.adjustXPositions(pts, timeVals) ## maxe sure x positions match x positions of data points
|
||||
|
||||
## construct an array that represents the baseline
|
||||
arr = np.zeros(len(data), dtype=float)
|
||||
n = 1
|
||||
arr[0] = pts[0].y()
|
||||
for i in range(len(pts)-1):
|
||||
x1 = pts[i].x()
|
||||
x2 = pts[i+1].x()
|
||||
y1 = pts[i].y()
|
||||
y2 = pts[i+1].y()
|
||||
m = (y2-y1)/(x2-x1)
|
||||
b = y1
|
||||
|
||||
times = timeVals[(timeVals > x1)*(timeVals <= x2)]
|
||||
arr[n:n+len(times)] = (m*(times-times[0]))+b
|
||||
n += len(times)
|
||||
|
||||
return data - arr ## subract baseline from data
|
||||
|
||||
def adjustXPositions(self, pts, data):
|
||||
"""Return a list of Point() where the x position is set to the nearest x value in *data* for each point in *pts*."""
|
||||
points = []
|
||||
timeIndices = []
|
||||
for p in pts:
|
||||
x = np.argwhere(abs(data - p.x()) == abs(data - p.x()).min())
|
||||
points.append(Point(data[x], p.y()))
|
||||
timeIndices.append(x)
|
||||
|
||||
return points, timeIndices
|
||||
|
||||
|
||||
|
||||
class AdaptiveDetrend(CtrlNode):
|
||||
"""Removes baseline from data, ignoring anomalous events"""
|
||||
@ -275,4 +343,4 @@ class RemovePeriodic(CtrlNode):
|
||||
return ma
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -131,6 +131,42 @@ class CtrlNode(Node):
|
||||
l.show()
|
||||
|
||||
|
||||
class PlottingCtrlNode(CtrlNode):
|
||||
"""Abstract class for CtrlNodes that can connect to plots."""
|
||||
|
||||
def __init__(self, name, ui=None, terminals=None):
|
||||
#print "PlottingCtrlNode.__init__ called."
|
||||
CtrlNode.__init__(self, name, ui=ui, terminals=terminals)
|
||||
self.plotTerminal = self.addOutput('plot', optional=True)
|
||||
|
||||
def connected(self, term, remote):
|
||||
CtrlNode.connected(self, term, remote)
|
||||
if term is not self.plotTerminal:
|
||||
return
|
||||
node = remote.node()
|
||||
node.sigPlotChanged.connect(self.connectToPlot)
|
||||
self.connectToPlot(node)
|
||||
|
||||
def disconnected(self, term, remote):
|
||||
CtrlNode.disconnected(self, term, remote)
|
||||
if term is not self.plotTerminal:
|
||||
return
|
||||
remote.node().sigPlotChanged.disconnect(self.connectToPlot)
|
||||
self.disconnectFromPlot(remote.node().getPlot())
|
||||
|
||||
def connectToPlot(self, node):
|
||||
"""Define what happens when the node is connected to a plot"""
|
||||
raise Exception("Must be re-implemented in subclass")
|
||||
|
||||
def disconnectFromPlot(self, plot):
|
||||
"""Define what happens when the node is disconnected from a plot"""
|
||||
raise Exception("Must be re-implemented in subclass")
|
||||
|
||||
def process(self, In, display=True):
|
||||
out = CtrlNode.process(self, In, display)
|
||||
out['plot'] = None
|
||||
return out
|
||||
|
||||
|
||||
def metaArrayWrapper(fn):
|
||||
def newFn(self, data, *args, **kargs):
|
||||
|
@ -206,7 +206,7 @@ def adaptiveDetrend(data, x=None, threshold=3.0):
|
||||
#d3 = where(mask, 0, d2)
|
||||
#d4 = d2 - lowPass(d3, cutoffs[1], dt=dt)
|
||||
|
||||
lr = stats.linregress(x[mask], d[mask])
|
||||
lr = scipy.stats.linregress(x[mask], d[mask])
|
||||
base = lr[1] + lr[0]*x
|
||||
d4 = d - base
|
||||
|
||||
|
@ -591,6 +591,50 @@ def interpolateArray(data, x, default=0.0):
|
||||
return result
|
||||
|
||||
|
||||
def subArray(data, offset, shape, stride):
|
||||
"""
|
||||
Unpack a sub-array from *data* using the specified offset, shape, and stride.
|
||||
|
||||
Note that *stride* is specified in array elements, not bytes.
|
||||
For example, we have a 2x3 array packed in a 1D array as follows::
|
||||
|
||||
data = [_, _, 00, 01, 02, _, 10, 11, 12, _]
|
||||
|
||||
Then we can unpack the sub-array with this call::
|
||||
|
||||
subArray(data, offset=2, shape=(2, 3), stride=(4, 1))
|
||||
|
||||
..which returns::
|
||||
|
||||
[[00, 01, 02],
|
||||
[10, 11, 12]]
|
||||
|
||||
This function operates only on the first axis of *data*. So changing
|
||||
the input in the example above to have shape (10, 7) would cause the
|
||||
output to have shape (2, 3, 7).
|
||||
"""
|
||||
#data = data.flatten()
|
||||
data = data[offset:]
|
||||
shape = tuple(shape)
|
||||
stride = tuple(stride)
|
||||
extraShape = data.shape[1:]
|
||||
#print data.shape, offset, shape, stride
|
||||
for i in range(len(shape)):
|
||||
mask = (slice(None),) * i + (slice(None, shape[i] * stride[i]),)
|
||||
newShape = shape[:i+1]
|
||||
if i < len(shape)-1:
|
||||
newShape += (stride[i],)
|
||||
newShape += extraShape
|
||||
#print i, mask, newShape
|
||||
#print "start:\n", data.shape, data
|
||||
data = data[mask]
|
||||
#print "mask:\n", data.shape, data
|
||||
data = data.reshape(newShape)
|
||||
#print "reshape:\n", data.shape, data
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def transformToArray(tr):
|
||||
"""
|
||||
Given a QTransform, return a 3x3 numpy array.
|
||||
@ -2156,3 +2200,51 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
|
||||
yvals[i] = y
|
||||
|
||||
return yvals[np.argsort(inds)] ## un-shuffle values before returning
|
||||
|
||||
|
||||
|
||||
def toposort(deps, nodes=None, seen=None, stack=None, depth=0):
|
||||
"""Topological sort. Arguments are:
|
||||
deps dictionary describing dependencies where a:[b,c] means "a depends on b and c"
|
||||
nodes optional, specifies list of starting nodes (these should be the nodes
|
||||
which are not depended on by any other nodes). Other candidate starting
|
||||
nodes will be ignored.
|
||||
|
||||
Example::
|
||||
|
||||
# Sort the following graph:
|
||||
#
|
||||
# B ──┬─────> C <── D
|
||||
# │ │
|
||||
# E <─┴─> A <─┘
|
||||
#
|
||||
deps = {'a': ['b', 'c'], 'c': ['b', 'd'], 'e': ['b']}
|
||||
toposort(deps)
|
||||
=> ['b', 'd', 'c', 'a', 'e']
|
||||
"""
|
||||
# fill in empty dep lists
|
||||
deps = deps.copy()
|
||||
for k,v in list(deps.items()):
|
||||
for k in v:
|
||||
if k not in deps:
|
||||
deps[k] = []
|
||||
|
||||
if nodes is None:
|
||||
## run through deps to find nodes that are not depended upon
|
||||
rem = set()
|
||||
for dep in deps.values():
|
||||
rem |= set(dep)
|
||||
nodes = set(deps.keys()) - rem
|
||||
if seen is None:
|
||||
seen = set()
|
||||
stack = []
|
||||
sorted = []
|
||||
for n in nodes:
|
||||
if n in stack:
|
||||
raise Exception("Cyclic dependency detected", stack + [n])
|
||||
if n in seen:
|
||||
continue
|
||||
seen.add(n)
|
||||
sorted.extend( toposort(deps, deps[n], seen, stack+[n], depth=depth+1))
|
||||
sorted.append(n)
|
||||
return sorted
|
||||
|
@ -918,13 +918,17 @@ class AxisItem(GraphicsWidget):
|
||||
rects.append(br)
|
||||
textRects.append(rects[-1])
|
||||
|
||||
## measure all text, make sure there's enough room
|
||||
if axis == 0:
|
||||
textSize = np.sum([r.height() for r in textRects])
|
||||
textSize2 = np.max([r.width() for r in textRects]) if textRects else 0
|
||||
if len(textRects) > 0:
|
||||
## measure all text, make sure there's enough room
|
||||
if axis == 0:
|
||||
textSize = np.sum([r.height() for r in textRects])
|
||||
textSize2 = np.max([r.width() for r in textRects])
|
||||
else:
|
||||
textSize = np.sum([r.width() for r in textRects])
|
||||
textSize2 = np.max([r.height() for r in textRects])
|
||||
else:
|
||||
textSize = np.sum([r.width() for r in textRects])
|
||||
textSize2 = np.max([r.height() for r in textRects]) if textRects else 0
|
||||
textSize = 0
|
||||
textSize2 = 0
|
||||
|
||||
if i > 0: ## always draw top level
|
||||
## If the strings are too crowded, stop drawing text now.
|
||||
|
@ -3,6 +3,7 @@ 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
|
||||
@ -300,6 +301,7 @@ class TickSliderItem(GraphicsWidget):
|
||||
pos.setX(x)
|
||||
tick.setPos(pos)
|
||||
self.ticks[tick] = val
|
||||
self.updateGradient()
|
||||
|
||||
def tickValue(self, tick):
|
||||
## public
|
||||
@ -537,23 +539,22 @@ class GradientEditorItem(TickSliderItem):
|
||||
def tickClicked(self, tick, ev):
|
||||
#private
|
||||
if ev.button() == QtCore.Qt.LeftButton:
|
||||
if not tick.colorChangeAllowed:
|
||||
return
|
||||
self.currentTick = tick
|
||||
self.currentTickColor = tick.color
|
||||
self.colorDialog.setCurrentColor(tick.color)
|
||||
self.colorDialog.open()
|
||||
#color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel)
|
||||
#if color.isValid():
|
||||
#self.setTickColor(tick, color)
|
||||
#self.updateGradient()
|
||||
self.raiseColorDialog(tick)
|
||||
elif ev.button() == QtCore.Qt.RightButton:
|
||||
if not tick.removeAllowed:
|
||||
return
|
||||
if len(self.ticks) > 2:
|
||||
self.removeTick(tick)
|
||||
self.updateGradient()
|
||||
|
||||
self.raiseTickContextMenu(tick, ev)
|
||||
|
||||
def raiseColorDialog(self, tick):
|
||||
if not tick.colorChangeAllowed:
|
||||
return
|
||||
self.currentTick = tick
|
||||
self.currentTickColor = tick.color
|
||||
self.colorDialog.setCurrentColor(tick.color)
|
||||
self.colorDialog.open()
|
||||
|
||||
def raiseTickContextMenu(self, tick, ev):
|
||||
self.tickMenu = TickMenu(tick, self)
|
||||
self.tickMenu.popup(ev.screenPos().toQPoint())
|
||||
|
||||
def tickMoved(self, tick, pos):
|
||||
#private
|
||||
TickSliderItem.tickMoved(self, tick, pos)
|
||||
@ -726,6 +727,7 @@ class GradientEditorItem(TickSliderItem):
|
||||
def removeTick(self, tick, finish=True):
|
||||
TickSliderItem.removeTick(self, tick)
|
||||
if finish:
|
||||
self.updateGradient()
|
||||
self.sigGradientChangeFinished.emit(self)
|
||||
|
||||
|
||||
@ -867,44 +869,59 @@ class Tick(QtGui.QGraphicsObject): ## NOTE: Making this a subclass of GraphicsO
|
||||
self.currentPen = self.pen
|
||||
self.update()
|
||||
|
||||
#def mouseMoveEvent(self, ev):
|
||||
##print self, "move", ev.scenePos()
|
||||
#if not self.movable:
|
||||
#return
|
||||
#if not ev.buttons() & QtCore.Qt.LeftButton:
|
||||
#return
|
||||
|
||||
|
||||
#newPos = ev.scenePos() + self.mouseOffset
|
||||
#newPos.setY(self.pos().y())
|
||||
##newPos.setX(min(max(newPos.x(), 0), 100))
|
||||
#self.setPos(newPos)
|
||||
#self.view().tickMoved(self, newPos)
|
||||
#self.movedSincePress = True
|
||||
##self.emit(QtCore.SIGNAL('tickChanged'), self)
|
||||
#ev.accept()
|
||||
|
||||
#def mousePressEvent(self, ev):
|
||||
#self.movedSincePress = False
|
||||
#if ev.button() == QtCore.Qt.LeftButton:
|
||||
#ev.accept()
|
||||
#self.mouseOffset = self.pos() - ev.scenePos()
|
||||
#self.pressPos = ev.scenePos()
|
||||
#elif ev.button() == QtCore.Qt.RightButton:
|
||||
#ev.accept()
|
||||
##if self.endTick:
|
||||
##return
|
||||
##self.view.tickChanged(self, delete=True)
|
||||
|
||||
#def mouseReleaseEvent(self, ev):
|
||||
##print self, "release", ev.scenePos()
|
||||
#if not self.movedSincePress:
|
||||
#self.view().tickClicked(self, ev)
|
||||
class TickMenu(QtGui.QMenu):
|
||||
|
||||
def __init__(self, tick, sliderItem):
|
||||
QtGui.QMenu.__init__(self)
|
||||
|
||||
##if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos:
|
||||
##color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel)
|
||||
##if color.isValid():
|
||||
##self.color = color
|
||||
##self.setBrush(QtGui.QBrush(QtGui.QColor(self.color)))
|
||||
###self.emit(QtCore.SIGNAL('tickChanged'), self)
|
||||
##self.view.tickChanged(self)
|
||||
self.tick = weakref.ref(tick)
|
||||
self.sliderItem = weakref.ref(sliderItem)
|
||||
|
||||
self.removeAct = self.addAction("Remove Tick", lambda: self.sliderItem().removeTick(tick))
|
||||
if (not self.tick().removeAllowed) or len(self.sliderItem().ticks) < 3:
|
||||
self.removeAct.setEnabled(False)
|
||||
|
||||
positionMenu = self.addMenu("Set Position")
|
||||
w = QtGui.QWidget()
|
||||
l = QtGui.QGridLayout()
|
||||
w.setLayout(l)
|
||||
|
||||
value = sliderItem.tickValue(tick)
|
||||
self.fracPosSpin = SpinBox()
|
||||
self.fracPosSpin.setOpts(value=value, bounds=(0.0, 1.0), step=0.01, decimals=2)
|
||||
#self.dataPosSpin = SpinBox(value=dataVal)
|
||||
#self.dataPosSpin.setOpts(decimals=3, siPrefix=True)
|
||||
|
||||
l.addWidget(QtGui.QLabel("Position:"), 0,0)
|
||||
l.addWidget(self.fracPosSpin, 0, 1)
|
||||
#l.addWidget(QtGui.QLabel("Position (data units):"), 1, 0)
|
||||
#l.addWidget(self.dataPosSpin, 1,1)
|
||||
|
||||
#if self.sliderItem().dataParent is None:
|
||||
# self.dataPosSpin.setEnabled(False)
|
||||
|
||||
a = QtGui.QWidgetAction(self)
|
||||
a.setDefaultWidget(w)
|
||||
positionMenu.addAction(a)
|
||||
|
||||
self.fracPosSpin.sigValueChanging.connect(self.fractionalValueChanged)
|
||||
#self.dataPosSpin.valueChanged.connect(self.dataValueChanged)
|
||||
|
||||
colorAct = self.addAction("Set Color", lambda: self.sliderItem().raiseColorDialog(self.tick()))
|
||||
if not self.tick().colorChangeAllowed:
|
||||
colorAct.setEnabled(False)
|
||||
|
||||
def fractionalValueChanged(self, x):
|
||||
self.sliderItem().setTickValue(self.tick(), self.fracPosSpin.value())
|
||||
#if self.sliderItem().dataParent is not None:
|
||||
# self.dataPosSpin.blockSignals(True)
|
||||
# self.dataPosSpin.setValue(self.sliderItem().tickDataValue(self.tick()))
|
||||
# self.dataPosSpin.blockSignals(False)
|
||||
|
||||
#def dataValueChanged(self, val):
|
||||
# self.sliderItem().setTickValue(self.tick(), val, dataUnits=True)
|
||||
# self.fracPosSpin.blockSignals(True)
|
||||
# self.fracPosSpin.setValue(self.sliderItem().tickValue(self.tick()))
|
||||
# self.fracPosSpin.blockSignals(False)
|
||||
|
||||
|
@ -177,6 +177,12 @@ class ImageItem(GraphicsObject):
|
||||
self.translate(rect.left(), rect.top())
|
||||
self.scale(rect.width() / self.width(), rect.height() / self.height())
|
||||
|
||||
def clear(self):
|
||||
self.image = None
|
||||
self.prepareGeometryChange()
|
||||
self.informViewBoundsChanged()
|
||||
self.update()
|
||||
|
||||
def setImage(self, image=None, autoLevels=None, **kargs):
|
||||
"""
|
||||
Update the image displayed by this item. For more information on how the image
|
||||
@ -512,6 +518,9 @@ class ImageItem(GraphicsObject):
|
||||
def removeClicked(self):
|
||||
## Send remove event only after we have exited the menu event handler
|
||||
self.removeTimer = QtCore.QTimer()
|
||||
self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self))
|
||||
self.removeTimer.timeout.connect(self.emitRemoveRequested)
|
||||
self.removeTimer.start(0)
|
||||
|
||||
def emitRemoveRequested(self):
|
||||
self.removeTimer.timeout.disconnect(self.emitRemoveRequested)
|
||||
self.sigRemoveRequested.emit(self)
|
||||
|
@ -168,6 +168,7 @@ class PlotDataItem(GraphicsObject):
|
||||
'downsample': 1,
|
||||
'autoDownsample': False,
|
||||
'downsampleMethod': 'peak',
|
||||
'autoDownsampleFactor': 5., # draw ~5 samples per pixel
|
||||
'clipToView': False,
|
||||
|
||||
'data': None,
|
||||
@ -380,14 +381,23 @@ class PlotDataItem(GraphicsObject):
|
||||
|
||||
elif len(args) == 2:
|
||||
seq = ('listOfValues', 'MetaArray', 'empty')
|
||||
if dataType(args[0]) not in seq or dataType(args[1]) not in seq:
|
||||
dtyp = dataType(args[0]), dataType(args[1])
|
||||
if dtyp[0] not in seq or dtyp[1] not in seq:
|
||||
raise Exception('When passing two unnamed arguments, both must be a list or array of values. (got %s, %s)' % (str(type(args[0])), str(type(args[1]))))
|
||||
if not isinstance(args[0], np.ndarray):
|
||||
x = np.array(args[0])
|
||||
#x = np.array(args[0])
|
||||
if dtyp[0] == 'MetaArray':
|
||||
x = args[0].asarray()
|
||||
else:
|
||||
x = np.array(args[0])
|
||||
else:
|
||||
x = args[0].view(np.ndarray)
|
||||
if not isinstance(args[1], np.ndarray):
|
||||
y = np.array(args[1])
|
||||
#y = np.array(args[1])
|
||||
if dtyp[1] == 'MetaArray':
|
||||
y = args[1].asarray()
|
||||
else:
|
||||
y = np.array(args[1])
|
||||
else:
|
||||
y = args[1].view(np.ndarray)
|
||||
|
||||
@ -538,7 +548,7 @@ class PlotDataItem(GraphicsObject):
|
||||
x1 = (range.right()-x[0]) / dx
|
||||
width = self.getViewBox().width()
|
||||
if width != 0.0:
|
||||
ds = int(max(1, int(0.2 * (x1-x0) / width)))
|
||||
ds = int(max(1, int((x1-x0) / (width*self.opts['autoDownsampleFactor']))))
|
||||
## downsampling is expensive; delay until after clipping.
|
||||
|
||||
if self.opts['clipToView']:
|
||||
|
@ -469,7 +469,8 @@ class PlotItem(GraphicsWidget):
|
||||
|
||||
### Average data together
|
||||
(x, y) = curve.getData()
|
||||
if plot.yData is not None:
|
||||
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)
|
||||
else:
|
||||
@ -1207,10 +1208,13 @@ class PlotItem(GraphicsWidget):
|
||||
self.updateButtons()
|
||||
|
||||
def updateButtons(self):
|
||||
if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()):
|
||||
self.autoBtn.show()
|
||||
else:
|
||||
self.autoBtn.hide()
|
||||
try:
|
||||
if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()):
|
||||
self.autoBtn.show()
|
||||
else:
|
||||
self.autoBtn.hide()
|
||||
except RuntimeError:
|
||||
pass # this can happen if the plot has been deleted.
|
||||
|
||||
def _plotArray(self, arr, x=None, **kargs):
|
||||
if arr.ndim != 1:
|
||||
|
@ -25,7 +25,7 @@ from .UIGraphicsItem import UIGraphicsItem
|
||||
__all__ = [
|
||||
'ROI',
|
||||
'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI',
|
||||
'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI',
|
||||
'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI',
|
||||
]
|
||||
|
||||
|
||||
@ -862,8 +862,10 @@ class ROI(GraphicsObject):
|
||||
elif h['type'] == 'sr':
|
||||
if h['center'][0] == h['pos'][0]:
|
||||
scaleAxis = 1
|
||||
nonScaleAxis=0
|
||||
else:
|
||||
scaleAxis = 0
|
||||
nonScaleAxis=1
|
||||
|
||||
try:
|
||||
if lp1.length() == 0 or lp0.length() == 0:
|
||||
@ -885,6 +887,8 @@ class ROI(GraphicsObject):
|
||||
newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize
|
||||
if newState['size'][scaleAxis] == 0:
|
||||
newState['size'][scaleAxis] = 1
|
||||
if self.aspectLocked:
|
||||
newState['size'][nonScaleAxis] = newState['size'][scaleAxis]
|
||||
|
||||
c1 = c * newState['size']
|
||||
tr = QtGui.QTransform()
|
||||
@ -972,14 +976,16 @@ class ROI(GraphicsObject):
|
||||
return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
|
||||
|
||||
def paint(self, p, opt, widget):
|
||||
p.save()
|
||||
r = self.boundingRect()
|
||||
# p.save()
|
||||
# Note: don't use self.boundingRect here, because subclasses may need to redefine it.
|
||||
r = QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
|
||||
|
||||
p.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
p.setPen(self.currentPen)
|
||||
p.translate(r.left(), r.top())
|
||||
p.scale(r.width(), r.height())
|
||||
p.drawRect(0, 0, 1, 1)
|
||||
p.restore()
|
||||
# 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.
|
||||
@ -2139,6 +2145,102 @@ class SpiralROI(ROI):
|
||||
p.drawRect(self.boundingRect())
|
||||
|
||||
|
||||
class CrosshairROI(ROI):
|
||||
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable."""
|
||||
|
||||
def __init__(self, pos=None, size=None, **kargs):
|
||||
if size == None:
|
||||
#size = [100e-6,100e-6]
|
||||
size=[1,1]
|
||||
if pos == None:
|
||||
pos = [0,0]
|
||||
self._shape = None
|
||||
ROI.__init__(self, pos, size, **kargs)
|
||||
|
||||
self.sigRegionChanged.connect(self.invalidate)
|
||||
self.addScaleRotateHandle(Point(1, 0), Point(0, 0))
|
||||
self.aspectLocked = True
|
||||
|
||||
def invalidate(self):
|
||||
self._shape = None
|
||||
self.prepareGeometryChange()
|
||||
|
||||
def boundingRect(self):
|
||||
#size = self.size()
|
||||
#return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized()
|
||||
return self.shape().boundingRect()
|
||||
|
||||
#def getRect(self):
|
||||
### same as boundingRect -- for internal use so that boundingRect can be re-implemented in subclasses
|
||||
#size = self.size()
|
||||
#return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized()
|
||||
|
||||
|
||||
def shape(self):
|
||||
if self._shape is None:
|
||||
radius = self.getState()['size'][1]
|
||||
p = QtGui.QPainterPath()
|
||||
p.moveTo(Point(0, -radius))
|
||||
p.lineTo(Point(0, radius))
|
||||
p.moveTo(Point(-radius, 0))
|
||||
p.lineTo(Point(radius, 0))
|
||||
p = self.mapToDevice(p)
|
||||
stroker = QtGui.QPainterPathStroker()
|
||||
stroker.setWidth(10)
|
||||
outline = stroker.createStroke(p)
|
||||
self._shape = self.mapFromDevice(outline)
|
||||
|
||||
|
||||
|
||||
##h1 = self.handles[0]['item'].pos()
|
||||
##h2 = self.handles[1]['item'].pos()
|
||||
#w1 = Point(-0.5, 0)*self.size()
|
||||
#w2 = Point(0.5, 0)*self.size()
|
||||
#h1 = Point(0, -0.5)*self.size()
|
||||
#h2 = Point(0, 0.5)*self.size()
|
||||
|
||||
#dh = h2-h1
|
||||
#dw = w2-w1
|
||||
#if dh.length() == 0 or dw.length() == 0:
|
||||
#return p
|
||||
#pxv = self.pixelVectors(dh)[1]
|
||||
#if pxv is None:
|
||||
#return p
|
||||
|
||||
#pxv *= 4
|
||||
|
||||
#p.moveTo(h1+pxv)
|
||||
#p.lineTo(h2+pxv)
|
||||
#p.lineTo(h2-pxv)
|
||||
#p.lineTo(h1-pxv)
|
||||
#p.lineTo(h1+pxv)
|
||||
|
||||
#pxv = self.pixelVectors(dw)[1]
|
||||
#if pxv is None:
|
||||
#return p
|
||||
|
||||
#pxv *= 4
|
||||
|
||||
#p.moveTo(w1+pxv)
|
||||
#p.lineTo(w2+pxv)
|
||||
#p.lineTo(w2-pxv)
|
||||
#p.lineTo(w1-pxv)
|
||||
#p.lineTo(w1+pxv)
|
||||
|
||||
return self._shape
|
||||
|
||||
def paint(self, p, *args):
|
||||
#p.save()
|
||||
#r = self.getRect()
|
||||
radius = self.getState()['size'][1]
|
||||
p.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||
p.setPen(self.currentPen)
|
||||
#p.translate(r.left(), r.top())
|
||||
#p.scale(r.width()/10., r.height()/10.) ## need to scale up a little because drawLine has trouble dealing with 0.5
|
||||
#p.drawLine(0,5, 10,5)
|
||||
#p.drawLine(5,0, 5,10)
|
||||
#p.restore()
|
||||
|
||||
p.drawLine(Point(0, -radius), Point(0, radius))
|
||||
p.drawLine(Point(-radius, 0), Point(radius, 0))
|
||||
|
||||
|
||||
|
@ -5,6 +5,7 @@ from .TextItem import TextItem
|
||||
import numpy as np
|
||||
from .. import functions as fn
|
||||
from .. import getConfigOption
|
||||
from ..Point import Point
|
||||
|
||||
__all__ = ['ScaleBar']
|
||||
|
||||
@ -12,7 +13,7 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
|
||||
"""
|
||||
Displays a rectangular bar to indicate the relative scale of objects on the view.
|
||||
"""
|
||||
def __init__(self, size, width=5, brush=None, pen=None, suffix='m'):
|
||||
def __init__(self, size, width=5, brush=None, pen=None, suffix='m', offset=None):
|
||||
GraphicsObject.__init__(self)
|
||||
GraphicsWidgetAnchor.__init__(self)
|
||||
self.setFlag(self.ItemHasNoContents)
|
||||
@ -24,6 +25,9 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
|
||||
self.pen = fn.mkPen(pen)
|
||||
self._width = width
|
||||
self.size = size
|
||||
if offset == None:
|
||||
offset = (0,0)
|
||||
self.offset = offset
|
||||
|
||||
self.bar = QtGui.QGraphicsRectItem()
|
||||
self.bar.setPen(self.pen)
|
||||
@ -54,51 +58,14 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
|
||||
def boundingRect(self):
|
||||
return QtCore.QRectF()
|
||||
|
||||
def setParentItem(self, p):
|
||||
ret = GraphicsObject.setParentItem(self, p)
|
||||
if self.offset is not None:
|
||||
offset = Point(self.offset)
|
||||
anchorx = 1 if offset[0] <= 0 else 0
|
||||
anchory = 1 if offset[1] <= 0 else 0
|
||||
anchor = (anchorx, anchory)
|
||||
self.anchor(itemPos=anchor, parentPos=anchor, offset=offset)
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
|
||||
#class ScaleBar(UIGraphicsItem):
|
||||
#"""
|
||||
#Displays a rectangular bar with 10 divisions to indicate the relative scale of objects on the view.
|
||||
#"""
|
||||
#def __init__(self, size, width=5, color=(100, 100, 255)):
|
||||
#UIGraphicsItem.__init__(self)
|
||||
#self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
||||
|
||||
#self.brush = fn.mkBrush(color)
|
||||
#self.pen = fn.mkPen((0,0,0))
|
||||
#self._width = width
|
||||
#self.size = size
|
||||
|
||||
#def paint(self, p, opt, widget):
|
||||
#UIGraphicsItem.paint(self, p, opt, widget)
|
||||
|
||||
#rect = self.boundingRect()
|
||||
#unit = self.pixelSize()
|
||||
#y = rect.top() + (rect.bottom()-rect.top()) * 0.02
|
||||
#y1 = y + unit[1]*self._width
|
||||
#x = rect.right() + (rect.left()-rect.right()) * 0.02
|
||||
#x1 = x - self.size
|
||||
|
||||
#p.setPen(self.pen)
|
||||
#p.setBrush(self.brush)
|
||||
#rect = QtCore.QRectF(
|
||||
#QtCore.QPointF(x1, y1),
|
||||
#QtCore.QPointF(x, y)
|
||||
#)
|
||||
#p.translate(x1, y1)
|
||||
#p.scale(rect.width(), rect.height())
|
||||
#p.drawRect(0, 0, 1, 1)
|
||||
|
||||
#alpha = np.clip(((self.size/unit[0]) - 40.) * 255. / 80., 0, 255)
|
||||
#p.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0, alpha)))
|
||||
#for i in range(1, 10):
|
||||
##x2 = x + (x1-x) * 0.1 * i
|
||||
#x2 = 0.1 * i
|
||||
#p.drawLine(QtCore.QPointF(x2, 0), QtCore.QPointF(x2, 1))
|
||||
|
||||
|
||||
#def setSize(self, s):
|
||||
#self.size = s
|
||||
|
||||
|
@ -68,10 +68,12 @@ def renderSymbol(symbol, size, pen, brush, device=None):
|
||||
device = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
|
||||
device.fill(0)
|
||||
p = QtGui.QPainter(device)
|
||||
p.setRenderHint(p.Antialiasing)
|
||||
p.translate(device.width()*0.5, device.height()*0.5)
|
||||
drawSymbol(p, symbol, size, pen, brush)
|
||||
p.end()
|
||||
try:
|
||||
p.setRenderHint(p.Antialiasing)
|
||||
p.translate(device.width()*0.5, device.height()*0.5)
|
||||
drawSymbol(p, symbol, size, pen, brush)
|
||||
finally:
|
||||
p.end()
|
||||
return device
|
||||
|
||||
def makeSymbolPixmap(size, pen, brush, symbol):
|
||||
|
@ -760,7 +760,8 @@ class ViewBox(GraphicsWidget):
|
||||
x = vr.left()+x, vr.right()+x
|
||||
if y is not None:
|
||||
y = vr.top()+y, vr.bottom()+y
|
||||
self.setRange(xRange=x, yRange=y, padding=0)
|
||||
if x is not None or y is not None:
|
||||
self.setRange(xRange=x, yRange=y, padding=0)
|
||||
|
||||
|
||||
|
||||
@ -902,6 +903,14 @@ class ViewBox(GraphicsWidget):
|
||||
return
|
||||
args['padding'] = 0
|
||||
args['disableAutoRange'] = False
|
||||
|
||||
# check for and ignore bad ranges
|
||||
for k in ['xRange', 'yRange']:
|
||||
if k in args:
|
||||
if not np.all(np.isfinite(args[k])):
|
||||
r = args.pop(k)
|
||||
print "Warning: %s is invalid: %s" % (k, str(r))
|
||||
|
||||
self.setRange(**args)
|
||||
finally:
|
||||
self._autoRangeNeedsUpdate = False
|
||||
@ -1066,7 +1075,7 @@ class ViewBox(GraphicsWidget):
|
||||
return
|
||||
|
||||
self.state['yInverted'] = b
|
||||
#self.updateMatrix(changed=(False, True))
|
||||
self._matrixNeedsUpdate = True # updateViewRange won't detect this for us
|
||||
self.updateViewRange()
|
||||
self.sigStateChanged.emit(self)
|
||||
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
|
||||
@ -1485,7 +1494,7 @@ class ViewBox(GraphicsWidget):
|
||||
aspect = self.state['aspectLocked'] # size ratio / view ratio
|
||||
tr = self.targetRect()
|
||||
bounds = self.rect()
|
||||
if aspect is not False and aspect != 0 and tr.height() != 0 and bounds.height() != 0:
|
||||
if aspect is not False and 0 not in [aspect, tr.height(), bounds.height(), bounds.width()]:
|
||||
|
||||
## This is the view range aspect ratio we have requested
|
||||
targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1
|
||||
@ -1581,18 +1590,16 @@ class ViewBox(GraphicsWidget):
|
||||
if any(changed):
|
||||
self.sigRangeChanged.emit(self, self.state['viewRange'])
|
||||
self.update()
|
||||
self._matrixNeedsUpdate = True
|
||||
|
||||
# Inform linked views that the range has changed
|
||||
for ax in [0, 1]:
|
||||
if not changed[ax]:
|
||||
continue
|
||||
link = self.linkedView(ax)
|
||||
if link is not None:
|
||||
link.linkedViewChanged(self, ax)
|
||||
# Inform linked views that the range has changed
|
||||
for ax in [0, 1]:
|
||||
if not changed[ax]:
|
||||
continue
|
||||
link = self.linkedView(ax)
|
||||
if link is not None:
|
||||
link.linkedViewChanged(self, ax)
|
||||
|
||||
self.update()
|
||||
self._matrixNeedsUpdate = True
|
||||
|
||||
def updateMatrix(self, changed=None):
|
||||
## Make the childGroup's transform match the requested viewRange.
|
||||
bounds = self.rect()
|
||||
|
@ -12,7 +12,7 @@ Widget used for displaying 2D or 3D data. Features:
|
||||
- ROI plotting
|
||||
- Image normalization through a variety of methods
|
||||
"""
|
||||
import sys
|
||||
import os, sys
|
||||
import numpy as np
|
||||
|
||||
from ..Qt import QtCore, QtGui, USE_PYSIDE
|
||||
@ -136,6 +136,8 @@ class ImageView(QtGui.QWidget):
|
||||
|
||||
self.ui.histogram.setImageItem(self.imageItem)
|
||||
|
||||
self.menu = None
|
||||
|
||||
self.ui.normGroup.hide()
|
||||
|
||||
self.roi = PlotROI(10)
|
||||
@ -176,7 +178,8 @@ class ImageView(QtGui.QWidget):
|
||||
self.timeLine.sigPositionChanged.connect(self.timeLineChanged)
|
||||
self.ui.roiBtn.clicked.connect(self.roiClicked)
|
||||
self.roi.sigRegionChanged.connect(self.roiChanged)
|
||||
self.ui.normBtn.toggled.connect(self.normToggled)
|
||||
#self.ui.normBtn.toggled.connect(self.normToggled)
|
||||
self.ui.menuBtn.clicked.connect(self.menuClicked)
|
||||
self.ui.normDivideRadio.clicked.connect(self.normRadioChanged)
|
||||
self.ui.normSubtractRadio.clicked.connect(self.normRadioChanged)
|
||||
self.ui.normOffRadio.clicked.connect(self.normRadioChanged)
|
||||
@ -321,6 +324,10 @@ class ImageView(QtGui.QWidget):
|
||||
|
||||
profiler()
|
||||
|
||||
def clear(self):
|
||||
self.image = None
|
||||
self.imageItem.clear()
|
||||
|
||||
def play(self, rate):
|
||||
"""Begin automatically stepping frames forward at the given rate (in fps).
|
||||
This can also be accessed by pressing the spacebar."""
|
||||
@ -671,3 +678,43 @@ class ImageView(QtGui.QWidget):
|
||||
def getHistogramWidget(self):
|
||||
"""Return the HistogramLUTWidget for this ImageView"""
|
||||
return self.ui.histogram
|
||||
|
||||
def export(self, fileName):
|
||||
"""
|
||||
Export data from the ImageView to a file, or to a stack of files if
|
||||
the data is 3D. Saving an image stack will result in index numbers
|
||||
being added to the file name. Images are saved as they would appear
|
||||
onscreen, with levels and lookup table applied.
|
||||
"""
|
||||
img = self.getProcessedImage()
|
||||
if self.hasTimeAxis():
|
||||
base, ext = os.path.splitext(fileName)
|
||||
fmt = "%%s%%0%dd%%s" % int(np.log10(img.shape[0])+1)
|
||||
for i in range(img.shape[0]):
|
||||
self.imageItem.setImage(img[i], autoLevels=False)
|
||||
self.imageItem.save(fmt % (base, i, ext))
|
||||
self.updateImage()
|
||||
else:
|
||||
self.imageItem.save(fileName)
|
||||
|
||||
def exportClicked(self):
|
||||
fileName = QtGui.QFileDialog.getSaveFileName()
|
||||
if fileName == '':
|
||||
return
|
||||
self.export(fileName)
|
||||
|
||||
def buildMenu(self):
|
||||
self.menu = QtGui.QMenu()
|
||||
self.normAction = QtGui.QAction("Normalization", self.menu)
|
||||
self.normAction.setCheckable(True)
|
||||
self.normAction.toggled.connect(self.normToggled)
|
||||
self.menu.addAction(self.normAction)
|
||||
self.exportAction = QtGui.QAction("Export", self.menu)
|
||||
self.exportAction.triggered.connect(self.exportClicked)
|
||||
self.menu.addAction(self.exportAction)
|
||||
|
||||
def menuClicked(self):
|
||||
if self.menu is None:
|
||||
self.buildMenu()
|
||||
self.menu.popup(QtGui.QCursor.pos())
|
||||
|
||||
|
@ -53,7 +53,7 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="2">
|
||||
<widget class="QPushButton" name="normBtn">
|
||||
<widget class="QPushButton" name="menuBtn">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Minimum" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
@ -61,10 +61,7 @@
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Norm</string>
|
||||
</property>
|
||||
<property name="checkable">
|
||||
<bool>true</bool>
|
||||
<string>Menu</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui'
|
||||
# Form implementation generated from reading ui file 'ImageViewTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:52 2013
|
||||
# by: PyQt4 UI code generator 4.10
|
||||
# Created: Thu May 1 15:20:40 2014
|
||||
# by: PyQt4 UI code generator 4.10.4
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@ -55,15 +55,14 @@ class Ui_Form(object):
|
||||
self.roiBtn.setCheckable(True)
|
||||
self.roiBtn.setObjectName(_fromUtf8("roiBtn"))
|
||||
self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1)
|
||||
self.normBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.menuBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.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(_fromUtf8("normBtn"))
|
||||
self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1)
|
||||
sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth())
|
||||
self.menuBtn.setSizePolicy(sizePolicy)
|
||||
self.menuBtn.setObjectName(_fromUtf8("menuBtn"))
|
||||
self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1)
|
||||
self.roiPlot = PlotWidget(self.splitter)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@ -149,7 +148,7 @@ class Ui_Form(object):
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(_translate("Form", "Form", None))
|
||||
self.roiBtn.setText(_translate("Form", "ROI", None))
|
||||
self.normBtn.setText(_translate("Form", "Norm", None))
|
||||
self.menuBtn.setText(_translate("Form", "Menu", None))
|
||||
self.normGroup.setTitle(_translate("Form", "Normalization", None))
|
||||
self.normSubtractRadio.setText(_translate("Form", "Subtract", None))
|
||||
self.normDivideRadio.setText(_translate("Form", "Divide", None))
|
||||
|
@ -1,9 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Form implementation generated from reading ui file './pyqtgraph/imageview/ImageViewTemplate.ui'
|
||||
# Form implementation generated from reading ui file 'ImageViewTemplate.ui'
|
||||
#
|
||||
# Created: Mon Dec 23 10:10:52 2013
|
||||
# by: pyside-uic 0.2.14 running on PySide 1.1.2
|
||||
# Created: Thu May 1 15:20:42 2014
|
||||
# by: pyside-uic 0.2.15 running on PySide 1.2.1
|
||||
#
|
||||
# WARNING! All changes made in this file will be lost!
|
||||
|
||||
@ -41,15 +41,14 @@ class Ui_Form(object):
|
||||
self.roiBtn.setCheckable(True)
|
||||
self.roiBtn.setObjectName("roiBtn")
|
||||
self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1)
|
||||
self.normBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
self.menuBtn = QtGui.QPushButton(self.layoutWidget)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
sizePolicy.setVerticalStretch(1)
|
||||
sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth())
|
||||
self.normBtn.setSizePolicy(sizePolicy)
|
||||
self.normBtn.setCheckable(True)
|
||||
self.normBtn.setObjectName("normBtn")
|
||||
self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1)
|
||||
sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth())
|
||||
self.menuBtn.setSizePolicy(sizePolicy)
|
||||
self.menuBtn.setObjectName("menuBtn")
|
||||
self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1)
|
||||
self.roiPlot = PlotWidget(self.splitter)
|
||||
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
|
||||
sizePolicy.setHorizontalStretch(0)
|
||||
@ -135,7 +134,7 @@ class Ui_Form(object):
|
||||
def retranslateUi(self, Form):
|
||||
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.normBtn.setText(QtGui.QApplication.translate("Form", "Norm", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.menuBtn.setText(QtGui.QApplication.translate("Form", "Menu", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", None, QtGui.QApplication.UnicodeUTF8))
|
||||
self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8))
|
||||
|
@ -103,6 +103,14 @@ class MetaArray(object):
|
||||
"""
|
||||
|
||||
version = '2'
|
||||
|
||||
# Default hdf5 compression to use when writing
|
||||
# 'gzip' is widely available and somewhat slow
|
||||
# 'lzf' is faster, but generally not available outside h5py
|
||||
# 'szip' is also faster, but lacks write support on windows
|
||||
# (so by default, we use no compression)
|
||||
# May also be a tuple (filter, opts), such as ('gzip', 3)
|
||||
defaultCompression = None
|
||||
|
||||
## Types allowed as axis or column names
|
||||
nameTypes = [basestring, tuple]
|
||||
@ -122,7 +130,7 @@ class MetaArray(object):
|
||||
if file is not None:
|
||||
self._data = None
|
||||
self.readFile(file, **kwargs)
|
||||
if self._data is None:
|
||||
if kwargs.get("readAllData", True) and self._data is None:
|
||||
raise Exception("File read failed: %s" % file)
|
||||
else:
|
||||
self._info = info
|
||||
@ -720,25 +728,28 @@ class MetaArray(object):
|
||||
|
||||
"""
|
||||
## decide which read function to use
|
||||
fd = open(filename, 'rb')
|
||||
magic = fd.read(8)
|
||||
if magic == '\x89HDF\r\n\x1a\n':
|
||||
fd.close()
|
||||
self._readHDF5(filename, **kwargs)
|
||||
self._isHDF = True
|
||||
else:
|
||||
fd.seek(0)
|
||||
meta = MetaArray._readMeta(fd)
|
||||
if 'version' in meta:
|
||||
ver = meta['version']
|
||||
with open(filename, 'rb') as fd:
|
||||
magic = fd.read(8)
|
||||
if magic == '\x89HDF\r\n\x1a\n':
|
||||
fd.close()
|
||||
self._readHDF5(filename, **kwargs)
|
||||
self._isHDF = True
|
||||
else:
|
||||
ver = 1
|
||||
rFuncName = '_readData%s' % str(ver)
|
||||
if not hasattr(MetaArray, rFuncName):
|
||||
raise Exception("This MetaArray library does not support array version '%s'" % ver)
|
||||
rFunc = getattr(self, rFuncName)
|
||||
rFunc(fd, meta, **kwargs)
|
||||
self._isHDF = False
|
||||
fd.seek(0)
|
||||
meta = MetaArray._readMeta(fd)
|
||||
|
||||
if not kwargs.get("readAllData", True):
|
||||
self._data = np.empty(meta['shape'], dtype=meta['type'])
|
||||
if 'version' in meta:
|
||||
ver = meta['version']
|
||||
else:
|
||||
ver = 1
|
||||
rFuncName = '_readData%s' % str(ver)
|
||||
if not hasattr(MetaArray, rFuncName):
|
||||
raise Exception("This MetaArray library does not support array version '%s'" % ver)
|
||||
rFunc = getattr(self, rFuncName)
|
||||
rFunc(fd, meta, **kwargs)
|
||||
self._isHDF = False
|
||||
|
||||
@staticmethod
|
||||
def _readMeta(fd):
|
||||
@ -756,7 +767,7 @@ class MetaArray(object):
|
||||
#print ret
|
||||
return ret
|
||||
|
||||
def _readData1(self, fd, meta, mmap=False):
|
||||
def _readData1(self, fd, meta, mmap=False, **kwds):
|
||||
## Read array data from the file descriptor for MetaArray v1 files
|
||||
## read in axis values for any axis that specifies a length
|
||||
frameSize = 1
|
||||
@ -766,16 +777,18 @@ class MetaArray(object):
|
||||
frameSize *= ax['values_len']
|
||||
del ax['values_len']
|
||||
del ax['values_type']
|
||||
self._info = meta['info']
|
||||
if not kwds.get("readAllData", True):
|
||||
return
|
||||
## the remaining data is the actual array
|
||||
if mmap:
|
||||
subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape'])
|
||||
else:
|
||||
subarr = np.fromstring(fd.read(), dtype=meta['type'])
|
||||
subarr.shape = meta['shape']
|
||||
self._info = meta['info']
|
||||
self._data = subarr
|
||||
|
||||
def _readData2(self, fd, meta, mmap=False, subset=None):
|
||||
def _readData2(self, fd, meta, mmap=False, subset=None, **kwds):
|
||||
## read in axis values
|
||||
dynAxis = None
|
||||
frameSize = 1
|
||||
@ -792,7 +805,10 @@ class MetaArray(object):
|
||||
frameSize *= ax['values_len']
|
||||
del ax['values_len']
|
||||
del ax['values_type']
|
||||
|
||||
self._info = meta['info']
|
||||
if not kwds.get("readAllData", True):
|
||||
return
|
||||
|
||||
## No axes are dynamic, just read the entire array in at once
|
||||
if dynAxis is None:
|
||||
#if rewriteDynamic is not None:
|
||||
@ -1027,10 +1043,18 @@ class MetaArray(object):
|
||||
|
||||
def writeHDF5(self, fileName, **opts):
|
||||
## default options for writing datasets
|
||||
comp = self.defaultCompression
|
||||
if isinstance(comp, tuple):
|
||||
comp, copts = comp
|
||||
else:
|
||||
copts = None
|
||||
|
||||
dsOpts = {
|
||||
'compression': 'lzf',
|
||||
'compression': comp,
|
||||
'chunks': True,
|
||||
}
|
||||
if copts is not None:
|
||||
dsOpts['compression_opts'] = copts
|
||||
|
||||
## if there is an appendable axis, then we can guess the desired chunk shape (optimized for appending)
|
||||
appAxis = opts.get('appendAxis', None)
|
||||
|
@ -1,5 +1,6 @@
|
||||
import os, time, sys, traceback, weakref
|
||||
import numpy as np
|
||||
import threading
|
||||
try:
|
||||
import __builtin__ as builtins
|
||||
import cPickle as pickle
|
||||
@ -53,8 +54,10 @@ class RemoteEventHandler(object):
|
||||
## status is either 'result' or 'error'
|
||||
## if 'error', then result will be (exception, formatted exceprion)
|
||||
## where exception may be None if it could not be passed through the Connection.
|
||||
self.resultLock = threading.RLock()
|
||||
|
||||
self.proxies = {} ## maps {weakref(proxy): proxyId}; used to inform the remote process when a proxy has been deleted.
|
||||
self.proxyLock = threading.RLock()
|
||||
|
||||
## attributes that affect the behavior of the proxy.
|
||||
## See ObjectProxy._setProxyOptions for description
|
||||
@ -66,10 +69,15 @@ class RemoteEventHandler(object):
|
||||
'deferGetattr': False, ## True, False
|
||||
'noProxyTypes': [ type(None), str, int, float, tuple, list, dict, LocalObjectProxy, ObjectProxy ],
|
||||
}
|
||||
self.optsLock = threading.RLock()
|
||||
|
||||
self.nextRequestId = 0
|
||||
self.exited = False
|
||||
|
||||
# Mutexes to help prevent issues when multiple threads access the same RemoteEventHandler
|
||||
self.processLock = threading.RLock()
|
||||
self.sendLock = threading.RLock()
|
||||
|
||||
RemoteEventHandler.handlers[pid] = self ## register this handler as the one communicating with pid
|
||||
|
||||
@classmethod
|
||||
@ -86,46 +94,59 @@ class RemoteEventHandler(object):
|
||||
cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1)
|
||||
|
||||
def getProxyOption(self, opt):
|
||||
return self.proxyOptions[opt]
|
||||
with self.optsLock:
|
||||
return self.proxyOptions[opt]
|
||||
|
||||
def setProxyOptions(self, **kwds):
|
||||
"""
|
||||
Set the default behavior options for object proxies.
|
||||
See ObjectProxy._setProxyOptions for more info.
|
||||
"""
|
||||
self.proxyOptions.update(kwds)
|
||||
with self.optsLock:
|
||||
self.proxyOptions.update(kwds)
|
||||
|
||||
def processRequests(self):
|
||||
"""Process all pending requests from the pipe, return
|
||||
after no more events are immediately available. (non-blocking)
|
||||
Returns the number of events processed.
|
||||
"""
|
||||
if self.exited:
|
||||
self.debugMsg(' processRequests: exited already; raise ClosedError.')
|
||||
raise ClosedError()
|
||||
|
||||
numProcessed = 0
|
||||
while self.conn.poll():
|
||||
try:
|
||||
self.handleRequest()
|
||||
numProcessed += 1
|
||||
except ClosedError:
|
||||
self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.')
|
||||
self.exited = True
|
||||
raise
|
||||
#except IOError as err: ## let handleRequest take care of this.
|
||||
#self.debugMsg(' got IOError from handleRequest; try again.')
|
||||
#if err.errno == 4: ## interrupted system call; try again
|
||||
#continue
|
||||
#else:
|
||||
#raise
|
||||
except:
|
||||
print("Error in process %s" % self.name)
|
||||
sys.excepthook(*sys.exc_info())
|
||||
|
||||
if numProcessed > 0:
|
||||
self.debugMsg('processRequests: finished %d requests' % numProcessed)
|
||||
return numProcessed
|
||||
with self.processLock:
|
||||
|
||||
if self.exited:
|
||||
self.debugMsg(' processRequests: exited already; raise ClosedError.')
|
||||
raise ClosedError()
|
||||
|
||||
numProcessed = 0
|
||||
|
||||
while self.conn.poll():
|
||||
#try:
|
||||
#poll = self.conn.poll()
|
||||
#if not poll:
|
||||
#break
|
||||
#except IOError: # this can happen if the remote process dies.
|
||||
## might it also happen in other circumstances?
|
||||
#raise ClosedError()
|
||||
|
||||
try:
|
||||
self.handleRequest()
|
||||
numProcessed += 1
|
||||
except ClosedError:
|
||||
self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.')
|
||||
self.exited = True
|
||||
raise
|
||||
#except IOError as err: ## let handleRequest take care of this.
|
||||
#self.debugMsg(' got IOError from handleRequest; try again.')
|
||||
#if err.errno == 4: ## interrupted system call; try again
|
||||
#continue
|
||||
#else:
|
||||
#raise
|
||||
except:
|
||||
print("Error in process %s" % self.name)
|
||||
sys.excepthook(*sys.exc_info())
|
||||
|
||||
if numProcessed > 0:
|
||||
self.debugMsg('processRequests: finished %d requests' % numProcessed)
|
||||
return numProcessed
|
||||
|
||||
def handleRequest(self):
|
||||
"""Handle a single request from the remote process.
|
||||
@ -183,9 +204,11 @@ class RemoteEventHandler(object):
|
||||
returnType = opts.get('returnType', 'auto')
|
||||
|
||||
if cmd == 'result':
|
||||
self.results[resultId] = ('result', opts['result'])
|
||||
with self.resultLock:
|
||||
self.results[resultId] = ('result', opts['result'])
|
||||
elif cmd == 'error':
|
||||
self.results[resultId] = ('error', (opts['exception'], opts['excString']))
|
||||
with self.resultLock:
|
||||
self.results[resultId] = ('error', (opts['exception'], opts['excString']))
|
||||
elif cmd == 'getObjAttr':
|
||||
result = getattr(opts['obj'], opts['attr'])
|
||||
elif cmd == 'callObj':
|
||||
@ -259,7 +282,9 @@ class RemoteEventHandler(object):
|
||||
self.debugMsg(" handleRequest: sending return value for %d: %s" % (reqId, str(result)))
|
||||
#print "returnValue:", returnValue, result
|
||||
if returnType == 'auto':
|
||||
result = self.autoProxy(result, self.proxyOptions['noProxyTypes'])
|
||||
with self.optsLock:
|
||||
noProxyTypes = self.proxyOptions['noProxyTypes']
|
||||
result = self.autoProxy(result, noProxyTypes)
|
||||
elif returnType == 'proxy':
|
||||
result = LocalObjectProxy(result)
|
||||
|
||||
@ -378,54 +403,59 @@ class RemoteEventHandler(object):
|
||||
traceback
|
||||
============= =====================================================================
|
||||
"""
|
||||
#if len(kwds) > 0:
|
||||
#print "Warning: send() ignored args:", kwds
|
||||
if self.exited:
|
||||
self.debugMsg(' send: exited already; raise ClosedError.')
|
||||
raise ClosedError()
|
||||
|
||||
with self.sendLock:
|
||||
#if len(kwds) > 0:
|
||||
#print "Warning: send() ignored args:", kwds
|
||||
|
||||
if opts is None:
|
||||
opts = {}
|
||||
|
||||
if opts is None:
|
||||
opts = {}
|
||||
|
||||
assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"'
|
||||
if reqId is None:
|
||||
if callSync != 'off': ## requested return value; use the next available request ID
|
||||
reqId = self.nextRequestId
|
||||
self.nextRequestId += 1
|
||||
else:
|
||||
## If requestId is provided, this _must_ be a response to a previously received request.
|
||||
assert request in ['result', 'error']
|
||||
|
||||
if returnType is not None:
|
||||
opts['returnType'] = returnType
|
||||
assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"'
|
||||
if reqId is None:
|
||||
if callSync != 'off': ## requested return value; use the next available request ID
|
||||
reqId = self.nextRequestId
|
||||
self.nextRequestId += 1
|
||||
else:
|
||||
## If requestId is provided, this _must_ be a response to a previously received request.
|
||||
assert request in ['result', 'error']
|
||||
|
||||
#print os.getpid(), "send request:", request, reqId, opts
|
||||
|
||||
## double-pickle args to ensure that at least status and request ID get through
|
||||
try:
|
||||
optStr = pickle.dumps(opts)
|
||||
except:
|
||||
print("==== Error pickling this object: ====")
|
||||
print(opts)
|
||||
print("=======================================")
|
||||
raise
|
||||
|
||||
nByteMsgs = 0
|
||||
if byteData is not None:
|
||||
nByteMsgs = len(byteData)
|
||||
if returnType is not None:
|
||||
opts['returnType'] = returnType
|
||||
|
||||
#print os.getpid(), "send request:", request, reqId, opts
|
||||
|
||||
## double-pickle args to ensure that at least status and request ID get through
|
||||
try:
|
||||
optStr = pickle.dumps(opts)
|
||||
except:
|
||||
print("==== Error pickling this object: ====")
|
||||
print(opts)
|
||||
print("=======================================")
|
||||
raise
|
||||
|
||||
nByteMsgs = 0
|
||||
if byteData is not None:
|
||||
nByteMsgs = len(byteData)
|
||||
|
||||
## 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.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(' call sync: %s' % callSync)
|
||||
if callSync == 'off':
|
||||
return
|
||||
|
||||
## 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.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(' call sync: %s' % callSync)
|
||||
if callSync == 'off':
|
||||
return
|
||||
|
||||
req = Request(self, reqId, description=str(request), timeout=timeout)
|
||||
if callSync == 'async':
|
||||
return req
|
||||
@ -437,20 +467,30 @@ class RemoteEventHandler(object):
|
||||
return req
|
||||
|
||||
def close(self, callSync='off', noCleanup=False, **kwds):
|
||||
self.send(request='close', opts=dict(noCleanup=noCleanup), callSync=callSync, **kwds)
|
||||
try:
|
||||
self.send(request='close', opts=dict(noCleanup=noCleanup), callSync=callSync, **kwds)
|
||||
self.exited = True
|
||||
except ClosedError:
|
||||
pass
|
||||
|
||||
def getResult(self, reqId):
|
||||
## raises NoResultError if the result is not available yet
|
||||
#print self.results.keys(), os.getpid()
|
||||
if reqId not in self.results:
|
||||
with self.resultLock:
|
||||
haveResult = reqId in self.results
|
||||
|
||||
if not haveResult:
|
||||
try:
|
||||
self.processRequests()
|
||||
except ClosedError: ## even if remote connection has closed, we may have
|
||||
## received new data during this call to processRequests()
|
||||
pass
|
||||
if reqId not in self.results:
|
||||
raise NoResultError()
|
||||
status, result = self.results.pop(reqId)
|
||||
|
||||
with self.resultLock:
|
||||
if reqId not in self.results:
|
||||
raise NoResultError()
|
||||
status, result = self.results.pop(reqId)
|
||||
|
||||
if status == 'result':
|
||||
return result
|
||||
elif status == 'error':
|
||||
@ -494,11 +534,13 @@ class RemoteEventHandler(object):
|
||||
args = list(args)
|
||||
|
||||
## Decide whether to send arguments by value or by proxy
|
||||
noProxyTypes = opts.pop('noProxyTypes', None)
|
||||
if noProxyTypes is None:
|
||||
noProxyTypes = self.proxyOptions['noProxyTypes']
|
||||
|
||||
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
|
||||
with self.optsLock:
|
||||
noProxyTypes = opts.pop('noProxyTypes', None)
|
||||
if noProxyTypes is None:
|
||||
noProxyTypes = self.proxyOptions['noProxyTypes']
|
||||
|
||||
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
|
||||
|
||||
if autoProxy is True:
|
||||
args = [self.autoProxy(v, noProxyTypes) for v in args]
|
||||
for k, v in kwds.iteritems():
|
||||
@ -520,11 +562,14 @@ class RemoteEventHandler(object):
|
||||
return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), byteData=byteMsgs, **opts)
|
||||
|
||||
def registerProxy(self, proxy):
|
||||
ref = weakref.ref(proxy, self.deleteProxy)
|
||||
self.proxies[ref] = proxy._proxyId
|
||||
with self.proxyLock:
|
||||
ref = weakref.ref(proxy, self.deleteProxy)
|
||||
self.proxies[ref] = proxy._proxyId
|
||||
|
||||
def deleteProxy(self, ref):
|
||||
proxyId = self.proxies.pop(ref)
|
||||
with self.proxyLock:
|
||||
proxyId = self.proxies.pop(ref)
|
||||
|
||||
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
|
||||
|
@ -1,6 +1,7 @@
|
||||
from ..Qt import QtGui, QtCore
|
||||
import os, weakref, re
|
||||
from ..pgcollections import OrderedDict
|
||||
from ..python2_3 import asUnicode
|
||||
from .ParameterItem import ParameterItem
|
||||
|
||||
PARAM_TYPES = {}
|
||||
@ -13,7 +14,9 @@ def registerParameterType(name, cls, override=False):
|
||||
PARAM_TYPES[name] = cls
|
||||
PARAM_NAMES[cls] = name
|
||||
|
||||
|
||||
def __reload__(old):
|
||||
PARAM_TYPES.update(old.get('PARAM_TYPES', {}))
|
||||
PARAM_NAMES.update(old.get('PARAM_NAMES', {}))
|
||||
|
||||
class Parameter(QtCore.QObject):
|
||||
"""
|
||||
@ -46,6 +49,7 @@ class Parameter(QtCore.QObject):
|
||||
including during editing.
|
||||
sigChildAdded(self, child, index) Emitted when a child is added
|
||||
sigChildRemoved(self, child) Emitted when a child is removed
|
||||
sigRemoved(self) Emitted when this parameter is removed
|
||||
sigParentChanged(self, parent) Emitted when this parameter's parent has changed
|
||||
sigLimitsChanged(self, limits) Emitted when this parameter's limits have changed
|
||||
sigDefaultChanged(self, default) Emitted when this parameter's default value has changed
|
||||
@ -61,6 +65,7 @@ class Parameter(QtCore.QObject):
|
||||
|
||||
sigChildAdded = QtCore.Signal(object, object, object) ## self, child, index
|
||||
sigChildRemoved = QtCore.Signal(object, object) ## self, child
|
||||
sigRemoved = QtCore.Signal(object) ## self
|
||||
sigParentChanged = QtCore.Signal(object, object) ## self, parent
|
||||
sigLimitsChanged = QtCore.Signal(object, object) ## self, limits
|
||||
sigDefaultChanged = QtCore.Signal(object, object) ## self, default
|
||||
@ -133,6 +138,12 @@ class Parameter(QtCore.QObject):
|
||||
expanded If True, the Parameter will appear expanded when
|
||||
displayed in a ParameterTree (its children will be
|
||||
visible). (default=True)
|
||||
title (str or None) If specified, then the parameter will be
|
||||
displayed to the user using this string as its name.
|
||||
However, the parameter will still be referred to
|
||||
internally using the *name* specified above. Note that
|
||||
this option is not compatible with renamable=True.
|
||||
(default=None; added in version 0.9.9)
|
||||
======================= =========================================================
|
||||
"""
|
||||
|
||||
@ -148,6 +159,7 @@ class Parameter(QtCore.QObject):
|
||||
'removable': False,
|
||||
'strictNaming': False, # forces name to be usable as a python variable
|
||||
'expanded': True,
|
||||
'title': None,
|
||||
#'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits.
|
||||
}
|
||||
self.opts.update(opts)
|
||||
@ -266,16 +278,27 @@ class Parameter(QtCore.QObject):
|
||||
vals[ch.name()] = (ch.value(), ch.getValues())
|
||||
return vals
|
||||
|
||||
def saveState(self):
|
||||
def saveState(self, filter=None):
|
||||
"""
|
||||
Return a structure representing the entire state of the parameter tree.
|
||||
The tree state may be restored from this structure using restoreState()
|
||||
The tree state may be restored from this structure using restoreState().
|
||||
|
||||
If *filter* is set to 'user', then only user-settable data will be included in the
|
||||
returned state.
|
||||
"""
|
||||
state = self.opts.copy()
|
||||
state['children'] = OrderedDict([(ch.name(), ch.saveState()) for ch in self])
|
||||
if state['type'] is None:
|
||||
global PARAM_NAMES
|
||||
state['type'] = PARAM_NAMES.get(type(self), None)
|
||||
if filter is None:
|
||||
state = self.opts.copy()
|
||||
if state['type'] is None:
|
||||
global PARAM_NAMES
|
||||
state['type'] = PARAM_NAMES.get(type(self), None)
|
||||
elif filter == 'user':
|
||||
state = {'value': self.value()}
|
||||
else:
|
||||
raise ValueError("Unrecognized filter argument: '%s'" % filter)
|
||||
|
||||
ch = OrderedDict([(ch.name(), ch.saveState(filter=filter)) for ch in self])
|
||||
if len(ch) > 0:
|
||||
state['children'] = ch
|
||||
return state
|
||||
|
||||
def restoreState(self, state, recursive=True, addChildren=True, removeChildren=True, blockSignals=True):
|
||||
@ -293,8 +316,11 @@ class Parameter(QtCore.QObject):
|
||||
|
||||
## list of children may be stored either as list or dict.
|
||||
if isinstance(childState, dict):
|
||||
childState = childState.values()
|
||||
|
||||
cs = []
|
||||
for k,v in childState.items():
|
||||
cs.append(v.copy())
|
||||
cs[-1].setdefault('name', k)
|
||||
childState = cs
|
||||
|
||||
if blockSignals:
|
||||
self.blockTreeChangeSignal()
|
||||
@ -311,14 +337,14 @@ class Parameter(QtCore.QObject):
|
||||
|
||||
for ch in childState:
|
||||
name = ch['name']
|
||||
typ = ch['type']
|
||||
#typ = ch.get('type', None)
|
||||
#print('child: %s, %s' % (self.name()+'.'+name, typ))
|
||||
|
||||
## First, see if there is already a child with this name and type
|
||||
## First, see if there is already a child with this name
|
||||
gotChild = False
|
||||
for i, ch2 in enumerate(self.childs[ptr:]):
|
||||
#print " ", ch2.name(), ch2.type()
|
||||
if ch2.name() != name or not ch2.isType(typ):
|
||||
if ch2.name() != name: # or not ch2.isType(typ):
|
||||
continue
|
||||
gotChild = True
|
||||
#print " found it"
|
||||
@ -393,15 +419,22 @@ class Parameter(QtCore.QObject):
|
||||
Note that the value of the parameter can *always* be changed by
|
||||
calling setValue().
|
||||
"""
|
||||
return not self.opts.get('readonly', False)
|
||||
return not self.readonly()
|
||||
|
||||
def setWritable(self, writable=True):
|
||||
"""Set whether this Parameter should be editable by the user. (This is
|
||||
exactly the opposite of setReadonly)."""
|
||||
self.setOpts(readonly=not writable)
|
||||
|
||||
def readonly(self):
|
||||
"""
|
||||
Return True if this parameter is read-only. (this is the opposite of writable())
|
||||
"""
|
||||
return self.opts.get('readonly', False)
|
||||
|
||||
def setReadonly(self, readonly=True):
|
||||
"""Set whether this Parameter's value may be edited by the user."""
|
||||
"""Set whether this Parameter's value may be edited by the user
|
||||
(this is the opposite of setWritable())."""
|
||||
self.setOpts(readonly=readonly)
|
||||
|
||||
def setOpts(self, **opts):
|
||||
@ -453,11 +486,20 @@ class Parameter(QtCore.QObject):
|
||||
return ParameterItem(self, depth=depth)
|
||||
|
||||
|
||||
def addChild(self, child):
|
||||
"""Add another parameter to the end of this parameter's child list."""
|
||||
return self.insertChild(len(self.childs), child)
|
||||
def addChild(self, child, autoIncrementName=None):
|
||||
"""
|
||||
Add another parameter to the end of this parameter's child list.
|
||||
|
||||
See insertChild() for a description of the *autoIncrementName*
|
||||
argument.
|
||||
"""
|
||||
return self.insertChild(len(self.childs), child, autoIncrementName=autoIncrementName)
|
||||
|
||||
def addChildren(self, children):
|
||||
"""
|
||||
Add a list or dict of children to this parameter. This method calls
|
||||
addChild once for each value in *children*.
|
||||
"""
|
||||
## If children was specified as dict, then assume keys are the names.
|
||||
if isinstance(children, dict):
|
||||
ch2 = []
|
||||
@ -473,19 +515,24 @@ class Parameter(QtCore.QObject):
|
||||
self.addChild(chOpts)
|
||||
|
||||
|
||||
def insertChild(self, pos, child):
|
||||
def insertChild(self, pos, child, autoIncrementName=None):
|
||||
"""
|
||||
Insert a new child at pos.
|
||||
If pos is a Parameter, then insert at the position of that Parameter.
|
||||
If child is a dict, then a parameter is constructed using
|
||||
:func:`Parameter.create <pyqtgraph.parametertree.Parameter.create>`.
|
||||
|
||||
By default, the child's 'autoIncrementName' option determines whether
|
||||
the name will be adjusted to avoid prior name collisions. This
|
||||
behavior may be overridden by specifying the *autoIncrementName*
|
||||
argument. This argument was added in version 0.9.9.
|
||||
"""
|
||||
if isinstance(child, dict):
|
||||
child = Parameter.create(**child)
|
||||
|
||||
name = child.name()
|
||||
if name in self.names and child is not self.names[name]:
|
||||
if child.opts.get('autoIncrementName', False):
|
||||
if autoIncrementName is True or (autoIncrementName is None and child.opts.get('autoIncrementName', False)):
|
||||
name = self.incrementName(name)
|
||||
child.setName(name)
|
||||
else:
|
||||
@ -550,6 +597,7 @@ class Parameter(QtCore.QObject):
|
||||
if parent is None:
|
||||
raise Exception("Cannot remove; no parent.")
|
||||
parent.removeChild(self)
|
||||
self.sigRemoved.emit(self)
|
||||
|
||||
def incrementName(self, name):
|
||||
## return an unused name by adding a number to the name given
|
||||
@ -590,9 +638,12 @@ class Parameter(QtCore.QObject):
|
||||
names = (names,)
|
||||
return self.param(*names).setValue(value)
|
||||
|
||||
def param(self, *names):
|
||||
def child(self, *names):
|
||||
"""Return a child parameter.
|
||||
Accepts the name of the child or a tuple (path, to, child)"""
|
||||
Accepts the name of the child or a tuple (path, to, child)
|
||||
|
||||
Added in version 0.9.9. Ealier versions used the 'param' method, which is still
|
||||
implemented for backward compatibility."""
|
||||
try:
|
||||
param = self.names[names[0]]
|
||||
except KeyError:
|
||||
@ -603,8 +654,12 @@ class Parameter(QtCore.QObject):
|
||||
else:
|
||||
return param
|
||||
|
||||
def param(self, *names):
|
||||
# for backward compatibility.
|
||||
return self.child(*names)
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s '%s' at 0x%x>" % (self.__class__.__name__, self.name(), id(self))
|
||||
return asUnicode("<%s '%s' at 0x%x>") % (self.__class__.__name__, self.name(), id(self))
|
||||
|
||||
def __getattr__(self, attr):
|
||||
## Leaving this undocumented because I might like to remove it in the future..
|
||||
@ -692,7 +747,8 @@ class Parameter(QtCore.QObject):
|
||||
if self.blockTreeChangeEmit == 0:
|
||||
changes = self.treeStateChanges
|
||||
self.treeStateChanges = []
|
||||
self.sigTreeStateChanged.emit(self, changes)
|
||||
if len(changes) > 0:
|
||||
self.sigTreeStateChanged.emit(self, changes)
|
||||
|
||||
|
||||
class SignalBlocker(object):
|
||||
|
@ -1,4 +1,5 @@
|
||||
from ..Qt import QtGui, QtCore
|
||||
from ..python2_3 import asUnicode
|
||||
import os, weakref, re
|
||||
|
||||
class ParameterItem(QtGui.QTreeWidgetItem):
|
||||
@ -15,8 +16,11 @@ class ParameterItem(QtGui.QTreeWidgetItem):
|
||||
"""
|
||||
|
||||
def __init__(self, param, depth=0):
|
||||
QtGui.QTreeWidgetItem.__init__(self, [param.name(), ''])
|
||||
|
||||
title = param.opts.get('title', None)
|
||||
if title is None:
|
||||
title = param.name()
|
||||
QtGui.QTreeWidgetItem.__init__(self, [title, ''])
|
||||
|
||||
self.param = param
|
||||
self.param.registerItem(self) ## let parameter know this item is connected to it (for debugging)
|
||||
self.depth = depth
|
||||
@ -30,7 +34,6 @@ class ParameterItem(QtGui.QTreeWidgetItem):
|
||||
param.sigOptionsChanged.connect(self.optsChanged)
|
||||
param.sigParentChanged.connect(self.parentChanged)
|
||||
|
||||
|
||||
opts = param.opts
|
||||
|
||||
## Generate context menu for renaming/removing parameter
|
||||
@ -38,6 +41,8 @@ class ParameterItem(QtGui.QTreeWidgetItem):
|
||||
self.contextMenu.addSeparator()
|
||||
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
|
||||
if opts.get('renamable', False):
|
||||
if param.opts.get('title', None) is not None:
|
||||
raise Exception("Cannot make parameter with both title != None and renamable == True.")
|
||||
flags |= QtCore.Qt.ItemIsEditable
|
||||
self.contextMenu.addAction('Rename').triggered.connect(self.editName)
|
||||
if opts.get('removable', False):
|
||||
@ -107,15 +112,15 @@ class ParameterItem(QtGui.QTreeWidgetItem):
|
||||
self.contextMenu.popup(ev.globalPos())
|
||||
|
||||
def columnChangedEvent(self, col):
|
||||
"""Called when the text in a column has been edited.
|
||||
"""Called when the text in a column has been edited (or otherwise changed).
|
||||
By default, we only use changes to column 0 to rename the parameter.
|
||||
"""
|
||||
if col == 0:
|
||||
if col == 0 and (self.param.opts.get('title', None) is None):
|
||||
if self.ignoreNameColumnChange:
|
||||
return
|
||||
try:
|
||||
newName = self.param.setName(str(self.text(col)))
|
||||
except:
|
||||
newName = self.param.setName(asUnicode(self.text(col)))
|
||||
except Exception:
|
||||
self.setText(0, self.param.name())
|
||||
raise
|
||||
|
||||
@ -127,8 +132,9 @@ class ParameterItem(QtGui.QTreeWidgetItem):
|
||||
|
||||
def nameChanged(self, param, name):
|
||||
## called when the parameter's name has changed.
|
||||
self.setText(0, name)
|
||||
|
||||
if self.param.opts.get('title', None) is None:
|
||||
self.setText(0, name)
|
||||
|
||||
def limitsChanged(self, param, limits):
|
||||
"""Called when the parameter's limits have changed"""
|
||||
pass
|
||||
|
127
pyqtgraph/parametertree/ParameterSystem.py
Normal file
127
pyqtgraph/parametertree/ParameterSystem.py
Normal file
@ -0,0 +1,127 @@
|
||||
from .parameterTypes import GroupParameter
|
||||
from .. import functions as fn
|
||||
from .SystemSolver import SystemSolver
|
||||
|
||||
|
||||
class ParameterSystem(GroupParameter):
|
||||
"""
|
||||
ParameterSystem is a subclass of GroupParameter that manages a tree of
|
||||
sub-parameters with a set of interdependencies--changing any one parameter
|
||||
may affect other parameters in the system.
|
||||
|
||||
See parametertree/SystemSolver for more information.
|
||||
|
||||
NOTE: This API is experimental and may change substantially across minor
|
||||
version numbers.
|
||||
"""
|
||||
def __init__(self, *args, **kwds):
|
||||
GroupParameter.__init__(self, *args, **kwds)
|
||||
self._system = None
|
||||
self._fixParams = [] # all auto-generated 'fixed' params
|
||||
sys = kwds.pop('system', None)
|
||||
if sys is not None:
|
||||
self.setSystem(sys)
|
||||
self._ignoreChange = [] # params whose changes should be ignored temporarily
|
||||
self.sigTreeStateChanged.connect(self.updateSystem)
|
||||
|
||||
def setSystem(self, sys):
|
||||
self._system = sys
|
||||
|
||||
# auto-generate defaults to match child parameters
|
||||
defaults = {}
|
||||
vals = {}
|
||||
for param in self:
|
||||
name = param.name()
|
||||
constraints = ''
|
||||
if hasattr(sys, '_' + name):
|
||||
constraints += 'n'
|
||||
|
||||
if not param.readonly():
|
||||
constraints += 'f'
|
||||
if 'n' in constraints:
|
||||
ch = param.addChild(dict(name='fixed', type='bool', value=False))
|
||||
self._fixParams.append(ch)
|
||||
param.setReadonly(True)
|
||||
param.setOpts(expanded=False)
|
||||
else:
|
||||
vals[name] = param.value()
|
||||
ch = param.addChild(dict(name='fixed', type='bool', value=True, readonly=True))
|
||||
#self._fixParams.append(ch)
|
||||
|
||||
defaults[name] = [None, param.type(), None, constraints]
|
||||
|
||||
sys.defaultState.update(defaults)
|
||||
sys.reset()
|
||||
for name, value in vals.items():
|
||||
setattr(sys, name, value)
|
||||
|
||||
self.updateAllParams()
|
||||
|
||||
def updateSystem(self, param, changes):
|
||||
changes = [ch for ch in changes if ch[0] not in self._ignoreChange]
|
||||
|
||||
#resets = [ch[0] for ch in changes if ch[1] == 'setToDefault']
|
||||
sets = [ch[0] for ch in changes if ch[1] == 'value']
|
||||
#for param in resets:
|
||||
#setattr(self._system, param.name(), None)
|
||||
|
||||
for param in sets:
|
||||
#if param in resets:
|
||||
#continue
|
||||
|
||||
#if param in self._fixParams:
|
||||
#param.parent().setWritable(param.value())
|
||||
#else:
|
||||
if param in self._fixParams:
|
||||
parent = param.parent()
|
||||
if param.value():
|
||||
setattr(self._system, parent.name(), parent.value())
|
||||
else:
|
||||
setattr(self._system, parent.name(), None)
|
||||
else:
|
||||
setattr(self._system, param.name(), param.value())
|
||||
|
||||
self.updateAllParams()
|
||||
|
||||
def updateAllParams(self):
|
||||
try:
|
||||
self.sigTreeStateChanged.disconnect(self.updateSystem)
|
||||
for name, state in self._system._vars.items():
|
||||
param = self.child(name)
|
||||
try:
|
||||
v = getattr(self._system, name)
|
||||
if self._system._vars[name][2] is None:
|
||||
self.updateParamState(self.child(name), 'autoSet')
|
||||
param.setValue(v)
|
||||
else:
|
||||
self.updateParamState(self.child(name), 'fixed')
|
||||
except RuntimeError:
|
||||
self.updateParamState(param, 'autoUnset')
|
||||
finally:
|
||||
self.sigTreeStateChanged.connect(self.updateSystem)
|
||||
|
||||
def updateParamState(self, param, state):
|
||||
if state == 'autoSet':
|
||||
bg = fn.mkBrush((200, 255, 200, 255))
|
||||
bold = False
|
||||
readonly = True
|
||||
elif state == 'autoUnset':
|
||||
bg = fn.mkBrush(None)
|
||||
bold = False
|
||||
readonly = False
|
||||
elif state == 'fixed':
|
||||
bg = fn.mkBrush('y')
|
||||
bold = True
|
||||
readonly = False
|
||||
|
||||
param.setReadonly(readonly)
|
||||
|
||||
#for item in param.items:
|
||||
#item.setBackground(0, bg)
|
||||
#f = item.font(0)
|
||||
#f.setWeight(f.Bold if bold else f.Normal)
|
||||
#item.setFont(0, f)
|
||||
|
||||
|
||||
|
||||
|
381
pyqtgraph/parametertree/SystemSolver.py
Normal file
381
pyqtgraph/parametertree/SystemSolver.py
Normal file
@ -0,0 +1,381 @@
|
||||
from collections import OrderedDict
|
||||
import numpy as np
|
||||
|
||||
class SystemSolver(object):
|
||||
"""
|
||||
This abstract class is used to formalize and manage user interaction with a
|
||||
complex system of equations (related to "constraint satisfaction problems").
|
||||
It is often the case that devices must be controlled
|
||||
through a large number of free variables, and interactions between these
|
||||
variables make the system difficult to manage and conceptualize as a user
|
||||
interface. This class does _not_ attempt to numerically solve the system
|
||||
of equations. Rather, it provides a framework for subdividing the system
|
||||
into manageable pieces and specifying closed-form solutions to these small
|
||||
pieces.
|
||||
|
||||
For an example, see the simple Camera class below.
|
||||
|
||||
Theory of operation: Conceptualize the system as 1) a set of variables
|
||||
whose values may be either user-specified or automatically generated, and
|
||||
2) a set of functions that define *how* each variable should be generated.
|
||||
When a variable is accessed (as an instance attribute), the solver first
|
||||
checks to see if it already has a value (either user-supplied, or cached
|
||||
from a previous calculation). If it does not, then the solver calls a
|
||||
method on itself (the method must be named `_variableName`) that will
|
||||
either return the calculated value (which usually involves acccessing
|
||||
other variables in the system), or raise RuntimeError if it is unable to
|
||||
calculate the value (usually because the user has not provided sufficient
|
||||
input to fully constrain the system).
|
||||
|
||||
Each method that calculates a variable value may include multiple
|
||||
try/except blocks, so that if one method generates a RuntimeError, it may
|
||||
fall back on others.
|
||||
In this way, the system may be solved by recursively searching the tree of
|
||||
possible relationships between variables. This allows the user flexibility
|
||||
in deciding which variables are the most important to specify, while
|
||||
avoiding the apparent combinatorial explosion of calculation pathways
|
||||
that must be considered by the developer.
|
||||
|
||||
Solved values are cached for efficiency, and automatically cleared when
|
||||
a state change invalidates the cache. The rules for this are simple: any
|
||||
time a value is set, it invalidates the cache *unless* the previous value
|
||||
was None (which indicates that no other variable has yet requested that
|
||||
value). More complex cache management may be defined in subclasses.
|
||||
|
||||
|
||||
Subclasses must define:
|
||||
|
||||
1) The *defaultState* class attribute: This is a dict containing a
|
||||
description of the variables in the system--their default values,
|
||||
data types, and the ways they can be constrained. The format is::
|
||||
|
||||
{ name: [value, type, constraint, allowed_constraints], ...}
|
||||
|
||||
* *value* is the default value. May be None if it has not been specified
|
||||
yet.
|
||||
* *type* may be float, int, bool, np.ndarray, ...
|
||||
* *constraint* may be None, single value, or (min, max)
|
||||
* None indicates that the value is not constrained--it may be
|
||||
automatically generated if the value is requested.
|
||||
* *allowed_constraints* is a string composed of (n)one, (f)ixed, and (r)ange.
|
||||
|
||||
Note: do not put mutable objects inside defaultState!
|
||||
|
||||
2) For each variable that may be automatically determined, a method must
|
||||
be defined with the name `_variableName`. This method may either return
|
||||
the
|
||||
"""
|
||||
|
||||
defaultState = OrderedDict()
|
||||
|
||||
def __init__(self):
|
||||
self.__dict__['_vars'] = OrderedDict()
|
||||
self.__dict__['_currentGets'] = set()
|
||||
self.reset()
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset all variables in the solver to their default state.
|
||||
"""
|
||||
self._currentGets.clear()
|
||||
for k in self.defaultState:
|
||||
self._vars[k] = self.defaultState[k][:]
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name in self._vars:
|
||||
return self.get(name)
|
||||
raise AttributeError(name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
"""
|
||||
Set the value of a state variable.
|
||||
If None is given for the value, then the constraint will also be set to None.
|
||||
If a tuple is given for a scalar variable, then the tuple is used as a range constraint instead of a value.
|
||||
Otherwise, the constraint is set to 'fixed'.
|
||||
|
||||
"""
|
||||
# First check this is a valid attribute
|
||||
if name in self._vars:
|
||||
if value is None:
|
||||
self.set(name, value, None)
|
||||
elif isinstance(value, tuple) and self._vars[name][1] is not np.ndarray:
|
||||
self.set(name, None, value)
|
||||
else:
|
||||
self.set(name, value, 'fixed')
|
||||
else:
|
||||
# also allow setting any other pre-existing attribute
|
||||
if hasattr(self, name):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
raise AttributeError(name)
|
||||
|
||||
def get(self, name):
|
||||
"""
|
||||
Return the value for parameter *name*.
|
||||
|
||||
If the value has not been specified, then attempt to compute it from
|
||||
other interacting parameters.
|
||||
|
||||
If no value can be determined, then raise RuntimeError.
|
||||
"""
|
||||
if name in self._currentGets:
|
||||
raise RuntimeError("Cyclic dependency while calculating '%s'." % name)
|
||||
self._currentGets.add(name)
|
||||
try:
|
||||
v = self._vars[name][0]
|
||||
if v is None:
|
||||
cfunc = getattr(self, '_' + name, None)
|
||||
if cfunc is None:
|
||||
v = None
|
||||
else:
|
||||
v = cfunc()
|
||||
if v is None:
|
||||
raise RuntimeError("Parameter '%s' is not specified." % name)
|
||||
v = self.set(name, v)
|
||||
finally:
|
||||
self._currentGets.remove(name)
|
||||
|
||||
return v
|
||||
|
||||
def set(self, name, value=None, constraint=True):
|
||||
"""
|
||||
Set a variable *name* to *value*. The actual set value is returned (in
|
||||
some cases, the value may be cast into another type).
|
||||
|
||||
If *value* is None, then the value is left to be determined in the
|
||||
future. At any time, the value may be re-assigned arbitrarily unless
|
||||
a constraint is given.
|
||||
|
||||
If *constraint* is True (the default), then supplying a value that
|
||||
violates a previously specified constraint will raise an exception.
|
||||
|
||||
If *constraint* is 'fixed', then the value is set (if provided) and
|
||||
the variable will not be updated automatically in the future.
|
||||
|
||||
If *constraint* is a tuple, then the value is constrained to be within the
|
||||
given (min, max). Either constraint may be None to disable
|
||||
it. In some cases, a constraint cannot be satisfied automatically,
|
||||
and the user will be forced to resolve the constraint manually.
|
||||
|
||||
If *constraint* is None, then any constraints are removed for the variable.
|
||||
"""
|
||||
var = self._vars[name]
|
||||
if constraint is None:
|
||||
if 'n' not in var[3]:
|
||||
raise TypeError("Empty constraints not allowed for '%s'" % name)
|
||||
var[2] = constraint
|
||||
elif constraint == 'fixed':
|
||||
if 'f' not in var[3]:
|
||||
raise TypeError("Fixed constraints not allowed for '%s'" % name)
|
||||
var[2] = constraint
|
||||
elif isinstance(constraint, tuple):
|
||||
if 'r' not in var[3]:
|
||||
raise TypeError("Range constraints not allowed for '%s'" % name)
|
||||
assert len(constraint) == 2
|
||||
var[2] = constraint
|
||||
elif constraint is not True:
|
||||
raise TypeError("constraint must be None, True, 'fixed', or tuple. (got %s)" % constraint)
|
||||
|
||||
# type checking / massaging
|
||||
if var[1] is np.ndarray:
|
||||
value = np.array(value, dtype=float)
|
||||
elif var[1] in (int, float, tuple) and value is not None:
|
||||
value = var[1](value)
|
||||
|
||||
# constraint checks
|
||||
if constraint is True and not self.check_constraint(name, value):
|
||||
raise ValueError("Setting %s = %s violates constraint %s" % (name, value, var[2]))
|
||||
|
||||
# invalidate other dependent values
|
||||
if var[0] is not None:
|
||||
# todo: we can make this more clever..(and might need to)
|
||||
# we just know that a value of None cannot have dependencies
|
||||
# (because if anyone else had asked for this value, it wouldn't be
|
||||
# None anymore)
|
||||
self.resetUnfixed()
|
||||
|
||||
var[0] = value
|
||||
return value
|
||||
|
||||
def check_constraint(self, name, value):
|
||||
c = self._vars[name][2]
|
||||
if c is None or value is None:
|
||||
return True
|
||||
if isinstance(c, tuple):
|
||||
return ((c[0] is None or c[0] <= value) and
|
||||
(c[1] is None or c[1] >= value))
|
||||
else:
|
||||
return value == c
|
||||
|
||||
def saveState(self):
|
||||
"""
|
||||
Return a serializable description of the solver's current state.
|
||||
"""
|
||||
state = OrderedDict()
|
||||
for name, var in self._vars.items():
|
||||
state[name] = (var[0], var[2])
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
"""
|
||||
Restore the state of all values and constraints in the solver.
|
||||
"""
|
||||
self.reset()
|
||||
for name, var in state.items():
|
||||
self.set(name, var[0], var[1])
|
||||
|
||||
def resetUnfixed(self):
|
||||
"""
|
||||
For any variable that does not have a fixed value, reset
|
||||
its value to None.
|
||||
"""
|
||||
for var in self._vars.values():
|
||||
if var[2] != 'fixed':
|
||||
var[0] = None
|
||||
|
||||
def solve(self):
|
||||
for k in self._vars:
|
||||
getattr(self, k)
|
||||
|
||||
def __repr__(self):
|
||||
state = OrderedDict()
|
||||
for name, var in self._vars.items():
|
||||
if var[2] == 'fixed':
|
||||
state[name] = var[0]
|
||||
state = ', '.join(["%s=%s" % (n, v) for n,v in state.items()])
|
||||
return "<%s %s>" % (self.__class__.__name__, state)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
class Camera(SystemSolver):
|
||||
"""
|
||||
Consider a simple SLR camera. The variables we will consider that
|
||||
affect the camera's behavior while acquiring a photo are aperture, shutter speed,
|
||||
ISO, and flash (of course there are many more, but let's keep the example simple).
|
||||
|
||||
In rare cases, the user wants to manually specify each of these variables and
|
||||
no more work needs to be done to take the photo. More often, the user wants to
|
||||
specify more interesting constraints like depth of field, overall exposure,
|
||||
or maximum allowed ISO value.
|
||||
|
||||
If we add a simple light meter measurement into this system and an 'exposure'
|
||||
variable that indicates the desired exposure (0 is "perfect", -1 is one stop
|
||||
darker, etc), then the system of equations governing the camera behavior would
|
||||
have the following variables:
|
||||
|
||||
aperture, shutter, iso, flash, exposure, light meter
|
||||
|
||||
The first four variables are the "outputs" of the system (they directly drive
|
||||
the camera), the last is a constant (the camera itself cannot affect the
|
||||
reading on the light meter), and 'exposure' specifies a desired relationship
|
||||
between other variables in the system.
|
||||
|
||||
So the question is: how can I formalize a system like this as a user interface?
|
||||
Typical cameras have a fairly limited approach: provide the user with a list
|
||||
of modes, each of which defines a particular set of constraints. For example:
|
||||
|
||||
manual: user provides aperture, shutter, iso, and flash
|
||||
aperture priority: user provides aperture and exposure, camera selects
|
||||
iso, shutter, and flash automatically
|
||||
shutter priority: user provides shutter and exposure, camera selects
|
||||
iso, aperture, and flash
|
||||
program: user specifies exposure, camera selects all other variables
|
||||
automatically
|
||||
action: camera selects all variables while attempting to maximize
|
||||
shutter speed
|
||||
portrait: camera selects all variables while attempting to minimize
|
||||
aperture
|
||||
|
||||
A more general approach might allow the user to provide more explicit
|
||||
constraints on each variable (for example: I want a shutter speed of 1/30 or
|
||||
slower, an ISO no greater than 400, an exposure between -1 and 1, and the
|
||||
smallest aperture possible given all other constraints) and have the camera
|
||||
solve the system of equations, with a warning if no solution is found. This
|
||||
is exactly what we will implement in this example class.
|
||||
"""
|
||||
|
||||
defaultState = OrderedDict([
|
||||
# Field stop aperture
|
||||
('aperture', [None, float, None, 'nf']),
|
||||
# Duration that shutter is held open.
|
||||
('shutter', [None, float, None, 'nf']),
|
||||
# ISO (sensitivity) value. 100, 200, 400, 800, 1600..
|
||||
('iso', [None, int, None, 'nf']),
|
||||
|
||||
# Flash is a value indicating the brightness of the flash. A table
|
||||
# is used to decide on "balanced" settings for each flash level:
|
||||
# 0: no flash
|
||||
# 1: s=1/60, a=2.0, iso=100
|
||||
# 2: s=1/60, a=4.0, iso=100 ..and so on..
|
||||
('flash', [None, float, None, 'nf']),
|
||||
|
||||
# exposure is a value indicating how many stops brighter (+1) or
|
||||
# darker (-1) the photographer would like the photo to appear from
|
||||
# the 'balanced' settings indicated by the light meter (see below).
|
||||
('exposure', [None, float, None, 'f']),
|
||||
|
||||
# Let's define this as an external light meter (not affected by
|
||||
# aperture) with logarithmic output. We arbitrarily choose the
|
||||
# following settings as "well balanced" for each light meter value:
|
||||
# -1: s=1/60, a=2.0, iso=100
|
||||
# 0: s=1/60, a=4.0, iso=100
|
||||
# 1: s=1/120, a=4.0, iso=100 ..and so on..
|
||||
# Note that the only allowed constraint mode is (f)ixed, since the
|
||||
# camera never _computes_ the light meter value, it only reads it.
|
||||
('lightMeter', [None, float, None, 'f']),
|
||||
|
||||
# Indicates the camera's final decision on how it thinks the photo will
|
||||
# look, given the chosen settings. This value is _only_ determined
|
||||
# automatically.
|
||||
('balance', [None, float, None, 'n']),
|
||||
])
|
||||
|
||||
def _aperture(self):
|
||||
"""
|
||||
Determine aperture automatically under a variety of conditions.
|
||||
"""
|
||||
iso = self.iso
|
||||
exp = self.exposure
|
||||
light = self.lightMeter
|
||||
|
||||
try:
|
||||
# shutter-priority mode
|
||||
sh = self.shutter # this raises RuntimeError if shutter has not
|
||||
# been specified
|
||||
ap = 4.0 * (sh / (1./60.)) * (iso / 100.) * (2 ** exp) * (2 ** light)
|
||||
ap = np.clip(ap, 2.0, 16.0)
|
||||
except RuntimeError:
|
||||
# program mode; we can select a suitable shutter
|
||||
# value at the same time.
|
||||
sh = (1./60.)
|
||||
raise
|
||||
|
||||
|
||||
|
||||
return ap
|
||||
|
||||
def _balance(self):
|
||||
iso = self.iso
|
||||
light = self.lightMeter
|
||||
sh = self.shutter
|
||||
ap = self.aperture
|
||||
fl = self.flash
|
||||
|
||||
bal = (4.0 / ap) * (sh / (1./60.)) * (iso / 100.) * (2 ** light)
|
||||
return np.log2(bal)
|
||||
|
||||
camera = Camera()
|
||||
|
||||
camera.iso = 100
|
||||
camera.exposure = 0
|
||||
camera.lightMeter = 2
|
||||
camera.shutter = 1./60.
|
||||
camera.flash = 0
|
||||
|
||||
camera.solve()
|
||||
print camera.saveState()
|
||||
|
@ -1,5 +1,5 @@
|
||||
from .Parameter import Parameter, registerParameterType
|
||||
from .ParameterTree import ParameterTree
|
||||
from .ParameterItem import ParameterItem
|
||||
|
||||
from .ParameterSystem import ParameterSystem, SystemSolver
|
||||
from . import parameterTypes as types
|
@ -78,6 +78,7 @@ class WidgetParameterItem(ParameterItem):
|
||||
## no starting value was given; use whatever the widget has
|
||||
self.widgetValueChanged()
|
||||
|
||||
self.updateDefaultBtn()
|
||||
|
||||
def makeWidget(self):
|
||||
"""
|
||||
@ -191,6 +192,9 @@ class WidgetParameterItem(ParameterItem):
|
||||
def updateDefaultBtn(self):
|
||||
## enable/disable default btn
|
||||
self.defaultBtn.setEnabled(not self.param.valueIsDefault() and self.param.writable())
|
||||
|
||||
# hide / show
|
||||
self.defaultBtn.setVisible(not self.param.readonly())
|
||||
|
||||
def updateDisplayLabel(self, value=None):
|
||||
"""Update the display label to reflect the value of the parameter."""
|
||||
@ -234,6 +238,8 @@ class WidgetParameterItem(ParameterItem):
|
||||
self.widget.show()
|
||||
self.displayLabel.hide()
|
||||
self.widget.setFocus(QtCore.Qt.OtherFocusReason)
|
||||
if isinstance(self.widget, SpinBox):
|
||||
self.widget.selectNumber() # select the numerical portion of the text for quick editing
|
||||
|
||||
def hideEditor(self):
|
||||
self.widget.hide()
|
||||
@ -277,7 +283,7 @@ class WidgetParameterItem(ParameterItem):
|
||||
if 'readonly' in opts:
|
||||
self.updateDefaultBtn()
|
||||
if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)):
|
||||
w.setEnabled(not opts['readonly'])
|
||||
self.widget.setEnabled(not opts['readonly'])
|
||||
|
||||
## If widget is a SpinBox, pass options straight through
|
||||
if isinstance(self.widget, SpinBox):
|
||||
@ -315,8 +321,8 @@ class SimpleParameter(Parameter):
|
||||
def colorValue(self):
|
||||
return fn.mkColor(Parameter.value(self))
|
||||
|
||||
def saveColorState(self):
|
||||
state = Parameter.saveState(self)
|
||||
def saveColorState(self, *args, **kwds):
|
||||
state = Parameter.saveState(self, *args, **kwds)
|
||||
state['value'] = fn.colorTuple(self.value())
|
||||
return state
|
||||
|
||||
@ -539,7 +545,6 @@ class ListParameter(Parameter):
|
||||
self.forward, self.reverse = self.mapping(limits)
|
||||
|
||||
Parameter.setLimits(self, limits)
|
||||
#print self.name(), self.value(), limits, self.reverse
|
||||
if len(self.reverse[0]) > 0 and self.value() not in self.reverse[0]:
|
||||
self.setValue(self.reverse[0][0])
|
||||
|
||||
|
@ -61,6 +61,19 @@ def test_interpolateArray():
|
||||
|
||||
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))
|
||||
c = np.array([[[111,112,113], [121,122,123]], [[211,212,213], [221,222,223]]])
|
||||
assert np.all(b == c)
|
||||
|
||||
# operate over first axis; broadcast over the rest
|
||||
aa = np.vstack([a, a/100.]).T
|
||||
cc = np.empty(c.shape + (2,))
|
||||
cc[..., 0] = c
|
||||
cc[..., 1] = c / 100.
|
||||
bb = pg.subArray(aa, offset=2, shape=(2,2,3), stride=(10,4,1))
|
||||
assert np.all(bb == cc)
|
||||
|
||||
|
||||
|
||||
|
50
pyqtgraph/util/garbage_collector.py
Normal file
50
pyqtgraph/util/garbage_collector.py
Normal file
@ -0,0 +1,50 @@
|
||||
import gc
|
||||
|
||||
from ..Qt import QtCore
|
||||
|
||||
class GarbageCollector(object):
|
||||
'''
|
||||
Disable automatic garbage collection and instead collect manually
|
||||
on a timer.
|
||||
|
||||
This is done to ensure that garbage collection only happens in the GUI
|
||||
thread, as otherwise Qt can crash.
|
||||
|
||||
Credit: Erik Janssens
|
||||
Source: http://pydev.blogspot.com/2014/03/should-python-garbage-collector-be.html
|
||||
'''
|
||||
|
||||
def __init__(self, interval=1.0, debug=False):
|
||||
self.debug = debug
|
||||
if debug:
|
||||
gc.set_debug(gc.DEBUG_LEAK)
|
||||
|
||||
self.timer = QtCore.QTimer()
|
||||
self.timer.timeout.connect(self.check)
|
||||
|
||||
self.threshold = gc.get_threshold()
|
||||
gc.disable()
|
||||
self.timer.start(interval * 1000)
|
||||
|
||||
def check(self):
|
||||
#return self.debug_cycles() # uncomment to just debug cycles
|
||||
l0, l1, l2 = gc.get_count()
|
||||
if self.debug:
|
||||
print('gc_check called:', l0, l1, l2)
|
||||
if l0 > self.threshold[0]:
|
||||
num = gc.collect(0)
|
||||
if self.debug:
|
||||
print('collecting gen 0, found: %d unreachable' % num)
|
||||
if l1 > self.threshold[1]:
|
||||
num = gc.collect(1)
|
||||
if self.debug:
|
||||
print('collecting gen 1, found: %d unreachable' % num)
|
||||
if l2 > self.threshold[2]:
|
||||
num = gc.collect(2)
|
||||
if self.debug:
|
||||
print('collecting gen 2, found: %d unreachable' % num)
|
||||
|
||||
def debug_cycles(self):
|
||||
gc.collect()
|
||||
for obj in gc.garbage:
|
||||
print (obj, repr(obj), type(obj))
|
@ -19,8 +19,8 @@ class ColorMapWidget(ptree.ParameterTree):
|
||||
"""
|
||||
sigColorMapChanged = QtCore.Signal(object)
|
||||
|
||||
def __init__(self):
|
||||
ptree.ParameterTree.__init__(self, showHeader=False)
|
||||
def __init__(self, parent=None):
|
||||
ptree.ParameterTree.__init__(self, parent=parent, showHeader=False)
|
||||
|
||||
self.params = ColorMapParameter()
|
||||
self.setParameters(self.params)
|
||||
@ -32,6 +32,15 @@ class ColorMapWidget(ptree.ParameterTree):
|
||||
|
||||
def mapChanged(self):
|
||||
self.sigColorMapChanged.emit(self)
|
||||
|
||||
def widgetGroupInterface(self):
|
||||
return (self.sigColorMapChanged, self.saveState, self.restoreState)
|
||||
|
||||
def saveState(self):
|
||||
return self.params.saveState()
|
||||
|
||||
def restoreState(self, state):
|
||||
self.params.restoreState(state)
|
||||
|
||||
|
||||
class ColorMapParameter(ptree.types.GroupParameter):
|
||||
@ -48,9 +57,11 @@ class ColorMapParameter(ptree.types.GroupParameter):
|
||||
def addNew(self, name):
|
||||
mode = self.fields[name].get('mode', 'range')
|
||||
if mode == 'range':
|
||||
self.addChild(RangeColorMapItem(name, self.fields[name]))
|
||||
item = RangeColorMapItem(name, self.fields[name])
|
||||
elif mode == 'enum':
|
||||
self.addChild(EnumColorMapItem(name, self.fields[name]))
|
||||
item = EnumColorMapItem(name, self.fields[name])
|
||||
self.addChild(item)
|
||||
return item
|
||||
|
||||
def fieldNames(self):
|
||||
return self.fields.keys()
|
||||
@ -95,6 +106,9 @@ class ColorMapParameter(ptree.types.GroupParameter):
|
||||
returned as 0.0-1.0 float values.
|
||||
============== =================================================================
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
data = np.array([tuple(data.values())], dtype=[(k, float) for k in data.keys()])
|
||||
|
||||
colors = np.zeros((len(data),4))
|
||||
for item in self.children():
|
||||
if not item['Enabled']:
|
||||
@ -126,8 +140,26 @@ class ColorMapParameter(ptree.types.GroupParameter):
|
||||
|
||||
return colors
|
||||
|
||||
def saveState(self):
|
||||
items = OrderedDict()
|
||||
for item in self:
|
||||
itemState = item.saveState(filter='user')
|
||||
itemState['field'] = item.fieldName
|
||||
items[item.name()] = itemState
|
||||
state = {'fields': self.fields, 'items': items}
|
||||
return state
|
||||
|
||||
def restoreState(self, state):
|
||||
if 'fields' in state:
|
||||
self.setFields(state['fields'])
|
||||
for itemState in state['items']:
|
||||
item = self.addNew(itemState['field'])
|
||||
item.restoreState(itemState)
|
||||
|
||||
|
||||
class RangeColorMapItem(ptree.types.SimpleParameter):
|
||||
mapType = 'range'
|
||||
|
||||
def __init__(self, name, opts):
|
||||
self.fieldName = name
|
||||
units = opts.get('units', '')
|
||||
@ -151,8 +183,6 @@ class RangeColorMapItem(ptree.types.SimpleParameter):
|
||||
def map(self, data):
|
||||
data = data[self.fieldName]
|
||||
|
||||
|
||||
|
||||
scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1)
|
||||
cmap = self.value()
|
||||
colors = cmap.map(scaled, mode='float')
|
||||
@ -162,10 +192,11 @@ class RangeColorMapItem(ptree.types.SimpleParameter):
|
||||
nanColor = (nanColor.red()/255., nanColor.green()/255., nanColor.blue()/255., nanColor.alpha()/255.)
|
||||
colors[mask] = nanColor
|
||||
|
||||
return colors
|
||||
|
||||
return colors
|
||||
|
||||
class EnumColorMapItem(ptree.types.GroupParameter):
|
||||
mapType = 'enum'
|
||||
|
||||
def __init__(self, name, opts):
|
||||
self.fieldName = name
|
||||
vals = opts.get('values', [])
|
||||
|
@ -1,5 +1,6 @@
|
||||
from ..Qt import QtGui, QtCore
|
||||
from ..SignalProxy import SignalProxy
|
||||
import sys
|
||||
from ..pgcollections import OrderedDict
|
||||
from ..python2_3 import asUnicode
|
||||
|
||||
@ -20,6 +21,10 @@ class ComboBox(QtGui.QComboBox):
|
||||
self.currentIndexChanged.connect(self.indexChanged)
|
||||
self._ignoreIndexChange = False
|
||||
|
||||
#self.value = default
|
||||
if 'darwin' in sys.platform: ## because MacOSX can show names that are wider than the comboBox
|
||||
self.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToMinimumContentsLength)
|
||||
#self.setMinimumContentsLength(10)
|
||||
self._chosenText = None
|
||||
self._items = OrderedDict()
|
||||
|
||||
|
@ -57,7 +57,7 @@ class DataTreeWidget(QtGui.QTreeWidget):
|
||||
}
|
||||
|
||||
if isinstance(data, dict):
|
||||
for k in data:
|
||||
for k in data.keys():
|
||||
self.buildTree(data[k], node, str(k))
|
||||
elif isinstance(data, list) or isinstance(data, tuple):
|
||||
for i in range(len(data)):
|
||||
|
@ -47,29 +47,29 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
"""
|
||||
============== ========================================================================
|
||||
**Arguments:**
|
||||
parent Sets the parent widget for this SpinBox (optional)
|
||||
value (float/int) initial value
|
||||
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.
|
||||
suffix (str) suffix (units) to display after the numerical value
|
||||
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).
|
||||
"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.
|
||||
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.
|
||||
'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
|
||||
decimals (int) Number of decimal values to display
|
||||
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.
|
||||
============== ========================================================================
|
||||
"""
|
||||
QtGui.QAbstractSpinBox.__init__(self, parent)
|
||||
@ -233,6 +233,18 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
|
||||
def setDecimals(self, decimals):
|
||||
self.setOpts(decimals=decimals)
|
||||
|
||||
def selectNumber(self):
|
||||
"""
|
||||
Select the numerical portion of the text to allow quick editing by the user.
|
||||
"""
|
||||
le = self.lineEdit()
|
||||
text = le.text()
|
||||
try:
|
||||
index = text.index(' ')
|
||||
except ValueError:
|
||||
return
|
||||
le.setSelection(0, index)
|
||||
|
||||
def value(self):
|
||||
"""
|
||||
|
@ -365,7 +365,7 @@ class TableWidget(QtGui.QTableWidget):
|
||||
ev.ignore()
|
||||
|
||||
def handleItemChanged(self, item):
|
||||
item.textChanged()
|
||||
item.itemChanged()
|
||||
|
||||
|
||||
class TableWidgetItem(QtGui.QTableWidgetItem):
|
||||
@ -425,7 +425,8 @@ class TableWidgetItem(QtGui.QTableWidgetItem):
|
||||
def _updateText(self):
|
||||
self._blockValueChange = True
|
||||
try:
|
||||
self.setText(self.format())
|
||||
self._text = self.format()
|
||||
self.setText(self._text)
|
||||
finally:
|
||||
self._blockValueChange = False
|
||||
|
||||
@ -433,14 +434,22 @@ class TableWidgetItem(QtGui.QTableWidgetItem):
|
||||
self.value = value
|
||||
self._updateText()
|
||||
|
||||
def itemChanged(self):
|
||||
"""Called when the data of this item has changed."""
|
||||
if self.text() != self._text:
|
||||
self.textChanged()
|
||||
|
||||
def textChanged(self):
|
||||
"""Called when this item's text has changed for any reason."""
|
||||
self._text = self.text()
|
||||
|
||||
if self._blockValueChange:
|
||||
# text change was result of value or format change; do not
|
||||
# propagate.
|
||||
return
|
||||
|
||||
try:
|
||||
|
||||
self.value = type(self.value)(self.text())
|
||||
except ValueError:
|
||||
self.value = str(self.text())
|
||||
|
Loading…
Reference in New Issue
Block a user