717 lines
23 KiB
Python
717 lines
23 KiB
Python
from __future__ import annotations
|
|
|
|
from asyncio import gather
|
|
from dataclasses import dataclass
|
|
from itertools import zip_longest
|
|
from typing import Awaitable
|
|
|
|
from rich.repr import Result
|
|
from typing_extensions import Final
|
|
|
|
from textual import events
|
|
from textual.app import ComposeResult
|
|
from textual.await_complete import AwaitComplete
|
|
from textual.content import ContentText, ContentType
|
|
from textual.css.query import NoMatches
|
|
from textual.message import Message
|
|
from textual.reactive import reactive
|
|
from textual.widget import Widget
|
|
from textual.widgets._content_switcher import ContentSwitcher
|
|
from textual.widgets._tabs import Tab, Tabs
|
|
|
|
__all__ = [
|
|
"ContentTab",
|
|
"TabbedContent",
|
|
"TabPane",
|
|
]
|
|
|
|
|
|
class ContentTab(Tab):
|
|
"""A Tab with an associated content id."""
|
|
|
|
_PREFIX: Final[str] = "--content-tab-"
|
|
"""The prefix given to the tab IDs."""
|
|
|
|
@classmethod
|
|
def add_prefix(cls, content_id: str) -> str:
|
|
"""Add the prefix to the given ID.
|
|
|
|
Args:
|
|
content_id: The ID to add the prefix to.
|
|
|
|
Returns:
|
|
The ID with the prefix added.
|
|
"""
|
|
return f"{cls._PREFIX}{content_id}" if content_id else content_id
|
|
|
|
@classmethod
|
|
def sans_prefix(cls, content_id: str) -> str:
|
|
"""Remove the prefix from the given ID.
|
|
|
|
Args:
|
|
content_id: The ID to remove the prefix from.
|
|
|
|
Returns:
|
|
The ID with the prefix removed.
|
|
"""
|
|
return (
|
|
content_id[len(cls._PREFIX) :]
|
|
if content_id.startswith(cls._PREFIX)
|
|
else content_id
|
|
)
|
|
|
|
def __init__(
|
|
self, label: ContentType, content_id: str, disabled: bool = False
|
|
) -> None:
|
|
"""Initialize a ContentTab.
|
|
|
|
Args:
|
|
label: The label to be displayed within the tab.
|
|
content_id: The id of the content associated with the tab.
|
|
disabled: Is the tab disabled?
|
|
"""
|
|
super().__init__(label, id=self.add_prefix(content_id), disabled=disabled)
|
|
|
|
|
|
class ContentTabs(Tabs):
|
|
"""A Tabs which is associated with a TabbedContent."""
|
|
|
|
def __init__(
|
|
self,
|
|
*tabs: Tab | ContentText,
|
|
active: str | None = None,
|
|
tabbed_content: TabbedContent,
|
|
):
|
|
"""Initialize a ContentTabs.
|
|
|
|
Args:
|
|
*tabs: The child tabs.
|
|
active: ID of the tab which should be active on start.
|
|
tabbed_content: The associated TabbedContent instance.
|
|
"""
|
|
super().__init__(
|
|
*tabs, active=active if active is None else ContentTab.add_prefix(active)
|
|
)
|
|
self.tabbed_content = tabbed_content
|
|
|
|
def get_content_tab(self, tab_id: str) -> ContentTab:
|
|
"""Get the `ContentTab` associated with the given `TabPane` ID.
|
|
|
|
Args:
|
|
tab_id: The ID of the tab to get.
|
|
|
|
Returns:
|
|
The tab associated with that ID.
|
|
"""
|
|
return self.query_one(f"#{ContentTab.add_prefix(tab_id)}", ContentTab)
|
|
|
|
def disable(self, tab_id: str) -> Tab:
|
|
"""Disable the indicated tab.
|
|
|
|
Args:
|
|
tab_id: The ID of the [`Tab`][textual.widgets.Tab] to disable.
|
|
|
|
Returns:
|
|
The [`Tab`][textual.widgets.Tab] that was targeted.
|
|
|
|
Raises:
|
|
TabError: If there are any issues with the request.
|
|
"""
|
|
return super().disable(ContentTab.add_prefix(tab_id))
|
|
|
|
def enable(self, tab_id: str) -> Tab:
|
|
"""Enable the indicated tab.
|
|
|
|
Args:
|
|
tab_id: The ID of the [`Tab`][textual.widgets.Tab] to enable.
|
|
|
|
Returns:
|
|
The [`Tab`][textual.widgets.Tab] that was targeted.
|
|
|
|
Raises:
|
|
TabError: If there are any issues with the request.
|
|
"""
|
|
return super().enable(ContentTab.add_prefix(tab_id))
|
|
|
|
def hide(self, tab_id: str) -> Tab:
|
|
"""Hide the indicated tab.
|
|
|
|
Args:
|
|
tab_id: The ID of the [`Tab`][textual.widgets.Tab] to hide.
|
|
|
|
Returns:
|
|
The [`Tab`][textual.widgets.Tab] that was targeted.
|
|
|
|
Raises:
|
|
TabError: If there are any issues with the request.
|
|
"""
|
|
return super().hide(ContentTab.add_prefix(tab_id))
|
|
|
|
def show(self, tab_id: str) -> Tab:
|
|
"""Show the indicated tab.
|
|
|
|
Args:
|
|
tab_id: The ID of the [`Tab`][textual.widgets.Tab] to show.
|
|
|
|
Returns:
|
|
The [`Tab`][textual.widgets.Tab] that was targeted.
|
|
|
|
Raises:
|
|
TabError: If there are any issues with the request.
|
|
"""
|
|
return super().show(ContentTab.add_prefix(tab_id))
|
|
|
|
|
|
class TabPane(Widget):
|
|
"""A container for switchable content, with additional title.
|
|
|
|
This widget is intended to be used with [TabbedContent][textual.widgets.TabbedContent].
|
|
"""
|
|
|
|
DEFAULT_CSS = """
|
|
TabPane {
|
|
height: auto;
|
|
}
|
|
"""
|
|
|
|
@dataclass
|
|
class TabPaneMessage(Message):
|
|
"""Base class for `TabPane` messages."""
|
|
|
|
tab_pane: TabPane
|
|
"""The `TabPane` that is he object of this message."""
|
|
|
|
@property
|
|
def control(self) -> TabPane:
|
|
"""The tab pane that is the object of this message.
|
|
|
|
This is an alias for the attribute `tab_pane` and is used by the
|
|
[`on`][textual.on] decorator.
|
|
"""
|
|
return self.tab_pane
|
|
|
|
@dataclass
|
|
class Disabled(TabPaneMessage):
|
|
"""Sent when a tab pane is disabled via its reactive `disabled`."""
|
|
|
|
@dataclass
|
|
class Enabled(TabPaneMessage):
|
|
"""Sent when a tab pane is enabled via its reactive `disabled`."""
|
|
|
|
@dataclass
|
|
class Focused(TabPaneMessage):
|
|
"""Sent when a child widget is focused."""
|
|
|
|
def __init__(
|
|
self,
|
|
title: ContentType,
|
|
*children: Widget,
|
|
name: str | None = None,
|
|
id: str | None = None,
|
|
classes: str | None = None,
|
|
disabled: bool = False,
|
|
):
|
|
"""Initialize a TabPane.
|
|
|
|
Args:
|
|
title: Title of the TabPane (will be displayed in a tab label).
|
|
*children: Widget to go inside the TabPane.
|
|
name: Optional name for the TabPane.
|
|
id: Optional ID for the TabPane.
|
|
classes: Optional initial classes for the widget.
|
|
disabled: Whether the TabPane is disabled or not.
|
|
"""
|
|
self._title = self.render_str(title)
|
|
super().__init__(
|
|
*children, name=name, id=id, classes=classes, disabled=disabled
|
|
)
|
|
|
|
def _watch_disabled(self, disabled: bool) -> None:
|
|
"""Notify the parent `TabbedContent` that a tab pane was enabled/disabled."""
|
|
self.post_message(self.Disabled(self) if disabled else self.Enabled(self))
|
|
|
|
def _on_descendant_focus(self, event: events.DescendantFocus):
|
|
"""Tell TabbedContent parent something is focused in this pane."""
|
|
self.post_message(self.Focused(self))
|
|
|
|
|
|
class TabbedContent(Widget):
|
|
"""A container with associated tabs to toggle content visibility."""
|
|
|
|
ALLOW_MAXIMIZE = True
|
|
DEFAULT_CSS = """
|
|
TabbedContent {
|
|
height: auto;
|
|
&> ContentTabs {
|
|
dock: top;
|
|
}
|
|
}
|
|
"""
|
|
|
|
active: reactive[str] = reactive("", init=False)
|
|
"""The ID of the active tab, or empty string if none are active."""
|
|
|
|
class TabActivated(Message):
|
|
"""Posted when the active tab changes."""
|
|
|
|
ALLOW_SELECTOR_MATCH = {"pane"}
|
|
"""Additional message attributes that can be used with the [`on` decorator][textual.on]."""
|
|
|
|
def __init__(self, tabbed_content: TabbedContent, tab: ContentTab) -> None:
|
|
"""Initialize message.
|
|
|
|
Args:
|
|
tabbed_content: The TabbedContent widget.
|
|
tab: The Tab widget that was selected (contains the tab label).
|
|
"""
|
|
self.tabbed_content = tabbed_content
|
|
"""The `TabbedContent` widget that contains the tab activated."""
|
|
self.tab = tab
|
|
"""The `Tab` widget that was selected (contains the tab label)."""
|
|
self.pane = tabbed_content.get_pane(tab)
|
|
"""The `TabPane` widget that was activated by selecting the tab."""
|
|
super().__init__()
|
|
|
|
@property
|
|
def control(self) -> TabbedContent:
|
|
"""The `TabbedContent` widget that contains the tab activated.
|
|
|
|
This is an alias for [`TabActivated.tabbed_content`][textual.widgets.TabbedContent.TabActivated.tabbed_content]
|
|
and is used by the [`on`][textual.on] decorator.
|
|
"""
|
|
return self.tabbed_content
|
|
|
|
def __rich_repr__(self) -> Result:
|
|
yield self.tabbed_content
|
|
yield self.tab
|
|
yield self.pane
|
|
|
|
class Cleared(Message):
|
|
"""Posted when no tab pane is active.
|
|
|
|
This can happen if all tab panes are removed or if the currently active tab
|
|
pane is unset.
|
|
"""
|
|
|
|
def __init__(self, tabbed_content: TabbedContent) -> None:
|
|
"""Initialize message.
|
|
|
|
Args:
|
|
tabbed_content: The TabbedContent widget.
|
|
"""
|
|
self.tabbed_content = tabbed_content
|
|
"""The `TabbedContent` widget that contains the tab activated."""
|
|
super().__init__()
|
|
|
|
@property
|
|
def control(self) -> TabbedContent:
|
|
"""The `TabbedContent` widget that was cleared of all tab panes.
|
|
|
|
This is an alias for [`Cleared.tabbed_content`][textual.widgets.TabbedContent.Cleared.tabbed_content]
|
|
and is used by the [`on`][textual.on] decorator.
|
|
"""
|
|
return self.tabbed_content
|
|
|
|
def __init__(
|
|
self,
|
|
*titles: ContentType,
|
|
initial: str = "",
|
|
name: str | None = None,
|
|
id: str | None = None,
|
|
classes: str | None = None,
|
|
disabled: bool = False,
|
|
):
|
|
"""Initialize a TabbedContent widgets.
|
|
|
|
Args:
|
|
*titles: Positional argument will be used as title.
|
|
initial: The id of the initial tab, or empty string to select the first tab.
|
|
name: The name of the tabbed content.
|
|
id: The ID of the tabbed content in the DOM.
|
|
classes: The CSS classes of the tabbed content.
|
|
disabled: Whether the tabbed content is disabled or not.
|
|
"""
|
|
self.titles = [self.render_str(title) for title in titles]
|
|
self._tab_content: list[Widget] = []
|
|
self._initial = initial
|
|
self._tab_counter = 0
|
|
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
|
|
|
|
@property
|
|
def active_pane(self) -> TabPane | None:
|
|
"""The currently active pane, or `None` if no pane is active."""
|
|
active = self.active
|
|
if not active:
|
|
return None
|
|
return self.get_pane(self.active)
|
|
|
|
@staticmethod
|
|
def _set_id(content: TabPane, new_id: int) -> TabPane:
|
|
"""Set an id on the content, if not already present.
|
|
|
|
Args:
|
|
content: a TabPane.
|
|
new_id: Numeric ID to make the pane ID from.
|
|
|
|
Returns:
|
|
The same TabPane.
|
|
"""
|
|
if content.id is None:
|
|
content.id = f"tab-{new_id}"
|
|
return content
|
|
|
|
def _generate_tab_id(self) -> int:
|
|
"""Auto generate a new tab id.
|
|
|
|
Returns:
|
|
An auto-incrementing integer.
|
|
"""
|
|
self._tab_counter += 1
|
|
return self._tab_counter
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Compose the tabbed content."""
|
|
|
|
# Wrap content in a `TabPane` if required.
|
|
pane_content = [
|
|
self._set_id(
|
|
(
|
|
content
|
|
if isinstance(content, TabPane)
|
|
else TabPane(title or self.render_str(f"Tab {index}"), content)
|
|
),
|
|
self._generate_tab_id(),
|
|
)
|
|
for index, (title, content) in enumerate(
|
|
zip_longest(self.titles, self._tab_content), 1
|
|
)
|
|
]
|
|
# Get a tab for each pane
|
|
tabs = [
|
|
ContentTab(
|
|
content._title,
|
|
content.id or "",
|
|
disabled=content.disabled,
|
|
)
|
|
for content in pane_content
|
|
]
|
|
|
|
# Yield the tabs, and ensure they're linked to this TabbedContent.
|
|
# It's important to associate the Tabs with the TabbedContent, so that this
|
|
# TabbedContent can determine whether a message received from a Tabs instance
|
|
# has been sent from this Tabs, or from a Tabs that may exist as a descendant
|
|
# deeper in the DOM.
|
|
yield ContentTabs(*tabs, active=self._initial or None, tabbed_content=self)
|
|
|
|
# Yield the content switcher and panes
|
|
with ContentSwitcher(initial=self._initial or None):
|
|
yield from pane_content
|
|
|
|
def add_pane(
|
|
self,
|
|
pane: TabPane,
|
|
*,
|
|
before: TabPane | str | None = None,
|
|
after: TabPane | str | None = None,
|
|
) -> AwaitComplete:
|
|
"""Add a new pane to the tabbed content.
|
|
|
|
Args:
|
|
pane: The pane to add.
|
|
before: Optional pane or pane ID to add the pane before.
|
|
after: Optional pane or pane ID to add the pane after.
|
|
|
|
Returns:
|
|
An optionally awaitable object that waits for the pane to be added.
|
|
|
|
Raises:
|
|
Tabs.TabError: If there is a problem with the addition request.
|
|
|
|
Note:
|
|
Only one of `before` or `after` can be provided. If both are
|
|
provided an exception is raised.
|
|
"""
|
|
if isinstance(before, TabPane):
|
|
before = before.id
|
|
if isinstance(after, TabPane):
|
|
after = after.id
|
|
tabs = self.get_child_by_type(ContentTabs)
|
|
pane = self._set_id(pane, self._generate_tab_id())
|
|
assert pane.id is not None
|
|
pane.display = False
|
|
return AwaitComplete(
|
|
tabs.add_tab(
|
|
ContentTab(pane._title, pane.id),
|
|
before=before if before is None else ContentTab.add_prefix(before),
|
|
after=after if after is None else ContentTab.add_prefix(after),
|
|
),
|
|
self.get_child_by_type(ContentSwitcher).mount(pane),
|
|
)
|
|
|
|
def remove_pane(self, pane_id: str) -> AwaitComplete:
|
|
"""Remove a given pane from the tabbed content.
|
|
|
|
Args:
|
|
pane_id: The ID of the pane to remove.
|
|
|
|
Returns:
|
|
An optionally awaitable object that waits for the pane to be removed
|
|
and the Cleared message to be posted.
|
|
"""
|
|
removal_awaitables: list[Awaitable] = [
|
|
self.get_child_by_type(ContentTabs).remove_tab(
|
|
ContentTab.add_prefix(pane_id)
|
|
)
|
|
]
|
|
try:
|
|
removal_awaitables.append(
|
|
self.get_child_by_type(ContentSwitcher)
|
|
.get_child_by_id(pane_id)
|
|
.remove()
|
|
)
|
|
except NoMatches:
|
|
# It's possible that the content itself may have gone away via
|
|
# other means; so allow that to be a no-op.
|
|
pass
|
|
|
|
return AwaitComplete(*removal_awaitables)
|
|
|
|
def clear_panes(self) -> AwaitComplete:
|
|
"""Remove all the panes in the tabbed content.
|
|
|
|
Returns:
|
|
An optionally awaitable object which waits for all panes to be removed
|
|
and the Cleared message to be posted.
|
|
"""
|
|
await_clear = gather(
|
|
self.get_child_by_type(ContentTabs).clear(),
|
|
self.get_child_by_type(ContentSwitcher).remove_children(),
|
|
)
|
|
|
|
async def _clear_content() -> None:
|
|
await await_clear
|
|
|
|
return AwaitComplete(_clear_content())
|
|
|
|
def compose_add_child(self, widget: Widget) -> None:
|
|
"""When using the context manager compose syntax, we want to attach nodes to the switcher.
|
|
|
|
Args:
|
|
widget: A Widget to add.
|
|
"""
|
|
self._tab_content.append(widget)
|
|
|
|
def _on_tabs_tab_activated(self, event: Tabs.TabActivated) -> None:
|
|
"""User clicked a tab."""
|
|
if self._is_associated_tabs(event.tabs):
|
|
# The message is relevant, so consume it and update state accordingly.
|
|
event.stop()
|
|
assert event.tab.id is not None
|
|
switcher = self.get_child_by_type(ContentSwitcher)
|
|
switcher.current = ContentTab.sans_prefix(event.tab.id)
|
|
with self.prevent(self.TabActivated):
|
|
# We prevent TabbedContent.TabActivated because it is also
|
|
# posted from the watcher for active, we're also about to
|
|
# post it below too, which is valid as here we're reacting
|
|
# to what the Tabs are doing. This ensures we don't get
|
|
# doubled-up messages.
|
|
self.active = ContentTab.sans_prefix(event.tab.id)
|
|
self.post_message(
|
|
TabbedContent.TabActivated(
|
|
tabbed_content=self,
|
|
tab=self.get_child_by_type(ContentTabs).get_content_tab(
|
|
self.active
|
|
),
|
|
)
|
|
)
|
|
|
|
def _on_tab_pane_focused(self, event: TabPane.Focused) -> None:
|
|
"""One of the panes contains a widget that was programmatically focused."""
|
|
event.stop()
|
|
if event.tab_pane.id is not None:
|
|
self.active = event.tab_pane.id
|
|
|
|
def _on_tabs_cleared(self, event: Tabs.Cleared) -> None:
|
|
"""Called when there are no active tabs. The tabs may have been cleared,
|
|
or they may all be hidden."""
|
|
if self._is_associated_tabs(event.tabs):
|
|
event.stop()
|
|
self.get_child_by_type(ContentSwitcher).current = None
|
|
self.active = ""
|
|
|
|
def _is_associated_tabs(self, tabs: Tabs) -> bool:
|
|
"""Determine whether a tab is associated with this TabbedContent or not.
|
|
|
|
A tab is "associated" with a `TabbedContent`, if it's one of the tabs that can
|
|
be used to control it. These have a special type: `ContentTab`, and are linked
|
|
back to this `TabbedContent` instance via a `tabbed_content` attribute.
|
|
|
|
Args:
|
|
tabs: The Tabs instance to check.
|
|
|
|
Returns:
|
|
True if the tab is associated with this `TabbedContent`.
|
|
"""
|
|
return isinstance(tabs, ContentTabs) and tabs.tabbed_content is self
|
|
|
|
def _watch_active(self, active: str) -> None:
|
|
"""Switch tabs when the active attributes changes."""
|
|
with self.prevent(Tabs.TabActivated, Tabs.Cleared):
|
|
self.get_child_by_type(ContentTabs).active = ContentTab.add_prefix(active)
|
|
self.get_child_by_type(ContentSwitcher).current = active
|
|
if active:
|
|
self.post_message(
|
|
TabbedContent.TabActivated(
|
|
tabbed_content=self,
|
|
tab=self.get_child_by_type(ContentTabs).get_content_tab(active),
|
|
)
|
|
)
|
|
else:
|
|
self.post_message(
|
|
TabbedContent.Cleared(tabbed_content=self).set_sender(self)
|
|
)
|
|
|
|
@property
|
|
def tab_count(self) -> int:
|
|
"""Total number of tabs."""
|
|
return self.get_child_by_type(ContentTabs).tab_count
|
|
|
|
def get_tab(self, pane_id: str | TabPane) -> Tab:
|
|
"""Get the `Tab` associated with the given ID or `TabPane`.
|
|
|
|
Args:
|
|
pane_id: The ID of the pane, or the pane itself.
|
|
|
|
Returns:
|
|
The Tab associated with the ID.
|
|
|
|
Raises:
|
|
ValueError: Raised if no ID was available.
|
|
"""
|
|
if target_id := (pane_id if isinstance(pane_id, str) else pane_id.id):
|
|
return self.get_child_by_type(ContentTabs).get_content_tab(target_id)
|
|
raise ValueError(
|
|
"'pane_id' must be a non-empty string or a TabPane with an id."
|
|
)
|
|
|
|
def get_pane(self, pane_id: str | ContentTab) -> TabPane:
|
|
"""Get the `TabPane` associated with the given ID or tab.
|
|
|
|
Args:
|
|
pane_id: The ID of the pane to get, or the Tab it is associated with.
|
|
|
|
Returns:
|
|
The `TabPane` associated with the ID or the given tab.
|
|
|
|
Raises:
|
|
ValueError: Raised if no ID was available.
|
|
"""
|
|
target_id: str | None = None
|
|
if isinstance(pane_id, ContentTab):
|
|
target_id = (
|
|
pane_id.id if pane_id.id is None else ContentTab.sans_prefix(pane_id.id)
|
|
)
|
|
else:
|
|
target_id = pane_id
|
|
if target_id:
|
|
pane = self.get_child_by_type(ContentSwitcher).get_child_by_id(target_id)
|
|
assert isinstance(pane, TabPane)
|
|
return pane
|
|
raise ValueError(
|
|
"'pane_id' must be a non-empty string or a ContentTab with an id."
|
|
)
|
|
|
|
def _on_tabs_tab_disabled(self, event: Tabs.TabDisabled) -> None:
|
|
"""Disable the corresponding tab pane."""
|
|
if event.tabs.parent is not self:
|
|
return
|
|
event.stop()
|
|
tab_id = event.tab.id or ""
|
|
try:
|
|
with self.prevent(TabPane.Disabled):
|
|
self.get_child_by_type(ContentSwitcher).get_child_by_id(
|
|
ContentTab.sans_prefix(tab_id), expect_type=TabPane
|
|
).disabled = True
|
|
except NoMatches:
|
|
return
|
|
|
|
def _on_tab_pane_disabled(self, event: TabPane.Disabled) -> None:
|
|
"""Disable the corresponding tab."""
|
|
event.stop()
|
|
try:
|
|
with self.prevent(Tab.Disabled):
|
|
self.get_tab(event.tab_pane).disabled = True
|
|
except NoMatches:
|
|
return
|
|
|
|
def _on_tabs_tab_enabled(self, event: Tabs.TabEnabled) -> None:
|
|
"""Enable the corresponding tab pane."""
|
|
if event.tabs.parent is not self:
|
|
return
|
|
event.stop()
|
|
tab_id = event.tab.id or ""
|
|
try:
|
|
with self.prevent(TabPane.Enabled):
|
|
self.get_child_by_type(ContentSwitcher).get_child_by_id(
|
|
ContentTab.sans_prefix(tab_id), expect_type=TabPane
|
|
).disabled = False
|
|
except NoMatches:
|
|
return
|
|
|
|
def _on_tab_pane_enabled(self, event: TabPane.Enabled) -> None:
|
|
"""Enable the corresponding tab."""
|
|
event.stop()
|
|
try:
|
|
with self.prevent(Tab.Disabled):
|
|
self.get_tab(event.tab_pane).disabled = False
|
|
except NoMatches:
|
|
return
|
|
|
|
def disable_tab(self, tab_id: str) -> None:
|
|
"""Disables the tab with the given ID.
|
|
|
|
Args:
|
|
tab_id: The ID of the [`TabPane`][textual.widgets.TabPane] to disable.
|
|
|
|
Raises:
|
|
Tabs.TabError: If there are any issues with the request.
|
|
"""
|
|
|
|
self.get_child_by_type(ContentTabs).disable(tab_id)
|
|
|
|
def enable_tab(self, tab_id: str) -> None:
|
|
"""Enables the tab with the given ID.
|
|
|
|
Args:
|
|
tab_id: The ID of the [`TabPane`][textual.widgets.TabPane] to enable.
|
|
|
|
Raises:
|
|
Tabs.TabError: If there are any issues with the request.
|
|
"""
|
|
|
|
self.get_child_by_type(ContentTabs).enable(tab_id)
|
|
|
|
def hide_tab(self, tab_id: str) -> None:
|
|
"""Hides the tab with the given ID.
|
|
|
|
Args:
|
|
tab_id: The ID of the [`TabPane`][textual.widgets.TabPane] to hide.
|
|
|
|
Raises:
|
|
Tabs.TabError: If there are any issues with the request.
|
|
"""
|
|
|
|
self.get_child_by_type(ContentTabs).hide(tab_id)
|
|
|
|
def show_tab(self, tab_id: str) -> None:
|
|
"""Shows the tab with the given ID.
|
|
|
|
Args:
|
|
tab_id: The ID of the [`TabPane`][textual.widgets.TabPane] to show.
|
|
|
|
Raises:
|
|
Tabs.TabError: If there are any issues with the request.
|
|
"""
|
|
|
|
self.get_child_by_type(ContentTabs).show(tab_id)
|