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