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

Move default scroll navigation keys out of Widget and into ScrollableContainer #2337

Merged
merged 11 commits into from
Apr 20, 2023
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## Unreleased

### Changed

- Breaking change: standard keyboard scrollable navigation bindings have been moved off `Widget` and onto a new base class for scrollable containers (see also below addition) https://github.com/Textualize/textual/issues/2332
- `ScrollView` now inherits from `ScrollableContainer` rather than `Widget` https://github.com/Textualize/textual/issues/2332
- Containers no longer inherit any bindings from `Widget` https://github.com/Textualize/textual/issues/2331

### Added

- Added `ScrollableContainer`; a container class that binds the common navigation keys to scroll actions (see also above breaking change) https://github.com/Textualize/textual/issues/2332

### Fixed

- Fixed dark mode toggles in a "child" screen not updating a "parent" screen https://github.com/Textualize/textual/issues/1999
Expand Down
48 changes: 40 additions & 8 deletions src/textual/containers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@

"""

from __future__ import annotations

from typing import ClassVar

from .binding import Binding, BindingType
from .widget import Widget


Expand All @@ -19,7 +23,35 @@ class Container(Widget):
"""


class Vertical(Widget):
class ScrollableContainer(Widget, inherit_bindings=False):
"""Base container widget that binds navigation keys for scrolling."""

BINDINGS: ClassVar[list[BindingType]] = [
Binding("up", "scroll_up", "Scroll Up", show=False),
Binding("down", "scroll_down", "Scroll Down", show=False),
Binding("left", "scroll_left", "Scroll Up", show=False),
Binding("right", "scroll_right", "Scroll Right", show=False),
Binding("home", "scroll_home", "Scroll Home", show=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),
]
"""Keyboard bindings for scrollable containers.

| Key(s) | Description |
| :- | :- |
| up | Scroll up, if vertical scrolling is available. |
| down | Scroll down, if vertical scrolling is available. |
| left | Scroll left, if horizontal scrolling is available. |
| right | Scroll right, if horizontal scrolling is available. |
| home | Scroll to the home position, if scrolling is available. |
| 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. |
"""


class Vertical(Widget, inherit_bindings=False):
"""A container which arranges children vertically."""

DEFAULT_CSS = """
Expand All @@ -31,7 +63,7 @@ class Vertical(Widget):
"""


class VerticalScroll(Widget, can_focus=True):
class VerticalScroll(ScrollableContainer, can_focus=True):
"""A container which arranges children vertically, with an automatic vertical scrollbar."""

DEFAULT_CSS = """
Expand All @@ -43,7 +75,7 @@ class VerticalScroll(Widget, can_focus=True):
"""


class Horizontal(Widget):
class Horizontal(Widget, inherit_bindings=False):
"""A container which arranges children horizontally."""

DEFAULT_CSS = """
Expand All @@ -55,7 +87,7 @@ class Horizontal(Widget):
"""


class HorizontalScroll(Widget, can_focus=True):
class HorizontalScroll(ScrollableContainer, can_focus=True):
"""A container which arranges children horizontally, with an automatic horizontal scrollbar."""

DEFAULT_CSS = """
Expand All @@ -67,7 +99,7 @@ class HorizontalScroll(Widget, can_focus=True):
"""


class Center(Widget):
class Center(Widget, inherit_bindings=False):
"""A container which centers children horizontally."""

DEFAULT_CSS = """
Expand All @@ -79,7 +111,7 @@ class Center(Widget):
"""


class Middle(Widget):
class Middle(Widget, inherit_bindings=False):
"""A container which aligns children vertically in the middle."""

DEFAULT_CSS = """
Expand All @@ -91,7 +123,7 @@ class Middle(Widget):
"""


class Grid(Widget):
class Grid(Widget, inherit_bindings=False):
"""A container with grid alignment."""

DEFAULT_CSS = """
Expand All @@ -102,7 +134,7 @@ class Grid(Widget):
"""


class Content(Widget, can_focus=True, can_focus_children=False):
class Content(Widget, can_focus=True, can_focus_children=False, inherit_bindings=False):
"""A container for content such as text."""

