Added ComboBox, HorizontalLayout, TreeNode, TreeModel, TreeView. Widget is now descendant of EventSource. Improved color management (color prefixes).
authorRadek Brich <radek.brich@devl.cz>
Sun, 31 Jul 2011 13:04:39 +0200
changeset 9 7175ed629a76
parent 8 fcaabd817774
child 10 ec1d47e6fe09
Added ComboBox, HorizontalLayout, TreeNode, TreeModel, TreeView. Widget is now descendant of EventSource. Improved color management (color prefixes).
docs/events.rst
docs/focus.rst
tuikit/__init__.py
tuikit/application.py
tuikit/backend_curses.py
tuikit/button.py
tuikit/combobox.py
tuikit/container.py
tuikit/editbox.py
tuikit/editfield.py
tuikit/eventsource.py
tuikit/layout.py
tuikit/menu.py
tuikit/scrollbar.py
tuikit/treeview.py
tuikit/widget.py
tuikit/window.py
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/events.rst	Sun Jul 31 13:04:39 2011 +0200
@@ -0,0 +1,16 @@
+Event handling
+==============
+
+Keyboard event propagation
+--------------------------
+
+top widget -> focuswidget -> focuswidget's parent -> focuswidget's parent's parent -> ...and so forth
+
+1 top window
+4   window
+3     container
+2       edit box
+
+
+Other event propagation
+-----------------------
--- a/docs/focus.rst	Wed Apr 13 13:07:26 2011 +0200
+++ b/docs/focus.rst	Sun Jul 31 13:04:39 2011 +0200
@@ -14,5 +14,6 @@
 hide() -> unfocus
 
 tab/shift-tab into / out off containers?
+trapfocus # if True, tab cycles inside container
 
 widget.hasfocus()
--- a/tuikit/__init__.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/__init__.py	Sun Jul 31 13:04:39 2011 +0200
@@ -2,15 +2,17 @@
 
 from tuikit.application import Application
 from tuikit.button import Button
+from tuikit.combobox import ComboBox
 from tuikit.common import Rect
 from tuikit.container import Container
 from tuikit.editbox import EditBox
 from tuikit.editfield import EditField
 from tuikit.label import Label
-from tuikit.layout import VerticalLayout, GridLayout
+from tuikit.layout import VerticalLayout, HorizontalLayout, GridLayout
 from tuikit.menu import Menu
 from tuikit.menubar import MenuBar
 from tuikit.scrollbar import VScrollbar
 from tuikit.textedit import TextEdit
+from tuikit.treeview import TreeNode, TreeModel, TreeView
 from tuikit.widget import Widget
 from tuikit.window import Window
--- a/tuikit/application.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/application.py	Sun Jul 31 13:04:39 2011 +0200
@@ -23,10 +23,12 @@
 
         self.connect('draw', self.on_draw)
 
+
     def keypress(self, keyname, char):
+        if self.handle('keypress', keyname, char):
+            return
         if self.focuswidget and self.focuswidget != self:
             self.focuswidget.emit('keypress', keyname, char)
-        self.handle('keypress', keyname, char)
 
 
     def on_draw(self, screen, x, y):
@@ -118,10 +120,10 @@
 
     def applytheme(self):
         screen = self.screen
-        screen.setcolor('normal',                  'white on blue')
-        screen.setcolor('window',                  'white on blue')
-        screen.setcolor('window-controls',         'white on blue, bold')
-        screen.setcolor('window-controls-active',  'cyan on blue, bold')
+        screen.setcolor('normal',                  'white on black')
+        screen.setcolor('window:normal',           'white on blue')
+        screen.setcolor('window:controls',         'white on blue, bold')
+        screen.setcolor('window:controls-active',  'cyan on blue, bold')
         screen.setcolor('button',                  'black on white')
         screen.setcolor('button-active',           'black on cyan')
         screen.setcolor('menu',                    'black on cyan')
