2016-03-12 01:29:21 +08:00
|
|
|
import logging
|
|
|
|
|
2016-02-26 03:34:04 +08:00
|
|
|
from PyQt5 import QtGui, QtCore, QtWidgets
|
2016-03-16 22:13:31 +08:00
|
|
|
import numpy as np
|
2016-02-26 03:34:04 +08:00
|
|
|
|
2016-03-11 22:38:22 +08:00
|
|
|
from .ticker import Ticker
|
|
|
|
|
2016-02-26 03:34:04 +08:00
|
|
|
|
2016-03-12 01:29:21 +08:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2016-03-16 22:13:31 +08:00
|
|
|
class ScanWidget(QtWidgets.QSlider):
|
|
|
|
startChanged = QtCore.pyqtSignal(float)
|
|
|
|
stopChanged = QtCore.pyqtSignal(float)
|
|
|
|
numChanged = QtCore.pyqtSignal(int)
|
|
|
|
|
|
|
|
def __init__(self, zoomFactor=1.05, zoomMargin=.1, dynamicRange=1e9):
|
|
|
|
QtWidgets.QSlider.__init__(self, QtCore.Qt.Horizontal)
|
|
|
|
self.zoomMargin = zoomMargin
|
|
|
|
self.dynamicRange = dynamicRange
|
|
|
|
self.zoomFactor = zoomFactor
|
|
|
|
|
2016-02-26 03:34:04 +08:00
|
|
|
self.ticker = Ticker()
|
|
|
|
|
2016-03-16 22:13:31 +08:00
|
|
|
self.menu = QtWidgets.QMenu(self)
|
|
|
|
action = QtWidgets.QAction("&View Range", self)
|
|
|
|
action.triggered.connect(self.viewRange)
|
|
|
|
self.menu.addAction(action)
|
|
|
|
action = QtWidgets.QAction("&Snap Range", self)
|
|
|
|
action.triggered.connect(self.snapRange)
|
|
|
|
self.menu.addAction(action)
|
2016-03-11 22:38:22 +08:00
|
|
|
|
2016-03-16 22:13:31 +08:00
|
|
|
self._startSlider = QtWidgets.QSlider()
|
|
|
|
self._startSlider.setStyleSheet("QSlider::handle {background:blue}")
|
|
|
|
self._stopSlider = QtWidgets.QSlider()
|
|
|
|
self._stopSlider.setStyleSheet("QSlider::handle {background:red}")
|
2016-03-11 22:38:22 +08:00
|
|
|
|
2016-03-16 22:13:31 +08:00
|
|
|
self.setRange(0, 4095)
|
2016-02-26 03:34:04 +08:00
|
|
|
|
2016-03-16 22:13:31 +08:00
|
|
|
self._start, self._stop, self._num = None, None, None
|
|
|
|
self._axisView, self._sliderView = None, None
|
2016-03-16 22:59:49 +08:00
|
|
|
self._offset, self._pressed, self._dragLeft = None, None, None
|
2016-02-26 03:34:04 +08:00
|
|
|
|
2016-03-16 22:13:31 +08:00
|
|
|
def contextMenuEvent(self, ev):
|
|
|
|
self.menu.popup(ev.globalPos())
|
|
|
|
|
|
|
|
def _axisToPixel(self, val):
|
|
|
|
a, b = self._axisView
|
|
|
|
return a + val*b
|
|
|
|
|
|
|
|
def _pixelToAxis(self, val):
|
|
|
|
a, b = self._axisView
|
|
|
|
return (val - a)/b
|
|
|
|
|
|
|
|
def _setView(self, axis_left, axis_scale):
|
2016-02-26 03:34:04 +08:00
|
|
|
opt = QtWidgets.QStyleOptionSlider()
|
|
|
|
self.initStyleOption(opt)
|
2016-03-16 22:13:31 +08:00
|
|
|
g = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
|
|
|
QtWidgets.QStyle.SC_SliderGroove,
|
|
|
|
self)
|
|
|
|
h = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
|
|
|
QtWidgets.QStyle.SC_SliderHandle,
|
|
|
|
self)
|
|
|
|
slider_left = g.x() + h.width()/2
|
|
|
|
slider_scale = (self.maximum() - self.minimum())/(
|
|
|
|
g.width() - h.width())
|
|
|
|
|
|
|
|
self._axisView = axis_left, axis_scale
|
|
|
|
self._sliderView = ((axis_left - slider_left)*slider_scale,
|
|
|
|
axis_scale*slider_scale)
|
|
|
|
self.update()
|
|
|
|
|
|
|
|
def setStart(self, val):
|
|
|
|
if self._start == val:
|
|
|
|
return
|
|
|
|
self._start = val
|
|
|
|
self.update()
|
|
|
|
self.startChanged.emit(val)
|
|
|
|
|
|
|
|
def setStop(self, val):
|
|
|
|
if self._stop == val:
|
|
|
|
return
|
|
|
|
self._stop = val
|
|
|
|
self.update()
|
|
|
|
self.stopChanged.emit(val)
|
|
|
|
|
|
|
|
def setNum(self, val):
|
|
|
|
if self._num == val:
|
|
|
|
return
|
|
|
|
self._num = val
|
|
|
|
self.update()
|
|
|
|
self.numChanged.emit(val)
|
|
|
|
|
|
|
|
def viewRange(self):
|
|
|
|
center = (self._stop + self._start)/2
|
|
|
|
scale = self.width()*(1 - 2*self.zoomMargin)
|
|
|
|
if self._stop != self._start:
|
|
|
|
scale /= abs(self._stop - self._start)
|
|
|
|
if center:
|
|
|
|
scale = min(scale, self.dynamicRange/abs(center))
|
|
|
|
else:
|
|
|
|
scale = self.dynamicRange
|
|
|
|
if center:
|
|
|
|
scale /= abs(center)
|
|
|
|
left = self.width()/2 - center*scale
|
|
|
|
self._setView(left, scale)
|
|
|
|
|
|
|
|
def snapRange(self):
|
|
|
|
self.setStart(self._pixelToAxis(self.zoomMargin*self.width()))
|
|
|
|
self.setStop(self._pixelToAxis((1 - self.zoomMargin)*self.width()))
|
2016-02-26 03:34:04 +08:00
|
|
|
|
2016-03-14 18:45:26 +08:00
|
|
|
def _getStyleOptionSlider(self, val):
|
2016-03-16 22:13:31 +08:00
|
|
|
a, b = self._sliderView
|
|
|
|
val = a + val*b
|
|
|
|
if not (self.minimum() <= val <= self.maximum()):
|
|
|
|
return None
|
2016-02-26 03:34:04 +08:00
|
|
|
opt = QtWidgets.QStyleOptionSlider()
|
2016-03-14 18:45:26 +08:00
|
|
|
self.initStyleOption(opt)
|
|
|
|
opt.sliderPosition = val
|
|
|
|
opt.sliderValue = val
|
|
|
|
opt.subControls = QtWidgets.QStyle.SC_SliderHandle
|
|
|
|
return opt
|
2016-03-08 19:35:13 +08:00
|
|
|
|
2016-03-14 18:45:26 +08:00
|
|
|
def _hitHandle(self, pos, val):
|
|
|
|
opt = self._getStyleOptionSlider(val)
|
2016-03-16 22:13:31 +08:00
|
|
|
if not opt:
|
|
|
|
return False
|
2016-02-26 03:34:04 +08:00
|
|
|
control = self.style().hitTestComplexControl(
|
|
|
|
QtWidgets.QStyle.CC_Slider, opt, pos, self)
|
2016-03-14 18:45:26 +08:00
|
|
|
if control != QtWidgets.QStyle.SC_SliderHandle:
|
|
|
|
return False
|
2016-03-16 22:13:31 +08:00
|
|
|
sr = self.style().subControlRect(QtWidgets.QStyle.CC_Slider, opt,
|
|
|
|
QtWidgets.QStyle.SC_SliderHandle,
|
|
|
|
self)
|
|
|
|
self._offset = pos.x() - sr.center().x()
|
2016-03-14 18:45:26 +08:00
|
|
|
self.setSliderDown(True)
|
|
|
|
return True
|
2016-02-26 03:34:04 +08:00
|
|
|
|
|
|
|
def mousePressEvent(self, ev):
|
2016-03-12 04:37:55 +08:00
|
|
|
if ev.buttons() ^ ev.button():
|
2016-02-26 03:34:04 +08:00
|
|
|
ev.ignore()
|
|
|
|
return
|
2016-03-16 22:13:31 +08:00
|
|
|
if self._hitHandle(ev.pos(), self._stop):
|
|
|
|
self._pressed = "stop"
|
|
|
|
elif self._hitHandle(ev.pos(), self._start):
|
|
|
|
self._pressed = "start"
|
2016-03-14 18:45:26 +08:00
|
|
|
else:
|
2016-03-16 22:59:49 +08:00
|
|
|
self._pressed = "axis"
|
|
|
|
self._offset = ev.x()
|
|
|
|
self._dragLeft = self._axisView[0]
|
2016-02-26 03:34:04 +08:00
|
|
|
|
|
|
|
def mouseMoveEvent(self, ev):
|
2016-03-16 22:13:31 +08:00
|
|
|
if not self._pressed:
|
2016-02-26 03:34:04 +08:00
|
|
|
ev.ignore()
|
|
|
|
return
|
2016-03-16 22:13:31 +08:00
|
|
|
if self._pressed == "stop":
|
2016-03-16 22:59:49 +08:00
|
|
|
self._stop = self._pixelToAxis(ev.x() - self._offset)
|
2016-03-16 22:13:31 +08:00
|
|
|
self.update()
|
2016-03-14 18:45:26 +08:00
|
|
|
if self.hasTracking():
|
2016-03-16 22:59:49 +08:00
|
|
|
self.stopChanged.emit(self._stop)
|
2016-03-16 22:13:31 +08:00
|
|
|
elif self._pressed == "start":
|
2016-03-16 22:59:49 +08:00
|
|
|
self._start = self._pixelToAxis(ev.x() - self._offset)
|
2016-03-16 22:13:31 +08:00
|
|
|
self.update()
|
2016-03-14 18:45:26 +08:00
|
|
|
if self.hasTracking():
|
2016-03-16 22:59:49 +08:00
|
|
|
self.startChanged.emit(self._start)
|
|
|
|
elif self._pressed == "axis":
|
|
|
|
self._setView(self._dragLeft + ev.x() - self._offset,
|
|
|
|
self._axisView[1])
|
2016-02-26 03:34:04 +08:00
|
|
|
|
|
|
|
def mouseReleaseEvent(self, ev):
|
|
|
|
QtWidgets.QSlider.mouseReleaseEvent(self, ev)
|
2016-03-16 22:13:31 +08:00
|
|
|
self.setSliderDown(False)
|
|
|
|
if self._pressed == "start":
|
|
|
|
self.startChanged.emit(self._start)
|
|
|
|
elif self._pressed == "stop":
|
|
|
|
self.stopChanged.emit(self._stop)
|
|
|
|
self._pressed = None
|
|
|
|
|
|
|
|
def _zoom(self, z, x):
|
|
|
|
a, b = self._axisView
|
|
|
|
scale = z*b
|
|
|
|
left = x - z*(x - a)
|
|
|
|
if z > 1 and abs(left - self.width()/2) > self.dynamicRange:
|
2016-03-14 18:45:26 +08:00
|
|
|
return
|
2016-03-16 22:13:31 +08:00
|
|
|
self._setView(left, scale)
|
2016-03-14 18:45:26 +08:00
|
|
|
|
2016-03-15 01:06:18 +08:00
|
|
|
def wheelEvent(self, ev):
|
2016-03-16 22:13:31 +08:00
|
|
|
y = ev.angleDelta().y()/120.
|
2016-03-14 18:45:26 +08:00
|
|
|
if ev.modifiers() & QtCore.Qt.ShiftModifier:
|
2016-03-16 22:13:31 +08:00
|
|
|
if y:
|
|
|
|
self.setNum(max(1, self._num + y))
|
2016-03-14 18:45:26 +08:00
|
|
|
elif ev.modifiers() & QtCore.Qt.ControlModifier:
|
|
|
|
if y:
|
2016-03-16 22:13:31 +08:00
|
|
|
self._zoom(self.zoomFactor**y, ev.x())
|
2016-03-14 18:45:26 +08:00
|
|
|
else:
|
|
|
|
ev.ignore()
|
2016-02-26 03:34:04 +08:00
|
|
|
|
2016-03-14 23:53:06 +08:00
|
|
|
def resizeEvent(self, ev):
|
2016-03-16 22:13:31 +08:00
|
|
|
if not ev.oldSize().isValid():
|
2016-03-14 18:45:26 +08:00
|
|
|
self.viewRange()
|
2016-03-16 22:13:31 +08:00
|
|
|
return
|
|
|
|
a, b = self._axisView
|
|
|
|
scale = b*ev.size().width()/ev.oldSize().width()
|
|
|
|
center = (self._stop + self._start)/2
|
|
|
|
if center:
|
|
|
|
scale = min(scale, self.dynamicRange/abs(center))
|
|
|
|
left = a*scale/b
|
|
|
|
self._setView(left, scale)
|
2016-03-14 23:53:06 +08:00
|
|
|
|
2016-03-16 22:13:31 +08:00
|
|
|
def paintEvent(self, ev):
|
|
|
|
self._paintSliders()
|
|
|
|
self._paintAxis()
|
|
|
|
|
|
|
|
def _paintAxis(self):
|
|
|
|
painter = QtGui.QPainter(self)
|
|
|
|
qfm = QtGui.QFontMetrics(painter.font())
|
|
|
|
avgCharWidth = qfm.averageCharWidth()
|
|
|
|
lineSpacing = qfm.lineSpacing()
|
|
|
|
descent = qfm.descent()
|
|
|
|
ascent = qfm.ascent()
|
|
|
|
height = qfm.height()
|
|
|
|
# painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
|
|
|
|
|
|
|
# TODO: make drawable area big enough and move axis higher
|
|
|
|
painter.translate(0, ascent - 15)
|
|
|
|
ticks, prefix, labels = self.ticker(self._pixelToAxis(0),
|
|
|
|
self._pixelToAxis(self.width()))
|
|
|
|
painter.drawText(0, 0, prefix)
|
|
|
|
|
|
|
|
pen = QtGui.QPen()
|
|
|
|
pen.setWidth(2)
|
|
|
|
painter.setPen(pen)
|
|
|
|
|
|
|
|
painter.translate(0, lineSpacing)
|
|
|
|
for t, l in zip(ticks, labels):
|
|
|
|
t = self._axisToPixel(t)
|
|
|
|
painter.drawLine(t, descent, t, height/2)
|
|
|
|
painter.drawText(t - len(l)/2*avgCharWidth, 0, l)
|
|
|
|
painter.drawLine(0, height/2, self.width(), height/2)
|
|
|
|
|
|
|
|
painter.translate(0, height)
|
|
|
|
for p in np.linspace(self._axisToPixel(self._start),
|
|
|
|
self._axisToPixel(self._stop),
|
|
|
|
self._num):
|
|
|
|
# TODO: is drawing far outside the viewport dangerous?
|
|
|
|
painter.drawLine(p, 0, p, -height/2)
|
|
|
|
|
|
|
|
def _paintSliders(self):
|
|
|
|
startPainter = QtWidgets.QStylePainter(self, self._startSlider)
|
|
|
|
stopPainter = QtWidgets.QStylePainter(self, self._stopSlider)
|
|
|
|
opt = self._getStyleOptionSlider(self._start)
|
|
|
|
if opt:
|
|
|
|
startPainter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
|
|
|
|
opt = self._getStyleOptionSlider(self._stop)
|
|
|
|
if opt:
|
|
|
|
stopPainter.drawComplexControl(QtWidgets.QStyle.CC_Slider, opt)
|