Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tab hide disable #3112

Merged
merged 7 commits into from
Aug 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

- Fixed `page_up` and `page_down` bug in `DataTable` when `show_header = False` https://github.com/Textualize/textual/pull/3093
### Added

- Methods `TabbedContent.disable_tab` and `TabbedContent.enable_tab` https://github.com/Textualize/textual/pull/3112
- Methods `Tabs.disable` and `Tabs.enable` https://github.com/Textualize/textual/pull/3112
- Messages `Tab.Disabled`, `Tab.Enabled`, `Tabs.TabDisabled` and `Tabs.Enabled` https://github.com/Textualize/textual/pull/3112
- Methods `TabbedContent.hide_tab` and `TabbedContent.show_tab` https://github.com/Textualize/textual/pull/3112
- Methods `Tabs.hide` and `Tabs.show` https://github.com/Textualize/textual/pull/3112
- Messages `Tabs.TabHidden` and `Tabs.TabShown` https://github.com/Textualize/textual/pull/3112

### Changed

Expand All @@ -16,6 +23,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed

- Fixed auto height container with default grid-rows https://github.com/Textualize/textual/issues/1597
- Fixed `page_up` and `page_down` bug in `DataTable` when `show_header = False` https://github.com/Textualize/textual/pull/3093

## [0.33.0] - 2023-08-15

Expand Down
66 changes: 66 additions & 0 deletions src/textual/widgets/_tabbed_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -375,3 +375,69 @@ def _watch_active(self, active: str) -> None:
def tab_count(self) -> int:
"""Total number of tabs."""
return self.get_child_by_type(Tabs).tab_count

def _on_tabs_tab_disabled(self, event: Tabs.TabDisabled) -> None:
"""Disable the corresponding tab pane."""
event.stop()
tab_id = event.tab.id
try:
self.query_one(f"TabPane#{tab_id}").disabled = True
except NoMatches:
return

def _on_tabs_tab_enabled(self, event: Tabs.TabEnabled) -> None:
"""Enable the corresponding tab pane."""
event.stop()
tab_id = event.tab.id
try:
self.query_one(f"TabPane#{tab_id}").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.query_one(Tabs).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.query_one(Tabs).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.query_one(Tabs).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.query_one(Tabs).show(tab_id)
198 changes: 182 additions & 16 deletions src/textual/widgets/_tabs.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import ClassVar

import rich.repr
Expand Down Expand Up @@ -104,17 +105,42 @@ class Tab(Static):
Tab.-active:hover {
color: $text;
}
Tab:disabled {
color: $text-disabled;
text-opacity: 50%;
}
Tab.-hidden {
display: none;
}
"""

class Clicked(Message):
"""A tab was clicked."""
@dataclass
class TabMessage(Message):
"""Tab-related messages.

These are mostly intended for internal use when interacting with `Tabs`.
"""

tab: Tab
"""The tab that was clicked."""
"""The tab that is the object of this message."""

def __init__(self, tab: Tab) -> None:
self.tab = tab
super().__init__()
@property
def control(self) -> Tab:
"""The tab that is the object of this message.

This is an alias for the attribute `tab` and is used by the
[`on`][textual.on] decorator.
"""
return self.tab

class Clicked(TabMessage):
"""A tab was clicked."""

class Disabled(TabMessage):
"""A tab was disabled."""

class Enabled(TabMessage):
"""A tab was enabled."""

def __init__(
self,
Expand Down Expand Up @@ -143,6 +169,10 @@ def _on_click(self):
"""Inform the message that the tab was clicked."""
self.post_message(self.Clicked(self))

def _watch_disabled(self, disabled: bool) -> None:
"""Notify the parent `Tabs` that a tab was enabled/disabled."""
self.post_message(self.Disabled(self) if disabled else self.Enabled(self))


class Tabs(Widget, can_focus=True):
"""A row of tabs."""
Expand Down Expand Up @@ -184,8 +214,8 @@ class Tabs(Widget, can_focus=True):
class TabError(Exception):
"""Exception raised when there is an error relating to tabs."""

class TabActivated(Message):
"""Sent when a new tab is activated."""
class TabMessage(Message):
"""Parent class for all messages that have to do with a specific tab."""

ALLOW_SELECTOR_MATCH = {"tab"}
"""Additional message attributes that can be used with the [`on` decorator][textual.on]."""
Expand All @@ -195,27 +225,42 @@ def __init__(self, tabs: Tabs, tab: Tab) -> None:

Args:
tabs: The Tabs widget.
tab: The tab that was activated.
tab: The tab that is the object of this message.
"""
self.tabs: Tabs = tabs
"""The tabs widget containing the tab."""
self.tab: Tab = tab
"""The tab that was activated."""
"""The tab that is the object of this message."""
super().__init__()

@property
def control(self) -> Tabs:
"""The tabs widget containing the tab that was activated.
"""The tabs widget containing the tab that is the object of this message.

This is an alias for [`TabActivated.tabs`][textual.widgets.Tabs.TabActivated.tabs]
which is used by the [`on`][textual.on] decorator.
This is an alias for the attribute `tabs` and is used by the
[`on`][textual.on] decorator.
"""
return self.tabs

def __rich_repr__(self) -> rich.repr.Result:
yield self.tabs
yield self.tab

class TabActivated(TabMessage):
"""Sent when a new tab is activated."""

class TabDisabled(TabMessage):
"""Sent when a tab is disabled."""

class TabEnabled(TabMessage):
"""Sent when a tab is enabled."""

class TabHidden(TabMessage):
"""Sent when a tab is hidden."""

class TabShown(TabMessage):
"""Sent when a tab is shown."""

class Cleared(Message):
"""Sent when there are no active tabs."""

Expand Down Expand Up @@ -299,10 +344,24 @@ def tab_count(self) -> int:
"""Total number of tabs."""
return len(self.query("#tabs-list > Tab"))

@property
def _potentially_active_tabs(self) -> list[Tab]:
"""List of all tabs that could be active.

This list is comprised of all tabs that are shown and enabled,
plus the active tab in case it is disabled.
"""
return [
tab
for tab in self.query("#tabs-list > Tab").results(Tab)
if ((not tab.disabled or tab is self.active_tab) and tab.display)
]

@property
def _next_active(self) -> Tab | None:
"""Next tab to make active if the active tab is removed."""
tabs = list(self.query("#tabs-list > Tab").results(Tab))
active_tab = self.active_tab
tabs = self._potentially_active_tabs
if self.active_tab is None:
return None
try:
Expand Down Expand Up @@ -412,7 +471,7 @@ def remove_tab(self, tab_or_id: Tab | str | None) -> AwaitRemove:
"""Remove a tab.

Args:
tab_or_id: The Tab's id.
tab_or_id: The Tab to remove or its id.

Returns:
An awaitable object that waits for the tab to be removed.
Expand Down Expand Up @@ -590,10 +649,117 @@ def _move_tab(self, direction: int) -> None:
active_tab = self.active_tab
if active_tab is None:
return
tabs = list(self.query(Tab))
tabs = self._potentially_active_tabs
if not tabs:
return
tab_count = len(tabs)
new_tab_index = (tabs.index(active_tab) + direction) % tab_count
self.active = tabs[new_tab_index].id or ""
self._scroll_active_tab()

def _on_tab_disabled(self, event: Tab.Disabled) -> None:
"""Re-post the disabled message."""
event.stop()
self.post_message(self.TabDisabled(self, event.tab))

def _on_tab_enabled(self, event: Tab.Enabled) -> None:
"""Re-post the enabled message."""
event.stop()
self.post_message(self.TabEnabled(self, event.tab))

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.
"""

try:
tab_to_disable = self.query_one(f"#tabs-list > Tab#{tab_id}", Tab)
except NoMatches:
raise self.TabError(
f"There is no tab with ID {tab_id!r} to disable."
) from None

tab_to_disable.disabled = True
return tab_to_disable

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.
"""

try:
tab_to_enable = self.query_one(f"#tabs-list > Tab#{tab_id}", Tab)
except NoMatches:
raise self.TabError(
f"There is no tab with ID {tab_id!r} to enable."
) from None

tab_to_enable.disabled = False
return tab_to_enable

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.
"""

try:
tab_to_hide = self.query_one(f"#tabs-list > Tab#{tab_id}", Tab)
except NoMatches:
raise self.TabError(f"There is no tab with ID {tab_id!r} to hide.")

if tab_to_hide.has_class("-active"):
next_tab = self._next_active
self.active = next_tab.id or "" if next_tab else ""
tab_to_hide.add_class("-hidden")
self.post_message(self.TabHidden(self, tab_to_hide))
self.call_after_refresh(self._highlight_active)
return tab_to_hide

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.
"""

try:
tab_to_show = self.query_one(f"#tabs-list > Tab#{tab_id}", Tab)
except NoMatches:
raise self.TabError(f"There is no tab with ID {tab_id!r} to show.")

tab_to_show.remove_class("-hidden")
self.post_message(self.TabShown(self, tab_to_show))
if not self.active:
self._activate_tab(tab_to_show)
self.call_after_refresh(self._highlight_active)
return tab_to_show
Loading
Loading