Merge.
authorRadek Brich <radek.brich@devl.cz>
Wed, 03 Sep 2014 21:56:20 +0200
changeset 113 6796adfdc7eb
parent 112 ce2e67e7bbb8 (diff)
parent 108 11dac45bfba4 (current diff)
child 114 26c02bd94bd9
Merge. Due to my schizophrenia, I've accidentally forked my own code. The other set of changes were found in another computer.
tuikit.conf
tuikit/core/container.py
tuikit/core/widget.py
tuikit/core/window.py
tuikit/driver/curses.py
tuikit/driver/cursesw.py
tuikit/widgets/textbox.py
tuikit/widgets/textfield.py
--- a/demos/03_application.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/demos/03_application.py	Wed Sep 03 21:56:20 2014 +0200
@@ -10,15 +10,26 @@
 label = Label('Hello there!')
 label.pos.update(20, 10)
 
-button = Button()
-button.pos.update(20, 20)
+button1 = Button()
+button1.pos.update(20, 20)
+button2 = Button()
+button2.pos.update(30, 20)
 
 field = TextField('text field')
 field.pos.update(20, 30)
 
 app = Application()
 app.root_window.add(label)
-app.root_window.add(button)
+app.root_window.add(button1)
+app.root_window.add(button2)
 app.root_window.add(field)
-app.root_window.focus_child = field
+app.root_window.focus_widget = field
+
+def on_keypress(ev):
+    if ev.keyname == 'escape':
+        app.stop()
+
+app.window_manager.sig_keypress.connect(on_keypress)
+
 app.start()
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/curses_get_wch.py	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,18 @@
+#!/usr/bin/python3
+
+import curses
+import locale
+
+locale.setlocale(locale.LC_ALL, "")
+
+def doStuff(stdscr):
+  stdscr.keypad(1)
+  message = "Press 'q' to quit.\n"
+  stdscr.addstr(0, 0, message, 0)
+  while True:
+    c = stdscr.get_wch() # pauses until a key's hit
+    if c == 'q':
+      break
+    stdscr.addstr('%s %r\n' % (c, c))
+
+curses.wrapper(doStuff)
--- a/tests/curses_getkey.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tests/curses_getkey.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,4 @@
-#!/usr/bin/python
-# coding=UTF-8
+#!/usr/bin/python3
 
 import curses
 import locale
--- a/tests/curses_keycodes.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tests/curses_keycodes.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,4 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
+#!/usr/bin/python3
 
 import curses
 import locale
--- a/tests/curses_mouse.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tests/curses_mouse.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,4 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
+#!/usr/bin/python3
 
 import curses
 import locale
@@ -21,8 +20,8 @@
         screen.addstr('key: %x %s\n' % (c, char))
 
         if c == curses.KEY_MOUSE:
-            m = curses.getmouse()
-            screen.addstr('(%d %d %d %d %x)\n' % m)
+            id_, x, y, z, bstate = curses.getmouse()
+            screen.addstr('(%d %d %d %d %s)\n' % (id_, x, y, z, bin(bstate)))
 
         screen.refresh()
 
--- a/tests/curses_unicode.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tests/curses_unicode.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,4 @@
-#!/usr/bin/python
-# coding=UTF-8
+#!/usr/bin/python3
 
 import curses
 import locale
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_event_class.py	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,13 @@
+#!/usr/bin/env python3
+
+import sys
+sys.path.append('..')
+
+from tuikit.core.events import KeypressEvent
+
+x = KeypressEvent('tab', '\t', set(['shift', 'ctrl']))
+print(repr(x))
+print(x.mod_key())
+
+x = KeypressEvent(None, 'c', set(['shift', 'ctrl']))
+print(x.mod_key())
--- a/tuikit/core/application.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/application.py	Wed Sep 03 21:56:20 2014 +0200
@@ -2,6 +2,7 @@
 from tuikit.core.theme import default_theme
 from tuikit.core.timer import Timer
 from tuikit.core.buffer import ProxyBuffer
+from tuikit.core.events import ResizeEvent
 
 import logging
 
