Add spinbox 'regex' and 'evalFunc' options to complete user-formatting functionality

This commit is contained in:
Luke Campagnola 2016-12-08 10:12:45 -08:00
parent f0e26d3add
commit e5a17edb4d
4 changed files with 63 additions and 32 deletions

View File

@ -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,13 @@ 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))),
]

View File

@ -37,8 +37,8 @@ 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>.*)$')
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):
@ -121,8 +121,16 @@ def siParse(s, regex=FLOAT_REGEX):
m = regex.match(s)
if m is None:
raise ValueError('Cannot parse number "%s"' % s)
sip = m.group('siprefix')
suf = m.group('suffix')
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

View File

@ -1,13 +1,14 @@
# -*- 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
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']
@ -89,7 +90,9 @@ class SpinBox(QtGui.QAbstractSpinBox):
'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
}
@ -152,28 +155,41 @@ class SpinBox(QtGui.QAbstractSpinBox):
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(opts[k])
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:
@ -266,14 +282,11 @@ 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)
@ -483,7 +496,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
# tokenize into numerical value, si prefix, and suffix
try:
val, siprefix, suffix = fn.siParse(strn)
val, siprefix, suffix = fn.siParse(strn, self.opts['regex'])
except Exception:
return False
@ -492,7 +505,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
return False
# generate value
val = D(val)
val = self.opts['evalFunc'](val)
if self.opts['int']:
val = int(fn.siApply(val, siprefix))
else:
@ -504,7 +517,7 @@ class SpinBox(QtGui.QAbstractSpinBox):
return False
return val
def editingFinishedEvent(self):
"""Edit has finished; set value."""
#print "Edit finished."

View File

@ -1,6 +1,7 @@
import pyqtgraph as pg
pg.mkQApp()
def test_spinbox():
sb = pg.SpinBox()
assert sb.opts['decimals'] == 3
@ -13,8 +14,10 @@ def test_spinbox():
(100, '100', dict()),
(1000000, '1e+06', dict()),
(1000, '1e+03', dict(decimals=2)),
(1000000, '1000000', dict(int=True)),
(12345678955, '12345678955', dict(int=True)),
(1000000, '1e+06', dict(int=True, decimals=6)),
(12345678955, '12345678955', dict(int=True, decimals=100)),
(1.45e-9, '1.45e-9 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)),
]
for (value, text, opts) in conds: