# HG changeset patch # User Radek Brich # Date 1424595193 -3600 # Node ID dd91747504dd2a7caf4e3bdc6a29925a44c8a4e9 # Parent 8c79705206324481021fbbe1a0cc47aec70664bf Redraw widgets on request. Add scrollbar demo. diff -r 8c7970520632 -r dd91747504dd demos/03_event.py --- a/demos/03_event.py Sat Feb 21 12:01:57 2015 +0100 +++ b/demos/03_event.py Sun Feb 22 09:53:13 2015 +0100 @@ -4,16 +4,16 @@ from tuikit.driver.cursesw import CursesWDriver buffer = Buffer() -line = 0 with CursesWDriver() as driver: buffer.resize(*driver.size) - buffer.puts(str(driver.size), 0, 10) + buffer.puts("Press a key or mouse button. Ctrl-C to exit.", 0, 0) + line = 1 while True: - for event in driver.getevents(): - buffer.puts(str(event), 0, line) - line += 1 driver.draw(buffer) driver.flush() if line >= buffer.size.h: buffer.fill() line = 0 + for event in driver.getevents(): + buffer.puts(str(event), 0, line) + line += 1 diff -r 8c7970520632 -r dd91747504dd demos/15_scrollbar.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/demos/15_scrollbar.py Sun Feb 22 09:53:13 2015 +0100 @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 + +from tuikit.core.application import Application +from tuikit.widgets.scrollbar import HScrollbar + +app = Application() + +hscroll = HScrollbar(80) +app.add(hscroll, 0, 0) +app.start() diff -r 8c7970520632 -r dd91747504dd tuikit/core/application.py --- a/tuikit/core/application.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/core/application.py Sun Feb 22 09:53:13 2015 +0100 @@ -63,7 +63,7 @@ screen = ProxyBuffer(self.driver) while not self._quit: - self.window_manager.draw(screen) + self.window_manager.draw_if_needed(screen) self.driver.cursor = self.window_manager.get_cursor_if_visible() self.driver.flush() diff -r 8c7970520632 -r dd91747504dd tuikit/core/container.py --- a/tuikit/core/container.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/core/container.py Sun Feb 22 09:53:13 2015 +0100 @@ -16,7 +16,7 @@ #: List of child widgets. self._widgets = [] #: Widget with keyboard focus - self.focus_widget = None + self._focus_widget = None #: Widget on last mouse position self.mouse_widget = None #: If True, tab cycles inside container @@ -29,22 +29,33 @@ widget.parent = self widget.window = self.window widget.set_theme(self.theme) + widget.timer = self.timer self.layout.add(widget, *args, **kwargs) - if self.focus_widget is None and widget.can_focus(): - self.focus_widget = widget + if self._focus_widget is None and widget.can_focus(): + self._focus_widget = widget + widget.redraw() def resize(self, w, h): Widget.resize(self, w, h) self.layout.update(w, h) - def draw(self, buffer): - """Draw child widgets.""" - Widget.draw(self, buffer) + # ------------------------------------------------------------------------- + # Presentation + + def draw_if_needed(self, buffer): + """Draw this and child widgets, whichever was requested by redraw.""" + Widget.draw_if_needed(self, buffer) for child in self._widgets: with buffer.moved_origin(child.x, child.y): with buffer.clip(buffer.origin.x, buffer.origin.y, child.width, child.height): - child.draw(buffer) + child.draw_if_needed(buffer) + + def redraw(self): + """Request redraw of all widgets in container.""" + Widget.redraw(self) + for child in self._widgets: + child.redraw() def set_theme(self, theme): Widget.set_theme(self, theme) @@ -56,7 +67,8 @@ """Return cursor coordinates or None if cursor is not set or is set outside of widget boundaries. - If this container has child with focus, return its cursor position instead. + If this container has child with focus, + return its cursor position instead. """ if self.focus_widget: @@ -66,7 +78,7 @@ cursor = cursor.moved(*self.focus_widget.pos) else: cursor = self._cursor.immutable() - if cursor in Rect._make((0, 0), self._size): + if cursor in Rect(0, 0, *self._size): return cursor @property @@ -76,7 +88,8 @@ else: return self._cursor_visible - ## input events ## + # ------------------------------------------------------------------------- + # Events def keypress_event(self, ev): # First, handle the keypress event to focused child widget @@ -106,7 +119,8 @@ if self.mouse_widget: self.mouse_widget.mousemove_event(ev.rebase(self.mouse_widget.pos)) - ## focus ## + # ------------------------------------------------------------------------- + # Focus def focus_next(self, step=1): """Focus next child. @@ -150,3 +164,15 @@ def focus_previous(self): """Focus previous child.""" self.focus_next(-1) + + @property + def focus_widget(self): + return self._focus_widget + + @focus_widget.setter + def focus_widget(self, widget): + orig_focus_widget = self._focus_widget + self._focus_widget = widget + widget.redraw() + if orig_focus_widget: + orig_focus_widget.redraw() diff -r 8c7970520632 -r dd91747504dd tuikit/core/coords.py --- a/tuikit/core/coords.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/core/coords.py Sun Feb 22 09:53:13 2015 +0100 @@ -200,23 +200,27 @@ """Rectangle is defined by 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 __init__(self, *args, **kwargs): + self.x = 0 + self.y = 0 + self.w = 0 + self.h = 0 + if len(args) == 4: + self.x, self.y, self.w, self.h = args + elif len(args) == 2: + self.set_origin_and_size(*args) + for key, val in kwargs.items(): + setattr(self, key, val) - @classmethod - def _make(cls, origin, size): - """Make new Rect instance with origin and size as specified. + def set_origin_and_size(self, origin, size): + """Set rect origin and size as specified. `origin` should be Point or pair of coordinates, `size` should be Size or pair of integers """ - x, y = origin - w, h = size - return Rect(x, y, w, h) + self.x, self.y = origin + self.w, self.h = size def __repr__(self): return '{0.__class__.__name__}(x={0.x}, y={0.y}, w={0.w}, h={0.h})'.format(self) diff -r 8c7970520632 -r dd91747504dd tuikit/core/theme.py --- a/tuikit/core/theme.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/core/theme.py Sun Feb 22 09:53:13 2015 +0100 @@ -5,10 +5,10 @@ """Default color style""" - normal = 'lightgray' - active = 'black on cyan' - button = 'black on lightgray' - button_active = 'black on cyan' + clr_normal = 'lightgray' + clr_active = 'black on cyan' + clr_button = 'black on lightgray' + clr_button_active = 'black on cyan' class GraphicalTheme: diff -r 8c7970520632 -r dd91747504dd tuikit/core/widget.py --- a/tuikit/core/widget.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/core/widget.py Sun Feb 22 09:53:13 2015 +0100 @@ -23,6 +23,8 @@ self.window = None #: Theme self.theme = default_theme + #: Timer + self.timer = None #: Position inside parent widget. Modified by layout manager. self.pos = Point() @@ -43,6 +45,8 @@ #: Cursor is displayed on screen only when the widget is focused. self._cursor_visible = False + #: True if this widget requests redraw + self.need_draw = True #: Hidden widget does not affect layout. self.hidden = False #: Allow keyboard focus for this widget. @@ -50,7 +54,8 @@ self.sig_keypress = Signal(allow_stop=True) - ## position and size ## + # ------------------------------------------------------------------------- + # Position and size @property def x(self): @@ -82,14 +87,24 @@ @property def boundaries(self): - return Rect._make(self.pos, self._size) + return Rect(self.pos, self._size) - ## drawing, looks ## + # ------------------------------------------------------------------------- + # Presentation + + def draw_if_needed(self, buffer): + if self.need_draw: + self.draw(buffer) def draw(self, buffer): """Draw self into buffer.""" self.log.debug('Draw into %r at %s (exposed %s)', buffer, buffer.origin, self.exposed(buffer)) + self.need_draw = False + + def redraw(self): + """Request redraw.""" + self.need_draw = True def set_theme(self, theme): self.theme = theme @@ -113,14 +128,15 @@ Returns None if cursor is set outside of widget boundaries. """ - if self._cursor in Rect._make((0, 0), self._size): + if self._cursor in Rect(0, 0, *self._size): return self._cursor @property def cursor_visible(self): return self._cursor_visible - ## events ## + # ------------------------------------------------------------------------- + # Events def resize_event(self, ev): self.resize(ev.w, ev.h) @@ -149,21 +165,8 @@ def mousemove_event(self, ev): self.log.debug('Not consumed: %s', ev) - ## timeouts ## - - def add_timeout(self, delay, callback, *args): - """Register `callback` to be called after `delay` seconds.""" - self.parent.add_timeout(self, delay, callback, *args) - - def remove_timeout(self, callback, *args): - """Unregister callback previously registered with add_timeout. - - Removes all timeouts with the `callback` and `args`. - - """ - self.parent.remove_timeout(self, callback, *args) - - ## focus ## + # ------------------------------------------------------------------------- + # Focus def can_focus(self): return not self.hidden and self.allow_focus @@ -171,10 +174,11 @@ def has_focus(self): if self.parent is None: return True - return (self.parent.has_focus() - and self.parent.focus_widget == self) + return (self.parent.has_focus() and + self.parent.focus_widget == self) - ## utilities ## + # ------------------------------------------------------------------------- + # Utilities @property def log(self): diff -r 8c7970520632 -r dd91747504dd tuikit/core/window.py --- a/tuikit/core/window.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/core/window.py Sun Feb 22 09:53:13 2015 +0100 @@ -41,15 +41,24 @@ self.buffer.resize(w, h) self.redraw() - def redraw(self): - self.buffer.reset_origin() - Container.draw(self, self.buffer) -# self.buffer.puts('{0.w} {0.h}'.format(self.size), 10, 5) -# self.buffer.frame() + def draw_content_if_needed(self): + """Draw window contents into its own buffer.""" + buffer = self.buffer + buffer.reset_origin() + for child in self._widgets: + with buffer.moved_origin(child.x, child.y): + with buffer.clip(buffer.origin.x, buffer.origin.y, + child.width, child.height): + child.draw_if_needed(buffer) + + def draw_if_needed(self, buffer): + """Refresh window content and draw it into `buffer` if required.""" + self.draw_content_if_needed() + # Draw window unconditionally (for now) + self.draw(buffer) def draw(self, buffer): - """Draw this window into `buffer`.""" - self.redraw() + """Draw window buffer into given`buffer`.""" buffer.draw(self.buffer) diff -r 8c7970520632 -r dd91747504dd tuikit/driver/cursesw.py --- a/tuikit/driver/cursesw.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/driver/cursesw.py Sun Feb 22 09:53:13 2015 +0100 @@ -170,6 +170,8 @@ List of Event objects. """ + res = [] + # Set timeout if timeout is None: # wait indefinitely @@ -183,9 +185,11 @@ self.stdscr.nodelay(1) # Get key or char - c = self.stdscr.get_wch() - - res = [] + try: + c = self.stdscr.get_wch() + except curses.error: + # No input in nodelay mode, or timeout + return res if c == -1: # Timeout diff -r 8c7970520632 -r dd91747504dd tuikit/widgets/button.py --- a/tuikit/widgets/button.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/widgets/button.py Sun Feb 22 09:53:13 2015 +0100 @@ -44,8 +44,8 @@ def set_theme(self, theme): Widget.set_theme(self, theme) - self.color = theme.button - self.color_active = theme.button_active + self.color = theme.clr_button + self.color_active = theme.clr_button_active def _get_color(self): if self.highlight: @@ -76,11 +76,11 @@ def mousedown_event(self, ev): self.highlight = True - #self.redraw() + self.redraw() def mouseup_event(self, ev): self.highlight = False - #self.redraw() + self.redraw() self.sig_clicked() def keypress_event(self, ev): diff -r 8c7970520632 -r dd91747504dd tuikit/widgets/label.py --- a/tuikit/widgets/label.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/widgets/label.py Sun Feb 22 09:53:13 2015 +0100 @@ -10,7 +10,7 @@ self.color = 'default' def set_theme(self, theme): - self.color = self.theme.normal + self.color = self.theme.clr_normal def draw(self, buffer): with buffer.attr(self.color): diff -r 8c7970520632 -r dd91747504dd tuikit/widgets/scrollbar.py --- a/tuikit/widgets/scrollbar.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/widgets/scrollbar.py Sun Feb 22 09:53:13 2015 +0100 @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from tuikit.core.widget import Widget from tuikit.core.signal import Signal @@ -8,11 +6,11 @@ """Abstract base class for scrollbars.""" - def __init__(self): + def __init__(self, scroll_max): Widget.__init__(self) # Scrolling range is 0 .. _scroll_max - self._scroll_max = self._get_length() - 3 + self._scroll_max = scroll_max # Current position of scrollbar in scrolling range. self._scroll_pos = 0 # Current position of thumb on scrollbar - used for draw. @@ -66,12 +64,14 @@ if self._scroll_pos > 0: self.scroll_pos = self._scroll_pos - 1 self._move = 'up' + self.redraw() def move_down(self): """Move scrolling position down/right.""" if self._scroll_pos < self._scroll_max: self.scroll_pos = self._scroll_pos + 1 self._move = 'down' + self.redraw() def drag(self, mouse_pos): """Scroll using mouse drag on thumb. @@ -109,7 +109,7 @@ self.move_up() if self._move == 'down': self.move_down() - self.add_timeout(self.scroll_interval, self._continuous_scroll) + self.timer.add_timeout(self.scroll_interval, self._continuous_scroll) class VScrollbar(Scrollbar): @@ -137,7 +137,7 @@ self.move_up() else: self.move_down() - self.add_timeout(self.scroll_delay, self._continuous_scroll) + self.timer.add_timeout(self.scroll_delay, self._continuous_scroll) return # thumb bar if ev.wy == 1 + self._thumb_pos: @@ -154,7 +154,7 @@ self._dragging = False return if self._move: - self.remove_timeout(self._continuous_scroll) + self.timer.remove_timeout(self._continuous_scroll) self._move = None return @@ -163,50 +163,48 @@ class HScrollbar(Scrollbar): - def __init__(self): - Scrollbar.__init__(self) - self.sizereq.update(20, 1) + def __init__(self, length=20): + Scrollbar.__init__(self, length - 3) + self.sizereq.update(length, 1) - def on_draw(self, ev): - ug = ev.driver.unigraph - ev.driver.pushcolor('normal') - ev.driver.putch(ev.x, ev.y, ug.get_char('sb_left')) - for i in range(1, self.width - 1): - ev.driver.putch(ev.x + i, ev.y, ug.get_char('sb_htrack')) - ev.driver.putch(ev.x + 1 + self._thumb_pos, ev.y, ug.get_char('sb_thumb')) - ev.driver.putch(ev.x + self.width - 1, ev.y, ug.get_char('sb_right')) - ev.driver.popcolor() + def draw(self, buffer): + Widget.draw(self, buffer) + with buffer.attr(self.theme.clr_normal): + buffer.putch(self.theme.sb_left) + for i in range(1, self.width - 1): + buffer.putch(self.theme.sb_htrack, i) + buffer.putch(self.theme.sb_thumb, 1 + self._thumb_pos) + buffer.putch(self.theme.sb_right, self.width - 1) - def on_mousedown(self, ev): + def mousedown_event(self, ev): self._dragging = False self._move = None # arrow buttons - if ev.wx == 0 or ev.wx == self.width - 1: - if ev.wx == 0: + if ev.pos.x == 0 or ev.pos.x == self.width - 1: + if ev.pos.x == 0: self.move_up() else: self.move_down() - self.add_timeout(self.scroll_delay, self._continuous_scroll) + self.timer.add_timeout(self.scroll_delay, self._continuous_scroll) return # thumb bar - if ev.wx == 1 + self._thumb_pos: + if ev.pos.x == 1 + self._thumb_pos: self._dragging = True return - def on_mousemove(self, ev): + def mousemove_event(self, ev): if self._dragging: - self.drag(ev.wx) + self.drag(ev.pos.x) - def on_mouseup(self, ev): + def mouseup_event(self, ev): if self._dragging: - self.drag(ev.wx) + self.drag(ev.pos.x) self._dragging = False return if self._move: - self.remove_timeout(self._continuous_scroll) + self.timer.remove_timeout(self._continuous_scroll) self._move = None return def _get_length(self): return self.width - diff -r 8c7970520632 -r dd91747504dd tuikit/widgets/textbox.py --- a/tuikit/widgets/textbox.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/widgets/textbox.py Sun Feb 22 09:53:13 2015 +0100 @@ -44,7 +44,7 @@ def draw(self, buffer): exposed = self.exposed(buffer) - with buffer.attr(self.theme.normal): + with buffer.attr(self.theme.clr_normal): buffer.fill() end_y = min(len(self._lines), exposed.y + exposed.h) for j in range(exposed.y, end_y): diff -r 8c7970520632 -r dd91747504dd tuikit/widgets/textfield.py --- a/tuikit/widgets/textfield.py Sat Feb 21 12:01:57 2015 +0100 +++ b/tuikit/widgets/textfield.py Sun Feb 22 09:53:13 2015 +0100 @@ -30,7 +30,7 @@ self.ofs = self.curspos - self.tw def draw(self, buffer): - color = self.theme.active if self.has_focus() else self.theme.normal + color = self.theme.clr_active if self.has_focus() else self.theme.clr_normal with buffer.attr(color): # draw value val = self.value + ' ' * self.tw # add spaces to fill rest of field