Skip to content

Commit

Permalink
Merge pull request #2652 from davep/multiselect
Browse files Browse the repository at this point in the history
  • Loading branch information
davep authored May 25, 2023
2 parents c16b32b + 400043d commit ea8c603
Show file tree
Hide file tree
Showing 18 changed files with 1,869 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- `work` decorator accepts `description` parameter to add debug string https://github.com/Textualize/textual/issues/2597
- Added `SelectionList` widget https://github.com/Textualize/textual/pull/2652
- `App.AUTO_FOCUS` to set auto focus on all screens https://github.com/Textualize/textual/issues/2594

### Changed
Expand Down
10 changes: 10 additions & 0 deletions docs/examples/widgets/selection_list.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Screen {
align: center middle;
}

SelectionList {
padding: 1;
border: solid $accent;
width: 80%;
height: 80%;
}
19 changes: 19 additions & 0 deletions docs/examples/widgets/selection_list_selected.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Screen {
align: center middle;
}

Horizontal {
width: 80%;
height: 80%;
}

SelectionList {
padding: 1;
border: solid $accent;
width: 1fr;
}

Pretty {
width: 1fr;
border: solid $accent;
}
40 changes: 40 additions & 0 deletions docs/examples/widgets/selection_list_selected.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from textual import on
from textual.app import App, ComposeResult
from textual.containers import Horizontal
from textual.events import Mount
from textual.widgets import Footer, Header, Pretty, SelectionList
from textual.widgets.selection_list import Selection


class SelectionListApp(App[None]):
CSS_PATH = "selection_list_selected.css"

def compose(self) -> ComposeResult:
yield Header()
with Horizontal():
yield SelectionList[str]( # (1)!
Selection("Falken's Maze", "secret_back_door", True),
Selection("Black Jack", "black_jack"),
Selection("Gin Rummy", "gin_rummy"),
Selection("Hearts", "hearts"),
Selection("Bridge", "bridge"),
Selection("Checkers", "checkers"),
Selection("Chess", "a_nice_game_of_chess", True),
Selection("Poker", "poker"),
Selection("Fighter Combat", "fighter_combat", True),
)
yield Pretty([])
yield Footer()

def on_mount(self) -> None:
self.query_one(SelectionList).border_title = "Shall we play some games?"
self.query_one(Pretty).border_title = "Selected games"

@on(Mount)
@on(SelectionList.SelectedChanged)
def update_selected_view(self) -> None:
self.query_one(Pretty).update(self.query_one(SelectionList).selected)


if __name__ == "__main__":
SelectionListApp().run()
29 changes: 29 additions & 0 deletions docs/examples/widgets/selection_list_selections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, SelectionList
from textual.widgets.selection_list import Selection


class SelectionListApp(App[None]):
CSS_PATH = "selection_list.css"

def compose(self) -> ComposeResult:
yield Header()
yield SelectionList[int]( # (1)!
Selection("Falken's Maze", 0, True),
Selection("Black Jack", 1),
Selection("Gin Rummy", 2),
Selection("Hearts", 3),
Selection("Bridge", 4),
Selection("Checkers", 5),
Selection("Chess", 6, True),
Selection("Poker", 7),
Selection("Fighter Combat", 8, True),
)
yield Footer()

def on_mount(self) -> None:
self.query_one(SelectionList).border_title = "Shall we play some games?"


if __name__ == "__main__":
SelectionListApp().run()
28 changes: 28 additions & 0 deletions docs/examples/widgets/selection_list_tuples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from textual.app import App, ComposeResult
from textual.widgets import Footer, Header, SelectionList


class SelectionListApp(App[None]):
CSS_PATH = "selection_list.css"

def compose(self) -> ComposeResult:
yield Header()
yield SelectionList[int]( # (1)!
("Falken's Maze", 0, True),
("Black Jack", 1),
("Gin Rummy", 2),
("Hearts", 3),
("Bridge", 4),
("Checkers", 5),
("Chess", 6, True),
("Poker", 7),
("Fighter Combat", 8, True),
)
yield Footer()

def on_mount(self) -> None:
self.query_one(SelectionList).border_title = "Shall we play some games?"


if __name__ == "__main__":
SelectionListApp().run()
8 changes: 8 additions & 0 deletions docs/widget_gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,14 @@ Select from a number of possible options.
```{.textual path="docs/examples/widgets/select_widget.py" press="tab,enter,down,down"}
```

## SelectionList

Select multiple values from a list of options.

[SelectionList reference](./widgets/selection_list.md){ .md-button .md-button--primary }

```{.textual path="docs/examples/widgets/selection_list_selections.py" press="down,down,down"}
```

## Static

Expand Down
171 changes: 171 additions & 0 deletions docs/widgets/selection_list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
# SelectionList

!!! tip "Added in version 0.27.0"

A widget for showing a vertical list of selectable options.

- [x] Focusable
- [ ] Container

## Typing

