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

Add SelectionList #2652

Merged
merged 105 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
e0ac60c
Initial framework for the SelectionList
davep May 11, 2023
8208388
Allow for type unions under Python 3.7
davep May 15, 2023
258180c
Add a selected flag to the Selection
davep May 15, 2023
d296fc5
Allow for passing in a selection as a tuple
davep May 15, 2023
471ab15
Merge branch 'main' into multiselect
davep May 17, 2023
9d0a6d8
Merge branch 'main' into multiselect
davep May 18, 2023
0c18839
WiP selection list
davep May 18, 2023
8459a8c
Swap to overriding render_line
davep May 18, 2023
b63e85f
Remove _make_label
davep May 18, 2023
beb3645
Remove Selection's knowledge of its parent
davep May 18, 2023
12416d8
Remove unused import of Text
davep May 18, 2023
bc126ce
Build the selection list back in __init__ again
davep May 18, 2023
6bea9f8
Sprinkle bold over all the buttons
davep May 18, 2023
c0b5832
Explain things a wee bit better for the future reader
davep May 18, 2023
8339e8b
Merge branch 'main' into multiselect
davep May 22, 2023
a570b44
Swap the order of the prompt and value for selection items
davep May 22, 2023
4dab6d3
Start the `SelectionList` messages
davep May 22, 2023
41b1c08
Docstring tweak
davep May 22, 2023
6bc2a6e
Add support for a selection message
davep May 22, 2023
424c30f
Add a method of getting at the selected values
davep May 22, 2023
127d93a
Remove a couple of annoying type errors
davep May 22, 2023
1d925da
Ensure selection casting works in earlier Pythons
davep May 22, 2023
dae0cd7
Raise a widget-specific exception when given a bad option
davep May 22, 2023
07515e2
Add an interface for changing selections from code
davep May 22, 2023
51d1dad
Ensure access to options is actually access to selections
davep May 22, 2023
13e796b
Ensure selections are only one line in length
davep May 22, 2023
a25ef78
Fully hint the type of the selection list in mesages
davep May 22, 2023
189181b
Add support for sending a message when the selection list changes
davep May 22, 2023
68250e6
Override _remove_option to update the selected values
davep May 22, 2023
f530efd
Extend SelectionList.add_options to better support the selection list
davep May 22, 2023
9e6bf08
Extend add_option so that it accepts selections and selection tuples
davep May 23, 2023
ca07d7a
Fill in the blanks with docstrings
davep May 23, 2023
195e9b4
Add the docstring for the bindings
davep May 23, 2023
4c9afca
Add a docstring for the component classes.
davep May 23, 2023
a2fc3fa
Add a method to apply a state change to all selection options
davep May 23, 2023
3ce04c8
Add a method of selecting all selection options
davep May 23, 2023
a4148d0
Add a method for deselecting all options
davep May 23, 2023
db273ea
Add a method for toggling all options
davep May 23, 2023
d861cce
Improve how the _all methods work
davep May 23, 2023
ff404e2
Only refresh on deselect if something was deselected
davep May 23, 2023
23d8999
Correct a docstring
davep May 23, 2023
81abac1
Tidy up some docstrings
davep May 23, 2023
fefb33a
Add a docstring to the internal copy of the selection value
davep May 23, 2023
bee438b
Get the selection value tracker in place before calling the superclass
davep May 23, 2023
d579937
Document _selected
davep May 23, 2023
d38780b
Ensure we don't try and post messages before the widget is ready
davep May 23, 2023
f9780d0
Add basic selection list creation unit tests
davep May 23, 2023
c448fa1
Add unit tests for selection list messages
davep May 23, 2023
9f6d35b
Start unit tests for the actual selected property
davep May 23, 2023
50d77b2
Add tests for the wrong sized tuple
davep May 23, 2023
56103c5
Ensure we log any OptionList messages in the messages test
davep May 24, 2023
9d6e977
Test messages when toggling a selection via user input
davep May 24, 2023
2e54054
Add a test that removed selected selections are removed from selected
davep May 24, 2023
da1faf8
Allow for storing the initial state of a selection
davep May 24, 2023
d3fe23f
Allow passing a Selection into a SelctionList
davep May 24, 2023
7110b30
Make sure adding a selection later updates selected
davep May 24, 2023
0a63748
Add a test for later addition of selected selections
davep May 24, 2023
cb05cff
Test that the control of selection list events is always correct
davep May 24, 2023
65375e8
Remove an outdated note
davep May 24, 2023
b113663
Add a note about SelctionToggled vs SelectedChanged
davep May 24, 2023
64ed982
Make it very clear when SelectedChanged is posted
davep May 24, 2023
2e37541
Correct the types in a copied docstring
davep May 24, 2023
9742144
Remove a note that isn't relevant any more
davep May 24, 2023
a31c3f0
Correct the Selection.__init__ docstring
davep May 24, 2023
258181d
Flesh out the docstring for the selected property
davep May 24, 2023
2874b24
Export genetic types for SelectionList
davep May 24, 2023
64dd7d0
Better linking for the docstring for SelectionType
davep May 24, 2023
a32cfdb
Better linking for the docstring for MessageSelectionType
davep May 24, 2023
9c0df44
Supply the generic type when creating a Selection
davep May 24, 2023
ac7a892
Link most(all?) docstring mentions of SelectionList
davep May 24, 2023
113ab41
Some more linking to types within the SelectionList docstrings
davep May 24, 2023
910c478
Add the main framework for the OptionList documentation
davep May 24, 2023
71d7f7d
Merge branch 'main' into multiselect
davep May 24, 2023
49c7b20
Link mention of Strip in a docstring
davep May 24, 2023
e7876ca
Merge branch 'main' into multiselect
davep May 24, 2023
3e4291c
Remove unnecessary inclusion of Selection
davep May 24, 2023
a910098
Make a start on the SelectionList example apps
davep May 24, 2023
2d544ca
Rename the tuples selection list example to mention tuples
davep May 25, 2023
fe26b89
Add some more hints about type hinting
davep May 25, 2023
02c4f4d
Add an example of using SelectionList.SelectedChanged
davep May 25, 2023
4ceeefb
Remove the attempt to link to Pretty
davep May 25, 2023
112f18b
Add SelectionList to the widget gallery
davep May 25, 2023
9a0e82f
Merge branch 'main' into multiselect
davep May 25, 2023
939586f
Add snapshot tests for the SelectionList examples
davep May 25, 2023
3796a84
Simplify _make_selection a wee bit
davep May 25, 2023
4472c86
Anticipate SelectionList making it into 0.27.0
davep May 25, 2023
bec362e
Improve the title for the widget
davep May 25, 2023
34f7136
Fix a typo
davep May 25, 2023
51133b3
Typo fix
davep May 25, 2023
51f8d0d
Break up the `SelectionList` snapshit tests
davep May 25, 2023
d656fa6
Fix a typo
davep May 25, 2023
ad4c68b
Fix a typo
davep May 25, 2023
6d82d7a
Fix a typo
davep May 25, 2023
4c93e63
Fix a copy/pasteo
davep May 25, 2023
baa060f
Remove annotation from RHS of the typing example
davep May 25, 2023
a944554
Finish a half-finished docstring
davep May 25, 2023
93c3c36
Merge branch 'main' into multiselect
davep May 25, 2023
c69e53f
Save a word!
davep May 25, 2023
95389eb
Fix a typo
davep May 25, 2023
45e6425
Be clear that _apply_to_all sends a SelectedChange message
davep May 25, 2023
aca8ec4
Merge branch 'multiselect' of github.com:davep/textual into multiselect
davep May 25, 2023
4764c10
Fix a copy/pasteo
davep May 25, 2023
658c1cd
Documentation punctuation change
davep May 25, 2023
93cae8d
Merge branch 'multiselect' of github.com:davep/textual into multiselect
davep May 25, 2023
400043d
Update snapshit tests
davep May 25, 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
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"),
rodrigogiraoserrao marked this conversation as resolved.
Show resolved Hide resolved
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