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:
Luke Campagnola 2014-08-07 09:03:26 -04:00
parent 55a07b0bec
commit 753ac9b4c4
51 changed files with 1913 additions and 541 deletions

View File

@ -81,5 +81,7 @@ class Vector(QtGui.QVector3D):
# ang *= -1. # ang *= -1.
return ang * 180. / np.pi return ang * 180. / np.pi
def __abs__(self):
return Vector(abs(self.x()), abs(self.y()), abs(self.z()))

View File

@ -325,8 +325,13 @@ def exit():
atexit._run_exitfuncs() atexit._run_exitfuncs()
## close file handles ## 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) os._exit(0)

View File

@ -67,8 +67,8 @@ class Canvas(QtGui.QWidget):
self.ui.itemList.sigItemMoved.connect(self.treeItemMoved) self.ui.itemList.sigItemMoved.connect(self.treeItemMoved)
self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected) self.ui.itemList.itemSelectionChanged.connect(self.treeItemSelected)
self.ui.autoRangeBtn.clicked.connect(self.autoRange) self.ui.autoRangeBtn.clicked.connect(self.autoRange)
self.ui.storeSvgBtn.clicked.connect(self.storeSvg) #self.ui.storeSvgBtn.clicked.connect(self.storeSvg)
self.ui.storePngBtn.clicked.connect(self.storePng) #self.ui.storePngBtn.clicked.connect(self.storePng)
self.ui.redirectCheck.toggled.connect(self.updateRedirect) self.ui.redirectCheck.toggled.connect(self.updateRedirect)
self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect) self.ui.redirectCombo.currentIndexChanged.connect(self.updateRedirect)
self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged) self.multiSelectBox.sigRegionChanged.connect(self.multiSelectBoxChanged)
@ -94,11 +94,13 @@ class Canvas(QtGui.QWidget):
self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent
def storeSvg(self): #def storeSvg(self):
self.ui.view.writeSvg() #from pyqtgraph.GraphicsScene.exportDialog import ExportDialog
#ex = ExportDialog(self.ui.view)
#ex.show()
def storePng(self): #def storePng(self):
self.ui.view.writeImage() #self.ui.view.writeImage()
def splitterMoved(self): def splitterMoved(self):
self.resizeEvent() self.resizeEvent()
@ -571,7 +573,9 @@ class Canvas(QtGui.QWidget):
self.menu.popup(ev.globalPos()) self.menu.popup(ev.globalPos())
def removeClicked(self): def removeClicked(self):
self.removeItem(self.menuItem) #self.removeItem(self.menuItem)
for item in self.selectedItems():
self.removeItem(item)
self.menuItem = None self.menuItem = None
import gc import gc
gc.collect() gc.collect()

View File

@ -28,21 +28,7 @@
<widget class="GraphicsView" name="view"/> <widget class="GraphicsView" name="view"/>
<widget class="QWidget" name="layoutWidget"> <widget class="QWidget" name="layoutWidget">
<layout class="QGridLayout" name="gridLayout_2"> <layout class="QGridLayout" name="gridLayout_2">
<item row="1" column="0"> <item row="2" column="0" colspan="2">
<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">
<widget class="QPushButton" name="autoRangeBtn"> <widget class="QPushButton" name="autoRangeBtn">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
@ -55,7 +41,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="6" column="0" colspan="2"> <item row="5" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout"> <layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>0</number>
@ -75,7 +61,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="7" column="0" colspan="2"> <item row="6" column="0" colspan="2">
<widget class="TreeWidget" name="itemList"> <widget class="TreeWidget" name="itemList">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding"> <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
@ -93,28 +79,28 @@
</column> </column>
</widget> </widget>
</item> </item>
<item row="11" column="0" colspan="2"> <item row="10" column="0" colspan="2">
<layout class="QGridLayout" name="ctrlLayout"> <layout class="QGridLayout" name="ctrlLayout">
<property name="spacing"> <property name="spacing">
<number>0</number> <number>0</number>
</property> </property>
</layout> </layout>
</item> </item>
<item row="8" column="0"> <item row="7" column="0">
<widget class="QPushButton" name="resetTransformsBtn"> <widget class="QPushButton" name="resetTransformsBtn">
<property name="text"> <property name="text">
<string>Reset Transforms</string> <string>Reset Transforms</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="3" column="0">
<widget class="QPushButton" name="mirrorSelectionBtn"> <widget class="QPushButton" name="mirrorSelectionBtn">
<property name="text"> <property name="text">
<string>Mirror Selection</string> <string>Mirror Selection</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="3" column="1">
<widget class="QPushButton" name="reflectSelectionBtn"> <widget class="QPushButton" name="reflectSelectionBtn">
<property name="text"> <property name="text">
<string>MirrorXY</string> <string>MirrorXY</string>

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- 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 # Created: Thu Jan 2 11:13:07 2014
# by: PyQt4 UI code generator 4.10 # by: PyQt4 UI code generator 4.9
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -12,16 +12,7 @@ from PyQt4 import QtCore, QtGui
try: try:
_fromUtf8 = QtCore.QString.fromUtf8 _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError: except AttributeError:
def _fromUtf8(s): _fromUtf8 = lambda s: s
return s
try:
_encoding = QtGui.QApplication.UnicodeUTF8
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig, _encoding)
except AttributeError:
def _translate(context, text, disambig):
return QtGui.QApplication.translate(context, text, disambig)
class Ui_Form(object): class Ui_Form(object):
def setupUi(self, Form): def setupUi(self, Form):
@ -41,12 +32,6 @@ class Ui_Form(object):
self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget)
self.gridLayout_2.setMargin(0) self.gridLayout_2.setMargin(0)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) 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) self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
@ -54,7 +39,7 @@ class Ui_Form(object):
sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth())
self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setSizePolicy(sizePolicy)
self.autoRangeBtn.setObjectName(_fromUtf8("autoRangeBtn")) 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 = QtGui.QHBoxLayout()
self.horizontalLayout.setSpacing(0) self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout")) self.horizontalLayout.setObjectName(_fromUtf8("horizontalLayout"))
@ -64,7 +49,7 @@ class Ui_Form(object):
self.redirectCombo = CanvasCombo(self.layoutWidget) self.redirectCombo = CanvasCombo(self.layoutWidget)
self.redirectCombo.setObjectName(_fromUtf8("redirectCombo")) self.redirectCombo.setObjectName(_fromUtf8("redirectCombo"))
self.horizontalLayout.addWidget(self.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) self.itemList = TreeWidget(self.layoutWidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
@ -74,35 +59,33 @@ class Ui_Form(object):
self.itemList.setHeaderHidden(True) self.itemList.setHeaderHidden(True)
self.itemList.setObjectName(_fromUtf8("itemList")) self.itemList.setObjectName(_fromUtf8("itemList"))
self.itemList.headerItem().setText(0, _fromUtf8("1")) 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 = QtGui.QGridLayout()
self.ctrlLayout.setSpacing(0) self.ctrlLayout.setSpacing(0)
self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout")) 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 = QtGui.QPushButton(self.layoutWidget)
self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn")) 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 = QtGui.QPushButton(self.layoutWidget)
self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn")) 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 = QtGui.QPushButton(self.layoutWidget)
self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn")) 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.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
self.retranslateUi(Form) self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form) QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form): def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None)) Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.storeSvgBtn.setText(_translate("Form", "Store SVG", None)) self.autoRangeBtn.setText(QtGui.QApplication.translate("Form", "Auto Range", None, QtGui.QApplication.UnicodeUTF8))
self.storePngBtn.setText(_translate("Form", "Store PNG", None)) self.redirectCheck.setToolTip(QtGui.QApplication.translate("Form", "Check to display all local items in a remote canvas.", None, QtGui.QApplication.UnicodeUTF8))
self.autoRangeBtn.setText(_translate("Form", "Auto Range", None)) self.redirectCheck.setText(QtGui.QApplication.translate("Form", "Redirect", None, QtGui.QApplication.UnicodeUTF8))
self.redirectCheck.setToolTip(_translate("Form", "Check to display all local items in a remote canvas.", None)) self.resetTransformsBtn.setText(QtGui.QApplication.translate("Form", "Reset Transforms", None, QtGui.QApplication.UnicodeUTF8))
self.redirectCheck.setText(_translate("Form", "Redirect", None)) self.mirrorSelectionBtn.setText(QtGui.QApplication.translate("Form", "Mirror Selection", None, QtGui.QApplication.UnicodeUTF8))
self.resetTransformsBtn.setText(_translate("Form", "Reset Transforms", None)) self.reflectSelectionBtn.setText(QtGui.QApplication.translate("Form", "MirrorXY", None, QtGui.QApplication.UnicodeUTF8))
self.mirrorSelectionBtn.setText(_translate("Form", "Mirror Selection", None))
self.reflectSelectionBtn.setText(_translate("Form", "MirrorXY", None))
from ..widgets.TreeWidget import TreeWidget from ..widgets.TreeWidget import TreeWidget
from CanvasManager import CanvasCombo from CanvasManager import CanvasCombo

View File

@ -244,4 +244,7 @@ class ColorMap(object):
else: else:
return np.all(self.color == np.array([[0,0,0,255], [255,255,255,255]])) 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)

View File

@ -14,6 +14,10 @@ from .pgcollections import OrderedDict
GLOBAL_PATH = None # so not thread safe. GLOBAL_PATH = None # so not thread safe.
from . import units from . import units
from .python2_3 import asUnicode from .python2_3 import asUnicode
from .Qt import QtCore
from .Point import Point
from .colormap import ColorMap
import numpy
class ParseError(Exception): class ParseError(Exception):
def __init__(self, message, lineNum, line, fileName=None): def __init__(self, message, lineNum, line, fileName=None):
@ -46,7 +50,7 @@ def readConfigFile(fname):
fname2 = os.path.join(GLOBAL_PATH, fname) fname2 = os.path.join(GLOBAL_PATH, fname)
if os.path.exists(fname2): if os.path.exists(fname2):
fname = fname2 fname = fname2
GLOBAL_PATH = os.path.dirname(os.path.abspath(fname)) GLOBAL_PATH = os.path.dirname(os.path.abspath(fname))
try: try:
@ -135,6 +139,17 @@ def parseString(lines, start=0):
local = units.allUnits.copy() local = units.allUnits.copy()
local['OrderedDict'] = OrderedDict local['OrderedDict'] = OrderedDict
local['readConfigFile'] = readConfigFile 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: if len(k) < 1:
raise ParseError('Missing name preceding colon', ln+1, l) 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. if k[0] == '(' and k[-1] == ')': ## If the key looks like a tuple, try evaluating it.

View File

