Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: m-labs/artiq
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 8ad799a8504f
Choose a base ref
...
head repository: m-labs/artiq
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 2e73d6c89cbb
Choose a head ref
  • 2 commits
  • 1 file changed
  • 1 contributor

Commits on Mar 14, 2016

  1. Copy the full SHA
    1dc7263 View commit details
  2. Copy the full SHA
    2e73d6c View commit details
Showing with 143 additions and 250 deletions.
  1. +143 −250 artiq/gui/scanwidget.py
393 changes: 143 additions & 250 deletions artiq/gui/scanwidget.py
Original file line number Diff line number Diff line change
@@ -62,43 +62,27 @@ class ScanSlider(QtWidgets.QSlider):

def __init__(self):
QtWidgets.QSlider.__init__(self, QtCore.Qt.Horizontal)
self.startPos = 0 # Pos and Val can differ in event handling.
# perhaps prevPos and currPos is more accurate.
self.stopPos = 99
self.startVal = 0 # lower
self.stopVal = 99 # upper
self.offset = 0
self.position = 0
self.upperPressed = QtWidgets.QStyle.SC_None
self.lowerPressed = QtWidgets.QStyle.SC_None
self.firstMovement = False # State var for handling slider overlap.
self.blockTracking = False

self.setMinimum(0)
self.setMaximum(4095)
self.startVal = None
self.stopVal = None
self.offset = None
self.position = None
self.pressed = None

self.setRange(0, (1 << 15) - 1)

# We need fake sliders to keep around so that we can dynamically
# set the stylesheets for drawing each slider later. See paintEvent.
# QPalettes would be nicer to use, since palette entries can be set
# individually for each slider handle, but Windows 7 does not
# use them. This seems to be the only way to override the colors
# regardless of platform.
self.dummyStartSlider = QtWidgets.QSlider()
self.dummyStopSlider = QtWidgets.QSlider()
self.dummyStartSlider.setStyleSheet(
"QSlider::handle {background:blue}")
self.dummyStopSlider.setStyleSheet(
"QSlider::handle {background:red}")

# We basically superimpose two QSliders on top of each other, discarding
# the state that remains constant between the two when drawing.
# Everything except the handles remain constant.
def initHandleStyleOption(self, opt, handle):
self.initStyleOption(opt)
if handle == "start":
opt.sliderPosition = self.startPos
opt.sliderValue = self.startVal
elif handle == "stop":
opt.sliderPosition = self.stopPos
opt.sliderValue = self.stopVal

# We get the range of each slider separately.
def pixelPosToRangeValue(self, pos):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
@@ -137,98 +121,59 @@ def effectiveWidth(self):
self)
return gr.width() - self.handleWidth()

def handleMousePress(self, pos, control, val, handle):
def _getStyleOptionSlider(self, val):
opt = QtWidgets.QStyleOptionSlider()
self.initHandleStyleOption(opt, handle)
startAtEdges = (handle == "start" and
(self.startVal == self.minimum() or
self.startVal == self.maximum()))
stopAtEdges = (handle == "stop" and
(self.stopVal == self.minimum() or
self.stopVal == self.maximum()))
self.initStyleOption(opt)
opt.sliderPosition = val
opt.sliderValue = val
opt.subControls = QtWidgets.QStyle.SC_SliderHandle
return opt

def _hitHandle(self, pos, val):
# If chosen slider at edge, treat it as non-interactive.
if startAtEdges or stopAtEdges:
return QtWidgets.QStyle.SC_None

oldControl = control
if not (self.minimum() < val < self.maximum()):
return False
opt = self._getStyleOptionSlider(val)
control = self.style().hitTestComplexControl(
QtWidgets.QStyle.CC_Slider, opt, pos, self)
sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
QtWidgets.QStyle.SC_SliderHandle,
self)
if control == QtWidgets.QStyle.SC_SliderHandle:
# no pick()- slider orientation static
self.offset = pos.x() - sr.topLeft().x()
self.setSliderDown(True)
# emit

if control != QtWidgets.QStyle.SC_SliderHandle:
return False
sr = self.style().subControlRect(
QtWidgets.QStyle.CC_Slider, opt,
QtWidgets.QStyle.SC_SliderHandle, self)
self.offset = pos.x() - sr.topLeft().x()
self.setSliderDown(True)
# Needed?
if control != oldControl:
self.update(sr)
return control

def drawHandle(self, painter, handle):
opt = QtWidgets.QStyleOptionSlider()
self.initStyleOption(opt)
self.initHandleStyleOption(opt, handle)
opt.subControls = QtWidgets.QStyle.SC_SliderHandle
painter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)

def setSpan(self, low, high):
# TODO: Is this necessary? QStyle::sliderPositionFromValue appears
# to clamp already.
low = min(max(self.minimum(), low), self.maximum())
high = min(max(self.minimum(), high), self.maximum())

if low != self.startVal or high != self.stopVal:
if low != self.startVal:
self.startVal = low
self.startPos = low
if high != self.stopVal:
self.stopVal = high
self.stopPos = high
self.update()
self.update(sr)
return True

def setStartPosition(self, val):
if val != self.startPos:
self.startPos = val
if not self.hasTracking():
self.update()
if self.isSliderDown():
self.sigStartMoved.emit(self.startPos)
if self.hasTracking() and not self.blockTracking:
self.setSpan(self.startPos, self.stopVal)
if val == self.startVal:
return
self.startVal = val
self.update()

def setStopPosition(self, val):
if val != self.stopPos:
self.stopPos = val
if not self.hasTracking():
self.update()
if self.isSliderDown():
self.sigStopMoved.emit(self.stopPos)
if self.hasTracking() and not self.blockTracking:
self.setSpan(self.startVal, self.stopPos)
if val == self.stopVal:
return
self.stopVal = val
self.update()

def mousePressEvent(self, ev):
if ev.buttons() ^ ev.button():
ev.ignore()
return

# Prefer stopVal in the default case.
self.upperPressed = self.handleMousePress(
ev.pos(), self.upperPressed, self.stopVal, "stop")
if self.upperPressed != QtWidgets.QStyle.SC_SliderHandle:
self.lowerPressed = self.handleMousePress(
ev.pos(), self.upperPressed, self.startVal, "start")

# State that is needed to handle the case where two sliders are equal.
self.firstMovement = True
if self._hitHandle(ev.pos(), self.stopVal):
self.pressed = "stop"
elif self._hitHandle(ev.pos(), self.startVal):
self.pressed = "start"
else:
self.pressed = None
ev.accept()

def mouseMoveEvent(self, ev):
if (self.lowerPressed != QtWidgets.QStyle.SC_SliderHandle and
self.upperPressed != QtWidgets.QStyle.SC_SliderHandle):
if not self.pressed:
ev.ignore()
return

@@ -246,49 +191,38 @@ def mouseMoveEvent(self, ev):
if not r.contains(ev.pos()):
newPos = self.position

if self.firstMovement:
if self.startPos == self.stopPos:
# StopSlider is preferred, except in the case where
# start == max possible value the slider can take.
if self.startPos == self.maximum():
self.lowerPressed = QtWidgets.QStyle.SC_SliderHandle
self.upperPressed = QtWidgets.QStyle.SC_None
self.firstMovement = False

if self.lowerPressed == QtWidgets.QStyle.SC_SliderHandle:
if self.pressed == "start":
self.setStartPosition(newPos)

if self.upperPressed == QtWidgets.QStyle.SC_SliderHandle:
if self.hasTracking():
self.sigStartMoved.emit(self.startVal)
elif self.pressed == "stop":
self.setStopPosition(newPos)
if self.hasTracking():
self.sigStopMoved.emit(self.stopVal)

ev.accept()

def mouseReleaseEvent(self, ev):
QtWidgets.QSlider.mouseReleaseEvent(self, ev)
self.setSliderDown(False) # AbstractSlider needs this
self.lowerPressed = QtWidgets.QStyle.SC_None
self.upperPressed = QtWidgets.QStyle.SC_None
if not self.hasTracking():
if self.pressed == "start":
self.sigStartMoved.emit(self.startVal)
elif self.pressed == "stop":
self.sigStopMoved.emit(self.stopVal)
self.pressed = None

def paintEvent(self, ev):
# Use QStylePainters to make redrawing as painless as possible.
# Paint on the custom widget, using the attributes of the fake
# slider references we keep around. setStyleSheet within paintEvent
# leads to heavy performance penalties (and recursion?).
# QPalettes would be nicer to use, since palette entries can be set
# individually for each slider handle, but Windows 7 does not
# use them. This seems to be the only way to override the colors
# regardless of platform.
# Use the pre-parsed, styled sliders.
startPainter = QtWidgets.QStylePainter(self, self.dummyStartSlider)
stopPainter = QtWidgets.QStylePainter(self, self.dummyStopSlider)

# Handles
# 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
# the handles if they are at 0 or max.
# Only draw handles that are not railed
if self.minimum() < self.startVal < self.maximum():
self.drawHandle(startPainter, "start")
opt = self._getStyleOptionSlider(self.startVal)
startPainter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
if self.minimum() < self.stopVal < self.maximum():
self.drawHandle(stopPainter, "stop")
opt = self._getStyleOptionSlider(self.stopVal)
stopPainter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)


