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): ...@@ -15,6 +15,7 @@ class PlotData(object):
- removal of nan/inf values - removal of nan/inf values
- option for single value shared by entire column - option for single value shared by entire column
- cached downsampling - cached downsampling
- cached min / max / hasnan / isuniform
""" """
def __init__(self): def __init__(self):
self.fields = {} self.fields = {}
......
...@@ -80,6 +80,12 @@ class Point(QtCore.QPointF): ...@@ -80,6 +80,12 @@ class Point(QtCore.QPointF):
def __div__(self, a): def __div__(self, a):
return self._math_('__div__', 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): def __rpow__(self, a):
return self._math_('__rpow__', a) return self._math_('__rpow__', a)
...@@ -146,4 +152,4 @@ class Point(QtCore.QPointF): ...@@ -146,4 +152,4 @@ class Point(QtCore.QPointF):
return Point(self) return Point(self)
def toQPoint(self): def toQPoint(self):
return QtCore.QPoint(*self) return QtCore.QPoint(*self)
\ No newline at end of file
...@@ -130,11 +130,14 @@ class SRTTransform(QtGui.QTransform): ...@@ -130,11 +130,14 @@ class SRTTransform(QtGui.QTransform):
self._state['angle'] = angle self._state['angle'] = angle
self.update() self.update()
def __div__(self, t): def __truediv__(self, t):
"""A / B == B^-1 * A""" """A / B == B^-1 * A"""
dt = t.inverted()[0] * self dt = t.inverted()[0] * self
return SRTTransform(dt) return SRTTransform(dt)
def __div__(self, t):
return self.__truediv__(t)
def __mul__(self, t): def __mul__(self, t):
return SRTTransform(QtGui.QTransform.__mul__(self, t)) return SRTTransform(QtGui.QTransform.__mul__(self, t))
......
...@@ -123,7 +123,6 @@ class SRTTransform3D(pg.Transform3D): ...@@ -123,7 +123,6 @@ class SRTTransform3D(pg.Transform3D):
m = self.matrix().reshape(4,4) m = self.matrix().reshape(4,4)
## translation is 4th column ## translation is 4th column
self._state['pos'] = m[:3,3] self._state['pos'] = m[:3,3]
## scale is vector-length of first three columns ## scale is vector-length of first three columns
scale = (m[:3,:3]**2).sum(axis=0)**0.5 scale = (m[:3,:3]**2).sum(axis=0)**0.5
## see whether there is an inversion ## see whether there is an inversion
...@@ -141,18 +140,30 @@ class SRTTransform3D(pg.Transform3D): ...@@ -141,18 +140,30 @@ class SRTTransform3D(pg.Transform3D):
print("Scale: %s" % str(scale)) print("Scale: %s" % str(scale))
print("Original matrix: %s" % str(m)) print("Original matrix: %s" % str(m))
raise raise
eigIndex = np.argwhere(np.abs(evals-1) < 1e-7) eigIndex = np.argwhere(np.abs(evals-1) < 1e-6)
if len(eigIndex) < 1: if len(eigIndex) < 1:
print("eigenvalues: %s" % str(evals)) print("eigenvalues: %s" % str(evals))
print("eigenvectors: %s" % str(evecs)) print("eigenvectors: %s" % str(evecs))
print("index: %s, %s" % (str(eigIndex), str(evals-1))) print("index: %s, %s" % (str(eigIndex), str(evals-1)))
raise Exception("Could not determine rotation axis.") 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 axis /= ((axis**2).sum())**0.5
self._state['axis'] = axis self._state['axis'] = axis
## trace(r) == 2 cos(angle) + 1, so: ## 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: if self._state['angle'] == 0:
self._state['axis'] = (0,0,1) self._state['axis'] = (0,0,1)
......
...@@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola ...@@ -5,7 +5,7 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation. 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 import numpy as np
class Vector(QtGui.QVector3D): class Vector(QtGui.QVector3D):
...@@ -33,7 +33,13 @@ class Vector(QtGui.QVector3D): ...@@ -33,7 +33,13 @@ class Vector(QtGui.QVector3D):
def __len__(self): def __len__(self):
return 3 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): #def __reduce__(self):
#return (Point, (self.x(), self.y())) #return (Point, (self.x(), self.y()))
......
...@@ -54,6 +54,8 @@ CONFIG_OPTIONS = { ...@@ -54,6 +54,8 @@ CONFIG_OPTIONS = {
'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets 'editorCommand': None, ## command used to invoke code editor from ConsoleWidgets
'useWeave': True, ## Use weave to speed up some operations, if it is available 'useWeave': True, ## Use weave to speed up some operations, if it is available
'weaveDebug': False, ## Print full error message if weave compile fails '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=()): ...@@ -137,7 +139,7 @@ def importModules(path, globals, locals, excludes=()):
d = os.path.join(os.path.split(globals['__file__'])[0], path) d = os.path.join(os.path.split(globals['__file__'])[0], path)
files = set() files = set()
for f in frozenSupport.listdir(d): 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) files.add(f)
elif f[-3:] == '.py' and f != '__init__.py': elif f[-3:] == '.py' and f != '__init__.py':
files.add(f[:-3]) files.add(f[:-3])
...@@ -152,7 +154,8 @@ def importModules(path, globals, locals, excludes=()): ...@@ -152,7 +154,8 @@ def importModules(path, globals, locals, excludes=()):
try: try:
if len(path) > 0: if len(path) > 0:
modName = path + '.' + modName modName = path + '.' + modName
mod = __import__(modName, globals, locals, fromlist=['*']) #mod = __import__(modName, globals, locals, fromlist=['*'])
mod = __import__(modName, globals, locals, ['*'], 1)
mods[modName] = mod mods[modName] = mod
except: except:
import traceback import traceback
...@@ -175,7 +178,8 @@ def importAll(path, globals, locals, excludes=()): ...@@ -175,7 +178,8 @@ def importAll(path, globals, locals, excludes=()):
globals[k] = getattr(mod, k) globals[k] = getattr(mod, k)
importAll('graphicsItems', globals(), locals()) importAll('graphicsItems', globals(), locals())
importAll('widgets', globals(), locals(), excludes=['MatplotlibWidget', 'RemoteGraphicsView']) importAll('widgets', globals(), locals(),
excludes=['MatplotlibWidget', 'RawImageWidget', 'RemoteGraphicsView'])
from .imageview import * from .imageview import *
from .WidgetGroup import * from .WidgetGroup import *
...@@ -190,9 +194,20 @@ from .SignalProxy import * ...@@ -190,9 +194,20 @@ from .SignalProxy import *
from .colormap import * from .colormap import *
from .ptime import time 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 import atexit
def cleanup(): def cleanup():
if not getConfigOption('exitCleanup'):
return
ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore. ViewBox.quit() ## tell ViewBox that it doesn't need to deregister views anymore.
## Workaround for Qt exit crash: ## Workaround for Qt exit crash:
...@@ -212,6 +227,38 @@ def cleanup(): ...@@ -212,6 +227,38 @@ def cleanup():
atexit.register(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 ## Convenience functions for command-line use
...@@ -235,7 +282,7 @@ def plot(*args, **kargs): ...@@ -235,7 +282,7 @@ def plot(*args, **kargs):
#if len(args)+len(kargs) > 0: #if len(args)+len(kargs) > 0:
#w.plot(*args, **kargs) #w.plot(*args, **kargs)
pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom'] pwArgList = ['title', 'labels', 'name', 'left', 'right', 'top', 'bottom', 'background']
pwArgs = {} pwArgs = {}
dataArgs = {} dataArgs = {}
for k in kargs: for k in kargs:
...@@ -265,13 +312,15 @@ def image(*args, **kargs): ...@@ -265,13 +312,15 @@ def image(*args, **kargs):
return w return w
show = image ## for backward compatibility show = image ## for backward compatibility
def dbg(): def dbg(*args, **kwds):
""" """
Create a console window and begin watching for exceptions. Create a console window and begin watching for exceptions.
All arguments are passed to :func:`ConsoleWidget.__init__() <pyqtgraph.console.ConsoleWidget.__init__>`.
""" """
mkQApp() mkQApp()
import console from . import console
c = console.ConsoleWidget() c = console.ConsoleWidget(*args, **kwds)
c.catchAllExceptions() c.catchAllExceptions()
c.show() c.show()
global consoles global consoles
......
...@@ -169,7 +169,7 @@ class ConsoleWidget(QtGui.QWidget): ...@@ -169,7 +169,7 @@ class ConsoleWidget(QtGui.QWidget):
def execMulti(self, nextLine): def execMulti(self, nextLine):
self.stdout.write(nextLine+"\n") #self.stdout.write(nextLine+"\n")
if nextLine.strip() != '': if nextLine.strip() != '':
self.multiline += "\n" + nextLine self.multiline += "\n" + nextLine
return return
...@@ -372,4 +372,4 @@ class ConsoleWidget(QtGui.QWidget): ...@@ -372,4 +372,4 @@ class ConsoleWidget(QtGui.QWidget):
return False return False
return True return True
\ No newline at end of file
...@@ -28,6 +28,15 @@ def ftrace(func): ...@@ -28,6 +28,15 @@ def ftrace(func):
return rv return rv
return w 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='| '): def getExc(indent=4, prefix='| '):
tb = traceback.format_exc() tb = traceback.format_exc()
lines = [] lines = []
......
...@@ -209,6 +209,13 @@ class Dock(QtGui.QWidget, DockDrop): ...@@ -209,6 +209,13 @@ class Dock(QtGui.QWidget, DockDrop):
self.setOrientation(force=True) 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): def __repr__(self):
return "<Dock %s %s>" % (self.name(), self.stretch()) return "<Dock %s %s>" % (self.name(), self.stretch())
......
...@@ -40,11 +40,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop): ...@@ -40,11 +40,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
Arguments: Arguments:
dock The new Dock object to add. If None, then a new Dock will be dock The new Dock object to add. If None, then a new Dock will be
created. 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 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 entire edge of the window. If relativeTo is another Dock, then
the new Dock is placed adjacent to it (or in a tabbed 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 All extra keyword arguments are passed to Dock.__init__() if *dock* is
...@@ -316,4 +316,4 @@ class DockArea(Container, QtGui.QWidget, DockDrop): ...@@ -316,4 +316,4 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
DockDrop.dropEvent(self, *args) DockDrop.dropEvent(self, *args)
\ No newline at end of file
from pyqtgraph.widgets.FileDialog import FileDialog from pyqtgraph.widgets.FileDialog import FileDialog
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore, QtSvg from pyqtgraph.Qt import QtGui, QtCore, QtSvg
from pyqtgraph.python2_3 import asUnicode
import os, re import os, re
LastExportDirectory = None LastExportDirectory = None
...@@ -56,13 +57,13 @@ class Exporter(object): ...@@ -56,13 +57,13 @@ class Exporter(object):
return return
def fileSaveFinished(self, fileName): def fileSaveFinished(self, fileName):
fileName = str(fileName) fileName = asUnicode(fileName)
global LastExportDirectory global LastExportDirectory
LastExportDirectory = os.path.split(fileName)[0] LastExportDirectory = os.path.split(fileName)[0]
## If file name does not match selected extension, append it now ## If file name does not match selected extension, append it now
ext = os.path.splitext(fileName)[1].lower().lstrip('.') 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: if selectedExt is not None:
selectedExt = selectedExt.groups()[0].lower() selectedExt = selectedExt.groups()[0].lower()
if ext != selectedExt: if ext != selectedExt:
...@@ -118,7 +119,7 @@ class Exporter(object): ...@@ -118,7 +119,7 @@ class Exporter(object):
else: else:
childs = root.childItems() childs = root.childItems()
rootItem = [root] rootItem = [root]
childs.sort(lambda a,b: cmp(a.zValue(), b.zValue())) childs.sort(key=lambda a: a.zValue())
while len(childs) > 0: while len(childs) > 0:
ch = childs.pop(0) ch = childs.pop(0)
tree = self.getPaintItems(ch) tree = self.getPaintItems(ch)
......
from .Exporter import Exporter from .Exporter import Exporter
from pyqtgraph.parametertree import Parameter 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 pyqtgraph as pg
import numpy as np import numpy as np
...@@ -17,7 +17,11 @@ class ImageExporter(Exporter): ...@@ -17,7 +17,11 @@ class ImageExporter(Exporter):
scene = item.scene() scene = item.scene()
else: else:
scene = item 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=[ self.params = Parameter(name='params', type='group', children=[
{'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)}, {'name': 'width', 'type': 'int', 'value': tr.width(), 'limits': (0, None)},
{'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)}, {'name': 'height', 'type': 'int', 'value': tr.height(), 'limits': (0, None)},
...@@ -42,7 +46,10 @@ class ImageExporter(Exporter): ...@@ -42,7 +46,10 @@ class ImageExporter(Exporter):
def export(self, fileName=None, toBytes=False, copy=False): def export(self, fileName=None, toBytes=False, copy=False):
if fileName is None and not toBytes and not copy: 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'] preferred = ['*.png', '*.tif', '*.jpg']
for p in preferred[::-1]: for p in preferred[::-1]:
if p in filter: if p in filter:
...@@ -57,6 +64,9 @@ class ImageExporter(Exporter): ...@@ -57,6 +64,9 @@ class ImageExporter(Exporter):
#self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32) #self.png = QtGui.QImage(targetRect.size(), QtGui.QImage.Format_ARGB32)
#self.png.fill(pyqtgraph.mkColor(self.params['background'])) #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) bg = np.empty((self.params['width'], self.params['height'], 4), dtype=np.ubyte)
color = self.params['background'] color = self.params['background']
bg[:,:,0] = color.blue() bg[:,:,0] = color.blue()
......
from .Exporter import Exporter from .Exporter import Exporter
from pyqtgraph.python2_3 import asUnicode
from pyqtgraph.parametertree import Parameter from pyqtgraph.parametertree import Parameter
from pyqtgraph.Qt import QtGui, QtCore, QtSvg from pyqtgraph.Qt import QtGui, QtCore, QtSvg
import pyqtgraph as pg import pyqtgraph as pg
...@@ -91,8 +92,8 @@ class SVGExporter(Exporter): ...@@ -91,8 +92,8 @@ class SVGExporter(Exporter):
md.setData('image/svg+xml', QtCore.QByteArray(xml.encode('UTF-8'))) md.setData('image/svg+xml', QtCore.QByteArray(xml.encode('UTF-8')))
QtGui.QApplication.clipboard().setMimeData(md) QtGui.QApplication.clipboard().setMimeData(md)
else: else:
with open(fileName, 'w') as fh: with open(fileName, 'wb') as fh:
fh.write(xml.encode('UTF-8')) fh.write(asUnicode(xml).encode('utf-8'))
xmlHeader = """\ xmlHeader = """\
...@@ -221,8 +222,8 @@ def _generateItemSvg(item, nodes=None, root=None): ...@@ -221,8 +222,8 @@ def _generateItemSvg(item, nodes=None, root=None):
## this is taken care of in generateSvg instead. ## this is taken care of in generateSvg instead.
#if hasattr(item, 'setExportMode'): #if hasattr(item, 'setExportMode'):
#item.setExportMode(False) #item.setExportMode(False)
xmlStr = str(arr) xmlStr = bytes(arr).decode('utf-8')
doc = xml.parseString(xmlStr) doc = xml.parseString(xmlStr)
try: try:
...@@ -304,14 +305,43 @@ def _generateItemSvg(item, nodes=None, root=None): ...@@ -304,14 +305,43 @@ def _generateItemSvg(item, nodes=None, root=None):
def correctCoordinates(node, item): def correctCoordinates(node, item):
## 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 inner group contains one or more drawing primitives, possibly of different types.
groups = node.getElementsByTagName('g') 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':