@ -341,6 +341,17 @@ class ConsoleWidget(QtGui.QWidget):
filename = tb.tb_frame.f_code.co_filename filename = tb.tb_frame.f_code.co_filename
function = tb.tb_frame.f_code.co_name 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: ## Go through a list of common exception points we like to ignore:
if excType is GeneratorExit or excType is StopIteration: if excType is GeneratorExit or excType is StopIteration:
return False return False

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>710</width> <width>694</width>
<height>497</height> <height>497</height>
</rect> </rect>
</property> </property>
@ -89,6 +89,16 @@
<property name="spacing"> <property name="spacing">
<number>0</number> <number>0</number>
</property> </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"> <item row="0" column="1">
<widget class="QPushButton" name="catchAllExceptionsBtn"> <widget class="QPushButton" name="catchAllExceptionsBtn">
<property name="text"> <property name="text">
@ -109,7 +119,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="2"> <item row="0" column="4">
<widget class="QCheckBox" name="onlyUncaughtCheck"> <widget class="QCheckBox" name="onlyUncaughtCheck">
<property name="text"> <property name="text">
<string>Only Uncaught Exceptions</string> <string>Only Uncaught Exceptions</string>
@ -119,14 +129,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0" colspan="5"> <item row="2" column="0" colspan="7">
<widget class="QListWidget" name="exceptionStackList"> <widget class="QListWidget" name="exceptionStackList">
<property name="alternatingRowColors"> <property name="alternatingRowColors">
<bool>true</bool> <bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0" colspan="5"> <item row="3" column="0" colspan="7">
<widget class="QCheckBox" name="runSelectedFrameCheck"> <widget class="QCheckBox" name="runSelectedFrameCheck">
<property name="text"> <property name="text">
<string>Run commands in selected stack frame</string> <string>Run commands in selected stack frame</string>
@ -136,24 +146,14 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0" colspan="5"> <item row="1" column="0" colspan="7">
<widget class="QLabel" name="exceptionInfoLabel"> <widget class="QLabel" name="exceptionInfoLabel">
<property name="text"> <property name="text">
<string>Exception Info</string> <string>Exception Info</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="0" column="4"> <item row="0" column="5">
<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">
<spacer name="horizontalSpacer"> <spacer name="horizontalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -166,6 +166,16 @@
</property> </property>
</spacer> </spacer>
</item> </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> </layout>
</widget> </widget>
</widget> </widget>

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- 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 # Created: Fri May 02 18:55:28 2014
# by: PyQt4 UI code generator 4.10 # by: PyQt4 UI code generator 4.10.4
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -26,7 +26,7 @@ except AttributeError:
class Ui_Form(object): class Ui_Form(object):
def setupUi(self, Form): def setupUi(self, Form):
Form.setObjectName(_fromUtf8("Form")) Form.setObjectName(_fromUtf8("Form"))
Form.resize(710, 497) Form.resize(694, 497)
self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout = QtGui.QGridLayout(Form)
self.gridLayout.setMargin(0) self.gridLayout.setMargin(0)
self.gridLayout.setSpacing(0) self.gridLayout.setSpacing(0)
@ -71,6 +71,10 @@ class Ui_Form(object):
self.gridLayout_2.setSpacing(0) self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) 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 = QtGui.QPushButton(self.exceptionGroup)
self.catchAllExceptionsBtn.setCheckable(True) self.catchAllExceptionsBtn.setCheckable(True)
self.catchAllExceptionsBtn.setObjectName(_fromUtf8("catchAllExceptionsBtn")) self.catchAllExceptionsBtn.setObjectName(_fromUtf8("catchAllExceptionsBtn"))
@ -82,24 +86,26 @@ class Ui_Form(object):
self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup) self.onlyUncaughtCheck = QtGui.QCheckBox(self.exceptionGroup)
self.onlyUncaughtCheck.setChecked(True) self.onlyUncaughtCheck.setChecked(True)
self.onlyUncaughtCheck.setObjectName(_fromUtf8("onlyUncaughtCheck")) 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 = QtGui.QListWidget(self.exceptionGroup)
self.exceptionStackList.setAlternatingRowColors(True) self.exceptionStackList.setAlternatingRowColors(True)
self.exceptionStackList.setObjectName(_fromUtf8("exceptionStackList")) 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 = QtGui.QCheckBox(self.exceptionGroup)
self.runSelectedFrameCheck.setChecked(True) self.runSelectedFrameCheck.setChecked(True)
self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck")) 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 = QtGui.QLabel(self.exceptionGroup)
self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel")) self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel"))
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
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)
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) 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.gridLayout.addWidget(self.splitter, 0, 0, 1, 1)
self.retranslateUi(Form) self.retranslateUi(Form)
@ -110,11 +116,12 @@ class Ui_Form(object):
self.historyBtn.setText(_translate("Form", "History..", None)) self.historyBtn.setText(_translate("Form", "History..", None))
self.exceptionBtn.setText(_translate("Form", "Exceptions..", None)) self.exceptionBtn.setText(_translate("Form", "Exceptions..", None))
self.exceptionGroup.setTitle(_translate("Form", "Exception Handling", 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.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions", None))
self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None)) self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception", None))
self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None)) self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions", None))
self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None)) self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame", None))
self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None)) self.exceptionInfoLabel.setText(_translate("Form", "Exception Info", None))
self.clearExceptionBtn.setText(_translate("Form", "Clear Exception", None)) self.label.setText(_translate("Form", "Filter (regex):", None))
from .CmdInput import CmdInput from .CmdInput import CmdInput

View File

@ -32,6 +32,57 @@ def ftrace(func):
return rv return rv
return w 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): def warnOnException(func):
"""Decorator which catches/ignores exceptions and prints a stack trace.""" """Decorator which catches/ignores exceptions and prints a stack trace."""
def w(*args, **kwds): def w(*args, **kwds):
@ -41,17 +92,22 @@ def warnOnException(func):
printExc('Ignored exception:') printExc('Ignored exception:')
return w return w
def getExc(indent=4, prefix='| '): def getExc(indent=4, prefix='| ', skip=1):
tb = traceback.format_exc() lines = (traceback.format_stack()[:-skip]
lines = [] + [" ---- exception caught ---->\n"]
for l in tb.split('\n'): + traceback.format_tb(sys.exc_info()[2])
lines.append(" "*indent + prefix + l) + traceback.format_exception_only(*sys.exc_info()[:2]))
return '\n'.join(lines) 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='|'): def printExc(msg='', indent=4, prefix='|'):
"""Print an error message followed by an indented exception backtrace """Print an error message followed by an indented exception backtrace
(This function is intended to be called within except: blocks)""" (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("[%s] %s\n" % (time.strftime("%H:%M:%S"), msg))
print(" "*indent + prefix + '='*30 + '>>') print(" "*indent + prefix + '='*30 + '>>')
print(exc) print(exc)
@ -407,6 +463,7 @@ class Profiler(object):
_depth = 0 _depth = 0
_msgs = [] _msgs = []
disable = False # set this flag to disable all or individual profilers at runtime
class DisabledProfiler(object): class DisabledProfiler(object):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
@ -418,12 +475,11 @@ class Profiler(object):
def mark(self, msg=None): def mark(self, msg=None):
pass pass
_disabledProfiler = DisabledProfiler() _disabledProfiler = DisabledProfiler()
def __new__(cls, msg=None, disabled='env', delayed=True): def __new__(cls, msg=None, disabled='env', delayed=True):
"""Optionally create a new profiler based on caller's qualname. """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 return cls._disabledProfiler
# determine the qualified name of the caller function # determine the qualified name of the caller function
@ -431,11 +487,11 @@ class Profiler(object):
try: try:
caller_object_type = type(caller_frame.f_locals["self"]) caller_object_type = type(caller_frame.f_locals["self"])
except KeyError: # we are in a regular function 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 else: # we are in a method
qualifier = caller_object_type.__name__ qualifier = caller_object_type.__name__
func_qualname = qualifier + "." + caller_frame.f_code.co_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 return cls._disabledProfiler
# create an actual profiling object # create an actual profiling object
cls._depth += 1 cls._depth += 1
@ -447,13 +503,12 @@ class Profiler(object):
obj._firstTime = obj._lastTime = ptime.time() obj._firstTime = obj._lastTime = ptime.time()
obj._newMsg("> Entering " + obj._name) obj._newMsg("> Entering " + obj._name)
return obj return obj
#else:
#def __new__(cls, delayed=True):
#return lambda msg=None: None
def __call__(self, msg=None): def __call__(self, msg=None):
"""Register or print a new message with timing information. """Register or print a new message with timing information.
""" """
if self.disable:
return
if msg is None: if msg is None:
msg = str(self._markCount) msg = str(self._markCount)
self._markCount += 1 self._markCount += 1
@ -479,7 +534,7 @@ class Profiler(object):
def finish(self, msg=None): def finish(self, msg=None):
"""Add a final message; flush the message list if no parent profiler. """Add a final message; flush the message list if no parent profiler.
""" """
if self._finished: if self._finished or self.disable:
return return
self._finished = True self._finished = True
if msg is not None: if msg is not None:
@ -984,6 +1039,7 @@ def qObjectReport(verbose=False):
class PrintDetector(object): class PrintDetector(object):
"""Find code locations that print to stdout."""
def __init__(self): def __init__(self):
self.stdout = sys.stdout self.stdout = sys.stdout
sys.stdout = self sys.stdout = self
@ -1002,6 +1058,45 @@ class PrintDetector(object):
self.stdout.flush() 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): class PeriodicTrace(object):
""" """
Used to debug freezing by starting a new thread that reports on the Used to debug freezing by starting a new thread that reports on the

View File

@ -49,29 +49,45 @@ def setTracebackClearing(clear=True):
class ExceptionHandler(object): class ExceptionHandler(object):
def __call__(self, *args): def __call__(self, *args):
## call original exception handler first (prints exception) ## Start by extending recursion depth just a bit.
global original_excepthook, callbacks, clear_tracebacks ## If the error we are catching is due to recursion, we don't want to generate another one here.
print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time())))) recursionLimit = sys.getrecursionlimit()
ret = original_excepthook(*args) try:
sys.setrecursionlimit(recursionLimit+100)
for cb in callbacks:
## call original exception handler first (prints exception)
global original_excepthook, callbacks, clear_tracebacks
try: try:
cb(*args) print("===== %s =====" % str(time.strftime("%Y.%m.%d %H:%m:%S", time.localtime(time.time()))))
except: except Exception:
print(" --------------------------------------------------------------") sys.stderr.write("Warning: stdout is broken! Falling back to stderr.\n")
print(" Error occurred during exception callback %s" % str(cb)) sys.stdout = sys.stderr
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
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): def implements(self, interface=None):
## this just makes it easy for us to detect whether an ExceptionHook is already installed. ## this just makes it easy for us to detect whether an ExceptionHook is already installed.
if interface is None: if interface is None:

View File

@ -14,6 +14,7 @@ class CSVExporter(Exporter):
self.params = Parameter(name='params', type='group', children=[ self.params = Parameter(name='params', type='group', children=[
{'name': 'separator', 'type': 'list', 'value': 'comma', 'values': ['comma', 'tab']}, {'name': 'separator', 'type': 'list', 'value': 'comma', 'values': ['comma', 'tab']},
{'name': 'precision', 'type': 'int', 'value': 10, 'limits': [0, None]}, {'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): def parameters(self):
@ -31,15 +32,24 @@ class CSVExporter(Exporter):
fd = open(fileName, 'w') fd = open(fileName, 'w')
data = [] data = []
header = [] 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() cd = c.getData()
if cd[0] is None: if cd[0] is None:
continue continue
data.append(cd) data.append(cd)
name = ''
if hasattr(c, 'implements') and c.implements('plotData') and c.name() is not None: if hasattr(c, 'implements') and c.implements('plotData') and c.name() is not None:
name = c.name().replace('"', '""') + '_' 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': if self.params['separator'] == 'comma':
sep = ',' sep = ','
@ -51,12 +61,20 @@ class CSVExporter(Exporter):
numFormat = '%%0.%dg' % self.params['precision'] numFormat = '%%0.%dg' % self.params['precision']
numRows = max([len(d[0]) for d in data]) numRows = max([len(d[0]) for d in data])
for i in range(numRows): for i in range(numRows):
for d in data: for j, d in enumerate(data):
for j in [0, 1]: # write x value if this is the first column, or if we want x
if i < len(d[j]): # for all rows
fd.write(numFormat % d[j][i] + sep) if appendAllX or j == 0:
if d is not None and i < len(d[0]):
fd.write(numFormat % d[0][i] + sep)
else: else:
fd.write(' %s' % sep) 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.write('\n')
fd.close() fd.close()

View 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()

View File

@ -4,7 +4,29 @@ from .. import PlotItem
from .. import functions as fn from .. import functions as fn
__all__ = ['MatplotlibExporter'] __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): class MatplotlibExporter(Exporter):
Name = "Matplotlib Window" Name = "Matplotlib Window"
@ -14,18 +36,43 @@ class MatplotlibExporter(Exporter):
def parameters(self): def parameters(self):
return None 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): def export(self, fileName=None):
if isinstance(self.item, PlotItem): if isinstance(self.item, PlotItem):
mpw = MatplotlibWindow() mpw = MatplotlibWindow()
MatplotlibExporter.windows.append(mpw) MatplotlibExporter.windows.append(mpw)
stdFont = 'Arial'
fig = mpw.getFigure() 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() ax.clear()
self.cleanAxes(ax)
#ax.grid(True) #ax.grid(True)
for item in self.item.curves: for item in self.item.curves:
x, y = item.getData() x, y = item.getData()
opts = item.opts opts = item.opts
@ -42,17 +89,21 @@ class MatplotlibExporter(Exporter):
symbolBrush = fn.mkBrush(opts['symbolBrush']) symbolBrush = fn.mkBrush(opts['symbolBrush'])
markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())]) markeredgecolor = tuple([c/255. for c in fn.colorTuple(symbolPen.color())])
markerfacecolor = tuple([c/255. for c in fn.colorTuple(symbolBrush.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: if opts['fillLevel'] is not None and opts['fillBrush'] is not None:
fillBrush = fn.mkBrush(opts['fillBrush']) fillBrush = fn.mkBrush(opts['fillBrush'])
fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())]) fillcolor = tuple([c/255. for c in fn.colorTuple(fillBrush.color())])
ax.fill_between(x=x, y1=y, y2=opts['fillLevel'], facecolor=fillcolor) 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() xr, yr = self.item.viewRange()
ax.set_xbound(*xr) ax.set_xbound(*xr)
ax.set_ybound(*yr) ax.set_ybound(*yr)
ax.set_xlabel(xlabel) # place the labels.
ax.set_ylabel(ylabel)
mpw.draw() mpw.draw()
else: else:
raise Exception("Matplotlib export currently only works with plot items") raise Exception("Matplotlib export currently only works with plot items")

View File

@ -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"> <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> <title>pyqtgraph SVG export</title>
<desc>Generated with Qt and pyqtgraph</desc> <desc>Generated with Qt and pyqtgraph</desc>
<defs>
</defs>
""" """
def generateSvg(item): def generateSvg(item):
global xmlHeader global xmlHeader
try: try:
node = _generateItemSvg(item) node, defs = _generateItemSvg(item)
finally: finally:
## reset export mode for all items in the tree ## reset export mode for all items in the tree
if isinstance(item, QtGui.QGraphicsScene): if isinstance(item, QtGui.QGraphicsScene):
@ -124,7 +122,11 @@ def generateSvg(item):
cleanXml(node) 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): def _generateItemSvg(item, nodes=None, root=None):
@ -230,6 +232,10 @@ def _generateItemSvg(item, nodes=None, root=None):
g1 = doc.getElementsByTagName('g')[0] g1 = doc.getElementsByTagName('g')[0]
## get list of sub-groups ## get list of sub-groups
g2 = [n for n in g1.childNodes if isinstance(n, xml.Element) and n.tagName == 'g'] 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: except:
print(doc.toxml()) print(doc.toxml())
raise raise
@ -238,7 +244,7 @@ def _generateItemSvg(item, nodes=None, root=None):
## Get rid of group transformation matrices by applying ## Get rid of group transformation matrices by applying
## transformation to inner coordinates ## transformation to inner coordinates
correctCoordinates(g1, item) correctCoordinates(g1, defs, item)
profiler('correct') profiler('correct')
## make sure g1 has the transformation matrix ## make sure g1 has the transformation matrix
#m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32()) #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())) path = QtGui.QGraphicsPathItem(item.mapToScene(item.shape()))
item.scene().addItem(path) item.scene().addItem(path)
try: 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: finally:
item.scene().removeItem(path) item.scene().removeItem(path)
@ -294,14 +302,19 @@ def _generateItemSvg(item, nodes=None, root=None):
## Add all child items as sub-elements. ## Add all child items as sub-elements.
childs.sort(key=lambda c: c.zValue()) childs.sort(key=lambda c: c.zValue())
for ch in childs: for ch in childs:
cg = _generateItemSvg(ch, nodes, root) csvg = _generateItemSvg(ch, nodes, root)
if cg is None: if csvg is None:
continue continue
cg, cdefs = csvg
childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now) childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now)
defs.extend(cdefs)
profiler('children') 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. ## 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 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. ## Each inner group contains one or more drawing primitives, possibly of different types.

View File

@ -4,7 +4,7 @@ from .SVGExporter import *
from .Matplotlib import * from .Matplotlib import *
from .CSVExporter import * from .CSVExporter import *
from .PrintExporter import * from .PrintExporter import *
from .HDF5Exporter import *
def listExporters(): def listExporters():
return Exporter.Exporters[:] return Exporter.Exporters[:]

View File

@ -20,41 +20,12 @@ from ..debug import printExc
from .. import configfile as configfile from .. import configfile as configfile
from .. import dockarea as dockarea from .. import dockarea as dockarea
from . import FlowchartGraphicsView from . import FlowchartGraphicsView
from .. import functions as fn
def strDict(d): def strDict(d):
return dict([(str(k), v) for k, v in d.items()]) 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): class Flowchart(Node):
@ -278,9 +249,10 @@ class Flowchart(Node):
## Record inputs given to process() ## Record inputs given to process()
for n, t in self.inputNode.outputs().items(): for n, t in self.inputNode.outputs().items():
if n not in args: # if n not in args:
raise Exception("Parameter %s required to process this chart." % n) # raise Exception("Parameter %s required to process this chart." % n)
data[t] = args[n] if n in args:
data[t] = args[n]
ret = {} ret = {}
@ -305,7 +277,7 @@ class Flowchart(Node):
if len(inputs) == 0: if len(inputs) == 0:
continue continue
if inp.isMultiValue(): ## multi-input terminals require a dict of all inputs 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 else: ## single-inputs terminals only need the single input value available
args[inp.name()] = data[inputs[0]] args[inp.name()] = data[inputs[0]]
@ -325,9 +297,8 @@ class Flowchart(Node):
#print out.name() #print out.name()
try: try:
data[out] = result[out.name()] data[out] = result[out.name()]
except: except KeyError:
print(out, out.name()) pass
raise
elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory) elif c == 'd': ## delete a terminal result (no longer needed; may be holding a lot of memory)
#print "===> delete", arg #print "===> delete", arg
if arg in data: if arg in data:
@ -352,7 +323,7 @@ class Flowchart(Node):
#print "DEPS:", deps #print "DEPS:", deps
## determine correct node-processing order ## determine correct node-processing order
#deps[self] = [] #deps[self] = []
order = toposort(deps) order = fn.toposort(deps)
#print "ORDER1:", order #print "ORDER1:", order
## construct list of operations ## construct list of operations
@ -401,7 +372,7 @@ class Flowchart(Node):
deps[node].extend(t.dependentNodes()) deps[node].extend(t.dependentNodes())
## determine order of updates ## determine order of updates
order = toposort(deps, nodes=[startNode]) order = fn.toposort(deps, nodes=[startNode])
order.reverse() order.reverse()
## keep track of terminals that have been updated ## keep track of terminals that have been updated
@ -542,7 +513,7 @@ class Flowchart(Node):
return return
## NOTE: was previously using a real widget for the file dialog's parent, but this caused weird mouse event bugs.. ## 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 = QtGui.QFileDialog.getOpenFileName(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
fileName = str(fileName) fileName = unicode(fileName)
state = configfile.readConfigFile(fileName) state = configfile.readConfigFile(fileName)
self.restoreState(state, clear=True) self.restoreState(state, clear=True)
self.viewBox.autoRange() self.viewBox.autoRange()
@ -563,7 +534,7 @@ class Flowchart(Node):
self.fileDialog.fileSelected.connect(self.saveFile) self.fileDialog.fileSelected.connect(self.saveFile)
return return
#fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") #fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
fileName = str(fileName) fileName = unicode(fileName)
configfile.writeConfigFile(self.saveState(), fileName) configfile.writeConfigFile(self.saveState(), fileName)
self.sigFileSaved.emit(fileName) self.sigFileSaved.emit(fileName)
@ -685,7 +656,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile) #self.setCurrentFile(newFile)
def fileSaved(self, fileName): def fileSaved(self, fileName):
self.setCurrentFile(str(fileName)) self.setCurrentFile(unicode(fileName))
self.ui.saveBtn.success("Saved.") self.ui.saveBtn.success("Saved.")
def saveClicked(self): def saveClicked(self):
@ -714,7 +685,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile) #self.setCurrentFile(newFile)
def setCurrentFile(self, fileName): def setCurrentFile(self, fileName):
self.currentFileName = str(fileName) self.currentFileName = unicode(fileName)
if fileName is None: if fileName is None:
self.ui.fileNameLabel.setText("<b>[ new ]</b>") self.ui.fileNameLabel.setText("<b>[ new ]</b>")
else: else:

View File

