diff --git a/src/aiidalab_qe/app/wrapper.py b/src/aiidalab_qe/app/wrapper.py index 59cc530a1..8ecedb45a 100644 --- a/src/aiidalab_qe/app/wrapper.py +++ b/src/aiidalab_qe/app/wrapper.py @@ -60,7 +60,13 @@ def _on_guide_toggle(self, change: dict): if change["new"]: self._view.info_container.children = [ self._view.guide, - self._view.guide_selection, + ipw.HBox( + children=[ + self._view.guide_category_selection, + self._view.guide_selection, + ], + layout=ipw.Layout(align_items="baseline"), + ), ] self._view.info_container.layout.display = "flex" self._view.job_history_toggle.value = False @@ -94,21 +100,50 @@ def _on_job_history_toggle(self, change: dict): else: self._view.main.children = self._old_view - def _on_guide_select(self, change: dict): + def _on_guide_category_select(self, change: dict): + self._view.guide_selection.options = guide_manager.get_guides(change["new"]) + self._update_active_guide() + + def _on_guide_select(self, _): + self._update_active_guide() + + def _update_active_guide(self): """Sets the current active guide.""" - guide_manager.active_guide = change["new"] + category = self._view.guide_category_selection.value + guide = self._view.guide_selection.value + active_guide = f"{category}/{guide}" if category != "none" else category + guide_manager.active_guide = active_guide - def _set_guide_options(self, _): + def _set_guide_category_options(self, _): """Fetch the available guides.""" - self._view.guide_selection.options = ["none", *guide_manager.get_guides()] + self._view.guide_category_selection.options = [ + "none", + *guide_manager.get_guide_categories(), + ] def _set_event_handlers(self) -> None: """Set up event handlers.""" - self._view.guide_toggle.observe(self._on_guide_toggle, "value") - self._view.about_toggle.observe(self._on_about_toggle, "value") - self._view.job_history_toggle.observe(self._on_job_history_toggle, "value") - self._view.guide_selection.observe(self._on_guide_select, "value") - self._view.on_displayed(self._set_guide_options) + self._view.guide_toggle.observe( + self._on_guide_toggle, + "value", + ) + self._view.about_toggle.observe( + self._on_about_toggle, + "value", + ) + self._view.job_history_toggle.observe( + self._on_job_history_toggle, + "value", + ) + self._view.guide_category_selection.observe( + self._on_guide_category_select, + "value", + ) + self._view.guide_selection.observe( + self._on_guide_select, + "value", + ) + self._view.on_displayed(self._set_guide_category_options) class AppWrapperModel(tl.HasTraits): @@ -199,11 +234,13 @@ def __init__(self) -> None: self.guide = ipw.HTML(env.from_string(guide_template).render()) self.about = ipw.HTML(env.from_string(about_template).render()) - self.guide_selection = ipw.RadioButtons( + self.guide_category_selection = ipw.RadioButtons( options=["none"], description="Guides:", value="none", + layout=ipw.Layout(width="max-content"), ) + self.guide_selection = ipw.RadioButtons(layout=ipw.Layout(margin="2px 20px")) self.job_history = QueryInterface() diff --git a/src/aiidalab_qe/common/guide_manager.py b/src/aiidalab_qe/common/guide_manager.py index 2bcc6e72a..15e313693 100644 --- a/src/aiidalab_qe/common/guide_manager.py +++ b/src/aiidalab_qe/common/guide_manager.py @@ -19,7 +19,13 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) guides = Path(aiidalab_qe.__file__).parent.joinpath("guides").glob("*") - self._guides = {guide.stem: guide.absolute() for guide in guides} + self._guides = { + "general": { + guide.stem.split("_", maxsplit=1)[1]: guide.absolute() + for guide in sorted(guides, key=lambda x: x.stem.split("_")[0]) + } + } + self._fetch_plugin_guides() self.content = BeautifulSoup() @@ -33,7 +39,7 @@ def __init__(self, *args, **kwargs): def has_guide(self) -> bool: return self.active_guide != "none" - def get_guides(self) -> list[str]: + def get_guide_categories(self) -> list[str]: """Return a list of available guides. Returns @@ -43,6 +49,16 @@ def get_guides(self) -> list[str]: """ return [*self._guides.keys()] + def get_guides(self, identifier: str) -> list[str]: + """Return a list of available sub-guides. + + Returns + ------- + `list[str]` + A list of the names of available sub-guides. + """ + return [*self._guides[identifier].keys()] if identifier != "none" else [] + def get_guide_section_by_id(self, content_id: str) -> PageElement | None: """Return a guide section by its HTML `id` attribute. @@ -60,16 +76,23 @@ def get_guide_section_by_id(self, content_id: str) -> PageElement | None: def _on_active_guide_change(self, _): """Load the contents of the active guide.""" - guide_path = self._guides.get(self.active_guide) + if self.active_guide == "none": + self.content = BeautifulSoup() + return + category, guide = self.active_guide.split("/") + guide_path = self._guides[category][guide] html = Path(guide_path).read_text() if guide_path else "" self.content = BeautifulSoup(html, "html.parser") def _fetch_plugin_guides(self): """Fetch guides from plugins.""" entries: dict[str, Path] = get_entry_items("aiidalab_qe.properties", "guides") - for guides in entries.values(): - for guide in guides.glob("*"): - self._guides[guide.stem] = guide.absolute() + for identifier, guides in entries.items(): + if identifier not in self._guides: + self._guides[identifier] = {} + for guide in sorted(guides.glob("*"), key=lambda x: x.stem.split("_")[0]): + stem = guide.stem.split("_", maxsplit=1)[1] + self._guides[identifier][stem] = guide.absolute() guide_manager = GuideManager() diff --git a/src/aiidalab_qe/guides/basic.html b/src/aiidalab_qe/guides/0_basic.html similarity index 94% rename from src/aiidalab_qe/guides/basic.html rename to src/aiidalab_qe/guides/0_basic.html index bd28dd655..e51f8763d 100644 --- a/src/aiidalab_qe/guides/basic.html +++ b/src/aiidalab_qe/guides/0_basic.html @@ -1,6 +1,6 @@