Merge.
Due to my schizophrenia, I've accidentally forked my own code. The other set of changes were found in another computer.
--- a/.hgignore	Wed Sep 03 19:17:04 2014 +0200
+++ b/.hgignore	Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,6 @@
 .*~$
 ^tuikit/.*\.pyc$
+^tuikit\.conf$
 ^docs/_build
 tuikit\.log
 ^build
@@ -11,5 +12,6 @@
 ^(.*/)?\.pydevproject$
 ^\.settings
 ^\.idea/
+^\.ackrc$
 .*\.appstats
 __pycache__
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/DOC	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,25 @@
+LayoutManager
+  VerticalLayout
+  HorizontalLayout
+  TileLayout
+
+
+events:
+
+  draw()
+    - caller
+  on_draw()
+    - core handler, for overloading
+  connect('draw', my_draw)
+    - additional handler
+
+
+Focus
+-----
+
+ * only one node in hierarchy can have focus
+ * all parent containers have focus, so they can relay events to child
+ * top container has always focus
+
+ * grab_focus() on any node will clean old focus and set focus to this child
+ * global shortcuts can be handled in keypress handler of top widget
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/INSPIRATION	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,35 @@
+Wikipedia:
+  http://en.wikipedia.org/wiki/Event_loop
+
+Python/Gnome:
+  http://developer.gnome.org/gnome-devel-demos/stable/tutorial.py.html.en
+
+PyQt4 class reference:
+  http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html/classes.html
+  http://www.riverbankcomputing.co.uk/static/Docs/PyQt4/html/qevent.html
+
+PyQt4 tutorial:
+  http://zetcode.com/tutorials/pyqt4/
+  http://zetcode.com/tutorials/pyqt4/layoutmanagement/
+
+Pyglet:
+  http://www.pyglet.org/doc/api/pyglet.event.EventDispatcher-class.html
+
+GTK:
+  http://developer.gnome.org/gtk3/stable/
+  http://python-gtk-3-tutorial.readthedocs.org/en/latest/layout.html
+
+Anchor Layout in QML:
+  http://harmattan-dev.nokia.com/docs/library/html/qt4/qml-anchor-layout.html
+
+PDCurses:
+  http://pdcurses.sourceforge.net/doc/PDCurses.txt
+
+XTerm Control Sequences
+  http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
+
+Kivy
+  http://kivy.org/
+
+Urwid
+  http://urwid.org/
--- a/demo_tableview.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/demo_tableview.py	Wed Sep 03 21:56:20 2014 +0200
@@ -6,6 +6,7 @@
 
 from tuikit import Application
 from tuikit.tableview import TableView, TableModel
+from tuikit.scrollview import ScrollView
 
 
 class MyApplication(Application):
@@ -13,20 +14,23 @@
         Application.__init__(self)
         self.top.add_handler('keypress', self.on_top_keypress)
 
-        data = []
-        for y in range(100):
-            row = [str(y+1)]
-            for x in range(10):
-                row.append('r{}:c{}'.format(y, x))
-            data.append(row)
-        model = TableModel(data)
+        model = TableModel()
+        model.set_num_headers(1, 1)
+        for col in range(10):
+            model.insert_column(col)
+            model.set_column_header(col, 0, 'col'+str(col+1))
+        for row in range(100):
+            model.insert_row(row)
+            model.set_row_header(row, 0, 'row'+str(row+1))
+            for col in range(10):
+                model.set_item(row, col, 'r{}:c{}'.format(row+1, col+1))
 
         view = TableView(model)
-        view.addcolumn(header=True, expand=False, sizereq=5)
-        for x in range(10):
-            view.addcolumn(title='head'+str(x))
 
-        self.top.add(view, halign='fill', valign='fill')
+        scroll = ScrollView()
+        scroll.add(view)
+
+        self.top.add(scroll, halign='fill', valign='fill')
 
     def on_top_keypress(self, ev):
         if ev.keyname == 'escape':
--- a/demos/04_texteditor.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/demos/04_texteditor.py	Wed Sep 03 21:56:20 2014 +0200
@@ -8,6 +8,7 @@
 
 
 class MyApplication(Application):
+
     def __init__(self):
         Application.__init__(self)
         #self.top.add_handler('keypress', self.on_top_keypress)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/docs/terminology.rst	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,18 @@