--- a/tuikit/backend_curses.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/backend_curses.py	Sun Jul 31 13:04:39 2011 +0200
@@ -90,6 +90,7 @@
         self.colors = {}     # maps names to curses attributes
         self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
         self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this
+        self.colorprefix = [] # stack of color prefixes
 
         self.inputqueue = []
         self.mbtnstack = []
@@ -174,7 +175,7 @@
         return Rect(x, y, w, h)
 
 
-    ## attributes ##
+    ## colors, attributes ##
 
     def _parsecolor(self, name):
         name = name.lower().strip()
@@ -218,6 +219,11 @@
 
 
     def pushcolor(self, name):
+        # add prefix if available
+        if len(self.colorprefix):
+            prefixname = self.colorprefix[-1] + name
+            if prefixname in self.colors:
+                name = prefixname
         attr = self.colors[name]
         self.screen.attrset(attr)
         self.colorstack.append(attr)
@@ -232,6 +238,14 @@
         self.screen.attrset(attr)
 
 
+    def pushcolorprefix(self, name):
+        self.colorprefix.append(name)
+
+
+    def popcolorprefix(self):
+        self.colorprefix.pop()
+
+
     ## drawing ##
 
     def putch(self, x, y, c):
--- a/tuikit/button.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/button.py	Sun Jul 31 13:04:39 2011 +0200
@@ -21,8 +21,9 @@
         self.connect('draw', self.on_draw)
         self.connect('mousedown', self.on_mousedown)
         self.connect('mouseup', self.on_mouseup)
+        self.connect('keypress', self.on_keypress)
 
-        self.newevent('click')
+        self.addevents('click')
 
 
     def on_draw(self, screen, x, y):
@@ -51,8 +52,13 @@
             self.handle('click')
 
 
+    def on_keypress(self, keyname, char):
+        if keyname == 'enter':
+            self.handle('click')
+
+
     def getcolor(self):
-        if self.highlight:
+        if self.highlight or self.hasfocus():
             return self.bghi
         return self.bg
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/combobox.py	Sun Jul 31 13:04:39 2011 +0200
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+from .container import Container
+from .editfield import EditField
+from .button import Button
+from .menu import Menu
+
+
+class ComboBox(Container):
+    def __init__(self, width=15, value='', items=[]):
+        Container.__init__(self, width, 1)
+        
+        self._edit = EditField(width - 3, value)
+        self.add(self._edit)
+        
+        self._btn = Button('v')
+        self._btn.x = width - 3
+        self._btn.width = 3
+        self._btn.connect('click', self._on_btn_click)
+        self.add(self._edit)
+        
+        self._menu = Menu(items)
+        self._menu.hide()
+        self._menu.allowlayout = False
+#        self.top.add(self._menu)
+        
+    
+    def _on_btn_click(self):
+        pass
+    
+    
--- a/tuikit/container.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/container.py	Sun Jul 31 13:04:39 2011 +0200
@@ -1,7 +1,7 @@
 # -*- coding: utf-8 -*-
 
 from .widget import Widget
-
+import logging
 
 class Container(Widget):
     def __init__(self, width = 10, height = 10):
@@ -14,6 +14,10 @@
         self.widthrequest = (None, None)
         self.heightrequest = (None, None)
 
+        self.colorprefix = None
+
+        self.trapfocus = False  # if True, tab cycles inside container
+
 
     def add(self, widget, **kw):
         self.children.append(widget)
@@ -34,6 +38,26 @@
             child.settop(top)
 
 
+    def focusnext(self):
+        i = self.children.index(self.top.focuswidget)
+        while True:
+            i += 1
+            if i >= len(self.children):
+                i = 0
+            if self.children[i].canfocus():
+                self.children[i].setfocus()
+                break
+        log = logging.getLogger('tuikit')
+        log.debug(str(self.top.focuswidget.__class__))
+
+
+    def keypress(self, keyname, char):
+        if keyname == 'tab':
+            self.focusnext()
+            return
+        Widget.keypress(self, keyname, char)
+
+
     def resize(self):
         Widget.resize(self)
         for child in self.children:
@@ -45,6 +69,8 @@
             return
 
         screen.pushclip(x, y, self.width, self.height)
+        if self.colorprefix:
+            screen.pushcolorprefix(self.colorprefix)
 
         Widget.draw(self, screen, x, y)
 
@@ -59,6 +85,8 @@
 
         screen.popclip()
 
+        if self.colorprefix:
+            screen.popcolorprefix()
         screen.popclip()
 
 
--- a/tuikit/editbox.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/editbox.py	Sun Jul 31 13:04:39 2011 +0200
@@ -21,8 +21,7 @@
         self.connect('keypress', self.on_keypress)
         self.connect('mousewheel', self.on_mousewheel)
 
-        self.newevent('scroll')
-        self.newevent('areasize')
+        self.addevents('scroll', 'areasize')
 
         self.set_text(text)
 
--- a/tuikit/editfield.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/editfield.py	Sun Jul 31 13:04:39 2011 +0200
@@ -50,27 +50,35 @@
 
 
     def on_keypress(self, keyname, char):
+        handled = False
         if keyname:
+            handled = True
             if keyname == 'left':
                 self.move_left()
 
-            if keyname == 'right':
+            elif keyname == 'right':
                 self.move_right()
 
-            if keyname == 'backspace':
+            elif keyname == 'backspace':
                 if self.pos > 0:
                     self.move_left()
                     self.del_char()
 
-            if keyname == 'delete':
+            elif keyname == 'delete':
                 self.del_char()
 
+            else:
+                handled = False
+
         if char:
             self.add_char(char)
             self.move_right()
+            handled = True
 
         self.redraw()
 
+        return handled
+
 
     def move_left(self):
         if self.pos - self.ofs > 1 or (self.ofs == 0 and self.pos == 1):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/eventsource.py	Sun Jul 31 13:04:39 2011 +0200
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+
+
+class EventSource:
+
+    def __init__(self):
+        self.event = dict()
+
+    def addevents(self, *events):
+        '''Create new events with empty handler list.'''
+        for event in events:
+            self.event[event] = []
+
+    def connect(self, event, handler):
+        '''Add handler to handler list of the event.'''
+        if event in list(self.event.keys()):
+            self.event[event].append(handler)
+        else:
+            raise KeyError('Event %s not known.', event)
+
+    def disconnect(self, event, handler=None):
+        '''Remove handler from event's handler list.
+
+        If no handler is given, remove all handlers.
+
+        '''
+        if event in list(self.event.keys()):
+            if handler:
+                self.event[event].remove(handler)
+            else:
+                self.event[event] = []
+        else:
+            raise KeyError('Event %s not known.', event)
+
+    def handle(self, event, *args, **kwargs):
+        '''Call all handlers from event's handler list.
+
+        This is used when user defined handlers are to be called.
+
+        '''
+        handled = False
+        for handler in self.event[event]:
+            res = handler(*args, **kwargs)
+            if res:
+                handled = True
+        return handled
+
+    def emit(self, event, *args, **kwargs):
+        '''Emit event.
+
+        This is used by original event source when the event is detected.
+
+        '''
+        try:
+            getattr(self, event)(*args, **kwargs)
+        except AttributeError:
+            self.handle(event, *args, **kwargs)
--- a/tuikit/layout.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/layout.py	Sun Jul 31 13:04:39 2011 +0200
@@ -18,6 +18,8 @@
         v = 0
         c = self.container
         bl, bt, br, bb = c.borders
+
+        last = None
         for child in c.children:
             if not child.allowlayout:
                 continue
@@ -26,6 +28,29 @@
             child.y = bt + v
             v += child.height
             child.handle('resize')
+            last = child
+
+        if last and v < c.height - bt - bb:
+            last.height += c.height - bt - bb - v
+
+        c.redraw()
+
+
+class HorizontalLayout:
+    def resize(self):
+        v = 0
+        c = self.container
+        bl, bt, br, bb = c.borders
+
+        for child in c.children:
+            if not child.allowlayout:
+                continue
+            child.y = bt
+            child.height = c.height - bt - bb
+            child.x = bl + v
+            v += child.width
+            child.handle('resize')
+
         c.redraw()
 
 
