Added ComboBox, HorizontalLayout, TreeNode, TreeModel, TreeView. Widget is now descendant of EventSource. Improved color management (color prefixes).
--- /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()