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

context sensitive help #4915

Merged
merged 19 commits into from
Aug 22, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `App.COMMAND_PALETTE_KEY` to change default command palette key binding https://github.com/Textualize/textual/pull/4867
- Added `App.get_key_display` https://github.com/Textualize/textual/pull/4890
- Added `DOMNode.BINDING_GROUP` https://github.com/Textualize/textual/pull/4906
- Added `DOMNode.HELP` classvar which contains Markdown help to be shown in the help panel https://github.com/Textualize/textual/pull/4915

### Changed

Expand Down
12 changes: 6 additions & 6 deletions src/textual/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3527,18 +3527,18 @@ def action_focus_previous(self) -> None:
"""An [action](/guide/actions) to focus the previous widget."""
self.screen.focus_previous()

def action_hide_keys(self) -> None:
def action_hide_help_panel(self) -> None:
"""Hide the keys panel (if present)."""
self.screen.query("KeyPanel").remove()
self.screen.query("HelpPanel").remove()

def action_show_keys(self) -> None:
def action_show_help_panel(self) -> None:
"""Show the keys panel."""
from .widgets import KeyPanel
from .widgets import HelpPanel

try:
self.query_one(KeyPanel)
self.query_one(HelpPanel)
except NoMatches:
self.mount(KeyPanel())
self.mount(HelpPanel())

def _on_terminal_supports_synchronized_output(
self, message: messages.TerminalSupportsSynchronizedOutput
Expand Down
15 changes: 14 additions & 1 deletion src/textual/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,17 @@ class CommandPalette(SystemModalScreen):
}
CommandPalette {
background: $background 60%;
align-horizontal: center;
align-horizontal: center;

#--container {
display: none;
}
}

CommandPalette.-ready {
#--container {
display: block;
}
}

CommandPalette > .command-palette--help-text {
Expand Down Expand Up @@ -883,6 +893,7 @@ def _refresh_command_list(
command_list.clear_options().add_options(commands)
if highlighted is not None and highlighted.id:
command_list.highlighted = command_list.get_option_index(highlighted.id)

self._list_visible = bool(command_list.option_count)
self._hit_count = command_list.option_count

Expand Down Expand Up @@ -1019,6 +1030,8 @@ async def _gather_commands(self, search_value: str) -> None:
self._hit_count = 0
self._start_no_matches_countdown(search_value)

self.add_class("-ready")

def _cancel_gather_commands(self) -> None:
"""Cancel any operation that is gather commands."""
self.workers.cancel_group(self, self._GATHER_COMMANDS_GROUP)
Expand Down
3 changes: 3 additions & 0 deletions src/textual/dom.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,9 @@ class DOMNode(MessagePump):
SCOPED_CSS: ClassVar[bool] = True
"""Should default css be limited to the widget type?"""

HELP: ClassVar[str | None] = None
"""Optional help text shown in help panel (Markdown format)."""

# True if this node inherits the CSS from the base class.
_inherit_css: ClassVar[bool] = True

Expand Down
15 changes: 10 additions & 5 deletions src/textual/fuzzy.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,15 @@ def highlight(self, candidate: str) -> Text:
if match is None:
return text
assert match.lastindex is not None
offsets = [
match.span(group_no)[0] for group_no in range(1, match.lastindex + 1)
]
for offset in offsets:
text.stylize(self._match_style, offset, offset + 1)
if self._query in text.plain:
# Favor complete matches
offset = text.plain.index(self._query)
text.stylize(self._match_style, offset, offset + len(self._query))
else:
offsets = [
match.span(group_no)[0] for group_no in range(1, match.lastindex + 1)
]
for offset in offsets:
text.stylize(self._match_style, offset, offset + 1)

return text
14 changes: 9 additions & 5 deletions src/textual/system_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,17 @@ def _system_commands(self) -> Iterable[tuple[str, IgnoreReturnCallbackType, str]
"Quit the application as soon as possible",
)

if self.screen.query("KeyPanel"):
yield ("Hide keys", self.app.action_hide_keys, "Hide the keys panel")
if self.screen.query("HelpPanel"):
yield (
"Hide keys and help panel",
self.app.action_hide_help_panel,
"Hide the keys and widget help panel",
)
else:
yield (
"Show keys",
self.app.action_show_keys,
"Show a summary of available keys",
"Show keys and help panel",
self.app.action_show_help_panel,
"Show help for the focused widget and a summary of available keys",
)

async def discover(self) -> Hits:
Expand Down
2 changes: 2 additions & 0 deletions src/textual/widgets/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ._directory_tree import DirectoryTree
from ._footer import Footer
from ._header import Header
from ._help_panel import HelpPanel
from ._input import Input
from ._key_panel import KeyPanel
from ._label import Label
Expand Down Expand Up @@ -59,6 +60,7 @@
"DirectoryTree",
"Footer",
"Header",
"HelpPanel",
"Input",
"KeyPanel",
"Label",
Expand Down
1 change: 1 addition & 0 deletions src/textual/widgets/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ from ._digits import Digits as Digits
from ._directory_tree import DirectoryTree as DirectoryTree
from ._footer import Footer as Footer
from ._header import Header as Header
from ._help_panel import HelpPanel as HelpPanel
from ._input import Input as Input
from ._key_panel import KeyPanel as KeyPanel
from ._label import Label as Label
Expand Down
99 changes: 99 additions & 0 deletions src/textual/widgets/_help_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from __future__ import annotations

from textwrap import dedent

from ..app import ComposeResult
from ..css.query import NoMatches
from ..widget import Widget
from ..widgets import KeyPanel, Label, Markdown


class HelpPanel(Widget):
"""
Shows context sensitive help for the currently focused widget.
"""

DEFAULT_CSS = """

HelpPanel {
split: right;
width: 33%;
min-width: 30;
max-width: 60;
border-left: vkey $foreground 30%;
padding: 0 1;
height: 1fr;
padding-right: 1;
layout: vertical;
height: 100%;

#title {
width: 1fr;
text-align: center;
text-style: bold;
dock: top;
display: none;
}

#widget-help {
height: auto;
max-height: 50%;
width: 1fr;
padding: 0;
margin: 0;
padding: 1 0;
margin-top: 1;
display: none;
background: $panel;

MarkdownBlock {
padding-left: 2;
padding-right: 2;
}
}

&.-show-help #widget-help {
display: block;
}

KeyPanel#keys-help {
width: 1fr;
height: 1fr;
min-width: initial;
split: initial;
border-left: none;
padding: 0;
}
}

"""

DEFAULT_CLASSES = "-textual-system"

def on_mount(self):
self.watch(self.screen, "focused", self.update_help)

def update_help(self, focused_widget: Widget | None) -> None:
"""Update the help for the focused widget.

Args:
focused_widget: The currently focused widget, or `None` if no widget was focused.
"""
if not self.app.app_focus:
return
if not self.screen.is_active:
return
self.set_class(focused_widget is not None, "-show-help")
if focused_widget is not None:
help = focused_widget.HELP or ""
if not help:
self.remove_class("-show-help")
try:
self.query_one(Markdown).update(dedent(help.rstrip()))
except NoMatches:
pass

def compose(self) -> ComposeResult:
yield Label("Help", id="title")
yield Markdown(id="widget-help")
yield KeyPanel(id="keys-help")
4 changes: 4 additions & 0 deletions src/textual/widgets/_key_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ def render(self) -> Table:


class KeyPanel(VerticalScroll, can_focus=False):
"""
Shows bindings for currently focused widget.
"""

DEFAULT_CSS = """
KeyPanel {
split: right;
Expand Down
14 changes: 14 additions & 0 deletions src/textual/widgets/_option_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,20 @@ def get_content_width(self, container: Size, viewport: Size) -> int:
for option in self._options
)

def get_content_height(self, container: Size, viewport: Size, width: int) -> int:
# Get the content height without requiring a refresh
# TODO: Internal data structure could be simplified
style = self.rich_style
_render_option_content = self._render_option_content
heights = [
len(_render_option_content(index, option, style, width))
for index, option in enumerate(self._options)
]
separator_count = sum(
1 for content in self._contents if isinstance(content, Separator)
)
return sum(heights) + separator_count

def _on_mouse_move(self, event: events.MouseMove) -> None:
"""React to the mouse moving.

Expand Down
2 changes: 1 addition & 1 deletion src/textual/widgets/_sparkline.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class Sparkline(Widget):
| Class | Description |
| :- | :- |
| `sparkline--max-color` | The color used for the larger values in the data. |
| `sparkline--min-color` | The colour used for the smaller values in the data. |
| `sparkline--min-color` | The color used for the smaller values in the data. |
"""

DEFAULT_CSS = """
Expand Down
Loading
Loading