Extended Calendar Control (Phoenix)

Keywords : Calendar, Custom.


Demonstrating :

Tested py3.8.10, wx4.x and Linux.

Tested py3.10.5, wx4.2.1 and Win11.

Are you ready to use some samples ? ;)

Test, modify, correct, complete, improve and share your discoveries ! (!)


First example

As the source is too heavy for wxpywiki, please use the link to download all useful files :

Latest version here : https://discuss.wxpython.org/t/extendedcalendarctrl/36680

img_sample_one.png img_sample_two.png img_sample_three.png img_sample_four.png

   1 """
   2     ExtendedCalendarCtrl.py
   3 
   4     A custom class using wx.adv.GenericCalendarCtrl with the ability to customise the calendar and the output format,
   5      based on some of the attributes of minidatepicker.py
   6     Works with wx.DateTime or python datetime values
   7     Uses wx.adv.GenericCalendarCtrl
   8     Uses wx.Locale to enable different languages for the calendar
   9 
  10     An attempt has been made to allow days marked with attributes denoting Holiday, Marked and Restricted
  11      to live with each other
  12 
  13     Holidays as far as the wx.CalendarCtrl is concerned seems to revolve round the weekends, for the most part,
  14      so for the purposes of this ExtendedCalendarCtrl, they are referred to as weekends and actual holidays,
  15      either defined by you or the python holidays module, are the actual holidays.
  16      The weekends are still marked by the attribute SetHoliday(True) because I cannot override it.
  17 
  18     Dates can be marked, restricted, defined as holidays and have notes.
  19      Marked Dates, Restricted dates and notes can be defined as a simple date or use more advanced rules for example
  20      the 3rd Friday of the month, every Tuesday of the month.or every 3rd Friday of every month
  21      Note: they can be year specific or every year
  22 
  23     Marked dates are marked with a Border, either Square or Oval, with optional Foreground/Background colours.
  24     Holidays are normally highlighted with a different Foreground/Background colour but only if you are Showing Holidays
  25     Restricted dates are marked using an Italic StrikeThrough font
  26 
  27     Defined Holidays can be year specific or fixed holidays i.e. every year on that Month/Day
  28 
  29     Public Holidays rely on the python 'holidays' module being available (pip install --upgrade holidays)
  30         public holidays are automatically entered into the Notes for you and the holiday module does make different
  31         different languages available, sometimes.
  32     You may add in more than one region's public holidays and they will be denoted by the country code
  33      and region code if appropriate.
  34 
  35     Notes are date specific or every year and in addition can follow the rules for Marked and Restricted dates, namely:
  36             All of a specified week day in a month;
  37             The specified occurrence of a weekday in a month e.g. 3rd Tuesday or last Friday of the month
  38             The specified occurrence of a weekday in every month e.g. 3rd Tuesday or last Friday of every month
  39             The last day of the month
  40             The last weekday of the month
  41         Dates with Notes are treated like Marked i.e. they can have a border, a text and background colour of their own
  42 
  43     Navigation:
  44      The Arrow keys will navigate the calendar
  45      The PageUp/PageDown keys will retreat and advance the month respectively, as will MouseScrollUp and MouseScrollDown
  46      The Home and End keys jump to the First and Last day of the month, respectively.
  47      A right click on the calendar, will display All the notes for the month.
  48      The F1 key is the keyboard equivalent of Right Click and will display All the notes for the month.
  49      Date ToolTips will be displayed as and when appropriate, depending of the position in the calendar and settings
  50      The F2 key opens a Year Calendar for reference.
  51 
  52     CalendarCtrl(parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
  53                  style=0, name="ExtendedCalCtrl", date=0, formatter='', calendar_style=None, calendar_locale=None)
  54 
  55         @param parent:   Parent window. Must not be None.
  56         @param id:       identifier. A value of -1 indicates a default value.
  57         @param pos:      MiniDatePicker position. If the position (-1, -1) is specified
  58                          then a default position is chosen.
  59         @param size:     If the default size (-1, -1) is specified then a default size is calculated.
  60                          Size should be able to accomodate the specified formatter string + button
  61         @param style:    The style of the wx.Panel that the calendarctrl is set in.
  62         @param name:     Widget name.
  63         @param date:     Initial date (an invalid date = today)
  64         @param formatter A date formatting string in the form of a lambda function
  65                          The formatter will be called with a wx.DateTime thus we can use .Format()
  66                           the wxPython version of the standard ANSI C strftime
  67                          default lambda dt: dt.FormatISODate() = ISO 8601 format "YYYY-MM-DD".
  68                          or a lambda function with a format string e.g.:
  69                             lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
  70                          e.g.:
  71                             format = lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
  72                             format = lambda dt: (f'{dt.Format("%A %d %B %Y")}')
  73                          or
  74                             fmt = "%Y/%m/%d"
  75                             format = lambda dt: (dt.Format(fmt))
  76                             format = lambda dt: (dt.Format("%Y/%m/%d"))
  77                             format = lambda dt: (dt.FormatISODate())
  78                         for those who prefer strftime formatting:
  79                             format = (lambda dt: (f'{wx.wxdate2pydate(dt).strftime("%A %d-%B-%Y")}'))
  80         @param calendar_style
  81                         Sets the initial calendar style
  82                         Useful for styles not changeable after creation e.g. wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION
  83         @param calendar_locale
  84                         Controls the Language used in the Calendar either None (default)
  85                         or a wx.Locale language e.g, wx.LANGUAGE_SPANISH or wx.LANGUAGE_GERMAN etc
  86 
  87 
  88     Events: EVT_DATE_CHANGED A date change occurred in the control
  89             existing wx.adv.GenericCalendarCtrl events are skipped, so you may bind to them if you wish
  90 
  91     Event Functions:
  92         GetValue()          Returns formatted date in the event as a string
  93 
  94         GetDate()           Returns wxDateTime date in the event, with all of its attendant functions
  95 
  96         GetNote()           Retrieve the note string of the date
  97 
  98         IsMarked()          True or False if the date is marked
  99 
 100         IsHoliday()         True or False if the date is a holiday
 101 
 102         IsPublicHoliday()   Retrieve the public holiday value of the date
 103 
 104         GetDateTime()       Returns python datetime of date in the event
 105 
 106         GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for current date
 107 
 108 
 109     CalendarCtrl Functions:
 110 
 111         GetValue()          Returns formatted date in the event as a string
 112 
 113         GetDate()           Returns wxDateTime date in the control
 114 
 115         GetDateTimeValue()  Returns python datetime of date in the control
 116 
 117         GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected date
 118 
 119         GetLocale()         Returns tuple of current language code and encoding
 120 
 121         GetNotes()          Returns the current Notes dictionary
 122 
 123         GetHolidays()       Returns the current Holidays dictionary
 124 
 125         GetRestrictedDate() Returns the current RestrictedDate dictionary
 126 
 127         GetMarkDates()      Returns the current MarkDates dictionary
 128 
 129         TriggerDateEvent(date=None)
 130                             Triggers a calendar event for the date specified or current date if no date supplied
 131                             The date passed can be a wx.Date or a datetime.datetime date
 132 
 133         SetValue(date)      Sets the date in the control
 134                             expects a wx.DateTime, a python datetime datetime or a timestamp
 135                             Any invalid date defaults to wx.DateTime.Today()
 136 
 137         SetFormatter(formatter) Date format in the form of a lambda
 138             default:    lambda dt: dt.FormatISODate()
 139 
 140         SetCalendarStyle(style)
 141             wx.adv.CAL_SUNDAY_FIRST: Show Sunday as the first day in the week
 142             wx.adv.CAL_MONDAY_FIRST: Show Monday as the first day in the week
 143             wx.adv.CAL_SHOW_HOLIDAYS: Highlight holidays in the calendar (only generic)
 144             wx.adv.CAL_NO_YEAR_CHANGE: Disable the year changing (deprecated, only generic)
 145             wx.adv.CAL_NO_MONTH_CHANGE: Disable the month (and, implicitly, the year) changing
 146             wx.adv.CAL_SHOW_SURROUNDING_WEEKS: Show the neighbouring weeks in the previous and next months
 147             wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION: more compact, style for the month and year selection controls.
 148             wx.adv.CAL_SHOW_WEEK_NUMBERS
 149 
 150             Note: Some styles are Not changeable after the initial creation of the calendar
 151 
 152         SetCalendarHighlights(colFg, colBg)     Colours to mark the currently selected date
 153 
 154         SetCalendarHolidayColours(colFg, colBg) Colours to mark Holidays defaults None and Yellow
 155 
 156         SetWeekendColours(colFg, colBg)         Colours to mark weekends
 157 
 158         SetCalendarHeaders(colFg, colBg)        Colours for the calendar header area
 159 
 160         SetCalendarFg(colFg)    Set Calendar ForegroundColour
 161 
 162         SetCalendarBg(colBg)    Set Calendar BackgroundColour
 163 
 164         SetCalendarFont(font=None)  Set font of the calendar to a wx.Font
 165                 Alter the font family, weight, size, etc
 166 
 167         SetCalendarMarkDates(markdates = {}) Mark dates with a Border
 168                 A dictionary containing year and month tuple as the key and a list of days for the values to be marked
 169                 e.g.
 170                     {
 171                      (2023, 7) : [2,5,7,11,30],
 172                      (2023, 8) : [7,12,13,20,27],
 173                      (2023, 9) : [1,27]
 174                     }
 175 
 176                 Setting the year to zero in the key, will mark these dates every year
 177                 Setting the month to zero, will mark these dates in every month
 178 
 179                 Values of less than 0 indicate not a specific date but a day: -1 Monday, -2 Tuesday, ... -7 Sunday
 180                  allowing you to mark all Mondays and Fridays in the month of January e.g {(2023, 1) : [-1, -5]}
 181                  You may include a mixture of negative and positive numbers (days and specific dates)
 182 
 183                 Negative values beyond that indicate the nth weekday, (the indexing is a bit confusing because it's off by 1
 184                  the first digit represents the day and the 2nd digit represents the occurrence i.e.
 185 
 186                 -11, 1st Monday | -12, 2nd Monday | -13, 3rd Monday | -14, 4th Monday | -15, 5th or Last Monday
 187                 -21, 1st Tuesday | -22, 2nd Tuesday | -23, 3rd Tuesday | -24, 4th Tuesday | -25, 5th or Last Tuesday
 188                 ..............................................................................................................
 189                 -71, 1st Sunday | -72, 2nd Sunday | -73, 3rd Sunday | -74, 4th Sunday | -75, 5th or Last Sunday
 190 
 191                 This way all the individual days are grouped together.
 192                 If the 5th occurrence of a weekday doesn't exist, the last occurrence of the weekday is substituted.
 193 
 194                 -99 Stands for the last day of the month
 195                 -98 is for the last weekday of the month
 196 
 197         SetCalendarMarkBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.NullColour, colFg=wx.NullColour, colBg=wx.NullColour)
 198                 Defines the border type to mark dates wx.adv.CAL_BORDER_SQUARE (default)
 199                 a border colour e.g. wx.NullColour (Default), wx.RED  or a hex value '#800080' etc
 200                 a foreground colour
 201                 and a background colour
 202                 Valid border values are:
 203                     wx.adv.CAL_BORDER_NONE      - 0
 204                     wx.adv.CAL_BORDER_SQUARE    - 1
 205                     wx.adv.CAL_BORDER_ROUND     - 2
 206 
 207         SetCalendarHolidays(holidays = {})
 208                 A dictionary containing year and month tuple as the key and a list of days for the values e.g.
 209                     {
 210                      (2023, 1) : [1,],
 211                      (2023, 7) : [1,30],
 212                      (2023, 8) : [7,15,27],
 213                      (2023, 12) : [25,26]
 214                     }
 215 
 216                 Holidays can also be 'fixed' Holidays occurring every year on the same day by setting the year to zero in the key
 217                 e.g.
 218                     {
 219                      (0, 1) : [1,],                            # January 1st is a Holiday every year
 220                      (2023, 7) : [1,30],
 221                      (2023, 8) : [7,15,27],
 222                      (0, 12) : [25,26]                         # Christmas Day and Boxing Day are Holidays every year
 223                     }
 224 
 225         SetCalendarNotes(notes = {})
 226                 A dictionary containing a year, month, day tuple as the key and a string for the note e.g.
 227                     {
 228                      (2023, 1, 1) : "New Year's Day",
 229                      (2023, 12, 25) : "Christmas Day"
 230                     }
 231 
 232                 Like Holidays, Notes can be assigned to a specific day every year, by setting the year to zero
 233                 Setting the month to zero, will set the note on this date in every month
 234                     {
 235                      (0, 1, 1) : "New Year's Day",
 236                      (0, 12, 25) : "Christmas Day",
 237                      (2023, 0, -53) : "Pay day"                 # Every 3rd Friday in every month in 2023
 238                     }
 239 
 240                 To compliment Marked Dates and Restricted Dates, notes can also be assigned a negative day following the
 241                  the same pattern as Marked Dates and Restricted Dates.
 242                 Allowing you to match Notes with Marked Dates and Restricted Dates.
 243 
 244                     {
 245                      (0, 1, -11) : "The 1st Monday of January/the year",
 246                      (0, 1, -35) : "The last Wednesday of January",
 247                      (0, 2, -5)  : "Every Friday in February"
 248                     }
 249 
 250                 If you set Public Holidays, they are enter automatically into the notes, marked with a leading asterix (*).
 251 
 252                 Notes are displayed as a ToolTip, when the day is hovered over or Right clicked
 253                  or if the mouse is over the calendar or the Arrow keys are used to navigate the calendar to that day.
 254 
 255                 A right click on the calendar, will display All the notes for the month, in a popup menu.
 256                 Pressing F1 will do the same.
 257 
 258         SetCalendarNotesBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.GREEN, colFg=wx.NullColour, colBg=wx.NullColour))
 259                 Defines the border type to dates with notes wx.adv.CAL_BORDER_SQUARE (default)
 260                 a border colour e.g. wx.GREEN (Default), wx.RED  or a hex value '#800080' etc
 261                 a foreground colour
 262                 and a background colour
 263                 Valid border values are:
 264                     wx.adv.CAL_BORDER_NONE      - 0
 265                     wx.adv.CAL_BORDER_SQUARE    - 1
 266                     wx.adv.CAL_BORDER_ROUND     - 2
 267 
 268         SetCalendarRestrictDates(rdates = {})
 269                 A dictionary containing a year and month tuple as the key and a list of days, for the days that
 270                  are Not selectable within that year/month
 271                  e.g.
 272                     {
 273                      (2023, 1) : [1,15],
 274                      (2023, 3) : [1,15],
 275                      (2023, 5) : [1,15],
 276                      (2023, 7) : [1,15,23],
 277                      (2023, 9) : [1,15],
 278                      (2023, 11) : [1,15]
 279                     }
 280 
 281                 All dates in the 'restricted' dictionary use an Italic StruckThrough font and cannot be selected
 282 
 283                 See SetCalendarMarkDates for the ability to use negative values to calculate dictionary values to restrict
 284                 more complicated entries like All Mondays or the 2nd and 4th Tuesday for example, by using negative values.
 285 
 286         SetCalendarDateRange(lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime)
 287                 Either 2 wx.DateTime values to restrict the selectable dates
 288                 or just a lower date or just an upper date
 289                 (**** Remember **** wx.DateTime months start from 0 which can wildly confusing ****)
 290                 Returns False if the dates are not wx.DateTime objects
 291                 wx.DefaultDateTime equals no date selected.
 292 
 293                 Dates outside of the range will display an "Out of Range" ToolTip, with the defined range.
 294 
 295         SetCalendarOnlyWeekDays(boolean) Default False
 296                 If set only weekdays are selectable. weekends and holidays use an Italic StruckThrough font and cannot be selected
 297                 Holidays are treated as Not a weekday i.e. no work
 298 
 299         AddPublicHolidays(country='', subdiv='', language='', replace=False) Default blank, blank, blank, False
 300                 Only available if the python 'holidays' module was successfully imported
 301                 Currently supports 134 country codes using country ISO 3166-1 alpha-2 codes and the optional subdivision
 302                  (state, region etc) using ISO 3166-2 codes.
 303                 Language must be an ISO 639-1 (2-letter) language code, except the ubiquitous 'en_US'.
 304                  If the language translation is not supported the original holiday names are returned.
 305                 The replace parameter if set to True, will create a new set of public holidays, rather than adding
 306                  to the existing set. Aimed at replacing public holidays, based on circumstances.
 307 
 308                 For details: https://python-holidays.readthedocs.io/en/latest/
 309                  (or the file 'python-holidays — holidays documentation.html' supplied with this program)
 310                 e.g.
 311                     country='ES'                    Spain
 312                     country='ES' and subdiv='AN'    Spain, Andalucia
 313                     country='GB' and subdiv='ENG'   Great Britain, England
 314                     country='US' and subdiv='SC'    USA, South Carolina
 315 
 316                 function returns True if successful, for an existing country and subdivision (if supplied)
 317                  or False if there was an error
 318 
 319                 This function can be called multiple times, once for each country or region in a country
 320                  that you wish marked on the calendar, unless the replace parameter is set to True.
 321                 The first call sets the primary holiday region.
 322                 May be useful if you are operating in more than one geographical area, with differing holidays,
 323                  or you may wish to display the holidays once in the original language and again in English.
 324                  e.g.
 325                     AddPublicHolidays(country="DE", subdiv="ST", language=())         # Germany region Lower Saxony
 326                     AddPublicHolidays(country="DE", subdiv="ST", language=('en_US'))  # In English
 327 
 328         LoadDataFrom(CalSaveFile=None)  - defining the path and file name to use to load predefined calendar events
 329 
 330         SaveData(CalSaveFile=None)      - defining the path and file name to use to save calendar events
 331                                            if the filename is not present the filename used to load from will be used.
 332 
 333 
 334 Author:     J Healey
 335 Created:    16/09/2023
 336 Copyright:  J Healey - 2023
 337 License:    GPL 3 or any later version
 338 Email:      <rolfofsaxony@gmx.com>
 339 Version     2.0
 340 
 341 Changelog:
 342 
 343 Version 2   Fixes a bug in the calendar date range function. by insisting the dates passed into
 344             function SetCalendarDateRange be wx.DateTime format i.e, the month numbers start from 0
 345             So January is 0 and December is 11.
 346             The bug showed up when inputting 12 for December and it returns October rather than reporting an error,
 347              I've no idea why!
 348             >>> wx.DateTime(24, 12, 2023)
 349                 <wx.DateTime: "Tue Oct 24 00:00:00 2023">
 350 
 351             Adds the ability to load and save the Calendar's Holidays, Notes, Restricted, Marked Dictionaries etc
 352             to file, removing the need to load them separately, every time.
 353             This allows for predefined calendar events to be loaded based on various criteria
 354             It also means that items can be added to the dictionaries seperately by the user and those items
 355             will be retained for each user, by basing the CalSaveFile name on something unique to the user.
 356             This is a simplistic step before you decide to hold these items and load them from a database,
 357             which would be the proper way of doing it.
 358 
 359             To enable this functionality the python module 'ast' (abstract syntax tree) is used
 360             Why ast and not pickle or json, because pickle is not human readable and json struggles with
 361              tuple keys to dictionaries.
 362             I wanted human readable and editable files that can be simply written and read, without a
 363              plethora of type fiddling. The ast module, provided a round peg in a round hole solution.
 364             If there are drawbacks to this approach, I'm unaware of them.
 365 
 366             The text file is a simple text line file, with 1 entry per item e.g.
 367 
 368             {(0, 1, 1): "New Year's Day\n2nd Note", (0, 1, -11): 'First Monday of the year', (0, 1, -23): '3rd Tuesday of the year', (0, 1, -99): 'Last day of January', (0, 1, -35): 'The last Wednesday of January', (0, 2, 1): '1st February', (2023, 2, -5): 'Every Friday in February', (2023, 8, 7): 'Marked for no reason whatsoever', (2023, 8, 15): 'A holiday August 15 2023', (2023, 8, 20): 'Marked for reason X', (0, 9, -98): 'Last weekday of September', (0, 12, 25): 'Merry Christmas!', (0, 2, -99): 'Last day of February'}
 369             {(0, 1): [1], (0, 2): [1], (2023, 8): [15], (0, 12): [25, 26]}
 370             ['DE', 'ST', '']
 371             [['GB', 'ENG', ''], ['ES', 'AN', '']]
 372             {}
 373             {}
 374             [[], []]
 375 
 376             These 7 entries represent the Notes, defined holidays, public holidays, additional public holidays,
 377              restricted dates, marked dates and calendar date range, respectively and must be in that order
 378              and that number.
 379              Even if there no entry, it must be represented as an empty structure, be that a dictionary
 380              a list or list of lists.
 381 
 382             The additional functions are:
 383                 LoadDataFrom(CalSaveFile=None)  - defining the path and file name to use
 384                 SaveData(CalSaveFile=None)      - defining the path and file name to use
 385                                                   if the filename is not present the filename used
 386                                                   to load from will be used.
 387 
 388 """
 389 
 390 import wx
 391 import wx.adv
 392 import datetime
 393 import calendar
 394 try:
 395     import holidays
 396     holidays_available = True
 397 except ModuleNotFoundError:
 398     holidays_available = False
 399 
 400 from wx.lib.embeddedimage import PyEmbeddedImage
 401 
 402 cal_event = PyEmbeddedImage(
 403     b'iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABmJLR0QARQD3AAo5DuANAAAC'
 404     b'AElEQVRIx83VTYiNYRQH8N975465zYgISU2yMJORj2gWdmKprNgrSx8lpeyGhZUsfGzIShKT'
 405     b'hRILCwspoix8ra18JDJMkxvva3Pu9Hh7773cW3Lqqaf3Oe//f87/Oec8/AdWQ72Lz0CsnmwA'
 406     b'h3EDE218luISzmC4F5KFeIccJ5BV+OxGE3OYbAdUr5BncaxZLAjwIazASOLbRCPOa+GzGjP4'
 407     b'EsHNg7ZsEMfwEs+wuSTdKTxP1q0AbtkIHuJV4AxWZbILU/FjsxSAyCrVvVGhzDCWBM7rCGQe'
 408     b'qIYdpcgy/Ix9XkFaSyQpYt+6t6HAq5XlapTkGcd1vMADrCuRrMR7PMb9kKdRhVe++O9JdEdw'
 409     b'GvewB2NRRZJoj+NsSDkVGc2Vs86S1M/hKR4l53msTg35I3DShtyGrTiEvPzzm7iwfm0VtlSV'
 410     b'cC+2BgewrJNTvQ+CtVEYE/iA6U4DsNuAHKkYgGMx0zYG+N1uIJ1se0yAo0kHjwfBBlwLub71'
 411     b'I1cTi3AyArqNq1ifEMz8zbtxIbq0/H0vPkcPvY2SvRLk7WwnzreU6pZJjpuxv4jlvWSQkhRt'
 412     b'3owW0Sw2RYRfu+BmgfcbSYFPGO2Q0Z2oouIPgh8NvKJMMh2j5WN0fdFD72QxSPfhYAsjKzlM'
 413     b'Yn9M2KwHkiKe7Mt4UkWSjvl+xk2evEP/zn4BOsdrpbkFvrcAAAAASUVORK5CYII=')
 414 
 415 cal_noevent = PyEmbeddedImage(
 416     b'iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABmJLR0QA/wD/AP+gvaeTAAAD'
 417     b'PklEQVRIx82WT2hVRxTGfzNzc/OYpKRGK1ppotDYaBcqGoIbg3HTUhCEEkpdhUIXFQttoail'
 418     b'JS4UCtqFZqWCcdEGU5W24B9w0VIb6R/EiPoU0SIJpUqL1URv4subGRfvvMfz9j3D040HBi7M'
 419     b'me8755s551x4DkwD0Qw+RtZTmQE+BIaApVV8moH9wFeAfRqSRuAW4IHtgKrgsx7IAZNARzWg'
 420     b'qII8TbIeALGA1wNzgYYy3xyQkX0tPq3AOHBPgiuBFq0O+BS4DJwHlqek2wlcLFvfC3DRGoBh'
 421     b'ICs4dZUyeQvok4O5VABIVuW6ZyooY4FZgnNFAikBaaA7FZkCnHz7CqS6TJIg38V7qxc8nZYr'
 422     b'k5LnNeAwcAk4AyxJkcwDbgO/AT+KPJlKeCW5upQyLZCbAP9dCAH4CNgFnAbeBhbLK6Is2q3A'
 423     b'HpGyTzKaTGddTE9/ovXBjVHk52idPTE9feZr5yYO1Nd3nsznz//i/fSREFyVF5oXnPKCXA2s'
 424     b'BDYDvkTSAv07omj43TieD3QB/8leFrhmkuRYDTW2DtggxexLaY1C2J3P3xJZOiT1NqAHWOSs'
 425     b'XeWsbQRw1r4qxxYBm4A5M/Wmko0UgAeAXuAF2T8HvAJsBNqctYuBzDtKtQNHgd3A2ieR/K/5'
 426     b'mSQ566xtBhZ6uK3gC+APVchqBLhy0/sVmYLe7cAgcLImErG7wGDf1NSaNq1fXmHMgteNua+g'
 427     b'Iwlhg4HuTq0b/vF+6HgIm4D7NZOYJPFCdOeNEOp+d25dlzED6+O4M+tc6zQ0ro2i6z1af/Oi'
 428     b'Ui3ATcCbJElqyaRoZ0+F8D6wL3Gu9++HDycued/0ZSZzxyr1Q1y4+DelhZxz1l4Fxk2KayYS'
 429     b'L5fLQAj7ljrXNBuGFByO4S/guHSFVmAhMFb0r0YSqsyMItGDLCwD+puUmpQq/0Be4OdC0AmM'
 430     b'LYPxCwW8xypeyWD6U55wNVOUHXbWRkJ2GvhJ5tDquyFEH09NnToUwhYgRGVZfAvsBf4VjcOM'
 431     b'8zlJ6FZKb4uiLXO1bm43Ztd4CKODudzse4WnHUjJo6TS35MOq2qZ1UtAfRZFL13w/sblEPpP'
 432     b'hPDrkwI10rZrXj/HccOz/Lk8kz0C/Mb+m+9IOV0AAAAASUVORK5CYII=')
 433 
 434 cal_fiesta = PyEmbeddedImage(
 435     b'iVBORw0KGgoAAAANSUhEUgAAABkAAAAZCAYAAADE6YVjAAAABmJLR0QARQD3AAo5DuANAAAG'
 436     b'WElEQVRIx5WWyY9cVxXGf/e9+2p6XVU9lnvuTrvbTrAdx9hRwE5sJ0KKUIwiocgLvEXiP0As'
 437     b'Yc2/wIIFwwJWCARCIFAUBRO17aSJ3W73ZPdQXfPwanjzvSyq7dhGQnCkq6O7Od8953463yd4'
 438     b'MSRgXLu+yE9++kNrsz2retWy+vu9Le1Uq6z+6Y84pTKgQWv+Sygger4ogAFcBr6fHkpP940Z'
 439     b'8ee/bqSVvyYPan0/5fVo9XVw5utn2L5vUTkogVag1CC/GBooAj8DPgWUABCCty0pfqu0KCwv'
 440     b'F8T1b79Hr90Ae4JUWrL5+T1MSzIzPUpPnuDOP+7jVEo0m110FEIcgIpfBqoAHwGfmICUpvjx'
 441     b'0mL28vzX3hA3PnyHbten1XEolxrs7DVgeI6OzmBYSS5cPMnk+Djdnk+95aMMC4zjgehnQAKw'
 442     b'ARP4vTmUMaXS+gd1R5/8zs33eW1GEQVdCgvLNOtt2l3NuTMLxIFPJCRxrJB+A88ax/A7NF2B'
 443     b'FhIMc/B+FR83ggCqwK/lW5dmqdU7Ij3zBrYlWH3gUJgq8NmdPZLZSeKDXT65vYtlauzJaaL9'
 444     b'DitLk1xeSPBPDLZqXww6MczjukDYf0oMASA//nQPhcH7Z4dQQZdq3yLd6uA5Dg8362QTGiub'
 445     b'RbgdbDPC8xX3d1vUqppqqcLs8Aj77TYIcfwZejC20PuKsmGkMSwTQ4XEymA+1+LeRoyYWOLa'
 446     b'Up/q4yMOQs3UK9PUKk3mlqeI3B5SCKYWpsgYVV5fniPwXar1Co7T4qCoiLQijoKvKHx2ZYiD'
 447     b'ZsCJScHMiRzdR20qTyq8+8EKlf0mEymB48YYUlDvakasFIbuUzrw+NaFd/ADRRh2SSbyjJ4a'
 448     b'wnMbrN3/nK3tDcIwHIBsPXaQjXVydpqNUprz82myGYPCmCCblRSGQ3biNNnZLHUnwjnaY3px'
 449     b'ghvfvMLefpVI+ewVy4zlBY/2drlx9Roj+SzZTIrP7q5iAmaojFuhlkuGZfGNK+d582yenaM+'
 450     b'20UPbSXJTozx8b9qeJ0Oudjl/LkCr5+7RCqcwQ8DolBTqhXJpYZJWAZRHGIKWJyf3n37ysVf'
 451     b'mghhYshbWqaX+gFsbpXZKUe0OjFWOoPf6yPSOZJunSE7w8lpye27HZYmX+Wo9IS247O++5Ao'
 452     b'VDScEmEY43ktLFMykhs5unnzo18ZA5YJEIL5MUngB+xvl9CxJm3nOLsyScprs7g4g+p0eLQR'
 453     b'4PcCaodVXFfj9DqMDWdJygxCmCQsSSaVpdfvoSItx+180kRgYhi3kMmlhmcRCIt+bFJtejhN'
 454     b'F6kypJWFKvvMjY5RLLdxA8XoSJqMzHB7fY1ziytIkSHGx+l6DKUTqNjHdbsbjXbp5wZaD5ac'
 455     b'jo+zZlgKLo7YnEorriYs5uodZsOQRMvl+olxXrOT3H3wCDds8b2rHzI3tQCGwiDF8vwJEgmB'
 456     b'bU0wVZiWnU5HD5aOUhBHoCKkjjmZTXFhdoJqrU3YctgutWgaBivjeZSAdxcm8Q9qNLsuSWuP'
 457     b'jYf7TI+OYyVyHJWPGLNzIDy6XhD+5nd/OF71WkEcQhwyRMx7r75CVkFiJIcWMDtTwGt36bs+'
 458     b'o6kEvusxk7b4xeqXzNgJLp07RUqaZOQQYW6YZvOIau0J99fXCcPomZ5AHCAiH1soOr0uPT+i'
 459     b'kM9zWGnSS1g4nT57WiClpCw0bhRzNp9ip+Pxt9U1lgpZVByzufOEQj7Nl+sPnq3/r0BUTEIF'
 460     b'vDk1xs7+IVGkKXoBAoOdSoOkJclJyRe1FlN2kulshqShSRFx6PQo1msQeuigR6XsvKAvxvNK'
 461     b'4/t9HlXKnBzJIlXE6bEcLbdP3oRat0dGGqSlYMZO4gU+B402e45DFHro0EOHffB7EPkvSKXx'
 462     b'nJKB1hTrVSw7ybgFW4eHzA8lGZKa2YyFqUJsHfK4eMRus8m246ADF/w+BD3wuhC6z+u/fjou'
 463     b'dazJGhCoGM/3yY+PIKKYWEdYUcBKPovf9/F9n640WTs8RAceROHg5aE7yC8CFAFlHoPUgA8A'
 464     b'24siUXMclCXJmiYoxWTeRhiChcI4ndBn7bDIXrMJQX9wwv6Anf+p8T8CHouX3QowDQg7keCt'
 465     b'06c5NTeHnUjgRpGxX62pO5tbFB3n/3crL/uup5d8KsV3r13jL6urnFlcpOo41FptntRqx2PR'
 466     b'/5Pv+jdB5lKV7c0slAAAAABJRU5ErkJggg==')
 467 
 468 __version__ = 1.0
 469 
 470 
 471 exCalEVT = wx.NewEventType()
 472 EVT_DATE_CHANGED = wx.PyEventBinder(exCalEVT, 1)
 473 
 474 # day ranges for negative day codes see GenerateDates and GenerateNotes
 475 mondays     = range(-11, -16, -1)
 476 tuesdays    = range(-21, -26, -1)
 477 wednesdays  = range(-31, -36, -1)
 478 thursdays   = range(-41, -46, -1)
 479 fridays     = range(-51, -56, -1)
 480 saturdays   = range(-61, -66, -1)
 481 sundays     = range(-71, -76, -1)
 482 
 483 
 484 class exCalEvent(wx.PyCommandEvent):
 485     def __init__(self, eventType, eventId=1, date=None, note=None, marked=None, holiday=None, public_holiday=None, value=''):
 486         """
 487         Default class constructor.
 488 
 489         :param `eventType`: the event type;
 490         :param `eventId`: the event identifier.
 491         """
 492         wx.PyCommandEvent.__init__(self, eventType, eventId)
 493         self._eventType = eventType
 494         self.date = date
 495         self.note = note
 496         self.marked = marked
 497         self.holiday = holiday
 498         self.public_holiday = public_holiday
 499         self.value = value
 500 
 501     def GetDate(self):
 502         """
 503         Retrieve the date value of the control at the time
 504         this event was generated, Returning a wx.DateTime object"""
 505         return self.date
 506 
 507     def GetNote(self):
 508         """
 509         Retrieve the note value of the control at the time
 510         this event was generated, Returning a string"""
 511         return self.note
 512 
 513     def IsMarked(self):
 514         """
 515         Retrieve the marked value of the control at the time
 516         this event was generated, Returning a string"""
 517         return self.marked
 518 
 519     def IsHoliday(self):
 520         """
 521         Retrieve the holiday value of the control at the time
 522         this event was generated, Returning a string"""
 523         return self.holiday
 524 
 525     def IsPublicHoliday(self):
 526         """
 527         Retrieve the public holiday value of the control at the time
 528         this event was generated, Returning a string"""
 529         return self.public_holiday
 530 
 531     def GetValue(self):
 532         """
 533         Retrieve the formatted date value of the control at the time
 534         this event was generated, Returning a string"""
 535         return self.value.title()
 536 
 537     def GetDateTime(self):
 538         """
 539         Retrieve the date value of the control at the time
 540         this event was generated, Returning a python datetime object"""
 541         return wx.wxdate2pydate(self.date)
 542 
 543     def GetTimeStamp(self):
 544         """
 545         Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
 546         Returning a integer
 547         """
 548         return int(self.date.GetValue()/1000)
 549 
 550 
 551 
 552 class CalendarCtrl(wx.Panel):
 553     def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
 554                  style=0, name="ExtendedCalCtrl", date=0, formatter='', calendar_style=None, calendar_locale=0):
 555         wx.Panel.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
 556         self.parent = parent
 557         self._date = date
 558         if formatter:
 559             format = formatter
 560         else:
 561             format = lambda dt: dt.FormatISODate()
 562         self._formatter = format
 563         font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
 564         self.SetWindowStyle(wx.BORDER_NONE)
 565         self.locale =  wx.Locale(calendar_locale, wx.LOCALE_LOAD_DEFAULT)
 566         self._style = style
 567         if calendar_style:
 568             self._cal_style = calendar_style
 569         else:
 570             self._cal_style = wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SHOW_HOLIDAYS
 571         self.ShowHolidays = True
 572         self._cal_headercolours = (wx.NullColour, wx.NullColour)
 573         self._cal_highlightcolours = (wx.NullColour, wx.NullColour)
 574         self._cal_holidaycolours = (wx.NullColour, wx.YELLOW)
 575         self._cal_Bg = wx.NullColour
 576         self._cal_Fg = wx.NullColour
 577         self._cal_Font = font
 578         self._cal_MarkDates = {}
 579         self._cal_MarkBorder = (wx.adv.CAL_BORDER_SQUARE, wx.NullColour, wx.NullColour, wx.NullColour)
 580         self._cal_MarkNotes = (wx.adv.CAL_BORDER_SQUARE, wx.GREEN, wx.NullColour, wx.NullColour)
 581         self._cal_Holidays = {}
 582         self._cal_RestrictDates = {}
 583         self._cal_daterange = (wx.DefaultDateTime, wx.DefaultDateTime)
 584         self._cal_Notes = {}
 585         self._cal_SetOnlyWeekDays = False
 586         self._cal_PublicHolidays = []
 587         self._cal_AddPublicHolidays = []
 588         self.CalSaveFile = None
 589 
 590         self.calendar = wx.adv.GenericCalendarCtrl(self, style=self._cal_style)
 591         date = wx.DateTime.Today()
 592         date = date.ResetTime()
 593         self.calendar.SetDate(date)
 594         self.Holidays = {}
 595         self.PublicHolidays = {}
 596         self.RestrictedDates = {}
 597         self.MarkDates = {}
 598         self.Notes = {}
 599 
 600         sizer = wx.BoxSizer(wx.VERTICAL)
 601         sizer.Add(self.calendar, 0, wx.ALL|wx.EXPAND, 5)
 602         self.SetSizer(sizer)
 603 
 604         self.calendar.Bind(wx.adv.EVT_CALENDAR_MONTH, self.OnChange)
 605         self.calendar.Bind(wx.adv.EVT_CALENDAR_YEAR, self.OnChange)
 606         self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnChosen)
 607         self.calendar.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnToolTip)
 608         self.calendar.Bind(wx.EVT_MOTION, self.OnToolTip)
 609         self.calendar.Bind(wx.EVT_RIGHT_DOWN, self.OnRightClick)
 610         self.calendar.Bind(wx.EVT_KEY_DOWN, self.OnKey)
 611         self.Show()
 612 
 613     def SetFormatter(self, formatter):
 614         '''formatter will be called with a wx.DateTime'''
 615         self._formatter = formatter
 616         self.OnDate(self._date)
 617 
 618     def SetCalendarStyle(self, style=0):
 619         self._cal_style = style
 620         self.calendar.SetWindowStyleFlag(style)
 621         if style & wx.adv.CAL_SHOW_HOLIDAYS or style == wx.adv.CAL_SHOW_HOLIDAYS:
 622             self.ShowHolidays = True
 623         else:
 624             self.ShowHolidays = False
 625 
 626     def SetCalendarHeaders(self, colFg=wx.NullColour, colBg=wx.NullColour):
 627         self._cal_headercolours = colFg, colBg
 628         self.calendar.SetHeaderColours(self._cal_headercolours[0],self._cal_headercolours[1])
 629 
 630     def SetCalendarHighlights(self, colFg=wx.NullColour, colBg=wx.NullColour):
 631         self._cal_highlightcolours = colFg, colBg
 632         self.calendar.SetHighlightColours(self._cal_highlightcolours[0],self._cal_highlightcolours[1])
 633 
 634     def SetCalendarHolidayColours(self, colFg=wx.NullColour, colBg=wx.NullColour):
 635         self._cal_holidaycolours = colFg, colBg
 636 
 637     def SetWeekendColours(self, colFg=wx.NullColour, colBg=wx.NullColour):
 638         self._cal_weekendcolours = colFg, colBg
 639         self.calendar.SetHolidayColours(colFg, colBg)
 640 
 641     def SetCalendarFg(self, colFg=wx.NullColour):
 642         self._cal_Fg = colFg
 643         self.calendar.SetForegroundColour(self._cal_Fg)
 644 
 645     def SetCalendarBg(self, colBg=wx.NullColour):
 646         self._cal_Bg = colBg
 647         self.SetBackgroundColour(self._cal_Bg)
 648 
 649     def SetCalendarFont(self, font=None):
 650         self._cal_Font = font
 651         self.calendar.SetFont(self._cal_Font)
 652         bz = self.calendar.GetBestSize()
 653         self.calendar.SetSize(bz)
 654         self.Refresh()
 655 
 656     def SetCalendarMarkDates(self, markdates = {}):
 657         self._cal_MarkDates = markdates
 658         self.SetMarkDates(self._cal_MarkDates)
 659 
 660     def SetCalendarMarkBorder(self, border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.NullColour,\
 661                               colFg=wx.NullColour, colBg=wx.NullColour):
 662         self._cal_MarkBorder = (border, bcolour, colFg, colBg)
 663 
 664     def SetCalendarMarkNotes(self, border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.GREEN,\
 665                              colFg=wx.NullColour, colBg=wx.NullColour):
 666         self._cal_MarkNotes = (border, bcolour, colFg, colBg)
 667 
 668     def SetCalendarHolidays(self, holidays = {}):
 669         self._cal_Holidays = holidays
 670         self.SetHolidays(self._cal_Holidays)
 671 
 672     def AddPublicHolidays(self, country='', subdiv='', language='', replace=False):
 673         if not holidays_available:
 674             return False
 675         country = country.upper()
 676         subdiv = subdiv.upper()
 677         try:
 678             supported = holidays.country_holidays(country=country).subdivisions
 679         except Exception as e:
 680             return False
 681         if subdiv:
 682             if subdiv not in supported:
 683                 return False
 684         if replace: # Do not add to existing public holidays, create a new set.
 685             self._cal_PublicHolidays = []
 686             self._cal_AddPublicHolidays = []
 687         if not self._cal_PublicHolidays:
 688             self._cal_PublicHolidays = [country, subdiv, language]
 689         else:
 690             self._cal_AddPublicHolidays.append([country, subdiv, language])
 691         self.SetPublicHolidays(self._cal_PublicHolidays)
 692         return True
 693 
 694     def SetCalendarRestrictDates(self, rdates = {}):
 695         self._cal_RestrictDates = rdates
 696         self.SetRestrictDates(self._cal_RestrictDates)
 697 
 698     def SetCalendarDateRange(self, lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime):
 699         if not isinstance(lowerdate, wx.DateTime):
 700             return False
 701         if not isinstance(upperdate, wx.DateTime):
 702             return False
 703         self._cal_daterange = (lowerdate, upperdate)
 704         self.calendar.SetDateRange(lowerdate, upperdate)
 705 
 706     def SetCalendarNotes(self, notes = {}):
 707         self._cal_Notes = notes
 708         self.SetNotes(self._cal_Notes)
 709 
 710     def SetCalendarOnlyWeekDays(self, wds = False):
 711         self._cal_SetOnlyWeekDays = wds
 712         if wds:
 713             self._cal_style = self._cal_style | wx.adv.CAL_SHOW_HOLIDAYS
 714             self.ShowHolidays = True
 715 
 716     def TriggerDateEvent(self, date=None):
 717         if not date:
 718             date = self.calendar.GetDate()
 719         if isinstance(date, datetime.datetime):
 720             date = wx.DateTime.FromDMY(date.day, date.month-1, date.year)
 721         self.OnDate(date)
 722 
 723     def OnDate(self, date):
 724         self._date = date
 725         d = (date.year, date.month + 1, date.day)
 726         ym = (date.year, date.month + 1)
 727         note = self.Notes.get(d, '')
 728         marked = self.MarkDates.get(ym, [])
 729         if date.day in marked:
 730             marked = True
 731         else:
 732             marked = False
 733         hol = self.Holidays.get(ym, [])
 734         if hol:
 735             pass
 736         else:
 737             hol = self.Holidays.get((0, date.month + 1), [])
 738         if date.day in hol:
 739             hol = True
 740         else:
 741             hol = False
 742         ohol = self.PublicHolidays.get(ym, [])
 743         if date.day in ohol:
 744             ohol = True
 745         else:
 746             ohol = False
 747         event = exCalEvent(exCalEVT, self.GetId(), date=date, note=note, marked=marked,\
 748                          holiday=hol, public_holiday=ohol, value=self._formatter(date))
 749         event.SetEventObject(self)
 750         self.GetEventHandler().ProcessEvent(event)
 751 
 752     def OnKey(self, event):
 753         keycode = event.GetKeyCode()
 754         if keycode == wx.WXK_F1:
 755             self.OnRightClick(None)
 756         if keycode == wx.WXK_F2:
 757             d = self.calendar.GetDate()
 758             if self._cal_style & wx.adv.CAL_SUNDAY_FIRST:
 759                 fwd = 6
 760             else:
 761                 fwd = 0
 762             ycal = calendar.TextCalendar(firstweekday=fwd).formatyear(d.year, c=3)
 763             win = YearPopup(self, txt=ycal)
 764             win.Show()
 765         event.Skip()
 766 
 767     def GetValue(self):
 768         #return self.calendar.GetValue()
 769         return self._formatter(self.calendar.GetDate())
 770 
 771     def GetDate(self):
 772         return self.calendar.GetDate()
 773 
 774     def GetDateTimeValue(self):
 775         """
 776         Return a python datetime object"""
 777         return wx.wxdate2pydate(self.calendar.GetDate())
 778 
 779     def GetTimeStamp(self):
 780         """
 781         Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
 782         Returning a integer
 783         """
 784         return int(self.calendar.GetDate().GetTicks())
 785 
 786     def GetLocale(self):
 787         return self.locale.GetLocale()
 788 
 789     def GetNotes(self):
 790         return self.Notes
 791 
 792     def GetHolidays(self):
 793         return self.Holidays
 794 
 795     def GetMarkDates(self):
 796         return self.MarkDates
 797 
 798     def GetRestrictedDates(self):
 799         return self.RestrictedDates
 800 
 801     def SetValue(self, date):
 802         if isinstance(date, wx.DateTime):
 803             pass
 804         elif isinstance(date, datetime.date):
 805             date = wx.pydate2wxdate(date)
 806         elif isinstance(date, int) and date > 0:
 807             date = wx.DateTime.FromTimeT(date)
 808         elif isinstance(date, float) and date > 0:
 809             date = wx.DateTime.FromTimeT(int(date))
 810         else:  # Invalid date value default to today's date
 811             date = wx.DateTime.Today()
 812         self._date = date.ResetTime()
 813         self.SetFormatter(self._formatter)
 814 
 815     def OnChosen(self, event=None):
 816         ''' Test chosen date for inclusion in restricted dates if set
 817             Test if set to only allow weekdays, test if it is a weekday or a holiday, which is treated as not a weekday
 818         '''
 819         d = self.calendar.GetDate()
 820         event.Skip()
 821         if self.RestrictedDates:
 822             test = (d.year, d.month+1)
 823             days = self.RestrictedDates.get(test, ())
 824             if not days or d.day not in days:
 825                 pass
 826             else:
 827                 return
 828 
 829         if self._cal_SetOnlyWeekDays and not d.IsWorkDay(): # Weekend
 830             return
 831         if self._cal_SetOnlyWeekDays: # Holiday
 832             attr = self.calendar.GetAttr(d.day)
 833             if attr.IsHoliday():
 834                 return
 835 
 836         self.calendar.GetDate()
 837         self.OnDate(self.calendar.GetDate())
 838 
 839     def OnChange(self, event):
 840         # If the year changed, recalculate the dictionaries for Marked, Restricted, Public Holidays and Note dates
 841         if event.GetEventType() == wx.adv.EVT_CALENDAR_YEAR.typeId:
 842             self.MarkDates = self.GenerateDates(self._cal_MarkDates)
 843             self.RestrictedDates = self.GenerateDates(self._cal_RestrictDates)
 844             self.SetPublicHolidays(self._cal_PublicHolidays)
 845             self.Notes = self.GenerateNotes(self._cal_Notes)
 846         date = event.GetDate()
 847         self.OnMonthChange()
 848         # Permit calling programs to see Month and Year Events
 849         event.Skip()
 850 
 851     def SetMarkDates(self, markdates):
 852         self.MarkDates = self.GenerateDates(markdates)
 853         self.OnMonthChange()
 854 
 855     def OnMonthChange(self):
 856         font = self.calendar.GetFont()
 857         font.SetStrikethrough(True)
 858         font.MakeItalic()
 859         date = self.calendar.GetDate()
 860         days_in_month = date.GetLastMonthDay().day
 861         mark_days = self.MarkDates.get((date.year, date.month+1), [])    # get dict values or an empty list if none
 862         h_days = self.Holidays.get((date.year, date.month+1), [])
 863         fixed_h_days = self.Holidays.get((0, date.month+1), [])
 864         r_days = self.RestrictedDates.get((date.year, date.month+1), [])
 865         oh_days = self.PublicHolidays.get((date.year, date.month+1), [])
 866 
 867         if isinstance(mark_days, int): # Allow for people forgetting it must be a tuple, when entering a single day
 868             mark_days = tuple((mark_days,))
 869         if isinstance(h_days, int):
 870             h_days = tuple((h_days,))
 871         if isinstance(fixed_h_days, int):
 872             fixed_h_days = tuple((fixed_h_days,))
 873         if isinstance(r_days, int):
 874             r_days = tuple((r_days,))
 875 
 876         mark_border, mb_clr, mb_fg, mb_bg = self._cal_MarkBorder    #border type, border colour, background colour, text colour
 877         mark_notes, mn_clr, mn_fg, mn_bg = self._cal_MarkNotes
 878 
 879         for d in range(1, days_in_month+1):
 880             note = self.Notes.get((date.year, date.month +1, d), '')            # get notes for the day
 881             attr = self.calendar.GetAttr(d)
 882             highlight_attr = wx.adv.CalendarDateAttr()
 883             if d in mark_days and mark_border:                                  # Marked Day & borders on
 884                 highlight_attr.SetBorder(mark_border)
 885                 highlight_attr.SetBorderColour(mb_clr)
 886                 highlight_attr.SetBackgroundColour(mb_bg)
 887                 highlight_attr.SetTextColour(mb_fg)
 888             if note and mark_notes:                                             # Note for the day & borders on
 889                 highlight_attr.SetBorder(mark_notes)
 890                 highlight_attr.SetBorderColour(mn_clr)
 891                 highlight_attr.SetBackgroundColour(mn_bg)
 892                 highlight_attr.SetTextColour(mn_fg)
 893             if d in h_days or d in fixed_h_days or d in oh_days:            # Holiday/fixed holiday/public holiday
 894                 highlight_attr.SetTextColour(self._cal_holidaycolours[0])
 895                 highlight_attr.SetBackgroundColour(self._cal_holidaycolours[1])
 896             if self.ShowHolidays:
 897                 if not wx.DateTime(d, date.month, date.year).IsWorkDay():       # Weekend
 898                     highlight_attr.SetHoliday(True)
 899             if d in r_days:                                                     # Resticted Day (override holiday)
 900                 highlight_attr.SetFont(font)
 901             if highlight_attr.IsHoliday():
 902                 if self._cal_SetOnlyWeekDays:
 903                     highlight_attr.SetFont(font)
 904             if highlight_attr is not None:
 905                 self.calendar.SetAttr(d, highlight_attr)
 906             else:
 907                 self.calendar.ResetAttr(d)
 908 
 909         self.calendar.Refresh()
 910 
 911     def SetHolidays(self, holidays):
 912         self.Holidays = holidays
 913         self.OnMonthChange()
 914 
 915     def SetPublicHolidays(self, holiday_codes):
 916         self.PublicHolidays = {}
 917         if not holiday_codes:                                                   # holiday codes not set
 918             return
 919         country, subdiv, language = holiday_codes
 920         self.country_name = country
 921         for c in holidays.registry.COUNTRIES.values():
 922             if country in c:
 923                 self.country_name = c[0]
 924                 break
 925         d = self.calendar.GetDate()
 926         for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
 927             existing = self.PublicHolidays.get((k.year, k.month), [])
 928             if k.day not in existing:
 929                 self.PublicHolidays[(k.year, k.month)] = existing + [k.day]
 930 
 931         for item in self._cal_AddPublicHolidays:
 932             country, subdiv, language = item
 933             for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
 934                 existing = self.PublicHolidays.get((k.year, k.month), [])
 935                 if k.day not in existing:
 936                     self.PublicHolidays[(k.year, k.month)] = existing + [k.day]
 937         self.SetNotes(self._cal_Notes)                                 # Update Notes with public holidays
 938         self.OnMonthChange()
 939 
 940     def SetNotes(self, notes):
 941         self.Notes = self.GenerateNotes(notes)
 942         self.OnMonthChange()
 943 
 944     def SetRestrictDates(self, rdates):
 945         self.RestrictedDates = self.GenerateDates(rdates)
 946         self.OnMonthChange()
 947 
 948     def restricted_date_range(self, start, end):
 949         '''
 950             Generate dates between a start and end date
 951         '''
 952         for i in range((end - start).days + 1):
 953             yield start + datetime.timedelta(days = i)
 954 
 955     def day_in_range(self, start, end, day):
 956         '''
 957             Test if date is the required day of the week
 958         '''
 959         for d in self.restricted_date_range(start, end):
 960             if d.isoweekday() == day:
 961                 yield d
 962 
 963     def GenerateDates(self, date_dict):
 964         ''' Generated on start and when the year changes (Marked and Restricted dictionaries)
 965             This routine generates a new dictionary of complete dates, from the one passed in
 966              and returns the generated dictionary.
 967             This is because the original passed in dictionary may include date codes e.g. -99 for the last day of a month
 968              or -1 all Mondays or -23 the 3rd Tuesday, which need to be calculated for the given month in the given year.
 969             An added complication is that the year may be set to zero, denoting all years, so if the calendar year is
 970              changed, this routine is run again, to ensure that the dates are relevant to the current year.
 971         '''
 972         generated_dict = {}
 973 
 974         for year, month in date_dict:
 975             gen_year = year
 976             if gen_year == 0:               # Zero entry = All years, so generate dates for the currently selected year
 977                 d = self.calendar.GetDate()
 978                 gen_year = d.year
 979             if month <= 0:                  # Zero entry = All months, generate dates for every month in the year
 980                 month_start = 1
 981                 month_end = 13
 982             else:
 983                 month_start = month
 984                 month_end = month + 1
 985             for month in range(month_start, month_end):
 986                 day_map = calendar.monthcalendar(gen_year, month)
 987                 for neg in list(date_dict.get((year, month))):
 988                     if neg >= 0:
 989                         existing = generated_dict.get((gen_year, month), [])
 990                         if neg not in existing:
 991                             generated_dict[(gen_year, month)] = existing + [neg]
 992                         continue
 993                     first_week_day, last_day_no = calendar.monthrange(gen_year, month)
 994                     d1 = datetime.datetime(gen_year, month, 1)
 995                     d2 = datetime.datetime(gen_year, month, last_day_no)
 996                     if neg < 0 and neg >= -7:                                       # Every specified weekday
 997                         for i in self.day_in_range(d1, d2, abs(neg)):
 998                             existing = generated_dict.get((gen_year, month), [])
 999                             if i.day not in existing:
