TreeView: Add cursor, node collapse, reworked draw.
authorRadek Brich <radek.brich@devl.cz>
Wed, 17 Aug 2011 23:43:52 +0200
changeset 11 762513aacc87
parent 10 ec1d47e6fe09
child 12 bb7d41be0c44
TreeView: Add cursor, node collapse, reworked draw.
example_treeview.py
tests/gridlayout_rowspan.py
tests/test_treeview.py
tuikit/application.py
tuikit/treeview.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/example_treeview.py	Wed Aug 17 23:43:52 2011 +0200
@@ -0,0 +1,37 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import locale
+locale.setlocale(locale.LC_ALL, '')
+
+from tuikit import *
+
+
+class MyApplication(Application):
+    def __init__(self):
+        Application.__init__(self)
+        self.top.connect('keypress', self.globalkeypress)
+
+        model = TreeModel()
+        model.add('/',  ['a', 'b'])
+        model.add('/a', ['c', 'd'])
+        model.add((0,1), ['e', 'f'])
+        model.add('/0/1/0', 'g')
+        model.add('/a/d/f', 'h')
+        
+        view = TreeView(model)
+        view.collapse('/a/d')
+        self.top.add(view)
+
+        vert = VerticalLayout()
+        self.top.layout(vert)
+
+    def globalkeypress(self, keyname, char):
+        if keyname == 'escape':
+            self.terminate()
+
+
+if __name__ == '__main__':
+    app = MyApplication()
+    app.start()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/gridlayout_rowspan.py	Wed Aug 17 23:43:52 2011 +0200
@@ -0,0 +1,63 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+from tuikit import *
+
+# +---+---+---+
+# | 0     | 1 |
+# +---+---+   +
+# | 2 | 3 |   |
+# +---+   +   +
+# | 4 |   |   |
+# +---+---+---+
+#
+# 0 - colspan = 2
+# 1 - rowspan = 3
+# 3 - rowspan = 2
+
+if __name__ == '__main__':
+    cont = Container()
+
+    w = [None] * 6
+    for i in range(6):
+        w[i] = Widget()
+        cont.add(w[i])
+
+    w[0].layouthints['colspan'] = 2
+
+    w[1].sizemin = (2,2)
+    w[3].sizemin = (6,1)
+
+    grid = GridLayout(3)
+    grid.container = cont
+
+    print('* _fillgrid')
+    grid._fillgrid()
+
+    print('span:')
+    for row in grid._grid:
+        for col in row:
+            print('[%s,%d,%d]' % (
+                col['widget'].__class__.__name__[0],
+                col['colspan'],
+                col['rowspan']), end=' ')
+        print()
+
+    print('* _computesizes')
+    grid._computesizes()
+
+    print('sizemin:')
+    for row in grid._grid:
+        for col in row:
+            w = col['widget']
+            if w is None:
+                print('[0,0]', end=' ')
+            else:
+                print('[%d,%d]' % w.sizemin, end=' ')
+        print()
+
+    print('colminw:')
+    print(grid._colminw)
+
+    print('rowminh:')
+    print(grid._rowminh)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_treeview.py	Wed Aug 17 23:43:52 2011 +0200
@@ -0,0 +1,34 @@
+#!/usr/bin/env python3
+
+from tuikit.treeview import *
+import unittest
+
+class TestTreeView(unittest.TestCase):
+    def test_treemodel(self):
+        '''Build tree model, iterate through the tree, test result.'''
+        # build tree model
+        # root
+        # ├ a
+        # │ ├ c
+        # │ └ d
+        # │   ├ e
+        # │   │ └ g
+        # │   └ f
+        # │     └ h
+        # └ b
+        model = TreeModel()
+        model.add('/',  ['a', 'b'])
+        model.add('/a', ['c', 'd'])
+        model.add((0,1), ['e', 'f'])
+        model.add('/0/1/0', 'g')
+        model.add('/a/d/f', 'h')
+        
+        res = ''
+        for l, i, c, n in model:
+            res += str(l) + str(i) + str(c) + n.name
+            
+        self.assertEqual(res, '112a212c222d312e411g322f411h122b')
+
+
+if __name__ == '__main__':
+    unittest.main()
--- a/tuikit/application.py	Wed Aug 17 21:11:47 2011 +0200
+++ b/tuikit/application.py	Wed Aug 17 23:43:52 2011 +0200
@@ -121,6 +121,7 @@
     def applytheme(self):
         screen = self.screen
         screen.setcolor('normal',                  'white on black')
+        screen.setcolor('active',                  'black on cyan')
         screen.setcolor('window:normal',           'white on blue')
         screen.setcolor('window:controls',         'white on blue, bold')
         screen.setcolor('window:controls-active',  'cyan on blue, bold')
--- a/tuikit/treeview.py	Wed Aug 17 21:11:47 2011 +0200
+++ b/tuikit/treeview.py	Wed Aug 17 23:43:52 2011 +0200
@@ -4,16 +4,18 @@
 from .widget import Widget
 
 class TreeIter:
-    def __init__(self, model):
-        self.model = model
-        self._node = self.model.root
+    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')
@@ -23,8 +25,8 @@
                 else:
                     raise StopIteration
 
-        name = node.name
         level = len(self._stack) + 1
+        index = self._index + 1
         count = len(self._node)
 
         self._index += 1
@@ -33,13 +35,18 @@
         self._node = node
         self._index = 0
         
-        return (level, count, name)
+        return (level, index, count, node)
 
 
 class TreeNode(list):
-    def __init__(self, name=''):
+    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(EventSource):
@@ -49,9 +56,9 @@
         self.root = TreeNode()
         
     def __iter__(self):
-        return TreeIter(self)
+        return TreeIter(self.root)
 
-    def add(self, path, names):
+    def find(self, path):
         if isinstance(path, str):
             path = path.split('/')
         # strip empty strings from both ends
@@ -75,21 +82,38 @@
                     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(name))
+            node.append(TreeNode(node, name))
             
         self.emit('change')
 
 
 class TreeView(Widget):
-    def __init__(self, width=20, height=20, model=None):
+    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.'''
@@ -101,27 +125,118 @@
         self._model = value
         if self._model:
             self._model.connect('change', self.redraw)
+            try:
+                self.cnode = self._model.root[0]
+            except IndexError:
+                pass
     
     model = property(getmodel, setmodel)
 
+    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')
         
-        self.draw_children(self._model.root, screen, x, y)
-        
-        screen.popcolor()
-        
-        #screen.VLINE
-        #screen.LTEE
-        #screen.LLCORNER
-        
+        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.VLINE + ' ')
+                else:
+                    head.append('  ')
+            # add vertical line if needed
+            if index < count:
+                head.append(screen.LTEE)
+                lines |= 1 << level-1
+            else:
+                head.append(screen.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
         
-    def draw_children(self, parent, screen, x, y):
-        orig_y = y
-        for node in parent:
-            screen.puts(x, y, '- ' + node.name)
-            y += 1
-            if len(node):
-                y += self.draw_children(node, screen, x + 2, y)
-        return y - orig_y
+        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):
+        next = self.next_node(self.cnode)
+        if next is not None:
+            self.cnode = next
+            return True
+        return False
+
+    def move_left(self):
+        self.collapse_node(self.cnode, True)
+    
+    def move_right(self):
+        self.collapse_node(self.cnode, False)
+