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

Widget collapsible #2989

Merged
merged 34 commits into from
Sep 14, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6d827ff
Collapsible container widget.
Jul 22, 2023
7850728
Expose collapsible widget.
Jul 22, 2023
9e81a39
Add collapsible container example
Jul 22, 2023
489641b
Rename member variables as label and apply formatting
Jul 22, 2023
ad59a98
Apply hover effect
Jul 22, 2023
7525a75
Apply formatting
Jul 22, 2023
3909a0e
Add collapsible construction example with children.
Jul 22, 2023
7e27c82
Wrap contents within Container and move _collapsed flag to Collapsibl…
Jul 27, 2023
e0272f5
Add collapsible example that is expanded by default.
Jul 27, 2023
0ac8fc4
Update collapsed property to be reactive
Jul 28, 2023
934a240
Add footer to collapse and expand all with bound keys.
Jul 28, 2023
156cdd9
Expose summary property of Collapsible
Jul 28, 2023
3c36c8a
Assign ids of ollapsed, expanded label instead of classes
Jul 28, 2023
2e1adc4
Add unit tests of Collapsible
Jul 28, 2023
252288b
Rename class Summary to Title
Jul 28, 2023
6e4816e
Rename variables of expanded/collapsed symbols and add it to arguments..
Jul 31, 2023
3ddca8b
Add documentation for Collapsible
Jul 31, 2023
99ce8db
Update symbol ids of Collapsible title
Jul 31, 2023
a5eb5af
Update src/textual/widgets/_collapsible.py
YooSunYoung Aug 9, 2023
3fba4a7
Sort module names in alphabetical order
Aug 9, 2023
eb1e2a7
Clarify that collapsible is non-focusable in documentation.
Aug 9, 2023
e247d96
Add version hint
Aug 9, 2023
b8df854
Fix documentation of Collapsible.
Aug 9, 2023
6236ccc
Add snapshot test for collapsible widget
Aug 14, 2023
8f7c700
Stop on click event from Collapsible.
Aug 14, 2023
581d9c4
Handle Title.Toggle event to prevent event in Contents from propagati…
Aug 23, 2023
6bd3a5d
Update Collapsible default css to have 1 fraction of width instead of…
Aug 23, 2023
d3cddb0
Update Collapsible custom symbol snapshot
Aug 23, 2023
8b49b96
Add Collapsible custom symbol snapshot as an example
Aug 23, 2023
156533b
Update docs/widgets/collapsible.md
YooSunYoung Aug 29, 2023
2766a6c
Update src/textual/widgets/_collapsible.py
YooSunYoung Aug 29, 2023
75eb9bd
Fix typo in Collapsible docs
Aug 30, 2023
6f5ec39
Rework collapsible documentation.
rodrigogiraoserrao Aug 30, 2023
a166ec2
Merge branch 'main' into widget-collapsible
rodrigogiraoserrao Aug 30, 2023
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
47 changes: 47 additions & 0 deletions docs/examples/widgets/collapsible.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from textual.app import App, ComposeResult
from textual.widgets import Collapsible, Footer, Label, Markdown

LETO = """
# Duke Leto I Atreides

Head of House Atreides.
"""

JESSICA = """
# Lady Jessica

Bene Gesserit and concubine of Leto, and mother of Paul and Alia.
"""

PAUL = """
# Paul Atreides

Son of Leto and Jessica.
"""


class CollapsibleApp(App):
"""An example of colllapsible container."""

BINDINGS = [
("c", "collapse_or_expand(True)", "Collapse All"),
("e", "collapse_or_expand(False)", "Expand All"),
]

def compose(self) -> ComposeResult:
"""Compose app with collapsible containers."""
yield Footer()
with Collapsible(collapsed=False, title="Leto"):
yield Label(LETO)
yield Collapsible(Markdown(JESSICA), collapsed=False, title="Jessica")
with Collapsible(collapsed=True, title="Paul"):
yield Markdown(PAUL)

def action_collapse_or_expand(self, collapse: bool) -> None:
for child in self.walk_children(Collapsible):
child.collapsed = collapse


if __name__ == "__main__":
app = CollapsibleApp()
app.run()
100 changes: 100 additions & 0 deletions docs/widgets/collapsible.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You followed the structure of the documentation well and added good examples.
However, I'd like to polish the English sentences a bit more.