@ -182,8 +182,8 @@ class EvalNode(Node):
def __init__(self, name): def __init__(self, name):
Node.__init__(self, name, Node.__init__(self, name,
terminals = { terminals = {
'input': {'io': 'in', 'renamable': True}, 'input': {'io': 'in', 'renamable': True, 'multiable': True},
'output': {'io': 'out', 'renamable': True}, 'output': {'io': 'out', 'renamable': True, 'multiable': True},
}, },
allowAddInput=True, allowAddOutput=True) allowAddInput=True, allowAddOutput=True)

View File

@ -6,6 +6,8 @@ from ... import functions as pgfn
from .common import * from .common import *
import numpy as np import numpy as np
from ... import PolyLineROI
from ... import Point
from ... import metaarray as metaarray from ... import metaarray as metaarray
@ -201,6 +203,72 @@ class Detrend(CtrlNode):
raise Exception("DetrendFilter node requires the package scipy.signal.") raise Exception("DetrendFilter node requires the package scipy.signal.")
return detrend(data) 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): class AdaptiveDetrend(CtrlNode):
"""Removes baseline from data, ignoring anomalous events""" """Removes baseline from data, ignoring anomalous events"""
@ -275,4 +343,4 @@ class RemovePeriodic(CtrlNode):
return ma return ma

View File

@ -131,6 +131,42 @@ class CtrlNode(Node):
l.show() 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 metaArrayWrapper(fn):
def newFn(self, data, *args, **kargs): def newFn(self, data, *args, **kargs):

View File

@ -206,7 +206,7 @@ def adaptiveDetrend(data, x=None, threshold=3.0):
#d3 = where(mask, 0, d2) #d3 = where(mask, 0, d2)
#d4 = d2 - lowPass(d3, cutoffs[1], dt=dt) #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 base = lr[1] + lr[0]*x
d4 = d - base d4 = d - base

View File

@ -591,6 +591,50 @@ def interpolateArray(data, x, default=0.0):
return result 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): def transformToArray(tr):
""" """
Given a QTransform, return a 3x3 numpy array. Given a QTransform, return a 3x3 numpy array.
@ -2156,3 +2200,51 @@ def pseudoScatter(data, spacing=None, shuffle=True, bidir=False):
yvals[i] = y yvals[i] = y
return yvals[np.argsort(inds)] ## un-shuffle values before returning 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

View File

@ -918,13 +918,17 @@ class AxisItem(GraphicsWidget):
rects.append(br) rects.append(br)
textRects.append(rects[-1]) textRects.append(rects[-1])
## measure all text, make sure there's enough room if len(textRects) > 0:
if axis == 0: ## measure all text, make sure there's enough room
textSize = np.sum([r.height() for r in textRects]) if axis == 0:
textSize2 = np.max([r.width() for r in textRects]) if textRects else 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: else:
textSize = np.sum([r.width() for r in textRects]) textSize = 0
textSize2 = np.max([r.height() for r in textRects]) if textRects else 0 textSize2 = 0
if i > 0: ## always draw top level if i > 0: ## always draw top level
## If the strings are too crowded, stop drawing text now. ## If the strings are too crowded, stop drawing text now.

View File

@ -3,6 +3,7 @@ from ..python2_3 import sortList
from .. import functions as fn from .. import functions as fn
from .GraphicsObject import GraphicsObject from .GraphicsObject import GraphicsObject
from .GraphicsWidget import GraphicsWidget from .GraphicsWidget import GraphicsWidget
from ..widgets.SpinBox import SpinBox
import weakref import weakref
from ..pgcollections import OrderedDict from ..pgcollections import OrderedDict
from ..colormap import ColorMap from ..colormap import ColorMap
@ -300,6 +301,7 @@ class TickSliderItem(GraphicsWidget):
pos.setX(x) pos.setX(x)
tick.setPos(pos) tick.setPos(pos)
self.ticks[tick] = val self.ticks[tick] = val
self.updateGradient()
def tickValue(self, tick): def tickValue(self, tick):
## public ## public
@ -537,23 +539,22 @@ class GradientEditorItem(TickSliderItem):
def tickClicked(self, tick, ev): def tickClicked(self, tick, ev):
#private #private
if ev.button() == QtCore.Qt.LeftButton: if ev.button() == QtCore.Qt.LeftButton:
if not tick.colorChangeAllowed: self.raiseColorDialog(tick)
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()
elif ev.button() == QtCore.Qt.RightButton: elif ev.button() == QtCore.Qt.RightButton:
if not tick.removeAllowed: self.raiseTickContextMenu(tick, ev)
return
if len(self.ticks) > 2: def raiseColorDialog(self, tick):
self.removeTick(tick) if not tick.colorChangeAllowed:
self.updateGradient() 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): def tickMoved(self, tick, pos):
#private #private
TickSliderItem.tickMoved(self, tick, pos) TickSliderItem.tickMoved(self, tick, pos)
@ -726,6 +727,7 @@ class GradientEditorItem(TickSliderItem):
def removeTick(self, tick, finish=True): def removeTick(self, tick, finish=True):
TickSliderItem.removeTick(self, tick) TickSliderItem.removeTick(self, tick)
if finish: if finish:
self.updateGradient()
self.sigGradientChangeFinished.emit(self) self.sigGradientChangeFinished.emit(self)
@ -867,44 +869,59 @@ class Tick(QtGui.QGraphicsObject): ## NOTE: Making this a subclass of GraphicsO
self.currentPen = self.pen self.currentPen = self.pen
self.update() 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): class TickMenu(QtGui.QMenu):
#self.movedSincePress = False
#if ev.button() == QtCore.Qt.LeftButton: def __init__(self, tick, sliderItem):
#ev.accept() QtGui.QMenu.__init__(self)
#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)
##if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: self.tick = weakref.ref(tick)
##color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) self.sliderItem = weakref.ref(sliderItem)
##if color.isValid():
##self.color = color self.removeAct = self.addAction("Remove Tick", lambda: self.sliderItem().removeTick(tick))
##self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) if (not self.tick().removeAllowed) or len(self.sliderItem().ticks) < 3:
###self.emit(QtCore.SIGNAL('tickChanged'), self) self.removeAct.setEnabled(False)
##self.view.tickChanged(self)
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)

View File

@ -177,6 +177,12 @@ class ImageItem(GraphicsObject):
self.translate(rect.left(), rect.top()) self.translate(rect.left(), rect.top())
self.scale(rect.width() / self.width(), rect.height() / self.height()) 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): def setImage(self, image=None, autoLevels=None, **kargs):
""" """
Update the image displayed by this item. For more information on how the image Update the image displayed by this item. For more information on how the image
@ -512,6 +518,9 @@ class ImageItem(GraphicsObject):
def removeClicked(self): def removeClicked(self):
## Send remove event only after we have exited the menu event handler ## Send remove event only after we have exited the menu event handler
self.removeTimer = QtCore.QTimer() self.removeTimer = QtCore.QTimer()
self.removeTimer.timeout.connect(lambda: self.sigRemoveRequested.emit(self)) self.removeTimer.timeout.connect(self.emitRemoveRequested)
self.removeTimer.start(0) self.removeTimer.start(0)
def emitRemoveRequested(self):
self.removeTimer.timeout.disconnect(self.emitRemoveRequested)
self.sigRemoveRequested.emit(self)

View File

@ -168,6 +168,7 @@ class PlotDataItem(GraphicsObject):
'downsample': 1, 'downsample': 1,
'autoDownsample': False, 'autoDownsample': False,
'downsampleMethod': 'peak', 'downsampleMethod': 'peak',
'autoDownsampleFactor': 5., # draw ~5 samples per pixel
'clipToView': False, 'clipToView': False,
'data': None, 'data': None,
@ -380,14 +381,23 @@ class PlotDataItem(GraphicsObject):
elif len(args) == 2: elif len(args) == 2:
seq = ('listOfValues', 'MetaArray', 'empty') 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])))) 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): 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: else:
x = args[0].view(np.ndarray) x = args[0].view(np.ndarray)
if not isinstance(args[1], 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: else:
y = args[1].view(np.ndarray) y = args[1].view(np.ndarray)
@ -538,7 +548,7 @@ class PlotDataItem(GraphicsObject):
x1 = (range.right()-x[0]) / dx x1 = (range.right()-x[0]) / dx
width = self.getViewBox().width() width = self.getViewBox().width()
if width != 0.0: 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. ## downsampling is expensive; delay until after clipping.
if self.opts['clipToView']: if self.opts['clipToView']:

View File

@ -469,7 +469,8 @@ class PlotItem(GraphicsWidget):
### Average data together ### Average data together
(x, y) = curve.getData() (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) newData = plot.yData * (n-1) / float(n) + y * 1.0 / float(n)
plot.setData(plot.xData, newData) plot.setData(plot.xData, newData)
else: else:
@ -1207,10 +1208,13 @@ class PlotItem(GraphicsWidget):
self.updateButtons() self.updateButtons()
def updateButtons(self): def updateButtons(self):
if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()): try:
self.autoBtn.show() if self._exportOpts is False and self.mouseHovering and not self.buttonsHidden and not all(self.vb.autoRangeEnabled()):
else: self.autoBtn.show()
self.autoBtn.hide() else:
self.autoBtn.hide()
except RuntimeError:
pass # this can happen if the plot has been deleted.
def _plotArray(self, arr, x=None, **kargs): def _plotArray(self, arr, x=None, **kargs):
if arr.ndim != 1: if arr.ndim != 1:

View File

