Rework MenuBar. Add MenuButton. Add mouse event cascading to floaters.
authorRadek Brich <radek.brich@devl.cz>
Sat, 19 Jan 2013 13:05:21 +0100
changeset 63 2a0e04091898
parent 62 2f61931520c9
child 64 03f591f5fe5c
Rework MenuBar. Add MenuButton. Add mouse event cascading to floaters. LinearLayout: spacing now applies to all children, not just those with expand. Fix Window resize request inside layouts. UnicodeGraphics: prepare for styling/theming.
demo_layout.py
demo_menu.py
tests/make_select.py
tuikit/__init__.py
tuikit/application.py
tuikit/common.py
tuikit/container.py
tuikit/events.py
tuikit/layout.py
tuikit/menu.py
tuikit/menubar.py
tuikit/scrollbar.py
tuikit/widget.py
--- a/demo_layout.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/demo_layout.py	Sat Jan 19 13:05:21 2013 +0100
@@ -17,6 +17,7 @@
 
         self._row_num = 0
         self.buildrow()
+        self.buildrow(spacing=1)
         self.buildrow(expand=True)
         self.buildrow(expand=True, fill=True)
         self.buildrow(homogeneous=True)
--- a/demo_menu.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/demo_menu.py	Sat Jan 19 13:05:21 2013 +0100
@@ -4,28 +4,22 @@
 import locale
 locale.setlocale(locale.LC_ALL, '')
 
-from tuikit import Application, MenuBar, Menu, Window, VerticalLayout
+from tuikit import Application, MenuBar, MenuButton, Menu, Window
 
 
 class MyApplication(Application):
     def __init__(self):
         Application.__init__(self)
-        self.top.add_handler('keypress', self.on_top_keypress)
-
-        menubar = MenuBar()
-        self.top.add(menubar)
+        self.top.add_handler('keypress', self.on_top_keypress, last=True)
 
         helpwin = Window()
+        helpwin.title = 'About'
+        helpwin.hide()
         self.top.add(helpwin)
-        helpwin.x = 10
-        helpwin.y = 5
-        helpwin.allow_layout = False
-        helpwin.hidden = True
-        helpwin.title = 'About'
+        helpwin.move(10, 5)
         #helpwin.closebutton = False
         #helpwin.resizable = False
 
-
         filemenu = Menu([
             ('New', None),
             None,
@@ -34,21 +28,21 @@
             None,
             ('Quit', self.terminate),
             ])
-        self.top.add(filemenu)
-
         editmenu = Menu([('Copy', None), ('Paste', None)])
         helpmenu = Menu([('About', helpwin)])
 
-        self.top.add(editmenu)
-        self.top.add(helpmenu)
-
-        menubar.setitems([
+        menubar_items = [
             ('File', filemenu),
             ('Edit', editmenu),
             ('Help', helpmenu),
-            ])
+            ]
 
-        self.top.layout = VerticalLayout()
+        menubar = MenuBar(menubar_items)
+        self.top.add(menubar, halign='fill')
+
+        menu = Menu([('Copy', None), ('Paste', None)])
+        menubtn = MenuButton('MenuButton', menu)
+        self.top.add(menubtn, halign='center', valign='center')
 
     def on_top_keypress(self, ev):
         if ev.keyname == 'escape':
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/make_select.py	Sat Jan 19 13:05:21 2013 +0100
@@ -0,0 +1,24 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+import sys
+sys.path.append('..')
+
+from tuikit.common import *
+
+XSel = make_select('left', 'right', 'bottom', 'top')
+print(XSel)
+
+sel = XSel()
+print('XSel().selected:', sel.selected)
+
+try:
+    sel.update('center')
+except ValueError as e:
+    print('Error:', e)
+
+try:
+    pure_sel = Select()
+except TypeError as e:
+    print('Error:', e)
+
--- a/tuikit/__init__.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/__init__.py	Sat Jan 19 13:05:21 2013 +0100
@@ -11,7 +11,7 @@
 from tuikit.label import Label
 from tuikit.layout import VerticalLayout, HorizontalLayout, GridLayout, AnchorLayout
 from tuikit.menu import Menu
-from tuikit.menubar import MenuBar
+from tuikit.menubar import MenuBar, MenuButton
 from tuikit.pager import Pager
 from tuikit.scrollbar import VScrollbar
 from tuikit.scrollview import ScrollView