# real (Sliders) => pixel (one pixel movement of sliders would increment by X)
@@ -320,27 +254,20 @@ def __init__(self, zoomFactor=1.05, zoomMargin=.1, dynamicRange=1e9):
snapRangeAct.triggered.connect(self.snapRange)
self.menu.addAction(snapRangeAct)

self.realStart = -1.
self.realStop = 1.
self.numPoints = 11
self.realStart = None
self.realStop = None
self.numPoints = None
self.zoomMargin = zoomMargin
self.dynamicRange = dynamicRange
self.zoomFactor = zoomFactor

# Transform that maps the spinboxes to a pixel position on the
# axis. 0 to axis.width() exclusive indicate positions which will be
# displayed on the axis.
# Because the axis's width will change when placed within a layout,
# the realToPixelTransform will initially be invalid. It will be set
# properly during the first resizeEvent, with the below transform.
self.realToPixelTransform = -self.axis.width()/2, 1.
self.invalidOldSizeExpected = True

# Connect event observers.
axis.installEventFilter(self)
slider.installEventFilter(self)
slider.sigStopMoved.connect(self.handleStopMoved)
slider.sigStartMoved.connect(self.handleStartMoved)
slider.sigStopMoved.connect(self._handleStopMoved)
slider.sigStartMoved.connect(self._handleStartMoved)

def contextMenuEvent(self, ev):
self.menu.popup(ev.globalPos())
@@ -353,7 +280,6 @@ def realToPixel(self, val):
rawVal = min(max(-(1 << 31), rawVal), (1 << 31) - 1)
return rawVal

# Get a point from pixel units to what the sliders display.
def pixelToReal(self, val):
a, b = self.realToPixelTransform
return val/b + a
@@ -366,42 +292,38 @@ def realToRange(self, val):
pixelVal = self.realToPixel(val)
return self.slider.pixelPosToRangeValue(pixelVal)

def setView(self, left, scale):
self.realToPixelTransform = left, scale
sliderX = self.realToRange(self.realStop)
self.slider.setStopPosition(sliderX)
sliderX = self.realToRange(self.realStart)
self.slider.setStartPosition(sliderX)
self.axis.update()

def setStop(self, val):
if self.realStop == val:
return
sliderX = self.realToRange(val)
self.slider.setStopPosition(sliderX)
self.realStop = val
self.axis.update() # Number of points ticks changed positions.
self.sigStopMoved.emit(val)

def setStart(self, val):
if self.realStart == val:
return
sliderX = self.realToRange(val)
self.slider.setStartPosition(sliderX)
self.realStart = val
self.axis.update()
self.sigStartMoved.emit(val)

def setNumPoints(self, val):
if self.numPoints == val:
return
self.numPoints = val
self.axis.update()

def handleStopMoved(self, rangeVal):
# FIXME: this relies on the event being fed back and ending up calling
# setStop()
self.sigStopMoved.emit(self.rangeToReal(rangeVal))

def handleStartMoved(self, rangeVal):
# FIXME: this relies on the event being fed back and ending up calling
# setStart()
self.sigStartMoved.emit(self.rangeToReal(rangeVal))

def handleZoom(self, zoomFactor, mouseXPos):
newScale = self.realToPixelTransform[1] * zoomFactor
refReal = self.pixelToReal(mouseXPos)
newLeft = refReal - mouseXPos/newScale
newZero = newLeft*newScale + self.slider.effectiveWidth()/2
if zoomFactor > 1 and abs(newZero) > self.dynamicRange:
return
self.realToPixelTransform = newLeft, newScale
self.setStop(self.realStop)
self.setStart(self.realStart)
self.sigNumChanged.emit(val)