@@ -15,7 +16,7 @@
 
     """
 
-    def __init__(self, driver='curses'):
+    def __init__(self, driver='cursesw'):
         self.log = logging.getLogger(__name__)
         self.driver = None
         self.timer = Timer()
@@ -56,12 +57,12 @@
     def main_loop(self):
         """The main loop."""
         self._started = True
-        self.window_manager.handle_event('resize', *self.driver.size)
+        self.window_manager.handle_event(ResizeEvent(*self.driver.size))
 
         screen = ProxyBuffer(self.driver)
         while not self._quit:
             self.window_manager.draw(screen)
-            self.driver.cursor = self.window_manager.cursor
+            self.driver.cursor = self.window_manager.get_cursor_if_visible()
             self.driver.flush()
 
             timeout = self.timer.nearest_timeout()
@@ -69,7 +70,7 @@
 
             self.timer.process_timeouts()
             for event in events:
-                self.window_manager.handle_event(event[0], *event[1:])
+                self.window_manager.handle_event(event)
 
         self._started = False
         self.log.info('=== End ===')
--- a/tuikit/core/container.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/container.py	Wed Sep 03 21:56:20 2014 +0200
@@ -14,20 +14,24 @@
     def __init__(self, layout_class=FixedLayout):
         Widget.__init__(self)
         #: List of child widgets.
-        self.children = []
-        self.focus_child = None
-        self.mouse_child = None
+        self._widgets = []
+        #: Widget with keyboard focus
+        self.focus_widget = None
+        #: Widget on last mouse position
+        self.mouse_widget = None
+        #: If True, tab cycles inside container
+        self.trap_focus = False
         self.layout = layout_class()
 
     def add(self, widget):
         """Add widget into container."""
-        self.children.append(widget)
+        self._widgets.append(widget)
         widget.parent = self
         widget.window = self.window
         widget.set_theme(self.theme)
         self.layout.add(widget)
-        if self.focus_child is None:
-            self.focus_child = widget
+        if self.focus_widget is None and widget.can_focus():
+            self.focus_widget = widget
 
     def resize(self, w, h):
         Widget.resize(self, w, h)
@@ -36,7 +40,7 @@
     def draw(self, buffer):
         """Draw child widgets."""
         Widget.draw(self, buffer)
-        for child in self.children:
+        for child in self._widgets:
             with buffer.moved_origin(child.x, child.y):
                 with buffer.clip(buffer.origin.x, buffer.origin.y,
                                  child.width, child.height):
@@ -44,7 +48,7 @@
 
     def set_theme(self, theme):
         Widget.set_theme(self, theme)
-        for child in self.children:
+        for child in self._widgets:
             child.set_theme(theme)
 
     @property
@@ -55,21 +59,37 @@
         If this container has child with focus, return its cursor position instead.
 
         """
-        if self.focus_child:
-            cursor = self.focus_child.cursor
+        if self.focus_widget:
+            cursor = self.focus_widget.cursor
             if not cursor:
                 return None
-            cursor = cursor.moved(*self.focus_child.pos)
+            cursor = cursor.moved(*self.focus_widget.pos)
         else:
             cursor = self._cursor.immutable()
         if cursor in Rect._make((0, 0), self._size):
             return cursor
 
+    @property
+    def cursor_visible(self):
+        if self.focus_widget:
+            return self.focus_widget.cursor_visible
+        else:
+            return self._cursor_visible
+
     ## input events ##
 
-    def keypress(self, keyname, char, mod=0):
-        if self.focus_child:
-            self.focus_child.keypress(keyname, char, mod)
+    def keypress_event(self, ev):
+        # First, handle the keypress event to focused child widget
+        if self.focus_widget is not None:
+            if self.focus_widget.keypress_event(ev):
+                return True
+        # Next, handle default key behaviour by Container
+        if ev.keyname == 'tab':
+            return self.focus_next(-1 if 'shift' in ev.mods else 1)
+        # Finally, handle default keys by Widget
+        # and send keypress signal
+        if Widget.keypress_event(self, ev):
+            return True
 
     def mousedown(self, button, pos):
         self.mouse_child = None
@@ -87,3 +107,47 @@
             self.mouse_child.mousemove(button,
                 pos - self.mouse_child.pos, relpos)
 
