#!/usr/bin/env python
# ###########################################################################
#
# This file is part of Taurus
#
# http://taurus-scada.org
#
# Copyright 2011 CELLS / ALBA Synchrotron, Bellaterra, Spain
#
# Taurus is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Taurus is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Taurus. If not, see <http://www.gnu.org/licenses/>.
#
# ###########################################################################
"""
This module provides a set of basic taurus widgets based on QLineEdit
"""
import re
import sys
import numpy
from taurus.external.qt import Qt, PYQT4, PYSIDE
from taurus.core.units import Quantity
from taurus.qt.qtgui.base import TaurusBaseWritableWidget
from taurus.qt.qtgui.util import PintValidator
from taurus.core import DataType, DataFormat, TaurusEventType
__docformat__ = "restructuredtext"
[docs]
class TaurusValueLineEdit(Qt.QLineEdit, TaurusBaseWritableWidget):
"""
A taurus-aware ``QLineEdit``. It will display the value (or fragment
of the value) referenced by its model. It is a
:class:`~taurus.qt.qtgui.base.TaurusBaseWritableWidget`
and as such it does not apply the changes straight away to the model
(unless ``autoApply`` is enabled), but instead shows that there are pending
operations which can be applied by pressing "ENTER".
When used with numerical value Attributes as its model, it provides some
extended behaviour:
- It represents out-of-limit values using different colours (for warning,
range, invalid,...)
- It uses a validator that is range-aware
- The mouse wheel and keyboard arrows can be enabled for doing value
increments
.. note::
when used with models whose value is a pint `Quantity`, the text
is parsed by pint and therefore one can write e.g. `2 3 mm` which is
equivalent to `6 mm` !
"""
_bytesEncoding = sys.stdin.encoding
def __init__(self, qt_parent=None, designMode=False):
name = self.__class__.__name__
self.call__init__wo_kw(Qt.QLineEdit, qt_parent)
self.call__init__(
TaurusBaseWritableWidget, name, designMode=designMode
)
self._enableWheelEvent = False
self._wheel_delta_reminder = 0
self._last_value = None
self._singleStep = 1.0
self._allow_auto_enable = True
self.setAlignment(Qt.Qt.AlignRight)
self.setValidator(None)
self.textChanged.connect(self.notifyValueChanged)
self.returnPressed.connect(self.writeValue)
self.valueChangedSignal.connect(self.updatePendingOperations)
self.editingFinished.connect(self._onEditingFinished)
def _updateValidator(self, value):
"""This method sets a validator depending on the data type"""
val = None
if value is not None and isinstance(value.wvalue, Quantity):
val = self.validator()
if not isinstance(val, PintValidator):
val = PintValidator(self)
self.setValidator(val)
attr = self.getModelObj()
if attr is not None:
bottom, top = attr.range
if bottom != val.bottom:
val.setBottom(bottom)
if top != val.top:
val.setTop(top)
units = value.wvalue.units
if units != val.units:
val.setUnits(units)
# @TODO Other validators can be configured for other types
# (e.g. with string lengths, tango names,...)
else:
self.setValidator(None)
self.debug("Validator disabled")
return val
def __decimalDigits(self, fmt):
"""returns the number of decimal digits from a format string
(or None if they are not defined)
"""
try:
if fmt[-1].lower() in ["f", "g"] and "." in fmt:
return int(fmt[:-1].split(".")[-1])
else:
return None
except Exception:
return None
def _onEditingFinished(self):
"""slot for performing autoapply only when edition is finished"""
if self._autoApply:
self.writeValue()
# ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~
# TaurusBaseWritableWidget overwriting
# ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~
[docs]
def notifyValueChanged(self, *args):
"""reimplement to avoid autoapply on every partial edition"""
self.emitValueChanged()
[docs]
def handleEvent(self, evt_src, evt_type, evt_value):
# handle the case in which the line edit is not yet initialized
if self._last_value is None:
try:
value = self.getModelObj().read(cache=True)
self._updateValidator(value)
if value is not None:
self.setValue(value.wvalue)
except Exception as e:
self.info("Failed attempt to initialize value: {}".format(e))
if self._allow_auto_enable:
# use QLineEdit.setEnabled in order to avoid changing
# _allow_auto_enable status
Qt.QLineEdit.setEnabled(self, evt_type != TaurusEventType.Error)
if evt_type in (TaurusEventType.Change, TaurusEventType.Periodic):
self._updateValidator(evt_value)
TaurusBaseWritableWidget.handleEvent(
self, evt_src, evt_type, evt_value
)
if evt_type == TaurusEventType.Error:
self.updateStyle()
self._last_value = None
[docs]
def setEnabled(self, enabled):
"""Reimplement from :class:`QLineEdit` to avoid autoenabling if the
widget is explicitly disabled (but allow auto-disabling if the
widget is explicitly enabled)
"""
self._allow_auto_enable = enabled
return Qt.QLineEdit.setEnabled(self, enabled)
[docs]
def isTextValid(self):
"""
Validates current text
:return: Returns False if there is a validator and the current text is
not Acceptable. Returns True otherwise.
:rtype: bool
"""
val = self.validator()
if val is None:
return True
return val.validate(str(self.text()), 0)[0] == val.Acceptable
[docs]
def updateStyle(self):
"""Reimplemented from :class:`TaurusBaseWritableWidget`"""
TaurusBaseWritableWidget.updateStyle(self)
value = self.getValue()
if value is None or not self.isTextValid() or not self.isEnabled():
# invalid value
color, weight = "gray", "normal"
else:
# check if there are pending operations
if self.hasPendingOperations():
color, weight = "blue", "bold"
else:
color, weight = "black", "normal"
# also check alarms (if applicable)
modelObj = self.getModelObj()
if modelObj and modelObj.type in [
DataType.Integer,
DataType.Float,
]:
min_, max_ = modelObj.alarms
if (min_ is not None and value < min_) or (
max_ is not None and value > max_
):
color = "orange"
# apply style
style = "TaurusValueLineEdit {{color: {}; font-weight: {}}}".format(
color,
weight,
)
self.setStyleSheet(style)
[docs]
def wheelEvent(self, evt):
"""Wheel event handler"""
if not self.getEnableWheelEvent() or Qt.QLineEdit.isReadOnly(self):
return Qt.QLineEdit.wheelEvent(self, evt)
model = self.getModelObj()
if model is None or not model.isNumeric():
return Qt.QLineEdit.wheelEvent(self, evt)
evt.accept()
if PYQT4 or PYSIDE:
self._wheel_delta_reminder += evt.delta()
else:
self._wheel_delta_reminder += evt.angleDelta().y()
numSteps = self._wheel_delta_reminder // 120
self._wheel_delta_reminder -= numSteps * 120
self._stepBy(numSteps)
[docs]
def keyPressEvent(self, evt):
"""Key press event handler"""
if evt.key() in (Qt.Qt.Key_Return, Qt.Qt.Key_Enter):
Qt.QLineEdit.keyPressEvent(self, evt)
evt.accept()
return
if Qt.QLineEdit.isReadOnly(self):
return Qt.QLineEdit.keyPressEvent(self, evt)
model = self.getModelObj()
if model is None or not model.isNumeric():
return Qt.QLineEdit.keyPressEvent(self, evt)
if evt.key() == Qt.Qt.Key_Up:
numSteps = 1
elif evt.key() == Qt.Qt.Key_Down:
numSteps = -1
else:
return Qt.QLineEdit.keyPressEvent(self, evt)
evt.accept()
self._stepBy(numSteps)
def _stepBy(self, steps):
value = self.getValue()
self.setValue(value + Quantity(steps * self._singleStep, value.units))
if self.getAutoApply():
self.editingFinished.emit()
else:
kmods = Qt.QCoreApplication.instance().keyboardModifiers()
controlpressed = bool(kmods & Qt.Qt.ControlModifier)
if controlpressed:
self.writeValue(forceApply=True)
[docs]
def setValue(self, v):
"""Set the displayed text from a given value object"""
# Support displaying the value without units (enabled by fragment)
# Other fragments are ignored by setValue
if self.modelFragmentName == "wvalue.magnitude":
try:
validator = self.validator()
if validator is None:
value = self.getModelValueObj()
validator = self._updateValidator(value)
units = validator.units
v = v.to(units).magnitude
except Exception as e:
self.debug("Cannot enforce fragment. Reason: {}".format(e))
self._last_value = v
self.setText(str(self.displayValue(v)).strip())
[docs]
def getValue(self):
text = self.text()
model_obj = self.getModelObj()
if model_obj is None:
return None
val = self.validator()
try:
model_type = model_obj.type
model_format = model_obj.data_format
if model_type in [DataType.Integer, DataType.Float]:
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# workaround for
# https://github.com/hgrecco/pint/issues/614
# see it also in PintValidator.validate()
if re.match(r"^[\+\-]?\s*0+[1-9]+[^.]*$", text.strip()):
return None
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
try:
q = Quantity(text)
# allow implicit units (assume wvalue.units implicitly)
if q.unitless:
q = Quantity(q.magnitude, val.units)
return q
except Exception as e:
self.debug('Problem with input "{}": {}'.format(text, e))
return None
elif model_type == DataType.Boolean:
if model_format == DataFormat._0D:
return bool(int(eval(text)))
else:
return numpy.array(eval(text), dtype=int).astype(bool)
elif model_type == DataType.String:
if model_format == DataFormat._0D:
return str(text)
else:
return numpy.array(eval(text), dtype=str).tolist()
elif model_type == DataType.Bytes:
return bytes(text, self._bytesEncoding)
else:
raise TypeError(
'Unsupported model type "{}"'.format(model_type)
)
except Exception as e:
msg = 'Cannot return value for "{}". Reason: {}'.format(text, e)
if text in (str(None), self.getNoneValue()):
self.debug(msg)
else:
self.warning(msg)
return None
[docs]
def setEnableWheelEvent(self, b):
self._enableWheelEvent = b
[docs]
def getEnableWheelEvent(self):
return self._enableWheelEvent
[docs]
def resetEnableWheelEvent(self):
self.setEnableWheelEvent(False)
[docs]
def getSingleStep(self):
return self._singleStep
[docs]
def setSingleStep(self, step):
self._singleStep = step
[docs]
def resetSingleStep(self):
self.setSingleStep(1.0)
[docs]
@classmethod
def getQtDesignerPluginInfo(cls):
ret = TaurusBaseWritableWidget.getQtDesignerPluginInfo()
ret["module"] = "taurus.qt.qtgui.input"
ret["icon"] = "designer:lineedit.png"
return ret
# ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~
# QT properties
# ~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~-~
model = Qt.pyqtProperty(
"QString",
TaurusBaseWritableWidget.getModel,
TaurusBaseWritableWidget.setModel,
TaurusBaseWritableWidget.resetModel,
)
#: (deprecated))
useParentModel = Qt.pyqtProperty(
"bool",
TaurusBaseWritableWidget.getUseParentModel,
TaurusBaseWritableWidget.setUseParentModel,
TaurusBaseWritableWidget.resetUseParentModel,
)
autoApply = Qt.pyqtProperty(
"bool",
TaurusBaseWritableWidget.getAutoApply,
TaurusBaseWritableWidget.setAutoApply,
TaurusBaseWritableWidget.resetAutoApply,
)
forcedApply = Qt.pyqtProperty(
"bool",
TaurusBaseWritableWidget.getForcedApply,
TaurusBaseWritableWidget.setForcedApply,
TaurusBaseWritableWidget.resetForcedApply,
)
enableWheelEvent = Qt.pyqtProperty(
"bool", getEnableWheelEvent, setEnableWheelEvent, resetEnableWheelEvent
)
def main():
import sys
import taurus.qt.qtgui.application
Application = taurus.qt.qtgui.application.TaurusApplication
app = Application.instance()
owns_app = app is None
if owns_app:
import taurus.core.util.argparse
parser = taurus.core.util.argparse.get_taurus_parser()
parser.usage = "%prog [options] <full_attribute_name(s)>"
app = Application(
sys.argv,
cmd_line_parser=parser,
app_name="Taurus lineedit demo",
app_version="1.0",
org_domain="Taurus",
org_name="Tango community",
)
args = app.get_command_line_args()
form = Qt.QWidget()
layout = Qt.QVBoxLayout()
form.setLayout(layout)
if len(args) == 0:
models = ["sys/tg_test/1/double_scalar", "sys/tg_test/1/double_scalar"]
else:
models = args
for model in models:
w = TaurusValueLineEdit()
w.setModel(model)
layout.addWidget(w)
form.resize(300, 50)
form.show()
if owns_app:
sys.exit(app.exec_())
else:
return form
if __name__ == "__main__":
sys.exit(main())