Commit c5539296 authored by Luke Campagnola's avatar Luke Campagnola
Browse files

Merge from ACQ4:

 - Lots of bug fixes
 - API change in PlotItem.plot(...)
 - Started replacing QObjectWorkaround with QWidget
 - Made plotCurveItems clickable
 - Added curve-following arrows
parent 661e4411
......@@ -66,8 +66,14 @@ class GraphicsView(QtGui.QGraphicsView):
self.updateMatrix()
self.sceneObj = QtGui.QGraphicsScene()
self.setScene(self.sceneObj)
## by default we set up a central widget with a grid layout.
## this can be replaced if needed.
self.centralWidget = None
self.setCentralItem(QtGui.QGraphicsWidget())
self.centralLayout = QtGui.QGraphicsGridLayout()
self.centralWidget.setLayout(self.centralLayout)
self.mouseEnabled = False
self.scaleCenter = False ## should scaling center around view center (True) or mouse click (False)
self.clickAccepted = False
......@@ -85,6 +91,7 @@ class GraphicsView(QtGui.QGraphicsView):
ev.ignore()
def setCentralItem(self, item):
"""Sets a QGraphicsWidget to automatically fill the entire view."""
if self.centralWidget is not None:
self.scene().removeItem(self.centralWidget)
self.centralWidget = item
......@@ -305,21 +312,24 @@ class GraphicsView(QtGui.QGraphicsView):
#self.currentItem = None
def mouseMoveEvent(self, ev):
#if self.lastMousePos is None:
#self.lastMousePos = Point(ev.pos())
#delta = Point(ev.pos()) - self.lastMousePos
#if abs(delta[0]) > 100 or abs(delta[1]) > 100: ## Weird bug generating extra events..
#return
#self.lastMousePos = Point(ev.pos())
#print "move", delta
QtGui.QGraphicsView.mouseMoveEvent(self, ev)
if not self.mouseEnabled:
return
self.emit(QtCore.SIGNAL("sceneMouseMoved(PyQt_PyObject)"), self.mapToScene(ev.pos()))
#print "moved. Grabber:", self.scene().mouseGrabberItem()
if self.lastMousePos is None:
self.lastMousePos = Point(ev.pos())
if self.clickAccepted: ## Ignore event if an item in the scene has already claimed it.
return
delta = Point(ev.pos()) - self.lastMousePos
self.lastMousePos = Point(ev.pos())
if ev.buttons() == QtCore.Qt.RightButton:
delta = Point(clip(delta[0], -50, 50), clip(-delta[1], -50, 50))
......@@ -377,6 +387,10 @@ class GraphicsView(QtGui.QGraphicsView):
self.render(painter)
painter.end()
def dragEnterEvent(self, ev):
ev.ignore() ## not sure why, but for some reason this class likes to consume drag events
#def getFreehandLine(self):
......
......@@ -106,12 +106,12 @@ class ImageView(QtGui.QWidget):
setattr(self, fn, getattr(self.ui.graphicsView, fn))
#QtCore.QObject.connect(self.ui.timeSlider, QtCore.SIGNAL('valueChanged(int)'), self.timeChanged)
self.timeLine.connect(QtCore.SIGNAL('positionChanged'), self.timeLineChanged)
self.timeLine.connect(self.timeLine, QtCore.SIGNAL('positionChanged'), self.timeLineChanged)
#QtCore.QObject.connect(self.ui.whiteSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage)
#QtCore.QObject.connect(self.ui.blackSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateImage)
QtCore.QObject.connect(self.ui.gradientWidget, QtCore.SIGNAL('gradientChanged'), self.updateImage)
QtCore.QObject.connect(self.ui.roiBtn, QtCore.SIGNAL('clicked()'), self.roiClicked)
self.roi.connect(QtCore.SIGNAL('regionChanged'), self.roiChanged)
self.roi.connect(self.roi, QtCore.SIGNAL('regionChanged'), self.roiChanged)
QtCore.QObject.connect(self.ui.normBtn, QtCore.SIGNAL('toggled(bool)'), self.normToggled)
QtCore.QObject.connect(self.ui.normDivideRadio, QtCore.SIGNAL('clicked()'), self.updateNorm)
QtCore.QObject.connect(self.ui.normSubtractRadio, QtCore.SIGNAL('clicked()'), self.updateNorm)
......@@ -124,7 +124,7 @@ class ImageView(QtGui.QWidget):
##QtCore.QObject.connect(self.ui.normStartSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm)
#QtCore.QObject.connect(self.ui.normStopSlider, QtCore.SIGNAL('valueChanged(int)'), self.updateNorm)
self.normProxy = proxyConnect(self.normRgn, QtCore.SIGNAL('regionChanged'), self.updateNorm)
self.normRoi.connect(QtCore.SIGNAL('regionChangeFinished'), self.updateNorm)
self.normRoi.connect(self.normRoi, QtCore.SIGNAL('regionChangeFinished'), self.updateNorm)
self.ui.roiPlot.registerPlot(self.name + '_ROI')
......@@ -347,7 +347,19 @@ class ImageView(QtGui.QWidget):
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': None}
elif img.ndim == 4:
self.axes = {'t': 0, 'x': 1, 'y': 2, 'c': 3}
else:
raise Exception("Can not interpret image with dimensions %s" % (str(img)))
elif isinstance(axes, dict):
self.axes = axes.copy()
elif isinstance(axes, list) or isinstance(axes, tuple):
self.axes = {}
for i in range(len(axes)):
self.axes[axes[i]] = i
else:
raise Exception("Can not interpret axis specification %s. Must be like {'t': 2, 'x': 0, 'y': 1} or ('t', 'x', 'y', 'c')" % (str(axes)))
for x in ['t', 'x', 'y', 'c']:
self.axes[x] = self.axes.get(x, None)
self.imageDisp = None
if autoLevels:
......
# -*- coding: utf-8 -*-
from PyQt4 import QtGui, QtCore
"""For circumventing PyQt's lack of multiple inheritance (just until PySide becomes stable)"""
class Obj(QtCore.QObject):
def event(self, ev):
self.emit(QtCore.SIGNAL('event'), ev)
return QtCore.QObject.event(self, ev)
class QObjectWorkaround:
def __init__(self):
self._qObj_ = QtCore.QObject()
self._qObj_ = Obj()
self.connect(QtCore.SIGNAL('event'), self.event)
def connect(self, *args):
if args[0] is self:
return QtCore.QObject.connect(self._qObj_, *args[1:])
......@@ -15,8 +23,20 @@ class QObjectWorkaround:
return QtCore.QObject.emit(self._qObj_, *args)
def blockSignals(self, b):
return self._qObj_.blockSignals(b)
def setProperty(self, prop, val):
return self._qObj_.setProperty(prop, val)
def property(self, prop):
return self._qObj_.property(prop)
def event(self, ev):
pass
class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround):
def __init__(self, *args):
QtGui.QGraphicsItem.__init__(self, *args)
QObjectWorkaround.__init__(self)
#class QGraphicsObject(QtGui.QGraphicsItem, QObjectWorkaround):
#def __init__(self, *args):
#QtGui.QGraphicsItem.__init__(self, *args)
#QObjectWorkaround.__init__(self)
class QGraphicsObject(QtGui.QGraphicsWidget):
def shape(self):
return QtGui.QGraphicsItem.shape(self)
#QGraphicsObject = QtGui.QGraphicsObject
\ No newline at end of file
......@@ -20,6 +20,7 @@ This class is very heavily featured:
from graphicsItems import *
from plotConfigTemplate import *
from PyQt4 import QtGui, QtCore, QtSvg
from functions import *
#from ObjectWorkaround import *
#tryWorkaround(QtCore, QtGui)
import weakref
......@@ -43,7 +44,7 @@ class PlotItem(QtGui.QGraphicsWidget):
lastFileDir = None
managers = {}
def __init__(self, parent=None, name=None):
def __init__(self, parent=None, name=None, labels=None, **kargs):
QtGui.QGraphicsWidget.__init__(self, parent)
## Set up control buttons
......@@ -226,6 +227,15 @@ class PlotItem(QtGui.QGraphicsWidget):
if name is not None:
self.registerPlot(name)
if labels is not None:
for k in labels:
if isinstance(labels[k], basestring):
labels[k] = (labels[k],)
self.setLabel(k, *labels[k])
if len(kargs) > 0:
self.plot(**kargs)
def __del__(self):
if self.manager is not None:
......@@ -274,8 +284,8 @@ class PlotItem(QtGui.QGraphicsWidget):
v = self.scene().views()[0]
b = self.vb.mapRectToScene(self.vb.boundingRect())
wr = v.mapFromScene(b).boundingRect()
pos = v.pos()
wr.adjust(v.x(), v.y(), v.x(), v.y())
pos = v.mapToGlobal(v.pos())
wr.adjust(pos.x(), pos.y(), pos.x(), pos.y())
return wr
......@@ -324,6 +334,7 @@ class PlotItem(QtGui.QGraphicsWidget):
self.manager.linkY(self, plot)
def linkXChanged(self, plot):
"""Called when a linked plot has changed its X scale"""
#print "update from", plot
if self.linksBlocked:
return
......@@ -337,11 +348,13 @@ class PlotItem(QtGui.QGraphicsWidget):
x1 = pr.left() + (sg.x()-pg.x()) * upp
x2 = x1 + sg.width() * upp
plot.blockLink(True)
self.setManualXScale()
self.setXRange(x1, x2, padding=0)
plot.blockLink(False)
self.replot()
def linkYChanged(self, plot):
"""Called when a linked plot has changed its Y scale"""
if self.linksBlocked:
return
pr = plot.vb.viewRect()
......@@ -351,6 +364,7 @@ class PlotItem(QtGui.QGraphicsWidget):
y1 = pr.bottom() + (sg.y()-pg.y()) * upp
y2 = y1 + sg.height() * upp
plot.blockLink(True)
self.setManualYScale()
self.setYRange(y1, y2, padding=0)
plot.blockLink(False)
self.replot()
......@@ -554,7 +568,7 @@ class PlotItem(QtGui.QGraphicsWidget):
self.curves.remove(item)
self.updateDecimation()
self.updateParamList()
item.connect(QtCore.SIGNAL('plotChanged'), self.plotChanged)
item.connect(item, QtCore.SIGNAL('plotChanged'), self.plotChanged)
def clear(self):
for i in self.items[:]:
......@@ -567,7 +581,18 @@ class PlotItem(QtGui.QGraphicsWidget):
self.avgCurves = {}
def plot(self, data=None, x=None, clear=False, params=None, pen=None):
def plot(self, data=None, data2=None, x=None, y=None, clear=False, params=None, pen=None):
"""Add a new plot curve. Data may be specified a few ways:
plot(yVals) # x vals will be integers
plot(xVals, yVals)
plot(y=yVals, x=xVals)
"""
if y is not None:
data = y
if data2 is not None:
x = data
data = data2
if clear:
self.clear()
if params is None:
......@@ -588,7 +613,7 @@ class PlotItem(QtGui.QGraphicsWidget):
#print data, curve
self.addCurve(curve, params)
if pen is not None:
curve.setPen(pen)
curve.setPen(mkPen(pen))
return curve
......@@ -619,7 +644,7 @@ class PlotItem(QtGui.QGraphicsWidget):
if self.ctrl.averageGroup.isChecked():
self.addAvgCurve(c)
c.connect(QtCore.SIGNAL('plotChanged'), self.plotChanged)
c.connect(c, QtCore.SIGNAL('plotChanged'), self.plotChanged)
self.plotChanged()
def plotChanged(self, curve=None):
......@@ -643,6 +668,7 @@ class PlotItem(QtGui.QGraphicsWidget):
mn -= 1
mx += 1
self.setRange(ax, mn, mx)
#print "Auto range:", ax, mn, mx
def replot(self):
self.plotChanged()
......@@ -876,25 +902,6 @@ class PlotItem(QtGui.QGraphicsWidget):
mode = False
return mode
#def mousePressEvent(self, ev):
#self.mousePos = array([ev.pos().x(), ev.pos().y()])
#self.pressPos = self.mousePos.copy()
#QtGui.QGraphicsWidget.mousePressEvent(self, ev)
## NOTE: we will only receive move/release events if we run ev.accept()
#print 'press'
#def mouseReleaseEvent(self, ev):
#pos = array([ev.pos().x(), ev.pos().y()])
#print 'release'
#if sum(abs(self.pressPos - pos)) < 3: ## Detect click
#if ev.button() == QtCore.Qt.RightButton:
#print 'popup'
#self.ctrlMenu.popup(self.mapToGlobal(ev.pos()))
#self.mousePos = pos
#QtGui.QGraphicsWidget.mouseReleaseEvent(self, ev)
def resizeEvent(self, ev):
self.ctrlBtn.move(0, self.size().height() - self.ctrlBtn.size().height())
self.autoBtn.move(self.ctrlBtn.width(), self.size().height() - self.autoBtn.size().height())
......@@ -906,14 +913,8 @@ class PlotItem(QtGui.QGraphicsWidget):
def ctrlBtnClicked(self):
self.ctrlMenu.popup(self.mouseScreenPos)
#def _checkLabelKey(self, key):
#if key not in self.labels:
#raise Exception("Label '%s' not found. Labels are: %s" % (key, str(self.labels.keys())))
def getLabel(self, key):
pass
#self._checkLabelKey(key)
#return self.labels[key]['item']
def _checkScaleKey(self, key):
if key not in self.scales:
......@@ -925,43 +926,9 @@ class PlotItem(QtGui.QGraphicsWidget):
def setLabel(self, key, text=None, units=None, unitPrefix=None, **args):
self.getScale(key).setLabel(text=text, units=units, unitPrefix=unitPrefix, **args)
#if text is not None:
#self.labels[key]['text'] = text
#if units != None:
#self.labels[key]['units'] = units
#if unitPrefix != None:
#self.labels[key]['unitPrefix'] = unitPrefix
#text = self.labels[key]['text']
#units = self.labels[key]['units']
#unitPrefix = self.labels[key]['unitPrefix']
#if text is not '' or units is not '':
#l = self.getLabel(key)
#l.setText("%s (%s%s)" % (text, unitPrefix, units), **args)
#self.showLabel(key)
def showLabel(self, key, show=True):
self.getScale(key).showLabel(show)
#l = self.getLabel(key)
#p = self.labels[key]['pos']
#if show:
#l.show()
#if key in ['left', 'right']:
#self.layout.setColumnFixedWidth(p[1], l.size().width())
#l.setMaximumWidth(20)
#else:
#self.layout.setRowFixedHeight(p[0], l.size().height())
#l.setMaximumHeight(20)
#else:
#l.hide()
#if key in ['left', 'right']:
#self.layout.setColumnFixedWidth(p[1], 0)
#l.setMaximumWidth(0)
#else:
#self.layout.setRowFixedHeight(p[0], 0)
#l.setMaximumHeight(0)
def setTitle(self, title=None, **args):
if title is None:
......@@ -979,20 +946,8 @@ class PlotItem(QtGui.QGraphicsWidget):
p = self.scales[key]['pos']
if show:
s.show()
#if key in ['left', 'right']:
#self.layout.setColumnFixedWidth(p[1], s.maximumWidth())
##s.setMaximumWidth(40)
#else:
#self.layout.setRowFixedHeight(p[0], s.maximumHeight())
#s.setMaximumHeight(20)
else:
s.hide()
#if key in ['left', 'right']:
#self.layout.setColumnFixedWidth(p[1], 0)
##s.setMaximumWidth(0)
#else:
#self.layout.setRowFixedHeight(p[0], 0)
#s.setMaximumHeight(0)
def _plotArray(self, arr, x=None):
if arr.ndim != 1:
......
......@@ -51,4 +51,7 @@ class PlotWidget(GraphicsView):
return self.plotItem.restoreState(state)
def getPlotItem(self):
return self.plotItem
\ No newline at end of file
return self.plotItem
\ No newline at end of file
......@@ -92,20 +92,24 @@ class Point(QtCore.QPointF):
return Point(getattr(self[0], op)(x[0]), getattr(self[1], op)(x[1]))
def length(self):
"""Returns the vector length of this Point."""
return (self[0]**2 + self[1]**2) ** 0.5
def angle(self, a):
"""Returns the angle between this vector and the vector a."""
n1 = self.length()
n2 = a.length()
if n1 == 0. or n2 == 0.:
return None
ang = acos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0))
## Probably this should be done with arctan2 instead..
ang = acos(clip(self.dot(a) / (n1 * n2), -1.0, 1.0)) ### in radians
c = self.cross(a)
if c > 0:
ang *= -1.
return ang
def dot(self, a):
"""Returns the dot product of a and this Point."""
a = Point(a)
return self[0]*a[0] + self[1]*a[1]
......
# -*- coding: utf-8 -*-
### import all the goodies and add some helper functions for easy CLI use
from functions import *
from graphicsItems import *
from graphicsWindows import *
#import PlotWidget
#import ImageView
from PyQt4 import QtGui
plots = []
images = []
QAPP = None
def plot(*args, **kargs):
mkQApp()
if 'title' in kargs:
w = PlotWindow(title=kargs['title'])
del kargs['title']
else:
w = PlotWindow()
w.plot(*args, **kargs)
plots.append(w)
w.show()
return w
def show(*args, **kargs):
mkQApp()
w = ImageWindow(*args, **kargs)
images.append(w)
w.show()
return w
def mkQApp():
if QtGui.QApplication.instance() is None:
global QAPP
QAPP = QtGui.QApplication([])
\ No newline at end of file
......@@ -88,6 +88,6 @@ t.start(50)
for i in range(0, 5):
for j in range(0, 3):
yd, xd = rand(10000)
pw2.plot(yd*(j+1), xd, params={'iter': i, 'val': j})
pw2.plot(y=yd*(j+1), x=xd, params={'iter': i, 'val': j})
#app.exec_()
......@@ -4,7 +4,8 @@
import sys, os
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..'))
from scipy import zeros
from numpy import random
from scipy import zeros, ones
from pyqtgraph.graphicsWindows import *
from pyqtgraph.graphicsItems import *
from pyqtgraph.widgets import *
......@@ -84,6 +85,7 @@ rois.append(MultiLineROI([[0, 50], [50, 60], [60, 30]], width=5, pen=mkPen(2)))
rois.append(EllipseROI([110, 10], [30, 20], pen=mkPen(3)))
rois.append(CircleROI([110, 50], [20, 20], pen=mkPen(4)))
rois.append(PolygonROI([[2,0], [2.1,0], [2,.1]], pen=mkPen(5)))
#rois.append(SpiralROI([20,30], [1,1], pen=mkPen(0)))
for r in rois:
s.addItem(r)
c = pi1.plot(pen=r.pen)
......
......@@ -5,6 +5,18 @@ Copyright 2010 Luke Campagnola
Distributed under MIT/X11 license. See license.txt for more infomation.
"""
colorAbbrev = {
'b': (0,0,255,255),
'g': (0,255,0,255),
'r': (255,0,0,255),
'c': (0,255,255,255),
'm': (255,0,255,255),
'y': (255,255,0,255),
'k': (0,0,0,255),
'w': (255,255,255,255),
}
from PyQt4 import QtGui
from numpy import clip, floor, log
......@@ -26,11 +38,25 @@ def siScale(x, minVal=1e-25):
p = .001**m
return (p, pref)
def mkBrush(color):
return QtGui.QBrush(mkColor(color))
def mkPen(color=None, width=1, style=None, cosmetic=True, hsv=None, ):
def mkPen(arg=None, color=None, width=1, style=None, cosmetic=True, hsv=None, ):
"""Convenience function for making pens. Examples:
mkPen(color)
mkPen(color, width=2)
mkPen(cosmetic=False, width=4.5, color='r')
mkPen({'color': "FF0", width: 2})
"""
if isinstance(arg, dict):
return mkPen(**arg)
elif arg is not None:
if isinstance(arg, QtGui.QPen):
return arg
color = arg
if color is None:
color = [255, 255, 255]
color = mkColor(200, 200, 200)
if hsv is not None:
color = hsvColor(*hsv)
else:
......@@ -48,20 +74,64 @@ def hsvColor(h, s=1.0, v=1.0, a=1.0):
return c
def mkColor(*args):
"""make a QColor from a variety of argument types"""
"""make a QColor from a variety of argument types
accepted types are:
r, g, b, [a]
(r, g, b, [a])
float (greyscale, 0.0-1.0)
int (uses intColor)
(int, hues) (uses intColor)
QColor
"c" (see colorAbbrev dictionary)
"RGB" (strings may optionally begin with "#")
"RGBA"
"RRGGBB"
"RRGGBBAA"
"""
err = 'Not sure how to make a color from "%s"' % str(args)
if len(args) == 1:
if isinstance(args[0], QtGui.QColor):
return QtGui.QColor(args[0])
elif isinstance(args[0], float):
r = g = b = int(args[0] * 255)
a = 255
elif isinstance(args[0], basestring):
c = args[0]
if c[0] == '#':
c = c[1:]
if len(c) == 1:
(r, g, b, a) = colorAbbrev[c]
if len(c) == 3:
r = int(c[0]*2, 16)
g = int(c[1]*2, 16)
b = int(c[2]*2, 16)
a = 255
elif len(c) == 4:
r = int(c[0]*2, 16)
g = int(c[1]*2, 16)
b = int(c[2]*2, 16)
a = int(c[3]*2, 16)
elif len(c) == 6:
r = int(c[0:2], 16)
g = int(c[2:4], 16)
b = int(c[4:6], 16)
a = 255
elif len(c) == 8:
r = int(c[0:2], 16)
g = int(c[2:4], 16)
b = int(c[4:6], 16)
a = int(c[6:8], 16)
elif hasattr(args[0], '__len__'):
if len(args[0]) == 3:
(r, g, b) = args[0]
a = 255
elif len(args[0]) == 4:
(r, g, b, a) = args[0]
elif len(args[0]) == 2:
return intColor(*args[0])
else:
raise Exception(err)
if type(args[0]) == int:
elif type(args[0]) == int:
return intColor(args[0])
else:
raise Exception(err)
......@@ -74,19 +144,27 @@ def mkColor(*args):
raise Exception(err)
return QtGui.QColor(r, g, b, a)
def colorTuple(c):
return (c.red(), c.blue(), c.green(), c.alpha())
def colorStr(c):
"""Generate a hex string code from a QColor"""
return ('%02x'*4) % (c.red(), c.blue(), c.green(), c.alpha())
return ('%02x'*4) % colorTuple(c)
def intColor(ind, colors=9, values=3, maxValue=255, minValue=150, sat=255):
"""Creates a QColor from a single index. Useful for stepping through a predefined list of colors."""
colors = int(colors)
def intColor(index, hues=9, values=3, maxValue=255, minValue=150, maxHue=360, minHue=0, sat=255):
"""Creates a QColor from a single index. Useful for stepping through a predefined list of colors.
- The argument "index" determines which color from the set will be returned
- All other arguments determine what the set of predefined colors will be
Colors are chosen by cycling across hues while varying the value (brightness). By default, there