pythonverse

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

pvui_wx.py (33321B)


      1 # -*-Python-*-
      2 # Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org>
      3 #
      4 # This file is part of PythonVerse.
      5 #
      6 # PythonVerse is free software; you can redistribute it and/or modify
      7 # it under the terms of the GNU General Public License as published by
      8 # the Free Software Foundation; either version 2 of the License, or
      9 # (at your option) any later version.
     10 # 
     11 # PythonVerse is distributed in the hope that it will be useful,
     12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
     13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     14 # GNU General Public License for more details.
     15 # 
     16 # You should have received a copy of the GNU General Public License
     17 # along with PythonVerse; if not, write to the Free Software
     18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
     19 #
     20 # vim:syntax=python
     21 
     22 import sys, bisect, string, thread, asyncore, time
     23 import OpenVerse, transutil
     24 from wxPython.wx import *
     25 from math import *
     26 from types import *
     27 
     28 def wrap_lines(dc, lines, width):
     29     r = []
     30     for text in lines:
     31         r.extend(wrap(dc, text, width))
     32 
     33     return r
     34 
     35 def wrap(dc, text, width):
     36     """Wrap a line of text, returning a list of lines."""
     37 
     38     lines = []
     39     while text:
     40         if dc.GetTextExtent(text)[0] <= width: return lines + [text]
     41         try:
     42             i = string.rindex(text, ' ')
     43             while dc.GetTextExtent(text[:i])[0] > width:
     44                 i = string.rindex(text, ' ', 0, i)
     45         except ValueError:
     46             i = len(text)-1
     47             while dc.GetTextExtent(text[:i])[0] > width and i: i = i - 1
     48             if not i: raise ValueError, 'width %d too narrow' % width
     49 
     50         lines.append(text[:i])
     51         text = string.lstrip(text[i:])
     52 
     53     return lines
     54 
     55 
     56 def progress(fraction, size=(20, 50)):
     57     w, h = size
     58     bitmap = wxEmptyBitmap(w, h)
     59     y = h * fraction
     60     dc = wxMemoryDC()
     61     dc.SelectObject(bitmap)
     62     dc.SetPen(wxTRANSPARENT_PEN)
     63     dc.SetBrush(wxBLACK_BRUSH)
     64     dc.DrawRectangle(0, 0, w, h-y)
     65     dc.SetBrush(wxGREEN_BRUSH)
     66     dc.DrawRectangle(0, h-y+1, w, y)
     67     return bitmap
     68 
     69 
     70 def invertrect(left, top, width, height, rects, min_width=50, min_height=10):
     71     irects = []
     72     rects = rects + [(left-1, top, 0, height),
     73                      (left, top-1, width, 0),
     74                      (left+width, top, 0, height),
     75                      (left, top+height, width, 0)]
     76 
     77     for ax, ay, aw, ah in rects:
     78         # Pick a left side
     79         l = ax+aw
     80         if l < left: continue
     81         # Pick a top
     82         for bx, by, bw, bh in rects:
     83             # Make sure the rect can actually border ours
     84             if bx+bw <= l or by+bh > ay+ah: continue
     85             # Pick a top
     86             t = by+bh
     87             # Pick a right side
     88             for cx, cy, cw, ch in rects:
     89                 # Make sure this rect can border ours
     90                 if cx-1 <= l or cy+ch <= t or \
     91                    cy >= top+height or \
     92                    cx-1 <= bx: continue
     93                 r = cx-1
     94                 #w = r - l
     95                 #h = bottom - t
     96                 if r-l < min_width or top+height-t < min_height: continue
     97                 bot = top+height-1
     98                 # There can be only one rect with these three sides
     99                 #rect = wxRect(l, t, w, h)
    100                 # Now find the bottom
    101                 for dx, dy, dw, dh in rects:
    102                     # Check if this rect overlaps ours
    103                     if r >= dx and l < dx+dw and \
    104                        bot >= dy and t < dy+dh:
    105                         bot = dy-1
    106                         # Make sure the rect is still sane and still borders
    107                         # on the original border rects
    108                         if bot-t < min_height or \
    109                            bot < ay or bot < cy: break
    110                 else: irects.append((l, t, r-l+1, bot-t+1))
    111                 
    112     return irects
    113 
    114 
    115 class Sprite(wxEvtHandler):
    116     mouseme = 0
    117     pollme = 0
    118     def __init__(self, parent, x, y):
    119         wxEvtHandler.__init__(self)
    120         self.dead = 0
    121         self.parent = parent
    122         self.x = x
    123         self.y = y
    124         self.rect = None
    125         self.parent.AddSprite(self)
    126 
    127     def __repr__(self):
    128         return '<%s(%s, %s)>' % (self.__class__, self.image, self.rect)
    129 
    130     def move(self, x, y):
    131         self.x = x
    132         self.y = y
    133         self.CalcRect()
    134 
    135     def SetRect(self, x, y, w, h):
    136         dc = wxClientDC(self.parent)
    137         if self.rect is not None:
    138             ox, oy, ow, oh = self.rect
    139             self.rect = x, y, w, h
    140             if ox+oh > x and ox <= x+w and oy+oh > y and oy <= y+h:
    141                 # The rectangles overlap
    142                 nx = min(x, ox)
    143                 ny = min(y, oy)
    144                 w = max(x+w, ox+ow) - nx
    145                 h = max(y+h, oy+oh) - ny
    146                 x = nx
    147                 y = ny
    148             else: self.parent.DoDrawing(dc, ox, oy, ow, oh)
    149         else: self.rect = x, y, w, h
    150         self.parent.DoDrawing(dc, x, y, w, h)
    151         
    152 
    153 class Mouseover(Sprite):
    154     def __init__(self, parent, x, y, image1, image2):
    155         self.image1 = image1
    156         self.image2 = image2
    157         self.on = 0
    158         Sprite.__init__(self, parent, x, y)
    159         self.CalcRect()
    160 
    161     def mouseon(self):
    162         self.on = 1
    163         self.CalcRect()
    164         
    165     def mouseoff(self):
    166         self.on = 0
    167         self.CalcRect()
    168     
    169     def set_image1(self, image):
    170         self.image1 = image
    171         self.CalcRect()
    172 
    173     def set_image2(self, image):
    174         self.image2 = image
    175         self.CalcRect()
    176 
    177     def CalcRect(self):
    178         if self.on: image = self.image2
    179         else: image = self.image1
    180         w = image.GetWidth()
    181         h = image.GetHeight()
    182         x = self.x-w/2
    183         y = self.y-h/2
    184         self.SetRect(x, y, w, h)
    185 
    186     def draw(self, dc):
    187         wxLogVerbose('draw')
    188         if self.on: image = self.image2
    189         else: image = self.image1
    190         w = self.image2.GetWidth()
    191         h = self.image2.GetHeight()
    192         dc.DrawBitmap(image, self.x-w/2, self.y-h/2, 1)
    193 
    194 
    195 class MoveTimer(wxTimer):
    196     """Call the update function for avatars"""
    197     def __init__(self):
    198         wxTimer.__init__(self)
    199         self.sprites = []
    200         
    201     def Notify(self):
    202         t = time.time()
    203         sprites = self.sprites
    204         for i in range(len(sprites)-1, -1, -1):
    205             sprite = sprites[i]
    206             r = sprite.update(t)
    207             if r: del sprites[i]
    208 
    209         if not sprites: self.Stop()
    210 
    211     def add(self, sprite):
    212         if sprite in self.sprites: return
    213         self.sprites.append(sprite)
    214         if len(self.sprites) == 1:
    215             wxLogVerbose('starting timer')
    216             self.Start(40)
    217 
    218 _timer = MoveTimer()
    219 
    220         
    221 class Avatar(Sprite):
    222     mouseme = 1
    223     def __init__(self, parent, x, y, bitmap, nick, noffset, boffset):
    224         Sprite.__init__(self, parent, x, y)
    225         self.bitmap = bitmap
    226         self.nick = nick
    227         self.destpos = None
    228         self.speed = None
    229         self.balloon = None
    230         self.label_on = 1
    231         self.set(noffset, boffset)
    232 
    233     def SaneNametagOffset(self):
    234         w = self.bitmap.GetWidth()
    235         h = self.bitmap.GetHeight()
    236         nx = max(-w/2-10, min(self.nx, w/2+10))
    237         ny = max(-h/2-10, min(self.ny, h/2+10))
    238         return nx, ny
    239 
    240     def SaneBalloonOffset(self):
    241         w = self.bitmap.GetWidth()
    242         h = self.bitmap.GetHeight()
    243         bx = max(-w/2-10, min(self.bx, w/2+10))
    244         by = max(-h/2-10, min(self.by, h/2+10))
    245         return bx, by
    246 
    247     def Balloon(self):
    248         w = self.bitmap.GetWidth()
    249         h = self.bitmap.GetHeight()
    250         bx, by = self.SaneBalloonOffset()
    251         if self.balloon is None or self.balloon.dead:
    252             self.balloon = Balloon(self.parent, self, self.x+bx, self.y+by)
    253             
    254         return self.balloon
    255 
    256     def SetImage(self, bitmap):
    257         self.bitmap = bitmap
    258         self.CalcRect()
    259 
    260     def draw(self, dc):
    261         nx, ny = self.SaneNametagOffset()
    262         dc.DrawBitmap(self.bitmap, self.x-self.bitmap.GetWidth()/2,
    263                       self.y-self.bitmap.GetHeight()/2, 1)
    264         if self.label_on:
    265             dc.SetFont(wxSMALL_FONT)
    266             w, h = dc.GetTextExtent(self.nick)
    267             dc.SetTextForeground(wxCYAN)
    268             dc.SetLogicalFunction(wxXOR)
    269             dc.DrawText(self.nick, self.x+nx-w/2, self.y+ny-h/2)
    270             dc.SetTextForeground(wxBLACK)
    271             dc.SetLogicalFunction(wxCOPY)
    272 
    273     def CalcRect(self):
    274         w = self.bitmap.GetWidth()
    275         h = self.bitmap.GetHeight()
    276         x = self.x-w/2
    277         y = self.y-h/2
    278         if self.label_on:
    279             dc = wxClientDC(self.parent)
    280             dc.SetFont(wxSMALL_FONT)
    281             tw, th = dc.GetTextExtent(self.nick)
    282             nx, ny = self.SaneNametagOffset()
    283             tx, ty = self.x+nx-tw/2, self.y+ny-th/2
    284             x, y, w, h = RectUnion((x, y, w, h),
    285                                    (tx, ty, tw, th))
    286         self.SetRect(x, y, w, h)
    287 
    288     def set(self, noffset, boffset):
    289         self.bx, self.by = boffset
    290         self.nx, self.ny = noffset
    291         self.CalcRect()
    292         
    293     def AnimateMove(self, position, speed):
    294         """Changes the location of the avatar's center to the new position"""
    295         wxLogVerbose('AnimateMove')
    296         if position[0] >= 640 or position[1] > 480:
    297             wxLogWarning('Attempt to move outside the screen')
    298             return
    299         self.speed = speed
    300         pos = self.x, self.y
    301         self.startpos = pos
    302         self.starttime = time.time()
    303         distance = dist(pos, position)
    304         if distance == 0.0: return
    305         self.dx = 300.0 * (position[0]-pos[0]) * self.speed / distance 
    306         self.dy = 300.0 * (position[1]-pos[1]) * self.speed / distance 
    307         self.stoptime = self.starttime + distance / 300.0 / speed
    308         self.destpos = position
    309         _timer.add(self)
    310 
    311     def update(self, t):
    312         """Move the avatar if necessary"""
    313 
    314         if t >= self.stoptime:
    315             self.move(self.destpos[0], self.destpos[1])
    316             if self.balloon is not None and not self.balloon.dead:
    317                 bx, by = self.SaneBalloonOffset()
    318                 x, y = self.destpos
    319                 self.balloon.move(x+bx, y+by)
    320                 
    321             return 1
    322         else:
    323             delta_t = t - self.starttime
    324             x, y = self.startpos
    325             x = x+int(round(self.dx*delta_t))
    326             y = y+int(round(self.dy*delta_t))
    327             self.move(x, y)
    328       
    329 
    330 def arc(radius, center, start_angle, stop_angle, n):
    331     x, y = center
    332     step = (stop_angle - start_angle) / n
    333     points = [0] * (n+1)
    334     for i in range(n+1):
    335         angle = start_angle + i*step
    336         points[i] = (x + int(round(radius*sin(angle))),
    337                      y - int(round(radius*cos(angle))))
    338 
    339     return tuple(points)
    340 
    341 
    342 def closest(rect, x, y):
    343     """Find the closest point on a rect to a given point"""
    344     rx, ry, rw, rh = rect
    345     return min(rx+rw-1, max(x, rx)), min(ry+rh-1, max(y, ry))
    346 
    347 
    348 def dist(point1, point2):
    349     x1, y1 = point1
    350     x2, y2 = point2
    351     return sqrt((x1-x2)**2 + (y1-y2)**2)
    352 
    353 
    354 def rectdist(rect, x, y):
    355     return dist(closest(rect, x, y), (x, y))
    356 
    357 
    358 def ClampRect(r1, r2):
    359     x1, y1, w1, h1 = r1
    360     x2, y2, w2, h2 = r2
    361     x = max(x1, x2)
    362     y = max(y1, y2)
    363     if x+w1 > x2+w2: x = x2+w2-w1
    364     if y+h1 > y2+h2: y = y2+h2-h1
    365     return x, y, w1, h1
    366 
    367 
    368 def RectUnion(r1, r2):
    369     x1, y1, w1, h1 = r1
    370     x2, y2, w2, h2 = r2
    371     x = min(x1, x2)
    372     y = min(y1, y2)
    373     w = max(x1+w1, x2+w2)-x
    374     h = max(y1+h1, y2+h2)-y
    375     return x, y, w, h
    376 
    377 
    378 class Balloon(Sprite):
    379     """A speech balloon"""
    380     # Padding for balloon rectangles, also radius of corner arcs
    381     pad = 3
    382     
    383     def __init__(self, parent, avatar, x, y):
    384         Sprite.__init__(self, parent, x, y)
    385         self.avatar = avatar
    386         self.text = []
    387         #EVT_TIMER(self, -1, self.OnTimer)
    388 
    389     def OnTimer(self):
    390         if self.dead: return
    391         del self.text[0]
    392         if self.text: self.CalcRect()
    393         else:
    394             self.dead = 1
    395             self.parent.RemoveSprite(self)
    396         
    397     def move(self, x, y):
    398         self.x = x
    399         self.y = y
    400         self.CalcRect()
    401 
    402     def nearer(self, rect1, rect2):
    403         """Compare two rects based on their distance from our position"""
    404         dist1 = rectdist(rect1, self.x, self.y)
    405         dist2 = rectdist(rect2, self.x, self.y)
    406         if dist1 == dist2: return cmp(rect2[2], rect1[2])
    407         return cmp(dist1, dist2)
    408 
    409     def CalcRect(self):
    410         rects = self.parent.BalloonRects(self)
    411         dc = wxClientDC(self.parent)
    412         dc.SetFont(wxSMALL_FONT)
    413         notdone = 1
    414         while notdone:
    415             # Loop until we can fit the balloon into *some* rect
    416             for r in rects:
    417                 try: lines = wrap_lines(dc, self.text, r[2]-self.pad*2)
    418                 except ValueError: continue
    419                 height = 0
    420                 for l in lines: height = height + dc.GetTextExtent(l)[1]
    421                 if height < r[3]:
    422                     notdone = 0
    423                     break
    424             else:
    425                 # Couldn't render the balloon, delete some lines
    426                 del self.text[0]
    427 
    428         # Count the number of lines in each sequence of text
    429         width = max(map(lambda l, dc=dc: dc.GetTextExtent(l)[0], lines)) +\
    430                 (self.pad * 2)
    431 
    432         self.lines = lines
    433         self.textrect = ClampRect((self.x-width/2, self.y-height/2, width,
    434                                    height), r)
    435         bigrect = RectUnion(self.textrect,
    436                             (self.x, self.y, 1, 1))
    437         self.SetRect(bigrect[0], bigrect[1], bigrect[2], bigrect[3])
    438 
    439     def draw(self, dc):
    440         # The rectangles had better already be calculated.
    441         pad = self.pad
    442         left, top, w, h = self.textrect
    443         right = left+w-1
    444         bottom = top+h-1
    445         dc.SetPen(wxBLACK_PEN)
    446         dc.DrawRoundedRectangle(left, top, w, h, pad)
    447         x = self.x
    448         y = self.y
    449         if x < left or x > right or y < top or y > bottom:
    450             cx, cy = closest(self.textrect, x, y)
    451             if x > right-pad*2:
    452                 # Drawing to the east
    453                 if y > bottom-pad*2:
    454                     # Draw the arrow to the southeast
    455                     x1 = right
    456                     y1 = bottom-pad
    457                     x2 = right-pad
    458                     y2 = bottom
    459                 elif y < top+pad*2:
    460                     # Draw the arrow to the northeast
    461                     x1 = right
    462                     x2 = right-pad
    463                     y2 = top+1
    464                     y1 = top+pad
    465                 elif x > right:
    466                     # Due east
    467                     x1 = right
    468                     y1 = cy-pad
    469                     x2 = right
    470                     y2 = cy+pad
    471             elif x < left+pad*2:
    472                 # Drawing to the west
    473                 if y > bottom-pad*2:
    474                     # Southwest
    475                     x1 = left
    476                     y1 = bottom-pad
    477                     x2 = left+pad
    478                     y2 = bottom
    479                 elif y < top+pad*2:
    480                     # Northwest
    481                     x1 = left
    482                     y1 = top+pad
    483                     x2 = left+pad
    484                     y2 = top+1
    485                 elif x < left:
    486                     # Due west
    487                     x1 = left+1
    488                     y1 = cy+pad
    489                     x2 = left+1
    490                     y2 = cy-pad
    491             else:
    492                 # Top or bottom
    493                 x1 = cx-pad
    494                 x2 = cx+pad
    495                 if y < top:
    496                     # Due north
    497                     y1 = top+1
    498                     y2 = top+1
    499                 elif y > bottom:
    500                     # Due south
    501                     y1 = bottom
    502                     y2 = bottom
    503             points = [wxPoint(x1, y1), wxPoint(x, y), wxPoint(x2, y2)]
    504             dc.SetPen(wxTRANSPARENT_PEN)
    505             dc.DrawPolygon(points)
    506             dc.SetPen(wxBLACK_PEN)
    507             dc.DrawLines(points)
    508         
    509         ty = top
    510         tx = left + w/2
    511         dc.SetFont(wxSMALL_FONT)
    512         for line in self.lines:
    513             lw, lh = dc.GetTextExtent(line)
    514             dc.DrawText(line, tx-lw/2, ty)
    515             ty = ty + lh
    516 
    517         dc.SetPen(wxNullPen)
    518         
    519 
    520     def add_text(self, text, timeout=10):
    521         """Add text to the balloon, scrolling it if necessary."""
    522         self.text.append(text)
    523         self.CalcRect()
    524         _scheduler.ScheduleRel(10, self.OnTimer, ())
    525     
    526 
    527 class ClientCanvas(wxPanel):
    528     def __init__(self, parent):
    529         wxPanel.__init__(self, parent, -1)
    530         self.parent = parent
    531         self.sprites = []
    532         self.spriterects = {}
    533         #self.background_dc = wxMemoryDC()
    534         self.dc = wxMemoryDC()
    535         self.SetBackground(wxEmptyBitmap(640, 480))
    536         EVT_PAINT(self, self.OnPaint)
    537         EVT_LEFT_DOWN(self, self.OnLeftButtonEvent)
    538 
    539     def AddSprite(self, sprite):
    540         self.sprites.append(sprite)
    541         if sprite.rect is None: return
    542         x, y, w, h = sprite.rect
    543         self.DoDrawing(wxClientDC(self), x, y, w, h)
    544 
    545     def RemoveSprite(self, sprite):
    546         self.sprites.remove(sprite)
    547         if sprite.rect is None: return
    548         x, y, w, h = sprite.rect
    549         self.DoDrawing(wxClientDC(self), x, y, w, h)
    550 
    551     def BalloonRects(self, balloon):
    552         w = self.background.GetWidth()
    553         h = self.background.GetHeight()
    554         x = balloon.x
    555         y = balloon.y
    556         rects = map(lambda s: s.rect,
    557                     filter(lambda s,n=balloon: s!=n,
    558                            self.sprites))
    559         irects1 = invertrect(0, 0, w, h, rects)
    560         irects1 = filter(lambda r,x=x,y=y: rectdist(r, x, y) <= 200, irects1)
    561         irects1.sort(balloon.nearer)
    562         rects = map(lambda s: s.rect,
    563                     filter(lambda s,n=balloon:
    564                            s!=n and s.__class__ is Balloon, self.sprites))
    565         rects.append(balloon.avatar.rect)
    566         irects2 = invertrect(0, 0, w, h, rects)
    567         irects2.sort(balloon.nearer)
    568 
    569         return irects1 + irects2
    570 
    571     def Balloon(self, nick, text):
    572         try: avatar = self.parent.avatars[nick]
    573         except KeyError: self.debug('No avatar called %s' % nick)
    574         else:
    575             dc = wxClientDC(self)
    576             w = self.background.GetWidth()
    577             h = self.background.GetHeight()
    578             balloon = avatar.Balloon()
    579             if balloon not in self.sprites: self.sprites.append(balloon)
    580             balloon.add_text(text)
    581 
    582     def SetBackground(self, bitmap):
    583         #self.background_dc.SelectObject(bitmap)
    584         self.background = bitmap
    585         self.width = bitmap.GetWidth()
    586         self.height = bitmap.GetHeight()
    587         self.SetClientSizeWH(self.width, self.height)
    588         self.SetSizeHints(self.width, self.height, self.width, self.height)
    589         self.bitmap = wxEmptyBitmap(self.width, self.height)
    590         self.dc.SelectObject(self.bitmap)
    591         #self.SetScrollbars(20, 20, self.width/20, self.height/20)
    592         self.DoDrawing(wxClientDC(self), 0, 0, self.width, self.height)
    593 
    594     def OnLeftButtonEvent(self, event):
    595         x = event.GetX()
    596         y = event.GetY()
    597         self.parent.server.move((x, y))
    598         self.parent.entry.SetFocus()
    599 
    600     def OnPaint(self, event):
    601         dc = wxPaintDC(self)
    602         #self.PrepareDC(dc)
    603         upd = wxRegionIterator(self.GetUpdateRegion())
    604         while upd.HaveRects():
    605             x = upd.GetX()
    606             y = upd.GetY()
    607             w = upd.GetW()
    608             h = upd.GetH()
    609             self.DoDrawing(dc, x, y, w, h)
    610             upd.Next()
    611 
    612     def DoDrawing(self, dc, x, y, w, h):
    613         wxLogVerbose('%d %d %d %d' % (x, y, w, h)) 
    614         dc.SetClippingRegion(x, y, w, h)
    615         r = x+w-1
    616         b = y+h-1
    617         dc.BeginDrawing()
    618         dc.DrawBitmap(self.background, 0, 0)
    619         #dc.Blit(x, y, w, h, self.background_dc, x, y)
    620         for sprite in self.sprites:
    621             # Draw the sprite if its rect overlaps
    622             if sprite.rect is None:
    623                 wxLogVerbose('Null rect for sprite')
    624                 continue
    625             sx, sy, sw, sh = sprite.rect
    626             if sx+sw > x and sx <= x+w and sy+sh > y and sy <= y+h:
    627                 sprite.draw(dc)
    628 
    629         dc.EndDrawing()
    630         dc.DestroyClippingRegion()
    631         #dc.Blit(x, y, w, h, mdc, x, y)
    632 
    633 
    634 class Client(wxPanel):
    635     """Callbacks for the server connection"""
    636     def __init__(self, frame, parent, id, host, port, nick, avatar):
    637         wxPanel.__init__(self, parent, id)
    638         self.frame = frame
    639         self.parent = parent
    640         self.nick = nick
    641         self.title = '%s:%s' % (host, port)
    642         self.canvas = ClientCanvas(self)
    643         hbox = wxBoxSizer(wxHORIZONTAL)
    644         hbox.Add(self.canvas, 0, 0)
    645         self.listbox = wxListBox(self, -1,
    646                                  style=wxLB_EXTENDED|wxLB_NEEDED_SB|wxLB_SORT) 
    647         hbox.Add(self.listbox, 1, wxEXPAND)
    648         
    649         self.sizer = wxBoxSizer(wxVERTICAL)
    650         self.sizer.Add(hbox, 0, wxEXPAND)
    651         self.textchat = wxTextCtrl(self, -1, style=wxTE_MULTILINE|\
    652                                    wxTE_READONLY|wxTE_RICH)
    653         self.sizer.Add(self.textchat, 1, wxEXPAND)
    654         self.entry = wxTextCtrl(self, -1, style=wxTE_PROCESS_ENTER)
    655         self.sizer.Add(self.entry, 0, wxEXPAND)
    656         self.SetAutoLayout(true)
    657         self.SetSizer(self.sizer)
    658         self.sizer.SetSizeHints(self)
    659         self.server = OpenVerse.ServerConnection(host, port, self, nick,
    660                                                  avatar)
    661         self.avatars = {}
    662         self.handler = transutil.InputHandler(
    663             (('nick', self.cmd_nick, '(\S+)', (str,)),
    664              ('quote', self.cmd_quote, '(.*)', (str,)),
    665              ('avatar', self.cmd_avatar, '(\S+)', (str,)),
    666              ('whois', self.cmd_whois, '(\S+)', (str,))))
    667         EVT_TEXT_ENTER(self, self.entry.GetId(), self.OnSendText)
    668 
    669     # Event handlers
    670 
    671     def OnSendText(self, event):
    672         text = self.entry.GetValue()
    673         self.entry.SetValue('')
    674         self.entry.SetFocus()
    675         if text and text[0] == '/':
    676             text = text[1:]
    677             if text and text[0] != '/':
    678                 try: self.handler.handle(text)
    679                 except transutil.HandlerError, info: self.debug(info)
    680                 return
    681 
    682         self.server.chat(text)
    683         
    684     # Utility functions
    685 
    686     def debug(self, s):
    687         """Handle debug messages"""
    688         wxLogVerbose(s)
    689 
    690     # Command handlers
    691     
    692     def cmd_nick(self, nick):
    693         self.nick = nick
    694         self.frame.rename(self, '%s - %s' % (self.title, nick))
    695         self.server.set_nick(nick)
    696 
    697     def cmd_quote(self, text):
    698         self.server.quote(text)
    699 
    700     def cmd_avatar(self, avatar):
    701         self.server.set_avatar(avatar)
    702 
    703     def cmd_msg(self, nicks, text):
    704         nicks = string.split(nicks, ',')
    705         self.server.privmsg(nicks, text)
    706 
    707     def cmd_whois(self, nick):
    708         self.server.whois(nick)
    709 
    710     # Transport-called functions
    711 
    712     def close(self):
    713         self.frame.close(self)
    714 
    715     def background_image(self, image):
    716         """Change the background"""
    717         self.canvas.SetBackground(image)
    718 
    719     def background_progress(self, length, filename, size): pass
    720         #self.background_image(progress(float(length)/float(size)))
    721 
    722     def set_title(self, title):
    723         """Change the room's name"""
    724         self.title = title
    725         self.frame.rename(self, '%s - %s' % (title, self.nick))
    726 
    727     def raise_object(self, name):
    728 	"""Raise the named object to the top of the stacking order."""
    729         try: avatar = self.avatars[name]
    730         except: self.debug('No avatar called %s' % name)
    731         else:
    732             self.canvas.sprites.above(avatar)
    733 
    734     def mouseover(self, name, pos, image1, image2):
    735         """Create a mouseover object"""
    736         x, y = pos
    737         mo = Mouseover(self.canvas, x, y, image1, image2)
    738         self.avatars[name] = mo
    739 
    740     def newimage(self, filename=None):
    741         """Load an image from a file object"""
    742         self.debug('newimage %s' % repr(filename))
    743         if filename is None: return wxNullBitmap
    744         return wxImage(filename).ConvertToBitmap()
    745 
    746     def new_avatar(self, nick, pos, image, noffset, boffset):
    747         x, y = pos
    748         avatar = Avatar(self.canvas, x, y, image, nick, noffset, boffset)
    749         self.avatars[nick] = avatar
    750         self.listbox.Append(nick)
    751         self.canvas.Balloon(nick, '*%s* has entered the room.' % nick)
    752 
    753     def del_avatar(self, nick):
    754         self.textchat.AppendText('*%s* left the room.\n' % nick)
    755         self.canvas.Balloon(nick, '*%s* has left the room.' % nick)
    756         self.listbox.Delete(self.listbox.FindString(nick))
    757         try: avatar = self.avatars[nick]
    758         except KeyError: pass
    759         else:
    760             self.canvas.RemoveSprite(avatar)
    761             del self.avatars[nick]
    762     
    763     def avatar(self, nick, image, noffset, boffset):
    764         """Change the avatar for a nick"""
    765         try: avatar = self.avatars[nick]
    766         except KeyError: self.debug('No avatar called %s' % nick)
    767         else:
    768             avatar.SetImage(image)
    769             avatar.set(noffset, boffset)
    770 
    771     def mouseover_image1(self, name, image):
    772         """Set the unactivated image for a mouseover"""
    773         mo = self.avatars[name]
    774         mo.set_image1(image)
    775         #self.canvas.DoDrawing(mo.rect.x, mo.rect.y, mo.rect.width,
    776         #                      mo.rect.height)
    777 
    778     def mouseover_image2(self, name, image):
    779         """Set the activated image for a mouseover"""
    780         mo = self.avatars[name]
    781         mo.set_image2(image)
    782         #self.canvas.DoDrawing(mo.rect.x, mo.rect.y, mo.rect.width,
    783         #                      mo.rect.height)
    784 
    785     def avatar_image(self, nick, image):
    786         """Set the image for an avatar"""
    787         try: avatar = self.avatars[nick]
    788         except KeyError: self.debug('No avatar called %s' % nick)
    789         else:
    790             avatar.SetImage(image)
    791 
    792     def avatar_progress(self, nick, length, filename, size):
    793         """Set an avatar's image to a progress bar"""
    794         try: avatar = self.avatars[nick]
    795         except KeyError: self.debug('No avatar called %s' % nick)
    796         else: avatar.SetImage(progress(float(length)/float(size)))
    797 
    798     def move_avatar(self, nick, x, y, speed):
    799         wxLogVerbose('move_avatar')
    800         try: avatar = self.avatars[nick]
    801         except: self.debug('No avatar called %s' % nick)
    802         else: avatar.AnimateMove((x, y), speed)
    803 
    804     def privmsg(self, nick, s):
    805         self.textchat.AppendText('[%s] %s\n' % (nick, s))
    806         try: avatar = self.avatars[nick]
    807         except KeyError: self.debug('No avatar called %s' % nick)
    808         else: self.canvas.Balloon(nick, s)
    809 
    810     def chat(self, nick, s):
    811         self.textchat.AppendText('<%s> %s\n' % (nick, s))
    812         self.canvas.Balloon(nick, s)
    813 
    814 
    815 class Frame(wxFrame):
    816     """The main frame of the application"""
    817     def __init__(self, parent, ID, title):
    818         wxFrame.__init__(self, parent, ID, title,
    819                          wxDefaultPosition)
    820         self.CreateStatusBar()
    821         self.SetStatusText("This is the statusbar")
    822 
    823         ID_ABOUT = wxNewId()
    824         ID_EXIT = wxNewId()
    825         ID_CONNECT = wxNewId()
    826         
    827         menu = wxMenu()
    828         menu.Append(ID_CONNECT, "&Connect", "Connect to a new room")
    829         menu.AppendSeparator()
    830         menu.Append(ID_EXIT, "E&xit", "Terminate the program")
    831         
    832         menuBar = wxMenuBar()
    833         menuBar.Append(menu, "&File")
    834 
    835         menu = wxMenu()
    836         menu.Append(ID_ABOUT, "&About",
    837                     "More information about this program")
    838         menuBar.Append(menu, "&Help")
    839         
    840         self.SetMenuBar(menuBar)
    841         EVT_MENU(self, ID_ABOUT, self.OnAbout)
    842         EVT_MENU(self, ID_EXIT,  self.TimeToQuit)
    843         EVT_MENU(self, ID_CONNECT, self.OnConnection)
    844 
    845         self.client_nb = wxNotebook(self, -1)
    846         self.client_nb.AddPage(LogWindow(self.client_nb), "Log")
    847         self.sizer = wxNotebookSizer(self.client_nb)
    848         self.SetAutoLayout(true)
    849         self.SetSizer(self.sizer)
    850 
    851     def GetClientPage(self, client):
    852         """Return the page number for a given client"""
    853 
    854         ID = client.GetId()
    855         for i in range(self.client_nb.GetPageCount()):
    856             page = self.client_nb.GetPage(i)
    857             if page.GetId() == ID:
    858                 return i
    859         else: raise ValueError, 'Nonexistent page'
    860 
    861     def close(self, client):
    862         self.client_nb.DeletePage(self.GetClientPage(client))
    863 
    864     def rename(self, client, name):
    865         self.client_nb.SetPageText(self.GetClientPage(client), name)
    866             
    867     def resize(self):
    868         self.Fit()
    869         self.sizer.SetSizeHints(self)
    870        
    871     def OnAbout(self, event):
    872         dlg = wxMessageDialog(self, "PythonVerse\n"
    873                               "Copyright 2001 Christine McIntyre\n"
    874                               "All rights reserved.\n",
    875                               "About PythonVerse", wxOK | wxICON_INFORMATION)
    876         dlg.ShowModal()
    877         dlg.Destroy()
    878 
    879     def OnConnection(self, event):
    880         window = ConnectionDialog(self, -1)
    881         window.Show(true)
    882 
    883     def TimeToQuit(self, event):
    884         self.Close(true)
    885 
    886     def connect(self, server, port, nick, avatar):
    887         self.client_nb.AddPage(Client(self, self.client_nb, -1, server, port,
    888                                       nick, avatar),
    889                                "%s:%d" % (server, port))
    890 
    891 
    892 class LogWindow(wxPanel):
    893     def __init__(self, parent, ID=-1):
    894         wxPanel.__init__(self, parent, ID)
    895         self.tc = wxTextCtrl(self, ID,
    896                              style=wxTE_MULTILINE|wxTE_READONLY|wxTE_RICH)
    897         self.logger = wxLogTextCtrl(self.tc)
    898         self.logger.SetVerbose(true)
    899         wxLog_SetActiveTarget(self.logger)
    900         self.sizer = wxBoxSizer(wxVERTICAL)
    901         self.sizer.Add(self.tc, 1, wxEXPAND)
    902         self.SetSizer(self.sizer)
    903         self.SetAutoLayout(true)
    904 
    905 
    906 class ConnectionDialog(wxDialog):
    907     """Dialog for connecting to a server"""
    908     def __init__(self, parent, ID):
    909         wxDialog.__init__(self, parent, ID, "Connect to server")
    910         self.parent = parent
    911         gs = wxFlexGridSizer(4,2,5,5)
    912         gs.AddGrowableCol(1)
    913         gs.Add(wxStaticText(self, -1, "Server"), 0, wxEXPAND) 
    914         self.editserver = wxTextCtrl(self, 255, "openverse.com")
    915         gs.Add(self.editserver, 1, wxEXPAND) 
    916         #EVT_TEXT(self, 20, self.EvtText) 
    917         #EVT_CHAR(self.editname, self.EvtChar) 
    918         gs.Add(wxStaticText(self, -1, "Port"), 0, wxEXPAND) 
    919         self.editport = wxTextCtrl(self, 5, "6900")
    920         gs.Add(self.editport, 1, wxEXPAND)
    921         gs.Add(wxStaticText(self, -1, "Nick"), 0, wxEXPAND)
    922         self.editnick = wxTextCtrl(self, 9, "Ryoko")
    923         gs.Add(self.editnick, 1, wxEXPAND)
    924         gs.Add(wxStaticText(self, -1, "Avatar"), 0, wxEXPAND)
    925         self.editavatar = wxTextCtrl(self, 9, "ryoko")
    926         gs.Add(self.editavatar, 1, wxEXPAND)
    927         vbox = wxBoxSizer(wxVERTICAL)
    928         vbox.Add(gs)
    929         hbox = wxBoxSizer(wxHORIZONTAL)
    930         button = wxButton(self, wxID_OK, "Connect")
    931         EVT_BUTTON(self, wxID_OK, self.OnOK)
    932         hbox.Add(button)
    933         button = wxButton(self, wxID_CANCEL, "Cancel")
    934         hbox.Add(button)
    935         vbox.Add(hbox)
    936         self.sizer = vbox
    937         self.SetAutoLayout(true) 
    938         self.SetSizer(self.sizer)
    939         self.sizer.Fit(self)
    940 
    941     def OnOK(self, event):
    942         server = self.editserver.GetValue()
    943         port = self.editport.GetValue()
    944         nick = self.editnick.GetValue()
    945         avatar = self.editavatar.GetValue()
    946         self.parent.connect(server, int(port), nick, avatar)
    947         #wxDialog.OnOK(self, event)
    948         return event.Skip()
    949 
    950 
    951 class Application(wxApp):
    952     """The main application object"""
    953     def OnInit(self):
    954         """Initialization function called by wxPython"""
    955         # Create the main frame
    956         frame = Frame(NULL, -1, 'PythonVerse')
    957         frame.Show(true)
    958         self.SetTopWindow(frame)
    959         return true
    960 
    961 
    962 class Scheduler(wxTimer):
    963     def __init__(self):
    964         wxTimer.__init__(self)
    965         self.events = []
    966 
    967     def Schedule(self, t, func, args):
    968         event = t, func, args
    969         bisect.insort(self.events, event)
    970         # Restart the timer
    971         self.Start(min(0, t-time.time())*1000, true)
    972         return event
    973 
    974     def ScheduleRel(self, delay, func, args):
    975         now = time.time()
    976         return self.Schedule(now+delay, func, args)
    977 
    978     def Cancel(self, event):
    979         self.events.remove(event)
    980 
    981     def Notify(self):
    982         now = time.time()
    983         while self.events:
    984             t, func, args = self.events[0]
    985             if t > now: break
    986             apply(func, args)
    987             del self.events[0]
    988 
    989         # If there are still events left over, then the last
    990         # setting of t will be for the first event that's not yet
    991         # scheduled. Make sure it's a single shot :)
    992         if self.events: self.Start((t-now)*1000, true)
    993 
    994 _scheduler = Scheduler()
    995             
    996 
    997 class IOTimer(wxTimer):
    998     """Poll asyncore, would be better to use threads"""
    999     def Notify(self):
   1000         asyncore.poll(0.0)
   1001         
   1002 
   1003 def main(argv):
   1004     app = Application(0)
   1005     wxInitAllImageHandlers()
   1006     t = IOTimer()
   1007     t.Start(500)
   1008     app.MainLoop()
   1009 
   1010 
   1011 if __name__ == '__main__': main(sys.argv)
   1012