from pyqtgraph.Qt import QtGui, QtCore import pyqtgraph.functions as fn from GraphicsObject import GraphicsObject from GraphicsWidget import GraphicsWidget import weakref, collections import numpy as np __all__ = ['TickSliderItem', 'GradientEditorItem'] Gradients = collections.OrderedDict([ ('thermal', {'ticks': [(0.3333, (185, 0, 0, 255)), (0.6666, (255, 220, 0, 255)), (1, (255, 255, 255, 255)), (0, (0, 0, 0, 255))], 'mode': 'rgb'}), ('flame', {'ticks': [(0.2, (7, 0, 220, 255)), (0.5, (236, 0, 134, 255)), (0.8, (246, 246, 0, 255)), (1.0, (255, 255, 255, 255)), (0.0, (0, 0, 0, 255))], 'mode': 'rgb'}), ('yellowy', {'ticks': [(0.0, (0, 0, 0, 255)), (0.2328863796753704, (32, 0, 129, 255)), (0.8362738179251941, (255, 255, 0, 255)), (0.5257586450247, (115, 15, 255, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'} ), ('bipolar', {'ticks': [(0.0, (0, 255, 255, 255)), (1.0, (255, 255, 0, 255)), (0.5, (0, 0, 0, 255)), (0.25, (0, 0, 255, 255)), (0.75, (255, 0, 0, 255))], 'mode': 'rgb'}), ('spectrum', {'ticks': [(1.0, (255, 0, 255, 255)), (0.0, (255, 0, 0, 255))], 'mode': 'hsv'}), ('cyclic', {'ticks': [(0.0, (255, 0, 4, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'hsv'}), ('greyclip', {'ticks': [(0.0, (0, 0, 0, 255)), (0.99, (255, 255, 255, 255)), (1.0, (255, 0, 0, 255))], 'mode': 'rgb'}), ('grey', {'ticks': [(0.0, (0, 0, 0, 255)), (1.0, (255, 255, 255, 255))], 'mode': 'rgb'}), ]) class TickSliderItem(GraphicsWidget): def __init__(self, orientation='bottom', allowAdd=True, **kargs): GraphicsWidget.__init__(self) self.orientation = orientation self.length = 100 self.tickSize = 15 self.ticks = {} self.maxDim = 20 self.allowAdd = allowAdd if 'tickPen' in kargs: self.tickPen = fn.mkPen(kargs['tickPen']) else: self.tickPen = fn.mkPen('w') self.orientations = { 'left': (90, 1, 1), 'right': (90, 1, 1), 'top': (0, 1, -1), 'bottom': (0, 1, 1) } self.setOrientation(orientation) #self.setFrameStyle(QtGui.QFrame.NoFrame | QtGui.QFrame.Plain) #self.setBackgroundRole(QtGui.QPalette.NoRole) #self.setMouseTracking(True) #def boundingRect(self): #return self.mapRectFromParent(self.geometry()).normalized() #def shape(self): ## No idea why this is necessary, but rotated items do not receive clicks otherwise. #p = QtGui.QPainterPath() #p.addRect(self.boundingRect()) #return p def paint(self, p, opt, widget): #p.setPen(fn.mkPen('g', width=3)) #p.drawRect(self.boundingRect()) return def keyPressEvent(self, ev): ev.ignore() def setMaxDim(self, mx=None): if mx is None: mx = self.maxDim else: self.maxDim = mx if self.orientation in ['bottom', 'top']: self.setFixedHeight(mx) self.setMaximumWidth(16777215) else: self.setFixedWidth(mx) self.setMaximumHeight(16777215) def setOrientation(self, ort): self.orientation = ort self.setMaxDim() self.resetTransform() if ort == 'top': self.scale(1, -1) self.translate(0, -self.height()) elif ort == 'left': self.rotate(270) self.scale(1, -1) self.translate(-self.height(), -self.maxDim) elif ort == 'right': self.rotate(270) self.translate(-self.height(), 0) #self.setPos(0, -self.height()) self.translate(self.tickSize/2., 0) def addTick(self, x, color=None, movable=True): if color is None: color = QtGui.QColor(255,255,255) tick = Tick(self, [x*self.length, 0], color, movable, self.tickSize, pen=self.tickPen) self.ticks[tick] = x tick.setParentItem(self) return tick def removeTick(self, tick): del self.ticks[tick] tick.setParentItem(None) if self.scene() is not None: self.scene().removeItem(tick) def tickMoved(self, tick, pos): #print "tick changed" ## Correct position of tick if it has left bounds. newX = min(max(0, pos.x()), self.length) pos.setX(newX) tick.setPos(pos) self.ticks[tick] = float(newX) / self.length def tickClicked(self, tick, ev): if ev.button() == QtCore.Qt.RightButton: self.removeTick(tick) def widgetLength(self): if self.orientation in ['bottom', 'top']: return self.width() else: return self.height() def resizeEvent(self, ev): wlen = max(40, self.widgetLength()) self.setLength(wlen-self.tickSize) self.setOrientation(self.orientation) #bounds = self.scene().itemsBoundingRect() #bounds.setLeft(min(-self.tickSize*0.5, bounds.left())) #bounds.setRight(max(self.length + self.tickSize, bounds.right())) #self.setSceneRect(bounds) #self.fitInView(bounds, QtCore.Qt.KeepAspectRatio) def setLength(self, newLen): for t, x in self.ticks.items(): t.setPos(x * newLen, t.pos().y()) self.length = float(newLen) #def mousePressEvent(self, ev): #QtGui.QGraphicsView.mousePressEvent(self, ev) #self.ignoreRelease = False #for i in self.items(ev.pos()): #if isinstance(i, Tick): #self.ignoreRelease = True #break ##if len(self.items(ev.pos())) > 0: ## Let items handle their own clicks ##self.ignoreRelease = True #def mouseReleaseEvent(self, ev): #QtGui.QGraphicsView.mouseReleaseEvent(self, ev) #if self.ignoreRelease: #return #pos = self.mapToScene(ev.pos()) #if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: #if pos.x() < 0 or pos.x() > self.length: #return #if pos.y() < 0 or pos.y() > self.tickSize: #return #pos.setX(min(max(pos.x(), 0), self.length)) #self.addTick(pos.x()/self.length) #elif ev.button() == QtCore.Qt.RightButton: #self.showMenu(ev) def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton and self.allowAdd: pos = ev.pos() if pos.x() < 0 or pos.x() > self.length: return if pos.y() < 0 or pos.y() > self.tickSize: return pos.setX(min(max(pos.x(), 0), self.length)) self.addTick(pos.x()/self.length) elif ev.button() == QtCore.Qt.RightButton: self.showMenu(ev) #if ev.button() == QtCore.Qt.RightButton: #if self.moving: #ev.accept() #self.setPos(self.startPosition) #self.moving = False #self.sigMoving.emit(self) #self.sigMoved.emit(self) #else: #pass #self.view().tickClicked(self, ev) ###remove def hoverEvent(self, ev): if (not ev.isExit()) and ev.acceptClicks(QtCore.Qt.LeftButton): ev.acceptClicks(QtCore.Qt.RightButton) ## show ghost tick #self.currentPen = fn.mkPen(255, 0,0) #else: #self.currentPen = self.pen #self.update() def showMenu(self, ev): pass def setTickColor(self, tick, color): tick = self.getTick(tick) tick.color = color tick.update() #tick.setBrush(QtGui.QBrush(QtGui.QColor(tick.color))) def setTickValue(self, tick, val): tick = self.getTick(tick) val = min(max(0.0, val), 1.0) x = val * self.length pos = tick.pos() pos.setX(x) tick.setPos(pos) self.ticks[tick] = val def tickValue(self, tick): tick = self.getTick(tick) return self.ticks[tick] def getTick(self, tick): if type(tick) is int: tick = self.listTicks()[tick][0] return tick #def mouseMoveEvent(self, ev): #QtGui.QGraphicsView.mouseMoveEvent(self, ev) def listTicks(self): ticks = self.ticks.items() ticks.sort(lambda a,b: cmp(a[1], b[1])) return ticks class GradientEditorItem(TickSliderItem): sigGradientChanged = QtCore.Signal(object) def __init__(self, *args, **kargs): self.currentTick = None self.currentTickColor = None self.rectSize = 15 self.gradRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, self.rectSize, 100, self.rectSize)) self.backgroundRect = QtGui.QGraphicsRectItem(QtCore.QRectF(0, -self.rectSize, 100, self.rectSize)) self.backgroundRect.setBrush(QtGui.QBrush(QtCore.Qt.DiagCrossPattern)) self.colorMode = 'rgb' TickSliderItem.__init__(self, *args, **kargs) self.colorDialog = QtGui.QColorDialog() self.colorDialog.setOption(QtGui.QColorDialog.ShowAlphaChannel, True) self.colorDialog.setOption(QtGui.QColorDialog.DontUseNativeDialog, True) self.colorDialog.currentColorChanged.connect(self.currentColorChanged) self.colorDialog.rejected.connect(self.currentColorRejected) self.backgroundRect.setParentItem(self) self.gradRect.setParentItem(self) self.setMaxDim(self.rectSize + self.tickSize) self.rgbAction = QtGui.QAction('RGB', self) self.rgbAction.setCheckable(True) self.rgbAction.triggered.connect(lambda: self.setColorMode('rgb')) self.hsvAction = QtGui.QAction('HSV', self) self.hsvAction.setCheckable(True) self.hsvAction.triggered.connect(lambda: self.setColorMode('hsv')) self.menu = QtGui.QMenu() ## build context menu of gradients l = self.length self.length = 100 global Gradients for g in Gradients: px = QtGui.QPixmap(100, 15) p = QtGui.QPainter(px) self.restoreState(Gradients[g]) grad = self.getGradient() brush = QtGui.QBrush(grad) p.fillRect(QtCore.QRect(0, 0, 100, 15), brush) p.end() label = QtGui.QLabel() label.setPixmap(px) label.setContentsMargins(1, 1, 1, 1) act = QtGui.QWidgetAction(self) act.setDefaultWidget(label) act.triggered.connect(self.contextMenuClicked) act.name = g self.menu.addAction(act) self.length = l self.menu.addSeparator() self.menu.addAction(self.rgbAction) self.menu.addAction(self.hsvAction) for t in self.ticks.keys(): self.removeTick(t) self.addTick(0, QtGui.QColor(0,0,0), True) self.addTick(1, QtGui.QColor(255,0,0), True) self.setColorMode('rgb') self.updateGradient() def setOrientation(self, ort): TickSliderItem.setOrientation(self, ort) self.translate(0, self.rectSize) def showMenu(self, ev): self.menu.popup(ev.screenPos().toQPoint()) def contextMenuClicked(self, b=None): global Gradients act = self.sender() self.loadPreset(act.name) def loadPreset(self, name): self.restoreState(Gradients[name]) def setColorMode(self, cm): if cm not in ['rgb', 'hsv']: raise Exception("Unknown color mode %s. Options are 'rgb' and 'hsv'." % str(cm)) try: self.rgbAction.blockSignals(True) self.hsvAction.blockSignals(True) self.rgbAction.setChecked(cm == 'rgb') self.hsvAction.setChecked(cm == 'hsv') finally: self.rgbAction.blockSignals(False) self.hsvAction.blockSignals(False) self.colorMode = cm self.updateGradient() def updateGradient(self): self.gradient = self.getGradient() self.gradRect.setBrush(QtGui.QBrush(self.gradient)) self.sigGradientChanged.emit(self) def setLength(self, newLen): TickSliderItem.setLength(self, newLen) self.backgroundRect.setRect(0, -self.rectSize, newLen, self.rectSize) self.gradRect.setRect(0, -self.rectSize, newLen, self.rectSize) self.updateGradient() def currentColorChanged(self, color): if color.isValid() and self.currentTick is not None: self.setTickColor(self.currentTick, color) self.updateGradient() def currentColorRejected(self): self.setTickColor(self.currentTick, self.currentTickColor) self.updateGradient() def tickClicked(self, tick, ev): if ev.button() == QtCore.Qt.LeftButton: if not tick.colorChangeAllowed: return self.currentTick = tick self.currentTickColor = tick.color self.colorDialog.setCurrentColor(tick.color) self.colorDialog.open() #color = QtGui.QColorDialog.getColor(tick.color, self, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) #if color.isValid(): #self.setTickColor(tick, color) #self.updateGradient() elif ev.button() == QtCore.Qt.RightButton: if not tick.removeAllowed: return if len(self.ticks) > 2: self.removeTick(tick) self.updateGradient() def tickMoved(self, tick, pos): TickSliderItem.tickMoved(self, tick, pos) self.updateGradient() def getGradient(self): g = QtGui.QLinearGradient(QtCore.QPointF(0,0), QtCore.QPointF(self.length,0)) if self.colorMode == 'rgb': ticks = self.listTicks() g.setStops([(x, QtGui.QColor(t.color)) for t,x in ticks]) elif self.colorMode == 'hsv': ## HSV mode is approximated for display by interpolating 10 points between each stop ticks = self.listTicks() stops = [] stops.append((ticks[0][1], ticks[0][0].color)) for i in range(1,len(ticks)): x1 = ticks[i-1][1] x2 = ticks[i][1] dx = (x2-x1) / 10. for j in range(1,10): x = x1 + dx*j stops.append((x, self.getColor(x))) stops.append((x2, self.getColor(x2))) g.setStops(stops) return g def getColor(self, x, toQColor=True): ticks = self.listTicks() if x <= ticks[0][1]: c = ticks[0][0].color if toQColor: return QtGui.QColor(c) # always copy colors before handing them out else: return (c.red(), c.green(), c.blue(), c.alpha()) if x >= ticks[-1][1]: c = ticks[-1][0].color if toQColor: return QtGui.QColor(c) # always copy colors before handing them out else: return (c.red(), c.green(), c.blue(), c.alpha()) x2 = ticks[0][1] for i in range(1,len(ticks)): x1 = x2 x2 = ticks[i][1] if x1 <= x and x2 >= x: break dx = (x2-x1) if dx == 0: f = 0. else: f = (x-x1) / dx c1 = ticks[i-1][0].color c2 = ticks[i][0].color if self.colorMode == 'rgb': r = c1.red() * (1.-f) + c2.red() * f g = c1.green() * (1.-f) + c2.green() * f b = c1.blue() * (1.-f) + c2.blue() * f a = c1.alpha() * (1.-f) + c2.alpha() * f if toQColor: return QtGui.QColor(r, g, b,a) else: return (r,g,b,a) elif self.colorMode == 'hsv': h1,s1,v1,_ = c1.getHsv() h2,s2,v2,_ = c2.getHsv() h = h1 * (1.-f) + h2 * f s = s1 * (1.-f) + s2 * f v = v1 * (1.-f) + v2 * f c = QtGui.QColor() c.setHsv(h,s,v) if toQColor: return c else: return (c.red(), c.green(), c.blue(), c.alpha()) def getLookupTable(self, nPts, alpha=True): """Return an RGB/A lookup table.""" if alpha: table = np.empty((nPts,4), dtype=np.ubyte) else: table = np.empty((nPts,3), dtype=np.ubyte) for i in range(nPts): x = float(i)/(nPts-1) color = self.getColor(x, toQColor=False) table[i] = color[:table.shape[1]] return table def isLookupTrivial(self): """Return true if the gradient has exactly two stops in it: black at 0.0 and white at 1.0""" ticks = self.listTicks() if len(ticks) != 2: return False if ticks[0][1] != 0.0 or ticks[1][1] != 1.0: return False c1 = fn.colorTuple(ticks[0][0].color) c2 = fn.colorTuple(ticks[1][0].color) if c1 != (0,0,0,255) or c2 != (255,255,255,255): return False return True def mouseReleaseEvent(self, ev): TickSliderItem.mouseReleaseEvent(self, ev) self.updateGradient() def addTick(self, x, color=None, movable=True): if color is None: color = self.getColor(x) t = TickSliderItem.addTick(self, x, color=color, movable=movable) t.colorChangeAllowed = True t.removeAllowed = True return t def saveState(self): ticks = [] for t in self.ticks: c = t.color ticks.append((self.ticks[t], (c.red(), c.green(), c.blue(), c.alpha()))) state = {'mode': self.colorMode, 'ticks': ticks} return state def restoreState(self, state): self.setColorMode(state['mode']) for t in self.ticks.keys(): self.removeTick(t) for t in state['ticks']: c = QtGui.QColor(*t[1]) self.addTick(t[0], c) self.updateGradient() class Tick(GraphicsObject): sigMoving = QtCore.Signal(object) sigMoved = QtCore.Signal(object) def __init__(self, view, pos, color, movable=True, scale=10, pen='w'): self.movable = movable self.moving = False self.view = weakref.ref(view) self.scale = scale self.color = color self.pen = fn.mkPen(pen) self.hoverPen = fn.mkPen(255,255,0) self.currentPen = self.pen self.pg = QtGui.QPainterPath(QtCore.QPointF(0,0)) self.pg.lineTo(QtCore.QPointF(-scale/3**0.5, scale)) self.pg.lineTo(QtCore.QPointF(scale/3**0.5, scale)) self.pg.closeSubpath() GraphicsObject.__init__(self) self.setPos(pos[0], pos[1]) if self.movable: self.setZValue(1) else: self.setZValue(0) def boundingRect(self): return self.pg.boundingRect() def shape(self): return self.pg def paint(self, p, *args): p.setRenderHints(QtGui.QPainter.Antialiasing) p.fillPath(self.pg, fn.mkBrush(self.color)) p.setPen(self.currentPen) p.drawPath(self.pg) def mouseDragEvent(self, ev): if self.movable and 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 newPos = self.cursorOffset + self.mapToParent(ev.pos()) newPos.setY(self.pos().y()) self.setPos(newPos) self.view().tickMoved(self, newPos) self.sigMoving.emit(self) if ev.isFinish(): self.moving = False self.sigMoved.emit(self) def mouseClickEvent(self, ev): if ev.button() == QtCore.Qt.RightButton and self.moving: ev.accept() self.setPos(self.startPosition) self.view().tickMoved(self, self.startPosition) self.moving = False self.sigMoving.emit(self) self.sigMoved.emit(self) else: self.view().tickClicked(self, ev) ##remove def hoverEvent(self, ev): if (not ev.isExit()) and ev.acceptDrags(QtCore.Qt.LeftButton): ev.acceptClicks(QtCore.Qt.LeftButton) ev.acceptClicks(QtCore.Qt.RightButton) self.currentPen = self.hoverPen else: self.currentPen = self.pen self.update() #def mouseMoveEvent(self, ev): ##print self, "move", ev.scenePos() #if not self.movable: #return #if not ev.buttons() & QtCore.Qt.LeftButton: #return #newPos = ev.scenePos() + self.mouseOffset #newPos.setY(self.pos().y()) ##newPos.setX(min(max(newPos.x(), 0), 100)) #self.setPos(newPos) #self.view().tickMoved(self, newPos) #self.movedSincePress = True ##self.emit(QtCore.SIGNAL('tickChanged'), self) #ev.accept() #def mousePressEvent(self, ev): #self.movedSincePress = False #if ev.button() == QtCore.Qt.LeftButton: #ev.accept() #self.mouseOffset = self.pos() - ev.scenePos() #self.pressPos = ev.scenePos() #elif ev.button() == QtCore.Qt.RightButton: #ev.accept() ##if self.endTick: ##return ##self.view.tickChanged(self, delete=True) #def mouseReleaseEvent(self, ev): ##print self, "release", ev.scenePos() #if not self.movedSincePress: #self.view().tickClicked(self, ev) ##if ev.button() == QtCore.Qt.LeftButton and ev.scenePos() == self.pressPos: ##color = QtGui.QColorDialog.getColor(self.color, None, "Select Color", QtGui.QColorDialog.ShowAlphaChannel) ##if color.isValid(): ##self.color = color ##self.setBrush(QtGui.QBrush(QtGui.QColor(self.color))) ###self.emit(QtCore.SIGNAL('tickChanged'), self) ##self.view.tickChanged(self)