+    ## focus ##
+
+    def focus_next(self, step=1):
+        """Focus next child.
+
+        Sets focus to next child, if there is one
+        which can be focused. Cycles from last child
+        to first when needed. Return value depends on
+        this cycling:
+
+         * False means there wasn't any child to focus
+           before end of list. Focus was either not changed
+           or first child was focused.
+
+         * True when focus is set to next child in normal
+           way or when self.trap_focus is set.
+
+        Return value is supposed to be returned from keypress
+        event - in that case, True stops event propagation.
+
+        """
+        if self.focus_widget is None:
+            idx_current = 0
+        else:
+            idx_current = self._widgets.index(self.focus_widget)
+        idx_new = idx_current
+        cycled = False
+        while True:
+            idx_new += step
+            if idx_new >= len(self._widgets):
+                idx_new = 0
+                cycled = True
+            if idx_new < 0:  # for focus_previous
+                idx_new = len(self._widgets) - 1
+                cycled = True
+            if idx_current == idx_new:
+                return False
+            if self._widgets[idx_new].can_focus():
+                self.focus_widget = self._widgets[idx_new]
+                return self.trap_focus or not cycled
+
+    def focus_previous(self):
+        """Focus previous child."""
+        self.focus_next(-1)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/core/events.py	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,53 @@
+from collections import OrderedDict
+
+
+class Event:
+
+    """Base class for events."""
+
+    def __init__(self, event_name=None, arg_names=(), arg_values=()):
+        self.name = event_name
+        self.args = OrderedDict(zip(arg_names, arg_values))
+
+    def __getattr__(self, item):
+        if item in self.args:
+            return self.args[item]
+        else:
+            raise AttributeError(item)
+
+    def __getitem__(self, key):
+        return self.args[key]
+
+    def __repr__(self):
+        return '{}({})'.format(self.__class__.__name__,
+                               ', '.join("%s=%r" % (k,v) for k,v
+                                         in self.args.items()))
+
+
+class ResizeEvent(Event):
+
+    def __init__(self, w, h):
+        Event.__init__(self, 'resize', ('w', 'h'), (w, h))
+
+
+class KeypressEvent(Event):
+
+    def __init__(self, keyname, char, mods):
+        Event.__init__(self, 'keypress',
+                       ('keyname', 'char', 'mods'),
+                       (keyname, char, mods))
+
+    def mod_key(self, sep='+'):
+        """Return combined key with modifiers.
+
+        E.g. "shift+tab"
+
+        Order of modifiers is fixed: ctrl, alt, meta, shift
+
+        """
+        res = []
+        for mod in ('ctrl', 'alt', 'meta', 'shift'):
+            if mod in self.mods:
+                res.append(mod)
+        res.append(self.keyname or self.char)
+        return sep.join(res)
--- a/tuikit/core/signal.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/signal.py	Wed Sep 03 21:56:20 2014 +0200
@@ -15,13 +15,19 @@
 
     """
 
-    def __init__(self):
+    def __init__(self, allow_stop=False):
         self._handlers = []
+        #: Allow one of the handlers to stop processing signal
+        #: The handler should return True value,
+        #: then other handlers will not be called
+        self.allow_stop = allow_stop
 
     def __call__(self, *args, **kwargs):
         """Emit the signal to all connected handlers."""
         for handler in self._handlers:
-            handler(*args, **kwargs)
+            res = handler(*args, **kwargs)
+            if self.allow_stop and res:
+                return True
 
     def connect(self, handler):
         if not handler in self._handlers:
--- a/tuikit/core/theme.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/theme.py	Wed Sep 03 21:56:20 2014 +0200
@@ -6,6 +6,7 @@
     """Default color style"""
 
     normal = 'lightgray'
+    active = 'black on cyan'
     button = 'black on lightgray'
     button_active = 'black on cyan'
 
--- a/tuikit/core/widget.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/widget.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,6 @@
 from tuikit.core.coords import Point, Size, Rect
 from tuikit.core.theme import default_theme
+from tuikit.core.signal import Signal
 
 import logging
 
@@ -37,13 +38,17 @@
         self.sizemax = Size(None, None)
 
         #: Cursor is position where text input will occur.
-        #: It is displayed on screen if widget is active.
         #: The cursor coordinates are relative to widget.
-        #: Position outside of widget boundaries means no cursor (hidden).
-        self._cursor = Point(-1, -1)
+        self._cursor = Point()
+        #: Cursor is displayed on screen only when the widget is focused.
+        self._cursor_visible = False
 
-        #: Logger name contains full module name, class name and instance number
-        self._log = logging.getLogger('%s.%s' % (self.__module__, self.name))
+        #: Hidden widget does not affect layout.
+        self.hidden = False
+        #: Allow keyboard focus for this widget.
+        self.allow_focus = False
+
+        self.sig_keypress = Signal(allow_stop=True)
 
     ## position and size ##
 
@@ -78,8 +83,8 @@
 
     def draw(self, buffer):
         """Draw self into buffer."""
-        self._log.debug('draw into %r at %s (exposed %s)',
-                        buffer, buffer.origin, self.exposed(buffer))
+        self.log.debug('Draw into %r at %s (exposed %s)',
+                       buffer, buffer.origin, self.exposed(buffer))
 
     def set_theme(self, theme):
         self.theme = theme
@@ -98,16 +103,37 @@
 
     @property
     def cursor(self):
-        """Return cursor coordinates or None if cursor is not set
-        or is set outside of widget boundaries."""
+        """Return cursor coordinates.
+        
+        Returns None if cursor is set outside of widget boundaries.
+        
+        """
         if self._cursor in Rect._make((0, 0), self._size):
             return self._cursor
 
-    ## input events ##
+    @property
+    def cursor_visible(self):
+        return self._cursor_visible
+
+    ## events ##
+
+    def resize_event(self, ev):
+        self.resize(ev.w, ev.h)
+
+    def keypress_event(self, ev):
+        """Keypress event handler.
 
