133 lines
4.2 KiB
Python
133 lines
4.2 KiB
Python
"""Provides a widget for switching between the display of its immediate children."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Optional
|
|
|
|
from textual.await_complete import AwaitComplete
|
|
from textual.containers import Container
|
|
from textual.css.query import NoMatches
|
|
from textual.events import Mount
|
|
from textual.reactive import reactive
|
|
from textual.widget import Widget
|
|
|
|
|
|
class ContentSwitcher(Container):
|
|
"""A widget for switching between different children.
|
|
|
|
Note:
|
|
All child widgets that are to be switched between need a unique ID.
|
|
Children that have no ID will be hidden and ignored.
|
|
"""
|
|
|
|
DEFAULT_CSS = """
|
|
ContentSwitcher {
|
|
height: auto;
|
|
}
|
|
|
|
"""
|
|
|
|
current: reactive[str | None] = reactive[Optional[str]](None, init=False)
|
|
"""The ID of the currently-displayed widget.
|
|
|
|
If set to `None` then no widget is visible.
|
|
|
|
Note:
|
|
If set to an unknown ID, this will result in
|
|
[`NoMatches`][textual.css.query.NoMatches] being raised.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*children: Widget,
|
|
name: str | None = None,
|
|
id: str | None = None,
|
|
classes: str | None = None,
|
|
disabled: bool = False,
|
|
initial: str | None = None,
|
|
) -> None:
|
|
"""Initialise the content switching widget.
|
|
|
|
Args:
|
|
*children: The widgets to switch between.
|
|
name: The name of the content switcher.
|
|
id: The ID of the content switcher in the DOM.
|
|
classes: The CSS classes of the content switcher.
|
|
disabled: Whether the content switcher is disabled or not.
|
|
initial: The ID of the initial widget to show, ``None`` or empty string for the first tab.
|
|
|
|
Note:
|
|
If `initial` is not supplied no children will be shown to start with.
|
|
"""
|
|
super().__init__(
|
|
*children,
|
|
name=name,
|
|
id=id,
|
|
classes=classes,
|
|
disabled=disabled,
|
|
)
|
|
self._initial = initial
|
|
|
|
def _on_mount(self, _: Mount) -> None:
|
|
"""Perform the initial setup of the widget once the DOM is ready."""
|
|
initial = self._initial
|
|
with self.app.batch_update():
|
|
for child in self.children:
|
|
child.display = bool(initial) and child.id == initial
|
|
self._reactive_current = initial
|
|
|
|
@property
|
|
def visible_content(self) -> Widget | None:
|
|
"""A reference to the currently-visible widget.
|
|
|
|
`None` if nothing is visible.
|
|
"""
|
|
return self.get_child_by_id(self.current) if self.current is not None else None
|
|
|
|
def watch_current(self, old: str | None, new: str | None) -> None:
|
|
"""React to the current visible child choice being changed.
|
|
|
|
Args:
|
|
old: The old widget ID (or `None` if there was no widget).
|
|
new: The new widget ID (or `None` if nothing should be shown).
|
|
"""
|
|
with self.app.batch_update():
|
|
if old:
|
|
try:
|
|
self.get_child_by_id(old).display = False
|
|
except NoMatches:
|
|
pass
|
|
if new:
|
|
self.get_child_by_id(new).display = True
|
|
|
|
def add_content(
|
|
self, widget: Widget, *, id: str | None = None, set_current: bool = False
|
|
) -> AwaitComplete:
|
|
"""Add new content to the `ContentSwitcher`.
|
|
|
|
Args:
|
|
widget: A Widget to add.
|
|
id: ID for the widget, or `None` if the widget already has an ID.
|
|
set_current: Set the new widget as current (which will cause it to display).
|
|
|
|
Returns:
|
|
An awaitable to wait for the new content to be mounted.
|
|
"""
|
|
if id is not None and widget.id != id:
|
|
widget.id = id
|
|
|
|
if not widget.id:
|
|
raise ValueError(
|
|
"Widget must have an ID (or set id parameter when calling add_content)"
|
|
)
|
|
|
|
async def _add_content() -> None:
|
|
"""Add new widget and potentially change the current widget."""
|
|
widget.display = False
|
|
with self.app.batch_update():
|
|
await self.mount(widget)
|
|
if set_current:
|
|
self.current = widget.id
|
|
|
|
return AwaitComplete(_add_content())
|