Last active
January 10, 2016 05:55
-
-
Save cr1901/a0b9510da0804653a3dc to your computer and use it in GitHub Desktop.
Debug Rebase for Scanwidget
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import asyncio | |
import atexit | |
import os | |
import scanwidget | |
# First two are portable between PyQt4 and 5. Remaining are not. | |
from quamash import QApplication, QEventLoop, QtGui, QtCore, QtWidgets | |
class MainWindow(QtWidgets.QMainWindow): | |
def __init__(self, app, server): | |
QtWidgets.QMainWindow.__init__(self) | |
self.exit_request = asyncio.Event() | |
def closeEvent(self, *args): | |
self.exit_request.set() | |
def save_state(self): | |
return bytes(self.saveGeometry()) | |
def restore_state(self, state): | |
self.restoreGeometry(QtCore.QByteArray(state)) | |
def main(): | |
app = QApplication([]) | |
loop = QEventLoop(app) | |
asyncio.set_event_loop(loop) | |
atexit.register(loop.close) | |
# Create a window | |
win = MainWindow(app, None) | |
container = QtWidgets.QWidget(win) | |
layout = QtWidgets.QGridLayout() | |
container.setLayout(layout) | |
spinboxes = [QtWidgets.QDoubleSpinBox(), QtWidgets.QDoubleSpinBox(), \ | |
QtWidgets.QSpinBox()] | |
scanner = scanwidget.ScanWidget() | |
layout.addWidget(scanner, 0, 0, 1, -1) | |
for s in spinboxes: | |
if type(s) is QtWidgets.QDoubleSpinBox: | |
s.setDecimals(17) | |
s.setMaximum(float("Inf")) | |
s.setMinimum(float("-Inf")) | |
else: | |
s.setMinimum(2) | |
s.setValue(10) | |
for (col, w) in enumerate([QtWidgets.QLabel("Min"), spinboxes[0], \ | |
QtWidgets.QLabel("Max"), spinboxes[1], \ | |
QtWidgets.QLabel("Num Points"), spinboxes[2]]): | |
layout.addWidget(w, 1, col) | |
scanner.sigMinChanged.connect(spinboxes[0].setValue) | |
scanner.sigMaxChanged.connect(spinboxes[1].setValue) | |
scanner.sigNumChanged.connect(spinboxes[2].setValue) | |
spinboxes[0].valueChanged.connect(scanner.setMin) | |
spinboxes[1].valueChanged.connect(scanner.setMax) | |
spinboxes[2].valueChanged.connect(scanner.setNumPoints) | |
win.setCentralWidget(container) | |
win.show() | |
# BUG: MouseMoveEvents are not honored at this scale. | |
# scanner.setMax(1.0e15) | |
# scanner.setMin(-1.0e15) | |
# BUG: Read as zero. | |
# scanner.setMax(1.0e-12) | |
# scanner.setMin(-1.0e-12) | |
loop.run_until_complete(win.exit_request.wait()) | |
if __name__ == "__main__": | |
main() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import math | |
from PyQt5 import QtGui, QtCore, QtWidgets | |
from fractions import Fraction | |
# ScanAxis consists of a horizontal line extending indefinitely, major and | |
# minor ticks, and numbers over the major ticks. During a redraw, the width | |
# between adjacent ticks is recalculated, based on the new requested bounds. | |
# | |
# If the width is smaller than a certain threshold, the major ticks become | |
# minor ticks, and the minor ticks are deleted as objects. | |
# Because ScanAxis needs knowledge of units, it keeps a reference to the | |
# ScanSceneProxy. | |
class ScanAxis(QtWidgets.QGraphicsWidget): | |
def __init__(self, proxy): | |
QtWidgets.QGraphicsWidget.__init__(self) | |
self.proxy = proxy | |
def paint(self, painter, op, widget): | |
sceneRect = self.scene().sceneRect() | |
dispMin = sceneRect.left() | |
dispMax = sceneRect.right() | |
# dispRange = dispMax - dispMin | |
painter.drawLine(dispMin, 1, dispMax, 1) | |
realMin = self.proxy.sceneToReal(dispMin) | |
realMax = self.proxy.sceneToReal(dispMax) | |
realRange = realMax - realMin | |
majorTickInc = self.nearestPow10(realRange) | |
numMajorTicks = realRange/majorTickInc | |
firstMajorTick = self.nearestPow10(realMin) | |
lastMajorTick = self.nearestPow10(realMax) | |
for x in range(firstMajorTick, lastMajorTick, majorTickInc): | |
painter.drawLine(self.proxy.realToScene(x), 5, self.proxy.realToScene(x), -5) | |
# Major ticks | |
# for i in range( | |
# | |
# # Minor ticks | |
# for i in range(dispMin, dispMax, | |
def nearestPow10(self, val): | |
if val > 0: | |
return 10**round(math.log10(val)) | |
elif val < 0: | |
return -10**round(math.log10(abs(val))) | |
else: | |
return 0 | |
class DataPoint(QtWidgets.QGraphicsEllipseItem): | |
def __init__(self, pxSize = 2, color = QtGui.QColor(128,128,128,128)): | |
QtWidgets.QGraphicsEllipseItem.__init__(self, 0, 0, pxSize, pxSize) | |
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations, True) | |
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True) | |
self.setBrush(color) | |
self.setPen(color) | |
# ScanScene holds all the objects, and controls how events are received to | |
# the ScanSliders (in particular, constraining movement to the x-axis). | |
# TODO: SceneRect only ever has to be as large in the Y-direction as the sliders. | |
class ScanScene(QtWidgets.QGraphicsScene): | |
def __init__(self): | |
QtWidgets.QGraphicsScene.__init__(self) | |
def mouseMoveEvent(self, ev): | |
print("Pos: ", ev.scenePos()) | |
QtWidgets.QGraphicsScene.mouseMoveEvent(self, ev) | |
# def mouseMoveEvent(self, ev): | |
# | |
# def mouseClickEvent(self, ev): | |
# pass | |
# | |
# def mouseDragEvent(self, ev): | |
# pass | |
# | |
# def keyPressEvent(self, ev): | |
# Needs to be subclassed from QGraphicsItem* b/c Qt will not send mouse events | |
# to GraphicsItems that do not reimplement mousePressEvent (How does Qt know?). | |
# Qt decides this while iterating to find which GraphicsItem should get control | |
# of the mouse. mousePressEvent is accepted by default. | |
# TODO: The slider that was most recently clicked should have z-priority. | |
# Qt's mouseGrab logic should implement this correctly. | |
# ScanSlider assumes that it's parent is the scene. | |
# ScanSlider does not know about other ScanSlider instances; ScanSlider knows | |
# about it's position in the scene/how close it is to the edges. | |
# ScanScene must handle object collisions. | |
# * Subclassed from QGraphicsObject to get signal functionality. | |
class ScanSlider(QtWidgets.QGraphicsObject): | |
sigPosChanged = QtCore.pyqtSignal(float) | |
sigBoundsUpdate = QtCore.pyqtSignal() | |
def __init__(self, pxSize = 20, color = QtGui.QColor(128,128,128,128), bounds = 1/6): | |
QtWidgets.QGraphicsItem.__init__(self) | |
self.xChanged.connect(self.emitSigPosChanged) | |
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True) | |
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations, True) | |
self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges, True) | |
self.pxSize = pxSize | |
self.color = color | |
self.bounds = bounds # How close to the edge of the viewed scene | |
# before we start sending boundsUpdate signal to update the "real" | |
# values? | |
# Make slider an equilateral triangle w/ sides pxSize pixels wide. | |
altitude = math.ceil((pxSize/2)*math.sqrt(3)) | |
points = [QtCore.QPoint(-(self.pxSize/2), altitude), \ | |
QtCore.QPoint(0, 0), QtCore.QPoint((self.pxSize/2), altitude)] | |
self.shape = QtGui.QPolygon(points) | |
def boundingRect(self): | |
penWidth = 1 # Not user-settable. | |
# If bounding box does not cover whole polygon, trails will be left | |
# when the object is moved. | |
return QtCore.QRectF(-self.pxSize/2 - penWidth/2, 0 - penWidth/2, \ | |
self.pxSize + penWidth, self.pxSize + penWidth) | |
def paint(self, painter, op, widget): | |
painter.setBrush(self.color) | |
painter.setPen(self.color) | |
painter.drawConvexPolygon(self.shape) | |
def emitSigPosChanged(self): | |
self.sigPosChanged.emit(self.scenePos().x()) | |
# Constrain movement to X axis and ensure that the sliders (bounding box?) | |
# do not leave the scene. | |
# TODO: Resize event for the scene should maintain current slider | |
# positions or rescale the X-axis so the distance between | |
# sliders and the edge of the scene remains proportional? | |
def itemChange(self, change, val): | |
if change == QtWidgets.QGraphicsItem.ItemPositionChange: | |
newPos = val | |
newPos.setY(0) # Constrain movement to X-axis of parent. | |
rect = self.scene().sceneRect() #sceneRect will always match displayed. | |
boundsRect = QtCore.QRectF(rect) # Create a copy. | |
boundsLeft = self.bounds * rect.width() | |
boundsRect.translate(boundsLeft, 0) | |
boundsWidth = ((1 - 2 * self.bounds) * rect.width()) | |
assert(boundsWidth > 0) # Something really went wrong if this fails. | |
boundsRect.setWidth(boundsWidth) | |
print("sceneRect: ", rect) | |
print("boundsRect: ", boundsRect) | |
print("newPos: ", newPos) | |
if not boundsRect.contains(newPos): | |
self.sigBoundsUpdate.emit() | |
newPos.setX(self.pos().x()) # Keep sliders in scene at least | |
# self.bounds fraction from edge. And send a boundsUpdate. | |
return newPos | |
return QtWidgets.QGraphicsItem.itemChange(self, change, val) | |
# TODO: On a resize, should the spinbox's default increment change? | |
class ScanView(QtWidgets.QGraphicsView): | |
def __init__(self, zoomInc = 1.2): | |
self.zoomInc = zoomInc | |
self.scene = ScanScene() | |
QtWidgets.QGraphicsView.__init__(self, self.scene) | |
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) | |
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) | |
# self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter ) | |
# Debug Item to visualize origin. | |
self.scene.addItem(DataPoint(color = QtGui.QColor(0,0,0))) | |
def zoomOut(self): | |
pass | |
# self.scale(1/self.zoomInc, 1) | |
# print(self.transform().m11()) | |
def zoomIn(self): | |
pass | |
# self.scale(self.zoomInc, 1) | |
# print(self.transform().m11()) | |
def wheelEvent(self, ev): | |
# if ev.delta() > 0: # TODO: Qt-4 specific. | |
# TODO: If sliders are off screen after a zoom-in, what should we do? | |
# TODO: If shift modifier is pressed and scroll-wheel is grazed, should | |
# we honor zoom requests? | |
if ev.angleDelta().y() > 0: | |
self.zoomIn() | |
else: | |
self.zoomOut() | |
# Items in scene grab mouse in this function. If shift is pressed, skip | |
# deciding which slider to grab and the view itself will get mouse Events. | |
# This enables adding/deleting points. | |
def mousePressEvent(self, ev): | |
if QtWidgets.QApplication.keyboardModifiers() & QtCore.Qt.ShiftModifier: | |
pass | |
else: | |
QtWidgets.QGraphicsView.mousePressEvent(self, ev) | |
def mouseMoveEvent(self, ev): | |
if QtWidgets.QApplication.keyboardModifiers() & QtCore.Qt.ShiftModifier: | |
pass | |
else: | |
QtWidgets.QGraphicsView.mouseMoveEvent(self, ev) | |
def resizeEvent(self, ev): | |
QtWidgets.QGraphicsView.resizeEvent(self, ev) | |
self.centerOn(0, 0) | |
viewportRect = QtCore.QRect(0, 0, self.viewport().width(), \ | |
self.viewport().height()) | |
sceneRectFromViewport = self.mapToScene(viewportRect).boundingRect() | |
# View and Scene SceneRects are coupled until one or the other is | |
# manually set. We want them coupled, but without the default rules | |
# that Scene uses to set its SceneRect size. | |
self.setSceneRect(sceneRectFromViewport) | |
self.scene.setSceneRect(sceneRectFromViewport) | |
# A resize will automatically fitToView to avoid having sliders go | |
# out of bounds. | |
# self.update() | |
# ScanSceneProxy communicates information between the what numbers are displayed | |
# to the user and how they map to the sliders/data stored in the scene. | |
# Synchronization is maintained by limiting the access to members to functions | |
# either called by signals, or recalculated on request. | |
class ScanSceneProxy(QtCore.QObject): | |
def __init__(self, scene): | |
self.scene = scene | |
self.units = Fraction(1, 1) # Amount slider moved from user's POV per | |
# increment by one unit in the scene. | |
self.bias = 0 # Number of units from scene's origin in +/- x-direction | |
# Real value of sliders. | |
self.min = 0 | |
self.max = 0 | |
self.numPoints = 10 | |
def realToScene(self, val): | |
return float(Fraction(1, self.units) * Fraction.from_float(val)) + self.bias | |
def sceneToReal(self, val): | |
return float(Fraction.from_float(val) * self.units) + self.bias | |
def moveMax(self, val): | |
pass | |
# def recalculateBias(self, newMin, newMax): | |
# | |
# Monitor all events sent to QGraphicsScene and update internal state | |
# accordingly. | |
# def eventFilter(self, obj, ev): | |
# The ScanWidget proper. | |
class ScanWidget(QtWidgets.QWidget): | |
sigMinChanged = QtCore.pyqtSignal(float) | |
sigMaxChanged = QtCore.pyqtSignal(float) | |
sigNumChanged = QtCore.pyqtSignal(int) | |
def __init__(self, zoomInc = 1.2): | |
QtWidgets.QWidget.__init__(self) | |
self.view = ScanView(zoomInc) | |
self.proxy = ScanSceneProxy(self.view.scene) | |
self.zoomFitButton = QtWidgets.QPushButton("Zoom to Fit") | |
self.fitViewButton = QtWidgets.QPushButton("Fit to View") | |
layout = QtWidgets.QGridLayout() | |
layout.addWidget(self.view, 0, 0, 1, -1) | |
layout.addWidget(self.zoomFitButton, 1, 0) | |
layout.addWidget(self.fitViewButton, 1, 1) | |
self.setLayout(layout) | |
axis = ScanAxis(self.proxy) | |
minSlider = ScanSlider(color = QtGui.QColor(0,0,255,128)) | |
maxSlider = ScanSlider(color = QtGui.QColor(255,0,0,128)) | |
self.view.scene.addItem(axis) | |
self.view.scene.addItem(minSlider) | |
self.view.scene.addItem(maxSlider) | |
minSlider.sigPosChanged.connect(self.sigMinChanged) | |
maxSlider.sigPosChanged.connect(self.sigMaxChanged) | |
self.zoomFitButton.clicked.connect(self.zoomToFit) | |
self.fitViewButton.clicked.connect(self.fitToView) | |
# Attach these to the sliders and pushbutton signals respectively. | |
def setMax(self, val): | |
pass | |
# emitting sigPosChanged might be moved to setPos. This will prevent | |
# infinite recursion in that case. | |
# if val != self.max: # WARNING: COMPARING FLOATS! | |
# pass | |
# self.maxSlider.setPos(QtCore.QPointF(val, 0)) | |
# self.update() # Might be needed, but paint seems to occur correctly. | |
def setMin(self, val): | |
pass | |
# if val != self.min: # WARNING: COMPARING FLOATS! | |
# pass | |
# self.minSlider.setPos(QtCore.QPointF(val, 0)) | |
def setNumPoints(self, val): | |
pass | |
def zoomToFit(self): | |
pass | |
def fitToView(self): | |
pass |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment