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)