Source code for urwidtrees.widgets

# Copyright (C) 2013  Patrick Totzke <patricktotzke@gmail.com>
# This file is released under the GNU GPL, version 3 or a later revision.

from functools import lru_cache

import urwid
from urwid import WidgetWrap, ListBox

from .decoration import DecoratedTree, CollapseMixin
from .nested import NestedTree


# The following functions are used to check dynamically if a tree offers sub-APIs

def implementsDecorateAPI(tree):
    """determines if given tree offers line decoration"""
    return isinstance(tree, (DecoratedTree, NestedTree))

def implementsCollapseAPI(tree):
    """determines if given tree can collapse positions"""
    return isinstance(tree, (CollapseMixin, NestedTree))


[docs] class TreeListWalker(urwid.ListWalker): """ ListWalker to walk through a class:`Tree`. This translates a :class:`Tree` into a :class:`urwid.ListWalker` that is digestible by :class:`urwid.ListBox`. It uses :meth:`Tree.[next|prev]_position <Tree.next_position>` to determine the next/previous position in depth first order. """ def __init__(self, tree, focus=None): """ :param tree: the tree to be displayed :type tree: Tree :param focus: position of node to be focussed initially. This has to be a valid position in the Tree. It defaults to the value of `Tree.root`. """ self._tree = tree self._focus = focus self._lines = lru_cache()(self._line) @property def root(self): return self._tree.root @root.setter def root(self, pos): self._tree.root = pos def __getitem__(self, pos): """gets (possibly decorated) line widget at given position""" return self._lines(pos) def _line(self, pos): if implementsDecorateAPI(self._tree): entry = self._tree.get_decorated(pos) else: entry = self._tree[pos] return entry
[docs] def clear_cache(self): """removes all cached lines""" self._lines.cache_clear() self._modified()
def _get(self, pos): """looks up widget for given position; handling invalid arguments""" res = None, None if pos is not None: try: res = self[pos], pos except (IndexError, KeyError): pass return res # List Walker API. def get_focus(self): # without a focus, follow the root, which may appear later return self._get(self.root if self._focus is None else self._focus) def set_focus(self, pos): self._focus = pos self._modified() def get_next(self, pos): return self._get(self._tree.next_position(pos)) def get_prev(self, pos): return self._get(self._tree.prev_position(pos))
[docs] def positions(self, reverse=False): """returns a generator that walks the tree's positions""" return self._tree.positions(reverse)
# end of List Walker API
[docs] class TreeBox(WidgetWrap): """ A widget that displays a given :class:`Tree`. This is essentially a :class:`ListBox` with the ability to move the focus based on directions in the Tree and to collapse/expand subtrees if possible. Keys are passed to the underlying ListBox first. Of those it leaves, TreeBox interprets `left`/`right` to move the focus to the parent/first child, `[`/`]` to move it to the previous/next sibling, `-`/`+` to collapse/expand the focussed node and `C`/`E` to collapse/expand all. A movement key that does not move the focus, and a collapse key on a tree that cannot collapse, are returned as unhandled, so that enclosing widgets can use them. """ def __init__(self, tree, focus=None): """ :param tree: tree of widgets to be displayed. :type tree: Tree :param focus: initially focussed position """ self._tree = tree self._walker = TreeListWalker(tree) self._outer_list = ListBox(self._walker) if focus is not None: self.set_focus(focus) super().__init__(self._outer_list) # Widget API def get_focus(self): return self._outer_list.body.get_focus() def set_focus(self, pos, valign=None): position = self._outer_list.set_focus(pos) if valign: position = self._outer_list.set_focus_valign(valign) return position def refresh(self): # move the focus out of subtrees collapsed in the meantime focuspos = self._focus_position() if implementsCollapseAPI(self._tree) and focuspos is not None: visible = focuspos pos = self._tree.parent_position(focuspos) while pos is not None: if self._tree.is_collapsed(pos): visible = pos pos = self._tree.parent_position(pos) if visible != focuspos: self.set_focus(visible) self._walker.clear_cache() _tree_keys = { 'left': 'focus_parent', 'right': 'focus_first_child', '[': 'focus_prev_sibling', ']': 'focus_next_sibling', '-': 'collapse_focussed', '+': 'expand_focussed', 'C': 'collapse_all', 'E': 'expand_all', } def keypress(self, size, key): key = self._outer_list.keypress(size, key) if key in self._tree_keys: before = self._focus_position() getattr(self, self._tree_keys[key])() if key in '-+CE': acted = implementsCollapseAPI(self._tree) else: acted = self._focus_position() != before if acted: return None return key # Collapse operations
[docs] def collapse_focussed(self): """ Collapse currently focussed position; works only if the underlying tree allows it. """ focuspos = self._focus_position() if implementsCollapseAPI(self._tree) and focuspos is not None: self._tree.collapse(focuspos) self.refresh()
[docs] def expand_focussed(self): """ Expand currently focussed position; works only if the underlying tree allows it. """ focuspos = self._focus_position() if implementsCollapseAPI(self._tree) and focuspos is not None: self._tree.expand(focuspos) self.refresh()
[docs] def collapse_all(self): """ Collapse all positions; works only if the underlying tree allows it. The focus moves to the top-level node it was under. """ if implementsCollapseAPI(self._tree): self._tree.collapse_all() self.refresh()
[docs] def expand_all(self): """ Expand all positions; works only if the underlying tree allows it. """ if implementsCollapseAPI(self._tree): self._tree.expand_all() self.refresh()
# Tree based focus movement def _focus_position(self): """position of the focussed widget, or None if there is none""" w, focuspos = self.get_focus() return None if w is None else focuspos def _focus_via(self, move): """move focus to `move(focus)`, unless either is None""" focuspos = self._focus_position() if focuspos is not None: target = move(focuspos) if target is not None: self.set_focus(target)
[docs] def focus_parent(self): """move focus to parent node of currently focussed one""" self._focus_via(self._tree.parent_position)
[docs] def focus_first_child(self): """move focus to first child of currently focussed one""" self._focus_via(self._tree.first_child_position)
[docs] def focus_last_child(self): """move focus to last child of currently focussed one""" self._focus_via(self._tree.last_child_position)
[docs] def focus_next_sibling(self): """move focus to next sibling of currently focussed one""" self._focus_via(self._tree.next_sibling_position)
[docs] def focus_prev_sibling(self): """move focus to previous sibling of currently focussed one""" self._focus_via(self._tree.prev_sibling_position)
[docs] def focus_next(self): """move focus to next position (DFO)""" self._focus_via(self._tree.next_position)
[docs] def focus_prev(self): """move focus to previous position (DFO)""" self._focus_via(self._tree.prev_position)