forked from M-Labs/artiq
1
0
Fork 0

scanwidget: wire up signals better, set values late, take scanwidget from 7aa6397

This commit is contained in:
Robert Jördens 2016-03-08 12:35:13 +01:00
parent ef217f7fe2
commit 7e99780891
2 changed files with 212 additions and 143 deletions

View File

@ -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):

View File

@ -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()