Commit 757dc504 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Merge tag 'pyqtgraph-0.9.8' into pyqtgraph-core

parents ef0ee7c6 5309483a
......@@ -15,6 +15,7 @@ class PlotData(object):
- removal of nan/inf values
- option for single value shared by entire column
- cached downsampling
- cached min / max / hasnan / isuniform
"""
def __init__(self):
self.fields = {}
......
......@@ -80,6 +80,12 @@ class Point(QtCore.QPointF):
def __div__(self, a):
return self._math_('__div__', a)
def __truediv__(self, a):
return self._math_('__truediv__', a)
def __rtruediv__(self, a):
return self._math_('__rtruediv__', a)
def __rpow__(self, a):
return self._math_('__rpow__', a)
......@@ -146,4 +152,4 @@ class Point(QtCore.QPointF):
return Point(self)
def toQPoint(self):
return QtCore.QPoint(*self)
\ No newline at end of file
return QtCore.QPoint(*self)
......@@ -130,11 +130,14 @@ class SRTTransform(QtGui.QTransform):
self._state['angle'] = angle
self.update()
def __div__(self, t):
def __truediv__(self, t):
"""A / B == B^-1 * A"""
dt = t.inverted()[0] * self
return SRTTransform(dt)
def __div__(self, t):
return self.__truediv__(t)
def __mul__(self, t):
return SRTTransform(QtGui.QTransform.__mul__(self, t))
......
......@@ -123,7 +123,6 @@ class SRTTransform3D(pg.Transform3D):
m = self.matrix().reshape(4,4)
## translation is 4th column
self._state['pos'] = m[:3,3]
## scale is vector-length of first three columns
scale = (m[:3,:3]**2).sum(axis=0)**0.5
## see whether there is an inversion
......@@ -141,18 +140,30 @@ class SRTTransform3D(pg.Transform3D):
print("Scale: %s" % str(scale))
print("Original matrix: %s" % str(m))
raise
eigIndex = np.argwhere(np.abs(evals-1) < 1e-7)
eigIndex = np.argwhere(np.abs(evals-1) < 1e-6)
if len(eigIndex) < 1:
print("eigenvalues: %s" % str(evals))
print("eigenvectors: %s" % str(evecs))
print("index: %s, %s" % (str(eigIndex), str(evals-1)))
raise Exception("Could not determine rotation axis.")
axis = evecs[eigIndex[0,0]].real
axis = evecs[:,eigIndex[0,0]].real
axis /= ((axis**2).sum())**0.5
self._state['axis'] = axis
## trace(r) == 2 cos(angle) + 1, so:
self._state['angle'] = np.arccos((r.trace()-1)*0.5) * 180 / np.pi
cos = (r.trace()-1)*0.5 ## this only gets us abs(angle)
## The off-diagonal values can be used to correct the angle ambiguity,
## but we need to figure out which element to use:
axisInd = np.argmax(np.abs(axis))
rInd,sign = [((1,2), -1), ((0,2), 1), ((0,1), -1)][axisInd]
## Then we have r-r.T = sin(angle) * 2 * sign * axis[axisInd];
## solve for sin(angle)
sin = (r-r.T)[rInd] / (2. * sign * axis[axisInd])
## finally, we get the complete angle from arctan(sin/cos)
self._state['angle'] = np.arctan2(sin, cos) * 180 / np.pi
if self._state['angle'] == 0:
self._state['axis'] = (0,0,1)
......
......@@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
"""
from .Qt import QtGui, QtCore
from .Qt import QtGui, QtCore, USE_PYSIDE
import numpy as np
class Vector(QtGui.QVector3D):
......@@ -33,7 +33,13 @@ class Vector(QtGui.QVector3D):
def __len__(self):
return 3
def __add__(self, b):
# workaround for pyside bug. see https://bugs.launchpad.net/pyqtgraph/+bug/1223173
if USE_PYSIDE and isinstance(b, QtGui.QVector3D):
b = Vector(b)
return QtGui.QVector3D.__add__(self, b)
#def __reduce__(self):
#return (Point, (self.x(), self.y()))
......
......@@ -54,6 +54,8 @@ CONFIG_OPTIONS = {
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
'useWeave': True, ## Use weave to speed up some operations, if it is available
'weaveDebug': False, ## Print full error message if weave compile fails
'exitCleanup': True, ## Attempt to work around some exit crash bugs in PyQt and PySide
'enableExperimental': False, ## Enable experimental features (the curious can search for this key in the code)
}
......@@ -137,7 +139,7 @@ def importModules(path, globals, locals, excludes=()):
d = os.path.join(os.path.split(globals['__file__'])[0], path)
files = set()
for f in frozenSupport.listdir(d):
if frozenSupport.isdir(os.path.join(d, f)) and f != '__pycache__':
if frozenSupport.isdir(os.path.join(d, f)) and f not in ['__pycache__', 'tests']:
files.add(f)
elif f[-3:] == '.py' and f != '__init__.py':
files.add(f[:-3])
......@@ -152,7 +154,8 @@ def importModules(path, globals, locals, excludes=()):
try:
if len(path) > 0:
modName = path + '.' + modName
mod = __import__(modName, globals, locals, fromlist=['*'])
#mod = __import__(modName, globals, locals, fromlist=['*'])
mod = __import__(modName, globals, locals, ['*'], 1)
mods[modName] = mod
except:
import traceback
......@@ -175,7 +178,8 @@ def importAll(path, globals, locals, excludes=()):
globals[k] = getattr(mod, k)
importAll('graphicsItems', globals(), locals())
importAll('widgets', globals(), locals(), excludes=['MatplotlibWidget', 'RemoteGraphicsView'])
importAll('widgets', globals(), locals(),
excludes=['MatplotlibWidget', 'RawImageWidget', 'RemoteGraphicsView'])
from .imageview import *
from .WidgetGroup import *
......@@ -190,9 +194,20 @@ from .SignalProxy import *
from .colormap import *
from .ptime import time
##############################################################
## PyQt and PySide both are prone to crashing on exit.
## There are two general approaches to dealing with this:
## 1. Install atexit handlers that assist in tearing down to avoid crashes.
## This helps, but is never perfect.
## 2. Terminate the process before python starts tearing down
## This is potentially dangerous
## Attempts to work around exit crashes:
import atexit
def cleanup():
if not getConfigOption('exitCleanup'):
return
ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore.
## Workaround for Qt exit crash:
......@@ -212,6 +227,38 @@ def cleanup():
atexit.register(cleanup)
## Optional function for exiting immediately (with some manual teardown)
def exit():
"""
Causes python to exit without garbage-collecting any objects, and thus avoids
calling object destructor methods. This is a sledgehammer workaround for
a variety of bugs in PyQt and Pyside that cause crashes on exit.
This function does the following in an attempt to 'safely' terminate
the process:
* Invoke atexit callbacks
* Close all open file handles
* os._exit()
Note: there is some potential for causing damage with this function if you
are using objects that _require_ their destructors to be called (for example,
to properly terminate log files, disconnect from devices, etc). Situations
like this are probably quite rare, but use at your own risk.
"""
## first disable our own cleanup function; won't be needing it.
setConfigOptions(exitCleanup=False)
## invoke atexit callbacks
atexit._run_exitfuncs()
## close file handles
os.closerange(3, 4096) ## just guessing on the maximum descriptor count..
os._exit(0)
## Convenience functions for command-line use
......@@ -235,7 +282,7 @@ def plot(*args, **kargs):
#if len(args)+len(kargs) > 0:
#w.plot(*args, **kargs)
pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom']
pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom', 'background']
pwArgs = {}
dataArgs = {}
for k in kargs:
......@@ -265,13 +312,15 @@ def image(*args, **kargs):
return w
show = image ## for backward compatibility
def dbg():
def dbg(*args, **kwds):
"""
Create a console window and begin watching for exceptions.
All arguments are passed to :func:`ConsoleWidget.__init__() <pyqtgraph.console.ConsoleWidget.__init__>`.
"""
mkQApp()
import console
c = console.ConsoleWidget()
from . import console
c = console.ConsoleWidget(*args, **kwds)
c.catchAllExceptions()
c.show()
global consoles
......
......@@ -169,7 +169,7 @@ class ConsoleWidget(QtGui.QWidget):
def execMulti(self, nextLine):
self.stdout.write(nextLine+"\n")
#self.stdout.write(nextLine+"\n")
if nextLine.strip() != '':
self.multiline += "\n" + nextLine
return
......@@ -372,4 +372,4 @@ class ConsoleWidget(QtGui.QWidget):
return False
return True
\ No newline at end of file
......@@ -28,6 +28,15 @@ def ftrace(func):
return rv
return w
def warnOnException(func):
"""Decorator which catches/ignores exceptions and prints a stack trace."""
def w(*args, **kwds):
try:
func(*args, **kwds)
except:
printExc('Ignored exception:')
return w
def getExc(indent=4, prefix='| '):
tb = traceback.format_exc()
lines = []
......
......@@ -209,6 +209,13 @@ class Dock(QtGui.QWidget, DockDrop):
self.setOrientation(force=True)
def close(self):
"""Remove this dock from the DockArea it lives inside."""
self.setParent(None)
self.label.setParent(None)
self._container.apoptose()
self._container = None
def __repr__(self):
return "<Dock %s %s>" % (self.name(), self.stretch())
......
......@@ -40,11 +40,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
Arguments:
dock The new Dock object to add. If None, then a new Dock will be
created.
position 'bottom', 'top', 'left', 'right', 'over', or 'under'
position 'bottom', 'top', 'left', 'right', 'above', or 'below'
relativeTo If relativeTo is None, then the new Dock is added to fill an
entire edge of the window. If relativeTo is another Dock, then
the new Dock is placed adjacent to it (or in a tabbed
configuration for 'over' and 'under').
configuration for 'above' and 'below').
=========== =================================================================
All extra keyword arguments are passed to Dock.__init__() if *dock* is
......@@ -316,4 +316,4 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
DockDrop.dropEvent(self, *args)
\ No newline at end of file
from pyqtgraph.widgets.FileDialog import FileDialog
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
from pyqtgraph.python2_3 import asUnicode
import os, re
LastExportDirectory = None
......@@ -56,13 +57,13 @@ class Exporter(object):
return
def fileSaveFinished(self, fileName):
fileName = str(fileName)
fileName = asUnicode(fileName)
global LastExportDirectory
LastExportDirectory = os.path.split(fileName)[0]
## If file name does not match selected extension, append it now
ext = os.path.splitext(fileName)[1].lower().lstrip('.')
selectedExt = re.search(r'\*\.(\w+)\b', str(self.fileDialog.selectedNameFilter()))
selectedExt = re.search(r'\*\.(\w+)\b', asUnicode(self.fileDialog.selectedNameFilter()))
if selectedExt is not None:
selectedExt = selectedExt.groups()[0].lower()
if ext != selectedExt:
......@@ -118,7 +119,7 @@ class Exporter(object):
else:
childs = root.childItems()
rootItem = [root]
childs.sort(lambda a,b: cmp(a.zValue(), b.zValue()))
childs.sort(key=lambda a: a.zValue())
while len(childs) > 0:
ch = childs.pop(0)
tree = self.getPaintItems(ch)
......
from .Exporter import Exporter
from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
from pyqtgraph.Qt import QtGui, QtCore, QtSvg, USE_PYSIDE
import pyqtgraph as pg
import numpy as np
......@@ -17,7 +17,11 @@ class ImageExporter(Exporter):
scene = item.scene()
else:
scene = item
bg = scene.views()[0].backgroundBrush().color()
bgbrush = scene.views()[0].backgroundBrush()
bg = bgbrush.color()
if bgbrush.style() == QtCore.Qt.NoBrush:
bg.setAlpha(0)
self.params = Parameter(name='params', type='group', children=[
{'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)},
{'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)},
......@@ -42,7 +46,10 @@ class ImageExporter(Exporter):
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()]
if USE_PYSIDE:
filter = ["*."+str(f) for f in QtGui.QImageWriter.supportedImageFormats()]
else:
filter = ["*."+bytes(f).decode('utf-8') for f in QtGui.QImageWriter.supportedImageFormats()]
preferred = ['*.png', '*.tif', '*.jpg']
for p in preferred[::-1]:
if p in filter:
......@@ -57,6 +64,9 @@ class ImageExporter(Exporter):
#self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32)
#self.png.fill(pyqtgraph.mkColor(self.params['background']))
w, h = self.params['width'], self.params['height']
if w == 0 or h == 0:
raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h))
bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte)
color = self.params['background']
bg[:,:,0] = color.blue()
......
from .Exporter import Exporter
from pyqtgraph.python2_3 import asUnicode
from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import pyqtgraph as pg
......@@ -91,8 +92,8 @@ class SVGExporter(Exporter):
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'))
with open(fileName, 'wb') as fh:
fh.write(asUnicode(xml).encode('utf-8'))
xmlHeader = """\
......@@ -221,8 +222,8 @@ def _generateItemSvg(item, nodes=None, root=None):
## this is taken care of in generateSvg instead.
#if hasattr(item, 'setExportMode'):
#item.setExportMode(False)
xmlStr = str(arr)
xmlStr = bytes(arr).decode('utf-8')
doc = xml.parseString(xmlStr)
try:
......@@ -304,14 +305,43 @@ def _generateItemSvg(item, nodes=None, root=None):
def correctCoordinates(node, item):
## Remove transformation matrices from <g> tags by applying matrix to coordinates inside.
## Each item is represented by a single top-level group with one or more groups inside.
## Each inner group contains one or more drawing primitives, possibly of different types.
groups = node.getElementsByTagName('g')
## Since we leave text unchanged, groups which combine text and non-text primitives must be split apart.
## (if at some point we start correcting text transforms as well, then it should be safe to remove this)
groups2 = []
for grp in groups:
subGroups = [grp.cloneNode(deep=False)]
textGroup = None
for ch in grp.childNodes[:]:
if isinstance(ch, xml.Element):
if textGroup is None:
textGroup = ch.tagName == 'text'
if ch.tagName == 'text':
if textGroup is False:
subGroups.append(grp.cloneNode(deep=False))
textGroup = True
else:
if textGroup is True:
subGroups.append(grp.cloneNode(deep=False))
textGroup = False
subGroups[-1].appendChild(ch)
groups2.extend(subGroups)
for sg in subGroups:
node.insertBefore(sg, grp)
node.removeChild(grp)
groups = groups2
for grp in groups:
matrix = grp.getAttribute('transform')
match = re.match(r'matrix\((.*)\)', matrix)
if match is None:
vals = [1,0,0,1,0,0]
else:
vals = map(float, match.groups()[0].split(','))
vals = [float(a) for a in match.groups()[0].split(',')]
tr = np.array([[vals[0], vals[2], vals[4]], [vals[1], vals[3], vals[5]]])
removeTransform = False
......@@ -320,9 +350,9 @@ def correctCoordinates(node, item):
continue
if ch.tagName == 'polyline':
removeTransform = True
coords = np.array([map(float, c.split(',')) for c in ch.getAttribute('points').strip().split(' ')])
coords = np.array([[float(a) for a in c.split(',')] for c in ch.getAttribute('points').strip().split(' ')])
coords = pg.transformCoordinates(tr, coords, transpose=True)
ch.setAttribute('points', ' '.join([','.join(map(str, c)) for c in coords]))
ch.setAttribute('points', ' '.join([','.join([str(a) for a in c]) for c in coords]))
elif ch.tagName == 'path':
removeTransform = True
newCoords = ''
......@@ -374,7 +404,6 @@ def correctCoordinates(node, item):
if removeTransform:
grp.removeAttribute('transform')
def itemTransform(item, root):
## Return the transformation mapping item to root
......
......@@ -376,10 +376,10 @@ class Flowchart(Node):
#tdeps[t] = lastNode
if lastInd is not None:
dels.append((lastInd+1, t))
dels.sort(lambda a,b: cmp(b[0], a[0]))
#dels.sort(lambda a,b: cmp(b[0], a[0]))
dels.sort(key=lambda a: a[0], reverse=True)
for i, t in dels:
ops.insert(i, ('d', t))
return ops
......@@ -491,7 +491,8 @@ class Flowchart(Node):
self.clear()
Node.restoreState(self, state)
nodes = state['nodes']
nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0]))
#nodes.sort(lambda a, b: cmp(a['pos'][0], b['pos'][0]))
nodes.sort(key=lambda a: a['pos'][0])
for n in nodes:
if n['name'] in self._nodes:
#self._nodes[n['name']].graphicsItem().moveBy(*n['pos'])
......@@ -560,6 +561,7 @@ class Flowchart(Node):
self.fileDialog.fileSelected.connect(self.saveFile)
return
#fileName = QtGui.QFileDialog.getSaveFileName(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
fileName = str(fileName)
configfile.writeConfigFile(self.saveState(), fileName)
self.sigFileSaved.emit(fileName)
......@@ -681,7 +683,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile)
def fileSaved(self, fileName):
self.setCurrentFile(fileName)
self.setCurrentFile(str(fileName))
self.ui.saveBtn.success("Saved.")
def saveClicked(self):
......@@ -710,7 +712,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
#self.setCurrentFile(newFile)
def setCurrentFile(self, fileName):
self.currentFileName = fileName
self.currentFileName = str(fileName)
if fileName is None:
self.ui.fileNameLabel.setText("<b>[ new ]</b>")
else:
......
......@@ -521,6 +521,17 @@ class ConnectionItem(GraphicsObject):
self.target = target
self.length = 0
self.hovered = False
self.path = None
self.shapePath = None
self.style = {
'shape': 'line',
'color': (100, 100, 250),
'width': 1.0,
'hoverColor': (150, 150, 250),
'hoverWidth': 1.0,
'selectedColor': (200, 200, 0),
'selectedWidth': 3.0,
}
#self.line = QtGui.QGraphicsLineItem(self)
self.source.getViewBox().addItem(self)
self.updateLine()
......@@ -535,6 +546,13 @@ class ConnectionItem(GraphicsObject):
self.target = target
self.updateLine()
def setStyle(self, **kwds):
self.style.update(kwds)
if 'shape' in kwds:
self.updateLine()
else:
self.update()
def updateLine(self):
start = Point(self.source.connectPoint())
if isinstance(self.target, TerminalGraphicsItem):
......@@ -544,15 +562,21 @@ class ConnectionItem(GraphicsObject):
else:
return
self.prepareGeometryChange()
self.resetTransform()
ang = (stop-start).angle(Point(0, 1))
if ang is None:
ang = 0
self.rotate(ang)
self.setPos(start)
self.length = (start-stop).length()
self.path = self.generatePath(start, stop)
self.shapePath = None
self.update()
#self.line.setLine(start.x(), start.y(), stop.x(), stop.y())
def generatePath(self, start, stop):
path = QtGui.QPainterPath()
path.moveTo(start)
if self.style['shape'] == 'line':
path.lineTo(stop)
elif self.style['shape'] == 'cubic':
path.cubicTo(Point(stop.x(), start.y()), Point(start.x(), stop.y()), Point(stop.x(), stop.y()))
else:
raise Exception('Invalid shape "%s"; options are "line" or "cubic"' % self.style['shape'])
return path
def keyPressEvent(self, ev):
if ev.key() == QtCore.Qt.Key_Delete or ev.key() == QtCore.Qt.Key_Backspace:
......@@ -582,20 +606,33 @@ class ConnectionItem(GraphicsObject):
def boundingRect(self):
#return self.line.boundingRect()
px = self.pixelWidth()
return QtCore.QRectF(-5*px, 0, 10*px, self.length)
return self.shape().boundingRect()
##return self.line.boundingRect()
#px = self.pixelWidth()
#return QtCore.QRectF(-5*px, 0, 10*px, self.length)
def viewRangeChanged(self):