Skip to content

Commit

Permalink
Merge pull request Textualize#2490 from Textualize/messages-control
Browse files Browse the repository at this point in the history
Add control to widget messages.
  • Loading branch information
rodrigogiraoserrao authored May 8, 2023
2 parents 9c9829e + 8059e5c commit a31e086
Show file tree
Hide file tree
Showing 9 changed files with 246 additions and 69 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,30 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- Added `TreeNode.remove_children` https://github.com/Textualize/textual/pull/2510
- Added `TreeNode.remove` https://github.com/Textualize/textual/pull/2510

### Added

- Markdown document sub-widgets now reference the container document
- Table of contents of a markdown document now references the document
- Added the `control` property to messages
- `DirectoryTree.FileSelected`
- `ListView`
- `Highlighted`
- `Selected`
- `Markdown`
- `TableOfContentsUpdated`
- `TableOfContentsSelected`
- `LinkClicked`
- `OptionList`
- `OptionHighlighted`
- `OptionSelected`
- `RadioSet.Changed`
- `TabContent.TabActivated`
- `Tree`
- `NodeSelected`
- `NodeHighlighted`
- `NodeExpanded`
- `NodeCollapsed`

## [0.23.0] - 2023-05-03

### Fixed
Expand Down
13 changes: 1 addition & 12 deletions docs/widgets/option_list.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,18 +90,7 @@ tables](https://rich.readthedocs.io/en/latest/tables.html):
- [OptionList.OptionHighlight][textual.widgets.OptionList.OptionHighlighted]
- [OptionList.OptionSelected][textual.widgets.OptionList.OptionSelected]

Both of the messages above inherit from this common base, which makes
available the following properties relating to the `OptionList` and the
related `Option`:

### Common message properties

Both of the above messages provide the following properties:

#### ::: textual.widgets.OptionList.OptionMessage.option
#### ::: textual.widgets.OptionList.OptionMessage.option_id
#### ::: textual.widgets.OptionList.OptionMessage.option_index
#### ::: textual.widgets.OptionList.OptionMessage.option_list
Both of the messages above inherit from the common base [`OptionList`][textual.widgets.OptionList.OptionMessage], so refer to its documentation to see what attributes are available.

## Bindings

Expand Down
15 changes: 14 additions & 1 deletion src/textual/widgets/_directory_tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,32 @@ class FileSelected(Message, bubble=True):
`DirectoryTree` or in a parent widget in the DOM.
"""

def __init__(self, node: TreeNode[DirEntry], path: Path) -> None:
def __init__(
self, tree: DirectoryTree, node: TreeNode[DirEntry], path: Path
) -> None:
"""Initialise the FileSelected object.
Args:
node: The tree node for the file that was selected.
path: The path of the file that was selected.
"""
super().__init__()
self.tree: DirectoryTree = tree
"""The `DirectoryTree` that had a file selected."""
self.node: TreeNode[DirEntry] = node
"""The tree node of the file that was selected."""
self.path: Path = path
"""The path of the file that was selected."""

@property
def control(self) -> DirectoryTree:
"""The `DirectoryTree` that had a file selected.
This is an alias for [`FileSelected.tree`][textual.widgets.DirectoryTree.FileSelected.tree]
which is used by the [`on`][textual.on] decorator.
"""
return self.tree

path: var[str | Path] = var["str | Path"](Path("."), init=False)
"""The path that is the root of the directory tree.
Expand Down
32 changes: 24 additions & 8 deletions src/textual/widgets/_list_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,46 @@ class Highlighted(Message, bubble=True):
Highlighted item is controlled using up/down keys.
Can be handled using `on_list_view_highlighted` in a subclass of `ListView`
or in a parent widget in the DOM.
Attributes:
item: The highlighted item, if there is one highlighted.
"""

def __init__(self, list_view: ListView, item: ListItem | None) -> None:
super().__init__()
self.list_view = list_view
self.list_view: ListView = list_view
"""The view that contains the item highlighted."""
self.item: ListItem | None = item
"""The highlighted item, if there is one highlighted."""

@property
def control(self) -> ListView:
"""The view that contains the item highlighted.
This is an alias for [`Highlighted.list_view`][textual.widgets.ListView.Highlighted.list_view]
and is used by the [`on`][textual.on] decorator.
"""
return self.list_view

class Selected(Message, bubble=True):
"""Posted when a list item is selected, e.g. when you press the enter key on it.
Can be handled using `on_list_view_selected` in a subclass of `ListView` or in
a parent widget in the DOM.
Attributes:
item: The selected item.
"""

def __init__(self, list_view: ListView, item: ListItem) -> None:
super().__init__()
self.list_view = list_view
self.list_view: ListView = list_view
"""The view that contains the item selected."""
self.item: ListItem = item
"""The selected item."""

@property
def control(self) -> ListView:
"""The view that contains the item selected.
This is an alias for [`Selected.list_view`][textual.widgets.ListView.Selected.list_view]
and is used by the [`on`][textual.on] decorator.
"""
return self.list_view

def __init__(
self,
Expand Down
118 changes: 90 additions & 28 deletions src/textual/widgets/_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,9 @@ class MarkdownBlock(Static):
}
"""

def __init__(self, *args, **kwargs) -> None:
def __init__(self, markdown: Markdown, *args, **kwargs) -> None:
self._markdown: Markdown = markdown
"""A reference to the Markdown document that contains this block."""
self._text = Text()
self._blocks: list[MarkdownBlock] = []
super().__init__(*args, **kwargs)
Expand All @@ -103,7 +105,7 @@ def set_content(self, text: Text) -> None:

async def action_link(self, href: str) -> None:
"""Called on link click."""
self.post_message(Markdown.LinkClicked(href))
self.post_message(Markdown.LinkClicked(self._markdown, href))


class MarkdownHeader(MarkdownBlock):
Expand Down Expand Up @@ -453,9 +455,9 @@ class MarkdownListItem(MarkdownBlock):
}
"""

def __init__(self, bullet: str) -> None:
def __init__(self, markdown: Markdown, bullet: str) -> None:
self.bullet = bullet
super().__init__()
super().__init__(markdown)


class MarkdownOrderedListItem(MarkdownListItem):
Expand Down Expand Up @@ -484,10 +486,10 @@ class MarkdownFence(MarkdownBlock):
}
"""

