From e904d0bf052a3f72ee84e98e71b247047afcce86 Mon Sep 17 00:00:00 2001 From: jpenner Date: Tue, 14 Jun 2005 23:15:01 +0000 Subject: [PATCH] Initial check-in. --- TennisForTwo.leo | 156 +++++++ TennisForTwo.py | 814 +++++++++++++++++++++++++++++++++ data/bounce.wav | Bin 0 -> 6062 bytes data/connect.wav | Bin 0 -> 6062 bytes data/hit.wav | Bin 0 -> 6062 bytes data/score.wav | Bin 0 -> 6062 bytes pygame2app.py | 46 ++ pygame2exe.py | 40 ++ widgets.py | 1113 ++++++++++++++++++++++++++++++++++++++++++++++ widgetsdemo.py | 131 ++++++ 10 files changed, 2300 insertions(+) create mode 100644 TennisForTwo.leo create mode 100644 TennisForTwo.py create mode 100644 data/bounce.wav create mode 100644 data/connect.wav create mode 100644 data/hit.wav create mode 100644 data/score.wav create mode 100644 pygame2app.py create mode 100644 pygame2exe.py create mode 100644 widgets.py create mode 100644 widgetsdemo.py diff --git a/TennisForTwo.leo b/TennisForTwo.leo new file mode 100644 index 0000000..73c65ea --- /dev/null +++ b/TennisForTwo.leo @@ -0,0 +1,156 @@ + + + + + + + + + + +Files +@thin TennisForTwo.py +@thin widgets.py +@thin widgetsdemo.py +@thin pygame2exe.py +@thin pygame2app.py + +Tasks +Starting menu +__init__ +<< Menu layout >> + + +Utilities +@run TennisForTwo.py +@run TennisForTwo.py client localhost 7554 +@run widgetsdemo.py +@run PyGame2Exe.py py2exe +@run python2.4 pygame2app.py py2app + + + + + +@ignore +@language python +#make standalone, needs at least pygame-1.5.3 and py2exe-0.3.1 + +from distutils.core import setup +import sys, os, pygame, shutil, glob +import py2exe + +#setup the project variables here. +#i can't claim these will cover all the cases +#you need, but they seem to work for all my +#projects, just change as neeeded. + + +script = "TennisForTwo.py" #name of starting .PY +icon_file = "" #ICO file for the .EXE (not working well) +optimize = 2 #0, 1, or 2; like -O and -OO +dos_console = 0 #set to 0 for no dos shell when run +extra_data = ['data'] #extra files/dirs copied to game +extra_modules = ['pygame.locals'] #extra python modules not auto found + + + + + + +#use the default pygame icon, if none given +if not icon_file: + path = os.path.split(pygame.__file__)[0] + icon_file = '"' + os.path.join(path, 'pygame.ico') + '"' +#unfortunately, this cool icon stuff doesn't work in current py2exe :( +#icon_file = '' + +project_name = os.path.splitext(os.path.split(script)[1])[0] + + +#this will create the executable and all dependencies +setup(#name=project_name, + windows=[script], + data_files=[("data", glob.glob("data\\*"))] + ) + + + + + +def __init__( self ): + """Initialize Demo Class""" + # instead of pygame.init(), initialize modules manually to + # avoid initing pygame.sound + pygame.display.init() + pygame.font.init() + + # setup screen + screen = pygame.display.set_mode( (640, 480), DOUBLEBUF ) + pygame.display.set_caption( 'WidgetsDemo' ) + + # request regular event for updating animation + self.DRAWEVENT = USEREVENT + 1 + pygame.time.set_timer( self.DRAWEVENT, 33 ) #33 == 30fps + + # filter events + badevents = [NOEVENT, ACTIVEEVENT, JOYAXISMOTION, JOYBALLMOTION, JOYHATMOTION, JOYBUTTONDOWN ,JOYBUTTONUP, VIDEORESIZE, SYSWMEVENT, NUMEVENTS] + goodevents = [self.DRAWEVENT, KEYDOWN, KEYUP, MOUSEMOTION, MOUSEBUTTONDOWN, MOUSEBUTTONUP, QUIT ] + pygame.event.set_blocked( badevents ) + + # initialize the WidgetWindow base class + WidgetWindow.__init__( self, screen ) + + # create special widgets + edit = EditClass( self, self.editaction, (325, 300, 265, 25), "text" ) + self.page = PageClass( self, (325, 25, 265, 275), GRAY ) + + # put the widgets in the window + self.addwidget( MultiLineTextClass( self, (25, 25, 250, 145), "MultiLineTextClass\n(transparent)\nHas automatic word wrapping.", 36 ) ) + self.addwidget( MultiLineTextClass( self, (25, 195, 250, 145), "MultiLineTextClass\n(with background)\nHas automatic word wrapping.", 36, WHITE, BLACK ) ) + self.addwidget( EditClass( self, self.editaction, (25, 350, 150, 30), "second edit box" ), TABTARGET ) + self.addwidget( TextClass( self, (25, 400, 590, 19), "TextClass (transparent)", 20 ) ) + self.addwidget( TextClass( self, (25, 440, 590, 19), "TextClass (with background)", 20, WHITE, BLACK ) ) + self.addwidget( self.page ) + self.addwidget( edit, TABTARGET ) + self.addwidget( ButtonClass( self, self.buttonaction, (205, 110, 200, 105), "This button overlaps" ) ) + + # set keyboard focus to the edit widget + edit.focus() + + # initial screen draw + self.eventproc( pygame.event.Event( NOEVENT, {} ) ) + + # animation data + self.animrect = Rect( (325, 350, 265, 50) ) + self.animline = Rect( self.animrect ) + self.animline.width = 5 + self.animline.left = self.animrect.left + + + +self.addwidget(TextClass(self, (120, 40, 400, 30), "Tennis For Two", 36)) + +self.addwidget(ButtonClass(self, self.startSingle, (120, 120, 400, 30), "Start Single-Player Game")) + +self.addwidget(TextClass(self, (120, 190, 400, 20), "Your IP Address: " + getMyIP())) +self.addwidget(TextClass(self, (120, 215, 60, 20), "Port:")) +self.serverPort = EditClass(self, None, (180, 215, 60, 20), "7554") +self.addwidget(self.serverPort) +self.addwidget(ButtonClass(self, self.startServer, (120, 240, 400, 30), "Start Server")) + +self.addwidget(TextClass(self, (120, 310, 160, 20), "Server Address:")) +self.clientAddr = EditClass(self, None, (280, 310, 240, 20)) +self.addwidget(self.clientAddr) +self.addwidget(TextClass(self, (120, 335, 60, 20), "Port:")) +self.clientPort = EditClass(self, None, (180, 335, 60, 20), "7554") +self.addwidget(self.clientPort) +self.addwidget(ButtonClass(self, self.startClient, (120, 360, 400, 30), "Start Client")) + + + + + diff --git a/TennisForTwo.py b/TennisForTwo.py new file mode 100644 index 0000000..209e4c4 --- /dev/null +++ b/TennisForTwo.py @@ -0,0 +1,814 @@ +#@+leo-ver=4-thin +#@+node:jpenner.20050305105206:@thin TennisForTwo.py +#@@language python + +import pygame, math, cPickle, sys, StringIO, socket, re, urllib +from pygame.locals import * +from twisted.internet import task, reactor, protocol, udp +from widgets import * + +#@+others +#@+node:jpenner.20050601180947:Helper / Misc +#@+others +#@+node:jpenner.20050305105934:Constants +#constants +SCREENRECT = Rect(0, 0, 640, 480) +GRAVITY = 0.25 +FRICTION = 4 +BALL_STARTX = 100 +BALL_STARTY = 350 + +SINGLE_PLAYER = 10 +#@nonl +#@-node:jpenner.20050305105934:Constants +#@+node:jpenner.20050310195953:Globals +#globals +class Game: + currentplayer = 1 + SeverPort = 7554 + Port = None +#@nonl +#@-node:jpenner.20050310195953:Globals +#@+node:jpenner.20050424164431:Logging +def startLogging(fn): + Game.logfile = open(fn, 'wt') + +def log(text): + Game.logfile.write(str(text) + '\n') + +def stopLogging(): + Game.logfile.close() +#@nonl +#@-node:jpenner.20050424164431:Logging +#@+node:jpenner.20050427175747:Angles +def makeAngle(pos): + xdist = pos[0] - Game.ball.rect.centerx + ydist = pos[1] - Game.ball.rect.centery + return math.atan2(ydist, xdist) + +def anglePos(rect, angle, radius): + return( (rect.centerx + (math.cos(angle) * radius)), (rect.centery + (math.sin(angle) * radius))) + +#@-node:jpenner.20050427175747:Angles +#@+node:jpenner.20050605110605:IP address +def getMyIP(): + try: + f = urllib.urlopen('http://checkip.dyndns.org') + s = f.read() + m = re.search('([\d]*\.[\d]*\.[\d]*\.[\d]*)', s) + outsideip = m.group(0) + except: + outsideip = None + + try: + insideip = socket.gethostbyname(socket.gethostname()) + except: + insideip = None + + if (outsideip == None) and (insideip == None): + ip = "Unknown" + else: + if (outsideip == None): + ip = insideip + " (No internet?)" + else: + ip = outsideip + if (insideip <> None) and (insideip <> outsideip): + ip = ip + " (Firewalled?)" + return ip +#@-node:jpenner.20050605110605:IP address +#@-others +#@nonl +#@-node:jpenner.20050601180947:Helper / Misc +#@+node:jpenner.20050305124252:Events +#@+others +#@+node:jpenner.20050305130011.1:Event Manager +class EventManager: + def __init__(self): + self.queue = [] + self.handlers = [[] for i in range(NUM_EVENTS)] + + def postEvent(self,event): + self.queue.append(event); + + def tick(self): + for event in self.queue: + for handler in self.handlers[event.type]: + handler(event) + self.queue = [] + + def registerHandler(self, evtype, func): + if (evtype == NUM_EVENTS): + for i in range(NUM_EVENTS): + self.registerHandler(i, func) + else: + self.handlers[evtype].append(func) +#@nonl +#@-node:jpenner.20050305130011.1:Event Manager +#@+node:jpenner.20050305130011:Types +EV_HIT = 0 +EV_SCORE = 1 +EV_BOUNCE = 2 +EV_PLAYER_UPDATE = 3 +EV_SERVE = 4 +EV_CLICK = 5 +EV_BALLPOS = 6 +EV_CONNECT = 7 +EV_PLAYER_SWITCH = 8 +EV_HELLO = 9 +NUM_EVENTS = 10 + +class Event: + def __init__(self, type): + self.type = type + self.fromplayer = Game.myplayer + +class ClickEvent (Event): + def __init__(self, xvel, yvel, player): + Event.__init__(self, EV_CLICK) + self.xvel = xvel + self.yvel = yvel + self.player = player + +class HitEvent (Event): + def __init__(self, xvel, yvel): + Event.__init__(self, EV_HIT) + self.xvel = xvel + self.yvel = yvel + +class ScoreEvent (Event): + def __init__(self, player): + Event.__init__(self, EV_SCORE) + self.player = player + +class BounceEvent (Event): + def __init__(self): + Event.__init__(self, EV_BOUNCE) + +class PlayerUpdateEvent (Event): + def __init__(self): + Event.__init__(self, EV_PLAYER_UPDATE) + +class ServeEvent (Event): + def __init__(self): + Event.__init__(self, EV_SERVE) + +class BallPosEvent (Event): + def __init__(self, x, y): + Event.__init__(self, EV_BALLPOS) + self.x = x + self.y = y + +class ConnectEvent (Event): + def __init__(self): + Event.__init__(self, EV_CONNECT) + +class PlayerSwitchEvent (Event): + lastseq = 0 + def __init__(self, xvel, yvel, xpos, ypos, stopped): + Event.__init__(self, EV_PLAYER_SWITCH) + self.xvel = xvel + self.yvel = yvel + self.xpos = xpos + self.ypos = ypos + self.stopped = stopped + self.seq = PlayerSwitchEvent.lastseq + PlayerSwitchEvent.lastseq = PlayerSwitchEvent.lastseq + 1 + +class HelloEvent(Event): + def __init__(self): + Event.__init__(self, EV_HELLO) + +#@-node:jpenner.20050305130011:Types +#@-others +#@nonl +#@-node:jpenner.20050305124252:Events +#@+node:jpenner.20050319143816:Networking +#@+others +#@+node:jpenner.20050320120319:Message +class TFTMessage: + def __init__(self, sequence, event): + self.sequence = sequence + self.event = event +#@nonl +#@-node:jpenner.20050320120319:Message +#@+node:jpenner.20050424135832:Protocol +class TFTProtocol (protocol.DatagramProtocol): + def __init__(self): + self.mysequence = 0 + self.hissequence = -1 + self.address = None + Game.evMgr.registerHandler(EV_BALLPOS, self.sendEvent) + Game.evMgr.registerHandler(EV_SERVE, self.sendEvent) + Game.evMgr.registerHandler(EV_PLAYER_SWITCH, self.sendEvent) + Game.evMgr.registerHandler(EV_CONNECT, self.connectTransport) + + def startProtocol(self): + if self.address <> None: + log (self.address) + + def datagramReceived(self, data, address): + if self.address == None: + self.address = address + log(address) + msg = cPickle.Unpickler(StringIO.StringIO(data)).load() + + if (msg.sequence > self.hissequence): + self.hissequence = msg.sequence + if msg.event <> None: + log ("get " + str(msg.event.type) + ": " + str(msg.event.fromplayer)) + Game.evMgr.postEvent(msg.event) + + def sendEvent(self, event): + if (self.address <> None) and (self.transport <> None) and (event.fromplayer == Game.myplayer): + log ("send " + str(event.type)) + self.mysequence = self.mysequence + 1 + s = StringIO.StringIO() + cPickle.Pickler(s).dump(TFTMessage(self.mysequence, event)) + self.transport.write(s.getvalue(), self.address) + + def connectTransport(self, event): + Game.evMgr.postEvent(HelloEvent()) + + + +#@-node:jpenner.20050424135832:Protocol +#@-others +#@-node:jpenner.20050319143816:Networking +#@+node:jpenner.20050604112932:Menu +#@+others +#@+node:jpenner.20050605142055:Base Menu +class BaseMenu(WidgetWindow): + def __init__(self): + WidgetWindow.__init__(self, Game.screen) + + self.menuLayout() + + def onEnter(self): + self.invalidaterect() + self.eventproc( pygame.event.Event( NOEVENT, {} ) ) + + def tick(self): + for event in pygame.event.get(): + if event.type == QUIT: + reactor.stop() + try: + self.eventproc(event) + except: + pass + +#@-node:jpenner.20050605142055:Base Menu +#@+node:jpenner.20050605102056:Main Menu +class MainMenu(BaseMenu): + def menuLayout(self): + #@ << Menu layout >> + #@+node:jpenner.20050605102056.1:<< Menu layout >> + self.addwidget(TextClass(self, (120, 40, 400, 30), "Tennis For Two", 36)) + + self.addwidget(ButtonClass(self, self.startSingle, (120, 120, 400, 30), "Start Single-Player Game")) + + self.addwidget(TextClass(self, (120, 190, 400, 20), "Your IP Address: " + getMyIP())) + self.addwidget(TextClass(self, (120, 215, 60, 20), "Port:")) + self.serverPort = EditClass(self, None, (180, 215, 60, 20), "7554") + self.addwidget(self.serverPort) + self.addwidget(ButtonClass(self, self.startServer, (120, 240, 400, 30), "Start Server")) + + self.addwidget(TextClass(self, (120, 310, 160, 20), "Server Address:")) + self.clientAddr = EditClass(self, None, (280, 310, 240, 20)) + self.addwidget(self.clientAddr) + self.addwidget(TextClass(self, (120, 335, 60, 20), "Port:")) + self.clientPort = EditClass(self, None, (180, 335, 60, 20), "7554") + self.addwidget(self.clientPort) + self.addwidget(ButtonClass(self, self.startClient, (120, 360, 400, 30), "Start Client")) + #@-node:jpenner.20050605102056.1:<< Menu layout >> + #@nl + + def startSingle(self): + Game.myplayer = SINGLE_PLAYER + Game.isClient = False + Game.rules.infinitehits = True + Game.gameMgr.changeState(Game.gameMgr.STATE_SINGLE_PLAYER) + + def startServer(self): + Game.myplayer = 1 + Game.isClient = False + Game.ServerPort = int(self.serverPort.text) + Game.gameMgr.changeState(Game.gameMgr.STATE_WAIT_FOR_CLIENT) + + def startClient(self): + Game.myplayer = 2 + Game.isClient = True + Game.ServerPort = int(self.clientPort.text) + Game.ServerIP = self.clientAddr.text + Game.gameMgr.changeState(Game.gameMgr.STATE_CONNECT) +#@-node:jpenner.20050605102056:Main Menu +#@+node:jpenner.20050605173506:Server Wait Screen +class ServerWait(BaseMenu): + def menuLayout(self): + self.addwidget(TextClass(self, (120, 180, 400, 40), "Waiting For Client...", 48)) + self.addwidget(ButtonClass(self, self.cancel, (120, 360, 400, 30), "Cancel")) + + self.connected = False + Game.evMgr.registerHandler(EV_HELLO, self.connect) + + def cancel(self): + Game.gameMgr.changeState(Game.gameMgr.STATE_MENU) + + def connect(self, ev): + self.connected = True + + def tick(self): + BaseMenu.tick(self) + if self.connected: + self.connected = False + Game.gameMgr.changeState(Game.gameMgr.STATE_NETWORK_GAME) +#@nonl +#@-node:jpenner.20050605173506:Server Wait Screen +#@+node:jpenner.20050605180040:Client Wait Screen +class ClientWait(BaseMenu): + def menuLayout(self): + self.addwidget(TextClass(self, (120, 180, 400, 40), "Contacting Server...", 48)) + self.addwidget(ButtonClass(self, self.cancel, (120, 360, 400, 30), "Cancel")) + + self.connected = False + Game.evMgr.registerHandler(EV_BALLPOS, self.connect) + + def cancel(self): + Game.gameMgr.changeState(Game.gameMgr.STATE_MENU) + + def connect(self, ev): + self.connected = True + + def tick(self): + BaseMenu.tick(self) + if self.connected: + self.connected = False + Game.gameMgr.changeState(Game.gameMgr.STATE_NETWORK_GAME) + else: + Game.network.sendEvent(ConnectEvent()) + +#@-node:jpenner.20050605180040:Client Wait Screen +#@-others +#@-node:jpenner.20050604112932:Menu +#@+node:jpenner.20050305121157:Game +#@+others +#@+node:jpenner.20050604175241:Graphics +#@+others +#@+node:jpenner.20050604192205:Generic Graphics Manager +class GameGraphicsMgr: + def __init__(self): + self.all = pygame.sprite.RenderUpdates() + + self.bkg = pygame.Surface((SCREENRECT.width, SCREENRECT.height)) + self.bkg.fill(pygame.color.Color("black")) + + self.trackedSprites = [] + + def trackSprite(self, sprite, spriteToTrack): + self.trackedSprites.append({'sprite': sprite, 'spriteToTrack': spriteToTrack}) + + def clearScreen(self): + Game.screen.set_clip() + Game.screen.fill(pygame.color.Color("black")) + pygame.display.update() + + def tick(self): + for item in self.trackedSprites: + item['sprite'].rect.centerx = item['spriteToTrack'].rect.centerx + item['sprite'].rect.centery = item['spriteToTrack'].rect.centery + + self.all.clear(Game.screen, self.bkg) + self.all.update() + dirty = self.all.draw(Game.screen) + pygame.display.update(dirty) + +#@-node:jpenner.20050604192205:Generic Graphics Manager +#@+node:jpenner.20050604175241.1:TFT Graphics Manager +class TFTGraphicsMgr(GameGraphicsMgr): + def __init__(self): + GameGraphicsMgr.__init__(self) + Floor.containers = self.all + Net.containers = self.all + Ball.containers = self.all + ShotLine.containers = self.all + + Game.ball = Ball(BALL_STARTX, BALL_STARTY) + Game.floor = Floor() + Game.net = Net() + self.shotLine = ShotLine() + + self.trackSprite(self.shotLine, Game.ball) + + def tick(self): + self.shotLine.updateAngle() + GameGraphicsMgr.tick(self) +#@-node:jpenner.20050604175241.1:TFT Graphics Manager +#@+node:jpenner.20050305120654:Sprites +#@+others +#@+node:jpenner.20050305112032:Floor +class Floor(pygame.sprite.Sprite): + def __init__(self): + pygame.sprite.Sprite.__init__(self, self.containers) + self.image = pygame.Surface((SCREENRECT.width - (SCREENRECT.width / 8), SCREENRECT.height / 96)) + self.image.fill(pygame.color.Color("white")) + self.rect = self.image.get_rect() + self.reloading = 0 + self.rect.centerx = SCREENRECT.centerx + self.rect.bottom = SCREENRECT.bottom * 0.875 + self.origtop = self.rect.top + self.facing = -1 + + +#@nonl +#@-node:jpenner.20050305112032:Floor +#@+node:jpenner.20050305120626:Net +class Net(pygame.sprite.Sprite): + def __init__(self): + pygame.sprite.Sprite.__init__(self, self.containers) + self.image = pygame.Surface((SCREENRECT.width / 128, SCREENRECT.height / 12)) + self.image.fill(pygame.color.Color("white")) + self.rect = self.image.get_rect() + self.reloading = 0 + self.rect.centerx = SCREENRECT.centerx + self.rect.bottom = SCREENRECT.bottom * 0.875 + self.origtop = self.rect.top + self.facing = -1 + + +#@-node:jpenner.20050305120626:Net +#@+node:jpenner.20050427175728:Ball +class Ball(pygame.sprite.Sprite): + def __init__(self, x, y): + pygame.sprite.Sprite.__init__(self, self.containers) + self.image = pygame.Surface((SCREENRECT.width / 128, SCREENRECT.height / 96)) + self.image.fill(pygame.color.Color("white")) + self.rect = self.image.get_rect() + self.reloading = 0 + self.rect.centerx = x + self.rect.centery = y + self.origtop = self.rect.top + self.facing = -1 + + def currentPlayer(self): + if self.rect.centerx < Game.net.rect.centerx: + return 1 + else: + return 2 + + + +#@-node:jpenner.20050427175728:Ball +#@+node:jpenner.20050305123236:Shot Line +class ShotLine(pygame.sprite.Sprite): + def __init__(self): + pygame.sprite.Sprite.__init__(self, self.containers) + self.image = pygame.Surface((SCREENRECT.width / 32, SCREENRECT.height / 24), SRCALPHA) + self.image.set_colorkey(pygame.color.Color("purple")) + self.rect = self.image.get_rect() + self.updateAngle() + self.reloading = 0 + self.origtop = self.rect.top + self.facing = -1 + + def updateAngle(self): + angle = makeAngle(pygame.mouse.get_pos()) + self.image.fill(pygame.color.Color("purple")) # clear + pygame.draw.line(self.image, pygame.color.Color("white"), + anglePos(self.image.get_rect(), angle, 6), anglePos(self.image.get_rect(),angle,12)) + + +#@-node:jpenner.20050305123236:Shot Line +#@-others +#@nonl +#@-node:jpenner.20050305120654:Sprites +#@-others +#@nonl +#@-node:jpenner.20050604175241:Graphics +#@+node:jpenner.20050320112219:Physics +#@+others +#@+node:jpenner.20050424152556:Physics Manager +class PhysicsMgr: + def __init__(self): + self.gamePhysics = PhysicsEngine() + self.netPhysics = NetworkPhysicsEngine() + + Game.evMgr.registerHandler(EV_PLAYER_UPDATE, self.switchEngines) + Game.evMgr.registerHandler(EV_PLAYER_SWITCH, self.switchToGame) + + def onEnter(self): + if Game.currentplayer == Game.myplayer: + self.current = self.gamePhysics + else: + self.current = self.netPhysics + self.lastSeq = -1 + + def switchEngines(self, ev): + if Game.ball.currentPlayer() <> Game.myplayer: + self.current = self.netPhysics + self.current.switchStarted = True + else: + self.current = self.gamePhysics + + def switchToGame(self, ev): + if ev.fromplayer <> Game.myplayer: + if ev.seq > self.lastSeq: + self.lastSeq = ev.seq + self.gamePhysics.resync(ev) + Game.evMgr.postEvent(PlayerUpdateEvent()) + + + def tick(self): + self.current.tick() +#@nonl +#@-node:jpenner.20050424152556:Physics Manager +#@+node:jpenner.20050306103246:Game Physics +class PhysicsEngine: + def __init__(self): + Game.evMgr.registerHandler(EV_HIT, self.hitHandler) + Game.evMgr.registerHandler(EV_SERVE, self.serveHandler) + + self.xvel = 0 + self.yvel = 0 + self.stopped = True + + def tick(self): + # only do gravity / collision detection if ball is moving + if not self.stopped: + # Gravity + self.yvel += GRAVITY + + #@ << Floor collision >> + #@+node:jpenner.20050310185348:<< Floor collision >> + if (Game.ball.rect.bottom + self.yvel) > Game.floor.rect.top: + self.yvel = -abs(self.yvel) + FRICTION + if self.yvel > 0: + self.yvel = 0 + self.xvel = 0 + self.stopped = True + + Game.evMgr.postEvent(BounceEvent()) + #@nonl + #@-node:jpenner.20050310185348:<< Floor collision >> + #@nl + #@ << Net collision >> + #@+node:jpenner.20050310185348.1:<< Net collision >> + if (( ((Game.ball.rect.right < Game.net.rect.left) and ((Game.ball.rect.right + self.xvel) >= Game.net.rect.left) ) or + ((Game.ball.rect.left > Game.net.rect.right) and ((Game.ball.rect.left + self.xvel) <= Game.net.rect.right) ) ) and + ((Game.ball.rect.bottom + self.yvel) > Game.net.rect.top)): + self.xvel = -self.xvel + + #@-node:jpenner.20050310185348.1:<< Net collision >> + #@nl + #@ << Switch players >> + #@+node:jpenner.20050310185414:<< Switch players >> + if ((Game.ball.rect.centerx < Game.net.rect.centerx) and ((Game.ball.rect.centerx + self.xvel) >= Game.net.rect.centerx) or + (Game.ball.rect.centerx >= Game.net.rect.centerx) and ((Game.ball.rect.centerx + self.xvel) < Game.net.rect.centerx)): + Game.evMgr.postEvent(PlayerUpdateEvent()) + + #@-node:jpenner.20050310185414:<< Switch players >> + #@nl + + Game.ball.rect.centerx += self.xvel + Game.ball.rect.centery += self.yvel + + Game.evMgr.postEvent(BallPosEvent(Game.ball.rect.centerx, Game.ball.rect.centery)) + Game.switchEv = PlayerSwitchEvent(self.xvel, self.yvel, Game.ball.rect.centerx, Game.ball.rect.centery, self.stopped) + + def hitHandler(self,hitEvent): + self.xvel = hitEvent.xvel + self.yvel = hitEvent.yvel + self.stopped = False + + def serveHandler(self, ev): + self.stopped = True + + def resync(self, ev): + self.xvel = ev.xvel + self.yvel = ev.yvel + Game.ball.rect.centerx = ev.xpos + Game.ball.rect.centery = ev.ypos + self.stopped = ev.stopped + +#@nonl +#@-node:jpenner.20050306103246:Game Physics +#@+node:jpenner.20050320112219.1:Network Physics +class NetworkPhysicsEngine: + def __init__(self): + Game.evMgr.registerHandler(EV_BALLPOS, self.updatePos) + self.switchStarted = False + + def updatePos(self, ev): + if (ev.fromplayer <> Game.myplayer): + self.switchStarted = False + Game.ball.rect.centerx = ev.x + Game.ball.rect.centery = ev.y + + def tick(self): + log ("netphysics") + if self.switchStarted: + Game.evMgr.postEvent(Game.switchEv) + +#@-node:jpenner.20050320112219.1:Network Physics +#@-others +#@nonl +#@-node:jpenner.20050320112219:Physics +#@+node:jpenner.20050307180329:Sound +class SoundEngine: + def __init__(self): + f = { EV_HIT: 'hit.wav', + EV_BOUNCE: 'bounce.wav', + EV_SCORE: 'score.wav', + EV_HELLO: 'connect.wav' } + + self.sound = {} + for evtype, filename in f.iteritems(): + try: + self.sound[evtype] = pygame.mixer.Sound('data/' + filename) + Game.evMgr.registerHandler(evtype, self.noise) + except: + pass + + def noise(self, ev): + self.sound[ev.type].play() + + +#@-node:jpenner.20050307180329:Sound +#@+node:jpenner.20050312135503:Rules +class RuleManager: + def __init__(self): + Game.evMgr.registerHandler(EV_CLICK, self.clickHandler) + Game.evMgr.registerHandler(EV_SERVE, self.serveHandler) + Game.evMgr.registerHandler(EV_PLAYER_UPDATE, self.updateHandler) + self.playerhit = False + self.infinitehits = False + Game.currentplayer = 1 + + def clickHandler(self, ev): + if not self.playerhit and (ev.player == Game.currentplayer or ev.player == SINGLE_PLAYER): + Game.evMgr.postEvent(HitEvent(ev.xvel, ev.yvel)) + if not self.infinitehits: + self.playerhit = True + + def updateHandler(self, ev): + self.playerhit = False + Game.currentplayer = Game.ball.currentPlayer() + + def serveHandler(self, ev): + Game.ball.rect.centerx = BALL_STARTX + Game.ball.rect.centery = BALL_STARTY + Game.evMgr.postEvent(PlayerUpdateEvent()) + + +#@-node:jpenner.20050312135503:Rules +#@+node:jpenner.20050310200258:Input +class InputManager: + def tick(self): + for event in pygame.event.get(): + if event.type == QUIT: + reactor.stop() + self.sdl_event(event) + + def sdl_event(self, event): + if event.type == MOUSEBUTTONUP: + angle = makeAngle(event.pos) + Game.evMgr.postEvent( ClickEvent(math.cos(angle) * 12, math.sin(angle) * 9, Game.myplayer) ) + + if event.type == KEYUP: + Game.evMgr.postEvent( ServeEvent() ) + +#@nonl +#@-node:jpenner.20050310200258:Input +#@+node:jpenner.20050305122430:Game Loop + +#@+others +#@+node:jpenner.20050606072251:State Machine +class GameState: + def __init__(self, manager, onenter = None, onexit = None): + def doNothing(): pass + + if manager <> None: + self.tick = manager.tick + else: + self.tick = doNothing + + if onenter <> None: + self.enter = onenter + else: + self.enter = doNothing + + if onexit <> None: + self.exit = onexit + else: + self.exit = doNothing + +class StateMachine: + def __init__(self, states, initialState = 0): + self.state = initialState + self.states = states + self.states[self.state].enter() + self.newstate = -1 + + def changeState(self, newstate): + # Don't change states in the middle of a tick! + self.newstate = newstate + + def tick(self): + if self.newstate >= 0: + self.states[self.state].exit() + self.state = self.newstate + self.states[self.state].enter() + self.newstate = -1 + + self.states[self.state].tick() + +class GameLoop: + def __init__(self, managers): + self.managers = managers + + def tick(self): + for manager in self.managers: + manager.tick() +#@-node:jpenner.20050606072251:State Machine +#@+node:jpenner.20050606072251.1:Game Manager +class GameMgr (StateMachine): + def __init__(self): + self.STATE_MENU = 0 + self.STATE_NETWORK_GAME = 1 + self.STATE_SINGLE_PLAYER = 2 + self.STATE_WAIT_FOR_CLIENT = 3 + self.STATE_CONNECT = 4 + + inputMgr = InputManager() + self.graphicsMgr = TFTGraphicsMgr() + self.physicsMgr = PhysicsMgr() + + mainMenu = MainMenu() + self.serverWait = ServerWait() + self.clientWait = ClientWait() + + StateMachine.__init__(self, + [GameState(MainMenu(), mainMenu.onEnter), + GameState(GameLoop([inputMgr, Game.evMgr, self.physicsMgr, self.graphicsMgr]), self.gameEnter), + GameState(GameLoop([inputMgr, Game.evMgr, PhysicsEngine(), self.graphicsMgr]), self.gameEnter), + GameState(GameLoop([Game.evMgr, self.serverWait]), self.serverEnter, self.networkExit), + GameState(GameLoop([Game.evMgr, self.clientWait]), self.clientEnter, self.networkExit) + ]) + + def gameEnter(self): + self.graphicsMgr.clearScreen() + self.physicsMgr.onEnter() + + def clientEnter(self): + Game.network = TFTProtocol() + def onResolve(ip): + Game.network.address = (ip, Game.ServerPort) + Game.port = reactor.listenUDP(0, Game.network) + reactor.resolve(Game.ServerIP).addCallback(onResolve) + self.clientWait.onEnter() + + def serverEnter(self): + Game.network = TFTProtocol() + Game.port = reactor.listenUDP(Game.ServerPort, Game.network) + log ("Listening on " + str(Game.ServerPort)) + self.serverWait.onEnter() + + def networkExit(self): + if (self.newstate == self.STATE_MENU) and (Game.port <> None): # cancelled + Game.port.stopListening() + +#@-node:jpenner.20050606072251.1:Game Manager +#@-others + + +#@-node:jpenner.20050305122430:Game Loop +#@+node:jpenner.20050319125841:Setup +def main(): + pygame.init() + + startLogging('tennis.log') + + Game.screen = pygame.display.set_mode(SCREENRECT.size, DOUBLEBUF, 16) + + Game.evMgr = EventManager() + Game.sound = SoundEngine() + Game.rules = RuleManager() + Game.gameMgr = GameMgr() + + task.LoopingCall(Game.gameMgr.tick).start(0.03) + + reactor.run() + + #cleanup + stopLogging() + + + +#@-node:jpenner.20050319125841:Setup +#@-others +#@nonl +#@-node:jpenner.20050305121157:Game +#@-others + +if __name__ == '__main__': main() +#@nonl +#@-node:jpenner.20050305105206:@thin TennisForTwo.py +#@-leo diff --git a/data/bounce.wav b/data/bounce.wav new file mode 100644 index 0000000000000000000000000000000000000000..adeb6e8f7217c7ece8af69181a51f59df90df3ca GIT binary patch literal 6062 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 literal 0 HcmV?d00001 diff --git a/data/connect.wav b/data/connect.wav new file mode 100644 index 0000000000000000000000000000000000000000..adeb6e8f7217c7ece8af69181a51f59df90df3ca GIT binary patch literal 6062 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 literal 0 HcmV?d00001 diff --git a/data/hit.wav b/data/hit.wav new file mode 100644 index 0000000000000000000000000000000000000000..adeb6e8f7217c7ece8af69181a51f59df90df3ca GIT binary patch literal 6062 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 literal 0 HcmV?d00001 diff --git a/data/score.wav b/data/score.wav new file mode 100644 index 0000000000000000000000000000000000000000..adeb6e8f7217c7ece8af69181a51f59df90df3ca GIT binary patch literal 6062 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 literal 0 HcmV?d00001 diff --git a/pygame2app.py b/pygame2app.py new file mode 100644 index 0000000..aa10f6e --- /dev/null +++ b/pygame2app.py @@ -0,0 +1,46 @@ +#@+leo-ver=4-thin +#@+node:jpenner.20050604144534:@thin pygame2app.py +#@@language python +#make standalone, needs at least pygame-1.5.3 and py2exe-0.3.1 + +from distutils.core import setup +import sys, os, pygame, shutil, glob +import py2app + +#setup the project variables here. +#i can't claim these will cover all the cases +#you need, but they seem to work for all my +#projects, just change as neeeded. + + +script = "TennisForTwo.py" #name of starting .PY +icon_file = "" #ICO file for the .EXE (not working well) +optimize = 2 #0, 1, or 2; like -O and -OO +dos_console = 0 #set to 0 for no dos shell when run +extra_data = ['data'] #extra files/dirs copied to game +extra_modules = ['pygame.locals'] #extra python modules not auto found + + + + + + +#use the default pygame icon, if none given +if not icon_file: + path = os.path.split(pygame.__file__)[0] + icon_file = '"' + os.path.join(path, 'pygame.ico') + '"' +#unfortunately, this cool icon stuff doesn't work in current py2exe :( +#icon_file = '' + +project_name = os.path.splitext(os.path.split(script)[1])[0] + + +#this will create the executable and all dependencies +setup(#name=project_name, + app=[script], + data_files=[("data", glob.glob("data\\*"))] + ) + + +#@-node:jpenner.20050604144534:@thin pygame2app.py +#@-leo diff --git a/pygame2exe.py b/pygame2exe.py new file mode 100644 index 0000000..7bb6155 --- /dev/null +++ b/pygame2exe.py @@ -0,0 +1,40 @@ +#make standalone, needs at least pygame-1.5.3 and py2exe-0.3.1 + +from distutils.core import setup +import sys, os, pygame, shutil, glob +import py2exe + +#setup the project variables here. +#i can't claim these will cover all the cases +#you need, but they seem to work for all my +#projects, just change as neeeded. + + +script = "TennisForTwo.py" #name of starting .PY +icon_file = "" #ICO file for the .EXE (not working well) +optimize = 2 #0, 1, or 2; like -O and -OO +dos_console = 0 #set to 0 for no dos shell when run +extra_data = ['data'] #extra files/dirs copied to game +extra_modules = ['pygame.locals'] #extra python modules not auto found + + + + + + +#use the default pygame icon, if none given +if not icon_file: + path = os.path.split(pygame.__file__)[0] + icon_file = '"' + os.path.join(path, 'pygame.ico') + '"' +#unfortunately, this cool icon stuff doesn't work in current py2exe :( +#icon_file = '' + +project_name = os.path.splitext(os.path.split(script)[1])[0] + + +#this will create the executable and all dependencies +setup(#name=project_name, + windows=[script], + data_files=[("data", glob.glob("data\\*"))] + ) + diff --git a/widgets.py b/widgets.py new file mode 100644 index 0000000..795f6e7 --- /dev/null +++ b/widgets.py @@ -0,0 +1,1113 @@ +#@+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.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""" + # 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 diff --git a/widgetsdemo.py b/widgetsdemo.py new file mode 100644 index 0000000..33351f2 --- /dev/null +++ b/widgetsdemo.py @@ -0,0 +1,131 @@ +#@+leo-ver=4-thin +#@+node:jpenner.20050604113053:@thin widgetsdemo.py +#@@language python +#@<< widgetsdemo declarations >> +#@+node:jpenner.20050604113053.1:<< widgetsdemo declarations >> +#!/usr/bin/env python +# +# WidgetsDemo.py demonstrates the usage Widgets.py +# Copyright (C) 2003 Michael Leonhard +# http://tamale.net/ +# +# version 1.0 RC1 (2003/07/06 00:42) + +import pygame, pygame.display, pygame.event, pygame.time, pygame.font, pygame.draw +from pygame.locals import * +import math, string +from widgets import * + +#@-node:jpenner.20050604113053.1:<< widgetsdemo declarations >> +#@nl +#@+others +#@+node:jpenner.20050604113053.2:class WidgetsDemo +class WidgetsDemo( WidgetWindow ): + #@ @+others + #@+node:jpenner.20050604113053.3:__init__ + def __init__( self ): + """Initialize Demo Class""" + # instead of pygame.init(), initialize modules manually to + # avoid initing pygame.sound + pygame.display.init() + pygame.font.init() + + # setup screen + screen = pygame.display.set_mode( (640, 480), DOUBLEBUF ) + pygame.display.set_caption( 'WidgetsDemo' ) + + # request regular event for updating animation + self.DRAWEVENT = USEREVENT + 1 + pygame.time.set_timer( self.DRAWEVENT, 33 ) #33 == 30fps + + # filter events + badevents = [NOEVENT, ACTIVEEVENT, JOYAXISMOTION, JOYBALLMOTION, JOYHATMOTION, JOYBUTTONDOWN ,JOYBUTTONUP, VIDEORESIZE, SYSWMEVENT, NUMEVENTS] + goodevents = [self.DRAWEVENT, KEYDOWN, KEYUP, MOUSEMOTION, MOUSEBUTTONDOWN, MOUSEBUTTONUP, QUIT ] + pygame.event.set_blocked( badevents ) + + # initialize the WidgetWindow base class + WidgetWindow.__init__( self, screen ) + + # create special widgets + edit = EditClass( self, self.editaction, (325, 300, 265, 25), "text" ) + self.page = PageClass( self, (325, 25, 265, 275), GRAY ) + + # put the widgets in the window + self.addwidget( MultiLineTextClass( self, (25, 25, 250, 145), "MultiLineTextClass\n(transparent)\nHas automatic word wrapping.", 36 ) ) + self.addwidget( MultiLineTextClass( self, (25, 195, 250, 145), "MultiLineTextClass\n(with background)\nHas automatic word wrapping.", 36, WHITE, BLACK ) ) + self.addwidget( EditClass( self, self.editaction, (25, 350, 150, 30), "second edit box" ), TABTARGET ) + self.addwidget( TextClass( self, (25, 400, 590, 19), "TextClass (transparent)", 20 ) ) + self.addwidget( TextClass( self, (25, 440, 590, 19), "TextClass (with background)", 20, WHITE, BLACK ) ) + self.addwidget( self.page ) + self.addwidget( edit, TABTARGET ) + self.addwidget( ButtonClass( self, self.buttonaction, (205, 110, 200, 105), "This button overlaps" ) ) + + # set keyboard focus to the edit widget + edit.focus() + + # initial screen draw + self.eventproc( pygame.event.Event( NOEVENT, {} ) ) + + # animation data + self.animrect = Rect( (325, 350, 265, 50) ) + self.animline = Rect( self.animrect ) + self.animline.width = 5 + self.animline.left = self.animrect.left + #@-node:jpenner.20050604113053.3:__init__ + #@+node:jpenner.20050604113053.4:loop + def loop( self ): + """Program loop""" + while 1: + # get the next event + event = pygame.event.wait() + # Redraw the animation + if event.type == self.DRAWEVENT: + # get all pending DRAWEVENTs + pending = pygame.event.get( self.DRAWEVENT ) + # number of events we are doing + num = len( pending ) + 1 + + # reset the clipping region + self.screen.set_clip() + # erase the animation + self.screen.fill( self.background, self.animline ) + + # update the animation + self.animline.left += 1 * num + # line is outside the region + while not self.animline.colliderect( self.animrect ): + # move it back to the left + self.animline.left -= self.animrect.width + # draw the animation + self.screen.fill( RED, self.animline ) + # flip the display + pygame.display.update() + + # user pressed the X button to close the window + elif event.type == QUIT: break + # pass the even to the widgets + else: self.eventproc( event ) + + #clean up + pygame.time.set_timer( self.DRAWEVENT, 0 ) + pygame.quit() + #@-node:jpenner.20050604113053.4:loop + #@+node:jpenner.20050604113053.5:editaction + def editaction( self, widget ): + """Function called when enter key is pressed""" + self.page.append( widget.text ) + widget.settext( "" ) + #@-node:jpenner.20050604113053.5:editaction + #@+node:jpenner.20050604113053.6:buttonaction + def buttonaction( self ): + """Function called when button is pressed""" + self.page.append( "ACTION" ) + #@-node:jpenner.20050604113053.6:buttonaction + #@-others +#@-node:jpenner.20050604113053.2:class WidgetsDemo +#@-others + +demo = WidgetsDemo() +demo.loop() +#@-node:jpenner.20050604113053:@thin widgetsdemo.py +#@-leo