# HG changeset patch # User Radek Brich # Date 1409764101 -7200 # Node ID 105b1affc3c24eea1b17b632e9ebcb0311f54d46 # Parent 0c2e0c09ba5cb2dcb4e436a99f4376d1405d5679 Update keypress propagation. Allow focus change by tab key. Add log property to Widget for smart logging. diff -r 0c2e0c09ba5c -r 105b1affc3c2 demos/03_application.py --- 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() + diff -r 0c2e0c09ba5c -r 105b1affc3c2 tuikit/core/container.py --- 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) diff -r 0c2e0c09ba5c -r 105b1affc3c2 tuikit/core/signal.py --- 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: diff -r 0c2e0c09ba5c -r 105b1affc3c2 tuikit/core/theme.py --- 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' diff -r 0c2e0c09ba5c -r 105b1affc3c2 tuikit/core/widget.py --- 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) diff -r 0c2e0c09ba5c -r 105b1affc3c2 tuikit/core/window.py --- 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 + diff -r 0c2e0c09ba5c -r 105b1affc3c2 tuikit/driver/curses.py --- 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< 1 or (self.ofs == 0 and self.curspos == 1):