1
+ import logging
2
+
1
3
from PyQt5 import QtGui , QtCore , QtWidgets
2
4
from numpy import linspace
3
5
4
6
from .ticker import Ticker
5
7
6
8
9
+ logger = logging .getLogger (__name__ )
10
+
11
+
7
12
class ScanAxis (QtWidgets .QWidget ):
8
13
sigZoom = QtCore .pyqtSignal (float , int )
9
14
sigPoints = QtCore .pyqtSignal (int )
@@ -52,6 +57,10 @@ def wheelEvent(self, ev):
52
57
if y :
53
58
if ev .modifiers () & QtCore .Qt .ShiftModifier :
54
59
# If shift+scroll, modify number of points.
60
+ # TODO: This is not perfect. For high-resolution touchpads you
61
+ # get many small events with y < 120 which should accumulate.
62
+ # That would also match the wheel behavior of an integer
63
+ # spinbox.
55
64
z = int (y / 120. )
56
65
self .sigPoints .emit (z )
57
66
else :
@@ -319,14 +328,14 @@ class ScanProxy(QtCore.QObject):
319
328
sigStopMoved = QtCore .pyqtSignal (float )
320
329
sigNumPoints = QtCore .pyqtSignal (int )
321
330
322
- def __init__ (self , slider , axis , rangeFactor , dynamicRange ):
331
+ def __init__ (self , slider , axis , zoomMargin , dynamicRange ):
323
332
QtCore .QObject .__init__ (self )
324
333
self .axis = axis
325
334
self .slider = slider
326
335
self .realStart = 0
327
336
self .realStop = 0
328
337
self .numPoints = 10
329
- self .rangeFactor = rangeFactor
338
+ self .zoomMargin = zoomMargin
330
339
self .dynamicRange = dynamicRange
331
340
332
341
# Transform that maps the spinboxes to a pixel position on the
@@ -335,21 +344,21 @@ def __init__(self, slider, axis, rangeFactor, dynamicRange):
335
344
# Because the axis's width will change when placed within a layout,
336
345
# the realToPixelTransform will initially be invalid. It will be set
337
346
# properly during the first resizeEvent, with the below transform.
338
- self .realToPixelTransform = self .axis .width ()/ 2 , 1.
347
+ self .realToPixelTransform = - self .axis .width ()/ 2 , 1.
339
348
self .invalidOldSizeExpected = True
340
349
341
350
# pixel vals for sliders: 0 to slider_width - 1
342
351
def realToPixel (self , val ):
343
352
a , b = self .realToPixelTransform
344
- rawVal = b * (val + a )
353
+ rawVal = b * (val - a )
345
354
# Clamp pixel values to 32 bits, b/c Qt will otherwise wrap values.
346
355
rawVal = min (max (- (1 << 31 ), rawVal ), (1 << 31 ) - 1 )
347
356
return rawVal
348
357
349
358
# Get a point from pixel units to what the sliders display.
350
359
def pixelToReal (self , val ):
351
360
a , b = self .realToPixelTransform
352
- return val / b - a
361
+ return val / b + a
353
362
354
363
def rangeToReal (self , val ):
355
364
pixelVal = self .slider .rangeValueToPixelPos (val )
@@ -387,26 +396,22 @@ def setNumPoints(self, val):
387
396
def handleZoom (self , zoomFactor , mouseXPos ):
388
397
newScale = self .realToPixelTransform [1 ] * zoomFactor
389
398
refReal = self .pixelToReal (mouseXPos )
390
- newLeft = mouseXPos / newScale - refReal
391
- if abs (newLeft * newScale ) > self .dynamicRange :
399
+ newLeft = refReal - mouseXPos / newScale
400
+ newZero = newLeft * newScale + self .slider .effectiveWidth ()/ 2
401
+ if zoomFactor > 1 and abs (newZero ) > self .dynamicRange :
392
402
return
393
403
self .realToPixelTransform = newLeft , newScale
394
404
self .moveStop (self .realStop )
395
405
self .moveStart (self .realStart )
396
406
397
- def zoomToFit (self ):
398
- currRangeReal = abs (self .realStop - self .realStart )
399
- # Slider closest to the left should be used to find the new axis left.
400
- if self .realStop < self .realStart :
401
- refSlider = self .realStop
402
- else :
403
- refSlider = self .realStart
404
- if self .rangeFactor <= 2 or currRangeReal == 0 :
405
- return # Ill-formed snap range- do nothing
406
- proportion = self .rangeFactor / (self .rangeFactor - 2 )
407
- newScale = self .slider .effectiveWidth ()/ (proportion * currRangeReal )
408
- newLeft = (self .slider .effectiveWidth ()/ (self .rangeFactor * newScale ) -
409
- refSlider )
407
+ def viewRange (self ):
408
+ newScale = self .slider .effectiveWidth ()/ abs (
409
+ self .realStop - self .realStart )
410
+ newScale *= 1 - 2 * self .zoomMargin
411
+ newCenter = (self .realStop + self .realStart )/ 2
412
+ if newCenter :
413
+ newScale = min (newScale , self .dynamicRange / abs (newCenter ))
414
+ newLeft = newCenter - self .slider .effectiveWidth ()/ 2 / newScale
410
415
self .realToPixelTransform = newLeft , newScale
411
416
self .moveStop (self .realStop )
412
417
self .moveStart (self .realStart )
@@ -419,9 +424,9 @@ def zoomToFit(self):
419
424
# This function handles handles the slider positions. Slider and axis
420
425
# handle its own width changes; proxy watches for axis width resizeEvent to
421
426
# alter mapping from real to pixel space.
422
- def zoomToFitInit (self ):
427
+ def viewRangeInit (self ):
423
428
currRangeReal = abs (self .realStop - self .realStart )
424
- if self . rangeFactor <= 2 or currRangeReal == 0 :
429
+ if currRangeReal == 0 :
425
430
self .moveStop (self .realStop )
426
431
self .moveStart (self .realStart )
427
432
# Ill-formed snap range- move the sliders anyway,
@@ -430,15 +435,15 @@ def zoomToFitInit(self):
430
435
# This will force the sliders to have positions on the axis
431
436
# which reflect the start/stop values currently set.
432
437
else :
433
- self .zoomToFit ()
438
+ self .viewRange ()
434
439
# Notify spinboxes manually, since slider wasn't clicked and will
435
440
# therefore not emit signals.
436
441
self .sigStopMoved .emit (self .realStop )
437
442
self .sigStartMoved .emit (self .realStart )
438
443
439
- def fitToView (self ):
440
- lowRange = 1.0 / self .rangeFactor
441
- highRange = ( self . rangeFactor - 1 ) / self .rangeFactor
444
+ def snapRange (self ):
445
+ lowRange = self .zoomMargin
446
+ highRange = 1 - self .zoomMargin
442
447
newStart = self .pixelToReal (lowRange * self .slider .effectiveWidth ())
443
448
newStop = self .pixelToReal (highRange * self .slider .effectiveWidth ())
444
449
sliderRange = self .slider .maximum () - self .slider .minimum ()
@@ -463,21 +468,21 @@ def eventFilter(self, obj, ev):
463
468
newWidth = ev .size ().width () - self .slider .handleWidth ()
464
469
# assert refRight > oldLeft
465
470
newScale = newWidth / (refRight - oldLeft )
466
- self .realToPixelTransform = - oldLeft , newScale
471
+ self .realToPixelTransform = oldLeft , newScale
467
472
else :
468
473
# TODO: self.axis.width() is invalid during object
469
474
# construction. The width will change when placed in a
470
475
# layout WITHOUT a resizeEvent. Why?
471
476
oldLeft = - ev .size ().width ()/ 2
472
477
newScale = 1.0
473
- self .realToPixelTransform = - oldLeft , newScale
478
+ self .realToPixelTransform = oldLeft , newScale
474
479
# We need to reinitialize the pixel transform b/c the old width
475
480
# of the axis is no longer valid. When we have a valid transform,
476
- # we can then zoomToFit based on the desired real values.
481
+ # we can then viewRange based on the desired real values.
477
482
# The slider handle values are invalid before this point as well;
478
483
# we set them to the correct value here, regardless of whether
479
484
# the slider has already resized itsef or not.
480
- self .zoomToFitInit ()
485
+ self .viewRangeInit ()
481
486
self .invalidOldSizeExpected = False
482
487
# assert self.pixelToReal(0) == oldLeft, \
483
488
# "{}, {}".format(self.pixelToReal(0), oldLeft)
@@ -493,13 +498,12 @@ class ScanWidget(QtWidgets.QWidget):
493
498
sigStopMoved = QtCore .pyqtSignal (float )
494
499
sigNumChanged = QtCore .pyqtSignal (int )
495
500
496
- def __init__ (self , zoomFactor = 1.05 , rangeFactor = 6 , dynamicRange = 1e8 ):
501
+ def __init__ (self , zoomFactor = 1.05 , zoomMargin = .1 , dynamicRange = 1e8 ):
497
502
QtWidgets .QWidget .__init__ (self )
498
503
self .slider = slider = ScanSlider ()
499
504
self .axis = axis = ScanAxis (zoomFactor )
500
- self .proxy = ScanProxy (slider , axis , rangeFactor , dynamicRange )
505
+ self .proxy = ScanProxy (slider , axis , zoomMargin , dynamicRange )
501
506
axis .proxy = self .proxy
502
- axis .slider = slider
503
507
slider .setMaximum (1023 )
504
508
505
509
# Layout.
@@ -524,10 +528,10 @@ def __init__(self, zoomFactor=1.05, rangeFactor=6, dynamicRange=1e8):
524
528
slider .installEventFilter (axis )
525
529
526
530
# Context menu entries
527
- self .zoomToFitAct = QtWidgets .QAction ("&View Range" , self )
528
- self .fitToViewAct = QtWidgets .QAction ("&Snap Range" , self )
529
- self .zoomToFitAct .triggered .connect (self .zoomToFit )
530
- self .fitToViewAct .triggered .connect (self .fitToView )
531
+ self .viewRangeAct = QtWidgets .QAction ("&View Range" , self )
532
+ self .snapRangeAct = QtWidgets .QAction ("&Snap Range" , self )
533
+ self .viewRangeAct .triggered .connect (self .viewRange )
534
+ self .snapRangeAct .triggered .connect (self .snapRange )
531
535
532
536
# Spinbox and button slots. Any time the spinboxes change, ScanWidget
533
537
# mirrors it and passes the information to the proxy.
@@ -540,14 +544,14 @@ def setStart(self, val):
540
544
def setNumPoints (self , val ):
541
545
self .proxy .setNumPoints (val )
542
546
543
- def zoomToFit (self ):
544
- self .proxy .zoomToFit ()
547
+ def viewRange (self ):
548
+ self .proxy .viewRange ()
545
549
546
- def fitToView (self ):
547
- self .proxy .fitToView ()
550
+ def snapRange (self ):
551
+ self .proxy .snapRange ()
548
552
549
553
def contextMenuEvent (self , ev ):
550
554
menu = QtWidgets .QMenu (self )
551
- menu .addAction (self .zoomToFitAct )
552
- menu .addAction (self .fitToViewAct )
555
+ menu .addAction (self .viewRangeAct )
556
+ menu .addAction (self .snapRangeAct )
553
557
menu .exec (ev .globalPos ())
0 commit comments