forked from M-Labs/artiq
scanwidget: rewrite, pending drawing area (3de1505)
This commit is contained in:
parent
0e1f75ec49
commit
d858ad4180
@ -158,8 +158,6 @@ class _RangeScan(LayoutWidget):
|
|||||||
scanner.setMinimumSize(150, 0)
|
scanner.setMinimumSize(150, 0)
|
||||||
scanner.setSizePolicy(QtWidgets.QSizePolicy(
|
scanner.setSizePolicy(QtWidgets.QSizePolicy(
|
||||||
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed))
|
QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed))
|
||||||
disable_scroll_wheel(scanner.axis)
|
|
||||||
disable_scroll_wheel(scanner.slider)
|
|
||||||
self.addWidget(scanner, 0, 0, -1, 1)
|
self.addWidget(scanner, 0, 0, -1, 1)
|
||||||
|
|
||||||
self.min = ScientificSpinBox()
|
self.min = ScientificSpinBox()
|
||||||
@ -191,17 +189,17 @@ class _RangeScan(LayoutWidget):
|
|||||||
|
|
||||||
def update_npoints(value):
|
def update_npoints(value):
|
||||||
state["npoints"] = value
|
state["npoints"] = value
|
||||||
scanner.setNumPoints(value)
|
scanner.setNum(value)
|
||||||
|
|
||||||
scanner.sigStartMoved.connect(self.min.setValue)
|
scanner.startChanged.connect(self.min.setValue)
|
||||||
scanner.sigNumChanged.connect(self.npoints.setValue)
|
scanner.numChanged.connect(self.npoints.setValue)
|
||||||
scanner.sigStopMoved.connect(self.max.setValue)
|
scanner.stopChanged.connect(self.max.setValue)
|
||||||
self.min.valueChanged.connect(update_min)
|
self.min.valueChanged.connect(update_min)
|
||||||
self.npoints.valueChanged.connect(update_npoints)
|
self.npoints.valueChanged.connect(update_npoints)
|
||||||
self.max.valueChanged.connect(update_max)
|
self.max.valueChanged.connect(update_max)
|
||||||
self.min.setValue(state["min"]/scale)
|
scanner.setStart(state["min"]/scale)
|
||||||
self.npoints.setValue(state["npoints"])
|
scanner.setNum(state["npoints"])
|
||||||
self.max.setValue(state["max"]/scale)
|
scanner.setStop(state["max"]/scale)
|
||||||
apply_properties(self.min)
|
apply_properties(self.min)
|
||||||
apply_properties(self.max)
|
apply_properties(self.max)
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||||
from numpy import linspace
|
import numpy as np
|
||||||
|
|
||||||
from .ticker import Ticker
|
from .ticker import Ticker
|
||||||
|
|
||||||
@ -9,119 +9,111 @@ from .ticker import Ticker
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ScanAxis(QtWidgets.QWidget):
|
class ScanWidget(QtWidgets.QSlider):
|
||||||
def __init__(self):
|
startChanged = QtCore.pyqtSignal(float)
|
||||||
QtWidgets.QWidget.__init__(self)
|
stopChanged = QtCore.pyqtSignal(float)
|
||||||
self.proxy = None
|
numChanged = QtCore.pyqtSignal(int)
|
||||||
self.sizePolicy().setControlType(QtWidgets.QSizePolicy.ButtonBox)
|
|
||||||
self.ticker = Ticker()
|
|
||||||
qfm = QtGui.QFontMetrics(QtGui.QFont())
|
|
||||||
lineSpacing = qfm.lineSpacing()
|
|
||||||
descent = qfm.descent()
|
|
||||||
self.setMinimumHeight(2*lineSpacing + descent + 5 + 5)
|
|
||||||
|
|
||||||
def paintEvent(self, ev):
|
def __init__(self, zoomFactor=1.05, zoomMargin=.1, dynamicRange=1e9):
|
||||||
painter = QtGui.QPainter(self)
|
|
||||||
qfm = QtGui.QFontMetrics(painter.font())
|
|
||||||
avgCharWidth = qfm.averageCharWidth()
|
|
||||||
lineSpacing = qfm.lineSpacing()
|
|
||||||
descent = qfm.descent()
|
|
||||||
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
|
||||||
# The center of the slider handles should reflect what's displayed
|
|
||||||
# on the spinboxes.
|
|
||||||
painter.translate(self.proxy.slider.handleWidth()/2, self.height() - 5)
|
|
||||||
painter.drawLine(0, 0, self.width(), 0)
|
|
||||||
realLeft = self.proxy.pixelToReal(0)
|
|
||||||
realRight = self.proxy.pixelToReal(self.width())
|
|
||||||
ticks, prefix, labels = self.ticker(realLeft, realRight)
|
|
||||||
painter.drawText(0, -5-descent-lineSpacing, prefix)
|
|
||||||
|
|
||||||
pen = QtGui.QPen()
|
|
||||||
pen.setWidth(2)
|
|
||||||
painter.setPen(pen)
|
|
||||||
|
|
||||||
for t, l in zip(ticks, labels):
|
|
||||||
t = self.proxy.realToPixel(t)
|
|
||||||
painter.drawLine(t, 0, t, -5)
|
|
||||||
painter.drawText(t - len(l)/2*avgCharWidth, -5-descent, l)
|
|
||||||
|
|
||||||
sliderStartPixel = self.proxy.realToPixel(self.proxy.realStart)
|
|
||||||
sliderStopPixel = self.proxy.realToPixel(self.proxy.realStop)
|
|
||||||
pixels = linspace(sliderStartPixel, sliderStopPixel,
|
|
||||||
self.proxy.numPoints)
|
|
||||||
for p in pixels:
|
|
||||||
p_int = int(p)
|
|
||||||
painter.drawLine(p_int, 0, p_int, 5)
|
|
||||||
ev.accept()
|
|
||||||
|
|
||||||
|
|
||||||
# Basic ideas from https://gist.github.com/Riateche/27e36977f7d5ea72cf4f
|
|
||||||
class ScanSlider(QtWidgets.QSlider):
|
|
||||||
sigStartMoved = QtCore.pyqtSignal(int)
|
|
||||||
sigStopMoved = QtCore.pyqtSignal(int)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
QtWidgets.QSlider.__init__(self, QtCore.Qt.Horizontal)
|
QtWidgets.QSlider.__init__(self, QtCore.Qt.Horizontal)
|
||||||
self.startVal = None
|
self.zoomMargin = zoomMargin
|
||||||
self.stopVal = None
|
self.dynamicRange = dynamicRange
|
||||||
self.offset = None
|
self.zoomFactor = zoomFactor
|
||||||
self.position = None
|
|
||||||
self.pressed = None
|
|
||||||
|
|
||||||
self.setRange(0, (1 << 15) - 1)
|
self.ticker = Ticker()
|
||||||
|
|
||||||
# We need fake sliders to keep around so that we can dynamically
|
self.menu = QtWidgets.QMenu(self)
|
||||||
# set the stylesheets for drawing each slider later. See paintEvent.
|
action = QtWidgets.QAction("&View Range", self)
|
||||||
# QPalettes would be nicer to use, since palette entries can be set
|
action.triggered.connect(self.viewRange)
|
||||||
# individually for each slider handle, but Windows 7 does not
|
self.menu.addAction(action)
|
||||||
# use them. This seems to be the only way to override the colors
|
action = QtWidgets.QAction("&Snap Range", self)
|
||||||
# regardless of platform.
|
action.triggered.connect(self.snapRange)
|
||||||
self.dummyStartSlider = QtWidgets.QSlider()
|
self.menu.addAction(action)
|
||||||
self.dummyStopSlider = QtWidgets.QSlider()
|
|
||||||
self.dummyStartSlider.setStyleSheet(
|
|
||||||
"QSlider::handle {background:blue}")
|
|
||||||
self.dummyStopSlider.setStyleSheet(
|
|
||||||
"QSlider::handle {background:red}")
|
|
||||||
|
|
||||||
def pixelPosToRangeValue(self, pos):
|
self._startSlider = QtWidgets.QSlider()
|
||||||
|
self._startSlider.setStyleSheet("QSlider::handle {background:blue}")
|
||||||
|
self._stopSlider = QtWidgets.QSlider()
|
||||||
|
self._stopSlider.setStyleSheet("QSlider::handle {background:red}")
|
||||||
|
|
||||||
|
self.setRange(0, 4095)
|
||||||
|
|
||||||
|
self._start, self._stop, self._num = None, None, None
|
||||||
|
self._axisView, self._sliderView = None, None
|
||||||
|
self._offset, self._pressed = None, None
|
||||||
|
|
||||||
|
def contextMenuEvent(self, ev):
|
||||||
|
self.menu.popup(ev.globalPos())
|
||||||
|
|
||||||
|
def _axisToPixel(self, val):
|
||||||
|
a, b = self._axisView
|
||||||
|
return a + val*b
|
||||||
|
|
||||||
|
def _pixelToAxis(self, val):
|
||||||
|
a, b = self._axisView
|
||||||
|
return (val - a)/b
|
||||||
|
|
||||||
|
def _setView(self, axis_left, axis_scale):
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
opt = QtWidgets.QStyleOptionSlider()
|
||||||
self.initStyleOption(opt)
|
self.initStyleOption(opt)
|
||||||
gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
g = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
||||||
QtWidgets.QStyle.SC_SliderGroove,
|
QtWidgets.QStyle.SC_SliderGroove,
|
||||||
self)
|
self)
|
||||||
rangeVal = QtWidgets.QStyle.sliderValueFromPosition(
|
h = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
||||||
self.minimum(), self.maximum(), pos - gr.x(),
|
QtWidgets.QStyle.SC_SliderHandle,
|
||||||
self.effectiveWidth(), opt.upsideDown)
|
self)
|
||||||
return rangeVal
|
slider_left = g.x() + h.width()/2
|
||||||
|
slider_scale = (self.maximum() - self.minimum())/(
|
||||||
|
g.width() - h.width())
|
||||||
|
|
||||||
def rangeValueToPixelPos(self, val):
|
self._axisView = axis_left, axis_scale
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
self._sliderView = ((axis_left - slider_left)*slider_scale,
|
||||||
self.initStyleOption(opt)
|
axis_scale*slider_scale)
|
||||||
pixel = QtWidgets.QStyle.sliderPositionFromValue(
|
self.update()
|
||||||
self.minimum(), self.maximum(), val, self.effectiveWidth(),
|
|
||||||
opt.upsideDown)
|
|
||||||
return pixel
|
|
||||||
|
|
||||||
# When calculating conversions to/from pixel space, not all of the slider's
|
def setStart(self, val):
|
||||||
# width is actually usable, because the slider handle has a nonzero width.
|
if self._start == val:
|
||||||
# We use this function as a helper when the axis needs slider information.
|
return
|
||||||
def handleWidth(self):
|
self._start = val
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
self.update()
|
||||||
self.initStyleOption(opt)
|
self.startChanged.emit(val)
|
||||||
sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
|
||||||
QtWidgets.QStyle.SC_SliderHandle,
|
|
||||||
self)
|
|
||||||
return sr.width()
|
|
||||||
|
|
||||||
def effectiveWidth(self):
|
def setStop(self, val):
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
if self._stop == val:
|
||||||
self.initStyleOption(opt)
|
return
|
||||||
gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
self._stop = val
|
||||||
QtWidgets.QStyle.SC_SliderGroove,
|
self.update()
|
||||||
self)
|
self.stopChanged.emit(val)
|
||||||
return gr.width() - self.handleWidth()
|
|
||||||
|
def setNum(self, val):
|
||||||
|
if self._num == val:
|
||||||
|
return
|
||||||
|
self._num = val
|
||||||
|
self.update()
|
||||||
|
self.numChanged.emit(val)
|
||||||
|
|
||||||
|
def viewRange(self):
|
||||||
|
center = (self._stop + self._start)/2
|
||||||
|
scale = self.width()*(1 - 2*self.zoomMargin)
|
||||||
|
if self._stop != self._start:
|
||||||
|
scale /= abs(self._stop - self._start)
|
||||||
|
if center:
|
||||||
|
scale = min(scale, self.dynamicRange/abs(center))
|
||||||
|
else:
|
||||||
|
scale = self.dynamicRange
|
||||||
|
if center:
|
||||||
|
scale /= abs(center)
|
||||||
|
left = self.width()/2 - center*scale
|
||||||
|
self._setView(left, scale)
|
||||||
|
|
||||||
|
def snapRange(self):
|
||||||
|
self.setStart(self._pixelToAxis(self.zoomMargin*self.width()))
|
||||||
|
self.setStop(self._pixelToAxis((1 - self.zoomMargin)*self.width()))
|
||||||
|
|
||||||
def _getStyleOptionSlider(self, val):
|
def _getStyleOptionSlider(self, val):
|
||||||
|
a, b = self._sliderView
|
||||||
|
val = a + val*b
|
||||||
|
if not (self.minimum() <= val <= self.maximum()):
|
||||||
|
return None
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
opt = QtWidgets.QStyleOptionSlider()
|
||||||
self.initStyleOption(opt)
|
self.initStyleOption(opt)
|
||||||
opt.sliderPosition = val
|
opt.sliderPosition = val
|
||||||
@ -130,284 +122,131 @@ class ScanSlider(QtWidgets.QSlider):
|
|||||||
return opt
|
return opt
|
||||||
|
|
||||||
def _hitHandle(self, pos, val):
|
def _hitHandle(self, pos, val):
|
||||||
# If chosen slider at edge, treat it as non-interactive.
|
|
||||||
if not (self.minimum() < val < self.maximum()):
|
|
||||||
return False
|
|
||||||
opt = self._getStyleOptionSlider(val)
|
opt = self._getStyleOptionSlider(val)
|
||||||
|
if not opt:
|
||||||
|
return False
|
||||||
control = self.style().hitTestComplexControl(
|
control = self.style().hitTestComplexControl(
|
||||||
QtWidgets.QStyle.CC_Slider, opt, pos, self)
|
QtWidgets.QStyle.CC_Slider, opt, pos, self)
|
||||||
if control != QtWidgets.QStyle.SC_SliderHandle:
|
if control != QtWidgets.QStyle.SC_SliderHandle:
|
||||||
return False
|
return False
|
||||||
sr = self.style().subControlRect(
|
sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
||||||
QtWidgets.QStyle.CC_Slider, opt,
|
QtWidgets.QStyle.SC_SliderHandle,
|
||||||
QtWidgets.QStyle.SC_SliderHandle, self)
|
self)
|
||||||
self.offset = pos.x() - sr.topLeft().x()
|
self._offset = pos.x() - sr.center().x()
|
||||||
self.setSliderDown(True)
|
self.setSliderDown(True)
|
||||||
# Needed?
|
|
||||||
self.update(sr)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def setStartPosition(self, val):
|
|
||||||
if val == self.startVal:
|
|
||||||
return
|
|
||||||
self.startVal = val
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def setStopPosition(self, val):
|
|
||||||
if val == self.stopVal:
|
|
||||||
return
|
|
||||||
self.stopVal = val
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
def mousePressEvent(self, ev):
|
def mousePressEvent(self, ev):
|
||||||
if ev.buttons() ^ ev.button():
|
if ev.buttons() ^ ev.button():
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
return
|
return
|
||||||
# Prefer stopVal in the default case.
|
if self._hitHandle(ev.pos(), self._stop):
|
||||||
if self._hitHandle(ev.pos(), self.stopVal):
|
self._pressed = "stop"
|
||||||
self.pressed = "stop"
|
elif self._hitHandle(ev.pos(), self._start):
|
||||||
elif self._hitHandle(ev.pos(), self.startVal):
|
self._pressed = "start"
|
||||||
self.pressed = "start"
|
|
||||||
else:
|
else:
|
||||||
self.pressed = None
|
self._pressed = None
|
||||||
ev.accept()
|
|
||||||
|
|
||||||
def mouseMoveEvent(self, ev):
|
def mouseMoveEvent(self, ev):
|
||||||
if not self.pressed:
|
if not self._pressed:
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
return
|
return
|
||||||
|
val = self._pixelToAxis(ev.pos().x() - self._offset)
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
if self._pressed == "stop":
|
||||||
self.initStyleOption(opt)
|
self._stop = val
|
||||||
|
self.update()
|
||||||
# This code seems to be needed so that returning the slider to the
|
|
||||||
# previous position is honored if a drag distance is exceeded.
|
|
||||||
m = self.style().pixelMetric(QtWidgets.QStyle.PM_MaximumDragDistance,
|
|
||||||
opt, self)
|
|
||||||
newPos = self.pixelPosToRangeValue(ev.pos().x() - self.offset)
|
|
||||||
|
|
||||||
if m >= 0:
|
|
||||||
r = self.rect().adjusted(-m, -m, m, m)
|
|
||||||
if not r.contains(ev.pos()):
|
|
||||||
newPos = self.position
|
|
||||||
|
|
||||||
if self.pressed == "start":
|
|
||||||
self.setStartPosition(newPos)
|
|
||||||
if self.hasTracking():
|
if self.hasTracking():
|
||||||
self.sigStartMoved.emit(self.startVal)
|
self.stopChanged.emit(val)
|
||||||
elif self.pressed == "stop":
|
elif self._pressed == "start":
|
||||||
self.setStopPosition(newPos)
|
self._start = val
|
||||||
|
self.update()
|
||||||
if self.hasTracking():
|
if self.hasTracking():
|
||||||
self.sigStopMoved.emit(self.stopVal)
|
self.startChanged.emit(val)
|
||||||
|
|
||||||
ev.accept()
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
QtWidgets.QSlider.mouseReleaseEvent(self, ev)
|
QtWidgets.QSlider.mouseReleaseEvent(self, ev)
|
||||||
self.setSliderDown(False) # AbstractSlider needs this
|
self.setSliderDown(False)
|
||||||
if not self.hasTracking():
|
if self._pressed == "start":
|
||||||
if self.pressed == "start":
|
self.startChanged.emit(self._start)
|
||||||
self.sigStartMoved.emit(self.startVal)
|
elif self._pressed == "stop":
|
||||||
elif self.pressed == "stop":
|
self.stopChanged.emit(self._stop)
|
||||||
self.sigStopMoved.emit(self.stopVal)
|
self._pressed = None
|
||||||
self.pressed = None
|
|
||||||
|
|
||||||
def paintEvent(self, ev):
|
def _zoom(self, z, x):
|
||||||
# Use the pre-parsed, styled sliders.
|
a, b = self._axisView
|
||||||
startPainter = QtWidgets.QStylePainter(self, self.dummyStartSlider)
|
scale = z*b
|
||||||
stopPainter = QtWidgets.QStylePainter(self, self.dummyStopSlider)
|
left = x - z*(x - a)
|
||||||
# Only draw handles that are not railed
|
if z > 1 and abs(left - self.width()/2) > self.dynamicRange:
|
||||||
if self.minimum() < self.startVal < self.maximum():
|
|
||||||
opt = self._getStyleOptionSlider(self.startVal)
|
|
||||||
startPainter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
|
|
||||||
if self.minimum() < self.stopVal < self.maximum():
|
|
||||||
opt = self._getStyleOptionSlider(self.stopVal)
|
|
||||||
stopPainter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
|
|
||||||
|
|
||||||
|
|
||||||
# real (Sliders) => pixel (one pixel movement of sliders would increment by X)
|
|
||||||
# => range (minimum granularity that sliders understand).
|
|
||||||
class ScanWidget(QtWidgets.QWidget):
|
|
||||||
sigStartMoved = QtCore.pyqtSignal(float)
|
|
||||||
sigStopMoved = QtCore.pyqtSignal(float)
|
|
||||||
sigNumChanged = QtCore.pyqtSignal(int)
|
|
||||||
|
|
||||||
def __init__(self, zoomFactor=1.05, zoomMargin=.1, dynamicRange=1e9):
|
|
||||||
QtWidgets.QWidget.__init__(self)
|
|
||||||
self.slider = slider = ScanSlider()
|
|
||||||
self.axis = axis = ScanAxis()
|
|
||||||
axis.proxy = self
|
|
||||||
|
|
||||||
# Layout.
|
|
||||||
layout = QtWidgets.QVBoxLayout()
|
|
||||||
layout.setSpacing(0)
|
|
||||||
layout.addWidget(axis)
|
|
||||||
layout.addWidget(slider)
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
# Context menu entries
|
|
||||||
self.menu = QtWidgets.QMenu(self)
|
|
||||||
viewRangeAct = QtWidgets.QAction("&View Range", self)
|
|
||||||
viewRangeAct.triggered.connect(self.viewRange)
|
|
||||||
self.menu.addAction(viewRangeAct)
|
|
||||||
snapRangeAct = QtWidgets.QAction("&Snap Range", self)
|
|
||||||
snapRangeAct.triggered.connect(self.snapRange)
|
|
||||||
self.menu.addAction(snapRangeAct)
|
|
||||||
|
|
||||||
self.realStart = None
|
|
||||||
self.realStop = None
|
|
||||||
self.numPoints = None
|
|
||||||
self.zoomMargin = zoomMargin
|
|
||||||
self.dynamicRange = dynamicRange
|
|
||||||
self.zoomFactor = zoomFactor
|
|
||||||
|
|
||||||
self.realToPixelTransform = -self.axis.width()/2, 1.
|
|
||||||
|
|
||||||
# Connect event observers.
|
|
||||||
axis.installEventFilter(self)
|
|
||||||
slider.installEventFilter(self)
|
|
||||||
slider.sigStopMoved.connect(self._handleStopMoved)
|
|
||||||
slider.sigStartMoved.connect(self._handleStartMoved)
|
|
||||||
|
|
||||||
def contextMenuEvent(self, ev):
|
|
||||||
self.menu.popup(ev.globalPos())
|
|
||||||
|
|
||||||
# pixel vals for sliders: 0 to slider_width - 1
|
|
||||||
def realToPixel(self, val):
|
|
||||||
a, b = self.realToPixelTransform
|
|
||||||
rawVal = b*(val - a)
|
|
||||||
# Clamp pixel values to 32 bits, b/c Qt will otherwise wrap values.
|
|
||||||
rawVal = min(max(-(1 << 31), rawVal), (1 << 31) - 1)
|
|
||||||
return rawVal
|
|
||||||
|
|
||||||
def pixelToReal(self, val):
|
|
||||||
a, b = self.realToPixelTransform
|
|
||||||
return val/b + a
|
|
||||||
|
|
||||||
def rangeToReal(self, val):
|
|
||||||
pixelVal = self.slider.rangeValueToPixelPos(val)
|
|
||||||
return self.pixelToReal(pixelVal)
|
|
||||||
|
|
||||||
def realToRange(self, val):
|
|
||||||
pixelVal = self.realToPixel(val)
|
|
||||||
return self.slider.pixelPosToRangeValue(pixelVal)
|
|
||||||
|
|
||||||
def setView(self, left, scale):
|
|
||||||
self.realToPixelTransform = left, scale
|
|
||||||
sliderX = self.realToRange(self.realStop)
|
|
||||||
self.slider.setStopPosition(sliderX)
|
|
||||||
sliderX = self.realToRange(self.realStart)
|
|
||||||
self.slider.setStartPosition(sliderX)
|
|
||||||
self.axis.update()
|
|
||||||
|
|
||||||
def setStop(self, val):
|
|
||||||
if self.realStop == val:
|
|
||||||
return
|
return
|
||||||
sliderX = self.realToRange(val)
|
self._setView(left, scale)
|
||||||
self.slider.setStopPosition(sliderX)
|
|
||||||
self.realStop = val
|
|
||||||
self.axis.update() # Number of points ticks changed positions.
|
|
||||||
self.sigStopMoved.emit(val)
|
|
||||||
|
|
||||||
def setStart(self, val):
|
|
||||||
if self.realStart == val:
|
|
||||||
return
|
|
||||||
sliderX = self.realToRange(val)
|
|
||||||
self.slider.setStartPosition(sliderX)
|
|
||||||
self.realStart = val
|
|
||||||
self.axis.update()
|
|
||||||
self.sigStartMoved.emit(val)
|
|
||||||
|
|
||||||
def setNumPoints(self, val):
|
|
||||||
if self.numPoints == val:
|
|
||||||
return
|
|
||||||
self.numPoints = val
|
|
||||||
self.axis.update()
|
|
||||||
self.sigNumChanged.emit(val)
|
|
||||||
|
|
||||||
def viewRange(self):
|
|
||||||
newScale = self.slider.effectiveWidth()/abs(
|
|
||||||
self.realStop - self.realStart)
|
|
||||||
newScale *= 1 - 2*self.zoomMargin
|
|
||||||
newCenter = (self.realStop + self.realStart)/2
|
|
||||||
if newCenter:
|
|
||||||
newScale = min(newScale, self.dynamicRange/abs(newCenter))
|
|
||||||
newLeft = newCenter - self.slider.effectiveWidth()/2/newScale
|
|
||||||
self.setView(newLeft, newScale)
|
|
||||||
|
|
||||||
def snapRange(self):
|
|
||||||
lowRange = self.zoomMargin
|
|
||||||
highRange = 1 - self.zoomMargin
|
|
||||||
newStart = self.pixelToReal(lowRange * self.slider.effectiveWidth())
|
|
||||||
newStop = self.pixelToReal(highRange * self.slider.effectiveWidth())
|
|
||||||
self.setStart(newStart)
|
|
||||||
self.setStop(newStop)
|
|
||||||
|
|
||||||
def _handleStartMoved(self, rangeVal):
|
|
||||||
val = self.rangeToReal(rangeVal)
|
|
||||||
self.realStart = val
|
|
||||||
self.axis.update()
|
|
||||||
self.sigStartMoved.emit(val)
|
|
||||||
|
|
||||||
def _handleStopMoved(self, rangeVal):
|
|
||||||
val = self.rangeToReal(rangeVal)
|
|
||||||
self.realStop = val
|
|
||||||
self.axis.update()
|
|
||||||
self.sigStopMoved.emit(val)
|
|
||||||
|
|
||||||
def _handleZoom(self, zoomFactor, mouseXPos):
|
|
||||||
newScale = self.realToPixelTransform[1] * zoomFactor
|
|
||||||
refReal = self.pixelToReal(mouseXPos)
|
|
||||||
newLeft = refReal - mouseXPos/newScale
|
|
||||||
newZero = newLeft*newScale + self.slider.effectiveWidth()/2
|
|
||||||
if zoomFactor > 1 and abs(newZero) > self.dynamicRange:
|
|
||||||
return
|
|
||||||
self.setView(newLeft, newScale)
|
|
||||||
|
|
||||||
def wheelEvent(self, ev):
|
def wheelEvent(self, ev):
|
||||||
y = ev.angleDelta().y()
|
y = ev.angleDelta().y()/120.
|
||||||
if ev.modifiers() & QtCore.Qt.ShiftModifier:
|
if ev.modifiers() & QtCore.Qt.ShiftModifier:
|
||||||
# If shift+scroll, modify number of points.
|
|
||||||
# TODO: This is not perfect. For high-resolution touchpads you
|
|
||||||
# get many small events with y < 120 which should accumulate.
|
|
||||||
# That would also match the wheel behavior of an integer
|
|
||||||
# spinbox.
|
|
||||||
z = int(y / 120.)
|
|
||||||
if z:
|
|
||||||
self.setNumPoints(max(1, self.numPoints + z))
|
|
||||||
ev.accept()
|
|
||||||
elif ev.modifiers() & QtCore.Qt.ControlModifier:
|
|
||||||
# Remove the slider-handle shift correction, b/c none of the
|
|
||||||
# other widgets know about it. If we have the mouse directly
|
|
||||||
# over a tick during a zoom, it should appear as if we are
|
|
||||||
# doing zoom relative to the ticks which live in axis
|
|
||||||
# pixel-space, not slider pixel-space.
|
|
||||||
if y:
|
if y:
|
||||||
z = self.zoomFactor**(y / 120.)
|
self.setNum(max(1, self._num + y))
|
||||||
self._handleZoom(z, ev.x() - self.slider.handleWidth()/2)
|
elif ev.modifiers() & QtCore.Qt.ControlModifier:
|
||||||
ev.accept()
|
if y:
|
||||||
|
self._zoom(self.zoomFactor**y, ev.x())
|
||||||
else:
|
else:
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
if ev.oldSize().isValid():
|
if not ev.oldSize().isValid():
|
||||||
oldLeft = self.pixelToReal(0)
|
|
||||||
refWidth = ev.oldSize().width() - self.slider.handleWidth()
|
|
||||||
refRight = self.pixelToReal(refWidth)
|
|
||||||
newWidth = ev.size().width() - self.slider.handleWidth()
|
|
||||||
newScale = newWidth/(refRight - oldLeft)
|
|
||||||
center = (self.realStop + self.realStart)/2
|
|
||||||
if center:
|
|
||||||
newScale = min(newScale, self.dynamicRange/abs(center))
|
|
||||||
self.setView(oldLeft, newScale)
|
|
||||||
else:
|
|
||||||
self.viewRange()
|
self.viewRange()
|
||||||
|
return
|
||||||
|
a, b = self._axisView
|
||||||
|
scale = b*ev.size().width()/ev.oldSize().width()
|
||||||
|
center = (self._stop + self._start)/2
|
||||||
|
if center:
|
||||||
|
scale = min(scale, self.dynamicRange/abs(center))
|
||||||
|
left = a*scale/b
|
||||||
|
self._setView(left, scale)
|
||||||
|
|
||||||
def eventFilter(self, obj, ev):
|
def paintEvent(self, ev):
|
||||||
if ev.type() == QtCore.QEvent.Wheel:
|
self._paintSliders()
|
||||||
ev.ignore()
|
self._paintAxis()
|
||||||
return True
|
|
||||||
if ev.type() == QtCore.QEvent.Resize:
|
def _paintAxis(self):
|
||||||
ev.ignore()
|
painter = QtGui.QPainter(self)
|
||||||
return True
|
qfm = QtGui.QFontMetrics(painter.font())
|
||||||
return False
|
avgCharWidth = qfm.averageCharWidth()
|
||||||
|
lineSpacing = qfm.lineSpacing()
|
||||||
|
descent = qfm.descent()
|
||||||
|
ascent = qfm.ascent()
|
||||||
|
height = qfm.height()
|
||||||
|
# painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||||
|
|
||||||
|
# TODO: make drawable area big enough and move axis higher
|
||||||
|
painter.translate(0, ascent - 15)
|
||||||
|
ticks, prefix, labels = self.ticker(self._pixelToAxis(0),
|
||||||
|
self._pixelToAxis(self.width()))
|
||||||
|
painter.drawText(0, 0, prefix)
|
||||||
|
|
||||||
|
pen = QtGui.QPen()
|
||||||
|
pen.setWidth(2)
|
||||||
|
painter.setPen(pen)
|
||||||
|
|
||||||
|
painter.translate(0, lineSpacing)
|
||||||
|
for t, l in zip(ticks, labels):
|
||||||
|
t = self._axisToPixel(t)
|
||||||
|
painter.drawLine(t, descent, t, height/2)
|
||||||
|
painter.drawText(t - len(l)/2*avgCharWidth, 0, l)
|
||||||
|
painter.drawLine(0, height/2, self.width(), height/2)
|
||||||
|
|
||||||
|
painter.translate(0, height)
|
||||||
|
for p in np.linspace(self._axisToPixel(self._start),
|
||||||
|
self._axisToPixel(self._stop),
|
||||||
|
self._num):
|
||||||
|
# TODO: is drawing far outside the viewport dangerous?
|
||||||
|
painter.drawLine(p, 0, p, -height/2)
|
||||||
|
|
||||||
|
def _paintSliders(self):
|
||||||
|
startPainter = QtWidgets.QStylePainter(self, self._startSlider)
|
||||||
|
stopPainter = QtWidgets.QStylePainter(self, self._stopSlider)
|
||||||
|
opt = self._getStyleOptionSlider(self._start)
|
||||||
|
if opt:
|
||||||
|
startPainter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
|
||||||
|
opt = self._getStyleOptionSlider(self._stop)
|
||||||
|
if opt:
|
||||||
|
stopPainter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
|
||||||
|
Loading…
Reference in New Issue
Block a user