@ -25,7 +25,7 @@ from .UIGraphicsItem import UIGraphicsItem
__all__ = [ __all__ = [
'ROI', 'ROI',
'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', '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': elif h['type'] == 'sr':
if h['center'][0] == h['pos'][0]: if h['center'][0] == h['pos'][0]:
scaleAxis = 1 scaleAxis = 1
nonScaleAxis=0
else: else:
scaleAxis = 0 scaleAxis = 0
nonScaleAxis=1
try: try:
if lp1.length() == 0 or lp0.length() == 0: 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 newState['size'][scaleAxis] = round(newState['size'][scaleAxis] / self.snapSize) * self.snapSize
if newState['size'][scaleAxis] == 0: if newState['size'][scaleAxis] == 0:
newState['size'][scaleAxis] = 1 newState['size'][scaleAxis] = 1
if self.aspectLocked:
newState['size'][nonScaleAxis] = newState['size'][scaleAxis]
c1 = c * newState['size'] c1 = c * newState['size']
tr = QtGui.QTransform() tr = QtGui.QTransform()
@ -972,14 +976,16 @@ class ROI(GraphicsObject):
return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized() return QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
p.save() # p.save()
r = self.boundingRect() # 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.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen) p.setPen(self.currentPen)
p.translate(r.left(), r.top()) p.translate(r.left(), r.top())
p.scale(r.width(), r.height()) p.scale(r.width(), r.height())
p.drawRect(0, 0, 1, 1) p.drawRect(0, 0, 1, 1)
p.restore() # p.restore()
def getArraySlice(self, data, img, axes=(0,1), returnSlice=True): 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. """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()) 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))

View File

@ -5,6 +5,7 @@ from .TextItem import TextItem
import numpy as np import numpy as np
from .. import functions as fn from .. import functions as fn
from .. import getConfigOption from .. import getConfigOption
from ..Point import Point
__all__ = ['ScaleBar'] __all__ = ['ScaleBar']
@ -12,7 +13,7 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
""" """
Displays a rectangular bar to indicate the relative scale of objects on the view. 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) GraphicsObject.__init__(self)
GraphicsWidgetAnchor.__init__(self) GraphicsWidgetAnchor.__init__(self)
self.setFlag(self.ItemHasNoContents) self.setFlag(self.ItemHasNoContents)
@ -24,6 +25,9 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
self.pen = fn.mkPen(pen) self.pen = fn.mkPen(pen)
self._width = width self._width = width
self.size = size self.size = size
if offset == None:
offset = (0,0)
self.offset = offset
self.bar = QtGui.QGraphicsRectItem() self.bar = QtGui.QGraphicsRectItem()
self.bar.setPen(self.pen) self.bar.setPen(self.pen)
@ -54,51 +58,14 @@ class ScaleBar(GraphicsObject, GraphicsWidgetAnchor):
def boundingRect(self): def boundingRect(self):
return QtCore.QRectF() 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

View File

@ -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 = QtGui.QImage(int(size+penPxWidth), int(size+penPxWidth), QtGui.QImage.Format_ARGB32)
device.fill(0) device.fill(0)
p = QtGui.QPainter(device) p = QtGui.QPainter(device)
p.setRenderHint(p.Antialiasing) try:
p.translate(device.width()*0.5, device.height()*0.5) p.setRenderHint(p.Antialiasing)
drawSymbol(p, symbol, size, pen, brush) p.translate(device.width()*0.5, device.height()*0.5)
p.end() drawSymbol(p, symbol, size, pen, brush)
finally:
p.end()
return device return device
def makeSymbolPixmap(size, pen, brush, symbol): def makeSymbolPixmap(size, pen, brush, symbol):

View File

@ -760,7 +760,8 @@ class ViewBox(GraphicsWidget):
x = vr.left()+x, vr.right()+x x = vr.left()+x, vr.right()+x
if y is not None: if y is not None:
y = vr.top()+y, vr.bottom()+y 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 return
args['padding'] = 0 args['padding'] = 0
args['disableAutoRange'] = False 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) self.setRange(**args)
finally: finally:
self._autoRangeNeedsUpdate = False self._autoRangeNeedsUpdate = False
@ -1066,7 +1075,7 @@ class ViewBox(GraphicsWidget):
return return
self.state['yInverted'] = b self.state['yInverted'] = b
#self.updateMatrix(changed=(False, True)) self._matrixNeedsUpdate = True # updateViewRange won't detect this for us
self.updateViewRange() self.updateViewRange()
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
@ -1485,7 +1494,7 @@ class ViewBox(GraphicsWidget):
aspect = self.state['aspectLocked'] # size ratio / view ratio aspect = self.state['aspectLocked'] # size ratio / view ratio
tr = self.targetRect() tr = self.targetRect()
bounds = self.rect() 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 ## This is the view range aspect ratio we have requested
targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1 targetRatio = tr.width() / tr.height() if tr.height() != 0 else 1
@ -1581,18 +1590,16 @@ class ViewBox(GraphicsWidget):
if any(changed): if any(changed):
self.sigRangeChanged.emit(self, self.state['viewRange']) self.sigRangeChanged.emit(self, self.state['viewRange'])
self.update() self.update()
self._matrixNeedsUpdate = True
# Inform linked views that the range has changed # Inform linked views that the range has changed
for ax in [0, 1]: for ax in [0, 1]:
if not changed[ax]: if not changed[ax]:
continue continue
link = self.linkedView(ax) link = self.linkedView(ax)
if link is not None: if link is not None:
link.linkedViewChanged(self, ax) link.linkedViewChanged(self, ax)
self.update()
self._matrixNeedsUpdate = True
def updateMatrix(self, changed=None): def updateMatrix(self, changed=None):
## Make the childGroup's transform match the requested viewRange. ## Make the childGroup's transform match the requested viewRange.
bounds = self.rect() bounds = self.rect()

View File

@ -12,7 +12,7 @@ Widget used for displaying 2D or 3D data. Features:
- ROI plotting - ROI plotting
- Image normalization through a variety of methods - Image normalization through a variety of methods
""" """
import sys import os, sys
import numpy as np import numpy as np
from ..Qt import QtCore, QtGui, USE_PYSIDE from ..Qt import QtCore, QtGui, USE_PYSIDE
@ -136,6 +136,8 @@ class ImageView(QtGui.QWidget):
self.ui.histogram.setImageItem(self.imageItem) self.ui.histogram.setImageItem(self.imageItem)
self.menu = None
self.ui.normGroup.hide() self.ui.normGroup.hide()
self.roi = PlotROI(10) self.roi = PlotROI(10)
@ -176,7 +178,8 @@ class ImageView(QtGui.QWidget):
self.timeLine.sigPositionChanged.connect(self.timeLineChanged) self.timeLine.sigPositionChanged.connect(self.timeLineChanged)
self.ui.roiBtn.clicked.connect(self.roiClicked) self.ui.roiBtn.clicked.connect(self.roiClicked)
self.roi.sigRegionChanged.connect(self.roiChanged) 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.normDivideRadio.clicked.connect(self.normRadioChanged)
self.ui.normSubtractRadio.clicked.connect(self.normRadioChanged) self.ui.normSubtractRadio.clicked.connect(self.normRadioChanged)
self.ui.normOffRadio.clicked.connect(self.normRadioChanged) self.ui.normOffRadio.clicked.connect(self.normRadioChanged)
@ -321,6 +324,10 @@ class ImageView(QtGui.QWidget):
profiler() profiler()
def clear(self):
self.image = None
self.imageItem.clear()
def play(self, rate): def play(self, rate):
"""Begin automatically stepping frames forward at the given rate (in fps). """Begin automatically stepping frames forward at the given rate (in fps).
This can also be accessed by pressing the spacebar.""" This can also be accessed by pressing the spacebar."""
@ -671,3 +678,43 @@ class ImageView(QtGui.QWidget):
def getHistogramWidget(self): def getHistogramWidget(self):
"""Return the HistogramLUTWidget for this ImageView""" """Return the HistogramLUTWidget for this ImageView"""
return self.ui.histogram 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())

View File

@ -53,7 +53,7 @@
</widget> </widget>
</item> </item>
<item row="1" column="2"> <item row="1" column="2">
<widget class="QPushButton" name="normBtn"> <widget class="QPushButton" name="menuBtn">
<property name="sizePolicy"> <property name="sizePolicy">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<horstretch>0</horstretch> <horstretch>0</horstretch>
@ -61,10 +61,7 @@
</sizepolicy> </sizepolicy>
</property> </property>
<property name="text"> <property name="text">
<string>Norm</string> <string>Menu</string>
</property>
<property name="checkable">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- 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 # Created: Thu May 1 15:20:40 2014
# by: PyQt4 UI code generator 4.10 # by: PyQt4 UI code generator 4.10.4
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -55,15 +55,14 @@ class Ui_Form(object):
self.roiBtn.setCheckable(True) self.roiBtn.setCheckable(True)
self.roiBtn.setObjectName(_fromUtf8("roiBtn")) self.roiBtn.setObjectName(_fromUtf8("roiBtn"))
self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) 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 = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1) sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth())
self.normBtn.setSizePolicy(sizePolicy) self.menuBtn.setSizePolicy(sizePolicy)
self.normBtn.setCheckable(True) self.menuBtn.setObjectName(_fromUtf8("menuBtn"))
self.normBtn.setObjectName(_fromUtf8("normBtn")) self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1)
self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1)
self.roiPlot = PlotWidget(self.splitter) self.roiPlot = PlotWidget(self.splitter)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
@ -149,7 +148,7 @@ class Ui_Form(object):
def retranslateUi(self, Form): def retranslateUi(self, Form):
Form.setWindowTitle(_translate("Form", "Form", None)) Form.setWindowTitle(_translate("Form", "Form", None))
self.roiBtn.setText(_translate("Form", "ROI", 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.normGroup.setTitle(_translate("Form", "Normalization", None))
self.normSubtractRadio.setText(_translate("Form", "Subtract", None)) self.normSubtractRadio.setText(_translate("Form", "Subtract", None))
self.normDivideRadio.setText(_translate("Form", "Divide", None)) self.normDivideRadio.setText(_translate("Form", "Divide", None))

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*- # -*- 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 # Created: Thu May 1 15:20:42 2014
# by: pyside-uic 0.2.14 running on PySide 1.1.2 # by: pyside-uic 0.2.15 running on PySide 1.2.1
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -41,15 +41,14 @@ class Ui_Form(object):
self.roiBtn.setCheckable(True) self.roiBtn.setCheckable(True)
self.roiBtn.setObjectName("roiBtn") self.roiBtn.setObjectName("roiBtn")
self.gridLayout.addWidget(self.roiBtn, 1, 1, 1, 1) 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 = QtGui.QSizePolicy(QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1) sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.normBtn.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.menuBtn.sizePolicy().hasHeightForWidth())
self.normBtn.setSizePolicy(sizePolicy) self.menuBtn.setSizePolicy(sizePolicy)
self.normBtn.setCheckable(True) self.menuBtn.setObjectName("menuBtn")
self.normBtn.setObjectName("normBtn") self.gridLayout.addWidget(self.menuBtn, 1, 2, 1, 1)
self.gridLayout.addWidget(self.normBtn, 1, 2, 1, 1)
self.roiPlot = PlotWidget(self.splitter) self.roiPlot = PlotWidget(self.splitter)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
@ -135,7 +134,7 @@ class Ui_Form(object):
def retranslateUi(self, Form): def retranslateUi(self, Form):
Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8)) Form.setWindowTitle(QtGui.QApplication.translate("Form", "Form", None, QtGui.QApplication.UnicodeUTF8))
self.roiBtn.setText(QtGui.QApplication.translate("Form", "ROI", 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.normGroup.setTitle(QtGui.QApplication.translate("Form", "Normalization", None, QtGui.QApplication.UnicodeUTF8))
self.normSubtractRadio.setText(QtGui.QApplication.translate("Form", "Subtract", 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)) self.normDivideRadio.setText(QtGui.QApplication.translate("Form", "Divide", None, QtGui.QApplication.UnicodeUTF8))

View File

@ -103,6 +103,14 @@ class MetaArray(object):
""" """
version = '2' 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 ## Types allowed as axis or column names
nameTypes = [basestring, tuple] nameTypes = [basestring, tuple]
@ -122,7 +130,7 @@ class MetaArray(object):
if file is not None: if file is not None:
self._data = None self._data = None
self.readFile(file, **kwargs) 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) raise Exception("File read failed: %s" % file)
else: else:
self._info = info self._info = info
@ -720,25 +728,28 @@ class MetaArray(object):
""" """
## decide which read function to use ## decide which read function to use
fd = open(filename, 'rb') with open(filename, 'rb') as fd:
magic = fd.read(8) magic = fd.read(8)
if magic == '\x89HDF\r\n\x1a\n': if magic == '\x89HDF\r\n\x1a\n':
fd.close() fd.close()
self._readHDF5(filename, **kwargs) self._readHDF5(filename, **kwargs)
self._isHDF = True self._isHDF = True
else:
fd.seek(0)
meta = MetaArray._readMeta(fd)
if 'version' in meta:
ver = meta['version']
else: else:
ver = 1 fd.seek(0)
rFuncName = '_readData%s' % str(ver) meta = MetaArray._readMeta(fd)
if not hasattr(MetaArray, rFuncName):
raise Exception("This MetaArray library does not support array version '%s'" % ver) if not kwargs.get("readAllData", True):
rFunc = getattr(self, rFuncName) self._data = np.empty(meta['shape'], dtype=meta['type'])
rFunc(fd, meta, **kwargs) if 'version' in meta:
self._isHDF = False 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 @staticmethod
def _readMeta(fd): def _readMeta(fd):
@ -756,7 +767,7 @@ class MetaArray(object):
#print ret #print ret
return 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 array data from the file descriptor for MetaArray v1 files
## read in axis values for any axis that specifies a length ## read in axis values for any axis that specifies a length
frameSize = 1 frameSize = 1
@ -766,16 +777,18 @@ class MetaArray(object):
frameSize *= ax['values_len'] frameSize *= ax['values_len']
del ax['values_len'] del ax['values_len']
del ax['values_type'] del ax['values_type']
self._info = meta['info']
if not kwds.get("readAllData", True):
return
## the remaining data is the actual array ## the remaining data is the actual array
if mmap: if mmap:
subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape']) subarr = np.memmap(fd, dtype=meta['type'], mode='r', shape=meta['shape'])
else: else:
subarr = np.fromstring(fd.read(), dtype=meta['type']) subarr = np.fromstring(fd.read(), dtype=meta['type'])
subarr.shape = meta['shape'] subarr.shape = meta['shape']
self._info = meta['info']
self._data = subarr 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 ## read in axis values
dynAxis = None dynAxis = None
frameSize = 1 frameSize = 1
@ -792,7 +805,10 @@ class MetaArray(object):
frameSize *= ax['values_len'] frameSize *= ax['values_len']
del ax['values_len'] del ax['values_len']
del ax['values_type'] 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 ## No axes are dynamic, just read the entire array in at once
if dynAxis is None: if dynAxis is None:
#if rewriteDynamic is not None: #if rewriteDynamic is not None:
@ -1027,10 +1043,18 @@ class MetaArray(object):
def writeHDF5(self, fileName, **opts): def writeHDF5(self, fileName, **opts):
## default options for writing datasets ## default options for writing datasets
comp = self.defaultCompression
if isinstance(comp, tuple):
comp, copts = comp
else:
copts = None
dsOpts = { dsOpts = {
'compression': 'lzf', 'compression': comp,
'chunks': True, '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) ## if there is an appendable axis, then we can guess the desired chunk shape (optimized for appending)
appAxis = opts.get('appendAxis', None) appAxis = opts.get('appendAxis', None)

View File

@ -1,5 +1,6 @@
import os, time, sys, traceback, weakref import os, time, sys, traceback, weakref
import numpy as np import numpy as np
import threading
try: try:
import __builtin__ as builtins import __builtin__ as builtins
import cPickle as pickle import cPickle as pickle
@ -53,8 +54,10 @@ class RemoteEventHandler(object):
## status is either 'result' or 'error' ## status is either 'result' or 'error'
## if 'error', then result will be (exception, formatted exceprion) ## if 'error', then result will be (exception, formatted exceprion)
## where exception may be None if it could not be passed through the Connection. ## 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.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. ## attributes that affect the behavior of the proxy.
## See ObjectProxy._setProxyOptions for description ## See ObjectProxy._setProxyOptions for description
@ -66,10 +69,15 @@ class RemoteEventHandler(object):
'deferGetattr': False, ## True, False 'deferGetattr': False, ## True, False
'noProxyTypes': [ type(None), str, int, float, tuple, list, dict, LocalObjectProxy, ObjectProxy ], 'noProxyTypes': [ type(None), str, int, float, tuple, list, dict, LocalObjectProxy, ObjectProxy ],
} }
self.optsLock = threading.RLock()
self.nextRequestId = 0 self.nextRequestId = 0
self.exited = False 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 RemoteEventHandler.handlers[pid] = self ## register this handler as the one communicating with pid
@classmethod @classmethod
@ -86,46 +94,59 @@ class RemoteEventHandler(object):
cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1) cprint.cout(self.debug, "[%d] %s\n" % (os.getpid(), str(msg)), -1)
def getProxyOption(self, opt): def getProxyOption(self, opt):
return self.proxyOptions[opt] with self.optsLock:
return self.proxyOptions[opt]
def setProxyOptions(self, **kwds): def setProxyOptions(self, **kwds):
""" """
Set the default behavior options for object proxies. Set the default behavior options for object proxies.
See ObjectProxy._setProxyOptions for more info. See ObjectProxy._setProxyOptions for more info.
""" """
self.proxyOptions.update(kwds) with self.optsLock:
self.proxyOptions.update(kwds)
def processRequests(self): def processRequests(self):
"""Process all pending requests from the pipe, return """Process all pending requests from the pipe, return
after no more events are immediately available. (non-blocking) after no more events are immediately available. (non-blocking)
Returns the number of events processed. Returns the number of events processed.
""" """
if self.exited: with self.processLock:
self.debugMsg(' processRequests: exited already; raise ClosedError.')
raise ClosedError() if self.exited:
self.debugMsg(' processRequests: exited already; raise ClosedError.')
numProcessed = 0 raise ClosedError()
while self.conn.poll():
try: numProcessed = 0
self.handleRequest()
numProcessed += 1 while self.conn.poll():
except ClosedError: #try:
self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.') #poll = self.conn.poll()
self.exited = True #if not poll:
raise #break
#except IOError as err: ## let handleRequest take care of this. #except IOError: # this can happen if the remote process dies.
#self.debugMsg(' got IOError from handleRequest; try again.') ## might it also happen in other circumstances?
#if err.errno == 4: ## interrupted system call; try again #raise ClosedError()
#continue
#else: try:
#raise self.handleRequest()
except: numProcessed += 1
print("Error in process %s" % self.name) except ClosedError:
sys.excepthook(*sys.exc_info()) self.debugMsg('processRequests: got ClosedError from handleRequest; setting exited=True.')
self.exited = True
if numProcessed > 0: raise
self.debugMsg('processRequests: finished %d requests' % numProcessed) #except IOError as err: ## let handleRequest take care of this.
return numProcessed #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): def handleRequest(self):
"""Handle a single request from the remote process. """Handle a single request from the remote process.
@ -183,9 +204,11 @@ class RemoteEventHandler(object):
returnType = opts.get('returnType', 'auto') returnType = opts.get('returnType', 'auto')
if cmd == 'result': if cmd == 'result':
self.results[resultId] = ('result', opts['result']) with self.resultLock:
self.results[resultId] = ('result', opts['result'])
elif cmd == 'error': 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': elif cmd == 'getObjAttr':
result = getattr(opts['obj'], opts['attr']) result = getattr(opts['obj'], opts['attr'])
elif cmd == 'callObj': elif cmd == 'callObj':
@ -259,7 +282,9 @@ class RemoteEventHandler(object):
self.debugMsg(" handleRequest: sending return value for %d: %s" % (reqId, str(result))) self.debugMsg(" handleRequest: sending return value for %d: %s" % (reqId, str(result)))
#print "returnValue:", returnValue, result #print "returnValue:", returnValue, result
if returnType == 'auto': 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': elif returnType == 'proxy':
result = LocalObjectProxy(result) result = LocalObjectProxy(result)
@ -378,54 +403,59 @@ class RemoteEventHandler(object):
traceback traceback
============= ===================================================================== ============= =====================================================================
""" """
#if len(kwds) > 0: if self.exited:
#print "Warning: send() ignored args:", kwds 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: assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"'
opts = {} if reqId is None:
if callSync != 'off': ## requested return value; use the next available request ID
assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"' reqId = self.nextRequestId
if reqId is None: self.nextRequestId += 1
if callSync != 'off': ## requested return value; use the next available request ID else:
reqId = self.nextRequestId ## If requestId is provided, this _must_ be a response to a previously received request.
self.nextRequestId += 1 assert request in ['result', 'error']
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
#print os.getpid(), "send request:", request, reqId, opts if returnType is not None:
opts['returnType'] = returnType
## double-pickle args to ensure that at least status and request ID get through
try: #print os.getpid(), "send request:", request, reqId, opts
optStr = pickle.dumps(opts)
except: ## double-pickle args to ensure that at least status and request ID get through
print("==== Error pickling this object: ====") try:
print(opts) optStr = pickle.dumps(opts)
print("=======================================") except:
raise print("==== Error pickling this object: ====")
print(opts)
nByteMsgs = 0 print("=======================================")
if byteData is not None: raise
nByteMsgs = len(byteData)
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) req = Request(self, reqId, description=str(request), timeout=timeout)
if callSync == 'async': if callSync == 'async':
return req return req
@ -437,20 +467,30 @@ class RemoteEventHandler(object):
return req return req
def close(self, callSync='off', noCleanup=False, **kwds): 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): def getResult(self, reqId):
## raises NoResultError if the result is not available yet ## raises NoResultError if the result is not available yet
#print self.results.keys(), os.getpid() #print self.results.keys(), os.getpid()
if reqId not in self.results: with self.resultLock:
haveResult = reqId in self.results
if not haveResult:
try: try:
self.processRequests() self.processRequests()
except ClosedError: ## even if remote connection has closed, we may have except ClosedError: ## even if remote connection has closed, we may have
## received new data during this call to processRequests() ## received new data during this call to processRequests()
pass pass
if reqId not in self.results:
raise NoResultError() with self.resultLock:
status, result = self.results.pop(reqId) if reqId not in self.results:
raise NoResultError()
status, result = self.results.pop(reqId)
if status == 'result': if status == 'result':
return result return result
elif status == 'error': elif status == 'error':
@ -494,11 +534,13 @@ class RemoteEventHandler(object):
args = list(args) args = list(args)
## Decide whether to send arguments by value or by proxy ## Decide whether to send arguments by value or by proxy
noProxyTypes = opts.pop('noProxyTypes', None) with self.optsLock:
if noProxyTypes is None: noProxyTypes = opts.pop('noProxyTypes', None)
noProxyTypes = self.proxyOptions['noProxyTypes'] if noProxyTypes is None:
noProxyTypes = self.proxyOptions['noProxyTypes']
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
autoProxy = opts.pop('autoProxy', self.proxyOptions['autoProxy'])
if autoProxy is True: if autoProxy is True:
args = [self.autoProxy(v, noProxyTypes) for v in args] args = [self.autoProxy(v, noProxyTypes) for v in args]
for k, v in kwds.iteritems(): 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) return self.send(request='callObj', opts=dict(obj=obj, args=args, kwds=kwds), byteData=byteMsgs, **opts)
def registerProxy(self, proxy): def registerProxy(self, proxy):
ref = weakref.ref(proxy, self.deleteProxy) with self.proxyLock:
self.proxies[ref] = proxy._proxyId ref = weakref.ref(proxy, self.deleteProxy)
self.proxies[ref] = proxy._proxyId
def deleteProxy(self, ref): def deleteProxy(self, ref):
proxyId = self.proxies.pop(ref) with self.proxyLock:
proxyId = self.proxies.pop(ref)
try: try:
self.send(request='del', opts=dict(proxyId=proxyId), callSync='off') self.send(request='del', opts=dict(proxyId=proxyId), callSync='off')
except IOError: ## if remote process has closed down, there is no need to send delete requests anymore except IOError: ## if remote process has closed down, there is no need to send delete requests anymore

View File

@ -1,6 +1,7 @@
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
import os, weakref, re import os, weakref, re
from ..pgcollections import OrderedDict from ..pgcollections import OrderedDict
from ..python2_3 import asUnicode
from .ParameterItem import ParameterItem from .ParameterItem import ParameterItem
PARAM_TYPES = {} PARAM_TYPES = {}
@ -13,7 +14,9 @@ def registerParameterType(name, cls, override=False):
PARAM_TYPES[name] = cls PARAM_TYPES[name] = cls
PARAM_NAMES[cls] = name 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): class Parameter(QtCore.QObject):
""" """
@ -46,6 +49,7 @@ class Parameter(QtCore.QObject):
including during editing. including during editing.
sigChildAdded(self, child, index) Emitted when a child is added sigChildAdded(self, child, index) Emitted when a child is added
sigChildRemoved(self, child) Emitted when a child is removed 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 sigParentChanged(self, parent) Emitted when this parameter's parent has changed
sigLimitsChanged(self, limits) Emitted when this parameter's limits have changed sigLimitsChanged(self, limits) Emitted when this parameter's limits have changed
sigDefaultChanged(self, default) Emitted when this parameter's default value has 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 sigChildAdded = QtCore.Signal(object, object, object) ## self, child, index
sigChildRemoved = QtCore.Signal(object, object) ## self, child sigChildRemoved = QtCore.Signal(object, object) ## self, child
sigRemoved = QtCore.Signal(object) ## self
sigParentChanged = QtCore.Signal(object, object) ## self, parent sigParentChanged = QtCore.Signal(object, object) ## self, parent
sigLimitsChanged = QtCore.Signal(object, object) ## self, limits sigLimitsChanged = QtCore.Signal(object, object) ## self, limits
sigDefaultChanged = QtCore.Signal(object, object) ## self, default sigDefaultChanged = QtCore.Signal(object, object) ## self, default
@ -133,6 +138,12 @@ class Parameter(QtCore.QObject):
expanded If True, the Parameter will appear expanded when expanded If True, the Parameter will appear expanded when
displayed in a ParameterTree (its children will be displayed in a ParameterTree (its children will be
visible). (default=True) 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, 'removable': False,
'strictNaming': False, # forces name to be usable as a python variable 'strictNaming': False, # forces name to be usable as a python variable
'expanded': True, 'expanded': True,
'title': None,
#'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits. #'limits': None, ## This is a bad plan--each parameter type may have a different data type for limits.
} }
self.opts.update(opts) self.opts.update(opts)
@ -266,16 +278,27 @@ class Parameter(QtCore.QObject):
vals[ch.name()] = (ch.value(), ch.getValues()) vals[ch.name()] = (ch.value(), ch.getValues())
return vals return vals
def saveState(self): def saveState(self, filter=None):
""" """
Return a structure representing the entire state of the parameter tree. 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() if filter is None:
state['children'] = OrderedDict([(ch.name(), ch.saveState()) for ch in self]) state = self.opts.copy()
if state['type'] is None: if state['type'] is None:
global PARAM_NAMES global PARAM_NAMES
state['type'] = PARAM_NAMES.get(type(self), None) 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 return state
def restoreState(self, state, recursive=True, addChildren=True, removeChildren=True, blockSignals=True): 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. ## list of children may be stored either as list or dict.
if isinstance(childState, 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: if blockSignals:
self.blockTreeChangeSignal() self.blockTreeChangeSignal()
@ -311,14 +337,14 @@ class Parameter(QtCore.QObject):
for ch in childState: for ch in childState:
name = ch['name'] name = ch['name']
typ = ch['type'] #typ = ch.get('type', None)
#print('child: %s, %s' % (self.name()+'.'+name, typ)) #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 gotChild = False
for i, ch2 in enumerate(self.childs[ptr:]): for i, ch2 in enumerate(self.childs[ptr:]):
#print " ", ch2.name(), ch2.type() #print " ", ch2.name(), ch2.type()
if ch2.name() != name or not ch2.isType(typ): if ch2.name() != name: # or not ch2.isType(typ):
continue continue
gotChild = True gotChild = True
#print " found it" #print " found it"
@ -393,15 +419,22 @@ class Parameter(QtCore.QObject):
Note that the value of the parameter can *always* be changed by Note that the value of the parameter can *always* be changed by
calling setValue(). calling setValue().
""" """
return not self.opts.get('readonly', False) return not self.readonly()
def setWritable(self, writable=True): def setWritable(self, writable=True):
"""Set whether this Parameter should be editable by the user. (This is """Set whether this Parameter should be editable by the user. (This is
exactly the opposite of setReadonly).""" exactly the opposite of setReadonly)."""
self.setOpts(readonly=not writable) 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): 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) self.setOpts(readonly=readonly)
def setOpts(self, **opts): def setOpts(self, **opts):
@ -453,11 +486,20 @@ class Parameter(QtCore.QObject):
return ParameterItem(self, depth=depth) return ParameterItem(self, depth=depth)
def addChild(self, child): def addChild(self, child, autoIncrementName=None):
"""Add another parameter to the end of this parameter's child list.""" """
return self.insertChild(len(self.childs), child) 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): 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 children was specified as dict, then assume keys are the names.
if isinstance(children, dict): if isinstance(children, dict):
ch2 = [] ch2 = []
@ -473,19 +515,24 @@ class Parameter(QtCore.QObject):
self.addChild(chOpts) self.addChild(chOpts)
def insertChild(self, pos, child): def insertChild(self, pos, child, autoIncrementName=None):
""" """
Insert a new child at pos. Insert a new child at pos.
If pos is a Parameter, then insert at the position of that Parameter. If pos is a Parameter, then insert at the position of that Parameter.
If child is a dict, then a parameter is constructed using If child is a dict, then a parameter is constructed using
:func:`Parameter.create <pyqtgraph.parametertree.Parameter.create>`. :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): if isinstance(child, dict):
child = Parameter.create(**child) child = Parameter.create(**child)
name = child.name() name = child.name()
if name in self.names and child is not self.names[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) name = self.incrementName(name)
child.setName(name) child.setName(name)
else: else:
@ -550,6 +597,7 @@ class Parameter(QtCore.QObject):
if parent is None: if parent is None:
raise Exception("Cannot remove; no parent.") raise Exception("Cannot remove; no parent.")
parent.removeChild(self) parent.removeChild(self)
self.sigRemoved.emit(self)
def incrementName(self, name): def incrementName(self, name):
## return an unused name by adding a number to the name given ## return an unused name by adding a number to the name given
@ -590,9 +638,12 @@ class Parameter(QtCore.QObject):
names = (names,) names = (names,)
return self.param(*names).setValue(value) return self.param(*names).setValue(value)
def param(self, *names): def child(self, *names):
"""Return a child parameter. """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: try:
param = self.names[names[0]] param = self.names[names[0]]
except KeyError: except KeyError:
@ -603,8 +654,12 @@ class Parameter(QtCore.QObject):
else: else:
return param return param
def param(self, *names):
# for backward compatibility.
return self.child(*names)
def __repr__(self): 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): def __getattr__(self, attr):
## Leaving this undocumented because I might like to remove it in the future.. ## 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: if self.blockTreeChangeEmit == 0:
changes = self.treeStateChanges changes = self.treeStateChanges
self.treeStateChanges = [] self.treeStateChanges = []
self.sigTreeStateChanged.emit(self, changes) if len(changes) > 0:
self.sigTreeStateChanged.emit(self, changes)
class SignalBlocker(object): class SignalBlocker(object):

