Update keypress propagation. Allow focus change by tab key. Add log property to Widget for smart logging.
--- a/demos/03_application.py Fri Mar 28 19:58:59 2014 +0100
+++ b/demos/03_application.py Wed Sep 03 19:08:21 2014 +0200
@@ -10,15 +10,26 @@
label = Label('Hello there!')
label.pos.update(20, 10)
-button = Button()
-button.pos.update(20, 20)
+button1 = Button()
+button1.pos.update(20, 20)
+button2 = Button()
+button2.pos.update(30, 20)
field = TextField('text field')
field.pos.update(20, 30)
app = Application()
app.root_window.add(label)
-app.root_window.add(button)
+app.root_window.add(button1)
+app.root_window.add(button2)
app.root_window.add(field)
-app.root_window.focus_child = field
+app.root_window.focus_widget = field
+
+def on_keypress(keyname, char, mod):
+ if keyname == 'escape':
+ app.stop()
+
+app.window_manager.sig_keypress.connect(on_keypress)
+
app.start()
+
--- a/tuikit/core/container.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/core/container.py Wed Sep 03 19:08:21 2014 +0200
@@ -14,19 +14,22 @@
def __init__(self, layout_class=FixedLayout):
Widget.__init__(self)
#: List of child widgets.
- self.children = []
- self.focus_child = None
+ self._widgets = []
+ #: Widget with keyboard focus
+ self.focus_widget = None
+ #: If True, tab cycles inside container
+ self.trap_focus = False
self.layout = layout_class()
def add(self, widget):
"""Add widget into container."""
- self.children.append(widget)
+ self._widgets.append(widget)
widget.parent = self
widget.window = self.window
widget.set_theme(self.theme)
self.layout.add(widget)
- if self.focus_child is None:
- self.focus_child = widget
+ if self.focus_widget is None and widget.can_focus():
+ self.focus_widget = widget
def resize(self, w, h):
Widget.resize(self, w, h)
@@ -35,7 +38,7 @@
def draw(self, buffer):
"""Draw child widgets."""
Widget.draw(self, buffer)
- for child in self.children:
+ 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):
@@ -43,21 +46,75 @@
def set_theme(self, theme):
Widget.set_theme(self, theme)
- for child in self.children:
+ for child in self._widgets:
child.set_theme(theme)
@property
def cursor(self):
- if self.focus_child:
- cursor = self.focus_child.cursor
+ if self.focus_widget:
+ cursor = self.focus_widget.cursor
if cursor is not None:
- return cursor.moved(*self.focus_child.pos)
+ return cursor.moved(*self.focus_widget.pos)
else:
if self._cursor is not None:
return Point(self._cursor)
## input events ##
- def keypress(self, keyname, char, mod=0):
- if self.focus_child:
- self.focus_child.keypress(keyname, char, mod)
+ def keypress(self, keyname, char, mod):
+ # First, handle the keypress event to focused child widget
+ if self.focus_widget is not None:
+ if self.focus_widget.keypress(keyname, char, mod):
+ return True
+ # Next, handle default key behaviour by Container
+ if keyname == 'tab':
+ return self.focus_next(-1 if 'shift' in mod else 1)
+ # Finally, handle default keys by Widget
+ # and send keypress signal
+ if Widget.keypress(self, keyname, char, mod):
+ return True
+
+ ## focus ##
+
+ def focus_next(self, step=1):
+ """Focus next child.
+
+ Sets focus to next child, if there is one
+ which can be focused. Cycles from last child
+ to first when needed. Return value depends on
+ this cycling:
+
+ * False means there wasn't any child to focus
+ before end of list. Focus was either not changed
+ or first child was focused.
+
+ * True when focus is set to next child in normal
+ way or when self.trap_focus is set.
+
+ Return value is supposed to be returned from keypress
+ event - in that case, True stops event propagation.
+
+ """
+ if self.focus_widget is None:
+ idx_current = 0
+ else:
+ idx_current = self._widgets.index(self.focus_widget)
+ idx_new = idx_current
+ cycled = False
+ while True:
+ idx_new += step
+ if idx_new >= len(self._widgets):
+ idx_new = 0
+ cycled = True
+ if idx_new < 0: # for focus_previous
+ idx_new = len(self._widgets) - 1
+ cycled = True
+ if idx_current == idx_new:
+ return False
+ if self._widgets[idx_new].can_focus():
+ self.focus_widget = self._widgets[idx_new]
+ return self.trap_focus or not cycled
+
+ def focus_previous(self):
+ """Focus previous child."""
+ self.focus_next(-1)
--- a/tuikit/core/signal.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/core/signal.py Wed Sep 03 19:08:21 2014 +0200
@@ -15,13 +15,19 @@
"""
- def __init__(self):
+ def __init__(self, allow_stop=False):
self._handlers = []
+ #: Allow one of the handlers to stop processing signal
+ #: The handler should return True value,
+ #: then other handlers will not be called
+ self.allow_stop = allow_stop
def __call__(self, *args, **kwargs):
"""Emit the signal to all connected handlers."""
for handler in self._handlers:
- handler(*args, **kwargs)
+ res = handler(*args, **kwargs)
+ if self.allow_stop and res:
+ return True
def connect(self, handler):
if not handler in self._handlers:
--- a/tuikit/core/theme.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/core/theme.py Wed Sep 03 19:08:21 2014 +0200
@@ -6,6 +6,7 @@
"""Default color style"""
normal = 'lightgray'
+ active = 'black on cyan'
button = 'black on lightgray'
button_active = 'black on cyan'
--- a/tuikit/core/widget.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/core/widget.py Wed Sep 03 19:08:21 2014 +0200
@@ -1,5 +1,6 @@
from tuikit.core.coords import Point, Size
from tuikit.core.theme import default_theme
+from tuikit.core.signal import Signal
import logging
@@ -12,7 +13,6 @@
def __init__(self):
self._num_instances += 1
- self._log = logging.getLogger(__name__)
#: Widget name is used for logging etc. Not visible anywhere.
self.name = '{}{}'.format(
@@ -44,6 +44,13 @@
#: None means no cursor (hidden).
self._cursor = None
+ #: Hidden widget does not affect layout.
+ self.hidden = False
+ #: Allow keyboard focus for this widget.
+ self.allow_focus = False
+
+ self.sig_keypress = Signal(allow_stop=True)
+
## position and size ##
@property
@@ -73,8 +80,8 @@
def draw(self, buffer):
"""Draw self into buffer."""
- self._log.debug('%s draw into %r at %s (exposed %s)',
- self.name, buffer, buffer.origin, self.exposed(buffer))
+ self.log.debug('Draw into %r at %s (exposed %s)',
+ buffer, buffer.origin, self.exposed(buffer))
def set_theme(self, theme):
self.theme = theme
@@ -99,8 +106,19 @@
## input events ##
def keypress(self, keyname, char, mod):
- self._log.debug('%s keypress(%r, %r, %r)',
- self.name, keyname, char, mod)
+ """Keypress event handler.
+
+ Override to accept keyboard input.
+
+ Returns True if event was consumed.
+
+ Call this implementation from inherited classes
+ if it does not consume the event.
+
+ """
+ if self.sig_keypress(keyname, char, mod):
+ return True
+ self.log.debug('Not consumed: keypress(%r, %r, %r)', keyname, char, mod)
## timeouts ##
@@ -115,3 +133,20 @@
"""
self.parent.remove_timeout(self, callback, *args)
+
+ ## 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.focus_widget == self)
+
+ ## utilities ##
+
+ @property
+ def log(self):
+ return logging.getLogger('tuikit.' + self.name)
--- a/tuikit/core/window.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/core/window.py Wed Sep 03 19:08:21 2014 +0200
@@ -17,6 +17,8 @@
"""New buffer for the window will be created unless given existing
`buffer` as parameter."""
Container.__init__(self)
+ self.allow_focus = True
+ self.trap_focus = True
self._buffer = None
self.buffer = buffer or Buffer()
@@ -57,8 +59,16 @@
Container.__init__(self)
self.timer = timer
+ def draw(self, buffer):
+ Container.draw(self, buffer)
+ self.log.debug('%s has focus.', self.get_focused_widget().name)
+
def resize(self, w, h):
- self.children[0].resize(w, h)
+ self._widgets[0].resize(w, h)
+
+ def keypress(self, keyname, char, mod):
+ self.log.debug('keypress(%r, %r, %r)', keyname, char, mod)
+ return Container.keypress(self, keyname, char, mod)
def handle_event(self, event_name, *args):
"""Handle input event to managed windows."""
@@ -67,3 +77,12 @@
handler(*args)
else:
raise Exception('Unknown event: %r %r' % (event_name, args))
+
+ def get_focused_widget(self):
+ """Traverse the widget hierarchy to bottom
+ and return actually focused Widget."""
+ node = self
+ while isinstance(node, Container) and node.focus_widget:
+ node = node.focus_widget
+ return node
+
--- a/tuikit/driver/curses.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/driver/curses.py Wed Sep 03 19:08:21 2014 +0200
@@ -43,6 +43,7 @@
(0x44, 1, 'left' ),
(0x46, 1, 'end' ), # xterm
(0x48, 1, 'home' ), # xterm
+ (0x5a, 1, 'shift+tab' ), # xterm
(0x5b, 0x41, 1, 'f1' ), # linux
(0x5b, 0x42, 1, 'f2' ), # linux
(0x5b, 0x43, 1, 'f3' ), # linux
@@ -79,7 +80,7 @@
def __init__(self):
Driver.__init__(self)
- self._log = logging.getLogger('tuikit')
+ self._log = logging.getLogger(__name__)
self.stdscr = None
self.cursor = None
self.colors = {} # maps names to curses attributes
@@ -378,7 +379,7 @@
keyname = match[-1]
if match is None:
- self.log.debug('Unknown control sequence: %s',
+ self._log.debug('Unknown control sequence: %s',
','.join(['0x%x' % x for x in consumed]))
return [('keypress', 'Unknown', None, set())]
@@ -460,7 +461,7 @@
if len(codes) == 0:
# no match -> unknown code
seq = ','.join(['0x%x' % x for x in debug_seq])
- self.log.debug('Unknown control sequence: %s', seq)
+ self._log.debug('Unknown control sequence: %s', seq)
return [('keypress', 'Unknown:' + seq, None, set())]
elif len(codes) == 1:
# one match -> we got the winner
@@ -483,7 +484,7 @@
if len(matching_codes) == 0:
# no match -> unknown code
seq = ','.join(['0x%x' % x for x in debug_seq])
- self.log.debug('Unknown control sequence: %s', seq)
+ self._log.debug('Unknown control sequence: %s', seq)
return [('keypress', 'Unknown:' + seq, None, set())]
if len(matching_codes) > 1:
@@ -502,6 +503,14 @@
if mod_bits & 1<<bit:
mod_set.add(name)
+ # parse keynames in form "shift+tab"
+ if '+' in keyname:
+ parts = keyname.split('+')
+ for mod in parts[:-1]:
+ assert(mod in ('shift', 'alt', 'ctrl', 'meta'))
+ mod_set.add(mod)
+ keyname = parts[-1]
+
return [('keypress', keyname, None, mod_set)]
--- a/tuikit/widgets/button.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/widgets/button.py Wed Sep 03 19:08:21 2014 +0200
@@ -9,6 +9,7 @@
def __init__(self, label='btn'):
"""Create button with given label, size according to label."""
Widget.__init__(self)
+ self.allow_focus = True
#: Button label.
self._label = ''
@@ -21,10 +22,8 @@
#: Padding between prefix/suffix and label
self.padding = 1
- self.allow_focus = True
-
self.color = 'default'
- self.color_highlighted = 'default on red'
+ self.color_active = 'default on red'
self.highlight = False
self.sig_clicked = Signal()
@@ -45,11 +44,11 @@
def set_theme(self, theme):
Widget.set_theme(self, theme)
self.color = theme.button
- self.color_highlighted = theme.button_active
+ self.color_active = theme.button_active
def _get_color(self):
- if self.highlight: # or self.has_focus():
- return self.color_highlighted
+ if self.has_focus():
+ return self.color_active
return self.color
def draw(self, buffer):
--- a/tuikit/widgets/textbox.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/widgets/textbox.py Wed Sep 03 19:08:21 2014 +0200
@@ -12,13 +12,12 @@
def __init__(self, text=''):
Widget.__init__(self)
+ self.allow_focus = True
# Text content, splitted as lines
self._lines = []
self.text = text
- self.allow_focus = True
-
# Cursor position is same as spot.
# This variable rememberes horizontal position
# for the case when cursor moves to shorter line.
--- a/tuikit/widgets/textfield.py Fri Mar 28 19:58:59 2014 +0100
+++ b/tuikit/widgets/textfield.py Wed Sep 03 19:08:21 2014 +0200
@@ -7,10 +7,9 @@
def __init__(self, value=''):
Widget.__init__(self)
+ self.allow_focus = True
self.sizereq.update(10, 1)
- self.allow_focus = True
-
self.code = locale.getpreferredencoding()
if not isinstance(value, str):
value = str(value, self.code)
@@ -25,11 +24,9 @@
def resize(self, w, h):
self.tw = self.width - 2
- def set_theme(self, theme):
- self.color = theme.normal
-
def draw(self, buffer):
- with buffer.attr(self.color):
+ color = self.theme.active if self.has_focus() else self.theme.normal
+ with buffer.attr(color):
# draw value
val = self.value + ' ' * self.tw # add spaces to fill rest of field
val = val[self.ofs : self.ofs + self.tw] # cut value - begin from ofs, limit to tw chars
@@ -49,8 +46,7 @@
self._cursor = (1 + self.curspos - self.ofs, 0)
def keypress(self, keyname, char, mod=0):
- Widget.keypress(self, keyname, char, mod)
- accepted = True
+ consumed = True
if keyname == 'left':
self.move_left()
elif keyname == 'right':
@@ -62,16 +58,17 @@
elif keyname == 'delete':
self.del_char()
else:
- accepted = False
+ consumed = False
if char:
self.add_char(char)
self.move_right()
- accepted = True
+ consumed = True
- #if accepted:
+ if consumed:
#self.redraw()
- return accepted
+ return True
+ Widget.keypress(self, keyname, char, mod)
def move_left(self):
if self.curspos - self.ofs > 1 or (self.ofs == 0 and self.curspos == 1):