New GridLayout. Change cursor behavior (hide on unfocus event). Change resize event to propagate through containers. Change container clipping - allowlayout=false children are clipped without borders. More Widget doc.
authorRadek Brich <radek.brich@devl.cz>
Sun, 10 Apr 2011 22:54:38 +0200
changeset 5 ae128c885d0f
parent 4 d197ca00496f
child 6 d4ee152d7d07
New GridLayout. Change cursor behavior (hide on unfocus event). Change resize event to propagate through containers. Change container clipping - allowlayout=false children are clipped without borders. More Widget doc.
docs/focus.rst
docs/widget.rst
tuikit/__init__.py
tuikit/backend_curses.py
tuikit/common.py
tuikit/container.py
tuikit/editfield.py
tuikit/layout.py
tuikit/widget.py
tuikit/window.py
--- a/docs/focus.rst	Fri Mar 18 20:14:44 2011 +0100
+++ b/docs/focus.rst	Sun Apr 10 22:54:38 2011 +0200
@@ -14,3 +14,5 @@
 hide() -> unfocus
 
 tab/shift-tab into / out off containers?
+
+widget.hasfocus()
--- a/docs/widget.rst	Fri Mar 18 20:14:44 2011 +0100
+++ b/docs/widget.rst	Sun Apr 10 22:54:38 2011 +0200
@@ -12,20 +12,58 @@
 
 .. attribute:: Widget.parent
 
+   Parrent widget.
+
+
 .. attribute:: Widget.top
 
+   Top widget (same for every widget in one application).
+
+
 .. attribute:: Widget.x
+               Widget.y
 
-.. attribute:: Widget.y
+   Position inside parent widget. Modified by layout manager.
+
 
 .. attribute:: Widget.width
+               Widget.height
 
-.. attribute:: Widget.height
+   Actual size. Modified by layout manager.
+
+
+.. attribute:: Widget.sizemin
+
+   Minimal size of widget. Under normal circumstances, widget will never be sized smaller than this.
+   Tuple (w, h). Both must be integers >= 1.
+
+
+.. attribute:: Widget.sizemax
+
+   Maximum size of widget. Widget will never be sized bigger than this.
+   Tuple (w, h). Integers >= 1 or None (meaning no maximum size or infinite).
+
+
+.. attribute:: Widget.sizereq
+
+   Size request. This is default size of the widget. Will be fulfilled if possible.
+   Tuple (w, h). Integers >= 1 or None (meaning use minumal size).
+
 
 .. attribute:: Widget.hidden
 
+   Hidden widget does not affect layout.
+
+
 .. attribute:: Widget.allowlayout
 
+   When false, the widget is not considered in layout.
+
+
+.. attribute:: Widget.layouthints
+
+   Dictionary containing optional parameters for layout managers.
+
 
 Event management
 ----------------
--- a/tuikit/__init__.py	Fri Mar 18 20:14:44 2011 +0100
+++ b/tuikit/__init__.py	Sun Apr 10 22:54:38 2011 +0200
@@ -6,7 +6,8 @@
 from tuikit.container import Container
 from tuikit.editbox import EditBox
 from tuikit.editfield import EditField
-from tuikit.layout import VerticalLayout
+from tuikit.label import Label
+from tuikit.layout import VerticalLayout, GridLayout
 from tuikit.menu import Menu
 from tuikit.menubar import MenuBar
 from tuikit.scrollbar import VScrollbar
--- a/tuikit/backend_curses.py	Fri Mar 18 20:14:44 2011 +0100
+++ b/tuikit/backend_curses.py	Sun Apr 10 22:54:38 2011 +0200
@@ -73,6 +73,7 @@
         self.screen = screen
         self.height, self.width = screen.getmaxyx()
 
+        self.cursor = None
         self.clipstack = []
         self.colorstack = []
         self.inputqueue = []
@@ -238,8 +239,6 @@
 
 
     def erase(self):
-        curses.curs_set(False)
-        self.cursor = None
         self.screen.erase()
 
 
@@ -258,6 +257,11 @@
         if not self.testclip(x, y):
             return
         self.cursor = (y, x)
+    
+        
+    def hidecursor(self):
+        curses.curs_set(False)
+        self.cursor = None
 
 
     ## input ##
--- a/tuikit/common.py	Fri Mar 18 20:14:44 2011 +0100
+++ b/tuikit/common.py	Sun Apr 10 22:54:38 2011 +0200
@@ -1,5 +1,16 @@
 # -*- coding: utf-8 -*-
 
+
+class Coords:
+    def __init__(self, x=0, y=0):
+        self.x = x
+        self.y = y
+
+
+    def __repr__(self):
+        return 'Coords(%(x)s,%(y)s)' % self.__dict__
+
+
 class Rect:
     def __init__(self, x=0, y=0, w=0, h=0):
         self.x = x
--- a/tuikit/container.py	Fri Mar 18 20:14:44 2011 +0100
+++ b/tuikit/container.py	Sun Apr 10 22:54:38 2011 +0200
@@ -15,10 +15,11 @@
         self.heightrequest = (None, None)
 
 
-    def add(self, widget):
+    def add(self, widget, **kw):
         self.children.append(widget)
         widget.parent = self
         widget.settop(self.top)
+        widget.layouthints.update(kw)
 
 
     def layout(self, layout):
@@ -33,24 +34,32 @@
             child.settop(top)
 
 
+    def resize(self):
+        Widget.resize(self)
+        for child in self.children:
+            child.emit('resize')
+
+
     def draw(self, screen, x=0, y=0):
         if self.hidden:
             return
-        #if self._redraw:
-            #self.fill(screen, self.y, self.x, self.height, self.width)
-        self.handle('draw', screen, x, y)
+
+        screen.pushclip(x, y, self.width, self.height)
+
+        Widget.draw(self, screen, x, y)
+
+        for child in [x for x in self.children if not x.allowlayout]:
+            child.draw(screen, x + child.x, y + child.y)
 
         l, t, r, b = self.borders
-        screen.pushclip(x + l, y + t, self.width - l - r, self.height - t - b)
+        screen.pushclip(x+l, y+t, self.width-l-r, self.height-t-b)
 
-        for child in self.children:
-            #if self._redraw:
-             #   child._redraw = True
+        for child in [x for x in self.children if x.allowlayout]:
             child.draw(screen, x + child.x, y + child.y)
 
         screen.popclip()
 
-        #self._redraw = False
+        screen.popclip()
 
 
     def mousedown(self, ev):
--- a/tuikit/editfield.py	Fri Mar 18 20:14:44 2011 +0100
+++ b/tuikit/editfield.py	Sun Apr 10 22:54:38 2011 +0200
@@ -1,6 +1,5 @@
 # -*- coding: utf-8 -*-
 
-import curses
 import locale
 
 from .widget import Widget
@@ -17,15 +16,20 @@
         self.value = value
         self.maxlen = None   # unlimited
 
-        self.tw = self.width - 2  # real width of text field (minus space for arrows)
-        self.cursor = 0   # position of cursor on screen
+        self.tw = 0
+        self.cur = 0   # position of cursor on screen
         self.pos = 0      # position of cursor in value
         self.ofs = 0      # position of value beginning on screen
 
+        self.connect('resize', self.on_resize)
         self.connect('draw', self.on_draw)
         self.connect('keypress', self.on_keypress)
 
 
+    def on_resize(self):
+        self.tw = self.width - 2  # real width of text field (minus space for arrows)
+
+
     def on_draw(self, screen, x, y):
         # draw value
         val = self.value + ' ' * self.tw         # add spaces to fill rest of field
@@ -43,8 +47,7 @@
             c = '>'
         screen.putch(x + self.width-1, y, c)
 
-        # move cursor to the position
-        screen.showcursor(x + 1 + self.cursor, y)
+        self.cursor = (1 + self.cur, 0)
 
 
     def on_keypress(self, keyname, char):
@@ -71,24 +74,24 @@
 
 
     def move_left(self):
-        if self.cursor > 1 or (self.cursor == 1 and self.pos == 1):
+        if self.cur > 1 or (self.cur == 1 and self.pos == 1):
             # move cursor
             self.pos -= 1
-            self.cursor -= 1
+            self.cur -= 1
         else:
             # move content in field