DEFAULT_CSS = """
Expand Down
4 changes: 2 additions & 2 deletions src/textual/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from textual.app import App, ComposeResult
from textual.binding import Binding
from textual.containers import Container, Horizontal
from textual.containers import Container, Horizontal, ScrollableContainer
from textual.reactive import reactive
from textual.widgets import (
Button,
Expand Down Expand Up @@ -189,7 +189,7 @@
"""


class Body(Container):
class Body(ScrollableContainer):
pass


Expand Down
6 changes: 3 additions & 3 deletions src/textual/scroll_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@

from rich.console import RenderableType

from .containers import ScrollableContainer
from .geometry import Size
from .widget import Widget


class ScrollView(Widget):
class ScrollView(ScrollableContainer):
"""
A base class for a Widget that handles it's own scrolling (i.e. doesn't rely
A base class for a Widget that handles its own scrolling (i.e. doesn't rely
on the compositor to render children).

"""
Expand Down
12 changes: 0 additions & 12 deletions src/textual/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@
from ._styles_cache import StylesCache
from .actions import SkipAction
from .await_remove import AwaitRemove
from .binding import Binding
from .box_model import BoxModel
from .css.query import NoMatches, WrongType
from .css.scalar import ScalarOffset
Expand Down Expand Up @@ -237,17 +236,6 @@ class Widget(DOMNode):

"""

BINDINGS = [
Binding("up", "scroll_up", "Scroll Up", show=False),
Binding("down", "scroll_down", "Scroll Down", show=False),
Binding("left", "scroll_left", "Scroll Up", show=False),
Binding("right", "scroll_right", "Scroll Right", show=False),
Binding("home", "scroll_home", "Scroll Home", show=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),
]

DEFAULT_CSS = """
Widget{
scrollbar-background: $panel-darken-1;
Expand Down
24 changes: 4 additions & 20 deletions tests/test_binding_inheritance.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ async def test_just_app_no_bindings() -> None:
# An application with a single alpha binding.
#
# Sticking with just an app and the default screen: this configuration has a
# BINDINGS on the app itself, and simply binds the letter a -- in other
# words avoiding anything to do with movement keys. The result should be
# that we see the letter a, ctrl+c, and nothing else.
# BINDINGS on the app itself, and simply binds the letter a. The result
# should be that we see the letter a, ctrl+c, and nothing else.


class AlphaBinding(App[None]):
Expand Down Expand Up @@ -114,22 +113,13 @@ def on_mount(self) -> None:
async def test_app_screen_with_bindings() -> None:
"""Test a screen with a single key binding defined."""
async with AppWithScreenThatHasABinding().run_test() as pilot:
# The screen will contain all of the movement keys, because it
# inherits from Widget. That's fine. Let's check they're there, but
# also let's check that they all have a non-priority binding.
assert all(
pilot.app.screen._bindings.get_key(key).priority is False
for key in MOVEMENT_KEYS
)
# Let's also check that the 'a' key is there, and it *is* a priority
# binding.
assert pilot.app.screen._bindings.get_key("a").priority is True


##############################################################################
# A non-default screen with a single low-priority alpha key binding.
#
# As above, but because Screen sets akk keys as high priority by default, we
# As above, but because Screen sets all keys as high priority by default, we
# want to be sure that if we set our keys in our subclass as low priority as
# default, they come through as such.

Expand All @@ -152,13 +142,7 @@ def on_mount(self) -> None:
async def test_app_screen_with_low_bindings() -> None:
"""Test a screen with a single low-priority key binding defined."""
async with AppWithScreenThatHasALowBinding().run_test() as pilot:
# Screens inherit from Widget which means they get movement keys
# too, so let's ensure they're all in there, along with our own key,
# and that everyone is low-priority.
assert all(
pilot.app.screen._bindings.get_key(key).priority is False
for key in ["a", *MOVEMENT_KEYS]
)
assert pilot.app.screen._bindings.get_key("a").priority is False


##############################################################################
Expand Down