diff --git a/src/aiidalab_qe/common/guide_manager.py b/src/aiidalab_qe/common/guide_manager.py index 9e4e8a75..2bcc6e72 100644 --- a/src/aiidalab_qe/common/guide_manager.py +++ b/src/aiidalab_qe/common/guide_manager.py @@ -3,16 +3,20 @@ from pathlib import Path import traitlets as tl -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, PageElement import aiidalab_qe from aiidalab_qe.app.utils import get_entry_items class GuideManager(tl.HasTraits): + """A global guide manager that loads and manages guide sections.""" + active_guide = tl.Unicode("none", allow_none=True) def __init__(self, *args, **kwargs): + """`GuideManager` constructor.""" + super().__init__(*args, **kwargs) guides = Path(aiidalab_qe.__file__).parent.joinpath("guides").glob("*") self._guides = {guide.stem: guide.absolute() for guide in guides} @@ -26,21 +30,42 @@ def __init__(self, *args, **kwargs): ) @property - def has_guide(self): + def has_guide(self) -> bool: return self.active_guide != "none" - def get_guides(self): + def get_guides(self) -> list[str]: + """Return a list of available guides. + + Returns + ------- + `list[str]` + A list of the names of available guides. + """ return [*self._guides.keys()] - def get_guide_section_by_id(self, content_id: str): - return self.content.find(attrs={"id": content_id}) + def get_guide_section_by_id(self, content_id: str) -> PageElement | None: + """Return a guide section by its HTML `id` attribute. + + Parameters + ---------- + `content_id` : `str` + The HTML `id` attribute of the guide section. + + Returns + ------- + `PageElement` | `None` + The guide section or `None` if not found. + """ + return self.content.find(attrs={"id": content_id}) # type: ignore def _on_active_guide_change(self, _): + """Load the contents of the active guide.""" guide_path = self._guides.get(self.active_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("*"): diff --git a/src/aiidalab_qe/common/infobox.py b/src/aiidalab_qe/common/infobox.py index 841dc762..bdf72e98 100644 --- a/src/aiidalab_qe/common/infobox.py +++ b/src/aiidalab_qe/common/infobox.py @@ -23,7 +23,22 @@ def __init__(self, classes: list[str] | None = None, **kwargs): class InAppGuide(InfoBox): - """The `InfoAppGuide` is used to set up toggleable in-app guides.""" + """The `InAppGuide` is used to set up toggleable in-app guides. + + Attributes + ---------- + `manager` : `GuideManager` + A local reference to the global guide manager. + `identifier` : `str`, optional + If content `children` are not provided directly, the `identifier` + is used to fetch the corresponding guide section from the guide + currently loaded by the guide manager. + + Raises + ------ + `ValueError` + If neither content `children` or a guide section `identifier` are provided. + """ def __init__( self, @@ -37,9 +52,11 @@ def __init__( Parameters ---------- children : `list`, optional - The children of the guide. - `identifier` : `str` - The identifier used to load the guide file. + The content children of this guide section. + `identifier` : `str`, optional + If content `children` are not provided directly, the `identifier` + is used to fetch the corresponding guide section from the guide + currently loaded by the guide manager. `classes` : `list[str]`, optional One or more CSS classes. """ @@ -71,13 +88,18 @@ def __init__( # This manual toggle call is necessary because the guide # may be contained in a component that was not yet rendered # when a guide was selected. - self._toggle_guide() + self._on_active_guide_change(None) def _on_active_guide_change(self, _): + self._update_contents() self._toggle_guide() - def _toggle_guide(self): + def _update_contents(self): + """Update the contents of the guide section.""" if hasattr(self, "identifier"): html = self.manager.get_guide_section_by_id(self.identifier) self.children = [ipw.HTML(str(html))] if html else [] + + def _toggle_guide(self): + """Toggle the visibility of the guide section.""" self.layout.display = "flex" if self.manager.has_guide else "none"