Link to nodes in DOMTree from NodeInfo panel

This commit is contained in:
Isaiah Odhner 2023-06-04 01:58:42 -04:00
parent 0c05e17223
commit f5074d836f

View File

@ -219,9 +219,14 @@ class DOMTree(Tree[DOMNode]):
del self._wait_for_expand del self._wait_for_expand
break break
# Select the node in the tree. # Select the node in the tree.
# Note: `select_node` just places the cursor on the node. It doesn't actually select it.
self.select_node(tree_node) self.select_node(tree_node)
self.scroll_to_node(tree_node) self.scroll_to_node(tree_node)
# Don't toggle the node when selecting it.
auto_expand = self.auto_expand
self.auto_expand = False
self.action_select_cursor() self.action_select_cursor()
self.auto_expand = auto_expand
class _ShowMoreSentinelType: pass class _ShowMoreSentinelType: pass
@ -253,12 +258,14 @@ class PropertiesTree(Tree[object]):
classes=classes, classes=classes,
disabled=disabled, disabled=disabled,
) )
self._already_loaded: dict[TreeNode[object], set[str]] = {} self._already_loaded: dict[TreeNode[object], set[str]] = {}
"""A mapping of tree nodes to the keys that have already been loaded. """A mapping of tree nodes to the keys that have already been loaded.
This allows the tree to be collapsed and expanded without duplicating nodes. This allows the tree to be collapsed and expanded without duplicating nodes.
It's also used for lazy-loading nodes when clicking the ellipsis in long lists... It's also used for lazy-loading nodes when clicking the ellipsis in long lists...
""" """
self._num_keys_accessed: dict[TreeNode[object], int] = {} self._num_keys_accessed: dict[TreeNode[object], int] = {}
"""A mapping of tree nodes to the number of keys that have been accessed.""" """A mapping of tree nodes to the number of keys that have been accessed."""
@ -481,9 +488,51 @@ class PropertiesTree(Tree[object]):
class NodeInfo(Container): class NodeInfo(Container):
class FollowLinkToNode(Message):
"""A message sent when a link is clicked, pointing to a DOM node."""
def __init__(self, dom_node: DOMNode) -> None:
super().__init__()
self.dom_node = dom_node
class StaticWithLinkSupport(Static):
"""Static text that supports DOM node links.
This class exists because actions can't target an arbitrary parent.
The only supported namespaces are `screen` and `app`.
So action_select_node has to be defined directly on the widget that
contains the @click actions.
(Maybe it could be an ad-hoc method on the widget instead.)
https://textual.textualize.io/guide/actions/#namespaces
"""
def __init__(self, node_info: "NodeInfo", *, name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False) -> None:
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
self._node_info = node_info
def action_select_node(self, link_id: int) -> None:
"""Select a DOM node."""
dom_node = self._node_info._link_id_to_node.get(link_id)
print("action_select_node", link_id, dom_node)
if dom_node is None:
return
self.post_message(NodeInfo.FollowLinkToNode(dom_node))
dom_node: var[DOMNode | None] = var[Optional[DOMNode]](None) dom_node: var[DOMNode | None] = var[Optional[DOMNode]](None)
"""The DOM node being inspected.""" """The DOM node being inspected."""
def __init__(self, *, name: str | None = None, id: str | None = None, classes: str | None = None, disabled: bool = False) -> None:
super().__init__(name=name, id=id, classes=classes, disabled=disabled)
self._link_id_counter = 0
"""A counter used to generate unique IDs for links,
since CSS selectors aren't unique (without something like `nth-child()`),
and DOMNodes can't be used as arguments to an action function.
"""
self._link_id_to_node: dict[int, DOMNode] = {}
"""A mapping of link IDs to DOM nodes."""
def compose(self) -> ComposeResult: def compose(self) -> ComposeResult:
"""Add sub-widgets.""" """Add sub-widgets."""
with TabbedContent(initial="properties"): with TabbedContent(initial="properties"):
@ -495,11 +544,14 @@ class NodeInfo(Container):
with TabPane("Keys", id="key_bindings"): with TabPane("Keys", id="key_bindings"):
yield VerticalScroll(Static(classes="key_bindings")) yield VerticalScroll(Static(classes="key_bindings"))
with TabPane("Events", id="events"): with TabPane("Events", id="events"):
yield VerticalScroll(Static(classes="events")) yield VerticalScroll(self.StaticWithLinkSupport(self, classes="events"))
def watch_dom_node(self, dom_node: DOMNode | None) -> None: def watch_dom_node(self, dom_node: DOMNode | None) -> None:
"""Update the info displayed when the DOM node changes.""" """Update the info displayed when the DOM node changes."""
print("watch_dom_node", dom_node) print("watch_dom_node", dom_node)
self._link_id_to_node.clear()
properties_tree = self.query_one(PropertiesTree) properties_tree = self.query_one(PropertiesTree)
properties_static = self.query_one(".properties_nothing_selected", Static) properties_static = self.query_one(".properties_nothing_selected", Static)
styles_static = self.query_one(".styles", Static) styles_static = self.query_one(".styles", Static)
@ -592,10 +644,13 @@ class NodeInfo(Container):
def_location = Text.from_markup(f"{escape(file)}:{line_number} [link={escape(file_uri)}](open file)[/link]") def_location = Text.from_markup(f"{escape(file)}:{line_number} [link={escape(file_uri)}](open file)[/link]")
except OSError as e: except OSError as e:
def_location = Text.from_markup(f"[#808080](error getting location: [red]{escape(repr(e))}[/red])[/#808080]") def_location = Text.from_markup(f"[#808080](error getting location: [red]{escape(repr(e))}[/red])[/#808080]")
# TODO: link to the DOM node in the tree that has the listener
# Note: css_path_nodes is just like ancestors_with_self, but reversed; it's still DOM nodes # Note: css_path_nodes is just like ancestors_with_self, but reversed; it's still DOM nodes
descendant_arrow = Text.from_markup("[#808080] > [/#808080]") descendant_arrow = Text.from_markup("[#808080] > [/#808080]")
dom_path = descendant_arrow.join([css_path_node.css_identifier_styled for css_path_node in ancestor.css_path_nodes]) dom_path = descendant_arrow.join([css_path_node.css_identifier_styled for css_path_node in ancestor.css_path_nodes])
link_id = self._link_id_counter
self._link_id_counter += 1
self._link_id_to_node[link_id] = ancestor
dom_path.apply_meta({"@click": f"select_node({link_id})"})
handler_qualname = f"{defining_class.__qualname__}.{handler_name}" handler_qualname = f"{defining_class.__qualname__}.{handler_name}"
usages.append(Text.assemble( usages.append(Text.assemble(
"Listener on DOM node: ", "Listener on DOM node: ",
@ -777,6 +832,10 @@ class Inspector(Container):
"""Handle a DOM node being hovered/highlighted.""" """Handle a DOM node being hovered/highlighted."""
self.highlight(event.dom_node) self.highlight(event.dom_node)
async def on_node_info_follow_link_to_node(self, event: NodeInfo.FollowLinkToNode) -> None:
"""Handle a link being clicked in the NodeInfo panel."""
await self.query_one(DOMTree).expand_to_dom_node(event.dom_node)
def reset_highlight(self, except_widgets: Iterable[Widget] = ()) -> None: def reset_highlight(self, except_widgets: Iterable[Widget] = ()) -> None:
"""Reset the highlight.""" """Reset the highlight."""
for widget in self._highlight_boxes: for widget in self._highlight_boxes: