{# One pane's content. Swapped in by an HTMX GET on tab click. Client-side dirty tracking lives in programmer_batch.js — it watches .cv-value-input inputs on this table, marks their row dirty on change, and drives the Write/Cancel buttons at the top of the pane. Layout is authored by JMRI's pane XML (///…); the ``pane_layout_tree`` walker builds a nested dict of nodes and we render it recursively via the ``layout_node`` macro below. #} {% if active_slug == "roster-entry" %} {# ----- synthetic Roster Entry pane ----- #}

Roster Entry

{% if loco.comment %}{% endif %}
ID{{ loco.id }}
DCC Address{{ loco.dccAddress }}{% if loco.longAddress %} (long){% else %} (short){% endif %}
Road Name{{ loco.roadName or "" }}
Road Number{{ loco.roadNumber or "" }}
Decoder Family{{ family or "unknown" }}
Decoder Model{{ model or "unknown" }}
Owner{{ loco.owner or "" }}
Manufacturer{{ loco.mfg or "" }}
Model{{ loco.model or "" }}
Comment{{ loco.comment }}

Edit these on the roster entry page — this pane is read-only.

{% else %} {# ----- decoder pane ----- #}

{{ pane.name }}

0 changed
{% if pane.pane and pane.pane.has_dcc_address %}
DCC address {{ loco.dccAddress }} {% if loco.longAddress %}long{% else %}short{% endif %}
{% endif %} {# ----- Rich layout: walk the pane's authored column/row tree ----- #} {% macro layout_node(node) -%} {%- if node.type == "column" -%}
{% for child in node.children %}{{ layout_node(child) }}{% endfor %}
{%- elif node.type == "row" -%}
{% for child in node.children %}{{ layout_node(child) }}{% endfor %}
{%- elif node.type == "group" -%}
{% for child in node.children %}{{ layout_node(child) }}{% endfor %}
{%- elif node.type == "label" -%}
{{ node.text | safe }}
{%- elif node.type == "separator" -%}
{%- elif node.type == "display" -%} {%- set row = row_by_item.get(node.item) -%} {%- if row -%}{{ variable_widget(row) }}{%- endif -%} {%- elif node.type == "cvtable" -%} {# handled below in the raw-CV section #} {%- elif node.type == "speedtable" -%} {# handled below in the speed-table section #} {%- elif node.type == "dccaddress" -%} {# handled above in the pane header for panes with has_dcc_address #} {%- elif node.type == "fnmapping" -%} {%- if fnmap_matrix and fnmap_matrix.fn_labels and fnmap_matrix.columns -%} {{ fnmap_grid(fnmap_matrix) }} {%- endif -%} {%- elif node.type == "grid" -%} {{ grid_container(node) }} {%- endif -%} {%- endmacro %} {# ----- Grid container / cells (Swing GridBagLayout emulation) ----- JMRI's grid is a rectangular sparse mapping of ```` cells to child nodes. Each cell can hold a label, a display (checkbox / dropdown / etc.), or nested containers. We render as a CSS grid where the maximum gridx+1 gives the column count. Displays with ``format="checkbox"`` or an empty ``label=""`` render compactly (no widget-row chrome). #} {% macro grid_container(node) -%} {%- set max_x = namespace(v=0) -%} {%- for c in node.children -%} {%- if c.gridx is not none and c.gridx > max_x.v -%} {%- set max_x.v = c.gridx -%} {%- endif -%} {%- endfor -%}
{% for c in node.children -%} {{ grid_cell(c, max_x.v + 1) }} {%- endfor %}
{%- endmacro %} {% macro grid_cell(node, col_count) -%} {%- set anchor_class = { "LINE_END": "gc-end", "LINE_START": "gc-start", "CENTER": "gc-center", "FIRST_LINE_END": "gc-end", "FIRST_LINE_START": "gc-start", "LAST_LINE_END": "gc-end", "LAST_LINE_START": "gc-start", }.get(node.anchor, "") -%} {%- set style = [] -%} {%- if node.gridx is not none -%} {%- set _ = style.append("grid-column-start: " ~ (node.gridx + 1)) -%} {%- endif -%} {%- if node.gridy is not none -%} {%- set _ = style.append("grid-row-start: " ~ (node.gridy + 1)) -%} {%- endif -%} {%- if node.gridwidth == "REMAINDER" -%} {%- set _ = style.append("grid-column-end: -1") -%} {%- elif node.gridwidth and node.gridwidth != "1" -%} {%- set _ = style.append("grid-column-end: span " ~ node.gridwidth) -%} {%- endif -%}
{% for child in node.children %} {%- if child.type == "display" -%} {%- set row = row_by_item.get(child.item) -%} {%- if row -%}{{ compact_widget(row, child) }}{%- endif -%} {%- elif child.type == "label" -%} {{ child.text | safe }} {%- else -%} {{ layout_node(child) }} {%- endif -%} {% endfor %}
{%- endmacro %} {# ----- Compact widget for grid cells ----- Renders the input/select/checkbox only, no label/CV/stored/Read chrome. Format="checkbox" wraps as a bool (0=unchecked, else checked). The widget still carries data-item/data-cv/data-current so the pane's batch-read + dirty tracking pick it up. #} {% macro compact_widget(row, node) -%} {%- set is_checkbox = node.format == "checkbox" -%} {%- set label_suppressed = node.label_override is not none and node.label_override == "" -%} {%- if is_checkbox -%} {%- elif row.kind == "enum" and row.enum_choices -%} {%- if not label_suppressed and row.label -%} {{ row.label | safe }} {%- endif -%} {%- else -%} {%- if not label_suppressed and row.label -%} {{ row.label | safe }} {%- endif -%} {%- endif -%} {%- endmacro %} {% macro fnmap_grid(m) -%} {# JMRI's function-map matrix. Rows = F-keys (STOP/DRIVE/FL/F0-F28); columns = physical outputs, headed with a two-line label (Output number on top, decoder-declared name below when defined). Each cell is a checkbox that toggles a single CV bit variable and carries ``data-item``/``data-cv`` so the existing dirty tracking + pane batch Read/Write pick it up. #}
{% for col in m.columns %}{% endfor %} {% for col in m.columns %} {% endfor %} {% for fn in m.fn_labels %} {% for col in m.columns %} {%- set row = m.cells.get((fn, col.num)) -%} {% endfor %} {% endfor %}
Logical OR of Outputs
{{ col.num }}
{% if col.label_lines %} {% for line in col.label_lines %}
{{ line }}
{% endfor %} {% else %}   {% endif %}
{{ fn }} {% if row %} {% endif %}
{%- endmacro %} {% macro variable_widget(row) -%}
{{ (row.label or row.item) | safe }} CV {{ row.cv_display }}
{% if row.current_value is none %}stored: — {% else %}stored: {{ row.current_value }}{% endif %} {% if row.kind == "enum" and row.enum_choices %} {% elif row.kind == "composite" and row.composite_choices %} {% elif row.kind == "constant" %} {{ row.constant_value }} {% elif row.kind == "split_datetime" %} {# Read-only on JMRI Java too (SplitDateTimeVariableValue.setValue is a no-op "skipping set of date/time value"). The pane layer formats the stored integer against the variable's base/factor/unit/display attributes; see ``routes._format_split_datetime``. #} {{ row.datetime_display or '--' }} {% else %} {% endif %} {% if not row.write_only %} {% else %}{% endif %} {% if not row.read_only and row.kind != "constant" %} {% else %}{% endif %}
{% if row.tooltip %}
{{ row.tooltip | safe }}
{% endif %}
{%- endmacro %} {% if layout_tree %}
{{ layout_node(layout_tree) }}
{% endif %} {# ----- Speed Table pane: 28 step rows (still tabular for now) ----- #} {% if variable_rows and is_speed_table_pane %}

Speed table

{% for row in variable_rows %} {% if row.kind == "decimal" and row.item.startswith("Speed Table[") %} {% endif %} {% endfor %}
StepCVStored valueNew value
{{ row.label or row.item }} {{ row.cv_display }} {% if row.current_value is none %}— {% else %}{{ row.current_value }}{% endif %}
{% endif %} {# ----- Raw CV table (CVs pane) ----- #} {% if pane.pane and pane.pane.has_cvtable %}

Raw CV table

Every CV the decoder defines is listed. Use "Add CV" to inspect a CV that isn't in the catalog — plain (7) or indexed (16.1.275) both work.

{% for row in raw_cv_rows %} {% endfor %}
CVStoredNew value
{{ row.name }} {% if row.value is not none %} {{ row.value }} (0x{{ row.hex }}) {% else %} — {% endif %}
{% if not raw_cv_rows %}

No decoder loaded and no CVs added yet — load a catalog or use "Add CV" to inspect a specific CV.

{% endif %} {% endif %} {% if not variable_rows and not (pane.pane.has_cvtable or pane.pane.has_dcc_address or pane.pane.has_speed_table or pane.pane.has_fnmapping) %}

This pane has no rows for the loaded decoder.

{% endif %}
{% endif %}