Redraw widgets on request. Add scrollbar demo. default tip api1
authorRadek Brich <radek.brich@devl.cz>
Sun, 22 Feb 2015 09:53:13 +0100
changeset 119 dd91747504dd
parent 118 8c7970520632
Redraw widgets on request. Add scrollbar demo.
demos/03_event.py
demos/15_scrollbar.py
tuikit/core/application.py
tuikit/core/container.py
tuikit/core/coords.py
tuikit/core/theme.py
tuikit/core/widget.py
tuikit/core/window.py
tuikit/driver/cursesw.py
tuikit/widgets/button.py
tuikit/widgets/label.py
tuikit/widgets/scrollbar.py
tuikit/widgets/textbox.py
tuikit/widgets/textfield.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
--- /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()
--- 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()
 
--- 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()
--- 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)
--- 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:
--- 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):
--- 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)
 
 
--- 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
--- 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):
--- 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):
--- 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
-
--- 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):
--- 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