tree control with keyboard

This commit is contained in:
Will McGugan
2021-08-22 16:14:54 +01:00
parent 1e2941339b
commit d240f4ef8c
6 changed files with 50 additions and 24 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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]

View File

@@ -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)

View File

@@ -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

View File

@@ -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__":