def viewRange(self):
newScale = self.slider.effectiveWidth()/abs(
@@ -411,83 +333,63 @@ def viewRange(self):
if newCenter:
newScale = min(newScale, self.dynamicRange/abs(newCenter))
newLeft = newCenter - self.slider.effectiveWidth()/2/newScale
self.realToPixelTransform = newLeft, newScale
self.setStop(self.realStop)
self.setStart(self.realStart)
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.

# 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 viewRangeInit(self):
currRangeReal = abs(self.realStop - self.realStart)
if currRangeReal == 0:
self.setStop(self.realStop)
self.setStart(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.viewRange()
# Notify spinboxes manually, since slider wasn't clicked and will
# therefore not emit signals.
self.sigStopMoved.emit(self.realStop)
self.sigStartMoved.emit(self.realStart)
self.setView(newLeft, newScale)

def snapRange(self):
lowRange = self.zoomMargin
highRange = 1 - self.zoomMargin
newStart = self.pixelToReal(lowRange * self.slider.effectiveWidth())
newStop = self.pixelToReal(highRange * self.slider.effectiveWidth())
# Signals won't fire unless slider was actually grabbed, so
# manually update so the spinboxes know that knew values were set.
# self.realStop/Start and the sliders themselves will be updated as a
# consequence of ValueChanged signal in spinboxes. The slider widget
# has guards against recursive signals in setSpan().
# FIXME: this relies on the events being fed back and ending up
# calling setStart() and setStop()
self.sigStopMoved.emit(newStop)
self.sigStartMoved.emit(newStart)

def wheelEvent(self, ev):
self.setStart(newStart)
self.setStop(newStop)

def _handleStartMoved(self, rangeVal):
val = self.rangeToReal(rangeVal)
self.realStart = val
self.axis.update()
self.sigStartMoved.emit(val)

def _handleStopMoved(self, rangeVal):
val = self.rangeToReal(rangeVal)
self.realStop = val
self.axis.update()
self.sigStopMoved.emit(val)

def _handleZoom(self, zoomFactor, mouseXPos):
newScale = self.realToPixelTransform[1] * zoomFactor
refReal = self.pixelToReal(mouseXPos)
newLeft = refReal - mouseXPos/newScale
newZero = newLeft*newScale + self.slider.effectiveWidth()/2
if zoomFactor > 1 and abs(newZero) > self.dynamicRange:
return
self.setView(newLeft, newScale)

def _wheelEvent(self, ev):
y = ev.angleDelta().y()
if y:
if ev.modifiers() & QtCore.Qt.ShiftModifier:
# If shift+scroll, modify number of points.
# TODO: This is not perfect. For high-resolution touchpads you
# get many small events with y < 120 which should accumulate.
# That would also match the wheel behavior of an integer
# spinbox.
z = int(y / 120.)
# FIXME: this relies on the event being fed back and ending up
# calling setNumPoints()
self.sigNumChanged.emit(self.numPoints + z)
self.axis.update()
ev.accept()
elif ev.modifiers() & QtCore.Qt.ControlModifier:
if ev.modifiers() & QtCore.Qt.ShiftModifier:
# If shift+scroll, modify number of points.
# TODO: This is not perfect. For high-resolution touchpads you
# get many small events with y < 120 which should accumulate.
# That would also match the wheel behavior of an integer
# spinbox.
z = int(y / 120.)
if z:
self.setNumPoints(max(1, self.numPoints + z))
ev.accept()
elif ev.modifiers() & QtCore.Qt.ControlModifier:
# Remove the slider-handle shift correction, b/c none of the
# other widgets know about it. If we have the mouse directly
# over a tick during a zoom, it should appear as if we are
# doing zoom relative to the ticks which live in axis
# pixel-space, not slider pixel-space.
if y:
z = self.zoomFactor**(y / 120.)
# 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.handleZoom(z, ev.x() - self.slider.handleWidth()/2)
ev.accept()
else:
ev.ignore()
self._handleZoom(z, ev.x() - self.slider.handleWidth()/2)
ev.accept()
else:
ev.ignore()

def eventFilter(self, obj, ev):
if ev.type() == QtCore.QEvent.Wheel:
self.wheelEvent(ev)
return True
if not (obj is self.axis and ev.type() == QtCore.QEvent.Resize):
return False
def resizeEvent(self, ev):
if ev.oldSize().isValid():
oldLeft = self.pixelToReal(0)
refWidth = ev.oldSize().width() - self.slider.handleWidth()
@@ -497,24 +399,15 @@ def eventFilter(self, obj, ev):
center = (self.realStop + self.realStart)/2
if center:
newScale = min(newScale, self.dynamicRange/abs(center))
self.realToPixelTransform = oldLeft, newScale
self.setView(oldLeft, newScale)
else:
# TODO: self.axis.width() is invalid during object
# construction. The width will change when placed in a
# layout WITHOUT a resizeEvent. Why?
oldLeft = -ev.size().width()/2
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 viewRange 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.viewRangeInit()
self.invalidOldSizeExpected = False
# Slider will update independently, making sure that the old
# slider positions are preserved. Because of this, we can be
# confident that the new slider position will still map to the
# same positions in the new axis-space.
self.viewRange()

def eventFilter(self, obj, ev):
if ev.type() == QtCore.QEvent.Wheel:
self._wheelEvent(ev)
return True
if ev.type() == QtCore.QEvent.Resize:
ev.ignore()
return True
return False