"""Module Documentation here
"""
#=========================================================================================
# Licence, Reference and Credits
#=========================================================================================
__copyright__ = "Copyright (C) CCPN project (http://www.ccpn.ac.uk) 2014 - 2021"
__credits__ = ("Ed Brooksbank, Joanna Fox, Victoria A Higman, Luca Mureddu, Eliza Płoskoń",
"Timothy J Ragan, Brian O Smith, Gary S Thompson & Geerten W Vuister")
__licence__ = ("CCPN licence. See http://www.ccpn.ac.uk/v3-software/downloads/license")
__reference__ = ("Skinner, S.P., Fogh, R.H., Boucher, W., Ragan, T.J., Mureddu, L.G., & Vuister, G.W.",
"CcpNmr AnalysisAssign: a flexible platform for integrated NMR analysis",
"J.Biomol.Nmr (2016), 66, 111-124, http://doi.org/10.1007/s10858-016-0060-y")
#=========================================================================================
# Last code modification
#=========================================================================================
__modifiedBy__ = "$modifiedBy: Geerten Vuister $"
__dateModified__ = "$dateModified: 2021-12-23 11:27:18 +0000 (Thu, December 23, 2021) $"
__version__ = "$Revision: 3.0.4 $"
#=========================================================================================
# Created
#=========================================================================================
__author__ = "$Author: Geerten Vuister $"
__date__ = "$Date: 2017-04-07 10:28:41 +0000 (Fri, April 07, 2017) $"
#=========================================================================================
# Start of code
#=========================================================================================
import sys
import re
import typing
from functools import partial
from contextlib import contextmanager
from PyQt5 import QtGui, QtWidgets, QtCore
from PyQt5.QtCore import pyqtSignal
from ccpn.ui.gui.widgets.Base import Base
from math import floor, log10
DOUBLESPINBOXSTEP = 10
SCIENTIFICSPINBOXSTEP = 5
KEYVALIDATELIST = (QtCore.Qt.Key_Return,
QtCore.Qt.Key_Enter,
QtCore.Qt.Key_Tab,
QtCore.Qt.Key_Up,
QtCore.Qt.Key_Down,
QtCore.Qt.Key_Left,
QtCore.Qt.Key_Right,
QtCore.Qt.Key_0,
QtCore.Qt.Key_1,
QtCore.Qt.Key_2,
QtCore.Qt.Key_3,
QtCore.Qt.Key_4,
QtCore.Qt.Key_5,
QtCore.Qt.Key_6,
QtCore.Qt.Key_7,
QtCore.Qt.Key_8,
QtCore.Qt.Key_9,
# QtCore.Qt.Key_Minus,
# QtCore.Qt.Key_Plus,
# QtCore.Qt.Key_E,
# QtCore.Qt.Key_Period,
)
KEYVALIDATEDECIMAL = (QtCore.Qt.Key_0,) # need to discard if after decimal
KEYVALIDATEDIGIT = (QtCore.Qt.Key_0,
QtCore.Qt.Key_1,
QtCore.Qt.Key_2,
QtCore.Qt.Key_3,
QtCore.Qt.Key_4,
QtCore.Qt.Key_5,
QtCore.Qt.Key_6,
QtCore.Qt.Key_7,
QtCore.Qt.Key_8,
QtCore.Qt.Key_9,
)
[docs]class DoubleSpinbox(QtWidgets.QDoubleSpinBox, Base):
# # To be done more rigorously later
# _styleSheet = """
# DoubleSpinbox {
# background-color: #f7ffff;
# color: #122043;
# margin: 0px 0px 0px 0px;
# padding: 3px 3px 3px 3px;
# border: 1px solid #182548;
# }
#
# DoubleSpinbox::hover {
# background-color: #e4e15b;
# }
# """
returnPressed = pyqtSignal(float)
wheelChanged = pyqtSignal(float)
_showValidation = True
_validationIntermediate = QtGui.QColor('lightpink')
_validationInvalid = QtGui.QColor('lightpink') # red is a bit harsh
def __init__(self, parent, value=None, min=None, max=None, step=None, prefix=None, suffix=None,
showButtons=True, decimals=None, callback=None, editable=True, locale=None, **kwds):
"""
From the QTdocumentation
Constructs a spin box with a step value of 1.0 and a precision of 2 decimal places.
Change the default 0.0 minimum value to -sys.float_info.max
Change the default 99.99 maximum value to sys.float_info.max
The value is default set to 0.00.
The spin box has the given parent.
"""
self._keyPressed = None
self.validator = QtGui.QDoubleValidator()
self.validator.Notation = 1
super().__init__(parent)
Base._init(self, **kwds)
self._qLocale = locale or QtCore.QLocale()
self.setLocale(self._qLocale)
if min is not None:
self.setMinimum(min)
else:
self.setMinimum(-1.0 * sys.float_info.max)
if max is not None:
self.setMaximum(max)
else:
self.setMaximum(sys.float_info.max)
self.isSelected = False
self._internalWheelEvent = True
if step is not None:
self.setSingleStep(step)
if decimals is not None:
self.setDecimals(decimals)
if showButtons is False:
self.setButtonSymbols(QtWidgets.QAbstractSpinBox.NoButtons)
if prefix:
self.setPrefix(prefix + ' ')
if suffix:
self.setSuffix(' ' + suffix)
if value is not None:
value = value
self.setValue(value)
lineEdit = self.lineEdit()
lineEdit.returnPressed.connect(self._returnPressed)
self.baseColour = self.lineEdit().palette().color(QtGui.QPalette.Base)
# must be set after setting value/limits
self._callback = None
self.setCallback(callback)
# change focusPolicy so that spinboxes don't grab focus unless selected
self.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setStyleSheet('DoubleSpinbox { padding: 3px 3px 3px 3px; }'
'DoubleSpinbox:disabled { background-color: whitesmoke; }')
[docs] def wheelEvent(self, event: QtGui.QWheelEvent) -> None:
"""Process the wheelEvent for the doubleSpinBox
"""
# emit the value when wheel event has occurred, only when hasFocus
if self.hasFocus() or not self._internalWheelEvent:
super().wheelEvent(event)
value = self.value()
self.wheelChanged.emit(value)
else:
event.ignore()
@contextmanager
def _useExternalWheelEvent(self):
try:
self._internalWheelEvent = False
yield
finally:
self._internalWheelEvent = True
def _externalWheelEvent(self, event):
with self._useExternalWheelEvent():
self.wheelEvent(event)
[docs] def stepBy(self, steps: int) -> None:
if self._internalWheelEvent:
super().stepBy(min(steps, DOUBLESPINBOXSTEP) if steps > 0 else max(steps, -DOUBLESPINBOXSTEP))
else:
# disable multiple stepping for wheelMouse events in a spectrumDisplay
super().stepBy(1 if steps > 0 else -1 if steps < 0 else steps)
def _returnPressed(self, *args):
"""emit the value when return has been pressed
"""
self.returnPressed.emit(self.value())
[docs] def get(self):
return self.value()
[docs] def set(self, value):
self.setValue(value)
[docs] def setSelected(self):
self.isSelected = True
[docs] def focusInEvent(self, QFocusEvent):
self.setSelected()
super(DoubleSpinbox, self).focusInEvent(QFocusEvent)
[docs] def setCallback(self, callback):
"""Sets callback; disconnects if callback=None
"""
if self._callback is not None:
self.valueChanged.disconnect()
if callback:
self.valueChanged.connect(callback)
self._callback = callback
[docs] def textFromValue(self, v: typing.Union[float, int]) -> str:
"""Subclass to remove extra zeroes
"""
if isinstance(v, int):
return super(DoubleSpinbox, self).textFromValue(v)
else:
return self._qLocale.toString(round(v, self.decimals()), 'g', QtCore.QLocale.FloatingPointShortest)
[docs] def validate(self, text: str, pos: int) -> typing.Tuple[QtGui.QValidator.State, str, int]:
_state = super().validate(text, pos)
if self._showValidation:
self._checkState(_state)
return _state
def _checkState(self, _state):
try:
state, text, position = _state
for obj in [self, self.lineEdit()]:
# set the validate colours for the object
palette = obj.palette()
if state == QtGui.QValidator.Acceptable:
palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, self.baseColour)
elif state == QtGui.QValidator.Intermediate:
palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, self._validationIntermediate)
else: # Invalid
palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, self._validationInvalid)
obj.setPalette(palette)
except Exception as es:
pass
[docs] def keyPressEvent(self, event):
# allow the typing of other stuff into the box and validate only when required
self._keyPressed = event.key() if event.key() in KEYVALIDATELIST else 0
super().keyPressEvent(event)
[docs] def keyReleaseEvent(self, event):
self._keyPressed = None
super().keyReleaseEvent(event)
[docs] def setMinimumCharacters(self, value):
from ccpn.ui.gui.widgets.Font import getTextDimensionsFromFont
_, maxDim = getTextDimensionsFromFont(textList=['_' * value])
self.setMinimumWidth(maxDim.width())
def _getSaveState(self):
"""
Internal. Called for saving/restoring the widget state.
"""
return self.get()
def _setSavedState(self, value):
"""
Internal. Called for saving/restoring the widget state.
"""
return self.set(value)
def _flashColour(self, colour):
try:
# set the background colour for the object
for obj in [self, self.lineEdit()]:
# set the colours for the object
palette = obj.palette()
palette.setColor(QtGui.QPalette.Active, QtGui.QPalette.Base, colour)
obj.setPalette(palette)
except Exception as es:
pass
def _flashError(self, timer=300):
# set the warning colour and then set back to background colour
self._flashColour(self._validationInvalid)
QtCore.QTimer.singleShot(timer, partial(self._flashColour, self.baseColour))
# Regular expression to find floats. Match groups are the whole string, the
# whole coefficient, the decimal part of the coefficient, and the exponent
# part. (decimal point, not decimal comma)
_float_re = re.compile(r'(([+-]?\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?)')
[docs]def fexp(f):
return int(floor(log10(abs(f)))) if f != 0 else 0
# def validFloat(string):
# match = _float_re.search(string)
# return match.groups()[0] == string if match else False
[docs]class ScientificDoubleSpinBox(DoubleSpinbox):
"""Constructs a spinbox in which the values can be set using Sci notation
"""
def __init__(self, *args, **kwargs):
super(ScientificDoubleSpinBox, self).__init__(*args, **kwargs)
self.setDecimals(1000)
_decs = kwargs.get('step')
# step if defined by 'step' relative to current power
self._decimalStep = 0.1 if _decs is None else _decs
[docs] def valueFromText(self, text):
"""Values in the spinbox are constrained to the correct sign if the min/max values are
either both positive or both negative
"""
val = super().valueFromText(text)
if self.minimum() <= self.maximum() <= 0:
# check for maximum
_lineEdit = self.lineEdit()
val = min(-abs(val), self.maximum())
# keep the cursor position so it doesn't jump to the end; same below
_lastPos = _lineEdit.cursorPosition()
_lineEdit.setText(self.textFromValue(val))
_lineEdit.setCursorPosition(_lastPos)
elif 0 <= self.minimum() <= self.maximum():
# check for minimum
_lineEdit = self.lineEdit()
val = max(abs(val), self.minimum())
_lastPos = _lineEdit.cursorPosition()
_lineEdit.setText(self.textFromValue(val))
_lineEdit.setCursorPosition(_lastPos)
return val
[docs] def textFromValue(self, value):
return self.formatFloat(value)
[docs] def stepBy(self, steps):
"""Increment the current value.
Step if 1/10th of the current rounded value * step
Now defined by 'step' in kwargs, i.e., step=0.1, 0.01
"""
# clip number of steps to SCIENTIFICSPINBOXSTEP as 10* will go directly to zero from 1 when Ctrl/Cmd pressed
steps = min(steps, SCIENTIFICSPINBOXSTEP) if steps > 0 else max(steps, -SCIENTIFICSPINBOXSTEP)
text = self.cleanText()
decimal, valid = self._qLocale.toFloat(text)
if valid:
decimal += steps * 10 ** fexp(decimal * self._decimalStep)
new_string = self.formatFloat(decimal)
self.lineEdit().setText(new_string)
def _getSaveState(self):
"""
Internal. Called for saving/restoring the widget state.
"""
return self.get()
def _setSavedState(self, value):
"""
Internal. Called for saving/restoring the widget state.
"""
return self.set(value)
[docs] def validate(self, text, position):
# activate the validator when
if self._keyPressed == 0:
_state = self.validator.Intermediate, text, position
return _state
if self._keyPressed in KEYVALIDATEDECIMAL:
ii = position - 1
while ii > 0:
# allow the user to enter zeroes after the decimal point/decimal comma
if text[ii] in ['.', ',']:
_state = self.validator.Intermediate, text, position
return _state
if text[ii] in ['e']:
_state = self.validator.validate(text, position)
return _state
ii -= 1
_state = self.validator.validate(text, position)
return _state
else:
_state = self.validator.validate(text, position)
return _state
v = float("{0:.3f}".format(0.024))
v1 = 24678.45
if __name__ == '__main__':
from ccpn.ui.gui.widgets.Application import TestApplication
from ccpn.ui.gui.popups.Dialog import CcpnDialog
from ccpn.ui.gui.widgets.Frame import Frame
from PyQt5 import QtWidgets, QtCore, QtGui
app = TestApplication()
popup = CcpnDialog()
# test setting the dialog to French (different float format)
# QtCore.QLocale.setDefault(QtCore.QLocale(QtCore.QLocale.French))
fr = Frame(popup, setLayout=True)
sb = DoubleSpinbox(fr, value=v1, decimals=3, step=0.01, grid=(0, 0))
sb2 = ScientificDoubleSpinBox(fr, value=v1, decimals=3, grid=(1, 0), min=0.001, max=1e32)
sb3 = ScientificDoubleSpinBox(fr, value=v1, decimals=3, grid=(2, 0), max=-0.001, min=-1e32)
popup.show()
popup.raise_()
app.start()