-            if self.pos > self.cursor:
+            if self.pos > self.cur:
                 self.pos -= 1
                 self.ofs -= 1
 
 
     def move_right(self):
         if self.pos < len(self.value):
-            if self.cursor < self.tw - 2 \
-            or (self.cursor == self.tw - 2 and self.pos == len(self.value)-1):
+            if self.cur < self.tw - 2 \
+            or (self.cur == self.tw - 2 and self.pos == len(self.value)-1):
                 # move cursor
                 self.pos += 1
-                self.cursor += 1
+                self.cur += 1
             else:
                 # move content in field
                 self.pos += 1
--- a/tuikit/layout.py	Fri Mar 18 20:14:44 2011 +0100
+++ b/tuikit/layout.py	Sun Apr 10 22:54:38 2011 +0200
@@ -1,16 +1,166 @@
 # -*- coding: utf-8 -*-
 
+from .common import Rect
+
+class Layout:
+    def _getchildren(self):
+        return [child for child in self.container.children if child.allowlayout]
+
+
+    def _getregion(self):
+        c = self.container
+        bl, bt, br, bb = c.borders
+        return Rect(bl, bt, c.width - bl - br, c.height - bt - bb)
+
 
 class VerticalLayout:
     def resize(self):
         v = 0
         c = self.container
+        bl, bt, br, bb = c.borders
         for child in c.children:
             if not child.allowlayout:
                 continue
-            child.x = 0
-            child.width = c.width
-            child.y = v
+            child.x = bl
+            child.width = c.width - bl - br
+            child.y = bt + v
             v += child.height
             child.handle('resize')
         c.redraw()
+
+
+class GridLayout(Layout):
+    def __init__(self, numcols=2):
+        self.numcols = numcols
+
+
+    def _fillgrid(self):
+        ''' fill grid with widgeds '''
+        self._grid = []
+        rown = 0
+        coln = 0
+        for child in self._getchildren():
+            if coln == 0:
+                row = []
+                for i in range(self.numcols):
+                    row.append({'widget': None, 'colspan': 0, 'rowspan': 0})
+
+            colspan = 1
+            if 'colspan' in child.layouthints:
+                colspan = child.layouthints['colspan']
+            colspan = min(colspan, self.numcols - coln + 1)
+
+            row[coln]['widget'] = child
+            row[coln]['colspan'] = colspan
+
+            coln += colspan
+            if coln >= self.numcols:
+                coln = 0
+                self._grid.append(row)
+                rown += 1
+
+        # autospan last child
+        if coln > 0:
+            row[coln-1]['colspan'] = self.numcols - coln + 1
+            self._grid.append(row)
+
+
+    def _computesizes(self):
+        self._colminw = [0] * self.numcols
+
+        # compute min column width for all widgets with colspan = 1
+        for row in self._grid:
+            for coln in range(self.numcols):
+                w = row[coln]['widget']
+                if row[coln]['colspan'] == 1:
+                    self._colminw[coln] = max(w.sizemin[0], self._colminw[coln])
+
+        # adjust min width for widgets with colspan > 1
+        for row in self._grid:
+            for coln in range(self.numcols):
+                w = row[coln]['widget']
+                colspan = row[coln]['colspan']
+                if colspan > 1:
+                    # find min width over spanned columns
+                    totalminw = 0
+                    for i in range(colspan):
+                        totalminw += self._colminw[coln + i]
+                    # check if spanned widget fits in
+                    if w.sizemin[0] > totalminw:
+                        # need to adjust colminw
+                        addspace = w.sizemin[0] - totalminw
+                        addall = addspace // colspan
+                        rest = addspace % colspan
+                        for i in range(colspan):
+                            self._colminw[coln + i] += addall
+                            if i < rest:
+                                self._colminw[coln + i] += 1
+
+        self._rowminh = [0] * len(self._grid)
+        rown = 0
+        for row in self._grid:
+            for col in row:
+                w = col['widget']
+                if w is not None:
+                    self._rowminh[rown] = max(self._rowminh[rown], w.sizemin[1])
+            rown += 1
+
+        self._gridminw = sum(self._colminw)
+        self._gridminh = sum(self._rowminh)
+
+
+    def resize(self):
+        self._fillgrid()
+        self._computesizes()
+
+        # enlarge container if needed
+        lreg = self._getregion() # layout region
+        cont = self.container
+        bl, bt, br, bb = cont.borders
+        if self._gridminw > lreg.w:
+            cont.width = self._gridminw + bl + br
+        if self._gridminh > lreg.h:
+            cont.height = self._gridminh + bt + bb
+
+        # compute actual width of columns
+        colw = [0] * self.numcols
+        lreg = self._getregion() # layout region
+        restw = lreg.w - self._gridminw
+        resth = lreg.h - self._gridminh
+        for c in range(self.numcols):
+            colw[c] = self._colminw[c] + restw // self.numcols
+            if c < restw % self.numcols:
+                colw[c] += 1
+
+        # place widgets
+        colx = lreg.x
+        coly = lreg.y
+        rown = 0
+        numrows = len(self._grid)
+        for row in self._grid:
+            coln = 0
+            rowh = self._rowminh[rown] + resth // numrows
+            if rown < resth % numrows:
+                rowh += 1
+
+            for col in row:
+                w = col['widget']
+                if w is not None:
+                    w.x = colx
+                    w.y = coly
+                    w.width = colw[coln]
+                    w.height = rowh
+                    w.handle('resize')
+
+                    colspan = col['colspan']
+                    if colspan > 1:
+                        for i in range(1,colspan):
+                            w.width += colw[coln + i]
+
+                colx += colw[coln]
+
+            rown += 1
+            colx = lreg.x
+            coly += rowh
+
+        cont.redraw()
--- a/tuikit/widget.py	Fri Mar 18 20:14:44 2011 +0100
+++ b/tuikit/widget.py	Sun Apr 10 22:54:38 2011 +0200
@@ -7,13 +7,19 @@
     def __init__(self, width = 10, height = 10):
         self.parent = None
         self.top = None
