mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
Merge pull request #1547 from Textualize/datatable-events
DataTable improvements (and more)
This commit is contained in:
@@ -14,8 +14,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- Added read-only public access to the children of a `TreeNode` via `TreeNode.children` https://github.com/Textualize/textual/issues/1398
|
||||
- Added `Tree.get_node_by_id` to allow getting a node by its ID https://github.com/Textualize/textual/pull/1535
|
||||
- Added a `Tree.NodeHighlighted` message, giving a `on_tree_node_highlighted` event handler https://github.com/Textualize/textual/issues/1400
|
||||
- Added a `inherit_component_classes` subclassing parameter to control whether or not component classes are inherited from base classes https://github.com/Textualize/textual/issues/1399
|
||||
- Added a `inherit_component_classes` subclassing parameter to control whether component classes are inherited from base classes https://github.com/Textualize/textual/issues/1399
|
||||
- Added `diagnose` as a `textual` command https://github.com/Textualize/textual/issues/1542
|
||||
- Added `row` and `column` cursors to `DataTable` https://github.com/Textualize/textual/pull/1547
|
||||
- Added an optional parameter `selector` to the methods `Screen.focus_next` and `Screen.focus_previous` that enable using a CSS selector to narrow down which widgets can get focus https://github.com/Textualize/textual/issues/1196
|
||||
|
||||
### Changed
|
||||
@@ -37,6 +38,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
- The styles `scrollbar-background-active` and `scrollbar-color-hover` are no longer ignored https://github.com/Textualize/textual/pull/1480
|
||||
- The widget `Placeholder` can now have its width set to `auto` https://github.com/Textualize/textual/pull/1508
|
||||
- Behavior of widget `Input` when rendering after programmatic value change and related scenarios https://github.com/Textualize/textual/issues/1477 https://github.com/Textualize/textual/issues/1443
|
||||
- `DataTable.show_cursor` now correctly allows cursor toggling https://github.com/Textualize/textual/pull/1547
|
||||
- Fixed cursor not being visible on `DataTable` mount when `fixed_columns` were used https://github.com/Textualize/textual/pull/1547
|
||||
- Fixed TextLog wrapping issue https://github.com/Textualize/textual/issues/1554
|
||||
- Fixed issue with TextLog not writing anything before layout https://github.com/Textualize/textual/issues/1498
|
||||
|
||||
|
||||
1
docs/api/coordinate.md
Normal file
1
docs/api/coordinate.md
Normal file
@@ -0,0 +1 @@
|
||||
::: textual.coordinate.Coordinate
|
||||
@@ -6,7 +6,7 @@ hide:
|
||||
|
||||
# Roadmap
|
||||
|
||||
We ([textualize.io](https://www.textualize.io/)) are actively building and maintaining Textual.
|
||||
We ([textualize.io](https://www.textualize.io/)) are actively building and maintaining Textual.
|
||||
|
||||
We have many new features in the pipeline. This page will keep track of that work.
|
||||
|
||||
@@ -18,24 +18,24 @@ High-level features we plan on implementing.
|
||||
* [ ] Integration with screen readers
|
||||
* [x] Monochrome mode
|
||||
* [ ] High contrast theme
|
||||
* [ ] Color blind themes
|
||||
* [ ] Color-blind themes
|
||||
- [ ] Command interface
|
||||
* [ ] Command menu
|
||||
* [ ] Fuzzy search
|
||||
- [ ] Configuration (.toml based extensible configuration format)
|
||||
- [x] Console
|
||||
- [x] Console
|
||||
- [ ] Devtools
|
||||
* [ ] Integrated log
|
||||
* [ ] DOM tree view
|
||||
* [ ] Integrated log
|
||||
* [ ] DOM tree view
|
||||
* [ ] REPL
|
||||
- [ ] Reactive state abstraction
|
||||
- [x] Themes
|
||||
* [ ] Customize via config
|
||||
- [x] Themes
|
||||
* [ ] Customize via config
|
||||
* [ ] Builtin theme editor
|
||||
|
||||
## Widgets
|
||||
|
||||
Widgets are key to making user friendly interfaces. The builtin widgets should cover many common (and some uncommon) use-cases. The following is a list of the widgets we have built or are planning to build.
|
||||
Widgets are key to making user-friendly interfaces. The builtin widgets should cover many common (and some uncommon) use-cases. The following is a list of the widgets we have built or are planning to build.
|
||||
|
||||
- [x] Buttons
|
||||
* [x] Error / warning variants
|
||||
@@ -44,8 +44,8 @@ Widgets are key to making user friendly interfaces. The builtin widgets should c
|
||||
- [ ] Content switcher
|
||||
- [x] DataTable
|
||||
* [x] Cell select
|
||||
* [ ] Row / Column select
|
||||
* [ ] API to update cells / rows
|
||||
* [x] Row / Column select
|
||||
* [ ] API to update cells / rows
|
||||
* [ ] Lazy loading API
|
||||
- [ ] Date picker
|
||||
- [ ] Drop-down menus
|
||||
@@ -76,4 +76,3 @@ Widgets are key to making user friendly interfaces. The builtin widgets should c
|
||||
* [ ] Indentation guides
|
||||
* [ ] Smart features for various languages
|
||||
* [ ] Syntax highlighting
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DataTable
|
||||
|
||||
A data table widget.
|
||||
A table widget optimized for displaying a lot of data.
|
||||
|
||||
- [x] Focusable
|
||||
- [ ] Container
|
||||
@@ -20,18 +20,33 @@ The example below populates a table with CSV data.
|
||||
--8<-- "docs/examples/widgets/data_table.py"
|
||||
```
|
||||
|
||||
|
||||
## Reactive Attributes
|
||||
|
||||
| Name | Type | Default | Description |
|
||||
| --------------- | ------ | ------- | ---------------------------------- |
|
||||
| `show_header` | `bool` | `True` | Show the table header |
|
||||
| `fixed_rows` | `int` | `0` | Number of fixed rows |
|
||||
| `fixed_columns` | `int` | `0` | Number of fixed columns |
|
||||
| `zebra_stripes` | `bool` | `False` | Display alternating colors on rows |
|
||||
| `header_height` | `int` | `1` | Height of header row |
|
||||
| `show_cursor` | `bool` | `True` | Show a cell cursor |
|
||||
| Name | Type | Default | Description |
|
||||
|-----------------|---------------------------------------------|--------------------|---------------------------------------------------------|
|
||||
| `show_header` | `bool` | `True` | Show the table header |
|
||||
| `fixed_rows` | `int` | `0` | Number of fixed rows (rows which do not scroll) |
|
||||
| `fixed_columns` | `int` | `0` | Number of fixed columns (columns which do not scroll) |
|
||||
| `zebra_stripes` | `bool` | `False` | Display alternating colors on rows |
|
||||
| `header_height` | `int` | `1` | Height of header row |
|
||||
| `show_cursor` | `bool` | `True` | Show the cursor |
|
||||
| `cursor_type` | `str` | `"cell"` | One of `"cell"`, `"row"`, `"column"`, or `"none"` |
|
||||
| `cursor_cell` | [Coordinate][textual.coordinate.Coordinate] | `Coordinate(0, 0)` | The coordinates of the cell the cursor is currently on |
|
||||
| `hover_cell` | [Coordinate][textual.coordinate.Coordinate] | `Coordinate(0, 0)` | The coordinates of the cell the _mouse_ cursor is above |
|
||||
|
||||
## Messages
|
||||
|
||||
### ::: textual.widgets.DataTable.CellHighlighted
|
||||
|
||||
### ::: textual.widgets.DataTable.CellSelected
|
||||
|
||||
### ::: textual.widgets.DataTable.RowHighlighted
|
||||
|
||||
### ::: textual.widgets.DataTable.RowSelected
|
||||
|
||||
### ::: textual.widgets.DataTable.ColumnHighlighted
|
||||
|
||||
### ::: textual.widgets.DataTable.ColumnSelected
|
||||
|
||||
## See Also
|
||||
|
||||
|
||||
@@ -148,6 +148,7 @@ nav:
|
||||
- "api/checkbox.md"
|
||||
- "api/color.md"
|
||||
- "api/containers.md"
|
||||
- "api/coordinate.md"
|
||||
- "api/data_table.md"
|
||||
- "api/directory_tree.md"
|
||||
- "api/dom_node.md"
|
||||
@@ -253,6 +254,8 @@ plugins:
|
||||
handlers:
|
||||
python:
|
||||
options:
|
||||
show_root_heading: true
|
||||
show_root_full_path: false
|
||||
show_source: false
|
||||
filters:
|
||||
- "!^_"
|
||||
|
||||
@@ -1800,8 +1800,8 @@ class App(Generic[ReturnType], DOMNode):
|
||||
"""Get the widget under the given coordinates.
|
||||
|
||||
Args:
|
||||
x (int): X Coord.
|
||||
y (int): Y Coord.
|
||||
x (int): X Coordinate.
|
||||
y (int): Y Coordinate.
|
||||
|
||||
Returns:
|
||||
tuple[Widget, Region]: The widget and the widget's screen region.
|
||||
@@ -1952,7 +1952,7 @@ class App(Generic[ReturnType], DOMNode):
|
||||
)
|
||||
except SkipAction:
|
||||
# The action method raised this to explicitly not handle the action
|
||||
log("<action> {action_name!r} skipped.")
|
||||
log(f"<action> {action_name!r} skipped.")
|
||||
return False
|
||||
|
||||
async def _broker_event(
|
||||
|
||||
46
src/textual/coordinate.py
Normal file
46
src/textual/coordinate.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Coordinate(NamedTuple):
|
||||
"""An object representing a row/column coordinate within a grid."""
|
||||
|
||||
row: int
|
||||
column: int
|
||||
|
||||
def left(self) -> Coordinate:
|
||||
"""Get the coordinate to the left.
|
||||
|
||||
Returns:
|
||||
Coordinate: The coordinate to the left.
|
||||
"""
|
||||
row, column = self
|
||||
return Coordinate(row, column - 1)
|
||||
|
||||
def right(self) -> Coordinate:
|
||||
"""Get the coordinate to the right.
|
||||
|
||||
Returns:
|
||||
Coordinate: The coordinate to the right.
|
||||
"""
|
||||
row, column = self
|
||||
return Coordinate(row, column + 1)
|
||||
|
||||
def up(self) -> Coordinate:
|
||||
"""Get the coordinate above.
|
||||
|
||||
Returns:
|
||||
Coordinate: The coordinate above.
|
||||
"""
|
||||
row, column = self
|
||||
return Coordinate(row - 1, column)
|
||||
|
||||
def down(self) -> Coordinate:
|
||||
"""Get the coordinate below.
|
||||
|
||||
Returns:
|
||||
Coordinate: The coordinate below.
|
||||
"""
|
||||
row, column = self
|
||||
return Coordinate(row + 1, column)
|
||||
@@ -2,8 +2,9 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from itertools import chain, zip_longest
|
||||
from typing import ClassVar, Generic, Iterable, NamedTuple, TypeVar, cast
|
||||
from typing import ClassVar, Generic, Iterable, TypeVar, cast
|
||||
|
||||
import rich.repr
|
||||
from rich.console import RenderableType
|
||||
from rich.padding import Padding
|
||||
from rich.protocol import is_renderable
|
||||
@@ -13,16 +14,19 @@ from rich.text import Text, TextType
|
||||
|
||||
from .. import events, messages
|
||||
from .._cache import LRUCache
|
||||
from ..coordinate import Coordinate
|
||||
from .._segment_tools import line_crop
|
||||
from .._types import SegmentLines
|
||||
from ..binding import Binding
|
||||
from ..geometry import Region, Size, Spacing, clamp
|
||||
from ..message import Message
|
||||
from ..reactive import Reactive
|
||||
from ..render import measure
|
||||
from ..scroll_view import ScrollView
|
||||
from ..strip import Strip
|
||||
from .._typing import Literal
|
||||
|
||||
CursorType = Literal["cell", "row", "column"]
|
||||
CursorType = Literal["cell", "row", "column", "none"]
|
||||
CELL: CursorType = "cell"
|
||||
CellType = TypeVar("CellType")
|
||||
|
||||
@@ -75,56 +79,6 @@ class Row:
|
||||
cell_renderables: list[RenderableType] = field(default_factory=list)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Cell:
|
||||
"""Table cell."""
|
||||
|
||||
value: object
|
||||
|
||||
|
||||
class Coord(NamedTuple):
|
||||
"""An object to represent the coordinate of a cell within the data table."""
|
||||
|
||||
row: int
|
||||
column: int
|
||||
|
||||
def left(self) -> Coord:
|
||||
"""Get coordinate to the left.
|
||||
|
||||
Returns:
|
||||
Coord: The coordinate.
|
||||
"""
|
||||
row, column = self
|
||||
return Coord(row, column - 1)
|
||||
|
||||
def right(self) -> Coord:
|
||||
"""Get coordinate to the right.
|
||||
|
||||
Returns:
|
||||
Coord: The coordinate.
|
||||
"""
|
||||
row, column = self
|
||||
return Coord(row, column + 1)
|
||||
|
||||
def up(self) -> Coord:
|
||||
"""Get coordinate above.
|
||||
|
||||
Returns:
|
||||
Coord: The coordinate.
|
||||
"""
|
||||
row, column = self
|
||||
return Coord(row - 1, column)
|
||||
|
||||
def down(self) -> Coord:
|
||||
"""Get coordinate below.
|
||||
|
||||
Returns:
|
||||
Coord: The coordinate.
|
||||
"""
|
||||
row, column = self
|
||||
return Coord(row + 1, column)
|
||||
|
||||
|
||||
class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
DEFAULT_CSS = """
|
||||
App.-dark DataTable {
|
||||
@@ -158,6 +112,15 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
color: $text;
|
||||
}
|
||||
|
||||
DataTable > .datatable--cursor-fixed {
|
||||
background: $secondary-darken-1;
|
||||
color: $text;
|
||||
}
|
||||
|
||||
DataTable > .datatable--highlight-fixed {
|
||||
background: $secondary 30%;
|
||||
}
|
||||
|
||||
.-dark-mode DataTable > .datatable--even-row {
|
||||
background: $primary 15%;
|
||||
}
|
||||
@@ -169,6 +132,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
|
||||
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
||||
"datatable--header",
|
||||
"datatable--cursor-fixed",
|
||||
"datatable--highlight-fixed",
|
||||
"datatable--fixed",
|
||||
"datatable--odd-row",
|
||||
"datatable--even-row",
|
||||
@@ -176,6 +141,14 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
"datatable--cursor",
|
||||
}
|
||||
|
||||
BINDINGS = [
|
||||
Binding("enter", "select_cursor", "Select", show=False),
|
||||
Binding("up", "cursor_up", "Cursor Up", show=False),
|
||||
Binding("down", "cursor_down", "Cursor Down", show=False),
|
||||
Binding("right", "cursor_right", "Cursor Right", show=False),
|
||||
Binding("left", "cursor_left", "Cursor Left", show=False),
|
||||
]
|
||||
|
||||
show_header = Reactive(True)
|
||||
fixed_rows = Reactive(0)
|
||||
fixed_columns = Reactive(0)
|
||||
@@ -184,8 +157,10 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
show_cursor = Reactive(True)
|
||||
cursor_type = Reactive(CELL)
|
||||
|
||||
cursor_cell: Reactive[Coord] = Reactive(Coord(0, 0), repaint=False)
|
||||
hover_cell: Reactive[Coord] = Reactive(Coord(0, 0), repaint=False)
|
||||
cursor_cell: Reactive[Coordinate] = Reactive(
|
||||
Coordinate(0, 0), repaint=False, always_update=True
|
||||
)
|
||||
hover_cell: Reactive[Coordinate] = Reactive(Coordinate(0, 0), repaint=False)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -228,6 +203,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
self.zebra_stripes = zebra_stripes
|
||||
self.header_height = header_height
|
||||
self.show_cursor = show_cursor
|
||||
self._show_hover_cursor = False
|
||||
|
||||
@property
|
||||
def hover_row(self) -> int:
|
||||
@@ -245,6 +221,18 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
def cursor_column(self) -> int:
|
||||
return self.cursor_cell.column
|
||||
|
||||
def get_cell_value(self, coordinate: Coordinate) -> CellType:
|
||||
"""Get the value from the cell at the given coordinate.
|
||||
|
||||
Args:
|
||||
coordinate (Coordinate): The coordinate to retrieve the value from.
|
||||
|
||||
Returns:
|
||||
CellType: The value of the cell.
|
||||
"""
|
||||
row, column = coordinate
|
||||
return self.data[row][column]
|
||||
|
||||
def _clear_caches(self) -> None:
|
||||
self._row_render_cache.clear()
|
||||
self._cell_render_cache.clear()
|
||||
@@ -260,6 +248,19 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
self._clear_caches()
|
||||
self.refresh()
|
||||
|
||||
def watch_show_cursor(self, show_cursor: bool) -> None:
|
||||
self._clear_caches()
|
||||
if show_cursor and self.cursor_type != "none":
|
||||
# When we re-enable the cursor, apply highlighting and
|
||||
# emit the appropriate [Row|Column|Cell]Highlighted event.
|
||||
self._scroll_cursor_into_view(animate=False)
|
||||
if self.cursor_type == "cell":
|
||||
self._highlight_cell(self.cursor_cell)
|
||||
elif self.cursor_type == "row":
|
||||
self._highlight_row(self.cursor_row)
|
||||
elif self.cursor_type == "column":
|
||||
self._highlight_column(self.cursor_column)
|
||||
|
||||
def watch_show_header(self, show_header: bool) -> None:
|
||||
self._clear_caches()
|
||||
|
||||
@@ -269,19 +270,73 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
def watch_zebra_stripes(self, zebra_stripes: bool) -> None:
|
||||
self._clear_caches()
|
||||
|
||||
def watch_hover_cell(self, old: Coord, value: Coord) -> None:
|
||||
def watch_hover_cell(self, old: Coordinate, value: Coordinate) -> None:
|
||||
self.refresh_cell(*old)
|
||||
self.refresh_cell(*value)
|
||||
|
||||
def watch_cursor_cell(self, old: Coord, value: Coord) -> None:
|
||||
self.refresh_cell(*old)
|
||||
self.refresh_cell(*value)
|
||||
def watch_cursor_cell(
|
||||
self, old_coordinate: Coordinate, new_coordinate: Coordinate
|
||||
) -> None:
|
||||
if old_coordinate != new_coordinate:
|
||||
# Refresh the old and the new cell, and emit the appropriate
|
||||
# message to tell users of the newly highlighted row/cell/column.
|
||||
if self.cursor_type == "cell":
|
||||
self.refresh_cell(*old_coordinate)
|
||||
self._highlight_cell(new_coordinate)
|
||||
elif self.cursor_type == "row":
|
||||
self.refresh_row(old_coordinate.row)
|
||||
self._highlight_row(new_coordinate.row)
|
||||
elif self.cursor_type == "column":
|
||||
self.refresh_column(old_coordinate.column)
|
||||
self._highlight_column(new_coordinate.column)
|
||||
|
||||
def validate_cursor_cell(self, value: Coord) -> Coord:
|
||||
row, column = value
|
||||
def _highlight_cell(self, coordinate: Coordinate) -> None:
|
||||
"""Apply highlighting to the cell at the coordinate, and emit event."""
|
||||
self.refresh_cell(*coordinate)
|
||||
cell_value = self.get_cell_value(coordinate)
|
||||
self.emit_no_wait(DataTable.CellHighlighted(self, cell_value, coordinate))
|
||||
|
||||
def _highlight_row(self, row_index: int) -> None:
|
||||
"""Apply highlighting to the row at the given index, and emit event."""
|
||||
self.refresh_row(row_index)
|
||||
self.emit_no_wait(DataTable.RowHighlighted(self, row_index))
|
||||
|
||||
def _highlight_column(self, column_index: int) -> None:
|
||||
"""Apply highlighting to the column at the given index, and emit event."""
|
||||
self.refresh_column(column_index)
|
||||
self.emit_no_wait(DataTable.ColumnHighlighted(self, column_index))
|
||||
|
||||
def validate_cursor_cell(self, value: Coordinate) -> Coordinate:
|
||||
return self._clamp_cursor_cell(value)
|
||||
|
||||
def _clamp_cursor_cell(self, cursor_cell: Coordinate) -> Coordinate:
|
||||
row, column = cursor_cell
|
||||
row = clamp(row, 0, self.row_count - 1)
|
||||
column = clamp(column, self.fixed_columns, len(self.columns) - 1)
|
||||
return Coord(row, column)
|
||||
return Coordinate(row, column)
|
||||
|
||||
def watch_cursor_type(self, old: str, new: str) -> None:
|
||||
self._set_hover_cursor(False)
|
||||
row_index, column_index = self.cursor_cell
|
||||
|
||||
# Apply the highlighting to the newly relevant cells
|
||||
if new == "cell":
|
||||
self._highlight_cell(self.cursor_cell)
|
||||
elif new == "row":
|
||||
self._highlight_row(row_index)
|
||||
elif new == "column":
|
||||
self._highlight_column(column_index)
|
||||
|
||||
# Refresh cells that were previously impacted by the cursor
|
||||
# but may no longer be.
|
||||
if old == "cell":
|
||||
self.refresh_cell(row_index, column_index)
|
||||
elif old == "row":
|
||||
self.refresh_row(row_index)
|
||||
elif old == "column":
|
||||
self.refresh_column(column_index)
|
||||
|
||||
self._scroll_cursor_into_view()
|
||||
|
||||
def _update_dimensions(self, new_rows: Iterable[int]) -> None:
|
||||
"""Called to recalculate the virtual (scrollable) size."""
|
||||
@@ -301,6 +356,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
)
|
||||
|
||||
def _get_cell_region(self, row_index: int, column_index: int) -> Region:
|
||||
"""Get the region of the cell at the given coordinate (row_index, column_index)"""
|
||||
if row_index not in self.rows:
|
||||
return Region(0, 0, 0, 0)
|
||||
row = self.rows[row_index]
|
||||
@@ -313,6 +369,32 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
cell_region = Region(x, y, width, height)
|
||||
return cell_region
|
||||
|
||||
def _get_row_region(self, row_index: int) -> Region:
|
||||
"""Get the region of the row at the given index."""
|
||||
rows = self.rows
|
||||
if row_index < 0 or row_index >= len(rows):
|
||||
return Region(0, 0, 0, 0)
|
||||
row = rows[row_index]
|
||||
row_width = sum(column.render_width for column in self.columns)
|
||||
y = row.y
|
||||
if self.show_header:
|
||||
y += self.header_height
|
||||
row_region = Region(0, y, row_width, row.height)
|
||||
return row_region
|
||||
|
||||
def _get_column_region(self, column_index: int) -> Region:
|
||||
"""Get the region of the column at the given index."""
|
||||
columns = self.columns
|
||||
if column_index < 0 or column_index >= len(columns):
|
||||
return Region(0, 0, 0, 0)
|
||||
|
||||
x = sum(column.render_width for column in self.columns[:column_index])
|
||||
width = columns[column_index].render_width
|
||||
header_height = self.header_height if self.show_header else 0
|
||||
height = len(self._y_offsets) + header_height
|
||||
full_column_region = Region(x, 0, width, height)
|
||||
return full_column_region
|
||||
|
||||
def clear(self, columns: bool = False) -> None:
|
||||
"""Clear the table.
|
||||
|
||||
@@ -335,7 +417,6 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
|
||||
Args:
|
||||
*labels: Column headers.
|
||||
|
||||
"""
|
||||
for label in labels:
|
||||
self.add_column(label, width=None)
|
||||
@@ -387,6 +468,7 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
|
||||
self._new_rows.add(row_index)
|
||||
self._require_update_dimensions = True
|
||||
self.cursor_cell = self.cursor_cell
|
||||
self.check_idle()
|
||||
|
||||
def add_rows(self, rows: Iterable[Iterable[CellType]]) -> None:
|
||||
@@ -416,6 +498,36 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
if row_index < 0 or column_index < 0:
|
||||
return
|
||||
region = self._get_cell_region(row_index, column_index)
|
||||
self._refresh_region(region)
|
||||
|
||||
def refresh_row(self, row_index: int) -> None:
|
||||
"""Refresh the row at the given index.
|
||||
|
||||
Args:
|
||||
row_index (int): The index of the row to refresh.
|
||||
"""
|
||||
if row_index < 0 or row_index >= len(self.rows):
|
||||
return
|
||||
|
||||
region = self._get_row_region(row_index)
|
||||
self._refresh_region(region)
|
||||
|
||||
def refresh_column(self, column_index: int) -> None:
|
||||
"""Refresh the column at the given index.
|
||||
|
||||
Args:
|
||||
column_index (int): The index of the column to refresh.
|
||||
"""
|
||||
if column_index < 0 or column_index >= len(self.columns):
|
||||
return
|
||||
|
||||
region = self._get_column_region(column_index)
|
||||
self._refresh_region(region)
|
||||
|
||||
def _refresh_region(self, region: Region) -> None:
|
||||
"""Refresh a region of the DataTable, if it's visible within
|
||||
the window. This method will translate the region to account
|
||||
for scrolling."""
|
||||
if not self.window_region.overlaps(region):
|
||||
return
|
||||
region = region.translate(-self.scroll_offset)
|
||||
@@ -461,19 +573,38 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
column_index (int): Index of the column.
|
||||
style (Style): Style to apply.
|
||||
width (int): Width of the cell.
|
||||
cursor (bool): Is this cell affected by cursor highlighting?
|
||||
hover (bool): Is this cell affected by hover cursor highlighting?
|
||||
|
||||
Returns:
|
||||
Lines: A list of segments per line.
|
||||
"""
|
||||
if hover:
|
||||
is_header_row = row_index == -1
|
||||
|
||||
# The header row *and* fixed columns both have a different style (blue bg)
|
||||
is_fixed_style = is_header_row or column_index < self.fixed_columns
|
||||
show_cursor = self.show_cursor
|
||||
|
||||
if hover and show_cursor and self._show_hover_cursor:
|
||||
style += self.get_component_styles("datatable--highlight").rich_style
|
||||
if cursor:
|
||||
if is_fixed_style:
|
||||
# Apply subtle variation in style for the fixed (blue background by default)
|
||||
# rows and columns affected by the cursor, to ensure we can still differentiate
|
||||
# between the labels and the data.
|
||||
style += self.get_component_styles(
|
||||
"datatable--highlight-fixed"
|
||||
).rich_style
|
||||
|
||||
if cursor and show_cursor:
|
||||
style += self.get_component_styles("datatable--cursor").rich_style
|
||||
if is_fixed_style:
|
||||
style += self.get_component_styles("datatable--cursor-fixed").rich_style
|
||||
|
||||
cell_key = (row_index, column_index, style, cursor, hover)
|
||||
if cell_key not in self._cell_render_cache:
|
||||
style += Style.from_meta({"row": row_index, "column": column_index})
|
||||
height = (
|
||||
self.header_height if row_index == -1 else self.rows[row_index].height
|
||||
self.header_height if is_header_row else self.rows[row_index].height
|
||||
)
|
||||
cell = self._get_row_renderables(row_index)[column_index]
|
||||
lines = self.app.console.render_lines(
|
||||
@@ -489,8 +620,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
row_index: int,
|
||||
line_no: int,
|
||||
base_style: Style,
|
||||
cursor_column: int = -1,
|
||||
hover_column: int = -1,
|
||||
cursor_location: Coordinate,
|
||||
hover_location: Coordinate,
|
||||
) -> tuple[SegmentLines, SegmentLines]:
|
||||
"""Render a row in to lines for each cell.
|
||||
|
||||
@@ -498,27 +629,68 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
row_index (int): Index of the row.
|
||||
line_no (int): Line number (on screen, 0 is top)
|
||||
base_style (Style): Base style of row.
|
||||
cursor_location (Coordinate): The location of the cursor in the DataTable.
|
||||
hover_location (Coordinate): The location of the hover cursor in the DataTable.
|
||||
|
||||
Returns:
|
||||
tuple[Lines, Lines]: Lines for fixed cells, and Lines for scrollable cells.
|
||||
"""
|
||||
|
||||
cache_key = (row_index, line_no, base_style, cursor_column, hover_column)
|
||||
cursor_type = self.cursor_type
|
||||
show_cursor = self.show_cursor
|
||||
cache_key = (
|
||||
row_index,
|
||||
line_no,
|
||||
base_style,
|
||||
cursor_location,
|
||||
hover_location,
|
||||
cursor_type,
|
||||
show_cursor,
|
||||
self._show_hover_cursor,
|
||||
)
|
||||
|
||||
if cache_key in self._row_render_cache:
|
||||
return self._row_render_cache[cache_key]
|
||||
|
||||
render_cell = self._render_cell
|
||||
|
||||
def _should_highlight(
|
||||
cursor_location: Coordinate,
|
||||
cell_location: Coordinate,
|
||||
cursor_type: CursorType,
|
||||
) -> bool:
|
||||
"""Determine whether we should highlight a cell given the location
|
||||
of the cursor, the location of the cell, and the type of cursor that
|
||||
is currently active."""
|
||||
if cursor_type == "cell":
|
||||
return cursor_location == cell_location
|
||||
elif cursor_type == "row":
|
||||
cursor_row, _ = cursor_location
|
||||
cell_row, _ = cell_location
|
||||
return cursor_row == cell_row
|
||||
elif cursor_type == "column":
|
||||
_, cursor_column = cursor_location
|
||||
_, cell_column = cell_location
|
||||
return cursor_column == cell_column
|
||||
else:
|
||||
return False
|
||||
|
||||
if self.fixed_columns:
|
||||
fixed_style = self.get_component_styles("datatable--fixed").rich_style
|
||||
fixed_style += Style.from_meta({"fixed": True})
|
||||
fixed_row = [
|
||||
render_cell(row_index, column.index, fixed_style, column.render_width)[
|
||||
line_no
|
||||
]
|
||||
for column in self.columns[: self.fixed_columns]
|
||||
]
|
||||
fixed_row = []
|
||||
for column in self.columns[: self.fixed_columns]:
|
||||
cell_location = Coordinate(row_index, column.index)
|
||||
fixed_cell_lines = render_cell(
|
||||
row_index,
|
||||
column.index,
|
||||
fixed_style,
|
||||
column.render_width,
|
||||
cursor=_should_highlight(
|
||||
cursor_location, cell_location, cursor_type
|
||||
),
|
||||
hover=_should_highlight(hover_location, cell_location, cursor_type),
|
||||
)[line_no]
|
||||
fixed_row.append(fixed_cell_lines)
|
||||
else:
|
||||
fixed_row = []
|
||||
|
||||
@@ -533,17 +705,18 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
else:
|
||||
row_style = base_style
|
||||
|
||||
scrollable_row = [
|
||||
render_cell(
|
||||
scrollable_row = []
|
||||
for column in self.columns:
|
||||
cell_location = Coordinate(row_index, column.index)
|
||||
cell_lines = render_cell(
|
||||
row_index,
|
||||
column.index,
|
||||
row_style,
|
||||
column.render_width,
|
||||
cursor=cursor_column == column.index,
|
||||
hover=hover_column == column.index,
|
||||
cursor=_should_highlight(cursor_location, cell_location, cursor_type),
|
||||
hover=_should_highlight(hover_location, cell_location, cursor_type),
|
||||
)[line_no]
|
||||
for column in self.columns
|
||||
]
|
||||
scrollable_row.append(cell_lines)
|
||||
|
||||
row_pair = (fixed_row, scrollable_row)
|
||||
self._row_render_cache[cache_key] = row_pair
|
||||
@@ -585,14 +758,18 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
row_index, line_no = self._get_offsets(y)
|
||||
except LookupError:
|
||||
return Strip.blank(width, base_style)
|
||||
cursor_column = (
|
||||
self.cursor_column
|
||||
if (self.show_cursor and self.cursor_row == row_index)
|
||||
else -1
|
||||
)
|
||||
hover_column = self.hover_column if (self.hover_row == row_index) else -1
|
||||
|
||||
cache_key = (y, x1, x2, width, cursor_column, hover_column, base_style)
|
||||
cache_key = (
|
||||
y,
|
||||
x1,
|
||||
x2,
|
||||
width,
|
||||
self.cursor_cell,
|
||||
self.hover_cell,
|
||||
base_style,
|
||||
self.cursor_type,
|
||||
self._show_hover_cursor,
|
||||
)
|
||||
if cache_key in self._line_cache:
|
||||
return self._line_cache[cache_key]
|
||||
|
||||
@@ -600,8 +777,8 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
row_index,
|
||||
line_no,
|
||||
base_style,
|
||||
cursor_column=cursor_column,
|
||||
hover_column=hover_column,
|
||||
cursor_location=self.cursor_cell,
|
||||
hover_location=self.hover_cell,
|
||||
)
|
||||
fixed_width = sum(
|
||||
column.render_width for column in self.columns[: self.fixed_columns]
|
||||
@@ -625,22 +802,21 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
if self.show_header:
|
||||
fixed_top_row_count += self.get_row_height(-1)
|
||||
|
||||
style = self.rich_style
|
||||
|
||||
if y >= fixed_top_row_count:
|
||||
y += scroll_y
|
||||
|
||||
return self._render_line(y, scroll_x, scroll_x + width, style)
|
||||
return self._render_line(y, scroll_x, scroll_x + width, self.rich_style)
|
||||
|
||||
def on_mouse_move(self, event: events.MouseMove):
|
||||
self._set_hover_cursor(True)
|
||||
meta = event.style.meta
|
||||
if meta:
|
||||
if meta and self.show_cursor and self.cursor_type != "none":
|
||||
try:
|
||||
self.hover_cell = Coord(meta["row"], meta["column"])
|
||||
self.hover_cell = Coordinate(meta["row"], meta["column"])
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def _get_cell_border(self) -> Spacing:
|
||||
def _get_fixed_offset(self) -> Spacing:
|
||||
top = self.header_height if self.show_header else 0
|
||||
top += sum(
|
||||
self.rows[row_index].height
|
||||
@@ -650,38 +826,233 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):
|
||||
left = sum(column.render_width for column in self.columns[: self.fixed_columns])
|
||||
return Spacing(top, 0, 0, left)
|
||||
|
||||
def _scroll_cursor_in_to_view(self, animate: bool = False) -> None:
|
||||
region = self._get_cell_region(self.cursor_row, self.cursor_column)
|
||||
spacing = self._get_cell_border()
|
||||
self.scroll_to_region(region, animate=animate, spacing=spacing)
|
||||
def _scroll_cursor_into_view(self, animate: bool = False) -> None:
|
||||
fixed_offset = self._get_fixed_offset()
|
||||
top, _, _, left = fixed_offset
|
||||
|
||||
if self.cursor_type == "row":
|
||||
x, y, width, height = self._get_row_region(self.cursor_row)
|
||||
region = Region(int(self.scroll_x) + left, y, width - left, height)
|
||||
elif self.cursor_type == "column":
|
||||
x, y, width, height = self._get_column_region(self.cursor_column)
|
||||
region = Region(x, int(self.scroll_y) + top, width, height - top)
|
||||
else:
|
||||
region = self._get_cell_region(self.cursor_row, self.cursor_column)
|
||||
|
||||
self.scroll_to_region(region, animate=animate, spacing=fixed_offset)
|
||||
|
||||
def _set_hover_cursor(self, active: bool) -> None:
|
||||
"""Set whether the hover cursor (the faint cursor you see when you
|
||||
hover the mouse cursor over a cell) is visible or not. Typically,
|
||||
when you interact with the keyboard, you want to switch the hover
|
||||
cursor off.
|
||||
|
||||
Args:
|
||||
active (bool): Display the hover cursor.
|
||||
"""
|
||||
self._show_hover_cursor = active
|
||||
cursor_type = self.cursor_type
|
||||
if cursor_type == "column":
|
||||
self.refresh_column(self.hover_column)
|
||||
elif cursor_type == "row":
|
||||
self.refresh_row(self.hover_row)
|
||||
elif cursor_type == "cell":
|
||||
self.refresh_cell(*self.hover_cell)
|
||||
|
||||
def on_click(self, event: events.Click) -> None:
|
||||
meta = self.get_style_at(event.x, event.y).meta
|
||||
if meta:
|
||||
self.cursor_cell = Coord(meta["row"], meta["column"])
|
||||
self._scroll_cursor_in_to_view()
|
||||
event.stop()
|
||||
self._set_hover_cursor(True)
|
||||
if self.show_cursor and self.cursor_type != "none":
|
||||
# Only emit selection events if there is a visible row/col/cell cursor.
|
||||
self._emit_selected_message()
|
||||
meta = self.get_style_at(event.x, event.y).meta
|
||||
if meta:
|
||||
self.cursor_cell = Coordinate(meta["row"], meta["column"])
|
||||
self._scroll_cursor_into_view(animate=True)
|
||||
event.stop()
|
||||
|
||||
def key_down(self, event: events.Key):
|
||||
self.cursor_cell = self.cursor_cell.down()
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
self._scroll_cursor_in_to_view()
|
||||
def action_cursor_up(self) -> None:
|
||||
self._set_hover_cursor(False)
|
||||
cursor_type = self.cursor_type
|
||||
if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
|
||||
self.cursor_cell = self.cursor_cell.up()
|
||||
self._scroll_cursor_into_view()
|
||||
else:
|
||||
# If the cursor doesn't move up (e.g. column cursor can't go up),
|
||||
# then ensure that we instead scroll the DataTable.
|
||||
super().action_scroll_up()
|
||||
|
||||
def key_up(self, event: events.Key):
|
||||
self.cursor_cell = self.cursor_cell.up()
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
self._scroll_cursor_in_to_view()
|
||||
def action_cursor_down(self) -> None:
|
||||
self._set_hover_cursor(False)
|
||||
cursor_type = self.cursor_type
|
||||
if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
|
||||
self.cursor_cell = self.cursor_cell.down()
|
||||
self._scroll_cursor_into_view()
|
||||
else:
|
||||
super().action_scroll_down()
|
||||
|
||||
def key_right(self, event: events.Key):
|
||||
self.cursor_cell = self.cursor_cell.right()
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
self._scroll_cursor_in_to_view(animate=True)
|
||||
def action_cursor_right(self) -> None:
|
||||
self._set_hover_cursor(False)
|
||||
cursor_type = self.cursor_type
|
||||
if self.show_cursor and (cursor_type == "cell" or cursor_type == "column"):
|
||||
self.cursor_cell = self.cursor_cell.right()
|
||||
self._scroll_cursor_into_view(animate=True)
|
||||
else:
|
||||
super().action_scroll_right()
|
||||
|
||||
def key_left(self, event: events.Key):
|
||||
self.cursor_cell = self.cursor_cell.left()
|
||||
event.stop()
|
||||
event.prevent_default()
|
||||
self._scroll_cursor_in_to_view(animate=True)
|
||||
def action_cursor_left(self) -> None:
|
||||
self._set_hover_cursor(False)
|
||||
cursor_type = self.cursor_type
|
||||
if self.show_cursor and (cursor_type == "cell" or cursor_type == "column"):
|
||||
self.cursor_cell = self.cursor_cell.left()
|
||||
self._scroll_cursor_into_view(animate=True)
|
||||
else:
|
||||
super().action_scroll_left()
|
||||
|
||||
def action_select_cursor(self) -> None:
|
||||
self._set_hover_cursor(False)
|
||||
if self.show_cursor and self.cursor_type != "none":
|
||||
self._emit_selected_message()
|
||||
|
||||
def _emit_selected_message(self):
|
||||
"""Emit the appropriate message for a selection based on the `cursor_type`."""
|
||||
cursor_cell = self.cursor_cell
|
||||
cursor_type = self.cursor_type
|
||||
if cursor_type == "cell":
|
||||
self.emit_no_wait(
|
||||
DataTable.CellSelected(
|
||||
self,
|
||||
self.get_cell_value(cursor_cell),
|
||||
cursor_cell,
|
||||
)
|
||||
)
|
||||
elif cursor_type == "row":
|
||||
row, _ = cursor_cell
|
||||
self.emit_no_wait(DataTable.RowSelected(self, row))
|
||||
elif cursor_type == "column":
|
||||
_, column = cursor_cell
|
||||
self.emit_no_wait(DataTable.ColumnSelected(self, column))
|
||||
|
||||
class CellHighlighted(Message, bubble=True):
|
||||
"""Emitted when the cursor moves to highlight a new cell.
|
||||
It's only relevant when the `cursor_type` is `"cell"`.
|
||||
It's also emitted when the cell cursor is re-enabled (by setting `show_cursor=True`),
|
||||
and when the cursor type is changed to `"cell"`. Can be handled using
|
||||
`on_data_table_cell_highlighted` in a subclass of `DataTable` or in a parent
|
||||
widget in the DOM.
|
||||
|
||||
Attributes:
|
||||
sender (DataTable): The DataTable the cell was highlighted in.
|
||||
value (CellType): The value in the highlighted cell.
|
||||
coordinate (Coordinate): The coordinate of the highlighted cell.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: DataTable, value: CellType, coordinate: Coordinate
|
||||
) -> None:
|
||||
self.value = value
|
||||
self.coordinate = coordinate
|
||||
super().__init__(sender)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "value", self.value
|
||||
yield "coordinate", self.coordinate
|
||||
|
||||
class CellSelected(Message, bubble=True):
|
||||
"""Emitted by the `DataTable` widget when a cell is selected.
|
||||
It's only relevant when the `cursor_type` is `"cell"`. Can be handled using
|
||||
`on_data_table_cell_selected` in a subclass of `DataTable` or in a parent
|
||||
widget in the DOM.
|
||||
|
||||
Attributes:
|
||||
sender (DataTable): The DataTable the cell was selected in.
|
||||
value (CellType): The value in the cell that was selected.
|
||||
coordinate (Coordinate): The coordinate of the cell that was selected.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, sender: DataTable, value: CellType, coordinate: Coordinate
|
||||
) -> None:
|
||||
self.value = value
|
||||
self.coordinate = coordinate
|
||||
super().__init__(sender)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "value", self.value
|
||||
yield "coordinate", self.coordinate
|
||||
|
||||
class RowHighlighted(Message, bubble=True):
|
||||
"""Emitted when a row is highlighted. This message is only emitted when the
|
||||
`cursor_type` is set to `"row"`. Can be handled using `on_data_table_row_highlighted`
|
||||
in a subclass of `DataTable` or in a parent widget in the DOM.
|
||||
|
||||
Attributes:
|
||||
sender (DataTable): The DataTable the row was highlighted in.
|
||||
cursor_row (int): The y-coordinate of the cursor that highlighted the row.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: DataTable, cursor_row: int) -> None:
|
||||
self.cursor_row = cursor_row
|
||||
super().__init__(sender)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "cursor_row", self.cursor_row
|
||||
|
||||
class RowSelected(Message, bubble=True):
|
||||
"""Emitted when a row is selected. This message is only emitted when the
|
||||
`cursor_type` is set to `"row"`. Can be handled using
|
||||
`on_data_table_row_selected` in a subclass of `DataTable` or in a parent
|
||||
widget in the DOM.
|
||||
|
||||
Attributes:
|
||||
sender (DataTable): The DataTable the row was selected in.
|
||||
cursor_row (int): The y-coordinate of the cursor that made the selection.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: DataTable, cursor_row: int) -> None:
|
||||
self.cursor_row = cursor_row
|
||||
super().__init__(sender)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "cursor_row", self.cursor_row
|
||||
|
||||
class ColumnHighlighted(Message, bubble=True):
|
||||
"""Emitted when a column is highlighted. This message is only emitted when the
|
||||
`cursor_type` is set to `"column"`. Can be handled using
|
||||
`on_data_table_column_highlighted` in a subclass of `DataTable` or in a parent
|
||||
widget in the DOM.
|
||||
|
||||
Attributes:
|
||||
sender (DataTable): The DataTable the column was highlighted in.
|
||||
cursor_column (int): The x-coordinate of the column that was highlighted.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: DataTable, cursor_column: int) -> None:
|
||||
self.cursor_column = cursor_column
|
||||
super().__init__(sender)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "cursor_column", self.cursor_column
|
||||
|
||||
class ColumnSelected(Message, bubble=True):
|
||||
"""Emitted when a column is selected. This message is only emitted when the
|
||||
`cursor_type` is set to `"column"`. Can be handled using
|
||||
`on_data_table_column_selected` in a subclass of `DataTable` or in a parent
|
||||
widget in the DOM.
|
||||
|
||||
Attributes:
|
||||
sender (DataTable): The DataTable the column was selected in.
|
||||
cursor_column (int): The x-coordinate of the column that was selected.
|
||||
"""
|
||||
|
||||
def __init__(self, sender: DataTable, cursor_column: int) -> None:
|
||||
self.cursor_column = cursor_column
|
||||
super().__init__(sender)
|
||||
|
||||
def __rich_repr__(self) -> rich.repr.Result:
|
||||
yield "sender", self.sender
|
||||
yield "cursor_column", self.cursor_column
|
||||
|
||||
@@ -19,16 +19,10 @@ class ListView(Vertical, can_focus=True, can_focus_children=False):
|
||||
index: The index in the list that's currently highlighted.
|
||||
"""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
ListView {
|
||||
scrollbar-size-vertical: 2;
|
||||
}
|
||||
"""
|
||||
|
||||
BINDINGS = [
|
||||
Binding("down", "cursor_down", "Down", show=False),
|
||||
Binding("up", "cursor_up", "Up", show=False),
|
||||
Binding("enter", "select_cursor", "Select", show=False),
|
||||
Binding("up", "cursor_up", "Cursor Up", show=False),
|
||||
Binding("down", "cursor_down", "Cursor Down", show=False),
|
||||
]
|
||||
|
||||
index = reactive(0, always_update=True)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,35 @@
|
||||
import csv
|
||||
import io
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import DataTable
|
||||
|
||||
CSV = """lane,swimmer,country,time
|
||||
4,Joseph Schooling,Singapore,50.39
|
||||
2,Michael Phelps,United States,51.14
|
||||
5,Chad le Clos,South Africa,51.14
|
||||
6,László Cseh,Hungary,51.14
|
||||
3,Li Zhuhao,China,51.26
|
||||
8,Mehdy Metella,France,51.58
|
||||
7,Tom Shields,United States,51.73
|
||||
1,Aleksandr Sadovnikov,Russia,51.84"""
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
table = DataTable()
|
||||
table.focus()
|
||||
table.cursor_type = "column"
|
||||
table.fixed_columns = 1
|
||||
yield table
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one(DataTable)
|
||||
rows = csv.reader(io.StringIO(CSV))
|
||||
table.add_columns(*next(rows))
|
||||
table.add_rows(rows)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TableApp()
|
||||
app.run()
|
||||
34
tests/snapshot_tests/snapshot_apps/data_table_row_cursor.py
Normal file
34
tests/snapshot_tests/snapshot_apps/data_table_row_cursor.py
Normal file
@@ -0,0 +1,34 @@
|
||||
import csv
|
||||
import io
|
||||
|
||||
from textual.app import App, ComposeResult
|
||||
from textual.widgets import DataTable
|
||||
|
||||
CSV = """lane,swimmer,country,time
|
||||
4,Joseph Schooling,Singapore,50.39
|
||||
2,Michael Phelps,United States,51.14
|
||||
5,Chad le Clos,South Africa,51.14
|
||||
6,László Cseh,Hungary,51.14
|
||||
3,Li Zhuhao,China,51.26
|
||||
8,Mehdy Metella,France,51.58
|
||||
7,Tom Shields,United States,51.73
|
||||
1,Aleksandr Sadovnikov,Russia,51.84"""
|
||||
|
||||
|
||||
class TableApp(App):
|
||||
def compose(self) -> ComposeResult:
|
||||
table = DataTable()
|
||||
table.focus()
|
||||
table.cursor_type = "row"
|
||||
yield table
|
||||
|
||||
def on_mount(self) -> None:
|
||||
table = self.query_one(DataTable)
|
||||
rows = csv.reader(io.StringIO(CSV))
|
||||
table.add_columns(*next(rows))
|
||||
table.add_rows(rows)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = TableApp()
|
||||
app.run()
|
||||
@@ -89,10 +89,20 @@ def test_placeholder_render(snap_compare):
|
||||
|
||||
|
||||
def test_datatable_render(snap_compare):
|
||||
press = ["tab", "down", "down", "right", "up", "left"]
|
||||
press = ["tab", "down", "down", "right", "up", "left", "_"]
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "data_table.py", press=press)
|
||||
|
||||
|
||||
def test_datatable_row_cursor_render(snap_compare):
|
||||
press = ["up", "left", "right", "down", "down", "_"]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_row_cursor.py", press=press)
|
||||
|
||||
|
||||
def test_datatable_column_cursor_render(snap_compare):
|
||||
press = ["left", "up", "down", "right", "right", "_"]
|
||||
assert snap_compare(SNAPSHOT_APPS_DIR / "data_table_column_cursor.py", press=press)
|
||||
|
||||
|
||||
def test_footer_render(snap_compare):
|
||||
assert snap_compare(WIDGET_EXAMPLES_DIR / "footer.py")
|
||||
|
||||
@@ -147,11 +157,11 @@ def test_multiple_css(snap_compare):
|
||||
|
||||
|
||||
def test_order_independence(snap_compare):
|
||||
assert snap_compare("snapshot_apps/order_independence.py")
|
||||
assert snap_compare("snapshot_apps/layer_order_independence.py")
|
||||
|
||||
|
||||
def test_order_independence_toggle(snap_compare):
|
||||
assert snap_compare("snapshot_apps/order_independence.py", press="t,_")
|
||||
assert snap_compare("snapshot_apps/layer_order_independence.py", press="t,_")
|
||||
|
||||
|
||||
def test_columns_height(snap_compare):
|
||||
|
||||
@@ -68,6 +68,7 @@ async def test_input_value_visible_if_mounted_later_and_focused():
|
||||
app = MyApp()
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.press("a")
|
||||
await pilot.pause()
|
||||
console = Console(width=5)
|
||||
with console.capture() as capture:
|
||||
console.print(app.query_one(Input).render())
|
||||
|
||||
@@ -144,10 +144,9 @@ async def test_reactive_always_update():
|
||||
# Value is the same, but always_update=True, so watcher called...
|
||||
app.first_name = "Darren"
|
||||
assert calls == ["first_name Darren"]
|
||||
# TODO: Commented out below due to issue#1230, should work after issue fixed
|
||||
# Value is the same, and always_update=False, so watcher NOT called...
|
||||
# app.last_name = "Burns"
|
||||
# assert calls == ["first_name Darren"]
|
||||
app.last_name = "Burns"
|
||||
assert calls == ["first_name Darren"]
|
||||
# Values changed, watch method always called regardless of always_update
|
||||
app.first_name = "abc"
|
||||
app.last_name = "def"
|
||||
@@ -157,12 +156,6 @@ async def test_reactive_always_update():
|
||||
async def test_reactive_with_callable_default():
|
||||
"""A callable can be supplied as the default value for a reactive.
|
||||
Textual will call it in order to retrieve the default value."""
|
||||
called_with_app = False
|
||||
|
||||
def set_called() -> int:
|
||||
nonlocal called_with_app
|
||||
called_with_app = True
|
||||
return OLD_VALUE
|
||||
|
||||
class ReactiveCallable(App):
|
||||
value = reactive(lambda: 123)
|
||||
@@ -173,9 +166,8 @@ async def test_reactive_with_callable_default():
|
||||
|
||||
app = ReactiveCallable()
|
||||
async with app.run_test():
|
||||
assert (
|
||||
app.value == 123
|
||||
) # The value should be set to the return val of the callable
|
||||
assert app.value == 123
|
||||
assert app.watcher_called_with == 123
|
||||
|
||||
|
||||
async def test_validate_init_true():
|
||||
@@ -228,7 +220,6 @@ async def test_reactive_compute_first_time_set():
|
||||
|
||||
app = ReactiveComputeFirstTimeSet()
|
||||
async with app.run_test():
|
||||
await asyncio.sleep(0.2) # TODO: We sleep here while issue#1218 is open
|
||||
assert app.double_number == 2
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user