Would you like to have another go at them, or would you prefer I go over them and make concrete change suggestions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you...!
I fixed some weird sentences but it'd be better if you make suggestions as well : D...!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're on the right track!

I was also looking at your sections where you explain how to change the title, the collapsed state, etc.
Now that you have the tests that cover everything, you also have apps that show how all of that is done, correct?

Take a look at the Button documentation: https://textual.textualize.io/widgets/button/#example
The “Example” section has a couple of tabs with an app screenshot and the code for an app.
That's created by this syntax:

=== "Output"
```{.textual path="docs/examples/widgets/button.py"}
```
=== "button.py"
```python
--8<-- "docs/examples/widgets/button.py"
```
=== "button.css"
```sass
--8<-- "docs/examples/widgets/button.css"
```

Now that you have example apps for all of that (because of the screenshot tests) you can also add them to the documentation directly!

So, you can essentially move your sections inside the example, and automatically include screenshots of the apps so that people see the changes.

How does this sound?
If this sounds like a good idea, you can try to do it.

You can use the command make docs-serve-offline to build the documentation and see it live, which should help you make sure the screenshots are ending up where they need to be.

Finally, inside that funny syntax {.textual path="..."} you can also add press="key1,key2,..." to press keys before the screenshot.
For example, {.textual path="path/to/my/app.py" press="e,a,enter"} would press the keys E, A, Enter before taking the screenshot.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I understood what you mean! I will re-organize them again after work.... thanks...!

Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Collapsible

<!-- !!! tip "Added in version .." -->
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved

Collapsible contents with title.

rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
- [x] Container

This widget wraps other widgets as `Contents` and control the visibility.

## Composing

There are two ways to wrap other widgets.
You can pass them as positional arguments to the [Collapsible][textual.widgets.Collapsible] constructor:

```python
def compose(self) -> ComposeResult:
yield Collapsible(Label("Verbose sentence to show by default."))

```

Alternatively you can compose other widgets under the context manager:

```python
def compose(self) -> ComposeResult:
with Collapsible():
yield Label("Verbose sentence to show by default.")

```

## Title

`Collapsible` can have a custom title instead of "Toggle" by `title` argument of the constructor:

```python
def compose(self) -> ComposeResult:
with Collapsible(title="An interesting story."):
yield Label("Interesting but verbose story.")

```

## Collapse/Expand Symbols

`Collapsible` can have different symbol(label)s for each expanded/collapsed status.

```python
def compose(self) -> ComposeResult:
with Collapsible(title="", collapsed_symbol="► Show more", expanded_symbol="▼ Close"):
yield Label("Many words.")

```

## Collapse/Expand

If can set the initial status of `collapsed` by `collapsed` argument of the constructor:

```python
def compose(self) -> ComposeResult:
with Collapsible(title="Contents 1", collapsed=False):
yield Label("Short sentence to show by default.")

with Collapsible(title="Contents 2", collapsed=True): # Default is True
yield Label("Verbose unecessary sentence to show by default.")
```

## Example

The following example contains three `Collapsible`s.

=== "Output"

```{.textual path="docs/examples/widgets/collapsible.py"}
```

=== "collapsible.py"

```python
--8<-- "docs/examples/widgets/collapsible.py"
```

## Reactive attributes

| Name | Type | Default | Description |
| ----------- | ------ | ------- | -------------------------------------------------------------- |
| `collapsed` | `bool` | `True` | Invisibility of `Contents`. Set it `False` to show `Contents`. |

## Messages

- [Collapsible.Summary.Toggle][Collapsible.Summary.Toggle]
YooSunYoung marked this conversation as resolved.
Show resolved Hide resolved

## See also

<!-- TODO: Add Accordion widgets later -->

---


::: textual.widgets.Collapsible
options:
heading_level: 2
3 changes: 2 additions & 1 deletion src/textual/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from ..widget import Widget
from ._button import Button
from ._checkbox import Checkbox
from ._collapsible import Collapsible
from ._content_switcher import ContentSwitcher
from ._data_table import DataTable
from ._directory_tree import DirectoryTree
Expand Down Expand Up @@ -41,7 +42,6 @@
from ._tree import Tree
from ._welcome import Welcome


