pythonverse

Python-based client for OpenVerse with extra features
git clone https://code.literati.org/pythonverse.git
Log | Files | Refs | README | LICENSE

commit ad4cfaca6906df855392ae0e016e9df57d3e1a7d
Author: seanl <seanl>
Date:   Wed, 16 Jan 2002 19:06:48 +0000

Initial checkin from users.openverse.com repository.

Diffstat:
ACOPYING | 340+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AOpenVerse.py | 428+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
APythonVerse | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ageneva.ttf | 0
Aopenverse.txt | 62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apvstart.jpg | 0
Apvui_pygame.py | 1200+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Apvui_wx.py | 1012+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atransutil.py | 97+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 3205 insertions(+), 0 deletions(-)

diff --git a/COPYING b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + <signature of Ty Coon>, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/OpenVerse.py b/OpenVerse.py @@ -0,0 +1,428 @@ +# Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org> +# +# This file is part of PythonVerse. +# +# PythonVerse is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# PythonVerse is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PythonVerse; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import sys, os, asyncore, asynchat, socket, string, struct, stat +import transutil + +# Global constants are all caps; global variables start with _ + +BALLOONXOFFSET = 15 +HOME = os.path.expanduser('~/.OpenVerse') +ANIMDIR = os.path.join(HOME, 'anims') +DLDIR = os.path.join(HOME, 'download') +ICONDIR = os.path.join(HOME, 'icons') +IMAGEDIR = os.path.join(HOME, 'images') +OBJDIR = os.path.join(HOME, 'objects') +RIMAGEDIR = os.path.join(HOME, 'rimages') +ROOMDIR = os.path.join(HOME, 'rooms') + + +def checkcache(filename, size): + try: s = os.stat(filename)[stat.ST_SIZE] + except OSError: return None + else: + if s == size or size < 0: return filename + + +class DCC(asyncore.dispatcher): + def __init__(self, host, port, filename, size, progress_callback, + close_callback, sock=()): + asyncore.dispatcher.__init__(self, sock) + self.host = host + self.port = port + self.filename = filename + self.size = size + self.length = 0 + self.outbuf = '' + self.buffer = '' + self.progress_callback = progress_callback + self.close_callback = close_callback + + def __repr__(self): + return '<DCC %s %s:%d %d/%d>' % (self.filename, self.host, self.port, + self.length, self.size) + + def handle_connect(self): + pass + + def handle_write(self): + sent = self.send(self.outbuf) + self.outbuf = self.outbuf[sent:] + + def writable(self): + return len(self.outbuf) > 0 + + def handle_close(self): + print self, 'closing' + self.close() + if self.close_callback is not None: + apply(self.close_callback, (self.tempfilename, self.filename, + self.size)) + + + +class DCCGet(DCC): + def __init__(self, host, port, filename, size, + progress_callback, close_callback): + DCC.__init__(self, host, port, filename, size, progress_callback, + close_callback) + self.tempfilename = os.path.join(RIMAGEDIR, filename) + self.file = open(self.tempfilename, 'wb') + self.size = size + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect((host, port)) + + def handle_read(self): + data = self.recv(4096) + if data: + self.file.write(data) + self.length = self.length + len(data) + self.outbuf = self.outbuf + struct.pack('>I', self.length) + if self.progress_callback is not None: + apply(self.progress_callback, (self.length, self.tempfilename, + self.filename, self.size)) + + if self.length == self.size: + self.file.close() + + +class DCCSendPassive(DCC): + def __init__(self, host, port, filename, size, progress_callback, + close_callback): + DCC.__init__(self, host, port, filename, size, progress_callback, + close_callback) + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect((host, port)) + + self.file = open(filename, 'rb') + self.outbuf = self.file.read(4096) + self.sent = 0 + + def handle_read(self): + data = self.recv(4) + print self, 'received %d bytes' % len(data) + if data: + data = self.buffer + data + # Drop all but one length received + drop = len(data) / 4 * 4 - 4 + data = data[drop:] + if len(data) >= 4: + # Get the number of bytes received by the client + (self.length,) = struct.unpack('>I', data[:4]) + self.buffer = data[4:] + print self + if self.progress_callback is not None: + apply(self.progress_callback, (self.length, self.filename, + self.size)) + + if self.length == self.size: + self.file.close() + self.handle_close() + + def handle_write(self): + #if self.length < self.sent: return + sent = self.send(self.outbuf) + self.outbuf = self.outbuf[sent:] + self.sent = self.sent + sent + if len(self.outbuf) < 4096: + self.outbuf = self.outbuf + self.file.read(4096) + + +class ServerConnection(transutil.Connection): + def __init__(self, host, port, client, nick, avatar): + transutil.Connection.__init__(self, transutil.InputHandler( + (('ABOVE', self.cmd_ABOVE, '(\S+)', (str,)), + ('CHAT', self.cmd_CHAT, '(\S+) (.*)', (str, str)), + ('SCHAT', self.cmd_SCHAT, '(\S+) (\S+) (.*)', (str, str, str)), + ('MOVE', self.cmd_MOVE, '(\S+) (\d+) (\d+) (\d+)', + (str, int, int, int)), + ('EFFECT', self.cmd_EFFECT, '(\S+) (\S+)', (str, str)), + ('PRIVMSG', self.cmd_PRIVMSG, '(\S+) (.*)', + (str, str)), + ('AVATAR', self.cmd_AVATAR, + '(\S+) (\S+) (-?\d+) (-?\d+) (\d+) (-?\d+) (-?\d+)', + (str, str, int, int, int, int, int)), + ('URL', self.cmd_URL, '(\S+) (.*)', (str, str)), + ('NEW', self.cmd_NEW, + '(\S+) (\d+) (\d+) (\S+) (-?\d+) (-?\d+) (\d+) (-?\d+) (-?\d+)', + (str, int, int, str, int, int, int, int, int)), + ('NOMORE', self.cmd_NOMORE, '(\S+)', (str,)), + ('DCCGETAV', self.cmd_DCCGET, '(\d+) (\S+) (\d+)', + (int, str, int)), + ('DCCGETROOM', self.cmd_DCCGET, '(\d+) (\S+) (\d+)', + (int, str, int)), + ('DCCGETOB', self.cmd_DCCGET, '(\d+) (\S+) (\d+)', + (int, str, int)), + ('PING', self.cmd_PING, '', ()), + ('ROOM', self.cmd_ROOM, '(\S+) (\d+)', (str, int)), + ('ROOMNAME', self.cmd_ROOMNAME, '(.*)', (str,)), + ('MOUSEOVER', self.cmd_MOUSEOVER, + '(\S+) (\d+) (\d+) (\S+) (\d+) (\S+) (\d+) (\d+)', + (str, int, int, str, int, str, int, int)), + ('DCCSENDAV', self.cmd_DCCSENDAV, '(\d+) (\S+)', (int, str)), + ('WHOIS', self.cmd_WHOIS, '(\S+) (.*)', (str, str))))) + self.host = host + self.port = port + self.pending_images = {} + self.client = client + self.images = {} + self.nick = nick + avdata = self.parse_anim(avatar) + self.avatar_filename = avdata[0] + self.nx = avdata[1] + self.ny = avdata[2] + self.bx = avdata[3] + self.by = avdata[4] + # Connect last + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect((host, port)) + + def handle_connect(self): + size = os.stat(os.path.join(IMAGEDIR, + self.avatar_filename))[stat.ST_SIZE] + self.write("AUTH %s %d %d %s %d %d %d %d %d\r\n" % + (self.nick, 320, 200, self.avatar_filename, self.nx, self.ny, + size, self.bx, self.by)) + + def handle_close(self): + transutil.Connection.handle_close(self) + self.client.close() + + def debug(self, info): + self.client.debug(info) + + # Utility functions + + def get_image(self, filename, size, command, pcallback, callback, args=()): + # Prevent possible embedded '/' attacks + filename = os.path.basename(filename) + if filename == 'default.gif': size = -1 + try: image = self.images[filename, size] + except KeyError: + # Check in locally cached images + file = checkcache(os.path.join(RIMAGEDIR, filename), size) + if file is None: + # Check my own avatars as well + file = checkcache(os.path.join(IMAGEDIR, filename), size) + + if file is not None: + image = self.client.newimage(file) + self.images[filename, size] = image + return image + + blob = (pcallback, callback, args) + # Take the callback out if it's already in there + for pending in self.pending_images.values(): + if blob in pending: pending.remove(blob) + + try: self.pending_images[filename, size].append(blob) + except KeyError: self.pending_images[filename, size] = [blob] + else: pending.append(blob) + + self.write('%s %s\r\n' % (command, filename)) + + else: return image + + def progress_callback(self, length, tempfilename, filename, size): + for pcallback, callback, args in self.pending_images[(filename, size)]: + if pcallback: apply(pcallback, args + (length,tempfilename,size)) + + def image_callback(self, tempfilename, filename, size): + image = self.client.newimage(tempfilename) + self.images[(filename, size)] = image + for pcallback, callback, args in self.pending_images[(filename, size)]: + apply(callback, args + (image,)) + + # Client functions + + def getnick(self): + return self.nick + + def new_connect(self, host, port): + self.close() + self.host = host + self.port = port + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + self.connect((host, port)) + + def move(self, pos): + x, y = pos + self.write('MOVE %s %d %d 1\r\n' % (self.nick, x, y)) + + def push(self): + self.write('PUSH 100\r\n') + + def effect(self, action): + self.write('EFFECT %s\r\n' % action.lower()) + + def privmsg(self, nicks, text): + for n in nicks: + self.write('PRIVMSG %s %s\r\n' % (n, text)) + return self.nick + + def url(self, nicks, url): + for n in nicks: + self.write('URL %s %s\r\n' % (n, url)) + + def chat(self, text): + self.write('CHAT %s\r\n' % text) + + def set_nick(self, nick): + self.nick = nick + self.write('NICK %s\r\n' % nick) + + def ignore(self, what, nick): + self.write('IGNORE %s %s\r\n' % (what.upper(), nick)) + + def unignore(self, what, nick): + self.write('UNIGNORE %s %s\r\n' % (what.upper(), nick)) + + def quote(self, text): + """Send raw commands to the server""" + self.write('%s\r\n' % text) + + def set_avatar(self, avatar): + av = self.parse_anim(avatar) + try: size = os.stat(os.path.join(IMAGEDIR, av[0]))[stat.ST_SIZE] + except OSError, info: self.client.debug(info) + else: + self.avatar_filename = av[0] + self.nx = av[1] + self.ny = av[2] + self.bx = av[3] + self.by = av[4] + self.write('AVATAR %s %d %d %d %d %d\r\n' % + (av[0], av[1], av[2], size, av[3], av[4])) + + def parse_anim(self, avatar): + """Parse the avatar definition file for information""" + try: avfile = open(os.path.join(ANIMDIR, avatar), 'r') + except: + try: avfile = open(os.path.join(ANIMDIR, avatar + '.av'), 'r') + except: + print "Avatar", avatar, "not found." + if self.avatar_filename is not None: + return [self.avatar_filename, + self.nx, self.ny, self.bx, self.by] + return ["default.gif", 0, 36, 24, 6] + animdata = avfile.readlines() + avfile.close() + for animitem in animdata: + # parse the whole file for future addition of animation + splitanimitem = animitem.split() + if splitanimitem[1] == "MV(anim.x_off)": + nx = int(splitanimitem[2]) + if splitanimitem[1] == "MV(anim.y_off)": + ny = int(splitanimitem[2]) + if splitanimitem[1] == "MV(anim.baloon_x)": + bx = int(splitanimitem[2]) + if splitanimitem[1] == "MV(anim.baloon_y)": + by = int(splitanimitem[2]) + if splitanimitem[1] == "MV(anim.0)": + avimage = splitanimitem[2].strip() + return [avimage, nx, ny, bx, by] + + def whois(self, nick): + self.write('WHOIS %s\r\n' % nick) + + # Command handlers + + def cmd_ABOVE(self, name): + """Raise the named object to the top of the stacking order""" + self.client.raise_object(name) + + def cmd_CHAT(self, nick, text): + self.client.chat(nick, text) + + def cmd_SCHAT(self, emote, nick, text): + self.client.chat(nick, '*%s* %s' % (emote, text)) + + def cmd_MOVE(self, nick, x, y, speed): + # FIXME: Need to use speed too + self.client.move_avatar(nick, x, y, speed) + + def cmd_EFFECT(self, nick, action): + self.client.effect(nick, action) + + def cmd_PRIVMSG(self, nick, text): + #PRIVMSG nick text / nick text + self.client.privmsg(nick, text) + + def cmd_ROOM(self, filename, filesize): + """Set a new background image""" + image = self.get_image(filename, filesize, 'DCCSENDROOM', + self.client.background_progress, + self.client.background_image) + if image is not None: self.client.background_image(image) + + def cmd_AVATAR(self, nick, filename, nx, ny, size, bx, by): + image = self.get_image(filename, size, 'DCCSENDAV', + self.client.avatar_progress, + self.client.avatar_image, (nick,)) + if image is None: image = self.client.newimage() + # Need to shift 15 pixels to the left because OV uses the edge of + # the balloon rather than the arrow as the offset point. I do this + # here because it's OV specific. + self.client.avatar(nick, image, (nx, ny), (bx-BALLOONXOFFSET, by)) + + def cmd_PING(self): + self.write('PONG\r\n') + + def cmd_URL(self, nick, text): + self.client.url(nick, text) + + def cmd_NEW(self, nick, x, y, filename, nx, ny, size, bx, by): + image = self.get_image(filename, size, 'DCCSENDAV', + self.client.avatar_progress, + self.client.avatar_image, (nick,)) + if image is None: image = self.client.newimage() + self.client.new_avatar(nick, (x, y), image, (nx, ny), + (bx-BALLOONXOFFSET, by)) + + def cmd_NOMORE(self, nick): + self.client.del_avatar(nick) + + def cmd_DCCGET(self, port, filename, size): + DCCGet(self.host, port, filename, size, + self.progress_callback, self.image_callback) + + def cmd_DCCSENDAV(self, port, filename): + filename = os.path.join(IMAGEDIR, os.path.basename(filename)) + try: + size = os.stat(filename)[stat.ST_SIZE] + DCCSendPassive(self.host, port, filename, size, None, None) + except IOError, info: self.debug(info) + + def cmd_ROOMNAME(self, name): + self.client.set_title(name) + + def cmd_MOUSEOVER(self, name, x, y, image1, size1, image2, size2, flag): + image1 = self.get_image(image1, size1, 'DCCSENDOB', None, + self.client.mouseover_image1, (name,)) + image2 = self.get_image(image2, size2, 'DCCSENDOB', None, + self.client.mouseover_image2, (name,)) + if image1 is None: image1 = self.client.newimage() + if image2 is None: image2 = self.client.newimage() + self.client.mouseover(name, (x, y), image1, image2) + + def cmd_WHOIS(self, nick, text): + self.client.chat(nick, '*%s* is %s' % (nick, text)) + + +def poll(): + asyncore.poll() diff --git a/PythonVerse b/PythonVerse @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org> +# +# This file is part of PythonVerse. +# +# PythonVerse is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# PythonVerse is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PythonVerse; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# -*-Python-*- + + +import os, sys, string, bisect, transutil, asyncore, time +from types import * +import OpenVerse +import pvui_pygame as ui + + +def loop(): + """Loop until the ui says we're done.""" + while 1: + delay = ui.poll() + if delay < 0: break + elif delay == 0: asyncore.poll() + else: + now = time.time() + when = now + delay + while now < when: + asyncore.poll(when - now) + now = time.time() + + +def main(argv): + # FIXME - just for testing, specific to OpenVerse + try: + nick = argv[1] + server = argv[2] + port = int(argv[3]) + avatar = argv[4] + except: + print >> sys.stderr, 'Usage: %s <nick> <server> <port> <avatar>' % argv[0] + print >> sys.stderr, 'Uses your existing ~/.OpenVerse directories (for now)' + print >> sys.stderr, '<avatar> must be a valid avatar definition file in anims' + print >> sys.stderr, 'You must have pygame from www.pygame.org installed, with the modules' + print >> sys.stderr, 'pygame.image and pygame.font working (depends on SDL, SDL_image, and SDL_ttf)' + print >> sys.stderr, 'Escape to quit, alt-f to toggle fullscreen mode.' + sys.exit(1) + + ui.init() + client = ui.Client() + conn = OpenVerse.ServerConnection(server, port, client, nick, avatar) + client.set_server(conn) + # Enter the event loop + loop() + +if __name__ == '__main__': main(sys.argv) diff --git a/geneva.ttf b/geneva.ttf Binary files differ. diff --git a/openverse.txt b/openverse.txt @@ -0,0 +1,62 @@ +# Notes +Balloon and name x and y are relative to the center of the avatar +Movement final position is the position of the center of the avatar + +# Bidirectional +MOVE x y speed / nick x y speed +EFFECT type / nick type +WHOIS nick / nick user@host +PRIVMSG nick text / nick text +AVATAR filename name_x name_y size balloon_x balloon_y / nick ... +URL url / nick url + +# Client to server only +SEND # Send a file +DCCSENDAV filename # client to server +DCCSENDOB filename # client to server +DCCSENDROOM filename # client to server +IGNORE nick type + AVATAR + EFFECT + MOVE + CHAT + SUB + ALL + +UNIGNORE nick type # Same subcommands as ignore +USERS +SUB user command ... (DCCGET host port size) +PONG +RSEND +CHAT text +SCHAT +NICK newnick +TELL what / what left top right bottom 0? # Clicked in a region / region to click? +SUBMIT what / what x? y? background foreground 0? |text +ENTRY text / what x y length background foreground 0? |default? +PUSH velocity + +# Before user logs in + +USERS +TRANS +AUTH + +# Otherwise send "AUTH REQD" + +# Server to client only +NEW nick x y filename name_x name_y size balloon_x balloon_y (same as MOVE) +NOMORE nick +EXIT_OBJ name left top right bottom 0? host port +DCCGETAV port filename size # server to client +DCCGETOB port filename size # server to client +DCCGETROOM port filename size # server to client +PING +TEXT what x y foreground 3? 0? |text +BOX left top right bottom fillcolor? bordercolor? isgoingaway? +MOUSEOVER what x y filename_on 150? filename_off 150? 1? + +# Stupid commands +QUERY + POS_ALL + diff --git a/pvstart.jpg b/pvstart.jpg Binary files differ. diff --git a/pvui_pygame.py b/pvui_pygame.py @@ -0,0 +1,1200 @@ +# -*-Python-*- +# Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org> +# +# This file is part of PythonVerse. +# +# PythonVerse is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# PythonVerse is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PythonVerse; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# vim:syntax=python + +import sys, bisect, string +import pygame, pygame.font, pygame.image, pygame.time, pygame.draw +from math import * +from pygame.locals import * +from types import * +import transutil + +def wrap_lines(lines, width, font): + r = [] + for text in lines: + r.extend(wrap(text, width, font)) + + c = 0 + for t in r: + if t == "": del r[c] + c = c + 1 + + return r + +def wrap(text, width, font): + """Wrap a line of text, returning a list of lines.""" + + lines = [] + while text: + if font.size(text)[0] <= width: return lines + [text] + try: + i = string.rindex(text, ' ') + while font.size(text[:i])[0] > width: + i = string.rindex(text, ' ', 0, i) + except ValueError: + i = len(text)-1 + while font.size(text[:i])[0] > width and i: i = i - 1 + if not i: raise ValueError, 'width %d too narrow' % width + + lines.append(text[:i]) + text = string.lstrip(text[i:]) + + return lines + + +def progress(fraction, size=(20, 50), fgcolor=(0,255,0), + bgcolor=(255,255,255)): + w, h = size + y = h * fraction + s = pygame.Surface(size) + s.fill(bgcolor, (0, 0, w, h-y)) + s.fill(fgcolor, (0, h-y, w, y)) + pygame.draw.rect(s, fgcolor, (0, 0, w-1, h-1), 1) + s.set_alpha(127) + return s + + +def invertrect(initrect, rects, min_width=50, min_height=10): + left, top = initrect.topleft + right, bottom = initrect.bottomright + width, height = initrect.size + + irects = [] + rects = rects + [Rect(left-1, top, 0, height), + Rect(left, top-1, width, 0), + Rect(right, top, 0, height), + Rect(left, bottom+1, width, 0)] + + for a in rects: + # Pick a left side + l = a.right+1 + if l < left: continue + # Pick a top + for b in rects: + # Make sure the rect can actually border ours + if b.right < l or b.bottom > a.bottom: continue + # Pick a top + t = b.bottom+1 + # Pick a right side + for c in rects: + # Make sure this rect can border ours + if c.left <= l or c.bottom < t or c.top > bottom or \ + c.left <= b.left: continue + r = c.left-1 + w = r - l + h = bottom - t + if w < min_width or h < min_height: continue + # There can be only one rect with these three sides + rect = Rect(l, t, w, h) + # Now find the bottom + for d in rects: + if d.colliderect(rect): + rect.height = d.top-t-1 + # Make sure the rect is still sane and still borders + # on the original border rects + if rect.height < min_height or rect.bottom < a.top or \ + rect.bottom < c.top: break + else: irects.append(rect) + + return irects + + +class Group: + def __init__(self): + self.list = [] + self.pollers = [] + self.active = None + + def add(self, sprite): + self.list.append(sprite) + + def remove(self, sprite): + self.list.remove(sprite) + + def above(self, sprite): + """Move to the top of the stacking order""" + # The way the group is implemented, just reinserting will do it. + self.list.remove(sprite) + self.list.append(sprite) + + def mouse(self, mousepos): + """If a sprite is active, send events only to that sprite until + it deactivates. Otherwise, send to all sprites in order until + one activates.""" + + dirtyrects = [] + # Send the event to all sprites until one activates + sprites = self.list + i = len(sprites) + while i: + i = i - 1 + sprite = sprites[i] + if sprite.mouseme and sprite.rect.collidepoint(mousepos): + if self.active is sprite: return dirtyrects + if self.active is not None: + dirtyrects.extend(self.active.mouseoff()) + + self.active = sprite + dirtyrects.extend(sprite.mouseon()) + return dirtyrects + + if self.active is not None: + dirtyrects.extend(self.active.mouseoff()) + self.active = None + return dirtyrects + + def update(self): + """Update the positions of all the sprites that are moving""" + # Move any avatars that need moving + # FIXME: probably needs optimizing + dirtyrects = [] + ticks = pygame.time.get_ticks() + pollers = self.list + i = len(pollers) + while i: + i = i - 1 + sprite = pollers[i] + if sprite.pollme: + dirtyrects.extend(sprite.poll(ticks)) + + return dirtyrects + + def draw(self, surface, rect=None): + if rect is None: + for sprite in self.list: + surface.blit(sprite.image, sprite.rect) + else: + for sprite in self.list: + # Redraw sprites whose rects overlap this rect, + # but only in the overlapping area + if rect.colliderect(sprite.rect): + r = sprite.rect + surface.blit(sprite.image, rect, + rect.move((-r[0], -r[1]))) + + + +class Sprite: + mouseme = 0 + pollme = 0 + def __init__(self, position, image=None): + self.dead = 0 + # self.group = group + self.image = image + if len(position) == 4: self.rect = position + else: + if image is None: w, h = 0, 0 + else: w, h = image.get_size() + + x, y = position + self.rect = Rect(x-w/2, y-h/2, w, h) + + # group.add(self) + + def __repr__(self): + return '<%s(%s, %s)>' % (self.__class__, self.image, self.rect) + + def poll(self, t, delta): return [] + + def move(self, pos): + rect = self.rect + oldrect = Rect(rect) + rect.center = pos + if oldrect.colliderect(rect): return [oldrect.union(rect)] + else: return [oldrect, rect] + +## def die(self): +## self.group.remove(self) +## self.dead = 1 +## return [self.rect] + + def set_image(self, image): + """Change the image without moving the center""" + self.image = image + oldrect = self.rect + x, y = oldrect.center + w, h = image.get_size() + self.rect = Rect((x-w/2, y-h/2, w, h)) + # Return the affected area + return oldrect.union(self.rect) + + +class ClampingSprite(Sprite): + def __init__(self, pos, image, clamprect): + Sprite.__init__(self, pos, image) + self.pos = pos + self.clamprect = clamprect + self.clamp() + + def move(self, pos): + self.pos = pos + oldrect = Rect(self.rect) + self.rect.center = pos + self.clamp() + if oldrect.colliderect(self.rect): return [oldrect.union(self.rect)] + else: return [oldrect, self.rect] + + def clamp(self): + self.rect = self.rect.clamp(self.clamprect) + + +class Mouseover(Sprite): + mouseme = 1 + def __init__(self, pos, image1, image2): + Sprite.__init__(self, pos, image1) + self.image1 = image1 + self.image2 = image2 + + def mouseon(self): + return [self.set_image(self.image2)] + + def mouseoff(self): + return [self.set_image(self.image1)] + + def set_image1(self, image): + self.image1 = image + return self.set_image(image) + + def set_image2(self, image): + self.image2 = image + + +class Avatar(Sprite): + mouseme = 1 + def __init__(self, group, position, image, nick, noffset, boffset): + Sprite.__init__(self, position, image) + self.rect = self.rect.clamp((0, 0, 640, 480)) + self.group = group + self.nick = nick + self.destpos = None + self.speed = None + self.balloon = None + self.label = None + self.effects = [] + self.effect_count = -1 + self.effect_step = 0 + self.pollme = 0 + self.stoptime = 0 + self.moving = 0 + self.set(noffset, boffset) + + def set(self, noffset, boffset): + self.boffset = boffset + nx, ny = noffset + if nx < -self.rect.width/2-10: nx = -self.rect.width/2-10 + elif nx > self.rect.width/2+10: nx = self.rect.width/2+10 + if ny < -self.rect.height/2-10: ny = -self.rect.height/2-10 + elif ny > self.rect.height/2+10: ny = self.rect.height/2+10 + self.noffset = nx, ny + + def mouseon(self): + if self.label is not None: + print self.nick, 'on' + self.label.die() + # Strip color codes from the nick for display + nick = self.nick + # TODO + sx, sy = _font.size(nick) + s = _font.render(nick, 0, (255, 255, 255)) + image = pygame.Surface((sx+2, sy+2)) + image.set_colorkey((255,165,0)) + image.fill((255,165,0)) + for x in range(3): + for y in range(3): + image.blit(s, (x, y)) + image.blit(_font.render(nick, 1, (0,0,0)), (1, 1)) + image.set_alpha(127) + x, y = self.rect.center + nx, ny = self.noffset + self.label = ClampingSprite((x+nx, y+ny), image, Rect(0, 0, 640, 480)) + self.group.add(self.label) + return [self.label.rect] + + def mouseoff(self): + if self.label is None: + print self.nick, 'off' + return [] + else: + label = self.label + self.group.remove(label) + self.label = None + return [label.rect] + + def move(self, position, speed): + """Changes the location of the avatar's center to the new position""" + self.moving = 1 + if position[0] >= 640 or position[1] > 480: + print >> sys.stderr, 'Attempt to move outside the screen' + return + if speed == 0: speed = 1 + self.speed = speed + pos = self.rect.center + self.startpos = pos + self.starttime = pygame.time.get_ticks() + distance = dist(pos, position) + if distance == 0.0: return + self.dx = 3.0 * (position[0]-pos[0]) * self.speed / distance / 10.0 + self.dy = 3.0 * (position[1]-pos[1]) * self.speed / distance / 10.0 + self.stoptime = self.starttime + distance * 10.0 / 3.0 / speed + self.destpos = position + self.pollme = 1 + + def effect(self, action): + self.effects.append(action) + self.pollme = 1 + + def poll(self, t): + """Move the avatar if necessary""" + + dirtyrects = [] + oldrect = Rect(self.rect) + if self.stoptime != 0: + if t >= self.stoptime: + self.rect.center = self.destpos + self.pollme = 0 + if self.balloon is not None and not self.balloon.dead \ + and self.moving == 1: + bx, by = self.boffset + x, y = self.destpos + dirtyrects.extend(self.balloon.move((x+bx, y+by))) + self.moving = 0 + else: + delta_t = t - self.starttime + x, y = self.startpos + self.rect.center = x+int(round(self.dx*delta_t)), \ + y+int(round(self.dy*delta_t)) + + # apply effects, if any + if self.stoptime == 0: + pos = (320, 200) + else: + pos = self.rect.center + if self.effects != []: + effect = self.effects[0] + self.pollme = 1 + x, y = pos + if self.effect_count == -1: + if effect == 'shiver': + self.effect_step = 3 + self.effect_count = 100 + if effect == 'jump': + self.effect_step = 5 + self.effect_count = 200 + if effect == 'shiver' and self.effect_count >= 0: + if self.effect_step == 3: x = x - 15 + if self.effect_step == 1: x = x + 15 + if self.effect_step == 0: + if self.effect_count > 0: + self.effect_step = 4 + if effect == 'jump' and self.effect_count > 0: + if self.effect_step == 5: y = y - 15 + if self.effect_step == 4: y = y - 30 + if self.effect_step == 3: y = y - 45 + if self.effect_step == 2: y = y - 30 + if self.effect_step == 1: y = y - 15 + if self.effect_step == 0: + if self.effect_count > 0: + self.effect_step = 6 + self.rect.center = (x, y) + pygame.time.wait(15) + self.effect_step = self.effect_step -1 + self.effect_count = self.effect_count -1 + if self.effect_count == -1: + self.rect.center = pos + del self.effects[0] + if self.effects == []: + self.pollme = 0 + + return dirtyrects + [oldrect.union(self.rect)] + + def chat(self, group, text): + x, y = self.rect.center + xoff, yoff = self.boffset + + if self.balloon is None or self.balloon.dead: + self.balloon = Balloon(group, self.group, (x+xoff, y+yoff), text, + self) + group.add(self.balloon) + return [self.balloon.rect] + else: + return self.balloon.add_text(text) + + +def arc(radius, center, start_angle, stop_angle, n): + x, y = center + step = (stop_angle - start_angle) / n + points = [0] * (n+1) + for i in range(n+1): + angle = start_angle + i*step + points[i] = (x + int(round(radius*sin(angle))), + y - int(round(radius*cos(angle)))) + + return tuple(points) + + +def closest(rect, point): + """Find the closest point on a rect to a given point""" + return min(rect.right, max(point[0], rect.left)), \ + min(rect.bottom, max(point[1], rect.top)) + + +def dist(point1, point2): + x1, y1 = point1 + x2, y2 = point2 + return sqrt((x1-x2)**2 + (y1-y2)**2) + + +def rectdist(rect, point): + return dist(closest(rect, point), point) + + +class Balloon(Sprite): + """A speech balloon""" + pollme = 1 + def __init__(self, group, avgroup, pos, text, avatar, + timeout=10000, fgcolor=(0, 0, 0), bgcolor=(255, 255, 255)): + self.timeouts = [pygame.time.get_ticks() + timeout] + self.group = group + self.avgroup = avgroup + Sprite.__init__(self, pos) + self.pos = pos + self.timeout = timeout + self.fgcolor = fgcolor + self.bgcolor = bgcolor + self.avatar = avatar + self.text = [text] + self.rect = None + self.render() + + def move(self, pos): + self.pos = pos + oldrect = self.rect + rect = self.render() + if oldrect.colliderect(rect): return [oldrect.union(rect)] + else: return [oldrect, rect] + + def nearer(self, rect1, rect2): + """Compare two rects based on their distance from our position""" + dist1 = rectdist(rect1, self.pos) + dist2 = rectdist(rect2, self.pos) + if dist1 == dist2: return cmp(rect2.width, rect1.width) + return cmp(dist1, dist2) + + def render(self): + """Render the text, returning the affected rect.""" + + pad = 3 + maxdist = 200 + screen_rect = Rect(0, 0, 640, 480 - (linesize(_font)+2)) + size = linesize(_font) + + # Try not overlapping anything + balloonrects = map(lambda s: s.rect, self.group.list) + try: balloonrects.remove(self.rect) + except ValueError: pass + + # First, try to avoid everything + rects1 = invertrect(screen_rect, balloonrects + + map(lambda s: s.rect, self.avgroup.list)) + rects1.sort(self.nearer) + + # Next, try to avoid just balloons and my av + rects2 = invertrect(screen_rect, balloonrects + + [self.avatar.rect]) + rects2.sort(self.nearer) + + # Finally, just avoid my own av + rects3 = invertrect(screen_rect, [self.avatar.rect]) + rects3.sort(self.nearer) + + rects = filter(lambda r,p=self.pos,m=maxdist: rectdist(r,p) < m, + rects1 + rects2 + rects3) + [screen_rect] + + notdone = 1 + while notdone: + # Loop until we can fit the balloon into *some* rect + for r in rects: + try: lines = wrap_lines(self.text, r.width-pad*2, _font) + except ValueError: continue + if len(lines) * size + pad*2 < r.height: + notdone = 0 + break + else: + # Couldn't render the balloon, delete some lines + del self.text[0] + del self.timeouts[0] + + # Count the number of lines in each sequence of text + surfaces = map(lambda t,f=_font,c=self.fgcolor: f.render(t, 1, c), + lines) + width = max(map(lambda l: _font.size(l)[0], lines)) + (pad * 2) + height = len(surfaces) * size + (pad * 2) + + x, y = self.pos + rect = Rect(x-width/2, y-height/2, width, height).clamp(r) + + bigrect = rect.union((self.pos, (1, 1))) + image = pygame.Surface(bigrect.size) + image.set_colorkey((255,165,0)) + image.fill((255,165,0)) + + cx, cy = closest(rect, self.pos) + cx = cx - bigrect.left + cy = cy - bigrect.top + x = x - bigrect.left + y = y - bigrect.top + left = rect.left - bigrect.left + right = rect.right - bigrect.left - 1 + top = rect.top - bigrect.top + bottom = rect.bottom - bigrect.top - 1 + # Calculate arcs for corners + nwarc = arc(pad, (left+pad, top+pad), pi*1.5, pi*2, 5) + nearc = arc(pad, (right-pad, top+pad), 0, pi*0.5, 5) + searc = arc(pad, (right-pad, bottom-pad), pi*0.5, pi, 5) + swarc = arc(pad, (left+pad, bottom-pad), pi, pi*1.5, 5) + + # Go in a clockwise direction + if x > right-pad*2: + # Drawing to the east + if y > bottom-pad*2: + # Draw the arrow to the southeast + points = ((right, bottom-pad), (x, y), + (right-pad, bottom)) + swarc + nwarc + nearc + elif y < top+pad*2: + # Draw the arrow to the northeast + points = ((right-pad, top), (x, y), + (right, top+pad)) + searc + swarc + nwarc + elif x > right: + # Due east + points = ((right, cy-pad), (x, y), + (right, cy+pad)) + \ + searc + swarc + nwarc + nearc + else: + # Arrow is inside balloon + points = searc + swarc + nwarc + nearc + elif x < left+pad*2: + # Drawing to the west + if y > bottom-pad*2: + # Southwest + points = ((left+pad, bottom), (x, y), + (left, bottom-pad)) + \ + nwarc + nearc + searc + elif y < top+pad*2: + # Northwest + points = ((left, top+pad), (x, y), + (left+pad, top)) + \ + nearc + searc + swarc + elif x < left: + # Due west + points = ((left, cy+pad), (x, y), + (left, cy-pad)) + \ + nwarc + nearc + searc + swarc + else: + # Arrow is inside balloon + points = nwarc + nearc + searc + swarc + elif y < top: + # Due north + points = ((cx-pad, top), (x, y), (cx+pad, top)) + \ + nearc + searc + swarc + nwarc + elif y > bottom: + # Due south + points = ((cx+pad, bottom), (x, y), + (cx-pad, bottom)) + \ + swarc + nwarc + nearc + searc + else: + print >> sys.stderr, 'Oops, arrow point is inside balloon!' + points = swarc + nwarc + nearc + searc + + pygame.draw.polygon(image, (255,255,255), points, 0) + pygame.draw.polygon(image, (0,0,0), points, 1) + y = rect.top - bigrect.top + pad + x = rect.left - bigrect.left + for s in surfaces: + image.blit(s, (((width - s.get_width())/2+x), y)) + y = y + size + + self.image = image + + #rect = rect.clamp((0, 0, 640, 480)) + self.rect = bigrect + + return bigrect + + def add_text(self, text): + """Add text to the balloon, scrolling it if necessary.""" + self.text.append(text) + self.timeouts.append(pygame.time.get_ticks() + self.timeout) + return [self.rect.union(self.render())] + + def poll(self, ticks): + if ticks >= self.timeouts[0]: + del self.timeouts[0] + del self.text[0] + if self.text: return [self.rect.union(self.render())] + else: + self.group.remove(self) + self.dead = 1 + return [self.rect] + else: return [] + + +class Entry(Sprite): + def __init__(self, pos, width=640, histlength=100, color=(255,255,255), + bgcolor=(0,0,0)): + Sprite.__init__(self, pos, pygame.Surface((0, 0))) + self.pos = pos + self.font = _font + self.text = u'' + self.color = color + self.bgcolor = bgcolor + self.width = width + self.cursor = 0 + self.buffer = [] + self.index = 0 + self.histlength = histlength + + def __len__(self): return len(self.text) + + def render(self): + oldrect = self.rect + cursorsize = 1 + start = 0 + while self.font.size(self.text[start:self.cursor+5])[0] > self.width: + start = start + 1 + if self.text: + image = self.font.render(self.text[start:], 1, + self.color, self.bgcolor) + self.image = pygame.Surface((image.get_width() + cursorsize, + image.get_height())) + self.image.blit(image, (0, 0)) + # Render the cursor + cursor_pos = self.font.size(self.text[start:self.cursor])[0] + cursor_height = self.image.get_height() + self.image.fill((0,255,0), (cursor_pos, 0, cursorsize, + cursor_height)) + else: self.image = pygame.Surface((0, 0)) + self.rect = Rect(self.pos, self.image.get_size()) + return oldrect.union(self.rect) + + def insert(self, c): + self.text = self.text[:self.cursor] + c + self.text[self.cursor:] + self.cursor = self.cursor + len(c) + return self.render() + + def backspace(self, n=1): + """Delete one character at the end""" + + if self.cursor == 0: return + self.text = self.text[:self.cursor-n] + self.text[self.cursor:] + self.cursor = self.cursor - n + return self.render() + + def delete(self, n=1): + if self.cursor == len(self.text): return + self.text = self.text[:self.cursor] + self.text[self.cursor+n:] + return self.render() + + def home(self): + self.cursor = 0 + return self.render() + + def end(self): + self.cursor = len(self.text) + return self.render() + + def move_cursor(self, n): + self.cursor = self.cursor + n + if self.cursor > len(self.text): self.cursor = len(self.text) + if self.cursor < 0: self.cursor = 0 + return self.render() + + def history(self, n): + if not self.buffer: return + if self.text and self.index < len(self.buffer) and \ + self.text != self.buffer[self.index]: + self.add_history() + self.index = self.index + n + if self.index >= len(self.buffer): self.index = len(self.buffer) - 1 + if self.index < 0: self.index = 0 + self.text = self.buffer[self.index] + self.cursor = len(self.text) + return self.render() + + def add_history(self): + self.buffer.append(self.text) + if len(self.buffer) > self.histlength: + del self.buffer[:len(self.buffer) - self.histlength] + + def clear(self): + if self.text: self.add_history() + self.index = len(self.buffer) + self.text = u'' + self.cursor = 0 + return self.render() + + +class Text: + def __init__(self, text, font, width, fgcolor, bgcolor=None): + self.text = text + self.font = font + self.fgcolor = fgcolor + self.bgcolor = bgcolor + if bgcolor is None: + self.image = font.render(text, 1, fgcolor) + self.shadow = font.render(text, 1, (0, 0, 0)) + else: + self.image = font.render(text, 1, fgcolor, bgcolor) + self.shadow = None + + def draw(self, pos, rect=None): + """Pos is the position of the upper left of my rect""" + myrect = self.image.get_rect().move(pos) + if rect is None: + if self.shadow is not None: + surface.blit(self.shadow, (pos[0]+1,pos[1]+1)) + return surface.blit(self.image, pos) + + rect = myrect.clip(rect) + if self.shadow is not None: + surface.blit(self.shadow, rect.move(1, 1), + ((rect.left-myrect.left-1, rect.top-myrect.top-1), + rect.size)) + return surface.blit(self.image, rect, + ((rect.left-myrect.left, rect.top-myrect.top), + rect.size)) + + +class Console(Sprite): + """Transparent scrollable text widget.""" + def __init__(self, rect, font, scrollback=1000, + bg=(255, 165, 0)): + image = pygame.Surface(rect.size) + image.set_colorkey(bg) + image.set_alpha(127) + image.fill(bg) + Sprite.__init__(self, rect, image) + self.scrollback = scrollback + # Use a circular buffer for efficiency + self.buffer = [''] * scrollback + self.font = font + self.bg = bg + self.index = 0 + self.linesize = linesize(font) + + def add_lines(self, paragraphs): + for paragraph in paragraphs: + self.buffer[self.index] = paragraph + self.index = self.index + 1 + if self.index >= self.scrollback: self.index = 0 + + return self.render() + + def render(self): + """Update my image to match the current buffer""" + self.image.fill(self.bg) + y = self.rect.height - 1 + i = self.index + while i and y > 0-linesize(_font): + i = i - 1 + lines = wrap(self.buffer[i], self.rect.width - 2, self.font) + j = len(lines) + while j and y > 0: + j = j - 1 + y = y - self.linesize + line = lines[j] + ss = self.font.render(line, 0, (0,0,0)) + for xx in range(3): + for yy in range(3): + self.image.blit(ss, (xx, y + (yy - 1))) + self.image.blit(self.font.render(line, 1, (255,255,255)), (1, y)) + + return [self.rect] + + +class Client: + """Callbacks for the server connection""" + def __init__(self): + global _clients, _active + self.width, self.height = 640, 480 + self.background = progress(0, (self.width, self.height)) + self.server = None + self.sprites = Group() + self.balloons = Group() + self.text = Group() + self.avatars = {} + self.console_height = self.height - (linesize(_font) + 2) + self.entry = Entry((0, self.console_height + 1)) + self.text.add(self.entry) + self.console = Console(Rect(0, 0, self.width, self.console_height), + _font) + self.console_active = 0 + self.handler = transutil.InputHandler( + (('connect', self.cmd_connect, '(\S+) (\d+)', (str, int)), + ('reconnect', self.cmd_reconnect, '', ()), + ('nick', self.cmd_nick, '(\S+)', (str,)), + ('ignore', self.cmd_ignore, '(\S+) (\S+)', (str, str)), + ('unignore', self.cmd_unignore, '(\S+) (\S+)', (str, str)), + ('msg', self.cmd_msg, '(\S+) (.*)', (str, str)), + ('url', self.cmd_url, '(\S+) (\S+)', (str, str)), + ('quote', self.cmd_quote, '(.*)', (str,)), + ('avatar', self.cmd_avatar, '(\S+)', (str,)), + ('push', self.cmd_push, '', ()), + ('effect', self.cmd_effect, '(\S+)', (str,)), + ('whois', self.cmd_whois, '(\S+)', (str,)))) + _clients.append(self) + _active = self + + # Utility functions + + def set_server(self, server): + """Set the server connection that the client talks to""" + self.server = server + + def redraw(self, rects=None): + """Redraw entire screen. Unfortunately required for scrolling text""" + if self is not _active: return + if rects is None: + _display.blit(self.background, (0, 0)) + self.sprites.draw(_display) + self.balloons.draw(_display) + self.text.draw(_display) + pygame.display.update() + else: + if not type(rects) is ListType: rects = [rects] + for rect in rects: + _display.blit(self.background, rect, rect) + self.sprites.draw(_display, rect) + self.text.draw(_display, rect) + self.balloons.draw(_display, rect) + + pygame.display.update(rects) + + def debug(self, s): + """Handle debug messages""" + print >> sys.stderr, "DEBUG: %s" % s + + def write(self, s): + dirtyrects = self.console.add_lines(string.split(s, '\n')) + if self.console_active: return dirtyrects + else: return [] + + # Command handlers + + def cmd_connect(self, host, port): + self.write('-> Changing rooms to %s:%d' % (host, port)) + # delete all objects/avatars here + self.sprites = Group() + self.avatars = {} + self.set_title('PythonVerse') + self.redraw() + self.server.new_connect(host, port) + + def cmd_reconnect(self): + host, port = self.server.gethostport() + self.cmd_connect(host, port) + + def cmd_nick(self, nick): + self.server.set_nick(nick) + + def cmd_ignore(self, nick, what): + self.server.ignore(what, nick) + + def cmd_unignore(self, nick, what): + self.server.unignore(what, nick) + + def cmd_quote(self, text): + self.server.quote(text) + + def cmd_avatar(self, avatar): + self.server.set_avatar(avatar) + + def cmd_msg(self, nicks, text): + dirtyrects = self.write('!Whispering to %s! %s' % (nicks, text)) + nicks = string.split(nicks, ',') + nick = self.server.privmsg(nicks, text) + avatar = self.avatars[nick] + dirtyrects.extend(avatar.chat(self.balloons, text)) + dirtyrects.append(avatar.rect) + self.redraw(dirtyrects) + + def cmd_url(self, nicks, url): + nicks = string.split(nicks, ',') + self.server.url(nicks, url) + + def cmd_push(self): + self.server.push() + + def cmd_effect(self, action): + self.server.effect(action) + + def cmd_whois(self, nick): + self.server.whois(nick) + + # UI functions + + def test_rect(self): + rects = invertrect(Rect(0, 0, 640, 480 - (linesize(_font) + 2)), map(lambda s: s.rect, self.sprites.list + self.balloons.list)) + for r in rects: pygame.draw.rect(_display, (0, 0, 0), r, 1) + pygame.display.update() + + def poll(self): + """Update moving stuff, clean up old balloons, etc""" + dirtyrects = self.sprites.update() + dirtyrects.extend(self.balloons.update()) + if dirtyrects: self.redraw(dirtyrects) + + def mouse(self, event): + """Check for mouse movement""" + + dirtyrects = self.sprites.mouse(event.pos) + if dirtyrects: self.redraw(dirtyrects) + + def toggle_console(self): + if self.console_active: + self.console_active = 0 + self.text.remove(self.console) + else: + self.console_active = 1 + self.text.add(self.console) + + self.redraw(self.console.rect) + + def handle_event(self, event): + if event.type == KEYDOWN: + if event.mod & KMOD_CTRL: + if event.key == K_u: self.redraw(self.entry.clear()) + elif event.key == K_BACKSPACE: self.redraw(self.entry.backspace()) + elif event.key == K_DELETE: self.redraw(self.entry.delete()) + elif event.key == K_LEFT: self.redraw(self.entry.move_cursor(-1)) + elif event.key == K_RIGHT: self.redraw(self.entry.move_cursor(1)) + elif event.key == K_UP: self.redraw(self.entry.history(-1)) + elif event.key == K_DOWN: self.redraw(self.entry.history(1)) + elif event.key == K_HOME: self.redraw(self.entry.home()) + elif event.key == K_END: self.redraw(self.entry.end()) + elif event.key == K_RETURN: + text = self.entry.text + self.redraw(self.entry.clear()) + if text and text[0] == '/': + text = text[1:] + if text and text[0] != '/': + try: self.handler.handle(text) + except transutil.HandlerError, info: self.debug(info) + return + + self.server.chat(text.encode('utf-8')) + else: self.redraw(self.entry.insert(event.unicode)) + elif event.type == MOUSEBUTTONDOWN: self.server.move(event.pos) + + # Transport-called functions + + def background_image(self, image): + """Change the background""" + self.background = image + self.redraw() + + def background_progress(self, length, filename, size): + self.background = progress(float(length)/float(size), (640, 480), + (0, 0, 255), (0, 0, 0)) + self.redraw() + + def set_title(self, title): + """Change the room's name""" + pygame.display.set_caption(title) + + def raise_object(self, name): + """Raise the named object to the top of the stacking order.""" + try: avatar = self.avatars[name] + except: self.debug('No avatar called %s' % name) + else: + self.sprites.above(avatar) + self.redraw(avatar.rect) + + def mouseover(self, name, pos, image1, image2): + """Create a mouseover object""" + mo = Mouseover(pos, image1, image2) + self.sprites.add(mo) + self.avatars[name] = mo + self.redraw(mo.rect) + + def newimage(self, filename=None): + """Load an image from a file object""" + #self.debug('newimage %s' % repr(filename)) + if filename is None: return progress(0) + image = pygame.image.load(filename).convert_alpha() + return image + + def new_avatar(self, nick, pos, image, noffset, boffset): + # Fixme: need a real default image + avatar = Avatar(self.sprites, pos, image, nick, noffset, boffset) + self.sprites.add(avatar) + self.avatars[nick] = avatar + dirtyrects = self.write('*%s* entered the room.' % nick) +# dirtyrects.extend(avatar.chat(self.balloons, +# '*%s* entered the room.' % nick)) + dirtyrects.append(avatar.rect) + self.redraw(dirtyrects) + + def del_avatar(self, nick): + try: + avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: + dirtyrects = self.write('*%s* left the room.' % nick) +# dirtyrects.extend(avatar.chat(self.balloons, +# '*%s* left the room' % nick)) + dirtyrects.append(avatar.rect) + del self.avatars[nick] + self.sprites.remove(avatar) + self.redraw(dirtyrects) + + def avatar(self, nick, image, noffset, boffset): + """Change the avatar for a nick""" + # FIXME: need to handle other parameters (bubble position, nametag) + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: + self.redraw(avatar.set_image(image)) + avatar.set(noffset, boffset) + + def mouseover_image1(self, name, image): + """Set the unactivated image for a mouseover""" + self.redraw(self.avatars[name].set_image1(image)) + + def mouseover_image2(self, name, image): + """Set the activated image for a mouseover""" + self.avatars[name].set_image2(image) + + def avatar_image(self, nick, image): + """Set the image for an avatar""" + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: + self.redraw(avatar.set_image(image)) + + def avatar_progress(self, nick, length, filename, size): + """Set an avatar's image to a progress bar""" + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: + rect = avatar.set_image(progress(float(length)/float(size))) + self.redraw(rect) + + def move_avatar(self, nick, x, y, speed): + try: avatar = self.avatars[nick] + except: self.debug('No avatar called %s' % nick) + else: + avatar.move((x, y), speed) + + def effect(self, nick, action): + action.lower() + try: avatar = self.avatars[nick] + except: self.debug('No avatar called %s' % nick) + else: avatar.effect(action) + + def privmsg(self, nick, s): + dirtyrects = self.write('!%s whispers! %s' % (nick, s)) + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: self.redraw(dirtyrects + avatar.chat(self.balloons, s)) + + def url(self, nick, url): + dirtyrects = self.write('[URL from %s] %s' % (nick, url)) + # add in temporary URL links here + self.redraw(dirtyrects) + + def chat(self, nick, s): + dirtyrects = self.write('<%s> %s' % (nick, s)) + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: dirtyrects.extend(avatar.chat(self.balloons, s)) + self.redraw(dirtyrects) + + +# Exported functions + +def linesize(font): + # Bug in pygame or SDL_ttf? + size = font.get_linesize() + if size == 0: + size = font.get_height() + + return size + + +_clients = [] +_display = None +_active = None + +def init(): + global _font, _display, _lastpoll + + pygame.init() + _display = pygame.display.set_mode((640,480)) + # display startup image + s = pygame.display.get_surface() + i = pygame.image.load('pvstart.jpg') + s.blit(i, (0, 0)) + # other settings + pygame.display.set_caption('PythonVerse') + _font = pygame.font.Font('geneva.ttf', 12) + pygame.key.set_repeat(300, 30) + pygame.event.set_blocked((ACTIVEEVENT, KEYUP, MOUSEBUTTONUP, + JOYAXISMOTION, JOYBALLMOTION, JOYHATMOTION, + JOYBUTTONUP, JOYBUTTONDOWN, VIDEORESIZE, + VIDEOEXPOSE)) + _lastpoll = pygame.time.get_ticks() + + +def poll(): + global _lastpoll + # Handle mouseevents + events = pygame.event.get(MOUSEMOTION) + if events: + event = events[-1] + _active.mouse(event) + + events = pygame.event.get((KEYDOWN, MOUSEBUTTONDOWN, QUIT)) + pygame.event.pump() + for event in events: + if event.type == KEYDOWN: + if event.key == K_ESCAPE: + pygame.event.post(pygame.event.Event(QUIT)) + elif event.mod & KMOD_ALT: + if event.key == K_f: pygame.display.toggle_fullscreen() + elif event.key == K_c: _active.toggle_console() + elif event.key == K_t: _active.test_rect() + else: _active.handle_event(event) + + elif event.type == QUIT: return -1 + else: _active.handle_event(event) + + i = len(_clients) + while i: + i = i - 1 + done = _clients[i].poll() + if done: del _clients[i] + + now = pygame.time.get_ticks() + delay = 15 - (now - _lastpoll) + _lastpoll = now + if delay < 0: return 0 + return delay/1000.0 + + diff --git a/pvui_wx.py b/pvui_wx.py @@ -0,0 +1,1012 @@ +# -*-Python-*- +# Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org> +# +# This file is part of PythonVerse. +# +# PythonVerse is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# PythonVerse is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PythonVerse; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# vim:syntax=python + +import sys, bisect, string, thread, asyncore, time +import OpenVerse, transutil +from wxPython.wx import * +from math import * +from types import * + +def wrap_lines(dc, lines, width): + r = [] + for text in lines: + r.extend(wrap(dc, text, width)) + + return r + +def wrap(dc, text, width): + """Wrap a line of text, returning a list of lines.""" + + lines = [] + while text: + if dc.GetTextExtent(text)[0] <= width: return lines + [text] + try: + i = string.rindex(text, ' ') + while dc.GetTextExtent(text[:i])[0] > width: + i = string.rindex(text, ' ', 0, i) + except ValueError: + i = len(text)-1 + while dc.GetTextExtent(text[:i])[0] > width and i: i = i - 1 + if not i: raise ValueError, 'width %d too narrow' % width + + lines.append(text[:i]) + text = string.lstrip(text[i:]) + + return lines + + +def progress(fraction, size=(20, 50)): + w, h = size + bitmap = wxEmptyBitmap(w, h) + y = h * fraction + dc = wxMemoryDC() + dc.SelectObject(bitmap) + dc.SetPen(wxTRANSPARENT_PEN) + dc.SetBrush(wxBLACK_BRUSH) + dc.DrawRectangle(0, 0, w, h-y) + dc.SetBrush(wxGREEN_BRUSH) + dc.DrawRectangle(0, h-y+1, w, y) + return bitmap + + +def invertrect(left, top, width, height, rects, min_width=50, min_height=10): + irects = [] + rects = rects + [(left-1, top, 0, height), + (left, top-1, width, 0), + (left+width, top, 0, height), + (left, top+height, width, 0)] + + for ax, ay, aw, ah in rects: + # Pick a left side + l = ax+aw + if l < left: continue + # Pick a top + for bx, by, bw, bh in rects: + # Make sure the rect can actually border ours + if bx+bw <= l or by+bh > ay+ah: continue + # Pick a top + t = by+bh + # Pick a right side + for cx, cy, cw, ch in rects: + # Make sure this rect can border ours + if cx-1 <= l or cy+ch <= t or \ + cy >= top+height or \ + cx-1 <= bx: continue + r = cx-1 + #w = r - l + #h = bottom - t + if r-l < min_width or top+height-t < min_height: continue + bot = top+height-1 + # There can be only one rect with these three sides + #rect = wxRect(l, t, w, h) + # Now find the bottom + for dx, dy, dw, dh in rects: + # Check if this rect overlaps ours + if r >= dx and l < dx+dw and \ + bot >= dy and t < dy+dh: + bot = dy-1 + # Make sure the rect is still sane and still borders + # on the original border rects + if bot-t < min_height or \ + bot < ay or bot < cy: break + else: irects.append((l, t, r-l+1, bot-t+1)) + + return irects + + +class Sprite(wxEvtHandler): + mouseme = 0 + pollme = 0 + def __init__(self, parent, x, y): + wxEvtHandler.__init__(self) + self.dead = 0 + self.parent = parent + self.x = x + self.y = y + self.rect = None + self.parent.AddSprite(self) + + def __repr__(self): + return '<%s(%s, %s)>' % (self.__class__, self.image, self.rect) + + def move(self, x, y): + self.x = x + self.y = y + self.CalcRect() + + def SetRect(self, x, y, w, h): + dc = wxClientDC(self.parent) + if self.rect is not None: + ox, oy, ow, oh = self.rect + self.rect = x, y, w, h + if ox+oh > x and ox <= x+w and oy+oh > y and oy <= y+h: + # The rectangles overlap + nx = min(x, ox) + ny = min(y, oy) + w = max(x+w, ox+ow) - nx + h = max(y+h, oy+oh) - ny + x = nx + y = ny + else: self.parent.DoDrawing(dc, ox, oy, ow, oh) + else: self.rect = x, y, w, h + self.parent.DoDrawing(dc, x, y, w, h) + + +class Mouseover(Sprite): + def __init__(self, parent, x, y, image1, image2): + self.image1 = image1 + self.image2 = image2 + self.on = 0 + Sprite.__init__(self, parent, x, y) + self.CalcRect() + + def mouseon(self): + self.on = 1 + self.CalcRect() + + def mouseoff(self): + self.on = 0 + self.CalcRect() + + def set_image1(self, image): + self.image1 = image + self.CalcRect() + + def set_image2(self, image): + self.image2 = image + self.CalcRect() + + def CalcRect(self): + if self.on: image = self.image2 + else: image = self.image1 + w = image.GetWidth() + h = image.GetHeight() + x = self.x-w/2 + y = self.y-h/2 + self.SetRect(x, y, w, h) + + def draw(self, dc): + wxLogVerbose('draw') + if self.on: image = self.image2 + else: image = self.image1 + w = self.image2.GetWidth() + h = self.image2.GetHeight() + dc.DrawBitmap(image, self.x-w/2, self.y-h/2, 1) + + +class MoveTimer(wxTimer): + """Call the update function for avatars""" + def __init__(self): + wxTimer.__init__(self) + self.sprites = [] + + def Notify(self): + t = time.time() + sprites = self.sprites + for i in range(len(sprites)-1, -1, -1): + sprite = sprites[i] + r = sprite.update(t) + if r: del sprites[i] + + if not sprites: self.Stop() + + def add(self, sprite): + if sprite in self.sprites: return + self.sprites.append(sprite) + if len(self.sprites) == 1: + wxLogVerbose('starting timer') + self.Start(40) + +_timer = MoveTimer() + + +class Avatar(Sprite): + mouseme = 1 + def __init__(self, parent, x, y, bitmap, nick, noffset, boffset): + Sprite.__init__(self, parent, x, y) + self.bitmap = bitmap + self.nick = nick + self.destpos = None + self.speed = None + self.balloon = None + self.label_on = 1 + self.set(noffset, boffset) + + def SaneNametagOffset(self): + w = self.bitmap.GetWidth() + h = self.bitmap.GetHeight() + nx = max(-w/2-10, min(self.nx, w/2+10)) + ny = max(-h/2-10, min(self.ny, h/2+10)) + return nx, ny + + def SaneBalloonOffset(self): + w = self.bitmap.GetWidth() + h = self.bitmap.GetHeight() + bx = max(-w/2-10, min(self.bx, w/2+10)) + by = max(-h/2-10, min(self.by, h/2+10)) + return bx, by + + def Balloon(self): + w = self.bitmap.GetWidth() + h = self.bitmap.GetHeight() + bx, by = self.SaneBalloonOffset() + if self.balloon is None or self.balloon.dead: + self.balloon = Balloon(self.parent, self, self.x+bx, self.y+by) + + return self.balloon + + def SetImage(self, bitmap): + self.bitmap = bitmap + self.CalcRect() + + def draw(self, dc): + nx, ny = self.SaneNametagOffset() + dc.DrawBitmap(self.bitmap, self.x-self.bitmap.GetWidth()/2, + self.y-self.bitmap.GetHeight()/2, 1) + if self.label_on: + dc.SetFont(wxSMALL_FONT) + w, h = dc.GetTextExtent(self.nick) + dc.SetTextForeground(wxCYAN) + dc.SetLogicalFunction(wxXOR) + dc.DrawText(self.nick, self.x+nx-w/2, self.y+ny-h/2) + dc.SetTextForeground(wxBLACK) + dc.SetLogicalFunction(wxCOPY) + + def CalcRect(self): + w = self.bitmap.GetWidth() + h = self.bitmap.GetHeight() + x = self.x-w/2 + y = self.y-h/2 + if self.label_on: + dc = wxClientDC(self.parent) + dc.SetFont(wxSMALL_FONT) + tw, th = dc.GetTextExtent(self.nick) + nx, ny = self.SaneNametagOffset() + tx, ty = self.x+nx-tw/2, self.y+ny-th/2 + x, y, w, h = RectUnion((x, y, w, h), + (tx, ty, tw, th)) + self.SetRect(x, y, w, h) + + def set(self, noffset, boffset): + self.bx, self.by = boffset + self.nx, self.ny = noffset + self.CalcRect() + + def AnimateMove(self, position, speed): + """Changes the location of the avatar's center to the new position""" + wxLogVerbose('AnimateMove') + if position[0] >= 640 or position[1] > 480: + wxLogWarning('Attempt to move outside the screen') + return + self.speed = speed + pos = self.x, self.y + self.startpos = pos + self.starttime = time.time() + distance = dist(pos, position) + if distance == 0.0: return + self.dx = 300.0 * (position[0]-pos[0]) * self.speed / distance + self.dy = 300.0 * (position[1]-pos[1]) * self.speed / distance + self.stoptime = self.starttime + distance / 300.0 / speed + self.destpos = position + _timer.add(self) + + def update(self, t): + """Move the avatar if necessary""" + + if t >= self.stoptime: + self.move(self.destpos[0], self.destpos[1]) + if self.balloon is not None and not self.balloon.dead: + bx, by = self.SaneBalloonOffset() + x, y = self.destpos + self.balloon.move(x+bx, y+by) + + return 1 + else: + delta_t = t - self.starttime + x, y = self.startpos + x = x+int(round(self.dx*delta_t)) + y = y+int(round(self.dy*delta_t)) + self.move(x, y) + + +def arc(radius, center, start_angle, stop_angle, n): + x, y = center + step = (stop_angle - start_angle) / n + points = [0] * (n+1) + for i in range(n+1): + angle = start_angle + i*step + points[i] = (x + int(round(radius*sin(angle))), + y - int(round(radius*cos(angle)))) + + return tuple(points) + + +def closest(rect, x, y): + """Find the closest point on a rect to a given point""" + rx, ry, rw, rh = rect + return min(rx+rw-1, max(x, rx)), min(ry+rh-1, max(y, ry)) + + +def dist(point1, point2): + x1, y1 = point1 + x2, y2 = point2 + return sqrt((x1-x2)**2 + (y1-y2)**2) + + +def rectdist(rect, x, y): + return dist(closest(rect, x, y), (x, y)) + + +def ClampRect(r1, r2): + x1, y1, w1, h1 = r1 + x2, y2, w2, h2 = r2 + x = max(x1, x2) + y = max(y1, y2) + if x+w1 > x2+w2: x = x2+w2-w1 + if y+h1 > y2+h2: y = y2+h2-h1 + return x, y, w1, h1 + + +def RectUnion(r1, r2): + x1, y1, w1, h1 = r1 + x2, y2, w2, h2 = r2 + x = min(x1, x2) + y = min(y1, y2) + w = max(x1+w1, x2+w2)-x + h = max(y1+h1, y2+h2)-y + return x, y, w, h + + +class Balloon(Sprite): + """A speech balloon""" + # Padding for balloon rectangles, also radius of corner arcs + pad = 3 + + def __init__(self, parent, avatar, x, y): + Sprite.__init__(self, parent, x, y) + self.avatar = avatar + self.text = [] + #EVT_TIMER(self, -1, self.OnTimer) + + def OnTimer(self): + if self.dead: return + del self.text[0] + if self.text: self.CalcRect() + else: + self.dead = 1 + self.parent.RemoveSprite(self) + + def move(self, x, y): + self.x = x + self.y = y + self.CalcRect() + + def nearer(self, rect1, rect2): + """Compare two rects based on their distance from our position""" + dist1 = rectdist(rect1, self.x, self.y) + dist2 = rectdist(rect2, self.x, self.y) + if dist1 == dist2: return cmp(rect2[2], rect1[2]) + return cmp(dist1, dist2) + + def CalcRect(self): + rects = self.parent.BalloonRects(self) + dc = wxClientDC(self.parent) + dc.SetFont(wxSMALL_FONT) + notdone = 1 + while notdone: + # Loop until we can fit the balloon into *some* rect + for r in rects: + try: lines = wrap_lines(dc, self.text, r[2]-self.pad*2) + except ValueError: continue + height = 0 + for l in lines: height = height + dc.GetTextExtent(l)[1] + if height < r[3]: + notdone = 0 + break + else: + # Couldn't render the balloon, delete some lines + del self.text[0] + + # Count the number of lines in each sequence of text + width = max(map(lambda l, dc=dc: dc.GetTextExtent(l)[0], lines)) +\ + (self.pad * 2) + + self.lines = lines + self.textrect = ClampRect((self.x-width/2, self.y-height/2, width, + height), r) + bigrect = RectUnion(self.textrect, + (self.x, self.y, 1, 1)) + self.SetRect(bigrect[0], bigrect[1], bigrect[2], bigrect[3]) + + def draw(self, dc): + # The rectangles had better already be calculated. + pad = self.pad + left, top, w, h = self.textrect + right = left+w-1 + bottom = top+h-1 + dc.SetPen(wxBLACK_PEN) + dc.DrawRoundedRectangle(left, top, w, h, pad) + x = self.x + y = self.y + if x < left or x > right or y < top or y > bottom: + cx, cy = closest(self.textrect, x, y) + if x > right-pad*2: + # Drawing to the east + if y > bottom-pad*2: + # Draw the arrow to the southeast + x1 = right + y1 = bottom-pad + x2 = right-pad + y2 = bottom + elif y < top+pad*2: + # Draw the arrow to the northeast + x1 = right + x2 = right-pad + y2 = top+1 + y1 = top+pad + elif x > right: + # Due east + x1 = right + y1 = cy-pad + x2 = right + y2 = cy+pad + elif x < left+pad*2: + # Drawing to the west + if y > bottom-pad*2: + # Southwest + x1 = left + y1 = bottom-pad + x2 = left+pad + y2 = bottom + elif y < top+pad*2: + # Northwest + x1 = left + y1 = top+pad + x2 = left+pad + y2 = top+1 + elif x < left: + # Due west + x1 = left+1 + y1 = cy+pad + x2 = left+1 + y2 = cy-pad + else: + # Top or bottom + x1 = cx-pad + x2 = cx+pad + if y < top: + # Due north + y1 = top+1 + y2 = top+1 + elif y > bottom: + # Due south + y1 = bottom + y2 = bottom + points = [wxPoint(x1, y1), wxPoint(x, y), wxPoint(x2, y2)] + dc.SetPen(wxTRANSPARENT_PEN) + dc.DrawPolygon(points) + dc.SetPen(wxBLACK_PEN) + dc.DrawLines(points) + + ty = top + tx = left + w/2 + dc.SetFont(wxSMALL_FONT) + for line in self.lines: + lw, lh = dc.GetTextExtent(line) + dc.DrawText(line, tx-lw/2, ty) + ty = ty + lh + + dc.SetPen(wxNullPen) + + + def add_text(self, text, timeout=10): + """Add text to the balloon, scrolling it if necessary.""" + self.text.append(text) + self.CalcRect() + _scheduler.ScheduleRel(10, self.OnTimer, ()) + + +class ClientCanvas(wxScrollingWindow): + def __init__(self, parent): + wxPanel.__init__(self, parent, -1) + self.parent = parent + self.sprites = [] + self.spriterects = {} + #self.background_dc = wxMemoryDC() + self.dc = wxMemoryDC() + self.SetBackground(wxEmptyBitmap(640, 480)) + EVT_PAINT(self, self.OnPaint) + EVT_LEFT_DOWN(self, self.OnLeftButtonEvent) + + def AddSprite(self, sprite): + self.sprites.append(sprite) + if sprite.rect is None: return + x, y, w, h = sprite.rect + self.DoDrawing(wxClientDC(self), x, y, w, h) + + def RemoveSprite(self, sprite): + self.sprites.remove(sprite) + if sprite.rect is None: return + x, y, w, h = sprite.rect + self.DoDrawing(wxClientDC(self), x, y, w, h) + + def BalloonRects(self, balloon): + w = self.background.GetWidth() + h = self.background.GetHeight() + x = balloon.x + y = balloon.y + rects = map(lambda s: s.rect, + filter(lambda s,n=balloon: s!=n, + self.sprites)) + irects1 = invertrect(0, 0, w, h, rects) + irects1 = filter(lambda r,x=x,y=y: rectdist(r, x, y) <= 200, irects1) + irects1.sort(balloon.nearer) + rects = map(lambda s: s.rect, + filter(lambda s,n=balloon: + s!=n and s.__class__ is Balloon, self.sprites)) + rects.append(balloon.avatar.rect) + irects2 = invertrect(0, 0, w, h, rects) + irects2.sort(balloon.nearer) + + return irects1 + irects2 + + def Balloon(self, nick, text): + try: avatar = self.parent.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: + dc = wxClientDC(self) + w = self.background.GetWidth() + h = self.background.GetHeight() + balloon = avatar.Balloon() + if balloon not in self.sprites: self.sprites.append(balloon) + balloon.add_text(text) + + def SetBackground(self, bitmap): + #self.background_dc.SelectObject(bitmap) + self.background = bitmap + self.width = bitmap.GetWidth() + self.height = bitmap.GetHeight() + self.SetClientSizeWH(self.width, self.height) + self.SetSizeHints(self.width, self.height, self.width, self.height) + self.bitmap = wxEmptyBitmap(self.width, self.height) + self.dc.SelectObject(self.bitmap) + self.SetScrollbars(20, 20, self.width/20, self.height/20) + self.DoDrawing(wxClientDC(self), 0, 0, self.width, self.height) + + def OnLeftButtonEvent(self, event): + x = event.GetX() + y = event.GetY() + self.parent.server.move((x, y)) + self.parent.entry.SetFocus() + + def OnPaint(self, event): + dc = wxPaintDC(self) + #self.PrepareDC(dc) + upd = wxRegionIterator(self.GetUpdateRegion()) + while upd.HaveRects(): + x = upd.GetX() + y = upd.GetY() + w = upd.GetW() + h = upd.GetH() + self.DoDrawing(dc, x, y, w, h) + upd.Next() + + def DoDrawing(self, dc, x, y, w, h): + wxLogVerbose('%d %d %d %d' % (x, y, w, h)) + dc.SetClippingRegion(x, y, w, h) + r = x+w-1 + b = y+h-1 + dc.BeginDrawing() + dc.DrawBitmap(self.background, 0, 0) + #dc.Blit(x, y, w, h, self.background_dc, x, y) + for sprite in self.sprites: + # Draw the sprite if its rect overlaps + if sprite.rect is None: + wxLogVerbose('Null rect for sprite') + continue + sx, sy, sw, sh = sprite.rect + if sx+sw > x and sx <= x+w and sy+sh > y and sy <= y+h: + sprite.draw(dc) + + dc.EndDrawing() + dc.DestroyClippingRegion() + #dc.Blit(x, y, w, h, mdc, x, y) + + +class Client(wxPanel): + """Callbacks for the server connection""" + def __init__(self, frame, parent, id, host, port, nick, avatar): + wxPanel.__init__(self, parent, id) + self.frame = frame + self.parent = parent + self.nick = nick + self.title = '%s:%s' % (host, port) + self.canvas = ClientCanvas(self) + hbox = wxBoxSizer(wxHORIZONTAL) + hbox.Add(self.canvas, 0, 0) + self.listbox = wxListBox(self, -1, + style=wxLB_EXTENDED|wxLB_NEEDED_SB|wxLB_SORT) + hbox.Add(self.listbox, 1, wxEXPAND) + + self.sizer = wxBoxSizer(wxVERTICAL) + self.sizer.Add(hbox, 0, wxEXPAND) + self.textchat = wxTextCtrl(self, -1, style=wxTE_MULTILINE|\ + wxTE_READONLY|wxTE_RICH) + self.sizer.Add(self.textchat, 1, wxEXPAND) + self.entry = wxTextCtrl(self, -1, style=wxTE_PROCESS_ENTER) + self.sizer.Add(self.entry, 0, wxEXPAND) + self.SetAutoLayout(true) + self.SetSizer(self.sizer) + self.sizer.SetSizeHints(self) + self.server = OpenVerse.ServerConnection(host, port, self, nick, + avatar) + self.avatars = {} + self.handler = transutil.InputHandler( + (('nick', self.cmd_nick, '(\S+)', (str,)), + ('quote', self.cmd_quote, '(.*)', (str,)), + ('avatar', self.cmd_avatar, '(\S+)', (str,)), + ('whois', self.cmd_whois, '(\S+)', (str,)))) + EVT_TEXT_ENTER(self, self.entry.GetId(), self.OnSendText) + + # Event handlers + + def OnSendText(self, event): + text = self.entry.GetValue() + self.entry.SetValue('') + self.entry.SetFocus() + if text and text[0] == '/': + text = text[1:] + if text and text[0] != '/': + try: self.handler.handle(text) + except transutil.HandlerError, info: self.debug(info) + return + + self.server.chat(text) + + # Utility functions + + def debug(self, s): + """Handle debug messages""" + wxLogVerbose(s) + + # Command handlers + + def cmd_nick(self, nick): + self.nick = nick + self.frame.rename(self, '%s - %s' % (self.title, nick)) + self.server.set_nick(nick) + + def cmd_quote(self, text): + self.server.quote(text) + + def cmd_avatar(self, avatar): + self.server.set_avatar(avatar) + + def cmd_msg(self, nicks, text): + nicks = string.split(nicks, ',') + self.server.privmsg(nicks, text) + + def cmd_whois(self, nick): + self.server.whois(nick) + + # Transport-called functions + + def close(self): + self.frame.close(self) + + def background_image(self, image): + """Change the background""" + self.canvas.SetBackground(image) + + def background_progress(self, length, filename, size): pass + #self.background_image(progress(float(length)/float(size))) + + def set_title(self, title): + """Change the room's name""" + self.title = title + self.frame.rename(self, '%s - %s' % (title, self.nick)) + + def raise_object(self, name): + """Raise the named object to the top of the stacking order.""" + try: avatar = self.avatars[name] + except: self.debug('No avatar called %s' % name) + else: + self.canvas.sprites.above(avatar) + + def mouseover(self, name, pos, image1, image2): + """Create a mouseover object""" + x, y = pos + mo = Mouseover(self.canvas, x, y, image1, image2) + self.avatars[name] = mo + + def newimage(self, filename=None): + """Load an image from a file object""" + self.debug('newimage %s' % repr(filename)) + if filename is None: return wxNullBitmap + return wxImage(filename).ConvertToBitmap() + + def new_avatar(self, nick, pos, image, noffset, boffset): + x, y = pos + avatar = Avatar(self.canvas, x, y, image, nick, noffset, boffset) + self.avatars[nick] = avatar + self.listbox.Append(nick) + self.canvas.Balloon(nick, '*%s* has entered the room.' % nick) + + def del_avatar(self, nick): + self.textchat.AppendText('*%s* left the room.\n' % nick) + self.canvas.Balloon(nick, '*%s* has left the room.' % nick) + self.listbox.Delete(self.listbox.FindString(nick)) + try: avatar = self.avatars[nick] + except KeyError: pass + else: + self.canvas.RemoveSprite(avatar) + del self.avatars[nick] + + def avatar(self, nick, image, noffset, boffset): + """Change the avatar for a nick""" + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: + avatar.SetImage(image) + avatar.set(noffset, boffset) + + def mouseover_image1(self, name, image): + """Set the unactivated image for a mouseover""" + mo = self.avatars[name] + mo.set_image1(image) + #self.canvas.DoDrawing(mo.rect.x, mo.rect.y, mo.rect.width, + # mo.rect.height) + + def mouseover_image2(self, name, image): + """Set the activated image for a mouseover""" + mo = self.avatars[name] + mo.set_image2(image) + #self.canvas.DoDrawing(mo.rect.x, mo.rect.y, mo.rect.width, + # mo.rect.height) + + def avatar_image(self, nick, image): + """Set the image for an avatar""" + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: + avatar.SetImage(image) + + def avatar_progress(self, nick, length, filename, size): + """Set an avatar's image to a progress bar""" + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: avatar.SetImage(progress(float(length)/float(size))) + + def move_avatar(self, nick, x, y, speed): + wxLogVerbose('move_avatar') + try: avatar = self.avatars[nick] + except: self.debug('No avatar called %s' % nick) + else: avatar.AnimateMove((x, y), speed) + + def privmsg(self, nick, s): + self.textchat.AppendText('[%s] %s\n' % (nick, s)) + try: avatar = self.avatars[nick] + except KeyError: self.debug('No avatar called %s' % nick) + else: self.canvas.Balloon(nick, s) + + def chat(self, nick, s): + self.textchat.AppendText('<%s> %s\n' % (nick, s)) + self.canvas.Balloon(nick, s) + + +class Frame(wxFrame): + """The main frame of the application""" + def __init__(self, parent, ID, title): + wxFrame.__init__(self, parent, ID, title, + wxDefaultPosition) + self.CreateStatusBar() + self.SetStatusText("This is the statusbar") + + ID_ABOUT = wxNewId() + ID_EXIT = wxNewId() + ID_CONNECT = wxNewId() + + menu = wxMenu() + menu.Append(ID_CONNECT, "&Connect", "Connect to a new room") + menu.AppendSeparator() + menu.Append(ID_EXIT, "E&xit", "Terminate the program") + + menuBar = wxMenuBar() + menuBar.Append(menu, "&File") + + menu = wxMenu() + menu.Append(ID_ABOUT, "&About", + "More information about this program") + menuBar.Append(menu, "&Help") + + self.SetMenuBar(menuBar) + EVT_MENU(self, ID_ABOUT, self.OnAbout) + EVT_MENU(self, ID_EXIT, self.TimeToQuit) + EVT_MENU(self, ID_CONNECT, self.OnConnection) + + self.client_nb = wxNotebook(self, -1) + self.client_nb.AddPage(LogWindow(self.client_nb), "Log") + self.sizer = wxNotebookSizer(self.client_nb) + self.SetAutoLayout(true) + self.SetSizer(self.sizer) + + def GetClientPage(self, client): + """Return the page number for a given client""" + + ID = client.GetId() + for i in range(self.client_nb.GetPageCount()): + page = self.client_nb.GetPage(i) + if page.GetId() == ID: + return i + else: raise ValueError, 'Nonexistent page' + + def close(self, client): + self.client_nb.DeletePage(self.GetClientPage(client)) + + def rename(self, client, name): + self.client_nb.SetPageText(self.GetClientPage(client), name) + + def resize(self): + self.Fit() + self.sizer.SetSizeHints(self) + + def OnAbout(self, event): + dlg = wxMessageDialog(self, "PythonVerse\n" + "Copyright 2001 Christine McIntyre\n" + "All rights reserved.\n", + "About PythonVerse", wxOK | wxICON_INFORMATION) + dlg.ShowModal() + dlg.Destroy() + + def OnConnection(self, event): + window = ConnectionDialog(self, -1) + window.Show(true) + + def TimeToQuit(self, event): + self.Close(true) + + def connect(self, server, port, nick, avatar): + self.client_nb.AddPage(Client(self, self.client_nb, -1, server, port, + nick, avatar), + "%s:%d" % (server, port)) + + +class LogWindow(wxPanel): + def __init__(self, parent, ID=-1): + wxPanel.__init__(self, parent, ID) + self.tc = wxTextCtrl(self, ID, + style=wxTE_MULTILINE|wxTE_READONLY|wxTE_RICH) + self.logger = wxLogTextCtrl(self.tc) + self.logger.SetVerbose(true) + wxLog_SetActiveTarget(self.logger) + self.sizer = wxBoxSizer(wxVERTICAL) + self.sizer.Add(self.tc, 1, wxEXPAND) + self.SetSizer(self.sizer) + self.SetAutoLayout(true) + + +class ConnectionDialog(wxDialog): + """Dialog for connecting to a server""" + def __init__(self, parent, ID): + wxDialog.__init__(self, parent, ID, "Connect to server") + self.parent = parent + gs = wxFlexGridSizer(4,2,5,5) + gs.AddGrowableCol(1) + gs.Add(wxStaticText(self, -1, "Server"), 0, wxEXPAND) + self.editserver = wxTextCtrl(self, 255, "openverse.com") + gs.Add(self.editserver, 1, wxEXPAND) + #EVT_TEXT(self, 20, self.EvtText) + #EVT_CHAR(self.editname, self.EvtChar) + gs.Add(wxStaticText(self, -1, "Port"), 0, wxEXPAND) + self.editport = wxTextCtrl(self, 5, "6900") + gs.Add(self.editport, 1, wxEXPAND) + gs.Add(wxStaticText(self, -1, "Nick"), 0, wxEXPAND) + self.editnick = wxTextCtrl(self, 9, "Ryoko") + gs.Add(self.editnick, 1, wxEXPAND) + gs.Add(wxStaticText(self, -1, "Avatar"), 0, wxEXPAND) + self.editavatar = wxTextCtrl(self, 9, "ryoko") + gs.Add(self.editavatar, 1, wxEXPAND) + vbox = wxBoxSizer(wxVERTICAL) + vbox.Add(gs) + hbox = wxBoxSizer(wxHORIZONTAL) + button = wxButton(self, wxID_OK, "Connect") + EVT_BUTTON(self, wxID_OK, self.OnOK) + hbox.Add(button) + button = wxButton(self, wxID_CANCEL, "Cancel") + hbox.Add(button) + vbox.Add(hbox) + self.sizer = vbox + self.SetAutoLayout(true) + self.SetSizer(self.sizer) + self.sizer.Fit(self) + + def OnOK(self, event): + server = self.editserver.GetValue() + port = self.editport.GetValue() + nick = self.editnick.GetValue() + avatar = self.editavatar.GetValue() + self.parent.connect(server, int(port), nick, avatar) + #wxDialog.OnOK(self, event) + return event.Skip() + + +class Application(wxApp): + """The main application object""" + def OnInit(self): + """Initialization function called by wxPython""" + # Create the main frame + frame = Frame(NULL, -1, 'PythonVerse') + frame.Show(true) + self.SetTopWindow(frame) + return true + + +class Scheduler(wxTimer): + def __init__(self): + wxTimer.__init__(self) + self.events = [] + + def Schedule(self, t, func, args): + event = t, func, args + bisect.insort(self.events, event) + # Restart the timer + self.Start(min(0, t-time.time())*1000, true) + return event + + def ScheduleRel(self, delay, func, args): + now = time.time() + return self.Schedule(now+delay, func, args) + + def Cancel(self, event): + self.events.remove(event) + + def Notify(self): + now = time.time() + while self.events: + t, func, args = self.events[0] + if t > now: break + apply(func, args) + del self.events[0] + + # If there are still events left over, then the last + # setting of t will be for the first event that's not yet + # scheduled. Make sure it's a single shot :) + if self.events: self.Start((t-now)*1000, true) + +_scheduler = Scheduler() + + +class IOTimer(wxTimer): + """Poll asyncore, would be better to use threads""" + def Notify(self): + asyncore.poll(0.0) + + +def main(argv): + app = Application(0) + wxInitAllImageHandlers() + t = IOTimer() + t.Start(500) + app.MainLoop() + + +if __name__ == '__main__': main(sys.argv) + diff --git a/transutil.py b/transutil.py @@ -0,0 +1,97 @@ +# Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org> +# +# This file is part of PythonVerse. +# +# PythonVerse is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# PythonVerse is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PythonVerse; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +import re, string, asyncore, asynchat, socket + + +class HandlerError(Exception): pass + + +class InputHandler: + def __init__(self, callbacks): + """Callbacks is a sequence of (command, callback, regular expression, + sequence of thunks)""" + + self.dict = {} + + for command, callback, r, thunks in callbacks: + self.dict[command] = (callback, re.compile(r), thunks) + + def handle(self, s): + try: + # Extract the command + cmd, args = string.split(s, ' ', 1) + except ValueError: + # Assume a command with no arguments + cmd = s + args = '' + + try: + func, r, conv = self.dict[cmd] + except KeyError: + raise HandlerError, 'Unknown: %s' % cmd + else: + # Parse the arguments + match = r.match(args) + if match is None: raise HandlerError, 'Failed parse: %s' % args + elif match.end() != len(args): + raise HandlerError, 'Not all arguments parsed: %s' % \ + (len(args), match.end(), args) + else: + # Convert the arguments to the correct types + apply(func, map(lambda x: x[0](x[1]), + zip(conv, match.groups()))) + + +class Connection(asynchat.async_chat): + """Connection object that can handle a client or server""" + def __init__(self, handler, sock=None): + asynchat.async_chat.__init__(self, sock) + self.buffer = '' + self.outbuf = '' + self.handler = handler + self.set_terminator('\r\n') + + # asynchat/asyncore handlers + + def debug(self): pass + + def handle_connect(self): + # Should override this + pass + + def collect_incoming_data(self, data): + self.buffer = self.buffer + data + + def found_terminator(self): + self.debug('-> %s' % string.strip(self.buffer)) + try: self.handler.handle(self.buffer) + except HandlerError, info: self.debug(str(info)) + self.buffer = '' + + def writable(self): + return len(self.outbuf) > 0 + + def handle_write(self): + bytes = self.send(self.outbuf) + self.outbuf = self.outbuf[bytes:] + + def write(self, data): + self.debug("<- %s" % string.strip(data)) + self.outbuf = self.outbuf + data +