Ian Ward wrote:
I think that's a great idea. Florian Festi mentioned a similar
discomfort with the way Columns and Piles align widgets, but suggested
adding alignment attributes to the widgets as a solution. I like your
approach better because it lends itself more to Urwid's style of
widget-reuse and small, simple widget classes.
I look forward to your patch :-)
Hi there,
I've been experimenting with the best way to implement this approach,
and the attached patch is what I eventually came up with. I've created a
"hint" module that contains a set of decorator classes that hints to
containing classes how the widget might best be displayed. A quick example:
import urwid
import urwid.hint as hint
...
pile = urwid.Pile([
hint.Fixed(widget1, size = 5),
hint.Flow(widget2),
widget3,
hint.Weight(widget4, 2.0)
])
I've adapted the Columns and Pile widgets to work with the layout hint
wrappers. The code is essentially mostly unchanged; I just replaced all
of the tuple-handling code with isinstance checks, etc. I also updated
the unit tests in test_urwid.py.
I'm undecided on the syntax, but it seems reasonable.
What are your thoughts?
--
James Reeves
Index: test_urwid.py
===================================================================
--- test_urwid.py (revision 137)
+++ test_urwid.py (working copy)
@@ -23,6 +23,7 @@
from __future__ import nested_scopes
import urwid
+import urwid.hint as hint
import unittest
from test import test_support
@@ -1914,21 +1915,21 @@
self.cwtest( "squish 4+1", [x,x,x,x], 1, (6,), [1,2,1] )
self.cwtest( "squish 4+1", [x,x,x,x], 1, (4,), [2,1] )
- self.cwtest( "fixed 3", [('fixed',4,x),('fixed',6,x),
- ('fixed',2,x)], 1, (25,), [4,6,2] )
- self.cwtest( "fixed 3 cut", [('fixed',4,x),('fixed',6,x),
- ('fixed',2,x)], 1, (13,), [4,6] )
- self.cwtest( "fixed 3 cut2", [('fixed',4,x),('fixed',6,x),
- ('fixed',2,x)], 1, (10,), [4] )
+ self.cwtest( "fixed 3", [hint.Fixed(x,4),hint.Fixed(x,6),
+ urwid.hint.Fixed(x,2)], 1, (25,), [4,6,2] )
+ self.cwtest( "fixed 3 cut", [hint.Fixed(x,4),hint.Fixed(x,6),
+ urwid.hint.Fixed(x,2)], 1, (13,), [4,6] )
+ self.cwtest( "fixed 3 cut2", [hint.Fixed(x,4),hint.Fixed(x,6),
+ urwid.hint.Fixed(x,2)], 1, (10,), [4] )
- self.cwtest( "mixed 4", [('weight',2,x),('fixed',5,x),
- x, ('weight',3,x)], 1, (14,), [2,5,1,3] )
- self.cwtest( "mixed 4 a", [('weight',2,x),('fixed',5,x),
- x, ('weight',3,x)], 1, (12,), [1,5,1,2] )
- self.cwtest( "mixed 4 b", [('weight',2,x),('fixed',5,x),
- x, ('weight',3,x)], 1, (10,), [2,5,1] )
- self.cwtest( "mixed 4 c", [('weight',2,x),('fixed',5,x),
- x, ('weight',3,x)], 1, (20,), [4,5,2,6] )
+ self.cwtest( "mixed 4", [hint.Weight(x,2),hint.Fixed(x,5),
+ x, urwid.hint.Weight(x,3)], 1, (14,), [2,5,1,3] )
+ self.cwtest( "mixed 4 a", [hint.Weight(x,2),hint.Fixed(x,5),
+ x, urwid.hint.Weight(x,3)], 1, (12,), [1,5,1,2] )
+ self.cwtest( "mixed 4 b", [hint.Weight(x,2),hint.Fixed(x,5),
+ x, urwid.hint.Weight(x,3)], 1, (10,), [2,5,1] )
+ self.cwtest( "mixed 4 c", [hint.Weight(x,2),hint.Fixed(x,5),
+ x, urwid.hint.Weight(x,3)], 1, (20,), [4,5,2,6] )
def mctest(self, desc, l, divide, size, col, row, exp, f_col, pref_col):
c = urwid.Columns( l, divide )
Index: urwid/__init__.py
===================================================================
--- urwid/__init__.py (revision 137)
+++ urwid/__init__.py (working copy)
@@ -42,3 +42,4 @@
from graphics import *
from canvas import *
from font import *
+import hint
Index: urwid/hint.py
===================================================================
--- urwid/hint.py (revision 0)
+++ urwid/hint.py (revision 0)
@@ -0,0 +1,25 @@
+class LayoutHint:
+ """Decorator that gives Pile and Columns widgets a hint at how the contained
+ widgets should be arranged."""
+
+ def __init__(self, widget):
+ self.w = widget
+
+ def __getattr__(self, name):
+ """Call getattr on wrapped widget."""
+ return getattr(self.w, name)
+
+class Flow(LayoutHint):
+ """Hints that the contained widget should be treated as a flow widget."""
+
+class Weight(LayoutHint):
+ """Uses a weighting value to hint at the contained widget's size."""
+ def __init__(self, widget, weight):
+ LayoutHint.__init__(self, widget)
+ self.weight = weight
+
+class Fixed(LayoutHint):
+ """Hints that the contained widget should be a fixed size."""
+ def __init__(self, widget, size):
+ LayoutHint.__init__(self, widget)
+ self.size = size
Index: urwid/widget.py
===================================================================
--- urwid/widget.py (revision 137)
+++ urwid/widget.py (working copy)
@@ -23,6 +23,7 @@
from util import *
from canvas import *
+import hint
try: sum # old python?
except: sum = lambda l: reduce(lambda a,b: a+b, l, 0)
@@ -700,13 +701,13 @@
def keypress(self,(maxcol,),key):
"""Handle editing keystrokes. Return others."""
- if key in list("0123456789"):
+ if key in list("0123456789"):
# trim leading zeros
- while self.edit_pos > 0 and self.edit_text[:1] == "0":
- self.set_edit_pos( self.edit_pos - 1)
- self.set_edit_text(self.edit_text[1:])
+ while self.edit_pos > 0 and self.edit_text[:1] == "0":
+ self.set_edit_pos( self.edit_pos - 1)
+ self.set_edit_text(self.edit_text[1:])
- unhandled = Edit.keypress(self,(maxcol,),key)
+ unhandled = Edit.keypress(self,(maxcol,),key)
return unhandled
@@ -2115,50 +2116,38 @@
pass
class Pile(Widget): # either FlowWidget or BoxWidget
- def __init__(self, widget_list, focus_item=None):
+ 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 objects in widget_list may be wrapped in a LayoutHint:
+ hint.Flow(widget) always treat widget as a flow widget
+ hint.Fixed(widget, size) give this box widget a fixed height
+ hint.Weight(widget, weight) 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)
+ widgets not in a tuple are the same as hint.Weight(widget, 1)
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
+ for w in self.widget_list:
+ if w.selectable():
+ focus_item = w
+ break
+ if focus_item is None:
+ focus_item = self.widget_list[0]
+
self.set_focus(focus_item)
self.pref_col = None
@@ -2194,15 +2183,16 @@
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:
+ w = self.widget_list[i]
+ if isinstance(w, hint.Fixed):
+ return (maxcol, w.size)
+ elif isinstance(w, hint.Flow) or len(size) == 1:
+ return (maxcol,)
+ else:
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):
"""
@@ -2218,10 +2208,9 @@
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 )
+ for w in self.widget_list:
+ if isinstance(w, hint.Fixed):
+ l.append( w.size )
else:
l.append( w.rows( (maxcol,), focus=focus
and self.focus_item == w ))
@@ -2230,18 +2219,21 @@
# 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':
+ for w in self.widget_list:
+ if isinstance(w, hint.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
+ elif isinstance(w, hint.Fixed):
+ l.append(w.size)
+ remaining -= w.size
+ elif isinstance(w, hint.Weight):
+ l.append(None)
+ wtotal += w.weight
else:
l.append(None)
- wtotal += height
+ wtotal += 1.0
if wtotal == 0:
raise PileError, "No weighted widgets found for Pile treated as a box widget"
@@ -2249,15 +2241,15 @@
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)
+ for i, w in enumerate(self.widget_list):
+ if l[i] is None:
+ weight = 1
+ if isinstance(w, hint.Weight):
+ weight = w.weight
+ rows = int(float(remaining) * weight / wtotal + 0.5)
l[i] = rows
remaining -= rows
- wtotal -= height
- i += 1
+ wtotal -= weight
return l
@@ -2270,14 +2262,13 @@
item_rows = None
combinelist = []
- i = 0
- for (f, height), w in zip(self.item_types, self.widget_list):
+ for i, w in enumerate(self.widget_list):
item_focus = self.focus_item == w
canv = None
- if f == 'fixed':
- canv = w.render( (maxcol, height),
+ if isinstance(w, hint.Fixed):
+ canv = w.render( (maxcol, w.size),
focus=focus and item_focus)
- elif f == 'flow' or len(size)==1:
+ elif isinstance(w, hint.Flow) or len(size) == 1:
canv = w.render( (maxcol,),
focus=focus and item_focus)
else:
@@ -2290,7 +2281,6 @@
focus=focus and item_focus )
if canv:
combinelist.append((canv, i, item_focus))
- i+=1
return CanvasCombine(combinelist)
@@ -2301,12 +2291,13 @@
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]
+ w = self.focus_item
+ i = self.widget_list.index(w)
item_rows = None
maxcol = size[0]
- if f == 'fixed' or (f=='weight' and len(size)==2):
- if f == 'fixed':
+ if isinstance(w, hint.Fixed) or (len(size) == 2 and
+ not isinstance(w, hint.Flow)):
+ if isinstance(w, hint.Fixed):
maxrow = height
else:
if item_rows is None:
@@ -2344,7 +2335,7 @@
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 )
@@ -2369,7 +2360,6 @@
if not hasattr(self.focus_item,'move_cursor_to_coords'):
return
- f, height = self.item_types[j]
rows = item_rows[j]
if key=='up':
rowlist = range(rows-1, -1, -1)
@@ -2469,11 +2459,11 @@
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
+ widget objects in widget_list may be wrapped in a LayoutHint:
+ hint.Fixed(widget, size) give this box widget a fixed height
+ hint.Weight(widget, weight) give this column a relative weight
- widgets not in a tuple are the same as ('weight', 1, widget)
+ widgets not in a tuple are the same as hint.Weight(widget, 1)
box_columns is ignored when this widget is being used as a
box widget because in that case all columns are treated as box
@@ -2481,27 +2471,18 @@
"""
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:
+ for i, w in enumerate(self.widget_list):
+ if w.selectable():
+ focus_column = i
+ break
+ if focus_column is None:
focus_column = 0
- self.focus_col = focus_column
+
+ self.focus_col = focus_column
+ self.dividechars = dividechars
self.pref_col = None
self.min_width = min_width
self.box_columns = box_columns
@@ -2545,24 +2526,15 @@
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=[]
-
+
+ widths = []
weighted = []
shared = maxcol + self.dividechars
growable = 0
- i = 0
- for t, width in col_types:
- if t == 'fixed':
- static_w = width
+ for i, w in enumerate(self.widget_list):
+ if isinstance(w, hint.Fixed):
+ static_w = w.size
else:
static_w = self.min_width
@@ -2571,11 +2543,11 @@
widths.append( static_w )
shared -= static_w + self.dividechars
- if t != 'fixed':
- weighted.append( (width,i) )
+ if isinstance(w, hint.Weight):
+ weighted.append( (w.weight, i) )
+ elif not isinstance(w, hint.Fixed):
+ weighted.append( (1, i) )
- i += 1
-
if shared:
# divide up the remaining space between weighted cols
weighted.sort()
_______________________________________________
Urwid mailing list
[email protected]
http://lists.excess.org/mailman/listinfo/urwid