Module calendarcontrol
[hide private]
[frames] | no frames]

Source Code for Module calendarcontrol

  1  #!/usr/bin/env python 
  2   
  3  ### BITPIM 
  4  ### 
  5  ### Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com> 
  6  ### 
  7  ### This program is free software; you can redistribute it and/or modify 
  8  ### it under the terms of the BitPim license as detailed in the LICENSE file. 
  9  ### 
 10  ### $Id: calendarcontrol.py 4407 2007-09-25 20:39:48Z djpham $ 
 11   
 12  """A calendar control that shows several weeks in one go 
 13   
 14  The design is inspired by the Alan Cooper article U{http://www.cooper.com/articles/art_goal_directed_design.htm} 
 15  about goal directed design.  I also have to apologise for it not quite living up to that vision :-) 
 16   
 17  It is fairly feature complete and supports all sorts of interaction, scrolling and customization 
 18  of appearance""" 
 19   
 20  import wx 
 21  import wx.lib.rcsizer 
 22  import wx.calendar 
 23  import cStringIO 
 24  import calendar 
 25  import time 
 26  import widgets 
 27  import tipwin 
 28   
29 -class FontscaleCache(dict):
30 """A cache used internally to remember how much to shrink fonts by""" 31 # cache results of what the scale factor is to fit a number of lines in a space is
32 - def get(self, y, attr, numentries):
33 return dict.get(self, (y, id(attr), numentries), 1)
34 - def set(self, y, attr, numentries, scale):
35 self[(y, id(attr), numentries)]=scale
36 - def uncache(self, *args):
37 # clear out any cached attrs listed in args (eg when they are changed) 38 keys=self.keys() 39 l2=[id(x) for x in args] 40 for y, idattr, numentries in keys: 41 if idattr in l2: 42 del self[ (y, idattr, numentries) ]
43 44 thefontscalecache=FontscaleCache() 45
46 -class CalendarCellAttributes:
47 """A class represnting appearance attributes for an individual day. 48 49 You should subclass this if you wish to change the appearance from 50 the defaults"""
51 - def __init__(self):
52 # Set some defaults 53 #self.cellbackground=wx.TheBrushList.FindOrCreateBrush(wx.Colour(230,255,255), wx.SOLID) 54 self.cellbackground=wx.Brush(wx.Colour(197,255,255), wx.SOLID) 55 self.labelfont=wx.Font(14, wx.SWISS, wx.NORMAL, wx.NORMAL ) 56 self.labelforeground=wx.NamedColour("CORNFLOWER BLUE") 57 self.labelalign=wx.ALIGN_RIGHT 58 self.timefont=wx.Font(8, wx.SWISS, wx.NORMAL, wx.NORMAL ) 59 self.timeforeground=wx.NamedColour("ORCHID") 60 self.entryfont=wx.Font(9, wx.SWISS, wx.NORMAL, wx.NORMAL ) 61 self.entryforeground=wx.NamedColour("BLACK") 62 self.miltime=False 63 self.initdone=True
64
65 - def __setattr__(self, k, v):
66 self.__dict__[k]=v 67 if hasattr(self, 'initdone'): 68 thefontscalecache.uncache(self)
69
70 - def isrightaligned(self):
71 """Is the number representing the day right aligned within the cell? 72 73 @rtype: Bool 74 @return: True is it should be shown right aligned""" 75 return self.labelalign==wx.ALIGN_RIGHT
76
77 - def ismiltime(self):
78 """Are times shown in military (aka 24 hour) time? 79 80 @rtype: Bool 81 @return: True is militart/24 hour format should be used""" 82 return self.miltime
83
84 - def setforcellbackground(self, dc):
85 """Set the cell background attributes 86 87 Colour 88 @type dc: wx.DC""" 89 dc.SetBackground(self.cellbackground)
90
91 - def setforlabel(self, dc, fontscale=1):
92 """Set the attributes for the day label 93 94 Colour, font 95 @type dc: wx.DC 96 @param fontscale: You should multiply the font point size 97 by this number 98 @type fontscale: float 99 """ 100 101 dc.SetTextForeground(self.labelforeground) 102 return self.setscaledfont(dc, self.labelfont, fontscale)
103
104 - def setfortime(self,dc, fontscale=1):
105 """Set the attributes for the time of an event text 106 107 Colour, font 108 @type dc: wx.DC 109 @param fontscale: You should multiply the font point size 110 by this number 111 @type fontscale: float 112 """ 113 dc.SetTextForeground(self.timeforeground) 114 return self.setscaledfont(dc, self.timefont, fontscale)
115
116 - def setforentry(self, dc, fontscale=1):
117 """Set the attributes for the label of an event text 118 119 Colour, font 120 @type dc: wx.DC 121 @param fontscale: You should multiply the font point size 122 by this number 123 @type fontscale: float 124 """ 125 dc.SetTextForeground(self.entryforeground) 126 return self.setscaledfont(dc, self.entryfont, fontscale)
127
128 - def setscaledfont(self, dc, font, fontscale):
129 """Changes the in the device context to the supplied font suitably scaled 130 131 @type dc: wx.DC 132 @type font: wx.Font 133 @type fontscale: float 134 @return: Returns True if the scaling succeeded, and False if the font was already 135 too small to scale smaller (the smallest size will still have been 136 selected into the device context) 137 @rtype: Bool""" 138 if fontscale==1: 139 dc.SetFont(font) 140 return True 141 ps=int(font.GetPointSize()*fontscale) 142 if ps<2: 143 ps=2 144 f=wx.TheFontList.FindOrCreateFont(int(ps), font.GetFamily(), font.GetStyle(), font.GetWeight()) 145 dc.SetFont(f) 146 if ps==2: 147 return False 148 return True
149 150 151 # a hack - this used to be an instance, but wx 2.5 doesn't allow using brushes/pens etc until 152 # app instance is created 153 DefaultCalendarCellAttributes=None 154
155 -def GetCalendarCellAttributes(attr=None):
156 if attr is not None: 157 return attr 158 global DefaultCalendarCellAttributes 159 if DefaultCalendarCellAttributes is None: 160 DefaultCalendarCellAttributes=CalendarCellAttributes() 161 return DefaultCalendarCellAttributes
162
163 -class CalendarCell(wx.PyWindow):
164 """A control that is used for each day in the calendar 165 166 As the user scrolls around the calendar, each cell is updated with new dates rather 167 than creating new CalendarCell objects. Internally it uses a backing buffer so 168 that redraws are quick and flicker free.""" 169 170 fontscalecache=FontscaleCache() 171
172 - def __init__(self, parent, id, attr=DefaultCalendarCellAttributes, style=wx.SIMPLE_BORDER):
173 wx.PyWindow.__init__(self, parent, id, style=style|wx.WANTS_CHARS|wx.FULL_REPAINT_ON_RESIZE) 174 self.attr=GetCalendarCellAttributes(attr) 175 self.day=33 176 self.year=2033 177 self.month=3 178 self.buffer=None 179 self.needsupdate=True 180 self.entries=() 181 self._tipwindow=None 182 183 wx.EVT_PAINT(self, self.OnPaint) 184 wx.EVT_SIZE(self, self.OnSize) 185 wx.EVT_ENTER_WINDOW(self, self.OnEnterWindow) 186 wx.EVT_LEAVE_WINDOW(self, self.OnLeaveWindow) 187 self._timer=wx.Timer(self) 188 wx.EVT_TIMER(self, self._timer.GetId(), self.OnTimer) 189 self.OnSize(None)
190
191 - def DoGetBestSize(self):
192 return (10,10)
193
194 - def setdate(self, year, month, day):
195 """Set the date we are""" 196 self.year=year 197 self.month=month 198 self.day=day 199 self.needsupdate=True 200 self.Refresh(False)
201
202 - def setattr(self, attr):
203 """Sets what CalendarCellAtrributes we use for appearance 204 205 @type attr: CalendarCellAtrributes""" 206 self.attr=GetCalendarCellAttributes(attr) 207 self.needsupdate=True 208 self.Refresh(False)
209
210 - def setentries(self, entries):
211 """Sets the entries we will display 212 213 @type entries: list 214 @param entries: A list of entries. Format is ( ( hour, minute, description), (hour, minute, decription) ... ). hour is in 24 hour 215 """ 216 self.entries=entries 217 self.needsupdate=True 218 self.Refresh(False)
219
220 - def getdate(self):
221 """Returns what date we are currently displaying 222 223 @rtype: tuple 224 @return: tuple of (year, month, day)""" 225 return (self.year, self.month, self.day)
226
227 - def OnSize(self, evt=None):
228 """Callback for when we are resized""" 229 self.width, self.height = self.GetClientSizeTuple() 230 self.needsupdate=True 231 if evt is not None: 232 evt.Skip()
233 234
235 - def redraw(self):
236 """Causes a forced redraw into our back buffer""" 237 if self.buffer is None or \ 238 self.buffer.GetWidth()!=self.width or \ 239 self.buffer.GetHeight()!=self.height: 240 if self.buffer is not None: 241 del self.buffer 242 self.buffer=wx.EmptyBitmap(self.width, self.height) 243 244 mdc=wx.MemoryDC() 245 mdc.SelectObject(self.buffer) 246 self.attr.setforcellbackground(mdc) 247 mdc.Clear() 248 self.draw(mdc) 249 mdc.SelectObject(wx.NullBitmap) 250 del mdc
251
252 - def _tipstr(self):
253 # return a summary of events for displaying in a tooltip 254 _res=[] 255 lastap="" 256 for h,m,desc in self.entries: 257 if h is None: 258 _res.append('\t'+desc) 259 else: 260 _text, lastap=self._timestr(h, m, lastap) 261 _res.append('%s\t%s'%(_text, desc)) 262 return '\n'.join(_res)
263
264 - def OnPaint(self, _=None):
265 """Callback for when we need to repaint""" 266 if self.needsupdate: 267 self.needsupdate=False 268 self.redraw() 269 dc=wx.PaintDC(self) 270 dc.DrawBitmap(self.buffer, 0, 0, False)
271
272 - def _timestr(self, h, m, lastap=''):
273 text="" 274 if self.attr.ismiltime(): 275 ap="" 276 else: 277 ap="a" 278 if h>=12: ap="p" 279 h%=12 280 if h==0: h=12 281 if ap==lastap: 282 ap="" 283 else: 284 lastap=ap 285 if h<10: text+=" " 286 return (text+"%d:%02d%s" % (h,m,ap), lastap)
287
288 - def draw(self, dc):
289 """Draw ourselves 290 291 @type dc: wx.DC""" 292 293 # do the label 294 self.attr.setforlabel(dc) 295 w,h=dc.GetTextExtent(`self.day`) 296 x=1 297 if self.attr.isrightaligned(): 298 x=self.width-(w+5) 299 dc.DrawText(`self.day`, x, 0) 300 301 if len(self.entries)==0: 302 return 303 304 entrystart=h # +5 305 dc.DestroyClippingRegion() 306 dc.SetClippingRegion( 0, entrystart, self.width, self.height-entrystart) 307 308 fontscale=thefontscalecache.get(self.height-entrystart, self.attr, len(self.entries)) 309 iteration=0 310 # this loop scales the contents to fit the space available 311 # we do it as a loop because even when we ask for a smaller font 312 # after finding out that it was too big the first time, the 313 # smaller font may not be as small as we requested 314 while 1: 315 y=entrystart 316 iteration+=1 317 # now calculate how much space is needed for the time fields 318 self.attr.setfortime(dc, fontscale) 319 boundingspace=2 320 space,_=dc.GetTextExtent("i") 321 timespace,timeheight=dc.GetTextExtent("mm:mm") 322 if self.attr.ismiltime(): 323 ampm=0 324 else: 325 ampm,_=dc.GetTextExtent("a") 326 327 r=self.attr.setforentry(dc, fontscale) 328 if not r: iteration=-1 # font can't be made this small 329 _,entryheight=dc.GetTextExtent("I") 330 firstrowheight=max(timeheight, entryheight) 331 332 # Now draw each item 333 lastap="" 334 for h,m,desc in self.entries: 335 x=0 336 if h is not None: 337 self.attr.setfortime(dc, fontscale) 338 # bounding 339 x+=boundingspace # we don't draw anything yet 340 timey=y 341 if timeheight<firstrowheight: 342 timey+=(firstrowheight-timeheight)/2 343 text, lastap=self._timestr(h, m, lastap) 344 dc.DrawText(text, x, timey) 345 x+=timespace 346 if not self.attr.ismiltime: x+=ampm 347 348 self.attr.setforentry(dc, fontscale) 349 ey=y 350 if entryheight<firstrowheight: 351 ey+=(firstrowheight-entryheight)/2 352 dc.DrawText(desc, x, ey) 353 # that row is dealt with! 354 y+=firstrowheight 355 if iteration==1 and fontscale!=1: 356 # came from cache 357 break 358 if iteration==-1: 359 # update cache 360 thefontscalecache.set(self.height-entrystart, self.attr, len(self.entries), fontscale) 361 break # reached limit of font scaling 362 if iteration<10 and y>self.height: 363 dc.Clear() 364 # how much too big were we? 365 fontscale=fontscale*float(self.height-entrystart)/(y-entrystart) 366 # print iteration, y, self.height, fontscale 367 else: 368 thefontscalecache.set(self.height-entrystart, self.attr, len(self.entries), fontscale) 369 break
370
371 - def OnEnterWindow(self, _):
372 if self.entries: 373 self._timer.Start(1000, wx.TIMER_ONE_SHOT)
374 - def OnLeaveWindow(self, _):
375 self._timer.Stop()
376 - def OnTimer(self, _):
377 if not self.entries or wx.GetApp().critical.isSet(): 378 return 379 _rect=self.GetRect() 380 _x,_y=self.GetParent().ClientToScreen(_rect[:2]) 381 _rect.SetX(_x) 382 _rect.SetY(_y) 383 if self._tipwindow: 384 self._tipwindow.Destroy() 385 self._tipwindow=tipwin.TipWindow(self, self._tipstr(), 1024, _rect)
386
387 -class CalendarLabel(wx.PyWindow):
388 """The label window on the left of the day cells that shows the month with rotated text 389 390 It uses double buffering etc for a flicker free experience""" 391
392 - def __init__(self, parent, cells, id=-1):
393 wx.PyWindow.__init__(self, parent, id, style=wx.FULL_REPAINT_ON_RESIZE) 394 self.needsupdate=True 395 self.buffer=None 396 self.cells=cells 397 wx.EVT_PAINT(self, self.OnPaint) 398 wx.EVT_SIZE(self, self.OnSize) 399 self.setfont(wx.Font(20, wx.SWISS, wx.NORMAL, wx.BOLD )) 400 self.settextcolour(wx.NamedColour("BLACK")) 401 self.OnSize(None)
402
403 - def DoGetBestSize(self):
404 return (10,10)
405
406 - def OnSize(self, _=None):
407 self.width, self.height = self.GetClientSizeTuple() 408 self.needsupdate=True
409
410 - def OnPaint(self, _=None):
411 if self.needsupdate: 412 self.needsupdate=False 413 self.redraw() 414 dc=wx.PaintDC(self) 415 dc.DrawBitmap(self.buffer, 0, 0, False)
416
417 - def setfont(self, font):
418 self.font=font
419
420 - def settextcolour(self, colour):
421 self.colour=colour
422
423 - def changenotify(self):
424 self.needsupdate=True 425 self.Refresh()
426
427 - def redraw(self):
428 if self.buffer is None or \ 429 self.buffer.GetWidth()!=self.width or \ 430 self.buffer.GetHeight()!=self.height: 431 if self.buffer is not None: 432 del self.buffer 433 self.buffer=wx.EmptyBitmap(self.width, self.height) 434 435 mdc=wx.MemoryDC() 436 mdc.SelectObject(self.buffer) 437 mdc.SetBackground(wx.TheBrushList.FindOrCreateBrush(self.GetBackgroundColour(), wx.SOLID)) 438 mdc.Clear() 439 self.draw(mdc) 440 mdc.SelectObject(wx.NullBitmap) 441 del mdc
442
443 - def draw(self, dc):
444 # find the lines for each cell 445 row=0 446 while row<len(self.cells): 447 month=self.cells[row].month 448 endrow=row 449 for r2 in range(row+1,len(self.cells)): 450 if month==self.cells[r2].month: 451 endrow=r2 452 else: 453 break 454 # row is begining row, endrow is end, inclusive 455 456 # find the space available. we do lots of lovely math 457 # in order to translate the coordinates from the rows 458 # into our window 459 x=0 460 y=self.cells[row].GetPositionTuple()[1]-self.cells[0].GetPositionTuple()[1] 461 w=self.width 462 h=self.cells[endrow].GetPositionTuple()[1]+self.cells[endrow].GetRect().height \ 463 -self.cells[row].GetPositionTuple()[1] 464 465 466 dc.DestroyClippingRegion() 467 dc.SetClippingRegion(x,y,w,h) 468 dc.SetPen(wx.ThePenList.FindOrCreatePen("BLACK", 3, wx.SOLID)) 469 # draw line at top and bottom 470 if row!=0: 471 dc.DrawLine(x, y, x+w, y) 472 if endrow!=len(self.cells)-1: 473 dc.DrawLine(x, y+h, x+w, y+h) 474 month=calendar.month_name[month] 475 dc.SetFont(self.font) 476 dc.SetTextForeground(self.colour) 477 tw,th=dc.GetTextExtent(month) 478 # Now figure out where to draw it 479 if tw<h: 480 # it fits, so centre 481 dc.DrawRotatedText(month, w/2-th/2, y + h/2 + tw/2, 90) 482 else: 483 # it doesn't fit 484 if row==0: 485 # top one shows start of text 486 dc.DrawRotatedText(month, w/2-th/2, y + h -5, 90) 487 else: 488 # show end of text at bottom 489 dc.DrawRotatedText(month, w/2-th/2, y + 5 + tw, 90) 490 491 # Loop around 492 row=endrow+1
493 494
495 -class Calendar(wx.Panel, widgets.BitPimWidget):
496 """The main calendar control. 497 498 You should subclass this clas and need to 499 implement the following methods: 500 501 L{OnGetEntries} 502 L{OnEdit} 503 504 The following methods you may want to call at some point: 505 506 L{RefreshEntry} 507 L{RefreshAllEntries} 508 509 510 """ 511 512 # All the horrible date code is an excellent case for metric time! 513 ID_UP=wx.NewId() 514 ID_DOWN=wx.NewId() 515 ID_YEARBUTTON=wx.NewId() 516 ID_TODAYBUTTON=wx.NewId() 517 518 attrevenmonth=None 519 attroddmonth=None 520 attrselectedcell=None 521
522 - def _initvars(self):
523 # this is needed to avoid issues with the wx. library not being initialised 524 # as the module is imported. We initialise the values when the first 525 # calendar constructor is run 526 if Calendar.attrevenmonth is None: 527 Calendar.attrevenmonth=CalendarCellAttributes() 528 if Calendar.attroddmonth is None: 529 Calendar.attroddmonth=CalendarCellAttributes() 530 Calendar.attroddmonth.cellbackground=wx.TheBrushList.FindOrCreateBrush( wx.Colour(255, 255, 230), wx.SOLID) 531 if Calendar.attrselectedcell is None: 532 Calendar.attrselectedcell=CalendarCellAttributes() 533 Calendar.attrselectedcell.cellbackground=wx.TheBrushList.FindOrCreateBrush( wx.Colour(240,240,240), wx.SOLID) 534 Calendar.attrselectedcell.labelfont=wx.Font(17, wx.SWISS, wx.NORMAL, wx.BOLD ) 535 Calendar.attrselectedcell.labelforeground=wx.NamedColour("BLACK")
536
537 - def __init__(self, parent, rows=5, id=-1):
538 self._initvars() 539 wx.Panel.__init__(self, parent, id, style=wx.WANTS_CHARS|wx.FULL_REPAINT_ON_RESIZE) 540 sizer=wx.GridBagSizer() 541 self.upbutt=wx.BitmapButton(self, self.ID_UP, getupbitmapBitmap()) 542 sizer.Add(self.upbutt, flag=wx.EXPAND, pos=(0,1), span=(1,7)) 543 self.year=wx.Button(self, self.ID_YEARBUTTON, "2003") 544 sizer.Add(self.year, flag=wx.EXPAND, pos=(1,0)) 545 sizer.Add(wx.Button(self, self.ID_TODAYBUTTON, "Today"), flag=wx.EXPAND, pos=(0,0)) 546 p=1 547 calendar.setfirstweekday(calendar.SUNDAY) 548 for i in ( "Sun", "Mon", "Tue", "Wed" , "Thu", "Fri", "Sat" ): 549 sizer.Add( wx.StaticText( self, -1, i, style=wx.ALIGN_CENTER|wx.ALIGN_CENTER_VERTICAL), 550 flag=wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER_HORIZONTAL|wx.EXPAND, pos=(1,p)) 551 sizer.AddGrowableCol(p) 552 p+=1 553 self.numrows=rows 554 self.showrow=rows/2 555 self.rows=[] 556 for i in range(0, rows): 557 self.rows.append( self.makerow(sizer, i+2) ) 558 self.downbutt=wx.BitmapButton(self, self.ID_DOWN, getdownbitmapBitmap()) 559 sizer.Add(self.downbutt, flag=wx.EXPAND, pos=(2+rows, 0), span=(1,8)) 560 self.label=CalendarLabel(self, map(lambda x: x[0], self.rows)) 561 sizer.Add(self.label, flag=wx.EXPAND, pos=(2,0), span=(self.numrows,1)) 562 self.sizer=sizer 563 564 self.popupcalendar=PopupCalendar(self, self) 565 566 wx.EVT_BUTTON(self, self.ID_UP, self.OnScrollUp) 567 wx.EVT_BUTTON(self, self.ID_DOWN, self.OnScrollDown) 568 wx.EVT_BUTTON(self, self.ID_YEARBUTTON, self.OnYearButton) 569 wx.EVT_BUTTON(self, self.ID_TODAYBUTTON, self.OnTodayButton) 570 # grab key down from all children 571 map(lambda child: wx.EVT_KEY_DOWN(child, self.OnKeyDown), self.GetChildren()) 572 # and mousewheel 573 map(lambda child: wx.EVT_MOUSEWHEEL(child, self.OnMouseWheel), self.GetChildren()) 574 # grab left down, left dclick from all cells 575 for r in self.rows: 576 map(lambda cell: wx.EVT_LEFT_DOWN(cell, self.OnLeftDown), r) 577 map(lambda cell: wx.EVT_LEFT_DCLICK(cell, self.OnLeftDClick), r) 578 579 self.selectedcell=(-1,-1) 580 self.selecteddate=(-1,-1,-1) 581 582 self.showday(*time.localtime()[:3]+(self.showrow,)) 583 self.setday(*time.localtime()[:3]) 584 585 self.SetSizer(sizer) 586 sizer.Fit(self) 587 self.SetAutoLayout(True)
588
589 - def OnKeyDown(self, event):
590 key=event.GetKeyCode() 591 if key==wx.WXK_NEXT: 592 self.scrollby( (self.numrows-1)*7) 593 elif key==wx.WXK_PRIOR: 594 self.scrollby( (self.numrows-1)*-7) 595 elif key==wx.WXK_LEFT: 596 self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1], self.selecteddate[2]-1) ) 597 elif key==wx.WXK_RIGHT: 598 self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1], self.selecteddate[2]+1) ) 599 elif key==wx.WXK_UP: 600 self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1], self.selecteddate[2]-7) ) 601 elif key==wx.WXK_DOWN: 602 self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1], self.selecteddate[2]+7) ) 603 elif key==wx.WXK_HOME: # back a month 604 self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1]-1, self.selecteddate[2]) ) 605 elif key==wx.WXK_END: # forward a month 606 self.setday(*normalizedate( self.selecteddate[0], self.selecteddate[1]+1, self.selecteddate[2]) ) 607 # ::TODO:: activate edit code for return or space on a calendarcell 608 else: 609 event.Skip() # control can have it
610
611 - def OnMouseWheel(self, event):
612 delta=event.GetWheelDelta() 613 if delta==0: # as it does on linux 614 delta=120 615 lines=event.GetWheelRotation()/delta 616 self.scrollby(-7*lines)
617
618 - def OnLeftDown(self, event):
619 cell=event.GetEventObject() 620 self.setselection(cell.year, cell.month, cell.day)
621
622 - def OnLeftDClick(self,event):
623 cell=event.GetEventObject() 624 self.OnEdit(cell.year, cell.month, cell.day)
625
626 - def OnYearButton(self, event):
627 self.popupcalendar.Popup( * (self.selecteddate + (event,)) )
628
629 - def OnTodayButton(self, _):
630 self.setday(*time.localtime()[:3])
631
632 - def makerow(self, sizer, row):
633 res=[] 634 sizer.AddGrowableRow(row) 635 for i in range(0,7): 636 res.append( CalendarCell(self, -1) ) 637 sizer.Add( res[-1], flag=wx.EXPAND, pos=(row,i+1)) 638 return res
639
640 - def scrollby(self, amount):
641 assert abs(amount)%7==0 642 for row in range(0, self.numrows): 643 y,m,d=self.rows[row][0].getdate() 644 y,m,d=normalizedate(y,m,d+amount) 645 self.updaterow(row, y,m,d) 646 self.setselection(*self.selecteddate) 647 self.label.changenotify() 648 self.ensureallpainted()
649
650 - def ensureallpainted(self):
651 # doesn't return until cells have been painted 652 self.Update()
653
654 - def OnScrollDown(self, _=None):
655 # user has pressed scroll down button 656 self.scrollby(7)
657
658 - def OnScrollUp(self, _=None):
659 # user has pressed scroll up button 660 self.scrollby(-7)
661
662 - def setday(self, year, month, day):
663 # makes specified day be shown and selected 664 self.showday(year, month, day) 665 self.setselection(year, month, day)
666
667 - def showday(self, year, month, day, rowtoshow=-1):
668 """Ensures specified date is onscreen 669 670 @param rowtoshow: if is >=0 then it will be forced to appear in that row 671 """ 672 # check first cell 673 y,m,d=self.rows[0][0].year, self.rows[0][0].month, self.rows[0][0].day 674 if rowtoshow==-1: 675 if year<y or (year<=y and month<m) or (year<=y and month<=m and day<d): 676 rowtoshow=0 677 # check last cell 678 y,m,d=self.rows[-1][-1].year, self.rows[-1][-1].month, self.rows[-1][-1].day 679 if rowtoshow==-1: 680 if year>y or (year>=y and month>m) or (year>=y and month>=m and day>d): 681 rowtoshow=self.numrows-1 682 if rowtoshow!=-1: 683 d=calendar.weekday(year, month, day) 684 d=(d+1)%7 685 686 d=day-d # go back to begining of week 687 d-=7*rowtoshow # then begining of screen 688 y,m,d=normalizedate(year, month, d) 689 for row in range(0,self.numrows): 690 self.updaterow(row, *normalizedate(y, m, d+7*row)) 691 self.label.changenotify() 692 self.ensureallpainted()
693
694 - def isvisible(self, year, month, day):
695 """Tests if the date is visible to the user 696 697 @rtype: Bool 698 """ 699 y,m,d=self.rows[0][0].year, self.rows[0][0].month, self.rows[0][0].day 700 if year<y or (year<=y and month<m) or (year<=y and month<=m and day<d): 701 return False 702 y,m,d=self.rows[-1][-1].year, self.rows[-1][-1].month, self.rows[-1][-1].day 703 if year>y or (year>=y and month>m) or (year>=y and month>=m and day>d): 704 return False 705 return True
706
707 - def RefreshEntry(self, year, month, day):
708 """Causes that date's entries to be refreshed. 709 710 Call this if you have changed the data for one day. 711 Note that your OnGetEntries will only be called if the date is 712 currently visible.""" 713 if self.isvisible(year,month,day): 714 # ::TODO:: find correct cell and only update that 715 self.RefreshAllEntries()
716
717 - def RefreshAllEntries(self):
718 """Call this if you have completely changed all your data. 719 720 OnGetEntries will be called for each visible day.""" 721 722 for row in self.rows: 723 for cell in row: 724 cell.setentries(self.OnGetEntries(cell.year, cell.month, cell.day))
725 726
727 - def setselection(self, year, month, day):
728 """Selects the specifed date if it is visible""" 729 self.selecteddate=(year,month,day) 730 d=calendar.weekday(year, month, day) 731 d=(d+1)%7 732 for row in range(0, self.numrows): 733 cell=self.rows[row][d] 734 if cell.year==year and cell.month==month and cell.day==day: 735 self._unselect() 736 self.rows[row][d].setattr(self.attrselectedcell) 737 self.selectedcell=(row,d) 738 self.ensureallpainted() 739 return
740
741 - def _unselect(self):
742 if self.selectedcell!=(-1,-1): 743 self.updatecell(*self.selectedcell) 744 self.selectedcell=(-1,-1)
745
746 - def updatecell(self, row, column, y=-1, m=-1, d=-1):
747 if y!=-1: 748 self.rows[row][column].setdate(y,m,d) 749 if self.rows[row][column].month%2: 750 self.rows[row][column].setattr(self.attroddmonth) 751 else: 752 self.rows[row][column].setattr(self.attrevenmonth) 753 if y!=-1 and row==0 and column==0: 754 self.year.SetLabel(`y`) 755 if y!=-1: 756 self.rows[row][column].setentries(self.OnGetEntries(y,m,d))
757
758 - def updaterow(self, row, y, m, d):
759 daysinmonth=monthrange(y, m) 760 for c in range(0,7): 761 self.updatecell(row, c, y, m, d) 762 if d==daysinmonth: 763 d=1 764 m+=1 765 if m==13: 766 m=1 767 y+=1 768 daysinmonth=monthrange(y, m) 769 else: 770 d+=1
771 772 # The following methods should be implemented in derived class. 773 # Implementations here are to make it be a nice demo if not subclassed 774
775 - def OnGetEntries(self, year, month, day):
776 """Return a list of entries for the specified y,m,d. 777 778 B{You must implement this method in a derived class} 779 780 The format is ( (hour,min,desc), (hour,min,desc)... ) Hour 781 should be in 24 hour format. You should sort the entries. 782 783 Note that Calendar does not cache any results so you will be 784 asked for the same dates as the user scrolls around.""" 785 786 return ( 787 (1, 88, "Early morning"), 788 (10,15, "Some explanatory text" ), 789 (10,30, "It is %04d-%02d-%02d" % (year,month,day)), 790 (11,11, "Look at me!"), 791 (12,30, "More text here"), 792 (15,30, "A very large amount of text that will never fit"), 793 (20,30, "Evening drinks"), 794 )
795
796 - def OnEdit(self, year, month, day):
797 """The user wishes to edit the entries for the specified date 798 799 B{You should implement this method in a derived class} 800 """ 801 print "The user wants to edit %04d-%02d-%02d" % (year,month,day)
802 803
804 -class PopupCalendar(wx.Dialog):
805 """The control that pops up when you click the year button"""
806 - def __init__(self, parent, calendar, style=wx.SIMPLE_BORDER):
807 wx.Dialog.__init__(self, parent, -1, '', style=wx.STAY_ON_TOP|style) 808 self.calendar=calendar 809 self.control=wx.calendar.CalendarCtrl(self, 1, style=wx.calendar.CAL_SUNDAY_FIRST, pos=(0,0)) 810 sz=self.control.GetBestSize() 811 self.SetSize(sz) 812 wx.calendar.EVT_CALENDAR(self, self.control.GetId(), self.OnCalSelected)
813
814 - def Popup(self, year, month, day, event):
815 d=wx.DateTimeFromDMY(day, month-1, year) 816 self.control.SetDate(d) 817 btn=event.GetEventObject() 818 pos=btn.ClientToScreen( (0,0) ) 819 sz=btn.GetSize() 820 self.Move( (pos[0], pos[1]+sz.height ) ) 821 self.ShowModal()
822
823 - def OnCalSelected(self, evt):
824 dt=evt.GetDate() 825 self.calendar.setday(dt.GetYear(), dt.GetMonth()+1, dt.GetDay()) 826 self.calendar.ensureallpainted() 827 self.EndModal(1)
828 829 830 _monthranges=[0, 31, -1, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 831
832 -def monthrange(year, month):
833 """How many days are in the specified month? 834 835 @rtype: int""" 836 if month==2: 837 return calendar.monthrange(year, month)[1] 838 return _monthranges[month]
839
840 -def normalizedate(year, month, day):
841 """Return a valid date (and an excellent case for metric time) 842 843 And example is the 32nd of January is first of Feb, or Jan -2 is 844 December 29th of previous year. You should call this after doing 845 arithmetic on dates (for example you can just subtract 14 from the 846 current day and then call this to get the correct date for two weeks 847 ago. 848 849 @rtype: tuple 850 @return: (year, month, day) 851 """ 852 853 while day<1 or month<1 or month>12 or (day>28 and day>monthrange(year, month)): 854 if day<1: 855 month-=1 856 if month<1: 857 month=12 858 year-=1 859 num=monthrange(year, month) 860 day=num+day 861 continue 862 if day>28 and day>monthrange(year, month): 863 num=calendar.monthrange(year, month)[1] 864 month+=1 865 if month>12: 866 month=1 867 year+=1 868 day=day-num 869 continue 870 if month<1: 871 year-=1 872 month=month+12 873 continue 874 if month>12: 875 year+=1 876 month-=12 877 continue 878 assert False, "can't get here" 879 880 return year, month, day
881 882 # Up and down bitmap icons 883
884 -def getupbitmapData():
885 """Returns raw data for the up icon""" 886 return \ 887 '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00\x10\x08\x06\ 888 \x00\x00\x00w\x00}Y\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\ 889 \x00}IDATx\x9c\xbd\xd5K\x0e\xc0 \x08\x04P\xe0\x04\xdc\xff\x94\xdc\xa0\xdd6\ 890 \xad\xca\xf0)n\\\xa83/1FVU\xca\x0e3\xbbT\x95\xd3\x01D$\x95\xf2\xe7<\nx\x97V\ 891 \x10a\xc0\xae,\x8b\x08\x01\xbc\x92\x0c\x02\x06\xa0\xe1Q\x04\x04\x88\x86F\xf6\ 892 \xbb\x80\xec\xdd\xa2\xe7\x8e\x80\xea\x13C\xceo\x01\xd5r4g\t\xe8*G\xf2>\x80\ 893 \xeer/W\x90M\x7f"\xe4\xb48\x81\x90\xc9\xf2\x15\x82+\xdfq\xc7\xb8\x01;]o#\xdc\ 894 D \x03\x00\x00\x00\x00IEND\xaeB`\x82'
895
896 -def getupbitmapBitmap():
897 """Returns a wx.Bitmap of the up icon""" 898 return wx.BitmapFromImage(getupbitmapImage())
899
900 -def getupbitmapImage():
901 """Returns wx.Image of the up icon""" 902 stream = cStringIO.StringIO(getupbitmapData()) 903 return wx.ImageFromStream(stream)
904
905 -def getdownbitmapData():
906 """Returns raw data for the down icon""" 907 return \ 908 '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00 \x00\x00\x00\x10\x08\x06\ 909 \x00\x00\x00w\x00}Y\x00\x00\x00\x04sBIT\x08\x08\x08\x08|\x08d\x88\x00\x00\ 910 \x00\x80IDATx\x9c\xc5\xd3\xd1\r\x80 \x0c\x04\xd0B\x1c\xe0\xf6\x9f\xb2\x1b\ 911 \xe8\x97\t\x91R\xda\x02\x95/!r\xf7bj\x01@\x7f\xae\xeb}`\xe6;\xbb\x1c@\xa9\ 912 \xed&\xbb\x9c\x88\xa8J\x87Y\xe5\x1d \x03\xf1\xcd\xef\x00\'\x11R\xae\x088\x81\ 913 \x18\xe5\r\x01;\x11Z\x8e\n\xd8\x81\x98\xdd\x9f\x02V\x10\x96{&@\x04a}\xdf\x0c\ 914 \xf0\x84z\xb0.\x80%\xdc\xfb\xa5\xdc\x00\xad$2+!\x80T\x16\x1d\xd40\xa0-]\xf9U\ 915 \x1f\xf8\xca\t\xael-\x16\x86\x00\x00\x00\x00IEND\xaeB`\x82'
916
917 -def getdownbitmapBitmap():
918 """Returns a wx.Bitmap of the down icon""" 919 return wx.BitmapFromImage(getdownbitmapImage())
920
921 -def getdownbitmapImage():
922 """Returns wx.Image of the down icon""" 923 stream = cStringIO.StringIO(getdownbitmapData()) 924 return wx.ImageFromStream(stream)
925 926 927 928 929 # If run by self, then is a nice demo 930 931 if __name__=="__main__":
932 - class MainWindow(wx.Frame):
933 - def __init__(self, parent, id, title):
934 wx.Frame.__init__(self, parent, id, title, size=(640,480), 935 style=wx.DEFAULT_FRAME_STYLE) 936 #self.control=CalendarCell(self, 1) # test just the cell 937 #hbs=wx.BoxSizer(wx.VERTICAL) 938 self.control=Calendar(self) 939 #hbs.Add(self.control, 1, wx.EXPAND) 940 #self.SetSizer(hbs) 941 self.Show(True)
942 943 app=wx.PySimpleApp() 944 frame=MainWindow(None, -1, "Calendar Test") 945 if False: # change to True to do profiling 946 import profile 947 profile.run("app.MainLoop()", "fooprof") 948 else: 949 app.MainLoop() 950