diff --git a/src/runconf_ui/apps/__main__shifter_view__.py b/src/runconf_ui/apps/__main__shifter_view__.py index 35978a1..ee6a445 100644 --- a/src/runconf_ui/apps/__main__shifter_view__.py +++ b/src/runconf_ui/apps/__main__shifter_view__.py @@ -52,19 +52,12 @@ def __init__(self, *args, **kwargs): use_local = kwargs.get("use_local", False) if use_local: - interface_config = f"{Path(__file__).parent.absolute()}/../config_files/interface_configs/local_configuration.yml" + interface_config = f"{Path(__file__).parent.absolute()}/../config_files/local_configuration.yml" else: - interface_config = f"{Path(__file__).parent.absolute()}/../config_files/interface_configs/ehn1_configuration.yml" + interface_config = f"{Path(__file__).parent.absolute()}/../config_files/ehn1_configuration.yml" apparatus = kwargs.get("apparatus", os.environ.get("APPARATUS", "np02")) - # messy... - detector_configuration = f"{Path(__file__).parent.absolute()}/../config_files/detector_configs/{apparatus}_configuration.yml" - - if not Path(detector_configuration).exists(): - raise Exception( - f"Detector configuration file {detector_configuration} does not exist" - ) if not Path(interface_config).exists(): raise Exception( @@ -73,7 +66,7 @@ def __init__(self, *args, **kwargs): # Now we've done logs, we can read the configuration interface_config = ShifterConfigReader( - detector_config_file=detector_configuration, + # detector_config_file=detector_configuration, settings_config_file=interface_config, **kwargs, ) diff --git a/src/runconf_ui/config_files/detector_configs/np02_configuration.yml b/src/runconf_ui/config_files/detector_configs/np02_configuration.yml deleted file mode 100644 index 1bd6c2f..0000000 --- a/src/runconf_ui/config_files/detector_configs/np02_configuration.yml +++ /dev/null @@ -1,189 +0,0 @@ ---- -# Options to generate panels -Settings: - classes_to_show: - - Segment - - SmartDaqApplication - - CTBHLT - - CTBoardConf - - -PanelOptions: - Detector: - label: "detector" - view_panel: "Detector View" - - Systems: - - TPC: - subsystem_dependent: True - components: - - id: tpc-segment - class: Segment - - id: crp4-segment - class: Segment - system_label: CRP4 - separate_system: True - - - id: crp5-segment - class: Segment - system_label: CRP5 - separate_system: True - - - id: crp23-segment - class: Segment - system_label: TDE - separate_system: True - tooltip: "TDE" - - - PDS: - subsystem_dependent: True - components: - - id: pds-segment - class: Segment - - - id: membrane-segment - class: Segment - system_label: Membrane - separate_system: True - - - id: cathode-segment - class: Segment - system_label: Cathode - separate_system: True - - - CTB: - components: - - id: ctb-segment - class: Segment - - - DQM: - components: - - id: dqm-segment - class: Segment - - - TPG: - label: "TPG" - view_panel: "TPG View" - - Systems: - - TPG: - subsystem_dependent: True - display_full_system: True - - components: - - id: tc-maker-tpc - class: TriggerApplication - - - id: tp-stream-writer - class: TPStreamWriterApplication - - attributes: - - id: tp_generation_enabled - segments: ["crp4-segment"] - class: ReadoutApplication - system_label: CRP4 - separate_system: True - - - id: ta_generation_enabled - segments: ["crp4-segment"] - class: ReadoutApplication - system_label: CRP4 - separate_system: True - - - id : tp_generation_enabled - segments: ["crp5-segment"] - class: ReadoutApplication - system_label: CRP5 - separate_system: True - - - id : ta_generation_enabled - segments: ["crp5-segment"] - class: ReadoutApplication - system_label: CRP5 - separate_system: True - - - id : tp_generation_enabled - segments: ["crp23-segment"] - class: NP02ReadoutApplication - system_label: TDE - separate_system: True - - - id : ta_generation_enabled - segments: ["crp23-segment"] - class: NP02ReadoutApplication - system_label: TDE - separate_system: True - - - id: tp_generation_enabled - segments: ["pds-segment"] - class: ReadoutApplication - system_label: PDS - separate_system: True - - Trigger: - label: "trigger" - view_panel: "Trigger View" - Systems: - - - RandomTrigger: - relationships: - - id: standalone_candidate_maker_confs - class: MLTApplication - segments: ['trg-segment'] - relationship_class: RandomTCMakerConf - enabled_state: [random-tc-generator] - disabled_state: [] - tooltip: "Enable/Disable random trigger" - - - Trigger: - display_full_system: False - subsystem_dependent: False - components: - - id: "" - class: CTBHLT - each_component_separate: True - filters: - - attribute: "description" - values: ["Spare", "spare"] - tooltip: 'description' - # Dataflow - Dataflow: - label: "dataflow" - Systems: - - Dataflow: - subsystem_dependent: False - display_full_system: False - components: - - id: "" - class: "DFApplication" - each_component_separate: True - - -AdjustableAttributes: - Random Trigger Rates: - label: "TriggerRates" - Systems: - - object_id: "random-tc-generator" - object_class: RandomTCMakerConf - attribute_name: "trigger_rate_hz" - unit_label: "Hz" - - - object_id: "LLT_0" - object_class: "CTBRandomTrigger" - attribute_name: "period" - unit_label : "ticks" - tooltip: 'description' - - Prescales: - label: "PreScales" - Systems: - - object_class: "CTBHLT" - attribute_name: "prescale" - is_hex: True - unit_label: "" - filters: - - attribute: "description" - values: ["Spare", "spare"] - - tooltip: 'description' diff --git a/src/runconf_ui/config_files/detector_configs/np04_configuration.yml b/src/runconf_ui/config_files/detector_configs/np04_configuration.yml deleted file mode 100644 index 9ab53ba..0000000 --- a/src/runconf_ui/config_files/detector_configs/np04_configuration.yml +++ /dev/null @@ -1,113 +0,0 @@ ---- -Settings: - classes_to_show: - - Segment - - TriggerApplication - - TPStreamWriterApplication - - ReadoutApplication - - MLTApplication - - RandomTCMakerConf - - CTBHLT - - DFApplication - - CTBoardConf - - DetectorStream - -PanelOptions: - Detector: - label: "detector" - view_panel: "Detector View" - - Systems: - - TPC: - subsystem_dependent: True - components: - - id: tpc-segment - class: Segment - - - id: apa1-segment - class: Segment - system_label: APA1 - separate_system: True - - - id: apa2-segment - class: Segment - system_label: APA2 - separate_system: True - - - id: apa3-segment - class: Segment - system_label: APA3 - separate_system: True - - - id: apa4-segment - class: Segment - system_label: APA4 - separate_system: True - - - DQM: - components: - - id: dqm-segment - class: Segment - - - PDS: - components: - - id: pds-segment - class: Segment - - - # Dataflow - Dataflow: - label: "dataflow" - Systems: - - Dataflow: - subsystem_dependent: True - display_full_system: False - components: - - id: "" - class: "DFApplication" - each_component_separate: True - - TPG: - label: "tpg" - view_panel: "TPG View" - - Systems: - - TPG: - subsystem_dependent: True - display_full_system: True - - components: - - id: tc-maker-tpc - class: TriggerApplication - - - id: tp-stream-writer - class: TPStreamWriterApplication - - attributes: - - id: tp_generation_enabled - segments: ["tpc-segment"] - class: ReadoutApplication - system_label: TPC - separate_system: True - - - id: ta_generation_enabled - segments: ["tpc-segment", "crp4-segment", "crp5-segment"] - class: ReadoutApplication - system_label: TPC - separate_system: True - - - id: tp_generation_enabled - segments: ["pds-segment"] - class: ReadoutApplication - system_label: PDS - separate_system: True - -AdjustableAttributes: - Random Trigger Rates: - label: "TriggerRates" - Systems: - - object_id: "random-tc-generator" - object_class: RandomTCMakerConf - attribute_name: "trigger_rate_hz" - unit_label: "Hz" - diff --git a/src/runconf_ui/config_files/interface_configs/ehn1_configuration.yml b/src/runconf_ui/config_files/ehn1_configuration.yml similarity index 100% rename from src/runconf_ui/config_files/interface_configs/ehn1_configuration.yml rename to src/runconf_ui/config_files/ehn1_configuration.yml diff --git a/src/runconf_ui/config_files/interface_configs/local_configuration.yml b/src/runconf_ui/config_files/local_configuration.yml similarity index 100% rename from src/runconf_ui/config_files/interface_configs/local_configuration.yml rename to src/runconf_ui/config_files/local_configuration.yml diff --git a/src/runconf_ui/configuration_manager_interfaces/local_daq_conf_manager.py b/src/runconf_ui/configuration_manager_interfaces/local_daq_conf_manager.py index 594409f..8ac9945 100644 --- a/src/runconf_ui/configuration_manager_interfaces/local_daq_conf_manager.py +++ b/src/runconf_ui/configuration_manager_interfaces/local_daq_conf_manager.py @@ -1,4 +1,5 @@ from pathlib import Path +import os from runconf_ui.configuration_manager_interfaces.management_interface import ( ManagementInterface, ) @@ -9,6 +10,7 @@ ShifterInterfaceState, ) +import logging class LocalDaqConfManager(ManagementInterface): def __init__(self, application_controller: ShifterInterfaceState): @@ -23,6 +25,15 @@ def __init__(self, application_controller: ShifterInterfaceState): ":" ) ] + + # We can also immediately load in our detector config since this won't change + detector_config_path = Path(os.environ["DUNEDAQ_DB_DATA_ROOT"]) / 'runconf-ui-settings' / f"{self.application_controller.apparatus}.yml" + if not detector_config_path.exists(): + raise FileNotFoundError(f"Detector configuration file {detector_config_path} does not exist") + + self.application_controller.shifter_interface_config.open_detector_config(str(detector_config_path)) + + def get_daq_versions(self) -> list[Path]: """ diff --git a/src/runconf_ui/configuration_manager_interfaces/management_interface.py b/src/runconf_ui/configuration_manager_interfaces/management_interface.py index c0b0d86..a7d69e5 100644 --- a/src/runconf_ui/configuration_manager_interfaces/management_interface.py +++ b/src/runconf_ui/configuration_manager_interfaces/management_interface.py @@ -51,6 +51,7 @@ def open_file(self, file_path: Path) -> DaqConfigurationWrapper: @classmethod def find_session(cls, file_name: str): + config_file = DaqConfigurationWrapper(file_name) file_sessions = ca.GetDalsOfClassAction(config_file)("Session") if file_sessions: diff --git a/src/runconf_ui/configuration_manager_interfaces/remote_daq_conf_manger.py b/src/runconf_ui/configuration_manager_interfaces/remote_daq_conf_manger.py index 93d1d4b..84f7396 100644 --- a/src/runconf_ui/configuration_manager_interfaces/remote_daq_conf_manger.py +++ b/src/runconf_ui/configuration_manager_interfaces/remote_daq_conf_manger.py @@ -75,7 +75,15 @@ def open_file(self, daq_configuration: str): except Exception as e: logging.error(traceback.format_exc()) CiderInvalidRepoException(e) - + + + detector_config_path = Path(self.application_controller.shifter_interface_config.download_directory) / 'runconf-ui-settings' / f"{self.application_controller.apparatus}.yml" + if not detector_config_path.exists(): + raise FileNotFoundError(f"Detector configuration file {detector_config_path} does not exist") + + logging.info(f"Using detector configuration file {detector_config_path}") + + self.application_controller.shifter_interface_config.open_detector_config(str(detector_config_path)) # Now we can open the file config_path_reader = DaqConfPathReader() @@ -108,6 +116,7 @@ def open_file(self, daq_configuration: str): f"Found multiple config files with the same name: {valid_config_files}, using the first one" ) + config_file = valid_config_files[0] return super().open_file(Path(config_file)) diff --git a/src/runconf_ui/daq_config_interfaces/actions/actions.py b/src/runconf_ui/daq_config_interfaces/actions/actions.py index ca09671..030db81 100644 --- a/src/runconf_ui/daq_config_interfaces/actions/actions.py +++ b/src/runconf_ui/daq_config_interfaces/actions/actions.py @@ -139,15 +139,12 @@ def action(self, dal, session_name: str, disable: bool): session = GetDalObjectAction(self._daq_configuration)(session_name, "Session") disabled_objects = getattr(session, "disabled") - logging.info(f"Disabled before {disabled_objects}, {dal}") if disable: disabled_objects.append(dal) elif dal in disabled_objects: disabled_objects.remove(dal) setattr(session, "disabled", list(set(disabled_objects))) - logging.info(f"Disabled after {disabled_objects}, {dal}") - return session diff --git a/src/runconf_ui/runconf_ui_configuration/object_extractors/adjustable_attribute_extractor.py b/src/runconf_ui/runconf_ui_configuration/object_extractors/adjustable_attribute_extractor.py index e76ac98..c87da02 100644 --- a/src/runconf_ui/runconf_ui_configuration/object_extractors/adjustable_attribute_extractor.py +++ b/src/runconf_ui/runconf_ui_configuration/object_extractors/adjustable_attribute_extractor.py @@ -52,16 +52,11 @@ def __init__(self, application_controller: ShifterInterfaceState, **kwargs): # Means we can use the attribute class to get the object list if object_id is None or "": - logging.info(f"HERE {self._object_class}") unfliltered_list = ca.GetDalsOfClassAction( self._application_controller.buffer_daq_config - )(self._object_class) - - logging.info(f"Unfiltered object list: {unfliltered_list}") - + )(self._object_class) self._object_list = [obj for obj in unfliltered_list if self._filter(obj)] - logging.info(f"Object list filtered by filter_by attribute. {self._object_list}") else: self._object_ids = [object_id] diff --git a/src/runconf_ui/runconf_ui_configuration/shifter_config_reader.py b/src/runconf_ui/runconf_ui_configuration/shifter_config_reader.py index 596538f..7498861 100644 --- a/src/runconf_ui/runconf_ui_configuration/shifter_config_reader.py +++ b/src/runconf_ui/runconf_ui_configuration/shifter_config_reader.py @@ -6,10 +6,7 @@ # Class for reading a YAML config and producing panels class ShifterConfigReader: - def __init__(self, settings_config_file: str, detector_config_file: str, **kwargs): - - with open(detector_config_file, "r") as f: - self._detector_config = yaml.safe_load(f) + def __init__(self, settings_config_file: str, **kwargs): with open(settings_config_file, "r") as f: self._settings_config = yaml.safe_load(f) @@ -43,9 +40,21 @@ def __init__(self, settings_config_file: str, detector_config_file: str, **kwarg ) # Get settings from the detector config + self._detector_config = {} + self._classes_to_show = [] + self.detector_config_settings = {} + + + def open_detector_config(self, detector_config_file: str): + with open(detector_config_file, "r") as f: + self._detector_config = yaml.safe_load(f) detector_config_settings = self._detector_config.get("Settings", {}) self._classes_to_show = detector_config_settings.get("classes_to_show", []) + @property + def detector_config(self): + return self._detector_config + @property def output_directory(self): return f"{self._download_directory}/../shifter_configs/{self._session_name}" diff --git a/src/runconf_ui/screens/shifter_view_screen.py b/src/runconf_ui/screens/shifter_view_screen.py index 610410b..df71d06 100644 --- a/src/runconf_ui/screens/shifter_view_screen.py +++ b/src/runconf_ui/screens/shifter_view_screen.py @@ -24,6 +24,7 @@ ) from runconf_ui.screens.popup_manager import PopupManager from runconf_ui.widgets.adjustable_attribute_panel import AdjustableAttributePanel +from runconf_ui.widgets.enable_disable_base import EnableDisablePanel from runconf_ui.runconf_ui_configuration.detector_config_readers.generate_adjustable_attribute_map import ( AdjustableAttributeMapGen, ) @@ -48,18 +49,20 @@ def __init__( def compose(self): """Generate the screen layout""" - enable_disable_generator = EnableDisableMapGen(self._application_controller) - adjustable_attribute_panel = AdjustableAttributeMapGen( - self._application_controller - ) - + # Do not generate detector-dependent panels here. Create placeholder + # containers which will be populated after a detector configuration is + # loaded. This avoids requiring a detector config to exist at + # compose-time. with ScrollableContainer(id="main_container"): yield FilePanelWidget(self._application_controller, classes="file_io_panel") with Grid(id="enable_disable_panel_container"): + # selection tabs placeholder - enable/disable panels will be + # mounted here after a config is loaded. Provide an empty + # TabPane so Textual doesn't attempt to wrap a `None` child. with TabbedContent(id="selection_tabs"): - for panel in enable_disable_generator.panel_list: - yield panel + with TabPane("Selection", id="selection_tab_placeholder"): + yield Static("", id="selection_tab_placeholder_static") with TabbedContent( "Map Views", @@ -72,16 +75,13 @@ def compose(self): id="systematic_map_tabs", classes="systematic_map_tabs", ): - with TabPane("System View", id="full_system_map_tab"): yield ScrollableContainer( Static("", id="tree_view_full"), id="tree_view_full_container", classes="tree_view_full_container", ) - for panel in enable_disable_generator.map_list: - if panel is not None: - yield panel + # map panels will be mounted into systematic_map_tabs with TabPane("Adjustable Rates", id="detector_map_tab"): with TabbedContent( @@ -89,10 +89,9 @@ def compose(self): id="attribute_map_tabs", classes="systematic_map_tabs multi_view_tabs", ): - - for panel in adjustable_attribute_panel.panel_list: - if panel is not None: - yield panel + # attribute panels will be mounted into attribute_map_tabs + with TabPane("Attributes", id="attribute_tab_placeholder"): + yield Static("", id="attribute_tab_placeholder_static") yield OptionPanel( application_controller=self._application_controller, @@ -108,7 +107,7 @@ def compose(self): async def select_new_file(self): """Handle new file selection""" try: - self._load_new_configuration() + await self._load_new_configuration() except CiderInvalidConfigurationException: self.popups.show( f"[white]Invalid configuration[/white] [bold grey3]{self._application_controller.shifter_interface_config} set up incorrectly!" @@ -152,13 +151,15 @@ async def attribute_out_of_bounds( timer=5.0, ) - def _load_new_configuration(self): - """Handle loading a new configuration file""" + async def _load_new_configuration(self): + """Handle loading a new configuration file.""" logging.info( f"Opening new file: {self._application_controller.session_name}:{self._application_controller.current_daq_config}" ) + # load configuration self.file_service.load_configuration() + # update option panel UI self.query_one(OptionPanel).open_new_session() if not ( @@ -168,29 +169,119 @@ def _load_new_configuration(self): logging.info("No session or configuration") return - self._update_ui_after_config_load() + await self._update_ui_after_config_load() + + async def _update_ui_after_config_load(self): + """Update UI components after loading a new configuration. + + This generates detector-dependent panels and mounts them into the + placeholder containers created during compose(). + """ + logging.debug("Generating and mounting enable/disable & attribute panels") + + # instantiate generators now that the detector config is available + enable_disable_generator = EnableDisableMapGen(self._application_controller) + adjustable_attribute_panel = AdjustableAttributeMapGen( + self._application_controller + ) + + # locate placeholder containers by id + selection_tabs = self.query_one("#selection_tabs", TabbedContent) + systematic_map_tabs = self.query_one( + "#systematic_map_tabs", TabbedContent + ) + attribute_map_tabs = self.query_one("#attribute_map_tabs", TabbedContent) - def _update_ui_after_config_load(self): - """Update UI components after loading a new configuration""" - logging.debug("Updating enable/disable panels") - for panel in self.query("EnableDisablePanel"): + # remove any previously mounted generator panels to avoid duplicates + # on reload. We remove by id to avoid removing built-in placeholder + # panes (for example the 'full_system_map_tab' that contains the + # main tree view). + for panel_tab in enable_disable_generator.panel_list: + if panel_tab is None: + continue + try: + # Use TabbedContent.remove_pane to remove both the Tab and the pane + await selection_tabs.remove_pane(panel_tab.id) + except Exception: + pass + + for map_tab in enable_disable_generator.map_list: + if map_tab is None: + continue + try: + await systematic_map_tabs.remove_pane(map_tab.id) + except Exception: + pass + + for attr_tab in adjustable_attribute_panel.panel_list: + if attr_tab is None: + continue + try: + await attribute_map_tabs.remove_pane(attr_tab.id) + except Exception: + pass + + # mount selection panels. The generators return TabPane objects; mount + # them directly into the `selection_tabs` container so TabbedContent + # creates tab entries accordingly. + for panel_tab in enable_disable_generator.panel_list: + if panel_tab is None: + continue + + await selection_tabs.add_pane(panel_tab) + + # remove the placeholder selection tab if it exists now we've added real panes + try: + await selection_tabs.remove_pane("selection_tab_placeholder") + except Exception: + pass + + # mount map views (TabPane objects) + for map_tab in enable_disable_generator.map_list: + if map_tab is None: + continue + + await systematic_map_tabs.add_pane(map_tab) + + # Debug: list children of the newly added pane and check for expected tree Static + try: + # expected tree static id uses label: derive label from panel id patterns + label = map_tab.id.replace("_tabs", "") + expected_tree_id = f"tree_view_{label}" + try: + self.query_one(f"#{expected_tree_id}") + except Exception: + pass + except Exception: + pass + + # mount adjustable attribute panels (TabPane objects) + for attr_tab in adjustable_attribute_panel.panel_list: + if attr_tab is None: + continue + + await attribute_map_tabs.add_pane(attr_tab) + + # now refresh panels and update controller state + for panel in self.query(EnableDisablePanel): panel.open_new_session() panel.refresh(recompose=True) panel.update_button_styles() self._application_controller.current_state = { - p.id: p.get_current_states() for p in self.query("EnableDisablePanel") + p.id: p.get_current_states() for p in self.query(EnableDisablePanel) } - for panel in self.query("AdjustableAttributePanel"): + for panel in self.query(AdjustableAttributePanel): panel.open_new_session() panel.refresh(recompose=True) self._application_controller.current_state = { p.id: p.get_current_states() - for p in self.query("AdjustableAttributePanel") + for p in self.query(AdjustableAttributePanel) } self.tree_manager.update_all_trees(self) - self.query_one("FilePanelWidget").update_file_info() + + self.query_one(FilePanelWidget).update_file_info() self.popups.show( f"[white]Successfully opened new configuration[/white] [bold white]{self._application_controller.current_daq_config}", @@ -198,6 +289,8 @@ def _update_ui_after_config_load(self): success=True, ) + + def on_enable_disable_panel_changed(self): """Handle changes in enable/disable panels""" self.tree_manager.update_all_trees(self) diff --git a/src/runconf_ui/widgets/enable_disable_base.py b/src/runconf_ui/widgets/enable_disable_base.py index a04c807..f8ad48c 100644 --- a/src/runconf_ui/widgets/enable_disable_base.py +++ b/src/runconf_ui/widgets/enable_disable_base.py @@ -47,6 +47,12 @@ def session_name(self) -> str | None: return self._application_controller.session_name def open_new_session(self): + # Allow subclasses to defer running this method during their + # construction by setting `_defer_open = True` before calling + # `super().__init__`. + if getattr(self, "_defer_open", False): + return + self._button_list = self.generate_button_list() # Need default initial states diff --git a/src/runconf_ui/widgets/file_select_panel.py b/src/runconf_ui/widgets/file_select_panel.py index e56d5e7..1409d3e 100644 --- a/src/runconf_ui/widgets/file_select_panel.py +++ b/src/runconf_ui/widgets/file_select_panel.py @@ -337,6 +337,7 @@ def update_file_info(self): selected_configuration = self.query_one("#daq_configuration_select").value self.query_one("#file_io_panel_message_static").update( f" [bold green]DAQ Version[/bold green]: [deep_pink4]{self._management_interface.daq_version}[/deep_pink4]\n" + f" [bold green]Apparatus[/bold green]: [deep_pink4]{self._application_controller.apparatus}[/deep_pink4]\n" f" [bold green]DAQ Config[/bold green]: [deep_pink4]{selected_configuration}[/deep_pink4]\n" f" [bold green]Current Config File[/bold green]: [deep_pink4]{self._application_controller.current_daq_config}[/deep_pink4]\n" f" [bold green]Session in Config[/bold green]: [deep_pink4]{self._application_controller.session_name}\n" diff --git a/src/runconf_ui/widgets/multicomponent_panel.py b/src/runconf_ui/widgets/multicomponent_panel.py index 8588467..f783316 100644 --- a/src/runconf_ui/widgets/multicomponent_panel.py +++ b/src/runconf_ui/widgets/multicomponent_panel.py @@ -38,6 +38,12 @@ def __init__( disabled: bool = False, ) -> None: + # Prevent the parent initializer from calling open_new_session before + # we have set up attributes the subclass expects (like _extractor). + # We use a small deferral flag that the overridden open_new_session + # will honour during construction. + self._defer_open = True + super().__init__( application_controller, content, @@ -50,8 +56,9 @@ def __init__( disabled=disabled, ) + # Now set up subclass-specific fields before running the real + # open_new_session implementation. self._object_list = object_list - self._disabled_items = [] self._build_tree = build_tree @@ -59,6 +66,11 @@ def __init__( self._extractor = DetectorExtractor(self._application_controller, object_list) logging.debug(f"Extractor initialized with {self._extractor.get_all_states()}") + # Mark initialization complete and run the real open_new_session to + # generate the button list now that _extractor exists. + self._defer_open = False + super().open_new_session() + logging.debug("MultiComponentEnableDisablePanel initialized.") def generate_button_list(self) -> Dict | None: