From 349a3041fa496e839e168033b73d2b9bc9440007 Mon Sep 17 00:00:00 2001 From: Eva Lott Date: Mon, 29 Jan 2024 15:38:03 +0000 Subject: [PATCH] Added an image widget Also added progress bar options, and made it so that if a widget has a height or width field then it will stretch the group. In a future PR we should make it so that height and width are set on the widgets. --- schemas/pvi.device.schema.json | 80 ++++++++++++++++++++++++++++ src/pvi/_format/adl.py | 4 +- src/pvi/_format/bob.py | 70 +++++++++++++++++++++++- src/pvi/_format/dls.bob | 3 +- src/pvi/_format/edl.py | 2 +- src/pvi/_format/screen.py | 5 ++ src/pvi/device.py | 34 +++++++++++- tests/format/output/mixedWidgets.bob | 3 ++ 8 files changed, 194 insertions(+), 7 deletions(-) diff --git a/schemas/pvi.device.schema.json b/schemas/pvi.device.schema.json index b3057b03..1f9aff6e 100644 --- a/schemas/pvi.device.schema.json +++ b/schemas/pvi.device.schema.json @@ -113,6 +113,11 @@ "title": "CheckBox", "type": "object" }, + "ColorMode": { + "const": 3, + "description": "The `color_mode` to use, not to be confused with the `color_map`.\nOther modes have strange results right now.", + "title": "ColorMode" + }, "ComboBox": { "additionalProperties": false, "description": "Selection of an enum PV", @@ -300,6 +305,61 @@ "const": "ImageRead", "default": "ImageRead", "title": "Type" + }, + "height": { + "default": 300, + "description": "Height of widget", + "exclusiveMinimum": 0, + "title": "Height", + "type": "integer" + }, + "width": { + "default": 300, + "description": "Width of widget", + "exclusiveMinimum": 0, + "title": "Width", + "type": "integer" + }, + "color_mode": { + "allOf": [ + { + "$ref": "#/$defs/ColorMode" + } + ], + "default": 3, + "description": "Color map to use for the image" + }, + "use_unsigned_data": { + "default": true, + "description": "If true then the image uses unsigned pixel values", + "title": "Use Unsigned Data", + "type": "boolean" + }, + "data_height": { + "default": 300, + "description": "Number of height pixels in the image", + "exclusiveMinimum": 0, + "title": "Data Height", + "type": "integer" + }, + "data_width": { + "default": 300, + "description": "Number of width pixels in the image", + "exclusiveMinimum": 0, + "title": "Data Width", + "type": "integer" + }, + "show_axes": { + "default": true, + "description": "Show axes on the image", + "title": "Show Axes", + "type": "boolean" + }, + "show_colorbar": { + "default": false, + "description": "Show colorbar on the image", + "title": "Show Colorbar", + "type": "boolean" } }, "title": "ImageRead", @@ -338,6 +398,26 @@ "const": "ProgressBar", "default": "ProgressBar", "title": "Type" + }, + "use_pv_limits": { + "default": false, + "description": "Use PV limits, if True then maximum and minumum are ignored", + "title": "Use Pv Limits", + "type": "boolean" + }, + "minimum": { + "default": 0.0, + "description": "Lower limit of progress bar", + "exclusiveMinimum": 0.0, + "title": "Minimum", + "type": "number" + }, + "maximum": { + "default": 1.0, + "description": "Upper limit of progress bar", + "exclusiveMinimum": 0.0, + "title": "Maximum", + "type": "number" } }, "title": "ProgressBar", diff --git a/src/pvi/_format/adl.py b/src/pvi/_format/adl.py index ff571e0e..118f517a 100644 --- a/src/pvi/_format/adl.py +++ b/src/pvi/_format/adl.py @@ -54,7 +54,9 @@ def set( case TextWrite(format=format) | TextRead(format=format) if ( is_text_widget(template) and format is not None ): - template = add_property(template, "format", ADL_TEXT_FORMATS[format]) + template = add_property( + template, "format", ADL_TEXT_FORMATS[TextFormat(format)] + ) return template diff --git a/src/pvi/_format/bob.py b/src/pvi/_format/bob.py index ec668323..a9741923 100644 --- a/src/pvi/_format/bob.py +++ b/src/pvi/_format/bob.py @@ -8,7 +8,10 @@ from pvi.device import ( LED, BitField, + ColorMode, ComboBox, + ImageRead, + ProgressBar, TableRead, TableWidgetType, TableWrite, @@ -50,9 +53,15 @@ def set( properties["y"] = bounds.y if isinstance(widget, BitField): properties["width"] = widget.number_of_bits * 20 + bounds.w = properties["width"] + elif isinstance(widget, ImageRead): + properties["width"] = widget.width + properties["height"] = widget.height + bounds.w = widget.width + bounds.h = widget.height else: - properties["height"] = bounds.h properties["width"] = bounds.w + properties["height"] = bounds.h widget_type = template.attrib.get("type", "") @@ -86,9 +95,24 @@ def set( ("textentry", TextWrite(format=format)) | ("textupdate", TextRead(format=format)) ) if format is not None: - add_format(t_copy, BOB_TEXT_FORMATS[format]) + add_format(t_copy, BOB_TEXT_FORMATS[TextFormat(format)]) case ("byte_monitor", BitField()): add_byte_number_of_bits(t_copy, widget.number_of_bits) + case ("image", ImageRead()): + add_data_height_and_width(t_copy, widget.data_height, widget.data_width) + add_color_mode_to_image(t_copy, widget.color_mode) + add_unsigned_to_image(t_copy, widget.use_unsigned_data) + add_axis_and_colorbar( + t_copy, + widget.show_axes, + widget.show_colorbar, + widget.data_height, + widget.data_width, + ) + case ("progressbar", ProgressBar()): + add_minimum_and_maximum_progressbar( + t_copy, widget.use_pv_limits, widget.minimum, widget.maximum + ) return t_copy @@ -217,6 +241,48 @@ def add_byte_number_of_bits(element: ElementBase, number_of_bits: int): SubElement(element, "numBits").text = str(number_of_bits) +def add_data_height_and_width(element: ElementBase, data_height: int, data_width: int): + SubElement(element, "data_height").text = str(data_height) + SubElement(element, "data_width").text = str(data_width) + + +def add_color_mode_to_image(element: ElementBase, color_mode: ColorMode): + SubElement(element, "color_mode").text = str(color_mode.value) + + +def add_unsigned_to_image(element: ElementBase, use_unsigned_data: bool): + SubElement(element, "unsigned").text = "true" if use_unsigned_data else "false" + + +def add_axis_and_colorbar( + element: ElementBase, + show_axes: bool, + show_colorbar: bool, + data_height: int, + data_width: int, +): + SubElement(SubElement(element, "color_bar"), "visible").text = ( + "true" if show_colorbar else "false" + ) + x_axis = SubElement(element, "x_axis") + SubElement(x_axis, "visible").text = "true" if show_axes else "false" + SubElement(x_axis, "minimum").text = "0.0" + SubElement(x_axis, "maximum").text = str(float(data_width)) + + y_axis = SubElement(element, "y_axis") + SubElement(y_axis, "visible").text = "true" if show_axes else "false" + SubElement(y_axis, "minimum").text = "0.0" + SubElement(y_axis, "maximum").text = str(float(data_height)) + + +def add_minimum_and_maximum_progressbar( + element: ElementBase, use_pv_limits: bool, minimum: float, maximum: float +): + SubElement(element, "limits_from_pv").text = "true" if use_pv_limits else "false" + SubElement(element, "minimum").text = str(minimum) + SubElement(element, "maximum").text = str(maximum) + + def find_element(root_element: ElementBase, tag: str, index: int = 0) -> ElementBase: """Iterate tree to find tag and replace text. diff --git a/src/pvi/_format/dls.bob b/src/pvi/_format/dls.bob index 3e6de57b..e83cfda4 100644 --- a/src/pvi/_format/dls.bob +++ b/src/pvi/_format/dls.bob @@ -191,7 +191,7 @@ Table - Table + pva://Table 671 670 200 @@ -224,5 +224,6 @@ 566 48 300 + 301 diff --git a/src/pvi/_format/edl.py b/src/pvi/_format/edl.py index 870506c1..264e52b9 100644 --- a/src/pvi/_format/edl.py +++ b/src/pvi/_format/edl.py @@ -56,7 +56,7 @@ def set( is_text_widget(template) and format is not None ): template = add_property( - template, "displayMode", EDL_TEXT_FORMATS[format] + template, "displayMode", EDL_TEXT_FORMATS[TextFormat(format)] ) return template diff --git a/src/pvi/_format/screen.py b/src/pvi/_format/screen.py index 1c45260f..7a24076f 100644 --- a/src/pvi/_format/screen.py +++ b/src/pvi/_format/screen.py @@ -472,6 +472,11 @@ def generate_component_formatters( add_label = False # Do not add row labels for Tables component_bounds.w = 100 * len(c.widget.widgets) component_bounds.h *= 10 # TODO: How do we know the number of rows? + elif hasattr(c, "widget"): + if hasattr(c.widget, "height"): + component_bounds.h = c.widget.height + self.layout.spacing + if hasattr(c.widget, "width"): + component_bounds.w = c.widget.width + self.layout.spacing if add_label: # Insert label and reduce width for widget diff --git a/src/pvi/device.py b/src/pvi/device.py index 345e83be..41209411 100644 --- a/src/pvi/device.py +++ b/src/pvi/device.py @@ -2,7 +2,7 @@ import json import re -from enum import IntEnum +from enum import Enum from pathlib import Path from typing import ( Annotated, @@ -60,7 +60,7 @@ def enforce_pascal_case(s: str) -> str: return s[0].upper() + s[1:] -class TextFormat(IntEnum): +class TextFormat(Enum): """Format to use for display of Text{Read,Write} widgets on a UI""" decimal = 0 @@ -97,6 +97,12 @@ class ProgressBar(ReadWidget): """Progress bar from lower to upper limit of a float PV""" type: Literal["ProgressBar"] = "ProgressBar" + use_pv_limits: bool = Field( + default=False, + description="Use PV limits, if True then maximum and minumum are ignored", + ) + minimum: float = Field(default=0.0, description="Lower limit of progress bar", gt=0) + maximum: float = Field(default=1.0, description="Upper limit of progress bar", gt=0) class TextRead(ReadWidget): @@ -136,11 +142,35 @@ class TableRead(ReadWidget): ) +class ColorMode(Enum): + """The `color_mode` to use, not to be confused with the `color_map`. + Other modes have strange results right now.""" + + RBG = 3 + + class ImageRead(ReadWidget): """2D Image view of an NTNDArray""" type: Literal["ImageRead"] = "ImageRead" + height: int = Field(default=300, description="Height of widget", gt=0) + width: int = Field(default=300, description="Width of widget", gt=0) + color_mode: ColorMode = Field( + default=ColorMode.RBG, description="Color map to use for the image" + ) + use_unsigned_data: bool = Field( + default=True, description="If true then the image uses unsigned pixel values" + ) + data_height: int = Field( + default=300, description="Number of height pixels in the image", gt=0 + ) + data_width: int = Field( + default=300, description="Number of width pixels in the image", gt=0 + ) + show_axes: bool = Field(default=True, description="Show axes on the image") + show_colorbar: bool = Field(default=False, description="Show colorbar on the image") + class WriteWidget(BaseSettings): """Widget that controls a PV""" diff --git a/tests/format/output/mixedWidgets.bob b/tests/format/output/mixedWidgets.bob index f6c611ca..25340fb1 100644 --- a/tests/format/output/mixedWidgets.bob +++ b/tests/format/output/mixedWidgets.bob @@ -1039,6 +1039,9 @@ 144 124 20 + false + 0.0 + 1.0