From 58bbf5f5d958e3099cf031c62d41c156f72f17d0 Mon Sep 17 00:00:00 2001 From: Gary Yendell Date: Tue, 18 Jul 2023 10:44:23 +0000 Subject: [PATCH] Add format to Text{Read,Write} widgets - Add TextFormat enum to store text format types --- schemas/pvi.device.schema.json | 34 +++ src/pvi/_format/adl.py | 29 ++- src/pvi/_format/bob.py | 33 ++- src/pvi/_format/edl.py | 30 ++- src/pvi/device.py | 13 ++ tests/format/output/text_format.adl | 338 +++++++++++++++++++++++++++ tests/format/output/text_format.bob | 188 +++++++++++++++ tests/format/output/text_format.edl | 340 ++++++++++++++++++++++++++++ tests/test_api.py | 36 +++ 9 files changed, 1034 insertions(+), 7 deletions(-) create mode 100644 tests/format/output/text_format.adl create mode 100644 tests/format/output/text_format.bob create mode 100644 tests/format/output/text_format.edl diff --git a/schemas/pvi.device.schema.json b/schemas/pvi.device.schema.json index dabbd282..ba4f637c 100644 --- a/schemas/pvi.device.schema.json +++ b/schemas/pvi.device.schema.json @@ -184,10 +184,32 @@ "type": "integer", "description": "Number of lines to display", "default": 1 + }, + "format": { + "anyOf": [ + { + "$ref": "#/definitions/TextFormat" + }, + { + "type": "null" + } + ], + "description": "Display format", + "default": null } }, "additionalProperties": false }, + "TextFormat": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3, + 4 + ] + }, "ArrayTrace": { "type": "object", "properties": { @@ -358,6 +380,18 @@ "type": "integer", "description": "Number of lines to display", "default": 1 + }, + "format": { + "anyOf": [ + { + "$ref": "#/definitions/TextFormat" + }, + { + "type": "null" + } + ], + "description": "Display format", + "default": null } }, "additionalProperties": false diff --git a/src/pvi/_format/adl.py b/src/pvi/_format/adl.py index 6f97f0ee..02b7a65e 100644 --- a/src/pvi/_format/adl.py +++ b/src/pvi/_format/adl.py @@ -5,7 +5,15 @@ from pvi._format.utils import Bounds, split_with_sep from pvi._format.widget import UITemplate, WidgetFormatter -from pvi.device import WidgetType +from pvi.device import TextFormat, TextRead, TextWrite, WidgetType + +ADL_TEXT_FORMATS = { + TextFormat.decimal: "decimal", + TextFormat.hexadecimal: "hexadecimal", + TextFormat.engineer: "engr. notation", + TextFormat.exponential: "exponential", + TextFormat.string: "string", +} class AdlTemplate(UITemplate[str]): @@ -27,15 +35,25 @@ def set( properties["y"] = bounds.y properties["width"] = bounds.w properties["height"] = bounds.h + for item, value in properties.items(): if template.startswith('"related display"'): value = f"{value}.adl" # Must include file extension + # Only need single line pattern = re.compile(r"^(\s*%s)=.*$" % item, re.MULTILINE) if isinstance(value, str): value = f'"{value}"' + template, n = pattern.subn(r"\g<1>=" + str(value), template) assert n == 1, f"No replacements made for {item}" + + match widget: + case TextWrite(_, format) | TextRead(_, format) if ( + is_text_widget(template) and format is not None + ): + template = add_property(template, "format", ADL_TEXT_FORMATS[format]) + return template def search(self, search: str) -> str: @@ -57,3 +75,12 @@ def create_group( texts += c.format() return group_object + texts + + +def is_text_widget(text: str): + return text.startswith('"text ') + + +def add_property(text: str, property: str, value: str) -> str: + end = "\n}" + return text.replace(end, f'\n\t{property}="{value}"{end}') diff --git a/src/pvi/_format/bob.py b/src/pvi/_format/bob.py index e9dd7885..15913f72 100644 --- a/src/pvi/_format/bob.py +++ b/src/pvi/_format/bob.py @@ -12,13 +12,24 @@ ComboBox, Group, Row, + TableRead, TableWidgetType, - TableWidgetTypes, TableWrite, + TextFormat, + TextRead, + TextWrite, WidgetType, WriteWidget, ) +BOB_TEXT_FORMATS = { + TextFormat.decimal: "1", + TextFormat.hexadecimal: "4", + TextFormat.engineer: "3", + TextFormat.exponential: "2", + TextFormat.string: "6", +} + class BobTemplate(UITemplate[ElementBase]): """Extract and modify elements from a template .bob file.""" @@ -58,10 +69,17 @@ def set( if new_text: replace_text(t_copy, item, new_text) - if widget_type == "table" and isinstance(widget, TableWidgetTypes): - add_table_columns(t_copy, widget) - elif widget_type == "combo" and isinstance(widget, ComboBox): - add_combo_box_items(t_copy, widget) + # Add additional properties from widget + match widget_type, widget: + case ("combo", ComboBox() as combo_box): + add_combo_box_items(t_copy, combo_box) + case ("table", TableRead() | TableWrite() as table): + add_table_columns(t_copy, table) + case ( + ("textentry", TextWrite(_, format)) + | ("textupdate", TextRead(_, format)) + ) if format is not None: + add_format(t_copy, BOB_TEXT_FORMATS[format]) return t_copy @@ -174,6 +192,11 @@ def add_editable(element: ElementBase, editable: bool): SubElement(element, "editable").text = "true" if editable else "false" +def add_format(element: ElementBase, format: str): + if format: + SubElement(element, "format").text = format + + def replace_text(element: ElementBase, tag: str, text: str): try: # Iterate tree to find tag and replace text diff --git a/src/pvi/_format/edl.py b/src/pvi/_format/edl.py index 4003db6a..2bad2108 100644 --- a/src/pvi/_format/edl.py +++ b/src/pvi/_format/edl.py @@ -5,7 +5,15 @@ from pvi._format.utils import Bounds, split_with_sep from pvi._format.widget import UITemplate, WidgetFormatter -from pvi.device import WidgetType +from pvi.device import TextFormat, TextRead, TextWrite, WidgetType + +EDL_TEXT_FORMATS = { + TextFormat.decimal: "decimal", + TextFormat.hexadecimal: "hex", + TextFormat.engineer: "engineer", + TextFormat.exponential: "exp", + TextFormat.string: "default", +} class EdlTemplate(UITemplate[str]): @@ -27,6 +35,7 @@ def set( for item, value in properties.items(): if item == "displayFileName": value = f"0 {value}" # These are items in an array but we only use one + multiline = re.compile(r"^%s {[^}]*}$" % item, re.MULTILINE | re.DOTALL) if multiline.search(template): pattern = multiline @@ -37,8 +46,18 @@ def set( pattern = re.compile(r"^%s .*$" % item, re.MULTILINE) if isinstance(value, str): value = f'"{value}"' + template, n = pattern.subn(f"{item} {value}", template) assert n == 1, f"No replacements made for {item}" + + match widget: + case TextWrite(_, format) | TextRead(_, format) if ( + is_text_widget(template) and format is not None + ): + template = add_property( + template, "displayMode", EDL_TEXT_FORMATS[format] + ) + return template def search(self, search: str) -> str: @@ -60,3 +79,12 @@ def create_group( texts += c.format() return group_object + texts + + +def is_text_widget(text: str): + return text.startswith("\n# (Text") + + +def add_property(text: str, property: str, value: str) -> str: + end = "endObjectProperties\n" + return text.replace(end, f'{property} "{value}"\n{end}') diff --git a/src/pvi/device.py b/src/pvi/device.py index 09e5fe2e..8224f392 100644 --- a/src/pvi/device.py +++ b/src/pvi/device.py @@ -3,6 +3,7 @@ import json import re from dataclasses import dataclass, field, fields +from enum import Enum from pathlib import Path from typing import ( Annotated, @@ -49,6 +50,16 @@ def to_snake_case(pascal_s: str) -> str: return PASCAL_CASE_REGEX.sub(lambda m: "_" + m.group().lower(), pascal_s)[1:] +class TextFormat(Enum): + """Format to use for display of Text{Read,Write} widgets on a UI""" + + decimal = 0 + hexadecimal = 1 + engineer = 2 + exponential = 3 + string = 4 + + @as_discriminated_union @dataclass class ReadWidget: @@ -75,6 +86,7 @@ class TextRead(ReadWidget): """Text view of any PV""" lines: Annotated[int, desc("Number of lines to display")] = 1 + format: Annotated[Optional[TextFormat], desc("Display format")] = None @dataclass @@ -141,6 +153,7 @@ class TextWrite(WriteWidget): """Text control of any PV""" lines: Annotated[int, desc("Number of lines to display")] = 1 + format: Annotated[Optional[TextFormat], desc("Display format")] = None @dataclass diff --git a/tests/format/output/text_format.adl b/tests/format/output/text_format.adl new file mode 100644 index 00000000..09eab20e --- /dev/null +++ b/tests/format/output/text_format.adl @@ -0,0 +1,338 @@ + +file { + name="/scratch/development/pvi/src/pvi/_format/aps.adl" + version=030107 +} +display { + object { + x=0 + y=0 + width=325 + height=155 + } + clr=14 + bclr=4 + cmap="" + gridSpacing=5 + gridOn=1 + snapToGrid=1 +} +"color map" { + ncolors=65 + colors { + ffffff, + ececec, + dadada, + c8c8c8, + bbbbbb, + aeaeae, + 9e9e9e, + 919191, + 858585, + 787878, + 696969, + 5a5a5a, + 464646, + 2d2d2d, + 000000, + 00d800, + 1ebb00, + 339900, + 2d7f00, + 216c00, + fd0000, + de1309, + be190b, + a01207, + 820400, + 5893ff, + 597ee1, + 4b6ec7, + 3a5eab, + 27548d, + fbf34a, + f9da3c, + eeb62b, + e19015, + cd6100, + ffb0ff, + d67fe2, + ae4ebc, + 8b1a96, + 610a75, + a4aaff, + 8793e2, + 6a73c1, + 4d52a4, + 343386, + c7bb6d, + b79d5c, + a47e3c, + 7d5627, + 58340f, + 99ffff, + 73dfff, + 4ea5f9, + 2a63e4, + 0a00b8, + ebf1b5, + d4db9d, + bbc187, + a6a462, + 8b8239, + 73ff6b, + 52da3b, + 3cb420, + 289315, + 1a7309, + } +} +rectangle { + object { + x=0 + y=0 + width=325 + height=25 + } + "basic attribute" { + clr=2 + } +} +text { + object { + x=0 + y=0 + width=325 + height=25 + } + "basic attribute" { + clr=54 + } + textix="Simple Device - $(P)" + align="horiz. centered" +} +text { + object { + x=5 + y=30 + width=205 + height=20 + } + "basic attribute" { + clr=14 + } + textix="Decimal" + align="horiz. right" +} +"text entry" { + object { + x=215 + y=30 + width=50 + height=20 + } + control { + chan="$(P)Decimal" + clr=14 + bclr=51 + } + limits { + } + format="decimal" +} +"text update" { + object { + x=270 + y=30 + width=50 + height=20 + } + monitor { + chan="$(P)Decimal_RBV" + clr=54 + bclr=4 + } + limits { + } + format="decimal" +} +text { + object { + x=5 + y=55 + width=205 + height=20 + } + "basic attribute" { + clr=14 + } + textix="Hexadecimal" + align="horiz. right" +} +"text entry" { + object { + x=215 + y=55 + width=50 + height=20 + } + control { + chan="$(P)Hexadecimal" + clr=14 + bclr=51 + } + limits { + } + format="hexadecimal" +} +"text update" { + object { + x=270 + y=55 + width=50 + height=20 + } + monitor { + chan="$(P)Hexadecimal_RBV" + clr=54 + bclr=4 + } + limits { + } + format="hexadecimal" +} +text { + object { + x=5 + y=80 + width=205 + height=20 + } + "basic attribute" { + clr=14 + } + textix="Engineer" + align="horiz. right" +} +"text entry" { + object { + x=215 + y=80 + width=50 + height=20 + } + control { + chan="$(P)Engineer" + clr=14 + bclr=51 + } + limits { + } + format="engr. notation" +} +"text update" { + object { + x=270 + y=80 + width=50 + height=20 + } + monitor { + chan="$(P)Engineer_RBV" + clr=54 + bclr=4 + } + limits { + } + format="engr. notation" +} +text { + object { + x=5 + y=105 + width=205 + height=20 + } + "basic attribute" { + clr=14 + } + textix="Exponential" + align="horiz. right" +} +"text entry" { + object { + x=215 + y=105 + width=50 + height=20 + } + control { + chan="$(P)Exponential" + clr=14 + bclr=51 + } + limits { + } + format="exponential" +} +"text update" { + object { + x=270 + y=105 + width=50 + height=20 + } + monitor { + chan="$(P)Exponential_RBV" + clr=54 + bclr=4 + } + limits { + } + format="exponential" +} +text { + object { + x=5 + y=130 + width=205 + height=20 + } + "basic attribute" { + clr=14 + } + textix="String" + align="horiz. right" +} +"text entry" { + object { + x=215 + y=130 + width=50 + height=20 + } + control { + chan="$(P)String" + clr=14 + bclr=51 + } + limits { + } + format="string" +} +"text update" { + object { + x=270 + y=130 + width=50 + height=20 + } + monitor { + chan="$(P)String_RBV" + clr=54 + bclr=4 + } + limits { + } + format="string" +} diff --git a/tests/format/output/text_format.bob b/tests/format/output/text_format.bob new file mode 100644 index 00000000..e3e673fc --- /dev/null +++ b/tests/format/output/text_format.bob @@ -0,0 +1,188 @@ + + Display + 0 + 0 + 274 + 150 + 4 + 4 + + Title + TITLE + Simple Device - $(P) + 0 + 0 + 274 + 26 + + + + + + + + + true + 1 + + + Label + Decimal + 22 + 30 + 120 + 20 + + + TextEntry + $(P)Decimal + 146 + 30 + 60 + 20 + 1 + 1 + + + TextUpdate + $(P)Decimal_RBV + 210 + 30 + 60 + 20 + + + + + 1 + 1 + + + Label + Hexadecimal + 22 + 54 + 120 + 20 + + + TextEntry + $(P)Hexadecimal + 146 + 54 + 60 + 20 + 1 + 4 + + + TextUpdate + $(P)Hexadecimal_RBV + 210 + 54 + 60 + 20 + + + + + 1 + 4 + + + Label + Engineer + 22 + 78 + 120 + 20 + + + TextEntry + $(P)Engineer + 146 + 78 + 60 + 20 + 1 + 3 + + + TextUpdate + $(P)Engineer_RBV + 210 + 78 + 60 + 20 + + + + + 1 + 3 + + + Label + Exponential + 22 + 102 + 120 + 20 + + + TextEntry + $(P)Exponential + 146 + 102 + 60 + 20 + 1 + 2 + + + TextUpdate + $(P)Exponential_RBV + 210 + 102 + 60 + 20 + + + + + 1 + 2 + + + Label + String + 22 + 126 + 120 + 20 + + + TextEntry + $(P)String + 146 + 126 + 60 + 20 + 1 + 6 + + + TextUpdate + $(P)String_RBV + 210 + 126 + 60 + 20 + + + + + 1 + 6 + + diff --git a/tests/format/output/text_format.edl b/tests/format/output/text_format.edl new file mode 100644 index 00000000..71e00b24 --- /dev/null +++ b/tests/format/output/text_format.edl @@ -0,0 +1,340 @@ +4 0 1 +beginScreenProperties +major 4 +minor 0 +release 1 +x 0 +y 0 +w 260 +h 155 +font "arial-bold-r-12.0" +ctlFont "arial-bold-r-12.0" +btnFont "arial-bold-r-12.0" +fgColor index 14 +bgColor index 3 +textColor index 14 +ctlFgColor1 index 16 +ctlFgColor2 index 25 +ctlBgColor1 index 10 +ctlBgColor2 index 3 +topShadowColor index 1 +botShadowColor index 11 +title "pilatusDetector features - $(P)$(R)" +showGrid +snapToGrid +gridSize 5 +endScreenProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 0 +y 0 +w 260 +h 25 +font "arial-bold-r-16.0" +fontAlign "center" +fgColor index 14 +bgColor index 48 +value { + "Simple Device - $(P)" +} +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 10 +y 30 +w 115 +h 20 +font "arial-bold-r-10.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Decimal" +} +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 130 +y 30 +w 60 +h 20 +controlPv "$(P)Decimal" +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "decimal" +endObjectProperties + +# (Textupdate) +object TextupdateClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 195 +y 30 +w 60 +h 20 +controlPv "$(P)Decimal_RBV" +fgColor index 16 +fgAlarm +bgColor index 10 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "decimal" +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 10 +y 55 +w 115 +h 20 +font "arial-bold-r-10.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Hexadecimal" +} +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 130 +y 55 +w 60 +h 20 +controlPv "$(P)Hexadecimal" +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "hex" +endObjectProperties + +# (Textupdate) +object TextupdateClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 195 +y 55 +w 60 +h 20 +controlPv "$(P)Hexadecimal_RBV" +fgColor index 16 +fgAlarm +bgColor index 10 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "hex" +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 10 +y 80 +w 115 +h 20 +font "arial-bold-r-10.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Engineer" +} +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 130 +y 80 +w 60 +h 20 +controlPv "$(P)Engineer" +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "engineer" +endObjectProperties + +# (Textupdate) +object TextupdateClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 195 +y 80 +w 60 +h 20 +controlPv "$(P)Engineer_RBV" +fgColor index 16 +fgAlarm +bgColor index 10 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "engineer" +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 10 +y 105 +w 115 +h 20 +font "arial-bold-r-10.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "Exponential" +} +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 130 +y 105 +w 60 +h 20 +controlPv "$(P)Exponential" +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "exp" +endObjectProperties + +# (Textupdate) +object TextupdateClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 195 +y 105 +w 60 +h 20 +controlPv "$(P)Exponential_RBV" +fgColor index 16 +fgAlarm +bgColor index 10 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "exp" +endObjectProperties + +# (Static Text) +object activeXTextClass +beginObjectProperties +major 4 +minor 1 +release 1 +x 10 +y 130 +w 115 +h 20 +font "arial-bold-r-10.0" +fgColor index 14 +bgColor index 3 +useDisplayBg +value { + "String" +} +endObjectProperties + +# (Textentry) +object TextentryClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 130 +y 130 +w 60 +h 20 +controlPv "$(P)String" +fgColor index 25 +fgAlarm +bgColor index 3 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "default" +endObjectProperties + +# (Textupdate) +object TextupdateClass +beginObjectProperties +major 10 +minor 0 +release 0 +x 195 +y 130 +w 60 +h 20 +controlPv "$(P)String_RBV" +fgColor index 16 +fgAlarm +bgColor index 10 +fill +font "arial-bold-r-12.0" +fontAlign "center" +displayMode "default" +endObjectProperties diff --git a/tests/test_api.py b/tests/test_api.py index 86be171d..7b7dadb1 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,5 +1,7 @@ from pathlib import Path +import pytest + from pvi._format.base import Formatter from pvi._yaml_utils import deserialize_yaml from pvi.device import ( @@ -12,6 +14,7 @@ SignalW, TableRead, TableWrite, + TextFormat, TextRead, TextWrite, ) @@ -19,6 +22,39 @@ HERE = Path(__file__).parent +@pytest.mark.parametrize( + "filename,formatter", + [ + ("text_format.adl", "aps.adl.pvi.formatter.yaml"), + ("text_format.edl", "dls.edl.pvi.formatter.yaml"), + ("text_format.bob", "dls.bob.pvi.formatter.yaml"), + ], +) +def test_text_format(tmp_path, helper, filename, formatter): + formatter_yaml = HERE / "format" / "input" / formatter + formatter = deserialize_yaml(Formatter, formatter_yaml) + + components = [] + for format in TextFormat: + components.append( + SignalRW( + format.name.title(), + pv=format.name.title(), + widget=TextWrite(format=format), + read_pv=f"{format.name.title()}_RBV", + read_widget=TextRead(format=format), + ) + ) + + device = Device("Simple Device", children=components) + + expected_bob = HERE / "format" / "output" / filename + output_bob = tmp_path / filename + formatter.format(device, "$(P)", output_bob) + + helper.assert_output_matches(expected_bob, output_bob) + + def test_button(tmp_path, helper): formatter_yaml = HERE / "format" / "input" / "dls.bob.pvi.formatter.yaml" formatter = deserialize_yaml(Formatter, formatter_yaml)