tuikit/layout.py
author Radek Brich <radek.brich@devl.cz>
Sat, 19 Jan 2013 13:05:21 +0100
changeset 63 2a0e04091898
parent 62 2f61931520c9
child 64 03f591f5fe5c
permissions -rw-r--r--
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.

# -*- coding: utf-8 -*-
'''layout module

VerticalLayout
HorizontalLayout
TableLayout

'''

import math
import logging

from tuikit.common import Coords, Size, Rect, Borders, make_select
from tuikit.container import Container


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]

    def _get_region(self):
        c = self.container
        bl, bt, br, bb = c.borders
        return Rect(bl, bt, c.width - bl - br, c.height - bt - bb)


class AnchorLayout(Layout):

    """Attach widgets to borders of container.

    Also allows absolute positioning when possible (not for center, fill).

    """

    def __init__(self):
        Layout.__init__(self)
        self.register_hints(
            'halign', make_select('left', 'right', 'fill', 'center'),
            'valign', make_select('top', 'bottom', 'fill', 'center'),
            'margin', Borders,
            )

    def on_resize(self, ev):
        for child in self._get_children():
            reqw = max(child.sizereq.w, child.sizemin.w)
            reqh = max(child.sizereq.h, child.sizemin.h)
            ha = child.hints['halign'].selected
            va = child.hints['valign'].selected
            margin = child.hints['margin']
            if ha == 'left':
                x = margin.l
                w = reqw
            if ha == 'right':
                x = self.width - margin.r - reqw
                w = reqw
            if ha == 'fill':
                x = margin.l
                w = self.width - margin.l - margin.r
            if ha == 'center':
                x = (self.width - reqw) // 2
                w = reqw
            if va == 'top':
                y = margin.t
                h = reqh
            if va == 'bottom':
                y = self.height - margin.b - reqh
                h = reqh
            if va == 'fill':
                y = margin.t
                h = self.height - margin.t - margin.b
            if va == 'center':
                y = (self.height - reqh) // 2
                h = reqh
            child._pos.update(x=x, y=y)
            child._size.update(w=w, h=h)

    def move_child(self, child, x, y):
        if not child in self.children:
            raise ValueError('AnchorLayout.move(): Cannot move foreign child.')
        margin = child.hints['margin']
        ha = child.hints['halign'].selected
        va = child.hints['valign'].selected
        ofsx = x - child.x
        ofsy = y - child.y
        if ha == 'left':
            margin.l += ofsx
            newx = margin.l
        elif ha == 'right':
            margin.r -= ofsx
            newx = self.width - margin.r - child.sizereq.w
        else:
            # do not move when halign is center,fill
            newx = child.x
        if va == 'top':
            margin.t += ofsy
            newy = margin.t
        elif va == 'bottom':
            margin.b -= ofsy
            newy = self.height - margin.b - child.sizereq.h
        else:
            # do not move when valign is center,fill
            newy = child.y
        child._pos.update(x=newx, y=newy)


class LinearLayout(Layout):
    def __init__(self, homogeneous=False, spacing=0):
        Layout.__init__(self)
        self.homogeneous = homogeneous
        self.spacing = spacing
        self.register_hints(
            'expand', bool,
            'fill', bool,
            )

    def _resize(self, ax1, ax2):
        children = self._get_children()
        b1 = self.borders[ax1]
        b2 = self.borders[ax2]
        # available space
        space1 = self._size[ax1] - b1 - self.borders[ax1+2]
        space2 = self._size[ax2] - b2 - self.borders[ax2+2]

        # all space minus spacing
        space_to_divide = space1 - (len(children) - 1) * self.spacing
        if not self.homogeneous:
            # reduce by space acquired by children
            space_to_divide -= sum([child.sizereq[ax1] for child in children])
            # number of children with expanded hint
            expanded_num = len([ch for ch in children if ch.hints['expand']])
        else:
            # all children are implicitly expanded
            expanded_num = len(children)

        if expanded_num:
            # reserved space for each expanded child
            space_child = space_to_divide / expanded_num

        offset = 0.
        for child in children:
            pos = Coords()
            pos[ax1] = b1 + int(offset)
            pos[ax2] = b2
            size = Size()
            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
                if not self.homogeneous:
                    maxsize += child.sizereq[ax1]
                    offset += child.sizereq[ax1]

                if child.hints['fill']:
                    size[ax1] = maxsize
                else:
                    pos[ax1] += int((maxsize - size[ax1])/2)
            else:
                offset += size[ax1]

            child._pos.update(pos)
            child._size.update(size)


class VerticalLayout(LinearLayout):
    def on_resize(self, ev):
        ax1 = 1 # primary dimension = y
        ax2 = 0 # secondary dimension = x
        self._resize(ax1, ax2)


class HorizontalLayout(LinearLayout):
    def on_resize(self, ev):
        ax1 = 0 # primary dimension = x
        ax2 = 1 # secondary dimension = y
        self._resize(ax1, ax2)


class GridLayout(Layout):
    def __init__(self, numcols=2):
        Layout.__init__(self)
        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.hints:
                colspan = child.hints['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.emit('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()