+Terminology
+===========
+
+
+Buffer
+------
+
+Buffer is rectangular offscreen area of known size.
+
+It can be drawn on the screen or into another buffer.
+
+
+Widget
+------
+
+Basic element of the application.
+
+* Can draw itself in given buffer on given position.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_tablemodel.py	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+
+import sys
+sys.path.insert(0, '..')
+
+from tuikit.tableview import *
+import unittest
+
+
+class TestTableView(unittest.TestCase):
+    def setUp(self):
+        self.abs_model = AbstractTableModel()
+        self.model = TableModel()
+
+    def test_initial(self):
+        self._test_empty_model(self.abs_model)
+        self._test_empty_model(self.model)
+
+    def test_one(self):
+        self._insert_data()
+        self._fill_data()
+        self._insert_headers()
+        self._test_empty_headers()
+        self._fill_headers()
+        self._remove_data()
+        self._clear_data()
+
+    def test_two(self):
+        self._insert_headers()
+        self._insert_data()
+        self._test_empty_headers()
+        self._fill_headers()
+        self._fill_data()
+
+    def _test_empty_model(self, model):
+        self.assertEqual(model.row_count(), 0)
+        self.assertEqual(model.column_count(), 0)
+        self.assertRaises(IndexError, model.get_row, 0)
+        self.assertRaises(IndexError, model.get_column, 0)
+        self.assertRaises(IndexError, model.get_item, 0, 0)
+
+    def _insert_data(self):
+        for _i in range(3):
+            self.model.insert_row(0)
+            self.model.insert_column(0)
+        self.assertEqual(self.model.row_count(), 3)
+        self.assertEqual(self.model.column_count(), 3)
+
+    def _fill_data(self):
+        for row in range(3):
+            for col in range(3):
+                self.model.set_item(row, col, row*3 + col+1)
+        self.assertEqual(self.model.get_row(0), [1,2,3])
+        self.assertEqual(self.model.get_row(1), [4,5,6])
+        self.assertEqual(self.model.get_row(2), [7,8,9])
+        self.assertEqual(self.model.get_column(0), [1,4,7])
+
+    def _insert_headers(self):
+        self.model.set_num_headers(2, 2)
+        self.assertEqual(self.model.row_header_count(), 2)
+        self.assertEqual(self.model.column_header_count(), 2)
+
+    def _test_empty_headers(self):
+        self.assertEqual(self.model.get_row_header(0, 0), None)
+        self.assertEqual(self.model.get_row_header(2, 1), None)
+        self.assertEqual(self.model.get_column_header(0, 0), None)
+        self.assertEqual(self.model.get_column_header(2, 1), None)
+
+    def _fill_headers(self):
+        for i in range(3):
+            for header in range(2):
+                self.model.set_column_header(i, header, header*3 + i + 1)
+                self.model.set_row_header(i, header, header*3 + i + 1)
+        self.assertEqual(self.model.get_column_header(1, 0), 2)
+        self.assertEqual(self.model.get_column_header(1, 1), 5)
+        self.assertEqual(self.model.get_row_header(1, 0), 2)
+        self.assertEqual(self.model.get_row_header(1, 1), 5)
+
+    def _remove_data(self):
+        self.model.remove_row(1)
+        self.assertEqual(self.model.get_row(0), [1,2,3])
+        self.assertEqual(self.model.get_row(1), [7,8,9])
+        self.assertEqual(self.model.get_column(0), [1,7])
+        self.model.remove_column(1)
+        self.assertEqual(self.model.get_row(0), [1,3])
+        self.assertEqual(self.model.get_row(1), [7,9])
+        self.assertEqual(self.model.get_column(1), [3,9])
+        self.assertRaises(IndexError, self.model.get_row, 2)
+        self.assertRaises(IndexError, self.model.get_column, 2)
+
+    def _clear_data(self):
+        self.model.clear()
+
+
+if __name__ == '__main__':
+    unittest.main()
+
--- a/tuikit.conf	Wed Sep 03 19:17:04 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,5 +0,0 @@
-# rendering driver: curses | sdl (default: curses)
-#driver='sdl'
-
-# log level: info | debug (default: info)
-#log_level='debug'
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tuikit.conf.example	Wed Sep 03 21:56:20 2014 +0200
@@ -0,0 +1,5 @@
+# rendering driver: curses | sdl (default: curses)
+#driver='sdl'
+
+# log level: info | debug (default: info)
+#log_level='debug'
--- a/tuikit/core/buffer.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/core/buffer.py	Wed Sep 03 21:56:20 2014 +0200
@@ -184,7 +184,7 @@
     @property
     def size(self):
         """Width and height of buffer, in characters."""
-        return self._size.readonly()
+        return self._size.immutable()
 
     def resize(self, w, h):
         """Resize buffer."""
--- a/tuikit/core/container.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/core/container.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,5 +1,5 @@
 from tuikit.core.widget import Widget
-from tuikit.core.coords import Point
+from tuikit.core.coords import Point, Rect
 from tuikit.layouts.fixed import FixedLayout
 
 
@@ -17,6 +17,8 @@
         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()
@@ -51,11 +53,21 @@
 
     @property
     def cursor(self):
+        """Return cursor coordinates or None if cursor is not set
+        or is set outside of widget boundaries.
+
+        If this container has child with focus, return its cursor position instead.
+
+        """
         if self.focus_widget:
             cursor = self.focus_widget.cursor
-            return cursor.moved(*self.focus_widget.pos)
+            if not cursor:
+                return None
+            cursor = cursor.moved(*self.focus_widget.pos)
         else:
-            return self._cursor
+            cursor = self._cursor.immutable()
+        if cursor in Rect._make((0, 0), self._size):
+            return cursor
 
     @property
     def cursor_visible(self):
@@ -79,6 +91,22 @@
         if Widget.keypress_event(self, ev):
             return True
 
+    def mousedown(self, button, pos):
+        self.mouse_child = None
+        for child in reversed(self.children):
+            if pos in child.boundaries:
+                child.mousedown(button, pos - child.pos)
+                self.mouse_child = child
+
+    def mouseup(self, button, pos):
+        if self.mouse_child:
+            self.mouse_child.mouseup(button, pos - self.mouse_child.pos)
+
+    def mousemove(self, button, pos, relpos):
+        if self.mouse_child:
+            self.mouse_child.mousemove(button,
+                pos - self.mouse_child.pos, relpos)
+
     ## focus ##
 
     def focus_next(self, step=1):