View File

@ -1,4 +1,5 @@
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
from ..python2_3 import asUnicode
import os, weakref, re import os, weakref, re
class ParameterItem(QtGui.QTreeWidgetItem): class ParameterItem(QtGui.QTreeWidgetItem):
@ -15,8 +16,11 @@ class ParameterItem(QtGui.QTreeWidgetItem):
""" """
def __init__(self, param, depth=0): 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 = param
self.param.registerItem(self) ## let parameter know this item is connected to it (for debugging) self.param.registerItem(self) ## let parameter know this item is connected to it (for debugging)
self.depth = depth self.depth = depth
@ -30,7 +34,6 @@ class ParameterItem(QtGui.QTreeWidgetItem):
param.sigOptionsChanged.connect(self.optsChanged) param.sigOptionsChanged.connect(self.optsChanged)
param.sigParentChanged.connect(self.parentChanged) param.sigParentChanged.connect(self.parentChanged)
opts = param.opts opts = param.opts
## Generate context menu for renaming/removing parameter ## Generate context menu for renaming/removing parameter
@ -38,6 +41,8 @@ class ParameterItem(QtGui.QTreeWidgetItem):
self.contextMenu.addSeparator() self.contextMenu.addSeparator()
flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled flags = QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled
if opts.get('renamable', False): 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 flags |= QtCore.Qt.ItemIsEditable
self.contextMenu.addAction('Rename').triggered.connect(self.editName) self.contextMenu.addAction('Rename').triggered.connect(self.editName)
if opts.get('removable', False): if opts.get('removable', False):
@ -107,15 +112,15 @@ class ParameterItem(QtGui.QTreeWidgetItem):
self.contextMenu.popup(ev.globalPos()) self.contextMenu.popup(ev.globalPos())
def columnChangedEvent(self, col): 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. 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: if self.ignoreNameColumnChange:
return return
try: try:
newName = self.param.setName(str(self.text(col))) newName = self.param.setName(asUnicode(self.text(col)))
except: except Exception:
self.setText(0, self.param.name()) self.setText(0, self.param.name())
raise raise
@ -127,8 +132,9 @@ class ParameterItem(QtGui.QTreeWidgetItem):
def nameChanged(self, param, name): def nameChanged(self, param, name):
## called when the parameter's name has changed. ## 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): def limitsChanged(self, param, limits):
"""Called when the parameter's limits have changed""" """Called when the parameter's limits have changed"""
pass pass

