Skip to content

Commit 391abe4

Browse files
committed
Autogenerate index.bob from beam pipe and vac pipe
Derive symbol SVG and execute open new tab action on mouse click Scale dynamically with number of components
1 parent 9735604 commit 391abe4

3 files changed

Lines changed: 159 additions & 148 deletions

File tree

src/techui_builder/builder.py

Lines changed: 65 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -178,55 +178,78 @@ def _validate_screen(self, screen_name: str):
178178
widget_group_name = widget_group.get_element_value("name")
179179
self.validator.validate_bob(screen_name, widget_group_name, widgets)
180180

181-
def create_screens(self):
182-
"""Create the screens for each component in techui.yaml"""
183-
if len(self.entities) == 0:
184-
logger_.critical(
185-
"No ioc entities found. This [italic]normally[/italic]"
186-
" suggests an issue with finding ixx-services."
187-
)
188-
exit()
189-
190-
# Loop over every component defined in techui.yaml and locate
191-
# any extras defined
181+
def _create_component_screens(self):
182+
"""Create screens for components defined in techui.yaml"""
192183
for component_name, component in self.conf.components.items():
193184
screen_entities: list[Entity] = []
194185

195-
# ONLY IF there is a matching component and entity, generate a screen
196-
if component.prefix in self.entities.keys():
197-
# Populate child labels for any entities
198-
# with the same prefix as the component
199-
for entity in self.entities[component.prefix]:
200-
entity.child_labels = component.child_labels
201-
202-
screen_entities.extend(self.entities[component.prefix])
203-
204-
if component.extras is not None:
205-
# If component has any extras, add them to the entries to generate
206-
for extra_p in component.extras:
207-
if extra_p not in self.entities.keys():
208-
logger_.error(
209-
f"Extra prefix {extra_p} for {component_name} does not"
210-
" exist."
211-
)
212-
continue
213-
screen_entities.extend(self.entities[extra_p])
186+
if component.prefix not in self.entities.keys():
187+
logger_.warning(
188+
f"{self.techui.name}: The prefix [bold]{component.prefix}[/bold] "
189+
f"set in the component [bold]{component_name}[/bold] does not match"
190+
" any P field in the ioc.yaml files in services"
191+
)
192+
continue
193+
194+
for entity in self.entities[component.prefix]:
195+
entity.child_labels = component.child_labels
214196

215-
# This is used by both generate and validate,
216-
# so called beforehand for tidyness
217-
self.generator.build_widgets(component_name, screen_entities)
218-
self.generator.build_groups(component_name, self.conf.components)
197+
screen_entities.extend(self.entities[component.prefix])
219198

220-
screens_to_validate = list(self.validator.validate.keys())
199+
if component.extras is not None:
200+
for extra_p in component.extras:
201+
if extra_p not in self.entities.keys():
202+
logger_.error(
203+
f"Extra prefix {extra_p} for {component_name} does not"
204+
" exist."
205+
)
206+
continue
207+
screen_entities.extend(self.entities[extra_p])
221208

222-
if component_name in screens_to_validate:
223-
self._validate_screen(component_name)
224-
else:
225-
self._generate_screen(component_name)
209+
self.generator.build_widgets(component_name, screen_entities)
210+
self.generator.build_groups(component_name, self.conf.components)
226211

212+
if component_name in list(self.validator.validate.keys()):
213+
self._validate_screen(component_name)
227214
else:
215+
self._generate_screen(component_name)
216+
217+
def _create_pipe_screens(self):
218+
"""Create screens for beam_pipe and vacuum_pipe components"""
219+
all_pipe_components = {
220+
**(self.conf.beam_pipe or {}),
221+
**(self.conf.vacuum_pipe or {}),
222+
}
223+
224+
for component_name, pipe_component in all_pipe_components.items():
225+
pv_root = pipe_component.prefix.split(":", maxsplit=1)[0]
226+
227+
if pv_root not in self.entities:
228228
logger_.warning(
229-
f"{self.techui.name}: The prefix [bold]{component.prefix}[/bold] "
230-
f"set in the component [bold]{component_name}[/bold] does not match"
231-
" any P field in the ioc.yaml files in services"
229+
f"Pipe component '{component_name}' with prefix "
230+
f"'{pipe_component.prefix}' does not match any entity "
231+
f"in the ioc.yaml files — skipping screen generation."
232232
)
233+
continue
234+
235+
screen_entities = self.entities[pv_root]
236+
237+
self.generator.build_widgets(component_name, screen_entities)
238+
self.generator.build_groups(component_name, {})
239+
240+
if component_name in list(self.validator.validate.keys()):
241+
self._validate_screen(component_name)
242+
else:
243+
self._generate_screen(component_name)
244+
245+
def create_screens(self):
246+
"""Create the screens for each component in techui.yaml"""
247+
if len(self.entities) == 0:
248+
logger_.critical(
249+
"No ioc entities found. This [italic]normally[/italic]"
250+
" suggests an issue with finding ixx-services."
251+
)
252+
return
253+
254+
self._create_component_screens()
255+
self._create_pipe_screens()

