mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Add list view and docs
This commit is contained in:
1
docs/api/list_view.md
Normal file
1
docs/api/list_view.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.widgets.ListView
|
||||
62
docs/widgets/list_view.md
Normal file
62
docs/widgets/list_view.md
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user