You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1366 lines
45 KiB

#!/usr/bin/python
#
# Urwid container widget classes
# Copyright (C) 2004-2008 Ian Ward
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Urwid web site: http://excess.org/urwid/
from util import *
from widget import *
from decoration import *
from command_map import command_map
class WidgetContainer(Widget):
def __init__(self, widget_list):
self.__super.__init__()
self._widget_list = MonitoredList([])
self._set_widget_list(widget_list)
self._widget_list.set_modified_callback(self._invalidate)
def _get_widget_list(self):
return self._widget_list
def _set_widget_list(self, widget_list):
"""
widget_list -- iterable containing widgets
Copy the values from widget_list into self.widget_list
"""
widget_list = property(_get_widget_list, _set_widget_list)
def __getitem__(self, index):
"""
Return the base widget of the widget at self.widget_list[index].
"""
w = self._widget_list[index]
if hasattr(w, 'base_widget'):
w = w.base_widget
return w
def __len__(self):
return len(self._widget_list)
def __iter__(self):
i = 0
try:
while True:
v = self[i]
yield v
i += 1
except IndexError:
return
def __contains__(self, value):
for v in self:
if v == value:
return True
return False
def __reversed__(self):
for i in reversed(range(len(self))):
yield self[i]
def index(self, value):
for i, v in enumerate(self):
if v == value:
return i
raise ValueError
def count(self, value):
return sum(1 for v in self if v == value)
class GridFlow(FlowWidget):
def selectable(self):
"""Return True if the cell in focus is selectable."""
return self.focus_cell and self.focus_cell.selectable()
def __init__(self, cells, cell_width, h_sep, v_sep, align):
"""
cells -- list of flow widgets to display
cell_width -- column width for each cell
h_sep -- blank columns between each cell horizontally
v_sep -- blank rows between cells vertically (if more than
one row is required to display all the cells)
align -- horizontal alignment of cells, see "align" parameter
of Padding widget for available options
"""
self.__super.__init__()
self.cells = cells
self.cell_width = cell_width
self.h_sep = h_sep
self.v_sep = v_sep
self.align = align
self.focus_cell = None
if cells:
self.focus_cell = cells[0]
self._cache_maxcol = None
def set_focus(self, cell):
"""Set the cell in focus.
cell -- widget or integer index into self.cells"""
if type(cell) == type(0):
assert cell>=0 and cell<len(self.cells)
self.focus_cell = self.cells[cell]
else:
assert cell in self.cells
self.focus_cell = cell
self._cache_maxcol = None
self._invalidate()
def get_display_widget(self, size):
"""
Arrange the cells into columns (and possibly a pile) for
display, input or to calculate rows.
"""
(maxcol,) = size
# use cache if possible
if self._cache_maxcol == maxcol:
return self._cache_display_widget
self._cache_maxcol = maxcol
self._cache_display_widget = self.generate_display_widget(
size)
return self._cache_display_widget
def generate_display_widget(self, size):
"""
Actually generate display widget (ignoring cache)
"""
(maxcol,) = size
d = Divider()
if len(self.cells) == 0: # how dull
return d
if self.v_sep > 1:
# increase size of divider
d.top = self.v_sep-1
# cells per row
bpr = (maxcol+self.h_sep) / (self.cell_width+self.h_sep)
if bpr == 0: # too narrow, pile them on top of eachother
l = [self.cells[0]]
f = 0
for b in self.cells[1:]:
if b is self.focus_cell:
f = len(l)
if self.v_sep:
l.append(d)
l.append(b)
return Pile(l, f)
if bpr >= len(self.cells): # all fit on one row
k = len(self.cells)
f = self.cells.index(self.focus_cell)
cols = Columns(self.cells, self.h_sep, f)
rwidth = (self.cell_width+self.h_sep)*k - self.h_sep
row = Padding(cols, self.align, rwidth)
return row
out = []
s = 0
f = 0
while s < len(self.cells):
if out and self.v_sep:
out.append(d)
k = min( len(self.cells), s+bpr )
cells = self.cells[s:k]
if self.focus_cell in cells:
f = len(out)
fcol = cells.index(self.focus_cell)
cols = Columns(cells, self.h_sep, fcol)
else:
cols = Columns(cells, self.h_sep)
rwidth = (self.cell_width+self.h_sep)*(k-s)-self.h_sep
row = Padding(cols, self.align, rwidth)
out.append(row)
s += bpr
return Pile(out, f)
def _set_focus_from_display_widget(self, w):
"""Set the focus to the item in focus in the display widget."""
if isinstance(w, Padding):
# unwrap padding
w = w._original_widget
w = w.get_focus()
if w in self.cells:
self.set_focus(w)
return
if isinstance(w, Padding):
# unwrap padding
w = w._original_widget
w = w.get_focus()
#assert w == self.cells[0], `w, self.cells`
self.set_focus(w)
def keypress(self, size, key):
"""
Pass keypress to display widget for handling.
Capture focus changes."""
d = self.get_display_widget(size)
if not d.selectable():
return key
key = d.keypress(size, key)
if key is None:
self._set_focus_from_display_widget(d)
return key
def rows(self, size, focus=False):
"""Return rows used by this widget."""
d = self.get_display_widget(size)
return d.rows(size, focus=focus)
def render(self, size, focus=False ):
"""Use display widget to render."""
d = self.get_display_widget(size)
return d.render(size, focus)
def get_cursor_coords(self, size):
"""Get cursor from display widget."""
d = self.get_display_widget(size)
if not d.selectable():
return None
return d.get_cursor_coords(size)
def move_cursor_to_coords(self, size, col, row ):
"""Set the widget in focus based on the col + row."""
d = self.get_display_widget(size)
if not d.selectable():
# happy is the default
return True
r = d.move_cursor_to_coords(size, col, row)
if not r:
return False
self._set_focus_from_display_widget(d)
self._invalidate()
return True
def mouse_event(self, size, event, button, col, row, focus):
"""Send mouse event to contained widget."""
d = self.get_display_widget(size)
r = d.mouse_event(size, event, button, col, row, focus)
if not r:
return False
self._set_focus_from_display_widget(d)
self._invalidate()
return True
def get_pref_col(self, size):
"""Return pref col from display widget."""
d = self.get_display_widget(size)
if not d.selectable():
return None
return d.get_pref_col(size)
class OverlayError(Exception):
pass
class Overlay(BoxWidget):
def __init__(self, top_w, bottom_w, align, width, valign, height,
min_width=None, min_height=None ):
"""
top_w -- a flow, box or fixed widget to overlay "on top"
bottom_w -- a box widget to appear "below" previous widget
align -- one of:
'left', 'center', 'right'
('fixed left', columns)
('fixed right', columns)
('relative', percentage 0=left 100=right)
width -- one of:
None if top_w is a fixed widget
number of columns wide
('fixed right', columns) Only if align is 'fixed left'
('fixed left', columns) Only if align is 'fixed right'
('relative', percentage of total width)
valign -- one of:
'top', 'middle', 'bottom'
('fixed top', rows)
('fixed bottom', rows)
('relative', percentage 0=top 100=bottom)
height -- one of:
None if top_w is a flow or fixed widget
number of rows high
('fixed bottom', rows) Only if valign is 'fixed top'
('fixed top', rows) Only if valign is 'fixed bottom'
('relative', percentage of total height)
min_width -- the minimum number of columns for top_w
when width is not fixed
min_height -- one of:
minimum number of rows for the widget when height not fixed
Overlay widgets behave similarly to Padding and Filler widgets
when determining the size and position of top_w. bottom_w is
always rendered the full size available "below" top_w.
"""
self.__super.__init__()
at,aa,wt,wa=decompose_align_width(align, width, OverlayError)
vt,va,ht,ha=decompose_valign_height(valign,height,OverlayError)
self.top_w = top_w
self.bottom_w = bottom_w
self.align_type, self.align_amount = at, aa
self.width_type, self.width_amount = wt, wa
if self.width_type and self.width_type != 'fixed':
self.min_width = min_width
else:
self.min_width = None
self.valign_type, self.valign_amount = vt, va
self.height_type, self.height_amount = ht, ha
if self.height_type not in ('fixed', None):
self.min_height = min_height
else:
self.min_height = None
def selectable(self):
"""Return selectable from top_w."""
return self.top_w.selectable()
def keypress(self, size, key):
"""Pass keypress to top_w."""
return self.top_w.keypress(self.top_w_size(size,
*self.calculate_padding_filler(size, True)), key)
def get_cursor_coords(self, size):
"""Return cursor coords from top_w, if any."""
if not hasattr(self.body, 'get_cursor_coords'):
return None
left, right, top, bottom = self.calculate_padding_filler(size,
True)
x, y = self.top_w.get_cursor_coords(
(maxcol-left-right, maxrow-top-bottom) )
if y >= maxrow: # required??
y = maxrow-1
return x+left, y+top
def calculate_padding_filler(self, size, focus):
"""Return (padding left, right, filler top, bottom)."""
(maxcol, maxrow) = size
height = None
if self.width_type is None:
# top_w is a fixed widget
width, height = self.top_w.pack((),focus=focus)
assert height, "fixed widget must have a height"
left, right = calculate_padding(self.align_type,
self.align_amount, 'fixed', width,
None, maxcol, clip=True )
else:
left, right = calculate_padding(self.align_type,
self.align_amount, self.width_type,
self.width_amount, self.min_width, maxcol)
if height:
# top_w is a fixed widget
top, bottom = calculate_filler(self.valign_type,
self.valign_amount, 'fixed', height,
None, maxrow)
if maxrow-top-bottom < height:
bottom = maxrow-top-height
elif self.height_type is None:
# top_w is a flow widget
height = self.top_w.rows((maxcol,),focus=focus)
top, bottom = calculate_filler( self.valign_type,
self.valign_amount, 'fixed', height,
None, maxrow )
else:
top, bottom = calculate_filler(self.valign_type,
self.valign_amount, self.height_type,
self.height_amount, self.min_height, maxrow)
return left, right, top, bottom
def top_w_size(self, size, left, right, top, bottom):
"""Return the size to pass to top_w."""
if self.width_type is None:
# top_w is a fixed widget
return ()
maxcol, maxrow = size
if self.width_type is not None and self.height_type is None:
# top_w is a flow widget
return (maxcol-left-right,)
return (maxcol-left-right, maxrow-top-bottom)
def render(self, size, focus=False):
"""Render top_w overlayed on bottom_w."""
left, right, top, bottom = self.calculate_padding_filler(size,
focus)
bottom_c = self.bottom_w.render(size)
top_c = self.top_w.render(
self.top_w_size(size, left, right, top, bottom), focus)
if left<0 or right<0:
top_c = CompositeCanvas(top_c)
top_c.pad_trim_left_right(min(0,left), min(0,right))
if top<0 or bottom<0:
top_c = CompositeCanvas(top_c)
top_c.pad_trim_top_bottom(min(0,top), min(0,bottom))
return CanvasOverlay(top_c, bottom_c, max(0,left), top)
def mouse_event(self, size, event, button, col, row, focus):
"""Pass event to top_w, ignore if outside of top_w."""
if not hasattr(self.top_w, 'mouse_event'):
return False
left, right, top, bottom = self.calculate_padding_filler(size,
focus)
maxcol, maxrow = size
if ( col<left or col>=maxcol-right or
row<top or row>=maxrow-bottom ):
return False
return self.top_w.mouse_event(
self.top_w_size(size, left, right, top, bottom),
event, button, col-left, row-top, focus )
class Frame(BoxWidget):
def __init__(self, body, header=None, footer=None, focus_part='body'):
"""
body -- a box widget for the body of the frame
header -- a flow widget for above the body (or None)
footer -- a flow widget for below the body (or None)
focus_part -- 'header', 'footer' or 'body'
"""
self.__super.__init__()
self._header = header
self._body = body
self._footer = footer
self.focus_part = focus_part
def get_header(self):
return self._header
def set_header(self, header):
self._header = header
self._invalidate()
header = property(get_header, set_header)
def get_body(self):
return self._body
def set_body(self, body):
self._body = body
self._invalidate()
body = property(get_body, set_body)
def get_footer(self):
return self._footer
def set_footer(self, footer):
self._footer = footer
self._invalidate()
footer = property(get_footer, set_footer)
def set_focus(self, part):
"""Set the part of the frame that is in focus.
part -- 'header', 'footer' or 'body'
"""
assert part in ('header', 'footer', 'body')
self.focus_part = part
self._invalidate()
def frame_top_bottom(self, size, focus):
"""Calculate the number of rows for the header and footer.
Returns (head rows, foot rows),(orig head, orig foot).
orig head/foot are from rows() calls.
"""
(maxcol, maxrow) = size
frows = hrows = 0
if self.header:
hrows = self.header.rows((maxcol,),
self.focus_part=='header' and focus)
if self.footer:
frows = self.footer.rows((maxcol,),
self.focus_part=='footer' and focus)
remaining = maxrow
if self.focus_part == 'footer':
if frows >= remaining:
return (0, remaining),(hrows, frows)
remaining -= frows
if hrows >= remaining:
return (remaining, frows),(hrows, frows)
elif self.focus_part == 'header':
if hrows >= maxrow:
return (remaining, 0),(hrows, frows)
remaining -= hrows
if frows >= remaining:
return (hrows, remaining),(hrows, frows)
elif hrows + frows >= remaining:
# self.focus_part == 'body'
rless1 = max(0, remaining-1)
if frows >= remaining-1:
return (0, rless1),(hrows, frows)
remaining -= frows
rless1 = max(0, remaining-1)
return (rless1,frows),(hrows, frows)
return (hrows, frows),(hrows, frows)
def render(self, size, focus=False):
"""Render frame and return it."""
(maxcol, maxrow) = size
(htrim, ftrim),(hrows, frows) = self.frame_top_bottom(
(maxcol, maxrow), focus)
combinelist = []
depends_on = []
head = None
if htrim and htrim < hrows:
head = Filler(self.header, 'top').render(
(maxcol, htrim),
focus and self.focus_part == 'header')
elif htrim:
head = self.header.render((maxcol,),
focus and self.focus_part == 'header')
assert head.rows() == hrows, "rows, render mismatch"
if head:
combinelist.append((head, 'header',
self.focus_part == 'header'))
depends_on.append(self.header)
if ftrim+htrim < maxrow:
body = self.body.render((maxcol, maxrow-ftrim-htrim),
focus and self.focus_part == 'body')
combinelist.append((body, 'body',
self.focus_part == 'body'))
depends_on.append(self.body)
foot = None
if ftrim and ftrim < frows:
foot = Filler(self.footer, 'bottom').render(
(maxcol, ftrim),
focus and self.focus_part == 'footer')
elif ftrim:
foot = self.footer.render((maxcol,),
focus and self.focus_part == 'footer')
assert foot.rows() == frows, "rows, render mismatch"
if foot:
combinelist.append((foot, 'footer',
self.focus_part == 'footer'))
depends_on.append(self.footer)
return CanvasCombine(combinelist)
def keypress(self, size, key):
"""Pass keypress to widget in focus."""
(maxcol, maxrow) = size
if self.focus_part == 'header' and self.header is not None:
if not self.header.selectable():
return key
return self.header.keypress((maxcol,),key)
if self.focus_part == 'footer' and self.footer is not None:
if not self.footer.selectable():
return key
return self.footer.keypress((maxcol,),key)
if self.focus_part != 'body':
return key
remaining = maxrow
if self.header is not None:
remaining -= self.header.rows((maxcol,))
if self.footer is not None:
remaining -= self.footer.rows((maxcol,))
if remaining <= 0: return key
if not self.body.selectable():
return key
return self.body.keypress( (maxcol, remaining), key )
def mouse_event(self, size, event, button, col, row, focus):
"""
Pass mouse event to appropriate part of frame.
Focus may be changed on button 1 press.
"""
(maxcol, maxrow) = size
(htrim, ftrim),(hrows, frows) = self.frame_top_bottom(
(maxcol, maxrow), focus)
if row < htrim: # within header
focus = focus and self.focus_part == 'header'
if is_mouse_press(event) and button==1:
if self.header.selectable():
self.set_focus('header')
if not hasattr(self.header, 'mouse_event'):
return False
return self.header.mouse_event( (maxcol,), event,
button, col, row, focus )
if row >= maxrow-ftrim: # within footer
focus = focus and self.focus_part == 'footer'
if is_mouse_press(event) and button==1:
if self.footer.selectable():
self.set_focus('footer')
if not hasattr(self.footer, 'mouse_event'):
return False
return self.footer.mouse_event( (maxcol,), event,
button, col, row-maxrow+frows, focus )
# within body
focus = focus and self.focus_part == 'body'
if is_mouse_press(event) and button==1:
if self.body.selectable():
self.set_focus('body')
if not hasattr(self.body, 'mouse_event'):
return False
return self.body.mouse_event( (maxcol, maxrow-htrim-ftrim),
event, button, col, row-htrim, focus )
class PileError(Exception):
pass
class Pile(Widget): # either FlowWidget or BoxWidget
def __init__(self, widget_list, focus_item=None):
"""
widget_list -- list of widgets
focus_item -- widget or integer index, if None the first
selectable widget will be chosen.
widget_list may also contain tuples such as:
('flow', widget) always treat widget as a flow widget
('fixed', height, widget) give this box widget a fixed height
('weight', weight, widget) if the pile is treated as a box
widget then treat widget as a box widget with a
height based on its relative weight value, otherwise
treat widget as a flow widget
widgets not in a tuple are the same as ('weight', 1, widget)
If the pile is treated as a box widget there must be at least
one 'weight' tuple in widget_list.
"""
self.__super.__init__()
self.widget_list = MonitoredList(widget_list)
self.item_types = []
for i in range(len(widget_list)):
w = widget_list[i]
if type(w) != type(()):
self.item_types.append(('weight',1))
elif w[0] == 'flow':
f, widget = w
self.widget_list[i] = widget
self.item_types.append((f,None))
w = widget
elif w[0] in ('fixed', 'weight'):
f, height, widget = w
self.widget_list[i] = widget
self.item_types.append((f,height))
w = widget
else:
raise PileError, "widget list item invalid %s" % `w`
if focus_item is None and w.selectable():
focus_item = i
self.widget_list.set_modified_callback(self._invalidate)
if focus_item is None:
focus_item = 0
self.set_focus(focus_item)
self.pref_col = 0
def selectable(self):
"""Return True if the focus item is selectable."""
return self.focus_item.selectable()
def set_focus(self, item):
"""Set the item in focus.
item -- widget or integer index"""
if type(item) == type(0):
assert item>=0 and item<len(self.widget_list)
self.focus_item = self.widget_list[item]
else:
assert item in self.widget_list
self.focus_item = item
self._invalidate()
def get_focus(self):
"""Return the widget in focus."""
return self.focus_item
def get_pref_col(self, size):
"""Return the preferred column for the cursor, or None."""
if not self.selectable():
return None
self._update_pref_col_from_focus(size)
return self.pref_col
def get_item_size(self, size, i, focus, item_rows=None):
"""
Return a size appropriate for passing to self.widget_list[i]
"""
maxcol = size[0]
f, height = self.item_types[i]
if f=='fixed':
return (maxcol, height)
elif f=='weight' and len(size)==2:
if not item_rows:
item_rows = self.get_item_rows(size, focus)
return (maxcol, item_rows[i])
else:
return (maxcol,)
def get_item_rows(self, size, focus):
"""
Return a list of the number of rows used by each widget
in self.item_list.
"""
remaining = None
maxcol = size[0]
if len(size)==2:
remaining = size[1]
l = []
if remaining is None:
# pile is a flow widget
for (f, height), w in zip(
self.item_types, self.widget_list):
if f == 'fixed':
l.append( height )
else:
l.append( w.rows( (maxcol,), focus=focus
and self.focus_item == w ))
return l
# pile is a box widget
# do an extra pass to calculate rows for each widget
wtotal = 0
for (f, height), w in zip(self.item_types, self.widget_list):
if f == 'flow':
rows = w.rows((maxcol,), focus=focus and
self.focus_item == w )
l.append(rows)
remaining -= rows
elif f == 'fixed':
l.append(height)
remaining -= height
else:
l.append(None)
wtotal += height
if wtotal == 0:
raise PileError, "No weighted widgets found for Pile treated as a box widget"
if remaining < 0:
remaining = 0
i = 0
for (f, height), li in zip(self.item_types, l):
if li is None:
rows = int(float(remaining)*height
/wtotal+0.5)
l[i] = rows
remaining -= rows
wtotal -= height
i += 1
return l
def render(self, size, focus=False):
"""
Render all widgets in self.widget_list and return the results
stacked one on top of the next.
"""
maxcol = size[0]
item_rows = None
combinelist = []
i = 0
for (f, height), w in zip(self.item_types, self.widget_list):
item_focus = self.focus_item == w
canv = None
if f == 'fixed':
canv = w.render( (maxcol, height),
focus=focus and item_focus)
elif f == 'flow' or len(size)==1:
canv = w.render( (maxcol,),
focus=focus and item_focus)
else:
if item_rows is None:
item_rows = self.get_item_rows(size,
focus)
rows = item_rows[i]
if rows>0:
canv = w.render( (maxcol, rows),
focus=focus and item_focus )
if canv:
combinelist.append((canv, i, item_focus))
i+=1
return CanvasCombine(combinelist)
def get_cursor_coords(self, size):
"""Return the cursor coordinates of the focus widget."""
if not self.focus_item.selectable():
return None
if not hasattr(self.focus_item,'get_cursor_coords'):
return None
i = self.widget_list.index(self.focus_item)
f, height = self.item_types[i]
item_rows = None
maxcol = size[0]
if f == 'fixed' or (f=='weight' and len(size)==2):
if f == 'fixed':
maxrow = height
else:
if item_rows is None:
item_rows = self.get_item_rows(size,
focus=True)
maxrow = item_rows[i]
coords = self.focus_item.get_cursor_coords(
(maxcol,maxrow))
else:
coords = self.focus_item.get_cursor_coords((maxcol,))
if coords is None:
return None
x,y = coords
if i > 0:
if item_rows is None:
item_rows = self.get_item_rows(size, focus=True)
for r in item_rows[:i]:
y += r
return x, y
def rows(self, size, focus=False ):
"""Return the number of rows required for this widget."""
return sum(self.get_item_rows(size, focus))
def keypress(self, size, key ):
"""Pass the keypress to the widget in focus.
Unhandled 'up' and 'down' keys may cause a focus change."""
maxcol = size[0]
item_rows = None
if len(size)==2:
item_rows = self.get_item_rows( size, focus=True )
i = self.widget_list.index(self.focus_item)
f, height = self.item_types[i]
if self.focus_item.selectable():
tsize = self.get_item_size(size,i,True,item_rows)
key = self.focus_item.keypress( tsize, key )
if command_map[key] not in ('cursor up', 'cursor down'):
return key
if command_map[key] == 'cursor up':
candidates = range(i-1, -1, -1) # count backwards to 0
else: # command_map[key] == 'cursor down'
candidates = range(i+1, len(self.widget_list))
if not item_rows:
item_rows = self.get_item_rows( size, focus=True )
for j in candidates:
if not self.widget_list[j].selectable():
continue
self._update_pref_col_from_focus(size)
old_focus = self.focus_item
self.set_focus(j)
if not hasattr(self.focus_item,'move_cursor_to_coords'):
return
f, height = self.item_types[j]
rows = item_rows[j]
if command_map[key] == 'cursor up':
rowlist = range(rows-1, -1, -1)
else: # command_map[key] == 'cursor down'
rowlist = range(rows)
for row in rowlist:
tsize=self.get_item_size(size,j,True,item_rows)
if self.focus_item.move_cursor_to_coords(
tsize,self.pref_col,row):
break
return
# nothing to select
return key
def _update_pref_col_from_focus(self, size ):
"""Update self.pref_col from the focus widget."""
widget = self.focus_item
if not hasattr(widget,'get_pref_col'):
return
i = self.widget_list.index(widget)
tsize = self.get_item_size(size,i,True)
pref_col = widget.get_pref_col(tsize)
if pref_col is not None:
self.pref_col = pref_col
def move_cursor_to_coords(self, size, col, row):
"""Capture pref col and set new focus."""
self.pref_col = col
#FIXME guessing focus==True
focus=True
wrow = 0
item_rows = self.get_item_rows(size,focus)
for r,w in zip(item_rows, self.widget_list):
if wrow+r > row:
break
wrow += r
if not w.selectable():
return False
if hasattr(w,'move_cursor_to_coords'):
i = self.widget_list.index(w)
tsize = self.get_item_size(size, i, focus, item_rows)
rval = w.move_cursor_to_coords(tsize,col,row-wrow)
if rval is False:
return False
self.set_focus(w)
return True
def mouse_event(self, size, event, button, col, row, focus):
"""
Pass the event to the contained widget.
May change focus on button 1 press.
"""
wrow = 0
item_rows = self.get_item_rows(size,focus)
for r,w in zip(item_rows, self.widget_list):
if wrow+r > row:
break
wrow += r
focus = focus and self.focus_item == w
if is_mouse_press(event) and button==1:
if w.selectable():
self.set_focus(w)
if not hasattr(w,'mouse_event'):
return False
i = self.widget_list.index(w)
tsize = self.get_item_size(size, i, focus, item_rows)
return w.mouse_event(tsize, event, button, col, row-wrow,
focus)
class ColumnsError(Exception):
pass
class Columns(Widget): # either FlowWidget or BoxWidget
def __init__(self, widget_list, dividechars=0, focus_column=None,
min_width=1, box_columns=None):
"""
widget_list -- list of flow widgets or list of box widgets
dividechars -- blank characters between columns
focus_column -- index into widget_list of column in focus,
if None the first selectable widget will be chosen.
min_width -- minimum width for each column before it is hidden
box_columns -- a list of column indexes containing box widgets
whose maxrow is set to the maximum of the rows
required by columns not listed in box_columns.
widget_list may also contain tuples such as:
('fixed', width, widget) give this column a fixed width
('weight', weight, widget) give this column a relative weight
widgets not in a tuple are the same as ('weight', 1, widget)
box_columns is ignored when this widget is being used as a
box widget because in that case all columns are treated as box
widgets.
"""
self.__super.__init__()
self.widget_list = MonitoredList(widget_list)
self.column_types = []
for i in range(len(widget_list)):
w = widget_list[i]
if type(w) != type(()):
self.column_types.append(('weight',1))
elif w[0] in ('fixed', 'weight'):
f,width,widget = w
self.widget_list[i] = widget
self.column_types.append((f,width))
w = widget
else:
raise ColumnsError, "widget list item invalid: %s" % `w`
if focus_column is None and w.selectable():
focus_column = i
self.widget_list.set_modified_callback(self._invalidate)
self.dividechars = dividechars
if focus_column is None:
focus_column = 0
self.focus_col = focus_column
self.pref_col = None
self.min_width = min_width
self.box_columns = box_columns
self._cache_maxcol = None
def _invalidate(self):
self._cache_maxcol = None
self.__super._invalidate()
def set_focus_column( self, num ):
"""Set the column in focus by its index in self.widget_list."""
self.focus_col = num
self._invalidate()
def get_focus_column( self ):
"""Return the focus column index."""
return self.focus_col
def set_focus(self, item):
"""Set the item in focus.
item -- widget or integer index"""
if type(item) == type(0):
assert item>=0 and item<len(self.widget_list)
position = item
else:
position = self.widget_list.index(item)
self.focus_col = position
self._invalidate()
def get_focus(self):
"""Return the widget in focus."""
return self.widget_list[self.focus_col]
def column_widths( self, size ):
"""Return a list of column widths.
size -- (maxcol,) if self.widget_list contains flow widgets or
(maxcol, maxrow) if it contains box widgets.
"""
maxcol = size[0]
if maxcol == self._cache_maxcol:
return self._cache_column_widths
col_types = self.column_types
# hack to support old practice of editing self.widget_list
# directly
lwl, lct = len(self.widget_list), len(self.column_types)
if lwl > lct:
col_types = col_types + [('weight',1)] * (lwl-lct)
widths=[]
weighted = []
shared = maxcol + self.dividechars
growable = 0
i = 0
for t, width in col_types:
if t == 'fixed':
static_w = width
else:
static_w = self.min_width
if shared < static_w + self.dividechars:
break
widths.append( static_w )
shared -= static_w + self.dividechars
if t != 'fixed':
weighted.append( (width,i) )
i += 1
if shared:
# divide up the remaining space between weighted cols
weighted.sort()
wtotal = sum([weight for weight,i in weighted])
grow = shared + len(weighted)*self.min_width
for weight, i in weighted:
width = int(float(grow) * weight / wtotal + 0.5)
width = max(self.min_width, width)
widths[i] = width
grow -= width
wtotal -= weight
self._cache_maxcol = maxcol
self._cache_column_widths = widths
return widths
def render(self, size, focus=False):
"""Render columns and return canvas.
size -- (maxcol,) if self.widget_list contains flow widgets or
(maxcol, maxrow) if it contains box widgets.
"""
widths = self.column_widths( size )
if not widths:
return SolidCanvas(" ", size[0], (size[1:]+(1,))[0])
box_maxrow = None
if len(size)==1 and self.box_columns:
box_maxrow = 1
# two-pass mode to determine maxrow for box columns
for i in range(len(widths)):
if i in self.box_columns:
continue
mc = widths[i]
w = self.widget_list[i]
rows = w.rows( (mc,),
focus = focus and self.focus_col == i )
box_maxrow = max(box_maxrow, rows)
l = []
for i in range(len(widths)):
mc = widths[i]
w = self.widget_list[i]
if box_maxrow and i in self.box_columns:
sub_size = (mc, box_maxrow)
else:
sub_size = (mc,) + size[1:]
canv = w.render(sub_size,
focus = focus and self.focus_col == i)
if i < len(widths)-1:
mc += self.dividechars
l.append((canv, i, self.focus_col == i, mc))
canv = CanvasJoin(l)
if canv.cols() < size[0]:
canv.pad_trim_left_right(0, size[0]-canv.cols())
return canv
def get_cursor_coords(self, size):
"""Return the cursor coordinates from the focus widget."""
w = self.widget_list[self.focus_col]
if not w.selectable():
return None
if not hasattr(w, 'get_cursor_coords'):
return None
widths = self.column_widths( size )
if len(widths) < self.focus_col+1:
return None
colw = widths[self.focus_col]
coords = w.get_cursor_coords( (colw,)+size[1:] )
if coords is None:
return None
x,y = coords
x += self.focus_col * self.dividechars
x += sum( widths[:self.focus_col] )
return x, y
def move_cursor_to_coords(self, size, col, row):
"""Choose a selectable column to focus based on the coords."""
widths = self.column_widths(size)
best = None
x = 0
for i in range(len(widths)):
w = self.widget_list[i]
end = x + widths[i]
if w.selectable():
if x > col and best is None:
# no other choice
best = i, x, end
break
if x > col and col-best[2] < x-col:
# choose one on left
break
best = i, x, end
if col < end:
# choose this one
break
x = end + self.dividechars
if best is None:
return False
i, x, end = best
w = self.widget_list[i]
if hasattr(w,'move_cursor_to_coords'):
if type(col)==type(0):
move_x = min(max(0,col-x),end-x-1)
else:
move_x = col
rval = w.move_cursor_to_coords((end-x,)+size[1:],
move_x, row)
if rval is False:
return False
self.focus_col = i
self.pref_col = col
self._invalidate()
return True
def mouse_event(self, size, event, button, col, row, focus):
"""
Send event to appropriate column.
May change focus on button 1 press.
"""
widths = self.column_widths(size)
x = 0
for i in range(len(widths)):
if col < x:
return False
w = self.widget_list[i]
end = x + widths[i]
if col >= end:
x = end + self.dividechars
continue
focus = focus and self.focus_col == i
if is_mouse_press(event) and button == 1:
if w.selectable():
self.set_focus(w)
if not hasattr(w,'mouse_event'):
return False
return w.mouse_event((end-x,)+size[1:], event, button,
col - x, row, focus)
return False
def get_pref_col(self, size):
"""Return the pref col from the column in focus."""
maxcol = size[0]
widths = self.column_widths( (maxcol,) )
w = self.widget_list[self.focus_col]
if len(widths) < self.focus_col+1:
return 0
col = None
if hasattr(w,'get_pref_col'):
col = w.get_pref_col((widths[self.focus_col],)+size[1:])
if type(col)==type(0):
col += self.focus_col * self.dividechars
col += sum( widths[:self.focus_col] )
if col is None:
col = self.pref_col
if col is None and w.selectable():
col = widths[self.focus_col]/2
col += self.focus_col * self.dividechars
col += sum( widths[:self.focus_col] )
return col
def rows(self, size, focus=0 ):
"""Return the number of rows required by the columns.
Only makes sense if self.widget_list contains flow widgets."""
widths = self.column_widths(size)
rows = 1
for i in range(len(widths)):
if self.box_columns and i in self.box_columns:
continue
mc = widths[i]
w = self.widget_list[i]
rows = max( rows, w.rows( (mc,),
focus = focus and self.focus_col == i ) )
return rows
def keypress(self, size, key):
"""Pass keypress to the focus column.
size -- (maxcol,) if self.widget_list contains flow widgets or
(maxcol, maxrow) if it contains box widgets.
"""
if self.focus_col is None: return key
widths = self.column_widths( size )
if self.focus_col < 0 or self.focus_col >= len(widths):
return key
i = self.focus_col
mc = widths[i]
w = self.widget_list[i]
if command_map[key] not in ('cursor up', 'cursor down',
'cursor page up', 'cursor page down'):
self.pref_col = None
key = w.keypress( (mc,)+size[1:], key )
if command_map[key] not in ('cursor left', 'cursor right'):
return key
if command_map[key] == 'cursor left':
candidates = range(i-1, -1, -1) # count backwards to 0
else: # key == 'right'
candidates = range(i+1, len(widths))
for j in candidates:
if not self.widget_list[j].selectable():
continue
self.set_focus_column( j )
return
return key
def selectable(self):
"""Return the selectable value of the focus column."""
return self.widget_list[self.focus_col].selectable()
def _test():
import doctest
doctest.testmod()
if __name__=='__main__':
_test()