--- a/tuikit/core/coords.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/core/coords.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,9 +1,12 @@
+
 class Point:
 
     """Point in cartesian space.
 
     Implements attribute access (.x, .y) and list-like access([0],[1]).
 
+    This is heavy-weight mutable object. See also ImmutablePoint.
+
     """
 
     def __init__(self, *args, **kwargs):
@@ -11,12 +14,6 @@
         self.y = 0
         self.update(*args, **kwargs)
 
-    def __getitem__(self, key):
-        return (self.x, self.y)[key]
-
-    def __repr__(self):
-        return 'Point(x={0.x},y={0.y})'.format(self)
-
     def move(self, relx, rely):
         self.x += relx
         self.y += rely
@@ -46,6 +43,79 @@
             else:
                 raise ValueError('Bad keyword arg: %r' % key)
 
+    # sequence interface
+
+    def __len__(self):
+        return 2
+
+    def __getitem__(self, key):
+        return (self.x, self.y)[key]
+
+    # point arithmetics
+
+    def __add__(self, other):
+        return Point(self.x + other[0], self.y + other[1])
+
+    def __sub__(self, other):
+        return Point(self.x - other[0], self.y - other[1])
+
+    def __eq__(self, other):
+        """Comparison operator.
+
+        Point can be compared to any sequence of at least two elements:
+
+        >>> p = Point(1, 2)
+        >>> p == Point(1, 2)
+        True
+        >>> p == (1, 2)
+        True
+        >>> p == (0, 0)
+        False
+        >>> p == None
+        False
+
+        """
+        try:
+            return self.x == other[0] and self.y == other[1]
+        except (TypeError, IndexError):
+            return False
+
+    # string representation
+
+    def __repr__(self):
+        return '{0.__class__.__name__}(x={0.x},y={0.y})'.format(self)
+
+    def immutable(self):
+        return ImmutablePoint(*self)
+
+
+class ImmutablePoint:
+
+    """Point class without write access."""
+
+    __slots__ = ('_x', '_y')
+
+    def __init__(self, x, y):
+        self._x = x
+        self._y = y
+
+    @property
+    def x(self):
+        return self._x
+
+    @property
+    def y(self):
+        return self._y
+
+    def moved(self, relx, rely):
+        return ImmutablePoint(self.x + relx, self.y + rely)
+
+    def __getitem__(self, key):
+        return (self.x, self.y)[key]
+
+    def __repr__(self):
+        return '{0.__class__.__name__}(x={0.x}, y={0.y})'.format(self)
+
 
 class Size:
 
@@ -53,6 +123,8 @@
 
     Implements attribute access (.w, .h) and list-like access([0],[1]).
 
+    This is heavy-weight mutable object. See also ImmutableSize.
+
     """
 
     def __init__(self, *args, **kwargs):
@@ -64,7 +136,7 @@
         return (self.w, self.h)[key]
 
     def __repr__(self):
-        return 'Size(w={0.w},h={0.h})'.format(self)
+        return '{0.__class__.__name__}(w={0.w},h={0.h})'.format(self)
 
     def update(self, *args, **kwargs):
         """Update size.
@@ -88,30 +160,40 @@
             else:
                 raise ValueError('Bad keyword arg: %r' % key)
 
-    def readonly(self):
-        return ReadonlySize(self)
+    def immutable(self):
+        return ImmutableSize(*self)
 
 
-class ReadonlySize:
+class ImmutableSize:
+
+    """Size class without write access.
 
-    """Wrapper for Size which makes it read-only."""
+    When using reference to (mutable) Size class, properties are not fixed.
+    They can still be changed by original owner and these changes will become
+    visible to other references of the object. ImmutableSize should be used
+    when this is not intended.
 
-    def __init__(self, size):
-        self._size = size
+    """
+
+    __slots__ = ('_w', '_h')
+
+    def __init__(self, w, h):
+        self._w = w
+        self._h = h
 
     @property
     def w(self):
-        return self._size.w
+        return self._w
 
     @property
     def h(self):
-        return self._size.h
+        return self._h
 
     def __getitem__(self, key):
-        return self._size[key]
+        return (self.w, self.h)[key]
 
     def __repr__(self):
-        return 'ReadonlySize(w={0.w},h={0.h})'.format(self._size)
+        return '{0.__class__.__name__}(w={0.w}, h={0.h})'.format(self)
 
 
 class Rect:
@@ -124,8 +206,20 @@
         self.w = w
         self.h = h
 
+    @classmethod
+    def _make(cls, origin, size):
+        """Make new Rect instance with origin and size as specified.
+
+        `origin` should be Point or pair of coordinates,
+        `size` should be Size or pair of integers
+
+        """
+        x, y = origin
+        w, h = size
+        return Rect(x, y, w, h)
+
     def __repr__(self):
