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