codekingpro/portable-devtools
114k
1# Generic TreeWidget/TreeWalker class2# Copyright (c) 2010 Rob Lanphier3# Copyright (C) 2004-2010 Ian Ward4#5# This library is free software; you can redistribute it and/or6# modify it under the terms of the GNU Lesser General Public7# License as published by the Free Software Foundation; either8# version 2.1 of the License, or (at your option) any later version.9#10# This library is distributed in the hope that it will be useful,11# but WITHOUT ANY WARRANTY; without even the implied warranty of12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU13# Lesser General Public License for more details.14#15# You should have received a copy of the GNU Lesser General Public16# License along with this library; if not, write to the Free Software17# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA18#19# Urwid web site: https://urwid.org/20 21 22"""23Urwid tree view24 25Features:26- custom selectable widgets for trees27- custom list walker for displaying widgets in a tree fashion28"""29 30from __future__ import annotations31 32import typing33 34from .columns import Columns35from .constants import WHSettings36from .listbox import ListBox, ListWalker37from .padding import Padding38from .text import Text39from .widget import WidgetWrap40from .wimp import SelectableIcon41 42if typing.TYPE_CHECKING:43 from collections.abc import Hashable, Sequence44 45__all__ = ("ParentNode", "TreeListBox", "TreeNode", "TreeWalker", "TreeWidget", "TreeWidgetError")46 47 48class TreeWidgetError(RuntimeError):49 pass50 51 52class TreeWidget(WidgetWrap[Padding[typing.Union[Text, Columns]]]):53 """A widget representing something in a nested tree display."""54 55 indent_cols = 356 unexpanded_icon = SelectableIcon("+", 0)57 expanded_icon = SelectableIcon("-", 0)58 59 def __init__(self, node: TreeNode) -> None:60 self._node = node61 self._innerwidget: Text | None = None62 self.is_leaf = not hasattr(node, "get_first_child")63 self.expanded = True64 widget = self.get_indented_widget()65 super().__init__(widget)66 67 def selectable(self) -> bool:68 """69 Allow selection of non-leaf nodes so children may be (un)expanded70 """71 return not self.is_leaf72 73 def get_indented_widget(self) -> Padding[Text | Columns]:74 widget = self.get_inner_widget()75 if not self.is_leaf:76 widget = Columns(77 [(1, [self.unexpanded_icon, self.expanded_icon][self.expanded]), widget],78 dividechars=1,79 )80 indent_cols = self.get_indent_cols()81 return Padding(widget, width=(WHSettings.RELATIVE, 100), left=indent_cols)82 83 def update_expanded_icon(self) -> None:84 """Update display widget text for parent widgets"""85 # icon is first element in columns indented widget86 icon = [self.unexpanded_icon, self.expanded_icon][self.expanded]87 self._w.base_widget.contents[0] = (icon, (WHSettings.GIVEN, 1, False))88 89 def get_indent_cols(self) -> int:90 return self.indent_cols * self.get_node().get_depth()91 92 def get_inner_widget(self) -> Text:93 if self._innerwidget is None:94 self._innerwidget = self.load_inner_widget()95 return self._innerwidget96 97 def load_inner_widget(self) -> Text:98 return Text(self.get_display_text())99 100 def get_node(self) -> TreeNode:101 return self._node102 103 def get_display_text(self) -> str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]:104 return f"{self.get_node().get_key()}: {self.get_node().get_value()!s}"105 106 def next_inorder(self) -> TreeWidget | None:107 """Return the next TreeWidget depth first from this one."""108 # first check if there's a child widget109 110 if (first_child := self.first_child()) is not None:111 return first_child112 113 # now we need to hunt for the next sibling114 this_node = self.get_node()115 next_node = this_node.next_sibling()116 depth = this_node.get_depth()117 while next_node is None and depth > 0:118 # keep going up the tree until we find an ancestor next sibling119 this_node = this_node.get_parent()120 next_node = this_node.next_sibling()121 depth -= 1122 if depth != this_node.get_depth():123 raise ValueError(depth)124 if next_node is None:125 # we're at the end of the tree126 return None127 128 return next_node.get_widget()129 130 def prev_inorder(self) -> TreeWidget | None:131 """Return the previous TreeWidget depth first from this one."""132 this_node = self._node133 134 if (prev_node := this_node.prev_sibling()) is not None:135 # we need to find the last child of the previous widget if its136 # expanded137 prev_widget = prev_node.get_widget()138 if (last_child := prev_widget.last_child()) is not None:139 return last_child140 141 return prev_widget142 143 # need to hunt for the parent144 if this_node.get_depth() == 0:145 return None146 147 return this_node.get_parent().get_widget()148 149 def keypress(150 self,151 size: tuple[int] | tuple[()],152 key: str,153 ) -> str | None:154 """Handle expand & collapse requests (non-leaf nodes)"""155 if self.is_leaf:156 return key157 158 if key in {"+", "right"}:159 self.expanded = True160 self.update_expanded_icon()161 return None162 if key == "-":163 self.expanded = False164 self.update_expanded_icon()165 return None166 if self._w.selectable():167 return super().keypress(size, key)168 169 return key170 171 def mouse_event(172 self,173 size: tuple[int] | tuple[()],174 event: str,175 button: int,176 col: int,177 row: int,178 focus: bool,179 ) -> bool:180 if self.is_leaf or event != "mouse press" or button != 1:181 return False182 183 if row == 0 and col == self.get_indent_cols():184 self.expanded = not self.expanded185 self.update_expanded_icon()186 return True187 188 return False189 190 def first_child(self) -> TreeWidget | None:191 """Return first child if expanded."""192 if self.is_leaf or not self.expanded:193 return None194 195 if self._node.has_children():196 first_node = self._node.get_first_child()197 return first_node.get_widget()198 199 return None200 201 def last_child(self) -> TreeWidget | None:202 """Return last child if expanded."""203 if self.is_leaf or not self.expanded:204 return None205 206 if self._node.has_children():207 last_child = self._node.get_last_child().get_widget()208 else:209 return None210 # recursively search down for the last descendant211 if (last_descendant := last_child.last_child()) is not None:212 return last_descendant213 214 return last_child215 216 217class TreeNode:218 """219 Store tree contents and cache TreeWidget objects.220 A TreeNode consists of the following elements:221 * key: accessor token for parent nodes222 * value: subclass-specific data223 * parent: a TreeNode which contains a pointer back to this object224 * widget: The widget used to render the object225 """226 227 def __init__(228 self,229 value: typing.Any,230 parent: ParentNode | None = None,231 key: Hashable | None = None,232 depth: int | None = None,233 ) -> None:234 self._key = key235 self._parent = parent236 self._value = value237 self._depth = depth238 self._widget: TreeWidget | None = None239 240 def get_widget(self, reload: bool = False) -> TreeWidget:241 """Return the widget for this node."""242 if self._widget is None or reload:243 self._widget = self.load_widget()244 return self._widget245 246 def load_widget(self) -> TreeWidget:247 return TreeWidget(self)248 249 def get_depth(self) -> int:250 if self._depth is self._parent is None:251 self._depth = 0252 elif self._depth is None:253 self._depth = self._parent.get_depth() + 1254 return self._depth255 256 def get_index(self) -> int | None:257 if self.get_depth() == 0:258 return None259 260 return self.get_parent().get_child_index(self.get_key())261 262 def get_key(self) -> Hashable | None:263 return self._key264 265 def set_key(self, key: Hashable | None) -> None:266 self._key = key267 268 def change_key(self, key: Hashable | None) -> None:269 self.get_parent().change_child_key(self._key, key)270 271 def get_parent(self) -> ParentNode:272 if self._parent is None and self.get_depth() > 0:273 self._parent = self.load_parent()274 return self._parent275 276 def load_parent(self):277 """Provide TreeNode with a parent for the current node. This function278 is only required if the tree was instantiated from a child node279 (virtual function)"""280 raise TreeWidgetError("virtual function. Implement in subclass")281 282 def get_value(self):283 return self._value284 285 def is_root(self) -> bool:286 return self.get_depth() == 0287 288 def next_sibling(self) -> TreeNode | None:289 if self.get_depth() > 0:290 return self.get_parent().next_child(self.get_key())291 292 return None293 294 def prev_sibling(self) -> TreeNode | None:295 if self.get_depth() > 0:296 return self.get_parent().prev_child(self.get_key())297 298 return None299 300 def get_root(self) -> ParentNode:301 root = self302 while root.get_parent() is not None:303 root = root.get_parent()304 return root305 306 307class ParentNode(TreeNode):308 """Maintain sort order for TreeNodes."""309 310 def __init__(311 self,312 value: typing.Any,313 parent: ParentNode | None = None,314 key: Hashable = None,315 depth: int | None = None,316 ) -> None:317 super().__init__(value, parent=parent, key=key, depth=depth)318 319 self._child_keys: Sequence[Hashable] | None = None320 self._children: dict[Hashable, TreeNode] = {}321 322 def get_child_keys(self, reload: bool = False) -> Sequence[Hashable]:323 """Return a possibly ordered list of child keys"""324 if self._child_keys is None or reload:325 self._child_keys = self.load_child_keys()326 return self._child_keys327 328 def load_child_keys(self) -> Sequence[Hashable]:329 """Provide ParentNode with an ordered list of child keys (virtual function)"""330 raise TreeWidgetError("virtual function. Implement in subclass")331 332 def get_child_widget(self, key) -> TreeWidget:333 """Return the widget for a given key. Create if necessary."""334 335 return self.get_child_node(key).get_widget()336 337 def get_child_node(self, key, reload: bool = False) -> TreeNode:338 """Return the child node for a given key. Create if necessary."""339 if key not in self._children or reload:340 self._children[key] = self.load_child_node(key)341 return self._children[key]342 343 def load_child_node(self, key: Hashable) -> TreeNode:344 """Load the child node for a given key (virtual function)"""345 raise TreeWidgetError("virtual function. Implement in subclass")346 347 def set_child_node(self, key: Hashable, node: TreeNode) -> None:348 """Set the child node for a given key.349 350 Useful for bottom-up, lazy population of a tree.351 """352 self._children[key] = node353 354 def change_child_key(self, oldkey: Hashable, newkey: Hashable) -> None:355 if newkey in self._children:356 raise TreeWidgetError(f"{newkey} is already in use")357 self._children[newkey] = self._children.pop(oldkey)358 self._children[newkey].set_key(newkey)359 360 def get_child_index(self, key: Hashable) -> int:361 try:362 return self.get_child_keys().index(key)363 except ValueError as exc:364 raise TreeWidgetError(365 f"Can't find key {key} in ParentNode {self.get_key()}\nParentNode items: {self.get_child_keys()!s}"366 ).with_traceback(exc.__traceback__) from exc367 368 def next_child(self, key: Hashable) -> TreeNode | None:369 """Return the next child node in index order from the given key."""370 if (index := self.get_child_index(key)) is not None and (index + 1) < len(child_keys := self.get_child_keys()):371 # get the next item at same level372 return self.get_child_node(child_keys[index + 1])373 374 # the given node may have just been deleted375 return None376 377 def prev_child(self, key: Hashable) -> TreeNode | None:378 """Return the previous child node in index order from the given key."""379 if (index := self.get_child_index(key)) is not None and index >= 1:380 # get the previous item at the same level381 return self.get_child_node(self.get_child_keys()[index - 1])382 383 return None384 385 def get_first_child(self) -> TreeNode:386 """Return the first TreeNode in the directory."""387 child_keys = self.get_child_keys()388 return self.get_child_node(child_keys[0])389 390 def get_last_child(self) -> TreeNode:391 """Return the last TreeNode in the directory."""392 child_keys = self.get_child_keys()393 return self.get_child_node(child_keys[-1])394 395 def has_children(self) -> bool:396 """Does this node have any children?"""397 return len(self.get_child_keys()) > 0398 399 400class TreeWalker(ListWalker):401 """ListWalker-compatible class for displaying TreeWidgets402 403 positions are TreeNodes."""404 405 def __init__(self, start_from) -> None:406 """start_from: TreeNode with the initial focus."""407 self.focus = start_from408 409 def get_focus(self):410 widget = self.focus.get_widget()411 return widget, self.focus412 413 def set_focus(self, focus) -> None:414 self.focus = focus415 self._modified()416 417 # pylint: disable=arguments-renamed # its bad, but we should not change API418 def get_next(self, start_from) -> tuple[TreeWidget, TreeNode] | tuple[None, None]:419 if (target := start_from.get_widget().next_inorder()) is not None:420 return target, target.get_node()421 422 return None, None423 424 def get_prev(self, start_from) -> tuple[TreeWidget, TreeNode] | tuple[None, None]:425 if (target := start_from.get_widget().prev_inorder()) is not None:426 return target, target.get_node()427 428 return None, None429 430 # pylint: enable=arguments-renamed431 432 433class TreeListBox(ListBox):434 """A ListBox with special handling for navigation and435 collapsing of TreeWidgets"""436 437 def keypress(438 self,439 size: tuple[int, int], # type: ignore[override]440 key: str,441 ) -> str | None:442 key: str | None = super().keypress(size, key)443 return self.unhandled_input(size, key)444 445 def unhandled_input(self, size: tuple[int, int], data: str) -> str | None:446 """Handle macro-navigation keys"""447 if data == "left":448 self.move_focus_to_parent(size)449 return None450 if data == "-":451 self.collapse_focus_parent(size)452 return None453 454 return data455 456 def collapse_focus_parent(self, size: tuple[int, int]) -> None:457 """Collapse parent directory."""458 459 _widget, pos = self.body.get_focus()460 self.move_focus_to_parent(size)461 462 _pwidget, ppos = self.body.get_focus()463 if pos != ppos:464 self.keypress(size, "-")465 466 def move_focus_to_parent(self, size: tuple[int, int]) -> None:467 """Move focus to parent of widget in focus."""468 469 _widget, pos = self.body.get_focus()470 471 parentpos = pos.get_parent()472 473 if parentpos is None:474 return475 476 middle, top, _bottom = self.calculate_visible(size)477 478 row_offset, _focus_widget, _focus_pos, _focus_rows, _cursor = middle # pylint: disable=unpacking-non-sequence479 _trim_top, fill_above = top # pylint: disable=unpacking-non-sequence480 481 for _widget, pos, rows in fill_above:482 row_offset -= rows483 if pos == parentpos:484 self.change_focus(size, pos, row_offset)485 return486 487 self.change_focus(size, pos.get_parent())488 489 def _keypress_max_left(self, size: tuple[int, int]) -> None:490 self.focus_home(size)491 492 def _keypress_max_right(self, size: tuple[int, int]) -> None:493 self.focus_end(size)494 495 def focus_home(self, size: tuple[int, int]) -> None:496 """Move focus to very top."""497 498 _widget, pos = self.body.get_focus()499 rootnode = pos.get_root()500 self.change_focus(size, rootnode)501 502 def focus_end(self, size: tuple[int, int]) -> None:503 """Move focus to far bottom."""504 505 maxrow, _maxcol = size506 _widget, pos = self.body.get_focus()507 508 if lastwidget := pos.get_root().get_widget().last_child():509 lastnode = lastwidget.get_node()510 511 self.change_focus(size, lastnode, maxrow - 1)512 