--- a/tuikit/application.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/application.py	Sat Jan 19 13:05:21 2013 +0100
@@ -58,7 +58,7 @@
     def __init__(self, top_layout=AnchorLayout, driver='curses'):
         '''Create application.'''
         self._setup_logging()
-        
+
         # Top widget
         self._top = None
         self._timer = Timer()
--- a/tuikit/common.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/common.py	Sat Jan 19 13:05:21 2013 +0100
@@ -274,3 +274,17 @@
     LTEE = '├'
     RTEE = '┤'
 
+    char_map = {
+        # scrollbar
+        'sb_thumb'  : CIRCLE,
+        'sb_htrack' : LINE[8],
+        'sb_vtrack' : LINE[10],
+        'sb_left'   : LEFT_ARROW,
+        'sb_right'  : RIGHT_ARROW,
+        'sb_up'     : UP_ARROW,
+        'sb_down'   : DOWN_ARROW,
+    }
+
+    def get_char(self, name):
+        return self.char_map[name]
+
--- a/tuikit/container.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/container.py	Sat Jan 19 13:05:21 2013 +0100
@@ -50,7 +50,7 @@
             except AttributeError:
                 widget.hints[key] = widget.hints[key].__class__(kwargs[key])
         if self.focuschild is None and widget.can_focus():
-            self.focuschild = widget
+            widget.set_focus()
 
     def add_floater(self, widget, **kwargs):
         widget.floater = True
@@ -81,7 +81,7 @@
             child.top = value
 
 
-    def focus_next(self):
+    def focus_next(self, step=1):
         """Focus next child.
 
         Sets focus to next child, if there is one
@@ -104,16 +104,31 @@
         idx_new = idx_current
         cycled = False
         while True:
-            idx_new += 1
+            idx_new += step
             if idx_new >= len(self.children):
                 idx_new = 0
                 cycled = True
+            if idx_new < 0: # for focus_previous
+                idx_new = len(self.children) - 1
+                cycled = True
             if idx_current == idx_new:
                 return False
             if self.children[idx_new].can_focus():
                 self.children[idx_new].set_focus()
                 return self.trap_focus or not cycled
 
+    def focus_previous(self):
+        """Focus previous child."""
+        self.focus_next(-1)
+
+    def on_focus(self, ev):
+        if self.focuschild:
+            self.focuschild.emit('focus', ev)
+
+    def on_unfocus(self, ev):
+        if self.focuschild:
+            self.focuschild.emit('unfocus', ev)
+
     def draw(self, driver, x, y):
         """Draw the container and its children.
 
@@ -149,8 +164,8 @@
         driver.clipstack.pop()
 
     def draw_floaters(self, driver, x, y):
