# HG changeset patch # User Radek Brich # Date 1408537488 -7200 # Node ID f3063f08ba81e3fee39c2c10a14fb7bb7ba8b0b4 # Parent dcfb185ac8665174717c7538637920fc91589320 Update TableView (old uncommitted work). diff -r dcfb185ac866 -r f3063f08ba81 demo_tableview.py --- 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': diff -r dcfb185ac866 -r f3063f08ba81 tests/test_tablemodel.py --- /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() + diff -r dcfb185ac866 -r f3063f08ba81 tuikit.conf --- 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' diff -r dcfb185ac866 -r f3063f08ba81 tuikit.conf.example --- /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' diff -r dcfb185ac866 -r f3063f08ba81 tuikit/tableview.py --- 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: