forked from M-Labs/artiq
scanwidget: wire up signals better, set values late, take scanwidget from 7aa6397
This commit is contained in:
parent
ef217f7fe2
commit
7e99780891
|
@ -164,43 +164,43 @@ class _RangeScan(LayoutWidget):
|
||||||
self.min.setMinimumSize(110, 0)
|
self.min.setMinimumSize(110, 0)
|
||||||
self.min.setSizePolicy(QtWidgets.QSizePolicy(
|
self.min.setSizePolicy(QtWidgets.QSizePolicy(
|
||||||
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed))
|
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed))
|
||||||
self.min.setValue(state["min"]/scale)
|
|
||||||
disable_scroll_wheel(self.min)
|
disable_scroll_wheel(self.min)
|
||||||
apply_properties(self.min)
|
|
||||||
self.addWidget(self.min, 0, 1)
|
self.addWidget(self.min, 0, 1)
|
||||||
|
|
||||||
self.npoints = QtWidgets.QSpinBox()
|
self.npoints = QtWidgets.QSpinBox()
|
||||||
self.npoints.setMinimum(1)
|
self.npoints.setMinimum(1)
|
||||||
self.npoints.setValue(state["npoints"])
|
|
||||||
disable_scroll_wheel(self.npoints)
|
disable_scroll_wheel(self.npoints)
|
||||||
self.addWidget(self.npoints, 1, 1)
|
self.addWidget(self.npoints, 1, 1)
|
||||||
|
|
||||||
self.max = ScientificSpinBox()
|
self.max = ScientificSpinBox()
|
||||||
self.max.setStyleSheet("QDoubleSpinBox {color:red}")
|
self.max.setStyleSheet("QDoubleSpinBox {color:red}")
|
||||||
self.max.setMinimumSize(110, 0)
|
self.max.setMinimumSize(110, 0)
|
||||||
self.max.setValue(state["max"]/scale)
|
|
||||||
disable_scroll_wheel(self.max)
|
disable_scroll_wheel(self.max)
|
||||||
apply_properties(self.max)
|
|
||||||
self.addWidget(self.max, 2, 1)
|
self.addWidget(self.max, 2, 1)
|
||||||
|
|
||||||
def update_min(value):
|
def update_min(value):
|
||||||
state["min"] = value*scale
|
state["min"] = value*scale
|
||||||
|
scanner.setStart(value)
|
||||||
|
|
||||||
def update_max(value):
|
def update_max(value):
|
||||||
state["min"] = value*scale
|
state["min"] = value*scale
|
||||||
|
scanner.setStop(value)
|
||||||
|
|
||||||
def update_npoints(value):
|
def update_npoints(value):
|
||||||
state["npoints"] = value
|
state["npoints"] = value
|
||||||
|
scanner.setNumPoints(value)
|
||||||
|
|
||||||
scanner.sigMinMoved.connect(self.min.setValue)
|
scanner.sigStartMoved.connect(self.min.setValue)
|
||||||
# scanner.sigNumChanged.connect(self.npoints.setValue)
|
scanner.sigNumChanged.connect(self.npoints.setValue)
|
||||||
scanner.sigMaxMoved.connect(self.max.setValue)
|
scanner.sigStopMoved.connect(self.max.setValue)
|
||||||
self.min.valueChanged.connect(update_min)
|
self.min.valueChanged.connect(update_min)
|
||||||
self.min.valueChanged.connect(scanner.setMin)
|
|
||||||
# self.npoints.valueChanged.connect(scanner.setNumPoints)
|
|
||||||
self.npoints.valueChanged.connect(update_npoints)
|
self.npoints.valueChanged.connect(update_npoints)
|
||||||
self.max.valueChanged.connect(scanner.setMax)
|
|
||||||
self.max.valueChanged.connect(update_max)
|
self.max.valueChanged.connect(update_max)
|
||||||
|
self.min.setValue(state["min"]/scale)
|
||||||
|
self.npoints.setValue(state["npoints"])
|
||||||
|
self.max.setValue(state["max"]/scale)
|
||||||
|
apply_properties(self.min)
|
||||||
|
apply_properties(self.max)
|
||||||
|
|
||||||
|
|
||||||
class _ExplicitScan(LayoutWidget):
|
class _ExplicitScan(LayoutWidget):
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
from PyQt5 import QtGui, QtCore, QtWidgets
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
||||||
|
|
||||||
from .ticker import Ticker
|
from .ticker import Ticker
|
||||||
|
from numpy import linspace
|
||||||
|
|
||||||
|
|
||||||
class ScanAxis(QtWidgets.QWidget):
|
class ScanAxis(QtWidgets.QWidget):
|
||||||
sigZoom = QtCore.pyqtSignal(float, int)
|
sigZoom = QtCore.pyqtSignal(float, int)
|
||||||
|
sigPoints = QtCore.pyqtSignal(int)
|
||||||
|
|
||||||
def __init__(self, zoomFactor):
|
def __init__(self, zoomFactor):
|
||||||
QtWidgets.QWidget.__init__(self)
|
QtWidgets.QWidget.__init__(self)
|
||||||
self.proxy = None
|
self.proxy = None
|
||||||
|
self.slider = None # Needed for eventFilter
|
||||||
self.sizePolicy().setControlType(QtWidgets.QSizePolicy.ButtonBox)
|
self.sizePolicy().setControlType(QtWidgets.QSizePolicy.ButtonBox)
|
||||||
self.ticker = Ticker()
|
self.ticker = Ticker()
|
||||||
self.zoomFactor = zoomFactor
|
self.zoomFactor = zoomFactor
|
||||||
|
@ -22,54 +24,76 @@ class ScanAxis(QtWidgets.QWidget):
|
||||||
# on the spinboxes.
|
# on the spinboxes.
|
||||||
painter.translate(self.proxy.slider.handleWidth()/2, self.height() - 5)
|
painter.translate(self.proxy.slider.handleWidth()/2, self.height() - 5)
|
||||||
painter.drawLine(0, 0, self.width(), 0)
|
painter.drawLine(0, 0, self.width(), 0)
|
||||||
realMin = self.proxy.pixelToReal(0)
|
realLeft = self.proxy.pixelToReal(0)
|
||||||
realMax = self.proxy.pixelToReal(self.width())
|
realRight = self.proxy.pixelToReal(self.width())
|
||||||
|
|
||||||
ticks, prefix, labels = self.ticker(realMin, realMax)
|
ticks, prefix, labels = self.ticker(realLeft, realRight)
|
||||||
for t, l in zip(ticks, labels):
|
for t, l in zip(ticks, labels):
|
||||||
t = self.proxy.realToPixel(t)
|
t = self.proxy.realToPixel(t)
|
||||||
textCenter = (len(l)/2.0)*avgCharWidth
|
textCenter = (len(l)/2.0)*avgCharWidth
|
||||||
painter.drawLine(t, 5, t, -5)
|
painter.drawLine(t, 5, t, -5)
|
||||||
painter.drawText(t - textCenter, -10, l)
|
painter.drawText(t - textCenter, -10, l)
|
||||||
painter.resetTransform()
|
|
||||||
painter.drawText(0, 10, prefix)
|
painter.save()
|
||||||
# TODO:
|
painter.setPen(QtGui.QColor(QtCore.Qt.green))
|
||||||
# QtWidgets.QWidget.paintEvent(self, ev)?
|
sliderStartPixel = self.proxy.realToPixel(self.proxy.realStart)
|
||||||
# ev.accept() ?
|
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)
|
||||||
|
|
||||||
|
painter.restore()
|
||||||
|
painter.drawText(0, -25, prefix)
|
||||||
|
ev.accept()
|
||||||
|
|
||||||
def wheelEvent(self, ev):
|
def wheelEvent(self, ev):
|
||||||
y = ev.angleDelta().y()
|
y = ev.angleDelta().y()
|
||||||
if y:
|
if y:
|
||||||
z = self.zoomFactor**(y / 120.)
|
if ev.modifiers() & QtCore.Qt.ShiftModifier:
|
||||||
# Remove the slider-handle shift correction, b/c none of the other
|
# If shift+scroll, modify number of points.
|
||||||
# widgets know about it. If we have the mouse directly over a tick
|
z = int(y / 120.)
|
||||||
# during a zoom, it should appear as if we are doing zoom relative
|
self.sigPoints.emit(z)
|
||||||
# to the ticks which live in axis pixel-space, not slider
|
else:
|
||||||
# pixel-space.
|
z = self.zoomFactor**(y / 120.)
|
||||||
self.sigZoom.emit(z, ev.x() - self.proxy.slider.handleWidth()/2)
|
# 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.
|
||||||
|
self.sigZoom.emit(z, ev.x() -
|
||||||
|
self.proxy.slider.handleWidth()/2)
|
||||||
self.update()
|
self.update()
|
||||||
ev.accept()
|
ev.accept()
|
||||||
|
|
||||||
|
def eventFilter(self, obj, ev):
|
||||||
|
if obj != self.slider:
|
||||||
|
return False
|
||||||
|
if ev.type() != QtCore.QEvent.Wheel:
|
||||||
|
return False
|
||||||
|
self.wheelEvent(ev)
|
||||||
|
return True
|
||||||
|
|
||||||
# Basic ideas from https://gist.github.com/Riateche/27e36977f7d5ea72cf4f
|
# Basic ideas from https://gist.github.com/Riateche/27e36977f7d5ea72cf4f
|
||||||
class ScanSlider(QtWidgets.QSlider):
|
class ScanSlider(QtWidgets.QSlider):
|
||||||
sigMinMoved = QtCore.pyqtSignal(int)
|
sigStartMoved = QtCore.pyqtSignal(int)
|
||||||
sigMaxMoved = QtCore.pyqtSignal(int)
|
sigStopMoved = QtCore.pyqtSignal(int)
|
||||||
noSlider, minSlider, maxSlider = range(3)
|
noSlider, startSlider, stopSlider = range(3)
|
||||||
maxStyle = "QSlider::handle {background:red}"
|
stopStyle = "QSlider::handle {background:red}"
|
||||||
minStyle = "QSlider::handle {background:blue}"
|
startStyle = "QSlider::handle {background:blue}"
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QtWidgets.QSlider.__init__(self, QtCore.Qt.Horizontal)
|
QtWidgets.QSlider.__init__(self, QtCore.Qt.Horizontal)
|
||||||
self.minPos = 0 # Pos and Val can differ in event handling.
|
self.startPos = 0 # Pos and Val can differ in event handling.
|
||||||
# perhaps prevPos and currPos is more accurate.
|
# perhaps prevPos and currPos is more accurate.
|
||||||
self.maxPos = 99
|
self.stopPos = 99
|
||||||
self.minVal = 0 # lower
|
self.startVal = 0 # lower
|
||||||
self.maxVal = 99 # upper
|
self.stopVal = 99 # upper
|
||||||
self.offset = 0
|
self.offset = 0
|
||||||
self.position = 0
|
self.position = 0
|
||||||
self.lastPressed = ScanSlider.noSlider
|
self.lastPressed = ScanSlider.noSlider
|
||||||
self.selectedHandle = ScanSlider.minSlider
|
self.selectedHandle = ScanSlider.startSlider
|
||||||
self.upperPressed = QtWidgets.QStyle.SC_None
|
self.upperPressed = QtWidgets.QStyle.SC_None
|
||||||
self.lowerPressed = QtWidgets.QStyle.SC_None
|
self.lowerPressed = QtWidgets.QStyle.SC_None
|
||||||
self.firstMovement = False # State var for handling slider overlap.
|
self.firstMovement = False # State var for handling slider overlap.
|
||||||
|
@ -77,22 +101,22 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
|
|
||||||
# We need fake sliders to keep around so that we can dynamically
|
# We need fake sliders to keep around so that we can dynamically
|
||||||
# set the stylesheets for drawing each slider later. See paintEvent.
|
# set the stylesheets for drawing each slider later. See paintEvent.
|
||||||
self.dummyMinSlider = QtWidgets.QSlider()
|
self.dummyStartSlider = QtWidgets.QSlider()
|
||||||
self.dummyMaxSlider = QtWidgets.QSlider()
|
self.dummyStopSlider = QtWidgets.QSlider()
|
||||||
self.dummyMinSlider.setStyleSheet(ScanSlider.minStyle)
|
self.dummyStartSlider.setStyleSheet(ScanSlider.startStyle)
|
||||||
self.dummyMaxSlider.setStyleSheet(ScanSlider.maxStyle)
|
self.dummyStopSlider.setStyleSheet(ScanSlider.stopStyle)
|
||||||
|
|
||||||
# We basically superimpose two QSliders on top of each other, discarding
|
# We basically superimpose two QSliders on top of each other, discarding
|
||||||
# the state that remains constant between the two when drawing.
|
# the state that remains constant between the two when drawing.
|
||||||
# Everything except the handles remain constant.
|
# Everything except the handles remain constant.
|
||||||
def initHandleStyleOption(self, opt, handle):
|
def initHandleStyleOption(self, opt, handle):
|
||||||
self.initStyleOption(opt)
|
self.initStyleOption(opt)
|
||||||
if handle == ScanSlider.minSlider:
|
if handle == ScanSlider.startSlider:
|
||||||
opt.sliderPosition = self.minPos
|
opt.sliderPosition = self.startPos
|
||||||
opt.sliderValue = self.minVal
|
opt.sliderValue = self.startVal
|
||||||
elif handle == ScanSlider.maxSlider:
|
elif handle == ScanSlider.stopSlider:
|
||||||
opt.sliderPosition = self.maxPos
|
opt.sliderPosition = self.stopPos
|
||||||
opt.sliderValue = self.maxVal
|
opt.sliderValue = self.stopVal
|
||||||
else:
|
else:
|
||||||
pass # AssertionErrors
|
pass # AssertionErrors
|
||||||
|
|
||||||
|
@ -109,13 +133,15 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
self)
|
self)
|
||||||
|
|
||||||
sliderLength = sr.width()
|
sliderLength = sr.width()
|
||||||
sliderMin = gr.x()
|
sliderStart = gr.x()
|
||||||
# For historical reasons right() returns left()+width() - 1
|
# For historical reasons right() returns left()+width() - 1
|
||||||
# x() is equivalent to left().
|
# x() is equivalent to left().
|
||||||
sliderMax = gr.right() - sliderLength + 1
|
sliderStop = gr.right() - sliderLength + 1
|
||||||
return QtWidgets.QStyle.sliderValueFromPosition(
|
|
||||||
self.minimum(), self.maximum(), pos - sliderMin,
|
rangeVal = QtWidgets.QStyle.sliderValueFromPosition(
|
||||||
sliderMax - sliderMin, opt.upsideDown)
|
self.minimum(), self.maximum(), pos - sliderStart,
|
||||||
|
sliderStop - sliderStart, opt.upsideDown)
|
||||||
|
return rangeVal
|
||||||
|
|
||||||
def rangeValueToPixelPos(self, val):
|
def rangeValueToPixelPos(self, val):
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
opt = QtWidgets.QStyleOptionSlider()
|
||||||
|
@ -129,11 +155,11 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
self)
|
self)
|
||||||
|
|
||||||
sliderLength = sr.width()
|
sliderLength = sr.width()
|
||||||
sliderMin = gr.x()
|
sliderStart = gr.x()
|
||||||
sliderMax = gr.right() - sliderLength + 1
|
sliderStop = gr.right() - sliderLength + 1
|
||||||
|
|
||||||
pixel = QtWidgets.QStyle.sliderPositionFromValue(
|
pixel = QtWidgets.QStyle.sliderPositionFromValue(
|
||||||
self.minimum(), self.maximum(), val, sliderMax - sliderMin,
|
self.minimum(), self.maximum(), val, sliderStop - sliderStart,
|
||||||
opt.upsideDown)
|
opt.upsideDown)
|
||||||
return pixel
|
return pixel
|
||||||
|
|
||||||
|
@ -155,9 +181,9 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
QtWidgets.QStyle.SC_SliderGroove,
|
QtWidgets.QStyle.SC_SliderGroove,
|
||||||
self)
|
self)
|
||||||
sliderLength = self.handleWidth()
|
sliderLength = self.handleWidth()
|
||||||
sliderMin = gr.x()
|
sliderStart = gr.x()
|
||||||
sliderMax = gr.right() - sliderLength + 1
|
sliderStop = gr.right() - sliderLength + 1
|
||||||
return sliderMax - sliderMin
|
return sliderStop - sliderStart
|
||||||
|
|
||||||
# If groove and axis are not aligned (and they should be), we can use
|
# If groove and axis are not aligned (and they should be), we can use
|
||||||
# this function to calculate the offset between them.
|
# this function to calculate the offset between them.
|
||||||
|
@ -172,6 +198,17 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
def handleMousePress(self, pos, control, val, handle):
|
def handleMousePress(self, pos, control, val, handle):
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
opt = QtWidgets.QStyleOptionSlider()
|
||||||
self.initHandleStyleOption(opt, handle)
|
self.initHandleStyleOption(opt, handle)
|
||||||
|
startAtEdges = (handle == ScanSlider.startSlider and
|
||||||
|
(self.startVal == self.minimum() or
|
||||||
|
self.startVal == self.maximum()))
|
||||||
|
stopAtEdges = (handle == ScanSlider.stopSlider and
|
||||||
|
(self.stopVal == self.minimum() or
|
||||||
|
self.stopVal == self.maximum()))
|
||||||
|
|
||||||
|
# If chosen slider at edge, treat it as non-interactive.
|
||||||
|
if startAtEdges or stopAtEdges:
|
||||||
|
return QtWidgets.QStyle.SC_None
|
||||||
|
|
||||||
oldControl = control
|
oldControl = control
|
||||||
control = self.style().hitTestComplexControl(
|
control = self.style().hitTestComplexControl(
|
||||||
QtWidgets.QStyle.CC_Slider, opt, pos, self)
|
QtWidgets.QStyle.CC_Slider, opt, pos, self)
|
||||||
|
@ -202,13 +239,15 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
# if action == QtWidgets.QAbstractSlider.SliderSingleStepAdd:
|
# if action == QtWidgets.QAbstractSlider.SliderSingleStepAdd:
|
||||||
# if
|
# if
|
||||||
|
|
||||||
def setLowerValue(self, val):
|
def setStartValue(self, val):
|
||||||
self.setSpan(val, self.maxVal)
|
self.setSpan(val, self.stopVal)
|
||||||
|
|
||||||
def setUpperValue(self, val):
|
def setStopValue(self, val):
|
||||||
self.setSpan(self.minVal, val)
|
self.setSpan(self.startVal, val)
|
||||||
|
|
||||||
def setSpan(self, lower, upper):
|
def setSpan(self, lower, upper):
|
||||||
|
# TODO: Is bound() necessary? QStyle::sliderPositionFromValue appears
|
||||||
|
# to clamp already.
|
||||||
def bound(min, curr, max):
|
def bound(min, curr, max):
|
||||||
if curr < min:
|
if curr < min:
|
||||||
return min
|
return min
|
||||||
|
@ -220,49 +259,47 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
low = bound(self.minimum(), lower, self.maximum())
|
low = bound(self.minimum(), lower, self.maximum())
|
||||||
high = bound(self.minimum(), upper, self.maximum())
|
high = bound(self.minimum(), upper, self.maximum())
|
||||||
|
|
||||||
if low != self.minVal or high != self.maxVal:
|
if low != self.startVal or high != self.stopVal:
|
||||||
if low != self.minVal:
|
if low != self.startVal:
|
||||||
self.minVal = low
|
self.startVal = low
|
||||||
self.minPos = low
|
self.startPos = low
|
||||||
# emit
|
if high != self.stopVal:
|
||||||
if high != self.maxVal:
|
self.stopVal = high
|
||||||
self.maxVal = high
|
self.stopPos = high
|
||||||
self.maxPos = high
|
|
||||||
# emit
|
|
||||||
# emit spanChanged
|
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def setLowerPosition(self, val):
|
def setStartPosition(self, val):
|
||||||
if val != self.minPos:
|
if val != self.startPos:
|
||||||
self.minPos = val
|
self.startPos = val
|
||||||
if not self.hasTracking():
|
if not self.hasTracking():
|
||||||
self.update()
|
self.update()
|
||||||
if self.isSliderDown():
|
if self.isSliderDown():
|
||||||
self.sigMinMoved.emit(self.minPos)
|
self.sigStartMoved.emit(self.startPos)
|
||||||
if self.hasTracking() and not self.blockTracking:
|
if self.hasTracking() and not self.blockTracking:
|
||||||
self.setLowerValue(val)
|
self.setStartValue(val)
|
||||||
|
|
||||||
def setUpperPosition(self, val):
|
def setStopPosition(self, val):
|
||||||
if val != self.maxPos:
|
if val != self.stopPos:
|
||||||
self.maxPos = val
|
self.stopPos = val
|
||||||
if not self.hasTracking():
|
if not self.hasTracking():
|
||||||
self.update()
|
self.update()
|
||||||
if self.isSliderDown():
|
if self.isSliderDown():
|
||||||
self.sigMaxMoved.emit(self.maxPos)
|
self.sigStopMoved.emit(self.stopPos)
|
||||||
if self.hasTracking() and not self.blockTracking:
|
if self.hasTracking() and not self.blockTracking:
|
||||||
self.setUpperValue(val)
|
self.setStopValue(val)
|
||||||
|
|
||||||
def mousePressEvent(self, ev):
|
def mousePressEvent(self, ev):
|
||||||
if self.minimum() == self.maximum() or (ev.buttons() ^ ev.button()):
|
if self.minimum() == self.maximum() or (ev.buttons() ^ ev.button()):
|
||||||
ev.ignore()
|
ev.ignore()
|
||||||
return
|
return
|
||||||
|
|
||||||
# Prefer maxVal in the default case.
|
# Prefer stopVal in the default case.
|
||||||
self.upperPressed = self.handleMousePress(
|
self.upperPressed = self.handleMousePress(
|
||||||
ev.pos(), self.upperPressed, self.maxVal, ScanSlider.maxSlider)
|
ev.pos(), self.upperPressed, self.stopVal, ScanSlider.stopSlider)
|
||||||
if self.upperPressed != QtWidgets.QStyle.SC_SliderHandle:
|
if self.upperPressed != QtWidgets.QStyle.SC_SliderHandle:
|
||||||
self.lowerPressed = self.handleMousePress(
|
self.lowerPressed = self.handleMousePress(
|
||||||
ev.pos(), self.upperPressed, self.minVal, ScanSlider.minSlider)
|
ev.pos(), self.upperPressed, self.startVal,
|
||||||
|
ScanSlider.startSlider)
|
||||||
|
|
||||||
# State that is needed to handle the case where two sliders are equal.
|
# State that is needed to handle the case where two sliders are equal.
|
||||||
self.firstMovement = True
|
self.firstMovement = True
|
||||||
|
@ -289,21 +326,19 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
newPos = self.position
|
newPos = self.position
|
||||||
|
|
||||||
if self.firstMovement:
|
if self.firstMovement:
|
||||||
if self.minPos == self.maxPos:
|
if self.startPos == self.stopPos:
|
||||||
# MaxSlider is preferred, except in the case where min == max
|
# StopSlider is preferred, except in the case where
|
||||||
# possible value the slider can take.
|
# start == max possible value the slider can take.
|
||||||
if self.minPos == self.maximum():
|
if self.startPos == self.maximum():
|
||||||
self.lowerPressed = QtWidgets.QStyle.SC_SliderHandle
|
self.lowerPressed = QtWidgets.QStyle.SC_SliderHandle
|
||||||
self.upperPressed = QtWidgets.QStyle.SC_None
|
self.upperPressed = QtWidgets.QStyle.SC_None
|
||||||
self.firstMovement = False
|
self.firstMovement = False
|
||||||
|
|
||||||
if self.lowerPressed == QtWidgets.QStyle.SC_SliderHandle:
|
if self.lowerPressed == QtWidgets.QStyle.SC_SliderHandle:
|
||||||
newPos = min(newPos, self.maxVal)
|
self.setStartPosition(newPos)
|
||||||
self.setLowerPosition(newPos)
|
|
||||||
|
|
||||||
if self.upperPressed == QtWidgets.QStyle.SC_SliderHandle:
|
if self.upperPressed == QtWidgets.QStyle.SC_SliderHandle:
|
||||||
newPos = max(newPos, self.minVal)
|
self.setStopPosition(newPos)
|
||||||
self.setUpperPosition(newPos)
|
|
||||||
|
|
||||||
ev.accept()
|
ev.accept()
|
||||||
|
|
||||||
|
@ -323,8 +358,8 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
# individually for each slider handle, but Windows 7 does not
|
# individually for each slider handle, but Windows 7 does not
|
||||||
# use them. This seems to be the only way to override the colors
|
# use them. This seems to be the only way to override the colors
|
||||||
# regardless of platform.
|
# regardless of platform.
|
||||||
minPainter = QtWidgets.QStylePainter(self, self.dummyMinSlider)
|
startPainter = QtWidgets.QStylePainter(self, self.dummyStartSlider)
|
||||||
maxPainter = QtWidgets.QStylePainter(self, self.dummyMaxSlider)
|
stopPainter = QtWidgets.QStylePainter(self, self.dummyStopSlider)
|
||||||
|
|
||||||
# Groove
|
# Groove
|
||||||
opt = QtWidgets.QStyleOptionSlider()
|
opt = QtWidgets.QStyleOptionSlider()
|
||||||
|
@ -335,22 +370,28 @@ class ScanSlider(QtWidgets.QSlider):
|
||||||
painter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
|
painter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
|
||||||
|
|
||||||
# Handles
|
# Handles
|
||||||
self.drawHandle(minPainter, ScanSlider.minSlider)
|
# Qt will snap sliders to 0 or maximum() if given a desired pixel
|
||||||
self.drawHandle(maxPainter, ScanSlider.maxSlider)
|
# location outside the mapped range. So we manually just don't draw
|
||||||
|
# the handles if they are at 0 or max.
|
||||||
|
if self.startVal > 0 and self.startVal < self.maximum():
|
||||||
|
self.drawHandle(startPainter, ScanSlider.startSlider)
|
||||||
|
if self.stopVal > 0 and self.stopVal < self.maximum():
|
||||||
|
self.drawHandle(stopPainter, ScanSlider.stopSlider)
|
||||||
|
|
||||||
|
|
||||||
# real (Sliders) => pixel (one pixel movement of sliders would increment by X)
|
# real (Sliders) => pixel (one pixel movement of sliders would increment by X)
|
||||||
# => range (minimum granularity that sliders understand).
|
# => range (minimum granularity that sliders understand).
|
||||||
class ScanProxy(QtCore.QObject):
|
class ScanProxy(QtCore.QObject):
|
||||||
sigMinMoved = QtCore.pyqtSignal(float)
|
sigStartMoved = QtCore.pyqtSignal(float)
|
||||||
sigMaxMoved = QtCore.pyqtSignal(float)
|
sigStopMoved = QtCore.pyqtSignal(float)
|
||||||
|
sigNumPoints = QtCore.pyqtSignal(int)
|
||||||
|
|
||||||
def __init__(self, slider, axis, rangeFactor):
|
def __init__(self, slider, axis, rangeFactor):
|
||||||
QtCore.QObject.__init__(self)
|
QtCore.QObject.__init__(self)
|
||||||
self.axis = axis
|
self.axis = axis
|
||||||
self.slider = slider
|
self.slider = slider
|
||||||
self.realMin = 0
|
self.realStart = 0
|
||||||
self.realMax = 0
|
self.realStop = 0
|
||||||
self.numPoints = 10
|
self.numPoints = 10
|
||||||
self.rangeFactor = rangeFactor
|
self.rangeFactor = rangeFactor
|
||||||
|
|
||||||
|
@ -363,7 +404,6 @@ class ScanProxy(QtCore.QObject):
|
||||||
self.realToPixelTransform = self.calculateNewRealToPixel(
|
self.realToPixelTransform = self.calculateNewRealToPixel(
|
||||||
-self.axis.width()/2, 1.0)
|
-self.axis.width()/2, 1.0)
|
||||||
self.invalidOldSizeExpected = True
|
self.invalidOldSizeExpected = True
|
||||||
self.axis.installEventFilter(self)
|
|
||||||
|
|
||||||
# What real value should map to the axis/slider left? This doesn't depend
|
# What real value should map to the axis/slider left? This doesn't depend
|
||||||
# on any public members so we can make decisions about centering during
|
# on any public members so we can make decisions about centering during
|
||||||
|
@ -374,7 +414,13 @@ class ScanProxy(QtCore.QObject):
|
||||||
|
|
||||||
# pixel vals for sliders: 0 to slider_width - 1
|
# pixel vals for sliders: 0 to slider_width - 1
|
||||||
def realToPixel(self, val):
|
def realToPixel(self, val):
|
||||||
return (QtCore.QPointF(val, 0) * self.realToPixelTransform).x()
|
rawVal = (QtCore.QPointF(val, 0) * self.realToPixelTransform).x()
|
||||||
|
# Clamp pixel values to 32 bits, b/c Qt will otherwise wrap values.
|
||||||
|
if rawVal < -(2**31):
|
||||||
|
rawVal = -(2**31)
|
||||||
|
elif rawVal > (2**31 - 1):
|
||||||
|
rawVal = (2**31 - 1)
|
||||||
|
return rawVal
|
||||||
|
|
||||||
# Get a point from pixel units to what the sliders display.
|
# Get a point from pixel units to what the sliders display.
|
||||||
def pixelToReal(self, val):
|
def pixelToReal(self, val):
|
||||||
|
@ -398,21 +444,30 @@ class ScanProxy(QtCore.QObject):
|
||||||
pixelVal = self.realToPixel(val)
|
pixelVal = self.realToPixel(val)
|
||||||
return self.slider.pixelPosToRangeValue(pixelVal)
|
return self.slider.pixelPosToRangeValue(pixelVal)
|
||||||
|
|
||||||
def moveMax(self, val):
|
def moveStop(self, val):
|
||||||
sliderX = self.realToRange(val)
|
sliderX = self.realToRange(val)
|
||||||
self.slider.setUpperPosition(sliderX)
|
self.slider.setStopPosition(sliderX)
|
||||||
self.realMax = val
|
self.realStop = val
|
||||||
|
self.axis.update() # Number of points ticks changed positions.
|
||||||
|
|
||||||
def moveMin(self, val):
|
def moveStart(self, val):
|
||||||
sliderX = self.realToRange(val)
|
sliderX = self.realToRange(val)
|
||||||
self.slider.setLowerPosition(sliderX)
|
self.slider.setStartPosition(sliderX)
|
||||||
self.realMin = val
|
self.realStart = val
|
||||||
|
self.axis.update()
|
||||||
|
|
||||||
def handleMaxMoved(self, rangeVal):
|
def handleStopMoved(self, rangeVal):
|
||||||
self.sigMaxMoved.emit(self.rangeToReal(rangeVal))
|
self.sigStopMoved.emit(self.rangeToReal(rangeVal))
|
||||||
|
|
||||||
def handleMinMoved(self, rangeVal):
|
def handleStartMoved(self, rangeVal):
|
||||||
self.sigMinMoved.emit(self.rangeToReal(rangeVal))
|
self.sigStartMoved.emit(self.rangeToReal(rangeVal))
|
||||||
|
|
||||||
|
def handleNumPoints(self, inc):
|
||||||
|
self.sigNumPoints.emit(self.numPoints + inc)
|
||||||
|
|
||||||
|
def setNumPoints(self, val):
|
||||||
|
self.numPoints = val
|
||||||
|
self.axis.update()
|
||||||
|
|
||||||
def handleZoom(self, zoomFactor, mouseXPos):
|
def handleZoom(self, zoomFactor, mouseXPos):
|
||||||
newScale = self.realToPixelTransform.m11() * zoomFactor
|
newScale = self.realToPixelTransform.m11() * zoomFactor
|
||||||
|
@ -420,50 +475,57 @@ class ScanProxy(QtCore.QObject):
|
||||||
newLeft = refReal - mouseXPos/newScale
|
newLeft = refReal - mouseXPos/newScale
|
||||||
self.realToPixelTransform = self.calculateNewRealToPixel(
|
self.realToPixelTransform = self.calculateNewRealToPixel(
|
||||||
newLeft, newScale)
|
newLeft, newScale)
|
||||||
self.moveMax(self.realMax)
|
self.moveStop(self.realStop)
|
||||||
self.moveMin(self.realMin)
|
self.moveStart(self.realStart)
|
||||||
|
|
||||||
def zoomToFit(self):
|
def zoomToFit(self):
|
||||||
currRangeReal = abs(self.realMax - self.realMin)
|
currRangeReal = abs(self.realStop - self.realStart)
|
||||||
assert self.rangeFactor > 2
|
# Slider closest to the left should be used to find the new axis left.
|
||||||
|
if self.realStop < self.realStart:
|
||||||
|
refSlider = self.realStop
|
||||||
|
else:
|
||||||
|
refSlider = self.realStart
|
||||||
|
if self.rangeFactor <= 2:
|
||||||
|
return # Ill-formed snap range- do nothing.
|
||||||
proportion = self.rangeFactor/(self.rangeFactor - 2)
|
proportion = self.rangeFactor/(self.rangeFactor - 2)
|
||||||
newScale = self.slider.effectiveWidth()/(proportion*currRangeReal)
|
newScale = self.slider.effectiveWidth()/(proportion*currRangeReal)
|
||||||
newLeft = self.realMin - self.slider.effectiveWidth() \
|
newLeft = refSlider - self.slider.effectiveWidth() \
|
||||||
/ (self.rangeFactor*newScale)
|
/ (self.rangeFactor*newScale)
|
||||||
self.realToPixelTransform = self.calculateNewRealToPixel(
|
self.realToPixelTransform = self.calculateNewRealToPixel(
|
||||||
newLeft, newScale)
|
newLeft, newScale)
|
||||||
self.printTransform()
|
self.printTransform()
|
||||||
self.moveMax(self.realMax)
|
self.moveStop(self.realStop)
|
||||||
self.moveMin(self.realMin)
|
self.moveStart(self.realStart)
|
||||||
self.axis.update()
|
self.axis.update() # Axis normally takes care to update itself during
|
||||||
|
# zoom. In this code path however, the zoom didn't arrive via the axis
|
||||||
|
# widget, so we need to notify manually.
|
||||||
|
|
||||||
def fitToView(self):
|
def fitToView(self):
|
||||||
lowRange = 1.0/self.rangeFactor
|
lowRange = 1.0/self.rangeFactor
|
||||||
highRange = (self.rangeFactor - 1)/self.rangeFactor
|
highRange = (self.rangeFactor - 1)/self.rangeFactor
|
||||||
newMin = self.pixelToReal(lowRange * self.slider.effectiveWidth())
|
newStart = self.pixelToReal(lowRange * self.slider.effectiveWidth())
|
||||||
newMax = self.pixelToReal(highRange * self.slider.effectiveWidth())
|
newStop = self.pixelToReal(highRange * self.slider.effectiveWidth())
|
||||||
sliderRange = self.slider.maximum() - self.slider.minimum()
|
sliderRange = self.slider.maximum() - self.slider.minimum()
|
||||||
assert sliderRange > 0
|
|
||||||
self.moveMin(newMin)
|
|
||||||
self.moveMax(newMax)
|
|
||||||
# Signals won't fire unless slider was actually grabbed, so
|
# Signals won't fire unless slider was actually grabbed, so
|
||||||
# manually update so the spinboxes know that knew values were set.
|
# manually update so the spinboxes know that knew values were set.
|
||||||
# self.realMax/Min will be updated as a consequence of ValueChanged
|
# self.realStop/Start and the sliders themselves will be updated as a
|
||||||
# signal in spinboxes.
|
# consequence of ValueChanged signal in spinboxes. The slider widget
|
||||||
self.sigMaxMoved.emit(newMax)
|
# has guards against recursive signals in setSpan().
|
||||||
self.sigMinMoved.emit(newMin)
|
if sliderRange > 0:
|
||||||
|
self.sigStopMoved.emit(newStop)
|
||||||
|
self.sigStartMoved.emit(newStart)
|
||||||
|
|
||||||
def eventFilter(self, obj, ev):
|
def eventFilter(self, obj, ev):
|
||||||
if obj != self.axis:
|
if obj != self.axis:
|
||||||
return False
|
return False
|
||||||
if ev.type() != QtCore.QEvent.Resize:
|
if ev.type() != QtCore.QEvent.Resize:
|
||||||
return False
|
return False
|
||||||
oldLeft = self.pixelToReal(0)
|
|
||||||
if ev.oldSize().isValid():
|
if ev.oldSize().isValid():
|
||||||
|
oldLeft = self.pixelToReal(0)
|
||||||
refWidth = ev.oldSize().width() - self.slider.handleWidth()
|
refWidth = ev.oldSize().width() - self.slider.handleWidth()
|
||||||
refRight = self.pixelToReal(refWidth)
|
refRight = self.pixelToReal(refWidth)
|
||||||
newWidth = ev.size().width() - self.slider.handleWidth()
|
newWidth = ev.size().width() - self.slider.handleWidth()
|
||||||
assert refRight > oldLeft
|
# assert refRight > oldLeft
|
||||||
newScale = newWidth/(refRight - oldLeft)
|
newScale = newWidth/(refRight - oldLeft)
|
||||||
else:
|
else:
|
||||||
# TODO: self.axis.width() is invalid during object
|
# TODO: self.axis.width() is invalid during object
|
||||||
|
@ -491,8 +553,9 @@ class ScanProxy(QtCore.QObject):
|
||||||
|
|
||||||
|
|
||||||
class ScanWidget(QtWidgets.QWidget):
|
class ScanWidget(QtWidgets.QWidget):
|
||||||
sigMinMoved = QtCore.pyqtSignal(float)
|
sigStartMoved = QtCore.pyqtSignal(float)
|
||||||
sigMaxMoved = QtCore.pyqtSignal(float)
|
sigStopMoved = QtCore.pyqtSignal(float)
|
||||||
|
sigNumChanged = QtCore.pyqtSignal(int)
|
||||||
|
|
||||||
def __init__(self, zoomFactor=1.05, rangeFactor=6):
|
def __init__(self, zoomFactor=1.05, rangeFactor=6):
|
||||||
QtWidgets.QWidget.__init__(self)
|
QtWidgets.QWidget.__init__(self)
|
||||||
|
@ -502,6 +565,8 @@ class ScanWidget(QtWidgets.QWidget):
|
||||||
fitViewButton = QtWidgets.QPushButton("Snap Range")
|
fitViewButton = QtWidgets.QPushButton("Snap Range")
|
||||||
self.proxy = ScanProxy(slider, axis, rangeFactor)
|
self.proxy = ScanProxy(slider, axis, rangeFactor)
|
||||||
axis.proxy = self.proxy
|
axis.proxy = self.proxy
|
||||||
|
axis.slider = slider
|
||||||
|
slider.setMaximum(1023)
|
||||||
|
|
||||||
# Layout.
|
# Layout.
|
||||||
layout = QtWidgets.QGridLayout()
|
layout = QtWidgets.QGridLayout()
|
||||||
|
@ -514,26 +579,30 @@ class ScanWidget(QtWidgets.QWidget):
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
slider.sigMaxMoved.connect(self.proxy.handleMaxMoved)
|
slider.sigStopMoved.connect(self.proxy.handleStopMoved)
|
||||||
slider.sigMinMoved.connect(self.proxy.handleMinMoved)
|
slider.sigStartMoved.connect(self.proxy.handleStartMoved)
|
||||||
self.proxy.sigMaxMoved.connect(self.sigMaxMoved)
|
self.proxy.sigStopMoved.connect(self.sigStopMoved)
|
||||||
self.proxy.sigMinMoved.connect(self.sigMinMoved)
|
self.proxy.sigStartMoved.connect(self.sigStartMoved)
|
||||||
|
self.proxy.sigNumPoints.connect(self.sigNumChanged)
|
||||||
axis.sigZoom.connect(self.proxy.handleZoom)
|
axis.sigZoom.connect(self.proxy.handleZoom)
|
||||||
|
axis.sigPoints.connect(self.proxy.handleNumPoints)
|
||||||
fitViewButton.clicked.connect(self.fitToView)
|
fitViewButton.clicked.connect(self.fitToView)
|
||||||
zoomFitButton.clicked.connect(self.zoomToFit)
|
zoomFitButton.clicked.connect(self.zoomToFit)
|
||||||
|
|
||||||
# Connect event observers.
|
# Connect event observers.
|
||||||
|
axis.installEventFilter(self.proxy)
|
||||||
|
slider.installEventFilter(axis)
|
||||||
|
|
||||||
# Spinbox and button slots. Any time the spinboxes change, ScanWidget
|
# Spinbox and button slots. Any time the spinboxes change, ScanWidget
|
||||||
# mirrors it and passes the information to the proxy.
|
# mirrors it and passes the information to the proxy.
|
||||||
def setMax(self, val):
|
def setStop(self, val):
|
||||||
self.proxy.moveMax(val)
|
self.proxy.moveStop(val)
|
||||||
|
|
||||||
def setMin(self, val):
|
def setStart(self, val):
|
||||||
self.proxy.moveMin(val)
|
self.proxy.moveStart(val)
|
||||||
|
|
||||||
def setNumPoints(self, val):
|
def setNumPoints(self, val):
|
||||||
pass
|
self.proxy.setNumPoints(val)
|
||||||
|
|
||||||
def zoomToFit(self):
|
def zoomToFit(self):
|
||||||
self.proxy.zoomToFit()
|
self.proxy.zoomToFit()
|
||||||
|
|
Loading…
Reference in New Issue