-        # placing - set by parent widget
+        # placing - set by parent widget's layout manager
         self.x = 0
         self.y = 0
         # size
-        self.width = width
-        self.height = height
+        self._width = width
+        self._height = height
+        self.sizemin = (1,1)
+        self.sizemax = (None, None)
+        self.sizereq = (10,10)
         self.allowlayout = True
+        self.layouthints = {}
+        # cursor
+        self.cursor = None
         # redraw request
         self._redraw = True
         self.hidden = False
@@ -31,6 +37,25 @@
             }
 
 
+    @property
+    def width(self):
+        return self._width
+
+    @width.setter
+    def width(self, value):
+        self._width = value
+        self.emit('resize')
+
+    @property
+    def height(self):
+        return self._height
+
+    @height.setter
+    def height(self, value):
+        self._height = value
+        self.emit('resize')
+
+
     def settop(self, top):
         self.top = top
 
@@ -79,10 +104,17 @@
 
 
     def draw(self, screen, x=0, y=0):
-        #if self._redraw:
-        if not self.hidden:
-            self.handle('draw', screen, x, y)
-            #self._redraw = False
+        if self.hidden:
+            return
+
+        self.handle('draw', screen, x, y)
+
+        if self.hasfocus():
+            if self.cursor:
+                cx, cy = self.cursor
+                screen.showcursor(x + cx, y + cy)
+            else:
+                screen.hidecursor()
 
 
     ### focus
@@ -113,10 +145,12 @@
 
 
     def focus(self):
+        '''handle focus event'''
         self.handle('focus')
 
 
     def unfocus(self, newfocus=None):
+        '''handle unfocus event'''
         self.handle('unfocus', newfocus)
 
 
--- a/tuikit/window.py	Fri Mar 18 20:14:44 2011 +0100
+++ b/tuikit/window.py	Sun Apr 10 22:54:38 2011 +0200
@@ -23,12 +23,15 @@
         self.moving = False
 
         self.closebtn = Button('x')
+        self.closebtn.allowlayout = False
         self.closebtn.x = self.width - 5
         self.closebtn.width = 3
         self.closebtn.connect('click', self.on_closebtn)
         self.closebtn.bg = (4, 'bold')
         self.closebtn.bghi = (5, 'bold')
         self.add(self.closebtn)
+        
+        self.borders = (1, 1, 1, 1)
 
 
     @property