--- a/tuikit/menu.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/menu.py	Sun Jul 31 13:04:39 2011 +0200
@@ -20,6 +20,8 @@
         self.connect('mousedown', self.on_mousedown)
         self.connect('mousemove', self.on_mousemove)
         self.connect('mouseup', self.on_mouseup)
+        
+        self.addevents('activate')
 
 
     def on_draw(self, screen, x, y):
@@ -90,12 +92,18 @@
         self.redraw()
 
 
+    def activate(self, name):
+        self.handle('activate', name)
+
+
     def run_selected(self):
         if self.selected and self.selected[1] is not None:
-            if isinstance(self.selected[1], Widget):
+            if type(self.selected[1]) is str:
+                self.emit('activate', self.selected[1])
+            elif isinstance(self.selected[1], Widget):
                 self.selected[1].show()
                 self.selected[1].setfocus()
             else:
-                self.menubar.unfocus()
+                self.menubar.resetfocus()
                 self.selected[1]()
 
--- a/tuikit/scrollbar.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/scrollbar.py	Sun Jul 31 13:04:39 2011 +0200
@@ -21,7 +21,7 @@
         self.connect('mouseup', self.on_mouseup)
         self.connect('mousemove', self.on_mousemove)
 
-        self.newevent('change')
+        self.addevents('change')
 
 
     def setpos(self, pos):
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/treeview.py	Sun Jul 31 13:04:39 2011 +0200
@@ -0,0 +1,127 @@
+# -*- coding: utf-8 -*-
+
+from .eventsource import EventSource
+from .widget import Widget
+
+class TreeIter:
+    def __init__(self, model):
+        self.model = model
+        self._node = self.model.root
+        self._index = 0
+        self._stack = []
+        
+    def __next__(self):
+        node = None
+        while node is None:
+            try:
+                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
+
+        name = node.name
+        level = len(self._stack) + 1
+        count = len(self._node)
+
+        self._index += 1
+        
+        self._stack.append((self._node, self._index))
+        self._node = node
+        self._index = 0
+        
+        return (level, count, name)
+
+
+class TreeNode(list):
+    def __init__(self, name=''):
+        list.__init__(self)
+        self.name = name
+
+
+class TreeModel(EventSource):
+    def __init__(self):
+        EventSource.__init__(self)
+        self.addevents('change')
+        self.root = TreeNode()
+        
+    def __iter__(self):
+        return TreeIter(self)
+
+    def add(self, path, names):
+        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]
+        
+        if isinstance(names, str):
+            names = (names,)
+            
+        for name in names:
+            node.append(TreeNode(name))
+            
+        self.emit('change')
+
+
+class TreeView(Widget):
+    def __init__(self, width=20, height=20, model=None):
+        Widget.__init__(self, width, height)
+        self._model = None
+        self.setmodel(model)
+        self.connect('draw', self.on_draw)
+
+    def getmodel(self):
+        '''TreeModel in use by this TreeView.'''
+        return self._model
+
+    def setmodel(self, value):
+        if self._model:
+            self._model.disconnect('change', self.redraw)
+        self._model = value
+        if self._model:
+            self._model.connect('change', self.redraw)
+    
+    model = property(getmodel, setmodel)
+
+    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
+        
+        
+    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
+
--- a/tuikit/widget.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/widget.py	Sun Jul 31 13:04:39 2011 +0200
@@ -2,9 +2,16 @@
 
 import curses
 
+from .eventsource import EventSource
 
-class Widget:
+
+class Widget(EventSource):
+
+    """Base class for all widgets."""
+
     def __init__(self, width = 10, height = 10):
+        EventSource.__init__(self)
+
         self.parent = None
         self.top = None
         # placing - set by parent widget's layout manager
@@ -24,17 +31,16 @@
         self._redraw = True
         self.hidden = False
         # event handlers
