forked from M-Labs/artiq
1
0
Fork 0

scanwidget: apply changes as of 98f0a56

This commit is contained in:
Robert Jördens 2016-03-11 15:38:22 +01:00
parent bc9203457e
commit e4b854b8bf
2 changed files with 101 additions and 161 deletions

View File

@ -155,6 +155,7 @@ class _RangeScan(LayoutWidget):
spinbox.setSuffix(" " + procdesc["unit"]) spinbox.setSuffix(" " + procdesc["unit"])
self.scanner = scanner = ScanWidget() self.scanner = scanner = ScanWidget()
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.axis)

View File

@ -1,7 +1,8 @@
from PyQt5 import QtGui, QtCore, QtWidgets from PyQt5 import QtGui, QtCore, QtWidgets
from .ticker import Ticker
from numpy import linspace from numpy import linspace
from .ticker import Ticker
class ScanAxis(QtWidgets.QWidget): class ScanAxis(QtWidgets.QWidget):
sigZoom = QtCore.pyqtSignal(float, int) sigZoom = QtCore.pyqtSignal(float, int)
@ -10,7 +11,6 @@ class ScanAxis(QtWidgets.QWidget):
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
@ -26,26 +26,25 @@ class ScanAxis(QtWidgets.QWidget):
painter.drawLine(0, 0, self.width(), 0) painter.drawLine(0, 0, self.width(), 0)
realLeft = self.proxy.pixelToReal(0) realLeft = self.proxy.pixelToReal(0)
realRight = self.proxy.pixelToReal(self.width()) realRight = self.proxy.pixelToReal(self.width())
ticks, prefix, labels = self.ticker(realLeft, realRight) ticks, prefix, labels = self.ticker(realLeft, realRight)
painter.drawText(0, -25, prefix)
pen = QtGui.QPen()
pen.setWidth(2)
painter.setPen(pen)
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 painter.drawLine(t, 0, t, -5)
painter.drawLine(t, 5, t, -5) painter.drawText(t - len(l)/2*avgCharWidth, -10, l)
painter.drawText(t - textCenter, -10, l)
painter.save()
painter.setPen(QtGui.QColor(QtCore.Qt.green))
sliderStartPixel = self.proxy.realToPixel(self.proxy.realStart) sliderStartPixel = self.proxy.realToPixel(self.proxy.realStart)
sliderStopPixel = self.proxy.realToPixel(self.proxy.realStop) sliderStopPixel = self.proxy.realToPixel(self.proxy.realStop)
pixels = linspace(sliderStartPixel, sliderStopPixel, pixels = linspace(sliderStartPixel, sliderStopPixel,
self.proxy.numPoints) self.proxy.numPoints)
for p in pixels: for p in pixels:
p_int = int(p) p_int = int(p)
painter.drawLine(p_int, 0, p_int, 5) painter.drawLine(p_int, 0, p_int, 5)
painter.restore()
painter.drawText(0, -25, prefix)
ev.accept() ev.accept()
def wheelEvent(self, ev): def wheelEvent(self, ev):
@ -62,26 +61,24 @@ class ScanAxis(QtWidgets.QWidget):
# over a tick during a zoom, it should appear as if we are # over a tick during a zoom, it should appear as if we are
# doing zoom relative to the ticks which live in axis # doing zoom relative to the ticks which live in axis
# pixel-space, not slider pixel-space. # pixel-space, not slider pixel-space.
self.sigZoom.emit(z, ev.x() - self.sigZoom.emit(
self.proxy.slider.handleWidth()/2) z, ev.x() - self.proxy.slider.handleWidth()/2)
self.update() self.update()
ev.accept() ev.accept()
def eventFilter(self, obj, ev): def eventFilter(self, obj, ev):
if obj != self.slider: if obj is not self.proxy.slider:
return False return False
if ev.type() != QtCore.QEvent.Wheel: if ev.type() != QtCore.QEvent.Wheel:
return False return False
self.wheelEvent(ev) self.wheelEvent(ev)
return True 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):
sigStartMoved = QtCore.pyqtSignal(int) sigStartMoved = QtCore.pyqtSignal(int)
sigStopMoved = QtCore.pyqtSignal(int) sigStopMoved = QtCore.pyqtSignal(int)
noSlider, startSlider, stopSlider = range(3)
stopStyle = "QSlider::handle {background:red}"
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)
@ -92,8 +89,6 @@ class ScanSlider(QtWidgets.QSlider):
self.stopVal = 99 # upper self.stopVal = 99 # upper
self.offset = 0 self.offset = 0
self.position = 0 self.position = 0
self.lastPressed = ScanSlider.noSlider
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.
@ -103,63 +98,40 @@ class ScanSlider(QtWidgets.QSlider):
# set the stylesheets for drawing each slider later. See paintEvent. # set the stylesheets for drawing each slider later. See paintEvent.
self.dummyStartSlider = QtWidgets.QSlider() self.dummyStartSlider = QtWidgets.QSlider()
self.dummyStopSlider = QtWidgets.QSlider() self.dummyStopSlider = QtWidgets.QSlider()
self.dummyStartSlider.setStyleSheet(ScanSlider.startStyle) self.dummyStartSlider.setStyleSheet(
self.dummyStopSlider.setStyleSheet(ScanSlider.stopStyle) "QSlider::handle {background:blue}")
self.dummyStopSlider.setStyleSheet(
"QSlider::handle {background:red}")
# 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.startSlider: if handle == "start":
opt.sliderPosition = self.startPos opt.sliderPosition = self.startPos
opt.sliderValue = self.startVal opt.sliderValue = self.startVal
elif handle == ScanSlider.stopSlider: elif handle == "stop":
opt.sliderPosition = self.stopPos opt.sliderPosition = self.stopPos
opt.sliderValue = self.stopVal opt.sliderValue = self.stopVal
else:
pass # AssertionErrors
# We get the range of each slider separately. # We get the range of each slider separately.
def pixelPosToRangeValue(self, pos): def pixelPosToRangeValue(self, pos):
opt = QtWidgets.QStyleOptionSlider() opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt) self.initStyleOption(opt)
gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt, gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
QtWidgets.QStyle.SC_SliderGroove, QtWidgets.QStyle.SC_SliderGroove,
self) self)
sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
QtWidgets.QStyle.SC_SliderHandle,
self)
sliderLength = sr.width()
sliderStart = gr.x()
# For historical reasons right() returns left()+width() - 1
# x() is equivalent to left().
sliderStop = gr.right() - sliderLength + 1
rangeVal = QtWidgets.QStyle.sliderValueFromPosition( rangeVal = QtWidgets.QStyle.sliderValueFromPosition(
self.minimum(), self.maximum(), pos - sliderStart, self.minimum(), self.maximum(), pos - gr.x(),
sliderStop - sliderStart, opt.upsideDown) self.effectiveWidth(), opt.upsideDown)
return rangeVal return rangeVal
def rangeValueToPixelPos(self, val): def rangeValueToPixelPos(self, val):
opt = QtWidgets.QStyleOptionSlider() opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt) self.initStyleOption(opt)
gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
QtWidgets.QStyle.SC_SliderGroove,
self)
sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
QtWidgets.QStyle.SC_SliderHandle,
self)
sliderLength = sr.width()
sliderStart = gr.x()
sliderStop = gr.right() - sliderLength + 1
pixel = QtWidgets.QStyle.sliderPositionFromValue( pixel = QtWidgets.QStyle.sliderPositionFromValue(
self.minimum(), self.maximum(), val, sliderStop - sliderStart, self.minimum(), self.maximum(), val, self.effectiveWidth(),
opt.upsideDown) opt.upsideDown)
return pixel return pixel
@ -180,28 +152,15 @@ class ScanSlider(QtWidgets.QSlider):
gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt, gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
QtWidgets.QStyle.SC_SliderGroove, QtWidgets.QStyle.SC_SliderGroove,
self) self)
sliderLength = self.handleWidth() return gr.width() - self.handleWidth()
sliderStart = gr.x()
sliderStop = gr.right() - sliderLength + 1
return sliderStop - sliderStart
# If groove and axis are not aligned (and they should be), we can use
# this function to calculate the offset between them.
def grooveX(self):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
gr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
QtWidgets.QStyle.SC_SliderGroove,
self)
return gr.x()
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 startAtEdges = (handle == "start" and
(self.startVal == self.minimum() or (self.startVal == self.minimum() or
self.startVal == self.maximum())) self.startVal == self.maximum()))
stopAtEdges = (handle == ScanSlider.stopSlider and stopAtEdges = (handle == "stop" and
(self.stopVal == self.minimum() or (self.stopVal == self.minimum() or
self.stopVal == self.maximum())) self.stopVal == self.maximum()))
@ -218,9 +177,7 @@ class ScanSlider(QtWidgets.QSlider):
if control == QtWidgets.QStyle.SC_SliderHandle: if control == QtWidgets.QStyle.SC_SliderHandle:
# no pick()- slider orientation static # no pick()- slider orientation static
self.offset = pos.x() - sr.topLeft().x() self.offset = pos.x() - sr.topLeft().x()
self.lastPressed = handle
self.setSliderDown(True) self.setSliderDown(True)
self.selectedHandle = handle
# emit # emit
# Needed? # Needed?
@ -239,25 +196,11 @@ class ScanSlider(QtWidgets.QSlider):
# if action == QtWidgets.QAbstractSlider.SliderSingleStepAdd: # if action == QtWidgets.QAbstractSlider.SliderSingleStepAdd:
# if # if
def setStartValue(self, val): def setSpan(self, low, high):
self.setSpan(val, self.stopVal) # TODO: Is this necessary? QStyle::sliderPositionFromValue appears
def setStopValue(self, val):
self.setSpan(self.startVal, val)
def setSpan(self, lower, upper):
# TODO: Is bound() necessary? QStyle::sliderPositionFromValue appears
# to clamp already. # to clamp already.
def bound(min, curr, max): low = min(max(self.minimum(), low), self.maximum())
if curr < min: high = min(max(self.minimum(), high), self.maximum())
return min
elif curr > max:
return max
else:
return curr
low = bound(self.minimum(), lower, self.maximum())
high = bound(self.minimum(), upper, self.maximum())
if low != self.startVal or high != self.stopVal: if low != self.startVal or high != self.stopVal:
if low != self.startVal: if low != self.startVal:
@ -276,7 +219,7 @@ class ScanSlider(QtWidgets.QSlider):
if self.isSliderDown(): if self.isSliderDown():
self.sigStartMoved.emit(self.startPos) self.sigStartMoved.emit(self.startPos)
if self.hasTracking() and not self.blockTracking: if self.hasTracking() and not self.blockTracking:
self.setStartValue(val) self.setSpan(self.startPos, self.stopVal)
def setStopPosition(self, val): def setStopPosition(self, val):
if val != self.stopPos: if val != self.stopPos:
@ -286,7 +229,7 @@ class ScanSlider(QtWidgets.QSlider):
if self.isSliderDown(): if self.isSliderDown():
self.sigStopMoved.emit(self.stopPos) self.sigStopMoved.emit(self.stopPos)
if self.hasTracking() and not self.blockTracking: if self.hasTracking() and not self.blockTracking:
self.setStopValue(val) self.setSpan(self.startVal, self.stopPos)
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()):
@ -295,11 +238,10 @@ class ScanSlider(QtWidgets.QSlider):
# Prefer stopVal in the default case. # Prefer stopVal in the default case.
self.upperPressed = self.handleMousePress( self.upperPressed = self.handleMousePress(
ev.pos(), self.upperPressed, self.stopVal, ScanSlider.stopSlider) ev.pos(), self.upperPressed, self.stopVal, "stop")
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.startVal, ev.pos(), self.upperPressed, self.startVal, "start")
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
@ -350,7 +292,6 @@ class ScanSlider(QtWidgets.QSlider):
def paintEvent(self, ev): def paintEvent(self, ev):
# Use QStylePainters to make redrawing as painless as possible. # Use QStylePainters to make redrawing as painless as possible.
painter = QtWidgets.QStylePainter(self)
# Paint on the custom widget, using the attributes of the fake # Paint on the custom widget, using the attributes of the fake
# slider references we keep around. setStyleSheet within paintEvent # slider references we keep around. setStyleSheet within paintEvent
# leads to heavy performance penalties (and recursion?). # leads to heavy performance penalties (and recursion?).
@ -361,22 +302,14 @@ class ScanSlider(QtWidgets.QSlider):
startPainter = QtWidgets.QStylePainter(self, self.dummyStartSlider) startPainter = QtWidgets.QStylePainter(self, self.dummyStartSlider)
stopPainter = QtWidgets.QStylePainter(self, self.dummyStopSlider) stopPainter = QtWidgets.QStylePainter(self, self.dummyStopSlider)
# Groove
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
opt.sliderValue = 0
opt.sliderPosition = 0
opt.subControls = QtWidgets.QStyle.SC_SliderGroove
painter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
# Handles # Handles
# Qt will snap sliders to 0 or maximum() if given a desired pixel # Qt will snap sliders to 0 or maximum() if given a desired pixel
# location outside the mapped range. So we manually just don't draw # location outside the mapped range. So we manually just don't draw
# the handles if they are at 0 or max. # the handles if they are at 0 or max.
if self.startVal > 0 and self.startVal < self.maximum(): if self.startVal > 0 and self.startVal < self.maximum():
self.drawHandle(startPainter, ScanSlider.startSlider) self.drawHandle(startPainter, "start")
if self.stopVal > 0 and self.stopVal < self.maximum(): if self.stopVal > 0 and self.stopVal < self.maximum():
self.drawHandle(stopPainter, ScanSlider.stopSlider) self.drawHandle(stopPainter, "stop")
# real (Sliders) => pixel (one pixel movement of sliders would increment by X) # real (Sliders) => pixel (one pixel movement of sliders would increment by X)
@ -386,7 +319,7 @@ class ScanProxy(QtCore.QObject):
sigStopMoved = QtCore.pyqtSignal(float) sigStopMoved = QtCore.pyqtSignal(float)
sigNumPoints = QtCore.pyqtSignal(int) sigNumPoints = QtCore.pyqtSignal(int)
def __init__(self, slider, axis, rangeFactor): def __init__(self, slider, axis, rangeFactor, dynamicRange):
QtCore.QObject.__init__(self) QtCore.QObject.__init__(self)
self.axis = axis self.axis = axis
self.slider = slider self.slider = slider
@ -394,6 +327,7 @@ class ScanProxy(QtCore.QObject):
self.realStop = 0 self.realStop = 0
self.numPoints = 10 self.numPoints = 10
self.rangeFactor = rangeFactor self.rangeFactor = rangeFactor
self.dynamicRange = dynamicRange
# Transform that maps the spinboxes to a pixel position on the # Transform that maps the spinboxes to a pixel position on the
# axis. 0 to axis.width() exclusive indicate positions which will be # axis. 0 to axis.width() exclusive indicate positions which will be
@ -401,42 +335,23 @@ class ScanProxy(QtCore.QObject):
# Because the axis's width will change when placed within a layout, # Because the axis's width will change when placed within a layout,
# the realToPixelTransform will initially be invalid. It will be set # the realToPixelTransform will initially be invalid. It will be set
# properly during the first resizeEvent, with the below transform. # properly during the first resizeEvent, with the below transform.
self.realToPixelTransform = self.calculateNewRealToPixel( self.realToPixelTransform = self.axis.width()/2, 1.
-self.axis.width()/2, 1.0)
self.invalidOldSizeExpected = True self.invalidOldSizeExpected = True
# 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
# resize and zoom events.
def calculateNewRealToPixel(self, targetLeft, targetScale):
return QtGui.QTransform.fromScale(targetScale, 1).translate(
-targetLeft, 0)
# 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):
rawVal = (QtCore.QPointF(val, 0) * self.realToPixelTransform).x() a, b = self.realToPixelTransform
rawVal = b*(val + a)
# Clamp pixel values to 32 bits, b/c Qt will otherwise wrap values. # Clamp pixel values to 32 bits, b/c Qt will otherwise wrap values.
if rawVal < -(2**31): rawVal = min(max(-(1 << 31), rawVal), (1 << 31) - 1)
rawVal = -(2**31)
elif rawVal > (2**31 - 1):
rawVal = (2**31 - 1)
return rawVal 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):
(revXform, invertible) = self.realToPixelTransform.inverted() a, b = self.realToPixelTransform
if not invertible: return val/b - a
revXform = (QtGui.QTransform.fromTranslate(
-self.realToPixelTransform.dx(), 0) *
QtGui.QTransform.fromScale(
1/self.realToPixelTransform.m11(), 0))
realPoint = QtCore.QPointF(val, 0) * revXform
return realPoint.x()
def rangeToReal(self, val): def rangeToReal(self, val):
# gx = self.slider.grooveX()
# ax = self.axis.x()
# assert gx == ax, "gx: {}, ax: {}".format(gx, ax)
pixelVal = self.slider.rangeValueToPixelPos(val) pixelVal = self.slider.rangeValueToPixelPos(val)
return self.pixelToReal(pixelVal) return self.pixelToReal(pixelVal)
@ -470,11 +385,12 @@ class ScanProxy(QtCore.QObject):
self.axis.update() self.axis.update()
def handleZoom(self, zoomFactor, mouseXPos): def handleZoom(self, zoomFactor, mouseXPos):
newScale = self.realToPixelTransform.m11() * zoomFactor newScale = self.realToPixelTransform[1] * zoomFactor
refReal = self.pixelToReal(mouseXPos) refReal = self.pixelToReal(mouseXPos)
newLeft = refReal - mouseXPos/newScale newLeft = mouseXPos/newScale - refReal
self.realToPixelTransform = self.calculateNewRealToPixel( if abs(newLeft*newScale) > self.dynamicRange:
newLeft, newScale) return
self.realToPixelTransform = newLeft, newScale
self.moveStop(self.realStop) self.moveStop(self.realStop)
self.moveStart(self.realStart) self.moveStart(self.realStart)
@ -485,21 +401,41 @@ class ScanProxy(QtCore.QObject):
refSlider = self.realStop refSlider = self.realStop
else: else:
refSlider = self.realStart refSlider = self.realStart
if self.rangeFactor <= 2: if self.rangeFactor <= 2 or currRangeReal == 0:
return # Ill-formed snap range- do nothing. 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 = refSlider - self.slider.effectiveWidth() \ newLeft = (self.slider.effectiveWidth()/(self.rangeFactor*newScale) -
/ (self.rangeFactor*newScale) refSlider)
self.realToPixelTransform = self.calculateNewRealToPixel( self.realToPixelTransform = newLeft, newScale
newLeft, newScale)
self.printTransform()
self.moveStop(self.realStop) self.moveStop(self.realStop)
self.moveStart(self.realStart) self.moveStart(self.realStart)
self.axis.update() # Axis normally takes care to update itself during 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 # zoom. In this code path however, the zoom didn't arrive via the axis
# widget, so we need to notify manually. # widget, so we need to notify manually.
# This function is called if the axis width, slider width, and slider
# positions are in an inconsistent state, to initialize the widget.
# This function handles handles the slider positions. Slider and axis
# handle its own width changes; proxy watches for axis width resizeEvent to
# alter mapping from real to pixel space.
def zoomToFitInit(self):
currRangeReal = abs(self.realStop - self.realStart)
if self.rangeFactor <= 2 or currRangeReal == 0:
self.moveStop(self.realStop)
self.moveStart(self.realStart)
# Ill-formed snap range- move the sliders anyway,
# because we arrived here during widget
# initialization, where the slider positions are likely invalid.
# This will force the sliders to have positions on the axis
# which reflect the start/stop values currently set.
else:
self.zoomToFit()
# Notify spinboxes manually, since slider wasn't clicked and will
# therefore not emit signals.
self.sigStopMoved.emit(self.realStop)
self.sigStartMoved.emit(self.realStart)
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
@ -527,15 +463,22 @@ class ScanProxy(QtCore.QObject):
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)
self.realToPixelTransform = -oldLeft, newScale
else: else:
# TODO: self.axis.width() is invalid during object # TODO: self.axis.width() is invalid during object
# construction. The width will change when placed in a # construction. The width will change when placed in a
# layout WITHOUT a resizeEvent. Why? # layout WITHOUT a resizeEvent. Why?
oldLeft = -ev.size().width()/2 oldLeft = -ev.size().width()/2
newScale = 1.0 newScale = 1.0
self.realToPixelTransform = -oldLeft, newScale
# We need to reinitialize the pixel transform b/c the old width
# of the axis is no longer valid. When we have a valid transform,
# we can then zoomToFit based on the desired real values.
# The slider handle values are invalid before this point as well;
# we set them to the correct value here, regardless of whether
# the slider has already resized itsef or not.
self.zoomToFitInit()
self.invalidOldSizeExpected = False self.invalidOldSizeExpected = False
self.realToPixelTransform = self.calculateNewRealToPixel(
oldLeft, newScale)
# assert self.pixelToReal(0) == oldLeft, \ # assert self.pixelToReal(0) == oldLeft, \
# "{}, {}".format(self.pixelToReal(0), oldLeft) # "{}, {}".format(self.pixelToReal(0), oldLeft)
# Slider will update independently, making sure that the old # Slider will update independently, making sure that the old
@ -544,26 +487,17 @@ class ScanProxy(QtCore.QObject):
# same positions in the new axis-space. # same positions in the new axis-space.
return False return False
def printTransform(self):
print("m11: {}, dx: {}".format(
self.realToPixelTransform.m11(), self.realToPixelTransform.dx()))
(inverted, invertible) = self.realToPixelTransform.inverted()
print("m11: {}, dx: {}, singular: {}".format(
inverted.m11(), inverted.dx(), not invertible))
class ScanWidget(QtWidgets.QWidget): class ScanWidget(QtWidgets.QWidget):
sigStartMoved = QtCore.pyqtSignal(float) sigStartMoved = QtCore.pyqtSignal(float)
sigStopMoved = QtCore.pyqtSignal(float) sigStopMoved = QtCore.pyqtSignal(float)
sigNumChanged = QtCore.pyqtSignal(int) sigNumChanged = QtCore.pyqtSignal(int)
def __init__(self, zoomFactor=1.05, rangeFactor=6): def __init__(self, zoomFactor=1.05, rangeFactor=6, dynamicRange=1e8):
QtWidgets.QWidget.__init__(self) QtWidgets.QWidget.__init__(self)
self.slider = slider = ScanSlider() self.slider = slider = ScanSlider()
self.axis = axis = ScanAxis(zoomFactor) self.axis = axis = ScanAxis(zoomFactor)
zoomFitButton = QtWidgets.QPushButton("View Range") self.proxy = ScanProxy(slider, axis, rangeFactor, dynamicRange)
fitViewButton = QtWidgets.QPushButton("Snap Range")
self.proxy = ScanProxy(slider, axis, rangeFactor)
axis.proxy = self.proxy axis.proxy = self.proxy
axis.slider = slider axis.slider = slider
slider.setMaximum(1023) slider.setMaximum(1023)
@ -574,11 +508,9 @@ class ScanWidget(QtWidgets.QWidget):
layout.setRowMinimumHeight(0, 40) layout.setRowMinimumHeight(0, 40)
layout.addWidget(axis, 0, 0, 1, -1) layout.addWidget(axis, 0, 0, 1, -1)
layout.addWidget(slider, 1, 0, 1, -1) layout.addWidget(slider, 1, 0, 1, -1)
layout.addWidget(zoomFitButton, 2, 0)
layout.addWidget(fitViewButton, 2, 1)
self.setLayout(layout) self.setLayout(layout)
# Connect signals # Connect signals (minus context menu)
slider.sigStopMoved.connect(self.proxy.handleStopMoved) slider.sigStopMoved.connect(self.proxy.handleStopMoved)
slider.sigStartMoved.connect(self.proxy.handleStartMoved) slider.sigStartMoved.connect(self.proxy.handleStartMoved)
self.proxy.sigStopMoved.connect(self.sigStopMoved) self.proxy.sigStopMoved.connect(self.sigStopMoved)
@ -586,13 +518,17 @@ class ScanWidget(QtWidgets.QWidget):
self.proxy.sigNumPoints.connect(self.sigNumChanged) self.proxy.sigNumPoints.connect(self.sigNumChanged)
axis.sigZoom.connect(self.proxy.handleZoom) axis.sigZoom.connect(self.proxy.handleZoom)
axis.sigPoints.connect(self.proxy.handleNumPoints) axis.sigPoints.connect(self.proxy.handleNumPoints)
fitViewButton.clicked.connect(self.fitToView)
zoomFitButton.clicked.connect(self.zoomToFit)
# Connect event observers. # Connect event observers.
axis.installEventFilter(self.proxy) axis.installEventFilter(self.proxy)
slider.installEventFilter(axis) slider.installEventFilter(axis)
# Context menu entries
self.zoomToFitAct = QtWidgets.QAction("&View Range", self)
self.fitToViewAct = QtWidgets.QAction("&Snap Range", self)
self.zoomToFitAct.triggered.connect(self.zoomToFit)
self.fitToViewAct.triggered.connect(self.fitToView)
# 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 setStop(self, val): def setStop(self, val):
@ -610,5 +546,8 @@ class ScanWidget(QtWidgets.QWidget):
def fitToView(self): def fitToView(self):
self.proxy.fitToView() self.proxy.fitToView()
def reset(self): def contextMenuEvent(self, ev):
self.proxy.reset() menu = QtWidgets.QMenu(self)
menu.addAction(self.zoomToFitAct)
menu.addAction(self.fitToViewAct)
menu.exec(ev.globalPos())