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()