Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
treetools.py512 linesDownload Raw Back to widget
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 
codekingpro/portable-devtools · Team Ai