Update TableView (old uncommitted work).
--- a/demo_tableview.py Mon Feb 04 20:12:09 2013 +0100
+++ b/demo_tableview.py Wed Aug 20 14:24:48 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':
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/test_tablemodel.py Wed Aug 20 14:24:48 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 Mon Feb 04 20:12:09 2013 +0100
+++ /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 Aug 20 14:24:48 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/tableview.py Mon Feb 04 20:12:09 2013 +0100
+++ b/tuikit/tableview.py Wed Aug 20 14:24:48 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 rowcount(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 get_rows(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.rowcount()
- numrows = min(self.rowcount - self.offset.y, self.height - self.headsize)
- rows = self.model.get_rows(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: