tuikit/tableview.py
author Radek Brich <radek.brich@devl.cz>
Sun, 03 Feb 2013 16:38:41 +0100
changeset 77 fc1989059e19
parent 64 03f591f5fe5c
child 98 dcfb185ac866
child 100 3b2df86d8f94
permissions -rw-r--r--
Propagate "quit" event, do not just terminate application. Resize: flag widgets to be resized, do resizes only once before draw. Draw: flag widgets to be redrawn, do not draw everything on any event.

# -*- 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):
        self.add_events('change', Event)
        self.data = list_of_lists

    def getcount(self):
        '''Get number of rows.'''
        return len(self.data)

    def getrows(self, begin, end):
        '''Get rows from begin to end, including begin, excluding end.'''
        return self.data[begin:end]

    def update(self, row, col, val):
        self.data[row][col] = val


class Column:

    '''Columns description.'''

    def __init__(self, title='', header=False, expand=True, sizereq=1,
        readonly=False, maxlength=None):
        '''Create column with default values.'''

        self.title = title
        '''Title is displayed in heading before first row.'''

        self.header = header
        '''Header column is highlighted, values in this column cannot be edited.'''

        self.expand = expand
        '''If true, this column will autoresize to consume any free space.'''

        self.sizereq = sizereq
        '''Size request. Meaning depends on value of expand:

        When false, sizereq is number of characters.
        When true, sizereq is relative size ratio.

        '''

        self.size = None
        '''Computed size of column.'''

        self.index = None
        '''Computed index.'''

        self.readonly = readonly
        '''If not readonly, values in this column can be changed by user.'''

        self.maxlength = maxlength
        '''Maximum length of value (for EditField).'''


class TableView(Widget):
    def __init__(self, model=None):
        Widget.__init__(self)
        self._default_size.update(20, 20)

        self.allow_focus = True

        # model
        self._model = None
        self.setmodel(model)

        self.columns = []
        self.spacing = 1
        self.rowcount = 0
        self.headsize = 1

        self.offset = Coords()
        #: Active cell (cursor)
        self.acell = Coords()

        self.add_events(
            'scroll', Event,
            'areasize', Event)

    def getmodel(self):
        return self._model

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

    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

        # 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

        # compute indexes
        idx = 0
        for col in self.columns:
            if not col.header:
                col.index = idx
                idx += 1

    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 on_draw(self, ev):
        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.popcolor()

    def on_keypress(self, ev):
        key_map = {
            'up': self.move_up,
            'down': self.move_down,
            'left': self.move_left,
            'right': self.move_right,
            'pageup':  self.move_pageup,
            'pagedown':  self.move_pagedown}
        if ev.keyname in key_map:
            key_map[ev.keyname]()
            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)
            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:
            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))
            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))
        else:
            self.acell.y = 0
            self.set_yofs(0)


    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))
        else:
            self.acell.y = self.rowcount - 1
            self.set_yofs(self.acell.y)

    def move_left(self):
        if self.acell.x > 0:
            self.acell.x -= 1
            return True
        return False

    def move_right(self):
        if self.acell.x < len([col for col in self.columns if not col.header]) - 1:
            self.acell.x += 1
            return True
        return False