tuikit/treeview.py
author Radek Brich <radek.brich@devl.cz>
Fri, 14 Dec 2012 10:32:43 +0100
changeset 33 45f1b6d590bd
parent 32 088b92ffb119
child 34 e3beacd5e536
permissions -rw-r--r--
Refactoring: rename eventsource module to emitter.

# -*- coding: utf-8 -*-

from tuikit.emitter import Emitter
from tuikit.widget import Widget


class TreeIter:
    def __init__(self, root, collapsed=[]):
        self._node = root
        self._index = 0
        self._stack = []
        self._collapsed = collapsed

    def __next__(self):
        node = None
        while node is None:
            try:
                if self._node in self._collapsed:
                    raise IndexError()
                node = self._node[self._index]
                if node is None:
                    raise Exception('Bad node: None')
            except IndexError:
                if len(self._stack):
                    self._node, self._index = self._stack.pop()
                else:
                    raise StopIteration

        level = len(self._stack) + 1
        index = self._index + 1
        count = len(self._node)

        self._index += 1

        self._stack.append((self._node, self._index))
        self._node = node
        self._index = 0

        return (level, index, count, node)


class TreeNode(list):
    def __init__(self, parent=None, name=''):
        list.__init__(self)
        self.parent = parent
        self.name = name

    def __eq__(self, other):
        # do not compare by list content
        return self is other


class TreeModel(Emitter):
    def __init__(self):
        self.add_events('change')
        self.root = TreeNode()

    def __iter__(self):
        return TreeIter(self.root)

    def find(self, path):
        if isinstance(path, str):
            path = path.split('/')
        # strip empty strings from both ends
        while path and path[0] == '':
            del path[0]
        while path and path[-1] == '':
            del path[-1]

        node = self.root
        for item in path:
            if isinstance(item, int):
                node = node[item]
            else:
                found = False
                for subnode in node:
                    if subnode.name == item:
                        node = subnode
                        found = True
                        break
                if not found:
                    item = int(item)
                    node = node[item]

        return node

    def add(self, path, names):
        node = self.find(path)

        if isinstance(names, str):
            names = (names,)

        for name in names:
            node.append(TreeNode(node, name))

        self.handle('change')


class TreeView(Widget):
    def __init__(self, model=None, width=20, height=20):
        Widget.__init__(self, width, height)

        # cursor
        self.cnode = None

        # model
        self._model = None
        self.setmodel(model)

        self.collapsed = []

        self.connect('draw', self.on_draw)
        self.connect('keypress', self.on_keypress)

    def __iter__(self):
        return TreeIter(self._model.root, self.collapsed)

    def getmodel(self):
        '''TreeModel in use by this TreeView.'''
        return self._model

    def setmodel(self, value):
        if self._model:
            self._model.disconnect('change', self.model_change)
        self._model = value
        if self._model:
            self._model.connect('change', self.model_change)
            try:
                self.cnode = self._model.root[0]
            except IndexError:
                pass

    model = property(getmodel, setmodel)

    def model_change(self):
        if self.cnode is None:
            try:
                self.cnode = self._model.root[0]
            except IndexError:
                pass
        self.redraw()

    def collapse(self, path, collapse=True):
        node = self._model.find(path)
        self.collapse_node(node, collapse)

    def collapse_node(self, node, collapse=True):
        if collapse:
            if not node in self.collapsed and len(node) > 0:
                self.collapsed.append(node)
        else:
            try:
                self.collapsed.remove(node)
            except ValueError:
                pass

    def on_draw(self, screen, x, y):
        screen.pushcolor('normal')

        lines = 0  # bit array, bit 0 - draw vertical line on first column, etc.
        for level, index, count, node in self:
            # prepare string with vertical lines where they should be
            head = []
            for l in range(level-1):
                if lines & (1 << l):
                    head.append(screen.unigraph.VLINE + ' ')
                else:
                    head.append('  ')
            # add vertical line if needed
            if index < count:
                head.append(screen.unigraph.LTEE)
                lines |= 1 << level-1
            else:
                head.append(screen.unigraph.LLCORNER)
                lines &= ~(1 << level-1)
            # draw lines and name
            head = ''.join(head)
            if node in self.collapsed:
                sep = '+'
            else:
                sep = ' '
            screen.puts(x, y, head + sep + node.name)
            if node is self.cnode:
                screen.pushcolor('active')
                screen.puts(x + len(head), y, sep + node.name + ' ')
                screen.popcolor()

            y += 1

        screen.popcolor()

    def on_keypress(self, keyname, char):
        if keyname:
            if keyname == 'up':    self.move_up()
            if keyname == 'down':  self.move_down()
            if keyname == 'left':  self.move_left()
            if keyname == 'right': self.move_right()

        self.redraw()

    def prev_node(self, node):
        # previous sibling
        parent = node.parent
        i = parent.index(node)
        if i > 0:
            node = parent[i-1]
            while node not in self.collapsed and len(node) > 0:
                node = node[-1]
            return node
        else:
            if parent.parent is None:
                return None
            return parent

    def next_node(self, node):
        if node in self.collapsed or len(node) == 0:
            # next sibling
            parent = node.parent
            while parent is not None:
                i = parent.index(node)
                try:
                    return parent[i+1]
                except IndexError:
                    node = parent
                    parent = node.parent
            return None
        else:
            # first child
            return node[0]

    def move_up(self):
        prev = self.prev_node(self.cnode)
        if prev is not None:
            self.cnode = prev
            return True
        return False

    def move_down(self):
        node = self.next_node(self.cnode)
        if node is not None:
            self.cnode = node
            return True
        return False

    def move_left(self):
        self.collapse_node(self.cnode, True)

    def move_right(self):
        self.collapse_node(self.cnode, False)