View 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)

View 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()

View File

@ -1,5 +1,5 @@
from .Parameter import Parameter, registerParameterType from .Parameter import Parameter, registerParameterType
from .ParameterTree import ParameterTree from .ParameterTree import ParameterTree
from .ParameterItem import ParameterItem from .ParameterItem import ParameterItem
from .ParameterSystem import ParameterSystem, SystemSolver
from . import parameterTypes as types from . import parameterTypes as types

View File

@ -78,6 +78,7 @@ class WidgetParameterItem(ParameterItem):
## no starting value was given; use whatever the widget has ## no starting value was given; use whatever the widget has
self.widgetValueChanged() self.widgetValueChanged()
self.updateDefaultBtn()
def makeWidget(self): def makeWidget(self):
""" """
@ -191,6 +192,9 @@ class WidgetParameterItem(ParameterItem):
def updateDefaultBtn(self): def updateDefaultBtn(self):
## enable/disable default btn ## enable/disable default btn
self.defaultBtn.setEnabled(not self.param.valueIsDefault() and self.param.writable()) 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): def updateDisplayLabel(self, value=None):
"""Update the display label to reflect the value of the parameter.""" """Update the display label to reflect the value of the parameter."""
@ -234,6 +238,8 @@ class WidgetParameterItem(ParameterItem):
self.widget.show() self.widget.show()
self.displayLabel.hide() self.displayLabel.hide()
self.widget.setFocus(QtCore.Qt.OtherFocusReason) 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): def hideEditor(self):
self.widget.hide() self.widget.hide()
@ -277,7 +283,7 @@ class WidgetParameterItem(ParameterItem):
if 'readonly' in opts: if 'readonly' in opts:
self.updateDefaultBtn() self.updateDefaultBtn()
if isinstance(self.widget, (QtGui.QCheckBox,ColorButton)): 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 widget is a SpinBox, pass options straight through
if isinstance(self.widget, SpinBox): if isinstance(self.widget, SpinBox):
@ -315,8 +321,8 @@ class SimpleParameter(Parameter):
def colorValue(self): def colorValue(self):
return fn.mkColor(Parameter.value(self)) return fn.mkColor(Parameter.value(self))
def saveColorState(self): def saveColorState(self, *args, **kwds):
state = Parameter.saveState(self) state = Parameter.saveState(self, *args, **kwds)
state['value'] = fn.colorTuple(self.value()) state['value'] = fn.colorTuple(self.value())
return state return state
@ -539,7 +545,6 @@ class ListParameter(Parameter):
self.forward, self.reverse = self.mapping(limits) self.forward, self.reverse = self.mapping(limits)
Parameter.setLimits(self, 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]: if len(self.reverse[0]) > 0 and self.value() not in self.reverse[0]:
self.setValue(self.reverse[0][0]) self.setValue(self.reverse[0][0])