The `SelectionList` control is a
[`Generic`](https://docs.python.org/3/library/typing.html#typing.Generic),
which allows you to set the type of the
[selection values][textual.widgets.selection_list.Selection.value]. For instance, if
the data type for your values is an integer, you would type the widget as
follows:

```python
selections = [("First", 1), ("Second", 2)]
my_selection_list: SelectionList[int] = SelectionList(selections)
```

!!! note

Typing is entirely optional.

If you aren't familiar with typing or don't want to worry about it right now, feel free to ignore it.

## Examples

A selection list is designed to be built up of single-line prompts (which
can be [Rich renderables](/guide/widgets/#rich-renderables)) and an
associated unique value.

### Selections as tuples

A selection list can be built with tuples, either of two or three values in
length. Each tuple must contain a prompt and a value, and it can also
optionally contain a flag for the initial selected state of the option.

=== "Output"

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

=== "selection_list_tuples.py"

~~~python
--8<-- "docs/examples/widgets/selection_list_tuples.py"
~~~

1. Note that the `SelectionList` is typed as `int`, for the type of the values.

=== "selection_list.css"

~~~python
--8<-- "docs/examples/widgets/selection_list.css"
~~~

### Selections as Selection objects

Alternatively, selections can be passed in as
[`Selection`][textual.widgets.selection_list.Selection]s:

=== "Output"

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

=== "selection_list_selections.py"

~~~python
--8<-- "docs/examples/widgets/selection_list_selections.py"
~~~

1. Note that the `SelectionList` is typed as `int`, for the type of the values.

=== "selection_list.css"

~~~python
--8<-- "docs/examples/widgets/selection_list.css"
~~~

### Handling changes to the selections

Most of the time, when using the `SelectionList`, you will want to know when
the collection of selected items has changed; this is ideally done using the
[`SelectedChanged`][textual.widgets.SelectionList.SelectedChanged] message.
Here is an example of using that message to update a `Pretty` with the
collection of selected values:

=== "Output"

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

=== "selection_list_selections.py"

~~~python
--8<-- "docs/examples/widgets/selection_list_selected.py"
~~~

1. Note that the `SelectionList` is typed as `str`, for the type of the values.

=== "selection_list.css"

~~~python
--8<-- "docs/examples/widgets/selection_list_selected.css"
~~~

## Reactive Attributes

| Name | Type | Default | Description |
|---------------|-----------------|---------|------------------------------------------------------------------------------|
| `highlighted` | `int` \| `None` | `None` | The index of the highlighted selection. `None` means nothing is highlighted. |

## Messages

The following messages will be posted as the user interacts with the list:

- [SelectionList.SelectionHighlighted][textual.widgets.SelectionList.SelectionHighlighted]
- [SelectionList.SelectionToggled][textual.widgets.SelectionList.SelectionToggled]

The following message will be posted if the content of
[`selected`][textual.widgets.SelectionList.selected] changes, either by user
interaction or by API calls:

- [SelectionList.SelectedChanged][textual.widgets.SelectionList.SelectedChanged]

## Bindings

The selection list widget defines the following bindings:

::: textual.widgets.SelectionList.BINDINGS
options:
show_root_heading: false
show_root_toc_entry: false

It inherits from [`OptionList`][textual.widgets.OptionList]
and so also inherits the following bindings:

::: textual.widgets.OptionList.BINDINGS
options:
show_root_heading: false
show_root_toc_entry: false

## Component Classes

The selection list provides the following component classes:

::: textual.widgets.SelectionList.COMPONENT_CLASSES
options:
show_root_heading: false
show_root_toc_entry: false

It inherits from [`OptionList`][textual.widgets.OptionList] and so also
makes use of the following component classes:

::: textual.widgets.OptionList.COMPONENT_CLASSES
options:
show_root_heading: false
show_root_toc_entry: false

::: textual.widgets.SelectionList
options:
heading_level: 2

::: textual.widgets.selection_list
options:
heading_level: 2
1 change: 1 addition & 0 deletions mkdocs-nav.yml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ nav:
- "widgets/radiobutton.md"
- "widgets/radioset.md"
- "widgets/select.md"
- "widgets/selection_list.md"
- "widgets/static.md"
- "widgets/switch.md"
- "widgets/tabbed_content.md"
Expand Down
2 changes: 2 additions & 0 deletions src/textual/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ._radio_button import RadioButton
from ._radio_set import RadioSet
from ._select import Select
from ._selection_list import SelectionList
from ._static import Static
from ._switch import Switch
from ._tabbed_content import TabbedContent, TabPane
Expand Down Expand Up @@ -61,6 +62,7 @@
"RadioButton",
"RadioSet",
"Select",
"SelectionList",
"Static",
"Switch",
"Tab",
Expand Down
1 change: 1 addition & 0 deletions src/textual/widgets/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ from ._progress_bar import ProgressBar as ProgressBar
from ._radio_button import RadioButton as RadioButton
from ._radio_set import RadioSet as RadioSet
from ._select import Select as Select
from ._selection_list import SelectionList as SelectionList
from ._static import Static as Static
from ._switch import Switch as Switch
from ._tabbed_content import TabbedContent as TabbedContent
Expand Down
Loading

0 comments on commit ea8c603

Please sign in to comment.