-        return 'Rect(x={0.x},y={0.y},w={0.w},h={0.h})'.format(self)
+        return '{0.__class__.__name__}(x={0.x},y={0.y},w={0.w},h={0.h})'.format(self)
 
     def __contains__(self, point):
         """Test if point is positioned inside rectangle.
--- a/tuikit/core/widget.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/core/widget.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,4 +1,4 @@
-from tuikit.core.coords import Point, Size
+from tuikit.core.coords import Point, Size, Rect
 from tuikit.core.theme import default_theme
 from tuikit.core.signal import Signal
 
@@ -15,9 +15,7 @@
         self._num_instances += 1
 
         #: Widget name is used for logging etc. Not visible anywhere.
-        self.name = '{}{}'.format(
-            self.__class__.__name__,
-            self._num_instances)
+        self.name = '%s#%s' % (self.__class__.__name__, self._num_instances)
 
         #: Parent Widget
         self.parent = None
@@ -40,8 +38,9 @@
         self.sizemax = Size(None, None)
 
         #: Cursor is position where text input will occur.
+        #: The cursor coordinates are relative to widget.
         self._cursor = Point()
-        #: Cursor is displayed on screen only if the widget is focused.
+        #: Cursor is displayed on screen only when the widget is focused.
         self._cursor_visible = False
 
         #: Hidden widget does not affect layout.
@@ -71,11 +70,15 @@
 
     @property
     def size(self):
-        return self._size.readonly()
+        return self._size.immutable()
 
     def resize(self, w, h):
         self._size.update(w, h)
 
+    @property
+    def boundaries(self):
+        return Rect._make(self.pos, self._size)
+
     ## drawing, looks ##
 
     def draw(self, buffer):
@@ -100,7 +103,13 @@
 
     @property
     def cursor(self):
-        return self._cursor
+        """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
 
     @property
     def cursor_visible(self):
@@ -126,6 +135,18 @@
             return True
         self.log.debug('Not consumed: %s', ev)
 
+    def mousedown(self, button, pos):
+        self._log.debug('mousedown(btn=%r, pos=%r)',
+                        button, pos)
+
+    def mouseup(self, button, pos):
+        self._log.debug('mouseup(btn=%r, pos=%r)',
+                        button, pos)
+
+    def mousemove(self, button, pos, relpos):
+        self._log.debug('mousemove(btn=%r, pos=%r, relpos=%r)',
+                        button, pos, relpos)
+
     ## timeouts ##
 
     def add_timeout(self, delay, callback, *args):
@@ -155,4 +176,9 @@
 
     @property
     def log(self):