__all__ = [
"Button",
"Checkbox",
Expand Down Expand Up @@ -76,6 +76,7 @@
"Tooltip",
"Tree",
"Welcome",
"Collapsible",
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
]

_WIDGETS_LAZY_LOADING_CACHE: dict[str, type[Widget]] = {}
Expand Down
1 change: 1 addition & 0 deletions src/textual/widgets/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# This stub file must re-export every classes exposed in the __init__.py's `__all__` list:
from ._button import Button as Button
from ._checkbox import Checkbox as Checkbox
from ._collapsible import Collapsible as Collapsible
from ._content_switcher import ContentSwitcher as ContentSwitcher
from ._data_table import DataTable as DataTable
from ._directory_tree import DirectoryTree as DirectoryTree
Expand Down
155 changes: 155 additions & 0 deletions src/textual/widgets/_collapsible.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
from __future__ import annotations

from textual.widget import Widget

from .. import events, on
from ..app import ComposeResult
from ..containers import Container, Horizontal
from ..message import Message
from ..reactive import reactive
from ..widget import Widget
from ._label import Label
YooSunYoung marked this conversation as resolved.
Show resolved Hide resolved

__all__ = ["Collapsible"]


class Collapsible(Widget):
"""A collapsible container."""

collapsed = reactive(True)

DEFAULT_CSS = """
Collapsible {
width: 100%;
height: auto;
}
"""

class Title(Horizontal):
DEFAULT_CSS = """
Title {
width: 100%;
height: auto;
}

Title:hover {
background: grey;
}

Title .label {
padding: 0 0 0 1;
}

Title #collapsed-symbol {
display:none;
}

Title.-collapsed #expanded-symbol {
display:none;
}

Title.-collapsed #collapsed-symbol {
display:block;
}
"""

def __init__(
self,
*,
label: str,
collapsed_symbol: str,
expanded_symbol: str,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
) -> None:
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
self.collapsed_symbol = collapsed_symbol
self.expanded_symbol = expanded_symbol
self.label = label

class Toggle(Message):
"""Request toggle."""

async def _on_click(self, event: events.Click) -> None:
"""Inform ancestor we want to toggle."""
self.post_message(self.Toggle())
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved

def compose(self) -> ComposeResult:
"""Compose right/down arrow and label."""
yield Label(self.expanded_symbol, classes="label", id="expanded-symbol")
yield Label(self.collapsed_symbol, classes="label", id="collapsed-symbol")
yield Label(self.label, classes="label")

class Contents(Container):
DEFAULT_CSS = """
Contents {
width: 100%;
height: auto;
padding: 0 0 0 3;
}

Contents.-collapsed {
display: none;
}
"""

def __init__(
self,
*children: Widget,
title: str = "Toggle",
collapsed: bool = True,
collapsed_symbol: str = "►",
expanded_symbol: str = "▼",
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
) -> None:
"""Initialize a Collapsible widget.

Args:
*children: Contents that will be collapsed/expanded.
title: Title of the collapsed/expanded contents.
collapsed: Default status of the contents.
collapsed_symbol: Collapsed symbol before the title.
expanded_symbol: Expanded symbol before the title.
name: The name of the collapsible.
id: The ID of the collapsible in the DOM.
classes: The CSS classes of the collapsible.
disabled: Whether the collapsible is disabled or not.
"""
self._title = self.Title(
label=title,
collapsed_symbol=collapsed_symbol,
expanded_symbol=expanded_symbol,
)
self._contents_list: list[Widget] = list(children)
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
self.collapsed = collapsed

@on(Title.Toggle)
def _update_collapsed(self) -> None:
self.collapsed = not self.collapsed

def watch_collapsed(self) -> None:
for child in self._nodes:
child.set_class(self.collapsed, "-collapsed")

def compose(self) -> ComposeResult:
yield from (
child.set_class(self.collapsed, "-collapsed")
for child in (
self._title,
self.Contents(*self._contents_list),
)
)

def compose_add_child(self, widget: Widget) -> None:
"""When using the context manager compose syntax, we want to attach nodes to the contents.

Args:
widget: A Widget to add.
"""
self._contents_list.append(widget)
Loading