Merge pull request #410 from campagnola/spinbox-formatting
Spinbox formatting
This commit is contained in:
commit
504c35202a
@ -13,7 +13,7 @@ import initExample ## Add path to library (just for examples; you do not need th
|
||||
import pyqtgraph as pg
|
||||
from pyqtgraph.Qt import QtCore, QtGui
|
||||
import numpy as np
|
||||
|
||||
import ast
|
||||
|
||||
app = QtGui.QApplication([])
|
||||
|
||||
@ -31,6 +31,15 @@ spins = [
|
||||
pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=0.5, minStep=0.01)),
|
||||
("Float with SI-prefixed units,<br>dec step=1.0, minStep=0.001",
|
||||
pg.SpinBox(value=1.0, suffix='V', siPrefix=True, dec=True, step=1.0, minStep=0.001)),
|
||||
("Float with custom formatting",
|
||||
pg.SpinBox(value=23.07, format='${value:0.02f}',
|
||||
regex='\$?(?P<number>(-?\d+(\.\d+)?)|(-?\.\d+))$')),
|
||||
("Int with custom formatting",
|
||||
pg.SpinBox(value=4567, step=1, int=True, bounds=[0,None], format='0x{value:X}',
|
||||
regex='(0x)?(?P<number>[0-9a-fA-F]+)$',
|
||||
evalFunc=lambda s: ast.literal_eval('0x'+s))),
|
||||
("Integer with bounds=[10, 20] and wrapping",
|
||||
pg.SpinBox(value=10, bounds=[10, 20], int=False, minStep=1, step=1, wrapping=True)),
|
||||
]
|
||||
|
||||
|
||||
|
@ -34,10 +34,13 @@ Colors = {
|
||||
|
||||
SI_PREFIXES = asUnicode('yzafpnµm kMGTPEZY')
|
||||
SI_PREFIXES_ASCII = 'yzafpnum kMGTPEZY'
|
||||
SI_PREFIX_EXPONENTS = dict([(SI_PREFIXES[i], (i-8)*3) for i in range(len(SI_PREFIXES))])
|
||||
SI_PREFIX_EXPONENTS['u'] = -6
|
||||
|
||||
FLOAT_REGEX = re.compile(r'(?P<number>[+-]?((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)\s*((?P<siPrefix>[u' + SI_PREFIXES + r']?)(?P<suffix>\w.*))?$')
|
||||
INT_REGEX = re.compile(r'(?P<number>[+-]?\d+)\s*(?P<siPrefix>[u' + SI_PREFIXES + r']?)(?P<suffix>.*)$')
|
||||
|
||||
|
||||
|
||||
|
||||
def siScale(x, minVal=1e-25, allowUnicode=True):
|
||||
"""
|
||||
Return the recommended scale factor and SI prefix string for x.
|
||||
@ -76,6 +79,7 @@ def siScale(x, minVal=1e-25, allowUnicode=True):
|
||||
|
||||
return (p, pref)
|
||||
|
||||
|
||||
def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, allowUnicode=True):
|
||||
"""
|
||||
Return the number x formatted in engineering notation with SI prefix.
|
||||
@ -104,31 +108,56 @@ def siFormat(x, precision=3, suffix='', space=True, error=None, minVal=1e-25, al
|
||||
plusminus = " +/- "
|
||||
fmt = "%." + str(precision) + "g%s%s%s%s"
|
||||
return fmt % (x*p, pref, suffix, plusminus, siFormat(error, precision=precision, suffix=suffix, space=space, minVal=minVal))
|
||||
|
||||
|
||||
def siParse(s, regex=FLOAT_REGEX):
|
||||
"""Convert a value written in SI notation to a tuple (number, si_prefix, suffix).
|
||||
|
||||
def siEval(s):
|
||||
Example::
|
||||
|
||||
siParse('100 μV") # returns ('100', 'μ', 'V')
|
||||
"""
|
||||
Convert a value written in SI notation to its equivalent prefixless value
|
||||
s = asUnicode(s)
|
||||
m = regex.match(s)
|
||||
if m is None:
|
||||
raise ValueError('Cannot parse number "%s"' % s)
|
||||
try:
|
||||
sip = m.group('siPrefix')
|
||||
except IndexError:
|
||||
sip = ''
|
||||
|
||||
try:
|
||||
suf = m.group('suffix')
|
||||
except IndexError:
|
||||
suf = ''
|
||||
|
||||
return m.group('number'), '' if sip is None else sip, '' if suf is None else suf
|
||||
|
||||
|
||||
def siEval(s, typ=float, regex=FLOAT_REGEX):
|
||||
"""
|
||||
Convert a value written in SI notation to its equivalent prefixless value.
|
||||
|
||||
Example::
|
||||
|
||||
siEval("100 μV") # returns 0.0001
|
||||
"""
|
||||
val, siprefix, suffix = siParse(s, regex)
|
||||
v = typ(val)
|
||||
return siApply(val, siprefix)
|
||||
|
||||
|
||||
s = asUnicode(s)
|
||||
m = re.match(r'(-?((\d+(\.\d*)?)|(\.\d+))([eE]-?\d+)?)\s*([u' + SI_PREFIXES + r']?).*$', s)
|
||||
if m is None:
|
||||
raise Exception("Can't convert string '%s' to number." % s)
|
||||
v = float(m.groups()[0])
|
||||
p = m.groups()[6]
|
||||
#if p not in SI_PREFIXES:
|
||||
#raise Exception("Can't convert string '%s' to number--unknown prefix." % s)
|
||||
if p == '':
|
||||
n = 0
|
||||
elif p == 'u':
|
||||
n = -2
|
||||
def siApply(val, siprefix):
|
||||
"""
|
||||
"""
|
||||
n = SI_PREFIX_EXPONENTS[siprefix] if siprefix != '' else 0
|
||||
if n > 0:
|
||||
return val * 10**n
|
||||
elif n < 0:
|
||||
# this case makes it possible to use Decimal objects here
|
||||
return val / 10**-n
|
||||
else:
|
||||
n = SI_PREFIXES.index(p) - 8
|
||||
return v * 1000**n
|
||||
return val
|
||||
|
||||
|
||||
class Color(QtGui.QColor):
|
||||
|
@ -108,7 +108,7 @@ class WidgetParameterItem(ParameterItem):
|
||||
if k in opts:
|
||||
defs[k] = opts[k]
|
||||
if 'limits' in opts:
|
||||
defs['bounds'] = opts['limits']
|
||||
defs['min'], defs['max'] = opts['limits']
|
||||
w = SpinBox()
|
||||
w.setOpts(**defs)
|
||||
w.sigChanged = w.sigValueChanged
|
||||
@ -122,6 +122,7 @@ class WidgetParameterItem(ParameterItem):
|
||||
self.hideWidget = False
|
||||
elif t == 'str':
|
||||
w = QtGui.QLineEdit()
|
||||
w.setStyleSheet('border: 0px')
|
||||
w.sigChanged = w.editingFinished
|
||||
w.value = lambda: asUnicode(w.text())
|
||||
w.setValue = lambda v: w.setText(asUnicode(v))
|
||||
@ -279,9 +280,14 @@ class WidgetParameterItem(ParameterItem):
|
||||
|
||||
## If widget is a SpinBox, pass options straight through
|
||||
if isinstance(self.widget, SpinBox):
|
||||
# send only options supported by spinbox
|
||||
sbOpts = {}
|
||||
if 'units' in opts and 'suffix' not in opts:
|
||||
opts['suffix'] = opts['units']
|
||||
self.widget.setOpts(**opts)
|
||||
sbOpts['suffix'] = opts['units']
|
||||
for k,v in opts.items():
|
||||
if k in self.widget.opts:
|
||||
sbOpts[k] = v
|
||||
self.widget.setOpts(**sbOpts)
|
||||
self.updateDisplayLabel()
|
||||
|
||||
|
||||
|
@ -1,25 +1,31 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ..Qt import QtGui, QtCore
|
||||
from ..python2_3 import asUnicode
|
||||
from ..SignalProxy import SignalProxy
|
||||
|
||||
from .. import functions as fn
|
||||
from math import log
|
||||
from decimal import Decimal as D ## Use decimal to avoid accumulating floating-point errors
|
||||
from decimal import *
|
||||
import decimal
|
||||
import weakref
|
||||
import re
|
||||
|
||||
from ..Qt import QtGui, QtCore
|
||||
from ..python2_3 import asUnicode, basestring
|
||||
from ..SignalProxy import SignalProxy
|
||||
from .. import functions as fn
|
||||
|
||||
|
||||
__all__ = ['SpinBox']
|
||||
|
||||
|
||||
class SpinBox(QtGui.QAbstractSpinBox):
|
||||
"""
|
||||
**Bases:** QtGui.QAbstractSpinBox
|
||||
|
||||
QSpinBox widget on steroids. Allows selection of numerical value, with extra features:
|
||||
Extension of QSpinBox widget for selection of a numerical value.
|
||||
Adds many extra features:
|
||||
|
||||
- SI prefix notation (eg, automatically display "300 mV" instead of "0.003 V")
|
||||
- Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.)
|
||||
- Option for unbounded values
|
||||
- Delayed signals (allows multiple rapid changes with only one change signal)
|
||||
* SI prefix notation (eg, automatically display "300 mV" instead of "0.003 V")
|
||||
* Float values with linear and decimal stepping (1-9, 10-90, 100-900, etc.)
|
||||
* Option for unbounded values
|
||||
* Delayed signals (allows multiple rapid changes with only one change signal)
|
||||
* Customizable text formatting
|
||||
|
||||
============================= ==============================================
|
||||
**Signals:**
|
||||
@ -42,67 +48,39 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
valueChanged = QtCore.Signal(object) # (value) for compatibility with QSpinBox
|
||||
sigValueChanged = QtCore.Signal(object) # (self)
|
||||
sigValueChanging = QtCore.Signal(object, object) # (self, value) sent immediately; no delay.
|
||||
|
||||
|
||||
def __init__(self, parent=None, value=0.0, **kwargs):
|
||||
"""
|
||||
============== ========================================================================
|
||||
**Arguments:**
|
||||
parent Sets the parent widget for this SpinBox (optional). Default is None.
|
||||
value (float/int) initial value. Default is 0.0.
|
||||
bounds (min,max) Minimum and maximum values allowed in the SpinBox.
|
||||
Either may be None to leave the value unbounded. By default, values are unbounded.
|
||||
suffix (str) suffix (units) to display after the numerical value. By default, suffix is an empty str.
|
||||
siPrefix (bool) If True, then an SI prefix is automatically prepended
|
||||
to the units and the value is scaled accordingly. For example,
|
||||
if value=0.003 and suffix='V', then the SpinBox will display
|
||||
"300 mV" (but a call to SpinBox.value will still return 0.003). Default is False.
|
||||
step (float) The size of a single step. This is used when clicking the up/
|
||||
down arrows, when rolling the mouse wheel, or when pressing
|
||||
keyboard arrows while the widget has keyboard focus. Note that
|
||||
the interpretation of this value is different when specifying
|
||||
the 'dec' argument. Default is 0.01.
|
||||
dec (bool) If True, then the step value will be adjusted to match
|
||||
the current size of the variable (for example, a value of 15
|
||||
might step in increments of 1 whereas a value of 1500 would
|
||||
step in increments of 100). In this case, the 'step' argument
|
||||
is interpreted *relative* to the current value. The most common
|
||||
'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is False.
|
||||
minStep (float) When dec=True, this specifies the minimum allowable step size.
|
||||
int (bool) if True, the value is forced to integer type. Default is False
|
||||
decimals (int) Number of decimal values to display. Default is 2.
|
||||
============== ========================================================================
|
||||
|
||||
All keyword arguments are passed to :func:`setOpts`.
|
||||
"""
|
||||
QtGui.QAbstractSpinBox.__init__(self, parent)
|
||||
self.lastValEmitted = None
|
||||
self.lastText = ''
|
||||
self.textValid = True ## If false, we draw a red border
|
||||
self.setMinimumWidth(0)
|
||||
self.setMaximumHeight(20)
|
||||
self._lastFontHeight = None
|
||||
|
||||
self.setSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Preferred)
|
||||
self.errorBox = ErrorBox(self.lineEdit())
|
||||
|
||||
self.opts = {
|
||||
'bounds': [None, None],
|
||||
|
||||
## Log scaling options #### Log mode is no longer supported.
|
||||
#'step': 0.1,
|
||||
#'minStep': 0.001,
|
||||
#'log': True,
|
||||
#'dec': False,
|
||||
|
||||
## decimal scaling option - example
|
||||
#'step': 0.1,
|
||||
#'minStep': .001,
|
||||
#'log': False,
|
||||
#'dec': True,
|
||||
'wrapping': False,
|
||||
|
||||
## normal arithmetic step
|
||||
'step': D('0.01'), ## if 'dec' is false, the spinBox steps by 'step' every time
|
||||
## if 'dec' is True, the step size is relative to the value
|
||||
## 'step' needs to be an integral divisor of ten, ie 'step'*n=10 for some integer value of n (but only if dec is True)
|
||||
'log': False,
|
||||
'log': False, # deprecated
|
||||
'dec': False, ## if true, does decimal stepping. ie from 1-10 it steps by 'step', from 10 to 100 it steps by 10*'step', etc.
|
||||
## if true, minStep must be set in order to cross zero.
|
||||
|
||||
|
||||
'int': False, ## Set True to force value to be integer
|
||||
|
||||
'suffix': '',
|
||||
@ -112,8 +90,13 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
|
||||
'delayUntilEditFinished': True, ## do not send signals until text editing has finished
|
||||
|
||||
'decimals': 3,
|
||||
'decimals': 6,
|
||||
|
||||
'format': asUnicode("{scaledValue:.{decimals}g}{suffixGap}{siPrefix}{suffix}"),
|
||||
'regex': fn.FLOAT_REGEX,
|
||||
'evalFunc': D,
|
||||
|
||||
'compactHeight': True, # manually remove extra margin outside of text
|
||||
}
|
||||
|
||||
self.decOpts = ['step', 'minStep']
|
||||
@ -124,38 +107,93 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
self.setCorrectionMode(self.CorrectToPreviousValue)
|
||||
self.setKeyboardTracking(False)
|
||||
self.setOpts(**kwargs)
|
||||
self._updateHeight()
|
||||
|
||||
self.editingFinished.connect(self.editingFinishedEvent)
|
||||
self.proxy = SignalProxy(self.sigValueChanging, slot=self.delayedChange, delay=self.opts['delay'])
|
||||
|
||||
|
||||
def event(self, ev):
|
||||
ret = QtGui.QAbstractSpinBox.event(self, ev)
|
||||
if ev.type() == QtCore.QEvent.KeyPress and ev.key() == QtCore.Qt.Key_Return:
|
||||
ret = True ## For some reason, spinbox pretends to ignore return key press
|
||||
return ret
|
||||
|
||||
##lots of config options, just gonna stuff 'em all in here rather than do the get/set crap.
|
||||
def setOpts(self, **opts):
|
||||
"""
|
||||
Changes the behavior of the SpinBox. Accepts most of the arguments
|
||||
allowed in :func:`__init__ <pyqtgraph.SpinBox.__init__>`.
|
||||
"""Set options affecting the behavior of the SpinBox.
|
||||
|
||||
============== ========================================================================
|
||||
**Arguments:**
|
||||
bounds (min,max) Minimum and maximum values allowed in the SpinBox.
|
||||
Either may be None to leave the value unbounded. By default, values are
|
||||
unbounded.
|
||||
suffix (str) suffix (units) to display after the numerical value. By default,
|
||||
suffix is an empty str.
|
||||
siPrefix (bool) If True, then an SI prefix is automatically prepended
|
||||
to the units and the value is scaled accordingly. For example,
|
||||
if value=0.003 and suffix='V', then the SpinBox will display
|
||||
"300 mV" (but a call to SpinBox.value will still return 0.003). Default
|
||||
is False.
|
||||
step (float) The size of a single step. This is used when clicking the up/
|
||||
down arrows, when rolling the mouse wheel, or when pressing
|
||||
keyboard arrows while the widget has keyboard focus. Note that
|
||||
the interpretation of this value is different when specifying
|
||||
the 'dec' argument. Default is 0.01.
|
||||
dec (bool) If True, then the step value will be adjusted to match
|
||||
the current size of the variable (for example, a value of 15
|
||||
might step in increments of 1 whereas a value of 1500 would
|
||||
step in increments of 100). In this case, the 'step' argument
|
||||
is interpreted *relative* to the current value. The most common
|
||||
'step' values when dec=True are 0.1, 0.2, 0.5, and 1.0. Default is
|
||||
False.
|
||||
minStep (float) When dec=True, this specifies the minimum allowable step size.
|
||||
int (bool) if True, the value is forced to integer type. Default is False
|
||||
wrapping (bool) If True and both bounds are not None, spin box has circular behavior.
|
||||
decimals (int) Number of decimal values to display. Default is 6.
|
||||
format (str) Formatting string used to generate the text shown. Formatting is
|
||||
done with ``str.format()`` and makes use of several arguments:
|
||||
|
||||
* *value* - the unscaled value of the spin box
|
||||
* *suffix* - the suffix string
|
||||
* *scaledValue* - the scaled value to use when an SI prefix is present
|
||||
* *siPrefix* - the SI prefix string (if any), or an empty string if
|
||||
this feature has been disabled
|
||||
* *suffixGap* - a single space if a suffix is present, or an empty
|
||||
string otherwise.
|
||||
regex (str or RegexObject) Regular expression used to parse the spinbox text.
|
||||
May contain the following group names:
|
||||
|
||||
* *number* - matches the numerical portion of the string (mandatory)
|
||||
* *siPrefix* - matches the SI prefix string
|
||||
* *suffix* - matches the suffix string
|
||||
|
||||
Default is defined in ``pyqtgraph.functions.FLOAT_REGEX``.
|
||||
evalFunc (callable) Fucntion that converts a numerical string to a number,
|
||||
preferrably a Decimal instance. This function handles only the numerical
|
||||
of the text; it does not have access to the suffix or SI prefix.
|
||||
compactHeight (bool) if True, then set the maximum height of the spinbox based on the
|
||||
height of its font. This allows more compact packing on platforms with
|
||||
excessive widget decoration. Default is True.
|
||||
============== ========================================================================
|
||||
"""
|
||||
#print opts
|
||||
for k in opts:
|
||||
for k,v in opts.items():
|
||||
if k == 'bounds':
|
||||
self.setMinimum(opts[k][0], update=False)
|
||||
self.setMaximum(opts[k][1], update=False)
|
||||
self.setMinimum(v[0], update=False)
|
||||
self.setMaximum(v[1], update=False)
|
||||
elif k == 'min':
|
||||
self.setMinimum(opts[k], update=False)
|
||||
self.setMinimum(v, update=False)
|
||||
elif k == 'max':
|
||||
self.setMaximum(opts[k], update=False)
|
||||
self.setMaximum(v, update=False)
|
||||
elif k in ['step', 'minStep']:
|
||||
self.opts[k] = D(asUnicode(opts[k]))
|
||||
self.opts[k] = D(asUnicode(v))
|
||||
elif k == 'value':
|
||||
pass ## don't set value until bounds have been set
|
||||
elif k == 'format':
|
||||
self.opts[k] = asUnicode(v)
|
||||
elif k == 'regex' and isinstance(v, basestring):
|
||||
self.opts[k] = re.compile(v)
|
||||
elif k in self.opts:
|
||||
self.opts[k] = opts[k]
|
||||
self.opts[k] = v
|
||||
else:
|
||||
raise TypeError("Invalid keyword argument '%s'." % k)
|
||||
if 'value' in opts:
|
||||
@ -205,6 +243,16 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
self.opts['bounds'][0] = m
|
||||
if update:
|
||||
self.setValue()
|
||||
|
||||
def wrapping(self):
|
||||
"""Return whether or not the spin box is circular."""
|
||||
return self.opts['wrapping']
|
||||
|
||||
def setWrapping(self, s):
|
||||
"""Set whether spin box is circular.
|
||||
|
||||
Both bounds must be set for this to have an effect."""
|
||||
self.opts['wrapping'] = s
|
||||
|
||||
def setPrefix(self, p):
|
||||
"""Set a string prefix.
|
||||
@ -248,14 +296,15 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
"""
|
||||
le = self.lineEdit()
|
||||
text = asUnicode(le.text())
|
||||
if self.opts['suffix'] == '':
|
||||
le.setSelection(0, len(text))
|
||||
else:
|
||||
try:
|
||||
index = text.index(' ')
|
||||
except ValueError:
|
||||
return
|
||||
le.setSelection(0, index)
|
||||
m = self.opts['regex'].match(text)
|
||||
if m is None:
|
||||
return
|
||||
s,e = m.start('number'), m.end('number')
|
||||
le.setSelection(s, e-s)
|
||||
|
||||
def focusInEvent(self, ev):
|
||||
super(SpinBox, self).focusInEvent(ev)
|
||||
self.selectNumber()
|
||||
|
||||
def value(self):
|
||||
"""
|
||||
@ -268,29 +317,39 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
return float(self.val)
|
||||
|
||||
def setValue(self, value=None, update=True, delaySignal=False):
|
||||
"""
|
||||
Set the value of this spin.
|
||||
If the value is out of bounds, it will be clipped to the nearest boundary.
|
||||
"""Set the value of this SpinBox.
|
||||
|
||||
If the value is out of bounds, it will be clipped to the nearest boundary
|
||||
or wrapped if wrapping is enabled.
|
||||
|
||||
If the spin is integer type, the value will be coerced to int.
|
||||
Returns the actual value set.
|
||||
|
||||
If value is None, then the current value is used (this is for resetting
|
||||
the value after bounds, etc. have changed)
|
||||
"""
|
||||
|
||||
if value is None:
|
||||
value = self.value()
|
||||
|
||||
bounds = self.opts['bounds']
|
||||
if bounds[0] is not None and value < bounds[0]:
|
||||
value = bounds[0]
|
||||
if bounds[1] is not None and value > bounds[1]:
|
||||
value = bounds[1]
|
||||
|
||||
if None not in bounds and self.opts['wrapping'] is True:
|
||||
# Casting of Decimals to floats required to avoid unexpected behavior of remainder operator
|
||||
value = float(value)
|
||||
l, u = float(bounds[0]), float(bounds[1])
|
||||
value = (value - l) % (u - l) + l
|
||||
else:
|
||||
if bounds[0] is not None and value < bounds[0]:
|
||||
value = bounds[0]
|
||||
if bounds[1] is not None and value > bounds[1]:
|
||||
value = bounds[1]
|
||||
|
||||
if self.opts['int']:
|
||||
value = int(value)
|
||||
|
||||
value = D(asUnicode(value))
|
||||
if not isinstance(value, D):
|
||||
value = D(asUnicode(value))
|
||||
|
||||
if value == self.val:
|
||||
return
|
||||
prev = self.val
|
||||
@ -304,7 +363,6 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
self.emitChanged()
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def emitChanged(self):
|
||||
self.lastValEmitted = self.val
|
||||
@ -324,13 +382,9 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
def sizeHint(self):
|
||||
return QtCore.QSize(120, 0)
|
||||
|
||||
|
||||
def stepEnabled(self):
|
||||
return self.StepUpEnabled | self.StepDownEnabled
|
||||
|
||||
#def fixup(self, *args):
|
||||
#print "fixup:", args
|
||||
|
||||
def stepBy(self, n):
|
||||
n = D(int(n)) ## n must be integral number of steps.
|
||||
s = [D(-1), D(1)][n >= 0] ## determine sign of step
|
||||
@ -352,7 +406,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
vs = [D(-1), D(1)][val >= 0]
|
||||
#exp = D(int(abs(val*(D('1.01')**(s*vs))).log10()))
|
||||
fudge = D('1.01')**(s*vs) ## fudge factor. at some places, the step size depends on the step sign.
|
||||
exp = abs(val * fudge).log10().quantize(1, ROUND_FLOOR)
|
||||
exp = abs(val * fudge).log10().quantize(1, decimal.ROUND_FLOOR)
|
||||
step = self.opts['step'] * D(10)**exp
|
||||
if 'minStep' in self.opts:
|
||||
step = max(step, self.opts['minStep'])
|
||||
@ -364,7 +418,6 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
if 'minStep' in self.opts and abs(val) < self.opts['minStep']:
|
||||
val = D(0)
|
||||
self.setValue(val, delaySignal=True) ## note all steps (arrow buttons, wheel, up/down keys..) emit delayed signals only.
|
||||
|
||||
|
||||
def valueInRange(self, value):
|
||||
bounds = self.opts['bounds']
|
||||
@ -378,61 +431,62 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
return True
|
||||
|
||||
def updateText(self, prev=None):
|
||||
# get the number of decimal places to print
|
||||
decimals = self.opts.get('decimals')
|
||||
|
||||
# temporarily disable validation
|
||||
self.skipValidate = True
|
||||
|
||||
# add a prefix to the units if requested
|
||||
if self.opts['siPrefix']:
|
||||
|
||||
# special case: if it's zero use the previous prefix
|
||||
if self.val == 0 and prev is not None:
|
||||
(s, p) = fn.siScale(prev)
|
||||
|
||||
# NOTE: insert optional format string here?
|
||||
txt = ("%."+str(decimals)+"g %s%s") % (0, p, self.opts['suffix'])
|
||||
else:
|
||||
# NOTE: insert optional format string here as an argument?
|
||||
txt = fn.siFormat(float(self.val), precision=decimals, suffix=self.opts['suffix'])
|
||||
|
||||
# otherwise, format the string manually
|
||||
else:
|
||||
# NOTE: insert optional format string here?
|
||||
txt = ('%.'+str(decimals)+'g%s') % (self.val , self.opts['suffix'])
|
||||
|
||||
|
||||
txt = self.formatText(prev=prev)
|
||||
|
||||
# actually set the text
|
||||
self.lineEdit().setText(txt)
|
||||
self.lastText = txt
|
||||
|
||||
# re-enable the validation
|
||||
self.skipValidate = False
|
||||
|
||||
|
||||
def formatText(self, prev=None):
|
||||
# get the number of decimal places to print
|
||||
decimals = self.opts['decimals']
|
||||
suffix = self.opts['suffix']
|
||||
|
||||
# format the string
|
||||
val = self.value()
|
||||
if self.opts['siPrefix'] is True and len(self.opts['suffix']) > 0:
|
||||
# SI prefix was requested, so scale the value accordingly
|
||||
|
||||
if self.val == 0 and prev is not None:
|
||||
# special case: if it's zero use the previous prefix
|
||||
(s, p) = fn.siScale(prev)
|
||||
else:
|
||||
(s, p) = fn.siScale(val)
|
||||
parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': p, 'scaledValue': s*val}
|
||||
|
||||
else:
|
||||
# no SI prefix /suffix requested; scale is 1
|
||||
parts = {'value': val, 'suffix': suffix, 'decimals': decimals, 'siPrefix': '', 'scaledValue': val}
|
||||
|
||||
parts['suffixGap'] = '' if (parts['suffix'] == '' and parts['siPrefix'] == '') else ' '
|
||||
|
||||
return self.opts['format'].format(**parts)
|
||||
|
||||
def validate(self, strn, pos):
|
||||
if self.skipValidate:
|
||||
ret = QtGui.QValidator.Acceptable
|
||||
else:
|
||||
try:
|
||||
## first make sure we didn't mess with the suffix
|
||||
suff = self.opts.get('suffix', '')
|
||||
if len(suff) > 0 and asUnicode(strn)[-len(suff):] != suff:
|
||||
ret = QtGui.QValidator.Invalid
|
||||
|
||||
## next see if we actually have an interpretable value
|
||||
val = self.interpret()
|
||||
if val is False:
|
||||
ret = QtGui.QValidator.Intermediate
|
||||
else:
|
||||
val = self.interpret()
|
||||
if val is False:
|
||||
ret = QtGui.QValidator.Intermediate
|
||||
if self.valueInRange(val):
|
||||
if not self.opts['delayUntilEditFinished']:
|
||||
self.setValue(val, update=False)
|
||||
ret = QtGui.QValidator.Acceptable
|
||||
else:
|
||||
if self.valueInRange(val):
|
||||
if not self.opts['delayUntilEditFinished']:
|
||||
self.setValue(val, update=False)
|
||||
ret = QtGui.QValidator.Acceptable
|
||||
else:
|
||||
ret = QtGui.QValidator.Intermediate
|
||||
ret = QtGui.QValidator.Intermediate
|
||||
|
||||
except:
|
||||
import sys
|
||||
sys.excepthook(*sys.exc_info())
|
||||
ret = QtGui.QValidator.Intermediate
|
||||
|
||||
## draw / clear border
|
||||
@ -444,42 +498,48 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
## since the text will be forced to its previous state anyway
|
||||
self.update()
|
||||
|
||||
self.errorBox.setVisible(not self.textValid)
|
||||
|
||||
## support 2 different pyqt APIs. Bleh.
|
||||
if hasattr(QtCore, 'QString'):
|
||||
return (ret, pos)
|
||||
else:
|
||||
return (ret, strn, pos)
|
||||
|
||||
def paintEvent(self, ev):
|
||||
QtGui.QAbstractSpinBox.paintEvent(self, ev)
|
||||
|
||||
## draw red border if text is invalid
|
||||
if not self.textValid:
|
||||
p = QtGui.QPainter(self)
|
||||
p.setRenderHint(p.Antialiasing)
|
||||
p.setPen(fn.mkPen((200,50,50), width=2))
|
||||
p.drawRoundedRect(self.rect().adjusted(2, 2, -2, -2), 4, 4)
|
||||
p.end()
|
||||
|
||||
def fixup(self, strn):
|
||||
# fixup is called when the spinbox loses focus with an invalid or intermediate string
|
||||
self.updateText()
|
||||
strn.clear()
|
||||
strn.append(self.lineEdit().text())
|
||||
|
||||
def interpret(self):
|
||||
"""Return value of text. Return False if text is invalid, raise exception if text is intermediate"""
|
||||
"""Return value of text or False if text is invalid."""
|
||||
strn = self.lineEdit().text()
|
||||
suf = self.opts['suffix']
|
||||
if len(suf) > 0:
|
||||
if strn[-len(suf):] != suf:
|
||||
return False
|
||||
#raise Exception("Units are invalid.")
|
||||
strn = strn[:-len(suf)]
|
||||
try:
|
||||
val = fn.siEval(strn)
|
||||
except:
|
||||
#sys.excepthook(*sys.exc_info())
|
||||
#print "invalid"
|
||||
return False
|
||||
#print val
|
||||
return val
|
||||
|
||||
# tokenize into numerical value, si prefix, and suffix
|
||||
try:
|
||||
val, siprefix, suffix = fn.siParse(strn, self.opts['regex'])
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# check suffix
|
||||
if suffix != self.opts['suffix'] or (suffix == '' and siprefix != ''):
|
||||
return False
|
||||
|
||||
# generate value
|
||||
val = self.opts['evalFunc'](val)
|
||||
if self.opts['int']:
|
||||
val = int(fn.siApply(val, siprefix))
|
||||
else:
|
||||
try:
|
||||
val = fn.siApply(val, siprefix)
|
||||
except Exception:
|
||||
import sys
|
||||
sys.excepthook(*sys.exc_info())
|
||||
return False
|
||||
|
||||
return val
|
||||
|
||||
def editingFinishedEvent(self):
|
||||
"""Edit has finished; set value."""
|
||||
#print "Edit finished."
|
||||
@ -488,7 +548,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
return
|
||||
try:
|
||||
val = self.interpret()
|
||||
except:
|
||||
except Exception:
|
||||
return
|
||||
|
||||
if val is False:
|
||||
@ -498,3 +558,44 @@ class SpinBox(QtGui.QAbstractSpinBox):
|
||||
#print "no value change:", val, self.val
|
||||
return
|
||||
self.setValue(val, delaySignal=False) ## allow text update so that values are reformatted pretty-like
|
||||
|
||||
def _updateHeight(self):
|
||||
# SpinBox has very large margins on some platforms; this is a hack to remove those
|
||||
# margins and allow more compact packing of controls.
|
||||
if not self.opts['compactHeight']:
|
||||
self.setMaximumHeight(1e6)
|
||||
return
|
||||
h = QtGui.QFontMetrics(self.font()).height()
|
||||
if self._lastFontHeight != h:
|
||||
self._lastFontHeight = h
|
||||
self.setMaximumHeight(h)
|
||||
|
||||
def paintEvent(self, ev):
|
||||
self._updateHeight()
|
||||
QtGui.QAbstractSpinBox.paintEvent(self, ev)
|
||||
|
||||
|
||||
class ErrorBox(QtGui.QWidget):
|
||||
"""Red outline to draw around lineedit when value is invalid.
|
||||
(for some reason, setting border from stylesheet does not work)
|
||||
"""
|
||||
def __init__(self, parent):
|
||||
QtGui.QWidget.__init__(self, parent)
|
||||
parent.installEventFilter(self)
|
||||
self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
|
||||
self._resize()
|
||||
self.setVisible(False)
|
||||
|
||||
def eventFilter(self, obj, ev):
|
||||
if ev.type() == QtCore.QEvent.Resize:
|
||||
self._resize()
|
||||
return False
|
||||
|
||||
def _resize(self):
|
||||
self.setGeometry(0, 0, self.parent().width(), self.parent().height())
|
||||
|
||||
def paintEvent(self, ev):
|
||||
p = QtGui.QPainter(self)
|
||||
p.setPen(fn.mkPen(color='r', width=2))
|
||||
p.drawRect(self.rect())
|
||||
p.end()
|
||||
|
28
pyqtgraph/widgets/tests/test_spinbox.py
Normal file
28
pyqtgraph/widgets/tests/test_spinbox.py
Normal file
@ -0,0 +1,28 @@
|
||||
import pyqtgraph as pg
|
||||
pg.mkQApp()
|
||||
|
||||
|
||||
def test_spinbox_formatting():
|
||||
sb = pg.SpinBox()
|
||||
assert sb.opts['decimals'] == 6
|
||||
assert sb.opts['int'] is False
|
||||
|
||||
# table of test conditions:
|
||||
# value, text, options
|
||||
conds = [
|
||||
(0, '0', dict(suffix='', siPrefix=False, dec=False, int=False)),
|
||||
(100, '100', dict()),
|
||||
(1000000, '1e+06', dict()),
|
||||
(1000, '1e+03', dict(decimals=2)),
|
||||
(1000000, '1e+06', dict(int=True, decimals=6)),
|
||||
(12345678955, '12345678955', dict(int=True, decimals=100)),
|
||||
(1.45e-9, '1.45e-09 A', dict(int=False, decimals=6, suffix='A', siPrefix=False)),
|
||||
(1.45e-9, '1.45 nA', dict(int=False, decimals=6, suffix='A', siPrefix=True)),
|
||||
(-2500.3427, '$-2500.34', dict(int=False, format='${value:0.02f}')),
|
||||
]
|
||||
|
||||
for (value, text, opts) in conds:
|
||||
sb.setOpts(**opts)
|
||||
sb.setValue(value)
|
||||
assert sb.value() == value
|
||||
assert pg.asUnicode(sb.text()) == text
|
Loading…
x
Reference in New Issue
Block a user