11import logging
22import os
3- import random
43import re
54from collections import defaultdict
65from collections .abc import Mapping
1211from phoebusgen import widget as pwidget
1312from 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
1722logger_ = 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