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()