#!/usr/bin/env python
#
# Widgets.py
# Copyright (C) 2003 Michael Leonhard
# http://tamale.net/
#
# version 1.0 RC1 (2003/07/06 00:19)

import string, pygame, pygame.event, pygame.font
from pygame.locals import *

# initialize modules
pygame.font.init()

TABTARGET = 1
BACKWARD = 1
# keyboard repeat event
KEYREPEAT = NUMEVENTS - 1
KEYREPEATTIME = 50 #in milliseconds
KEYREPEATWAIT = 10

# colors
WHITE = (0xFF, 0xFF, 0xFF, 0xFF)
NAVYBLUE = (0x22, 0x44, 0x66, 0xFF)
BLACK = (0x00, 0x00, 0x00, 0xFF)
DARKGRAY = (0x40, 0x40, 0x40, 0xFF)
GRAY = (0x80, 0x80, 0x80, 0xFF)
RED = (0xFF, 0x00, 0x00, 0xFF)
BLUE = (0x00, 0x00, 0xFF, 0xFF)
GREEN = (0x00, 0xFF, 0x00, 0xFF)
YELLOW = (0x00, 0xFF, 0xFF, 0xFF)

class WidgetException( Exception ):
	"""Widget Exception class"""
	def __init__( self, reason ):
		"""Initialize a new Widget exception"""
		self.reason = reason
	
	def __str__( self ):
		"""String representation of exception"""
		return `self.reason`

