PyXR

c:\projects\bitpim\src \ calendarcontrol.py



0001 #!/usr/bin/env python
0002 
0003 ### BITPIM
0004 ###
0005 ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com>
0006 ###
0007 ### This program is free software; you can redistribute it and/or modify
0008 ### it under the terms of the BitPim license as detailed in the LICENSE file.
0009 ###
0010 ### $Id: calendarcontrol.py 4407 2007-09-25 20:39:48Z djpham $
0011 
0012 """A calendar control that shows several weeks in one go
0013 
0014 The design is inspired by the Alan Cooper article U{http://www.cooper.com/articles/art_goal_directed_design.htm}
0015 about goal directed design.  I also have to apologise for it not quite living up to that vision :-)
0016 
0017 It is fairly feature complete and supports all sorts of interaction, scrolling and customization
0018 of appearance"""
0019 
0020 import wx
0021 import wx.lib.rcsizer
0022 import wx.calendar
0023 import cStringIO
0024 import calendar
0025 import time
0026 import widgets
0027 import tipwin
0028 
0029 class FontscaleCache(dict):
0030     """A cache used internally to remember how much to shrink fonts by"""
0031     # cache results of what the scale factor is to fit a number of lines in a space is
0032     def get(self, y, attr, numentries):
0033         return dict.get(self, (y, id(attr), numentries), 1)
0034     def set(self, y, attr, numentries, scale):
0035         self[(y, id(attr),  numentries)]=scale
0036     def uncache(self, *args):
0037         # clear out any cached attrs listed in args (eg when they are changed)
0038         keys=self.keys()
0039         l2=[id(x) for x in args]
0040         for y, idattr, numentries in keys:
0041             if idattr in l2:
0042                 del self[ (y, idattr, numentries) ]
0043 
0044 thefontscalecache=FontscaleCache()
0045 
0046 class CalendarCellAttributes:
0047     """A class represnting appearance attributes for an individual day.
0048 
0049     You should subclass this if you wish to change the appearance from
0050     the defaults"""
0051     def __init__(self):
0052         # Set some defaults
0053         #self.cellbackground=wx.TheBrushList.FindOrCreateBrush(wx.Colour(230,255,255), wx.SOLID)
0054         self.cellbackground=wx.Brush(wx.Colour(197,255,255), wx.SOLID)
0055         self.labelfont=wx.Font(14, wx.SWISS, wx.NORMAL, wx.NORMAL )
0056         self.labelforeground=wx.NamedColour("CORNFLOWER BLUE")
0057         self.labelalign=wx.ALIGN_RIGHT
0058         self.timefont=wx.Font(8, wx.SWISS, wx.NORMAL, wx.NORMAL )
0059         self.timeforeground=wx.NamedColour("ORCHID")
0060         self.entryfont=wx.Font(9, wx.SWISS, wx.NORMAL, wx.NORMAL )
0061         self.entryforeground=wx.NamedColour("BLACK")
0062         self.miltime=False
0063         self.initdone=True
0064 
0065     def __setattr__(self, k, v):
0066         self.__dict__[k]=v
0067         if hasattr(self, 'initdone'):
0068             thefontscalecache.uncache(self)
0069 
0070     def isrightaligned(self):
0071         """Is the number representing the day right aligned within the cell?
0072 
0073         @rtype: Bool
0074         @return:  True is it should be shown right aligned"""
0075         return self.labelalign==wx.ALIGN_RIGHT
0076 
0077     def ismiltime(self):
0078         """Are times shown in military (aka 24 hour) time?
0079 
0080         @rtype: Bool
0081         @return: True is militart/24 hour format should be used"""
0082         return self.miltime
0083 
0084     def setforcellbackground(self, dc):
0085         """Set the cell background attributes
0086 
0087         Colour
0088         @type dc: wx.DC"""
0089         dc.SetBackground(self.cellbackground)
0090 
0091     def setforlabel(self, dc, fontscale=1):
0092         """Set the attributes for the day label
0093 
0094         Colour, font
0095         @type dc: wx.DC
0096         @param fontscale: You should multiply the font point size
0097                           by this number
0098         @type fontscale: float
0099         """
0100         
0101         dc.SetTextForeground(self.labelforeground)
0102         return self.setscaledfont(dc, self.labelfont, fontscale)
0103 
0104     def setfortime(self,dc, fontscale=1):
0105         """Set the attributes for the time of an event text
0106  
0107         Colour, font
0108         @type dc: wx.DC
0109         @param fontscale: You should multiply the font point size
0110                           by this number
0111         @type fontscale: float
0112         """
0113         dc.SetTextForeground(self.timeforeground)
0114         return self.setscaledfont(dc, self.timefont, fontscale)
0115 
0116     def setforentry(self, dc, fontscale=1):
0117         """Set the attributes for the label of an event text
0118  
0119         Colour, font
0120         @type dc: wx.DC
0121         @param fontscale: You should multiply the font point size
0122                           by this number
0123         @type fontscale: float
0124         """        
0125         dc.SetTextForeground(self.entryforeground)
0126         return self.setscaledfont(dc, self.entryfont, fontscale)
0127                 
0128     def setscaledfont(self, dc, font, fontscale):
0129         """Changes the in the device context to the supplied font suitably scaled
0130 
0131         @type dc: wx.DC
0132         @type font: wx.Font
0133         @type fontscale: float
0134         @return: Returns True if the scaling succeeded, and False if the font was already
0135                  too small to scale smaller (the smallest size will still have been
0136                  selected into the device context)
0137         @rtype: Bool"""
0138         if fontscale==1:
0139             dc.SetFont(font)
0140             return True
0141         ps=int(font.GetPointSize()*fontscale)
0142         if ps<2:
0143             ps=2
0144         f=wx.TheFontList.FindOrCreateFont(int(ps), font.GetFamily(), font.GetStyle(), font.GetWeight())
0145         dc.SetFont(f)
0146         if ps==2:
0147             return False
0148         return True
0149 
0150 
0151 # a hack - this used to be an instance, but wx 2.5 doesn't allow using brushes/pens etc until
0152 # app instance is created
0153 DefaultCalendarCellAttributes=None
0154 
0155 def GetCalendarCellAttributes(attr=None):
0156     if attr is not None:
0157         return attr
0158     global DefaultCalendarCellAttributes
0159     if DefaultCalendarCellAttributes is None:
0160         DefaultCalendarCellAttributes=CalendarCellAttributes()
0161     return DefaultCalendarCellAttributes
0162                 
0163 class CalendarCell(wx.PyWindow):
0164     """A control that is used for each day in the calendar
0165 
0166     As the user scrolls around the calendar, each cell is updated with new dates rather
0167     than creating new CalendarCell objects.  Internally it uses a backing buffer so
0168     that redraws are quick and flicker free."""
0169     
0170     fontscalecache=FontscaleCache()
0171 
0172     def __init__(self, parent, id, attr=DefaultCalendarCellAttributes, style=wx.SIMPLE_BORDER):
0173         wx.PyWindow.__init__(self, parent, id, style=style|wx.WANTS_CHARS|wx.FULL_REPAINT_ON_RESIZE)
0174         self.attr=GetCalendarCellAttributes(attr)
0175         self.day=33
0176         self.year=2033
0177         self.month=3
0178         self.buffer=None
0179         self.needsupdate=True
0180         self.entries=()
0181         self._tipwindow=None
0182 
0183         wx.EVT_PAINT(self, self.OnPaint)
0184         wx.EVT_SIZE(self, self.OnSize)
0185         wx.EVT_ENTER_WINDOW(self, self.OnEnterWindow)
0186         wx.EVT_LEAVE_WINDOW(self, self.OnLeaveWindow)
0187         self._timer=wx.Timer(self)
0188         wx.EVT_TIMER(self, self._timer.GetId(), self.OnTimer)
0189         self.OnSize(None)
0190 
0191     def DoGetBestSize(self):
0192         return (10,10)
0193 
0194     def setdate(self, year, month, day):
0195         """Set the date we are"""
0196         self.year=year
0197         self.month=month
0198         self.day=day
0199         self.needsupdate=True
0200         self.Refresh(False)
0201 
0202     def setattr(self, attr):
0203         """Sets what CalendarCellAtrributes we use for appearance
0204 
0205         @type attr: CalendarCellAtrributes"""
0206         self.attr=GetCalendarCellAttributes(attr)
0207         self.needsupdate=True
0208         self.Refresh(False)
0209 
0210     def setentries(self, entries):
0211         """Sets the entries we will display
0212 
0213         @type entries: list
0214         @param entries: A list of entries.  Format is ( ( hour, minute, description), (hour, minute, decription) ... ).  hour is in 24 hour
0215         """
0216         self.entries=entries
0217         self.needsupdate=True
0218         self.Refresh(False)
0219 
0220     def getdate(self):
0221         """Returns what date we are currently displaying
0222 
0223         @rtype: tuple
0224         @return: tuple of (year, month, day)"""
0225         return (self.year, self.month, self.day)
0226 
0227     def OnSize(self, evt=None):
0228         """Callback for when we are resized"""
0229         self.width, self.height = self.GetClientSizeTuple()
0230         self.needsupdate=True
0231         if evt is not None:
0232             evt.Skip()
0233             
0234 
0235     def redraw(self):
0236         """Causes a forced redraw into our back buffer"""
0237         if self.buffer is None or \
0238            self.buffer.GetWidth()!=self.width or \
0239            self.buffer.GetHeight()!=self.height:
0240             if self.buffer is not None:
0241                 del self.buffer
0242             self.buffer=wx.EmptyBitmap(self.width, self.height)
0243 
0244         mdc=wx.MemoryDC()
0245         mdc.SelectObject(self.buffer)
0246         self.attr.setforcellbackground(mdc)
0247         mdc.Clear()
0248         self.draw(mdc)
0249         mdc.SelectObject(wx.NullBitmap)
0250         del mdc
0251 
0252     def _tipstr(self):
0253         # return a summary of events for displaying in a tooltip
0254         _res=[]
0255         lastap=""
0256         for h,m,desc in self.entries:
0257             if h is None:
0258                 _res.append('\t'+desc)
0259             else:
0260                 _text, lastap=self._timestr(h, m, lastap)
0261                 _res.append('%s\t%s'%(_text, desc))
0262         return '\n'.join(_res)
0263 
0264     def OnPaint(self, _=None):
0265         """Callback for when we need to repaint"""
0266         if self.needsupdate:
0267             self.needsupdate=False
0268             self.redraw()
0269         dc=wx.PaintDC(self)
0270         dc.DrawBitmap(self.buffer, 0, 0, False)
0271 
0272     def _timestr(self, h, m, lastap=''):
0273         text=""
0274         if self.attr.ismiltime():
0275             ap=""
0276         else:
0277             ap="a"
0278             if h>=12: ap="p"
0279             h%=12
0280             if h==0: h=12
0281             if ap==lastap:
0282                 ap=""
0283             else:
0284                 lastap=ap
0285         if h<10: text+=" "
0286         return (text+"%d:%02d%s" % (h,m,ap), lastap)
0287 
0288     def draw(self, dc):
0289         """Draw ourselves
0290 
0291         @type dc: wx.DC"""
0292         
0293         # do the label
0294         self.attr.setforlabel(dc)
0295         w,h=dc.GetTextExtent(`self.day`)
0296         x=1
0297         if self.attr.isrightaligned():
0298             x=self.width-(w+5)
0299         dc.DrawText(`self.day`, x, 0)
0300 
0301         if len(self.entries)==0:
0302             return
0303         
0304         entrystart=h # +5
0305         dc.DestroyClippingRegion()
0306         dc.SetClippingRegion( 0, entrystart, self.width, self.height-entrystart)
0307 
0308         fontscale=thefontscalecache.get(self.height-entrystart, self.attr, len(self.entries))
0309         iteration=0
0310          # this loop scales the contents to fit the space available
0311          # we do it as a loop because even when we ask for a smaller font
0312          # after finding out that it was too big the first time, the
0313          # smaller font may not be as small as we requested
0314         while 1:
0315             y=entrystart
0316             iteration+=1
0317             # now calculate how much space is needed for the time fields
0318             self.attr.setfortime(dc, fontscale)
0319             boundingspace=2
0320             space,_=dc.GetTextExtent("i")
0321             timespace,timeheight=dc.GetTextExtent("mm:mm")
0322             if self.attr.ismiltime():
0323                 ampm=0
0324             else:
0325                 ampm,_=dc.GetTextExtent("a")
0326 
0327             r=self.attr.setforentry(dc, fontscale)
0328             if not r: iteration=-1 # font can't be made this small
0329             _,entryheight=dc.GetTextExtent("I")
0330             firstrowheight=max(timeheight, entryheight)
0331 
0332             # Now draw each item
0333             lastap=""
0334             for h,m,desc in self.entries:
0335                 x=0
0336                 if h is not None:
0337                     self.attr.setfortime(dc, fontscale)
0338                     # bounding
0339                     x+=boundingspace # we don't draw anything yet
0340                     timey=y
0341                     if timeheight<firstrowheight:
0342                         timey+=(firstrowheight-timeheight)/2
0343                     text, lastap=self._timestr(h, m, lastap)
0344                     dc.DrawText(text, x, timey)
0345                     x+=timespace
0346                     if not self.attr.ismiltime: x+=ampm
0347                 
0348                 self.attr.setforentry(dc, fontscale)
0349                 ey=y
0350                 if entryheight<firstrowheight:
0351                     ey+=(firstrowheight-entryheight)/2
0352                 dc.DrawText(desc, x, ey)
0353                 # that row is dealt with!
0354                 y+=firstrowheight
0355             if iteration==1 and fontscale!=1:
0356                 # came from cache
0357                 break
0358             if iteration==-1:
0359                 # update cache
0360                 thefontscalecache.set(self.height-entrystart, self.attr, len(self.entries), fontscale)
0361                 break # reached limit of font scaling
0362             if iteration<10 and y>self.height:
0363                 dc.Clear()
0364                 # how much too big were we?
0365                 fontscale=fontscale*float(self.height-entrystart)/(y-entrystart)
0366                 # print iteration, y, self.height, fontscale
0367             else:
0368                 thefontscalecache.set(self.height-entrystart, self.attr, len(self.entries), fontscale)
0369                 break
0370 
0371     def OnEnterWindow(self, _):
0372         if self.entries:
0373             self._timer.Start(1000, wx.TIMER_ONE_SHOT)
0374     def OnLeaveWindow(self, _):
0375         self._timer.Stop()
0376     def OnTimer(self, _):
0377         if not self.entries or wx.GetApp().critical.isSet():
0378             return
0379         _rect=self.GetRect()
0380         _x,_y=self.GetParent().ClientToScreen(_rect[:2])
0381         _rect.SetX(_x)
0382         _rect.SetY(_y)
0383         if self._tipwindow:
0384             self._tipwindow.Destroy()
0385         self._tipwindow=tipwin.TipWindow(self, self._tipstr(), 1024, _rect)
0386 
0387 class CalendarLabel(wx.PyWindow):
0388     """The label window on the left of the day cells that shows the month with rotated text
0389 
0390     It uses double buffering etc for a flicker free experience"""
0391     
0392     def __init__(self, parent, cells, id=-1):
0393         wx.PyWindow.__init__(self, parent, id, style=wx.FULL_REPAINT_ON_RESIZE)
0394         self.needsupdate=True
0395         self.buffer=None
0396         self.cells=cells
0397         wx.EVT_PAINT(self, self.OnPaint)
0398         wx.EVT_SIZE(self, self.OnSize)
0399         self.setfont(wx.Font(20, wx.SWISS, wx.NORMAL, wx.BOLD ))
0400         self.settextcolour(wx.NamedColour("BLACK"))
0401         self.OnSize(None)
0402 
0403     def DoGetBestSize(self):
0404         return (10,10)
0405 
0406     def OnSize(self, _=None):
0407         self.width, self.height = self.GetClientSizeTuple()
0408         self.needsupdate=True
0409         
0410     def OnPaint(self, _=None):
0411         if self.needsupdate:
0412             self.needsupdate=False
0413             self.redraw()
0414         dc=wx.PaintDC(self)
0415         dc.DrawBitmap(self.buffer, 0, 0, False)
0416 
0417     def setfont(self, font):
0418         self.font=font
0419 
0420     def settextcolour(self, colour):
0421         self.colour=colour
0422 
0423     def changenotify(self):
0424         self.needsupdate=True
0425         self.Refresh()
0426 
0427     def redraw(self):
0428         if self.buffer is None or \
0429            self.buffer.GetWidth()!=self.width or \
0430            self.buffer.GetHeight()!=self.height:
0431             if self.buffer is not None:
0432                 del self.buffer
0433             self.buffer=wx.EmptyBitmap(self.width, self.height)
0434 
0435         mdc=wx.MemoryDC()
0436         mdc.SelectObject(self.buffer)
0437         mdc.SetBackground(wx.TheBrushList.FindOrCreateBrush(self.GetBackgroundColour(), wx.SOLID))
0438         mdc.Clear()
0439         self.draw(mdc)
0440         mdc.SelectObject(wx.NullBitmap)
0441         del mdc
0442 
0443     def draw(self, dc):
0444         # find the lines for each cell
0445         row=0
0446         while row<len(self.cells):
0447             month=self.cells[row].month
0448             endrow=row
0449             for r2 in range(row+1,len(self.cells)):
0450                 if month==self.cells[r2].month:
0451                     endrow=r2
0452                 else:
0453                     break
0454             # row is begining row, endrow is end, inclusive
0455 
0456             # find the space available.  we do lots of lovely math
0457             # in order to translate the coordinates from the rows
0458             # into our window
0459             x=0
0460             y=self.cells[row].GetPositionTuple()[1]-self.cells[0].GetPositionTuple()[1]
0461             w=self.width
0462             h=self.cells[endrow].GetPositionTuple()[1]+self.cells[endrow].GetRect().height \
0463                -self.cells[row].GetPositionTuple()[1]
0464 
0465             
0466             dc.DestroyClippingRegion()
0467             dc.SetClippingRegion(x,y,w,h)
0468             dc.SetPen(wx.ThePenList.FindOrCreatePen("BLACK", 3, wx.SOLID))
0469             # draw line at top and bottom
0470             if row!=0:
0471                 dc.DrawLine(x, y, x+w, y)
0472             if endrow!=len(self.cells)-1:
0473                 dc.DrawLine(x, y+h, x+w, y+h)
0474             month=calendar.month_name[month]
0475             dc.SetFont(self.font)
0476             dc.SetTextForeground(self.colour)
0477             tw,th=dc.GetTextExtent(month)
0478             # Now figure out where to draw it
0479             if tw<h:
0480                 # it fits, so centre
0481                 dc.DrawRotatedText(month, w/2-th/2, y + h/2 + tw/2, 90)
0482             else:
0483                  # it doesn't fit
0484                  if row==0:
0485                      # top one shows start of text
0486                      dc.DrawRotatedText(month, w/2-th/2, y + h -5, 90)
0487                  else:
0488                      # show end of text at bottom
0489                      dc.DrawRotatedText(month, w/2-th/2, y + 5 + tw, 90)
0490 
0491             # Loop around
0492             row=endrow+1
0493 
0494 
0495 class Calendar(wx.Panel, widgets.BitPimWidget):
0496     """The main calendar control.
0497 
0498     You should subclass this clas and need to
0499     implement the following methods:
0500 
0501     L{OnGetEntries}
0502     L{OnEdit}
0503 
0504     The following methods you may want to call at some point:
0505 
0506     L{RefreshEntry}
0507     L{RefreshAllEntries}
0508 
0509     
0510 """
0511 
0512     # All the horrible date code is an excellent case for metric time!
0513     ID_UP=wx.NewId()
0514     ID_DOWN=wx.NewId()
0515     ID_YEARBUTTON=wx.NewId()
0516     ID_TODAYBUTTON=wx.NewId()
0517 
0518     attrevenmonth=None
0519     attroddmonth=None
0520     attrselectedcell=None
0521 
0522     def _initvars(self):
0523         # this is needed to avoid issues with the wx. library not being initialised
0524         # as the module is imported.  We initialise the values when the first
0525         # calendar constructor is run
0526         if Calendar.attrevenmonth is None:
0527             Calendar.attrevenmonth=CalendarCellAttributes()
0528         if Calendar.attroddmonth is None:
0529             Calendar.attroddmonth=CalendarCellAttributes()
0530             Calendar.attroddmonth.cellbackground=wx.TheBrushList.FindOrCreateBrush( wx.Colour(255, 255, 230), wx.SOLID)
0531         if Calendar.attrselectedcell is None:
0532             Calendar.attrselectedcell=CalendarCellAttributes()
0533             Calendar.attrselectedcell.cellbackground=wx.TheBrushList.FindOrCreateBrush( wx.Colour(240,240,240), wx.SOLID)
0534             Calendar.attrselectedcell.labelfont=wx.Font(17, wx.SWISS, wx.NORMAL, wx.BOLD )
0535             Calendar.attrselectedcell.labelforeground=wx.NamedColour("BLACK")
0536     
0537     def __init__(self, parent, rows=5, id=-1):
0538         self._initvars()
0539         wx.Panel.__init__(self, parent, id, style=wx.WANTS_CHARS|wx.FULL_REPAINT_ON_RESIZE)
0540         sizer=wx.GridBagSizer()
0541         self.upbutt=wx.BitmapButton(self, self.ID_UP, getupbitmapBitmap())
0542         sizer.Add(self.upbutt, flag=wx.EXPAND, pos=(0,1), span=(1,7))
0543         self.year=wx.Button(self, self.ID_YEARBUTTON, "2003")
0544         sizer.Add(self.year, flag=wx.EXPAND, pos=(1,0))
0545         sizer.Add(wx.Button(self, self.ID_TODAYBUTTON, "Today"), flag=wx.EXPAND, pos=(0,0))
0546         p=1
0547         calendar.setfirstweekday(calendar.SUNDAY)
0548         for i in ( "Sun", "Mon", "Tue", "Wed" , "Thu", "Fri", "Sat" ):
0549            sizer.Add(  wx.StaticText( self, -1, i, style=wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL),
0550                        flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER_HORIZONTAL|wx.EXPAND, pos=(1,p))
0551            sizer.AddGrowableCol(p)
0552            p+=1
0553         self.numrows=rows
0554         self.showrow=rows/2
0555         self.rows=[]
0556         for i in range(0, rows):
0557             self.rows.append( self.makerow(sizer, i+2) )
0558         self.downbutt=wx.BitmapButton(self, self.ID_DOWN, getdownbitmapBitmap())
0559         sizer.Add(self.downbutt, flag=wx.EXPAND, pos=(2+rows, 0), span=(1,8))
0560         self.label=CalendarLabel(self, map(lambda x: x[0], self.rows))
0561         sizer.Add(self.label, flag=wx.EXPAND, pos=(2,0), span=(self.numrows,1))
0562         self.sizer=sizer
0563 
0564         self.popupcalendar=PopupCalendar(self, self)
0565 
0566         wx.EVT_BUTTON(self, self.ID_UP, self.OnScrollUp)
0567         wx.EVT_BUTTON(self, self.ID_DOWN, self.OnScrollDown)
0568         wx.EVT_BUTTON(self, self.ID_YEARBUTTON, self.OnYearButton)
0569         wx.EVT_BUTTON(self, self.ID_TODAYBUTTON, self.OnTodayButton)
0570         # grab key down from all children
0571         map(lambda child: wx.EVT_KEY_DOWN(child, self.OnKeyDown), self.GetChildren())
0572         # and mousewheel
0573         map(lambda child: wx.EVT_MOUSEWHEEL(child, self.OnMouseWheel), self.GetChildren())
0574         # grab left down, left dclick from all cells
0575         for r in self.rows:
0576             map(lambda cell: wx.EVT_LEFT_DOWN(cell, self.OnLeftDown), r)
0577             map(lambda cell: wx.EVT_LEFT_DCLICK(cell, self.OnLeftDClick), r)
0578 
0579         self.selectedcell=(-1,-1)
0580         self.selecteddate=(-1,-1,-1)
0581 
0582         self.showday(*time.localtime()[:3]+(self.showrow,))
0583         self.setday(*time.localtime()[:3])
0584 
0585         self.SetSizer(sizer)
0586         sizer.Fit(self)
0587         self.SetAutoLayout(True)
0588 
0589     def OnKeyDown(self, event):
0590         key=event.GetKeyCode()
0591         if key==wx.WXK_NEXT:
0592            self.scrollby( (self.numrows-1)*7)
0593         elif key==wx.WXK_PRIOR:
0594            self.scrollby( (self.numrows-1)*-7)
0595         elif key==wx.WXK_LEFT:
0596            self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1], self.selecteddate[2]-1) )
0597         elif key==wx.WXK_RIGHT:
0598            self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1], self.selecteddate[2]+1) )
0599         elif key==wx.WXK_UP:
0600            self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1], self.selecteddate[2]-7) )
0601         elif key==wx.WXK_DOWN:
0602            self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1], self.selecteddate[2]+7) )
0603         elif key==wx.WXK_HOME:  # back a month
0604            self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1]-1, self.selecteddate[2]) )
0605         elif key==wx.WXK_END: # forward a month
0606            self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1]+1, self.selecteddate[2]) )
0607         # ::TODO:: activate edit code for return or space on a calendarcell
0608         else:
0609            event.Skip()  # control can have it
0610 
0611     def OnMouseWheel(self, event):
0612         delta=event.GetWheelDelta()
0613         if delta==0: # as it does on linux
0614             delta=120
0615         lines=event.GetWheelRotation()/delta
0616         self.scrollby(-7*lines)
0617 
0618     def OnLeftDown(self, event):
0619         cell=event.GetEventObject()
0620         self.setselection(cell.year, cell.month, cell.day)
0621 
0622     def OnLeftDClick(self,event):
0623         cell=event.GetEventObject()
0624         self.OnEdit(cell.year, cell.month, cell.day)
0625 
0626     def OnYearButton(self, event):
0627         self.popupcalendar.Popup( * (self.selecteddate + (event,)) )
0628 
0629     def OnTodayButton(self, _):
0630         self.setday(*time.localtime()[:3])
0631 
0632     def makerow(self, sizer, row):
0633         res=[]
0634         sizer.AddGrowableRow(row)
0635         for i in range(0,7):
0636             res.append( CalendarCell(self, -1) )
0637             sizer.Add( res[-1], flag=wx.EXPAND, pos=(row,i+1))
0638         return res
0639 
0640     def scrollby(self, amount):
0641         assert abs(amount)%7==0
0642         for row in range(0, self.numrows):
0643                 y,m,d=self.rows[row][0].getdate()
0644                 y,m,d=normalizedate(y,m,d+amount)
0645                 self.updaterow(row, y,m,d)
0646         self.setselection(*self.selecteddate)
0647         self.label.changenotify()
0648         self.ensureallpainted()
0649 
0650     def ensureallpainted(self):
0651         # doesn't return until cells have been painted
0652         self.Update()
0653 
0654     def OnScrollDown(self, _=None):
0655         # user has pressed scroll down button
0656         self.scrollby(7)
0657         
0658     def OnScrollUp(self, _=None):
0659        # user has pressed scroll up button
0660        self.scrollby(-7)
0661 
0662     def setday(self, year, month, day):
0663        # makes specified day be shown and selected
0664        self.showday(year, month, day)
0665        self.setselection(year, month, day)
0666        
0667     def showday(self, year, month, day, rowtoshow=-1):
0668        """Ensures specified date is onscreen
0669 
0670        @param rowtoshow:   if is >=0 then it will be forced to appear in that row
0671        """
0672        # check first cell
0673        y,m,d=self.rows[0][0].year, self.rows[0][0].month, self.rows[0][0].day
0674        if rowtoshow==-1:
0675           if year<y or (year<=y and month<m) or (year<=y and month<=m and day<d):
0676              rowtoshow=0
0677        # check last cell   
0678        y,m,d=self.rows[-1][-1].year, self.rows[-1][-1].month, self.rows[-1][-1].day
0679        if rowtoshow==-1:
0680           if year>y or (year>=y and month>m) or (year>=y and month>=m and day>d):
0681              rowtoshow=self.numrows-1
0682        if rowtoshow!=-1:
0683           d=calendar.weekday(year, month, day)
0684           d=(d+1)%7
0685           
0686           d=day-d # go back to begining of week
0687           d-=7*rowtoshow # then begining of screen
0688           y,m,d=normalizedate(year, month, d)
0689           for row in range(0,self.numrows):
0690              self.updaterow(row, *normalizedate(y, m, d+7*row))
0691           self.label.changenotify()
0692           self.ensureallpainted()
0693 
0694     def isvisible(self, year, month, day):
0695        """Tests if the date is visible to the user
0696 
0697        @rtype: Bool
0698        """
0699        y,m,d=self.rows[0][0].year, self.rows[0][0].month, self.rows[0][0].day
0700        if year<y or (year<=y and month<m) or (year<=y and month<=m and day<d):
0701            return False
0702        y,m,d=self.rows[-1][-1].year, self.rows[-1][-1].month, self.rows[-1][-1].day
0703        if year>y or (year>=y and month>m) or (year>=y and month>=m and day>d):
0704            return False
0705        return True
0706 
0707     def RefreshEntry(self, year, month, day):
0708        """Causes that date's entries to be refreshed.
0709 
0710        Call this if you have changed the data for one day.
0711        Note that your OnGetEntries will only be called if the date is
0712        currently visible."""
0713        if self.isvisible(year,month,day):
0714            # ::TODO:: find correct cell and only update that
0715            self.RefreshAllEntries()
0716    
0717     def RefreshAllEntries(self):
0718        """Call this if you have completely changed all your data.
0719 
0720        OnGetEntries will be called for each visible day."""
0721 
0722        for row in self.rows:
0723            for cell in row:
0724                cell.setentries(self.OnGetEntries(cell.year, cell.month, cell.day))
0725 
0726    
0727     def setselection(self, year, month, day):
0728        """Selects the specifed date if it is visible"""
0729        self.selecteddate=(year,month,day)
0730        d=calendar.weekday(year, month, day)
0731        d=(d+1)%7
0732        for row in range(0, self.numrows):
0733           cell=self.rows[row][d]
0734           if cell.year==year and cell.month==month and cell.day==day:
0735              self._unselect()
0736              self.rows[row][d].setattr(self.attrselectedcell)
0737              self.selectedcell=(row,d)
0738              self.ensureallpainted()
0739              return
0740 
0741     def _unselect(self):
0742        if self.selectedcell!=(-1,-1):
0743           self.updatecell(*self.selectedcell)
0744           self.selectedcell=(-1,-1)
0745 
0746     def updatecell(self, row, column, y=-1, m=-1, d=-1):
0747        if y!=-1:
0748           self.rows[row][column].setdate(y,m,d)
0749        if self.rows[row][column].month%2:
0750           self.rows[row][column].setattr(self.attroddmonth)
0751        else:
0752           self.rows[row][column].setattr(self.attrevenmonth)
0753        if y!=-1 and row==0 and column==0:
0754           self.year.SetLabel(`y`)
0755        if y!=-1:
0756            self.rows[row][column].setentries(self.OnGetEntries(y,m,d))
0757 
0758     def updaterow(self, row, y, m, d):
0759         daysinmonth=monthrange(y, m)
0760         for c in range(0,7):
0761             self.updatecell(row, c, y, m, d)
0762             if d==daysinmonth:
0763                 d=1
0764                 m+=1
0765                 if m==13:
0766                     m=1
0767                     y+=1
0768                 daysinmonth=monthrange(y, m)
0769             else:
0770                 d+=1
0771                 
0772     # The following methods should be implemented in derived class.
0773     # Implementations here are to make it be a nice demo if not subclassed
0774     
0775     def OnGetEntries(self, year, month, day):
0776         """Return a list of entries for the specified y,m,d.
0777 
0778         B{You must implement this method in a derived class}
0779 
0780         The format is ( (hour,min,desc), (hour,min,desc)... )  Hour
0781         should be in 24 hour format.  You should sort the entries.
0782 
0783         Note that Calendar does not cache any results so you will be
0784         asked for the same dates as the user scrolls around."""
0785         
0786         return (
0787             (1, 88, "Early morning"),
0788             (10,15, "Some explanatory text" ),
0789             (10,30, "It is %04d-%02d-%02d" % (year,month,day)),
0790             (11,11, "Look at me!"),
0791             (12,30, "More text here"),
0792             (15,30, "A very large amount of text that will never fit"),
0793             (20,30, "Evening drinks"),
0794             )
0795 
0796     def OnEdit(self, year, month, day):
0797         """The user wishes to edit the entries for the specified date
0798 
0799         B{You should implement this method in a derived class}
0800         """
0801         print "The user wants to edit %04d-%02d-%02d" % (year,month,day)
0802 
0803 
0804 class PopupCalendar(wx.Dialog):
0805     """The control that pops up when you click the year button"""
0806     def __init__(self, parent, calendar, style=wx.SIMPLE_BORDER):
0807         wx.Dialog.__init__(self, parent, -1, '', style=wx.STAY_ON_TOP|style)
0808         self.calendar=calendar
0809         self.control=wx.calendar.CalendarCtrl(self, 1, style=wx.calendar.CAL_SUNDAY_FIRST, pos=(0,0))
0810         sz=self.control.GetBestSize()
0811         self.SetSize(sz)
0812         wx.calendar.EVT_CALENDAR(self, self.control.GetId(), self.OnCalSelected)
0813 
0814     def Popup(self, year, month, day, event):
0815         d=wx.DateTimeFromDMY(day, month-1, year)
0816         self.control.SetDate(d)
0817         btn=event.GetEventObject()
0818         pos=btn.ClientToScreen( (0,0) )
0819         sz=btn.GetSize()
0820         self.Move( (pos[0], pos[1]+sz.height ) )
0821         self.ShowModal()
0822 
0823     def OnCalSelected(self, evt):
0824         dt=evt.GetDate()
0825         self.calendar.setday(dt.GetYear(), dt.GetMonth()+1, dt.GetDay())
0826         self.calendar.ensureallpainted()
0827         self.EndModal(1)
0828         
0829 
0830 _monthranges=[0, 31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
0831 
0832 def monthrange(year, month):
0833     """How many days are in the specified month?
0834 
0835     @rtype: int"""
0836     if month==2:
0837         return calendar.monthrange(year, month)[1]
0838     return _monthranges[month]
0839 
0840 def normalizedate(year, month, day):
0841     """Return a valid date (and an excellent case for metric time)
0842 
0843     And example is the 32nd of January is first of Feb, or Jan -2 is
0844     December 29th of previous year.  You should call this after doing
0845     arithmetic on dates (for example you can just subtract 14 from the
0846     current day and then call this to get the correct date for two weeks
0847     ago.
0848 
0849     @rtype: tuple
0850     @return: (year, month, day)
0851     """
0852 
0853     while day<1 or month<1 or month>12 or (day>28 and day>monthrange(year, month)):
0854         if day<1:
0855             month-=1
0856             if month<1:
0857                 month=12
0858                 year-=1
0859             num=monthrange(year, month)
0860             day=num+day
0861             continue
0862         if day>28 and day>monthrange(year, month):
0863             num=calendar.monthrange(year, month)[1]
0864             month+=1
0865             if month>12:
0866                 month=1
0867                 year+=1
0868             day=day-num
0869             continue    
0870         if month<1:
0871             year-=1
0872             month=month+12
0873             continue
0874         if month>12:
0875             year+=1
0876             month-=12
0877             continue
0878         assert False, "can't get here"
0879 
0880     return year, month, day
0881             
0882 # Up and down bitmap icons
0883 
0884 def getupbitmapData():
0885     """Returns raw data for the up icon"""
0886     return \
0887 '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00\x10\x08\x06\
0888 \x00\x00\x00w\x00}Y\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\
0889 \x00}IDATx\x9c\xbd\xd5K\x0e\xc0 \x08\x04P\xe0\x04\xdc\xff\x94\xdc\xa0\xdd6\
0890 \xad\xca\xf0)n\\\xa83/1FVU\xca\x0e3\xbbT\x95\xd3\x01D$\x95\xf2\xe7<\nx\x97V\
0891 \x10a\xc0\xae,\x8b\x08\x01\xbc\x92\x0c\x02\x06\xa0\xe1Q\x04\x04\x88\x86F\xf6\
0892 \xbb\x80\xec\xdd\xa2\xe7\x8e\x80\xea\x13C\xceo\x01\xd5r4g\t\xe8*G\xf2>\x80\
0893 \xeer/W\x90M\x7f"\xe4\xb48\x81\x90\xc9\xf2\x15\x82+\xdfq\xc7\xb8\x01;]o#\xdc\
0894 D \x03\x00\x00\x00\x00IEND\xaeB`\x82' 
0895 
0896 def getupbitmapBitmap():
0897     """Returns a wx.Bitmap of the up icon"""
0898     return wx.BitmapFromImage(getupbitmapImage())
0899 
0900 def getupbitmapImage():
0901     """Returns wx.Image of the up icon"""
0902     stream = cStringIO.StringIO(getupbitmapData())
0903     return wx.ImageFromStream(stream)
0904 
0905 def getdownbitmapData():
0906     """Returns raw data for the down icon"""
0907     return \
0908 '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00\x10\x08\x06\
0909 \x00\x00\x00w\x00}Y\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\
0910 \x00\x80IDATx\x9c\xc5\xd3\xd1\r\x80 \x0c\x04\xd0B\x1c\xe0\xf6\x9f\xb2\x1b\
0911 \xe8\x97\t\x91R\xda\x02\x95/!r\xf7bj\x01@\x7f\xae\xeb}`\xe6;\xbb\x1c@\xa9\
0912 \xed&\xbb\x9c\x88\xa8J\x87Y\xe5\x1d \x03\xf1\xcd\xef\x00\'\x11R\xae\x088\x81\
0913 \x18\xe5\r\x01;\x11Z\x8e\n\xd8\x81\x98\xdd\x9f\x02V\x10\x96{&@\x04a}\xdf\x0c\
0914 \xf0\x84z\xb0.\x80%\xdc\xfb\xa5\xdc\x00\xad$2+!\x80T\x16\x1d\xd40\xa0-]\xf9U\
0915 \x1f\xf8\xca\t\xael-\x16\x86\x00\x00\x00\x00IEND\xaeB`\x82' 
0916 
0917 def getdownbitmapBitmap():
0918     """Returns a wx.Bitmap of the down icon"""
0919     return wx.BitmapFromImage(getdownbitmapImage())
0920 
0921 def getdownbitmapImage():
0922     """Returns wx.Image of the down icon"""
0923     stream = cStringIO.StringIO(getdownbitmapData())
0924     return wx.ImageFromStream(stream)
0925 
0926 
0927  
0928 
0929 # If run by self, then is a nice demo
0930 
0931 if __name__=="__main__":
0932     class MainWindow(wx.Frame):
0933         def __init__(self, parent, id, title):
0934             wx.Frame.__init__(self, parent, id, title, size=(640,480),
0935                              style=wx.DEFAULT_FRAME_STYLE)
0936             #self.control=CalendarCell(self, 1) # test just the cell
0937             #hbs=wx.BoxSizer(wx.VERTICAL)
0938             self.control=Calendar(self)
0939             #hbs.Add(self.control, 1, wx.EXPAND)
0940             #self.SetSizer(hbs)
0941             self.Show(True)
0942     
0943     app=wx.PySimpleApp()
0944     frame=MainWindow(None, -1, "Calendar Test")
0945     if False: # change to True to do profiling
0946         import profile
0947         profile.run("app.MainLoop()", "fooprof")
0948     else:
0949         app.MainLoop()
0950 

Generated by PyXR 0.9.4