tuikit/widget.py
author Radek Brich <radek.brich@devl.cz>
Sat, 02 Feb 2013 12:54:27 +0100
changeset 76 fa5301e58eca
parent 75 2430c643838a
child 77 fc1989059e19
permissions -rw-r--r--
Update demo_input, demo_editor. Update ScrollView: show/hide scrollbars as needed on child size requests.

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

from tuikit.events import Emitter, Event, DrawEvent, FocusEvent, KeyboardEvent, MouseEvent, GenericEvent
from tuikit.common import Coords, Size

import logging


class Widget(Emitter):

    '''Base class for all widgets.'''

    def __init__(self):
        #: Widget name is used for logging etc. Not visible anywhere.
        self.name = None

        #: Parent widget.
        self.parent = None

        #: Top widget (same for every widget in one application).
        self._top = None

        #: Floating widget
        self.floater = False

        ### placing and size
        #: Position inside parent widget. Modified by layout manager.
        self._pos = Coords()
        #: Actual size. Modified only by layout manager.
        self._size = Size(10, 10)
        self._size.add_handler('change', lambda ev: self.emit('resize'))
        #: Size of visible part of widget. Used in OffsetLayout. Modified only by layout manager.
        self._view_size = Size(10, 10)
        #: Default (natural) size of Widget.
        self._default_size = Size(1, 1)
        self._default_size.add_handler('change', lambda ev: self._sizereq.update(self._default_size))
        #: Size request. Equals to default size, unless changed by user.
        #: Size request will be fulfilled by layout manager when possible.
        self._sizereq = Size(1, 1)
        self._sizereq.add_handler('change', lambda ev: self.emit('sizereq'))
        #: Minimal size of widget. Under normal circumstances
        #: widget will never be sized smaller than this.
        #: Tuple (w, h). Both must be integers >= 1.
        self.sizemin = Size(1, 1)
        #: Maximum size of widget. Widget will never be sized bigger than this.
        #: Tuple (w, h). Integers >= 1 or None (meaning no maximum size or infinite).
        self.sizemax = Size(None, None)


        #: Allow keyboard focus for this widget.
        self.allow_focus = False

        #: Dictionary containing optional parameters for layout managers etc.
        self._hints = {}

        #: Hidden widget does not affect layout.
        self._hidden = False

        #: Cursor is position where text input will occur,
        #: i.e. classic blinking cursor in console.
        #: None means no cursor (hidden).
        self.cursor = None

        # See spot property.
        self._spot = Coords()
        self._spot.add_handler('change', lambda ev: self.emit('spotmove'))

        # pending resize/draw request
        self._need_resize = True
        self._need_draw = True

        # event handlers
        self.add_events(
            'resize', Event,
            'draw', DrawEvent,
            'keypress', KeyboardEvent,
            'mousedown', MouseEvent,
            'mouseup', MouseEvent,
            'mousemove', MouseEvent,
            'mousehover', MouseEvent,
            'mousewheel', MouseEvent,
            'sizereq', Event,
            'scrollreq', GenericEvent,
            'spotmove', Event,
            'focus', FocusEvent,
            'unfocus', FocusEvent,
            'show', Event,
            'hide', Event)

    ### placing and size

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

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

    def move(self, x=None, y=None):
        if self.floater:
            self._pos.update(x, y)
        if self.parent:
            self.parent.move_child(self, x, y)

    @property
    def width(self):
        return self._size.w

    @property
    def height(self):
        return self._size.h

    def resize(self, w=None, h=None):
        """Set size request.

        It's up to parent container if request will be fulfilled.

        """
        self._sizereq.update(w, h)

    @property
    def view_width(self):
        return self._view_size.w

    @property
    def view_height(self):
        return self._view_size.h

    @property
    def sizereq(self):
        """Size request.

        This is default size of the widget. Will be fulfilled if possible.
        Size(w, h). Integers >= 1 or None (meaning use minumal size).

        """
        return self._sizereq

    def on_sizereq(self, ev):
        # floater is not managet by layout,
        # always set its size to sizereq
        if self.floater:
            logging.getLogger('tuikit').info('xy')
            self._size.update(self._sizereq)

    ### misc

    @property
    def spot(self):
        """Spot of visibility.

        This is one point which should be always visible.
        It affects scrolling (moving spot in widget placed in ScrollView scrolls the view).

        """
        return self._spot


    @property
    def top(self):
        """Top widget (same for every widget in one application)."""
        return self._top

    @top.setter
    def top(self, value):
        self._set_top(value)

    def _set_top(self, value):
        """Real setter for top. Allows override."""
        self._top = value

    def reset_hints(self):
        """Reset all hints to their initial value.

        This must be called at before any call to update_hint.

        """
        self._hints.update({k:v() for k,v in self.parent._hint_class.items()})

    def update_hint(self, hint_name, *args, **kwargs):
        """Set or update hint value.

        Args after hint_name are forwarded to update() method or initializer
        of hint's class.

        """
        if hint_name not in self._hints:
            raise ValueError('Hint %r is not registered.' % hint_name)
        try:
            # try update
            self._hints[hint_name].update(*args, **kwargs)
        except AttributeError:
            # if update does not exist, call initializer instead
            self._hints[hint_name] = self._hints[hint_name].__class__(*args, **kwargs)

    def hint_value(self, hint_name):
        try:
            return self._hints[hint_name].get_value()
        except AttributeError:
            return self._hints[hint_name]

    def get_hint(self, hint_name):
        return self._hints[hint_name]

    ### events

    def need_resize(self):
        self._need_resize = True

    def redraw(self, parent=False):
        self._need_draw = True
        if parent and self.parent:
            self.parent._redraw = True

    def draw(self, driver, x, y):
        """Draw the widget.

        This method should not be overriden by subclasses,
        use on_draw method instead.

        """
        if self.hidden:
            return True

        driver.clipstack.push(x, y, self.width, self.height)
        self.emit('draw', driver, x, y)
        driver.clipstack.pop()

        if self.has_focus():
            if self.cursor:
                cx, cy = self.cursor
                driver.showcursor(x + cx, y + cy)
            else:
                driver.hidecursor()

    def on_mousedown(self, ev):
        self.grab_focus()


    ### focus


    def can_focus(self):
        return not self.hidden and self.allow_focus


    def has_focus(self):
        if self.parent is None:
            return True
        return (self.parent.has_focus() \
            and self.parent.focuschild == self)

    def set_focus(self):
        """Focus the widget.

        This changes focus state of parent widget,
        but it does not check if parent widget actually
        has focus. It still works if it has not,
        but keyboard events will go to really focused widget,
        not this one.

        See also grab_focus() which cares about parents.

        """
        if self.parent is None:
            return
        if not self.can_focus() or self.parent.focuschild == self:
            return
        oldfocuschild = self.parent.focuschild
        self.parent.focuschild = self
        if oldfocuschild:
            oldfocuschild.emit('unfocus', new=self)
        self.emit('focus', old=oldfocuschild)

    def grab_focus(self):
        """Focus the widget and its parents."""
        if self.parent and not self.parent.has_focus():
            self.parent.grab_focus()
        self.set_focus()


    ###

    def enclose(self, x, y):
        if self.hidden:
            return False
        if x < self.x or y < self.y \
        or x >= self.x + self.width or y >= self.y + self.height:
            return False
        return  True


    def screentest(self, y, x):
        sy, sx = self.screenyx()
        if y < sy or x < sx or y >= sy + self.height or x >= sx + self.width:
            return False
        return True


    def screenyx(self):
        if self.parent:
            y,x = self.parent.screenyx()
            return self.y + y, self.x + x
        return self.y, self.x

    @property
    def hidden(self):
        return self._hidden

    def hide(self):
        '''Hide widget. Convenience method.'''
        if not self._hidden:
            self._hidden = True
            self.emit('hide')
            self.redraw()

    def show(self):
        '''Show widget. Convenience method.'''
        if self._hidden:
            self._hidden = False
            self.emit('show')
            self.redraw()

    def bring_up(self):
        if self.parent:
            self.parent.bring_up_child(self)


    ## timeout ##

    def add_timeout(self, delay, callback):
        """Register callback to be called after delay seconds.

        delay -- in seconds, float
        callback -- function to be called with no parameters

        """
        self.top.timer.add_timeout(delay, callback)

    def remove_timeout(self, callback):
        """Unregister callback previously registered with add_timeout."""
        self.top.timer.remove_timeout(callback)