class WidgetWindow:
	def __init__( self, screen, background = NAVYBLUE ):
		"""Initialize WidgetWindow class"""
		# data
		self.screen = screen
		self.widgets = []
		self.reversewidgets = []
		self.taborder = []
		self.mousedest = None
		self.keydest = None
		self.background = background
		self.dirtyrect = self.screen.get_rect()
		self.focuslosecallback = None
		self.lastkeydownevent = None
		self.keyrepeatwaitcount = 0
		
		# initial blank screen draw
		self.eventproc( pygame.event.Event( NOEVENT, {} ) )
	
	def addwidget( self, widget, tabable=None ):
		"""Add a widget to the window"""
		self.widgets.append( widget )
		self.reversewidgets.insert( 0, widget )
		self.invalidaterect( widget.rect )
		
		# widget is tabable
		if tabable:
			# add to tab order
			self.taborder.append( widget )
	
	def prevtab( self, widget ):
		"""Switch the keyboard focus to the previous widget in the tab order"""
		# call nexttab with the BACKWARD option
		self.nexttab( widget, BACKWARD )
	
	def nexttab( self, widget, backward=None ):
		"""Switch the keyboard focus to next widget in the tab order"""
		
		last = len( self.taborder ) - 1
		
		# no tabable widgets available
		if last < 0:
			# error
			raise WidgetException( "no tabable widgets available" )
		
		# no focus currently
		if not self.keydest:
			# moving backward
			if backward:
				# set to last widget in taborder
				self.taborder[last].focus()
			# moving forward
			else:
				# set to first widget in taborder
				self.taborder[0].focus()
			return
		
		# catch exception
		try:
			# find index of focused widget
			focused = self.taborder.index( self.keydest )

			# moving backward
			if backward:
				# next widget
				focused -= 1
				# was first widget
				if focused < 0:
					# use last widget
					focused = last
			# moving forward
			else:
				# next widget
				focused += 1
				# was last widget
				if focused > last:
					# use first widget
					focused = 0
		
		# currently focused widget is not in tab order
		except ValueError:
			# moving backward
			if backward:
				# use the last widget in the tab order
				focused = last
			# moving forward
			else:
				# use the first widget in the tab order
				focused = 0
		
		# focus the widget
		self.taborder[focused].focus()
	
	def grabmouse( self, widget ):
		"""Cause all mouse messages to be received by specified widget"""
		self.mousedest = widget
	
	def grabkey( self, widget, focuslosecallback = None, sendrepeats = None ):
		"""Cause all keyboard messages to be received by specified widget"""
		# previous widget requested callback
		if self.keydest and self.focuslosecallback:
			# inform widget that it is losing key focus
			self.focuslosecallback( self.keydest )
		
		# widget not specified so unset
		if widget == None:
			# unregister event
			pygame.time.set_timer( KEYREPEAT, 0 )
			# keyboard focus is unset
			self.keydest = None
			self.focuslosingcallback = None
		
		# widget is specified
		else:
			# remember widget and specified callback
			self.keydest = widget
			self.focuslosecallback = focuslosecallback
			
			# keyboard repeats were requested
			if sendrepeats:
				# register regular event for repeats
				pygame.time.set_timer( KEYREPEAT, KEYREPEATTIME )
		
		# stop current repeat
		self.lastkeydownevent = None
		self.keyrepeatwaitcount = 0
		
	
	def invalidaterect( self, rect=None ):
		"""Cause the specified region of the screen to be redrawn"""
		# no rect supplied
		if not rect:
			# use entire screen
			self.dirtyrect = self.screen.get_rect()
			return
		
		# previous invalid rect exists
		if self.dirtyrect:
			# combine the two regions
			self.dirtyrect.union_ip( rect )
		# this is first invalid rect
		else: self.dirtyrect = Rect( rect )
	
	def dispatch( self, event ):
		"""Send the event to the widget under the mouse"""
		# check each widget, youngest to oldest
		for w in self.reversewidgets:
			# event occurs in widget's region
			if w.rect.collidepoint( event.pos ):
				# let the widget process the event
				w.eventproc( event )
				# only deliver event to one widget
				break
	
	def eventproc( self, event ):
		"""Dispatch events to widgets and redraw the screen"""
		# mouse events
		if event.type == MOUSEMOTION or event.type == MOUSEBUTTONDOWN or event.type == MOUSEBUTTONUP:
			# mouse is grabbed
			if self.mousedest != None:
				self.mousedest.eventproc( event )
			# not grabbed
			else: self.dispatch( event )
		
		# keyboard events
		elif event.type == KEYDOWN or event.type == KEYUP:
			# keyboard is grabbed
			if self.keydest:
				# send the keyboard event to the widget that has keyboard focus
				self.keydest.eventproc( event )
				# keydown event
				if event.type == KEYDOWN:
					# save the event for repeats
					self.lastkeydownevent = event
					# reset the wait count
					self.keyrepeatwaitcount = 0
				# keyup event
				else:
					# no repeats after keyup
					self.lastkeydownevent = None
		
		# regular event for keyboard repeats
		elif  event.type == KEYREPEAT:
			# keyboard is grabbed
			if self.keydest:
				# keydown event was saved
				if self.lastkeydownevent:
					# waited long enough
					if self.keyrepeatwaitcount > KEYREPEATWAIT:
						# resend the keydown event to the widget
						self.keydest.eventproc( self.lastkeydownevent )
					# must wait longer
					else: self.keyrepeatwaitcount += 1
		
		# there is stuff to be drawn
		if self.dirtyrect:
			# draw the background of the area to be drawn
			self.screen.set_clip( self.dirtyrect )
			self.screen.fill( self.background, self.dirtyrect )
			
			# check each widget
			for w in self.widgets:
				# widget is covered by invalidated rectangle
				if self.dirtyrect.colliderect( w.rect ):
					# keep the clipping area
					self.screen.set_clip( self.dirtyrect )
					
					# draw the widget
					w.draw( self.screen )
			
			# debug clipping regions
			#self.screen.set_clip()
			#self.screen.fill( (self.debugred, self.debugred, self.debugred, 0x80), self.dirtyrect )
			#self.debugred += 16
			#self.debugred %= 256
			
			# screen is clean
			self.dirtyrect = None
			
			# flip the display
			pygame.display.update()

