# How to Write a Site Specific Formatter This guide explains how you can create a pvi formatter to generate screens for your own use cases. ## Overview The formatters role is to take a `Device` - defined either in code or in a `pvi.device.yaml` file - and turn this into a screen file that can be used by the display software. The `Device` has a list of components that specify its name, a widget type and any additional properties that can be assigned to that widget (such as a pv name). During formatting, `Component` objects of the `Device` are translated into widgets to be written to a UI file. ```{literalinclude} ../../src/pvi/device.py :pyobject: Component ``` There are various types of `Component`. The simplest is a read-only signal. ```{literalinclude} ../../src/pvi/device.py :pyobject: SignalR ``` To add structure, there is a `Group` component, which itself has a list of `Components`. ```{literalinclude} ../../src/pvi/device.py :pyobject: Group ``` To make a screen from this, we need a template UI file. This contains a blank representation of each supported widget for each of the supported file formats (bob, edl etc...). Below is an example of a `textentry` widget for a .bob file. ```{literalinclude} ../../src/pvi/_format/dls.bob :language: xml :lines: 46-54 ``` By extracting and altering the template widgets with the information provided by the components, we can create a screen file. ## Create a Formatter subclass To start, we will need to create our own formatter class. These inherit from an abstract `Formatter` class that is defined in base.py. Inside, we need to define one mandatory `format` function, which will be used to create our screen file: ```{literalinclude} ../../src/pvi/_format/base.py :pyobject: Formatter.format ``` With a formatter defined, we now can start to populate this by defining the screen dependencies. ## Define the ScreenLayout properties Each screen requires a number of layout properties that allow you to customise the size and placement of widgets. These are stored within `ScreenLayout` dataclass with the following configurable parameters: ```{literalinclude} ../../src/pvi/_format/screen.py :pyobject: ScreenLayout ``` When defining these in our formatter, we have the option of deciding which properties should be configurable inside of the formatter.yaml. Properties defined as member variables of the formatter class (and then referenced by the layout properties in the screen format function) will be available to adjust inside of the formatter.yaml. Anything else, should be considered as defaults for the formatter: ```{literalinclude} ../../src/pvi/_format/dls.py :language: python :start-after: LP DOCS REF :end-before: SW DOCS REF ``` In the example above, everything has been made adjustable from the formatter.yaml except the properties relating to groups. This is because they are more dependant on the file format used rather than the users personal preference. For clarity, the example below shows how the formatter.yaml can be used to set the layout properties. Note that these are optional as each property is defined with a default value. ```{literalinclude} ../../formatters/dls.bob.pvi.formatter.yaml ``` ## Assign a template file As previously stated, a template file provides the formatter with a base model of all of the supported widgets that it can then overwrite with component data. Currently, pvi supports templates for edl, adl and bob files, which can be referenced from the \_format directory with the filename 'dls' + the file formats suffix (eg. dls.bob). Inside of the format function, we need to provide a reference to the template file that can then be used to identify what each widget should look like. ```python3 notest template = BobTemplate(str(Path(__file__).parent / "dls.bob")) ``` ## Divide the template into widgets With a template defined, we now need to assign each part of it to a supported widget formatter. This is achieved by instantiating a WidgetFormatterFactory composed of WidgetFormatters created from the UI template. WidgetFormatters are created by searching the UI template for the given search term and a set of properties in the template to replace with widget fields. :::{note} The `WidgetFormatter`s are generic types that must be parameterised depending on the specific UI. Commonly this would use `str` for formatting text to a file directly. In this case we use `_Element`, which will serialised to text with the `lxml` library. ::: ```{literalinclude} ../../src/pvi/_format/dls.py :language: python :start-after: SW DOCS REF :end-before: MAKE_WIDGETS DOCS REF ``` ```{warning} This function uses a unique search term to locate and extract a widget from the template. As such, the search term MUST be unique to avoid extracting multiple or irrelevant widgets from the template. ``` ## Define screen and group widget functions Additionally, formatters for the title and a group on a screen must be defined along with functions to create multiple components, for example a rectangle with a label on top. In this example, we provide two arguments: The `bounds`, to set the widgets size and position, and the `title` to populate the label with. ```{literalinclude} ../../src/pvi/_format/dls.py :language: python :start-after: MAKE_WIDGETS DOCS REF :end-before: SCREEN_INI DOCS REF ``` ## Construct a ScreenFormatter These formatters can be used to define a `ScreenFormatterFactory` ```{literalinclude} ../../src/pvi/_format/dls.py :language: python :start-after: SCREEN_INI DOCS REF :end-before: SCREEN_FORMAT DOCS REF ``` which can be used to instantiate a `ScreenFormatter` by passing a set of `Components` and a title. This can then create `WidgetFormatters` for each `Component` for the specific UI type the factory was parameterised with. ```{literalinclude} ../../src/pvi/_format/dls.py :language: python :start-after: SCREEN_FORMAT DOCS REF :end-before: SCREEN_WRITE DOCS REF ``` In this case the `write_bob` function calls into the `lxml` library to format the `_Element` instances to text. For `str` formatters this would call `pathlib.Path.write_file`.