-        logging.getLogger('tuikit').info('draw_floaters %s %r %r', self, x ,y )
-        # draw floaters in this container
+        #logging.getLogger('tuikit').info('draw_floaters %s %r %r', self, x ,y )
+        # draw our floaters
         for child in [ch for ch in self.children if ch.floater]:
             child.draw(driver,
                 x + self.offset.x + child.x,
@@ -166,23 +181,14 @@
             child.emit('resize')
 
     def on_keypress(self, ev):
-        if self.focuschild is not None:
-            handled = self.focuschild.emit('keypress', ev)
-            if handled:
+        if self.focuschild is not None and not self.focuschild.hidden:
+            if self.focuschild.emit('keypress', ev):
                 return True
         if ev.keyname == 'tab':
             return self.focus_next()
 
     def on_mousedown(self, ev):
-        handled = False
-        for child in reversed(self.children):
-            if child.enclose(ev.wx - self.offset.x, ev.wy - self.offset.y):
-                child_ev = ev.make_child_event(self, child)
-                child.emit('mousedown', child_ev)
-                self.mousechild = child
-                handled = True
-                break
-        return handled
+        self.cascade_mouse_event(ev)
 
     def on_mouseup(self, ev):
         if self.mousechild:
@@ -198,12 +204,47 @@
             return True
 
     def on_mousewheel(self, ev):
-        handled = False
-        for child in reversed(self.children):
+        self.cascade_mouse_event(ev)
+
+    def cascade_mouse_event(self, ev):
+        """Resend mouse event to child under cursor.
+
+        Args:
+            ev: Original mouse event. Event type and cursor
+                position is extracted from this.
+
+        Returns:
+            Boolean value. True when event was handled by a child.
+
+        """
+        if self.parent is None:
+            if self.floaters_mouse_event(ev):
+                return True
+        for child in reversed([ch for ch in self.children if not ch.floater]):
             if child.enclose(ev.wx - self.offset.x, ev.wy - self.offset.y):
                 child_ev = ev.make_child_event(self, child)
-                child.emit('mousewheel', child_ev)
-                handled = True
-                break
-        return handled
+                child.emit(ev.event_name, child_ev)
+                if ev.event_name == 'mousedown':
+                    self.mousechild = child
+                return True
+        return False
 
+    def floaters_mouse_event(self, ev):
+        """Resend mouse event to our floaters and any floaters in child containers."""
+        # delve into child containers, test their floaters
+        for child in reversed([ch for ch in self.children \
+                               if not ch.floater and isinstance(ch, Container)]):
+            child_ev = ev.make_child_event(self, child)
+            if child.floaters_mouse_event(child_ev):
+                if ev.event_name == 'mousedown':
+                    self.mousechild = child
+                return True
+        # test our floaters
+        for child in reversed([ch for ch in self.children if ch.floater]):
+            if child.enclose(ev.wx - self.offset.x, ev.wy - self.offset.y):
+                child_ev = ev.make_child_event(self, child)
+                child.emit(ev.event_name, child_ev)
+                if ev.event_name == 'mousedown':
+                    self.mousechild = child
+                return True
+
--- a/tuikit/events.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/events.py	Sat Jan 19 13:05:21 2013 +0100
@@ -71,6 +71,8 @@
 
     def make_child_event(self, container, child):
         ev = MouseEvent(self.x, self.y, self.button)
+        ev.originator = self.originator
+        ev.event_name = self.event_name
         # original local coordinates are new parent coordinates
         ev.px = self.wx
         ev.py = self.wy
--- a/tuikit/layout.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/layout.py	Sat Jan 19 13:05:21 2013 +0100
@@ -15,6 +15,13 @@
 
 
 class Layout(Container):
+    def add(self, widget, **kwargs):
+        Container.add(self, widget, **kwargs)
+        widget.add_handler('sizereq', self.on_child_sizereq)
+
+    def on_child_sizereq(self, ev):
+        self.emit('resize')
+
     def _get_children(self):
         return [child for child in self.children
             if not child.floater and not child.hidden]
@@ -146,9 +153,10 @@
             size[ax1] = max(child.sizereq[ax1], child.sizemin[ax1])
             size[ax2] = space2
 
+            offset += self.spacing
             if child.hints['expand'] or self.homogeneous:
                 maxsize = int(round(space_child + math.modf(offset)[0], 2))
-                offset += space_child + self.spacing
+                offset += space_child
                 if not self.homogeneous:
                     maxsize += child.sizereq[ax1]
                     offset += child.sizereq[ax1]
--- a/tuikit/menu.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/menu.py	Sat Jan 19 13:05:21 2013 +0100
@@ -18,12 +18,11 @@
         self.highlight = 'menu-active'
         self.items = items
         self.selected = items[0]
-        self.menubar = None
 
         self.add_events('activate', GenericEvent)
 
     def on_draw(self, ev):
-        logging.getLogger('tuikit').info('menu draw %s %s %s', ev, ev.x, ev.y)
+        #logging.getLogger('tuikit').info('menu draw %s %s %s', ev, ev.x, ev.y)
         ev.driver.pushcolor(self.bg)
         ev.driver.frame(ev.x, ev.y, self.width, self.height)
         i = 1
@@ -46,10 +45,13 @@
     def on_keypress(self, ev):
         if ev.keyname == 'up':
             self.move_selected(-1)
-        if ev.keyname == 'down':
+            return True
+        if ev.keyname in ('down', 'tab'):
             self.move_selected(+1)
+            return True
         if ev.keyname == 'enter':
             self.run_selected()
+            return True
         self.redraw()
 
     def on_mousedown(self, ev):
--- a/tuikit/menubar.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/menubar.py	Sat Jan 19 13:05:21 2013 +0100
@@ -1,101 +1,82 @@
 # -*- coding: utf-8 -*-
 
-from tuikit.widget import Widget
+from tuikit.layout import HorizontalLayout
+from tuikit.button import Button
+from tuikit.container import Container
 
 
-class MenuBar(Widget):
+class MenuButton(Container, Button):
+    def __init__(self, label='', menu=None):
+        Container.__init__(self)
+        Button.__init__(self, label)
+        if menu:
+            self.menu = menu
+
+    @property
+    def menu(self):
+        return self.children[0]
+
+    @menu.setter
+    def menu(self, widget):
+        self.add_floater(widget)
+        widget.move(0, 1)
+        widget.hide()
+
+    def on_click(self, ev):
+        self.menu.show()
+
+    def on_unfocus(self, ev):
+        self.menu.hide()
+
+    def show_menu(self):
+        if not self.menu.hidden:
+            return False
+        self.menu.show()
+        return True
+
+
+class MenuBar(HorizontalLayout):
     def __init__(self, items = []):
-        Widget.__init__(self, 0, 1)
-
+        HorizontalLayout.__init__(self, spacing=4)
+        self._default_size.update(20, 1)
         self.allow_focus = True
 
         self.bg = 'menu'
         self.highlight = 'menu-active'
 
-        self.setitems(items)
-        self.selected = None
-
-    def setitems(self, items):
-        self.items = items
+        for title, widget in items:
+            self.add_menu(title, widget)
 
-        i = 0
-        for item in self.items:
-            if isinstance(item[1], Widget):
-                item[1].x = i
-                item[1].y = self.y + 1
-                item[1].allow_layout = False
-                item[1].hidden = True
-                item[1].add_handler('focus', self.on_submenu_focus)
-                item[1].menubar = self
-            i += len(item[0]) + 4
+    def add_menu(self, title, widget):
+        btn = MenuButton(title)
+        btn.menu = widget
+        btn.bg = self.bg
+        btn.bghi = self.highlight
+        btn.prefix = ''
+        btn.suffix = ''
+        self.add(btn)
 
     def on_draw(self, ev):
         ev.driver.pushcolor(self.bg)
-        i = 0
-        for item in self.items:
-            if self.selected == item:
-                ev.driver.pushcolor(self.highlight)
-                ev.driver.puts(ev.x + i, ev.y, '  ' + item[0] + '  ')
-                ev.driver.popcolor()
-            else:
-                ev.driver.puts(ev.x + i, ev.y, '  ' + item[0] + '  ')
-            i += len(item[0]) + 4
-        if i < self.width:
-            ev.driver.puts(ev.x + i, ev.y, ' ' * (self.width - i))
+        ev.driver.puts(ev.x, ev.y, ' ' * self.width)
         ev.driver.popcolor()
 
-
     def on_keypress(self, ev):
+        # do we have some menu dropped down?
+        menu_visible = not self.focuschild.menu.hidden
         if ev.keyname == 'left':
-            self.move_selected(-1)
+            res = self.focus_previous()
+            if menu_visible:
+                return self.focuschild.show_menu()
+            return res
         elif ev.keyname == 'right':
-            self.move_selected(+1)
-        else:
-            if self.selected:
-                if isinstance(self.selected[1], Widget):
-                    self.selected[1].emit('keypress', ev.keyname, ev.char)
-
-
-    def move_selected(self, offset):
-        if self.selected:
-            i = self.items.index(self.selected)
-            item = self.items[(i + offset) % len(self.items)]
-            self.unselect()
-            self.select(item)
-        self.redraw()
-
-    def on_mousedown(self, ev):
-        self._select_xy(ev.wx, ev.wy)
-
-    def on_mousemove(self, ev):
-        self._select_xy(ev.wx, ev.wy)
+            res = self.focus_next()
+            if menu_visible:
+                return self.focuschild.show_menu()
+            return res
+        elif ev.keyname == 'down':
+            return self.focuschild.show_menu()
+        elif menu_visible and ev.keyname == 'escape':
+            self.focuschild.menu.hide()
+            return True
 
-    def _select_xy(self, wx, wy):
-        i = 0
-        self.unselect()
-        for item in self.items:
-            w = len(item[0]) + 4
-            if wx >= i and wx < i + w:
-                self.select(item)
-            i += w
-
-    def on_unfocus(self, ev):
-        if self.selected and ev.new == self.selected[1]:
-            return
-        self.unselect()
-
-    def on_submenu_focus(self, ev):
-        self.set_focus()
-
-    def select(self, item):
-        self.selected = item
-        if isinstance(item[1], Widget):
-            item[1].hidden = False
-            item[1].redraw()
-
-    def unselect(self):
-        if self.selected:
-            if isinstance(self.selected[1], Widget):
-                self.selected[1].hidden = True
-            self.selected = None
-
--- a/tuikit/scrollbar.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/scrollbar.py	Sat Jan 19 13:05:21 2013 +0100
@@ -2,7 +2,6 @@
 
 from tuikit.widget import Widget
 from tuikit.events import Event
-from tuikit.common import UnicodeGraphics
 
 
 class Scrollbar(Widget):
@@ -27,10 +26,6 @@
         self.scroll_delay = 0.150
         #: interval for continuous scrolling
         self.scroll_interval = 0.030
-        #: scrollbar styling
-        unigraph = UnicodeGraphics()
-        self.track_char = unigraph.MIDDLE_DOT
-        self.thumb_char = unigraph.CIRCLE
 
         # change event is emitted when user moves scrollbar (even programmatically)
         self.add_events('change', Event)
@@ -118,12 +113,13 @@
         self._default_size.update(1, 20)
 
     def on_draw(self, ev):
+        ug = ev.driver.unigraph
         ev.driver.pushcolor('normal')
-        ev.driver.putch(ev.x, ev.y, ev.driver.unigraph.UP_ARROW)
+        ev.driver.putch(ev.x, ev.y, ug.get_char('sb_up'))
         for i in range(1, self.height - 1):
-            ev.driver.putch(ev.x, ev.y + i, self.track_char)
-        ev.driver.putch(ev.x, ev.y + 1 + self._thumb_pos, self.thumb_char)
-        ev.driver.putch(ev.x, ev.y + self.height - 1, ev.driver.unigraph.DOWN_ARROW)
+            ev.driver.putch(ev.x, ev.y + i, ug.get_char('sb_vtrack'))
+        ev.driver.putch(ev.x, ev.y + 1 + self._thumb_pos, ug.get_char('sb_thumb'))
+        ev.driver.putch(ev.x, ev.y + self.height - 1, ug.get_char('sb_down'))
         ev.driver.popcolor()
 
     def on_mousedown(self, ev):
@@ -166,12 +162,13 @@
         self._default_size.update(20, 1)
 
     def on_draw(self, ev):
+        ug = ev.driver.unigraph
         ev.driver.pushcolor('normal')
-        ev.driver.putch(ev.x, ev.y, ev.driver.unigraph.LEFT_ARROW)
+        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, self.track_char)
-        ev.driver.putch(ev.x + 1 + self._thumb_pos, ev.y, self.thumb_char)
-        ev.driver.putch(ev.x + self.width - 1, ev.y, ev.driver.unigraph.RIGHT_ARROW)
+            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 on_mousedown(self, ev):
--- a/tuikit/widget.py	Fri Jan 18 22:36:50 2013 +0100
+++ b/tuikit/widget.py	Sat Jan 19 13:05:21 2013 +0100
@@ -110,8 +110,6 @@
 
         """
         self._sizereq.update(w, h)
-        #if self.parent:
-         #   self.parent.emit('resize')
 
     @property
     def sizereq(self):
@@ -200,7 +198,6 @@
         return (self.parent.has_focus() \
             and self.parent.focuschild == self)
 
-
     def set_focus(self):
         """Focus the widget.
 
@@ -213,7 +210,9 @@
         See also grab_focus() which cares about parents.
 
         """
-        if self.has_focus() or not self.can_focus():
+        if self.parent is None:
+            return
+        if not self.can_focus() or self.parent.focuschild == self:
             return
         oldfocuschild = self.parent.focuschild
         self.parent.focuschild = self
@@ -221,12 +220,11 @@
             oldfocuschild.emit('unfocus', new=self)
         self.emit('focus', old=oldfocuschild)
 
-
     def grab_focus(self):
         """Focus the widget and its parents."""
-        self.set_focus()
         if self.parent and not self.parent.has_focus():
             self.parent.grab_focus()
+        self.set_focus()
 
 
     ###
@@ -257,8 +255,6 @@
     def hide(self):
         '''Hide widget. Convenience method.'''
         self.hidden = True
-        if self.parent and self.has_focus():
-            self.parent.focus_next()
         self.redraw()