1000                                 generated_dict[(gen_year, month)] = existing + [i.day]
1001                         continue
1002                     if neg == -99:                                                  # Last day of the month
1003                         first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1004                         existing = generated_dict.get((gen_year, month), [])
1005                         if last_day_no not in existing:
1006                             generated_dict[(gen_year, month)] = existing + [last_day_no]
1007                         continue
1008                     if neg == -98:                                                  # Last weekday of the month
1009                         first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1010                         ld = datetime.date(gen_year, month, last_day_no)
1011                         while ld.isoweekday() > 5:                                  # Last day of month is not a weekday
1012                             ld -= datetime.timedelta(days=1)                        # deduct days to get to Friday
1013                         existing = generated_dict.get((gen_year, month), [])
1014                         if ld.day not in existing:
1015                             generated_dict[(gen_year, month)] = existing + [ld.day]
1016                         continue
1017                     if neg <= -11 and neg >= -75:                        # Occurrence of a weekday
1018                         if neg in mondays:                               # Monday 1-5
1019                             map_idx = 0
1020                             occ = neg + abs(mondays[0])
1021                         elif neg in tuesdays:                            # Tuesday 1-5
1022                             map_idx = 1
1023                             occ = neg + abs(tuesdays[0])
1024                         elif neg in wednesdays:                          # Wednesday 1-5
1025                             map_idx = 2
1026                             occ = neg + abs(wednesdays[0])
1027                         elif neg in thursdays:                           # Thursday 1-5
1028                             map_idx = 3
1029                             occ = neg + abs(thursdays[0])
1030                         elif neg in fridays:                             # Friday 1-5
1031                             map_idx = 4
1032                             occ = neg + abs(fridays[0])
1033                         elif neg in saturdays:                           # Saturday 1-5
1034                             map_idx = 5
1035                             occ = neg + abs(saturdays[0])
1036                         elif neg in sundays:                             # Sunday 1-5
1037                             map_idx = 6
1038                             occ = neg + abs(sundays[0])
1039                         else:                                            # Undefined
1040                             continue
1041                         week_map = [index for (index, item) in enumerate(day_map) if item[map_idx]]
1042                         if abs(occ) >= len(week_map):
1043                             occ = len(week_map) - 1
1044                         week_idx = week_map[abs(occ)]
1045                         map_day = day_map[week_idx][map_idx]
1046                         existing = generated_dict.get((gen_year, month), [])
1047                         if map_day not in existing:
1048                             generated_dict[(gen_year, month)] = existing + [map_day]
1049         return generated_dict
1050 
1051     def GenerateNotes(self, date_dict):
1052         ''' Generated on start and when the year changes
1053             This routine generates a new dictionary of Notes from the one passed in and returns the generated dictionary.
1054             This because the original passed in dictionary may include date codes e.g. -99 for the last of a month
1055              or -1 all Mondays or -23 the 3rd Tuesday, which need to be calculated for the given month in the given year.
1056             An added complication is that the year may be set to zero, denoting all years, so if the calendar year is changed,
1057              this routine is run again, to ensure that the dates are relevant to the current year.
1058             Plus the month might be set to zero, indicating a note for all months
1059             Because some of the notes are calculated, a date may have muliple notes, so the notes are accumulated, to form
1060             a single note entry, separated by a + sign in tooltips etc
1061             If Public Holidays are included, these too are recalculated for the current year.
1062         '''
1063         generated_dict = {}
1064         for year, month, day in date_dict:
1065             gen_year = year
1066             if gen_year == 0:               # Zero entry = All years, so generate dates for the currently selected year
1067                 d = self.calendar.GetDate()
1068                 gen_year = d.year
1069             if month <= 0:                  # Zero entry = All months, generate dates for every month in the year
1070                 month_start = 1
1071                 month_end = 13
1072             else:
1073                 month_start = month         # Valid month - restrict just to this month
1074                 month_end = month + 1
1075             for month in range(month_start, month_end):
1076                 day_map = calendar.monthcalendar(gen_year, month)
1077                 note = date_dict.get((year, month, day))            # Get the note
1078                 if not note:
1079                     note = date_dict.get((year, 0, day))            # if fail - get note for every month
1080                 if day >= 0:
1081                     use_note = generated_dict.get((gen_year, month, day), '')
1082                     if use_note:
1083                         use_note = use_note+"\n+ "+note
1084                     else:
1085                         use_note = note
1086                     generated_dict[(gen_year, month, day)] = use_note
1087                     continue
1088                 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1089                 d1 = datetime.datetime(gen_year, month, 1)
1090                 d2 = datetime.datetime(gen_year, month, last_day_no)
1091                 if day < 0 and day >= -7:                                       # Every specified weekday
1092                     for i in self.day_in_range(d1, d2, abs(day)):
1093                         use_note = generated_dict.get((gen_year, month, i.day), '')
1094                         if use_note:
1095                             use_note = use_note+"\n+ "+note
1096                         else:
1097                             use_note = note
1098                         generated_dict[(gen_year, month, i.day)] = use_note
1099                     continue
1100                 if day == -99:                                                  # Last day of the month
1101                     first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1102                     use_note = generated_dict.get((gen_year, month, last_day_no), '')
1103                     if use_note:
1104                         use_note = use_note+"\n+ "+note
1105                     else:
1106                         use_note = note
1107                     generated_dict[(gen_year, month, last_day_no)] = use_note
1108                     continue
1109                 if day == -98:                                                  # Last weekday of the month
1110                     first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1111                     ld = datetime.date(gen_year, month, last_day_no)
1112                     while ld.isoweekday() > 5:                                  # Last day of month is not a weekday
1113                         ld -= datetime.timedelta(days=1)                        # deduct days to get to Friday
1114                     use_note = generated_dict.get((gen_year, month, ld.day), '')
1115                     if use_note:
1116                         use_note = use_note+"\n+ "+note
1117                     else:
1118                         use_note = note
1119                     generated_dict[(gen_year, month, ld.day)] = use_note
1120                     continue
1121                 if day <= -11 and day >= -75:                            # Occurrence of a weekday
1122                     if day in mondays:                               # Monday 1-5
1123                         map_idx = 0
1124                         occ = day + abs(mondays[0])
1125                     elif day in tuesdays:                            # Tuesday 1-5
1126                         map_idx = 1
1127                         occ = day + abs(tuesdays[0])
1128                     elif day in wednesdays:                          # Wednesday 1-5
1129                         map_idx = 2
1130                         occ = day + abs(wednesdays[0])
1131                     elif day in thursdays:                           # Thursday 1-5
1132                         map_idx = 3
1133                         occ = day + abs(thursdays[0])
1134                     elif day in fridays:                             # Friday 1-5
1135                         map_idx = 4
1136                         occ = day + abs(fridays[0])
1137                     elif day in saturdays:                           # Saturday 1-5
1138                         map_idx = 5
1139                         occ = day + abs(saturdays[0])
1140                     elif day in sundays:                             # Sunday 1-5
1141                         map_idx = 6
1142                         occ = day + abs(sundays[0])
1143                     else:                                            # Undefined
1144                         continue
1145                     week_map = [index for (index, item) in enumerate(day_map) if item[map_idx]]
1146                     if abs(occ) >= len(week_map):
1147                         occ = len(week_map) - 1
1148                     week_idx = week_map[abs(occ)]
1149                     map_day = day_map[week_idx][map_idx]
1150                     use_note = generated_dict.get((gen_year, month, map_day), '')
1151                     if use_note:
1152                         use_note = use_note+"\n+ "+note
1153                     else:
1154                         use_note = note
1155                     generated_dict[(gen_year, month, map_day)] = use_note
1156 
1157         # If public holidays are available write them into the notes
1158         if holidays_available and self._cal_PublicHolidays:
1159             country, subdiv, language = self._cal_PublicHolidays
1160             d = self.calendar.GetDate()
1161             for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
1162                 use_note = generated_dict.get((k.year, k.month, k.day), '')
1163                 if use_note:
1164                     use_note = use_note+"\n+ * "+v
1165                 else:
1166                     use_note = " * "+v
1167                 generated_dict[(k.year, k.month, k.day)] = use_note
1168 
1169             for item in self._cal_AddPublicHolidays:
1170                 country, subdiv, language = item
1171                 for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
1172                     use_note = generated_dict.get((k.year, k.month, k.day), '')
1173                     if use_note:
1174                         use_note = use_note+"\n+ * "+v+' [ '+' '.join(item).title()+"]"
1175                     else:
1176                         use_note = " * "+v+' [ '+' '.join(item).title()+"]"
1177                     generated_dict[(k.year, k.month, k.day)] = use_note
1178 
1179         return generated_dict
1180 
1181     def OnToolTip(self, event):
1182         '''
1183         Test for date range restrictions.
1184         If there are Notes or Restricted entries for the day, generate and display tooltips
1185         '''
1186         self.calendar.SetToolTip('')
1187         if event.GetClassName() == 'wxMouseEvent':
1188             pos = event.GetPosition()
1189             pos_code, pos_date, pos_day = self.calendar.HitTest(pos)
1190             if pos_code != wx.adv.CAL_HITTEST_DAY and pos_code != wx.adv.CAL_HITTEST_HEADER:
1191                 return
1192         else:
1193             pos_date = event.Date
1194             pos_code = wx.adv.CAL_HITTEST_NOWHERE
1195 
1196         # Position on calendar header report Holidays for which countries
1197         if pos_code == wx.adv.CAL_HITTEST_HEADER:
1198             lcl = self.locale.GetLocale()
1199             if holidays_available and self._cal_PublicHolidays:
1200                 country, subdiv, language = self._cal_PublicHolidays
1201             else:
1202                 self.calendar.SetToolTip("No Public Holidays\nLocale = "+lcl)
1203                 return
1204             msg = ''
1205             msg = "Public Holidays for "+self.country_name
1206             if subdiv:
1207                 msg += " region "+subdiv
1208             for item in self._cal_AddPublicHolidays:
1209                 country, subdiv, language = item
1210                 for c in holidays.registry.COUNTRIES.values():
1211                     if country in c:
1212                         name = c[0]
1213                         msg += "\n+ "+name
1214                         break
1215             self.calendar.SetToolTip(msg+"\nLocale = "+lcl)
1216             return
1217 
1218         range_check, lower, upper = self.calendar.GetDateRange()
1219         if range_check:
1220             if (lower != wx.DefaultDateTime and pos_date.IsEarlierThan(lower)) or \
1221                 (upper != wx.DefaultDateTime and pos_date.IsLaterThan(upper)):
1222                 msg = str(self._formatter(pos_date)).title()+'\n'+"Out of Range\n"
1223                 if lower != wx.DefaultDateTime:
1224                     msg += str(lower.Format("%d-%b-%Y")).title()+' > '
1225                 else:
1226                     msg += "Any date > "
1227                 if upper != wx.DefaultDateTime:
1228                     msg += str(upper.Format("%d-%b-%Y")).title()
1229                 else:
1230                     msg += "Any date"
1231                 self.calendar.SetToolTip(msg)
1232                 return
1233 
1234         restricted = self.RestrictedDates.get((pos_date.year, pos_date.month + 1), [])
1235         restricted_set = False
1236         if pos_date.day in restricted:
1237             restricted_set = True
1238         if self._cal_SetOnlyWeekDays and not pos_date.IsWorkDay():
1239             restricted_set = True
1240         d = (pos_date.year, pos_date.month + 1, pos_date.day)
1241         note = self.Notes.get(d, '')                        # Year/Month/Day specific Note or blank
1242         if restricted_set:
1243             note = "** Restricted **\n\n"+ note
1244         if not note:
1245             self.calendar.SetToolTip('')
1246         else:
1247             self.calendar.SetToolTip(str(self._formatter(pos_date)).title()+'\n'+note)
1248 
1249     def OnRightClick(self, event):
1250         '''
1251         If Right click display all Notes for the month
1252         '''
1253         click_date = self.calendar.GetDate()
1254         hol0 = self.Holidays.get((click_date.year,click_date.month + 1), [])            #Calendar holidays
1255         hol1 = self.Holidays.get((0,click_date.month + 1), [])                          #Fixed holidays
1256         hol2 = self.PublicHolidays.get((click_date.year,click_date.month + 1), [])      #Public holidays
1257         popmenu = wx.Menu()
1258         if holidays_available and self._cal_PublicHolidays:
1259             country, subdiv, language = self._cal_PublicHolidays
1260         else:
1261             country = subdiv = language = ''
1262         hdr = msg = ''
1263         if country:
1264             hdr = "Events for "+click_date.GetMonthName(click_date.month)+" "
1265             hdr += str(click_date.year)+"\nPublic Holidays for "+self.country_name
1266             if subdiv:
1267                 hdr += " region "+subdiv
1268         else:
1269             hdr = 'Events for '+click_date.GetMonthName(click_date.month)+" "+str(click_date.year)
1270 
1271         for item in self._cal_AddPublicHolidays:
1272             country, subdiv, language = item
1273             for c in holidays.registry.COUNTRIES.values():
1274                 if country in c:
1275                     name = c[0]
1276                     msg += "\n+ "+name
1277                     break
1278         popmenu.Append(wx.ID_ANY, hdr+msg)
1279         popmenu.AppendSeparator()
1280 
1281         for k, v in sorted(self.Notes.items()):
1282             if k[0] == click_date.year and k[1] == click_date.month + 1:
1283                 if k[2] in hol0 or k[2] in hol1 or k[2] in hol2:
1284                     bmp = cal_fiesta.Bitmap
1285                 else:
1286                     bmp = cal_event.Bitmap
1287                 msg = str(k[2]).zfill(2)+ " "+ v
1288                 m1 = wx.MenuItem(popmenu, wx.ID_ANY, msg)
1289                 m1.SetBitmap(wx.Bitmap(bmp))
1290                 popmenu.Append(m1)
1291         mlen = popmenu.GetMenuItemCount()
1292         if mlen <= 2:
1293             m1 = wx.MenuItem(popmenu, wx.ID_ANY, "No events or holidays")
1294             m1.SetBitmap(wx.Bitmap(cal_noevent.Bitmap))
1295             popmenu.Append(m1)
1296         self.PopupMenu(popmenu)
1297         popmenu.Destroy()
1298 
1299     def LoadDataFrom(self, CalSaveFile=None):
1300         import os
1301         import ast
1302 
1303         self.CalSaveFile = CalSaveFile
1304 
1305         self._cal_MarkDates = {}
1306         self._cal_Holidays = {}
1307         self._cal_RestrictDates = {}
1308         self._cal_Notes = {}
1309         self._cal_PublicHolidays = []
1310         self._cal_AddPublicHolidays = []
1311         lowerdate = wx.DefaultDateTime
1312         upperdate = wx.DefaultDateTime
1313 
1314         if os.path.isfile(CalSaveFile):
1315             if os.path.getsize(CalSaveFile) > 0:
1316                 try:
1317                     with open(CalSaveFile, 'r') as f:
1318                         self._cal_Notes = ast.literal_eval(f.readline())
1319                         self._cal_Holidays = ast.literal_eval(f.readline())
1320                         self._cal_PublicHolidays = ast.literal_eval(f.readline())
1321                         self._cal_AddPublicHolidays = ast.literal_eval(f.readline())
1322                         self._cal_RestrictDates = ast.literal_eval(f.readline())
1323                         self._cal_MarkDates = ast.literal_eval(f.readline())
1324                         dr = ast.literal_eval(f.readline())
1325                         try:
1326                             lowerdate = wx.DateTime(dr[0][0], dr[0][1], dr[0][2])
1327                         except Exception as e:
1328                             lowerdate = wx.DefaultDateTime
1329                         try:
1330                             upperdate = wx.DateTime(dr[1][0], dr[1][1], dr[1][2])
1331                         except Exception as e:
1332                             upperdate = wx.DefaultDateTime
1333                 except Exception as e:
1334                     print(str(e))
1335             else:
1336                 return False
1337         else:
1338             return False
1339         self.SetHolidays(self._cal_Holidays)
1340         self.SetNotes(self._cal_Notes)
1341         self.SetPublicHolidays(self._cal_PublicHolidays)
1342         self.SetRestrictDates(self._cal_RestrictDates)
1343         self.SetMarkDates(self._cal_MarkDates)
1344         self.SetCalendarDateRange(lowerdate=lowerdate, upperdate=upperdate)
1345 
1346     def SaveData(self, CalSaveFile=None):
1347         if not CalSaveFile:
1348             CalSaveFile = self.CalSaveFile
1349 
1350         with open(CalSaveFile, 'w') as f:
1351             f.write(str(self._cal_Notes)+'\n')
1352             f.write(str(self._cal_Holidays)+'\n')
1353             f.write(str(self._cal_PublicHolidays)+'\n')
1354             f.write(str(self._cal_AddPublicHolidays)+'\n')
1355             f.write(str(self._cal_RestrictDates)+'\n')
1356             f.write(str(self._cal_MarkDates)+'\n')
1357             if self._cal_daterange[0] == wx.DefaultDateTime:
1358                 date0 = []
1359             else:
1360                 date0 = [self._cal_daterange[0].day, self._cal_daterange[0].month, self._cal_daterange[0].year]
1361             if self._cal_daterange[1] == wx.DefaultDateTime:
1362                 date1 = []
1363             else:
1364                 date1 = [self._cal_daterange[1].day, self._cal_daterange[1].month, self._cal_daterange[1].year]
1365             dr = [date0, date1]
1366             f.write(str(dr)+'\n')
1367 
1368 class YearPopup(wx.Frame):
1369     def __init__(self, parent, txt=''):
1370         wx.Frame.__init__(self, parent, wx.ID_ANY, title="Year Calendar", size=(600, 660))
1371         panel = wx.Panel(self)
1372         font = wx.Font(10, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL, False)
1373         tc = wx.TextCtrl(panel, -1, value=txt, size=(560, 640), style=wx.TE_MULTILINE|wx.TE_READONLY)
1374         tc.SetFont(font)
1375         butt = wx.Button(panel, -1, "&Quit")
1376         sizer = wx.BoxSizer(wx.VERTICAL)
1377         sizer.Add(tc, 0, wx.ALL|wx.EXPAND, 10)
1378         sizer.Add(butt, 0, wx.ALL, 10)
1379         panel.SetSizer(sizer)
1380         sizer.Fit(panel)
1381         sizer.Fit(self)
1382         self.Layout()
1383         butt.SetFocus()
1384         butt.Bind(wx.EVT_BUTTON, self.OnQuit)
1385         self.Bind(wx.EVT_CLOSE, self.OnQuit)
1386         self.Show()
1387 
1388     def OnQuit(self, event):
1389         self.Destroy()
1390 
1391 
1392 class DemoFrame(wx.Frame):
1393     '''
1394         This demonstration code attempts to provide at least one example of every option, even if it's commented out
1395         It may offer examples of various options for the same thing, which explains its rather messy look
1396         The bulk of the marked dates, holidays, restrictions and notes are set around August 2023, when the testing
1397         was performed, so feel free to navigate to that month or change the values.
1398     '''
1399     def __init__(self, parent):
1400         wx.Frame.__init__(self, parent, -1, size=(400, 300), title="Calender Demo")
1401 
1402         #format = (lambda dt:
1403         #    (f'{dt.GetWeekDayName(dt.GetWeekDay())} {str(dt.day).zfill(2)}/{str(dt.month+1).zfill(2)}/{dt.year}')
1404         #    )
1405 
1406         #format = (lambda dt: (f'{dt.Format("%a %d-%m-%Y")}'))
1407 
1408         #Using a strftime format converting wx.DateTime to datetime.datetime
1409         #format = (lambda dt: (f'{wx.wxdate2pydate(dt).strftime("%A %d-%B-%Y")}'))
1410 
1411         format = (lambda dt: (f'{dt.Format("%A %d %B %Y")}'))
1412 
1413         panel = wx.Panel(self)
1414 
1415         self.ctl = CalendarCtrl(panel, -1, date=0, formatter=format, calendar_locale=wx.LANGUAGE_GERMAN)
1416 
1417         x=datetime.datetime.now()
1418 
1419         self.ctl.SetValue(0)
1420 
1421         self.ctl.LoadDataFrom("CalSaveFile.txt")
1422 
1423         font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
1424         font.SetFractionalPointSize(16)
1425         self.ctl.SetCalendarFont(font)                                              # Set Calendar Font
1426         self.ctl.SetCalendarStyle(wx.adv.CAL_SHOW_WEEK_NUMBERS|wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SHOW_HOLIDAYS)
1427         self.ctl.SetBackgroundColour('#e1ffe1')                                     # lightgreen
1428         self.ctl.SetCalendarHeaders(colFg='#ff0000', colBg='#90ee90')               # red/lightgreen
1429         self.ctl.SetWeekendColours(colFg='#ff0000', colBg='')                       # Weekends red/None
1430         self.ctl.SetCalendarHolidayColours(colFg=wx.NullColour, colBg='#ffff00')    # Holidays None/Yellow
1431         self.ctl.SetCalendarBg(colBg='#f0ffff')                                     # Background Colour Azure
1432         # Mark Border NONE, SQUARE or ROUND + Colour + foreground colour and background colour
1433         self.ctl.SetCalendarMarkBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.BLUE, colFg=wx.NullColour, colBg=wx.NullColour)
1434         # Notes Border NONE, SQUARE or ROUND + Colour + foreground colour and background colour
1435         self.ctl.SetCalendarMarkNotes(border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.GREEN, colFg=wx.NullColour, colBg='#ffbf00')
1436 
1437         # Some other options
1438         #self.ctl.SetFormatter(format)                                              # Set format separately
1439         #self.ctl.SetValue(x.timestamp())                                           # Set Date
1440         #self.ctl.SetValue(wx.DateTime.Today())
1441         #self.ctl.SetCalendarHighlights(colFg='#ff0000', colBg='#90ee90')           # Highlight selected day red/lightgreen
1442         #self.ctl.SetCalendarFg(colFg='#0000ff')                                    # Foreground Colour Blue
1443         #self.ctl.SetCalendarMarkNotes(border=0, bcolour=wx.NullColour)             # Turn off borders for notes
1444         #self.ctl.SetCalendarOnlyWeekDays(True)                                     # Only non holiday weekdays are selectable
1445         #self.ctl.SetToolTip('Struck through dates are not selectable')
1446 
1447         #self.ctl.SetCalendarMarkDates({
1448         #                               (0, 1) : [-11,-23,1],                   # 1st Monday, 3rd Tuesday and the 1st January
1449         #                               (x.year, 7) : [2,5,30],
1450         #                               (x.year, 8) : [12,20,27],
1451         #                               (0, 9) : [1,27,-98]                     # 1st, 27th and the last weekday of September
1452         #                              })
1453 
1454         #self.ctl.SetCalendarHolidays({
1455         #                             (0, 1) : [1,],                                 # January 1st every year
1456         #                             (0, 2) : [1],
1457         #                             (x.year, 8) : [15],
1458         #                             (0, 12) : [25,26]                              # 25th & 26th December every year
1459         #                            })
1460 
1461         #self.ctl.SetCalendarNotes({
1462                                      #(0, 0, -55) : "Last Friday of the month - Pay Day!",# Last Friday, every month. every year
1463         #                             (0, 1, 1) : "New Year's Day\n2nd Note",             # January 1st every year
1464         #                             (0, 1, -11) : "First Monday of the year",
1465         #                             (0, 1, -23) : "3rd Tuesday of the year",
1466         #                             (0, 1, -99) : "Last day of January",
1467         #                             (0, 1, -35) : "The last Wednesday of January",
1468         #                             (0, 2, 1) : "1st February",
1469         #                             (x.year, 2, -5)  : "Every Friday in February",      # This year only
1470         #                             (x.year, 8, 7) : "Marked for no reason whatsoever", # This year only
1471         #                             (x.year, 8, 15) : "A holiday August 15 2023",       # This year only
1472         #                             (x.year, 8, 20) : "Marked for reason X",            # this year only
1473         #                             (0, 9, -98) : "Last weekday of September",
1474         #                             (0, 12, 25) : "Merry Christmas!",
1475         #                             (0, 2, -99) : "Last day of February"
1476         #                            })
1477 
1478         #self.ctl.SetCalendarRestrictDates({
1479         #                               (0, 1) : [-1, -2, 5],     # exclude Mondays and Tuesdays, 5th of January All years
1480         #                               (0, 2) : [-99,],          # exclude last day of February - All years
1481         #                               (0, 8) : [-98,]           # exclude last weekday of August - All years
1482         #                                })
1483 
1484         # Restrict Calendar to a date range (define a lowerdate or an upperdate or both
1485         # This MUST be a wx.DateTime, which is CONFUSING
1486         # But the months run from 0 to 11 --- below is the 23rd October 2023 to 23 December 2024
1487         #self.ctl.SetCalendarDateRange(lowerdate=wx.DateTime(23,9,2023), upperdate=wx.DateTime(23,11,2024))
1488 
1489         # Public Holidays requires the python holidays module (pip install --upgrade holidays)
1490         #self.ctl.AddPublicHolidays(country="DE", subdiv="ST", language="")    # Germany region Lower Saxony
1491         #self.ctl.AddPublicHolidays(country="GB", subdiv="ENG", language="")   # Great Britain region England
1492         #self.ctl.AddPublicHolidays(country="ES", subdiv="AN", language="")    # Spain region Andalucia
1493         #self.ctl.AddPublicHolidays(country="JP", subdiv="", language="en_US")    # Japan
1494 
1495         self.ctl.Bind(wx.adv.EVT_CALENDAR_MONTH, self.OnCalEvt)
1496         self.ctl.Bind(wx.adv.EVT_CALENDAR_YEAR, self.OnCalEvt)
1497         self.ctl.Bind(wx.adv.EVT_CALENDAR, self.OnCalEvt)
1498         self.ctl.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnCalEvt)
1499         self.ctl.Bind(EVT_DATE_CHANGED, self.OnEvent)
1500         self.Bind(wx.EVT_CLOSE, self.OnQuit)
1501 
1502         sizer=wx.BoxSizer(wx.HORIZONTAL)
1503         sizer.Add(self.ctl, 0, wx.EXPAND, 0)
1504         panel.SetSizerAndFit(sizer)
1505         self.Centre()
1506 
1507 
1508     def OnEvent(self, event):
1509         # ExtendedCalendarCtrl events
1510         print("Extended Calendar Event", event.GetEventType())
1511         obj = event.GetEventObject()
1512         print("\nevt", event.GetValue())
1513 
1514         x = event.GetDate() # wx.DateTime object
1515         print("evt", x)
1516         print("Day", x.GetDayOfYear(),"Week", x.GetWeekOfYear(), "Name", x.GetWeekDayName(x.GetWeekDay()))
1517 
1518         print("Datetime", event.GetDateTime()) # datetime.datetime object
1519         print("Timestamp", event.GetTimeStamp())
1520         print("Value", event.GetValue())
1521         print("Notes", event.GetNote())
1522         print("Marked", event.IsMarked())
1523         print("Calendar Holiday", event.IsHoliday())
1524         print("Public Holiday", event.IsPublicHoliday())
1525         print("Locale", obj.GetLocale())
1526 
1527     def OnCalEvt(self, event):
1528         # Internal wx.CalendarCtrl events
1529         obj = event.GetEventObject()
1530         print("wxCalendar Event", event.GetEventType())
1531 
1532     def OnQuit(self, event):
1533         #if self.ctl.CalSaveFile:
1534         self.ctl.SaveData(self.ctl.CalSaveFile)
1535         self.Destroy()
1536 
1537 if __name__ == '__main__':
1538     app = wx.App()
1539     frame = DemoFrame(None)
1540     frame.Show()
1541     app.MainLoop()


Download source

source.zip


Additional Information

Link :

- - - - -

https://wiki.wxpython.org/TitleIndex

https://docs.wxpython.org/

As the source is too heavy for wxpywiki, please use the link to download all useful files :

Latest version here : https://discuss.wxpython.org/t/extendedcalendarctrl/36680


Thanks to

J. Healey (ActiveText.py coding), the wxPython community...


About this page

Date(d/m/y) Person (bot) Comments :

27/11/23 - Ecco (Created page for wxPython Phoenix).


Comments

- blah, blah, blah....

Extended Calendar Control (Phoenix) (last edited 2023-11-28 10:07:35 by Ecco)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.