-        self.event = {
-            'resize' : [],
-            'draw' : [],
-            'focus' : [],
-            'unfocus' : [],
-            'keypress' : [],
-            'mousedown' : [],
-            'mouseup' : [],
-            'mousemove' : [],
-            'mousewheel' : [],
-            }
+        self.addevents(
+            'resize',
+            'draw',
+            'focus',
+            'unfocus',
+            'keypress',
+            'mousedown',
+            'mouseup',
+            'mousemove',
+            'mousewheel')
 
 
     @property
@@ -60,37 +66,7 @@
         self.top = top
 
 
-    ### event management
-
-
-    def newevent(self, event):
-        self.event[event] = []
-
-
-    def connect(self, event, handler):
-        if event in list(self.event.keys()):
-            self.event[event] += [handler]
-
-
-    def disconnect(self, event, handler=None):
-        if event in list(self.event.keys()):
-            if handler:
-                i = self.event[event].index(handler)
-                del self.event[event][i]
-            else:
-                self.event[event] = []
-
-
-    def handle(self, event, *args, **kwargs):
-        for handler in self.event[event]:
-            handler(*args, **kwargs)
-
-
-    def emit(self, event, *args, **kwargs):
-        getattr(self, event)(*args, **kwargs)
-
-
-    ###
+    ### events
 
 
     def resize(self):
@@ -124,26 +100,26 @@
         return bool(self.event['keypress'])
 
 
+    def hasfocus(self):
+        return self.top.focuswidget == self
+
+
     def setfocus(self):
-        if self.hasfocus():
+        if self.hasfocus() or not self.canfocus():
             return
         if self.top.focuswidget:
-            self.top.focuswidget.unfocus(self)
+            self.top.focuswidget.resetfocus()
         self.top.focuswidget = self
         self.emit('focus')
 
 
-    def unfocus(self):
+    def resetfocus(self):
         if self.top.focuswidget != self:
             return
         self.top.focuswidget = None
         self.emit('unfocus')
 
 
-    def hasfocus(self):
-        return self.top.focuswidget == self
-
-
     def focus(self):
         '''handle focus event'''
         self.handle('focus')
@@ -154,11 +130,13 @@
         self.handle('unfocus', newfocus)
 
 
-    ###
+    ### events
 
 
     def keypress(self, keyname, char):
-        self.handle('keypress', keyname, char)
+        handled = self.handle('keypress', keyname, char)
+        if not handled and self.parent and self.parent != self.top:
+            self.parent.emit('keypress', keyname, char)
 
 
     def mousedown(self, ev):
@@ -178,6 +156,9 @@
         self.handle('mousewheel', ev)
 
 
+    ###
+
+
     def enclose(self, x, y):
         if self.hidden:
             return False
--- a/tuikit/window.py	Wed Apr 13 13:07:26 2011 +0200
+++ b/tuikit/window.py	Sun Jul 31 13:04:39 2011 +0200
@@ -27,11 +27,12 @@
         self.closebtn.x = self.width - 5
         self.closebtn.width = 3
         self.closebtn.connect('click', self.on_closebtn)
-        self.closebtn.bg = 'window-controls'
-        self.closebtn.bghi = 'window-controls-active'
+        self.closebtn.bg = 'controls'
+        self.closebtn.bghi = 'controls-active'
         self.add(self.closebtn)
 
         self.borders = (1, 1, 1, 1)
+        self.colorprefix = 'window:'
 
 
     @property
@@ -45,14 +46,14 @@
 
 
     def on_draw(self, screen, x, y):
-        screen.pushcolor('window')
+        screen.pushcolor('normal')
         screen.frame(x, y, self.width, self.height)
 
         if self.resizable:
             if self.resizing:
-                screen.pushcolor('window-controls-active')
+                screen.pushcolor('controls-active')
             else:
-                screen.pushcolor('window-controls')
+                screen.pushcolor('controls')
             screen.puts(x + self.width - 2, y + self.height - 1, '─┘') # '━┛'
             screen.popcolor()