-    def keypress(self, keyname, char, mod):
-        self._log.debug('keypress(keyname=%r, char=%r, mod=%r)',
-                        keyname, char, mod)
+        Override to accept keyboard input.
+
+        Returns True if event was consumed.
+
+        Call this implementation from inherited classes
+        if it does not consume the event.
+
+        """
+        if self.sig_keypress(ev):
+            return True
+        self.log.debug('Not consumed: %s', ev)
 
     def mousedown(self, button, pos):
         self._log.debug('mousedown(btn=%r, pos=%r)',
@@ -134,3 +160,25 @@
 
         """
         self.parent.remove_timeout(self, callback, *args)
+
+    ## focus ##
+
+    def can_focus(self):
+        return not self.hidden and self.allow_focus
+
+    def has_focus(self):
+        if self.parent is None:
+            return True
+        return (self.parent.has_focus()
+            and self.parent.focus_widget == self)
+
+    ## utilities ##
+
+    @property
+    def log(self):
+        """Logger for widget debugging.
+        
+        Logger name contains full module name, class name and instance number.
+        
+        """
+        return logging.getLogger('%s.%s' % (self.__module__, self.name))
--- a/tuikit/core/window.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/core/window.py	Wed Sep 03 21:56:20 2014 +0200
@@ -17,6 +17,8 @@
         """New buffer for the window will be created unless given existing
         `buffer` as parameter."""
         Container.__init__(self)
+        self.allow_focus = True
+        self.trap_focus = True
         self._buffer = None
         self.buffer = buffer or Buffer()
 
@@ -57,15 +59,35 @@
         Container.__init__(self)
         self.timer = timer
 
+    def draw(self, buffer):
+        Container.draw(self, buffer)
+        self.log.debug('%s has focus.', self.get_focused_widget().name)
+
     def resize(self, w, h):
         Container.resize(self, w, h)
-        self.children[0].resize(w, h)
+        self._widgets[0].resize(w, h)
 
-    def handle_event(self, event_name, *args):
+    def keypress_event(self, ev):
+        self.log.debug('%s', ev)
+        return Container.keypress_event(self, ev)
+
+    def handle_event(self, event):
         """Handle input event to managed windows."""
-        self._log.debug('Handle event: %s %r', event_name, args)
-        handler = getattr(self, event_name, None)
+        self.log.debug('Handle event: %r', event)
+        handler = getattr(self, event.name + '_event', None)
         if handler:
-            handler(*args)
+            handler(event)
         else:
-            raise Exception('Unknown event: %r %r' % (event_name, args))
+            raise Exception('Unknown event: %r' % event)
+
+    def get_focused_widget(self):
+        """Traverse the widget hierarchy to bottom
+        and return actually focused Widget."""
+        node = self
+        while isinstance(node, Container) and node.focus_widget:
+            node = node.focus_widget
+        return node
+
+    def get_cursor_if_visible(self):
+        if self.cursor_visible:
+            return self.cursor
--- a/tuikit/driver/curses.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/driver/curses.py	Wed Sep 03 21:56:20 2014 +0200
@@ -44,6 +44,7 @@
         (0x44,              1,      'left'          ),
         (0x46,              1,      'end'           ),  # xterm
         (0x48,              1,      'home'          ),  # xterm
+        (0x5a,              1,      'shift+tab'     ),  # xterm
         (0x5b, 0x41,        1,      'f1'            ),  # linux
         (0x5b, 0x42,        1,      'f2'            ),  # linux
         (0x5b, 0x43,        1,      'f3'            ),  # linux
@@ -80,7 +81,7 @@
 
     def __init__(self):
         Driver.__init__(self)
-        self._log = logging.getLogger('tuikit')
+        self._log = logging.getLogger(__name__)
         self.stdscr = None
         self.cursor = None
         self.colors = {}     # maps names to curses attributes
@@ -379,7 +380,7 @@
             keyname = match[-1]
 
         if match is None:
-            self.log.debug('Unknown control sequence: %s',
+            self._log.debug('Unknown control sequence: %s',
                            ','.join(['0x%x' % x for x in consumed]))
             return [('keypress', 'Unknown', None, set())]
 
@@ -461,7 +462,7 @@
             if len(codes) == 0:
                 # no match -> unknown code
                 seq = ','.join(['0x%x' % x for x in debug_seq])
-                self.log.debug('Unknown control sequence: %s', seq)
+                self._log.debug('Unknown control sequence: %s', seq)
                 return [('keypress', 'Unknown:' + seq, None, set())]
             elif len(codes) == 1:
                 # one match -> we got the winner
@@ -484,7 +485,7 @@
         if len(matching_codes) == 0:
             # no match -> unknown code
             seq = ','.join(['0x%x' % x for x in debug_seq])
-            self.log.debug('Unknown control sequence: %s', seq)
+            self._log.debug('Unknown control sequence: %s', seq)
             return [('keypress', 'Unknown:' + seq, None, set())]
 
         if len(matching_codes) > 1:
@@ -503,6 +504,14 @@
             if mod_bits & 1<<bit:
                 mod_set.add(name)
 
+        # parse keynames in form "shift+tab"
+        if '+' in keyname:
+            parts = keyname.split('+')
+            for mod in parts[:-1]:
+                assert(mod in ('shift', 'alt', 'ctrl', 'meta'))
+                mod_set.add(mod)
+            keyname = parts[-1]
+
         return [('keypress', keyname, None, mod_set)]
 
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit/driver/cursesw.py	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,258 @@
+import curses.ascii
+import math
+import logging
+
+from tuikit.driver.driver import Driver
+from tuikit.core.events import ResizeEvent, KeypressEvent
+from tuikit.core.coords import Point
+
+
+class CursesWDriver(Driver):
+
+    key_names = {
+        '\t':                   'tab',
+        '\n':                   'enter',
+        '\x1b':                 'escape',
+    }
+
+    key_map = {
+        curses.KEY_UP:          'up',
+        curses.KEY_DOWN:        'down',
+        curses.KEY_LEFT:        'left',
+        curses.KEY_RIGHT:       'right',
+        curses.KEY_IC:          'insert',
+        curses.KEY_DC:          'delete',
+        curses.KEY_HOME:        'home',
+        curses.KEY_END:         'end',
+        curses.KEY_PPAGE:       'pageup',
+        curses.KEY_NPAGE:       'pagedown',
+        curses.KEY_BACKSPACE:   'backspace',
+        curses.KEY_BTAB:        'shift+tab',
+    }
+    for _i in range(1, 13):
+        key_map[curses.KEY_F0 + _i] = 'f' + str(_i)
+
+    color_map = {
+        'default':      (-1,                    0),
+        'black':        (curses.COLOR_BLACK,    0),
+        'blue':         (curses.COLOR_BLUE,     0),
+        'green':        (curses.COLOR_GREEN,    0),
+        'cyan':         (curses.COLOR_CYAN,     0),
+        'red':          (curses.COLOR_RED,      0),
+        'magenta':      (curses.COLOR_MAGENTA,  0),
+        'brown':        (curses.COLOR_YELLOW,   0),
+        'lightgray':    (curses.COLOR_WHITE,    0),
+        'gray':         (curses.COLOR_BLACK,    curses.A_BOLD),
+        'lightblue':    (curses.COLOR_BLUE,     curses.A_BOLD),
+        'lightgreen':   (curses.COLOR_GREEN,    curses.A_BOLD),
+        'lightcyan':    (curses.COLOR_CYAN,     curses.A_BOLD),
+        'lightred':     (curses.COLOR_RED,      curses.A_BOLD),
+        'lightmagenta': (curses.COLOR_MAGENTA,  curses.A_BOLD),
+        'yellow':       (curses.COLOR_YELLOW,   curses.A_BOLD),
+        'white':        (curses.COLOR_WHITE,    curses.A_BOLD),
+    }
+
+    attr_map = {
+        'bold':         curses.A_BOLD,
+        'underline':    curses.A_UNDERLINE,
+        'standout':     curses.A_STANDOUT,  # inverse bg/fg
+        'blink':        curses.A_BLINK,
+    }
+
+    def __init__(self):
+        Driver.__init__(self)
+        self._log = logging.getLogger(__name__)
+        self.stdscr = None
+        self.cursor = None
+        self.colors = {}     # maps names to curses attributes
+        self.colorpairs = {} # maps tuple (fg,bg) to curses color_pair
+        self._mouse_last_pos = None  # Point
+        self._mouse_last_bstate = None
+
+    ## initialization, finalization ##
+
+    def init(self):
+        """Initialize curses"""
+        self.stdscr = curses.initscr()
+        curses.start_color()
+        curses.use_default_colors()
+        curses.noecho()
+        curses.cbreak()
+        self.stdscr.keypad(1)
+        self.stdscr.immedok(0)
+
+        self.size.h, self.size.w = self.stdscr.getmaxyx()
+
+        curses.curs_set(False)  # hide cursor
+        curses.mousemask(curses.ALL_MOUSE_EVENTS | curses.REPORT_MOUSE_POSITION)
+        curses.mouseinterval(0)  # do not wait to detect clicks, we use only press/release
+
+    def close(self):
+        self.stdscr.keypad(0)
+        curses.echo()
+        curses.nocbreak()
+        curses.endwin()
+
+    ## drawing ##
+
+    def clear(self):
+        self.stdscr.erase()
+
+    def putch(self, ch, x, y):
+        try:
+            if isinstance(ch, int):
+                self.stdscr.addch(y, x, ch)
+            elif isinstance(ch, str) and len(ch) == 1:
+                self.stdscr.addstr(y, x, ch)
+            else:
+                raise TypeError('Integer or one-char string is required.')
+        except curses.error as e:
+            self._log.exception('putch(%r, %s, %s) error:' % (ch, x, y))
+
+    def draw(self, buffer, x=0, y=0):
+        for bufy in range(buffer.size.h):
+            for bufx in range(buffer.size.w):
+                char, attr_desc = buffer.get(bufx, bufy)
+                self.setattr(attr_desc)
+                self.putch(char, x + bufx, y + bufy)
+
+    def flush(self):
+        if self.cursor:
+            self.stdscr.move(self.cursor.y, self.cursor.x)
+            curses.curs_set(True)
+        else:
+            curses.curs_set(False)
+        self.stdscr.refresh()
+
+    ## colors, attributes ##
+
+    def setattr(self, attr_desc):
+        """Set attribute to be used for subsequent draw operations."""
+        attr = self.colors.get(attr_desc, None)
+        if attr is None:
+            # first encountered `attr_desc`, initialize
+            fg, bg, attrs = self._parse_attr_desc(attr_desc)
+            fgcol, fgattr = self.color_map[fg]
+            bgcol, _bgattr = self.color_map[bg]
+            colpair = self._getcolorpair(fgcol, bgcol)
+            attr = curses.color_pair(colpair) | self._parseattrs(attrs) | fgattr
+            self.colors[attr_desc] = attr
+        self.stdscr.attrset(attr)
+
+    def _getcolorpair(self, fgcol, bgcol):
+        pair = (fgcol, bgcol)
+        if pair in self.colorpairs:
+            return self.colorpairs[pair]
+        num = len(self.colorpairs) + 1
+        curses.init_pair(num, fgcol, bgcol)
+        self.colorpairs[pair] = num
+        return num
+
+    def _parseattrs(self, attrs):
+        res = 0
+        for a in attrs:
+            res = res | self.attr_map[a]
+        return res
+
+    ## input, events ##
+
+    def getevents(self, timeout=None):
+        """Process input, return list of events.
+
+        timeout -- float, in seconds (None=infinite)
+
+        Returns:
+            [('event', param1, ...), ...]
+
+        """
+        # Set timeout
+        if timeout is None:
+            # wait indefinitely
+            curses.cbreak()
+        elif timeout > 0:
+            # wait
+            timeout_tenths = math.ceil(timeout * 10)
+            curses.halfdelay(timeout_tenths)
+        else:
+            # timeout = 0 -> no wait
+            self.stdscr.nodelay(1)
+
+        # Get key or char
+        c = self.stdscr.get_wch()
+
+        res = []
+
+        if c == -1:
+            # Timeout
+            return res
+        elif c == curses.KEY_MOUSE:
+            res += self._process_mouse()
+        elif c == curses.KEY_RESIZE:
+            self.size.h, self.size.w = self.stdscr.getmaxyx()
+            res.append(ResizeEvent(self.size.w, self.size.h))
+        elif isinstance(c, int):
+            keyname, mods = self._split_keyname_mods(self.key_map[c])
+            res.append(KeypressEvent(keyname, None, mods))
+        else:
+            keyname = self.key_names.get(c)
+            res.append(KeypressEvent(keyname, c, set()))
+
+        return res
+
+    def _process_mouse(self):
+        out = []
+        try:
+            _id, x, y, _z, bstate = curses.getmouse()
+        except curses.error:
+            return out
+
+        pos = Point(x, y)
+        if bstate & curses.REPORT_MOUSE_POSITION:
+            if self._mouse_last_pos != pos:
+                if self._mouse_last_pos:
+                    relpos = pos - self._mouse_last_pos
+                    out += [('mousemove', 0, pos, relpos)]
+                self._mouse_last_pos = pos
+
+        # we are interested only in changes, not buttons already pressed before event
+        if self._mouse_last_bstate is not None:
+            old = self._mouse_last_bstate
+            new = bstate
+            bstate = ~old & new
+            self._mouse_last_bstate = new
+        else:
+            self._mouse_last_bstate = bstate
+
+        if bstate & curses.BUTTON1_PRESSED:
+            out += [('mousedown', 1, pos)]
+        if bstate & curses.BUTTON2_PRESSED:
+            out += [('mousedown', 2, pos)]
+        if bstate & curses.BUTTON3_PRESSED:
+            out += [('mousedown', 3, pos)]
+        if bstate & curses.BUTTON1_RELEASED:
+            out += [('mouseup', 1, pos)]
+        if bstate & curses.BUTTON2_RELEASED:
+            out += [('mouseup', 2, pos)]
+        if bstate & curses.BUTTON3_RELEASED:
+            out += [('mouseup', 3, pos)]
+
+        # reset last pos when pressed/released
+        if len(out) > 0 and out[-1][0] in ('mousedown', 'mouseup'):
+            self._mouse_last_pos = None
+
+        return out
+
+    def _split_keyname_mods(self, keyname):
+        """Parse keynames in form "shift+tab", return (keyname, mod)."""
+        mod_set = set()
+        if '+' in keyname:
+            parts = keyname.split('+')
+            for mod in parts[:-1]:
+                assert(mod in ('shift', 'alt', 'ctrl', 'meta'))
+                mod_set.add(mod)
+            keyname = parts[-1]
+
+        return keyname, mod_set
+
+
+driver_class = CursesWDriver
--- a/tuikit/widgets/button.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/widgets/button.py	Wed Sep 03 21:56:20 2014 +0200
@@ -9,6 +9,7 @@
     def __init__(self, label='btn'):
         """Create button with given label, size according to label."""
         Widget.__init__(self)
+        self.allow_focus = True
 
         #: Button label.
         self._label = ''
@@ -21,10 +22,8 @@
         #: Padding between prefix/suffix and label
         self.padding = 1
 
-        self.allow_focus = True
-
         self.color = 'default'
-        self.color_highlighted = 'default on red'
+        self.color_active = 'default on red'
         self.highlight = False
 
         self.sig_clicked = Signal()
@@ -45,11 +44,11 @@
     def set_theme(self, theme):
         Widget.set_theme(self, theme)
         self.color = theme.button
-        self.color_highlighted = theme.button_active
+        self.color_active = theme.button_active
 
     def _get_color(self):
-        if self.highlight: # or self.has_focus():
-            return self.color_highlighted
+        if self.has_focus():
+            return self.color_active
         return self.color
 
     def draw(self, buffer):
--- a/tuikit/widgets/textbox.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/widgets/textbox.py	Wed Sep 03 21:56:20 2014 +0200
@@ -4,25 +4,20 @@
 
 class TextBox(Widget):
 
-    """Multiline text view/edit widget.
-
-    Cursor is used for text cursor position.
-
-    """
+    """Multiline text view/edit widget."""
 
     def __init__(self, text=''):
         Widget.__init__(self)
+        self.allow_focus = True
 
         # Text content, splitted as lines
         self._lines = []
         self.text = text
 
-        self.allow_focus = True
-
-        # This variable rememberes horizontal position of cursor
+        self._cursor_visible = True
+        # This variable remembers horizontal cursor position
         # for the case when cursor moves to shorter line.
         self.cursor_column = 0
-        self._cursor.update(0, 0)
         # selection - line and column of selection start
         self.sel_line = 0
         self.sel_column = 0
@@ -47,41 +42,47 @@
     def cur_line(self, value):
         self._lines[self._cursor.y] = value
 
-    def set_theme(self, theme):
-        Widget.set_theme(self, theme)
-        self.color = theme.normal
-
     def draw(self, buffer):
         exposed = self.exposed(buffer)
-        with buffer.attr(self.color):
+        with buffer.attr(self.theme.normal):
             buffer.fill()
             end_y = min(len(self._lines), exposed.y + exposed.h)
             for j in range(exposed.y, end_y):
                 line = self._lines[j]
                 buffer.puts(line, 0, j)
 
-    def keypress(self, keyname, char, mod=0):
-        if keyname:
-            if keyname == 'left':        self.move_left()
-            if keyname == 'right':       self.move_right()
-            if keyname == 'home':        self.move_home()
-            if keyname == 'end':         self.move_end()
-            if keyname == 'up':          self.move_up()
-            if keyname == 'down':        self.move_down()
-            if keyname == 'pageup':      self.move_pageup()
-            if keyname == 'pagedown':    self.move_pagedown()
-            if keyname == 'backspace':   self.backspace()
-            if keyname == 'delete':      self.del_char()
-            if keyname == 'enter':       self.add_newline(move=True)
-            if 'ctrl' in mod:
-                if keyname == 'home':    self.move_top()
-                if keyname == 'end':     self.move_bottom()
+    def keypress_event(self, ev):
+        if ev.keyname and not ev.mods:
+            consumed = True
+            if   ev.keyname == 'left':        self.move_left()
+            elif ev.keyname == 'right':       self.move_right()
+            elif ev.keyname == 'home':        self.move_home()
+            elif ev.keyname == 'end':         self.move_end()
+            elif ev.keyname == 'up':          self.move_up()
+            elif ev.keyname == 'down':        self.move_down()
+            elif ev.keyname == 'pageup':      self.move_pageup()
+            elif ev.keyname == 'pagedown':    self.move_pagedown()
+            elif ev.keyname == 'backspace':   self.backspace()
+            elif ev.keyname == 'delete':      self.del_char()
+            elif ev.keyname == 'enter':       self.add_newline(move=True)
+            else:
+                consumed = False
+            if consumed:
+                return True
+        if ev.mods:
+            consumed = True
+            mk = ev.mod_key()
+            if   mk == 'ctrl+home':    self.move_top()
+            elif mk == 'ctrl+end':     self.move_bottom()
+            else:
+                consumed = False
+            if consumed:
+                return True
 
-        if char:
-            self.add_char(char)
+        if ev.char and not ev.keyname:
+            self.add_char(ev.char)
             self.move_right()
-
-        #self.redraw()
+            return True
 
     def on_mousedown(self, ev):
         y = ev.wy
@@ -169,7 +170,7 @@
         sx = self._cursor.x
         self.cur_line = ln[sx:]
         self._lines.insert(self._cursor.y, ln[:sx])
-        self._default_size.update(h=len(self._lines))
+        self.sizereq.update(h=len(self._lines))
         if move:
             self.move_right()
 
@@ -181,8 +182,8 @@
         self.cursor_column = 0
         self._cursor.x = 0
         self._cursor.y += 1
-        w = max(self._default_size.w, len(ln[:sx] + text))
-        self._default_size.update(w=w, h=len(self._lines))
+        w = max(self.sizereq.w, len(ln[:sx] + text))
+        self.sizereq.update(w=w, h=len(self._lines))
 
     def backspace(self):
         if self._cursor.y > 0 or self._cursor.x > 0:
@@ -196,7 +197,7 @@
             if self._cursor.y + 1 < len(self._lines):
                 self.cur_line += self._lines[self._cursor.y+1]
                 del self._lines[self._cursor.y+1]
-                self._default_size.update(h=len(self._lines))
+                self.sizereq.update(h=len(self._lines))
         else:
             self.cur_line = ln[:sx] + ln[sx+1:]
 
--- a/tuikit/widgets/textfield.py	Wed Sep 03 08:57:24 2014 +0200
+++ b/tuikit/widgets/textfield.py	Wed Sep 03 21:56:20 2014 +0200
@@ -7,10 +7,9 @@
 
     def __init__(self, value=''):
         Widget.__init__(self)
+        self.allow_focus = True
         self.sizereq.update(10, 1)
 
-        self.allow_focus = True
-
         self.code = locale.getpreferredencoding()
         if not isinstance(value, str):
             value = str(value, self.code)
@@ -22,6 +21,7 @@
         self.curspos = 0  # position of cursor in value
         self.ofs = 0      # position of value beginning on screen
 
+        self._cursor_visible = True
         self.move_end()
 
     def resize(self, w, h):
@@ -29,11 +29,9 @@
         if self.curspos >= self.tw:
             self.ofs = self.curspos - self.tw
 
-    def set_theme(self, theme):
-        self.color = theme.normal
-
     def draw(self, buffer):
-        with buffer.attr(self.color):
+        color = self.theme.active if self.has_focus() else self.theme.normal
+        with buffer.attr(color):
             # draw value
             val = self.value + ' ' * self.tw         # add spaces to fill rest of field
             val = val[self.ofs : self.ofs + self.tw]  # cut value - begin from ofs, limit to tw chars
@@ -52,8 +50,7 @@
 
             self._cursor.update(1 + self.curspos - self.ofs, 0)
 
-    def keypress(self, keyname, char, mod=0):
-        Widget.keypress(self, keyname, char, mod)
+    def keypress_event(self, ev):
         map_keyname_to_func = {
             'left':       self.move_left,
             'right':      self.move_right,
@@ -62,17 +59,18 @@
             'backspace':  self.move_left_and_del,
             'delete':     self.del_char,
         }
-        accepted = False
-        if keyname in map_keyname_to_func:
-            map_keyname_to_func[keyname]()
-            accepted = True
-        if char:
-            self.add_char(char)
+        consumed = False
+        if ev.keyname in map_keyname_to_func:
+            map_keyname_to_func[ev.keyname]()
+            consumed = True
+        if not ev.keyname and ev.char:
+            self.add_char(ev.char)
             self.move_right()
-            accepted = True
-        #if accepted:
+            consumed = True
+        if consumed:
             #self.redraw()
-        return accepted
+            return True
+        Widget.keypress_event(self, ev)
 
     def move_left(self):
         if self.curspos - self.ofs > 1 or (self.ofs == 0 and self.curspos == 1):