# HG changeset patch # User Radek Brich # Date 1409774180 -7200 # Node ID 6796adfdc7eb95ac61b4816c0da7613530fdb67f # Parent ce2e67e7bbb8673d4a918e2f226e16c00e0f469c# Parent 11dac45bfba423627684dda2ce91b7179a43444a Merge. Due to my schizophrenia, I've accidentally forked my own code. The other set of changes were found in another computer. diff -r ce2e67e7bbb8 -r 6796adfdc7eb .hgignore --- 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__ diff -r ce2e67e7bbb8 -r 6796adfdc7eb DOC --- /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 diff -r ce2e67e7bbb8 -r 6796adfdc7eb INSPIRATION --- /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/ diff -r ce2e67e7bbb8 -r 6796adfdc7eb demo_tableview.py --- 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': diff -r ce2e67e7bbb8 -r 6796adfdc7eb demos/04_texteditor.py --- 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) diff -r ce2e67e7bbb8 -r 6796adfdc7eb docs/terminology.rst --- /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. diff -r ce2e67e7bbb8 -r 6796adfdc7eb tests/test_tablemodel.py --- /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() + diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit.conf --- 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' diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit.conf.example --- /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' diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/core/buffer.py --- 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.""" diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/core/container.py --- 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): diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/core/coords.py --- 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. diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/core/widget.py --- 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)) diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/core/window.py --- 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) diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/driver/curses.py --- 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) diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/driver/cursesw.py --- 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 diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/tableview.py --- 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: diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/widgets/textbox.py --- 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 diff -r ce2e67e7bbb8 -r 6796adfdc7eb tuikit/widgets/textfield.py --- 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()