Merge pull request #1 from pyqtgraph/develop

Update to latest
This commit is contained in:
Matthew Shun-Shin 2018-03-12 14:59:57 +00:00 committed by GitHub
commit c8833e1d18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
106 changed files with 3345 additions and 1622 deletions

View File

@ -17,10 +17,10 @@ env:
# Enable python 2 and python 3 builds # Enable python 2 and python 3 builds
# Note that the 2.6 build doesn't get flake8, and runs old versions of # Note that the 2.6 build doesn't get flake8, and runs old versions of
# Pyglet and GLFW to make sure we deal with those correctly # Pyglet and GLFW to make sure we deal with those correctly
- PYTHON=2.6 QT=pyqt4 TEST=standard #- PYTHON=2.6 QT=pyqt4 TEST=standard # 2.6 support ended
- PYTHON=2.7 QT=pyqt4 TEST=extra - PYTHON=2.7 QT=pyqt4 TEST=extra
- PYTHON=2.7 QT=pyside TEST=standard - PYTHON=2.7 QT=pyside TEST=standard
- PYTHON=3.4 QT=pyqt5 TEST=standard - PYTHON=3.5 QT=pyqt5 TEST=standard
# - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda # - PYTHON=3.4 QT=pyside TEST=standard # pyside isn't available for 3.4 with conda
#- PYTHON=3.2 QT=pyqt5 TEST=standard #- PYTHON=3.2 QT=pyqt5 TEST=standard

View File

@ -1,3 +1,14 @@
pyqtgraph-0.11.0 (in development)
API / behavior changes:
- ArrowItem's `angle` option now rotates the arrow without affecting its coordinate system.
The result is visually the same, but children of ArrowItem are no longer rotated
(this allows screen-aligned text to be attached more easily).
To mimic the old behavior, use ArrowItem.rotate() instead of the `angle` argument.
- Deprecated graphicsWindow classes; these have been unnecessary for many years because
widgets can be placed into a new window just by calling show().
pyqtgraph-0.10.0 pyqtgraph-0.10.0
New Features: New Features:

View File

@ -1,9 +1,45 @@
Installation Installation
============ ============
PyQtGraph does not really require any installation scripts. All that is needed is for the pyqtgraph folder to be placed someplace importable. Most people will prefer to simply place this folder within a larger project folder. If you want to make pyqtgraph available system-wide, use one of the methods listed below: There are many different ways to install pyqtgraph, depending on your needs:
* **Debian, Ubuntu, and similar Linux:** Download the .deb file linked at the top of the pyqtgraph web page or install using apt by putting "deb http://luke.campagnola.me/debian dev/" in your /etc/apt/sources.list file and install the python-pyqtgraph package. * The most common way to install pyqtgraph is with pip::
* **Arch Linux:** Looks like someone has posted unofficial packages for Arch (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577)
* **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page. $ pip install pyqtgraph
* **Everybody (including OSX):** Download the .tar.gz source package linked at the top of the pyqtgraph web page, extract its contents, and run "python setup.py install" from within the extracted directory.
Some users may need to call ``pip3`` instead. This method should work on
all platforms.
* To get access to the very latest features and bugfixes, clone pyqtgraph from
github::
$ git clone https://github.com/pyqtgraph/pyqtgraph
Now you can install pyqtgraph from the source::
$ python setup.py install
..or you can simply place the pyqtgraph folder someplace importable, such as
inside the root of another project. PyQtGraph does not need to be "built" or
compiled in any way.
* Packages for pyqtgraph are also available in a few other forms:
* **Anaconda**: ``conda install pyqtgraph``
* **Debian, Ubuntu, and similar Linux:** Use ``apt install python-pyqtgraph`` or
download the .deb file linked at the top of the pyqtgraph web page.
* **Arch Linux:** has packages (thanks windel). (https://aur.archlinux.org/packages.php?ID=62577)
* **Windows:** Download and run the .exe installer file linked at the top of the pyqtgraph web page.
Requirements
============
PyQtGraph depends on:
* Python 2.7 or Python 3.x
* A Qt library such as PyQt4, PyQt5, or PySide
* numpy
The easiest way to meet these dependencies is with ``pip`` or with a scientific python
distribution like Anaconda.
.. _pyqtgraph: http://www.pyqtgraph.org/

View File

@ -9,11 +9,11 @@ Most applications that use pyqtgraph's data visualization will generate widgets
In pyqtgraph, most 2D visualizations follow the following mouse interaction: In pyqtgraph, most 2D visualizations follow the following mouse interaction:
* Left button: Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead. * **Left button:** Interacts with items in the scene (select/move objects, etc). If there are no movable objects under the mouse cursor, then dragging with the left button will pan the scene instead.
* Right button drag: Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis. * **Right button drag:** Scales the scene. Dragging left/right scales horizontally; dragging up/down scales vertically (although some scenes will have their x/y scales locked together). If there are x/y axes fisible in the scene, then right-dragging over the axis will _only_ affect that axis.
* Right button click: Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor. * **Right button click:** Clicking the right button in most cases will show a context menu with a variety of options depending on the object(s) under the mouse cursor.
* Middle button (or wheel) drag: Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene). * **Middle button (or wheel) drag:** Dragging the mouse with the wheel pressed down will always pan the scene (this is useful in instances where panning with the left button is prevented by other objects in the scene).
* Wheel spin: Zooms the scene in and out. * **Wheel spin:** Zooms the scene in and out.
For machines where dragging with the right or middle buttons is difficult (usually Mac), another mouse interaction mode exists. In this mode, dragging with the left mouse button draws a box over a region of the scene. After the button is released, the scene is scaled and panned to fit the box. This mode can be accessed in the context menu or by calling:: For machines where dragging with the right or middle buttons is difficult (usually Mac), another mouse interaction mode exists. In this mode, dragging with the left mouse button draws a box over a region of the scene. After the button is released, the scene is scaled and panned to fit the box. This mode can be accessed in the context menu or by calling::
@ -38,11 +38,11 @@ The exact set of items available in the menu depends on the contents of the scen
3D visualizations use the following mouse interaction: 3D visualizations use the following mouse interaction:
* Left button drag: Rotates the scene around a central point * **Left button drag:** Rotates the scene around a central point
* Middle button drag: Pan the scene by moving the central "look-at" point within the x-y plane * **Middle button drag:** Pan the scene by moving the central "look-at" point within the x-y plane
* Middle button drag + CTRL: Pan the scene by moving the central "look-at" point along the z axis * **Middle button drag + CTRL:** Pan the scene by moving the central "look-at" point along the z axis
* Wheel spin: zoom in/out * **Wheel spin:** zoom in/out
* Wheel + CTRL: change field-of-view angle * **Wheel + CTRL:** change field-of-view angle
And keyboard controls: And keyboard controls:

View File

@ -12,7 +12,7 @@ import numpy as np
# Enable antialiasing for prettier plots # Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)
w = pg.GraphicsWindow() w = pg.GraphicsLayoutWidget(show=True)
w.setWindowTitle('pyqtgraph example: CustomGraphItem') w.setWindowTitle('pyqtgraph example: CustomGraphItem')
v = w.addViewBox() v = w.addViewBox()
v.setAspectLocked() v.setAspectLocked()

View File

@ -13,7 +13,7 @@ import numpy as np
# Enable antialiasing for prettier plots # Enable antialiasing for prettier plots
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)
w = pg.GraphicsWindow() w = pg.GraphicsLayoutWidget(show=True)
w.setWindowTitle('pyqtgraph example: GraphItem') w.setWindowTitle('pyqtgraph example: GraphItem')
v = w.addViewBox() v = w.addViewBox()
v.setAspectLocked() v.setAspectLocked()

View File

@ -28,19 +28,27 @@ v = pg.GraphicsView()
vb = pg.ViewBox() vb = pg.ViewBox()
vb.setAspectLocked() vb.setAspectLocked()
v.setCentralItem(vb) v.setCentralItem(vb)
l.addWidget(v, 0, 0) l.addWidget(v, 0, 0, 3, 1)
w = pg.HistogramLUTWidget() w = pg.HistogramLUTWidget()
l.addWidget(w, 0, 1) l.addWidget(w, 0, 1)
data = pg.gaussianFilter(np.random.normal(size=(256, 256)), (20, 20)) monoRadio = QtGui.QRadioButton('mono')
rgbaRadio = QtGui.QRadioButton('rgba')
l.addWidget(monoRadio, 1, 1)
l.addWidget(rgbaRadio, 2, 1)
monoRadio.setChecked(True)
def setLevelMode():
mode = 'mono' if monoRadio.isChecked() else 'rgba'
w.setLevelMode(mode)
monoRadio.toggled.connect(setLevelMode)
data = pg.gaussianFilter(np.random.normal(size=(256, 256, 3)), (20, 20, 0))
for i in range(32): for i in range(32):
for j in range(32): for j in range(32):
data[i*8, j*8] += .1 data[i*8, j*8] += .1
img = pg.ImageItem(data) img = pg.ImageItem(data)
#data2 = np.zeros((2,) + data.shape + (2,))
#data2[0,:,:,0] = data ## make non-contiguous array for testing purposes
#img = pg.ImageItem(data2[0,:,:,0])
vb.addItem(img) vb.addItem(img)
vb.autoRange() vb.autoRange()

View File

@ -10,7 +10,7 @@ import pyqtgraph as pg
app = QtGui.QApplication([]) app = QtGui.QApplication([])
win = pg.GraphicsWindow(title="Plotting items examples") win = pg.GraphicsLayoutWidget(show=True, title="Plotting items examples")
win.resize(1000,600) win.resize(1000,600)
# Enable antialiasing for prettier plots # Enable antialiasing for prettier plots

View File

@ -12,7 +12,7 @@ import pyqtgraph as pg
app = QtGui.QApplication([]) app = QtGui.QApplication([])
win = pg.GraphicsWindow(title="Basic plotting examples") win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
win.resize(1000,600) win.resize(1000,600)
win.setWindowTitle('pyqtgraph example: LogPlotTest') win.setWindowTitle('pyqtgraph example: LogPlotTest')

View File

@ -9,7 +9,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: PanningPlot') win.setWindowTitle('pyqtgraph example: PanningPlot')
plt = win.addPlot() plt = win.addPlot()

View File

@ -16,7 +16,7 @@ app = QtGui.QApplication([])
#mw = QtGui.QMainWindow() #mw = QtGui.QMainWindow()
#mw.resize(800,800) #mw.resize(800,800)
win = pg.GraphicsWindow(title="Plot auto-range examples") win = pg.GraphicsLayoutWidget(show=True, title="Plot auto-range examples")
win.resize(800,600) win.resize(800,600)
win.setWindowTitle('pyqtgraph example: PlotAutoRange') win.setWindowTitle('pyqtgraph example: PlotAutoRange')

View File

@ -17,7 +17,7 @@ app = QtGui.QApplication([])
#mw = QtGui.QMainWindow() #mw = QtGui.QMainWindow()
#mw.resize(800,800) #mw.resize(800,800)
win = pg.GraphicsWindow(title="Basic plotting examples") win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
win.resize(1000,600) win.resize(1000,600)
win.setWindowTitle('pyqtgraph example: Plotting') win.setWindowTitle('pyqtgraph example: Plotting')

View File

@ -0,0 +1,53 @@
# -*- coding: utf-8 -*-
"""
Using ProgressDialog to show progress updates in a nested process.
"""
import initExample ## Add path to library (just for examples; you do not need this)
import time
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
app = QtGui.QApplication([])
def runStage(i):
"""Waste time for 2 seconds while incrementing a progress bar.
"""
with pg.ProgressDialog("Running stage %s.." % i, maximum=100, nested=True) as dlg:
for j in range(100):
time.sleep(0.02)
dlg += 1
if dlg.wasCanceled():
print("Canceled stage %s" % i)
break
def runManyStages(i):
"""Iterate over runStage() 3 times while incrementing a progress bar.
"""
with pg.ProgressDialog("Running stage %s.." % i, maximum=3, nested=True, wait=0) as dlg:
for j in range(1,4):
runStage('%d.%d' % (i, j))
dlg += 1
if dlg.wasCanceled():
print("Canceled stage %s" % i)
break
with pg.ProgressDialog("Doing a multi-stage process..", maximum=5, nested=True, wait=0) as dlg1:
for i in range(1,6):
if i == 3:
# this stage will have 3 nested progress bars
runManyStages(i)
else:
# this stage will have 2 nested progress bars
runStage(i)
dlg1 += 1
if dlg1.wasCanceled():
print("Canceled process")
break

View File

@ -33,7 +33,7 @@ arr[8:13, 44:46] = 10
## create GUI ## create GUI
app = QtGui.QApplication([]) app = QtGui.QApplication([])
w = pg.GraphicsWindow(size=(1000,800), border=True) w = pg.GraphicsLayoutWidget(show=True, size=(1000,800), border=True)
w.setWindowTitle('pyqtgraph example: ROI Examples') w.setWindowTitle('pyqtgraph example: ROI Examples')
text = """Data Selection From Image.<br>\n text = """Data Selection From Image.<br>\n

View File

@ -13,7 +13,7 @@ pg.setConfigOptions(imageAxisOrder='row-major')
## create GUI ## create GUI
app = QtGui.QApplication([]) app = QtGui.QApplication([])
w = pg.GraphicsWindow(size=(800,800), border=True) w = pg.GraphicsLayoutWidget(show=True, size=(800,800), border=True)
v = w.addViewBox(colspan=2) v = w.addViewBox(colspan=2)
v.invertY(True) ## Images usually have their Y-axis pointing downward v.invertY(True) ## Images usually have their Y-axis pointing downward
v.setAspectLocked(True) v.setAspectLocked(True)

View File

@ -9,7 +9,7 @@ from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
pg.mkQApp() pg.mkQApp()
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: ScaleBar') win.setWindowTitle('pyqtgraph example: ScaleBar')
vb = win.addViewBox() vb = win.addViewBox()

View File

@ -11,6 +11,7 @@ import initExample
from pyqtgraph.Qt import QtGui, QtCore from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg import pyqtgraph as pg
import numpy as np import numpy as np
from collections import namedtuple
app = QtGui.QApplication([]) app = QtGui.QApplication([])
mw = QtGui.QMainWindow() mw = QtGui.QMainWindow()
@ -32,8 +33,8 @@ print("Generating data, this takes a few seconds...")
## There are a few different ways we can draw scatter plots; each is optimized for different types of data: ## There are a few different ways we can draw scatter plots; each is optimized for different types of data:
## 1) All spots identical and transform-invariant (top-left plot). ## 1) All spots identical and transform-invariant (top-left plot).
## In this case we can get a huge performance boost by pre-rendering the spot ## In this case we can get a huge performance boost by pre-rendering the spot
## image and just drawing that image repeatedly. ## image and just drawing that image repeatedly.
n = 300 n = 300
@ -57,21 +58,41 @@ s1.sigClicked.connect(clicked)
## 2) Spots are transform-invariant, but not identical (top-right plot). ## 2) Spots are transform-invariant, but not identical (top-right plot).
## In this case, drawing is almsot as fast as 1), but there is more startup ## In this case, drawing is almsot as fast as 1), but there is more startup
## overhead and memory usage since each spot generates its own pre-rendered ## overhead and memory usage since each spot generates its own pre-rendered
## image. ## image.
TextSymbol = namedtuple("TextSymbol", "label symbol scale")
def createLabel(label, angle):
symbol = QtGui.QPainterPath()
#symbol.addText(0, 0, QFont("San Serif", 10), label)
f = QtGui.QFont()
f.setPointSize(10)
symbol.addText(0, 0, f, label)
br = symbol.boundingRect()
scale = min(1. / br.width(), 1. / br.height())
tr = QtGui.QTransform()
tr.scale(scale, scale)
tr.rotate(angle)
tr.translate(-br.x() - br.width()/2., -br.y() - br.height()/2.)
return TextSymbol(label, tr.map(symbol), 0.1 / scale)
random_str = lambda : (''.join([chr(np.random.randint(ord('A'),ord('z'))) for i in range(np.random.randint(1,5))]), np.random.randint(0, 360))
s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True) s2 = pg.ScatterPlotItem(size=10, pen=pg.mkPen('w'), pxMode=True)
pos = np.random.normal(size=(2,n), scale=1e-5) pos = np.random.normal(size=(2,n), scale=1e-5)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)] spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': i%5, 'size': 5+i/10.} for i in range(n)]
s2.addPoints(spots) s2.addPoints(spots)
spots = [{'pos': pos[:,i], 'data': 1, 'brush':pg.intColor(i, n), 'symbol': label[1], 'size': label[2]*(5+i/10.)} for (i, label) in [(i, createLabel(*random_str())) for i in range(n)]]
s2.addPoints(spots)
w2.addItem(s2) w2.addItem(s2)
s2.sigClicked.connect(clicked) s2.sigClicked.connect(clicked)
## 3) Spots are not transform-invariant, not identical (bottom-left). ## 3) Spots are not transform-invariant, not identical (bottom-left).
## This is the slowest case, since all spots must be completely re-drawn ## This is the slowest case, since all spots must be completely re-drawn
## every time because their apparent transformation may have changed. ## every time because their apparent transformation may have changed.
s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to transform with the view s3 = pg.ScatterPlotItem(pxMode=False) ## Set pxMode=False to allow spots to transform with the view

View File

@ -26,7 +26,7 @@ spins = [
("Float with SI-prefixed units<br>(n, u, m, k, M, etc)", ("Float with SI-prefixed units<br>(n, u, m, k, M, etc)",
pg.SpinBox(value=0.9, suffix='V', siPrefix=True)), pg.SpinBox(value=0.9, suffix='V', siPrefix=True)),
("Float with SI-prefixed units,<br>dec step=0.1, minStep=0.1", ("Float with SI-prefixed units,<br>dec step=0.1, minStep=0.1",
pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.1, minStep=0.1)), pg.SpinBox(value=1.0, suffix='PSI', siPrefix=True, dec=True, step=0.1, minStep=0.1)),
("Float with SI-prefixed units,<br>dec step=0.5, minStep=0.01", ("Float with SI-prefixed units,<br>dec step=0.5, minStep=0.01",
pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)), pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)),
("Float with SI-prefixed units,<br>dec step=1.0, minStep=0.001", ("Float with SI-prefixed units,<br>dec step=1.0, minStep=0.001",

View File

@ -11,7 +11,7 @@ from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg import pyqtgraph as pg
app = QtGui.QApplication([]) app = QtGui.QApplication([])
win = pg.GraphicsWindow(title="Scatter Plot Symbols") win = pg.GraphicsLayoutWidget(show=True, title="Scatter Plot Symbols")
win.resize(1000,600) win.resize(1000,600)
pg.setConfigOptions(antialias=True) pg.setConfigOptions(antialias=True)

View File

@ -16,7 +16,7 @@ x = np.arange(1000, dtype=float)
y = np.random.normal(size=1000) y = np.random.normal(size=1000)
y += 5 * np.sin(x/100) y += 5 * np.sin(x/100)
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: ____') win.setWindowTitle('pyqtgraph example: ____')
win.resize(1000, 800) win.resize(1000, 800)
win.ci.setBorder((50, 50, 100)) win.ci.setBorder((50, 50, 100))

View File

@ -14,7 +14,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: context menu') win.setWindowTitle('pyqtgraph example: context menu')

View File

@ -13,7 +13,7 @@ from pyqtgraph.Point import Point
#generate layout #generate layout
app = QtGui.QApplication([]) app = QtGui.QApplication([])
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: crosshair') win.setWindowTitle('pyqtgraph example: crosshair')
label = pg.LabelItem(justify='right') label = pg.LabelItem(justify='right')
win.addItem(label) win.addItem(label)

122
examples/fractal.py Normal file
View File

@ -0,0 +1,122 @@
# -*- coding: utf-8 -*-
"""
Displays an interactive Koch fractal
"""
import initExample ## Add path to library (just for examples; you do not need this)
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
import numpy as np
app = QtGui.QApplication([])
# Set up UI widgets
win = pg.QtGui.QWidget()
win.setWindowTitle('pyqtgraph example: fractal demo')
layout = pg.QtGui.QGridLayout()
win.setLayout(layout)
layout.setContentsMargins(0, 0, 0, 0)
depthLabel = pg.QtGui.QLabel('fractal depth:')
layout.addWidget(depthLabel, 0, 0)
depthSpin = pg.SpinBox(value=5, step=1, bounds=[1, 10], delay=0, int=True)
depthSpin.resize(100, 20)
layout.addWidget(depthSpin, 0, 1)
w = pg.GraphicsLayoutWidget()
layout.addWidget(w, 1, 0, 1, 2)
win.show()
# Set up graphics
v = w.addViewBox()
v.setAspectLocked()
baseLine = pg.PolyLineROI([[0, 0], [1, 0], [1.5, 1], [2, 0], [3, 0]], pen=(0, 255, 0, 100), movable=False)
v.addItem(baseLine)
fc = pg.PlotCurveItem(pen=(255, 255, 255, 200), antialias=True)
v.addItem(fc)
v.autoRange()
transformMap = [0, 0, None]
def update():
# recalculate and redraw the fractal curve
depth = depthSpin.value()
pts = baseLine.getState()['points']
nbseg = len(pts) - 1
nseg = nbseg**depth
# Get a transformation matrix for each base segment
trs = []
v1 = pts[-1] - pts[0]
l1 = v1.length()
for i in range(len(pts)-1):
p1 = pts[i]
p2 = pts[i+1]
v2 = p2 - p1
t = p1 - pts[0]
r = v2.angle(v1)
s = v2.length() / l1
trs.append(pg.SRTTransform({'pos': t, 'scale': (s, s), 'angle': r}))
basePts = [np.array(list(pt) + [1]) for pt in baseLine.getState()['points']]
baseMats = np.dstack([tr.matrix().T for tr in trs]).transpose(2, 0, 1)
# Generate an array of matrices to transform base points
global transformMap
if transformMap[:2] != [depth, nbseg]:
# we can cache the transform index to save a little time..
nseg = nbseg**depth
matInds = np.empty((depth, nseg), dtype=int)
for i in range(depth):
matInds[i] = np.tile(np.repeat(np.arange(nbseg), nbseg**(depth-1-i)), nbseg**i)
transformMap = [depth, nbseg, matInds]
# Each column in matInds contains the indices referring to the base transform
# matrices that must be multiplied together to generate the final transform
# for each segment of the fractal
matInds = transformMap[2]
# Collect all matrices needed for generating fractal curve
mats = baseMats[matInds]
# Magic-multiply stacks of matrices together
def matmul(a, b):
return np.sum(np.transpose(a,(0,2,1))[..., None] * b[..., None, :], axis=-3)
mats = reduce(matmul, mats)
# Transform base points through matrix array
pts = np.empty((nseg * nbseg + 1, 2))
for l in range(len(trs)):
bp = basePts[l]
pts[l:-1:len(trs)] = np.dot(mats, bp)[:, :2]
# Finish the curve with the last base point
pts[-1] = basePts[-1][:2]
# update fractal curve with new points
fc.setData(pts[:,0], pts[:,1])
# Update the fractal whenever the base shape or depth has changed
baseLine.sigRegionChanged.connect(update)
depthSpin.valueChanged.connect(update)
# Initialize
update()
## Start Qt event loop unless running in interactive mode or using pyside.
if __name__ == '__main__':
import sys
if (sys.flags.interactive != 1) or not hasattr(QtCore, 'PYQT_VERSION'):
QtGui.QApplication.instance().exec_()

View File

@ -8,7 +8,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.resize(800,350) win.resize(800,350)
win.setWindowTitle('pyqtgraph example: Histogram') win.setWindowTitle('pyqtgraph example: Histogram')
plt1 = win.addPlot() plt1 = win.addPlot()

View File

@ -20,7 +20,7 @@ data = np.concatenate([data, data], axis=0)
data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2] data = pg.gaussianFilter(data, (10, 10, 10))[frames/2:frames + frames/2]
data[:, 15:16, 15:17] += 1 data[:, 15:16, 15:17] += 1
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: Isocurve') win.setWindowTitle('pyqtgraph example: Isocurve')
vb = win.addViewBox() vb = win.addViewBox()
img = pg.ImageItem(data[0]) img = pg.ImageItem(data[0])

View File

@ -20,7 +20,7 @@ app = QtGui.QApplication([])
x = np.linspace(-50, 50, 1000) x = np.linspace(-50, 50, 1000)
y = np.sin(x) / x y = np.sin(x) / x
win = pg.GraphicsWindow(title="pyqtgraph example: Linked Views") win = pg.GraphicsLayoutWidget(show=True, title="pyqtgraph example: Linked Views")
win.resize(800,600) win.resize(800,600)
win.addLabel("Linked Views", colspan=2) win.addLabel("Linked Views", colspan=2)

View File

@ -11,7 +11,7 @@ import pyqtgraph as pg
app = QtGui.QApplication([]) app = QtGui.QApplication([])
w = pg.GraphicsWindow() w = pg.GraphicsLayoutWidget(show=True)
w.setWindowTitle('pyqtgraph example: logAxis') w.setWindowTitle('pyqtgraph example: logAxis')
p1 = w.addPlot(0,0, title="X Semilog") p1 = w.addPlot(0,0, title="X Semilog")
p2 = w.addPlot(1,0, title="Y Semilog") p2 = w.addPlot(1,0, title="Y Semilog")

View File

@ -17,7 +17,7 @@ from pyqtgraph import Point
app = pg.QtGui.QApplication([]) app = pg.QtGui.QApplication([])
w = pg.GraphicsWindow(border=0.5) w = pg.GraphicsLayoutWidget(show=True, border=0.5)
w.resize(1000, 900) w.resize(1000, 900)
w.show() w.show()

View File

@ -8,7 +8,7 @@ import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui from pyqtgraph.Qt import QtCore, QtGui
import numpy as np import numpy as np
win = pg.GraphicsWindow() win = pg.GraphicsLayoutWidget(show=True)
win.setWindowTitle('pyqtgraph example: Scrolling Plots') win.setWindowTitle('pyqtgraph example: Scrolling Plots')

View File

@ -3,6 +3,7 @@ import subprocess
import time import time
import os import os
import sys import sys
import errno
from pyqtgraph.pgcollections import OrderedDict from pyqtgraph.pgcollections import OrderedDict
from pyqtgraph.python2_3 import basestring from pyqtgraph.python2_3 import basestring
@ -31,6 +32,7 @@ examples = OrderedDict([
('Optics', 'optics_demos.py'), ('Optics', 'optics_demos.py'),
('Special relativity', 'relativity_demo.py'), ('Special relativity', 'relativity_demo.py'),
('Verlet chain', 'verlet_chain_demo.py'), ('Verlet chain', 'verlet_chain_demo.py'),
('Koch Fractal', 'fractal.py'),
])), ])),
('GraphicsItems', OrderedDict([ ('GraphicsItems', OrderedDict([
('Scatter Plot', 'ScatterPlot.py'), ('Scatter Plot', 'ScatterPlot.py'),
@ -143,7 +145,14 @@ except:
output = '' output = ''
fail = False fail = False
while True: while True:
c = process.stdout.read(1).decode() try:
c = process.stdout.read(1).decode()
except IOError as err:
if err.errno == errno.EINTR:
# Interrupted system call; just try again.
c = ''
else:
raise
output += c output += c
#sys.stdout.write(c) #sys.stdout.write(c)
#sys.stdout.flush() #sys.stdout.flush()

View File

@ -32,8 +32,6 @@ class ChainSim(pg.QtCore.QObject):
if self.initialized: if self.initialized:
return return
assert None not in [self.pos, self.mass, self.links, self.lengths]
if self.fixed is None: if self.fixed is None:
self.fixed = np.zeros(self.pos.shape[0], dtype=bool) self.fixed = np.zeros(self.pos.shape[0], dtype=bool)
if self.push is None: if self.push is None:

View File

@ -43,8 +43,29 @@ if QT_LIB is None:
if QT_LIB is None: if QT_LIB is None:
raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.") raise Exception("PyQtGraph requires one of PyQt4, PyQt5 or PySide; none of these packages could be imported.")
class FailedImport(object):
"""Used to defer ImportErrors until we are sure the module is needed.
"""
def __init__(self, err):
self.err = err
def __getattr__(self, attr):
raise self.err
if QT_LIB == PYSIDE: if QT_LIB == PYSIDE:
from PySide import QtGui, QtCore, QtOpenGL, QtSvg from PySide import QtGui, QtCore
try:
from PySide import QtOpenGL
except ImportError as err:
QtOpenGL = FailedImport(err)
try:
from PySide import QtSvg
except ImportError as err:
QtSvg = FailedImport(err)
try: try:
from PySide import QtTest from PySide import QtTest
if not hasattr(QtTest.QTest, 'qWait'): if not hasattr(QtTest.QTest, 'qWait'):
@ -55,9 +76,9 @@ if QT_LIB == PYSIDE:
while time.time() < start + msec * 0.001: while time.time() < start + msec * 0.001:
QtGui.QApplication.processEvents() QtGui.QApplication.processEvents()
QtTest.QTest.qWait = qWait QtTest.QTest.qWait = qWait
except ImportError as err:
except ImportError: QtTest = FailedImport(err)
pass
import PySide import PySide
try: try:
from PySide import shiboken from PySide import shiboken
@ -133,16 +154,16 @@ elif QT_LIB == PYQT4:
from PyQt4 import QtGui, QtCore, uic from PyQt4 import QtGui, QtCore, uic
try: try:
from PyQt4 import QtSvg from PyQt4 import QtSvg
except ImportError: except ImportError as err:
pass QtSvg = FailedImport(err)
try: try:
from PyQt4 import QtOpenGL from PyQt4 import QtOpenGL
except ImportError: except ImportError as err:
pass QtOpenGL = FailedImport(err)
try: try:
from PyQt4 import QtTest from PyQt4 import QtTest
except ImportError: except ImportError as err:
pass QtTest = FailedImport(err)
VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR VERSION_INFO = 'PyQt4 ' + QtCore.PYQT_VERSION_STR + ' Qt ' + QtCore.QT_VERSION_STR
@ -151,19 +172,31 @@ elif QT_LIB == PYQT5:
# We're using PyQt5 which has a different structure so we're going to use a shim to # We're using PyQt5 which has a different structure so we're going to use a shim to
# recreate the Qt4 structure for Qt5 # recreate the Qt4 structure for Qt5
from PyQt5 import QtGui, QtCore, QtWidgets, uic from PyQt5 import QtGui, QtCore, QtWidgets, uic
# PyQt5, starting in v5.5, calls qAbort when an exception is raised inside
# a slot. To maintain backward compatibility (and sanity for interactive
# users), we install a global exception hook to override this behavior.
ver = QtCore.PYQT_VERSION_STR.split('.')
if int(ver[1]) >= 5:
if sys.excepthook == sys.__excepthook__:
sys_excepthook = sys.excepthook
def pyqt5_qabort_override(*args, **kwds):
return sys_excepthook(*args, **kwds)
sys.excepthook = pyqt5_qabort_override
try: try:
from PyQt5 import QtSvg from PyQt5 import QtSvg
except ImportError: except ImportError as err:
pass QtSvg = FailedImport(err)
try: try:
from PyQt5 import QtOpenGL from PyQt5 import QtOpenGL
except ImportError: except ImportError as err:
pass QtOpenGL = FailedImport(err)
try: try:
from PyQt5 import QtTest from PyQt5 import QtTest
QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed QtTest.QTest.qWaitForWindowShown = QtTest.QTest.qWaitForWindowExposed
except ImportError: except ImportError as err:
pass QtTest = FailedImport(err)
# Re-implement deprecated APIs # Re-implement deprecated APIs
@ -239,3 +272,11 @@ m = re.match(r'(\d+)\.(\d+).*', QtVersion)
if m is not None and list(map(int, m.groups())) < versionReq: if m is not None and list(map(int, m.groups())) < versionReq:
print(list(map(int, m.groups()))) print(list(map(int, m.groups())))
raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion)) raise Exception('pyqtgraph requires Qt version >= %d.%d (your version is %s)' % (versionReq[0], versionReq[1], QtVersion))
QAPP = None
def mkQApp():
global QAPP
if QtGui.QApplication.instance() is None:
QAPP = QtGui.QApplication([])
return QAPP

View File

@ -1,13 +1,19 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from .Qt import QtCore, QtGui from .Qt import QtCore, QtGui
from . import functions as fn from . import functions as fn
from .Vector import Vector
import numpy as np import numpy as np
class Transform3D(QtGui.QMatrix4x4): class Transform3D(QtGui.QMatrix4x4):
""" """
Extension of QMatrix4x4 with some helpful methods added. Extension of QMatrix4x4 with some helpful methods added.
""" """
def __init__(self, *args): def __init__(self, *args):
if len(args) == 1 and isinstance(args[0], (list, tuple, np.ndarray)):
args = [x for y in args[0] for x in y]
if len(args) != 16:
raise TypeError("Single argument to Transform3D must have 16 elements.")
QtGui.QMatrix4x4.__init__(self, *args) QtGui.QMatrix4x4.__init__(self, *args)
def matrix(self, nd=3): def matrix(self, nd=3):
@ -25,8 +31,15 @@ class Transform3D(QtGui.QMatrix4x4):
""" """
Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates Extends QMatrix4x4.map() to allow mapping (3, ...) arrays of coordinates
""" """
if isinstance(obj, np.ndarray) and obj.ndim >= 2 and obj.shape[0] in (2,3): if isinstance(obj, np.ndarray) and obj.shape[0] in (2,3):
return fn.transformCoordinates(self, obj) if obj.ndim >= 2:
return fn.transformCoordinates(self, obj)
elif obj.ndim == 1:
v = QtGui.QMatrix4x4.map(self, Vector(obj))
return np.array([v.x(), v.y(), v.z()])[:obj.shape[0]]
elif isinstance(obj, (list, tuple)):
v = QtGui.QMatrix4x4.map(self, Vector(obj))
return type(obj)([v.x(), v.y(), v.z()])[:len(obj)]
else: else:
return QtGui.QMatrix4x4.map(self, obj) return QtGui.QMatrix4x4.map(self, obj)

View File

@ -10,7 +10,7 @@ __version__ = '0.10.0'
## 'Qt' is a local module; it is intended mainly to cover up the differences ## 'Qt' is a local module; it is intended mainly to cover up the differences
## between PyQt4 and PySide. ## between PyQt4 and PySide.
from .Qt import QtGui from .Qt import QtGui, mkQApp
## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause) ## not really safe--If we accidentally create another QApplication, the process hangs (and it is very difficult to trace the cause)
#if QtGui.QApplication.instance() is None: #if QtGui.QApplication.instance() is None:
@ -262,6 +262,7 @@ from .widgets.GraphicsView import *
from .widgets.LayoutWidget import * from .widgets.LayoutWidget import *
from .widgets.TableWidget import * from .widgets.TableWidget import *
from .widgets.ProgressDialog import * from .widgets.ProgressDialog import *
from .widgets.GroupBox import GroupBox
from .imageview import * from .imageview import *
from .WidgetGroup import * from .WidgetGroup import *
@ -319,7 +320,7 @@ def cleanup():
'are properly called before app shutdown (%s)\n' % (o,)) 'are properly called before app shutdown (%s)\n' % (o,))
s.addItem(o) s.addItem(o)
except RuntimeError: ## occurs if a python wrapper no longer has its underlying C++ object except (RuntimeError, ReferenceError): ## occurs if a python wrapper no longer has its underlying C++ object
continue continue
_cleanupCalled = True _cleanupCalled = True
@ -446,14 +447,22 @@ def dbg(*args, **kwds):
except NameError: except NameError:
consoles = [c] consoles = [c]
return c return c
def stack(*args, **kwds):
"""
Create a console window and show the current stack trace.
All arguments are passed to :func:`ConsoleWidget.__init__() <pyqtgraph.console.ConsoleWidget.__init__>`.
def mkQApp(): """
global QAPP mkQApp()
inst = QtGui.QApplication.instance() from . import console
if inst is None: c = console.ConsoleWidget(*args, **kwds)
QAPP = QtGui.QApplication([]) c.setStack()
else: c.show()
QAPP = inst global consoles
return QAPP try:
consoles.append(c)
except NameError:
consoles = [c]
return c

View File

@ -19,9 +19,11 @@ elif QT_LIB == 'PyQt5':
import numpy as np import numpy as np
from .. import debug from .. import debug
import weakref import weakref
import gc
from .CanvasManager import CanvasManager from .CanvasManager import CanvasManager
from .CanvasItem import CanvasItem, GroupCanvasItem from .CanvasItem import CanvasItem, GroupCanvasItem
class Canvas(QtGui.QWidget): class Canvas(QtGui.QWidget):
sigSelectionChanged = QtCore.Signal(object, object) sigSelectionChanged = QtCore.Signal(object, object)
@ -32,7 +34,6 @@ class Canvas(QtGui.QWidget):
QtGui.QWidget.__init__(self, parent) QtGui.QWidget.__init__(self, parent)
self.ui = Ui_Form() self.ui = Ui_Form()
self.ui.setupUi(self) self.ui.setupUi(self)
#self.view = self.ui.view
self.view = ViewBox() self.view = ViewBox()
self.ui.view.setCentralItem(self.view) self.ui.view.setCentralItem(self.view)
self.itemList = self.ui.itemList self.itemList = self.ui.itemList
@ -49,9 +50,7 @@ class Canvas(QtGui.QWidget):
self.redirect = None ## which canvas to redirect items to self.redirect = None ## which canvas to redirect items to
self.items = [] self.items = []
#self.view.enableMouse()
self.view.setAspectLocked(True) self.view.setAspectLocked(True)
#self.view.invertY()
grid = GridItem() grid = GridItem()
self.grid = CanvasItem(grid, name='Grid', movable=False) self.grid = CanvasItem(grid, name='Grid', movable=False)
@ -69,8 +68,6 @@ 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.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)
@ -88,21 +85,11 @@ class Canvas(QtGui.QWidget):
self.ui.redirectCombo.setHostName(self.registeredName) self.ui.redirectCombo.setHostName(self.registeredName)
self.menu = QtGui.QMenu() self.menu = QtGui.QMenu()
#self.menu.setTitle("Image")
remAct = QtGui.QAction("Remove item", self.menu) remAct = QtGui.QAction("Remove item", self.menu)
remAct.triggered.connect(self.removeClicked) remAct.triggered.connect(self.removeClicked)
self.menu.addAction(remAct) self.menu.addAction(remAct)
self.menu.remAct = remAct self.menu.remAct = remAct
self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent self.ui.itemList.contextMenuEvent = self.itemListContextMenuEvent
#def storeSvg(self):
#from pyqtgraph.GraphicsScene.exportDialog import ExportDialog
#ex = ExportDialog(self.ui.view)
#ex.show()
#def storePng(self):
#self.ui.view.writeImage()
def splitterMoved(self): def splitterMoved(self):
self.resizeEvent() self.resizeEvent()
@ -135,7 +122,6 @@ class Canvas(QtGui.QWidget):
s = min(self.width(), max(100, min(200, self.width()*0.25))) s = min(self.width(), max(100, min(200, self.width()*0.25)))
s2 = self.width()-s s2 = self.width()-s
self.ui.splitter.setSizes([s2, s]) self.ui.splitter.setSizes([s2, s])
def updateRedirect(self, *args): def updateRedirect(self, *args):
### Decide whether/where to redirect items and make it so ### Decide whether/where to redirect items and make it so
@ -154,7 +140,6 @@ class Canvas(QtGui.QWidget):
self.reclaimItems() self.reclaimItems()
else: else:
self.redirectItems(redirect) self.redirectItems(redirect)
def redirectItems(self, canvas): def redirectItems(self, canvas):
for i in self.items: for i in self.items:
@ -171,12 +156,9 @@ class Canvas(QtGui.QWidget):
else: else:
parent.removeChild(li) parent.removeChild(li)
canvas.addItem(i) canvas.addItem(i)
def reclaimItems(self): def reclaimItems(self):
items = self.items items = self.items
#self.items = {'Grid': items['Grid']}
#del items['Grid']
self.items = [self.grid] self.items = [self.grid]
items.remove(self.grid) items.remove(self.grid)
@ -185,9 +167,6 @@ class Canvas(QtGui.QWidget):
self.addItem(i) self.addItem(i)
def treeItemChanged(self, item, col): def treeItemChanged(self, item, col):
#gi = self.items.get(item.name, None)
#if gi is None:
#return
try: try:
citem = item.canvasItem() citem = item.canvasItem()
except AttributeError: except AttributeError:
@ -203,25 +182,16 @@ class Canvas(QtGui.QWidget):
def treeItemSelected(self): def treeItemSelected(self):
sel = self.selectedItems() sel = self.selectedItems()
#sel = []
#for listItem in self.itemList.selectedItems():
#if hasattr(listItem, 'canvasItem') and listItem.canvasItem is not None:
#sel.append(listItem.canvasItem)
#sel = [self.items[item.name] for item in sel]
if len(sel) == 0: if len(sel) == 0:
#self.selectWidget.hide()
return return
multi = len(sel) > 1 multi = len(sel) > 1
for i in self.items: for i in self.items:
#i.ctrlWidget().hide()
## updated the selected state of every item ## updated the selected state of every item
i.selectionChanged(i in sel, multi) i.selectionChanged(i in sel, multi)
if len(sel)==1: if len(sel)==1:
#item = sel[0]
#item.ctrlWidget().show()
self.multiSelectBox.hide() self.multiSelectBox.hide()
self.ui.mirrorSelectionBtn.hide() self.ui.mirrorSelectionBtn.hide()
self.ui.reflectSelectionBtn.hide() self.ui.reflectSelectionBtn.hide()
@ -229,14 +199,6 @@ class Canvas(QtGui.QWidget):
elif len(sel) > 1: elif len(sel) > 1:
self.showMultiSelectBox() self.showMultiSelectBox()
#if item.isMovable():
#self.selectBox.setPos(item.item.pos())
#self.selectBox.setSize(item.item.sceneBoundingRect().size())
#self.selectBox.show()
#else:
#self.selectBox.hide()
#self.emit(QtCore.SIGNAL('itemSelected'), self, item)
self.sigSelectionChanged.emit(self, sel) self.sigSelectionChanged.emit(self, sel)
def selectedItems(self): def selectedItems(self):
@ -245,19 +207,9 @@ class Canvas(QtGui.QWidget):
""" """
return [item.canvasItem() for item in self.itemList.selectedItems() if item.canvasItem() is not None] return [item.canvasItem() for item in self.itemList.selectedItems() if item.canvasItem() is not None]
#def selectedItem(self):
#sel = self.itemList.selectedItems()
#if sel is None or len(sel) < 1:
#return
#return self.items.get(sel[0].name, None)
def selectItem(self, item): def selectItem(self, item):
li = item.listItem li = item.listItem
#li = self.getListItem(item.name())
#print "select", li
self.itemList.setCurrentItem(li) self.itemList.setCurrentItem(li)
def showMultiSelectBox(self): def showMultiSelectBox(self):
## Get list of selected canvas items ## Get list of selected canvas items
@ -281,7 +233,6 @@ class Canvas(QtGui.QWidget):
self.ui.mirrorSelectionBtn.show() self.ui.mirrorSelectionBtn.show()
self.ui.reflectSelectionBtn.show() self.ui.reflectSelectionBtn.show()
self.ui.resetTransformsBtn.show() self.ui.resetTransformsBtn.show()
#self.multiSelectBoxBase = self.multiSelectBox.getState().copy()
def mirrorSelectionClicked(self): def mirrorSelectionClicked(self):
for ci in self.selectedItems(): for ci in self.selectedItems():
@ -312,7 +263,6 @@ class Canvas(QtGui.QWidget):
ci.setTemporaryTransform(transform) ci.setTemporaryTransform(transform)
ci.sigTransformChanged.emit(ci) ci.sigTransformChanged.emit(ci)
def addGraphicsItem(self, item, **opts): def addGraphicsItem(self, item, **opts):
"""Add a new GraphicsItem to the scene at pos. """Add a new GraphicsItem to the scene at pos.
Common options are name, pos, scale, and z Common options are name, pos, scale, and z
@ -321,13 +271,11 @@ class Canvas(QtGui.QWidget):
item._canvasItem = citem item._canvasItem = citem
self.addItem(citem) self.addItem(citem)
return citem return citem
def addGroup(self, name, **kargs): def addGroup(self, name, **kargs):
group = GroupCanvasItem(name=name) group = GroupCanvasItem(name=name)
self.addItem(group, **kargs) self.addItem(group, **kargs)
return group return group
def addItem(self, citem): def addItem(self, citem):
""" """
@ -363,7 +311,6 @@ class Canvas(QtGui.QWidget):
#name = newname #name = newname
## find parent and add item to tree ## find parent and add item to tree
#currentNode = self.itemList.invisibleRootItem()
insertLocation = 0 insertLocation = 0
#print "Inserting node:", name #print "Inserting node:", name
@ -413,11 +360,7 @@ class Canvas(QtGui.QWidget):
node.setCheckState(0, QtCore.Qt.Unchecked) node.setCheckState(0, QtCore.Qt.Unchecked)
node.name = name node.name = name
#if citem.opts['parent'] != None:
## insertLocation is incorrect in this case
parent.insertChild(insertLocation, node) parent.insertChild(insertLocation, node)
#else:
#root.insertChild(insertLocation, node)
citem.name = name citem.name = name
citem.listItem = node citem.listItem = node
@ -435,36 +378,6 @@ class Canvas(QtGui.QWidget):
if len(self.items) == 2: if len(self.items) == 2:
self.autoRange() self.autoRange()
#for n in name:
#nextnode = None
#for x in range(currentNode.childCount()):
#ch = currentNode.child(x)
#if hasattr(ch, 'name'): ## check Z-value of current item to determine insert location
#zval = ch.canvasItem.zValue()
#if zval > z:
###print " ->", x
#insertLocation = x+1
#if n == ch.text(0):
#nextnode = ch
#break
#if nextnode is None: ## If name doesn't exist, create it
#nextnode = QtGui.QTreeWidgetItem([n])
#nextnode.setFlags((nextnode.flags() | QtCore.Qt.ItemIsUserCheckable) & ~QtCore.Qt.ItemIsDropEnabled)
#nextnode.setCheckState(0, QtCore.Qt.Checked)
### Add node to correct position in list by Z-value
###print " ==>", insertLocation
#currentNode.insertChild(insertLocation, nextnode)
#if n == name[-1]: ## This is the leaf; add some extra properties.
#nextnode.name = name
#if n == name[0]: ## This is the root; make the item movable
#nextnode.setFlags(nextnode.flags() | QtCore.Qt.ItemIsDragEnabled)
#else:
#nextnode.setFlags(nextnode.flags() & ~QtCore.Qt.ItemIsDragEnabled)
#currentNode = nextnode
return citem return citem
def treeItemMoved(self, item, parent, index): def treeItemMoved(self, item, parent, index):
@ -481,31 +394,6 @@ class Canvas(QtGui.QWidget):
for i in range(len(siblings)): for i in range(len(siblings)):
item = siblings[i] item = siblings[i]
item.setZValue(zvals[i]) item.setZValue(zvals[i])
#item = self.itemList.topLevelItem(i)
##ci = self.items[item.name]
#ci = item.canvasItem
#if ci is None:
#continue
#if ci.zValue() != zvals[i]:
#ci.setZValue(zvals[i])
#if self.itemList.topLevelItemCount() < 2:
#return
#name = item.name
#gi = self.items[name]
#if index == 0:
#next = self.itemList.topLevelItem(1)
#z = self.items[next.name].zValue()+1
#else:
#prev = self.itemList.topLevelItem(index-1)
#z = self.items[prev.name].zValue()-1
#gi.setZValue(z)
def itemVisibilityChanged(self, item): def itemVisibilityChanged(self, item):
listItem = item.listItem listItem = item.listItem
@ -521,7 +409,6 @@ class Canvas(QtGui.QWidget):
if isinstance(item, QtGui.QTreeWidgetItem): if isinstance(item, QtGui.QTreeWidgetItem):
item = item.canvasItem() item = item.canvasItem()
if isinstance(item, CanvasItem): if isinstance(item, CanvasItem):
item.setCanvas(None) item.setCanvas(None)
listItem = item.listItem listItem = item.listItem
@ -532,25 +419,24 @@ class Canvas(QtGui.QWidget):
ctrl = item.ctrlWidget() ctrl = item.ctrlWidget()
ctrl.hide() ctrl.hide()
self.ui.ctrlLayout.removeWidget(ctrl) self.ui.ctrlLayout.removeWidget(ctrl)
ctrl.setParent(None)
else: else:
if hasattr(item, '_canvasItem'): if hasattr(item, '_canvasItem'):
self.removeItem(item._canvasItem) self.removeItem(item._canvasItem)
else: else:
self.view.removeItem(item) self.view.removeItem(item)
## disconnect signals, remove from list, etc.. gc.collect()
def clear(self): def clear(self):
while len(self.items) > 0: while len(self.items) > 0:
self.removeItem(self.items[0]) self.removeItem(self.items[0])
def addToScene(self, item): def addToScene(self, item):
self.view.addItem(item) self.view.addItem(item)
def removeFromScene(self, item): def removeFromScene(self, item):
self.view.removeItem(item) self.view.removeItem(item)
def listItems(self): def listItems(self):
"""Return a dictionary of name:item pairs""" """Return a dictionary of name:item pairs"""
@ -559,15 +445,10 @@ class Canvas(QtGui.QWidget):
def getListItem(self, name): def getListItem(self, name):
return self.items[name] return self.items[name]
#def scene(self):
#return self.view.scene()
def itemTransformChanged(self, item): def itemTransformChanged(self, item):
#self.emit(QtCore.SIGNAL('itemTransformChanged'), self, item)
self.sigItemTransformChanged.emit(self, item) self.sigItemTransformChanged.emit(self, item)
def itemTransformChangeFinished(self, item): def itemTransformChangeFinished(self, item):
#self.emit(QtCore.SIGNAL('itemTransformChangeFinished'), self, item)
self.sigItemTransformChangeFinished.emit(self, item) self.sigItemTransformChangeFinished.emit(self, item)
def itemListContextMenuEvent(self, ev): def itemListContextMenuEvent(self, ev):
@ -575,13 +456,13 @@ class Canvas(QtGui.QWidget):
self.menu.popup(ev.globalPos()) self.menu.popup(ev.globalPos())
def removeClicked(self): def removeClicked(self):
#self.removeItem(self.menuItem)
for item in self.selectedItems(): for item in self.selectedItems():
self.removeItem(item) self.removeItem(item)
self.menuItem = None self.menuItem = None
import gc import gc
gc.collect() gc.collect()
class SelectBox(ROI): class SelectBox(ROI):
def __init__(self, scalable=False): def __init__(self, scalable=False):
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) #QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
@ -593,14 +474,3 @@ class SelectBox(ROI):
self.addScaleHandle([0, 0], center, lockAspect=True) self.addScaleHandle([0, 0], center, lockAspect=True)
self.addRotateHandle([0, 1], center) self.addRotateHandle([0, 1], center)
self.addRotateHandle([1, 0], center) self.addRotateHandle([1, 0], center)

View File

@ -1,4 +1,5 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import numpy as np
from ..Qt import QtGui, QtCore, QtSvg, QT_LIB from ..Qt import QtGui, QtCore, QtSvg, QT_LIB
from ..graphicsItems.ROI import ROI from ..graphicsItems.ROI import ROI
from .. import SRTTransform, ItemGroup from .. import SRTTransform, ItemGroup
@ -87,14 +88,12 @@ class CanvasItem(QtCore.QObject):
self.alphaSlider.valueChanged.connect(self.alphaChanged) self.alphaSlider.valueChanged.connect(self.alphaChanged)
self.alphaSlider.sliderPressed.connect(self.alphaPressed) self.alphaSlider.sliderPressed.connect(self.alphaPressed)
self.alphaSlider.sliderReleased.connect(self.alphaReleased) self.alphaSlider.sliderReleased.connect(self.alphaReleased)
#self.canvas.sigSelectionChanged.connect(self.selectionChanged)
self.resetTransformBtn.clicked.connect(self.resetTransformClicked) self.resetTransformBtn.clicked.connect(self.resetTransformClicked)
self.copyBtn.clicked.connect(self.copyClicked) self.copyBtn.clicked.connect(self.copyClicked)
self.pasteBtn.clicked.connect(self.pasteClicked) self.pasteBtn.clicked.connect(self.pasteClicked)
self.setMovable(self.opts['movable']) ## update gui to reflect this option self.setMovable(self.opts['movable']) ## update gui to reflect this option
if 'transform' in self.opts: if 'transform' in self.opts:
self.baseTransform = self.opts['transform'] self.baseTransform = self.opts['transform']
else: else:
@ -114,7 +113,6 @@ class CanvasItem(QtCore.QObject):
## every CanvasItem implements its own individual selection box ## every CanvasItem implements its own individual selection box
## so that subclasses are free to make their own. ## so that subclasses are free to make their own.
self.selectBox = SelectBox(scalable=self.opts['scalable'], rotatable=self.opts['rotatable']) self.selectBox = SelectBox(scalable=self.opts['scalable'], rotatable=self.opts['rotatable'])
#self.canvas.scene().addItem(self.selectBox)
self.selectBox.hide() self.selectBox.hide()
self.selectBox.setZValue(1e6) self.selectBox.setZValue(1e6)
self.selectBox.sigRegionChanged.connect(self.selectBoxChanged) ## calls selectBoxMoved self.selectBox.sigRegionChanged.connect(self.selectBoxChanged) ## calls selectBoxMoved
@ -129,16 +127,7 @@ class CanvasItem(QtCore.QObject):
self.tempTransform = SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done. self.tempTransform = SRTTransform() ## holds the additional transform that happens during a move - gets added to the userTransform when move is done.
self.userTransform = SRTTransform() ## stores the total transform of the object self.userTransform = SRTTransform() ## stores the total transform of the object
self.resetUserTransform() self.resetUserTransform()
## now happens inside resetUserTransform -> selectBoxToItem
# self.selectBoxBase = self.selectBox.getState().copy()
#print "Created canvas item", self
#print " base:", self.baseTransform
#print " user:", self.userTransform
#print " temp:", self.tempTransform
#print " bounds:", self.item.sceneBoundingRect()
def setMovable(self, m): def setMovable(self, m):
self.opts['movable'] = m self.opts['movable'] = m
@ -239,7 +228,6 @@ class CanvasItem(QtCore.QObject):
# s=self.updateTransform() # s=self.updateTransform()
# self.setTranslate(-2*s['pos'][0], -2*s['pos'][1]) # self.setTranslate(-2*s['pos'][0], -2*s['pos'][1])
# self.selectBoxFromUser() # self.selectBoxFromUser()
def hasUserTransform(self): def hasUserTransform(self):
#print self.userRotate, self.userTranslate #print self.userRotate, self.userTranslate
@ -252,10 +240,15 @@ class CanvasItem(QtCore.QObject):
alpha = val / 1023. alpha = val / 1023.
self._graphicsItem.setOpacity(alpha) self._graphicsItem.setOpacity(alpha)
def setAlpha(self, alpha):
self.alphaSlider.setValue(int(np.clip(alpha * 1023, 0, 1023)))
def alpha(self):
return self.alphaSlider.value() / 1023.
def isMovable(self): def isMovable(self):
return self.opts['movable'] return self.opts['movable']
def selectBoxMoved(self): def selectBoxMoved(self):
"""The selection box has moved; get its transformation information and pass to the graphics item""" """The selection box has moved; get its transformation information and pass to the graphics item"""
self.userTransform = self.selectBox.getGlobalTransform(relativeTo=self.selectBoxBase) self.userTransform = self.selectBox.getGlobalTransform(relativeTo=self.selectBoxBase)
@ -290,7 +283,6 @@ class CanvasItem(QtCore.QObject):
self.userTransform.setScale(x, y) self.userTransform.setScale(x, y)
self.selectBoxFromUser() self.selectBoxFromUser()
self.updateTransform() self.updateTransform()
def setTemporaryTransform(self, transform): def setTemporaryTransform(self, transform):
self.tempTransform = transform self.tempTransform = transform
@ -302,21 +294,6 @@ class CanvasItem(QtCore.QObject):
self.resetTemporaryTransform() self.resetTemporaryTransform()
self.selectBoxFromUser() ## update the selection box to match the new userTransform self.selectBoxFromUser() ## update the selection box to match the new userTransform
#st = self.userTransform.saveState()
#self.userTransform = self.userTransform * self.tempTransform ## order is important!
#### matrix multiplication affects the scale factors, need to reset
#if st['scale'][0] < 0 or st['scale'][1] < 0:
#nst = self.userTransform.saveState()
#self.userTransform.setScale([-nst['scale'][0], -nst['scale'][1]])
#self.resetTemporaryTransform()
#self.selectBoxFromUser()
#self.selectBoxChangeFinished()
def resetTemporaryTransform(self): def resetTemporaryTransform(self):
self.tempTransform = SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere. self.tempTransform = SRTTransform() ## don't use Transform.reset()--this transform might be used elsewhere.
self.updateTransform() self.updateTransform()
@ -339,20 +316,13 @@ class CanvasItem(QtCore.QObject):
def displayTransform(self, transform): def displayTransform(self, transform):
"""Updates transform numbers in the ctrl widget.""" """Updates transform numbers in the ctrl widget."""
tr = transform.saveState() tr = transform.saveState()
self.transformGui.translateLabel.setText("Translate: (%f, %f)" %(tr['pos'][0], tr['pos'][1])) self.transformGui.translateLabel.setText("Translate: (%f, %f)" %(tr['pos'][0], tr['pos'][1]))
self.transformGui.rotateLabel.setText("Rotate: %f degrees" %tr['angle']) self.transformGui.rotateLabel.setText("Rotate: %f degrees" %tr['angle'])
self.transformGui.scaleLabel.setText("Scale: (%f, %f)" %(tr['scale'][0], tr['scale'][1])) self.transformGui.scaleLabel.setText("Scale: (%f, %f)" %(tr['scale'][0], tr['scale'][1]))
#self.transformGui.mirrorImageCheck.setChecked(False)
#if tr['scale'][0] < 0:
# self.transformGui.mirrorImageCheck.setChecked(True)
def resetUserTransform(self): def resetUserTransform(self):
#self.userRotate = 0
#self.userTranslate = pg.Point(0,0)
self.userTransform.reset() self.userTransform.reset()
self.updateTransform() self.updateTransform()
@ -368,8 +338,6 @@ class CanvasItem(QtCore.QObject):
def restoreTransform(self, tr): def restoreTransform(self, tr):
try: try:
#self.userTranslate = pg.Point(tr['trans'])
#self.userRotate = tr['rot']
self.userTransform = SRTTransform(tr) self.userTransform = SRTTransform(tr)
self.updateTransform() self.updateTransform()
@ -377,16 +345,11 @@ class CanvasItem(QtCore.QObject):
self.sigTransformChanged.emit(self) self.sigTransformChanged.emit(self)
self.sigTransformChangeFinished.emit(self) self.sigTransformChangeFinished.emit(self)
except: except:
#self.userTranslate = pg.Point([0,0])
#self.userRotate = 0
self.userTransform = SRTTransform() self.userTransform = SRTTransform()
debug.printExc("Failed to load transform:") debug.printExc("Failed to load transform:")
#print "set transform", self, self.userTranslate
def saveTransform(self): def saveTransform(self):
"""Return a dict containing the current user transform""" """Return a dict containing the current user transform"""
#print "save transform", self, self.userTranslate
#return {'trans': list(self.userTranslate), 'rot': self.userRotate}
return self.userTransform.saveState() return self.userTransform.saveState()
def selectBoxFromUser(self): def selectBoxFromUser(self):
@ -404,7 +367,6 @@ class CanvasItem(QtCore.QObject):
#self.selectBox.setAngle(self.userRotate) #self.selectBox.setAngle(self.userRotate)
#self.selectBox.setPos([x2, y2]) #self.selectBox.setPos([x2, y2])
self.selectBox.blockSignals(False) self.selectBox.blockSignals(False)
def selectBoxToItem(self): def selectBoxToItem(self):
"""Move/scale the selection box so it fits the item's bounding rect. (assumes item is not rotated)""" """Move/scale the selection box so it fits the item's bounding rect. (assumes item is not rotated)"""
@ -424,11 +386,6 @@ class CanvasItem(QtCore.QObject):
self.opts['z'] = z self.opts['z'] = z
if z is not None: if z is not None:
self._graphicsItem.setZValue(z) self._graphicsItem.setZValue(z)
#def selectionChanged(self, canvas, items):
#self.selected = len(items) == 1 and (items[0] is self)
#self.showSelectBox()
def selectionChanged(self, sel, multi): def selectionChanged(self, sel, multi):
""" """
@ -456,16 +413,12 @@ class CanvasItem(QtCore.QObject):
def hideSelectBox(self): def hideSelectBox(self):
self.selectBox.hide() self.selectBox.hide()
def selectBoxChanged(self): def selectBoxChanged(self):
self.selectBoxMoved() self.selectBoxMoved()
#self.updateTransform(self.selectBox)
#self.emit(QtCore.SIGNAL('transformChanged'), self)
self.sigTransformChanged.emit(self) self.sigTransformChanged.emit(self)
def selectBoxChangeFinished(self): def selectBoxChangeFinished(self):
#self.emit(QtCore.SIGNAL('transformChangeFinished'), self)
self.sigTransformChangeFinished.emit(self) self.sigTransformChangeFinished.emit(self)
def alphaPressed(self): def alphaPressed(self):
@ -500,6 +453,25 @@ class CanvasItem(QtCore.QObject):
def isVisible(self): def isVisible(self):
return self.opts['visible'] return self.opts['visible']
def saveState(self):
return {
'type': self.__class__.__name__,
'name': self.name,
'visible': self.isVisible(),
'alpha': self.alpha(),
'userTransform': self.saveTransform(),
'z': self.zValue(),
'scalable': self.opts['scalable'],
'rotatable': self.opts['rotatable'],
'movable': self.opts['movable'],
}
def restoreState(self, state):
self.setVisible(state['visible'])
self.setAlpha(state['alpha'])
self.restoreTransform(state['userTransform'])
self.setZValue(state['z'])
class GroupCanvasItem(CanvasItem): class GroupCanvasItem(CanvasItem):
""" """

View File

@ -6,14 +6,14 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>490</width> <width>821</width>
<height>414</height> <height>578</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string>Form</string>
</property> </property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout_2">
<property name="margin"> <property name="margin">
<number>0</number> <number>0</number>
</property> </property>
@ -26,88 +26,96 @@
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
</property> </property>
<widget class="GraphicsView" name="view"/> <widget class="GraphicsView" name="view"/>
<widget class="QWidget" name="layoutWidget"> <widget class="QSplitter" name="vsplitter">
<layout class="QGridLayout" name="gridLayout_2"> <property name="orientation">
<item row="2" column="0" colspan="2"> <enum>Qt::Vertical</enum>
<widget class="QPushButton" name="autoRangeBtn"> </property>
<property name="sizePolicy"> <widget class="QWidget" name="canvasCtrlWidget" native="true">
<sizepolicy hsizetype="Minimum" vsizetype="Fixed"> <layout class="QGridLayout" name="gridLayout">
<horstretch>0</horstretch> <item row="0" column="0" colspan="2">
<verstretch>1</verstretch> <widget class="QPushButton" name="autoRangeBtn">
</sizepolicy> <property name="sizePolicy">
</property> <sizepolicy hsizetype="Minimum" vsizetype="Fixed">
<property name="text"> <horstretch>0</horstretch>
<string>Auto Range</string> <verstretch>1</verstretch>
</property> </sizepolicy>
</widget>
</item>
<item row="5" column="0" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="spacing">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="redirectCheck">
<property name="toolTip">
<string>Check to display all local items in a remote canvas.</string>
</property>
<property name="text">
<string>Redirect</string>
</property>
</widget>
</item>
<item>
<widget class="CanvasCombo" name="redirectCombo"/>
</item>
</layout>
</item>
<item row="6" column="0" colspan="2">
<widget class="TreeWidget" name="itemList">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>100</verstretch>
</sizepolicy>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property> </property>
</column> <property name="text">
</widget> <string>Auto Range</string>
</item> </property>
<item row="10" column="0" colspan="2"> </widget>
<layout class="QGridLayout" name="ctrlLayout"> </item>
<property name="spacing"> <item row="1" column="0" colspan="2">
<number>0</number> <layout class="QHBoxLayout" name="horizontalLayout">
</property> <property name="spacing">
</layout> <number>0</number>
</item> </property>
<item row="7" column="0"> <item>
<widget class="QPushButton" name="resetTransformsBtn"> <widget class="QCheckBox" name="redirectCheck">
<property name="text"> <property name="toolTip">
<string>Reset Transforms</string> <string>Check to display all local items in a remote canvas.</string>
</property> </property>
</widget> <property name="text">
</item> <string>Redirect</string>
<item row="3" column="0"> </property>
<widget class="QPushButton" name="mirrorSelectionBtn"> </widget>
<property name="text"> </item>
<string>Mirror Selection</string> <item>
</property> <widget class="CanvasCombo" name="redirectCombo"/>
</widget> </item>
</item> </layout>
<item row="3" column="1"> </item>
<widget class="QPushButton" name="reflectSelectionBtn"> <item row="2" column="0" colspan="2">
<property name="text"> <widget class="TreeWidget" name="itemList">
<string>MirrorXY</string> <property name="sizePolicy">
</property> <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
</widget> <horstretch>0</horstretch>
</item> <verstretch>100</verstretch>
</layout> </sizepolicy>
</property>
<property name="headerHidden">
<bool>true</bool>
</property>
<column>
<property name="text">
<string notr="true">1</string>
</property>
</column>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="QPushButton" name="resetTransformsBtn">
<property name="text">
<string>Reset Transforms</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="mirrorSelectionBtn">
<property name="text">
<string>Mirror Selection</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QPushButton" name="reflectSelectionBtn">
<property name="text">
<string>MirrorXY</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="canvasItemCtrl" native="true">
<layout class="QGridLayout" name="ctrlLayout">
<property name="margin">
<number>0</number>
</property>
<property name="spacing">
<number>0</number>
</property>
</layout>
</widget>
</widget> </widget>
</widget> </widget>
</item> </item>

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' # Form implementation generated from reading ui file 'CanvasTemplate.ui'
# #
# Created by: PyQt4 UI code generator 4.11.4 # Created by: PyQt4 UI code generator 4.11.4
# #
@ -25,39 +25,42 @@ 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(490, 414) Form.resize(821, 578)
self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout_2 = QtGui.QGridLayout(Form)
self.gridLayout.setMargin(0) self.gridLayout_2.setMargin(0)
self.gridLayout.setSpacing(0) self.gridLayout_2.setSpacing(0)
self.gridLayout.setObjectName(_fromUtf8("gridLayout")) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
self.splitter = QtGui.QSplitter(Form) self.splitter = QtGui.QSplitter(Form)
self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName(_fromUtf8("splitter")) self.splitter.setObjectName(_fromUtf8("splitter"))
self.view = GraphicsView(self.splitter) self.view = GraphicsView(self.splitter)
self.view.setObjectName(_fromUtf8("view")) self.view.setObjectName(_fromUtf8("view"))
self.layoutWidget = QtGui.QWidget(self.splitter) self.vsplitter = QtGui.QSplitter(self.splitter)
self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.vsplitter.setOrientation(QtCore.Qt.Vertical)
self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) self.vsplitter.setObjectName(_fromUtf8("vsplitter"))
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.canvasCtrlWidget = QtGui.QWidget(self.vsplitter)
self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) self.canvasCtrlWidget.setObjectName(_fromUtf8("canvasCtrlWidget"))
self.gridLayout = QtGui.QGridLayout(self.canvasCtrlWidget)
self.gridLayout.setObjectName(_fromUtf8("gridLayout"))
self.autoRangeBtn = QtGui.QPushButton(self.canvasCtrlWidget)
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.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, 2, 0, 1, 2) self.gridLayout.addWidget(self.autoRangeBtn, 0, 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"))
self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) self.redirectCheck = QtGui.QCheckBox(self.canvasCtrlWidget)
self.redirectCheck.setObjectName(_fromUtf8("redirectCheck")) self.redirectCheck.setObjectName(_fromUtf8("redirectCheck"))
self.horizontalLayout.addWidget(self.redirectCheck) self.horizontalLayout.addWidget(self.redirectCheck)
self.redirectCombo = CanvasCombo(self.layoutWidget) self.redirectCombo = CanvasCombo(self.canvasCtrlWidget)
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, 5, 0, 1, 2) self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2)
self.itemList = TreeWidget(self.layoutWidget) self.itemList = TreeWidget(self.canvasCtrlWidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(100) sizePolicy.setVerticalStretch(100)
@ -66,21 +69,23 @@ 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, 6, 0, 1, 2) self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2)
self.ctrlLayout = QtGui.QGridLayout() self.resetTransformsBtn = QtGui.QPushButton(self.canvasCtrlWidget)
self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn"))
self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2)
self.mirrorSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget)
self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn"))
self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1)
self.reflectSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget)
self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn"))
self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1)
self.canvasItemCtrl = QtGui.QWidget(self.vsplitter)
self.canvasItemCtrl.setObjectName(_fromUtf8("canvasItemCtrl"))
self.ctrlLayout = QtGui.QGridLayout(self.canvasItemCtrl)
self.ctrlLayout.setMargin(0)
self.ctrlLayout.setSpacing(0) self.ctrlLayout.setSpacing(0)
self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout")) self.ctrlLayout.setObjectName(_fromUtf8("ctrlLayout"))
self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1)
self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget)
self.resetTransformsBtn.setObjectName(_fromUtf8("resetTransformsBtn"))
self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1)
self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget)
self.mirrorSelectionBtn.setObjectName(_fromUtf8("mirrorSelectionBtn"))
self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1)
self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget)
self.reflectSelectionBtn.setObjectName(_fromUtf8("reflectSelectionBtn"))
self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 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)

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' # Form implementation generated from reading ui file 'CanvasTemplate.ui'
# #
# Created by: PyQt5 UI code generator 5.5.1 # Created by: PyQt5 UI code generator 5.7.1
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -11,39 +11,43 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object): class Ui_Form(object):
def setupUi(self, Form): def setupUi(self, Form):
Form.setObjectName("Form") Form.setObjectName("Form")
Form.resize(490, 414) Form.resize(821, 578)
self.gridLayout = QtWidgets.QGridLayout(Form) self.gridLayout_2 = QtWidgets.QGridLayout(Form)
self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0) self.gridLayout_2.setSpacing(0)
self.gridLayout.setObjectName("gridLayout") self.gridLayout_2.setObjectName("gridLayout_2")
self.splitter = QtWidgets.QSplitter(Form) self.splitter = QtWidgets.QSplitter(Form)
self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter") self.splitter.setObjectName("splitter")
self.view = GraphicsView(self.splitter) self.view = GraphicsView(self.splitter)
self.view.setObjectName("view") self.view.setObjectName("view")
self.layoutWidget = QtWidgets.QWidget(self.splitter) self.vsplitter = QtWidgets.QSplitter(self.splitter)
self.layoutWidget.setObjectName("layoutWidget") self.vsplitter.setOrientation(QtCore.Qt.Vertical)
self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget) self.vsplitter.setObjectName("vsplitter")
self.gridLayout_2.setObjectName("gridLayout_2") self.canvasCtrlWidget = QtWidgets.QWidget(self.vsplitter)
self.autoRangeBtn = QtWidgets.QPushButton(self.layoutWidget) self.canvasCtrlWidget.setObjectName("canvasCtrlWidget")
self.gridLayout = QtWidgets.QGridLayout(self.canvasCtrlWidget)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setObjectName("gridLayout")
self.autoRangeBtn = QtWidgets.QPushButton(self.canvasCtrlWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(1) sizePolicy.setVerticalStretch(1)
sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth())
self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setSizePolicy(sizePolicy)
self.autoRangeBtn.setObjectName("autoRangeBtn") self.autoRangeBtn.setObjectName("autoRangeBtn")
self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2)
self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout = QtWidgets.QHBoxLayout()
self.horizontalLayout.setSpacing(0) self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setObjectName("horizontalLayout") self.horizontalLayout.setObjectName("horizontalLayout")
self.redirectCheck = QtWidgets.QCheckBox(self.layoutWidget) self.redirectCheck = QtWidgets.QCheckBox(self.canvasCtrlWidget)
self.redirectCheck.setObjectName("redirectCheck") self.redirectCheck.setObjectName("redirectCheck")
self.horizontalLayout.addWidget(self.redirectCheck) self.horizontalLayout.addWidget(self.redirectCheck)
self.redirectCombo = CanvasCombo(self.layoutWidget) self.redirectCombo = CanvasCombo(self.canvasCtrlWidget)
self.redirectCombo.setObjectName("redirectCombo") self.redirectCombo.setObjectName("redirectCombo")
self.horizontalLayout.addWidget(self.redirectCombo) self.horizontalLayout.addWidget(self.redirectCombo)
self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2)
self.itemList = TreeWidget(self.layoutWidget) self.itemList = TreeWidget(self.canvasCtrlWidget)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(100) sizePolicy.setVerticalStretch(100)
@ -52,21 +56,23 @@ class Ui_Form(object):
self.itemList.setHeaderHidden(True) self.itemList.setHeaderHidden(True)
self.itemList.setObjectName("itemList") self.itemList.setObjectName("itemList")
self.itemList.headerItem().setText(0, "1") self.itemList.headerItem().setText(0, "1")
self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2)
self.ctrlLayout = QtWidgets.QGridLayout() self.resetTransformsBtn = QtWidgets.QPushButton(self.canvasCtrlWidget)
self.resetTransformsBtn.setObjectName("resetTransformsBtn")
self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2)
self.mirrorSelectionBtn = QtWidgets.QPushButton(self.canvasCtrlWidget)
self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn")
self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1)
self.reflectSelectionBtn = QtWidgets.QPushButton(self.canvasCtrlWidget)
self.reflectSelectionBtn.setObjectName("reflectSelectionBtn")
self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1)
self.canvasItemCtrl = QtWidgets.QWidget(self.vsplitter)
self.canvasItemCtrl.setObjectName("canvasItemCtrl")
self.ctrlLayout = QtWidgets.QGridLayout(self.canvasItemCtrl)
self.ctrlLayout.setContentsMargins(0, 0, 0, 0)
self.ctrlLayout.setSpacing(0) self.ctrlLayout.setSpacing(0)
self.ctrlLayout.setObjectName("ctrlLayout") self.ctrlLayout.setObjectName("ctrlLayout")
self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2) self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1)
self.resetTransformsBtn = QtWidgets.QPushButton(self.layoutWidget)
self.resetTransformsBtn.setObjectName("resetTransformsBtn")
self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1)
self.mirrorSelectionBtn = QtWidgets.QPushButton(self.layoutWidget)
self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn")
self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1)
self.reflectSelectionBtn = QtWidgets.QPushButton(self.layoutWidget)
self.reflectSelectionBtn.setObjectName("reflectSelectionBtn")
self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 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)

View File

@ -1,8 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'pyqtgraph/canvas/CanvasTemplate.ui' # Form implementation generated from reading ui file 'CanvasTemplate.ui'
# #
# Created: Wed Nov 9 18:02:00 2016 # Created: Fri Mar 24 16:09:39 2017
# by: pyside-uic 0.2.15 running on PySide 1.2.2 # by: pyside-uic 0.2.15 running on PySide 1.2.2
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -12,40 +12,43 @@ from PySide import QtCore, QtGui
class Ui_Form(object): class Ui_Form(object):
def setupUi(self, Form): def setupUi(self, Form):
Form.setObjectName("Form") Form.setObjectName("Form")
Form.resize(490, 414) Form.resize(821, 578)
self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout_2 = QtGui.QGridLayout(Form)
self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout_2.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0) self.gridLayout_2.setSpacing(0)
self.gridLayout.setObjectName("gridLayout") self.gridLayout_2.setObjectName("gridLayout_2")
self.splitter = QtGui.QSplitter(Form) self.splitter = QtGui.QSplitter(Form)
self.splitter.setOrientation(QtCore.Qt.Horizontal) self.splitter.setOrientation(QtCore.Qt.Horizontal)
self.splitter.setObjectName("splitter") self.splitter.setObjectName("splitter")
self.view = GraphicsView(self.splitter) self.view = GraphicsView(self.splitter)
self.view.setObjectName("view") self.view.setObjectName("view")
self.layoutWidget = QtGui.QWidget(self.splitter) self.vsplitter = QtGui.QSplitter(self.splitter)
self.layoutWidget.setObjectName("layoutWidget") self.vsplitter.setOrientation(QtCore.Qt.Vertical)
self.gridLayout_2 = QtGui.QGridLayout(self.layoutWidget) self.vsplitter.setObjectName("vsplitter")
self.gridLayout_2.setContentsMargins(0, 0, 0, 0) self.canvasCtrlWidget = QtGui.QWidget(self.vsplitter)
self.gridLayout_2.setObjectName("gridLayout_2") self.canvasCtrlWidget.setObjectName("canvasCtrlWidget")
self.autoRangeBtn = QtGui.QPushButton(self.layoutWidget) self.gridLayout = QtGui.QGridLayout(self.canvasCtrlWidget)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setObjectName("gridLayout")
self.autoRangeBtn = QtGui.QPushButton(self.canvasCtrlWidget)
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.autoRangeBtn.sizePolicy().hasHeightForWidth()) sizePolicy.setHeightForWidth(self.autoRangeBtn.sizePolicy().hasHeightForWidth())
self.autoRangeBtn.setSizePolicy(sizePolicy) self.autoRangeBtn.setSizePolicy(sizePolicy)
self.autoRangeBtn.setObjectName("autoRangeBtn") self.autoRangeBtn.setObjectName("autoRangeBtn")
self.gridLayout_2.addWidget(self.autoRangeBtn, 2, 0, 1, 2) self.gridLayout.addWidget(self.autoRangeBtn, 0, 0, 1, 2)
self.horizontalLayout = QtGui.QHBoxLayout() self.horizontalLayout = QtGui.QHBoxLayout()
self.horizontalLayout.setSpacing(0) self.horizontalLayout.setSpacing(0)
self.horizontalLayout.setObjectName("horizontalLayout") self.horizontalLayout.setObjectName("horizontalLayout")
self.redirectCheck = QtGui.QCheckBox(self.layoutWidget) self.redirectCheck = QtGui.QCheckBox(self.canvasCtrlWidget)
self.redirectCheck.setObjectName("redirectCheck") self.redirectCheck.setObjectName("redirectCheck")
self.horizontalLayout.addWidget(self.redirectCheck) self.horizontalLayout.addWidget(self.redirectCheck)
self.redirectCombo = CanvasCombo(self.layoutWidget) self.redirectCombo = CanvasCombo(self.canvasCtrlWidget)
self.redirectCombo.setObjectName("redirectCombo") self.redirectCombo.setObjectName("redirectCombo")
self.horizontalLayout.addWidget(self.redirectCombo) self.horizontalLayout.addWidget(self.redirectCombo)
self.gridLayout_2.addLayout(self.horizontalLayout, 5, 0, 1, 2) self.gridLayout.addLayout(self.horizontalLayout, 1, 0, 1, 2)
self.itemList = TreeWidget(self.layoutWidget) self.itemList = TreeWidget(self.canvasCtrlWidget)
sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0) sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(100) sizePolicy.setVerticalStretch(100)
@ -54,21 +57,24 @@ class Ui_Form(object):
self.itemList.setHeaderHidden(True) self.itemList.setHeaderHidden(True)
self.itemList.setObjectName("itemList") self.itemList.setObjectName("itemList")
self.itemList.headerItem().setText(0, "1") self.itemList.headerItem().setText(0, "1")
self.gridLayout_2.addWidget(self.itemList, 6, 0, 1, 2) self.gridLayout.addWidget(self.itemList, 2, 0, 1, 2)
self.ctrlLayout = QtGui.QGridLayout() self.resetTransformsBtn = QtGui.QPushButton(self.canvasCtrlWidget)
self.ctrlLayout.setSpacing(0)
self.ctrlLayout.setObjectName("ctrlLayout")
self.gridLayout_2.addLayout(self.ctrlLayout, 10, 0, 1, 2)
self.resetTransformsBtn = QtGui.QPushButton(self.layoutWidget)
self.resetTransformsBtn.setObjectName("resetTransformsBtn") self.resetTransformsBtn.setObjectName("resetTransformsBtn")
self.gridLayout_2.addWidget(self.resetTransformsBtn, 7, 0, 1, 1) self.gridLayout.addWidget(self.resetTransformsBtn, 3, 0, 1, 2)
self.mirrorSelectionBtn = QtGui.QPushButton(self.layoutWidget) self.mirrorSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget)
self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn") self.mirrorSelectionBtn.setObjectName("mirrorSelectionBtn")
self.gridLayout_2.addWidget(self.mirrorSelectionBtn, 3, 0, 1, 1) self.gridLayout.addWidget(self.mirrorSelectionBtn, 4, 0, 1, 1)
self.reflectSelectionBtn = QtGui.QPushButton(self.layoutWidget) self.reflectSelectionBtn = QtGui.QPushButton(self.canvasCtrlWidget)
self.reflectSelectionBtn.setObjectName("reflectSelectionBtn") self.reflectSelectionBtn.setObjectName("reflectSelectionBtn")
self.gridLayout_2.addWidget(self.reflectSelectionBtn, 3, 1, 1, 1) self.gridLayout.addWidget(self.reflectSelectionBtn, 4, 1, 1, 1)
self.gridLayout.addWidget(self.splitter, 0, 0, 1, 1) self.canvasItemCtrl = QtGui.QWidget(self.vsplitter)
self.canvasItemCtrl.setObjectName("canvasItemCtrl")
self.ctrlLayout = QtGui.QGridLayout(self.canvasItemCtrl)
self.ctrlLayout.setContentsMargins(0, 0, 0, 0)
self.ctrlLayout.setSpacing(0)
self.ctrlLayout.setContentsMargins(0, 0, 0, 0)
self.ctrlLayout.setObjectName("ctrlLayout")
self.gridLayout_2.addWidget(self.splitter, 0, 0, 1, 1)
self.retranslateUi(Form) self.retranslateUi(Form)
QtCore.QMetaObject.connectSlotsByName(Form) QtCore.QMetaObject.connectSlotsByName(Form)

View File

@ -9,7 +9,7 @@ file format. Data structures may be nested and contain any data type as long
as it can be converted to/from a string using repr and eval. as it can be converted to/from a string using repr and eval.
""" """
import re, os, sys import re, os, sys, datetime
import numpy import numpy
from .pgcollections import OrderedDict from .pgcollections import OrderedDict
from . import units from . import units
@ -143,6 +143,7 @@ def parseString(lines, start=0):
local['Point'] = Point local['Point'] = Point
local['QtCore'] = QtCore local['QtCore'] = QtCore
local['ColorMap'] = ColorMap local['ColorMap'] = ColorMap
local['datetime'] = datetime
# Needed for reconstructing numpy arrays # Needed for reconstructing numpy arrays
local['array'] = numpy.array local['array'] = numpy.array
for dtype in ['int8', 'uint8', for dtype in ['int8', 'uint8',

View File

@ -53,6 +53,7 @@ class ConsoleWidget(QtGui.QWidget):
self.editor = editor self.editor = editor
self.multiline = None self.multiline = None
self.inCmd = False self.inCmd = False
self.frames = [] # stack frames to access when an item in the stack list is selected
self.ui = template.Ui_Form() self.ui = template.Ui_Form()
self.ui.setupUi(self) self.ui.setupUi(self)
@ -114,7 +115,7 @@ class ConsoleWidget(QtGui.QWidget):
self.write("<br><b>%s</b>\n"%encCmd, html=True) self.write("<br><b>%s</b>\n"%encCmd, html=True)
self.execMulti(cmd) self.execMulti(cmd)
else: else:
self.write("<br><div style='background-color: #CCF'><b>%s</b>\n"%encCmd, html=True) self.write("<br><div style='background-color: #CCF; color: black'><b>%s</b>\n"%encCmd, html=True)
self.inCmd = True self.inCmd = True
self.execSingle(cmd) self.execSingle(cmd)
@ -133,26 +134,24 @@ class ConsoleWidget(QtGui.QWidget):
def globals(self): def globals(self):
frame = self.currentFrame() frame = self.currentFrame()
if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): if frame is not None and self.ui.runSelectedFrameCheck.isChecked():
return self.currentFrame().tb_frame.f_globals return self.currentFrame().f_globals
else: else:
return self.localNamespace return self.localNamespace
def locals(self): def locals(self):
frame = self.currentFrame() frame = self.currentFrame()
if frame is not None and self.ui.runSelectedFrameCheck.isChecked(): if frame is not None and self.ui.runSelectedFrameCheck.isChecked():
return self.currentFrame().tb_frame.f_locals return self.currentFrame().f_locals
else: else:
return self.localNamespace return self.localNamespace
def currentFrame(self): def currentFrame(self):
## Return the currently selected exception stack frame (or None if there is no exception) ## Return the currently selected exception stack frame (or None if there is no exception)
if self.currentTraceback is None:
return None
index = self.ui.exceptionStackList.currentRow() index = self.ui.exceptionStackList.currentRow()
tb = self.currentTraceback if index >= 0 and index < len(self.frames):
for i in range(index): return self.frames[index]
tb = tb.tb_next else:
return tb return None
def execSingle(self, cmd): def execSingle(self, cmd):
try: try:
@ -171,7 +170,6 @@ class ConsoleWidget(QtGui.QWidget):
except: except:
self.displayException() self.displayException()
def execMulti(self, nextLine): def execMulti(self, nextLine):
#self.stdout.write(nextLine+"\n") #self.stdout.write(nextLine+"\n")
if nextLine.strip() != '': if nextLine.strip() != '':
@ -202,13 +200,17 @@ class ConsoleWidget(QtGui.QWidget):
self.multiline = None self.multiline = None
def write(self, strn, html=False): def write(self, strn, html=False):
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
if not isGuiThread:
self.stdout.write(strn)
return
self.output.moveCursor(QtGui.QTextCursor.End) self.output.moveCursor(QtGui.QTextCursor.End)
if html: if html:
self.output.textCursor().insertHtml(strn) self.output.textCursor().insertHtml(strn)
else: else:
if self.inCmd: if self.inCmd:
self.inCmd = False self.inCmd = False
self.output.textCursor().insertHtml("</div><br><div style='font-weight: normal; background-color: #FFF;'>") self.output.textCursor().insertHtml("</div><br><div style='font-weight: normal; background-color: #FFF; color: black'>")
#self.stdout.write("</div><br><div style='font-weight: normal; background-color: #FFF;'>") #self.stdout.write("</div><br><div style='font-weight: normal; background-color: #FFF;'>")
self.output.insertPlainText(strn) self.output.insertPlainText(strn)
#self.stdout.write(strn) #self.stdout.write(strn)
@ -275,6 +277,7 @@ class ConsoleWidget(QtGui.QWidget):
def clearExceptionClicked(self): def clearExceptionClicked(self):
self.currentTraceback = None self.currentTraceback = None
self.frames = []
self.ui.exceptionInfoLabel.setText("[No current exception]") self.ui.exceptionInfoLabel.setText("[No current exception]")
self.ui.exceptionStackList.clear() self.ui.exceptionStackList.clear()
self.ui.clearExceptionBtn.setEnabled(False) self.ui.clearExceptionBtn.setEnabled(False)
@ -293,14 +296,6 @@ class ConsoleWidget(QtGui.QWidget):
fileName = tb.tb_frame.f_code.co_filename fileName = tb.tb_frame.f_code.co_filename
subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True) subprocess.Popen(self.editor.format(fileName=fileName, lineNum=lineNum), shell=True)
#def allExceptionsHandler(self, *args):
#self.exceptionHandler(*args)
#def nextExceptionHandler(self, *args):
#self.ui.catchNextExceptionBtn.setChecked(False)
#self.exceptionHandler(*args)
def updateSysTrace(self): def updateSysTrace(self):
## Install or uninstall sys.settrace handler ## Install or uninstall sys.settrace handler
@ -319,24 +314,83 @@ class ConsoleWidget(QtGui.QWidget):
else: else:
sys.settrace(self.systrace) sys.settrace(self.systrace)
def exceptionHandler(self, excType, exc, tb): def exceptionHandler(self, excType, exc, tb, systrace=False):
if self.ui.catchNextExceptionBtn.isChecked(): if self.ui.catchNextExceptionBtn.isChecked():
self.ui.catchNextExceptionBtn.setChecked(False) self.ui.catchNextExceptionBtn.setChecked(False)
elif not self.ui.catchAllExceptionsBtn.isChecked(): elif not self.ui.catchAllExceptionsBtn.isChecked():
return return
self.ui.clearExceptionBtn.setEnabled(True)
self.currentTraceback = tb self.currentTraceback = tb
excMessage = ''.join(traceback.format_exception_only(excType, exc)) excMessage = ''.join(traceback.format_exception_only(excType, exc))
self.ui.exceptionInfoLabel.setText(excMessage) self.ui.exceptionInfoLabel.setText(excMessage)
self.ui.exceptionStackList.clear()
for index, line in enumerate(traceback.extract_tb(tb)): if systrace:
self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line) # exceptions caught using systrace don't need the usual
# call stack + traceback handling
self.setStack(sys._getframe().f_back.f_back)
else:
self.setStack(frame=sys._getframe().f_back, tb=tb)
def setStack(self, frame=None, tb=None):
"""Display a call stack and exception traceback.
This allows the user to probe the contents of any frame in the given stack.
*frame* may either be a Frame instance or None, in which case the current
frame is retrieved from ``sys._getframe()``.
If *tb* is provided then the frames in the traceback will be appended to
the end of the stack list. If *tb* is None, then sys.exc_info() will
be checked instead.
"""
self.ui.clearExceptionBtn.setEnabled(True)
if frame is None:
frame = sys._getframe().f_back
if tb is None:
tb = sys.exc_info()[2]
self.ui.exceptionStackList.clear()
self.frames = []
# Build stack up to this point
for index, line in enumerate(traceback.extract_stack(frame)):
# extract_stack return value changed in python 3.5
if 'FrameSummary' in str(type(line)):
print(dir(line))
line = (line.filename, line.lineno, line.name, line._line)
self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)
while frame is not None:
self.frames.insert(0, frame)
frame = frame.f_back
if tb is None:
return
self.ui.exceptionStackList.addItem('-- exception caught here: --')
item = self.ui.exceptionStackList.item(self.ui.exceptionStackList.count()-1)
item.setBackground(QtGui.QBrush(QtGui.QColor(200, 200, 200)))
item.setForeground(QtGui.QBrush(QtGui.QColor(50, 50, 50)))
self.frames.append(None)
# And finish the rest of the stack up to the exception
for index, line in enumerate(traceback.extract_tb(tb)):
# extract_stack return value changed in python 3.5
if 'FrameSummary' in str(type(line)):
print(dir(line))
line = (line.filename, line.lineno, line.name, line._line)
self.ui.exceptionStackList.addItem('File "%s", line %s, in %s()\n %s' % line)
while tb is not None:
self.frames.append(tb.tb_frame)
tb = tb.tb_next
def systrace(self, frame, event, arg): def systrace(self, frame, event, arg):
if event == 'exception' and self.checkException(*arg): if event == 'exception' and self.checkException(*arg):
self.exceptionHandler(*arg) self.exceptionHandler(*arg, systrace=True)
return self.systrace return self.systrace
def checkException(self, excType, exc, tb): def checkException(self, excType, exc, tb):

View File

@ -6,7 +6,7 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>694</width> <width>739</width>
<height>497</height> <height>497</height>
</rect> </rect>
</property> </property>
@ -86,7 +86,10 @@
<property name="bottomMargin"> <property name="bottomMargin">
<number>0</number> <number>0</number>
</property> </property>
<property name="spacing"> <property name="horizontalSpacing">
<number>2</number>
</property>
<property name="verticalSpacing">
<number>0</number> <number>0</number>
</property> </property>
<item row="0" column="6"> <item row="0" column="6">
@ -95,7 +98,7 @@
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="text"> <property name="text">
<string>Clear Exception</string> <string>Clear Stack</string>
</property> </property>
</widget> </widget>
</item> </item>
@ -149,7 +152,10 @@
<item row="1" column="0" colspan="7"> <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>Stack Trace</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'template.ui' # Form implementation generated from reading ui file 'pyqtgraph/console/template.ui'
# #
# Created: Fri May 02 18:55:28 2014 # Created by: PyQt4 UI code generator 4.11.4
# 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 +25,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(694, 497) Form.resize(739, 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)
@ -37,7 +36,6 @@ class Ui_Form(object):
self.layoutWidget = QtGui.QWidget(self.splitter) self.layoutWidget = QtGui.QWidget(self.splitter)
self.layoutWidget.setObjectName(_fromUtf8("layoutWidget")) self.layoutWidget.setObjectName(_fromUtf8("layoutWidget"))
self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget) self.verticalLayout = QtGui.QVBoxLayout(self.layoutWidget)
self.verticalLayout.setMargin(0)
self.verticalLayout.setObjectName(_fromUtf8("verticalLayout")) self.verticalLayout.setObjectName(_fromUtf8("verticalLayout"))
self.output = QtGui.QPlainTextEdit(self.layoutWidget) self.output = QtGui.QPlainTextEdit(self.layoutWidget)
font = QtGui.QFont() font = QtGui.QFont()
@ -68,8 +66,9 @@ class Ui_Form(object):
self.exceptionGroup = QtGui.QGroupBox(self.splitter) self.exceptionGroup = QtGui.QGroupBox(self.splitter)
self.exceptionGroup.setObjectName(_fromUtf8("exceptionGroup")) self.exceptionGroup.setObjectName(_fromUtf8("exceptionGroup"))
self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup) self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup)
self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
self.gridLayout_2.setHorizontalSpacing(2)
self.gridLayout_2.setVerticalSpacing(0)
self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2")) self.gridLayout_2.setObjectName(_fromUtf8("gridLayout_2"))
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup) self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
self.clearExceptionBtn.setEnabled(False) self.clearExceptionBtn.setEnabled(False)
@ -96,6 +95,7 @@ class Ui_Form(object):
self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck")) self.runSelectedFrameCheck.setObjectName(_fromUtf8("runSelectedFrameCheck"))
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7) self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7)
self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup) self.exceptionInfoLabel = QtGui.QLabel(self.exceptionGroup)
self.exceptionInfoLabel.setWordWrap(True)
self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel")) self.exceptionInfoLabel.setObjectName(_fromUtf8("exceptionInfoLabel"))
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7) self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
@ -116,12 +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.clearExceptionBtn.setText(_translate("Form", "Clear Stack", 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", "Stack Trace", None))
self.label.setText(_translate("Form", "Filter (regex):", None)) self.label.setText(_translate("Form", "Filter (regex):", None))
from .CmdInput import CmdInput from .CmdInput import CmdInput

View File

@ -1,9 +1,8 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Form implementation generated from reading ui file './pyqtgraph/console/template.ui' # Form implementation generated from reading ui file 'pyqtgraph/console/template.ui'
# #
# Created: Wed Mar 26 15:09:29 2014 # Created by: PyQt5 UI code generator 5.5.1
# by: PyQt5 UI code generator 5.0.1
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -12,7 +11,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_Form(object): class Ui_Form(object):
def setupUi(self, Form): def setupUi(self, Form):
Form.setObjectName("Form") Form.setObjectName("Form")
Form.resize(710, 497) Form.resize(739, 497)
self.gridLayout = QtWidgets.QGridLayout(Form) self.gridLayout = QtWidgets.QGridLayout(Form)
self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0) self.gridLayout.setSpacing(0)
@ -23,7 +22,6 @@ class Ui_Form(object):
self.layoutWidget = QtWidgets.QWidget(self.splitter) self.layoutWidget = QtWidgets.QWidget(self.splitter)
self.layoutWidget.setObjectName("layoutWidget") self.layoutWidget.setObjectName("layoutWidget")
self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget) self.verticalLayout = QtWidgets.QVBoxLayout(self.layoutWidget)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.verticalLayout.setObjectName("verticalLayout") self.verticalLayout.setObjectName("verticalLayout")
self.output = QtWidgets.QPlainTextEdit(self.layoutWidget) self.output = QtWidgets.QPlainTextEdit(self.layoutWidget)
font = QtGui.QFont() font = QtGui.QFont()
@ -54,9 +52,14 @@ class Ui_Form(object):
self.exceptionGroup = QtWidgets.QGroupBox(self.splitter) self.exceptionGroup = QtWidgets.QGroupBox(self.splitter)
self.exceptionGroup.setObjectName("exceptionGroup") self.exceptionGroup.setObjectName("exceptionGroup")
self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup) self.gridLayout_2 = QtWidgets.QGridLayout(self.exceptionGroup)
self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
self.gridLayout_2.setHorizontalSpacing(2)
self.gridLayout_2.setVerticalSpacing(0)
self.gridLayout_2.setObjectName("gridLayout_2") self.gridLayout_2.setObjectName("gridLayout_2")
self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup)
self.clearExceptionBtn.setEnabled(False)
self.clearExceptionBtn.setObjectName("clearExceptionBtn")
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 6, 1, 1)
self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup) self.catchAllExceptionsBtn = QtWidgets.QPushButton(self.exceptionGroup)
self.catchAllExceptionsBtn.setCheckable(True) self.catchAllExceptionsBtn.setCheckable(True)
self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn") self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn")
@ -68,24 +71,27 @@ class Ui_Form(object):
self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup) self.onlyUncaughtCheck = QtWidgets.QCheckBox(self.exceptionGroup)
self.onlyUncaughtCheck.setChecked(True) self.onlyUncaughtCheck.setChecked(True)
self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck") self.onlyUncaughtCheck.setObjectName("onlyUncaughtCheck")
self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 2, 1, 1) self.gridLayout_2.addWidget(self.onlyUncaughtCheck, 0, 4, 1, 1)
self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup) self.exceptionStackList = QtWidgets.QListWidget(self.exceptionGroup)
self.exceptionStackList.setAlternatingRowColors(True) self.exceptionStackList.setAlternatingRowColors(True)
self.exceptionStackList.setObjectName("exceptionStackList") self.exceptionStackList.setObjectName("exceptionStackList")
self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 5) self.gridLayout_2.addWidget(self.exceptionStackList, 2, 0, 1, 7)
self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup) self.runSelectedFrameCheck = QtWidgets.QCheckBox(self.exceptionGroup)
self.runSelectedFrameCheck.setChecked(True) self.runSelectedFrameCheck.setChecked(True)
self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck") self.runSelectedFrameCheck.setObjectName("runSelectedFrameCheck")
self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 5) self.gridLayout_2.addWidget(self.runSelectedFrameCheck, 3, 0, 1, 7)
self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup) self.exceptionInfoLabel = QtWidgets.QLabel(self.exceptionGroup)
self.exceptionInfoLabel.setWordWrap(True)
self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") self.exceptionInfoLabel.setObjectName("exceptionInfoLabel")
self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 5) self.gridLayout_2.addWidget(self.exceptionInfoLabel, 1, 0, 1, 7)
self.clearExceptionBtn = QtWidgets.QPushButton(self.exceptionGroup)
self.clearExceptionBtn.setEnabled(False)
self.clearExceptionBtn.setObjectName("clearExceptionBtn")
self.gridLayout_2.addWidget(self.clearExceptionBtn, 0, 4, 1, 1)
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.gridLayout_2.addItem(spacerItem, 0, 3, 1, 1) self.gridLayout_2.addItem(spacerItem, 0, 5, 1, 1)
self.label = QtWidgets.QLabel(self.exceptionGroup)
self.label.setObjectName("label")
self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1)
self.filterText = QtWidgets.QLineEdit(self.exceptionGroup)
self.filterText.setObjectName("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)
@ -97,11 +103,12 @@ class Ui_Form(object):
self.historyBtn.setText(_translate("Form", "History..")) self.historyBtn.setText(_translate("Form", "History.."))
self.exceptionBtn.setText(_translate("Form", "Exceptions..")) self.exceptionBtn.setText(_translate("Form", "Exceptions.."))
self.exceptionGroup.setTitle(_translate("Form", "Exception Handling")) self.exceptionGroup.setTitle(_translate("Form", "Exception Handling"))
self.clearExceptionBtn.setText(_translate("Form", "Clear Stack"))
self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions")) self.catchAllExceptionsBtn.setText(_translate("Form", "Show All Exceptions"))
self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception")) self.catchNextExceptionBtn.setText(_translate("Form", "Show Next Exception"))
self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions")) self.onlyUncaughtCheck.setText(_translate("Form", "Only Uncaught Exceptions"))
self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame")) self.runSelectedFrameCheck.setText(_translate("Form", "Run commands in selected stack frame"))
self.exceptionInfoLabel.setText(_translate("Form", "Exception Info")) self.exceptionInfoLabel.setText(_translate("Form", "Stack Trace"))
self.clearExceptionBtn.setText(_translate("Form", "Clear Exception")) self.label.setText(_translate("Form", "Filter (regex):"))
from .CmdInput import CmdInput from .CmdInput import CmdInput

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 'pyqtgraph/console/template.ui'
# #
# Created: Mon Dec 23 10:10:53 2013 # Created: Tue Sep 19 09:45:18 2017
# by: pyside-uic 0.2.14 running on PySide 1.1.2 # by: pyside-uic 0.2.15 running on PySide 1.2.2
# #
# WARNING! All changes made in this file will be lost! # WARNING! All changes made in this file will be lost!
@ -12,7 +12,7 @@ from PySide import QtCore, QtGui
class Ui_Form(object): class Ui_Form(object):
def setupUi(self, Form): def setupUi(self, Form):
Form.setObjectName("Form") Form.setObjectName("Form")
Form.resize(710, 497) Form.resize(739, 497)
self.gridLayout = QtGui.QGridLayout(Form) self.gridLayout = QtGui.QGridLayout(Form)
self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0) self.gridLayout.setSpacing(0)
@ -54,9 +54,14 @@ class Ui_Form(object):
self.exceptionGroup = QtGui.QGroupBox(self.splitter) self.exceptionGroup = QtGui.QGroupBox(self.splitter)
self.exceptionGroup.setObjectName("exceptionGroup") self.exceptionGroup.setObjectName("exceptionGroup")
self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup) self.gridLayout_2 = QtGui.QGridLayout(self.exceptionGroup)
self.gridLayout_2.setSpacing(0)
self.gridLayout_2.setContentsMargins(-1, 0, -1, 0) self.gridLayout_2.setContentsMargins(-1, 0, -1, 0)
self.gridLayout_2.setHorizontalSpacing(2)
self.gridLayout_2.setVerticalSpacing(0)
self.gridLayout_2.setObjectName("gridLayout_2") self.gridLayout_2.setObjectName("gridLayout_2")
self.clearExceptionBtn = QtGui.QPushButton(self.exceptionGroup)
self.clearExceptionBtn.setEnabled(False)
self.clearExceptionBtn.setObjectName("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("catchAllExceptionsBtn") self.catchAllExceptionsBtn.setObjectName("catchAllExceptionsBtn")
@ -68,24 +73,27 @@ 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("onlyUncaughtCheck") self.onlyUncaughtCheck.setObjectName("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("exceptionStackList") self.exceptionStackList.setObjectName("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("runSelectedFrameCheck") self.runSelectedFrameCheck.setObjectName("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.setWordWrap(True)
self.exceptionInfoLabel.setObjectName("exceptionInfoLabel") self.exceptionInfoLabel.setObjectName("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("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("label")
self.gridLayout_2.addWidget(self.label, 0, 2, 1, 1)
self.filterText = QtGui.QLineEdit(self.exceptionGroup)
self.filterText.setObjectName("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)
@ -96,11 +104,12 @@ class Ui_Form(object):
self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8)) self.historyBtn.setText(QtGui.QApplication.translate("Form", "History..", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionBtn.setText(QtGui.QApplication.translate("Form", "Exceptions..", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionGroup.setTitle(QtGui.QApplication.translate("Form", "Exception Handling", None, QtGui.QApplication.UnicodeUTF8))
self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Stack", None, QtGui.QApplication.UnicodeUTF8))
self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8)) self.catchAllExceptionsBtn.setText(QtGui.QApplication.translate("Form", "Show All Exceptions", None, QtGui.QApplication.UnicodeUTF8))
self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8)) self.catchNextExceptionBtn.setText(QtGui.QApplication.translate("Form", "Show Next Exception", None, QtGui.QApplication.UnicodeUTF8))
self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8)) self.onlyUncaughtCheck.setText(QtGui.QApplication.translate("Form", "Only Uncaught Exceptions", None, QtGui.QApplication.UnicodeUTF8))
self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8)) self.runSelectedFrameCheck.setText(QtGui.QApplication.translate("Form", "Run commands in selected stack frame", None, QtGui.QApplication.UnicodeUTF8))
self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Exception Info", None, QtGui.QApplication.UnicodeUTF8)) self.exceptionInfoLabel.setText(QtGui.QApplication.translate("Form", "Stack Trace", None, QtGui.QApplication.UnicodeUTF8))
self.clearExceptionBtn.setText(QtGui.QApplication.translate("Form", "Clear Exception", None, QtGui.QApplication.UnicodeUTF8)) self.label.setText(QtGui.QApplication.translate("Form", "Filter (regex):", None, QtGui.QApplication.UnicodeUTF8))
from .CmdInput import CmdInput from .CmdInput import CmdInput

View File

@ -1186,3 +1186,23 @@ class ThreadColor(object):
c = (len(self.colors) % 15) + 1 c = (len(self.colors) % 15) + 1
self.colors[tid] = c self.colors[tid] = c
return self.colors[tid] return self.colors[tid]
def enableFaulthandler():
""" Enable faulthandler for all threads.
If the faulthandler package is available, this function disables and then
re-enables fault handling for all threads (this is necessary to ensure any
new threads are handled correctly), and returns True.
If faulthandler is not available, then returns False.
"""
try:
import faulthandler
# necessary to disable first or else new threads may not be handled.
faulthandler.disable()
faulthandler.enable(all_threads=True)
return True
except ImportError:
return False

View File

@ -17,16 +17,20 @@ class Container(object):
def containerChanged(self, c): def containerChanged(self, c):
self._container = c self._container = c
if c is None:
self.area = None
else:
self.area = c.area
def type(self): def type(self):
return None return None
def insert(self, new, pos=None, neighbor=None): def insert(self, new, pos=None, neighbor=None):
# remove from existing parent first
new.setParent(None)
if not isinstance(new, list): if not isinstance(new, list):
new = [new] new = [new]
for n in new:
# remove from existing parent first
n.setParent(None)
if neighbor is None: if neighbor is None:
if pos == 'before': if pos == 'before':
index = 0 index = 0
@ -40,34 +44,37 @@ class Container(object):
index += 1 index += 1
for n in new: for n in new:
#print "change container", n, " -> ", self
n.containerChanged(self)
#print "insert", n, " -> ", self, index #print "insert", n, " -> ", self, index
self._insertItem(n, index) self._insertItem(n, index)
#print "change container", n, " -> ", self
n.containerChanged(self)
index += 1 index += 1
n.sigStretchChanged.connect(self.childStretchChanged) n.sigStretchChanged.connect(self.childStretchChanged)
#print "child added", self #print "child added", self
self.updateStretch() self.updateStretch()
def apoptose(self, propagate=True): def apoptose(self, propagate=True):
##if there is only one (or zero) item in this container, disappear. # if there is only one (or zero) item in this container, disappear.
# if propagate is True, then also attempt to apoptose parent containers.
cont = self._container cont = self._container
c = self.count() c = self.count()
if c > 1: if c > 1:
return return
if self.count() == 1: ## if there is one item, give it to the parent container (unless this is the top) if c == 1: ## if there is one item, give it to the parent container (unless this is the top)
if self is self.area.topContainer: ch = self.widget(0)
if (self.area is not None and self is self.area.topContainer and not isinstance(ch, Container)) or self.container() is None:
return return
self.container().insert(self.widget(0), 'before', self) self.container().insert(ch, 'before', self)
#print "apoptose:", self #print "apoptose:", self
self.close() self.close()
if propagate and cont is not None: if propagate and cont is not None:
cont.apoptose() cont.apoptose()
def close(self): def close(self):
self.area = None
self._container = None
self.setParent(None) self.setParent(None)
if self.area is not None and self.area.topContainer is self:
self.area.topContainer = None
self.containerChanged(None)
def childEvent(self, ev): def childEvent(self, ev):
ch = ev.child() ch = ev.child()
@ -92,7 +99,6 @@ class Container(object):
###Set the stretch values for this container to reflect its contents ###Set the stretch values for this container to reflect its contents
pass pass
def stretch(self): def stretch(self):
"""Return the stretch factors for this container""" """Return the stretch factors for this container"""
return self._stretch return self._stretch

View File

@ -36,6 +36,7 @@ class Dock(QtGui.QWidget, DockDrop):
self.widgetArea.setLayout(self.layout) self.widgetArea.setLayout(self.layout)
self.widgetArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) self.widgetArea.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding)
self.widgets = [] self.widgets = []
self._container = None
self.currentRow = 0 self.currentRow = 0
#self.titlePos = 'top' #self.titlePos = 'top'
self.raiseOverlay() self.raiseOverlay()
@ -187,9 +188,6 @@ class Dock(QtGui.QWidget, DockDrop):
def name(self): def name(self):
return self._name return self._name
def container(self):
return self._container
def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1): def addWidget(self, widget, row=None, col=0, rowspan=1, colspan=1):
""" """
Add a new widget to the interior of this Dock. Add a new widget to the interior of this Dock.
@ -202,7 +200,6 @@ class Dock(QtGui.QWidget, DockDrop):
self.layout.addWidget(widget, row, col, rowspan, colspan) self.layout.addWidget(widget, row, col, rowspan, colspan)
self.raiseOverlay() self.raiseOverlay()
def startDrag(self): def startDrag(self):
self.drag = QtGui.QDrag(self) self.drag = QtGui.QDrag(self)
mime = QtCore.QMimeData() mime = QtCore.QMimeData()
@ -216,21 +213,30 @@ class Dock(QtGui.QWidget, DockDrop):
def float(self): def float(self):
self.area.floatDock(self) self.area.floatDock(self)
def container(self):
return self._container
def containerChanged(self, c): def containerChanged(self, c):
if self._container is not None:
# ask old container to close itself if it is no longer needed
self._container.apoptose()
#print self.name(), "container changed" #print self.name(), "container changed"
self._container = c self._container = c
if c.type() != 'tab': if c is None:
self.moveLabel = True self.area = None
self.label.setDim(False)
else: else:
self.moveLabel = False self.area = c.area
if c.type() != 'tab':
self.setOrientation(force=True) self.moveLabel = True
self.label.setDim(False)
else:
self.moveLabel = False
self.setOrientation(force=True)
def raiseDock(self): def raiseDock(self):
"""If this Dock is stacked underneath others, raise it to the top.""" """If this Dock is stacked underneath others, raise it to the top."""
self.container().raiseDock(self) self.container().raiseDock(self)
def close(self): def close(self):
"""Remove this dock from the DockArea it lives inside.""" """Remove this dock from the DockArea it lives inside."""

View File

@ -61,6 +61,8 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
if isinstance(relativeTo, basestring): if isinstance(relativeTo, basestring):
relativeTo = self.docks[relativeTo] relativeTo = self.docks[relativeTo]
container = self.getContainer(relativeTo) container = self.getContainer(relativeTo)
if container is None:
raise TypeError("Dock %s is not contained in a DockArea; cannot add another dock relative to it." % relativeTo)
neighbor = relativeTo neighbor = relativeTo
## what container type do we need? ## what container type do we need?
@ -98,7 +100,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
#print "request insert", dock, insertPos, neighbor #print "request insert", dock, insertPos, neighbor
old = dock.container() old = dock.container()
container.insert(dock, insertPos, neighbor) container.insert(dock, insertPos, neighbor)
dock.area = self
self.docks[dock.name()] = dock self.docks[dock.name()] = dock
if old is not None: if old is not None:
old.apoptose() old.apoptose()
@ -142,23 +143,19 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def insert(self, new, pos=None, neighbor=None): def insert(self, new, pos=None, neighbor=None):
if self.topContainer is not None: if self.topContainer is not None:
# Adding new top-level container; addContainer() should
# take care of giving the old top container a new home.
self.topContainer.containerChanged(None) self.topContainer.containerChanged(None)
self.layout.addWidget(new) self.layout.addWidget(new)
new.containerChanged(self)
self.topContainer = new self.topContainer = new
#print self, "set top:", new
new._container = self
self.raiseOverlay() self.raiseOverlay()
#print "Insert top:", new
def count(self): def count(self):
if self.topContainer is None: if self.topContainer is None:
return 0 return 0
return 1 return 1
#def paintEvent(self, ev):
#self.drawDockOverlay()
def resizeEvent(self, ev): def resizeEvent(self, ev):
self.resizeOverlay(self.size()) self.resizeOverlay(self.size())
@ -180,7 +177,6 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
area.win.resize(dock.size()) area.win.resize(dock.size())
area.moveDock(dock, 'top', None) area.moveDock(dock, 'top', None)
def removeTempArea(self, area): def removeTempArea(self, area):
self.tempAreas.remove(area) self.tempAreas.remove(area)
#print "close window", area.window() #print "close window", area.window()
@ -212,14 +208,20 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
childs.append(self.childState(obj.widget(i))) childs.append(self.childState(obj.widget(i)))
return (obj.type(), childs, obj.saveState()) return (obj.type(), childs, obj.saveState())
def restoreState(self, state, missing='error', extra='bottom'):
def restoreState(self, state):
""" """
Restore Dock configuration as generated by saveState. Restore Dock configuration as generated by saveState.
Note that this function does not create any Docks--it will only This function does not create any Docks--it will only
restore the arrangement of an existing set of Docks. restore the arrangement of an existing set of Docks.
By default, docks that are described in *state* but do not exist
in the dock area will cause an exception to be raised. This behavior
can be changed by setting *missing* to 'ignore' or 'create'.
Extra docks that are in the dockarea but that are not mentioned in
*state* will be added to the bottom of the dockarea, unless otherwise
specified by the *extra* argument.
""" """
## 1) make dict of all docks and list of existing containers ## 1) make dict of all docks and list of existing containers
@ -229,17 +231,22 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
## 2) create container structure, move docks into new containers ## 2) create container structure, move docks into new containers
if state['main'] is not None: if state['main'] is not None:
self.buildFromState(state['main'], docks, self) self.buildFromState(state['main'], docks, self, missing=missing)
## 3) create floating areas, populate ## 3) create floating areas, populate
for s in state['float']: for s in state['float']:
a = self.addTempArea() a = self.addTempArea()
a.buildFromState(s[0]['main'], docks, a) a.buildFromState(s[0]['main'], docks, a, missing=missing)
a.win.setGeometry(*s[1]) a.win.setGeometry(*s[1])
a.apoptose() # ask temp area to close itself if it is empty
## 4) Add any remaining docks to the bottom ## 4) Add any remaining docks to a float
for d in docks.values(): for d in docks.values():
self.moveDock(d, 'below', None) if extra == 'float':
a = self.addTempArea()
a.addDock(d, 'below')
else:
self.moveDock(d, extra, None)
#print "\nKill old containers:" #print "\nKill old containers:"
## 5) kill old containers ## 5) kill old containers
@ -248,8 +255,7 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
for a in oldTemps: for a in oldTemps:
a.apoptose() a.apoptose()
def buildFromState(self, state, docks, root, depth=0, missing='error'):
def buildFromState(self, state, docks, root, depth=0):
typ, contents, state = state typ, contents, state = state
pfx = " " * depth pfx = " " * depth
if typ == 'dock': if typ == 'dock':
@ -257,7 +263,15 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
obj = docks[contents] obj = docks[contents]
del docks[contents] del docks[contents]
except KeyError: except KeyError:
raise Exception('Cannot restore dock state; no dock with name "%s"' % contents) if missing == 'error':
raise Exception('Cannot restore dock state; no dock with name "%s"' % contents)
elif missing == 'create':
obj = Dock(name=contents)
elif missing == 'ignore':
return
else:
raise ValueError('"missing" argument must be one of "error", "create", or "ignore".')
else: else:
obj = self.makeContainer(typ) obj = self.makeContainer(typ)
@ -266,10 +280,11 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
if typ != 'dock': if typ != 'dock':
for o in contents: for o in contents:
self.buildFromState(o, docks, obj, depth+1) self.buildFromState(o, docks, obj, depth+1, missing=missing)
# remove this container if possible. (there are valid situations when a restore will
# generate empty containers, such as when using missing='ignore')
obj.apoptose(propagate=False) obj.apoptose(propagate=False)
obj.restoreState(state) ## this has to be done later? obj.restoreState(state) ## this has to be done later?
def findAll(self, obj=None, c=None, d=None): def findAll(self, obj=None, c=None, d=None):
if obj is None: if obj is None:
@ -295,14 +310,15 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
d.update(d2) d.update(d2)
return (c, d) return (c, d)
def apoptose(self): def apoptose(self, propagate=True):
# remove top container if possible, close this area if it is temporary.
#print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count() #print "apoptose area:", self.temporary, self.topContainer, self.topContainer.count()
if self.topContainer.count() == 0: if self.topContainer is None or self.topContainer.count() == 0:
self.topContainer = None self.topContainer = None
if self.temporary: if self.temporary:
self.home.removeTempArea(self) self.home.removeTempArea(self)
#self.close() #self.close()
def clear(self): def clear(self):
docks = self.findAll()[1] docks = self.findAll()[1]
for dock in docks.values(): for dock in docks.values():
@ -322,12 +338,38 @@ class DockArea(Container, QtGui.QWidget, DockDrop):
def dropEvent(self, *args): def dropEvent(self, *args):
DockDrop.dropEvent(self, *args) DockDrop.dropEvent(self, *args)
def printState(self, state=None, name='Main'):
# for debugging
if state is None:
state = self.saveState()
print("=== %s dock area ===" % name)
if state['main'] is None:
print(" (empty)")
else:
self._printAreaState(state['main'])
for i, float in enumerate(state['float']):
self.printState(float[0], name='float %d' % i)
class TempAreaWindow(QtGui.QMainWindow): def _printAreaState(self, area, indent=0):
if area[0] == 'dock':
print(" " * indent + area[0] + " " + str(area[1:]))
return
else:
print(" " * indent + area[0])
for ch in area[1]:
self._printAreaState(ch, indent+1)
class TempAreaWindow(QtGui.QWidget):
def __init__(self, area, **kwargs): def __init__(self, area, **kwargs):
QtGui.QMainWindow.__init__(self, **kwargs) QtGui.QWidget.__init__(self, **kwargs)
self.setCentralWidget(area) self.layout = QtGui.QGridLayout()
self.setLayout(self.layout)
self.layout.setContentsMargins(0, 0, 0, 0)
self.dockarea = area
self.layout.addWidget(area)
def closeEvent(self, *args, **kwargs): def closeEvent(self, *args):
self.centralWidget().clear() self.dockarea.clear()
QtGui.QMainWindow.closeEvent(self, *args, **kwargs) QtGui.QWidget.closeEvent(self, *args)

View File

@ -0,0 +1,189 @@
# -*- coding: utf-8 -*-
import pytest
import pyqtgraph as pg
from pyqtgraph.ordereddict import OrderedDict
pg.mkQApp()
import pyqtgraph.dockarea as da
def test_dockarea():
a = da.DockArea()
d1 = da.Dock("dock 1")
a.addDock(d1, 'left')
assert a.topContainer is d1.container()
assert d1.container().container() is a
assert d1.area is a
assert a.topContainer.widget(0) is d1
d2 = da.Dock("dock 2")
a.addDock(d2, 'right')
assert a.topContainer is d1.container()
assert a.topContainer is d2.container()
assert d1.container().container() is a
assert d2.container().container() is a
assert d2.area is a
assert a.topContainer.widget(0) is d1
assert a.topContainer.widget(1) is d2
d3 = da.Dock("dock 3")
a.addDock(d3, 'bottom')
assert a.topContainer is d3.container()
assert d2.container().container() is d3.container()
assert d1.container().container() is d3.container()
assert d1.container().container().container() is a
assert d2.container().container().container() is a
assert d3.container().container() is a
assert d3.area is a
assert d2.area is a
assert a.topContainer.widget(0) is d1.container()
assert a.topContainer.widget(1) is d3
d4 = da.Dock("dock 4")
a.addDock(d4, 'below', d3)
assert d4.container().type() == 'tab'
assert d4.container() is d3.container()
assert d3.container().container() is d2.container().container()
assert d4.area is a
a.printState()
# layout now looks like:
# vcontainer
# hcontainer
# dock 1
# dock 2
# tcontainer
# dock 3
# dock 4
# test save/restore state
state = a.saveState()
a2 = da.DockArea()
# default behavior is to raise exception if docks are missing
with pytest.raises(Exception):
a2.restoreState(state)
# test restore with ignore missing
a2.restoreState(state, missing='ignore')
assert a2.topContainer is None
# test restore with auto-create
a2.restoreState(state, missing='create')
assert a2.saveState() == state
a2.printState()
# double-check that state actually matches the output of saveState()
c1 = a2.topContainer
assert c1.type() == 'vertical'
c2 = c1.widget(0)
c3 = c1.widget(1)
assert c2.type() == 'horizontal'
assert c2.widget(0).name() == 'dock 1'
assert c2.widget(1).name() == 'dock 2'
assert c3.type() == 'tab'
assert c3.widget(0).name() == 'dock 3'
assert c3.widget(1).name() == 'dock 4'
# test restore with docks already present
a3 = da.DockArea()
a3docks = []
for i in range(1, 5):
dock = da.Dock('dock %d' % i)
a3docks.append(dock)
a3.addDock(dock, 'right')
a3.restoreState(state)
assert a3.saveState() == state
# test restore with extra docks present
a3 = da.DockArea()
a3docks = []
for i in [1, 2, 5, 4, 3]:
dock = da.Dock('dock %d' % i)
a3docks.append(dock)
a3.addDock(dock, 'left')
a3.restoreState(state)
a3.printState()
# test a more complex restore
a4 = da.DockArea()
state1 = {'float': [], 'main':
('horizontal', [
('vertical', [
('horizontal', [
('tab', [
('dock', 'dock1', {}),
('dock', 'dock2', {}),
('dock', 'dock3', {}),
('dock', 'dock4', {})
], {'index': 1}),
('vertical', [
('dock', 'dock5', {}),
('horizontal', [
('dock', 'dock6', {}),
('dock', 'dock7', {})
], {'sizes': [184, 363]})
], {'sizes': [355, 120]})
], {'sizes': [9, 552]})
], {'sizes': [480]}),
('dock', 'dock8', {})
], {'sizes': [566, 69]})
}
state2 = {'float': [], 'main':
('horizontal', [
('vertical', [
('horizontal', [
('dock', 'dock2', {}),
('vertical', [
('dock', 'dock5', {}),
('horizontal', [
('dock', 'dock6', {}),
('dock', 'dock7', {})
], {'sizes': [492, 485]})
], {'sizes': [936, 0]})
], {'sizes': [172, 982]})
], {'sizes': [941]}),
('vertical', [
('dock', 'dock8', {}),
('dock', 'dock4', {}),
('dock', 'dock1', {})
], {'sizes': [681, 225, 25]})
], {'sizes': [1159, 116]})}
a4.restoreState(state1, missing='create')
# dock3 not mentioned in restored state; stays in dockarea by default
c, d = a4.findAll()
assert d['dock3'].area is a4
a4.restoreState(state2, missing='ignore', extra='float')
a4.printState()
c, d = a4.findAll()
# dock3 not mentioned in restored state; goes to float due to `extra` argument
assert d['dock3'].area is not a4
assert d['dock1'].container() is d['dock4'].container() is d['dock8'].container()
assert d['dock6'].container() is d['dock7'].container()
assert a4 is d['dock2'].area is d['dock2'].container().container().container()
assert a4 is d['dock5'].area is d['dock5'].container().container().container().container()
# States should be the same with two exceptions:
# dock3 is in a float because it does not appear in state2
# a superfluous vertical splitter in state2 has been removed
state4 = a4.saveState()
state4['main'][1][0] = state4['main'][1][0][1][0]
assert clean_state(state4['main']) == clean_state(state2['main'])
def clean_state(state):
# return state dict with sizes removed
ch = [clean_state(x) for x in state[1]] if isinstance(state[1], list) else state[1]
state = (state[0], ch, {})
if __name__ == '__main__':
test_dockarea()

View File

@ -42,14 +42,20 @@ class HDF5Exporter(Exporter):
dsname = self.params['Name'] dsname = self.params['Name']
fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite" fd = h5py.File(fileName, 'a') # forces append to file... 'w' doesn't seem to "delete/overwrite"
data = [] data = []
appendAllX = self.params['columnMode'] == '(x,y) per plot' appendAllX = self.params['columnMode'] == '(x,y) per plot'
for i,c in enumerate(self.item.curves): #print dir(self.item.curves[0])
tlen = 0
for i, c in enumerate(self.item.curves):
d = c.getData() d = c.getData()
if i > 0 and len(d[0]) != tlen:
raise ValueError ("HDF5 Export requires all curves in plot to have same length")
if appendAllX or i == 0: if appendAllX or i == 0:
data.append(d[0]) data.append(d[0])
tlen = len(d[0])
data.append(d[1]) data.append(d[1])
fdata = numpy.array(data).astype('double') fdata = numpy.array(data).astype('double')
dset = fd.create_dataset(dsname, data=fdata) dset = fd.create_dataset(dsname, data=fdata)
fd.close() fd.close()

View File

@ -27,6 +27,7 @@ class ImageExporter(Exporter):
{'name': 'height', 'type': 'int', 'value': int(tr.height()), 'limits': (0, None)}, {'name': 'height', 'type': 'int', 'value': int(tr.height()), 'limits': (0, None)},
{'name': 'antialias', 'type': 'bool', 'value': True}, {'name': 'antialias', 'type': 'bool', 'value': True},
{'name': 'background', 'type': 'color', 'value': bg}, {'name': 'background', 'type': 'color', 'value': bg},
{'name': 'invertValue', 'type': 'bool', 'value': False}
]) ])
self.params.param('width').sigValueChanged.connect(self.widthChanged) self.params.param('width').sigValueChanged.connect(self.widthChanged)
self.params.param('height').sigValueChanged.connect(self.heightChanged) self.params.param('height').sigValueChanged.connect(self.heightChanged)
@ -67,13 +68,15 @@ class ImageExporter(Exporter):
w, h = self.params['width'], self.params['height'] w, h = self.params['width'], self.params['height']
if w == 0 or h == 0: if w == 0 or h == 0:
raise Exception("Cannot export image with size=0 (requested export size is %dx%d)" % (w,h)) 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['height'], self.params['width'], 4), dtype=np.ubyte)
color = self.params['background'] color = self.params['background']
bg[:,:,0] = color.blue() bg[:,:,0] = color.blue()
bg[:,:,1] = color.green() bg[:,:,1] = color.green()
bg[:,:,2] = color.red() bg[:,:,2] = color.red()
bg[:,:,3] = color.alpha() bg[:,:,3] = color.alpha()
self.png = fn.makeQImage(bg, alpha=True)
self.png = fn.makeQImage(bg, alpha=True, copy=False, transpose=False)
self.bg = bg
## set resolution of image: ## set resolution of image:
origTargetRect = self.getTargetRect() origTargetRect = self.getTargetRect()
@ -91,6 +94,12 @@ class ImageExporter(Exporter):
self.setExportMode(False) self.setExportMode(False)
painter.end() painter.end()
if self.params['invertValue']:
mn = bg[...,:3].min(axis=2)
mx = bg[...,:3].max(axis=2)
d = (255 - mx) - mn
bg[...,:3] += d[...,np.newaxis]
if copy: if copy:
QtGui.QApplication.clipboard().setImage(self.png) QtGui.QApplication.clipboard().setImage(self.png)
elif toBytes: elif toBytes:

View File

@ -166,6 +166,8 @@ class Flowchart(Node):
n[oldName].rename(newName) n[oldName].rename(newName)
def createNode(self, nodeType, name=None, pos=None): def createNode(self, nodeType, name=None, pos=None):
"""Create a new Node and add it to this flowchart.
"""
if name is None: if name is None:
n = 0 n = 0
while True: while True:
@ -179,6 +181,10 @@ class Flowchart(Node):
return node return node
def addNode(self, node, name, pos=None): def addNode(self, node, name, pos=None):
"""Add an existing Node to this flowchart.
See also: createNode()
"""
if pos is None: if pos is None:
pos = [0, 0] pos = [0, 0]
if type(pos) in [QtCore.QPoint, QtCore.QPointF]: if type(pos) in [QtCore.QPoint, QtCore.QPointF]:
@ -189,13 +195,16 @@ class Flowchart(Node):
self.viewBox.addItem(item) self.viewBox.addItem(item)
item.moveBy(*pos) item.moveBy(*pos)
self._nodes[name] = node self._nodes[name] = node
self.widget().addNode(node) if node is not self.inputNode and node is not self.outputNode:
self.widget().addNode(node)
node.sigClosed.connect(self.nodeClosed) node.sigClosed.connect(self.nodeClosed)
node.sigRenamed.connect(self.nodeRenamed) node.sigRenamed.connect(self.nodeRenamed)
node.sigOutputChanged.connect(self.nodeOutputChanged) node.sigOutputChanged.connect(self.nodeOutputChanged)
self.sigChartChanged.emit(self, 'add', node) self.sigChartChanged.emit(self, 'add', node)
def removeNode(self, node): def removeNode(self, node):
"""Remove a Node from this flowchart.
"""
node.close() node.close()
def nodeClosed(self, node): def nodeClosed(self, node):
@ -233,7 +242,6 @@ class Flowchart(Node):
term2 = self.internalTerminal(term2) term2 = self.internalTerminal(term2)
term1.connectTo(term2) term1.connectTo(term2)
def process(self, **args): def process(self, **args):
""" """
Process data through the flowchart, returning the output. Process data through the flowchart, returning the output.
@ -325,7 +333,6 @@ class Flowchart(Node):
#print "DEPS:", deps #print "DEPS:", deps
## determine correct node-processing order ## determine correct node-processing order
#deps[self] = []
order = fn.toposort(deps) order = fn.toposort(deps)
#print "ORDER1:", order #print "ORDER1:", order
@ -349,7 +356,6 @@ class Flowchart(Node):
if lastNode is None or ind > lastInd: if lastNode is None or ind > lastInd:
lastNode = n lastNode = n
lastInd = ind lastInd = ind
#tdeps[t] = lastNode
if lastInd is not None: if lastInd is not None:
dels.append((lastInd+1, t)) dels.append((lastInd+1, t))
dels.sort(key=lambda a: a[0], reverse=True) dels.sort(key=lambda a: a[0], reverse=True)
@ -404,27 +410,25 @@ class Flowchart(Node):
self.inputWasSet = False self.inputWasSet = False
else: else:
self.sigStateChanged.emit() self.sigStateChanged.emit()
def chartGraphicsItem(self): def chartGraphicsItem(self):
"""Return the graphicsItem which displays the internals of this flowchart. """Return the graphicsItem that displays the internal nodes and
(graphicsItem() still returns the external-view item)""" connections of this flowchart.
#return self._chartGraphicsItem
Note that the similar method `graphicsItem()` is inherited from Node
and returns the *external* graphical representation of this flowchart."""
return self.viewBox return self.viewBox
def widget(self): def widget(self):
"""Return the control widget for this flowchart.
This widget provides GUI access to the parameters for each node and a
graphical representation of the flowchart.
"""
if self._widget is None: if self._widget is None:
self._widget = FlowchartCtrlWidget(self) self._widget = FlowchartCtrlWidget(self)
self.scene = self._widget.scene() self.scene = self._widget.scene()
self.viewBox = self._widget.viewBox() self.viewBox = self._widget.viewBox()
#self._scene = QtGui.QGraphicsScene()
#self._widget.setScene(self._scene)
#self.scene.addItem(self.chartGraphicsItem())
#ci = self.chartGraphicsItem()
#self.viewBox.addItem(ci)
#self.viewBox.autoRange()
return self._widget return self._widget
def listConnections(self): def listConnections(self):
@ -437,10 +441,11 @@ class Flowchart(Node):
return conn return conn
def saveState(self): def saveState(self):
"""Return a serializable data structure representing the current state of this flowchart.
"""
state = Node.saveState(self) state = Node.saveState(self)
state['nodes'] = [] state['nodes'] = []
state['connects'] = [] state['connects'] = []
#state['terminals'] = self.saveTerminals()
for name, node in self._nodes.items(): for name, node in self._nodes.items():
cls = type(node) cls = type(node)
@ -460,6 +465,8 @@ class Flowchart(Node):
return state return state
def restoreState(self, state, clear=False): def restoreState(self, state, clear=False):
"""Restore the state of this flowchart from a previous call to `saveState()`.
"""
self.blockSignals(True) self.blockSignals(True)
try: try:
if clear: if clear:
@ -469,7 +476,6 @@ class Flowchart(Node):
nodes.sort(key=lambda a: a['pos'][0]) nodes.sort(key=lambda a: a['pos'][0])
for n in nodes: for n in nodes:
if n['name'] in self._nodes: if n['name'] in self._nodes:
#self._nodes[n['name']].graphicsItem().moveBy(*n['pos'])
self._nodes[n['name']].restoreState(n['state']) self._nodes[n['name']].restoreState(n['state'])
continue continue
try: try:
@ -477,7 +483,6 @@ class Flowchart(Node):
node.restoreState(n['state']) node.restoreState(n['state'])
except: except:
printExc("Error creating node %s: (continuing anyway)" % n['name']) printExc("Error creating node %s: (continuing anyway)" % n['name'])
#node.graphicsItem().moveBy(*n['pos'])
self.inputNode.restoreState(state.get('inputNode', {})) self.inputNode.restoreState(state.get('inputNode', {}))
self.outputNode.restoreState(state.get('outputNode', {})) self.outputNode.restoreState(state.get('outputNode', {}))
@ -490,7 +495,6 @@ class Flowchart(Node):
print(self._nodes[n1].terminals) print(self._nodes[n1].terminals)
print(self._nodes[n2].terminals) print(self._nodes[n2].terminals)
printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2)) printExc("Error connecting terminals %s.%s - %s.%s:" % (n1, t1, n2, t2))
finally: finally:
self.blockSignals(False) self.blockSignals(False)
@ -498,48 +502,46 @@ class Flowchart(Node):
self.sigChartLoaded.emit() self.sigChartLoaded.emit()
self.outputChanged() self.outputChanged()
self.sigStateChanged.emit() self.sigStateChanged.emit()
#self.sigOutputChanged.emit()
def loadFile(self, fileName=None, startDir=None): def loadFile(self, fileName=None, startDir=None):
"""Load a flowchart (*.fc) file.
"""
if fileName is None: if fileName is None:
if startDir is None: if startDir is None:
startDir = self.filePath startDir = self.filePath
if startDir is None: if startDir is None:
startDir = '.' startDir = '.'
self.fileDialog = FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)") self.fileDialog = FileDialog(None, "Load Flowchart..", startDir, "Flowchart (*.fc)")
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
#self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
self.fileDialog.show() self.fileDialog.show()
self.fileDialog.fileSelected.connect(self.loadFile) self.fileDialog.fileSelected.connect(self.loadFile)
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 = unicode(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()
#self.emit(QtCore.SIGNAL('fileLoaded'), fileName)
self.sigFileLoaded.emit(fileName) self.sigFileLoaded.emit(fileName)
def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'): def saveFile(self, fileName=None, startDir=None, suggestedFileName='flowchart.fc'):
"""Save this flowchart to a .fc file
"""
if fileName is None: if fileName is None:
if startDir is None: if startDir is None:
startDir = self.filePath startDir = self.filePath
if startDir is None: if startDir is None:
startDir = '.' startDir = '.'
self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)") self.fileDialog = FileDialog(None, "Save Flowchart..", startDir, "Flowchart (*.fc)")
#self.fileDialog.setFileMode(QtGui.QFileDialog.AnyFile)
self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave) self.fileDialog.setAcceptMode(QtGui.QFileDialog.AcceptSave)
#self.fileDialog.setDirectory(startDir)
self.fileDialog.show() self.fileDialog.show()
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 = unicode(fileName) fileName = unicode(fileName)
configfile.writeConfigFile(self.saveState(), fileName) configfile.writeConfigFile(self.saveState(), fileName)
self.sigFileSaved.emit(fileName) self.sigFileSaved.emit(fileName)
def clear(self): def clear(self):
"""Remove all nodes from this flowchart except the original input/output nodes.
"""
for n in list(self._nodes.values()): for n in list(self._nodes.values()):
if n is self.inputNode or n is self.outputNode: if n is self.inputNode or n is self.outputNode:
continue continue
@ -552,18 +554,15 @@ class Flowchart(Node):
self.inputNode.clearTerminals() self.inputNode.clearTerminals()
self.outputNode.clearTerminals() self.outputNode.clearTerminals()
#class FlowchartGraphicsItem(QtGui.QGraphicsItem):
class FlowchartGraphicsItem(GraphicsObject): class FlowchartGraphicsItem(GraphicsObject):
def __init__(self, chart): def __init__(self, chart):
#print "FlowchartGraphicsItem.__init__"
#QtGui.QGraphicsItem.__init__(self)
GraphicsObject.__init__(self) GraphicsObject.__init__(self)
self.chart = chart ## chart is an instance of Flowchart() self.chart = chart ## chart is an instance of Flowchart()
self.updateTerminals() self.updateTerminals()
def updateTerminals(self): def updateTerminals(self):
#print "FlowchartGraphicsItem.updateTerminals"
self.terminals = {} self.terminals = {}
bounds = self.boundingRect() bounds = self.boundingRect()
inp = self.chart.inputs() inp = self.chart.inputs()
@ -759,6 +758,7 @@ class FlowchartCtrlWidget(QtGui.QWidget):
item = self.items[node] item = self.items[node]
self.ui.ctrlList.setCurrentItem(item) self.ui.ctrlList.setCurrentItem(item)
class FlowchartWidget(dockarea.DockArea): class FlowchartWidget(dockarea.DockArea):
"""Includes the actual graphical flowchart and debugging interface""" """Includes the actual graphical flowchart and debugging interface"""
def __init__(self, chart, ctrl): def __init__(self, chart, ctrl):

View File

@ -189,31 +189,36 @@ class EvalNode(Node):
self.ui = QtGui.QWidget() self.ui = QtGui.QWidget()
self.layout = QtGui.QGridLayout() self.layout = QtGui.QGridLayout()
#self.addInBtn = QtGui.QPushButton('+Input')
#self.addOutBtn = QtGui.QPushButton('+Output')
self.text = QtGui.QTextEdit() self.text = QtGui.QTextEdit()
self.text.setTabStopWidth(30) self.text.setTabStopWidth(30)
self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal") self.text.setPlainText("# Access inputs as args['input_name']\nreturn {'output': None} ## one key per output terminal")
#self.layout.addWidget(self.addInBtn, 0, 0)
#self.layout.addWidget(self.addOutBtn, 0, 1)
self.layout.addWidget(self.text, 1, 0, 1, 2) self.layout.addWidget(self.text, 1, 0, 1, 2)
self.ui.setLayout(self.layout) self.ui.setLayout(self.layout)
#QtCore.QObject.connect(self.addInBtn, QtCore.SIGNAL('clicked()'), self.addInput)
#self.addInBtn.clicked.connect(self.addInput)
#QtCore.QObject.connect(self.addOutBtn, QtCore.SIGNAL('clicked()'), self.addOutput)
#self.addOutBtn.clicked.connect(self.addOutput)
self.text.focusOutEvent = self.focusOutEvent self.text.focusOutEvent = self.focusOutEvent
self.lastText = None self.lastText = None
def ctrlWidget(self): def ctrlWidget(self):
return self.ui return self.ui
#def addInput(self): def setCode(self, code):
#Node.addInput(self, 'input', renamable=True) # unindent code; this allows nicer inline code specification when
# calling this method.
ind = []
lines = code.split('\n')
for line in lines:
stripped = line.lstrip()
if len(stripped) > 0:
ind.append(len(line) - len(stripped))
if len(ind) > 0:
ind = min(ind)
code = '\n'.join([line[ind:] for line in lines])
#def addOutput(self): self.text.clear()
#Node.addOutput(self, 'output', renamable=True) self.text.insertPlainText(code)
def code(self):
return self.text.toPlainText()
def focusOutEvent(self, ev): def focusOutEvent(self, ev):
text = str(self.text.toPlainText()) text = str(self.text.toPlainText())
@ -247,10 +252,10 @@ class EvalNode(Node):
def restoreState(self, state): def restoreState(self, state):
Node.restoreState(self, state) Node.restoreState(self, state)
self.text.clear() self.setCode(state['text'])
self.text.insertPlainText(state['text'])
self.restoreTerminals(state['terminals']) self.restoreTerminals(state['terminals'])
self.update() self.update()
class ColumnJoinNode(Node): class ColumnJoinNode(Node):
"""Concatenates record arrays and/or adds new columns""" """Concatenates record arrays and/or adds new columns"""
@ -354,3 +359,117 @@ class ColumnJoinNode(Node):
self.update() self.update()
class Mean(CtrlNode):
"""Calculate the mean of an array across an axis.
"""
nodeName = 'Mean'
uiTemplate = [
('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}),
]
def processData(self, data):
s = self.stateGroup.state()
ax = None if s['axis'] == -1 else s['axis']
return data.mean(axis=ax)
class Max(CtrlNode):
"""Calculate the maximum of an array across an axis.
"""
nodeName = 'Max'
uiTemplate = [
('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}),
]
def processData(self, data):
s = self.stateGroup.state()
ax = None if s['axis'] == -1 else s['axis']
return data.max(axis=ax)
class Min(CtrlNode):
"""Calculate the minimum of an array across an axis.
"""
nodeName = 'Min'
uiTemplate = [
('axis', 'intSpin', {'value': 0, 'min': -1, 'max': 1000000}),
]
def processData(self, data):
s = self.stateGroup.state()
ax = None if s['axis'] == -1 else s['axis']
return data.min(axis=ax)
class Stdev(CtrlNode):
"""Calculate the standard deviation of an array across an axis.
"""
nodeName = 'Stdev'
uiTemplate = [
('axis', 'intSpin', {'value': -0, 'min': -1, 'max': 1000000}),
]
def processData(self, data):
s = self.stateGroup.state()
ax = None if s['axis'] == -1 else s['axis']
return data.std(axis=ax)
class Index(CtrlNode):
"""Select an index from an array axis.
"""
nodeName = 'Index'
uiTemplate = [
('axis', 'intSpin', {'value': 0, 'min': 0, 'max': 1000000}),
('index', 'intSpin', {'value': 0, 'min': 0, 'max': 1000000}),
]
def processData(self, data):
s = self.stateGroup.state()
ax = s['axis']
ind = s['index']
if ax == 0:
# allow support for non-ndarray sequence types
return data[ind]
else:
return data.take(ind, axis=ax)
class Slice(CtrlNode):
"""Select a slice from an array axis.
"""
nodeName = 'Slice'
uiTemplate = [
('axis', 'intSpin', {'value': 0, 'min': 0, 'max': 1e6}),
('start', 'intSpin', {'value': 0, 'min': -1e6, 'max': 1e6}),
('stop', 'intSpin', {'value': -1, 'min': -1e6, 'max': 1e6}),
('step', 'intSpin', {'value': 1, 'min': -1e6, 'max': 1e6}),
]
def processData(self, data):
s = self.stateGroup.state()
ax = s['axis']
start = s['start']
stop = s['stop']
step = s['step']
if ax == 0:
# allow support for non-ndarray sequence types
return data[start:stop:step]
else:
sl = [slice(None) for i in range(data.ndim)]
sl[ax] = slice(start, stop, step)
return data[sl]
class AsType(CtrlNode):
"""Convert an array to a different dtype.
"""
nodeName = 'AsType'
uiTemplate = [
('dtype', 'combo', {'values': ['float', 'int', 'float32', 'float64', 'float128', 'int8', 'int16', 'int32', 'int64', 'uint8', 'uint16', 'uint32', 'uint64'], 'index': 0}),
]
def processData(self, data):
s = self.stateGroup.state()
return data.astype(s['dtype'])

View File

@ -38,7 +38,7 @@ class Bessel(CtrlNode):
nodeName = 'BesselFilter' nodeName = 'BesselFilter'
uiTemplate = [ uiTemplate = [
('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}),
('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('cutoff', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
('order', 'intSpin', {'value': 4, 'min': 1, 'max': 16}), ('order', 'intSpin', {'value': 4, 'min': 1, 'max': 16}),
('bidir', 'check', {'checked': True}) ('bidir', 'check', {'checked': True})
] ]
@ -57,10 +57,10 @@ class Butterworth(CtrlNode):
nodeName = 'ButterworthFilter' nodeName = 'ButterworthFilter'
uiTemplate = [ uiTemplate = [
('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}), ('band', 'combo', {'values': ['lowpass', 'highpass'], 'index': 0}),
('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
('bidir', 'check', {'checked': True}) ('bidir', 'check', {'checked': True})
] ]
@ -78,14 +78,14 @@ class ButterworthNotch(CtrlNode):
"""Butterworth notch filter""" """Butterworth notch filter"""
nodeName = 'ButterworthNotchFilter' nodeName = 'ButterworthNotchFilter'
uiTemplate = [ uiTemplate = [
('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('low_wPass', 'spin', {'value': 1000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('low_wStop', 'spin', {'value': 2000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('low_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('low_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('high_wPass', 'spin', {'value': 3000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}), ('high_wStop', 'spin', {'value': 4000., 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'Hz', 'siPrefix': True}),
('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('high_gPass', 'spin', {'value': 2.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'range': [0.0, None], 'suffix': 'dB', 'siPrefix': True}), ('high_gStop', 'spin', {'value': 20.0, 'step': 1, 'dec': True, 'bounds': [0.0, None], 'suffix': 'dB', 'siPrefix': True}),
('bidir', 'check', {'checked': True}) ('bidir', 'check', {'checked': True})
] ]
@ -160,19 +160,13 @@ class Gaussian(CtrlNode):
@metaArrayWrapper @metaArrayWrapper
def processData(self, data): def processData(self, data):
sigma = self.ctrls['sigma'].value()
try: try:
import scipy.ndimage import scipy.ndimage
return scipy.ndimage.gaussian_filter(data, sigma)
except ImportError: except ImportError:
raise Exception("GaussianFilter node requires the package scipy.ndimage.") return pgfn.gaussianFilter(data, sigma)
if hasattr(data, 'implements') and data.implements('MetaArray'):
info = data.infoCopy()
filt = pgfn.gaussianFilter(data.asarray(), self.ctrls['sigma'].value())
if 'values' in info[0]:
info[0]['values'] = info[0]['values'][:filt.shape[0]]
return metaarray.MetaArray(filt, info=info)
else:
return pgfn.gaussianFilter(data, self.ctrls['sigma'].value())
class Derivative(CtrlNode): class Derivative(CtrlNode):
"""Returns the pointwise derivative of the input""" """Returns the pointwise derivative of the input"""

View File

@ -1,5 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from ..Node import Node from ..Node import Node
from .common import CtrlNode
class UniOpNode(Node): class UniOpNode(Node):
"""Generic node for performing any operation like Out = In.fn()""" """Generic node for performing any operation like Out = In.fn()"""
@ -13,11 +15,22 @@ class UniOpNode(Node):
def process(self, **args): def process(self, **args):
return {'Out': getattr(args['In'], self.fn)()} return {'Out': getattr(args['In'], self.fn)()}
class BinOpNode(Node): class BinOpNode(CtrlNode):
"""Generic node for performing any operation like A.fn(B)""" """Generic node for performing any operation like A.fn(B)"""
_dtypes = [
'float64', 'float32', 'float16',
'int64', 'int32', 'int16', 'int8',
'uint64', 'uint32', 'uint16', 'uint8'
]
uiTemplate = [
('outputType', 'combo', {'values': ['no change', 'input A', 'input B'] + _dtypes , 'index': 0})
]
def __init__(self, name, fn): def __init__(self, name, fn):
self.fn = fn self.fn = fn
Node.__init__(self, name, terminals={ CtrlNode.__init__(self, name, terminals={
'A': {'io': 'in'}, 'A': {'io': 'in'},
'B': {'io': 'in'}, 'B': {'io': 'in'},
'Out': {'io': 'out', 'bypass': 'A'} 'Out': {'io': 'out', 'bypass': 'A'}
@ -36,6 +49,18 @@ class BinOpNode(Node):
out = fn(args['B']) out = fn(args['B'])
if out is NotImplemented: if out is NotImplemented:
raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B'])))) raise Exception("Operation %s not implemented between %s and %s" % (fn, str(type(args['A'])), str(type(args['B']))))
# Coerce dtype if requested
typ = self.stateGroup.state()['outputType']
if typ == 'no change':
pass
elif typ == 'input A':
out = out.astype(args['A'].dtype)
elif typ == 'input B':
out = out.astype(args['B'].dtype)
else:
out = out.astype(typ)
#print " ", fn, out #print " ", fn, out
return {'Out': out} return {'Out': out}
@ -71,4 +96,10 @@ class DivideNode(BinOpNode):
# try truediv first, followed by div # try truediv first, followed by div
BinOpNode.__init__(self, name, ('__truediv__', '__div__')) BinOpNode.__init__(self, name, ('__truediv__', '__div__'))
class FloorDivideNode(BinOpNode):
"""Returns A // B. Does not check input types."""
nodeName = 'FloorDivide'
def __init__(self, name):
BinOpNode.__init__(self, name, '__floordiv__')

View File

@ -30,6 +30,11 @@ def generateUi(opts):
k, t, o = opt k, t, o = opt
else: else:
raise Exception("Widget specification must be (name, type) or (name, type, {opts})") raise Exception("Widget specification must be (name, type) or (name, type, {opts})")
## clean out these options so they don't get sent to SpinBox
hidden = o.pop('hidden', False)
tip = o.pop('tip', None)
if t == 'intSpin': if t == 'intSpin':
w = QtGui.QSpinBox() w = QtGui.QSpinBox()
if 'max' in o: if 'max' in o:
@ -63,11 +68,12 @@ def generateUi(opts):
w = ColorButton() w = ColorButton()
else: else:
raise Exception("Unknown widget type '%s'" % str(t)) raise Exception("Unknown widget type '%s'" % str(t))
if 'tip' in o:
w.setToolTip(o['tip']) if tip is not None:
w.setToolTip(tip)
w.setObjectName(k) w.setObjectName(k)
l.addRow(k, w) l.addRow(k, w)
if o.get('hidden', False): if hidden:
w.hide() w.hide()
label = l.labelForField(w) label = l.labelForField(w)
label.hide() label.hide()

View File

@ -15,7 +15,7 @@ from .python2_3 import asUnicode, basestring
from .Qt import QtGui, QtCore, USE_PYSIDE from .Qt import QtGui, QtCore, USE_PYSIDE
from . import getConfigOption, setConfigOptions from . import getConfigOption, setConfigOptions
from . import debug from . import debug
from .metaarray import MetaArray
Colors = { Colors = {
@ -110,7 +110,7 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al
return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal)) return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal))
def siParse(s, regex=FLOAT_REGEX): def siParse(s, regex=FLOAT_REGEX, suffix=None):
"""Convert a value written in SI notation to a tuple (number, si_prefix, suffix). """Convert a value written in SI notation to a tuple (number, si_prefix, suffix).
Example:: Example::
@ -118,6 +118,12 @@ def siParse(s, regex=FLOAT_REGEX):
siParse('100 μV") # returns ('100', 'μ', 'V') siParse('100 μV") # returns ('100', 'μ', 'V')
""" """
s = asUnicode(s) s = asUnicode(s)
s = s.strip()
if suffix is not None and len(suffix) > 0:
if s[-len(suffix):] != suffix:
raise ValueError("String '%s' does not have the expected suffix '%s'" % (s, suffix))
s = s[:-len(suffix)] + 'X' # add a fake suffix so the regex still picks up the si prefix
m = regex.match(s) m = regex.match(s)
if m is None: if m is None:
raise ValueError('Cannot parse number "%s"' % s) raise ValueError('Cannot parse number "%s"' % s)
@ -126,15 +132,18 @@ def siParse(s, regex=FLOAT_REGEX):
except IndexError: except IndexError:
sip = '' sip = ''
try: if suffix is None:
suf = m.group('suffix') try:
except IndexError: suf = m.group('suffix')
suf = '' except IndexError:
suf = ''
else:
suf = suffix
return m.group('number'), '' if sip is None else sip, '' if suf is None else suf return m.group('number'), '' if sip is None else sip, '' if suf is None else suf
def siEval(s, typ=float, regex=FLOAT_REGEX): def siEval(s, typ=float, regex=FLOAT_REGEX, suffix=None):
""" """
Convert a value written in SI notation to its equivalent prefixless value. Convert a value written in SI notation to its equivalent prefixless value.
@ -142,9 +151,9 @@ def siEval(s, typ=float, regex=FLOAT_REGEX):
siEval("100 μV") # returns 0.0001 siEval("100 μV") # returns 0.0001
""" """
val, siprefix, suffix = siParse(s, regex) val, siprefix, suffix = siParse(s, regex, suffix=suffix)
v = typ(val) v = typ(val)
return siApply(val, siprefix) return siApply(v, siprefix)
def siApply(val, siprefix): def siApply(val, siprefix):
@ -200,7 +209,7 @@ def mkColor(*args):
try: try:
return Colors[c] return Colors[c]
except KeyError: except KeyError:
raise Exception('No color named "%s"' % c) raise ValueError('No color named "%s"' % c)
if len(c) == 3: if len(c) == 3:
r = int(c[0]*2, 16) r = int(c[0]*2, 16)
g = int(c[1]*2, 16) g = int(c[1]*2, 16)
@ -235,18 +244,18 @@ def mkColor(*args):
elif len(args[0]) == 2: elif len(args[0]) == 2:
return intColor(*args[0]) return intColor(*args[0])
else: else:
raise Exception(err) raise TypeError(err)
elif type(args[0]) == int: elif type(args[0]) == int:
return intColor(args[0]) return intColor(args[0])
else: else:
raise Exception(err) raise TypeError(err)
elif len(args) == 3: elif len(args) == 3:
(r, g, b) = args (r, g, b) = args
a = 255 a = 255
elif len(args) == 4: elif len(args) == 4:
(r, g, b, a) = args (r, g, b, a) = args
else: else:
raise Exception(err) raise TypeError(err)
args = [r,g,b,a] args = [r,g,b,a]
args = [0 if np.isnan(a) or np.isinf(a) else a for a in args] args = [0 if np.isnan(a) or np.isinf(a) else a for a in args]
@ -404,22 +413,53 @@ def makeArrowPath(headLen=20, tipAngle=20, tailLen=20, tailWidth=3, baseAngle=0)
def eq(a, b): def eq(a, b):
"""The great missing equivalence function: Guaranteed evaluation to a single bool value.""" """The great missing equivalence function: Guaranteed evaluation to a single bool value.
This function has some important differences from the == operator:
1. Returns True if a IS b, even if a==b still evaluates to False, such as with nan values.
2. Tests for equivalence using ==, but silently ignores some common exceptions that can occur
(AtrtibuteError, ValueError).
3. When comparing arrays, returns False if the array shapes are not the same.
4. When comparing arrays of the same shape, returns True only if all elements are equal (whereas
the == operator would return a boolean array).
"""
if a is b: if a is b:
return True return True
try: # Avoid comparing large arrays against scalars; this is expensive and we know it should return False.
with warnings.catch_warnings(module=np): # ignore numpy futurewarning (numpy v. 1.10) aIsArr = isinstance(a, (np.ndarray, MetaArray))
e = a==b bIsArr = isinstance(b, (np.ndarray, MetaArray))
except ValueError: if (aIsArr or bIsArr) and type(a) != type(b):
return False return False
except AttributeError:
# If both inputs are arrays, we can speeed up comparison if shapes / dtypes don't match
# NOTE: arrays of dissimilar type should be considered unequal even if they are numerically
# equal because they may behave differently when computed on.
if aIsArr and bIsArr and (a.shape != b.shape or a.dtype != b.dtype):
return False
# Test for equivalence.
# If the test raises a recognized exception, then return Falase
try:
try:
# Sometimes running catch_warnings(module=np) generates AttributeError ???
catcher = warnings.catch_warnings(module=np) # ignore numpy futurewarning (numpy v. 1.10)
catcher.__enter__()
except Exception:
catcher = None
e = a==b
except (ValueError, AttributeError):
return False return False
except: except:
print('failed to evaluate equivalence for:') print('failed to evaluate equivalence for:')
print(" a:", str(type(a)), str(a)) print(" a:", str(type(a)), str(a))
print(" b:", str(type(b)), str(b)) print(" b:", str(type(b)), str(b))
raise raise
finally:
if catcher is not None:
catcher.__exit__(None, None, None)
t = type(e) t = type(e)
if t is bool: if t is bool:
return e return e
@ -716,26 +756,17 @@ def subArray(data, offset, shape, stride):
the input in the example above to have shape (10, 7) would cause the the input in the example above to have shape (10, 7) would cause the
output to have shape (2, 3, 7). output to have shape (2, 3, 7).
""" """
#data = data.flatten() data = np.ascontiguousarray(data)[offset:]
data = data[offset:]
shape = tuple(shape) shape = tuple(shape)
stride = tuple(stride)
extraShape = data.shape[1:] extraShape = data.shape[1:]
#print data.shape, offset, shape, stride
for i in range(len(shape)): strides = list(data.strides[::-1])
mask = (slice(None),) * i + (slice(None, shape[i] * stride[i]),) itemsize = strides[-1]
newShape = shape[:i+1] for s in stride[1::-1]:
if i < len(shape)-1: strides.append(itemsize * s)
newShape += (stride[i],) strides = tuple(strides[::-1])
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 return np.ndarray(buffer=data, shape=shape+extraShape, strides=strides, dtype=data.dtype)
def transformToArray(tr): def transformToArray(tr):
@ -1062,7 +1093,9 @@ def makeARGB(data, lut=None, levels=None, scale=None, useRGBA=False):
minVal, maxVal = levels[i] minVal, maxVal = levels[i]
if minVal == maxVal: if minVal == maxVal:
maxVal += 1e-16 maxVal += 1e-16
newData[...,i] = rescaleData(data[...,i], scale/(maxVal-minVal), minVal, dtype=dtype) rng = maxVal-minVal
rng = 1 if rng == 0 else rng
newData[...,i] = rescaleData(data[...,i], scale / rng, minVal, dtype=dtype)
data = newData data = newData
else: else:
# Apply level scaling unless it would have no effect on the data # Apply level scaling unless it would have no effect on the data
@ -2139,7 +2172,7 @@ def isosurface(data, level):
## compute lookup table of index: vertexes mapping ## compute lookup table of index: vertexes mapping
faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte) faceTableI = np.zeros((len(triTable), i*3), dtype=np.ubyte)
faceTableInds = np.argwhere(nTableFaces == i) faceTableInds = np.argwhere(nTableFaces == i)
faceTableI[faceTableInds[:,0]] = np.array([triTable[j] for j in faceTableInds]) faceTableI[faceTableInds[:,0]] = np.array([triTable[j[0]] for j in faceTableInds])
faceTableI = faceTableI.reshape((len(triTable), i, 3)) faceTableI = faceTableI.reshape((len(triTable), i, 3))
faceShiftTables.append(edgeShifts[faceTableI]) faceShiftTables.append(edgeShifts[faceTableI])

View File

@ -39,7 +39,6 @@ class ArrowItem(QtGui.QGraphicsPathItem):
self.setStyle(**defaultOpts) self.setStyle(**defaultOpts)
self.rotate(self.opts['angle'])
self.moveBy(*self.opts['pos']) self.moveBy(*self.opts['pos'])
def setStyle(self, **opts): def setStyle(self, **opts):
@ -72,7 +71,10 @@ class ArrowItem(QtGui.QGraphicsPathItem):
self.opts.update(opts) self.opts.update(opts)
opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']]) opt = dict([(k,self.opts[k]) for k in ['headLen', 'tipAngle', 'baseAngle', 'tailLen', 'tailWidth']])
self.path = fn.makeArrowPath(**opt) tr = QtGui.QTransform()
tr.rotate(self.opts['angle'])
self.path = tr.map(fn.makeArrowPath(**opt))
self.setPath(self.path) self.setPath(self.path)
self.setPen(fn.mkPen(self.opts['pen'])) self.setPen(fn.mkPen(self.opts['pen']))
@ -82,7 +84,8 @@ class ArrowItem(QtGui.QGraphicsPathItem):
self.setFlags(self.flags() | self.ItemIgnoresTransformations) self.setFlags(self.flags() | self.ItemIgnoresTransformations)
else: else:
self.setFlags(self.flags() & ~self.ItemIgnoresTransformations) self.setFlags(self.flags() & ~self.ItemIgnoresTransformations)
def paint(self, p, *args): def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.Antialiasing)
QtGui.QGraphicsPathItem.paint(self, p, *args) QtGui.QGraphicsPathItem.paint(self, p, *args)

View File

@ -120,7 +120,7 @@ class BarGraphItem(GraphicsObject):
p.setPen(fn.mkPen(pen)) p.setPen(fn.mkPen(pen))
p.setBrush(fn.mkBrush(brush)) p.setBrush(fn.mkBrush(brush))
for i in range(len(x0)): for i in range(len(x0 if not np.isscalar(x0) else y0)):
if pens is not None: if pens is not None:
p.setPen(fn.mkPen(pens[i])) p.setPen(fn.mkPen(pens[i]))
if brushes is not None: if brushes is not None:

View File

@ -146,7 +146,8 @@ class GraphicsItem(object):
return parents return parents
def viewRect(self): def viewRect(self):
"""Return the bounds (in item coordinates) of this item's ViewBox or GraphicsWidget""" """Return the visible bounds of this item's ViewBox or GraphicsWidget,
in the local coordinate system of the item."""
view = self.getViewBox() view = self.getViewBox()
if view is None: if view is None:
return None return None

View File

@ -25,25 +25,41 @@ __all__ = ['HistogramLUTItem']
class HistogramLUTItem(GraphicsWidget): class HistogramLUTItem(GraphicsWidget):
""" """
This is a graphicsWidget which provides controls for adjusting the display of an image. This is a graphicsWidget which provides controls for adjusting the display of an image.
Includes: Includes:
- Image histogram - Image histogram
- Movable region over histogram to select black/white levels - Movable region over histogram to select black/white levels
- Gradient editor to define color lookup table for single-channel images - Gradient editor to define color lookup table for single-channel images
Parameters
----------
image : ImageItem or None
If *image* is provided, then the control will be automatically linked to
the image and changes to the control will be immediately reflected in
the image's appearance.
fillHistogram : bool
By default, the histogram is rendered with a fill.
For performance, set *fillHistogram* = False.
rgbHistogram : bool
Sets whether the histogram is computed once over all channels of the
image, or once per channel.
levelMode : 'mono' or 'rgba'
If 'mono', then only a single set of black/whilte level lines is drawn,
and the levels apply to all channels in the image. If 'rgba', then one
set of levels is drawn for each channel.
""" """
sigLookupTableChanged = QtCore.Signal(object) sigLookupTableChanged = QtCore.Signal(object)
sigLevelsChanged = QtCore.Signal(object) sigLevelsChanged = QtCore.Signal(object)
sigLevelChangeFinished = QtCore.Signal(object) sigLevelChangeFinished = QtCore.Signal(object)
def __init__(self, image=None, fillHistogram=True): def __init__(self, image=None, fillHistogram=True, rgbHistogram=False, levelMode='mono'):
"""
If *image* (ImageItem) is provided, then the control will be automatically linked to the image and changes to the control will be immediately reflected in the image's appearance.
By default, the histogram is rendered with a fill. For performance, set *fillHistogram* = False.
"""
GraphicsWidget.__init__(self) GraphicsWidget.__init__(self)
self.lut = None self.lut = None
self.imageItem = lambda: None # fake a dead weakref self.imageItem = lambda: None # fake a dead weakref
self.levelMode = levelMode
self.rgbHistogram = rgbHistogram
self.layout = QtGui.QGraphicsGridLayout() self.layout = QtGui.QGraphicsGridLayout()
self.setLayout(self.layout) self.setLayout(self.layout)
@ -56,9 +72,26 @@ class HistogramLUTItem(GraphicsWidget):
self.gradient = GradientEditorItem() self.gradient = GradientEditorItem()
self.gradient.setOrientation('right') self.gradient.setOrientation('right')
self.gradient.loadPreset('grey') self.gradient.loadPreset('grey')
self.region = LinearRegionItem([0, 1], LinearRegionItem.Horizontal) self.regions = [
self.region.setZValue(1000) LinearRegionItem([0, 1], 'horizontal', swapMode='block'),
self.vb.addItem(self.region) LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='r',
brush=fn.mkBrush((255, 50, 50, 50)), span=(0., 1/3.)),
LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='g',
brush=fn.mkBrush((50, 255, 50, 50)), span=(1/3., 2/3.)),
LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='b',
brush=fn.mkBrush((50, 50, 255, 80)), span=(2/3., 1.)),
LinearRegionItem([0, 1], 'horizontal', swapMode='block', pen='w',
brush=fn.mkBrush((255, 255, 255, 50)), span=(2/3., 1.))]
for region in self.regions:
region.setZValue(1000)
self.vb.addItem(region)
region.lines[0].addMarker('<|', 0.5)
region.lines[1].addMarker('|>', 0.5)
region.sigRegionChanged.connect(self.regionChanging)
region.sigRegionChangeFinished.connect(self.regionChanged)
self.region = self.regions[0] # for backward compatibility.
self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self) self.axis = AxisItem('left', linkView=self.vb, maxTickLength=-10, parent=self)
self.layout.addItem(self.axis, 0, 0) self.layout.addItem(self.axis, 0, 0)
self.layout.addItem(self.vb, 0, 1) self.layout.addItem(self.vb, 0, 1)
@ -67,76 +100,64 @@ class HistogramLUTItem(GraphicsWidget):
self.gradient.setFlag(self.gradient.ItemStacksBehindParent) self.gradient.setFlag(self.gradient.ItemStacksBehindParent)
self.vb.setFlag(self.gradient.ItemStacksBehindParent) self.vb.setFlag(self.gradient.ItemStacksBehindParent)
#self.grid = GridItem()
#self.vb.addItem(self.grid)
self.gradient.sigGradientChanged.connect(self.gradientChanged) self.gradient.sigGradientChanged.connect(self.gradientChanged)
self.region.sigRegionChanged.connect(self.regionChanging)
self.region.sigRegionChangeFinished.connect(self.regionChanged)
self.vb.sigRangeChanged.connect(self.viewRangeChanged) self.vb.sigRangeChanged.connect(self.viewRangeChanged)
self.plot = PlotDataItem() add = QtGui.QPainter.CompositionMode_Plus
self.plot.rotate(90) self.plots = [
PlotCurveItem(pen=(200, 200, 200, 100)), # mono
PlotCurveItem(pen=(255, 0, 0, 100), compositionMode=add), # r
PlotCurveItem(pen=(0, 255, 0, 100), compositionMode=add), # g
PlotCurveItem(pen=(0, 0, 255, 100), compositionMode=add), # b
PlotCurveItem(pen=(200, 200, 200, 100), compositionMode=add), # a
]
self.plot = self.plots[0] # for backward compatibility.
for plot in self.plots:
plot.rotate(90)
self.vb.addItem(plot)
self.fillHistogram(fillHistogram) self.fillHistogram(fillHistogram)
self._showRegions()
self.vb.addItem(self.plot) self.vb.addItem(self.plot)
self.autoHistogramRange() self.autoHistogramRange()
if image is not None: if image is not None:
self.setImageItem(image) self.setImageItem(image)
#self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding)
def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)): def fillHistogram(self, fill=True, level=0.0, color=(100, 100, 200)):
if fill: colors = [color, (255, 0, 0, 50), (0, 255, 0, 50), (0, 0, 255, 50), (255, 255, 255, 50)]
self.plot.setFillLevel(level) for i,plot in enumerate(self.plots):
self.plot.setFillBrush(color) if fill:
else: plot.setFillLevel(level)
self.plot.setFillLevel(None) plot.setBrush(colors[i])
else:
#def sizeHint(self, *args): plot.setFillLevel(None)
#return QtCore.QSizeF(115, 200)
def paint(self, p, *args): def paint(self, p, *args):
if self.levelMode != 'mono':
return
pen = self.region.lines[0].pen pen = self.region.lines[0].pen
rgn = self.getLevels() rgn = self.getLevels()
p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0])) p1 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[0]))
p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1])) p2 = self.vb.mapFromViewToItem(self, Point(self.vb.viewRect().center().x(), rgn[1]))
gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect()) gradRect = self.gradient.mapRectToParent(self.gradient.gradRect.rect())
for pen in [fn.mkPen('k', width=3), pen]: for pen in [fn.mkPen((0, 0, 0, 100), width=3), pen]:
p.setPen(pen) p.setPen(pen)
p.drawLine(p1, gradRect.bottomLeft()) p.drawLine(p1 + Point(0, 5), gradRect.bottomLeft())
p.drawLine(p2, gradRect.topLeft()) p.drawLine(p2 - Point(0, 5), gradRect.topLeft())
p.drawLine(gradRect.topLeft(), gradRect.topRight()) p.drawLine(gradRect.topLeft(), gradRect.topRight())
p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight()) p.drawLine(gradRect.bottomLeft(), gradRect.bottomRight())
#p.drawRect(self.boundingRect())
def setHistogramRange(self, mn, mx, padding=0.1): def setHistogramRange(self, mn, mx, padding=0.1):
"""Set the Y range on the histogram plot. This disables auto-scaling.""" """Set the Y range on the histogram plot. This disables auto-scaling."""
self.vb.enableAutoRange(self.vb.YAxis, False) self.vb.enableAutoRange(self.vb.YAxis, False)
self.vb.setYRange(mn, mx, padding) self.vb.setYRange(mn, mx, padding)
#d = mx-mn
#mn -= d*padding
#mx += d*padding
#self.range = [mn,mx]
#self.updateRange()
#self.vb.setMouseEnabled(False, True)
#self.region.setBounds([mn,mx])
def autoHistogramRange(self): def autoHistogramRange(self):
"""Enable auto-scaling on the histogram plot.""" """Enable auto-scaling on the histogram plot."""
self.vb.enableAutoRange(self.vb.XYAxes) self.vb.enableAutoRange(self.vb.XYAxes)
#self.range = None
#self.updateRange()
#self.vb.setMouseEnabled(False, False)
#def updateRange(self):
#self.vb.autoRange()
#if self.range is not None:
#self.vb.setYRange(*self.range)
#vr = self.vb.viewRect()
#self.region.setBounds([vr.top(), vr.bottom()])
def setImageItem(self, img): def setImageItem(self, img):
"""Set an ImageItem to have its levels and LUT automatically controlled """Set an ImageItem to have its levels and LUT automatically controlled
@ -145,10 +166,8 @@ class HistogramLUTItem(GraphicsWidget):
self.imageItem = weakref.ref(img) self.imageItem = weakref.ref(img)
img.sigImageChanged.connect(self.imageChanged) img.sigImageChanged.connect(self.imageChanged)
img.setLookupTable(self.getLookupTable) ## send function pointer, not the result img.setLookupTable(self.getLookupTable) ## send function pointer, not the result
#self.gradientChanged()
self.regionChanged() self.regionChanged()
self.imageChanged(autoLevel=True) self.imageChanged(autoLevel=True)
#self.vb.autoRange()
def viewRangeChanged(self): def viewRangeChanged(self):
self.update() self.update()
@ -161,14 +180,14 @@ class HistogramLUTItem(GraphicsWidget):
self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result self.imageItem().setLookupTable(self.getLookupTable) ## send function pointer, not the result
self.lut = None self.lut = None
#if self.imageItem is not None:
#self.imageItem.setLookupTable(self.gradient.getLookupTable(512))
self.sigLookupTableChanged.emit(self) self.sigLookupTableChanged.emit(self)
def getLookupTable(self, img=None, n=None, alpha=None): def getLookupTable(self, img=None, n=None, alpha=None):
"""Return a lookup table from the color gradient defined by this """Return a lookup table from the color gradient defined by this
HistogramLUTItem. HistogramLUTItem.
""" """
if self.levelMode is not 'mono':
return None
if n is None: if n is None:
if img.dtype == np.uint8: if img.dtype == np.uint8:
n = 256 n = 256
@ -180,36 +199,148 @@ class HistogramLUTItem(GraphicsWidget):
def regionChanged(self): def regionChanged(self):
if self.imageItem() is not None: if self.imageItem() is not None:
self.imageItem().setLevels(self.region.getRegion()) self.imageItem().setLevels(self.getLevels())
self.sigLevelChangeFinished.emit(self) self.sigLevelChangeFinished.emit(self)
#self.update()
def regionChanging(self): def regionChanging(self):
if self.imageItem() is not None: if self.imageItem() is not None:
self.imageItem().setLevels(self.region.getRegion()) self.imageItem().setLevels(self.getLevels())
self.sigLevelsChanged.emit(self) self.sigLevelsChanged.emit(self)
self.update() self.update()
def imageChanged(self, autoLevel=False, autoRange=False): def imageChanged(self, autoLevel=False, autoRange=False):
profiler = debug.Profiler() if self.imageItem() is None:
h = self.imageItem().getHistogram()
profiler('get histogram')
if h[0] is None:
return return
self.plot.setData(*h)
profiler('set plot') if self.levelMode == 'mono':
if autoLevel: for plt in self.plots[1:]:
mn = h[0][0] plt.setVisible(False)
mx = h[0][-1] self.plots[0].setVisible(True)
self.region.setRegion([mn, mx]) # plot one histogram for all image data
profiler('set region') profiler = debug.Profiler()
h = self.imageItem().getHistogram()
profiler('get histogram')
if h[0] is None:
return
self.plot.setData(*h)
profiler('set plot')
if autoLevel:
mn = h[0][0]
mx = h[0][-1]
self.region.setRegion([mn, mx])
profiler('set region')
else:
mn, mx = self.imageItem().levels
self.region.setRegion([mn, mx])
else:
# plot one histogram for each channel
self.plots[0].setVisible(False)
ch = self.imageItem().getHistogram(perChannel=True)
if ch[0] is None:
return
for i in range(1, 5):
if len(ch) >= i:
h = ch[i-1]
self.plots[i].setVisible(True)
self.plots[i].setData(*h)
if autoLevel:
mn = h[0][0]
mx = h[0][-1]
self.region[i].setRegion([mn, mx])
else:
# hide channels not present in image data
self.plots[i].setVisible(False)
# make sure we are displaying the correct number of channels
self._showRegions()
def getLevels(self): def getLevels(self):
"""Return the min and max levels. """Return the min and max levels.
"""
return self.region.getRegion()
def setLevels(self, mn, mx): For rgba mode, this returns a list of the levels for each channel.
"""Set the min and max levels.
""" """
self.region.setRegion([mn, mx]) if self.levelMode == 'mono':
return self.region.getRegion()
else:
nch = self.imageItem().channels()
if nch is None:
nch = 3
return [r.getRegion() for r in self.regions[1:nch+1]]
def setLevels(self, min=None, max=None, rgba=None):
"""Set the min/max (bright and dark) levels.
Arguments may be *min* and *max* for single-channel data, or
*rgba* = [(rmin, rmax), ...] for multi-channel data.
"""
if self.levelMode == 'mono':
if min is None:
min, max = rgba[0]
assert None not in (min, max)
self.region.setRegion((min, max))
else:
if rgba is None:
raise TypeError("Must specify rgba argument when levelMode != 'mono'.")
for i, levels in enumerate(rgba):
self.regions[i+1].setRegion(levels)
def setLevelMode(self, mode):
""" Set the method of controlling the image levels offered to the user.
Options are 'mono' or 'rgba'.
"""
assert mode in ('mono', 'rgba')
if mode == self.levelMode:
return
oldLevels = self.getLevels()
self.levelMode = mode
self._showRegions()
# do our best to preserve old levels
if mode == 'mono':
levels = np.array(oldLevels).mean(axis=0)
self.setLevels(*levels)
else:
levels = [oldLevels] * 4
self.setLevels(rgba=levels)
# force this because calling self.setLevels might not set the imageItem
# levels if there was no change to the region item
self.imageItem().setLevels(self.getLevels())
self.imageChanged()
self.update()
def _showRegions(self):
for i in range(len(self.regions)):
self.regions[i].setVisible(False)
if self.levelMode == 'rgba':
imax = 4
if self.imageItem() is not None:
# Only show rgb channels if connected image lacks alpha.
nch = self.imageItem().channels()
if nch is None:
nch = 3
xdif = 1.0 / nch
for i in range(1, nch+1):
self.regions[i].setVisible(True)
self.regions[i].setSpan((i-1) * xdif, i * xdif)
self.gradient.hide()
elif self.levelMode == 'mono':
self.regions[0].setVisible(True)
self.gradient.show()
else:
raise ValueError("Unknown level mode %r" % self.levelMode)
def saveState(self):
return {
'gradient': self.gradient.saveState(),
'levels': self.getLevels(),
'mode': self.levelMode,
}
def restoreState(self, state):
self.setLevelMode(state['mode'])
self.gradient.restoreState(state['gradient'])
self.setLevels(*state['levels'])

View File

@ -98,6 +98,11 @@ class ImageItem(GraphicsObject):
axis = 1 if self.axisOrder == 'col-major' else 0 axis = 1 if self.axisOrder == 'col-major' else 0
return self.image.shape[axis] return self.image.shape[axis]
def channels(self):
if self.image is None:
return None
return self.image.shape[2] if self.image.ndim == 3 else 1
def boundingRect(self): def boundingRect(self):
if self.image is None: if self.image is None:
return QtCore.QRectF(0., 0., 0., 0.) return QtCore.QRectF(0., 0., 0., 0.)
@ -214,7 +219,8 @@ class ImageItem(GraphicsObject):
border Sets the pen used when drawing the image border. Default is None. border Sets the pen used when drawing the image border. Default is None.
autoDownsample (bool) If True, the image is automatically downsampled to match the autoDownsample (bool) If True, the image is automatically downsampled to match the
screen resolution. This improves performance for large images and screen resolution. This improves performance for large images and
reduces aliasing. reduces aliasing. If autoDownsample is not specified, then ImageItem will
choose whether to downsample the image based on its size.
================= ========================================================================= ================= =========================================================================
@ -328,7 +334,7 @@ class ImageItem(GraphicsObject):
sl = [slice(None)] * data.ndim sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2) sl[ax] = slice(None, None, 2)
data = data[sl] data = data[sl]
return nanmin(data), nanmax(data) return np.nanmin(data), np.nanmax(data)
def updateImage(self, *args, **kargs): def updateImage(self, *args, **kargs):
## used for re-rendering qimage from self.image. ## used for re-rendering qimage from self.image.
@ -347,10 +353,15 @@ class ImageItem(GraphicsObject):
profile = debug.Profiler() profile = debug.Profiler()
if self.image is None or self.image.size == 0: if self.image is None or self.image.size == 0:
return return
if isinstance(self.lut, collections.Callable):
lut = self.lut(self.image) # Request a lookup table if this image has only one channel
if self.image.ndim == 2 or self.image.shape[2] == 1:
if isinstance(self.lut, collections.Callable):
lut = self.lut(self.image)
else:
lut = self.lut
else: else:
lut = self.lut lut = None
if self.autoDownsample: if self.autoDownsample:
# reduce dimensions of image based on screen resolution # reduce dimensions of image based on screen resolution
@ -394,9 +405,12 @@ class ImageItem(GraphicsObject):
lut = self._effectiveLut lut = self._effectiveLut
levels = None levels = None
# Convert single-channel image to 2D array
if image.ndim == 3 and image.shape[-1] == 1:
image = image[..., 0]
# Assume images are in column-major order for backward compatibility # Assume images are in column-major order for backward compatibility
# (most images are in row-major order) # (most images are in row-major order)
if self.axisOrder == 'col-major': if self.axisOrder == 'col-major':
image = image.transpose((1, 0, 2)[:image.ndim]) image = image.transpose((1, 0, 2)[:image.ndim])
@ -429,7 +443,8 @@ class ImageItem(GraphicsObject):
self.render() self.render()
self.qimage.save(fileName, *args) self.qimage.save(fileName, *args)
def getHistogram(self, bins='auto', step='auto', targetImageSize=200, targetHistogramSize=500, **kwds): def getHistogram(self, bins='auto', step='auto', perChannel=False, targetImageSize=200,
targetHistogramSize=500, **kwds):
"""Returns x and y arrays containing the histogram values for the current image. """Returns x and y arrays containing the histogram values for the current image.
For an explanation of the return format, see numpy.histogram(). For an explanation of the return format, see numpy.histogram().
@ -445,6 +460,9 @@ class ImageItem(GraphicsObject):
with each bin having an integer width. with each bin having an integer width.
* All other types will have *targetHistogramSize* bins. * All other types will have *targetHistogramSize* bins.
If *perChannel* is True, then the histogram is computed once per channel
and the output is a list of the results.
This method is also used when automatically computing levels. This method is also used when automatically computing levels.
""" """
if self.image is None: if self.image is None:
@ -457,21 +475,33 @@ class ImageItem(GraphicsObject):
stepData = self.image[::step[0], ::step[1]] stepData = self.image[::step[0], ::step[1]]
if bins == 'auto': if bins == 'auto':
mn = stepData.min()
mx = stepData.max()
if stepData.dtype.kind in "ui": if stepData.dtype.kind in "ui":
mn = stepData.min() # For integer data, we select the bins carefully to avoid aliasing
mx = stepData.max()
step = np.ceil((mx-mn) / 500.) step = np.ceil((mx-mn) / 500.)
bins = np.arange(mn, mx+1.01*step, step, dtype=np.int) bins = np.arange(mn, mx+1.01*step, step, dtype=np.int)
if len(bins) == 0:
bins = [mn, mx]
else: else:
bins = 500 # for float data, let numpy select the bins.
bins = np.linspace(mn, mx, 500)
if len(bins) == 0:
bins = [mn, mx]
kwds['bins'] = bins kwds['bins'] = bins
stepData = stepData[np.isfinite(stepData)]
hist = np.histogram(stepData, **kwds) if perChannel:
hist = []
return hist[1][:-1], hist[0] for i in range(stepData.shape[-1]):
stepChan = stepData[..., i]
stepChan = stepChan[np.isfinite(stepChan)]
h = np.histogram(stepChan, **kwds)
hist.append((h[1][:-1], h[0]))
return hist
else:
stepData = stepData[np.isfinite(stepData)]
hist = np.histogram(stepData, **kwds)
return hist[1][:-1], hist[0]
def setPxMode(self, b): def setPxMode(self, b):
""" """

View File

@ -31,7 +31,8 @@ class InfiniteLine(GraphicsObject):
sigPositionChanged = QtCore.Signal(object) sigPositionChanged = QtCore.Signal(object)
def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None, def __init__(self, pos=None, angle=90, pen=None, movable=False, bounds=None,
hoverPen=None, label=None, labelOpts=None, name=None): hoverPen=None, label=None, labelOpts=None, span=(0, 1), markers=None,
name=None):
""" """
=============== ================================================================== =============== ==================================================================
**Arguments:** **Arguments:**
@ -41,22 +42,28 @@ class InfiniteLine(GraphicsObject):
pen Pen to use when drawing line. Can be any arguments that are valid pen Pen to use when drawing line. Can be any arguments that are valid
for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent for :func:`mkPen <pyqtgraph.mkPen>`. Default pen is transparent
yellow. yellow.
hoverPen Pen to use when the mouse cursor hovers over the line.
Only used when movable=True.
movable If True, the line can be dragged to a new position by the user. movable If True, the line can be dragged to a new position by the user.
bounds Optional [min, max] bounding values. Bounds are only valid if the
line is vertical or horizontal.
hoverPen Pen to use when drawing line when hovering over it. Can be any hoverPen Pen to use when drawing line when hovering over it. Can be any
arguments that are valid for :func:`mkPen <pyqtgraph.mkPen>`. arguments that are valid for :func:`mkPen <pyqtgraph.mkPen>`.
Default pen is red. Default pen is red.
bounds Optional [min, max] bounding values. Bounds are only valid if the
line is vertical or horizontal.
label Text to be displayed in a label attached to the line, or label Text to be displayed in a label attached to the line, or
None to show no label (default is None). May optionally None to show no label (default is None). May optionally
include formatting strings to display the line value. include formatting strings to display the line value.
labelOpts A dict of keyword arguments to use when constructing the labelOpts A dict of keyword arguments to use when constructing the
text label. See :class:`InfLineLabel`. text label. See :class:`InfLineLabel`.
span Optional tuple (min, max) giving the range over the view to draw
the line. For example, with a vertical line, use span=(0.5, 1)
to draw only on the top half of the view.
markers List of (marker, position, size) tuples, one per marker to display
on the line. See the addMarker method.
name Name of the item name Name of the item
=============== ================================================================== =============== ==================================================================
""" """
self._boundingRect = None self._boundingRect = None
self._line = None
self._name = name self._name = name
@ -79,11 +86,25 @@ class InfiniteLine(GraphicsObject):
if pen is None: if pen is None:
pen = (200, 200, 100) pen = (200, 200, 100)
self.setPen(pen) self.setPen(pen)
if hoverPen is None: if hoverPen is None:
self.setHoverPen(color=(255,0,0), width=self.pen.width()) self.setHoverPen(color=(255,0,0), width=self.pen.width())
else: else:
self.setHoverPen(hoverPen) self.setHoverPen(hoverPen)
self.span = span
self.currentPen = self.pen self.currentPen = self.pen
self.markers = []
self._maxMarkerSize = 0
if markers is not None:
for m in markers:
self.addMarker(*m)
# Cache variables for managing bounds
self._endPoints = [0, 1] #
self._bounds = None
self._lastViewSize = None
if label is not None: if label is not None:
labelOpts = {} if labelOpts is None else labelOpts labelOpts = {} if labelOpts is None else labelOpts
@ -98,7 +119,12 @@ class InfiniteLine(GraphicsObject):
"""Set the (minimum, maximum) allowable values when dragging.""" """Set the (minimum, maximum) allowable values when dragging."""
self.maxRange = bounds self.maxRange = bounds
self.setValue(self.value()) self.setValue(self.value())
def bounds(self):
"""Return the (minimum, maximum) values allowed when dragging.
"""
return self.maxRange[:]
def setPen(self, *args, **kwargs): def setPen(self, *args, **kwargs):
"""Set the pen for drawing the line. Allowable arguments are any that are valid """Set the pen for drawing the line. Allowable arguments are any that are valid
for :func:`mkPen <pyqtgraph.mkPen>`.""" for :func:`mkPen <pyqtgraph.mkPen>`."""
@ -115,11 +141,70 @@ class InfiniteLine(GraphicsObject):
If the line is not movable, then hovering is also disabled. If the line is not movable, then hovering is also disabled.
Added in version 0.9.9.""" Added in version 0.9.9."""
# If user did not supply a width, then copy it from pen
widthSpecified = ((len(args) == 1 and
(isinstance(args[0], QtGui.QPen) or
(isinstance(args[0], dict) and 'width' in args[0]))
) or 'width' in kwargs)
self.hoverPen = fn.mkPen(*args, **kwargs) self.hoverPen = fn.mkPen(*args, **kwargs)
if not widthSpecified:
self.hoverPen.setWidth(self.pen.width())
if self.mouseHovering: if self.mouseHovering:
self.currentPen = self.hoverPen self.currentPen = self.hoverPen
self.update() self.update()
def addMarker(self, marker, position=0.5, size=10.0):
"""Add a marker to be displayed on the line.
============= =========================================================
**Arguments**
marker String indicating the style of marker to add:
'<|', '|>', '>|', '|<', '<|>', '>|<', '^', 'v', 'o'
position Position (0.0-1.0) along the visible extent of the line
to place the marker. Default is 0.5.
size Size of the marker in pixels. Default is 10.0.
============= =========================================================
"""
path = QtGui.QPainterPath()
if marker == 'o':
path.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
if '<|' in marker:
p = QtGui.QPolygonF([Point(0.5, 0), Point(0, -0.5), Point(-0.5, 0)])
path.addPolygon(p)
path.closeSubpath()
if '|>' in marker:
p = QtGui.QPolygonF([Point(0.5, 0), Point(0, 0.5), Point(-0.5, 0)])
path.addPolygon(p)
path.closeSubpath()
if '>|' in marker:
p = QtGui.QPolygonF([Point(0.5, -0.5), Point(0, 0), Point(-0.5, -0.5)])
path.addPolygon(p)
path.closeSubpath()
if '|<' in marker:
p = QtGui.QPolygonF([Point(0.5, 0.5), Point(0, 0), Point(-0.5, 0.5)])
path.addPolygon(p)
path.closeSubpath()
if '^' in marker:
p = QtGui.QPolygonF([Point(0, -0.5), Point(0.5, 0), Point(0, 0.5)])
path.addPolygon(p)
path.closeSubpath()
if 'v' in marker:
p = QtGui.QPolygonF([Point(0, -0.5), Point(-0.5, 0), Point(0, 0.5)])
path.addPolygon(p)
path.closeSubpath()
self.markers.append((path, position, size))
self._maxMarkerSize = max([m[2] / 2. for m in self.markers])
self.update()
def clearMarkers(self):
""" Remove all markers from this line.
"""
self.markers = []
self._maxMarkerSize = 0
self.update()
def setAngle(self, angle): def setAngle(self, angle):
""" """
Takes angle argument in degrees. Takes angle argument in degrees.
@ -128,7 +213,7 @@ class InfiniteLine(GraphicsObject):
Note that the use of value() and setValue() changes if the line is Note that the use of value() and setValue() changes if the line is
not vertical or horizontal. not vertical or horizontal.
""" """
self.angle = ((angle+45) % 180) - 45 ## -45 <= angle < 135 self.angle = angle #((angle+45) % 180) - 45 ## -45 <= angle < 135
self.resetTransform() self.resetTransform()
self.rotate(self.angle) self.rotate(self.angle)
self.update() self.update()
@ -199,35 +284,98 @@ class InfiniteLine(GraphicsObject):
#else: #else:
#print "ignore", change #print "ignore", change
#return GraphicsObject.itemChange(self, change, val) #return GraphicsObject.itemChange(self, change, val)
def setSpan(self, mn, mx):
if self.span != (mn, mx):
self.span = (mn, mx)
self.update()
def _invalidateCache(self): def _invalidateCache(self):
self._line = None
self._boundingRect = None self._boundingRect = None
def _computeBoundingRect(self):
#br = UIGraphicsItem.boundingRect(self)
vr = self.viewRect() # bounds of containing ViewBox mapped to local coords.
if vr is None:
return QtCore.QRectF()
## add a 4-pixel radius around the line for mouse interaction.
px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
if px is None:
px = 0
pw = max(self.pen.width() / 2, self.hoverPen.width() / 2)
w = max(4, self._maxMarkerSize + pw) + 1
w = w * px
br = QtCore.QRectF(vr)
br.setBottom(-w)
br.setTop(w)
length = br.width()
left = br.left() + length * self.span[0]
right = br.left() + length * self.span[1]
br.setLeft(left - w)
br.setRight(right + w)
br = br.normalized()
vs = self.getViewBox().size()
if self._bounds != br or self._lastViewSize != vs:
self._bounds = br
self._lastViewSize = vs
self.prepareGeometryChange()
self._endPoints = (left, right)
self._lastViewRect = vr
return self._bounds
def boundingRect(self): def boundingRect(self):
if self._boundingRect is None: if self._boundingRect is None:
#br = UIGraphicsItem.boundingRect(self) self._boundingRect = self._computeBoundingRect()
br = self.viewRect()
if br is None:
return QtCore.QRectF()
## add a 4-pixel radius around the line for mouse interaction.
px = self.pixelLength(direction=Point(1,0), ortho=True) ## get pixel length orthogonal to the line
if px is None:
px = 0
w = (max(4, self.pen.width()/2, self.hoverPen.width()/2)+1) * px
br.setBottom(-w)
br.setTop(w)
br = br.normalized()
self._boundingRect = br
self._line = QtCore.QLineF(br.right(), 0.0, br.left(), 0.0)
return self._boundingRect return self._boundingRect
def paint(self, p, *args): def paint(self, p, *args):
p.setPen(self.currentPen) p.setRenderHint(p.Antialiasing)
p.drawLine(self._line)
left, right = self._endPoints
pen = self.currentPen
pen.setJoinStyle(QtCore.Qt.MiterJoin)
p.setPen(pen)
p.drawLine(Point(left, 0), Point(right, 0))
if len(self.markers) == 0:
return
# paint markers in native coordinate system
tr = p.transform()
p.resetTransform()
start = tr.map(Point(left, 0))
end = tr.map(Point(right, 0))
up = tr.map(Point(left, 1))
dif = end - start
length = Point(dif).length()
angle = np.arctan2(dif.y(), dif.x()) * 180 / np.pi
p.translate(start)
p.rotate(angle)
up = up - start
det = up.x() * dif.y() - dif.x() * up.y()
p.scale(1, 1 if det > 0 else -1)
p.setBrush(fn.mkBrush(self.currentPen.color()))
#p.setPen(fn.mkPen(None))
tr = p.transform()
for path, pos, size in self.markers:
p.setTransform(tr)
x = length * pos
p.translate(x, 0)
p.scale(size, size)
p.drawPath(path)
def dataBounds(self, axis, frac=1.0, orthoRange=None): def dataBounds(self, axis, frac=1.0, orthoRange=None):
if axis == 0: if axis == 0:
return None ## x axis should never be auto-scaled return None ## x axis should never be auto-scaled

View File

@ -81,19 +81,19 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
self.layout.addItem(label, row, 1) self.layout.addItem(label, row, 1)
self.updateSize() self.updateSize()
def removeItem(self, name): def removeItem(self, item):
""" """
Removes one item from the legend. Removes one item from the legend.
============== ======================================================== ============== ========================================================
**Arguments:** **Arguments:**
title The title displayed for this item. item The item to remove or its name.
============== ======================================================== ============== ========================================================
""" """
# Thanks, Ulrich! # Thanks, Ulrich!
# cycle for a match # cycle for a match
for sample, label in self.items: for sample, label in self.items:
if label.text == name: # hit if sample.item is item or label.text == item:
self.items.remove( (sample, label) ) # remove from itemlist self.items.remove( (sample, label) ) # remove from itemlist
self.layout.removeItem(sample) # remove from layout self.layout.removeItem(sample) # remove from layout
sample.close() # remove from drawing sample.close() # remove from drawing
@ -130,7 +130,8 @@ class LegendItem(GraphicsWidget, GraphicsWidgetAnchor):
if ev.button() == QtCore.Qt.LeftButton: if ev.button() == QtCore.Qt.LeftButton:
dpos = ev.pos() - ev.lastPos() dpos = ev.pos() - ev.lastPos()
self.autoAnchor(self.pos() + dpos) self.autoAnchor(self.pos() + dpos)
class ItemSample(GraphicsWidget): class ItemSample(GraphicsWidget):
""" Class responsible for drawing a single item in a LegendItem (sans label). """ Class responsible for drawing a single item in a LegendItem (sans label).

View File

@ -1,14 +1,14 @@
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
from .UIGraphicsItem import UIGraphicsItem from .GraphicsObject import GraphicsObject
from .InfiniteLine import InfiniteLine from .InfiniteLine import InfiniteLine
from .. import functions as fn from .. import functions as fn
from .. import debug as debug from .. import debug as debug
__all__ = ['LinearRegionItem'] __all__ = ['LinearRegionItem']
class LinearRegionItem(UIGraphicsItem): class LinearRegionItem(GraphicsObject):
""" """
**Bases:** :class:`UIGraphicsItem <pyqtgraph.UIGraphicsItem>` **Bases:** :class:`GraphicsObject <pyqtgraph.GraphicsObject>`
Used for marking a horizontal or vertical region in plots. Used for marking a horizontal or vertical region in plots.
The region can be dragged and is bounded by lines which can be dragged individually. The region can be dragged and is bounded by lines which can be dragged individually.
@ -26,65 +26,110 @@ class LinearRegionItem(UIGraphicsItem):
sigRegionChanged = QtCore.Signal(object) sigRegionChanged = QtCore.Signal(object)
Vertical = 0 Vertical = 0
Horizontal = 1 Horizontal = 1
_orientation_axis = {
Vertical: 0,
Horizontal: 1,
'vertical': 0,
'horizontal': 1,
}
def __init__(self, values=[0,1], orientation=None, brush=None, movable=True, bounds=None): def __init__(self, values=(0, 1), orientation='vertical', brush=None, pen=None,
hoverBrush=None, hoverPen=None, movable=True, bounds=None,
span=(0, 1), swapMode='sort'):
"""Create a new LinearRegionItem. """Create a new LinearRegionItem.
============== ===================================================================== ============== =====================================================================
**Arguments:** **Arguments:**
values A list of the positions of the lines in the region. These are not values A list of the positions of the lines in the region. These are not
limits; limits can be set by specifying bounds. limits; limits can be set by specifying bounds.
orientation Options are LinearRegionItem.Vertical or LinearRegionItem.Horizontal. orientation Options are 'vertical' or 'horizontal', indicating the
If not specified it will be vertical. The default is 'vertical', indicating that the
brush Defines the brush that fills the region. Can be any arguments that brush Defines the brush that fills the region. Can be any arguments that
are valid for :func:`mkBrush <pyqtgraph.mkBrush>`. Default is are valid for :func:`mkBrush <pyqtgraph.mkBrush>`. Default is
transparent blue. transparent blue.
pen The pen to use when drawing the lines that bound the region.
hoverBrush The brush to use when the mouse is hovering over the region.
hoverPen The pen to use when the mouse is hovering over the region.
movable If True, the region and individual lines are movable by the user; if movable If True, the region and individual lines are movable by the user; if
False, they are static. False, they are static.
bounds Optional [min, max] bounding values for the region bounds Optional [min, max] bounding values for the region
span Optional [min, max] giving the range over the view to draw
the region. For example, with a vertical line, use span=(0.5, 1)
to draw only on the top half of the view.
swapMode Sets the behavior of the region when the lines are moved such that
their order reverses:
* "block" means the user cannot drag one line past the other
* "push" causes both lines to be moved if one would cross the other
* "sort" means that lines may trade places, but the output of
getRegion always gives the line positions in ascending order.
* None means that no attempt is made to handle swapped line
positions.
The default is "sort".
============== ===================================================================== ============== =====================================================================
""" """
UIGraphicsItem.__init__(self) GraphicsObject.__init__(self)
if orientation is None:
orientation = LinearRegionItem.Vertical
self.orientation = orientation self.orientation = orientation
self.bounds = QtCore.QRectF() self.bounds = QtCore.QRectF()
self.blockLineSignal = False self.blockLineSignal = False
self.moving = False self.moving = False
self.mouseHovering = False self.mouseHovering = False
self.span = span
self.swapMode = swapMode
self._bounds = None
if orientation == LinearRegionItem.Horizontal: # note LinearRegionItem.Horizontal and LinearRegionItem.Vertical
# are kept for backward compatibility.
lineKwds = dict(
movable=movable,
bounds=bounds,
span=span,
pen=pen,
hoverPen=hoverPen,
)
if orientation in ('horizontal', LinearRegionItem.Horizontal):
self.lines = [ self.lines = [
InfiniteLine(QtCore.QPointF(0, values[0]), 0, movable=movable, bounds=bounds), # rotate lines to 180 to preserve expected line orientation
InfiniteLine(QtCore.QPointF(0, values[1]), 0, movable=movable, bounds=bounds)] # with respect to region. This ensures that placing a '<|'
elif orientation == LinearRegionItem.Vertical: # marker on lines[0] causes it to point left in vertical mode
# and down in horizontal mode.
InfiniteLine(QtCore.QPointF(0, values[0]), angle=0, **lineKwds),
InfiniteLine(QtCore.QPointF(0, values[1]), angle=0, **lineKwds)]
self.lines[0].scale(1, -1)
self.lines[1].scale(1, -1)
elif orientation in ('vertical', LinearRegionItem.Vertical):
self.lines = [ self.lines = [
InfiniteLine(QtCore.QPointF(values[1], 0), 90, movable=movable, bounds=bounds), InfiniteLine(QtCore.QPointF(values[0], 0), angle=90, **lineKwds),
InfiniteLine(QtCore.QPointF(values[0], 0), 90, movable=movable, bounds=bounds)] InfiniteLine(QtCore.QPointF(values[1], 0), angle=90, **lineKwds)]
else: else:
raise Exception('Orientation must be one of LinearRegionItem.Vertical or LinearRegionItem.Horizontal') raise Exception("Orientation must be 'vertical' or 'horizontal'.")
for l in self.lines: for l in self.lines:
l.setParentItem(self) l.setParentItem(self)
l.sigPositionChangeFinished.connect(self.lineMoveFinished) l.sigPositionChangeFinished.connect(self.lineMoveFinished)
l.sigPositionChanged.connect(self.lineMoved) self.lines[0].sigPositionChanged.connect(lambda: self.lineMoved(0))
self.lines[1].sigPositionChanged.connect(lambda: self.lineMoved(1))
if brush is None: if brush is None:
brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50)) brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 50))
self.setBrush(brush) self.setBrush(brush)
if hoverBrush is None:
c = self.brush.color()
c.setAlpha(min(c.alpha() * 2, 255))
hoverBrush = fn.mkBrush(c)
self.setHoverBrush(hoverBrush)
self.setMovable(movable) self.setMovable(movable)
def getRegion(self): def getRegion(self):
"""Return the values at the edges of the region.""" """Return the values at the edges of the region."""
#if self.orientation[0] == 'h': r = (self.lines[0].value(), self.lines[1].value())
#r = (self.bounds.top(), self.bounds.bottom()) if self.swapMode == 'sort':
#else: return (min(r), max(r))
#r = (self.bounds.left(), self.bounds.right()) else:
r = [self.lines[0].value(), self.lines[1].value()] return r
return (min(r), max(r))
def setRegion(self, rgn): def setRegion(self, rgn):
"""Set the values for the edges of the region. """Set the values for the edges of the region.
@ -101,7 +146,8 @@ class LinearRegionItem(UIGraphicsItem):
self.blockLineSignal = False self.blockLineSignal = False
self.lines[1].setValue(rgn[1]) self.lines[1].setValue(rgn[1])
#self.blockLineSignal = False #self.blockLineSignal = False
self.lineMoved() self.lineMoved(0)
self.lineMoved(1)
self.lineMoveFinished() self.lineMoveFinished()
def setBrush(self, *br, **kargs): def setBrush(self, *br, **kargs):
@ -111,6 +157,13 @@ class LinearRegionItem(UIGraphicsItem):
self.brush = fn.mkBrush(*br, **kargs) self.brush = fn.mkBrush(*br, **kargs)
self.currentBrush = self.brush self.currentBrush = self.brush
def setHoverBrush(self, *br, **kargs):
"""Set the brush that fills the region when the mouse is hovering over.
Can have any arguments that are valid
for :func:`mkBrush <pyqtgraph.mkBrush>`.
"""
self.hoverBrush = fn.mkBrush(*br, **kargs)
def setBounds(self, bounds): def setBounds(self, bounds):
"""Optional [min, max] bounding values for the region. To have no bounds on the """Optional [min, max] bounding values for the region. To have no bounds on the
region use [None, None]. region use [None, None].
@ -128,81 +181,67 @@ class LinearRegionItem(UIGraphicsItem):
self.movable = m self.movable = m
self.setAcceptHoverEvents(m) self.setAcceptHoverEvents(m)
def setSpan(self, mn, mx):
if self.span == (mn, mx):
return
self.span = (mn, mx)
self.lines[0].setSpan(mn, mx)
self.lines[1].setSpan(mn, mx)
self.update()
def boundingRect(self): def boundingRect(self):
br = UIGraphicsItem.boundingRect(self) br = self.viewRect() # bounds of containing ViewBox mapped to local coords.
rng = self.getRegion() rng = self.getRegion()
if self.orientation == LinearRegionItem.Vertical: if self.orientation in ('vertical', LinearRegionItem.Vertical):
br.setLeft(rng[0]) br.setLeft(rng[0])
br.setRight(rng[1]) br.setRight(rng[1])
length = br.height()
br.setBottom(br.top() + length * self.span[1])
br.setTop(br.top() + length * self.span[0])
else: else:
br.setTop(rng[0]) br.setTop(rng[0])
br.setBottom(rng[1]) br.setBottom(rng[1])
return br.normalized() length = br.width()
br.setRight(br.left() + length * self.span[1])
br.setLeft(br.left() + length * self.span[0])
br = br.normalized()
if self._bounds != br:
self._bounds = br
self.prepareGeometryChange()
return br
def paint(self, p, *args): def paint(self, p, *args):
profiler = debug.Profiler() profiler = debug.Profiler()
UIGraphicsItem.paint(self, p, *args)
p.setBrush(self.currentBrush) p.setBrush(self.currentBrush)
p.setPen(fn.mkPen(None)) p.setPen(fn.mkPen(None))
p.drawRect(self.boundingRect()) p.drawRect(self.boundingRect())
def dataBounds(self, axis, frac=1.0, orthoRange=None): def dataBounds(self, axis, frac=1.0, orthoRange=None):
if axis == self.orientation: if axis == self._orientation_axis[self.orientation]:
return self.getRegion() return self.getRegion()
else: else:
return None return None
def lineMoved(self): def lineMoved(self, i):
if self.blockLineSignal: if self.blockLineSignal:
return return
# lines swapped
if self.lines[0].value() > self.lines[1].value():
if self.swapMode == 'block':
self.lines[i].setValue(self.lines[1-i].value())
elif self.swapMode == 'push':
self.lines[1-i].setValue(self.lines[i].value())
self.prepareGeometryChange() self.prepareGeometryChange()
#self.emit(QtCore.SIGNAL('regionChanged'), self)
self.sigRegionChanged.emit(self) self.sigRegionChanged.emit(self)
def lineMoveFinished(self): def lineMoveFinished(self):
#self.emit(QtCore.SIGNAL('regionChangeFinished'), self)
self.sigRegionChangeFinished.emit(self) self.sigRegionChangeFinished.emit(self)
#def updateBounds(self):
#vb = self.view().viewRect()
#vals = [self.lines[0].value(), self.lines[1].value()]
#if self.orientation[0] == 'h':
#vb.setTop(min(vals))
#vb.setBottom(max(vals))
#else:
#vb.setLeft(min(vals))
#vb.setRight(max(vals))
#if vb != self.bounds:
#self.bounds = vb
#self.rect.setRect(vb)
#def mousePressEvent(self, ev):
#if not self.movable:
#ev.ignore()
#return
#for l in self.lines:
#l.mousePressEvent(ev) ## pass event to both lines so they move together
##if self.movable and ev.button() == QtCore.Qt.LeftButton:
##ev.accept()
##self.pressDelta = self.mapToParent(ev.pos()) - QtCore.QPointF(*self.p)
##else:
##ev.ignore()
#def mouseReleaseEvent(self, ev):
#for l in self.lines:
#l.mouseReleaseEvent(ev)
#def mouseMoveEvent(self, ev):
##print "move", ev.pos()
#if not self.movable:
#return
#self.lines[0].blockSignals(True) # only want to update once
#for l in self.lines:
#l.mouseMoveEvent(ev)
#self.lines[0].blockSignals(False)
##self.setPos(self.mapToParent(ev.pos()) - self.pressDelta)
##self.emit(QtCore.SIGNAL('dragged'), self)
def mouseDragEvent(self, ev): def mouseDragEvent(self, ev):
if not self.movable or int(ev.button() & QtCore.Qt.LeftButton) == 0: if not self.movable or int(ev.button() & QtCore.Qt.LeftButton) == 0:
@ -218,12 +257,9 @@ class LinearRegionItem(UIGraphicsItem):
if not self.moving: if not self.moving:
return return
#delta = ev.pos() - ev.lastPos()
self.lines[0].blockSignals(True) # only want to update once self.lines[0].blockSignals(True) # only want to update once
for i, l in enumerate(self.lines): for i, l in enumerate(self.lines):
l.setPos(self.cursorOffsets[i] + ev.pos()) l.setPos(self.cursorOffsets[i] + ev.pos())
#l.setPos(l.pos()+delta)
#l.mouseDragEvent(ev)
self.lines[0].blockSignals(False) self.lines[0].blockSignals(False)
self.prepareGeometryChange() self.prepareGeometryChange()
@ -242,7 +278,6 @@ class LinearRegionItem(UIGraphicsItem):
self.sigRegionChanged.emit(self) self.sigRegionChanged.emit(self)
self.sigRegionChangeFinished.emit(self) self.sigRegionChangeFinished.emit(self)
def hoverEvent(self, ev): def hoverEvent(self, ev):
if self.movable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): if self.movable and (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
self.setMouseHover(True) self.setMouseHover(True)
@ -255,36 +290,7 @@ class LinearRegionItem(UIGraphicsItem):
return return
self.mouseHovering = hover self.mouseHovering = hover
if hover: if hover:
c = self.brush.color() self.currentBrush = self.hoverBrush
c.setAlpha(c.alpha() * 2)
self.currentBrush = fn.mkBrush(c)
else: else:
self.currentBrush = self.brush self.currentBrush = self.brush
self.update() self.update()
#def hoverEnterEvent(self, ev):
#print "rgn hover enter"
#ev.ignore()
#self.updateHoverBrush()
#def hoverMoveEvent(self, ev):
#print "rgn hover move"
#ev.ignore()
#self.updateHoverBrush()
#def hoverLeaveEvent(self, ev):
#print "rgn hover leave"
#ev.ignore()
#self.updateHoverBrush(False)
#def updateHoverBrush(self, hover=None):
#if hover is None:
#scene = self.scene()
#hover = scene.claimEvent(self, QtCore.Qt.LeftButton, scene.Drag)
#if hover:
#self.currentBrush = fn.mkBrush(255, 0,0,100)
#else:
#self.currentBrush = self.brush
#self.update()

View File

@ -68,6 +68,7 @@ class PlotCurveItem(GraphicsObject):
'antialias': getConfigOption('antialias'), 'antialias': getConfigOption('antialias'),
'connect': 'all', 'connect': 'all',
'mouseWidth': 8, # width of shape responding to mouse click 'mouseWidth': 8, # width of shape responding to mouse click
'compositionMode': None,
} }
self.setClickable(kargs.get('clickable', False)) self.setClickable(kargs.get('clickable', False))
self.setData(*args, **kargs) self.setData(*args, **kargs)
@ -93,6 +94,24 @@ class PlotCurveItem(GraphicsObject):
self._mouseShape = None self._mouseShape = None
self._boundingRect = None self._boundingRect = None
def setCompositionMode(self, mode):
"""Change the composition mode of the item (see QPainter::CompositionMode
in the Qt documentation). This is useful when overlaying multiple items.
============================================ ============================================================
**Most common arguments:**
QtGui.QPainter.CompositionMode_SourceOver Default; image replaces the background if it
is opaque. Otherwise, it uses the alpha channel to blend
the image with the background.
QtGui.QPainter.CompositionMode_Overlay The image color is mixed with the background color to
reflect the lightness or darkness of the background.
QtGui.QPainter.CompositionMode_Plus Both the alpha and color of the image and background pixels
are added together.
QtGui.QPainter.CompositionMode_Multiply The output is the image color multiplied by the background.
============================================ ============================================================
"""
self.opts['compositionMode'] = mode
self.update()
def getData(self): def getData(self):
return self.xData, self.yData return self.xData, self.yData
@ -132,6 +151,8 @@ class PlotCurveItem(GraphicsObject):
if any(np.isinf(b)): if any(np.isinf(b)):
mask = np.isfinite(d) mask = np.isfinite(d)
d = d[mask] d = d[mask]
if len(d) == 0:
return (None, None)
b = (d.min(), d.max()) b = (d.min(), d.max())
elif frac <= 0.0: elif frac <= 0.0:
@ -173,7 +194,7 @@ class PlotCurveItem(GraphicsObject):
if self._boundingRect is None: if self._boundingRect is None:
(xmn, xmx) = self.dataBounds(ax=0) (xmn, xmx) = self.dataBounds(ax=0)
(ymn, ymx) = self.dataBounds(ax=1) (ymn, ymx) = self.dataBounds(ax=1)
if xmn is None: if xmn is None or ymn is None:
return QtCore.QRectF() return QtCore.QRectF()
px = py = 0.0 px = py = 0.0
@ -272,7 +293,7 @@ class PlotCurveItem(GraphicsObject):
def setData(self, *args, **kargs): def setData(self, *args, **kargs):
""" """
============== ======================================================== =============== ========================================================
**Arguments:** **Arguments:**
x, y (numpy arrays) Data to show x, y (numpy arrays) Data to show
pen Pen to use when drawing. Any single argument accepted by pen Pen to use when drawing. Any single argument accepted by
@ -296,7 +317,9 @@ class PlotCurveItem(GraphicsObject):
to be drawn. "finite" causes segments to be omitted if to be drawn. "finite" causes segments to be omitted if
they are attached to nan or inf values. For any other they are attached to nan or inf values. For any other
connectivity, specify an array of boolean values. connectivity, specify an array of boolean values.
============== ======================================================== compositionMode See :func:`setCompositionMode
<pyqtgraph.PlotCurveItem.setCompositionMode>`.
=============== ========================================================
If non-keyword arguments are used, they will be interpreted as If non-keyword arguments are used, they will be interpreted as
setData(y) for a single argument and setData(x, y) for two setData(y) for a single argument and setData(x, y) for two
@ -309,6 +332,9 @@ class PlotCurveItem(GraphicsObject):
def updateData(self, *args, **kargs): def updateData(self, *args, **kargs):
profiler = debug.Profiler() profiler = debug.Profiler()
if 'compositionMode' in kargs:
self.setCompositionMode(kargs['compositionMode'])
if len(args) == 1: if len(args) == 1:
kargs['y'] = args[0] kargs['y'] = args[0]
elif len(args) == 2: elif len(args) == 2:
@ -428,7 +454,6 @@ class PlotCurveItem(GraphicsObject):
x = None x = None
y = None y = None
path = self.getPath() path = self.getPath()
profiler('generate path') profiler('generate path')
if self._exportOpts is not False: if self._exportOpts is not False:
@ -438,6 +463,9 @@ class PlotCurveItem(GraphicsObject):
p.setRenderHint(p.Antialiasing, aa) p.setRenderHint(p.Antialiasing, aa)
cmode = self.opts['compositionMode']
if cmode is not None:
p.setCompositionMode(cmode)
if self.opts['brush'] is not None and self.opts['fillLevel'] is not None: if self.opts['brush'] is not None and self.opts['fillLevel'] is not None:
if self.fillPath is None: if self.fillPath is None:

View File

@ -602,6 +602,9 @@ class PlotItem(GraphicsWidget):
#item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged) #item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged)
#item.sigPlotChanged.connect(self.plotChanged) #item.sigPlotChanged.connect(self.plotChanged)
if self.legend is not None:
self.legend.removeItem(item)
def clear(self): def clear(self):
""" """
Remove all items from the ViewBox. Remove all items from the ViewBox.
@ -646,9 +649,13 @@ class PlotItem(GraphicsWidget):
Create a new LegendItem and anchor it over the internal ViewBox. Create a new LegendItem and anchor it over the internal ViewBox.
Plots will be automatically displayed in the legend if they Plots will be automatically displayed in the legend if they
are created with the 'name' argument. are created with the 'name' argument.
If a LegendItem has already been created using this method, that
item will be returned rather than creating a new one.
""" """
self.legend = LegendItem(size, offset) if self.legend is None:
self.legend.setParentItem(self.vb) self.legend = LegendItem(size, offset)
self.legend.setParentItem(self.vb)
return self.legend return self.legend
def scatterPlot(self, *args, **kargs): def scatterPlot(self, *args, **kargs):

View File

@ -26,7 +26,8 @@ from .. import getConfigOption
__all__ = [ __all__ = [
'ROI', 'ROI',
'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI', 'TestROI', 'RectROI', 'EllipseROI', 'CircleROI', 'PolygonROI',
'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI', 'SpiralROI', 'CrosshairROI', 'LineROI', 'MultiLineROI', 'MultiRectROI', 'LineSegmentROI', 'PolyLineROI',
'CrosshairROI',
] ]
@ -112,7 +113,6 @@ class ROI(GraphicsObject):
sigRemoveRequested = QtCore.Signal(object) sigRemoveRequested = QtCore.Signal(object)
def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False): def __init__(self, pos, size=Point(1, 1), angle=0.0, invertible=False, maxBounds=None, snapSize=1.0, scaleSnap=False, translateSnap=False, rotateSnap=False, parent=None, pen=None, movable=True, removable=False):
#QObjectWorkaround.__init__(self)
GraphicsObject.__init__(self, parent) GraphicsObject.__init__(self, parent)
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
pos = Point(pos) pos = Point(pos)
@ -147,7 +147,6 @@ class ROI(GraphicsObject):
self.translateSnap = translateSnap self.translateSnap = translateSnap
self.rotateSnap = rotateSnap self.rotateSnap = rotateSnap
self.scaleSnap = scaleSnap self.scaleSnap = scaleSnap
#self.setFlag(self.ItemIsSelectable, True)
def getState(self): def getState(self):
return self.stateCopy() return self.stateCopy()
@ -231,6 +230,9 @@ class ROI(GraphicsObject):
multiple change functions to be called sequentially while minimizing processing overhead multiple change functions to be called sequentially while minimizing processing overhead
and repeated signals. Setting ``update=False`` also forces ``finish=False``. and repeated signals. Setting ``update=False`` also forces ``finish=False``.
""" """
if update not in (True, False):
raise TypeError("update argument must be bool")
if y is None: if y is None:
pos = Point(pos) pos = Point(pos)
else: else:
@ -238,6 +240,7 @@ class ROI(GraphicsObject):
if isinstance(y, bool): if isinstance(y, bool):
raise TypeError("Positional arguments to setPos() must be numerical.") raise TypeError("Positional arguments to setPos() must be numerical.")
pos = Point(pos, y) pos = Point(pos, y)
self.state['pos'] = pos self.state['pos'] = pos
QtGui.QGraphicsItem.setPos(self, pos) QtGui.QGraphicsItem.setPos(self, pos)
if update: if update:
@ -247,19 +250,22 @@ class ROI(GraphicsObject):
"""Set the size of the ROI. May be specified as a QPoint, Point, or list of two values. """Set the size of the ROI. May be specified as a QPoint, Point, or list of two values.
See setPos() for an explanation of the update and finish arguments. See setPos() for an explanation of the update and finish arguments.
""" """
if update not in (True, False):
raise TypeError("update argument must be bool")
size = Point(size) size = Point(size)
self.prepareGeometryChange() self.prepareGeometryChange()
self.state['size'] = size self.state['size'] = size
if update: if update:
self.stateChanged(finish=finish) self.stateChanged(finish=finish)
def setAngle(self, angle, update=True, finish=True): def setAngle(self, angle, update=True, finish=True):
"""Set the angle of rotation (in degrees) for this ROI. """Set the angle of rotation (in degrees) for this ROI.
See setPos() for an explanation of the update and finish arguments. See setPos() for an explanation of the update and finish arguments.
""" """
if update not in (True, False):
raise TypeError("update argument must be bool")
self.state['angle'] = angle self.state['angle'] = angle
tr = QtGui.QTransform() tr = QtGui.QTransform()
#tr.rotate(-angle * 180 / np.pi)
tr.rotate(angle) tr.rotate(angle)
self.setTransform(tr) self.setTransform(tr)
if update: if update:
@ -307,20 +313,14 @@ class ROI(GraphicsObject):
newState = self.stateCopy() newState = self.stateCopy()
newState['pos'] = newState['pos'] + pt newState['pos'] = newState['pos'] + pt
## snap position
#snap = kargs.get('snap', None)
#if (snap is not False) and not (snap is None and self.translateSnap is False):
snap = kargs.get('snap', None) snap = kargs.get('snap', None)
if snap is None: if snap is None:
snap = self.translateSnap snap = self.translateSnap
if snap is not False: if snap is not False:
newState['pos'] = self.getSnapPosition(newState['pos'], snap=snap) newState['pos'] = self.getSnapPosition(newState['pos'], snap=snap)
#d = ev.scenePos() - self.mapToScene(self.pressPos)
if self.maxBounds is not None: if self.maxBounds is not None:
r = self.stateRect(newState) r = self.stateRect(newState)
#r0 = self.sceneTransform().mapRect(self.boundingRect())
d = Point(0,0) d = Point(0,0)
if self.maxBounds.left() > r.left(): if self.maxBounds.left() > r.left():
d[0] = self.maxBounds.left() - r.left() d[0] = self.maxBounds.left() - r.left()
@ -332,12 +332,9 @@ class ROI(GraphicsObject):
d[1] = self.maxBounds.bottom() - r.bottom() d[1] = self.maxBounds.bottom() - r.bottom()
newState['pos'] += d newState['pos'] += d
#self.state['pos'] = newState['pos']
update = kargs.get('update', True) update = kargs.get('update', True)
finish = kargs.get('finish', True) finish = kargs.get('finish', True)
self.setPos(newState['pos'], update=update, finish=finish) self.setPos(newState['pos'], update=update, finish=finish)
#if 'update' not in kargs or kargs['update'] is True:
#self.stateChanged()
def rotate(self, angle, update=True, finish=True): def rotate(self, angle, update=True, finish=True):
""" """
@ -574,7 +571,6 @@ class ROI(GraphicsObject):
## Note: by default, handles are not user-removable even if this method returns True. ## Note: by default, handles are not user-removable even if this method returns True.
return True return True
def getLocalHandlePositions(self, index=None): def getLocalHandlePositions(self, index=None):
"""Returns the position of handles in the ROI's coordinate system. """Returns the position of handles in the ROI's coordinate system.
@ -620,7 +616,6 @@ class ROI(GraphicsObject):
for h in self.handles: for h in self.handles:
h['item'].hide() h['item'].hide()
def hoverEvent(self, ev): def hoverEvent(self, ev):
hover = False hover = False
if not ev.isExit(): if not ev.isExit():
@ -756,11 +751,6 @@ class ROI(GraphicsObject):
else: else:
raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.") raise Exception("New point location must be given in either 'parent' or 'scene' coordinates.")
## transform p0 and p1 into parent's coordinates (same as scene coords if there is no parent). I forget why.
#p0 = self.mapSceneToParent(p0)
#p1 = self.mapSceneToParent(p1)
## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1) ## Handles with a 'center' need to know their local position relative to the center point (lp0, lp1)
if 'center' in h: if 'center' in h:
c = h['center'] c = h['center']
@ -770,8 +760,6 @@ class ROI(GraphicsObject):
if h['type'] == 't': if h['type'] == 't':
snap = True if (modifiers & QtCore.Qt.ControlModifier) else None snap = True if (modifiers & QtCore.Qt.ControlModifier) else None
#if self.translateSnap or ():
#snap = Point(self.snapSize, self.snapSize)
self.translate(p1-p0, snap=snap, update=False) self.translate(p1-p0, snap=snap, update=False)
elif h['type'] == 'f': elif h['type'] == 'f':
@ -779,7 +767,6 @@ class ROI(GraphicsObject):
h['item'].setPos(newPos) h['item'].setPos(newPos)
h['pos'] = newPos h['pos'] = newPos
self.freeHandleMoved = True self.freeHandleMoved = True
#self.sigRegionChanged.emit(self) ## should be taken care of by call to stateChanged()
elif h['type'] == 's': elif h['type'] == 's':
## If a handle and its center have the same x or y value, we can't scale across that axis. ## If a handle and its center have the same x or y value, we can't scale across that axis.
@ -869,10 +856,8 @@ class ROI(GraphicsObject):
r = self.stateRect(newState) r = self.stateRect(newState)
if not self.maxBounds.contains(r): if not self.maxBounds.contains(r):
return return
#self.setTransform(tr)
self.setPos(newState['pos'], update=False) self.setPos(newState['pos'], update=False)
self.setAngle(ang, update=False) self.setAngle(ang, update=False)
#self.state = newState
## If this is a free-rotate handle, its distance from the center may change. ## If this is a free-rotate handle, its distance from the center may change.
@ -897,7 +882,6 @@ class ROI(GraphicsObject):
if ang is None: if ang is None:
return return
if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier): if self.rotateSnap or (modifiers & QtCore.Qt.ControlModifier):
#ang = round(ang / (np.pi/12.)) * (np.pi/12.)
ang = round(ang / 15.) * 15. ang = round(ang / 15.) * 15.
hs = abs(h['pos'][scaleAxis] - c[scaleAxis]) hs = abs(h['pos'][scaleAxis] - c[scaleAxis])
@ -921,10 +905,7 @@ class ROI(GraphicsObject):
r = self.stateRect(newState) r = self.stateRect(newState)
if not self.maxBounds.contains(r): if not self.maxBounds.contains(r):
return return
#self.setTransform(tr)
#self.setPos(newState['pos'], update=False)
#self.prepareGeometryChange()
#self.state = newState
self.setState(newState, update=False) self.setState(newState, update=False)
self.stateChanged(finish=finish) self.stateChanged(finish=finish)
@ -951,9 +932,6 @@ class ROI(GraphicsObject):
if h['item'] in self.childItems(): if h['item'] in self.childItems():
p = h['pos'] p = h['pos']
h['item'].setPos(h['pos'] * self.state['size']) h['item'].setPos(h['pos'] * self.state['size'])
#else:
# trans = self.state['pos']-self.lastState['pos']
# h['item'].setPos(h['pos'] + h['item'].parentItem().mapFromParent(trans))
self.update() self.update()
self.sigRegionChanged.emit(self) self.sigRegionChanged.emit(self)
@ -973,12 +951,10 @@ class ROI(GraphicsObject):
def stateRect(self, state): def stateRect(self, state):
r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1]) r = QtCore.QRectF(0, 0, state['size'][0], state['size'][1])
tr = QtGui.QTransform() tr = QtGui.QTransform()
#tr.rotate(-state['angle'] * 180 / np.pi)
tr.rotate(-state['angle']) tr.rotate(-state['angle'])
r = tr.mapRect(r) r = tr.mapRect(r)
return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1]) return r.adjusted(state['pos'][0], state['pos'][1], state['pos'][0], state['pos'][1])
def getSnapPosition(self, pos, snap=None): def getSnapPosition(self, pos, snap=None):
## Given that pos has been requested, return the nearest snap-to position ## Given that pos has been requested, return the nearest snap-to position
## optionally, snap may be passed in to specify a rectangular snap grid. ## optionally, snap may be passed in to specify a rectangular snap grid.
@ -998,7 +974,6 @@ 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()
# Note: don't use self.boundingRect here, because subclasses may need to redefine it. # 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() r = QtCore.QRectF(0, 0, self.state['size'][0], self.state['size'][1]).normalized()
@ -1007,7 +982,6 @@ class ROI(GraphicsObject):
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()
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 """Return a tuple of slice objects that can be used to slice the region
@ -1135,11 +1109,8 @@ class ROI(GraphicsObject):
lvx = np.sqrt(vx.x()**2 + vx.y()**2) lvx = np.sqrt(vx.x()**2 + vx.y()**2)
lvy = np.sqrt(vy.x()**2 + vy.y()**2) lvy = np.sqrt(vy.x()**2 + vy.y()**2)
#pxLen = img.width() / float(data.shape[axes[0]])
##img.width is number of pixels, not width of item. ##img.width is number of pixels, not width of item.
##need pxWidth and pxHeight instead of pxLen ? ##need pxWidth and pxHeight instead of pxLen ?
#sx = pxLen / lvx
#sy = pxLen / lvy
sx = 1.0 / lvx sx = 1.0 / lvx
sy = 1.0 / lvy sy = 1.0 / lvy
@ -1169,7 +1140,6 @@ class ROI(GraphicsObject):
if width == 0 or height == 0: if width == 0 or height == 0:
return np.empty((width, height), dtype=float) return np.empty((width, height), dtype=float)
# QImage(width, height, format)
im = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32) im = QtGui.QImage(width, height, QtGui.QImage.Format_ARGB32)
im.fill(0x0) im.fill(0x0)
p = QtGui.QPainter(im) p = QtGui.QPainter(im)
@ -1199,27 +1169,6 @@ class ROI(GraphicsObject):
t1 = SRTTransform(relativeTo) t1 = SRTTransform(relativeTo)
t2 = SRTTransform(st) t2 = SRTTransform(st)
return t2/t1 return t2/t1
#st = self.getState()
### rotation
#ang = (st['angle']-relativeTo['angle']) * 180. / 3.14159265358
#rot = QtGui.QTransform()
#rot.rotate(-ang)
### We need to come up with a universal transformation--one that can be applied to other objects
### such that all maintain alignment.
### More specifically, we need to turn the ROI's position and angle into
### a rotation _around the origin_ and a translation.
#p0 = Point(relativeTo['pos'])
### base position, rotated
#p1 = rot.map(p0)
#trans = Point(st['pos']) - p1
#return trans, ang
def applyGlobalTransform(self, tr): def applyGlobalTransform(self, tr):
st = self.getState() st = self.getState()
@ -1241,8 +1190,6 @@ class Handle(UIGraphicsItem):
Handles may be dragged to change the position, size, orientation, or other Handles may be dragged to change the position, size, orientation, or other
properties of the ROI they are attached to. properties of the ROI they are attached to.
""" """
types = { ## defines number of sides, start angle for each handle type types = { ## defines number of sides, start angle for each handle type
't': (4, np.pi/4), 't': (4, np.pi/4),
@ -1257,9 +1204,6 @@ class Handle(UIGraphicsItem):
sigRemoveRequested = QtCore.Signal(object) # self sigRemoveRequested = QtCore.Signal(object) # self
def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False): def __init__(self, radius, typ=None, pen=(200, 200, 220), parent=None, deletable=False):
#print " create item with parent", parent
#self.bounds = QtCore.QRectF(-1e-10, -1e-10, 2e-10, 2e-10)
#self.setFlags(self.ItemIgnoresTransformations | self.ItemSendsScenePositionChanges)
self.rois = [] self.rois = []
self.radius = radius self.radius = radius
self.typ = typ self.typ = typ
@ -1278,7 +1222,6 @@ class Handle(UIGraphicsItem):
self.deletable = deletable self.deletable = deletable
if deletable: if deletable:
self.setAcceptedMouseButtons(QtCore.Qt.RightButton) self.setAcceptedMouseButtons(QtCore.Qt.RightButton)
#self.updateShape()
self.setZValue(11) self.setZValue(11)
def connectROI(self, roi): def connectROI(self, roi):
@ -1287,13 +1230,6 @@ class Handle(UIGraphicsItem):
def disconnectROI(self, roi): def disconnectROI(self, roi):
self.rois.remove(roi) self.rois.remove(roi)
#for i, r in enumerate(self.roi):
#if r[0] == roi:
#self.roi.pop(i)
#def close(self):
#for r in self.roi:
#r.removeHandle(self)
def setDeletable(self, b): def setDeletable(self, b):
self.deletable = b self.deletable = b
@ -1319,21 +1255,12 @@ class Handle(UIGraphicsItem):
else: else:
self.currentPen = self.pen self.currentPen = self.pen
self.update() self.update()
#if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton):
#self.currentPen = fn.mkPen(255, 255,0)
#else:
#self.currentPen = self.pen
#self.update()
def mouseClickEvent(self, ev): def mouseClickEvent(self, ev):
## right-click cancels drag ## right-click cancels drag
if ev.button() == QtCore.Qt.RightButton and self.isMoving: if ev.button() == QtCore.Qt.RightButton and self.isMoving:
self.isMoving = False ## prevents any further motion self.isMoving = False ## prevents any further motion
self.movePoint(self.startPos, finish=True) self.movePoint(self.startPos, finish=True)
#for r in self.roi:
#r[0].cancelMove()
ev.accept() ev.accept()
elif int(ev.button() & self.acceptedMouseButtons()) > 0: elif int(ev.button() & self.acceptedMouseButtons()) > 0:
ev.accept() ev.accept()
@ -1342,12 +1269,6 @@ class Handle(UIGraphicsItem):
self.sigClicked.emit(self, ev) self.sigClicked.emit(self, ev)
else: else:
ev.ignore() ev.ignore()
#elif self.deletable:
#ev.accept()
#self.raiseContextMenu(ev)
#else:
#ev.ignore()
def buildMenu(self): def buildMenu(self):
menu = QtGui.QMenu() menu = QtGui.QMenu()
@ -1418,36 +1339,10 @@ class Handle(UIGraphicsItem):
self.path.lineTo(x, y) self.path.lineTo(x, y)
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
### determine rotation of transform
#m = self.sceneTransform()
##mi = m.inverted()[0]
#v = m.map(QtCore.QPointF(1, 0)) - m.map(QtCore.QPointF(0, 0))
#va = np.arctan2(v.y(), v.x())
### Determine length of unit vector in painter's coords
##size = mi.map(Point(self.radius, self.radius)) - mi.map(Point(0, 0))
##size = (size.x()*size.x() + size.y() * size.y()) ** 0.5
#size = self.radius
#bounds = QtCore.QRectF(-size, -size, size*2, size*2)
#if bounds != self.bounds:
#self.bounds = bounds
#self.prepareGeometryChange()
p.setRenderHints(p.Antialiasing, True) p.setRenderHints(p.Antialiasing, True)
p.setPen(self.currentPen) p.setPen(self.currentPen)
#p.rotate(va * 180. / 3.1415926)
#p.drawPath(self.path)
p.drawPath(self.shape()) p.drawPath(self.shape())
#ang = self.startAng + va
#dt = 2*np.pi / self.sides
#for i in range(0, self.sides):
#x1 = size * cos(ang)
#y1 = size * sin(ang)
#x2 = size * cos(ang+dt)
#y2 = size * sin(ang+dt)
#ang += dt
#p.drawLine(Point(x1, y1), Point(x2, y2))
def shape(self): def shape(self):
if self._shape is None: if self._shape is None:
@ -1459,18 +1354,10 @@ class Handle(UIGraphicsItem):
return self._shape return self._shape
def boundingRect(self): def boundingRect(self):
#print 'roi:', self.roi
s1 = self.shape() s1 = self.shape()
#print " s1:", s1
#s2 = self.shape()
#print " s2:", s2
return self.shape().boundingRect() return self.shape().boundingRect()
def generateShape(self): def generateShape(self):
## determine rotation of transform
#m = self.sceneTransform() ## Qt bug: do not access sceneTransform() until we know this object has a scene.
#mi = m.inverted()[0]
dt = self.deviceTransform() dt = self.deviceTransform()
if dt is None: if dt is None:
@ -1488,22 +1375,15 @@ class Handle(UIGraphicsItem):
return dti.map(tr.map(self.path)) return dti.map(tr.map(self.path))
def viewTransformChanged(self): def viewTransformChanged(self):
GraphicsObject.viewTransformChanged(self) GraphicsObject.viewTransformChanged(self)
self._shape = None ## invalidate shape, recompute later if requested. self._shape = None ## invalidate shape, recompute later if requested.
self.update() self.update()
#def itemChange(self, change, value):
#if change == self.ItemScenePositionHasChanged:
#self.updateShape()
class TestROI(ROI): class TestROI(ROI):
def __init__(self, pos, size, **args): def __init__(self, pos, size, **args):
#QtGui.QGraphicsRectItem.__init__(self, pos[0], pos[1], size[0], size[1])
ROI.__init__(self, pos, size, **args) ROI.__init__(self, pos, size, **args)
#self.addTranslateHandle([0, 0])
self.addTranslateHandle([0.5, 0.5]) self.addTranslateHandle([0.5, 0.5])
self.addScaleHandle([1, 1], [0, 0]) self.addScaleHandle([1, 1], [0, 0])
self.addScaleHandle([0, 0], [1, 1]) self.addScaleHandle([0, 0], [1, 1])
@ -1513,7 +1393,6 @@ class TestROI(ROI):
self.addRotateHandle([0, 1], [1, 1]) self.addRotateHandle([0, 1], [1, 1])
class RectROI(ROI): class RectROI(ROI):
""" """
Rectangular ROI subclass with a single scale handle at the top-right corner. Rectangular ROI subclass with a single scale handle at the top-right corner.
@ -1532,14 +1411,12 @@ class RectROI(ROI):
""" """
def __init__(self, pos, size, centered=False, sideScalers=False, **args): def __init__(self, pos, size, centered=False, sideScalers=False, **args):
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1])
ROI.__init__(self, pos, size, **args) ROI.__init__(self, pos, size, **args)
if centered: if centered:
center = [0.5, 0.5] center = [0.5, 0.5]
else: else:
center = [0, 0] center = [0, 0]
#self.addTranslateHandle(center)
self.addScaleHandle([1, 1], center) self.addScaleHandle([1, 1], center)
if sideScalers: if sideScalers:
self.addScaleHandle([1, 0.5], [center[0], 0.5]) self.addScaleHandle([1, 0.5], [center[0], 0.5])
@ -1648,7 +1525,6 @@ class MultiRectROI(QtGui.QGraphicsObject):
rgn = l.getArrayRegion(arr, img, axes=axes, **kwds) rgn = l.getArrayRegion(arr, img, axes=axes, **kwds)
if rgn is None: if rgn is None:
continue continue
#return None
rgns.append(rgn) rgns.append(rgn)
#print l.state['size'] #print l.state['size']
@ -1733,11 +1609,18 @@ class EllipseROI(ROI):
""" """
def __init__(self, pos, size, **args): def __init__(self, pos, size, **args):
#QtGui.QGraphicsRectItem.__init__(self, 0, 0, size[0], size[1]) self.path = None
ROI.__init__(self, pos, size, **args) ROI.__init__(self, pos, size, **args)
self.sigRegionChanged.connect(self._clearPath)
self._addHandles()
def _addHandles(self):
self.addRotateHandle([1.0, 0.5], [0.5, 0.5]) self.addRotateHandle([1.0, 0.5], [0.5, 0.5])
self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
def _clearPath(self):
self.path = None
def paint(self, p, opt, widget): def paint(self, p, opt, widget):
r = self.boundingRect() r = self.boundingRect()
p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.Antialiasing)
@ -1760,6 +1643,7 @@ class EllipseROI(ROI):
return arr return arr
w = arr.shape[axes[0]] w = arr.shape[axes[0]]
h = arr.shape[axes[1]] h = arr.shape[axes[1]]
## generate an ellipsoidal mask ## generate an ellipsoidal mask
mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h)) mask = np.fromfunction(lambda x,y: (((x+0.5)/(w/2.)-1)**2+ ((y+0.5)/(h/2.)-1)**2)**0.5 < 1, (w, h))
@ -1772,8 +1656,27 @@ class EllipseROI(ROI):
return arr * mask return arr * mask
def shape(self): def shape(self):
self.path = QtGui.QPainterPath() if self.path is None:
self.path.addEllipse(self.boundingRect()) path = QtGui.QPainterPath()
# Note: Qt has a bug where very small ellipses (radius <0.001) do
# not correctly intersect with mouse position (upper-left and
# lower-right quadrants are not clickable).
#path.addEllipse(self.boundingRect())
# Workaround: manually draw the path.
br = self.boundingRect()
center = br.center()
r1 = br.width() / 2.
r2 = br.height() / 2.
theta = np.linspace(0, 2*np.pi, 24)
x = center.x() + r1 * np.cos(theta)
y = center.y() + r2 * np.sin(theta)
path.moveTo(x[0], y[0])
for i in range(1, len(x)):
path.lineTo(x[i], y[i])
self.path = path
return self.path return self.path
@ -1790,10 +1693,15 @@ class CircleROI(EllipseROI):
============== ============================================================= ============== =============================================================
""" """
def __init__(self, pos, size, **args): def __init__(self, pos, size=None, radius=None, **args):
ROI.__init__(self, pos, size, **args) if size is None:
if radius is None:
raise TypeError("Must provide either size or radius.")
size = (radius*2, radius*2)
EllipseROI.__init__(self, pos, size, **args)
self.aspectLocked = True self.aspectLocked = True
#self.addTranslateHandle([0.5, 0.5])
def _addHandles(self):
self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5]) self.addScaleHandle([0.5*2.**-0.5 + 0.5, 0.5*2.**-0.5 + 0.5], [0.5, 0.5])
@ -1804,22 +1712,14 @@ class PolygonROI(ROI):
if pos is None: if pos is None:
pos = [0,0] pos = [0,0]
ROI.__init__(self, pos, [1,1], **args) ROI.__init__(self, pos, [1,1], **args)
#ROI.__init__(self, positions[0])
for p in positions: for p in positions:
self.addFreeHandle(p) self.addFreeHandle(p)
self.setZValue(1000) self.setZValue(1000)
print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.") print("Warning: PolygonROI is deprecated. Use PolyLineROI instead.")
def listPoints(self): def listPoints(self):
return [p['item'].pos() for p in self.handles] return [p['item'].pos() for p in self.handles]
#def movePoint(self, *args, **kargs):
#ROI.movePoint(self, *args, **kargs)
#self.prepareGeometryChange()
#for h in self.handles:
#h['pos'] = h['item'].pos()
def paint(self, p, *args): def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing) p.setRenderHint(QtGui.QPainter.Antialiasing)
p.setPen(self.currentPen) p.setPen(self.currentPen)
@ -1846,7 +1746,6 @@ class PolygonROI(ROI):
sc['pos'] = Point(self.state['pos']) sc['pos'] = Point(self.state['pos'])
sc['size'] = Point(self.state['size']) sc['size'] = Point(self.state['size'])
sc['angle'] = self.state['angle'] sc['angle'] = self.state['angle']
#sc['handles'] = self.handles
return sc return sc
@ -2066,13 +1965,16 @@ class LineSegmentROI(ROI):
pos = [0,0] pos = [0,0]
ROI.__init__(self, pos, [1,1], **args) ROI.__init__(self, pos, [1,1], **args)
#ROI.__init__(self, positions[0])
if len(positions) > 2: if len(positions) > 2:
raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.") raise Exception("LineSegmentROI must be defined by exactly 2 positions. For more points, use PolyLineROI.")
self.endpoints = []
for i, p in enumerate(positions): for i, p in enumerate(positions):
self.endpoints.append(self.addFreeHandle(p, item=handles[i])) self.addFreeHandle(p, item=handles[i])
@property
def endpoints(self):
# must not be cached because self.handles may change.
return [h['item'] for h in self.handles]
def listPoints(self): def listPoints(self):
return [p['item'].pos() for p in self.handles] return [p['item'].pos() for p in self.handles]
@ -2119,7 +2021,6 @@ class LineSegmentROI(ROI):
See ROI.getArrayRegion() for a description of the arguments. See ROI.getArrayRegion() for a description of the arguments.
""" """
imgPts = [self.mapToItem(img, h.pos()) for h in self.endpoints] imgPts = [self.mapToItem(img, h.pos()) for h in self.endpoints]
rgns = [] rgns = []
coords = [] coords = []
@ -2157,85 +2058,11 @@ class _PolyLineSegment(LineSegmentROI):
return LineSegmentROI.hoverEvent(self, ev) return LineSegmentROI.hoverEvent(self, ev)
class SpiralROI(ROI):
def __init__(self, pos=None, size=None, **args):
if size == None:
size = [100e-6,100e-6]
if pos == None:
pos = [0,0]
ROI.__init__(self, pos, size, **args)
self.translateSnap = False
self.addFreeHandle([0.25,0], name='a')
self.addRotateFreeHandle([1,0], [0,0], name='r')
#self.getRadius()
#QtCore.connect(self, QtCore.SIGNAL('regionChanged'), self.
def getRadius(self):
radius = Point(self.handles[1]['item'].pos()).length()
#r2 = radius[1]
#r3 = r2[0]
return radius
def boundingRect(self):
r = self.getRadius()
return QtCore.QRectF(-r*1.1, -r*1.1, 2.2*r, 2.2*r)
#return self.bounds
#def movePoint(self, *args, **kargs):
#ROI.movePoint(self, *args, **kargs)
#self.prepareGeometryChange()
#for h in self.handles:
#h['pos'] = h['item'].pos()/self.state['size'][0]
def stateChanged(self, finish=True):
ROI.stateChanged(self, finish=finish)
if len(self.handles) > 1:
self.path = QtGui.QPainterPath()
h0 = Point(self.handles[0]['item'].pos()).length()
a = h0/(2.0*np.pi)
theta = 30.0*(2.0*np.pi)/360.0
self.path.moveTo(QtCore.QPointF(a*theta*cos(theta), a*theta*sin(theta)))
x0 = a*theta*cos(theta)
y0 = a*theta*sin(theta)
radius = self.getRadius()
theta += 20.0*(2.0*np.pi)/360.0
i = 0
while Point(x0, y0).length() < radius and i < 1000:
x1 = a*theta*cos(theta)
y1 = a*theta*sin(theta)
self.path.lineTo(QtCore.QPointF(x1,y1))
theta += 20.0*(2.0*np.pi)/360.0
x0 = x1
y0 = y1
i += 1
return self.path
def shape(self):
p = QtGui.QPainterPath()
p.addEllipse(self.boundingRect())
return p
def paint(self, p, *args):
p.setRenderHint(QtGui.QPainter.Antialiasing)
#path = self.shape()
p.setPen(self.currentPen)
p.drawPath(self.path)
p.setPen(QtGui.QPen(QtGui.QColor(255,0,0)))
p.drawPath(self.shape())
p.setPen(QtGui.QPen(QtGui.QColor(0,0,255)))
p.drawRect(self.boundingRect())
class CrosshairROI(ROI): class CrosshairROI(ROI):
"""A crosshair ROI whose position is at the center of the crosshairs. By default, it is scalable, rotatable and translatable.""" """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): def __init__(self, pos=None, size=None, **kargs):
if size == None: if size == None:
#size = [100e-6,100e-6]
size=[1,1] size=[1,1]
if pos == None: if pos == None:
pos = [0,0] pos = [0,0]
@ -2251,16 +2078,8 @@ class CrosshairROI(ROI):
self.prepareGeometryChange() self.prepareGeometryChange()
def boundingRect(self): def boundingRect(self):
#size = self.size()
#return QtCore.QRectF(-size[0]/2., -size[1]/2., size[0], size[1]).normalized()
return self.shape().boundingRect() 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): def shape(self):
if self._shape is None: if self._shape is None:
radius = self.getState()['size'][1] radius = self.getState()['size'][1]
@ -2274,58 +2093,43 @@ class CrosshairROI(ROI):
stroker.setWidth(10) stroker.setWidth(10)
outline = stroker.createStroke(p) outline = stroker.createStroke(p)
self._shape = self.mapFromDevice(outline) 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 return self._shape
def paint(self, p, *args): def paint(self, p, *args):
#p.save()
#r = self.getRect()
radius = self.getState()['size'][1] radius = self.getState()['size'][1]
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.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(0, -radius), Point(0, radius))
p.drawLine(Point(-radius, 0), Point(radius, 0)) p.drawLine(Point(-radius, 0), Point(radius, 0))
class RulerROI(LineSegmentROI):
def paint(self, p, *args):
LineSegmentROI.paint(self, p, *args)
h1 = self.handles[0]['item'].pos()
h2 = self.handles[1]['item'].pos()
p1 = p.transform().map(h1)
p2 = p.transform().map(h2)
vec = Point(h2) - Point(h1)
length = vec.length()
angle = vec.angle(Point(1, 0))
pvec = p2 - p1
pvecT = Point(pvec.y(), -pvec.x())
pos = 0.5 * (p1 + p2) + pvecT * 40 / pvecT.length()
p.resetTransform()
txt = fn.siFormat(length, suffix='m') + '\n%0.1f deg' % angle
p.drawText(QtCore.QRectF(pos.x()-50, pos.y()-50, 100, 100), QtCore.Qt.AlignCenter | QtCore.Qt.AlignVCenter, txt)
def boundingRect(self):
r = LineSegmentROI.boundingRect(self)
pxl = self.pixelLength(Point([1, 0]))
if pxl is None:
return r
pxw = 50 * pxl
return r.adjusted(-50, -50, 50, 50)

View File

@ -126,7 +126,7 @@ class SymbolAtlas(object):
keyi = None keyi = None
sourceRecti = None sourceRecti = None
for i, rec in enumerate(opts): for i, rec in enumerate(opts):
key = (rec[3], rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes? key = (id(rec[3]), rec[2], id(rec[4]), id(rec[5])) # TODO: use string indexes?
if key == keyi: if key == keyi:
sourceRect[i] = sourceRecti sourceRect[i] = sourceRecti
else: else:
@ -136,6 +136,7 @@ class SymbolAtlas(object):
newRectSrc = QtCore.QRectF() newRectSrc = QtCore.QRectF()
newRectSrc.pen = rec['pen'] newRectSrc.pen = rec['pen']
newRectSrc.brush = rec['brush'] newRectSrc.brush = rec['brush']
newRectSrc.symbol = rec[3]
self.symbolMap[key] = newRectSrc self.symbolMap[key] = newRectSrc
self.atlasValid = False self.atlasValid = False
sourceRect[i] = newRectSrc sourceRect[i] = newRectSrc
@ -151,7 +152,7 @@ class SymbolAtlas(object):
images = [] images = []
for key, sourceRect in self.symbolMap.items(): for key, sourceRect in self.symbolMap.items():
if sourceRect.width() == 0: if sourceRect.width() == 0:
img = renderSymbol(key[0], key[1], sourceRect.pen, sourceRect.brush) img = renderSymbol(sourceRect.symbol, key[1], sourceRect.pen, sourceRect.brush)
images.append(img) ## we only need this to prevent the images being garbage collected immediately images.append(img) ## we only need this to prevent the images being garbage collected immediately
arr = fn.imageToArray(img, copy=False, transpose=False) arr = fn.imageToArray(img, copy=False, transpose=False)
else: else:

View File

@ -0,0 +1,125 @@
from ..Qt import QtGui, QtCore
import numpy as np
from ..Point import Point
from .. import functions as fn
from .GraphicsObject import GraphicsObject
from .TextItem import TextItem
class TargetItem(GraphicsObject):
"""Draws a draggable target symbol (circle plus crosshair).
The size of TargetItem will remain fixed on screen even as the view is zoomed.
Includes an optional text label.
"""
sigDragged = QtCore.Signal(object)
def __init__(self, movable=True, radii=(5, 10, 10), pen=(255, 255, 0), brush=(0, 0, 255, 100)):
GraphicsObject.__init__(self)
self._bounds = None
self._radii = radii
self._picture = None
self.movable = movable
self.moving = False
self.label = None
self.labelAngle = 0
self.pen = fn.mkPen(pen)
self.brush = fn.mkBrush(brush)
def setLabel(self, label):
if label is None:
if self.label is not None:
self.label.scene().removeItem(self.label)
self.label = None
else:
if self.label is None:
self.label = TextItem()
self.label.setParentItem(self)
self.label.setText(label)
self._updateLabel()
def setLabelAngle(self, angle):
self.labelAngle = angle
self._updateLabel()
def boundingRect(self):
if self._picture is None:
self._drawPicture()
return self._bounds
def dataBounds(self, axis, frac=1.0, orthoRange=None):
return [0, 0]
def viewTransformChanged(self):
self._picture = None
self.prepareGeometryChange()
self._updateLabel()
def _updateLabel(self):
if self.label is None:
return
# find an optimal location for text at the given angle
angle = self.labelAngle * np.pi / 180.
lbr = self.label.boundingRect()
center = lbr.center()
a = abs(np.sin(angle) * lbr.height()*0.5)
b = abs(np.cos(angle) * lbr.width()*0.5)
r = max(self._radii) + 2 + max(a, b)
pos = self.mapFromScene(self.mapToScene(QtCore.QPointF(0, 0)) + r * QtCore.QPointF(np.cos(angle), -np.sin(angle)) - center)
self.label.setPos(pos)
def paint(self, p, *args):
if self._picture is None:
self._drawPicture()
self._picture.play(p)
def _drawPicture(self):
self._picture = QtGui.QPicture()
p = QtGui.QPainter(self._picture)
p.setRenderHint(p.Antialiasing)
# Note: could do this with self.pixelLength, but this is faster.
o = self.mapToScene(QtCore.QPointF(0, 0))
px = abs(1.0 / (self.mapToScene(QtCore.QPointF(1, 0)) - o).x())
py = abs(1.0 / (self.mapToScene(QtCore.QPointF(0, 1)) - o).y())
r, w, h = self._radii
w = w * px
h = h * py
rx = r * px
ry = r * py
rect = QtCore.QRectF(-rx, -ry, rx*2, ry*2)
p.setPen(self.pen)
p.setBrush(self.brush)
p.drawEllipse(rect)
p.drawLine(Point(-w, 0), Point(w, 0))
p.drawLine(Point(0, -h), Point(0, h))
p.end()
bx = max(w, rx)
by = max(h, ry)
self._bounds = QtCore.QRectF(-bx, -by, bx*2, by*2)
def mouseDragEvent(self, ev):
if not self.movable:
return
if ev.button() == QtCore.Qt.LeftButton:
if ev.isStart():
self.moving = True
self.cursorOffset = self.pos() - self.mapToParent(ev.buttonDownPos())
self.startPosition = self.pos()
ev.accept()
if not self.moving:
return
self.setPos(self.cursorOffset + self.mapToParent(ev.pos()))
if ev.isFinish():
self.moving = False
self.sigDragged.emit(self)
def hoverEvent(self, ev):
if self.movable:
ev.acceptDrags(QtCore.Qt.LeftButton)

View File

@ -85,7 +85,6 @@ class ViewBox(GraphicsWidget):
sigXRangeChanged = QtCore.Signal(object, object) sigXRangeChanged = QtCore.Signal(object, object)
sigRangeChangedManually = QtCore.Signal(object) sigRangeChangedManually = QtCore.Signal(object)
sigRangeChanged = QtCore.Signal(object, object) sigRangeChanged = QtCore.Signal(object, object)
#sigActionPositionChanged = QtCore.Signal(object)
sigStateChanged = QtCore.Signal(object) sigStateChanged = QtCore.Signal(object)
sigTransformChanged = QtCore.Signal(object) sigTransformChanged = QtCore.Signal(object)
sigResized = QtCore.Signal(object) sigResized = QtCore.Signal(object)
@ -128,8 +127,6 @@ class ViewBox(GraphicsWidget):
self.name = None self.name = None
self.linksBlocked = False self.linksBlocked = False
self.addedItems = [] self.addedItems = []
#self.gView = view
#self.showGrid = showGrid
self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred self._matrixNeedsUpdate = True ## indicates that range has changed, but matrix update was deferred
self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed. self._autoRangeNeedsUpdate = True ## indicates auto-range needs to be recomputed.
@ -188,9 +185,6 @@ class ViewBox(GraphicsWidget):
self.background.setPen(fn.mkPen(None)) self.background.setPen(fn.mkPen(None))
self.updateBackground() self.updateBackground()
#self.useLeftButtonPan = pyqtgraph.getConfigOption('leftButtonPan') # normally use left button to pan
# this also enables capture of keyPressEvents.
## Make scale box that is shown when dragging on the view ## Make scale box that is shown when dragging on the view
self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1)
self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1)) self.rbScaleBox.setPen(fn.mkPen((255,255,100), width=1))
@ -239,7 +233,6 @@ class ViewBox(GraphicsWidget):
ViewBox.updateAllViewLists() ViewBox.updateAllViewLists()
sid = id(self) sid = id(self)
self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None) self.destroyed.connect(lambda: ViewBox.forgetView(sid, name) if (ViewBox is not None and 'sid' in locals() and 'name' in locals()) else None)
#self.destroyed.connect(self.unregister)
def unregister(self): def unregister(self):
""" """
@ -288,16 +281,12 @@ class ViewBox(GraphicsWidget):
self.prepareForPaint() self.prepareForPaint()
self._lastScene = scene self._lastScene = scene
def prepareForPaint(self): def prepareForPaint(self):
#autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False) #autoRangeEnabled = (self.state['autoRange'][0] is not False) or (self.state['autoRange'][1] is not False)
# don't check whether auto range is enabled here--only check when setting dirty flag. # don't check whether auto range is enabled here--only check when setting dirty flag.
if self._autoRangeNeedsUpdate: # and autoRangeEnabled: if self._autoRangeNeedsUpdate: # and autoRangeEnabled:
self.updateAutoRange() self.updateAutoRange()
if self._matrixNeedsUpdate: self.updateMatrix()
self.updateMatrix()
def getState(self, copy=True): def getState(self, copy=True):
"""Return the current state of the ViewBox. """Return the current state of the ViewBox.
@ -326,7 +315,6 @@ class ViewBox(GraphicsWidget):
del state['linkedViews'] del state['linkedViews']
self.state.update(state) self.state.update(state)
#self.updateMatrix()
self.updateViewRange() self.updateViewRange()
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
@ -353,12 +341,6 @@ class ViewBox(GraphicsWidget):
self.state['mouseMode'] = mode self.state['mouseMode'] = mode
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
#def toggleLeftAction(self, act): ## for backward compatibility
#if act.text() is 'pan':
#self.setLeftButtonAction('pan')
#elif act.text() is 'zoom':
#self.setLeftButtonAction('rect')
def setLeftButtonAction(self, mode='rect'): ## for backward compatibility def setLeftButtonAction(self, mode='rect'): ## for backward compatibility
if mode.lower() == 'rect': if mode.lower() == 'rect':
self.setMouseMode(ViewBox.RectMode) self.setMouseMode(ViewBox.RectMode)
@ -405,7 +387,6 @@ class ViewBox(GraphicsWidget):
if not ignoreBounds: if not ignoreBounds:
self.addedItems.append(item) self.addedItems.append(item)
self.updateAutoRange() self.updateAutoRange()
#print "addItem:", item, item.boundingRect()
def removeItem(self, item): def removeItem(self, item):
"""Remove an item from this view.""" """Remove an item from this view."""
@ -423,6 +404,7 @@ class ViewBox(GraphicsWidget):
ch.setParentItem(None) ch.setParentItem(None)
def resizeEvent(self, ev): def resizeEvent(self, ev):
self._matrixNeedsUpdate = True
self.linkedXChanged() self.linkedXChanged()
self.linkedYChanged() self.linkedYChanged()
self.updateAutoRange() self.updateAutoRange()
@ -562,10 +544,6 @@ class ViewBox(GraphicsWidget):
# If nothing has changed, we are done. # If nothing has changed, we are done.
if any(changed): if any(changed):
#if update and self.matrixNeedsUpdate:
#self.updateMatrix(changed)
#return
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
# Update target rect for debugging # Update target rect for debugging
@ -576,26 +554,9 @@ class ViewBox(GraphicsWidget):
# Note that aspect ratio constraints and auto-visible probably do not work together.. # Note that aspect ratio constraints and auto-visible probably do not work together..
if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False): if changed[0] and self.state['autoVisibleOnly'][1] and (self.state['autoRange'][0] is not False):
self._autoRangeNeedsUpdate = True self._autoRangeNeedsUpdate = True
#self.updateAutoRange() ## Maybe just indicate that auto range needs to be updated?
elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False): elif changed[1] and self.state['autoVisibleOnly'][0] and (self.state['autoRange'][1] is not False):
self._autoRangeNeedsUpdate = True self._autoRangeNeedsUpdate = True
#self.updateAutoRange()
## Update view matrix only if requested
#if update:
#self.updateMatrix(changed)
## Otherwise, indicate that the matrix needs to be updated
#else:
#self.matrixNeedsUpdate = True
## Inform linked views that the range has changed <<This should be moved>>
#for ax, range in changes.items():
#link = self.linkedView(ax)
#if link is not None:
#link.linkedViewChanged(self, ax)
def setYRange(self, min, max, padding=None, update=True): def setYRange(self, min, max, padding=None, update=True):
""" """
Set the visible Y range of the view to [*min*, *max*]. Set the visible Y range of the view to [*min*, *max*].
@ -675,10 +636,6 @@ class ViewBox(GraphicsWidget):
for kwd in kwds: for kwd in kwds:
if kwd not in allowed: if kwd not in allowed:
raise ValueError("Invalid keyword argument '%s'." % kwd) raise ValueError("Invalid keyword argument '%s'." % kwd)
#for kwd in ['xLimits', 'yLimits', 'minRange', 'maxRange']:
#if kwd in kwds and self.state['limits'][kwd] != kwds[kwd]:
#self.state['limits'][kwd] = kwds[kwd]
#update = True
for axis in [0,1]: for axis in [0,1]:
for mnmx in [0,1]: for mnmx in [0,1]:
kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx] kwd = [['xMin', 'xMax'], ['yMin', 'yMax']][axis][mnmx]
@ -694,9 +651,6 @@ class ViewBox(GraphicsWidget):
if update: if update:
self.updateViewRange() self.updateViewRange()
def scaleBy(self, s=None, center=None, x=None, y=None): def scaleBy(self, s=None, center=None, x=None, y=None):
""" """
@ -762,8 +716,6 @@ class ViewBox(GraphicsWidget):
y = vr.top()+y, vr.bottom()+y y = vr.top()+y, vr.bottom()+y
if x is not None or y is not None: if x is not None or y is not None:
self.setRange(xRange=x, yRange=y, padding=0) self.setRange(xRange=x, yRange=y, padding=0)
def enableAutoRange(self, axis=None, enable=True, x=None, y=None): def enableAutoRange(self, axis=None, enable=True, x=None, y=None):
""" """
@ -773,11 +725,6 @@ class ViewBox(GraphicsWidget):
The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should The argument *enable* may optionally be a float (0.0-1.0) which indicates the fraction of the data that should
be visible (this only works with items implementing a dataRange method, such as PlotDataItem). be visible (this only works with items implementing a dataRange method, such as PlotDataItem).
""" """
#print "autorange:", axis, enable
#if not enable:
#import traceback
#traceback.print_stack()
# support simpler interface: # support simpler interface:
if x is not None or y is not None: if x is not None or y is not None:
if x is not None: if x is not None:
@ -813,10 +760,6 @@ class ViewBox(GraphicsWidget):
self.state['autoRange'][ax] = enable self.state['autoRange'][ax] = enable
self._autoRangeNeedsUpdate |= (enable is not False) self._autoRangeNeedsUpdate |= (enable is not False)
self.update() self.update()
#if needAutoRangeUpdate:
# self.updateAutoRange()
self.sigStateChanged.emit(self) self.sigStateChanged.emit(self)
@ -828,6 +771,8 @@ class ViewBox(GraphicsWidget):
return self.state['autoRange'][:] return self.state['autoRange'][:]
def setAutoPan(self, x=None, y=None): def setAutoPan(self, x=None, y=None):
"""Set whether automatic range will only pan (not scale) the view.
"""
if x is not None: if x is not None:
self.state['autoPan'][0] = x self.state['autoPan'][0] = x
if y is not None: if y is not None:
@ -836,6 +781,9 @@ class ViewBox(GraphicsWidget):
self.updateAutoRange() self.updateAutoRange()
def setAutoVisible(self, x=None, y=None): def setAutoVisible(self, x=None, y=None):
"""Set whether automatic range uses only visible data when determining
the range to show.
"""
if x is not None: if x is not None:
self.state['autoVisibleOnly'][0] = x self.state['autoVisibleOnly'][0] = x
if x is True: if x is True:
@ -924,7 +872,6 @@ class ViewBox(GraphicsWidget):
"""Link this view's Y axis to another view. (see LinkView)""" """Link this view's Y axis to another view. (see LinkView)"""
self.linkView(self.YAxis, view) self.linkView(self.YAxis, view)
def linkView(self, axis, view): def linkView(self, axis, view):
""" """
Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis. Link X or Y axes of two views and unlink any previously connected axes. *axis* must be ViewBox.XAxis or ViewBox.YAxis.
@ -1118,7 +1065,6 @@ class ViewBox(GraphicsWidget):
return return
self.state['aspectLocked'] = ratio self.state['aspectLocked'] = ratio
if ratio != currentRatio: ## If this would change the current range, do that now if ratio != currentRatio: ## If this would change the current range, do that now
#self.setRange(0, self.state['viewRange'][0][0], self.state['viewRange'][0][1])
self.updateViewRange() self.updateViewRange()
self.updateAutoRange() self.updateAutoRange()
@ -1130,45 +1076,49 @@ class ViewBox(GraphicsWidget):
Return the transform that maps from child(item in the childGroup) coordinates to local coordinates. Return the transform that maps from child(item in the childGroup) coordinates to local coordinates.
(This maps from inside the viewbox to outside) (This maps from inside the viewbox to outside)
""" """
if self._matrixNeedsUpdate: self.updateMatrix()
self.updateMatrix()
m = self.childGroup.transform() m = self.childGroup.transform()
#m1 = QtGui.QTransform() return m
#m1.translate(self.childGroup.pos().x(), self.childGroup.pos().y())
return m #*m1
def mapToView(self, obj): def mapToView(self, obj):
"""Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox""" """Maps from the local coordinates of the ViewBox to the coordinate system displayed inside the ViewBox"""
self.updateMatrix()
m = fn.invertQTransform(self.childTransform()) m = fn.invertQTransform(self.childTransform())
return m.map(obj) return m.map(obj)
def mapFromView(self, obj): def mapFromView(self, obj):
"""Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox""" """Maps from the coordinate system displayed inside the ViewBox to the local coordinates of the ViewBox"""
self.updateMatrix()
m = self.childTransform() m = self.childTransform()
return m.map(obj) return m.map(obj)
def mapSceneToView(self, obj): def mapSceneToView(self, obj):
"""Maps from scene coordinates to the coordinate system displayed inside the ViewBox""" """Maps from scene coordinates to the coordinate system displayed inside the ViewBox"""
self.updateMatrix()
return self.mapToView(self.mapFromScene(obj)) return self.mapToView(self.mapFromScene(obj))
def mapViewToScene(self, obj): def mapViewToScene(self, obj):
"""Maps from the coordinate system displayed inside the ViewBox to scene coordinates""" """Maps from the coordinate system displayed inside the ViewBox to scene coordinates"""
self.updateMatrix()
return self.mapToScene(self.mapFromView(obj)) return self.mapToScene(self.mapFromView(obj))
def mapFromItemToView(self, item, obj): def mapFromItemToView(self, item, obj):
"""Maps *obj* from the local coordinate system of *item* to the view coordinates""" """Maps *obj* from the local coordinate system of *item* to the view coordinates"""
self.updateMatrix()
return self.childGroup.mapFromItem(item, obj) return self.childGroup.mapFromItem(item, obj)
#return self.mapSceneToView(item.mapToScene(obj)) #return self.mapSceneToView(item.mapToScene(obj))
def mapFromViewToItem(self, item, obj): def mapFromViewToItem(self, item, obj):
"""Maps *obj* from view coordinates to the local coordinate system of *item*.""" """Maps *obj* from view coordinates to the local coordinate system of *item*."""
self.updateMatrix()
return self.childGroup.mapToItem(item, obj) return self.childGroup.mapToItem(item, obj)
#return item.mapFromScene(self.mapViewToScene(obj))
def mapViewToDevice(self, obj): def mapViewToDevice(self, obj):
self.updateMatrix()
return self.mapToDevice(self.mapFromView(obj)) return self.mapToDevice(self.mapFromView(obj))
def mapDeviceToView(self, obj): def mapDeviceToView(self, obj):
self.updateMatrix()
return self.mapToView(self.mapFromDevice(obj)) return self.mapToView(self.mapFromDevice(obj))
def viewPixelSize(self): def viewPixelSize(self):
@ -1177,25 +1127,9 @@ class ViewBox(GraphicsWidget):
px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()] px, py = [Point(self.mapToView(v) - o) for v in self.pixelVectors()]
return (px.length(), py.length()) return (px.length(), py.length())
def itemBoundingRect(self, item): def itemBoundingRect(self, item):
"""Return the bounding rect of the item in view coordinates""" """Return the bounding rect of the item in view coordinates"""
return self.mapSceneToView(item.sceneBoundingRect()).boundingRect() return self.mapSceneToView(item.sceneBoundingRect()).boundingRect()
#def viewScale(self):
#vr = self.viewRect()
##print "viewScale:", self.range
#xd = vr.width()
#yd = vr.height()
#if xd == 0 or yd == 0:
#print "Warning: 0 range in view:", xd, yd
#return np.array([1,1])
##cs = self.canvas().size()
#cs = self.boundingRect()
#scale = np.array([cs.width() / xd, cs.height() / yd])
##print "view scale:", scale
#return scale
def wheelEvent(self, ev, axis=None): def wheelEvent(self, ev, axis=None):
mask = np.array(self.state['mouseEnabled'], dtype=np.float) mask = np.array(self.state['mouseEnabled'], dtype=np.float)
@ -1206,13 +1140,11 @@ class ViewBox(GraphicsWidget):
s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor s = ((mask * 0.02) + 1) ** (ev.delta() * self.state['wheelScaleFactor']) # actual scaling factor
center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos())) center = Point(fn.invertQTransform(self.childGroup.transform()).map(ev.pos()))
#center = ev.pos()
self._resetTarget() self._resetTarget()
self.scaleBy(s, center) self.scaleBy(s, center)
self.sigRangeChangedManually.emit(self.state['mouseEnabled']) self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
ev.accept() ev.accept()
def mouseClickEvent(self, ev): def mouseClickEvent(self, ev):
if ev.button() == QtCore.Qt.RightButton and self.menuEnabled(): if ev.button() == QtCore.Qt.RightButton and self.menuEnabled():
@ -1251,7 +1183,6 @@ class ViewBox(GraphicsWidget):
if ev.isFinish(): ## This is the final move in the drag; change the view scale now if ev.isFinish(): ## This is the final move in the drag; change the view scale now
#print "finish" #print "finish"
self.rbScaleBox.hide() self.rbScaleBox.hide()
#ax = QtCore.QRectF(Point(self.pressPos), Point(self.mousePos))
ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos)) ax = QtCore.QRectF(Point(ev.buttonDownPos(ev.button())), Point(pos))
ax = self.childGroup.mapRectFromParent(ax) ax = self.childGroup.mapRectFromParent(ax)
self.showAxRect(ax) self.showAxRect(ax)
@ -1301,12 +1232,6 @@ class ViewBox(GraphicsWidget):
ctrl-- : moves backward in the zooming stack (if it exists) ctrl-- : moves backward in the zooming stack (if it exists)
""" """
#print ev.key()
#print 'I intercepted a key press, but did not accept it'
## not implemented yet ?
#self.keypress.sigkeyPressEvent.emit()
ev.accept() ev.accept()
if ev.text() == '-': if ev.text() == '-':
self.scaleHistory(-1) self.scaleHistory(-1)
@ -1324,7 +1249,6 @@ class ViewBox(GraphicsWidget):
if ptr != self.axHistoryPointer: if ptr != self.axHistoryPointer:
self.axHistoryPointer = ptr self.axHistoryPointer = ptr
self.showAxRect(self.axHistory[ptr]) self.showAxRect(self.axHistory[ptr])
def updateScaleBox(self, p1, p2): def updateScaleBox(self, p1, p2):
r = QtCore.QRectF(p1, p2) r = QtCore.QRectF(p1, p2)
@ -1338,14 +1262,6 @@ class ViewBox(GraphicsWidget):
self.setRange(ax.normalized()) # be sure w, h are correct coordinates self.setRange(ax.normalized()) # be sure w, h are correct coordinates
self.sigRangeChangedManually.emit(self.state['mouseEnabled']) self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
#def mouseRect(self):
#vs = self.viewScale()
#vr = self.state['viewRange']
## Convert positions from screen (view) pixel coordinates to axis coordinates
#ax = QtCore.QRectF(self.pressPos[0]/vs[0]+vr[0][0], -(self.pressPos[1]/vs[1]-vr[1][1]),
#(self.mousePos[0]-self.pressPos[0])/vs[0], -(self.mousePos[1]-self.pressPos[1])/vs[1])
#return(ax)
def allChildren(self, item=None): def allChildren(self, item=None):
"""Return a list of all children and grandchildren of this ViewBox""" """Return a list of all children and grandchildren of this ViewBox"""
if item is None: if item is None:
@ -1356,8 +1272,6 @@ class ViewBox(GraphicsWidget):
children.extend(self.allChildren(ch)) children.extend(self.allChildren(ch))
return children return children
def childrenBounds(self, frac=None, orthoRange=(None,None), items=None): def childrenBounds(self, frac=None, orthoRange=(None,None), items=None):
"""Return the bounding range of all children. """Return the bounding range of all children.
[[xmin, xmax], [ymin, ymax]] [[xmin, xmax], [ymin, ymax]]
@ -1373,15 +1287,13 @@ class ViewBox(GraphicsWidget):
## First collect all boundary information ## First collect all boundary information
itemBounds = [] itemBounds = []
for item in items: for item in items:
if not item.isVisible(): if not item.isVisible() or not item.scene() is self.scene():
continue continue
useX = True useX = True
useY = True useY = True
if hasattr(item, 'dataBounds'): if hasattr(item, 'dataBounds'):
#bounds = self._itemBoundsCache.get(item, None)
#if bounds is None:
if frac is None: if frac is None:
frac = (1.0, 1.0) frac = (1.0, 1.0)
xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0]) xr = item.dataBounds(0, frac=frac[0], orthoRange=orthoRange[0])
@ -1414,9 +1326,6 @@ class ViewBox(GraphicsWidget):
itemBounds.append((bounds, useX, useY, pxPad)) itemBounds.append((bounds, useX, useY, pxPad))
#self._itemBoundsCache[item] = (bounds, useX, useY)
#else:
#bounds, useX, useY = bounds
else: else:
if int(item.flags() & item.ItemHasNoContents) > 0: if int(item.flags() & item.ItemHasNoContents) > 0:
continue continue
@ -1425,8 +1334,6 @@ class ViewBox(GraphicsWidget):
bounds = self.mapFromItemToView(item, bounds).boundingRect() bounds = self.mapFromItemToView(item, bounds).boundingRect()
itemBounds.append((bounds, True, True, 0)) itemBounds.append((bounds, True, True, 0))
#print itemBounds
## determine tentative new range ## determine tentative new range
range = [None, None] range = [None, None]
for bounds, useX, useY, px in itemBounds: for bounds, useX, useY, px in itemBounds:
@ -1442,14 +1349,11 @@ class ViewBox(GraphicsWidget):
range[0] = [bounds.left(), bounds.right()] range[0] = [bounds.left(), bounds.right()]
profiler() profiler()
#print "range", range
## Now expand any bounds that have a pixel margin ## Now expand any bounds that have a pixel margin
## This must be done _after_ we have a good estimate of the new range ## This must be done _after_ we have a good estimate of the new range
## to ensure that the pixel size is roughly accurate. ## to ensure that the pixel size is roughly accurate.
w = self.width() w = self.width()
h = self.height() h = self.height()
#print "w:", w, "h:", h
if w > 0 and range[0] is not None: if w > 0 and range[0] is not None:
pxSize = (range[0][1] - range[0][0]) / w pxSize = (range[0][1] - range[0][0]) / w
for bounds, useX, useY, px in itemBounds: for bounds, useX, useY, px in itemBounds:
@ -1585,9 +1489,9 @@ class ViewBox(GraphicsWidget):
self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1])) self.sigYRangeChanged.emit(self, tuple(self.state['viewRange'][1]))
if any(changed): if any(changed):
self._matrixNeedsUpdate = True
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]:
@ -1598,6 +1502,9 @@ class ViewBox(GraphicsWidget):
link.linkedViewChanged(self, ax) link.linkedViewChanged(self, ax)
def updateMatrix(self, changed=None): def updateMatrix(self, changed=None):
if not self._matrixNeedsUpdate:
return
## Make the childGroup's transform match the requested viewRange. ## Make the childGroup's transform match the requested viewRange.
bounds = self.rect() bounds = self.rect()
@ -1648,7 +1555,6 @@ class ViewBox(GraphicsWidget):
self.background.show() self.background.show()
self.background.setBrush(fn.mkBrush(bg)) self.background.setBrush(fn.mkBrush(bg))
def updateViewLists(self): def updateViewLists(self):
try: try:
self.window() self.window()
@ -1662,7 +1568,6 @@ class ViewBox(GraphicsWidget):
## make a sorted list of all named views ## make a sorted list of all named views
nv = list(ViewBox.NamedViews.values()) nv = list(ViewBox.NamedViews.values())
#print "new view list:", nv
sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList sortList(nv, cmpViews) ## see pyqtgraph.python2_3.sortList
if self in nv: if self in nv:
@ -1676,16 +1581,11 @@ class ViewBox(GraphicsWidget):
for v in nv: for v in nv:
if link == v.name: if link == v.name:
self.linkView(ax, v) self.linkView(ax, v)
#print "New view list:", nv
#print "linked views:", self.state['linkedViews']
@staticmethod @staticmethod
def updateAllViewLists(): def updateAllViewLists():
#print "Update:", ViewBox.AllViews.keys()
#print "Update:", ViewBox.NamedViews.keys()
for v in ViewBox.AllViews: for v in ViewBox.AllViews:
v.updateViewLists() v.updateViewLists()
@staticmethod @staticmethod
def forgetView(vid, name): def forgetView(vid, name):
@ -1766,4 +1666,5 @@ class ViewBox(GraphicsWidget):
self.scene().removeItem(self.locateGroup) self.scene().removeItem(self.locateGroup)
self.locateGroup = None self.locateGroup = None
from .ViewBoxMenu import ViewBoxMenu from .ViewBoxMenu import ViewBoxMenu

View File

@ -208,15 +208,23 @@ def test_PolyLineROI():
# click segment # click segment
mouseClick(plt, pt, QtCore.Qt.LeftButton) mouseClick(plt, pt, QtCore.Qt.LeftButton)
assertImageApproved(plt, 'roi/polylineroi/'+name+'_click_segment', 'Click mouse over segment.') assertImageApproved(plt, 'roi/polylineroi/'+name+'_click_segment', 'Click mouse over segment.')
# drag new handle
mouseMove(plt, pt+pg.Point(10, -10)) # pg bug: have to move the mouse off/on again to register hover
mouseDrag(plt, pt, pt + pg.Point(10, -10), QtCore.Qt.LeftButton)
assertImageApproved(plt, 'roi/polylineroi/'+name+'_drag_new_handle', 'Drag mouse over created handle.')
# clear all points
r.clearPoints() r.clearPoints()
assertImageApproved(plt, 'roi/polylineroi/'+name+'_clear', 'All points cleared.') assertImageApproved(plt, 'roi/polylineroi/'+name+'_clear', 'All points cleared.')
assert len(r.getState()['points']) == 0 assert len(r.getState()['points']) == 0
# call setPoints
r.setPoints(initState['points']) r.setPoints(initState['points'])
assertImageApproved(plt, 'roi/polylineroi/'+name+'_setpoints', 'Reset points to initial state.') assertImageApproved(plt, 'roi/polylineroi/'+name+'_setpoints', 'Reset points to initial state.')
assert len(r.getState()['points']) == 3 assert len(r.getState()['points']) == 3
# call setState
r.setState(initState) r.setState(initState)
assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.') assertImageApproved(plt, 'roi/polylineroi/'+name+'_setstate', 'Reset ROI to initial state.')
assert len(r.getState()['points']) == 3 assert len(r.getState()['points']) == 3

View File

@ -1,3 +1,4 @@
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph as pg import pyqtgraph as pg
import numpy as np import numpy as np
app = pg.mkQApp() app = pg.mkQApp()
@ -7,9 +8,16 @@ app.processEvents()
def test_scatterplotitem(): def test_scatterplotitem():
plot = pg.PlotWidget() plot = pg.PlotWidget()
# set view range equal to its bounding rect. # set view range equal to its bounding rect.
# This causes plots to look the same regardless of pxMode. # This causes plots to look the same regardless of pxMode.
plot.setRange(rect=plot.boundingRect()) plot.setRange(rect=plot.boundingRect())
# test SymbolAtlas accepts custom symbol
s = pg.ScatterPlotItem()
symbol = QtGui.QPainterPath()
symbol.addEllipse(QtCore.QRectF(-0.5, -0.5, 1, 1))
s.addPoints([{'pos': [0,0], 'data': 1, 'symbol': symbol}])
for i, pxMode in enumerate([True, False]): for i, pxMode in enumerate([True, False]):
for j, useCache in enumerate([True, False]): for j, useCache in enumerate([True, False]):
s = pg.ScatterPlotItem() s = pg.ScatterPlotItem()
@ -17,14 +25,14 @@ def test_scatterplotitem():
plot.addItem(s) plot.addItem(s)
s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode) s.setData(x=np.array([10,40,20,30])+i*100, y=np.array([40,60,10,30])+j*100, pxMode=pxMode)
s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30]) s.addPoints(x=np.array([60, 70])+i*100, y=np.array([60, 70])+j*100, size=[20, 30])
# Test uniform spot updates # Test uniform spot updates
s.setSize(10) s.setSize(10)
s.setBrush('r') s.setBrush('r')
s.setPen('g') s.setPen('g')
s.setSymbol('+') s.setSymbol('+')
app.processEvents() app.processEvents()
# Test list spot updates # Test list spot updates
s.setSize([10] * 6) s.setSize([10] * 6)
s.setBrush([pg.mkBrush('r')] * 6) s.setBrush([pg.mkBrush('r')] * 6)
@ -55,7 +63,7 @@ def test_scatterplotitem():
def test_init_spots(): def test_init_spots():
plot = pg.PlotWidget() plot = pg.PlotWidget()
# set view range equal to its bounding rect. # set view range equal to its bounding rect.
# This causes plots to look the same regardless of pxMode. # This causes plots to look the same regardless of pxMode.
plot.setRange(rect=plot.boundingRect()) plot.setRange(rect=plot.boundingRect())
spots = [ spots = [
@ -63,28 +71,28 @@ def test_init_spots():
{'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'}, {'pos': (1, 2), 'pen': None, 'brush': None, 'data': 'zzz'},
] ]
s = pg.ScatterPlotItem(spots=spots) s = pg.ScatterPlotItem(spots=spots)
# Check we can display without errors # Check we can display without errors
plot.addItem(s) plot.addItem(s)
app.processEvents() app.processEvents()
plot.clear() plot.clear()
# check data is correct # check data is correct
spots = s.points() spots = s.points()
defPen = pg.mkPen(pg.getConfigOption('foreground')) defPen = pg.mkPen(pg.getConfigOption('foreground'))
assert spots[0].pos().x() == 0 assert spots[0].pos().x() == 0
assert spots[0].pos().y() == 1 assert spots[0].pos().y() == 1
assert spots[0].pen() == defPen assert spots[0].pen() == defPen
assert spots[0].data() is None assert spots[0].data() is None
assert spots[1].pos().x() == 1 assert spots[1].pos().x() == 1
assert spots[1].pos().y() == 2 assert spots[1].pos().y() == 2
assert spots[1].pen() == pg.mkPen(None) assert spots[1].pen() == pg.mkPen(None)
assert spots[1].brush() == pg.mkBrush(None) assert spots[1].brush() == pg.mkBrush(None)
assert spots[1].data() == 'zzz' assert spots[1].data() == 'zzz'
if __name__ == '__main__': if __name__ == '__main__':
test_scatterplotitem() test_scatterplotitem()

View File

@ -1,25 +1,22 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
graphicsWindows.py - Convenience classes which create a new window with PlotWidget or ImageView. DEPRECATED: The classes below are convenience classes that create a new window
Copyright 2010 Luke Campagnola containting a single, specific widget. These classes are now unnecessary because
Distributed under MIT/X11 license. See license.txt for more infomation. it is possible to place any widget into its own window by simply calling its
show() method.
""" """
from .Qt import QtCore, QtGui from .Qt import QtCore, QtGui, mkQApp
from .widgets.PlotWidget import * from .widgets.PlotWidget import *
from .imageview import * from .imageview import *
from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget from .widgets.GraphicsLayoutWidget import GraphicsLayoutWidget
from .widgets.GraphicsView import GraphicsView from .widgets.GraphicsView import GraphicsView
QAPP = None
def mkQApp():
if QtGui.QApplication.instance() is None:
global QAPP
QAPP = QtGui.QApplication([])
class GraphicsWindow(GraphicsLayoutWidget): class GraphicsWindow(GraphicsLayoutWidget):
""" """
(deprecated; use GraphicsLayoutWidget instead)
Convenience subclass of :class:`GraphicsLayoutWidget Convenience subclass of :class:`GraphicsLayoutWidget
<pyqtgraph.GraphicsLayoutWidget>`. This class is intended for use from <pyqtgraph.GraphicsLayoutWidget>`. This class is intended for use from
the interactive python prompt. the interactive python prompt.
@ -34,6 +31,9 @@ class GraphicsWindow(GraphicsLayoutWidget):
class TabWindow(QtGui.QMainWindow): class TabWindow(QtGui.QMainWindow):
"""
(deprecated)
"""
def __init__(self, title=None, size=(800,600)): def __init__(self, title=None, size=(800,600)):
mkQApp() mkQApp()
QtGui.QMainWindow.__init__(self) QtGui.QMainWindow.__init__(self)
@ -52,6 +52,9 @@ class TabWindow(QtGui.QMainWindow):
class PlotWindow(PlotWidget): class PlotWindow(PlotWidget):
"""
(deprecated; use PlotWidget instead)
"""
def __init__(self, title=None, **kargs): def __init__(self, title=None, **kargs):
mkQApp() mkQApp()
self.win = QtGui.QMainWindow() self.win = QtGui.QMainWindow()
@ -65,6 +68,9 @@ class PlotWindow(PlotWidget):
class ImageWindow(ImageView): class ImageWindow(ImageView):
"""
(deprecated; use ImageView instead)
"""
def __init__(self, *args, **kargs): def __init__(self, *args, **kargs):
mkQApp() mkQApp()
self.win = QtGui.QMainWindow() self.win = QtGui.QMainWindow()

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 os 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
@ -26,6 +26,7 @@ from ..graphicsItems.ROI import *
from ..graphicsItems.LinearRegionItem import * from ..graphicsItems.LinearRegionItem import *
from ..graphicsItems.InfiniteLine import * from ..graphicsItems.InfiniteLine import *
from ..graphicsItems.ViewBox import * from ..graphicsItems.ViewBox import *
from ..graphicsItems.VTickGroup import VTickGroup
from ..graphicsItems.GradientEditorItem import addGradientListToDocstring from ..graphicsItems.GradientEditorItem import addGradientListToDocstring
from .. import ptime as ptime from .. import ptime as ptime
from .. import debug as debug from .. import debug as debug
@ -79,7 +80,8 @@ class ImageView(QtGui.QWidget):
sigTimeChanged = QtCore.Signal(object, object) sigTimeChanged = QtCore.Signal(object, object)
sigProcessingChanged = QtCore.Signal(object) sigProcessingChanged = QtCore.Signal(object)
def __init__(self, parent=None, name="ImageView", view=None, imageItem=None, *args): def __init__(self, parent=None, name="ImageView", view=None, imageItem=None,
levelMode='mono', *args):
""" """
By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data By default, this class creates an :class:`ImageItem <pyqtgraph.ImageItem>` to display image data
and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem. and a :class:`ViewBox <pyqtgraph.ViewBox>` to contain the ImageItem.
@ -101,6 +103,9 @@ class ImageView(QtGui.QWidget):
imageItem (ImageItem) If specified, this object will be used to imageItem (ImageItem) If specified, this object will be used to
display the image. Must be an instance of ImageItem display the image. Must be an instance of ImageItem
or other compatible object. or other compatible object.
levelMode See the *levelMode* argument to
:func:`HistogramLUTItem.__init__()
<pyqtgraph.HistogramLUTItem.__init__>`
============= ========================================================= ============= =========================================================
Note: to display axis ticks inside the ImageView, instantiate it Note: to display axis ticks inside the ImageView, instantiate it
@ -109,8 +114,10 @@ class ImageView(QtGui.QWidget):
pg.ImageView(view=pg.PlotItem()) pg.ImageView(view=pg.PlotItem())
""" """
QtGui.QWidget.__init__(self, parent, *args) QtGui.QWidget.__init__(self, parent, *args)
self.levelMax = 4096 self._imageLevels = None # [(min, max), ...] per channel image metrics
self.levelMin = 0 self.levelMin = None # min / max levels across all channels
self.levelMax = None
self.name = name self.name = name
self.image = None self.image = None
self.axes = {} self.axes = {}
@ -118,6 +125,7 @@ class ImageView(QtGui.QWidget):
self.ui = Ui_Form() self.ui = Ui_Form()
self.ui.setupUi(self) self.ui.setupUi(self)
self.scene = self.ui.graphicsView.scene() self.scene = self.ui.graphicsView.scene()
self.ui.histogram.setLevelMode(levelMode)
self.ignoreTimeLine = False self.ignoreTimeLine = False
@ -151,13 +159,15 @@ class ImageView(QtGui.QWidget):
self.normRoi.setZValue(20) self.normRoi.setZValue(20)
self.view.addItem(self.normRoi) self.view.addItem(self.normRoi)
self.normRoi.hide() self.normRoi.hide()
self.roiCurve = self.ui.roiPlot.plot() self.roiCurves = []
self.timeLine = InfiniteLine(0, movable=True) self.timeLine = InfiniteLine(0, movable=True, markers=[('^', 0), ('v', 1)])
self.timeLine.setPen((255, 255, 0, 200)) self.timeLine.setPen((255, 255, 0, 200))
self.timeLine.setZValue(1) self.timeLine.setZValue(1)
self.ui.roiPlot.addItem(self.timeLine) self.ui.roiPlot.addItem(self.timeLine)
self.ui.splitter.setSizes([self.height()-35, 35]) self.ui.splitter.setSizes([self.height()-35, 35])
self.ui.roiPlot.hideAxis('left') self.ui.roiPlot.hideAxis('left')
self.frameTicks = VTickGroup(yrange=[0.8, 1], pen=0.4)
self.ui.roiPlot.addItem(self.frameTicks, ignoreBounds=True)
self.keysPressed = {} self.keysPressed = {}
self.playTimer = QtCore.QTimer() self.playTimer = QtCore.QTimer()
@ -200,7 +210,7 @@ class ImageView(QtGui.QWidget):
self.roiClicked() ## initialize roi plot to correct shape / visibility self.roiClicked() ## initialize roi plot to correct shape / visibility
def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True): def setImage(self, img, autoRange=True, autoLevels=True, levels=None, axes=None, xvals=None, pos=None, scale=None, transform=None, autoHistogramRange=True, levelMode=None):
""" """
Set the image to be displayed in the widget. Set the image to be displayed in the widget.
@ -208,8 +218,9 @@ class ImageView(QtGui.QWidget):
**Arguments:** **Arguments:**
img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and img (numpy array) the image to be displayed. See :func:`ImageItem.setImage` and
*notes* below. *notes* below.
xvals (numpy array) 1D array of z-axis values corresponding to the third axis xvals (numpy array) 1D array of z-axis values corresponding to the first axis
in a 3D image. For video, this array should contain the time of each frame. in a 3D image. For video, this array should contain the time of each
frame.
autoRange (bool) whether to scale/pan the view to fit the image. autoRange (bool) whether to scale/pan the view to fit the image.
autoLevels (bool) whether to update the white/black levels to fit the image. autoLevels (bool) whether to update the white/black levels to fit the image.
levels (min, max); the white and black level values to use. levels (min, max); the white and black level values to use.
@ -224,6 +235,10 @@ class ImageView(QtGui.QWidget):
and *scale*. and *scale*.
autoHistogramRange If True, the histogram y-range is automatically scaled to fit the autoHistogramRange If True, the histogram y-range is automatically scaled to fit the
image data. image data.
levelMode If specified, this sets the user interaction mode for setting image
levels. Options are 'mono', which provides a single level control for
all image channels, and 'rgb' or 'rgba', which provide individual
controls for each channel.
================== =========================================================================== ================== ===========================================================================
**Notes:** **Notes:**
@ -252,6 +267,8 @@ class ImageView(QtGui.QWidget):
self.image = img self.image = img
self.imageDisp = None self.imageDisp = None
if levelMode is not None:
self.ui.histogram.setLevelMode(levelMode)
profiler() profiler()
@ -310,10 +327,9 @@ class ImageView(QtGui.QWidget):
profiler() profiler()
if self.axes['t'] is not None: if self.axes['t'] is not None:
#self.ui.roiPlot.show()
self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max()) self.ui.roiPlot.setXRange(self.tVals.min(), self.tVals.max())
self.frameTicks.setXVals(self.tVals)
self.timeLine.setValue(0) self.timeLine.setValue(0)
#self.ui.roiPlot.setMouseEnabled(False, False)
if len(self.tVals) > 1: if len(self.tVals) > 1:
start = self.tVals.min() start = self.tVals.min()
stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02 stop = self.tVals.max() + abs(self.tVals[-1] - self.tVals[0]) * 0.02
@ -325,8 +341,7 @@ class ImageView(QtGui.QWidget):
stop = 1 stop = 1
for s in [self.timeLine, self.normRgn]: for s in [self.timeLine, self.normRgn]:
s.setBounds([start, stop]) s.setBounds([start, stop])
#else:
#self.ui.roiPlot.hide()
profiler() profiler()
self.imageItem.resetTransform() self.imageItem.resetTransform()
@ -364,11 +379,14 @@ class ImageView(QtGui.QWidget):
def autoLevels(self): def autoLevels(self):
"""Set the min/max intensity levels automatically to match the image data.""" """Set the min/max intensity levels automatically to match the image data."""
self.setLevels(self.levelMin, self.levelMax) self.setLevels(rgba=self._imageLevels)
def setLevels(self, min, max): def setLevels(self, *args, **kwds):
"""Set the min/max (bright and dark) levels.""" """Set the min/max (bright and dark) levels.
self.ui.histogram.setLevels(min, max)
See :func:`HistogramLUTItem.setLevels <pyqtgraph.HistogramLUTItem.setLevels>`.
"""
self.ui.histogram.setLevels(*args, **kwds)
def autoRange(self): def autoRange(self):
"""Auto scale and pan the view around the image such that the image fills the view.""" """Auto scale and pan the view around the image such that the image fills the view."""
@ -377,12 +395,13 @@ class ImageView(QtGui.QWidget):
def getProcessedImage(self): def getProcessedImage(self):
"""Returns the image data after it has been processed by any normalization options in use. """Returns the image data after it has been processed by any normalization options in use.
This method also sets the attributes self.levelMin and self.levelMax """
to indicate the range of data in the image."""
if self.imageDisp is None: if self.imageDisp is None:
image = self.normalize(self.image) image = self.normalize(self.image)
self.imageDisp = image self.imageDisp = image
self.levelMin, self.levelMax = list(map(float, self.quickMinMax(self.imageDisp))) self._imageLevels = self.quickMinMax(self.imageDisp)
self.levelMin = min([level[0] for level in self._imageLevels])
self.levelMax = max([level[1] for level in self._imageLevels])
return self.imageDisp return self.imageDisp
@ -469,7 +488,7 @@ class ImageView(QtGui.QWidget):
n = int(self.playRate * dt) n = int(self.playRate * dt)
if n != 0: if n != 0:
self.lastPlayTime += (float(n)/self.playRate) self.lastPlayTime += (float(n)/self.playRate)
if self.currentIndex+n > self.image.shape[0]: if self.currentIndex+n > self.image.shape[self.axes['t']]:
self.play(0) self.play(0)
self.jumpFrames(n) self.jumpFrames(n)
@ -527,13 +546,15 @@ class ImageView(QtGui.QWidget):
#self.ui.roiPlot.show() #self.ui.roiPlot.show()
self.ui.roiPlot.setMouseEnabled(True, True) self.ui.roiPlot.setMouseEnabled(True, True)
self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4]) self.ui.splitter.setSizes([self.height()*0.6, self.height()*0.4])
self.roiCurve.show() for c in self.roiCurves:
c.show()
self.roiChanged() self.roiChanged()
self.ui.roiPlot.showAxis('left') self.ui.roiPlot.showAxis('left')
else: else:
self.roi.hide() self.roi.hide()
self.ui.roiPlot.setMouseEnabled(False, False) self.ui.roiPlot.setMouseEnabled(False, False)
self.roiCurve.hide() for c in self.roiCurves:
c.hide()
self.ui.roiPlot.hideAxis('left') self.ui.roiPlot.hideAxis('left')
if self.hasTimeAxis(): if self.hasTimeAxis():
@ -557,36 +578,65 @@ class ImageView(QtGui.QWidget):
return return
image = self.getProcessedImage() image = self.getProcessedImage()
if image.ndim == 2:
axes = (0, 1) # Extract image data from ROI
elif image.ndim == 3: axes = (self.axes['x'], self.axes['y'])
axes = (1, 2)
else:
return
data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True) data, coords = self.roi.getArrayRegion(image.view(np.ndarray), self.imageItem, axes, returnMappedCoords=True)
if data is not None: if data is None:
while data.ndim > 1: return
data = data.mean(axis=1)
if image.ndim == 3: # Convert extracted data into 1D plot data
self.roiCurve.setData(y=data, x=self.tVals) if self.axes['t'] is None:
# Average across y-axis of ROI
data = data.mean(axis=axes[1])
coords = coords[:,:,0] - coords[:,0:1,0]
xvals = (coords**2).sum(axis=0) ** 0.5
else:
# Average data within entire ROI for each frame
data = data.mean(axis=max(axes)).mean(axis=min(axes))
xvals = self.tVals
# Handle multi-channel data
if data.ndim == 1:
plots = [(xvals, data, 'w')]
if data.ndim == 2:
if data.shape[1] == 1:
colors = 'w'
else: else:
while coords.ndim > 2: colors = 'rgbw'
coords = coords[:,:,0] plots = []
coords = coords - coords[:,0,np.newaxis] for i in range(data.shape[1]):
xvals = (coords**2).sum(axis=0) ** 0.5 d = data[:,i]
self.roiCurve.setData(y=data, x=xvals) plots.append((xvals, d, colors[i]))
# Update plot line(s)
while len(plots) < len(self.roiCurves):
c = self.roiCurves.pop()
c.scene().removeItem(c)
while len(plots) > len(self.roiCurves):
self.roiCurves.append(self.ui.roiPlot.plot())
for i in range(len(plots)):
x, y, p = plots[i]
self.roiCurves[i].setData(x, y, pen=p)
def quickMinMax(self, data): def quickMinMax(self, data):
""" """
Estimate the min/max values of *data* by subsampling. Estimate the min/max values of *data* by subsampling.
Returns [(min, max), ...] with one item per channel
""" """
while data.size > 1e6: while data.size > 1e6:
ax = np.argmax(data.shape) ax = np.argmax(data.shape)
sl = [slice(None)] * data.ndim sl = [slice(None)] * data.ndim
sl[ax] = slice(None, None, 2) sl[ax] = slice(None, None, 2)
data = data[sl] data = data[sl]
return nanmin(data), nanmax(data)
cax = self.axes['c']
if cax is None:
return [(float(nanmin(data)), float(nanmax(data)))]
else:
return [(float(nanmin(data.take(i, axis=cax))),
float(nanmax(data.take(i, axis=cax)))) for i in range(data.shape[-1])]
def normalize(self, image): def normalize(self, image):
""" """

View File

@ -748,7 +748,6 @@ class MetaArray(object):
else: else:
fd.seek(0) fd.seek(0)
meta = MetaArray._readMeta(fd) meta = MetaArray._readMeta(fd)
if not kwargs.get("readAllData", True): if not kwargs.get("readAllData", True):
self._data = np.empty(meta['shape'], dtype=meta['type']) self._data = np.empty(meta['shape'], dtype=meta['type'])
if 'version' in meta: if 'version' in meta:
@ -1031,6 +1030,7 @@ class MetaArray(object):
"""Write this object to a file. The object can be restored by calling MetaArray(file=fileName) """Write this object to a file. The object can be restored by calling MetaArray(file=fileName)
opts: opts:
appendAxis: the name (or index) of the appendable axis. Allows the array to grow. appendAxis: the name (or index) of the appendable axis. Allows the array to grow.
appendKeys: a list of keys (other than "values") for metadata to append to on the appendable axis.
compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc. compression: None, 'gzip' (good compression), 'lzf' (fast compression), etc.
chunks: bool or tuple specifying chunk shape chunks: bool or tuple specifying chunk shape
""" """
@ -1096,7 +1096,6 @@ class MetaArray(object):
'chunks': None, 'chunks': None,
'compression': None 'compression': None
} }
## set maximum shape to allow expansion along appendAxis ## set maximum shape to allow expansion along appendAxis
append = False append = False
@ -1125,14 +1124,19 @@ class MetaArray(object):
data[tuple(sl)] = self.view(np.ndarray) data[tuple(sl)] = self.view(np.ndarray)
## add axis values if they are present. ## add axis values if they are present.
axKeys = ["values"]
axKeys.extend(opts.get("appendKeys", []))
axInfo = f['info'][str(ax)] axInfo = f['info'][str(ax)]
if 'values' in axInfo: for key in axKeys:
v = axInfo['values'] if key in axInfo:
v2 = self._info[ax]['values'] v = axInfo[key]
shape = list(v.shape) v2 = self._info[ax][key]
shape[0] += v2.shape[0] shape = list(v.shape)
v.resize(shape) shape[0] += v2.shape[0]
v[-v2.shape[0]:] = v2 v.resize(shape)
v[-v2.shape[0]:] = v2
else:
raise TypeError('Cannot append to axis info key "%s"; this key is not present in the target file.' % key)
f.close() f.close()
else: else:
f = h5py.File(fileName, 'w') f = h5py.File(fileName, 'w')

View File

@ -21,4 +21,4 @@ TODO:
from .processes import * from .processes import *
from .parallelizer import Parallelize, CanceledError from .parallelizer import Parallelize, CanceledError
from .remoteproxy import proxy from .remoteproxy import proxy, ClosedError, NoResultError

View File

@ -13,16 +13,31 @@ if __name__ == '__main__':
#print "key:", ' '.join([str(ord(x)) for x in authkey]) #print "key:", ' '.join([str(ord(x)) for x in authkey])
path = opts.pop('path', None) path = opts.pop('path', None)
if path is not None: if path is not None:
## rewrite sys.path without assigning a new object--no idea who already has a reference to the existing list. if isinstance(path, str):
while len(sys.path) > 0: # if string, just insert this into the path
sys.path.pop() sys.path.insert(0, path)
sys.path.extend(path) else:
# if list, then replace the entire sys.path
## modify sys.path in place--no idea who already has a reference to the existing list.
while len(sys.path) > 0:
sys.path.pop()
sys.path.extend(path)
pyqtapis = opts.pop('pyqtapis', None)
if pyqtapis is not None:
import sip
for k,v in pyqtapis.items():
sip.setapi(k, v)
if opts.pop('pyside', False): if opts.pop('pyside', False):
import PySide import PySide
targetStr = opts.pop('targetStr') targetStr = opts.pop('targetStr')
target = pickle.loads(targetStr) ## unpickling the target should import everything we need try:
target = pickle.loads(targetStr) ## unpickling the target should import everything we need
except:
print("Current sys.path:", sys.path)
raise
target(**opts) ## Send all other options to the target function target(**opts) ## Send all other options to the target function
sys.exit(0) sys.exit(0)

View File

@ -101,7 +101,10 @@ class Parallelize(object):
else: ## parent else: ## parent
if self.showProgress: if self.showProgress:
self.progressDlg.__exit__(None, None, None) try:
self.progressDlg.__exit__(None, None, None)
except Exception:
pass
def runSerial(self): def runSerial(self):
if self.showProgress: if self.showProgress:

View File

@ -1,4 +1,4 @@
import subprocess, atexit, os, sys, time, random, socket, signal import subprocess, atexit, os, sys, time, random, socket, signal, inspect
import multiprocessing.connection import multiprocessing.connection
try: try:
import cPickle as pickle import cPickle as pickle
@ -39,7 +39,7 @@ class Process(RemoteEventHandler):
""" """
_process_count = 1 # just used for assigning colors to each process for debugging _process_count = 1 # just used for assigning colors to each process for debugging
def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None): def __init__(self, name=None, target=None, executable=None, copySysPath=True, debug=False, timeout=20, wrapStdout=None, pyqtapis=None):
""" """
============== ============================================================= ============== =============================================================
**Arguments:** **Arguments:**
@ -47,10 +47,12 @@ class Process(RemoteEventHandler):
from the remote process. from the remote process.
target Optional function to call after starting remote process. target Optional function to call after starting remote process.
By default, this is startEventLoop(), which causes the remote By default, this is startEventLoop(), which causes the remote
process to process requests from the parent process until it process to handle requests from the parent process until it
is asked to quit. If you wish to specify a different target, is asked to quit. If you wish to specify a different target,
it must be picklable (bound methods are not). it must be picklable (bound methods are not).
copySysPath If True, copy the contents of sys.path to the remote process copySysPath If True, copy the contents of sys.path to the remote process.
If False, then only the path required to import pyqtgraph is
added.
debug If True, print detailed information about communication debug If True, print detailed information about communication
with the child process. with the child process.
wrapStdout If True (default on windows) then stdout and stderr from the wrapStdout If True (default on windows) then stdout and stderr from the
@ -59,6 +61,8 @@ class Process(RemoteEventHandler):
for a python bug: http://bugs.python.org/issue3905 for a python bug: http://bugs.python.org/issue3905
but has the side effect that child output is significantly but has the side effect that child output is significantly
delayed relative to the parent output. delayed relative to the parent output.
pyqtapis Optional dictionary of PyQt API version numbers to set before
importing pyqtgraph in the remote process.
============== ============================================================= ============== =============================================================
""" """
if target is None: if target is None:
@ -82,7 +86,13 @@ class Process(RemoteEventHandler):
port = l.address[1] port = l.address[1]
## start remote process, instruct it to run target function ## start remote process, instruct it to run target function
sysPath = sys.path if copySysPath else None if copySysPath:
sysPath = sys.path
else:
# what path do we need to make target importable?
mod = inspect.getmodule(target)
modroot = sys.modules[mod.__name__.split('.')[0]]
sysPath = os.path.abspath(os.path.join(os.path.dirname(modroot.__file__), '..'))
bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py')) bootstrap = os.path.abspath(os.path.join(os.path.dirname(__file__), 'bootstrap.py'))
self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap)) self.debugMsg('Starting child process (%s %s)' % (executable, bootstrap))
@ -122,7 +132,8 @@ class Process(RemoteEventHandler):
targetStr=targetStr, targetStr=targetStr,
path=sysPath, path=sysPath,
pyside=USE_PYSIDE, pyside=USE_PYSIDE,
debug=procDebug debug=procDebug,
pyqtapis=pyqtapis,
) )
pickle.dump(data, self.proc.stdin) pickle.dump(data, self.proc.stdin)
self.proc.stdin.close() self.proc.stdin.close()
@ -182,7 +193,8 @@ def startEventLoop(name, port, authkey, ppid, debug=False):
HANDLER.processRequests() # exception raised when the loop should exit HANDLER.processRequests() # exception raised when the loop should exit
time.sleep(0.01) time.sleep(0.01)
except ClosedError: except ClosedError:
break HANDLER.debugMsg('Exiting server loop.')
sys.exit(0)
class ForkedProcess(RemoteEventHandler): class ForkedProcess(RemoteEventHandler):
@ -321,9 +333,14 @@ class ForkedProcess(RemoteEventHandler):
#os.kill(pid, 9) #os.kill(pid, 9)
try: try:
self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation. self.close(callSync='sync', timeout=timeout, noCleanup=True) ## ask the child process to exit and require that it return a confirmation.
os.waitpid(self.childPid, 0)
except IOError: ## probably remote process has already quit except IOError: ## probably remote process has already quit
pass pass
try:
os.waitpid(self.childPid, 0)
except OSError: ## probably remote process has already quit
pass
self.hasJoined = True self.hasJoined = True
def kill(self): def kill(self):
@ -457,21 +474,20 @@ class FileForwarder(threading.Thread):
self.start() self.start()
def run(self): def run(self):
if self.output == 'stdout': if self.output == 'stdout' and self.color is not False:
while True: while True:
line = self.input.readline() line = self.input.readline()
with self.lock: with self.lock:
cprint.cout(self.color, line, -1) cprint.cout(self.color, line, -1)
elif self.output == 'stderr': elif self.output == 'stderr' and self.color is not False:
while True: while True:
line = self.input.readline() line = self.input.readline()
with self.lock: with self.lock:
cprint.cerr(self.color, line, -1) cprint.cerr(self.color, line, -1)
else: else:
if isinstance(self.output, str):
self.output = getattr(sys, self.output)
while True: while True:
line = self.input.readline() line = self.input.readline()
with self.lock: with self.lock:
self.output.write(line) self.output.write(line)

View File

@ -419,7 +419,7 @@ class RemoteEventHandler(object):
if opts is None: if opts is None:
opts = {} opts = {}
assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async"' assert callSync in ['off', 'sync', 'async'], 'callSync must be one of "off", "sync", or "async" (got %r)' % callSync
if reqId is None: if reqId is None:
if callSync != 'off': ## requested return value; use the next available request ID if callSync != 'off': ## requested return value; use the next available request ID
reqId = self.nextRequestId reqId = self.nextRequestId
@ -466,10 +466,7 @@ class RemoteEventHandler(object):
return req return req
if callSync == 'sync': if callSync == 'sync':
try: return req.result()
return req.result()
except NoResultError:
return req
def close(self, callSync='off', noCleanup=False, **kwds): def close(self, callSync='off', noCleanup=False, **kwds):
try: try:
@ -572,6 +569,10 @@ class RemoteEventHandler(object):
self.proxies[ref] = proxy._proxyId self.proxies[ref] = proxy._proxyId
def deleteProxy(self, ref): def deleteProxy(self, ref):
if self.send is None:
# this can happen during shutdown
return
with self.proxyLock: with self.proxyLock:
proxyId = self.proxies.pop(ref) proxyId = self.proxies.pop(ref)

View File

@ -450,6 +450,7 @@ class GLViewWidget(QtOpenGL.QGLWidget):
glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0) glfbo.glFramebufferTexture2D(glfbo.GL_FRAMEBUFFER, glfbo.GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex, 0)
self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region self.paintGL(region=(x, h-y-h2, w2, h2), viewport=(0, 0, w2, h2)) # only render sub-region
glBindTexture(GL_TEXTURE_2D, tex) # fixes issue #366
## read texture back to array ## read texture back to array
data = glGetTexImage(GL_TEXTURE_2D, 0, format, type) data = glGetTexImage(GL_TEXTURE_2D, 0, format, type)

View File

@ -20,108 +20,112 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE. # OTHER DEALINGS IN THE SOFTWARE.
from UserDict import DictMixin import sys
if sys.version[0] > '2':
from collections import OrderedDict
else:
from UserDict import DictMixin
class OrderedDict(dict, DictMixin): class OrderedDict(dict, DictMixin):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
if len(args) > 1: if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args)) raise TypeError('expected at most 1 arguments, got %d' % len(args))
try: try:
self.__end self.__end
except AttributeError: except AttributeError:
self.clear() self.clear()
self.update(*args, **kwds) self.update(*args, **kwds)
def clear(self): def clear(self):
self.__end = end = [] self.__end = end = []
end += [None, end, end] # sentinel node for doubly linked list end += [None, end, end] # sentinel node for doubly linked list
self.__map = {} # key --> [key, prev, next] self.__map = {} # key --> [key, prev, next]
dict.clear(self) dict.clear(self)
def __setitem__(self, key, value): def __setitem__(self, key, value):
if key not in self: if key not in self:
end = self.__end
curr = end[1]
curr[2] = end[1] = self.__map[key] = [key, curr, end]
dict.__setitem__(self, key, value)
def __delitem__(self, key):
dict.__delitem__(self, key)
key, prev, next = self.__map.pop(key)
prev[2] = next
next[1] = prev
def __iter__(self):
end = self.__end
curr = end[2]
while curr is not end:
yield curr[0]
curr = curr[2]
def __reversed__(self):
end = self.__end end = self.__end
curr = end[1] curr = end[1]
curr[2] = end[1] = self.__map[key] = [key, curr, end] while curr is not end:
dict.__setitem__(self, key, value) yield curr[0]
curr = curr[1]
def __delitem__(self, key): def popitem(self, last=True):
dict.__delitem__(self, key) if not self:
key, prev, next = self.__map.pop(key) raise KeyError('dictionary is empty')
prev[2] = next if last:
next[1] = prev key = reversed(self).next()
else:
key = iter(self).next()
value = self.pop(key)
return key, value
def __iter__(self): def __reduce__(self):
end = self.__end items = [[k, self[k]] for k in self]
curr = end[2] tmp = self.__map, self.__end
while curr is not end: del self.__map, self.__end
yield curr[0] inst_dict = vars(self).copy()
curr = curr[2] self.__map, self.__end = tmp
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def __reversed__(self): def keys(self):
end = self.__end return list(self)
curr = end[1]
while curr is not end:
yield curr[0]
curr = curr[1]
def popitem(self, last=True): setdefault = DictMixin.setdefault
if not self: update = DictMixin.update
raise KeyError('dictionary is empty') pop = DictMixin.pop
if last: values = DictMixin.values
key = reversed(self).next() items = DictMixin.items
else: iterkeys = DictMixin.iterkeys
key = iter(self).next() itervalues = DictMixin.itervalues
value = self.pop(key) iteritems = DictMixin.iteritems
return key, value
def __reduce__(self): def __repr__(self):
items = [[k, self[k]] for k in self] if not self:
tmp = self.__map, self.__end return '%s()' % (self.__class__.__name__,)
del self.__map, self.__end return '%s(%r)' % (self.__class__.__name__, self.items())
inst_dict = vars(self).copy()
self.__map, self.__end = tmp
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def keys(self): def copy(self):
return list(self) return self.__class__(self)
setdefault = DictMixin.setdefault @classmethod
update = DictMixin.update def fromkeys(cls, iterable, value=None):
pop = DictMixin.pop d = cls()
values = DictMixin.values for key in iterable:
items = DictMixin.items d[key] = value
iterkeys = DictMixin.iterkeys return d
itervalues = DictMixin.itervalues
iteritems = DictMixin.iteritems
def __repr__(self): def __eq__(self, other):
if not self: if isinstance(other, OrderedDict):
return '%s()' % (self.__class__.__name__,) if len(self) != len(other):
return '%s(%r)' % (self.__class__.__name__, self.items())
def copy(self):
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
if isinstance(other, OrderedDict):
if len(self) != len(other):
return False
for p, q in zip(self.items(), other.items()):
if p != q:
return False return False
return True for p, q in zip(self.items(), other.items()):
return dict.__eq__(self, other) if p != q:
return False
return True
return dict.__eq__(self, other)
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other

View File

@ -162,7 +162,11 @@ class Parameter(QtCore.QObject):
'title': None, '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.
} }
value = opts.get('value', None)
name = opts.get('name', None)
self.opts.update(opts) self.opts.update(opts)
self.opts['value'] = None # will be set later.
self.opts['name'] = None
self.childs = [] self.childs = []
self.names = {} ## map name:child self.names = {} ## map name:child
@ -172,17 +176,19 @@ class Parameter(QtCore.QObject):
self.blockTreeChangeEmit = 0 self.blockTreeChangeEmit = 0
#self.monitoringChildren = False ## prevent calling monitorChildren more than once #self.monitoringChildren = False ## prevent calling monitorChildren more than once
if 'value' not in self.opts: if not isinstance(name, basestring):
self.opts['value'] = None
if 'name' not in self.opts or not isinstance(self.opts['name'], basestring):
raise Exception("Parameter must have a string name specified in opts.") raise Exception("Parameter must have a string name specified in opts.")
self.setName(opts['name']) self.setName(name)
self.addChildren(self.opts.get('children', [])) self.addChildren(self.opts.get('children', []))
if 'value' in self.opts and 'default' not in self.opts: self.opts['value'] = None
self.opts['default'] = self.opts['value'] if value is not None:
self.setValue(value)
if 'default' not in self.opts:
self.opts['default'] = None
self.setDefault(self.opts['value'])
## Connect all state changed signals to the general sigStateChanged ## Connect all state changed signals to the general sigStateChanged
self.sigValueChanged.connect(lambda param, data: self.emitStateChanged('value', data)) self.sigValueChanged.connect(lambda param, data: self.emitStateChanged('value', data))
@ -647,18 +653,19 @@ class Parameter(QtCore.QObject):
"""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 Added in version 0.9.9. Earlier versions used the 'param' method, which is still
implemented for backward compatibility.""" implemented for backward compatibility.
"""
try: try:
param = self.names[names[0]] param = self.names[names[0]]
except KeyError: except KeyError:
raise Exception("Parameter %s has no child named %s" % (self.name(), names[0])) raise KeyError("Parameter %s has no child named %s" % (self.name(), names[0]))
if len(names) > 1: if len(names) > 1:
return param.param(*names[1:]) return param.child(*names[1:])
else: else:
return param return param
def param(self, *names): def param(self, *names):
# for backward compatibility. # for backward compatibility.
return self.child(*names) return self.child(*names)

View File

@ -1,5 +1,7 @@
from ..pgcollections import OrderedDict from ..pgcollections import OrderedDict
import numpy as np import numpy as np
import copy
class SystemSolver(object): class SystemSolver(object):
""" """
@ -73,6 +75,12 @@ class SystemSolver(object):
self.__dict__['_currentGets'] = set() self.__dict__['_currentGets'] = set()
self.reset() self.reset()
def copy(self):
sys = type(self)()
sys.__dict__['_vars'] = copy.deepcopy(self.__dict__['_vars'])
sys.__dict__['_currentGets'] = copy.deepcopy(self.__dict__['_currentGets'])
return sys
def reset(self): def reset(self):
""" """
Reset all variables in the solver to their default state. Reset all variables in the solver to their default state.
@ -167,6 +175,16 @@ class SystemSolver(object):
elif constraint == 'fixed': elif constraint == 'fixed':
if 'f' not in var[3]: if 'f' not in var[3]:
raise TypeError("Fixed constraints not allowed for '%s'" % name) raise TypeError("Fixed constraints not allowed for '%s'" % name)
# This is nice, but not reliable because sometimes there is 1 DOF but we set 2
# values simultaneously.
# if var[2] is None:
# try:
# self.get(name)
# # has already been computed by the system; adding a fixed constraint
# # would overspecify the system.
# raise ValueError("Cannot fix parameter '%s'; system would become overconstrained." % name)
# except RuntimeError:
# pass
var[2] = constraint var[2] = constraint
elif isinstance(constraint, tuple): elif isinstance(constraint, tuple):
if 'r' not in var[3]: if 'r' not in var[3]:
@ -177,7 +195,7 @@ class SystemSolver(object):
raise TypeError("constraint must be None, True, 'fixed', or tuple. (got %s)" % constraint) raise TypeError("constraint must be None, True, 'fixed', or tuple. (got %s)" % constraint)
# type checking / massaging # type checking / massaging
if var[1] is np.ndarray: if var[1] is np.ndarray and value is not None:
value = np.array(value, dtype=float) value = np.array(value, dtype=float)
elif var[1] in (int, float, tuple) and value is not None: elif var[1] in (int, float, tuple) and value is not None:
value = var[1](value) value = var[1](value)
@ -185,9 +203,9 @@ class SystemSolver(object):
# constraint checks # constraint checks
if constraint is True and not self.check_constraint(name, value): if constraint is True and not self.check_constraint(name, value):
raise ValueError("Setting %s = %s violates constraint %s" % (name, value, var[2])) raise ValueError("Setting %s = %s violates constraint %s" % (name, value, var[2]))
# invalidate other dependent values # invalidate other dependent values
if var[0] is not None: if var[0] is not None or value is None:
# todo: we can make this more clever..(and might need to) # todo: we can make this more clever..(and might need to)
# we just know that a value of None cannot have dependencies # we just know that a value of None cannot have dependencies
# (because if anyone else had asked for this value, it wouldn't be # (because if anyone else had asked for this value, it wouldn't be
@ -237,6 +255,31 @@ class SystemSolver(object):
for k in self._vars: for k in self._vars:
getattr(self, k) getattr(self, k)
def checkOverconstraint(self):
"""Check whether the system is overconstrained. If so, return the name of
the first overconstrained parameter.
Overconstraints occur when any fixed parameter can be successfully computed by the system.
(Ideally, all parameters are either fixed by the user or constrained by the
system, but never both).
"""
for k,v in self._vars.items():
if v[2] == 'fixed' and 'n' in v[3]:
oldval = v[:]
self.set(k, None, None)
try:
self.get(k)
return k
except RuntimeError:
pass
finally:
self._vars[k] = oldval
return False
def __repr__(self): def __repr__(self):
state = OrderedDict() state = OrderedDict()
for name, var in self._vars.items(): for name, var in self._vars.items():
@ -378,4 +421,4 @@ if __name__ == '__main__':
camera.solve() camera.solve()
print(camera.saveState()) print(camera.saveState())

View File

@ -326,25 +326,12 @@ class SimpleParameter(Parameter):
'int': int, 'int': int,
'float': float, 'float': float,
'bool': bool, 'bool': bool,
'str': self._interpStr, 'str': asUnicode,
'color': self._interpColor, 'color': self._interpColor,
'colormap': self._interpColormap, 'colormap': self._interpColormap,
}[self.opts['type']] }[self.opts['type']]
return fn(v) return fn(v)
def _interpStr(self, v):
if sys.version[0] == '2':
if isinstance(v, QtCore.QString):
v = unicode(v)
elif not isinstance(v, basestring):
raise TypeError("Cannot set str parmeter from object %r" % v)
else:
if isinstance(v, QtCore.QString):
v = str(v)
elif not isinstance(v, str):
raise TypeError("Cannot set str parmeter from object %r" % v)
return v
def _interpColor(self, v): def _interpColor(self, v):
return fn.mkColor(v) return fn.mkColor(v)
@ -413,6 +400,7 @@ class GroupParameterItem(ParameterItem):
else: else:
for c in [0,1]: for c in [0,1]:
self.setBackground(c, QtGui.QBrush(QtGui.QColor(220,220,220))) self.setBackground(c, QtGui.QBrush(QtGui.QColor(220,220,220)))
self.setForeground(c, QtGui.QBrush(QtGui.QColor(50,50,50)))
font = self.font(c) font = self.font(c)
font.setBold(True) font.setBold(True)
#font.setPointSize(font.pointSize()+1) #font.setPointSize(font.pointSize()+1)
@ -475,12 +463,15 @@ class GroupParameter(Parameter):
instead of a button. instead of a button.
""" """
itemClass = GroupParameterItem itemClass = GroupParameterItem
sigAddNew = QtCore.Signal(object, object) # self, type
def addNew(self, typ=None): def addNew(self, typ=None):
""" """
This method is called when the user has requested to add a new item to the group. This method is called when the user has requested to add a new item to the group.
By default, it emits ``sigAddNew(self, typ)``.
""" """
raise Exception("Must override this function in subclass.") self.sigAddNew.emit(self, typ)
def setAddList(self, vals): def setAddList(self, vals):
"""Change the list of options available for the user to add to the group.""" """Change the list of options available for the user to add to the group."""
@ -618,6 +609,7 @@ class ActionParameterItem(ParameterItem):
ParameterItem.__init__(self, param, depth) ParameterItem.__init__(self, param, depth)
self.layoutWidget = QtGui.QWidget() self.layoutWidget = QtGui.QWidget()
self.layout = QtGui.QHBoxLayout() self.layout = QtGui.QHBoxLayout()
self.layout.setContentsMargins(0, 0, 0, 0)
self.layoutWidget.setLayout(self.layout) self.layoutWidget.setLayout(self.layout)
self.button = QtGui.QPushButton(param.name()) self.button = QtGui.QPushButton(param.name())
#self.layout.addSpacing(100) #self.layout.addSpacing(100)

View File

@ -1,7 +1,19 @@
# ~*~ coding: utf8 ~*~
import sys
import pytest
from pyqtgraph.Qt import QtGui, QtCore
import pyqtgraph.parametertree as pt import pyqtgraph.parametertree as pt
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph.python2_3 import asUnicode
from pyqtgraph.functions import eq
import numpy as np
app = pg.mkQApp() app = pg.mkQApp()
def _getWidget(param):
return list(param.items.keys())[0].widget
def test_opts(): def test_opts():
paramSpec = [ paramSpec = [
dict(name='bool', type='bool', readonly=True), dict(name='bool', type='bool', readonly=True),
@ -12,7 +24,111 @@ def test_opts():
tree = pt.ParameterTree() tree = pt.ParameterTree()
tree.setParameters(param) tree.setParameters(param)
assert list(param.param('bool').items.keys())[0].widget.isEnabled() is False assert _getWidget(param.param('bool')).isEnabled() is False
assert list(param.param('color').items.keys())[0].widget.isEnabled() is False assert _getWidget(param.param('bool')).isEnabled() is False
def test_types():
paramSpec = [
dict(name='float', type='float'),
dict(name='int', type='int'),
dict(name='str', type='str'),
dict(name='list', type='list', values=['x','y','z']),
dict(name='dict', type='list', values={'x':1, 'y':3, 'z':7}),
dict(name='bool', type='bool'),
dict(name='color', type='color'),
]
param = pt.Parameter.create(name='params', type='group', children=paramSpec)
tree = pt.ParameterTree()
tree.setParameters(param)
all_objs = {
'int0': 0, 'int':7, 'float': -0.35, 'bigfloat': 1e129, 'npfloat': np.float(5),
'npint': np.int(5),'npinf': np.inf, 'npnan': np.nan, 'bool': True,
'complex': 5+3j, 'str': 'xxx', 'unicode': asUnicode('µ'),
'list': [1,2,3], 'dict': {'1': 2}, 'color': pg.mkColor('k'),
'brush': pg.mkBrush('k'), 'pen': pg.mkPen('k'), 'none': None
}
if hasattr(QtCore, 'QString'):
all_objs['qstring'] = QtCore.QString('xxxµ')
# float
types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'npinf', 'npnan', 'bool']
check_param_types(param.child('float'), float, float, 0.0, all_objs, types)
# int
types = ['int0', 'int', 'float', 'bigfloat', 'npfloat', 'npint', 'bool']
inttyps = int if sys.version[0] >= '3' else (int, long)
check_param_types(param.child('int'), inttyps, int, 0, all_objs, types)
# str (should be able to make a string out of any type)
types = all_objs.keys()
strtyp = str if sys.version[0] >= '3' else unicode
check_param_types(param.child('str'), strtyp, asUnicode, '', all_objs, types)
# bool (should be able to make a boolean out of any type?)
types = all_objs.keys()
check_param_types(param.child('bool'), bool, bool, False, all_objs, types)
# color
types = ['color', 'int0', 'int', 'float', 'npfloat', 'npint', 'list']
init = QtGui.QColor(128, 128, 128, 255)
check_param_types(param.child('color'), QtGui.QColor, pg.mkColor, init, all_objs, types)
def check_param_types(param, types, map_func, init, objs, keys):
"""Check that parameter setValue() accepts or rejects the correct types and
that value() returns the correct type.
Parameters
----------
param : Parameter instance
types : type or tuple of types
The allowed types for this parameter to return from value().
map_func : function
Converts an input value to the expected output value.
init : object
The expected initial value of the parameter
objs : dict
Contains a variety of objects that will be tested as arguments to
param.setValue().
keys : list
The list of keys indicating the valid objects in *objs*. When
param.setValue() is teasted with each value from *objs*, we expect
an exception to be raised if the associated key is not in *keys*.
"""
val = param.value()
if not isinstance(types, tuple):
types = (types,)
assert val == init and type(val) in types
# test valid input types
good_inputs = [objs[k] for k in keys if k in objs]
good_outputs = map(map_func, good_inputs)
for x,y in zip(good_inputs, good_outputs):
param.setValue(x)
val = param.value()
if not (eq(val, y) and type(val) in types):
raise Exception("Setting parameter %s with value %r should have resulted in %r (types: %r), "
"but resulted in %r (type: %r) instead." % (param, x, y, types, val, type(val)))
# test invalid input types
for k,v in objs.items():
if k in keys:
continue
try:
param.setValue(v)
except (TypeError, ValueError, OverflowError):
continue
except Exception as exc:
raise Exception("Setting %s parameter value to %r raised %r." % (param, v, exc))
raise Exception("Setting %s parameter value to %r should have raised an exception." % (param, v))

View File

@ -10,11 +10,13 @@ Procedure for unit-testing with images:
$ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py $ PYQTGRAPH_AUDIT=1 python pyqtgraph/graphicsItems/tests/test_PlotCurveItem.py
Any failing tests will Any failing tests will display the test results, standard image, and the
display the test results, standard image, and the differences between the differences between the two. If the test result is bad, then press (f)ail.
two. If the test result is bad, then press (f)ail. If the test result is If the test result is good, then press (p)ass and the new image will be
good, then press (p)ass and the new image will be saved to the test-data saved to the test-data directory.
directory.
To check all test results regardless of whether the test failed, set the
environment variable PYQTGRAPH_AUDIT_ALL=1.
3. After adding or changing test images, create a new commit: 3. After adding or changing test images, create a new commit:
@ -42,7 +44,7 @@ Procedure for unit-testing with images:
# pyqtgraph should be tested against. When adding or changing test images, # pyqtgraph should be tested against. When adding or changing test images,
# create and push a new tag and update this variable. To test locally, begin # create and push a new tag and update this variable. To test locally, begin
# by creating the tag in your ~/.pyqtgraph/test-data repository. # by creating the tag in your ~/.pyqtgraph/test-data repository.
testDataTag = 'test-data-6' testDataTag = 'test-data-7'
import time import time
@ -162,6 +164,8 @@ def assertImageApproved(image, standardFile, message=None, **kwargs):
# If the test image does not match, then we go to audit if requested. # If the test image does not match, then we go to audit if requested.
try: try:
if stdImage is None:
raise Exception("No reference image saved for this test.")
if image.shape[2] != stdImage.shape[2]: if image.shape[2] != stdImage.shape[2]:
raise Exception("Test result has different channel count than standard image" raise Exception("Test result has different channel count than standard image"
"(%d vs %d)" % (image.shape[2], stdImage.shape[2])) "(%d vs %d)" % (image.shape[2], stdImage.shape[2]))

View File

@ -1,5 +1,6 @@
import pyqtgraph as pg import pyqtgraph as pg
import numpy as np import numpy as np
import sys
from numpy.testing import assert_array_almost_equal, assert_almost_equal from numpy.testing import assert_array_almost_equal, assert_almost_equal
import pytest import pytest
@ -293,6 +294,68 @@ def test_makeARGB():
with AssertExc(): # 3d levels not allowed with AssertExc(): # 3d levels not allowed
pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2])) pg.makeARGB(np.zeros((2,2,3), dtype='float'), levels=np.zeros([3, 2, 2]))
def test_eq():
eq = pg.functions.eq
zeros = [0, 0.0, np.float(0), np.int(0)]
if sys.version[0] < '3':
zeros.append(long(0))
for i,x in enumerate(zeros):
for y in zeros[i:]:
assert eq(x, y)
assert eq(y, x)
assert eq(np.nan, np.nan)
# test
class NotEq(object):
def __eq__(self, x):
return False
noteq = NotEq()
assert eq(noteq, noteq) # passes because they are the same object
assert not eq(noteq, NotEq())
# Should be able to test for equivalence even if the test raises certain
# exceptions
class NoEq(object):
def __init__(self, err):
self.err = err
def __eq__(self, x):
raise self.err
noeq1 = NoEq(AttributeError())
noeq2 = NoEq(ValueError())
noeq3 = NoEq(Exception())
assert eq(noeq1, noeq1)
assert not eq(noeq1, noeq2)
assert not eq(noeq2, noeq1)
with pytest.raises(Exception):
eq(noeq3, noeq2)
# test array equivalence
# note that numpy has a weird behavior here--np.all() always returns True
# if one of the arrays has size=0; eq() will only return True if both arrays
# have the same shape.
a1 = np.zeros((10, 20)).astype('float')
a2 = a1 + 1
a3 = a2.astype('int')
a4 = np.empty((0, 20))
assert not eq(a1, a2) # same shape/dtype, different values
assert not eq(a1, a3) # same shape, different dtype and values
assert not eq(a1, a4) # different shape (note: np.all gives True if one array has size 0)
assert not eq(a2, a3) # same values, but different dtype
assert not eq(a2, a4) # different shape
assert not eq(a3, a4) # different shape and dtype
assert eq(a4, a4.copy())
assert not eq(a4, a4.T)
if __name__ == '__main__': if __name__ == '__main__':
test_interpolateArray() test_interpolateArray()

View File

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from ..Qt import QtCore
import traceback import traceback
from ..Qt import QtCore
class Mutex(QtCore.QMutex): class Mutex(QtCore.QMutex):
""" """
@ -17,7 +18,7 @@ class Mutex(QtCore.QMutex):
QtCore.QMutex.__init__(self, *args) QtCore.QMutex.__init__(self, *args)
self.l = QtCore.QMutex() ## for serializing access to self.tb self.l = QtCore.QMutex() ## for serializing access to self.tb
self.tb = [] self.tb = []
self.debug = True ## True to enable debugging functions self.debug = kargs.pop('debug', False) ## True to enable debugging functions
def tryLock(self, timeout=None, id=None): def tryLock(self, timeout=None, id=None):
if timeout is None: if timeout is None:
@ -72,6 +73,16 @@ class Mutex(QtCore.QMutex):
finally: finally:
self.l.unlock() self.l.unlock()
def acquire(self, blocking=True):
"""Mimics threading.Lock.acquire() to allow this class as a drop-in replacement.
"""
return self.tryLock()
def release(self):
"""Mimics threading.Lock.release() to allow this class as a drop-in replacement.
"""
self.unlock()
def depth(self): def depth(self):
self.l.lock() self.l.lock()
n = len(self.tb) n = len(self.tb)
@ -91,4 +102,13 @@ class Mutex(QtCore.QMutex):
def __enter__(self): def __enter__(self):
self.lock() self.lock()
return self return self
class RecursiveMutex(Mutex):
"""Mimics threading.RLock class.
"""
def __init__(self, **kwds):
kwds['recursive'] = True
Mutex.__init__(self, **kwds)

View File

@ -42,6 +42,11 @@ class ColorMapWidget(ptree.ParameterTree):
def restoreState(self, state): def restoreState(self, state):
self.params.restoreState(state) self.params.restoreState(state)
def addColorMap(self, name):
"""Add a new color mapping and return the created parameter.
"""
return self.params.addNew(name)
class ColorMapParameter(ptree.types.GroupParameter): class ColorMapParameter(ptree.types.GroupParameter):
sigColorMapChanged = QtCore.Signal(object) sigColorMapChanged = QtCore.Signal(object)
@ -152,7 +157,7 @@ class ColorMapParameter(ptree.types.GroupParameter):
def restoreState(self, state): def restoreState(self, state):
if 'fields' in state: if 'fields' in state:
self.setFields(state['fields']) self.setFields(state['fields'])
for itemState in state['items']: for name, itemState in state['items'].items():
item = self.addNew(itemState['field']) item = self.addNew(itemState['field'])
item.restoreState(itemState) item.restoreState(itemState)

View File

@ -102,7 +102,7 @@ class ComboBox(QtGui.QComboBox):
@blockIfUnchanged @blockIfUnchanged
def setItems(self, items): def setItems(self, items):
""" """
*items* may be a list or a dict. *items* may be a list, a tuple, or a dict.
If a dict is given, then the keys are used to populate the combo box If a dict is given, then the keys are used to populate the combo box
and the values will be used for both value() and setValue(). and the values will be used for both value() and setValue().
""" """
@ -191,13 +191,13 @@ class ComboBox(QtGui.QComboBox):
@ignoreIndexChange @ignoreIndexChange
@blockIfUnchanged @blockIfUnchanged
def addItems(self, items): def addItems(self, items):
if isinstance(items, list): if isinstance(items, list) or isinstance(items, tuple):
texts = items texts = items
items = dict([(x, x) for x in items]) items = dict([(x, x) for x in items])
elif isinstance(items, dict): elif isinstance(items, dict):
texts = list(items.keys()) texts = list(items.keys())
else: else:
raise TypeError("items argument must be list or dict (got %s)." % type(items)) raise TypeError("items argument must be list or dict or tuple (got %s)." % type(items))
for t in texts: for t in texts:
if t in self._items: if t in self._items:
@ -216,3 +216,30 @@ class ComboBox(QtGui.QComboBox):
QtGui.QComboBox.clear(self) QtGui.QComboBox.clear(self)
self.itemsChanged() self.itemsChanged()
def saveState(self):
ind = self.currentIndex()
data = self.itemData(ind)
#if not data.isValid():
if data is not None:
try:
if not data.isValid():
data = None
else:
data = data.toInt()[0]
except AttributeError:
pass
if data is None:
return asUnicode(self.itemText(ind))
else:
return data
def restoreState(self, v):
if type(v) is int:
ind = self.findData(v)
if ind > -1:
self.setCurrentIndex(ind)
return
self.setCurrentIndex(self.findText(str(v)))
def widgetGroupInterface(self):
return (self.currentIndexChanged, self.saveState, self.restoreState)

View File

@ -30,7 +30,12 @@ class DataFilterWidget(ptree.ParameterTree):
def parameters(self): def parameters(self):
return self.params return self.params
def addFilter(self, name):
"""Add a new filter and return the created parameter item.
"""
return self.params.addNew(name)
class DataFilterParameter(ptree.types.GroupParameter): class DataFilterParameter(ptree.types.GroupParameter):
@ -47,10 +52,10 @@ class DataFilterParameter(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(RangeFilterItem(name, self.fields[name])) child = self.addChild(RangeFilterItem(name, self.fields[name]))
elif mode == 'enum': elif mode == 'enum':
self.addChild(EnumFilterItem(name, self.fields[name])) child = self.addChild(EnumFilterItem(name, self.fields[name]))
return child
def fieldNames(self): def fieldNames(self):
return self.fields.keys() return self.fields.keys()

View File

@ -1,4 +1,4 @@
from ..Qt import QtGui from ..Qt import QtGui, mkQApp
from ..graphicsItems.GraphicsLayout import GraphicsLayout from ..graphicsItems.GraphicsLayout import GraphicsLayout
from .GraphicsView import GraphicsView from .GraphicsView import GraphicsView
@ -9,6 +9,31 @@ class GraphicsLayoutWidget(GraphicsView):
<pyqtgraph.GraphicsView>` with a single :class:`GraphicsLayout <pyqtgraph.GraphicsView>` with a single :class:`GraphicsLayout
<pyqtgraph.GraphicsLayout>` as its central item. <pyqtgraph.GraphicsLayout>` as its central item.
This widget is an easy starting point for generating multi-panel figures.
Example::
w = pg.GraphicsLayoutWidget()
p1 = w.addPlot(row=0, col=0)
p2 = w.addPlot(row=0, col=1)
v = w.addViewBox(row=1, col=0, colspan=2)
Parameters
----------
parent : QWidget or None
The parent widget (see QWidget.__init__)
show : bool
If True, then immediately show the widget after it is created.
If the widget has no parent, then it will be shown inside a new window.
size : (width, height) tuple
Optionally resize the widget. Note: if this widget is placed inside a
layout, then this argument has no effect.
title : str or None
If specified, then set the window title for this widget.
kargs :
All extra arguments are passed to
:func:`GraphicsLayout.__init__() <pyqtgraph.GraphicsLayout.__init__>`
This class wraps several methods from its internal GraphicsLayout: This class wraps several methods from its internal GraphicsLayout:
:func:`nextRow <pyqtgraph.GraphicsLayout.nextRow>` :func:`nextRow <pyqtgraph.GraphicsLayout.nextRow>`
:func:`nextColumn <pyqtgraph.GraphicsLayout.nextColumn>` :func:`nextColumn <pyqtgraph.GraphicsLayout.nextColumn>`
@ -22,9 +47,19 @@ class GraphicsLayoutWidget(GraphicsView):
:func:`itemIndex <pyqtgraph.GraphicsLayout.itemIndex>` :func:`itemIndex <pyqtgraph.GraphicsLayout.itemIndex>`
:func:`clear <pyqtgraph.GraphicsLayout.clear>` :func:`clear <pyqtgraph.GraphicsLayout.clear>`
""" """
def __init__(self, parent=None, **kargs): def __init__(self, parent=None, show=False, size=None, title=None, **kargs):
mkQApp()
GraphicsView.__init__(self, parent) GraphicsView.__init__(self, parent)
self.ci = GraphicsLayout(**kargs) self.ci = GraphicsLayout(**kargs)
for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']: for n in ['nextRow', 'nextCol', 'nextColumn', 'addPlot', 'addViewBox', 'addItem', 'getItem', 'addLayout', 'addLabel', 'removeItem', 'itemIndex', 'clear']:
setattr(self, n, getattr(self.ci, n)) setattr(self, n, getattr(self.ci, n))
self.setCentralItem(self.ci) self.setCentralItem(self.ci)
if size is not None:
self.resize(*size)
if title is not None:
self.setWindowTitle(title)
if show is True:
self.show()

View File

@ -0,0 +1,91 @@
from ..Qt import QtGui, QtCore
from .PathButton import PathButton
class GroupBox(QtGui.QGroupBox):
"""Subclass of QGroupBox that implements collapse handle.
"""
sigCollapseChanged = QtCore.Signal(object)
def __init__(self, *args):
QtGui.QGroupBox.__init__(self, *args)
self._collapsed = False
# We modify the size policy when the group box is collapsed, so
# keep track of the last requested policy:
self._lastSizePlocy = self.sizePolicy()
self.closePath = QtGui.QPainterPath()
self.closePath.moveTo(0, -1)
self.closePath.lineTo(0, 1)
self.closePath.lineTo(1, 0)
self.closePath.lineTo(0, -1)
self.openPath = QtGui.QPainterPath()
self.openPath.moveTo(-1, 0)
self.openPath.lineTo(1, 0)
self.openPath.lineTo(0, 1)
self.openPath.lineTo(-1, 0)
self.collapseBtn = PathButton(path=self.openPath, size=(12, 12), margin=0)
self.collapseBtn.setStyleSheet("""
border: none;
""")
self.collapseBtn.setPen('k')
self.collapseBtn.setBrush('w')
self.collapseBtn.setParent(self)
self.collapseBtn.move(3, 3)
self.collapseBtn.setFlat(True)
self.collapseBtn.clicked.connect(self.toggleCollapsed)
if len(args) > 0 and isinstance(args[0], basestring):
self.setTitle(args[0])
def toggleCollapsed(self):
self.setCollapsed(not self._collapsed)
def collapsed(self):
return self._collapsed
def setCollapsed(self, c):
if c == self._collapsed:
return
if c is True:
self.collapseBtn.setPath(self.closePath)
self.setSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Preferred, closing=True)
elif c is False:
self.collapseBtn.setPath(self.openPath)
self.setSizePolicy(self._lastSizePolicy)
else:
raise TypeError("Invalid argument %r; must be bool." % c)
for ch in self.children():
if isinstance(ch, QtGui.QWidget) and ch is not self.collapseBtn:
ch.setVisible(not c)
self._collapsed = c
self.sigCollapseChanged.emit(c)
def setSizePolicy(self, *args, **kwds):
QtGui.QGroupBox.setSizePolicy(self, *args)
if kwds.pop('closing', False) is True:
self._lastSizePolicy = self.sizePolicy()
def setHorizontalPolicy(self, *args):
QtGui.QGroupBox.setHorizontalPolicy(self, *args)
self._lastSizePolicy = self.sizePolicy()
def setVerticalPolicy(self, *args):
QtGui.QGroupBox.setVerticalPolicy(self, *args)
self._lastSizePolicy = self.sizePolicy()
def setTitle(self, title):
# Leave room for button
QtGui.QGroupBox.setTitle(self, " " + title)
def widgetGroupInterface(self):
return (self.sigCollapseChanged,
GroupBox.collapsed,
GroupBox.setCollapsed,
True)

View File

@ -5,9 +5,11 @@ __all__ = ['PathButton']
class PathButton(QtGui.QPushButton): class PathButton(QtGui.QPushButton):
"""Simple PushButton extension which paints a QPainterPath on its face""" """Simple PushButton extension that paints a QPainterPath centered on its face.
def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30)): """
def __init__(self, parent=None, path=None, pen='default', brush=None, size=(30,30), margin=7):
QtGui.QPushButton.__init__(self, parent) QtGui.QPushButton.__init__(self, parent)
self.margin = margin
self.path = None self.path = None
if pen == 'default': if pen == 'default':
pen = 'k' pen = 'k'
@ -19,7 +21,6 @@ class PathButton(QtGui.QPushButton):
self.setFixedWidth(size[0]) self.setFixedWidth(size[0])
self.setFixedHeight(size[1]) self.setFixedHeight(size[1])
def setBrush(self, brush): def setBrush(self, brush):
self.brush = fn.mkBrush(brush) self.brush = fn.mkBrush(brush)
@ -32,7 +33,7 @@ class PathButton(QtGui.QPushButton):
def paintEvent(self, ev): def paintEvent(self, ev):
QtGui.QPushButton.paintEvent(self, ev) QtGui.QPushButton.paintEvent(self, ev)
margin = 7 margin = self.margin
geom = QtCore.QRectF(0, 0, self.width(), self.height()).adjusted(margin, margin, -margin, -margin) geom = QtCore.QRectF(0, 0, self.width(), self.height()).adjusted(margin, margin, -margin, -margin)
rect = self.path.boundingRect() rect = self.path.boundingRect()
scale = min(geom.width() / float(rect.width()), geom.height() / float(rect.height())) scale = min(geom.width() / float(rect.width()), geom.height() / float(rect.height()))

View File

@ -76,7 +76,7 @@ class PlotWidget(GraphicsView):
m = getattr(self.plotItem, attr) m = getattr(self.plotItem, attr)
if hasattr(m, '__call__'): if hasattr(m, '__call__'):
return m return m
raise NameError(attr) raise AttributeError(attr)
def viewRangeChanged(self, view, range): def viewRangeChanged(self, view, range):
#self.emit(QtCore.SIGNAL('viewChanged'), *args) #self.emit(QtCore.SIGNAL('viewChanged'), *args)

View File

@ -2,9 +2,14 @@
from ..Qt import QtGui, QtCore from ..Qt import QtGui, QtCore
__all__ = ['ProgressDialog'] __all__ = ['ProgressDialog']
class ProgressDialog(QtGui.QProgressDialog): class ProgressDialog(QtGui.QProgressDialog):
""" """
Extends QProgressDialog for use in 'with' statements. Extends QProgressDialog:
* Adds context management so the dialog may be used in `with` statements
* Allows nesting multiple progress dialogs
Example:: Example::
@ -14,7 +19,10 @@ class ProgressDialog(QtGui.QProgressDialog):
if dlg.wasCanceled(): if dlg.wasCanceled():
raise Exception("Processing canceled by user") raise Exception("Processing canceled by user")
""" """
def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False):
allDialogs = []
def __init__(self, labelText, minimum=0, maximum=100, cancelText='Cancel', parent=None, wait=250, busyCursor=False, disable=False, nested=False):
""" """
============== ================================================================ ============== ================================================================
**Arguments:** **Arguments:**
@ -29,8 +37,18 @@ class ProgressDialog(QtGui.QProgressDialog):
and calls to wasCanceled() will always return False. and calls to wasCanceled() will always return False.
If ProgressDialog is entered from a non-gui thread, it will If ProgressDialog is entered from a non-gui thread, it will
always be disabled. always be disabled.
nested (bool) If True, then this progress bar will be displayed inside
any pre-existing progress dialogs that also allow nesting.
============== ================================================================ ============== ================================================================
""" """
# attributes used for nesting dialogs
self.nestedLayout = None
self._nestableWidgets = None
self._nestingReady = False
self._topDialog = None
self._subBars = []
self.nested = nested
isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread() isGuiThread = QtCore.QThread.currentThread() == QtCore.QCoreApplication.instance().thread()
self.disabled = disable or (not isGuiThread) self.disabled = disable or (not isGuiThread)
if self.disabled: if self.disabled:
@ -42,20 +60,34 @@ class ProgressDialog(QtGui.QProgressDialog):
noCancel = True noCancel = True
self.busyCursor = busyCursor self.busyCursor = busyCursor
QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent) QtGui.QProgressDialog.__init__(self, labelText, cancelText, minimum, maximum, parent)
self.setMinimumDuration(wait)
# If this will be a nested dialog, then we ignore the wait time
if nested is True and len(ProgressDialog.allDialogs) > 0:
self.setMinimumDuration(2**30)
else:
self.setMinimumDuration(wait)
self.setWindowModality(QtCore.Qt.WindowModal) self.setWindowModality(QtCore.Qt.WindowModal)
self.setValue(self.minimum()) self.setValue(self.minimum())
if noCancel: if noCancel:
self.setCancelButton(None) self.setCancelButton(None)
def __enter__(self): def __enter__(self):
if self.disabled: if self.disabled:
return self return self
if self.busyCursor: if self.busyCursor:
QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor)) QtGui.QApplication.setOverrideCursor(QtGui.QCursor(QtCore.Qt.WaitCursor))
if self.nested and len(ProgressDialog.allDialogs) > 0:
topDialog = ProgressDialog.allDialogs[0]
topDialog._addSubDialog(self)
self._topDialog = topDialog
topDialog.canceled.connect(self.cancel)
ProgressDialog.allDialogs.append(self)
return self return self
def __exit__(self, exType, exValue, exTrace): def __exit__(self, exType, exValue, exTrace):
@ -63,6 +95,12 @@ class ProgressDialog(QtGui.QProgressDialog):
return return
if self.busyCursor: if self.busyCursor:
QtGui.QApplication.restoreOverrideCursor() QtGui.QApplication.restoreOverrideCursor()
if self._topDialog is not None:
self._topDialog._removeSubDialog(self)
ProgressDialog.allDialogs.pop(-1)
self.setValue(self.maximum()) self.setValue(self.maximum())
def __iadd__(self, val): def __iadd__(self, val):
@ -72,6 +110,88 @@ class ProgressDialog(QtGui.QProgressDialog):
self.setValue(self.value()+val) self.setValue(self.value()+val)
return self return self
def _addSubDialog(self, dlg):
# insert widgets from another dialog into this one.
# set a new layout and arrange children into it (if needed).
self._prepareNesting()
bar, btn = dlg._extractWidgets()
# where should we insert this widget? Find the first slot with a
# "removed" widget (that was left as a placeholder)
inserted = False
for i,bar2 in enumerate(self._subBars):
if bar2.hidden:
self._subBars.pop(i)
bar2.hide()
bar2.setParent(None)
self._subBars.insert(i, bar)
inserted = True
break
if not inserted:
self._subBars.append(bar)
# reset the layout
while self.nestedLayout.count() > 0:
self.nestedLayout.takeAt(0)
for b in self._subBars:
self.nestedLayout.addWidget(b)
def _removeSubDialog(self, dlg):
# don't remove the widget just yet; instead we hide it and leave it in
# as a placeholder.
bar, btn = dlg._extractWidgets()
bar.hide()
def _prepareNesting(self):
# extract all child widgets and place into a new layout that we can add to
if self._nestingReady is False:
# top layout contains progress bars + cancel button at the bottom
self._topLayout = QtGui.QGridLayout()
self.setLayout(self._topLayout)
self._topLayout.setContentsMargins(0, 0, 0, 0)
# A vbox to contain all progress bars
self.nestedVBox = QtGui.QWidget()
self._topLayout.addWidget(self.nestedVBox, 0, 0, 1, 2)
self.nestedLayout = QtGui.QVBoxLayout()
self.nestedVBox.setLayout(self.nestedLayout)
# re-insert all widgets
bar, btn = self._extractWidgets()
self.nestedLayout.addWidget(bar)
self._subBars.append(bar)
self._topLayout.addWidget(btn, 1, 1, 1, 1)
self._topLayout.setColumnStretch(0, 100)
self._topLayout.setColumnStretch(1, 1)
self._topLayout.setRowStretch(0, 100)
self._topLayout.setRowStretch(1, 1)
self._nestingReady = True
def _extractWidgets(self):
# return:
# 1. a single widget containing the label and progress bar
# 2. the cancel button
if self._nestableWidgets is None:
widgets = [ch for ch in self.children() if isinstance(ch, QtGui.QWidget)]
label = [ch for ch in self.children() if isinstance(ch, QtGui.QLabel)][0]
bar = [ch for ch in self.children() if isinstance(ch, QtGui.QProgressBar)][0]
btn = [ch for ch in self.children() if isinstance(ch, QtGui.QPushButton)][0]
sw = ProgressWidget(label, bar)
self._nestableWidgets = (sw, btn)
return self._nestableWidgets
def resizeEvent(self, ev):
if self._nestingReady:
# don't let progress dialog manage widgets anymore.
return
return QtGui.QProgressDialog.resizeEvent(self, ev)
## wrap all other functions to make sure they aren't being called from non-gui threads ## wrap all other functions to make sure they aren't being called from non-gui threads
@ -80,6 +200,11 @@ class ProgressDialog(QtGui.QProgressDialog):
return return
QtGui.QProgressDialog.setValue(self, val) QtGui.QProgressDialog.setValue(self, val)
# Qt docs say this should happen automatically, but that doesn't seem
# to be the case.
if self.windowModality() == QtCore.Qt.WindowModal:
QtGui.QApplication.processEvents()
def setLabelText(self, val): def setLabelText(self, val):
if self.disabled: if self.disabled:
return return
@ -109,4 +234,29 @@ class ProgressDialog(QtGui.QProgressDialog):
if self.disabled: if self.disabled:
return 0 return 0
return QtGui.QProgressDialog.minimum(self) return QtGui.QProgressDialog.minimum(self)
class ProgressWidget(QtGui.QWidget):
"""Container for a label + progress bar that also allows its child widgets
to be hidden without changing size.
"""
def __init__(self, label, bar):
QtGui.QWidget.__init__(self)
self.hidden = False
self.layout = QtGui.QVBoxLayout()
self.setLayout(self.layout)
self.label = label
self.bar = bar
self.layout.addWidget(label)
self.layout.addWidget(bar)
def eventFilter(self, obj, ev):
return ev.type() == QtCore.QEvent.Paint
def hide(self):
# hide label and bar, but continue occupying the same space in the layout
for widget in (self.label, self.bar):
widget.installEventFilter(self)
widget.update()
self.hidden = True

Some files were not shown because too many files have changed in this diff Show More