from qtpy import QtCore, QtWidgets, QtGui
import numpy as np
import time
import sys
if sys.version_info < (3,):
integer_types = (int, long)
else:
integer_types = (int,)
[docs]class NumberSpinBox(QtWidgets.QWidget):
"""
Base class for spinbox with numerical value.
The button can be either in log_increment mode, or linear increment.
- In log_increment: the halflife_seconds value determines how long it
takes when the user keeps clicking on the "*"/"/" buttons to change
the value by a factor 2. Since the underlying register is assumed to
be represented by an int, its values are separated by a minimal
separation, called "increment". The time to wait before refreshing
the value is adjusted automatically so that the log behavior is still
correct, even when the value becomes comparable to the increment.
- In linear increment, the value is immediately incremented by the
increment, then, nothing happens during a time given by
timer_initial_latency. Only after that the value is incremented by
"increment" every timer_min_interval.
"""
MOUSE_WHEEL_ACTIVATED = False
value_changed = QtCore.Signal()
selected = QtCore.Signal(list)
# timeouts for updating values when mouse button / key is pessed
change_interval = 0.02
_change_initial_latency = 0.1 # 100 ms before starting to update continuously.
@property
def change_initial_latency(self):
""" latency for continuous update when a button is pressed """
# if sigleStep is zero, there is no need to wait for continuous update
if self.singleStep != 0:
return self._change_initial_latency
else:
return 0
[docs] def forward_to_subspinboxes(func):
"""
a decorator that forwards function calls to subspinboxes
"""
# in base class, the trivial forwarder is chosen
def func_wrapper(self, *args, **kwargs):
return func(*args, **kwargs)
return func_wrapper
def __init__(self, label="", min=-1, max=1, increment=2.**(-13),
log_increment=False, halflife_seconds=0.5, per_second=0.2):
"""
:param label: label of the button
:param min: min value
:param max: max value
:param increment: increment of the underlying register
:param log_increment: boolean: when buttons up/down are pressed, should the value change linearly or log
:param halflife_seconds: when button is in log, how long to change the value by a factor 2.
:param per_second: when button is in lin, how long to change the value by 1 unit.
"""
super(NumberSpinBox, self).__init__(None)
self._val = 0 # internal storage for value with best-possible accuracy
self.labeltext = label
self.log_increment = log_increment
self.minimum = min # imitates original QSpinBox API
self.maximum = max # imitates original QSpinBox API
self.halflife_seconds = halflife_seconds
self.per_second = per_second
self.singleStep = increment
self.change_timer = QtCore.QTimer()
self.change_timer.setSingleShot(True)
self.change_timer.setInterval(int(np.ceil(self.change_interval*1000)))
self.change_timer.timeout.connect(self.continue_step)
self.make_layout()
self.update_tooltip()
self.set_min_size()
self.val = 0
[docs] def make_layout(self):
self.lay = QtWidgets.QHBoxLayout()
self.lay.setContentsMargins(0,0,0,0)
self.lay.setSpacing(0)
self.setLayout(self.lay)
if self.labeltext is not None:
self.label = QtWidgets.QLabel(self.labeltext)
self.lay.addWidget(self.label)
if self.log_increment:
self.up = QtWidgets.QPushButton('*')
self.down = QtWidgets.QPushButton('/')
else:
self.up = QtWidgets.QPushButton('+')
self.down = QtWidgets.QPushButton('-')
self.line = QtWidgets.QLineEdit()
self.line.setStyleSheet("QLineEdit { qproperty-cursorPosition: 0; }") # align text on the left
# http://stackoverflow.com/questions/18662157/qt-qlineedit-widget-to-get-long-text-left-aligned
self.lay.addWidget(self.down)
self.lay.addWidget(self.line)
self.lay.addWidget(self.up)
self.up.setMaximumWidth(15)
self.down.setMaximumWidth(15)
self.up.pressed.connect(self.first_step)
self.down.pressed.connect(self.first_step)
self.up.released.connect(self.finish_step)
self.down.released.connect(self.finish_step)
self.line.editingFinished.connect(self.validate)
self._button_up_down = False
self._button_down_down = False
# keyboard interface
[docs] def keyPressEvent(self, event):
if not event.isAutoRepeat():
if event.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Right]:
self._button_up_down = True
self._button_down_down = False # avoids going left & right
self.first_step()
elif event.key() in [QtCore.Qt.Key_Down, QtCore.Qt.Key_Left]:
self._button_down_down = True
self._button_up_down = False # avoids going left & right
self.first_step()
else:
return super(NumberSpinBox, self).keyPressEvent(event)
[docs] def keyReleaseEvent(self, event):
if not event.isAutoRepeat():
if event.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Right]:
self._button_up_down = False
self.finish_step()
elif event.key() in [QtCore.Qt.Key_Down, QtCore.Qt.Key_Left]:
self._button_down_down = False
self.finish_step()
else:
return super(NumberSpinBox, self).keyReleaseEvent(event)
@property
def is_increasing(self):
return self.up.isDown() or self._button_up_down
@property
def is_decreasing(self):
return self.down.isDown() or self._button_down_down
@property
def change_sign(self):
if self.is_increasing:
return 1.0
elif self.is_decreasing:
return -1.0
else:
return 0.0
[docs] def wheelEvent(self, event):
"""
Handle mouse wheel event. No distinction between linear and log.
:param event:
:return:
"""
if self.MOUSE_WHEEL_ACTIVATED:
nsteps = int(event.delta() / 120)
func = self.step_up if nsteps > 0 else self.step_down
for i in range(abs(nsteps)):
func(single_increment=True)
# def sizeHint(self): #doesn t do anything, probably need to change
# # sizePolicy
# return QtCore.QSize(200, 20)
[docs] def set_min_size(self):
"""
sets the min size for content to fit.
"""
font = QtGui.QFont("", 0)
font_metric = QtGui.QFontMetrics(font)
pixel_wide = font_metric.width("0"*self.max_num_letter)
self.line.setFixedWidth(pixel_wide)
@property
def max_num_letter(self):
"""
Returns the maximum number of letters
"""
return 5
[docs] def set_log_increment(self):
#self.up.setText("*")
#self.down.setText("/")
self.up.setText(u'\u2191') # up arrow unicode symbol
self.down.setText(u'\u2193') # down arrow unicode symbol
#self.up.setStyleSheet("font-weight: italic; font-size: 8pt")
#self.down.setStyleSheet("font-weight: bold; font-size: 8pt")
self.log_increment = True
[docs] def setDecimals(self, val):
self.decimals = val
self.set_min_size()
[docs] def validate(self):
""" make sure a new value is inside the allowed bounds after a
manual change of the value """
if self.line.isModified():
self.setValue(self.saturate(self.val))
self.value_changed.emit()
[docs] def saturate(self, val):
if val > self.maximum:
return self.maximum
elif val < self.minimum:
return self.minimum
else:
return val
[docs] def setMaximum(self, val): # imitates original QSpinBox API
self.maximum = val
self.update_tooltip()
[docs] def setMinimum(self, val): # imitates original QSpinBox API
self.minimum = val
self.update_tooltip()
[docs] def setSingleStep(self, val): # imitates original QSpinBox API
self.singleStep = val
[docs] def set_per_second(self, val):
self.per_second = val
[docs] def setValue(self, val): # imitates original QSpinBox API
""" replace this function with something useful in derived classes """
self.val = val
[docs] def value(self): # imitates original QSpinBox API
""" replace this function with something useful in derived classes """
return self.val
# code for managing value change with buttons or keyboard
[docs] def first_step(self):
"""
Once +/- pressed for timer_initial_latency ms, start to update continuously
"""
self.start_time = time.time()
self.start_value = self.value()
value = self.start_value + self.singleStep * self.change_sign
if np.sign(value)*np.sign(self.start_value) < 0:
# zero passage occured, make sure to stop at exactly 0
value = 0
self.setValue(self.saturate(value))
if self.log_increment and self.start_value == 0:
# avoid zero start_value when in log mode
self.start_value = self.value()
self.change_timer.start()
[docs] def continue_step(self):
dt = time.time() - self.start_time
if dt > self.change_initial_latency: # only do if pressed long enough
if self.log_increment:
# ensure proper behavior for zero
if self.start_value == 0:
return self.first_step() # start over when zero is crossed
sign = self.change_sign * np.sign(self.start_value)
halflifes = dt / self.halflife_seconds * sign
value = self.start_value * 2 ** halflifes
# change behavior when value is effectively zero
if abs(value) <= self.singleStep / 2.0 and sign < 0:
self.start_value = 0
value = 0
self.start_time = time.time() # ensures to stay 0 some time
else:
# delta for linear sweep
value = self.start_value + self.per_second * dt * self.change_sign
if np.sign(value) * np.sign(self.start_value) < 0:
# change of sign occured, make a stop at zero
self.start_value = 0
value = 0
self.start_time = time.time() # ensures to stay 0 some time
# don't do anything if the change is smaller than singleStep
if abs(self.val - value)>self.singleStep:
self.setValue(self.saturate(value))
self.change_timer.start()
[docs] def finish_step(self):
self.change_timer.stop()
if hasattr(self, 'start_time'):
dt = time.time() - self.start_time
else:
dt = 0
if dt > self.change_initial_latency:
self.validate() # make sure we validate if continue_step was on
[docs]class IntSpinBox(NumberSpinBox):
"""
Number spin box for integer values
"""
def __init__(self, label, min=-2**13, max=2**13, increment=1,
per_second=10, **kwargs):
super(IntSpinBox, self).__init__(label=label,
min=min,
max=max,
increment=increment,
per_second=per_second,
**kwargs)
@property
def val(self):
return int(str(self.line.text()))
@val.setter
def val(self, new_val):
self.line.setText(("%.i")%round(new_val))
self.value_changed.emit()
return new_val
@property
def max_num_letter(self):
"""
Maximum number of letters in line
"""
if np.isinf(self.maximum):
return super(IntSpinBox, self).max_num_letter
else:
return int(np.log10(np.abs(self.maximum))+1)
[docs] def setMaximum(self, val): # imitates original QSpinBox API
super(IntSpinBox, self).setMaximum(val)
self.set_min_size() # changes with maximum
[docs]class FloatSpinBox(NumberSpinBox):
"""
Number spin box for float values
"""
def __init__(self, label, decimals=4, min=-1, max=1,
increment=2.**(-13), **kwargs):
self.decimals = decimals
super(FloatSpinBox, self).__init__(label=label,
min=min,
max=max,
increment=increment,
**kwargs)
@property
def val(self):
if str(self.line.text())!=("%."+str(self.decimals) + "e")%self._val:
return float(str(self.line.text()))
return self._val # the value needs to be known to a precision better
# than the display to avoid deadlocks in increments
@val.setter
def val(self, new_val):
# We have a cached value _val that gives finer control over the
# value than what is displayed. In this way, clicking up/down can
# change the value without apparent changes of the display.
self._val = self.saturate(new_val)
# block signal otherwise validate will be called there, however,
# we want to use the cached value rather than the corase grained
# value read-out from the lineedit.
self.line.blockSignals(True)
self.line.setText(('{:.'+str(self.decimals)+'e}').format(
float(new_val)))
self.line.blockSignals(False)
# This will cause a write of the value to the redpitaya, and in turns
# another read from the fpga (this function will be called a second
# time here)
self.value_changed.emit()
return new_val
@property
def max_num_letter(self):
"""
Returns the maximum number of letters
"""
# example: -1.123e-23 has 7+decimals(3) letters
return self.decimals + 7
[docs]class ComplexSpinBox(FloatSpinBox):
"""
Two spinboxes representing a complex number, with the right keyboard
shortcuts (up down for imag, left/right for real).
"""
[docs] def forward_to_subspinboxes(func):
"""
a decorator that forwards function calls to subspinboxes
"""
# in base class, the trivial forwarder is chosen
def func_wrapper(self, *args, **kwargs):
return func(*args, **kwargs)
return func_wrapper
def __init__(self, *args, **kwargs):
super(ComplexSpinBox, self).__init__(*args, **kwargs)
[docs] def make_layout(self):
self.lay = QtWidgets.QHBoxLayout()
self.lay.setContentsMargins(0, 0, 0, 0)
self.real = FloatSpinBox(label=self.labeltext,
min=self.minimum,
max=self.maximum,
increment=self.singleStep,
log_increment=self.log_increment,
halflife_seconds=self.halflife_seconds,
decimals=self.decimals)
self.imag = FloatSpinBox(label=self.labeltext,
min=self.minimum,
max=self.maximum,
increment=self.singleStep,
log_increment=self.log_increment,
halflife_seconds=self.halflife_seconds,
decimals=self.decimals)
self.real.value_changed.connect(self.value_changed)
self.lay.addWidget(self.real)
self.label = QtWidgets.QLabel(" + j")
self.lay.addWidget(self.label)
self.imag.value_changed.connect(self.value_changed)
self.lay.addWidget(self.imag)
self.setLayout(self.lay)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
@property
def val(self):
return complex(self.real.val, self.imag.val)
@val.setter
def val(self, new_val):
self.real.val = np.real(new_val)
self.imag.val = np.imag(new_val)
return new_val
[docs] def keyPressEvent(self, event):
if event.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left]:
return self.imag.keyPressEvent(event)
else:
return self.real.keyPressEvent(event)
[docs] def keyReleaseEvent(self, event):
if event.key() in [QtCore.Qt.Key_Right, QtCore.Qt.Key_Left]:
return self.imag.keyReleaseEvent(event)
else:
return self.real.keyReleaseEvent(event)
[docs] def wheelEvent(self, event):
return self.imag.wheelEvent(event)
# forward calls to real and imaginary part
# for function in ['set_min_size', 'update_tooltip', 'setDecimals',
# 'set_per_second', 'setMaximum', 'setMinimum',
# 'setSingleStep', 'set_log_increment']:
[docs] def setFixedWidth(self, *args, **kwargs):
self.real.setFixedWidth(*args, **kwargs)
return self.imag.setFixedWidth(*args, **kwargs)
[docs] def set_min_size(self, *args, **kwargs):
self.real.set_min_size(*args, **kwargs)
return self.imag.set_min_size(*args, **kwargs)
[docs] def setDecimals(self, *args, **kwargs):
self.real.setDecimals(*args, **kwargs)
return self.imag.setDecimals(*args, **kwargs)
[docs] def set_per_second(self, *args, **kwargs):
self.real.set_per_second(*args, **kwargs)
return self.imag.set_per_second(*args, **kwargs)
[docs] def setMaximum(self, *args, **kwargs):
self.real.setMaximum(*args, **kwargs)
return self.imag.setMaximum(*args, **kwargs)
[docs] def setMinimum(self, *args, **kwargs):
self.real.setMinimum(*args, **kwargs)
return self.imag.setMinimum(*args, **kwargs)
[docs] def setSingleStep(self, *args, **kwargs):
self.real.setSingleStep(*args, **kwargs)
return self.imag.setSingleStep(*args, **kwargs)
[docs] def set_log_increment(self, *args, **kwargs):
self.real.set_log_increment(*args, **kwargs)
self.imag.set_log_increment(*args, **kwargs)
self.imag.up.setText(u'\u2192') # right arrow unicode symbol
self.imag.down.setText(u'\u2190') # left arrow unicode symbol