"""A simple gui chat client. It's built off the simpleclient.py in the Twisted examples, and uses TwistedRenderer to tie the GUI and network elements together. The GUI has two screens: LoginScreen and ChatScreen. When the Protocol detects a server connection, it emits an event to switch to the ChatScreen. When the Protocol detects it has lost a connection, it emits an event to switch to the LoginScreen. The client and server doesn't really keep track of which users are connected. This could be done with some more complex data being sent across the wire (maybe even just a dict), but for this example, only chat text is sent. Also, I didn't bother making the chat window scroll down automatically when new items are added. This may be something that can be done with ScrolledList, but I haven't looked at it closely enough. written by Ben Olsen """ import pygame from pygame.locals import * from ocempgui.widgets import * from ocempgui.widgets.Constants import * from ocempgui.widgets.components import TextListItem from ocempgui.object import BaseObject from twisted.internet.threadedselectreactor import install install() from twisted.internet import reactor, protocol # local signals SIG_REC = 'rec' SIG_SEND = 'send' COMMAND = 'command' class LoginScreen (BaseObject): """The Login Screen contains Entries for server, port, and user name. The button emits a signal containing this info, which is picked up by the factory and used in reactor.connectTCP. It inherits from BaseObject to make signal processing easier. """ def __init__ (self, re): BaseObject.__init__ (self) # Remember to set _signals before setting the manager! self._signals[COMMAND] = [] self.connect_signal (COMMAND, self.processCommand) self.manager = re.active_layer[2] self.renderer = re # self.frame is the main widget for this class. self.frame = VFrame (Label ("Enter login information")) # labels on the left, entry fields on the right. self.table = Table (3, 3) self.table.set_column_align (0, ALIGN_RIGHT) self.table.set_column_align (1, ALIGN_LEFT) self.table.add_child (0, 0, Label ("Server:")) self.table.add_child (1, 0, Label ("Port:")) self.table.add_child (2, 0, Label ("Username:")) self.serverEntry = Entry ('localhost') self.portEntry = Entry ('8008') self.nameEntry = Entry () self.table.add_child (0, 1, self.serverEntry) self.table.add_child (1, 1, self.portEntry) self.table.add_child (2, 1, self.nameEntry) self.frame.add_child (self.table) self.button = Button ("Connect") self.button.connect_signal (SIG_MOUSEDOWN, self.sendConnect) self.frame.add_child (self.button) # Set the frame roughly in the middle. self.frame.topleft = (190, 70) def sendConnect (self, data): # Called when the button gets SIG_MOUSEDOWN. self.emit (COMMAND, (self.serverEntry.text, int (self.portEntry.text), self.nameEntry.text)) def processCommand (self, data): # This function adds or removes self.frame from the renderer # based on command signals sent from the Protocol. if data == 'switch_to_chat': self.renderer.remove_widget (self.frame) if data == 'switch_to_login': self.renderer.add_widget (self.frame) def notify (self, event): self.run_signal_handlers (event.signal, event.data) class ChatScreen (BaseObject): """The ChatScreen only has two widgets: the chat window, which is a ScrolledList, and the Entry field. The Entry is tied to the ENTER key and emits a signal telling the Protocol to send the attached data. Listens for SIG_REC events which are sent by the Protocol and adds the data to the ScrolledList. """ def __init__ (self, re): BaseObject.__init__ (self) # Remember to set _signals before setting the manager! self._signals[SIG_REC] = [] self.connect_signal (SIG_REC, self.addText) self._signals[COMMAND] = [] self.connect_signal (COMMAND, self.processCommand) self.manager = re.active_layer[2] self.renderer = re # self.table is the main widget for this class self.table = Table(2,2) self.chatframe = VFrame (Label ("Chat Window")) self.chatwindow = ScrolledList (520, 240) self.chatwindow.selectionmode = SELECTION_NONE self.chatframe.add_child (self.chatwindow) self.entryFrame = VFrame () self.entry = Entry () self.entry.minsize = (520, 24) self.entry.connect_signal ("input", self.sendText) self.entryFrame.add_child (self.entry) self.table.add_child (0, 0, self.chatframe) self.table.add_child (1, 0, self.entryFrame) def addText (self, text): # Called when the class gets a SIG_REC from the Protocol. self.chatwindow.items.append (TextListItem (text)) # Enforce update before proceeding, so scrolling will be # correct and is not influenced by the sprite update cycles. self.chatwindow.update () self.chatwindow.vscrollbar.value = self.chatwindow.vscrollbar.maximum def sendText (self): # Called when the Entry gets ENTER. self.emit (SIG_SEND, self.entry.text) self.entry.text = '' def processCommand (self, data): # This function adds or removes self.table from the renderer # based on command signals sent from the Protocol. if data == 'switch_to_chat': self.renderer.add_widget (self.table) if data == 'switch_to_login': self.renderer.remove_widget (self.table) def notify (self, event): self.run_signal_handlers (event.signal, event.data) class ChatClient (protocol.Protocol): """The Protocol does most of the work. When it detects a connection, it emits a signal to switch from LoginScreen to ChatScreen, and vice versa for losing a connetion. It watches for SIG_SEND events and sends the data across the connection. Note that because of the way the Factory instantiates the Protocol, it doesn't make sense to have this class inherit from BaseObject. Instead, once it knows it has a connection, it adds itself to the renderer's EventManager. """ def __init__ (self): self.user = None def connectionMade (self): # The connection was successful, and we now have communication # with a server. self.manager.add_object (self, SIG_SEND) self.manager.emit (COMMAND, 'switch_to_chat') # The factory gets the login info, including user name, so set # it here to match the factory one. self.user = self.factory.user def connectionLost (self, reason): self.manager.emit (COMMAND, 'switch_to_login') def dataReceived(self, data): # When we get data from the network, emit an event to have it # displayed by the ChatScreen. self.manager.emit (SIG_REC, data) def notify (self, event): # Since the server doesn't keep track of user names, prepend # self.user to the beginning of each message if event.signal == SIG_SEND: self.transport.write (str (self.user) + '>> ' + str (event.data)) class ChatFactory (protocol.ClientFactory): """The Factory is the object used by the reactor. It keeps track of the connection with self.connected, so that hitting the LoginScreen connect button doesn't try to connect multiple times. """ protocol = ChatClient def __init__ (self, re): # Set the Protocol's event manager. self.protocol.manager = re.active_layer[2] self.re = re self.connected = 0 self.user = None def clientConnectionFailed (self, connector, reason): self.connected = 0 def clientConnectionLost (self, connector, reason): self.connected = 0 def startedConnecting (self, connector): self.connected = 1 def notify (self, event): if (event.signal == 'command') and \ (type(event.data) == tuple) and not self.connected: server = event.data[0] port = event.data[1] self.user = event.data[2] # Attempt the connection. reactor.connectTCP (server, port, self) def main (): re = TwistedRenderer () re.create_screen (550, 320) re.title = "Chat client" re.color = (234, 228, 223) re.reactor = reactor # Use something more legible than the default base.GlobalStyle.styles["default"]["font"]["name"] = "Arial" base.GlobalStyle.styles["default"]["font"]["size"] = 12 # Create the two screen objects. chatter = ChatScreen (re) login = LoginScreen (re) # We always start at the login screen. re.add_widget (login.frame) # Create the factory. factory = ChatFactory (re) re.active_layer[2].add_object (factory, COMMAND) re.start() if __name__ == '__main__': main ()