From e5a17edb4d329e2e0aecd34a93c7d71de3cb768e Mon Sep 17 00:00:00 2001 From: Luke Campagnola Date: Thu, 8 Dec 2016 10:12:45 -0800 Subject: [PATCH] Add spinbox 'regex' and 'evalFunc' options to complete user-formatting functionality --- examples/SpinBox.py | 9 +++- pyqtgraph/functions.py | 16 +++++-- pyqtgraph/widgets/SpinBox.py | 63 +++++++++++++++---------- pyqtgraph/widgets/tests/test_spinbox.py | 7 ++- 4 files changed, 63 insertions(+), 32 deletions(-) diff --git a/examples/SpinBox.py b/examples/SpinBox.py index 2fa9b161..84c82332 100644 --- a/examples/SpinBox.py +++ b/examples/SpinBox.py @@ -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,
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(-?\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[0-9a-fA-F]+)$', + evalFunc=lambda s: ast.literal_eval('0x'+s))), ] diff --git a/pyqtgraph/functions.py b/pyqtgraph/functions.py index faa11820..1fd05946 100644 --- a/pyqtgraph/functions.py +++ b/pyqtgraph/functions.py @@ -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[+-]?((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)\s*((?P[u' + SI_PREFIXES + r']?)(?P\w.*))?$') -INT_REGEX = re.compile(r'(?P[+-]?\d+)\s*(?P[u' + SI_PREFIXES + r']?)(?P.*)$') +FLOAT_REGEX = re.compile(r'(?P[+-]?((\d+(\.\d*)?)|(\d*\.\d+))([eE][+-]?\d+)?)\s*((?P[u' + SI_PREFIXES + r']?)(?P\w.*))?$') +INT_REGEX = re.compile(r'(?P[+-]?\d+)\s*(?P[u' + SI_PREFIXES + r']?)(?P.*)$') 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 diff --git a/pyqtgraph/widgets/SpinBox.py b/pyqtgraph/widgets/SpinBox.py index df7acfcd..8e81f06d 100644 --- a/pyqtgraph/widgets/SpinBox.py +++ b/pyqtgraph/widgets/SpinBox.py @@ -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." diff --git a/pyqtgraph/widgets/tests/test_spinbox.py b/pyqtgraph/widgets/tests/test_spinbox.py index dcf15cb3..b9fbaeb2 100644 --- a/pyqtgraph/widgets/tests/test_spinbox.py +++ b/pyqtgraph/widgets/tests/test_spinbox.py @@ -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: