tennisfortwo/widgets.py

1116 lines
36 KiB
Python

#@+leo-ver=4-thin
#@+node:jpenner.20050604112932.1:@thin widgets.py
#@@language python
#@<< widgets declarations >>
#@+node:jpenner.20050604112932.2:<< widgets declarations >>
#!/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)
#@-node:jpenner.20050604112932.2:<< widgets declarations >>
#@nl
#@+others
#@+node:jpenner.20050604112932.3:class WidgetException
class WidgetException( Exception ):
"""Widget Exception class"""
#@ @+others
#@+node:jpenner.20050604112932.4:__init__
def __init__( self, reason ):
"""Initialize a new Widget exception"""
self.reason = reason
#@-node:jpenner.20050604112932.4:__init__
#@+node:jpenner.20050604112932.5:__str__
def __str__( self ):
"""String representation of exception"""
return `self.reason`
#@-node:jpenner.20050604112932.5:__str__
#@-others
#@-node:jpenner.20050604112932.3:class WidgetException
#@+node:jpenner.20050604112932.6:class WidgetWindow
class WidgetWindow:
#@ @+others
#@+node:jpenner.20050604112932.7:__init__
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, {} ) )
#@-node:jpenner.20050604112932.7:__init__
#@+node:jpenner.20050604112932.8:addwidget
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 )
#@-node:jpenner.20050604112932.8:addwidget
#@+node:jpenner.20050604112932.9:prevtab
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 )
#@-node:jpenner.20050604112932.9:prevtab
#@+node:jpenner.20050604112932.10:nexttab
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()
#@-node:jpenner.20050604112932.10:nexttab
#@+node:jpenner.20050604112932.11:grabmouse
def grabmouse( self, widget ):
"""Cause all mouse messages to be received by specified widget"""
self.mousedest = widget
#@-node:jpenner.20050604112932.11:grabmouse
#@+node:jpenner.20050604112932.12:grabkey
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
#@-node:jpenner.20050604112932.12:grabkey
#@+node:jpenner.20050604112932.13:invalidaterect
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 )
#@-node:jpenner.20050604112932.13:invalidaterect
#@+node:jpenner.20050604112932.14:dispatch
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
#@-node:jpenner.20050604112932.14:dispatch
#@+node:jpenner.20050604112932.15:eventproc
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()
#@-node:jpenner.20050604112932.15:eventproc
#@-others
#@-node:jpenner.20050604112932.6:class WidgetWindow
#@+node:jpenner.20050604112932.16:class ButtonClass
class ButtonClass:
#@ @+others
#@+node:jpenner.20050604112932.17:__init__
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 )
#@-node:jpenner.20050604112932.17:__init__
#@+node:jpenner.20050604112932.18:settext
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 )
#@-node:jpenner.20050604112932.18:settext
#@+node:jpenner.20050604112932.19:eventproc
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 )
#@-node:jpenner.20050604112932.19:eventproc
#@+node:jpenner.20050604112932.20:draw
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 )
#@-node:jpenner.20050604112932.20:draw
#@-others
#@-node:jpenner.20050604112932.16:class ButtonClass
#@+node:jpenner.20050604112932.21:class PageClass
class PageClass:
#@ @+others
#@+node:jpenner.20050604112932.22:eventproc
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 )
#@-node:jpenner.20050604112932.22:eventproc
#@+node:jpenner.20050604112932.23:__init__
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( " " )
#@-node:jpenner.20050604112932.23:__init__
#@+node:jpenner.20050604112932.24: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 )
#@-node:jpenner.20050604112932.24:append
#@+node:jpenner.20050604112932.25:fits
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
#@-node:jpenner.20050604112932.25:fits
#@+node:jpenner.20050604112932.26:appendline
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 )
#@-node:jpenner.20050604112932.26:appendline
#@+node:jpenner.20050604112932.27:scrollcheck
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 )
#@-node:jpenner.20050604112932.27:scrollcheck
#@+node:jpenner.20050604112932.28:scroll
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 )
#@-node:jpenner.20050604112932.28:scroll
#@+node:jpenner.20050604112932.29:draw
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 )
#@-node:jpenner.20050604112932.29:draw
#@-others
#@-node:jpenner.20050604112932.21:class PageClass
#@+node:jpenner.20050604112932.30:class EditClass
class EditClass:
#@ @+others
#@+node:jpenner.20050604112932.31:__init__
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 )
#@-node:jpenner.20050604112932.31:__init__
#@+node:jpenner.20050604112932.32:eventproc
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
try:
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 )
except: return
#@nonl
#@-node:jpenner.20050604112932.32:eventproc
#@+node:jpenner.20050604112932.33:showcaret
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 )
#@-node:jpenner.20050604112932.33:showcaret
#@+node:jpenner.20050604112932.34:hidecaret
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 )
#@-node:jpenner.20050604112932.34:hidecaret
#@+node:jpenner.20050604112932.35:focus
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()
#@-node:jpenner.20050604112932.35:focus
#@+node:jpenner.20050604112932.36:unfocusCALLBACK
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()
#@-node:jpenner.20050604112932.36:unfocusCALLBACK
#@+node:jpenner.20050604112932.37:settext
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()
#@-node:jpenner.20050604112932.37:settext
#@+node:jpenner.20050604112932.38:slidecheck
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
#@-node:jpenner.20050604112932.38:slidecheck
#@+node:jpenner.20050604112932.39:maketext
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()
#@-node:jpenner.20050604112932.39:maketext
#@+node:jpenner.20050604112932.40:draw
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 ) )
#@-node:jpenner.20050604112932.40:draw
#@-others
#@-node:jpenner.20050604112932.30:class EditClass
#@+node:jpenner.20050604112932.41:class TextClass
class TextClass:
#@ @+others
#@+node:jpenner.20050604112932.42:eventproc
def eventproc( self, event ): return
#@-node:jpenner.20050604112932.42:eventproc
#@+node:jpenner.20050604112932.43:__init__
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 )
#@-node:jpenner.20050604112932.43:__init__
#@+node:jpenner.20050604112932.44:settext
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()
#@-node:jpenner.20050604112932.44:settext
#@+node:jpenner.20050604112932.45: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 )
#@-node:jpenner.20050604112932.45:maketext
#@+node:jpenner.20050604112932.46:draw
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 )
#@-node:jpenner.20050604112932.46:draw
#@-others
#@-node:jpenner.20050604112932.41:class TextClass
#@+node:jpenner.20050604112932.47:class MultiLineTextClass
class MultiLineTextClass:
#@ @+others
#@+node:jpenner.20050604112932.48:eventproc
def eventproc( self, event ): return
#@-node:jpenner.20050604112932.48:eventproc
#@+node:jpenner.20050604112932.49:__init__
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 )
#@-node:jpenner.20050604112932.49:__init__
#@+node:jpenner.20050604112932.50:settext
def settext( self, newtext ):
"""Replace the text of the widget"""
self.lines = [] # clear current text
# expand tabs
newtext = newtext.expandtabs()
# append each line separately
for line in newtext.splitlines():
self.appendline( line )
#@-node:jpenner.20050604112932.50:settext
#@+node:jpenner.20050604112932.51:fits
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
#@-node:jpenner.20050604112932.51:fits
#@+node:jpenner.20050604112932.52:appendline
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 )
#@-node:jpenner.20050604112932.52:appendline
#@+node:jpenner.20050604112932.53:draw
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
#@-node:jpenner.20050604112932.53:draw
#@-others
#@-node:jpenner.20050604112932.47:class MultiLineTextClass
#@-others
#@-node:jpenner.20050604112932.1:@thin widgets.py
#@-leo