-        return logging.getLogger('tuikit.' + self.name)
+        """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 19:17:04 2014 +0200
+++ b/tuikit/core/window.py	Wed Sep 03 21:56:20 2014 +0200
@@ -64,6 +64,7 @@
         self.log.debug('%s has focus.', self.get_focused_widget().name)
 
     def resize(self, w, h):
+        Container.resize(self, w, h)
         self._widgets[0].resize(w, h)
 
     def keypress_event(self, ev):
@@ -72,6 +73,7 @@
 
     def handle_event(self, event):
         """Handle input event to managed windows."""
+        self.log.debug('Handle event: %r', event)
         handler = getattr(self, event.name + '_event', None)
         if handler:
             handler(event)
--- a/tuikit/driver/curses.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/driver/curses.py	Wed Sep 03 21:56:20 2014 +0200
@@ -3,6 +3,7 @@
 import logging
 
 from tuikit.driver.driver import Driver
+from tuikit.core.coords import Point
 
 
 class CursesDriver(Driver):
@@ -88,7 +89,7 @@
         self.colorstack = [] # pushcolor/popcolor puts or gets attributes from this
         self.inputqueue = []
         self.mbtnstack = []
-        self._mouse_last_pos = (None, None)
+        self._mouse_last_pos = None  # Point
         self._mouse_last_bstate = None
 
     ## initialization, finalization ##
@@ -280,15 +281,15 @@
         except curses.error:
             return []
 
+        pos = Point(x, y)
         out = []
 
         if bstate & curses.REPORT_MOUSE_POSITION:
-            if self._mouse_last_pos != (x, y):
-                if self._mouse_last_pos[0] is not None:
-                    relx = x - (self._mouse_last_pos[0] or 0)
-                    rely = y - (self._mouse_last_pos[1] or 0)
-                    out += [('mousemove', 0, x, y, relx, rely)]
-                self._mouse_last_pos = (x, y)
+            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:
@@ -300,21 +301,21 @@
             self._mouse_last_bstate = bstate
 
         if bstate & curses.BUTTON1_PRESSED:
-            out += [('mousedown', 1, x, y)]
+            out += [('mousedown', 1, pos)]
         if bstate & curses.BUTTON2_PRESSED:
-            out += [('mousedown', 2, x, y)]
+            out += [('mousedown', 2, pos)]
         if bstate & curses.BUTTON3_PRESSED:
-            out += [('mousedown', 3, x, y)]
+            out += [('mousedown', 3, pos)]
         if bstate & curses.BUTTON1_RELEASED:
-            out += [('mouseup', 1, x, y)]
+            out += [('mouseup', 1, pos)]
         if bstate & curses.BUTTON2_RELEASED:
-            out += [('mouseup', 2, x, y)]
+            out += [('mouseup', 2, pos)]
         if bstate & curses.BUTTON3_RELEASED:
-            out += [('mouseup', 3, x, y)]
+            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, None)
+            self._mouse_last_pos = None
 
         return out
 
@@ -395,6 +396,7 @@
         t = self._inputqueue_get_wait()
         x = self._inputqueue_get_wait() - 0x21
         y = self._inputqueue_get_wait() - 0x21
+        pos = Point(x, y)
 
         out = []
 
@@ -403,25 +405,24 @@
             btn = t - 0x1f
             if not btn in self.mbtnstack:
                 self.mbtnstack.append(btn)
-                self._mouse_last_pos = (None, None)
-                out += [('mousedown', btn, x, y)]
+                self._mouse_last_pos = None
+                out += [('mousedown', btn, pos)]
             else:
                 # mouse move
-                if self._mouse_last_pos != (x, y):
-                    if self._mouse_last_pos[0] is not None:
-                        relx = x - self._mouse_last_pos[0]
-                        rely = y - self._mouse_last_pos[1]
-                        out += [('mousemove', btn, x, y, relx, rely)]
-                    self._mouse_last_pos = (x, y)
+                if self._mouse_last_pos != pos:
+                    if self._mouse_last_pos:
+                        relpos = pos - self._mouse_last_pos
+                        out += [('mousemove', btn, pos, relpos)]
+                    self._mouse_last_pos = pos
         elif t == 0x23:
             # button release
             btn = self.mbtnstack.pop()
             self._mouse_last_pos = (None, None)
-            out += [('mouseup', btn, x, y)]
+            out += [('mouseup', btn, pos)]
         elif t in (0x60, 0x61):
             # wheel up, down
             btn = 4 + t - 0x60
-            out += [('mousewheel', btn, x, y)]
+            out += [('mousewheel', btn, pos)]
         else:
             raise Exception('Unknown mouse event: %x' % t)
 
--- a/tuikit/driver/cursesw.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/driver/cursesw.py	Wed Sep 03 21:56:20 2014 +0200
@@ -4,6 +4,7 @@
 
 from tuikit.driver.driver import Driver
 from tuikit.core.events import ResizeEvent, KeypressEvent
+from tuikit.core.coords import Point
 
 
 class CursesWDriver(Driver):
@@ -65,7 +66,7 @@
         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, None)
+        self._mouse_last_pos = None  # Point
         self._mouse_last_bstate = None
 
     ## initialization, finalization ##
@@ -205,13 +206,13 @@
         except curses.error:
             return out
 
+        pos = Point(x, y)
         if bstate & curses.REPORT_MOUSE_POSITION:
-            if self._mouse_last_pos != (x, y):
-                if self._mouse_last_pos[0] is not None:
-                    relx = x - (self._mouse_last_pos[0] or 0)
-                    rely = y - (self._mouse_last_pos[1] or 0)
-                    out += [('mousemove', 0, x, y, relx, rely)]
-                self._mouse_last_pos = (x, y)
+            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:
@@ -223,21 +224,21 @@
             self._mouse_last_bstate = bstate
 
         if bstate & curses.BUTTON1_PRESSED:
-            out += [('mousedown', 1, x, y)]
+            out += [('mousedown', 1, pos)]
         if bstate & curses.BUTTON2_PRESSED:
-            out += [('mousedown', 2, x, y)]
+            out += [('mousedown', 2, pos)]
         if bstate & curses.BUTTON3_PRESSED:
-            out += [('mousedown', 3, x, y)]
+            out += [('mousedown', 3, pos)]
         if bstate & curses.BUTTON1_RELEASED:
-            out += [('mouseup', 1, x, y)]
+            out += [('mouseup', 1, pos)]
         if bstate & curses.BUTTON2_RELEASED:
-            out += [('mouseup', 2, x, y)]
+            out += [('mouseup', 2, pos)]
         if bstate & curses.BUTTON3_RELEASED:
-            out += [('mouseup', 3, x, y)]
+            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, None)
+            self._mouse_last_pos = None
 
         return out
 
--- a/tuikit/tableview.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/tableview.py	Wed Sep 03 21:56:20 2014 +0200
@@ -1,179 +1,269 @@
 # -*- coding: utf-8 -*-
 
-import math
-import logging
-
 from tuikit.events import Event, Emitter
 from tuikit.widget import Widget
 from tuikit.common import Coords
 
 
-class TableModel(Emitter):
-    def __init__(self, list_of_lists):
+class AbstractTableModel(Emitter):
+    def __init__(self):
         self.add_events('change', Event)
-        self.data = list_of_lists
+
+    ## accessing data
+
+    def get_row(self, row_idx):
+        """Get row by index.
+
+        Row is list of items of any type.
+
+        """
+        raise IndexError()
+
+    def get_column(self, column_idx):
+        """Get column by index (list of items)."""
+        raise IndexError()
+
+    def get_item(self, row_idx, column_idx):
+        """Get item by row and column index."""
+        return self.get_row(row_idx)[column_idx]
+
+    def get_row_header(self, row_idx, header_idx):
+        raise IndexError()
+
+    def get_column_header(self, column_idx, header_idx):
+        raise IndexError()
+
+    ## filling data
+
+    def set_item(self, row_idx, column_idx, value):
+        """Set item by row and column index."""
+        self.get_row(row_idx)[column_idx] = value
+        self.emit('change')
+
+    def set_row_header(self, row_idx, header_idx, value):
+        raise NotImplementedError()
 
-    def getcount(self):
-        '''Get number of rows.'''
-        return len(self.data)
+    def set_column_header(self, column_idx, header_idx, value):
+        raise NotImplementedError()
+
+    ## reading sizes
+
+    def row_count(self):
+        """Get number of rows."""
+        return 0
+
+    def column_count(self):
+        """Get number of columns."""
+        return 0
+
+    def row_header_count(self):
+        """Get number of row headers (header columns on left side of table)"""
+        return 0
+
+    def column_header_count(self):
+        """Get number of column headers (header rows on top of table)"""
+        return 0
 
-    def getrows(self, begin, end):
-        '''Get rows from begin to end, including begin, excluding end.'''
-        return self.data[begin:end]
+    ## changing size
+
+    def insert_row(self, row_idx):
+        raise NotImplementedError()
+
+    def remove_row(self, row_idx):
+        raise NotImplementedError()
 
-    def update(self, row, col, val):
-        self.data[row][col] = val
+    def insert_column(self, column_idx):
+        raise NotImplementedError()
+
+    def remove_column(self, column_idx):
+        raise NotImplementedError()
+
+    def set_num_headers(self, row_header_count, column_header_count):
+        """Set number of rows headers and column headers."""
+        raise NotImplementedError()
+
+    def clear(self):
+        raise NotImplementedError()
 
 
-class Column:
-
-    '''Columns description.'''
+class TableModel(AbstractTableModel):
+    def __init__(self):
+        AbstractTableModel.__init__(self)
+        # list of lists of items
+        self._num_cols = 0
+        self._num_col_headers = 0
+        self._num_row_headers = 0
+        self._rows = []
+        self._col_header_rows = []
+        self._row_header_rows = []
 
-    def __init__(self, title='', header=False, expand=True, sizereq=1,
-        readonly=False, maxlength=None):
-        '''Create column with default values.'''
+    ## accessing data
+
+    def get_row(self, row_idx):
+        """Get row by index."""
+        return self._rows[row_idx]
+
+    def get_column(self, column_idx):
+        """Get column by index (list of items)."""
+        if column_idx >= self._num_cols or column_idx < 0:
+            raise IndexError()
+        return [row[column_idx] for row in self._rows]
 
-        self.title = title
-        '''Title is displayed in heading before first row.'''
+    def get_row_header(self, row_idx, header_idx):
+        return self._row_header_rows[row_idx][header_idx]
+
+    def get_column_header(self, column_idx, header_idx):
+        return self._col_header_rows[header_idx][column_idx]
+
+    ## filling data
+
+    def set_row_header(self, row_idx, header_idx, value):
+        self._row_header_rows[row_idx][header_idx] = value
+        self.emit('change')
 
-        self.header = header
-        '''Header column is highlighted, values in this column cannot be edited.'''
+    def set_column_header(self, column_idx, header_idx, value):
+        self._col_header_rows[header_idx][column_idx] = value
+        self.emit('change')
+
+    ## reading sizes
 
-        self.expand = expand
-        '''If true, this column will autoresize to consume any free space.'''
+    def row_count(self):
+        return len(self._rows)
+
+    def column_count(self):
+        return self._num_cols
 
-        self.sizereq = sizereq
-        '''Size request. Meaning depends on value of expand:
+    def row_header_count(self):
+        return self._num_row_headers
+
+    def column_header_count(self):
+        return self._num_col_headers
+
+    ## changing size
 
-        When false, sizereq is number of characters.
-        When true, sizereq is relative size ratio.
+    def insert_row(self, row_idx):
+        row = [None] * self._num_cols
+        self._rows.insert(row_idx, row)
+        self._row_header_rows.insert(row_idx, [None] * self._num_row_headers)
+        self.emit('change')
 
-        '''
+    def remove_row(self, row_idx):
+        del self._rows[row_idx]
+        del self._row_header_rows[row_idx]
+        self.emit('change')
 
-        self.size = None
-        '''Computed size of column.'''
+    def insert_column(self, column_idx):
+        for row in self._col_header_rows:
+            row.insert(column_idx, None)
+        for row in self._rows:
+            row.insert(column_idx, None)
+        self._num_cols += 1
+        self.emit('change')
 
-        self.index = None
-        '''Computed index.'''
+    def remove_column(self, column_idx):
+        for row in self._col_header_rows:
+            del row[column_idx]
+        for row in self._rows:
+            del row[column_idx]
+        self._num_cols -= 1
+        self.emit('change')
 
-        self.readonly = readonly
-        '''If not readonly, values in this column can be changed by user.'''
+    def set_num_headers(self, row_header_count, column_header_count):
+        """Set number of rows headers and column headers."""
+        self._num_col_headers = column_header_count
+        self._num_row_headers = row_header_count
+        self._col_header_rows = []
+        for _i in range(self._num_col_headers):
+            self._col_header_rows.append([None] * self._num_cols)
+        for row in range(len(self._row_header_rows)):
+            self._row_header_rows[row] = [None] * self._num_row_headers
+        self.emit('change')
 
-        self.maxlength = maxlength
-        '''Maximum length of value (for EditField).'''
+    def clear(self):
+        self._rows = []
+        self._row_header_rows = []
+        self._num_cols = 0
+        self.set_num_headers(0, 0)
 
 
 class TableView(Widget):
     def __init__(self, model=None):
         Widget.__init__(self)
         self._default_size.update(20, 20)
+        self.allow_focus = True
 
-        self.allow_focus = True
+        # widths of colums, including header columns
+        self.column_sizes = []
+
+        self.spacing = 1
+
+        #: Active cell (cursor)
+        self.acell = Coords()
 
         # model
         self._model = None
-        self.setmodel(model)
-
-        self.columns = []
-        self.spacing = 1
-        self.rowcount = 0
-        self.headsize = 1
+        self.model = model
 
-        self.offset = Coords()
-        #: Active cell (cursor)
-        self.acell = Coords()
+        self.add_events('scroll', Event)
 
-        self.add_events(
-            'scroll', Event,
-            'areasize', Event)
-
-    def getmodel(self):
+    @property
+    def model(self):
         return self._model
 
-    def setmodel(self, value):
+    @model.setter
+    def model(self, value):
         if self._model:
             self._model.remove_handler('change', self.redraw)
         self._model = value
         if self._model:
             self._model.add_handler('change', self.redraw)
-
-    model = property(getmodel, setmodel)
-
-    def addcolumn(self, *args, **kwargs):
-        for col in args:
-            self.columns.append(col)
-        if len(args) == 0:
-            col = Column(**kwargs)
-            self.columns.append(col)
+        self._update_sizes()
 
-    def compute_column_sizes(self):
-        total_space = self.width - self.spacing * len(self.columns)
-        no_expand_cols = [col for col in self.columns if not col.expand]
-        no_expand_size = sum([col.sizereq for col in no_expand_cols])
-        expand_cols = [col for col in self.columns if col.expand]
-        expand_num = len(expand_cols)
-        expand_size = total_space - no_expand_size
-
-        # compute size of cols without expand
-        for col in no_expand_cols:
-            col.size = col.sizereq
+    def _update_sizes(self):
+        for _i in range(self._model.row_header_count()):
+            self.column_sizes.append(6)
+        for _i in range(self._model.column_count()):
+            self.column_sizes.append(8)
+        width = sum(self.column_sizes) + self.spacing * (len(self.column_sizes) - 1)
+        height = self._model.row_count() + self._model.column_header_count()
+        self._default_size.update(w=width, h=height)
 
-        # compute size of cols with expand
-        if no_expand_size > total_space + expand_num:
-            for col in expand_cols:
-                col.size = 1
-        else:
-            total_req = sum([col.sizereq for col in expand_cols])
-            remaining_space = 0.
-            for col in expand_cols:
-                frac, intp = math.modf(expand_size * col.sizereq / total_req)
-                col.size = int(intp)
-                remaining_space += frac
-                if remaining_space > 0.99:
-                    remaining_space -= 1.
-                    col.size += 1
+    def _draw_column_headers(self, driver, x, y):
+        for col_idx, col_size in enumerate(self.column_sizes[self._model.row_header_count():]):
+            for header_idx in range(self._model.column_header_count()):
+                title = str(self._model.get_column_header(col_idx, header_idx))
+                driver.puts(x, y + header_idx, title[:col_size])
+            x += col_size + self.spacing
 
-        # compute indexes
-        idx = 0
-        for col in self.columns:
-            if not col.header:
-                col.index = idx
-                idx += 1
+    def _draw_row_headers(self, driver, x, y):
+        for row_idx in range(self._model.row_count()):
+            hx = x
+            for header_idx in range(self._model.row_header_count()):
+                col_size = self.column_sizes[header_idx]
+                title = str(self._model.get_row_header(row_idx, header_idx))
+                driver.puts(hx, y + row_idx, title[:col_size])
+                hx += self.spacing + col_size
 
-    def draw_head(self, screen, x, y):
-        screen.pushcolor('strong')
-        for col in self.columns:
-            screen.puts(x, y, col.title[:col.size])
-            x += col.size + self.spacing
-        screen.popcolor()
-
-    def draw_row(self, screen, x, y, row, highlight):
-        for col, data in zip(self.columns, row):
-            if col.header:
-                screen.pushcolor('strong')
-            elif col.index in highlight:
-                screen.pushcolor('active')
-            else:
-                screen.pushcolor('normal')
-            screen.puts(x, y, data[:col.size])
-            screen.popcolor()
-            x += col.size + self.spacing
+    def _draw_data(self, driver, x, y):
+        for col_idx, col_size in enumerate(self.column_sizes[self._model.row_header_count():]):
+            for row_idx in range(self._model.row_count()):
+                title = str(self._model.get_item(row_idx, col_idx))
+                driver.puts(x, y + row_idx, title[:col_size])
+            x += col_size + self.spacing
 
     def on_draw(self, ev):
+        # compute basic sizes
+        row_header_count = self._model.row_header_count()
+        row_header_skip = sum(self.column_sizes[:row_header_count]) + self.spacing * row_header_count
+        column_header_skip = self._model.column_header_count()
+        # draw
         ev.driver.pushcolor('normal')
         ev.driver.fill_clip()
-        self.rowcount = self.model.getcount()
-        numrows = min(self.rowcount - self.offset.y, self.height - self.headsize)
-        rows = self.model.getrows(self.offset.y, self.offset.y + numrows)
-        self.compute_column_sizes()
-        self.draw_head(ev.driver, ev.x, ev.y)
-        y = ev.y + self.headsize
-        for row in rows:
-            highlight = []
-            if self.offset.y + rows.index(row) == self.acell.y:
-                highlight.append(self.acell.x)
-            self.draw_row(ev.driver, ev.x, y, row, highlight)
-            y += 1
+        ev.driver.pushcolor('strong')
+        self._draw_column_headers(ev.driver, ev.x + row_header_skip, ev.y)
+        self._draw_row_headers(ev.driver, ev.x, ev.y + column_header_skip)
+        ev.driver.popcolor()
+        self._draw_data(ev.driver, ev.x + row_header_skip, ev.y + column_header_skip)
         ev.driver.popcolor()
 
     def on_keypress(self, ev):
@@ -189,48 +279,39 @@
             self.redraw()
             return True
 
-    def set_yofs(self, yofs):
-        if yofs > self.rowcount - (self.height - self.headsize):
-            yofs = self.rowcount - (self.height - self.headsize)
-        if yofs < 0:
-            yofs = 0
-        self.offset.y = yofs
-        self.emit('scroll')
-
     def move_up(self):
         if self.acell.y > 0:
             self.acell.y -= 1
-            if self.acell.y < self.offset.y:
-                self.set_yofs(self.acell.y)
+            self.spot.y = self.acell.y + self._model.column_header_count()
             return True
         return False
 
     def move_down(self):
-        log=logging.getLogger('tuikit')
-        log.debug('height %d', self.height)
-        if self.acell.y < self.rowcount - 1:
+        if self.acell.y < self._model.row_count() - 1:
             self.acell.y += 1
-            if self.acell.y > self.offset.y + (self.height - self.headsize - 1):
-                self.set_yofs(self.acell.y - (self.height - self.headsize - 1))
+            self.spot.y = self.acell.y + self._model.column_header_count()
             return True
         return False
 
     def move_pageup(self):
-        if self.acell.y >= self.height - self.headsize - 1:
-            self.acell.y -= self.height - self.headsize - 1
-            self.set_yofs(self.offset.y - (self.height - self.headsize - 1))
+        view_height = self.view_height - self._model.column_header_count()
+        if self.acell.y >= view_height - 1:
+            self.emit('scrollreq', - (view_height - 1))
+            self.acell.y -= view_height - 1
+            self.spot.y = self.acell.y + self._model.column_header_count()
         else:
             self.acell.y = 0
-            self.set_yofs(0)
-
+            self.spot.y = self._model.column_header_count()
 
     def move_pagedown(self):
-        if self.acell.y <= self.rowcount - (self.height - self.headsize - 1):
-            self.acell.y += self.height - self.headsize - 1
-            self.set_yofs(self.offset.y + (self.height - self.headsize - 1))
+        view_height = self.view_height - self._model.column_header_count()
+        if self.acell.y <= self._model.row_count() - (view_height - 1):
+            self.emit('scrollreq', (self.view_height - 1))
+            self.acell.y += view_height - 1
+            self.spot.y = self.acell.y + self._model.column_header_count()
         else:
-            self.acell.y = self.rowcount - 1
-            self.set_yofs(self.acell.y)
+            self.acell.y = self._model.row_count() - 1
+            self.spot.y = self.acell.y + self._model.column_header_count()
 
     def move_left(self):
         if self.acell.x > 0:
--- a/tuikit/widgets/textbox.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/widgets/textbox.py	Wed Sep 03 21:56:20 2014 +0200
@@ -15,7 +15,7 @@
         self.text = text
 
         self._cursor_visible = True
-        # This variable rememberes horizontal cursor position
+        # This variable remembers horizontal cursor position
         # for the case when cursor moves to shorter line.
         self.cursor_column = 0
         # selection - line and column of selection start
--- a/tuikit/widgets/textfield.py	Wed Sep 03 19:17:04 2014 +0200
+++ b/tuikit/widgets/textfield.py	Wed Sep 03 21:56:20 2014 +0200
@@ -18,13 +18,16 @@
         self.maxlen = None   # unlimited
 
         self.tw = 0       # real width of text field (minus space for arrows)
-        self.curspos = len(value)      # position of cursor in value
+        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):
         self.tw = self.width - 2
+        if self.curspos >= self.tw:
+            self.ofs = self.curspos - self.tw
 
     def draw(self, buffer):
         color = self.theme.active if self.has_focus() else self.theme.normal
@@ -48,25 +51,22 @@
             self._cursor.update(1 + self.curspos - self.ofs, 0)
 
     def keypress_event(self, ev):
-        consumed = True
-        if ev.keyname == 'left':
-            self.move_left()
-        elif ev.keyname == 'right':
-            self.move_right()
-        elif ev.keyname == 'backspace':
-            if self.curspos > 0:
-                self.move_left()
-                self.del_char()
-        elif ev.keyname == 'delete':
-            self.del_char()
-        else:
-            consumed = False
-
+        map_keyname_to_func = {
+            'left':       self.move_left,
+            'right':      self.move_right,
+            'home':       self.move_home,
+            'end':        self.move_end,
+            'backspace':  self.move_left_and_del,
+            'delete':     self.del_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()
             consumed = True
-
         if consumed:
             #self.redraw()
             return True
@@ -84,8 +84,8 @@
 
     def move_right(self):
         if self.curspos < len(self.value):
-            if self.curspos - self.ofs < self.tw - 2 \
-            or (self.curspos - self.ofs == self.tw - 2 and self.curspos == len(self.value)-1):
+            if self.curspos - self.ofs < self.tw - 1 \
+            or (self.curspos - self.ofs == self.tw - 1 and self.curspos == len(self.value)-1):
                 # move cursor
                 self.curspos += 1
             else:
@@ -93,9 +93,22 @@
                 self.curspos += 1
                 self.ofs += 1
 
+    def move_home(self):
+        self.curspos = 0
+        self.ofs = 0
+
+    def move_end(self):
+        self.curspos = len(self.value)
+        if self.curspos >= self.tw:
+            self.ofs = self.curspos - self.tw
+
     def add_char(self, c):
         self.value = self.value[:self.curspos] + c + self.value[self.curspos:]
 
     def del_char(self):
         self.value = self.value[:self.curspos] + self.value[self.curspos+1:]
 
+    def move_left_and_del(self):
+        if self.curspos > 0:
+            self.move_left()
+            self.del_char()