forked from M-Labs/artiq
scanwidget: apply changes as of 98f0a56
This commit is contained in:
parent
bc9203457e
commit
e4b854b8bf
|
@ -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)
|
||||||
|
|
|
@ -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,16 +26,18 @@ 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,
|
||||||
|
@ -43,9 +45,6 @@ class ScanAxis(QtWidgets.QWidget):
|
||||||
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())
|
||||||
|
|
Loading…
Reference in New Issue