from __future__ import annotations
#-----------------------------------------------------------------------------
# Company : SLAC National Accelerator Laboratory
#-----------------------------------------------------------------------------
# Description:
#
#-----------------------------------------------------------------------------
# This file is part of the rogue software platform. It is subject to
# the license terms in the LICENSE.txt file found in the top-level directory
# of this distribution and at:
# https://confluence.slac.stanford.edu/display/ppareg/LICENSE.html.
# No part of the rogue software platform, including this file, may be
# copied, modified, propagated, or distributed except according to the terms
# contained in the LICENSE.txt file.
#-----------------------------------------------------------------------------
import locale
import numpy as np
import ast
import logging
from pydm.widgets import PyDMLineEdit, PyDMLabel
from qtpy.QtCore import Property, Qt
from qtpy.QtGui import QFocusEvent
from qtpy.QtWidgets import QHBoxLayout, QWidget
from pydm.widgets.base import refresh_style, str_types
from pydm.widgets.display_format import DisplayFormat, parse_value_for_display
logger = logging.getLogger(__name__)
[docs]
class PyRogueLineEdit(PyDMLineEdit):
"""PyDM line edit with Rogue-specific display and unit handling.
Parameters
----------
parent : QWidget
Parent Qt widget.
init_channel : str | None, optional
Initial Rogue channel address.
show_units : bool, optional
If ``True``, display channel units in a dedicated unit label.
read_only : bool, optional
Unused compatibility parameter retained for legacy call sites.
"""
def __init__(
self,
parent: QWidget,
init_channel: str | None = None,
show_units: bool = True,
read_only: bool = False,
) -> None:
super().__init__(parent, init_channel=init_channel)
self._show_units = show_units
self.textEdited.connect(self.text_edited)
self._dirty = False
self.setStyleSheet("*[dirty='true']\
{background-color: orange;}\
*[readOnly='true'] { color: black; background-color: WhiteSmoke;}")
self._unitWidget = PyDMLabel(parent)
self._unitWidget.setStyleSheet("QLabel { color : DimGrey; }")
self._unitWidget.setAlignment(Qt.AlignRight)
self._unitWidget.setText("")
hbox = QHBoxLayout()
hbox.addStretch(1)
hbox.addWidget(self._unitWidget)
hbox.setContentsMargins(0, 0, 0, 0)
self.setLayout(hbox)
[docs]
def text_edited(self) -> None:
"""Mark widget dirty when user edits text."""
self._dirty = True
refresh_style(self)
[docs]
def focusOutEvent(self, event: QFocusEvent) -> None:
"""Clear dirty state when focus leaves the widget."""
self._dirty = False
super(PyRogueLineEdit, self).focusOutEvent(event)
refresh_style(self)
[docs]
def check_enable_state(self) -> None:
"""Update read-only state based on channel write access."""
status = self._write_access and self._connected
self.setReadOnly(not status)
refresh_style(self)
@Property(bool)
def dirty(self) -> bool:
"""Whether user-edited text is pending commit."""
return self._dirty
[docs]
def unit_changed(self, new_unit: str) -> None:
"""Update displayed units text."""
super().unit_changed(new_unit)
if self._show_units:
self._unitWidget.setText(new_unit)
[docs]
def send_value(self) -> None:
"""
Emit a :attr:`send_value_signal` to update channel value.
The text is cleaned of all units, user-formatting and scale values
before being sent back to the channel. This function is attached the
ReturnPressed signal of the PyDMLineEdit
"""
send_value = str(self.text())
try:
if self.channeltype not in [str, np.ndarray, bool]:
scale = self._scale
if scale is None or scale == 0:
scale = 1.0
if self._display_format_type in [DisplayFormat.Default, DisplayFormat.String]:
if self.channeltype == float:
num_value = locale.atof(send_value)
else:
num_value = self.channeltype(send_value)
scale = self.channeltype(scale)
elif self._display_format_type == DisplayFormat.Hex:
num_value = int(send_value, 16)
elif self._display_format_type == DisplayFormat.Binary:
num_value = int(send_value, 2)
elif self._display_format_type in [DisplayFormat.Exponential, DisplayFormat.Decimal]:
num_value = locale.atof(send_value)
num_value = num_value / scale
self.send_value_signal[self.channeltype].emit(num_value)
elif self.channeltype == np.ndarray:
# Arrays will be in the [1.2 3.4 22.214] format
if self._display_format_type == DisplayFormat.String:
self.send_value_signal[str].emit(send_value)
else:
arr_value = list(
filter(None, ast.literal_eval(str(send_value.replace("[", "").replace("]", "").split()))))
arr_value = np.array(arr_value, dtype=self.subtype)
self.send_value_signal[np.ndarray].emit(arr_value)
elif self.channeltype == bool:
try:
val = bool(PyDMLineEdit.strtobool(send_value))
self.send_value_signal[bool].emit(val)
# might want to add error to application screen
except ValueError:
logger.error("Not a valid boolean: %r", send_value)
else:
# Channel Type is String
# Lets just send what we have after all
self.send_value_signal[str].emit(send_value)
except ValueError:
logger.exception("Error trying to set data '{0}' with type '{1}' and format '{2}' at widget '{3}'."
.format(self.text(), self.channeltype, self._display_format_type, self.objectName()))
self.clearFocus()
self.set_display()
[docs]
def set_display(self) -> None:
"""
Set the text display of the PyDMLineEdit.
The original value given by the PV is converted to a text entry based
on the current settings for scale value, precision, a user-defined
format, and the current units. If the user is currently entering a
value in the PyDMLineEdit the text will not be changed.
"""
if self.value is None:
return
if self.hasFocus():
return
new_value = self.value
if self._display_format_type in [DisplayFormat.Default,
DisplayFormat.Decimal,
DisplayFormat.Exponential,
DisplayFormat.Hex,
DisplayFormat.Binary]:
if self.channeltype not in (str, np.ndarray):
try:
new_value *= self.channeltype(self._scale)
except TypeError:
logger.error("Cannot convert the value '{0}', for channel '{1}', to type '{2}'. ".format(
self._scale, self._channel, self.channeltype))
new_value = parse_value_for_display(value=new_value, precision=self.precision,
display_format_type=self._display_format_type,
string_encoding=self._string_encoding,
widget=self)
if type(new_value) in str_types:
self._display = new_value
else:
self._display = str(new_value)
if self._display_format_type == DisplayFormat.Default:
if isinstance(new_value, (int, float)):
self._display = str(self.format_string.format(new_value))
self.setText(self._display)
return
self.setText(self._display)