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

DataTable navigation updates #4633

Merged
merged 11 commits into from
Jun 11, 2024
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,18 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Added

- Added support for Kitty's key protocol https://github.com/Textualize/textual/pull/4631
- Added `App.CLOSE_TIMEOUT` https://github.com/Textualize/textual/pull/4635
- `ctrl+pageup`/`ctrl+pagedown` will scroll page left/right in DataTable https://github.com/Textualize/textual/pull/4633
- `g`/`G` will scroll to the top/bottom of the DataTable https://github.com/Textualize/textual/pull/4633
- Added simple `hjkl` key bindings to move the cursor in DataTable https://github.com/Textualize/textual/pull/4633

### Changed

- `home` and `end` now works horizontally instead of vertically in DataTable https://github.com/Textualize/textual/pull/4633

### Fixed

- Fixed pageup/pagedown behavior in DataTable https://github.com/Textualize/textual/pull/4633
- Added `App.CLOSE_TIMEOUT` https://github.com/Textualize/textual/pull/4635
- Fixed deadlock on shutdown https://github.com/Textualize/textual/pull/4635

## [0.66.0] - 2024-06-08
Expand Down
4 changes: 4 additions & 0 deletions src/textual/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ class ScrollableContainer(Widget, can_focus=True, inherit_bindings=False):
Binding("end", "scroll_end", "Scroll End", show=False),
Binding("pageup", "page_up", "Page Up", show=False),
Binding("pagedown", "page_down", "Page Down", show=False),
Binding("ctrl+pageup", "page_left", "Page Left", show=False),
Binding("ctrl+pagedown", "page_right", "Page Right", show=False),
Comment on lines +50 to +51
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Horizontally navigating widgets can be a bit slow when using the keyboard.

If you hold ctrl and use the scroll wheel, it applies it in the horizontal direction. So, I thought maybe we could have that apply to pageup/pagedown too.

]
"""Keyboard bindings for scrollable containers.

Expand All @@ -60,6 +62,8 @@ class ScrollableContainer(Widget, can_focus=True, inherit_bindings=False):
| end | Scroll to the end position, if scrolling is available. |
| pageup | Scroll up one page, if vertical scrolling is available. |
| pagedown | Scroll down one page, if vertical scrolling is available. |
| ctrl+pageup | Scroll left one page, if horizontal scrolling is available. |
| ctrl+pagedown | Scroll right one page, if horizontal scrolling is available. |
"""


Expand Down
12 changes: 12 additions & 0 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -3890,6 +3890,18 @@ def action_page_up(self) -> None:
self._clear_anchor()
self.scroll_page_up()

def action_page_left(self) -> None:
if not self.allow_horizontal_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_page_left()

def action_page_right(self) -> None:
if not self.allow_horizontal_scroll:
raise SkipAction()
self._clear_anchor()
self.scroll_page_right()

def notify(
self,
message: str,
Expand Down
112 changes: 79 additions & 33 deletions src/textual/widgets/_data_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,21 +216,31 @@ class DataTable(ScrollView, Generic[CellType], can_focus=True):

BINDINGS: ClassVar[list[BindingType]] = [
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),
Binding("up,k", "cursor_up", "Cursor Up", show=False),
Binding("down,j", "cursor_down", "Cursor Down", show=False),
Binding("right,l", "cursor_right", "Cursor Right", show=False),
Binding("left,h", "cursor_left", "Cursor Left", show=False),
Binding("pageup", "page_up", "Page Up", show=False),
Binding("pagedown", "page_down", "Page Down", show=False),
Binding("g", "scroll_top", "Top", show=False),
Binding("G", "scroll_bottom", "Bottom", show=False),
Binding("home", "scroll_home", "Home", show=False),
Binding("end", "scroll_end", "End", show=False),
Comment on lines +219 to +228
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some common bindings, particularly for horizontal navigation which I found to be lacking when using the keyboard.

]
"""
| Key(s) | Description |
| :- | :- |
| enter | Select cells under the cursor. |
| up | Move the cursor up. |
| down | Move the cursor down. |
| right | Move the cursor right. |
| left | Move the cursor left. |
| up,k | Move the cursor up. |
| down,j | Move the cursor down. |
| right,l | Move the cursor right. |
| left,h | Move the cursor left. |
| pageup | Move one page up. |
| pagedown | Move one page down. |
| g | Move to the top. |
| G | Move to the bottom. |
| home | Move to the home position (leftmost column). |
| end | Move to the end position (rightmost column). |
"""

COMPONENT_CLASSES: ClassVar[set[str]] = {
Expand Down Expand Up @@ -755,7 +765,7 @@ def _y_offsets(self) -> list[tuple[RowKey, int]]:
y-coordinate, we can index into this list to find which row that y-coordinate
lands on, and the y-offset *within* that row. The length of the returned list
is therefore the total height of all rows within the DataTable."""
y_offsets = []
y_offsets: list[tuple[RowKey, int]] = []
if self._update_count in self._offset_cache:
y_offsets = self._offset_cache[self._update_count]
else:
Expand Down Expand Up @@ -1096,6 +1106,7 @@ def watch_cursor_coordinate(
elif self.cursor_type == "column":
self.refresh_column(old_coordinate.column)
self._highlight_column(new_coordinate.column)

if self._require_update_dimensions:
self.call_after_refresh(self._scroll_cursor_into_view)
else:
Expand All @@ -1107,6 +1118,7 @@ def move_cursor(
row: int | None = None,
column: int | None = None,
animate: bool = False,
scroll: bool = True,
) -> None:
"""Move the cursor to the given position.

Expand All @@ -1123,6 +1135,7 @@ def move_cursor(
row: The new row to move the cursor to.
column: The new column to move the cursor to.
animate: Whether to animate the change of coordinates.
scroll: Scroll the cursor into view after moving.
"""

cursor_row, cursor_column = self.cursor_coordinate
Expand All @@ -1138,10 +1151,11 @@ def move_cursor(
# of rows then tried to immediately move the cursor.
# We do this before setting `cursor_coordinate` because its watcher will also
# schedule a call to `_scroll_cursor_into_view` without optionally animating.
if self._require_update_dimensions:
self.call_after_refresh(self._scroll_cursor_into_view, animate=animate)
else:
self._scroll_cursor_into_view(animate=animate)
if scroll:
if self._require_update_dimensions:
self.call_after_refresh(self._scroll_cursor_into_view, animate=animate)
else:
self._scroll_cursor_into_view(animate=animate)

self.cursor_coordinate = destination

Expand Down Expand Up @@ -2422,7 +2436,7 @@ def _scroll_cursor_into_view(self, animate: bool = False) -> None:
else:
region = self._get_cell_region(self.cursor_coordinate)

self.scroll_to_region(region, animate=animate, spacing=fixed_offset)
self.scroll_to_region(region, animate=animate, spacing=fixed_offset, force=True)

def _set_hover_cursor(self, active: bool) -> None:
"""Set whether the hover cursor (the faint cursor you see when you
Expand All @@ -2445,7 +2459,7 @@ def _set_hover_cursor(self, active: bool) -> None:
async def _on_click(self, event: events.Click) -> None:
self._set_hover_cursor(True)
meta = event.style.meta
if not "row" in meta or not "column" in meta:
if "row" not in meta or "column" not in meta:
return

row_index = meta["row"]
Expand Down Expand Up @@ -2476,66 +2490,98 @@ def action_page_down(self) -> None:
"""Move the cursor one page down."""
self._set_hover_cursor(False)
if self.show_cursor and self.cursor_type in ("cell", "row"):
height = self.size.height - (self.header_height if self.show_header else 0)
height = self.scrollable_content_region.height - (
self.header_height if self.show_header else 0
)

# Determine how many rows constitutes a "page"
offset = 0
rows_to_scroll = 0
row_index, column_index = self.cursor_coordinate
row_index, _ = self.cursor_coordinate
for ordered_row in self.ordered_rows[row_index:]:
offset += ordered_row.height
rows_to_scroll += 1
if offset > height:
break
rows_to_scroll += 1

self.cursor_coordinate = Coordinate(
row_index + rows_to_scroll - 1, column_index
)
target_row = row_index + rows_to_scroll - 1
self.scroll_relative(y=height, animate=False, force=True)
self.move_cursor(row=target_row, scroll=False)
else:
super().action_page_down()

def action_page_up(self) -> None:
"""Move the cursor one page up."""
self._set_hover_cursor(False)
if self.show_cursor and self.cursor_type in ("cell", "row"):
height = self.size.height - (self.header_height if self.show_header else 0)
height = self.scrollable_content_region.height - (
self.header_height if self.show_header else 0
)

# Determine how many rows constitutes a "page"
offset = 0
rows_to_scroll = 0
row_index, column_index = self.cursor_coordinate
row_index, _ = self.cursor_coordinate
for ordered_row in self.ordered_rows[: row_index + 1]:
offset += ordered_row.height
rows_to_scroll += 1
if offset > height:
break
rows_to_scroll += 1

self.cursor_coordinate = Coordinate(
row_index - rows_to_scroll + 1, column_index
)
target_row = row_index - rows_to_scroll + 1
self.scroll_relative(y=-height, animate=False)
self.move_cursor(row=target_row, scroll=False)
else:
super().action_page_up()

def action_scroll_home(self) -> None:
"""Scroll to the top of the data table."""
def action_page_left(self) -> None:
"""Move the cursor one page left."""
self._set_hover_cursor(False)
super().scroll_page_left()

def action_page_right(self) -> None:
"""Move the cursor one page right."""
self._set_hover_cursor(False)
super().scroll_page_right()

def action_scroll_top(self) -> None:
"""Move the cursor and scroll to the top."""
self._set_hover_cursor(False)
cursor_type = self.cursor_type
if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
row_index, column_index = self.cursor_coordinate
_, column_index = self.cursor_coordinate
self.cursor_coordinate = Coordinate(0, column_index)
else:
super().action_scroll_home()

def action_scroll_end(self) -> None:
"""Scroll to the bottom of the data table."""
def action_scroll_bottom(self) -> None:
"""Move the cursor and scroll to the bottom."""
self._set_hover_cursor(False)
cursor_type = self.cursor_type
if self.show_cursor and (cursor_type == "cell" or cursor_type == "row"):
row_index, column_index = self.cursor_coordinate
_, column_index = self.cursor_coordinate
self.cursor_coordinate = Coordinate(self.row_count - 1, column_index)
else:
super().action_scroll_end()

def action_scroll_home(self) -> None:
"""Move the cursor and scroll to the leftmost column."""
self._set_hover_cursor(False)
cursor_type = self.cursor_type
if self.show_cursor and (cursor_type == "cell" or cursor_type == "column"):
self.move_cursor(column=0)
else:
self.scroll_x = 0

def action_scroll_end(self) -> None:
"""Move the cursor and scroll to the rightmost column."""
self._set_hover_cursor(False)
cursor_type = self.cursor_type
if self.show_cursor and (cursor_type == "cell" or cursor_type == "column"):
self.move_cursor(column=len(self.columns) - 1)
else:
self.scroll_x = self.max_scroll_x
Comment on lines +2567 to +2583
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quickly jumping to the first/last cell in a row. Also intended to improve horizontal navigation with the keyboard.


def action_cursor_up(self) -> None:
self._set_hover_cursor(False)
cursor_type = self.cursor_type
Expand Down
6 changes: 3 additions & 3 deletions tests/test_data_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,11 +192,11 @@ async def test_cursor_movement_with_home_pagedown_etc(show_header):
await pilot.pause()
assert table.cursor_coordinate == Coordinate(0, 1)

await pilot.press("end")
await pilot.press("home")
await pilot.pause()
assert table.cursor_coordinate == Coordinate(2, 1)
assert table.cursor_coordinate == Coordinate(0, 0)

await pilot.press("home")
await pilot.press("end")
await pilot.pause()
assert table.cursor_coordinate == Coordinate(0, 1)

Expand Down
Loading