Add list view and docs

This commit is contained in:
Darren Burns
2022-11-08 17:15:24 +00:00
parent 17dc927ed1
commit 6042346779
5 changed files with 159 additions and 24 deletions

1
docs/api/list_view.md Normal file
View File

@@ -0,0 +1 @@
::: textual.widgets.ListView

62
docs/widgets/list_view.md Normal file
View File

@@ -0,0 +1,62 @@
# List View
Displays a vertical list of widgets which can be highlighted and selected.
Supports keyboard navigation.
- [x] Focusable
- [x] Container
## Example
The example below shows an app with a simple `ListView`.
=== "Output"
```{.textual path="docs/examples/widgets/list_view.py"}
```
=== "list_view.py"
```python
--8<-- "docs/examples/widgets/list_view.py"
```
## Reactive Attributes
| Name | Type | Default | Description |
|---------|-------|---------|---------------------------------|
| `index` | `int` | `0` | The currently highlighted index |
## Messages
### Highlighted
The `ListView.Highlighted` message is emitted when the highlight changes.
This happens when you use the arrow keys on your keyboard and when you
click on a list item.
- [x] Bubbles
#### Attributes
| attribute | type | purpose |
|-----------|------------|--------------------------------|
| `item` | `ListItem` | The item that was highlighted. |
### Selected
The `ListView.Selected` message is emitted when a list item is selected.
You can select a list item by pressing ++enter++ while it is highlighted,
or by clicking on it.
- [x] Bubbles
#### Attributes
| attribute | type | purpose |
|-----------|------------|-----------------------------|
| `item` | `ListItem` | The item that was selected. |
## See Also
* [ListView](../api/list_view.md) code reference

View File

@@ -109,6 +109,7 @@ nav:
- "api/footer.md"
- "api/geometry.md"
- "api/header.md"
- "api/list_view.md"
- "api/message_pump.md"
- "api/message.md"
- "api/pilot.md"
@@ -184,12 +185,12 @@ plugins:
- blog:
- rss:
match_path: blog/posts/.*
match_path: blog/posts/.*
date_from_meta:
as_creation: date
categories:
- categories
- tags
- tags
- search:
- autorefs:
- mkdocstrings:

View File

@@ -1,11 +1,31 @@
from textual import events
from textual.message import Message
from textual.reactive import reactive
from textual.widget import Widget
class ListItem(Widget):
DEFAULT_CSS = "ListItem {height: auto;}"
class ListItem(Widget, can_focus=False):
DEFAULT_CSS = """
ListItem {
color: $text;
height: auto;
background: $panel-lighten-1;
overflow: hidden hidden;
}
ListItem > Widget :hover {
background: $boost;
}
ListItem.--highlight {
background: $accent;
}
ListItem > Widget {
height: auto;
}
"""
highlighted = reactive(False)
def watch_highlighted(self, value: bool) -> None:
self.set_class(value, "--highlight")
def on_click(self, event: events.Click) -> None:
self.emit_no_wait(self.ChildSelected(self))
class ChildSelected(Message):
pass

View File

@@ -1,57 +1,108 @@
from __future__ import annotations
from textual import events
from textual.binding import Binding
from textual.containers import Vertical
from textual.geometry import clamp
from textual.message import Message
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets._list_item import ListItem
class ListView(Widget, can_focus=True, can_focus_children=True):
class ListView(Vertical, can_focus=True, can_focus_children=False):
DEFAULT_CSS = """
ListView {
layout: vertical;
overflow: auto;
scrollbar-size-vertical: 1;
}
"""
index = reactive(0)
BINDINGS = [
Binding("down", "down", "Down"),
Binding("up", "up", "Up"),
Binding("enter", "select", "Select"),
]
index = reactive(0, always_update=True)
def __init__(
self,
*children: ListItem,
initial_index: int | None = 0,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
) -> None:
super().__init__(*children, name=name, id=id, classes=classes)
self.index = initial_index
def validate_index(self, index: int) -> int:
last_index = len(self.children) - 1
@property
def highlighted_child(self) -> ListItem | None:
if 0 <= self.index < len(self.children):
return self.children[self.index]
return None
def validate_index(self, index: int) -> int | None:
if not self.children:
return None
return self._clamp_index(index)
def _clamp_index(self, index: int) -> int:
last_index = max(len(self.children) - 1, 0)
return clamp(index, 0, last_index)
def watch_index(self, index: int) -> None:
child = self.children[index]
child.highlighted = True
def _is_valid_index(self, index: int | None) -> bool:
if index is None:
return False
return 0 <= index < len(self.children)
def action_down(self) -> None:
self.index += 1
def watch_index(self, old_index: int, new_index: int) -> None:
if self._is_valid_index(old_index):
old_child = self.children[old_index]
old_child.highlighted = False
old_child.set_class(False, "--highlight")
def action_up(self) -> None:
if self._is_valid_index(new_index):
new_child = self.children[new_index]
new_child.highlighted = True
new_child.set_class(True, "--highlight")
else:
new_child = None
self._scroll_highlighted_region()
self.emit_no_wait(self.Highlighted(self, new_child))
def action_select(self) -> None:
selected_child = self.highlighted_child
self.emit_no_wait(self.Selected(self, selected_child))
def on_list_item_child_selected(self, event: ListItem.ChildSelected) -> None:
self.focus()
self.index = self.children.index(event.sender)
self.emit_no_wait(self.Selected(self, event.sender))
def key_up(self, event: events.Key) -> None:
event.stop()
event.prevent_default()
self.index -= 1
class Highlighted(Message):
def __init__(self, sender: ListView, item: ListItem) -> None:
def key_down(self, event: events.Key) -> None:
event.stop()
event.prevent_default()
self.index += 1
def _scroll_highlighted_region(self) -> None:
if self.highlighted_child is not None:
self.scroll_to_widget(self.highlighted_child, animate=False)
def __len__(self):
return len(self.children)
class Highlighted(Message, bubble=True):
def __init__(self, sender: ListView, item: ListItem | None) -> None:
super().__init__(sender)
self.item = item
class Selected(Message):
class Selected(Message, bubble=True):
def __init__(self, sender: ListView, item: ListItem) -> None:
super().__init__(sender)
self.item = item