class ButtonClass:
	def __init__( self, manager, action, rect, text="OK", foreground=WHITE, shadowcolor=BLACK, textcolor=BLACK, fontsize=24 ):
		"""Initialize the Button class"""
		self.action = action
		self.manager = manager
		self.rect = Rect( rect )
		self.text = None
		self.foreground = foreground
		self.shadowcolor = shadowcolor
		self.textcolor = textcolor
		self.pressed = 0
		self.buttondown = 0
		
		# button area
		self.button = Rect( self.rect )
		self.button.width -= 5
		self.button.height -= 5
		
		# shadow area
		self.shadow = Rect( self.button )
		self.shadow.right += 5
		self.shadow.bottom += 5
		
		# font
		self.font = pygame.font.Font( None, fontsize )
		
		# update button text
		self.settext( text )
	
	def settext( self, text ):
		# new text is same as old
		if self.text == text:
			# do nothing
			return
		
		# save text for action functions
		self.text = text
		# remove trailing whitespace
		text = text.rstrip()
		# if text is empty
		if len( text ) == 0:
			# use a space
			text = " "
		
		# make text
		self.surface = self.font.render( text, 1, self.textcolor, self.foreground )
		# optimize for blitting
		self.surface = self.surface.convert()
		
		# text position (unpressed)
		textrect = self.surface.get_rect()
		textrect.center = self.button.center
		self.textrect = Rect( textrect )
		
		# text position (pressed)
		textrect.center = self.shadow.center
		self.textrectpressed = textrect
		
		# cause widget to be drawn
		self.manager.invalidaterect( self.rect )
	
	def eventproc( self, event ):
		"""Process mouse events"""
		# button is clicked
		if event.type == MOUSEBUTTONDOWN and self.button.collidepoint( event.pos):
			self.pressed = 1
			self.manager.invalidaterect( self.rect )
			# watch the mouse
			self.buttondown = 1
			self.manager.grabmouse( self )
		
		# button is released
		if event.type == MOUSEBUTTONUP and self.buttondown == 1:
			# done watching the mouse
			self.buttondown = 0
			self.manager.grabmouse( None )

			# button was in pressed position
			if self.pressed == 1:
				self.pressed = 0
				self.manager.invalidaterect( self.rect )

				# do button action
				self.action()
		
		# mouse cursor moves and holds mouse button
		if event.type == MOUSEMOTION and self.buttondown == 1:
			# button is in pressed position
			if self.pressed == 1:
				# cursor is not over the button
				if not self.rect.collidepoint( event.pos ):
					# button becomes unpressed
					self.pressed = 0
					self.manager.invalidaterect( self.rect )
			# button is in unpressed position
			else:
				# cursor is over the button
				if self.button.collidepoint( event.pos ):
					# button becomes pressed 
					self.pressed = 1
					self.manager.invalidaterect( self.rect )
		
	def draw( self, screen ):
		"""Draw the widget"""
		# clip our drawing
		screen.set_clip( screen.get_clip().clip( self.rect ) )
		
		# button is in pressed state
		if self.pressed == 1:
			# draw the button
			screen.fill( self.foreground, self.shadow )
			# blit the text
			screen.blit( self.surface, self.textrectpressed )

		
		# button is in unpressed state
		else:
			# draw the shadow
			screen.fill( self.shadowcolor, self.shadow )
		
			# draw the button
			screen.fill( self.foreground, self.button )

			# blit the text
			screen.blit( self.surface, self.textrect )

class PageClass:
	def eventproc( self, event ):
		"""Process mouse events"""
		# button is clicked
		if event.type == MOUSEBUTTONDOWN:
			(x, y) = event.pos
			# click is on the scroll shuttle
			if self.shuttlerect.collidepoint( event.pos ):
				# distance from the click to the bottom of the shuttle
				self.clickoffset = self.shuttlerect.bottom - y
				# begin scrolling
				self.manager.grabmouse( self )
				self.scrollgrabbed = 1
		
		# button is released
		if event.type == MOUSEBUTTONUP:
			# currently scrolling
			if self.scrollgrabbed == 1:
				# final scroll position
				self.scroll( event.pos )
				# done scrolling
				self.manager.grabmouse( None )
				self.scrollgrabbed = 0
		
		# mouse cursor moves
		if event.type == MOUSEMOTION:
			# scroll bar is grabbed
			if self.scrollgrabbed == 1: self.scroll( event.pos )
	
	def __init__( self, manager, rect, background=(0, 0, 0, 0), fontsize=24, maxlines=40, foreground=WHITE ):
		"""Initialize scrollable text display"""
		self.manager = manager
		self.rect = Rect( rect )
		self.background = background
		self.font = pygame.font.Font( None, fontsize )
		self.maxlines = maxlines
		self.foreground = foreground
		
		# Scroll bar
		scrollwidth = 10
		self.scrollgrabbed = 0
		
		# list of surfaces with line text, youngest is first
		self.lines = []

		# line that is visible at the bottom of page (0 is youngest line)
		self.showline = 0
		
		# page rect
		self.pagerect = Rect( rect )
		self.pagerect.width -= scrollwidth
		
		# scrollbar area
		self.scrollrect = Rect( rect )
		self.scrollrect.width = scrollwidth
		self.scrollrect.right = self.rect.right
		
		# shuttle initially fills scrollbar
		self.shuttlerect = Rect( self.scrollrect )
		
		# blank line to start with
		self.append( " " )

	def append( self, newtext, foreground=None ):
		"""Add one or more new lines"""
		# use default foreground color
		if foreground == None: foreground = self.foreground
		
		# expand tabs
		newtext = newtext.expandtabs()
		
		# split text into lines
		for line in newtext.splitlines():
				# append the line
				self.appendline( line, foreground )
	
	def fits( self, text ):
		"""Check if text fits on one line"""
		# size of the text
		(fw, fh) = self.font.size( text )
		# too wide
		if fw > self.pagerect.width: return 0
		return 1
	
	def appendline( self, newtext, foreground ):
		"""Find and prepare each line of new text"""
		# remove trailing whitespace
		newtext = newtext.rstrip()
		
		# find largest piece of text that will fit on one line
		pt = len( newtext )
		while not self.fits( newtext[:pt] ):
			# find the last space
			pt = newtext.rfind( " ", 0, pt )
			# no spaces left
			if pt == -1: break
		
		# first word doesn't fit
		if pt == -1:
			# break word
			pt = len( newtext )
			while not self.fits( newtext[:pt] ):
				# not even one letter will fit
				if pt == 1: raise Exception
				# move the break one letter to the left
				pt -= 1
		# found string that fits on line
		else:
			# include the space
			pt += 1
		
		# this text fits on the line
		thistext = newtext[:pt]
		
		# check that line is not empty
		if len( thistext ) == 0:
			# use a space
			thistext = " "
		
		# make a new surface
		surface = self.font.render( thistext, 1, foreground, self.background )

		# optimize for blitting with transparency
		surface = surface.convert()
		
		# all of the lines should be the same height
		self.lineheight = surface.get_height()
		# add the line surface to the list
		self.lines.insert( 0, surface )
		# too many lines
		while len( self.lines ) > self.maxlines:
			# discard last line
			del self.lines[-1]
		
		# if not scrolled to the bottom
		if self.showline != 0:
			# keep scroll bar on same line
			self.showline += 1
			# if line disappears then stay at the top
			if self.showline >= len( self.lines ): self.showline = len( self.lines ) - 1
		
		# resize scrollbar
		self.scrollcheck()
		
		# text that wouldn't fit
		newtext = newtext[pt:]
		
		# leftover wrapped text
		if len( newtext ) > 0:
			# do the leftovers
			self.appendline( newtext, foreground )
		# no more lines to be added
		else:
			# need screen update
			self.manager.invalidaterect( self.rect )
	
	def scrollcheck( self ):
		"""Adjust the scroll shuttle after text is added"""
		#print ""
		
		# How many lines are visible?
		visiblelines = self.pagerect.height / self.lineheight
		#print "visiblelines", visiblelines
		
		#print "lines", len( self.lines )
		
		# All the lines fit on the page
		if len( self.lines ) <= visiblelines:
			self.shuttlerect = Rect( self.scrollrect )
			return
		
		# keep the oldest line at the top or above the page
		if self.showline > len( self.lines ) - visiblelines:
			self.showline = len( self.lines ) - visiblelines
		
		# Determine height of the shuttle
		self.shuttlerect.height = int( (float( visiblelines ) / float( len( self.lines ) )) * float( self.pagerect.height ) )
		#print "shuttle height", self.shuttlerect.height
		
		# Number of lines that are off the page (top and bottom)
		linetravel = len( self.lines ) - visiblelines
		#print "linetravel", linetravel
		
		# Pixels on the scroll bar representing the off-page lines
		travel = self.scrollrect.height - self.shuttlerect.height
		#print "travel", travel
		
		# Distance in scrollbar-pixels of the youngest visible line to the youngest line
		x = int( (float( self.showline ) / float( linetravel )) * float( travel ) )
		#print "x", x
		
		# Move the scroll bar to keep this distance from the bottom
		self.shuttlerect.bottom = self.scrollrect.bottom - x
		#print "shuttle top", self.shuttlerect.top
		
		# keep shuttle on scroll bar
		self.shuttlerect.clamp_ip( self.scrollrect )
		#print "shuttle", self.shuttlerect

		# will need redraw
		self.manager.invalidaterect( self.rect )
	
	def scroll( self, (x, y) ):
		"""Adjust the scroll shuttle and visible text when the user scrolls"""
		#print ""
		# shuttle must track mouse
		self.shuttlerect.bottom = self.clickoffset + y
		# keep shuttle on scroll bar
		self.shuttlerect.clamp_ip( self.scrollrect )
		#print "shuttle", self.shuttlerect
		
		# redraw scroll bar
		self.manager.invalidaterect( self.scrollrect )
		
		#print "lines", len( self.lines )
		
		# Pixels that the scrollbar can slide
		travel = self.scrollrect.height - self.shuttlerect.height
		#print "travel", travel
		# Number of lines that fit on the page
		visiblelines = self.pagerect.height / self.lineheight
		#print "visiblelines", visiblelines
		
		# All the lines fit on the page
		if len( self.lines ) <= visiblelines:
			# Keep the youngest line at the bottom
			self.showline = 0
			return
		
		# How many lines the scrollbar can slide through
		linetravel = len( self.lines ) - visiblelines
		#print "linetravel", linetravel
		
		# Distance in pixels of the scroll bar to the top
		x = self.shuttlerect.top - self.scrollrect.top
		#print "x", x
		# Number of oldest lines that should be above the top of the page
		l = int( (float( x ) / float( travel )) * float( linetravel ) )
		#print "l", l
		
		# Remember the current scroll position
		oldshowline = self.showline
		
		# Determine the new scroll position: this is oldest line, 
		# then down to oldest visible line, then down to youngest visible line
		self.showline = len( self.lines ) - l - visiblelines
		#print "showline", self.showline
		
		# text has moved
		if oldshowline != self.showline:
			# need to redraw text
			self.manager.invalidaterect( self.rect )
	
	def draw( self, screen ):
		"""Draw the widget"""
		# clip our drawing
		screen.set_clip( screen.get_clip().clip( self.rect ) )
		# clear the page area
		screen.fill( self.background, self.pagerect )
		
		# start at the bottom
		y = self.pagerect.bottom
		# draw each line
		for surface in self.lines[self.showline:]:
			# move up one line (upper left corner)
			y -= self.lineheight
			# blit it to the screen
			screen.blit( surface, (self.rect.left, y) )
			# drew the topmost visible line
			if y <= self.rect.top: break
		
		#draw scroll bar
		screen.fill( self.background, self.scrollrect )
		screen.fill( self.foreground, self.shuttlerect )

class EditClass:
	def __init__( self, manager, action, rect, initialtext="", fontsize=24, maxchars=60, foreground=WHITE, background=DARKGRAY ):
		"""Initialize the Edit class"""
		rect = Rect( rect )
		self.manager = manager
		self.action = action
		self.rect = Rect(rect)
		self.font = pygame.font.Font( None, fontsize )
		self.maxchars = maxchars
		self.foreground = foreground
		self.background = background
		self.blitoffset = 0
		self.allowed = """`1234567890-=	qwertyuiop[]\\asdfghjkl;'zxcvbnm,./ ~!@#$%^&*()_+QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<?"""
		self.focused = None
		self.text = None
		
		# caret
		self.font.set_underline( 1 )
		self.caret = self.font.render( "_", 1, self.foreground, self.background )
		self.font.set_underline( 0 )
		self.caretwidth = self.caret.get_width()
		self.caretvisible = None
		
		# hide the caret
		self.hidecaret()
		
		# Start out with the default text
		self.settext( initialtext )
	
	def eventproc( self, event ):
		"""Process mouse and keyboard events"""
		# mouse click
		if event.type == MOUSEBUTTONDOWN:
			# grab keyboard focus
			self.focus()
		
		# key press
		elif event.type == KEYDOWN:
			# don't have keyboard focus
			if not self.focused:
				# raise exception
				raise WidgetException( "Edit class not focused but received keyboard message" )
			
			# backspace
			if event.key == K_BACKSPACE:
				# remove last character
				self.settext( self.text[:-1] )
			# enter
			elif event.key == K_RETURN:
				# report it to the manager
				self.action( self )
			# tab
			elif event.key == K_TAB:
				# send focus to next widget in taborder
				self.manager.nexttab( self, pygame.key.get_mods() & KMOD_SHIFT )
			# some other key
			else:
				# buffer is at max
				if len( self.text ) >= self.maxchars: return
				
				# convert key to character code
				character = str( event.unicode )
				
				# character is not allowed
				if string.find( self.allowed, character ) == -1: return
				
				# add it to the buffer
				self.settext( self.text + character )
	def showcaret( self ):
		self.caretvisible = 1
		# widest section of text that can be visible
		self.renderwidth = self.rect.width - self.caretwidth
		# rightmost edge of text, leaving room for caret
		self.renderright = self.rect.right - self.caretwidth
		# will need redraw
		self.manager.invalidaterect( self.rect )
	
	def hidecaret( self ):
		self.caretvisible = 0
		# text can now take up whole width of widget
		self.renderwidth = self.rect.width
		# rightmost edge of text
		self.renderright = self.rect.right
		# will need redraw
		self.manager.invalidaterect( self.rect )
		
	def focus( self ):
		"""Grab keyboard focus"""
		# already focused
		if self.focused: return
		
		# get keyboard focus from widget window
		self.manager.grabkey( self, self.unfocusCALLBACK, 1 )

		# keyboard focus is gained
		self.focused = 1
		# show the caret
		self.showcaret()
	
	def unfocusCALLBACK( self, widget ):
		"""Called by manager when keyboard focus is lost"""
		
		# don't have focus
		if not self.focused: return
		
		# keyboard focus is lost
		self.focused = None
		# hide the caret
		self.hidecaret()
	
	def settext( self, newtext ):
		"""Replace the edit box text"""
		
		# no change in text
		if self.text == newtext: return
		
		# udpate widget with new text
		self.text = newtext
		self.maketext()
	
	def slidecheck( self ):
		"""Adjust the position of the text to keep the last portion visible"""
		# we can assume that self.surface != None
		
		# width of text that we have to display
		width = self.surface.get_width()
		
		# text doesn't fit
		if width > self.renderwidth:
			# align the right edge
			self.blitoffset = self.renderright - width
		# text fits
		else:
			# align the left edge
			self.blitoffset = self.rect.left
		
		# caret position (to the right of text)
		self.caretleft = self.blitoffset + width
	
	def maketext( self ):
		"""Prepare the new text for display"""
		
		# will need redraw
		self.manager.invalidaterect( self.rect )
		
		# text is empty
		if not len( self.text ):
			# clear the surface
			self.surface = None
			# caret at far left
			self.caretleft = self.rect.left
			return
		
		# make a new surface
		self.surface = self.font.render( self.text, 1, self.foreground, self.background )
		
		# text has transparent background
		if self.background == None: 
			# optimize for blitting with transparency
			self.surface = self.surface.convert_alpha()
		# opaque background
		else:
			# optimize for normal blitting
			self.surface = self.surface.convert()
		
		# adjust the new text position horizontally
		self.slidecheck()
	
	def draw( self, screen ):
		"""Draw the widget"""
		# clip our drawing
		screen.set_clip( screen.get_clip().clip( self.rect ) )
		
		# widget is not transparent
		if self.background:
			# clear the area
			screen.fill( self.background, self.rect )
		
		# not empty
		if self.surface:
			# blit it to the screen
			screen.blit( self.surface, (self.blitoffset, self.rect.top) )
		
		# caret is visible
		if self.caretvisible:
			# blit the caret
			screen.blit( self.caret, (self.caretleft, self.rect.top ) )

class TextClass:
	def eventproc( self, event ): return
	def __init__( self, manager, rect, text="", fontsize=24, foreground=WHITE, background=None ):
		"""Initialize the Text class"""
		rect = Rect( rect )
		self.manager = manager
		self.text = text
		self.rect = Rect(rect)
		self.foreground = foreground
		self.background = background
		self.font = pygame.font.Font( None, fontsize )
		self.settext( text )
	
	def settext( self, newtext ):
		"""Replace the text in the widget"""
		# expand tabs
		newtext = newtext.expandtabs()
		# remove trailing whitespace
		self.text = newtext.rstrip()
		# update surface
		self.maketext()
	
	def maketext( self ):
		"""Prepare the text for display"""
		# if text is empty
		if len( self.text ) == 0:
			# use a space
			self.text = " "
		# transparent background
		if self.background == None:
			# make text
			self.surface = self.font.render( self.text, 1, self.foreground )
			# optimize for blitting with transparency
			self.surface = self.surface.convert_alpha()
		# opaque background
		else:
			# make text
			self.surface = self.font.render( self.text, 1, self.foreground, self.background )
			# optimize for blitting
			self.surface = self.surface.convert()
		# need screen update
		self.manager.invalidaterect( self.rect )
	
	def draw( self, screen ):
		"""Draw the widget"""
		# clip our drawing
		screen.set_clip( screen.get_clip().clip( self.rect ) )
		# background is set
		if self.background != None:
			# fill background
			screen.fill( self.background, self.rect )
		# blit text to the screen
		screen.blit( self.surface, self.rect )

class MultiLineTextClass:
	def eventproc( self, event ): return
	def __init__( self, manager, rect, text="", fontsize=24, foreground=WHITE, background=None ):
		"""Initialize the MultLineText class"""
		rect = Rect( rect )
		self.manager = manager
		self.lines = []
		self.rect = Rect(rect)
		self.foreground = foreground
		self.background = background
		self.font = pygame.font.Font( None, fontsize )
		self.settext( text )
	
	def settext( self, newtext ):
		"""Replace the text of the widget"""
		# expand tabs
		newtext = newtext.expandtabs()
		# append each line separately
		for line in newtext.splitlines():
				self.appendline( line )
	
	def fits( self, text ):
		"""Determine if the text fits on one line"""
		# size of the text
		(fw, fh) = self.font.size( text )
		# too wide
		if fw > self.rect.width: return 0
		return 1
	
	def appendline( self, newtext ):
		"""Prepare the lines of new text for display"""
		# remove trailing whitespace
		newtext = newtext.rstrip()
		
		# find largest piece of text that will fit on one line
		pt = len( newtext )
		while not self.fits( newtext[:pt] ):
			# find the last space
			pt = newtext.rfind( " ", 0, pt )
			# no spaces left
			if pt == -1: break
		
		# first word doesn't fit
		if pt == -1:
			# break word
			pt = len( newtext )
			while not self.fits( newtext[:pt] ):
				# not even one letter will fit
				if pt == 1: raise Exception
				# move the break one letter to the left
				pt -= 1
		# found string that fits on line
		else:
			# include the space
			pt += 1
		
		# line that will fit
		thisline = newtext[:pt]

		# check that line is not empty
		if len( thisline ) == 0:
			# use a space
			thisline = " "
		
		# transparent background
		if self.background == None:
			# make text
			surface = self.font.render( thisline, 1, self.foreground )
			# optimize for blitting with transparency
			surface = surface.convert_alpha()
		# opaque background
		else:
			# make text
			surface = self.font.render( thisline, 1, self.foreground, self.background )
			# optimize for blitting
			surface = surface.convert()

		self.lineheight = surface.get_height()
		self.lines.append( surface )
		
		# text that wouldn't fit
		newtext = newtext[pt:]

		# remove trailing whitespace
		#newtext = newtext.rstrip()

		# leftover wrapped text
		if len( newtext ) > 0:
			# make lines out of it
			self.appendline( newtext )
		# last line to be appended
		else:
			# need screen update
			self.manager.invalidaterect( self.rect )
	
	def draw( self, screen ):
		"""Draw the widget"""
		# clip our drawing
		screen.set_clip( screen.get_clip().clip( self.rect ) )
		# background is set
		if self.background != None:
			# fill background
			screen.fill( self.background, self.rect )
		# start at the top
		y = self.rect.top
		# draw each line
		for surface in self.lines:
			# blit it to the screen
			screen.blit( surface, (self.rect.left, y) )
			# next line is lower
			y += self.lineheight
