Commit 4c887c8f authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Merge new fixes and features from acq4

parent 189b4ed6
......@@ -70,6 +70,7 @@ params = [
{'name': 'Named List', 'type': 'list', 'values': {"one": 1, "two": 2, "three": 3}, 'value': 2},
{'name': 'Boolean', 'type': 'bool', 'value': True, 'tip': "This is a checkbox"},
{'name': 'Color', 'type': 'color', 'value': "FF0", 'tip': "This is a color button"},
{'name': 'Gradient', 'type': 'colormap'},
{'name': 'Subgroup', 'type': 'group', 'children': [
{'name': 'Sub-param 1', 'type': 'int', 'value': 10},
{'name': 'Sub-param 2', 'type': 'float', 'value': 1.2e6},
......
......@@ -27,6 +27,7 @@ class ExportDialog(QtGui.QWidget):
self.ui.closeBtn.clicked.connect(self.close)
self.ui.exportBtn.clicked.connect(self.exportClicked)
self.ui.copyBtn.clicked.connect(self.copyClicked)
self.ui.itemTree.currentItemChanged.connect(self.exportItemChanged)
self.ui.formatList.currentItemChanged.connect(self.exportFormatChanged)
......@@ -116,11 +117,16 @@ class ExportDialog(QtGui.QWidget):
else:
self.ui.paramTree.setParameters(params)
self.currentExporter = exp
self.ui.copyBtn.setEnabled(exp.allowCopy)
def exportClicked(self):
self.selectBox.hide()
self.currentExporter.export()
def copyClicked(self):
self.selectBox.hide()
self.currentExporter.export(copy=True)
def close(self):
self.selectBox.setVisible(False)
self.setVisible(False)
......
......@@ -79,6 +79,13 @@
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QPushButton" name="copyBtn">
<property name="text">
<string>Copy</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
......
......@@ -2,8 +2,8 @@
# Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui'
#
# Created: Sun Sep 9 14:41:31 2012
# by: PyQt4 UI code generator 4.9.1
# Created: Wed Jan 30 21:02:28 2013
# by: PyQt4 UI code generator 4.9.3
#
# WARNING! All changes made in this file will be lost!
......@@ -49,6 +49,9 @@ class Ui_Form(object):
self.label_3 = QtGui.QLabel(Form)
self.label_3.setObjectName(_fromUtf8("label_3"))
self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3)
self.copyBtn = QtGui.QPushButton(Form)
self.copyBtn.setObjectName(_fromUtf8("copyBtn"))
self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
......@@ -60,5 +63,6 @@ class Ui_Form(object):
self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8))
self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8))
self.copyBtn.setText(QtGui.QApplication.translate("Form", "Copy", None, QtGui.QApplication.UnicodeUTF8))
from pyqtgraph.parametertree import ParameterTree
......@@ -2,8 +2,8 @@
# Form implementation generated from reading ui file './GraphicsScene/exportDialogTemplate.ui'
#
# Created: Sun Sep 9 14:41:31 2012
# by: pyside-uic 0.2.13 running on PySide 1.1.0
# Created: Wed Jan 30 21:02:28 2013
# by: pyside-uic 0.2.13 running on PySide 1.1.1
#
# WARNING! All changes made in this file will be lost!
......@@ -44,6 +44,9 @@ class Ui_Form(object):
self.label_3 = QtGui.QLabel(Form)
self.label_3.setObjectName("label_3")
self.gridLayout.addWidget(self.label_3, 4, 0, 1, 3)
self.copyBtn = QtGui.QPushButton(Form)
self.copyBtn.setObjectName("copyBtn")
self.gridLayout.addWidget(self.copyBtn, 6, 0, 1, 1)
self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form)
......@@ -55,5 +58,6 @@ class Ui_Form(object):
self.exportBtn.setText(QtGui.QApplication.translate("Form", "Export", None, QtGui.QApplication.UnicodeUTF8))
self.closeBtn.setText(QtGui.QApplication.translate("Form", "Close", None, QtGui.QApplication.UnicodeUTF8))
self.label_3.setText(QtGui.QApplication.translate("Form", "Export options", None, QtGui.QApplication.UnicodeUTF8))
self.copyBtn.setText(QtGui.QApplication.translate("Form", "Copy", None, QtGui.QApplication.UnicodeUTF8))
from pyqtgraph.parametertree import ParameterTree
class PlotData(object):
"""
Class used for managing plot data
- allows data sharing between multiple graphics items (curve, scatter, graph..)
- each item may define the columns it needs
- column groupings ('pos' or x, y, z)
- efficiently appendable
- log, fft transformations
- color mode conversion (float/byte/qcolor)
- pen/brush conversion
- per-field cached masking
- allows multiple masking fields (different graphics need to mask on different criteria)
- removal of nan/inf values
- option for single value shared by entire column
- cached downsampling
"""
def __init__(self):
self.fields = {}
self.maxVals = {} ## cache for max/min
self.minVals = {}
def addFields(self, fields):
for f in fields:
if f not in self.fields:
self.fields[f] = None
def hasField(self, f):
return f in self.fields
def __getitem__(self, field):
return self.fields[field]
def __setitem__(self, field, val):
self.fields[field] = val
def max(self, field):
mx = self.maxVals.get(field, None)
if mx is None:
mx = np.max(self[field])
self.maxVals[field] = mx
return mx
def min(self, field):
mn = self.minVals.get(field, None)
if mn is None:
mn = np.min(self[field])
self.minVals[field] = mn
return mn
\ No newline at end of file
import numpy as np
import scipy.interpolate
from pyqtgraph.Qt import QtGui, QtCore
class ColorMap(object):
## color interpolation modes
RGB = 1
HSV_POS = 2
HSV_NEG = 3
## boundary modes
CLIP = 1
REPEAT = 2
MIRROR = 3
## return types
BYTE = 1
FLOAT = 2
QCOLOR = 3
enumMap = {
'rgb': RGB,
'hsv+': HSV_POS,
'hsv-': HSV_NEG,
'clip': CLIP,
'repeat': REPEAT,
'mirror': MIRROR,
'byte': BYTE,
'float': FLOAT,
'qcolor': QCOLOR,
}
def __init__(self, pos, color, mode=None):
"""
========= ==============================================================
Arguments
pos Array of positions where each color is defined
color Array of RGBA colors.
Integer data types are interpreted as 0-255; float data types
are interpreted as 0.0-1.0
mode Array of color modes (ColorMap.RGB, HSV_POS, or HSV_NEG)
indicating the color space that should be used when
interpolating between stops. Note that the last mode value is
ignored. By default, the mode is entirely RGB.
========= ==============================================================
"""
self.pos = pos
self.color = color
if mode is None:
mode = np.ones(len(pos))
self.mode = mode
self.stopsCache = {}
def map(self, data, mode='byte'):
"""
Data must be either a scalar position or an array (any shape) of positions.
"""
if isinstance(mode, basestring):
mode = self.enumMap[mode.lower()]
if mode == self.QCOLOR:
pos, color = self.getStops(self.BYTE)
else:
pos, color = self.getStops(mode)
data = np.clip(data, pos.min(), pos.max())
if not isinstance(data, np.ndarray):
interp = scipy.interpolate.griddata(pos, color, np.array([data]))[0]
else:
interp = scipy.interpolate.griddata(pos, color, data)
if mode == self.QCOLOR:
if not isinstance(data, np.ndarray):
return QtGui.QColor(*interp)
else:
return [QtGui.QColor(*x) for x in interp]
else:
return interp
def mapToQColor(self, data):
return self.map(data, mode=self.QCOLOR)
def mapToByte(self, data):
return self.map(data, mode=self.BYTE)
def mapToFloat(self, data):
return self.map(data, mode=self.FLOAT)
def getGradient(self, p1=None, p2=None):
"""Return a QLinearGradient object."""
if p1 == None:
p1 = QtCore.QPointF(0,0)
if p2 == None:
p2 = QtCore.QPointF(self.pos.max()-self.pos.min(),0)
g = QtGui.QLinearGradient(p1, p2)
pos, color = self.getStops(mode=self.BYTE)
color = [QtGui.QColor(*x) for x in color]
g.setStops(zip(pos, color))
#if self.colorMode == 'rgb':
#ticks = self.listTicks()
#g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks])
#elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop
#ticks = self.listTicks()
#stops = []
#stops.append((ticks[0][1], ticks[0][0].color))
#for i in range(1,len(ticks)):
#x1 = ticks[i-1][1]
#x2 = ticks[i][1]
#dx = (x2-x1) / 10.
#for j in range(1,10):
#x = x1 + dx*j
#stops.append((x, self.getColor(x)))
#stops.append((x2, self.getColor(x2)))
#g.setStops(stops)
return g
def getColors(self, mode=None):
"""Return list of all colors converted to the specified mode.
If mode is None, then no conversion is done."""
if isinstance(mode, basestring):
mode = self.enumMap[mode.lower()]
color = self.color
if mode in [self.BYTE, self.QCOLOR] and color.dtype.kind == 'f':
color = (color * 255).astype(np.ubyte)
elif mode == self.FLOAT and color.dtype.kind != 'f':
color = color.astype(float) / 255.
if mode == self.QCOLOR:
color = [QtGui.QColor(*x) for x in color]
return color
def getStops(self, mode):
## Get fully-expanded set of RGBA stops in either float or byte mode.
if mode not in self.stopsCache:
color = self.color
if mode == self.BYTE and color.dtype.kind == 'f':
color = (color * 255).astype(np.ubyte)
elif mode == self.FLOAT and color.dtype.kind != 'f':
color = color.astype(float) / 255.
## to support HSV mode, we need to do a little more work..
#stops = []
#for i in range(len(self.pos)):
#pos = self.pos[i]
#color = color[i]
#imode = self.mode[i]
#if imode == self.RGB:
#stops.append((x,color))
#else:
#ns =
self.stopsCache[mode] = (self.pos, color)
return self.stopsCache[mode]
#def getColor(self, x, toQColor=True):
#"""
#Return a color for a given value.
#============= ==================================================================
#**Arguments**
#x Value (position on gradient) of requested color.
#toQColor If true, returns a QColor object, else returns a (r,g,b,a) tuple.
#============= ==================================================================
#"""
#ticks = self.listTicks()
#if x <= ticks[0][1]:
#c = ticks[0][0].color
#if toQColor:
#return QtGui.QColor(c) # always copy colors before handing them out
#else:
#return (c.red(), c.green(), c.blue(), c.alpha())
#if x >= ticks[-1][1]:
#c = ticks[-1][0].color
#if toQColor:
#return QtGui.QColor(c) # always copy colors before handing them out
#else:
#return (c.red(), c.green(), c.blue(), c.alpha())
#x2 = ticks[0][1]
#for i in range(1,len(ticks)):
#x1 = x2
#x2 = ticks[i][1]
#if x1 <= x and x2 >= x:
#break
#dx = (x2-x1)
#if dx == 0:
#f = 0.
#else:
#f = (x-x1) / dx
#c1 = ticks[i-1][0].color
#c2 = ticks[i][0].color
#if self.colorMode == 'rgb':
#r = c1.red() * (1.-f) + c2.red() * f
#g = c1.green() * (1.-f) + c2.green() * f
#b = c1.blue() * (1.-f) + c2.blue() * f
#a = c1.alpha() * (1.-f) + c2.alpha() * f
#if toQColor:
#return QtGui.QColor(int(r), int(g), int(b), int(a))
#else:
#return (r,g,b,a)
#elif self.colorMode == 'hsv':
#h1,s1,v1,_ = c1.getHsv()
#h2,s2,v2,_ = c2.getHsv()
#h = h1 * (1.-f) + h2 * f
#s = s1 * (1.-f) + s2 * f
#v = v1 * (1.-f) + v2 * f
#c = QtGui.QColor()
#c.setHsv(h,s,v)
#if toQColor:
#return c
#else:
#return (c.red(), c.green(), c.blue(), c.alpha())
def getLookupTable(self, start=0.0, stop=1.0, nPts=512, alpha=None, mode='byte'):
"""
Return an RGB(A) lookup table (ndarray).
============= ============================================================================
**Arguments**
nPts The number of points in the returned lookup table.
alpha True, False, or None - Specifies whether or not alpha values are included
in the table. If alpha is None, it will be automatically determined.
============= ============================================================================
"""
if isinstance(mode, basestring):
mode = self.enumMap[mode.lower()]
if alpha is None:
alpha = self.usesAlpha()
x = np.linspace(start, stop, nPts)
table = self.map(x, mode)
if not alpha:
return table[:,:3]
else:
return table
def usesAlpha(self):
"""Return True if any stops have an alpha < 255"""
max = 1.0 if self.color.dtype.kind == 'f' else 255
return np.any(self.color[:,3] != max)
def isMapTrivial(self):
"""Return True if the gradient has exactly two stops in it: black at 0.0 and white at 1.0"""
if len(self.pos) != 2:
return False
if self.pos[0] != 0.0 or self.pos[1] != 1.0:
return False
if self.color.dtype.kind == 'f':
return np.all(self.color == np.array([[0.,0.,0.,1.], [1.,1.,1.,1.]]))
else:
return np.all(self.color == np.array([[0,0,0,255], [255,255,255,255]]))
......@@ -917,3 +917,21 @@ def qObjectReport(verbose=False):
for t in typs:
print(count[t], "\t", t)
class PrintDetector(object):
def __init__(self):
self.stdout = sys.stdout
sys.stdout = self
def remove(self):
sys.stdout = self.stdout
def __del__(self):
self.remove()
def write(self, x):
self.stdout.write(x)
traceback.print_stack()
def flush(self):
self.stdout.flush()
\ No newline at end of file
......@@ -9,7 +9,8 @@ class Exporter(object):
"""
Abstract class used for exporting graphics to file / printer / whatever.
"""
allowCopy = False # subclasses set this to True if they can use the copy buffer
def __init__(self, item):
"""
Initialize with the item to be exported.
......@@ -25,10 +26,11 @@ class Exporter(object):
"""Return the parameters used to configure this exporter."""
raise Exception("Abstract method must be overridden in subclass.")
def export(self, fileName=None, toBytes=False):
def export(self, fileName=None, toBytes=False, copy=False):
"""
If *fileName* is None, pop-up a file dialog.
If *toString* is True, return a bytes object rather than writing to file.
If *toBytes* is True, return a bytes object rather than writing to file.
If *copy* is True, export to the copy buffer rather than writing to file.
"""
raise Exception("Abstract method must be overridden in subclass.")
......
......@@ -8,6 +8,8 @@ __all__ = ['ImageExporter']
class ImageExporter(Exporter):
Name = "Image File (PNG, TIF, JPG, ...)"
allowCopy = True
def __init__(self, item):
Exporter.__init__(self, item)
tr = self.getTargetRect()
......@@ -38,8 +40,8 @@ class ImageExporter(Exporter):
def parameters(self):
return self.params
def export(self, fileName=None):
if fileName is None:
def export(self, fileName=None, toBytes=False, copy=False):
if fileName is None and not toBytes and not copy:
filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()]
preferred = ['*.png', '*.tif', '*.jpg']
for p in preferred[::-1]:
......@@ -78,6 +80,12 @@ class ImageExporter(Exporter):
finally:
self.setExportMode(False)
painter.end()
self.png.save(fileName)
if copy:
QtGui.QApplication.clipboard().setImage(self.png)
elif toBytes:
return self.png
else:
self.png.save(fileName)
\ No newline at end of file
......@@ -11,6 +11,8 @@ __all__ = ['SVGExporter']
class SVGExporter(Exporter):
Name = "Scalable Vector Graphics (SVG)"
allowCopy=True
def __init__(self, item):
Exporter.__init__(self, item)
#tr = self.getTargetRect()
......@@ -37,8 +39,8 @@ class SVGExporter(Exporter):
def parameters(self):
return self.params
def export(self, fileName=None, toBytes=False):
if toBytes is False and fileName is None:
def export(self, fileName=None, toBytes=False, copy=False):
if toBytes is False and copy is False and fileName is None:
self.fileSaveDialog(filter="Scalable Vector Graphics (*.svg)")
return
#self.svg = QtSvg.QSvgGenerator()
......@@ -83,11 +85,16 @@ class SVGExporter(Exporter):
xml = generateSvg(self.item)
if toBytes:
return bytes(xml)
return xml.encode('UTF-8')
elif copy:
md = QtCore.QMimeData()
md.setData('image/svg+xml', QtCore.QByteArray(xml.encode('UTF-8')))
QtGui.QApplication.clipboard().setMimeData(md)
else:
with open(fileName, 'w') as fh:
fh.write(xml.encode('UTF-8'))
xmlHeader = """\
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.2" baseProfile="tiny">
......@@ -148,7 +155,7 @@ def _generateItemSvg(item, nodes=None, root=None):
##
## Both 2 and 3 can be addressed by drawing all items in world coordinates.
prof = pg.debug.Profiler('generateItemSvg %s' % str(item), disabled=True)
if nodes is None: ## nodes maps all node IDs to their XML element.
## this allows us to ensure all elements receive unique names.
......@@ -170,8 +177,12 @@ def _generateItemSvg(item, nodes=None, root=None):
tr = QtGui.QTransform()
if isinstance(item, QtGui.QGraphicsScene):
xmlStr = "<g>\n</g>\n"
doc = xml.parseString(xmlStr)
childs = [i for i in item.items() if i.parentItem() is None]
elif item.__class__.paint == QtGui.QGraphicsItem.paint:
xmlStr = "<g>\n</g>\n"
doc = xml.parseString(xmlStr)
childs = item.childItems()
else:
childs = item.childItems()
tr = itemTransform(item, item.scene())
......@@ -223,11 +234,12 @@ def _generateItemSvg(item, nodes=None, root=None):
print(doc.toxml())
raise
prof.mark('render')
## Get rid of group transformation matrices by applying
## transformation to inner coordinates
correctCoordinates(g1, item)
prof.mark('correct')
## make sure g1 has the transformation matrix
#m = (tr.m11(), tr.m12(), tr.m21(), tr.m22(), tr.m31(), tr.m32())
#g1.setAttribute('transform', "matrix(%f,%f,%f,%f,%f,%f)" % m)
......@@ -277,6 +289,8 @@ def _generateItemSvg(item, nodes=None, root=None):
childGroup = g1.ownerDocument.createElement('g')
childGroup.setAttribute('clip-path', 'url(#%s)' % clip)
g1.appendChild(childGroup)
prof.mark('clipping')
## Add all child items as sub-elements.
childs.sort(key=lambda c: c.zValue())
for ch in childs:
......@@ -284,7 +298,8 @@ def _generateItemSvg(item, nodes=None, root=None):
if cg is None:
continue
childGroup.appendChild(cg) ### this isn't quite right--some items draw below their parent (good enough for now)
prof.mark('children')
prof.finish()
return g1
def correctCoordinates(node, item):
......
......@@ -683,7 +683,7 @@ class AxisItem(GraphicsWidget):
if tickPositions[i][j] is None:
strings[j] = None
textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, s) for s in strings if s is not None])
textRects.extend([p.boundingRect(QtCore.QRectF(0, 0, 100, 100), QtCore.Qt.AlignCenter, str(s)) for s in strings if s is not None])
if i > 0: ## always draw top level
## measure all text, make sure there's enough room
if axis == 0:
......@@ -699,8 +699,9 @@ class AxisItem(GraphicsWidget):