diff --git a/CHANGELOG.md b/CHANGELOG.md index a26e4cc86..9b035dd4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/docs/widgets/option_list.md b/docs/widgets/option_list.md index 2c4a80789..a489e6b4f 100644 --- a/docs/widgets/option_list.md +++ b/docs/widgets/option_list.md @@ -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 diff --git a/src/textual/widgets/_directory_tree.py b/src/textual/widgets/_directory_tree.py index b2a8ab692..20b9117e1 100644 --- a/src/textual/widgets/_directory_tree.py +++ b/src/textual/widgets/_directory_tree.py @@ -64,7 +64,9 @@ class DirectoryTree(Tree[DirEntry]): `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: @@ -72,11 +74,22 @@ class DirectoryTree(Tree[DirEntry]): 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. diff --git a/src/textual/widgets/_list_view.py b/src/textual/widgets/_list_view.py index efd76d4d6..7c5145e96 100644 --- a/src/textual/widgets/_list_view.py +++ b/src/textual/widgets/_list_view.py @@ -44,30 +44,46 @@ class ListView(VerticalScroll, can_focus=True, can_focus_children=False): 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, diff --git a/src/textual/widgets/_markdown.py b/src/textual/widgets/_markdown.py index 654f838d9..738edd675 100644 --- a/src/textual/widgets/_markdown.py +++ b/src/textual/widgets/_markdown.py @@ -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) @@ -103,7 +105,7 @@ class MarkdownBlock(Static): 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): @@ -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): @@ -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( @@ -565,27 +567,62 @@ class Markdown(Widget): 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) @@ -629,20 +666,20 @@ class Markdown(Widget): 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 @@ -651,22 +688,23 @@ class Markdown(Widget): ) 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": @@ -742,12 +780,13 @@ class Markdown(Widget): 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) @@ -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 @@ -804,8 +864,9 @@ class MarkdownTableOfContents(Widget, can_focus_children=True): 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): @@ -896,8 +957,9 @@ class MarkdownViewer(VerticalScroll, can_focus=True, can_focus_children=True): 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 diff --git a/src/textual/widgets/_option_list.py b/src/textual/widgets/_option_list.py index 193c0385f..3b9413181 100644 --- a/src/textual/widgets/_option_list.py +++ b/src/textual/widgets/_option_list.py @@ -255,6 +255,15 @@ class OptionList(ScrollView, can_focus=True): 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 diff --git a/src/textual/widgets/_radio_set.py b/src/textual/widgets/_radio_set.py index 4003686b4..0366e42fc 100644 --- a/src/textual/widgets/_radio_set.py +++ b/src/textual/widgets/_radio_set.py @@ -92,6 +92,15 @@ class RadioSet(Container, can_focus=True, can_focus_children=False): self.index = radio_set.pressed_index """The index of the [`RadioButton`][textual.widgets.RadioButton] that was pressed to make the change.""" + @property + def control(self) -> RadioSet: + """A reference to the [`RadioSet`][textual.widgets.RadioSet] that was changed. + + This is an alias for [`Changed.radio_set`][textual.widgets.RadioSet.Changed.radio_set] + and is used by the [`on`][textual.on] decorator. + """ + return self.radio_set + def __rich_repr__(self) -> rich.repr.Result: yield "radio_set", self.radio_set yield "pressed", self.pressed diff --git a/src/textual/widgets/_tabbed_content.py b/src/textual/widgets/_tabbed_content.py index c16c3d3b8..1242ee2be 100644 --- a/src/textual/widgets/_tabbed_content.py +++ b/src/textual/widgets/_tabbed_content.py @@ -96,9 +96,20 @@ class TabbedContent(Widget): tab: The Tab widget that was selected (contains the tab label). """ self.tabbed_content = tabbed_content + """The `TabbedContent` widget that contains the tab activated.""" self.tab = tab + """The `Tab` widget that was selected (contains the tab label).""" super().__init__() + @property + def control(self) -> TabbedContent: + """The `TabbedContent` widget that contains the tab activated. + + This is an alias for [`TabActivated.tabbed_content`][textual.widgets.TabbedContent.TabActivated.tabbed_content] + and is used by the [`on`][textual.on] decorator. + """ + return self.tabbed_content + def __rich_repr__(self) -> Result: yield self.tabbed_content yield self.tab diff --git a/src/textual/widgets/_tree.py b/src/textual/widgets/_tree.py index 0f3998914..aab6e9cc5 100644 --- a/src/textual/widgets/_tree.py +++ b/src/textual/widgets/_tree.py @@ -510,57 +510,101 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): Can be handled using `on_tree_node_collapsed` in a subclass of `Tree` or in a parent node in the DOM. - - Attributes: - node: The node that was collapsed. """ - def __init__(self, node: TreeNode[EventTreeDataType]) -> None: + def __init__( + self, tree: Tree[EventTreeDataType], node: TreeNode[EventTreeDataType] + ) -> None: + self.tree = tree + """The tree that sent the message.""" self.node: TreeNode[EventTreeDataType] = node + """The node that was collapsed.""" super().__init__() + @property + def control(self) -> Tree[EventTreeDataType]: + """The tree that sent the message. + + This is an alias for [`NodeCollapsed.tree`][textual.widgets.Tree.NodeCollapsed.tree] + and is used by the [`on`][textual.on] decorator. + """ + return self.tree + class NodeExpanded(Generic[EventTreeDataType], Message, bubble=True): """Event sent when a node is expanded. Can be handled using `on_tree_node_expanded` in a subclass of `Tree` or in a parent node in the DOM. - - Attributes: - node: The node that was expanded. """ - def __init__(self, node: TreeNode[EventTreeDataType]) -> None: + def __init__( + self, tree: Tree[EventTreeDataType], node: TreeNode[EventTreeDataType] + ) -> None: + self.tree = tree + """The tree that sent the message.""" self.node: TreeNode[EventTreeDataType] = node + """The node that was expanded.""" super().__init__() + @property + def control(self) -> Tree[EventTreeDataType]: + """The tree that sent the message. + + This is an alias for [`NodeExpanded.tree`][textual.widgets.Tree.NodeExpanded.tree] + and is used by the [`on`][textual.on] decorator. + """ + return self.tree + class NodeHighlighted(Generic[EventTreeDataType], Message, bubble=True): """Event sent when a node is highlighted. Can be handled using `on_tree_node_highlighted` in a subclass of `Tree` or in a parent node in the DOM. - - Attributes: - node: The node that was highlighted. """ - def __init__(self, node: TreeNode[EventTreeDataType]) -> None: + def __init__( + self, tree: Tree[EventTreeDataType], node: TreeNode[EventTreeDataType] + ) -> None: + self.tree = tree + """The tree that sent the message.""" self.node: TreeNode[EventTreeDataType] = node + """The node that was highlighted.""" super().__init__() + @property + def control(self) -> Tree[EventTreeDataType]: + """The tree that sent the message. + + This is an alias for [`NodeHighlighted.tree`][textual.widgets.Tree.NodeHighlighted.tree] + and is used by the [`on`][textual.on] decorator. + """ + return self.tree + class NodeSelected(Generic[EventTreeDataType], Message, bubble=True): """Event sent when a node is selected. Can be handled using `on_tree_node_selected` in a subclass of `Tree` or in a parent node in the DOM. - - Attributes: - node: The node that was selected. """ - def __init__(self, node: TreeNode[EventTreeDataType]) -> None: + def __init__( + self, tree: Tree[EventTreeDataType], node: TreeNode[EventTreeDataType] + ) -> None: + self.tree = tree + """The tree that sent the message.""" self.node: TreeNode[EventTreeDataType] = node + """The node that was selected.""" super().__init__() + @property + def control(self) -> Tree[EventTreeDataType]: + """The tree that sent the message. + + This is an alias for [`NodeSelected.tree`][textual.widgets.Tree.NodeSelected.tree] + and is used by the [`on`][textual.on] decorator. + """ + return self.tree + def __init__( self, label: TextType, @@ -859,7 +903,7 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): node._selected = True self._cursor_node = node if previous_node != node: - self.post_message(self.NodeHighlighted(node)) + self.post_message(self.NodeHighlighted(self, node)) else: self._cursor_node = None @@ -1113,10 +1157,10 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): return if node.is_expanded: node.collapse() - self.post_message(self.NodeCollapsed(node)) + self.post_message(self.NodeCollapsed(self, node)) else: node.expand() - self.post_message(self.NodeExpanded(node)) + self.post_message(self.NodeExpanded(self, node)) async def _on_click(self, event: events.Click) -> None: meta = event.style.meta @@ -1203,4 +1247,4 @@ class Tree(Generic[TreeDataType], ScrollView, can_focus=True): node = line.path[-1] if self.auto_expand: self._toggle_node(node) - self.post_message(self.NodeSelected(node)) + self.post_message(self.NodeSelected(self, node))