2019-09-10 00:54:28 +00:00
|
|
|
# Copyright 2015 Hardcoded Software (http://www.hardcoded.net)
|
2020-01-01 02:16:27 +00:00
|
|
|
#
|
|
|
|
# This software is licensed under the "GPLv3" License as described in the "LICENSE" file,
|
|
|
|
# which should be included with this package. The terms are also available at
|
2019-09-10 00:54:28 +00:00
|
|
|
# http://www.gnu.org/licenses/gpl-3.0.html
|
|
|
|
|
2020-06-26 04:26:48 +00:00
|
|
|
from collections.abc import MutableSequence
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
from .base import GUIObject
|
|
|
|
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
class Node(MutableSequence):
|
|
|
|
"""Pretty bland node implementation to be used in a :class:`Tree`.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
It has a :attr:`parent`, behaves like a list, its content being its children. Link integrity
|
|
|
|
is somewhat enforced (adding a child to a node will set the child's :attr:`parent`, but that's
|
|
|
|
pretty much as far as we go, integrity-wise. Nodes don't tend to move around much in a GUI
|
|
|
|
tree). We don't even check for infinite node loops. Don't play around these grounds too much.
|
|
|
|
|
|
|
|
Nodes are designed to be subclassed and given meaningful attributes (those you'll want to
|
|
|
|
display in your tree view), but they all have a :attr:`name`, which is given on initialization.
|
|
|
|
"""
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def __init__(self, name):
|
|
|
|
self._name = name
|
|
|
|
self._parent = None
|
|
|
|
self._path = None
|
|
|
|
self._children = []
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def __repr__(self):
|
2020-01-01 02:16:27 +00:00
|
|
|
return "<Node %r>" % self.name
|
|
|
|
|
|
|
|
# --- MutableSequence overrides
|
2019-09-10 00:54:28 +00:00
|
|
|
def __delitem__(self, key):
|
|
|
|
self._children.__delitem__(key)
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def __getitem__(self, key):
|
|
|
|
return self._children.__getitem__(key)
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def __len__(self):
|
|
|
|
return len(self._children)
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def __setitem__(self, key, value):
|
|
|
|
self._children.__setitem__(key, value)
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def append(self, node):
|
|
|
|
self._children.append(node)
|
|
|
|
node._parent = self
|
|
|
|
node._path = None
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def insert(self, index, node):
|
|
|
|
self._children.insert(index, node)
|
|
|
|
node._parent = self
|
|
|
|
node._path = None
|
2020-01-01 02:16:27 +00:00
|
|
|
|
|
|
|
# --- Public
|
2019-09-10 00:54:28 +00:00
|
|
|
def clear(self):
|
|
|
|
"""Clears the node of all its children.
|
|
|
|
"""
|
|
|
|
del self[:]
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def find(self, predicate, include_self=True):
|
|
|
|
"""Return the first child to match ``predicate``.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
See :meth:`findall`.
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
return next(self.findall(predicate, include_self=include_self))
|
|
|
|
except StopIteration:
|
|
|
|
return None
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def findall(self, predicate, include_self=True):
|
|
|
|
"""Yield all children matching ``predicate``.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
:param predicate: ``f(node) --> bool``
|
|
|
|
:param include_self: Whether we can return ``self`` or we return only children.
|
|
|
|
"""
|
|
|
|
if include_self and predicate(self):
|
|
|
|
yield self
|
|
|
|
for child in self:
|
|
|
|
for found in child.findall(predicate, include_self=True):
|
|
|
|
yield found
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def get_node(self, index_path):
|
|
|
|
"""Returns the node at ``index_path``.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
:param index_path: a list of int indexes leading to our node. See :attr:`path`.
|
|
|
|
"""
|
|
|
|
result = self
|
|
|
|
if index_path:
|
|
|
|
for index in index_path:
|
|
|
|
result = result[index]
|
|
|
|
return result
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def get_path(self, target_node):
|
|
|
|
"""Returns the :attr:`path` of ``target_node``.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
If ``target_node`` is ``None``, returns ``None``.
|
|
|
|
"""
|
|
|
|
if target_node is None:
|
|
|
|
return None
|
|
|
|
return target_node.path
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def children_count(self):
|
|
|
|
"""Same as ``len(self)``.
|
|
|
|
"""
|
|
|
|
return len(self)
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Name for the node, supplied on init.
|
|
|
|
"""
|
|
|
|
return self._name
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def parent(self):
|
|
|
|
"""Parent of the node.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
If ``None``, we have a root node.
|
|
|
|
"""
|
|
|
|
return self._parent
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def path(self):
|
|
|
|
"""A list of node indexes leading from the root node to ``self``.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
The path of a node is always related to its :attr:`root`. It's the sequences of index that
|
|
|
|
we have to take to get to our node, starting from the root. For example, if
|
|
|
|
``node.path == [1, 2, 3, 4]``, it means that ``node.root[1][2][3][4] is node``.
|
|
|
|
"""
|
|
|
|
if self._path is None:
|
|
|
|
if self._parent is None:
|
|
|
|
self._path = []
|
|
|
|
else:
|
|
|
|
self._path = self._parent.path + [self._parent.index(self)]
|
|
|
|
return self._path
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def root(self):
|
|
|
|
"""Root node of current node.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
To get it, we recursively follow our :attr:`parent` chain until we have ``None``.
|
|
|
|
"""
|
|
|
|
if self._parent is None:
|
|
|
|
return self
|
|
|
|
else:
|
|
|
|
return self._parent.root
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
|
|
|
|
class Tree(Node, GUIObject):
|
|
|
|
"""Cross-toolkit GUI-enabled tree view.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
This class is a bit too thin to be used as a tree view controller out of the box and HS apps
|
|
|
|
that subclasses it each add quite a bit of logic to it to make it workable. Making this more
|
|
|
|
usable out of the box is a work in progress.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
This class is here (in addition to being a :class:`Node`) mostly to handle selection.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
Subclasses :class:`Node` (it is the root node of all its children) and :class:`.GUIObject`.
|
|
|
|
"""
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def __init__(self):
|
2020-01-01 02:16:27 +00:00
|
|
|
Node.__init__(self, "")
|
2019-09-10 00:54:28 +00:00
|
|
|
GUIObject.__init__(self)
|
|
|
|
#: Where we store selected nodes (as a list of :class:`Node`)
|
|
|
|
self._selected_nodes = []
|
2020-01-01 02:16:27 +00:00
|
|
|
|
|
|
|
# --- Virtual
|
2019-09-10 00:54:28 +00:00
|
|
|
def _select_nodes(self, nodes):
|
|
|
|
"""(Virtual) Customize node selection behavior.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
By default, simply set :attr:`_selected_nodes`.
|
|
|
|
"""
|
|
|
|
self._selected_nodes = nodes
|
2020-01-01 02:16:27 +00:00
|
|
|
|
|
|
|
# --- Override
|
2019-09-10 00:54:28 +00:00
|
|
|
def _view_updated(self):
|
|
|
|
self.view.refresh()
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
def clear(self):
|
|
|
|
self._selected_nodes = []
|
|
|
|
Node.clear(self)
|
2020-01-01 02:16:27 +00:00
|
|
|
|
|
|
|
# --- Public
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def selected_node(self):
|
|
|
|
"""Currently selected node.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
*:class:`Node`*. *get/set*.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
First of :attr:`selected_nodes`. ``None`` if empty.
|
|
|
|
"""
|
|
|
|
return self._selected_nodes[0] if self._selected_nodes else None
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@selected_node.setter
|
|
|
|
def selected_node(self, node):
|
|
|
|
if node is not None:
|
|
|
|
self._select_nodes([node])
|
|
|
|
else:
|
|
|
|
self._select_nodes([])
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def selected_nodes(self):
|
|
|
|
"""List of selected nodes in the tree.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
*List of :class:`Node`*. *get/set*.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
We use nodes instead of indexes to store selection because it's simpler when it's time to
|
|
|
|
manage selection of multiple node levels.
|
|
|
|
"""
|
|
|
|
return self._selected_nodes
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@selected_nodes.setter
|
|
|
|
def selected_nodes(self, nodes):
|
|
|
|
self._select_nodes(nodes)
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def selected_path(self):
|
|
|
|
"""Currently selected path.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
*:attr:`Node.path`*. *get/set*.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
First of :attr:`selected_paths`. ``None`` if empty.
|
|
|
|
"""
|
|
|
|
return self.get_path(self.selected_node)
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@selected_path.setter
|
|
|
|
def selected_path(self, index_path):
|
|
|
|
if index_path is not None:
|
|
|
|
self.selected_paths = [index_path]
|
|
|
|
else:
|
|
|
|
self._select_nodes([])
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@property
|
|
|
|
def selected_paths(self):
|
|
|
|
"""List of selected paths in the tree.
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
*List of :attr:`Node.path`*. *get/set*
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
Computed from :attr:`selected_nodes`.
|
|
|
|
"""
|
|
|
|
return list(map(self.get_path, self._selected_nodes))
|
2020-01-01 02:16:27 +00:00
|
|
|
|
2019-09-10 00:54:28 +00:00
|
|
|
@selected_paths.setter
|
|
|
|
def selected_paths(self, index_paths):
|
|
|
|
nodes = []
|
|
|
|
for path in index_paths:
|
|
|
|
try:
|
|
|
|
nodes.append(self.get_node(path))
|
|
|
|
except IndexError:
|
|
|
|
pass
|
|
|
|
self._select_nodes(nodes)
|