tuikit/common.py
author Radek Brich <radek.brich@devl.cz>
Fri, 01 Feb 2013 09:34:15 +0100
changeset 75 2430c643838a
parent 74 23767a33a781
permissions -rw-r--r--
Clean up hints - add methods update_hint, hint_value to Widget. Split ScrollView into OffsetView and Scrolling components. Reimplement ScrollWindow using Scrolling component.

# -*- coding: utf-8 -*-

from tuikit.events import Event, Emitter


class Select:
    def __init__(self, selected=None):
        if not hasattr(self, '_options'):
            raise TypeError('Cannot instantiate Select class.')
        self._selected = selected or self._options[0]

    def update(self, selected):
        if selected not in self._options:
            raise ValueError('Select: %r not in options %r' % (selected, self._options))
        self._selected = selected

    def select_next(self):
        i = self._options.index(self._selected)
        try:
            self._selected = self._options[i+1]
        except IndexError:
            self._selected = self._options[0]

    @property
    def selected(self):
        return self._selected

    def get_value(self):
        return self._selected

    def __repr__(self):
        return self.__class__.__name__ + '(%r)' % self._selected


def make_select(*args):
    name = ''.join([x.capitalize() for x in args]) + 'Select'
    return type(name, (Select,), {'_options': args})


class Coords(Emitter):

    '''2D coordinates.'''

    def __init__(self, x=0, y=0):
        self._x = x
        self._y = y
        self.add_events('change', Event)

    @property
    def x(self):
        return self._x

    @x.setter
    def x(self, value):
        self._x = value
        self.emit('change')

    @property
    def y(self):
        return self._y

    @y.setter
    def y(self, value):
        self._y = value
        self.emit('change')

    def __getitem__(self, key):
        try:
            tupl = (self.x, self.y)
            return tupl[key]
        except TypeError:
            pass
        return self.__dict__[key]

    def __setitem__(self, key, value):
        if key == 0:
            self.x = value
        if key == 1:
            self.y = value

    def __repr__(self):
        return 'Coords(x={0.x},y={0.y})'.format(self)

    def update(self, x=None, y=None):
        old_x, old_y = self._x, self._y
        if isinstance(x, Coords) and y is None:
            self._x, self._y = x
        else:
            if isinstance(x, int):
                self._x = x
            elif x is not None:
                raise ValueError('Coords.update(): first parameter must be int or Coords')
            if isinstance(y, int):
                self._y = y
            elif y is not None:
                raise ValueError('Coords.update(): second parameter must be int')
        if self._x != old_x or self._y != old_y:
            self.emit('change')


class Size(Emitter):

    '''Size class.

    Implements attribute access (.w, .h), list-like access([0],[1])
    and dict-like access (['w'],['h']).

    '''

    def __init__(self, w=None, h=None):
        self._w = w
        self._h = h
        self.add_events('change', Event)

    @property
    def w(self):
        return self._w

    @w.setter
    def w(self, value):
        self._w = value
        self.emit('change')

    @property
    def h(self):
        return self._h

    @h.setter
    def h(self, value):
        self._h = value
        self.emit('change')

    def __getitem__(self, key):
        try:
            tupl = (self.w, self.h)
            return tupl[key]
        except TypeError:
            pass
        return self.__dict__[key]

    def __setitem__(self, key, value):
        if key in [0, 'w']:
            self.w = value
        if key in [1, 'h']:
            self.h = value

    def __repr__(self):
        return 'Size(w={0._w},h={0._h})'.format(self)

    def update(self, w=None, h=None):
        old_w, old_h = self._w, self._h
        if isinstance(w, Size) and h is None:
            self._w, self._h = w
        else:
            if isinstance(w, int):
                self._w = w
            elif w is not None:
                raise ValueError('Size.update(): first parameter must be int or Size')
            if isinstance(h, int):
                self._h = h
            elif h is not None:
                raise ValueError('Size.update(): second parameter must be int')
        if self._w != old_w or self._h != old_h:
            self.emit('change')


class Rect:

    '''Rectangle is defined by 2D coordinates and size.'''

    def __init__(self, x=0, y=0, w=0, h=0):
        self.x = x
        self.y = y
        self.w = w
        self.h = h

    def __repr__(self):
        return 'Rect(x={0.x},y={0.y},w={0.w},h={0.h})'.format(self)


class Borders:

    '''Borders are defined by left, top, right, bottom border size.

    Ordering is clock-wise, starting with left. This may seem weird,
    but it corresponds to X/Y or W/H used elsewhere. Left and right are
    on X axis, so they are defined first.

    '''

    def __init__(self, l=0, t=0, r=0, b=0, full=None):
        self.l = l # left
        self.t = t # top
        self.r = r # right
        self.b = b # bottom
        if full is not None:
            self.l = self.t = self.r = self.b = full

    def __getitem__(self, key):
        try:
            tupl = (self.l, self.t, self.r, self.b)
            return tupl[key]
        except TypeError:
            pass
        return self.__dict__[key]

    def __repr__(self):
        return 'Borders(l={0.l},t={0.t},r={0.r},b={0.b})'.format(self)

    def update(self, *args, **kwargs):
        if len(args) == 4:
            self.l, self.t, self.r, self.b = args
        elif len(args) == 1 and isinstance(args[0], Borders):
            self.l, self.t, self.r, self.b = args[0]
        elif len(args):
            raise ValueError('Borders.update() takes exactly 4 positional arguments.')
        for arg in kwargs:
            setattr(self, arg, kwargs[arg])


class ClipStack:

    '''Stack of clipping regions.'''

    def __init__(self):
        self.stack = []

    def push(self, x, y, w, h):
        newclip = Rect(x, y, w, h)
        if len(self.stack):
            oldclip = self.top()
            newclip = self.intersect(oldclip, newclip)
        self.stack.append(newclip)

    def pop(self):
        self.stack.pop()

    def top(self):
        return self.stack[-1]

    def test(self, x, y):
        # no clip rectangle on stack => passed
        if not len(self.stack):
            return True
        # test against top clip rect from stack
        clip = self.top()
        if x < clip.x or y < clip.y \
        or x >= clip.x + clip.w or y >= clip.y + clip.h:
            return False
        # passed
        return True

    def intersect(self, r1, r2):
        x1 = max(r1.x, r2.x)
        y1 = max(r1.y, r2.y)
        x2 = min(r1.x + r1.w, r2.x + r2.w)
        y2 = min(r1.y + r1.h, r2.y + r2.h)
        if x1 >= x2 or y1 >= y2:
            return Rect()
        return Rect(x1, y1, x2-x1, y2-y1)

    def union(self, r1, r2):
        x = min(r1.x, r2.x)
        y = min(r1.y, r2.y)
        w = max(r1.x + r1.w, r2.x + r2.w) - x
        h = max(r1.y + r1.h, r2.y + r2.h) - y
        return Rect(x, y, w, h)


class UnicodeGraphics:

    '''Unicode graphics bank.

    This class can be overriden to change graphics style (round corners etc.).'''

    # http://en.wikipedia.org/wiki/List_of_Unicode_characters#Geometric_shapes
    UP_ARROW = '▲' #curses.ACS_UARROW
    DOWN_ARROW = '▼' #curses.ACS_DARROW
    LEFT_ARROW = '◀'
    RIGHT_ARROW = '▶'
    CIRCLE = '●'
    DIAMOND = '◆'
    MIDDLE_DOT = '·'

    # http://en.wikipedia.org/wiki/Box-drawing_characters
    LIGHT_SHADE = '░' #curses.ACS_BOARD
    MEDIUM_SHADE = '▒'
    DARK_SHADE = '▓'
    BLOCK = '█'

    COLUMN = '▁▂▃▄▅▆▇█'
    CORNER_ROUND = '╭╮╰╯'
    CORNER = '┌┐└┘'
    LINE = '─━│┃┄┅┆┇┈┉┊┋'

    HLINE = '─' # curses.ACS_HLINE
    VLINE = '│' # curses.ACS_VLINE
    ULCORNER = '┌' # curses.ACS_ULCORNER
    URCORNER = '┐' # curses.ACS_URCORNER
    LLCORNER = '└' # curses.ACS_LLCORNER
    LRCORNER = '┘' # curses.ACS_LRCORNER
    LTEE = '├'
    RTEE = '┤'

    char_map = {
        # scrollbar
        'sb_thumb'  : CIRCLE,
        'sb_htrack' : LINE[8],
        'sb_vtrack' : LINE[10],
        'sb_left'   : LEFT_ARROW,
        'sb_right'  : RIGHT_ARROW,
        'sb_up'     : UP_ARROW,
        'sb_down'   : DOWN_ARROW,
    }

    def get_char(self, name):
        return self.char_map[name]