[Urwid] New scroll columns proposal.

antonio araujo antoniovazquezaraujo at gmail.com
Sat Nov 19 15:36:57 EST 2005


Hello Ian.
I am working on the table record editor for Deckard, and I need some
scroll posibilities in the columns.
I send you my working version of the class Columns with horizontal scroll.
Take a look. I think may be interesting for urwid.

If you think this can be a problem with the rest of your library, then
say me if you are working in the same direction. I need something like
this to edit tables.

Cheers.
Antonio.
-------------- next part --------------
#!/usr/bin/python
#
# Urwid tour.  It slices, it dices.. 
#    Copyright (C) 2004-2005  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/

"""
Urwid tour.  Shows many of the standard widget types and features.
"""
import logging


import urwid
import urwid.curses_display
import urwid.web_display

try: True # old python?
except: False, True = 0, 1

# use appropriate Screen class
if urwid.web_display.is_web_request():
	Screen = urwid.web_display.Screen
else:
	Screen = urwid.curses_display.Screen

################################################################################
class NewColumns: # either FlowWidget or BoxWidget
	def __init__(self, widget_list, dividechars=0, focus_column=0,
		min_width=1):
		"""
		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
		min_width -- minimum width for each column before it is hidden

		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)	
		"""
		self.widget_list = 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))
			else:
				raise ColumnsError, "widget list item invalid: %s" % `w`
		
		self.dividechars = dividechars
		self.focus_col = focus_column
		self.pref_col = None
		self.min_width = min_width

		self.start_index = 0 
		
	def get_num_columns(self):	
		return len(self.columns)
	def get_last_column_index(self):	
		return len(self.columns) -1
	def get_column(self, column_index):	
		return self.columns[column_index]
	def set_column_focus(self, column_index):
		self.focus_col = column_index
	def get_column_focus(self):
		return self.focus_col
	def get_last_widget_index(self):
		return len(self.widget_list)-1
	def get_num_widgets(self):
		return len(self.widget_list)
	def set_start_index(self, index):
		self.start_index =index 
	def inc_start_index(self):
		self.start_index +=1
		if self.start_index >self. get_last_widget_index():
			self.start_index = self.get_last_widget_index() 
	def dec_start_index(self):
		self.start_index -=1
		if self.start_index < 0:
			self.start_index = 0
	def get_start_index(self):
		return self.start_index
	def get_widget(self, widget_index):
		return self.widget_list[widget_index]
	def get_widget_at_column(self, col_index):
		return self.widget_list[self.start_index+col_index]
	def recalc_columns(self, size):
		"""Recompute a list of column columns.

		size -- (maxcol,) if self.widget_list contains flow widgets or
			(maxcol, maxrow) if it contains box widgets.
		"""
		maxcol = size[0]

		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)
			
		self.columns=[]
		
		weighted = []
		shared = maxcol + self.dividechars
		growable = 0
		
		i = 0
		#for t, width in col_types:
		for t, width in col_types[self.start_index:]:
			if t == 'fixed':
				static_w = width
			else:
				static_w = self.min_width
				
			if shared < static_w + self.dividechars:
				break
		
			self.columns.append( static_w )	
			shared -= static_w + self.dividechars
			if t != 'fixed':
				weighted.append( (width,i) )
		
			i += 1
		
		if not shared:
			return 
			
		# 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)
			self.columns[i] = width
			grow -= width
			wtotal -= weight


	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.
		"""
		self.recalc_columns(size)
		
		l = []
		off = 0
		for column_index in range(self.get_num_columns()):
			column_size = self.get_column(column_index) 
			widget = self.get_widget_at_column(column_index) 
			sub_size = (column_size,) + size[1:]
			
			widget_has_focus = False 
			if focus == True:
				if column_index == self.get_column_focus(): 
					widget_has_focus = True 

			l.append(widget.render(sub_size, widget_has_focus) )
				
			off = column_size + self.dividechars 
			l.append(off)
		return urwid.CanvasJoin( l[:-1] )

	def get_cursor_coords(self, size):
		"""Return the cursor coordinates from the focus widget."""
		widget = self.get_widget_at_column(self.get_column_focus())

		if not widget.selectable():
			return None
		if not hasattr(widget, 'get_cursor_coords'):
			return None

		col_size =self.get_column(self.get_column_focus())

		coords = widget.get_cursor_coords( (col_size,)+size[1:] )
		if coords is None:
			return None
		x,y = coords
		x += self.get_column_focus() * self.dividechars

		total_size = 0
		for col_index in range(self.get_column_focus()):
			total_size += self.get_column(col_index)
		x += total_size 
		return x, y

	def move_cursor_to_coords(self, size, col, row):
		"""Choose a selectable column to focus based on the coords."""
		self.recalc_columns(size)
		
		best = None
		x = 0
		for i in range(len(columns)):
			w = self.widget_list[i]
			end = x + columns[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'):
			rval = w.move_cursor_to_coords((end-x,)+size[1:],
				min(max(0,col-x),end-x-1), row)
			if rval is False:
				return False
				
		self.focus_col = i
		self.pref_col = col
		return True

	def get_pref_col(self, size):
		"""Return the pref col from the column in focus."""
		maxcol = size[0]
		self.recalc_columns((maxcol,))
	
		w = self.widget_list[self.focus_col]
		col = None
		if hasattr(w,'get_pref_col'):
			col = w.get_pref_col((columns[self.focus_col],)+size[1:])
			if col is not None:
				col += self.focus_col * self.dividechars
				col += sum( columns[:self.focus_col] )
		if col is None:
			col = self.pref_col
		if col is None and w.selectable():
			col = columns[self.focus_col]/2
			col += self.focus_col * self.dividechars
			col += sum( columns[:self.focus_col] )
		return col

	def rows(self, (maxcol,), focus=0 ):
		"""Return the number of rows required by the columns.
		Only makes sense if self.widget_list contains flow widgets."""
		self.recalc_columns((maxcol,))
	
		rows = 0
		for column_index in range(self.get_num_columns()):
			column_size = self.get_column(column_index)
			widget = self.get_widget_at_column(column_index) 
			widget_has_focus = False 
			if focus == True:
				if column_index == self.get_column_focus(): 
					widget_has_focus = True 

			rows = max( 
						rows, 
						widget.rows( (column_size,), widget_has_focus ) 
					)
		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
		
		self.recalc_columns(size)
		focus_col = self.get_column_focus()
		if focus_col < 0 or focus_col > self.get_last_column_index():
			return key

		column_index  = self.get_column_focus() 
		column_size = self.get_column(column_index)
		widget = self.get_widget_at_column(column_index)
		if key not in ('up','down','page up','page down'):
			self.pref_col = None
		key = widget.keypress( (column_size,)+size[1:], key )
		
		if key not in ('left','right'):
			return key

		if key == 'left':
			prev_column_focus = column_index - 1
			first_column_focus = 0 
			if column_index-1 <= 0:
				if self.get_start_index() > 0:
					self.dec_start_index()
				else:
					prev_column_focus = 0 
					first_column_focus = -1 
			else:	
				if prev_column_focus < 0:
					prev_column_focus = 0
			candidates = range(prev_column_focus,first_column_focus, -1) 
		else: # key == 'right'
			if column_index+1 >= self.get_last_column_index():
				if self.get_start_index() <= self.get_last_widget_index(): 
					self.inc_start_index()

			next_column_focus = column_index + 1
			if next_column_focus > self.get_last_column_index():
				next_column_focus = self.get_last_column_index()-1 
				
			if next_column_focus < 0 :
				next_column_focus = 0

			last_column_focus = self.get_last_column_index() 
			candidates = range(next_column_focus, last_column_focus)

		for j in candidates:
			if not self.get_widget_at_column(j).selectable():
				continue

			self.set_column_focus(j)
			return
		return key
			

	def selectable(self):
		"""Return the selectable value of the focus column."""
		return self.widget_list[self.focus_col].selectable()

################################################################################

blank = urwid.Text("")

HEADER = urwid.AttrWrap( urwid.Text("WORKING SCROLL COLUMNS!  "
	"UP / DOWN / PAGE UP / PAGE DOWN scroll.  F8 exits."), 'header')

B_PRESS_FRAME = None # this is set in TourDisplay.__init__
def B_PRESS( button ):
	B_PRESS_FRAME.footer = urwid.AttrWrap( urwid.Text(
		["Pressed: ",button.get_label()]), 'header')

RADIO_LIST = []

CONTENT = [
	NewColumns( [ 
		(
			'fixed', 
			10, 
			urwid.AttrWrap( 
			urwid.Edit( 
				('editcp',"Edit"),
				"1111111111",
				multiline=False),
			'editbx','editfc') 
		),
		(
			'fixed', 
			20, 
			urwid.AttrWrap( 
			urwid.Edit( 
				('editcp',"Edit"),
				"2222222222",
				multiline=False),
			'editbx','editfc') 
		),
		(
			'fixed', 
			30, 
			urwid.AttrWrap( 
			urwid.Edit( 
				('editcp',"Edit"),
				"333333333333",
				multiline=False),
			'editbx','editfc') 
		),
		(
			'fixed', 
			40, 
			urwid.AttrWrap( 
			urwid.Edit( 
				('editcp',"Edit"),
				"444444444444",
				multiline=False),
			'editbx','editfc') 
		),
		(
			'fixed', 
			50, 
			urwid.AttrWrap( 
			urwid.Edit( 
				('editcp',"Edit"),
				"5555555555555",
				multiline=False),
			'editbx','editfc') 
		),
		(
			'fixed', 
			60, 
			urwid.AttrWrap( 
			urwid.Edit( 
				('editcp',"Edit"),
				"6666666666666666",
				multiline=False),
			'editbx','editfc') 
		)
	])

]
	
	


class TourDisplay:
	palette = [
		('body','black','light gray', 'standout'),
		('reverse','light gray','black'),
		('header','white','dark red', 'bold'),
		('important','dark blue','light gray',('standout','underline')),
		('editfc','white', 'dark blue', 'bold'),
		('editbx','light gray', 'dark blue'),
		('editcp','black','light gray', 'standout'),
		('bright','dark gray','light gray', ('bold','standout')),
		('buttn','black','dark cyan'),
		('buttnf','white','dark blue','bold'),
		]
	
	def __init__(self):
		self.listbox = urwid.ListBox( CONTENT )
		view = urwid.AttrWrap(self.listbox, 'body')
		self.view = urwid.Frame( view, header=HEADER )

		global B_PRESS_FRAME
		B_PRESS_FRAME = self.view
		
	
	def main(self):
		self.ui = Screen()
		self.ui.register_palette( self.palette )
		self.ui.run_wrapper( self.run )
	
	def run(self):
		size = self.ui.get_cols_rows()
		while 1:
			canvas = self.view.render( size, focus=1 )
			self.ui.draw_screen( size, canvas )
			keys = None
			while not keys: 
				keys = self.ui.get_input()
			for k in keys:
				if k == 'window resize':
					size = self.ui.get_cols_rows()
				elif k == 'f8':
					return
				self.view.keypress( size, k )

def main():
	logging.basicConfig(
		level=logging.DEBUG,
		format='%(asctime)s %(levelname)s %(message)s', 
			filename='columns_log.txt', 
			filemode='w'
	)
	urwid.web_display.set_preferences("SCROLL COLUMNS")
	# try to handle short web requests quickly
	if urwid.web_display.handle_short_request():
		return
		
	TourDisplay().main()
	
if '__main__'==__name__ or urwid.web_display.is_web_request():
	main()


More information about the Urwid mailing list