mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
tree control with keyboard
This commit is contained in:
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
### Added
|
||||
|
||||
- Added keyboard control of tree control
|
||||
- Added Widget.gutter to calculate space between renderable and outside edge
|
||||
- Added margin, padding, and border attributes to Widget
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -29,7 +29,7 @@ class UpdateMessage(Message, verbosity=3):
|
||||
@rich.repr.auto
|
||||
class LayoutMessage(Message, verbosity=3):
|
||||
def can_replace(self, message: Message) -> bool:
|
||||
return isinstance(message, LayoutMessage)
|
||||
return isinstance(message, LayoutMessage) or isinstance(message, UpdateMessage)
|
||||
|
||||
|
||||
@rich.repr.auto
|
||||
|
||||
@@ -242,8 +242,6 @@ class View(Widget):
|
||||
else:
|
||||
self.log("view.forwarded", event)
|
||||
await self.post_message(event)
|
||||
# if self.focused is not None:
|
||||
# await self.focused.forward_event(event)
|
||||
|
||||
async def action_toggle(self, name: str) -> None:
|
||||
widget = self.named_widgets[name]
|
||||
|
||||
@@ -24,7 +24,6 @@ class WindowView(View, layout=VerticalLayout):
|
||||
gutter: tuple[int, int] = (0, 1),
|
||||
name: str | None = None
|
||||
) -> None:
|
||||
self.gutter = gutter
|
||||
layout = VerticalLayout(gutter=gutter)
|
||||
self.widget = widget if isinstance(widget, Widget) else Static(widget)
|
||||
layout.add(self.widget)
|
||||
|
||||
@@ -40,6 +40,15 @@ if TYPE_CHECKING:
|
||||
log = getLogger("rich")
|
||||
|
||||
|
||||
class Spacing(NamedTuple):
|
||||
"""The spacing around a renderable."""
|
||||
|
||||
top: int = 0
|
||||
right: int = 0
|
||||
bottom: int = 0
|
||||
left: int = 0
|
||||
|
||||
|
||||
class RenderCache(NamedTuple):
|
||||
size: Size
|
||||
lines: Lines
|
||||
@@ -85,19 +94,19 @@ class Widget(MessagePump):
|
||||
layout_offset_y: Reactive[float] = Reactive(0.0, layout=True)
|
||||
|
||||
style: Reactive[str | None] = Reactive(None)
|
||||
padding: Reactive[PaddingDimensions | None] = Reactive(None, layout=True)
|
||||
margin: Reactive[PaddingDimensions | None] = Reactive(None, layout=True)
|
||||
padding: Reactive[Spacing | None] = Reactive(None, layout=True)
|
||||
margin: Reactive[Spacing | None] = Reactive(None, layout=True)
|
||||
border: Reactive[str] = Reactive("none", layout=True)
|
||||
border_style: Reactive[str] = Reactive("")
|
||||
border_title: Reactive[TextType] = Reactive("")
|
||||
|
||||
BOX_MAP = {"normal": box.SQUARE, "round": box.ROUNDED, "bold": box.HEAVY}
|
||||
|
||||
def validate_padding(self, padding: PaddingDimensions) -> tuple[int, int, int, int]:
|
||||
return Padding.unpack(padding)
|
||||
def validate_padding(self, padding: PaddingDimensions) -> Spacing:
|
||||
return Spacing(*Padding.unpack(padding))
|
||||
|
||||
def validate_margin(self, padding: PaddingDimensions) -> tuple[int, int, int, int]:
|
||||
return Padding.unpack(padding)
|
||||
def validate_margin(self, padding: PaddingDimensions) -> Spacing:
|
||||
return Spacing(*Padding.unpack(padding))
|
||||
|
||||
def validate_layout_offset_x(self, value) -> int:
|
||||
return int(value)
|
||||
@@ -170,6 +179,16 @@ class Widget(MessagePump):
|
||||
"""Get the layout offset as a tuple."""
|
||||
return (round(self.layout_offset_x), round(self.layout_offset_y))
|
||||
|
||||
@property
|
||||
def gutter(self) -> Spacing:
|
||||
mt, mr, mb, bl = self.margin or (0, 0, 0, 0)
|
||||
pt, pr, pb, pl = self.padding or (0, 0, 0, 0)
|
||||
border = 1 if self.border else 0
|
||||
gutter = Spacing(
|
||||
mt + pt + border, mr + pr + border, mb + pb + border, bl + pl + border
|
||||
)
|
||||
return gutter
|
||||
|
||||
def _update_size(self, size: Size) -> None:
|
||||
self._size = size
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ class TreeNode(Generic[NodeDataType]):
|
||||
|
||||
@property
|
||||
def is_cursor(self) -> bool:
|
||||
return self.control.cursor == self.id
|
||||
return self.control.cursor == self.id and self.control.show_cursor
|
||||
|
||||
@property
|
||||
def tree(self) -> Tree:
|
||||
@@ -124,7 +124,8 @@ class TreeNode(Generic[NodeDataType]):
|
||||
if node is self:
|
||||
return next(iter_siblings)
|
||||
except StopIteration:
|
||||
return None
|
||||
pass
|
||||
return None
|
||||
|
||||
@property
|
||||
def previous_sibling(self) -> TreeNode[NodeDataType] | None:
|
||||
@@ -189,13 +190,18 @@ class TreeControl(Generic[NodeDataType], Widget):
|
||||
hover_node: Reactive[NodeID | None] = Reactive(None)
|
||||
cursor: Reactive[NodeID] = Reactive(NodeID(0), layout=True)
|
||||
cursor_line: Reactive[int] = Reactive(0, repaint=False)
|
||||
show_cursor: Reactive[bool] = Reactive(False)
|
||||
|
||||
def watch_cursor(self, value: NodeID | None) -> None:
|
||||
self.cursor_line = self.find_cursor() or 0
|
||||
def watch_show_cursor(self, value: bool) -> None:
|
||||
self.emit_no_wait(CursorMoveMessage(self, self.cursor_line))
|
||||
|
||||
# def watch_cursor(self, value: NodeID | None) -> None:
|
||||
# self.cursor_line = self.find_cursor() or 0
|
||||
|
||||
def watch_cursor_line(self, value: int) -> None:
|
||||
self.log("Cursor line change", value)
|
||||
self.emit_no_wait(CursorMoveMessage(self, value + 1))
|
||||
if self.show_cursor:
|
||||
self.emit_no_wait(CursorMoveMessage(self, value + self.gutter.top))
|
||||
|
||||
async def add(
|
||||
self,
|
||||
@@ -258,18 +264,14 @@ class TreeControl(Generic[NodeDataType], Widget):
|
||||
)
|
||||
if node.id == self.hover_node:
|
||||
label.stylize("underline")
|
||||
label.apply_meta(
|
||||
{
|
||||
"@click": f"click_label({node.id})",
|
||||
"tree_node": node.id,
|
||||
"cursor": node.is_cursor,
|
||||
}
|
||||
)
|
||||
label.apply_meta({"@click": f"click_label({node.id})", "tree_node": node.id})
|
||||
return label
|
||||
|
||||
async def action_click_label(self, node_id: NodeID) -> None:
|
||||
node = self.nodes[node_id]
|
||||
self.cursor = node.id
|
||||
self.cursor_line = self.find_cursor() or 0
|
||||
self.show_cursor = False
|
||||
await self.post_message(TreeClick(self, node))
|
||||
|
||||
async def on_mouse_move(self, event: events.MouseMove) -> None:
|
||||
@@ -292,18 +294,24 @@ class TreeControl(Generic[NodeDataType], Widget):
|
||||
await self.post_message(TreeClick(self, cursor_node))
|
||||
|
||||
async def cursor_down(self) -> None:
|
||||
if not self.show_cursor:
|
||||
self.show_cursor = True
|
||||
return
|
||||
cursor_node = self.nodes[self.cursor]
|
||||
next_node = cursor_node.next_node
|
||||
if next_node is not None:
|
||||
self.hover_node = self.cursor = next_node.id
|
||||
self.log("CURSOR", self.find_cursor())
|
||||
self.cursor_line += 1
|
||||
|
||||
async def cursor_up(self) -> None:
|
||||
if not self.show_cursor:
|
||||
self.show_cursor = True
|
||||
return
|
||||
cursor_node = self.nodes[self.cursor]
|
||||
previous_node = cursor_node.previous_node
|
||||
if previous_node is not None:
|
||||
self.hover_node = self.cursor = previous_node.id
|
||||
self.log("CURSOR", self.find_cursor())
|
||||
self.cursor_line -= 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user