def __init__(self, code: str, lexer: str) -> None:
def __init__(self, markdown: Markdown, code: str, lexer: str) -> None:
self.code = code
self.lexer = lexer
super().__init__()
super().__init__(markdown)

def compose(self) -> ComposeResult:
yield Static(
Expand Down Expand Up @@ -565,27 +567,62 @@ def __init__(
class TableOfContentsUpdated(Message, bubble=True):
"""The table of contents was updated."""

def __init__(self, table_of_contents: TableOfContentsType) -> None:
def __init__(
self, markdown: Markdown, table_of_contents: TableOfContentsType
) -> None:
super().__init__()
self.markdown: Markdown = markdown
"""The `Markdown` widget associated with the table of contents."""
self.table_of_contents: TableOfContentsType = table_of_contents
"""Table of contents."""

@property
def control(self) -> Markdown:
"""The `Markdown` widget associated with the table of contents.
This is an alias for [`TableOfContentsUpdated.markdown`][textual.widgets.Markdown.TableOfContentsSelected.markdown]
and is used by the [`on`][textual.on] decorator.
"""
return self.markdown

class TableOfContentsSelected(Message, bubble=True):
"""An item in the TOC was selected."""

def __init__(self, block_id: str) -> None:
def __init__(self, markdown: Markdown, block_id: str) -> None:
super().__init__()
self.block_id = block_id
self.markdown: Markdown = markdown
"""The `Markdown` widget where the selected item is."""
self.block_id: str = block_id
"""ID of the block that was selected."""

@property
def control(self) -> Markdown:
"""The `Markdown` widget where the selected item is.
This is an alias for [`TableOfContentsSelected.markdown`][textual.widgets.Markdown.TableOfContentsSelected.markdown]
and is used by the [`on`][textual.on] decorator.
"""
return self.markdown

class LinkClicked(Message, bubble=True):
"""A link in the document was clicked."""

def __init__(self, href: str) -> None:
def __init__(self, markdown: Markdown, href: str) -> None:
super().__init__()
self.markdown: Markdown = markdown
"""The `Markdown` widget containing the link clicked."""
self.href: str = href
"""The link that was selected."""

@property
def control(self) -> Markdown:
"""The `Markdown` widget containing the link clicked.
This is an alias for [`LinkClicked.markdown`][textual.widgets.Markdown.LinkClicked.markdown]
and is used by the [`on`][textual.on] decorator.
"""
return self.markdown

def _on_mount(self, _: Mount) -> None:
if self._markdown is not None:
self.update(self._markdown)
Expand Down Expand Up @@ -629,20 +666,20 @@ def update(self, markdown: str) -> None:
for token in parser.parse(markdown):
if token.type == "heading_open":
block_id += 1
stack.append(HEADINGS[token.tag](id=f"block{block_id}"))
stack.append(HEADINGS[token.tag](self, id=f"block{block_id}"))
elif token.type == "hr":
output.append(MarkdownHorizontalRule())
output.append(MarkdownHorizontalRule(self))
elif token.type == "paragraph_open":
stack.append(MarkdownParagraph())
stack.append(MarkdownParagraph(self))
elif token.type == "blockquote_open":
stack.append(MarkdownBlockQuote())
stack.append(MarkdownBlockQuote(self))
elif token.type == "bullet_list_open":
stack.append(MarkdownBulletList())
stack.append(MarkdownBulletList(self))
elif token.type == "ordered_list_open":
stack.append(MarkdownOrderedList())
stack.append(MarkdownOrderedList(self))
elif token.type == "list_item_open":
if token.info:
stack.append(MarkdownOrderedListItem(token.info))
stack.append(MarkdownOrderedListItem(self, token.info))
else:
item_count = sum(
1
Expand All @@ -651,22 +688,23 @@ def update(self, markdown: str) -> None:
)
stack.append(
MarkdownUnorderedListItem(
self.BULLETS[item_count % len(self.BULLETS)]
self,
self.BULLETS[item_count % len(self.BULLETS)],
)
)

elif token.type == "table_open":
stack.append(MarkdownTable())
stack.append(MarkdownTable(self))
elif token.type == "tbody_open":
stack.append(MarkdownTBody())
stack.append(MarkdownTBody(self))
elif token.type == "thead_open":
stack.append(MarkdownTHead())
stack.append(MarkdownTHead(self))
elif token.type == "tr_open":
stack.append(MarkdownTR())
stack.append(MarkdownTR(self))
elif token.type == "th_open":
stack.append(MarkdownTH())
stack.append(MarkdownTH(self))
elif token.type == "td_open":
stack.append(MarkdownTD())
stack.append(MarkdownTD(self))
elif token.type.endswith("_close"):
block = stack.pop()
if token.type == "heading_close":
Expand Down Expand Up @@ -742,12 +780,13 @@ def update(self, markdown: str) -> None:
elif token.type == "fence":
output.append(
MarkdownFence(
self,
token.content.rstrip(),
token.info,
)
)

self.post_message(Markdown.TableOfContentsUpdated(table_of_contents))
self.post_message(Markdown.TableOfContentsUpdated(self, table_of_contents))
with self.app.batch_update():
self.query("MarkdownBlock").remove()
self.mount_all(output)
Expand All @@ -768,6 +807,27 @@ class MarkdownTableOfContents(Widget, can_focus_children=True):

table_of_contents = reactive["TableOfContentsType | None"](None, init=False)

def __init__(
self,
markdown: Markdown,
name: str | None = None,
id: str | None = None,
classes: str | None = None,
disabled: bool = False,
) -> None:
"""Initialize a table of contents.
Args:
markdown: The Markdown document associated with this table of contents.
name: The name of the widget.
id: The ID of the widget in the DOM.
classes: The CSS classes for the widget.
disabled: Whether the widget is disabled or not.
"""
self.markdown = markdown
"""The Markdown document associated with this table of contents."""
super().__init__(name=name, id=id, classes=classes, disabled=disabled)

def compose(self) -> ComposeResult:
tree: Tree = Tree("TOC")
tree.show_root = False
Expand Down Expand Up @@ -804,8 +864,9 @@ async def _on_tree_node_selected(self, message: Tree.NodeSelected) -> None:
node_data = message.node.data
if node_data is not None:
await self._post_message(
Markdown.TableOfContentsSelected(node_data["block_id"])
Markdown.TableOfContentsSelected(self.markdown, node_data["block_id"])
)
message.stop()


class MarkdownViewer(VerticalScroll, can_focus=True, can_focus_children=True):
Expand Down Expand Up @@ -896,8 +957,9 @@ def watch_show_table_of_contents(self, show_table_of_contents: bool) -> None:
self.set_class(show_table_of_contents, "-show-table-of-contents")

def compose(self) -> ComposeResult:
yield MarkdownTableOfContents()
yield Markdown(parser_factory=self._parser_factory)
markdown = Markdown(parser_factory=self._parser_factory)
yield MarkdownTableOfContents(markdown)
yield markdown

def _on_markdown_table_of_contents_updated(
self, message: Markdown.TableOfContentsUpdated
Expand Down
9 changes: 9 additions & 0 deletions src/textual/widgets/_option_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ def __init__(self, option_list: OptionList, index: int) -> None:
self.option_index: int = index
"""The index of the option that the message relates to."""

@property
def control(self) -> OptionList:
"""The option list that sent the message.
This is an alias for [`OptionMessage.option_list`][textual.widgets.OptionList.OptionMessage.option_list]
and is used by the [`on`][textual.on] decorator.
"""
return self.option_list

def __rich_repr__(self) -> Result:
yield "option_list", self.option_list
yield "option", self.option
Expand Down
Loading

0 comments on commit a31e086

Please sign in to comment.