src/techui_builder/generate.py

Lines changed: 66 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import logging
22
import os
3-
import random
43
import re
54
from collections import defaultdict
65
from collections.abc import Mapping
@@ -12,7 +11,13 @@
1211
from phoebusgen import widget as pwidget
1312
from phoebusgen.widget.widgets import ActionButton, EmbeddedDisplay, Group
1413

15-
from techui_builder.models import Component, Entity, TechUi, TechUiSupport
14+
from techui_builder.models import (
15+
Component,
16+
Entity,
17+
PipeComponent,
18+
TechUi,
19+
TechUiSupport,
20+
)
1621

1722
logger_ = logging.getLogger(__name__)
1823

@@ -327,24 +332,23 @@ def _create_beamline_widget(
327332
self._make_color_element(background_color, *color)
328333
return widget
329334

330-
def _get_available_symbols(self) -> list[Path]:
331-
"""Get list of available symbol SVG files."""
332-
symbols_dir = self.support_path.joinpath("symbols")
333-
if symbols_dir.exists():
334-
return sorted(symbols_dir.glob("*.svg"))
335-
return []
335+
def _symbol_path(self, icon_type: str) -> Path | None:
336+
"""Derive SVG path from icon_type by converting underscores to hyphens."""
337+
filename = icon_type.replace("_", "-") + ".svg"
338+
path = self.support_path / "symbols" / filename
339+
return path if path.exists() else None
336340

337341
def _create_symbol_widget(
338342
self,
343+
component_name: str,
339344
label: str,
340345
symbol_path: Path,
341346
x: int,
342347
y: int,
343348
width: int,
344349
height: int,
345350
) -> etree.Element:
346-
"""Create a symbol widget with display action."""
347-
# Make symbol path relative to synoptic dir
351+
"""Create a symbol widget that opens the component's bob on click."""
348352
try:
349353
rel_symbol_path = symbol_path.relative_to(
350354
self.synoptic_dir,
@@ -360,11 +364,26 @@ def _create_symbol_widget(
360364
y=y,
361365
width=width,
362366
height=height,
363-
pv_name=label,
364367
)
365368
symbols = etree.SubElement(widget, "symbols")
366369
symbol = etree.SubElement(symbols, "symbol")
367370
symbol.text = str(rel_symbol_path)
371+
372+
actions = etree.SubElement(widget, "actions")
373+
actions.set("execute_as_one", "true")
374+
action = etree.SubElement(actions, "action")
375+
action.set("type", "open_display")
376+
file_el = etree.SubElement(action, "file")
377+
file_el.text = f"{component_name}.bob"
378+
target_el = etree.SubElement(action, "target")
379+
target_el.text = "tab"
380+
381+
run_actions = etree.SubElement(widget, "run_actions_on_mouse_click")
382+
run_actions.text = "true"
383+
384+
desc_el = etree.SubElement(action, "description")
385+
desc_el.text = f"Open {label}"
386+
368387
return widget
369388

370389
def _create_label_widget(
@@ -388,41 +407,11 @@ def _create_label_widget(
388407
)
389408
return widget
390409

391-
def _create_component_widget(
392-
self,
393-
component_name: str,
394-
label: str,
395-
x: int,
396-
y: int,
397-
width: int,
398-
height: int,
399-
) -> etree.Element:
400-
"""Create a symbol widget with display action."""
401-
widget = self._new_widget_element(
402-
"action_button",
403-
name=label,
404-
x=x,
405-
y=y,
406-
width=width,
407-
height=height,
408-
)
409-
actions = etree.SubElement(widget, "actions")
410-
action = etree.SubElement(actions, "action")
411-
action.set("type", "open_display")
412-
file_el = etree.SubElement(action, "file")
413-
file_el.text = f"{component_name}.bob"
414-
target_el = etree.SubElement(action, "target")
415-
target_el.text = "replace"
416-
desc_el = etree.SubElement(action, "description")
417-
desc_el.text = "Open Display"
418-
return widget
419-
420410
def _format_pipe_section(
421411
self,
422412
display: etree.Element,
423413
section_name: str,
424-
component_names: list[str],
425-
components: dict[str, Component],
414+
components: dict[str, PipeComponent],
426415
pipe_left: int,
427416
pipe_top: int,
428417
pipe_width: int,
@@ -441,27 +430,23 @@ def _format_pipe_section(
441430
)
442431
)
443432

444-
if not component_names:
433+
if not components:
445434
return
446435

447-
available_symbols = self._get_available_symbols()
448436
symbol_width = 60
449437
symbol_height = 60
450-
# label_height = 20
451-
count = len(component_names)
452-
spacing = int((pipe_width - count * symbol_width) / (count + 1))
453-
spacing = max(20, spacing)
438+
count = len(components)
439+
spacing = max(20, int((pipe_width - count * symbol_width) / (count + 1)))
454440

455441
x = pipe_left + spacing
456-
for component_name in component_names:
457-
component = components[component_name]
442+
for component_name, component in components.items():
458443
label = component.label or component_name
459-
symbol_path = (
460-
random.choice(available_symbols) if available_symbols else None
461-
)
444+
symbol_path = self._symbol_path(component.icon_type)
445+
462446
if symbol_path:
463447
display.append(
464448
self._create_symbol_widget(
449+
component_name,
465450
label,
466451
symbol_path,
467452
x,
@@ -470,6 +455,15 @@ def _format_pipe_section(
470455
symbol_height,
471456
)
472457
)
458+
else:
459+
logger_.warning(
460+
f"No SVG found for icon_type '{component.icon_type}' "
461+
f"(component '{component_name}'): expected "
462+
f"{component.icon_type.replace('_', '-')}.svg in "
463+
f"{self.support_path / 'symbols'}. "
464+
f"Add the SVG to techui-support or fix the icon_type string."
465+
)
466+
473467
display.append(
474468
self._create_label_widget(
475469
label,
@@ -484,20 +478,24 @@ def generate_index_bob(
484478
techui: TechUi,
485479
output_dir: Path | None = None,
486480
) -> None:
487-
"""Generate an index.bob from ordered components in techui.yaml."""
481+
"""Generate an index.bob from beam_pipe and vacuum_pipe in techui.yaml."""
488482
if output_dir is None:
489483
output_dir = self.synoptic_dir
490484

491485
if not techui.beam_pipe and not techui.vacuum_pipe:
492486
logger_.warning(
493-
"No beam_pipe or vacuum_pipe sections defined; "
494-
"skipping index.bob generation."
487+
"No beam_pipe or vacuum_pipe defined; skipping index.bob generation."
495488
)
496489
return
497490

498491
pipe_left = 100
499-
pipe_width = 1200
500492
pipe_height = 8
493+
component_count = max(
494+
len(techui.beam_pipe or {}),
495+
len(techui.vacuum_pipe or {}),
496+
)
497+
pipe_width = max(1200, component_count * 120 + 200)
498+
501499
display = etree.Element("display", version="2.0.0")
502500
title = etree.SubElement(display, "name")
503501
title.text = techui.beamline.location
@@ -507,31 +505,29 @@ def generate_index_bob(
507505
display,
508506
"vacuum_pipe",
509507
techui.vacuum_pipe,
510-
techui.components,
511508
pipe_left,
512-
120,
513-
pipe_width,
514-
pipe_height,
515-
80,
516-
(180, 180, 180),
509+
pipe_top=120,
510+
pipe_width=pipe_width,
511+
pipe_height=pipe_height,
512+
button_y=80,
513+
color=(180, 180, 180),
517514
)
518515

519516
if techui.beam_pipe:
520517
self._format_pipe_section(
521518
display,
522519
"beam_pipe",
523520
techui.beam_pipe,
524-
techui.components,
525521
pipe_left,
526-
260,
527-
pipe_width,
528-
pipe_height,
529-
220,
530-
(0, 120, 215),
522+
pipe_top=260,
523+
pipe_width=pipe_width,
524+
pipe_height=pipe_height,
525+
button_y=180,
526+
color=(0, 255, 255),
531527
)
532528

533529
output_dir.mkdir(parents=True, exist_ok=True)
534-
output_path = output_dir.joinpath("index.bob")
530+
output_path = output_dir / "index.bob"
535531
tree = etree.ElementTree(display)
536532
tree.write(
537533
output_path,

0 commit comments

Comments
 (0)