View File

@ -61,6 +61,19 @@ def test_interpolateArray():
assert_array_almost_equal(r1, r2) 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)

View 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))

View File

@ -19,8 +19,8 @@ class ColorMapWidget(ptree.ParameterTree):
""" """
sigColorMapChanged = QtCore.Signal(object) sigColorMapChanged = QtCore.Signal(object)
def __init__(self): def __init__(self, parent=None):
ptree.ParameterTree.__init__(self, showHeader=False) ptree.ParameterTree.__init__(self, parent=parent, showHeader=False)
self.params = ColorMapParameter() self.params = ColorMapParameter()
self.setParameters(self.params) self.setParameters(self.params)
@ -32,6 +32,15 @@ class ColorMapWidget(ptree.ParameterTree):
def mapChanged(self): def mapChanged(self):
self.sigColorMapChanged.emit(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): class ColorMapParameter(ptree.types.GroupParameter):
@ -48,9 +57,11 @@ class ColorMapParameter(ptree.types.GroupParameter):
def addNew(self, name): def addNew(self, name):
mode = self.fields[name].get('mode', 'range') mode = self.fields[name].get('mode', 'range')
if mode == 'range': if mode == 'range':
self.addChild(RangeColorMapItem(name, self.fields[name])) item = RangeColorMapItem(name, self.fields[name])
elif mode == 'enum': elif mode == 'enum':
self.addChild(EnumColorMapItem(name, self.fields[name])) item = EnumColorMapItem(name, self.fields[name])
self.addChild(item)
return item
def fieldNames(self): def fieldNames(self):
return self.fields.keys() return self.fields.keys()
@ -95,6 +106,9 @@ class ColorMapParameter(ptree.types.GroupParameter):
returned as 0.0-1.0 float values. 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)) colors = np.zeros((len(data),4))
for item in self.children(): for item in self.children():
if not item['Enabled']: if not item['Enabled']:
@ -126,8 +140,26 @@ class ColorMapParameter(ptree.types.GroupParameter):
return colors 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): class RangeColorMapItem(ptree.types.SimpleParameter):
mapType = 'range'
def __init__(self, name, opts): def __init__(self, name, opts):
self.fieldName = name self.fieldName = name
units = opts.get('units', '') units = opts.get('units', '')
@ -151,8 +183,6 @@ class RangeColorMapItem(ptree.types.SimpleParameter):
def map(self, data): def map(self, data):
data = data[self.fieldName] data = data[self.fieldName]
scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1) scaled = np.clip((data-self['Min']) / (self['Max']-self['Min']), 0, 1)
cmap = self.value() cmap = self.value()
colors = cmap.map(scaled, mode='float') 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.) nanColor = (nanColor.red()/255., nanColor.green()/255., nanColor.blue()/255., nanColor.alpha()/255.)
colors[mask] = nanColor colors[mask] = nanColor
return colors return colors
class EnumColorMapItem(ptree.types.GroupParameter): class EnumColorMapItem(ptree.types.GroupParameter):
mapType = 'enum'
def __init__(self, name, opts): def __init__(self, name, opts):
self.fieldName = name self.fieldName = name
vals = opts.get('values', []) vals = opts.get('values', [])

View File

@ -1,5 +1,6 @@
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
from ..SignalProxy import SignalProxy from ..SignalProxy import SignalProxy
import sys
from ..pgcollections import OrderedDict from ..pgcollections import OrderedDict
from ..python2_3 import asUnicode from ..python2_3 import asUnicode
@ -20,6 +21,10 @@ class ComboBox(QtGui.QComboBox):
self.currentIndexChanged.connect(self.indexChanged) self.currentIndexChanged.connect(self.indexChanged)
self._ignoreIndexChange = False 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._chosenText = None
self._items = OrderedDict() self._items = OrderedDict()

View File

@ -57,7 +57,7 @@ class DataTreeWidget(QtGui.QTreeWidget):
} }
if isinstance(data, dict): if isinstance(data, dict):
for k in data: for k in data.keys():
self.buildTree(data[k], node, str(k)) self.buildTree(data[k], node, str(k))
elif isinstance(data, list) or isinstance(data, tuple): elif isinstance(data, list) or isinstance(data, tuple):
for i in range(len(data)): for i in range(len(data)):

View File

@ -47,29 +47,29 @@ class SpinBox(QtGui.QAbstractSpinBox):
""" """
============== ======================================================================== ============== ========================================================================
**Arguments:** **Arguments:**
parent Sets the parent widget for this SpinBox (optional) parent Sets the parent widget for this SpinBox (optional). Default is None.
value (float/int) initial value value (float/int) initial value. Default is 0.0.
bounds (min,max) Minimum and maximum values allowed in the SpinBox. bounds (min,max) Minimum and maximum values allowed in the SpinBox.
Either may be None to leave the value unbounded. Either may be None to leave the value unbounded. By default, values are unbounded.
suffix (str) suffix (units) to display after the numerical value 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 siPrefix (bool) If True, then an SI prefix is automatically prepended
to the units and the value is scaled accordingly. For example, to the units and the value is scaled accordingly. For example,
if value=0.003 and suffix='V', then the SpinBox will display 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/ 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 down arrows, when rolling the mouse wheel, or when pressing
keyboard arrows while the widget has keyboard focus. Note that keyboard arrows while the widget has keyboard focus. Note that
the interpretation of this value is different when specifying 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 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 the current size of the variable (for example, a value of 15
might step in increments of 1 whereas a value of 1500 would might step in increments of 1 whereas a value of 1500 would
step in increments of 100). In this case, the 'step' argument step in increments of 100). In this case, the 'step' argument
is interpreted *relative* to the current value. The most common 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. minStep (float) When dec=True, this specifies the minimum allowable step size.
int (bool) if True, the value is forced to integer type int (bool) if True, the value is forced to integer type. Default is False
decimals (int) Number of decimal values to display decimals (int) Number of decimal values to display. Default is 2.
============== ======================================================================== ============== ========================================================================
""" """
QtGui.QAbstractSpinBox.__init__(self, parent) QtGui.QAbstractSpinBox.__init__(self, parent)
@ -233,6 +233,18 @@ class SpinBox(QtGui.QAbstractSpinBox):
def setDecimals(self, decimals): def setDecimals(self, decimals):
self.setOpts(decimals=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): def value(self):
""" """

View File

@ -365,7 +365,7 @@ class TableWidget(QtGui.QTableWidget):
ev.ignore() ev.ignore()
def handleItemChanged(self, item): def handleItemChanged(self, item):
item.textChanged() item.itemChanged()
class TableWidgetItem(QtGui.QTableWidgetItem): class TableWidgetItem(QtGui.QTableWidgetItem):
@ -425,7 +425,8 @@ class TableWidgetItem(QtGui.QTableWidgetItem):
def _updateText(self): def _updateText(self):
self._blockValueChange = True self._blockValueChange = True
try: try:
self.setText(self.format()) self._text = self.format()
self.setText(self._text)
finally: finally:
self._blockValueChange = False self._blockValueChange = False
@ -433,14 +434,22 @@ class TableWidgetItem(QtGui.QTableWidgetItem):
self.value = value self.value = value
self._updateText() self._updateText()
def itemChanged(self):
"""Called when the data of this item has changed."""
if self.text() != self._text:
self.textChanged()
def textChanged(self): def textChanged(self):
"""Called when this item's text has changed for any reason.""" """Called when this item's text has changed for any reason."""
self._text = self.text()
if self._blockValueChange: if self._blockValueChange:
# text change was result of value or format change; do not # text change was result of value or format change; do not
# propagate. # propagate.
return return
try: try:
self.value = type(self.value)(self.text()) self.value = type(self.value)(self.text())
except ValueError: except ValueError:
self.value = str(self.text()) self.value = str(self.text())