Redraw widgets on request. Add scrollbar demo.
--- 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