Skip to content

Converter

content_extent(elements, min_width, min_height)

Return the (width, height) a layout-less container needs to cover its child widgets.

Without an explicit geometry, Qt calls adjustSize() on such a container when it is first shown and sizes it to the children visible at that moment, so a widget hidden at startup by a Visible rule (EDM visPv) is clipped when it later appears. The extent is at least min_width x min_height and reaches the furthest child's right/bottom edge, so widgets placed past the screen's declared size stay reachable.

Parameters

elements : Iterable[ET.Element] Serialized child elements. min_width : int The smallest width to return, normally the screen's declared width. min_height : int The smallest height to return, normally the screen's declared height.

Returns

tuple[int, int] The (width, height) covering the minimum size and every child's geometry. A child whose geometry rect is missing, incomplete or non-numeric is ignored.

Source code in pydmconverter/edm/converter.py
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def content_extent(elements, min_width, min_height):
    """Return the (width, height) a layout-less container needs to cover its child widgets.

    Without an explicit geometry, Qt calls adjustSize() on such a container when it is first
    shown and sizes it to the children visible at that moment, so a widget hidden at startup by
    a Visible rule (EDM visPv) is clipped when it later appears. The extent is at least
    min_width x min_height and reaches the furthest child's right/bottom edge, so widgets placed
    past the screen's declared size stay reachable.

    Parameters
    ----------
    elements : Iterable[ET.Element]
        Serialized child <widget> elements.
    min_width : int
        The smallest width to return, normally the screen's declared width.
    min_height : int
        The smallest height to return, normally the screen's declared height.

    Returns
    -------
    tuple[int, int]
        The (width, height) covering the minimum size and every child's geometry. A child
        whose geometry rect is missing, incomplete or non-numeric is ignored.
    """
    width, height = min_width, min_height
    for element in elements:
        rect = element.find("property[@name='geometry']/rect")
        if rect is None:
            continue
        try:
            x, y, w, h = (int(float(rect.findtext(part))) for part in ("x", "y", "width", "height"))
        except (TypeError, ValueError, OverflowError):
            continue
        width = max(width, x + w)
        height = max(height, y + h)
    return width, height

convert(input_path, output_path, scrollable=False, site=None, calc_list_file=None, color_list_file=None, search_paths=None, display_roots=None)

Convert an EDM .edl file to a PyDM .ui (plus a .py when it has menu muxes).

search_paths are extra directories searched, after the file's own directory and before EDMDATAFILES, for symbol files, calc.list and the displays its links open.

display_roots are absolute directories whose display names become root-relative (see :mod:pydmconverter.edm.display_roots); PYDMCONVERTER_DISPLAY_ROOTS when omitted.

Source code in pydmconverter/edm/converter.py
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
def convert(
    input_path,
    output_path,
    scrollable=False,
    site=None,
    calc_list_file=None,
    color_list_file=None,
    search_paths=None,
    display_roots=None,
):
    """Convert an EDM .edl file to a PyDM .ui (plus a .py when it has menu muxes).

    ``search_paths`` are extra directories searched, after the file's own directory
    and before ``EDMDATAFILES``, for symbol files, calc.list and the displays its links
    open.

    ``display_roots`` are absolute directories whose display names become root-relative
    (see :mod:`pydmconverter.edm.display_roots`); ``PYDMCONVERTER_DISPLAY_ROOTS`` when
    omitted.
    """
    try:
        edm_parser = EDMFileParser(
            input_path,
            output_path,
            calc_list_file=calc_list_file,
            color_list_file=color_list_file,
            search_paths=search_paths,
        )
        logger.info(f"Successfully parsed EDM file: {input_path}")
    except FileNotFoundError:
        logger.error("File Not Found")
        return

    # PyDM's .ui has no literal ${NAME}: a shell variable reads the same as a macro.
    clashes = literal_macro_clashes(edm_parser.text)
    if clashes:
        logger.warning(
            f"{input_path}: literal ${{NAME}} text (not a macro in EDM, e.g. a shell variable) shares its name "
            f"with a macro, so PyDM substitutes the macro's value into it: {', '.join(clashes)}"
        )

    # edm_parser.ui, _, _ = replace_calc_and_loc_in_edm_content(edm_parser.ui, input_path)

    pydm_widgets, used_classes = convert_edm_to_pydm_widgets(
        edm_parser, site=site, color_list_file=color_list_file, display_roots=display_roots
    )
    logger.info(f"Converted EDM objects to {len(pydm_widgets)} PyDM widgets.")

    # Serialize each widget once and measure those same elements: to_xml() is not idempotent
    # (e.g. PyDMPushButton appends rules and may look up a PV while generating properties).
    widget_elements = [widget.to_xml() for widget in pydm_widgets]
    content_size = content_extent(widget_elements, edm_parser.ui.width, edm_parser.ui.height)

    page_header = PageHeader()
    ui_element, central_widget = page_header.create_page_header(edm_parser, scrollable, content_size=content_size)

    if isinstance(edm_parser.ui, EDMObject) and "bgColor" in edm_parser.ui.properties:
        bg_color = edm_parser.ui.properties["bgColor"]
        bg_color_prop = ET.SubElement(central_widget, "property", attrib={"name": "styleSheet"})
        style_sheet_elem = ET.SubElement(bg_color_prop, "string")
        style_sheet_elem.text = f"background-color: {bg_color};"

    central_widget.extend(widget_elements)
    start_rules_at_loc_inits(ui_element)

    customwidgets_el = build_customwidgets_element(used_classes)
    ui_element.append(customwidgets_el)

    ET.SubElement(ui_element, "resources")
    ET.SubElement(ui_element, "connections")

    resolve_window_macros(ui_element, Path(output_path).stem)
    link_menu_screens(ui_element, edm_parser.edm_search_dirs(), get_skip_widgets(site))
    ET.indent(ui_element, space="  ", level=0)

    tree = ET.ElementTree(ui_element)
    tree.write(output_path, encoding="utf-8", xml_declaration=True)

    # The menu screen embeds the .ui, so it is written last: a conversion that fails
    # leaves no menu screen pointing at a missing or stale .ui.
    if edm_parser.menu_mux_buttons:
        generate_menumux_file(
            edm_parser.menu_mux_buttons, edm_parser.output_file_path, edm_parser.menu_loc_declarations
        )