A wx.DatePickerCtrl with a customisable format - Part 2 (Phoenix)

Keywords : DatePickerCtrl.


Demonstrating :

Tested py3.x, wx4.x and Linux/Win10.

Are you ready to use some samples ? ;)

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


Introduction :

Inspired by the need to set a deadline, where not only were a date and time required but also the ability to return a timestamp, which is easy to store in a database, easily sorted and easily converted back into a date. I’ve endeavoured to make this as friendly as possible, allowing for the retrieving of a wx.DateTime, a datetime, a string or a timestamp.


Sample one

Last version here : https://discuss.wxpython.org/t/a-wx-datepickerctrl-with-a-customisable-format/36295

img_sample_1.jpg

img_sample_2.jpg

   1 """
   2     MiniDatePickerButton.py
   3 
   4     A custom class that looks like the wx.DatePickerCtrl but with the ability to customise
   5     the calendar and the output format. (DatePickerCtrl is seemingly stuck with MM/DD/YYYY format)
   6     Works with wx.DateTime or python datetime values
   7     With or without a fixed calendar image
   8     If the image is included, it goes on the opposite side of the text style. wx.LEFT image on the right.
   9      (If the text_style is wx.TE_CENTRE it defaults left)
  10     Uses wx.adv.GenericCalendarCtrl
  11     Uses locale to enable different languages for the calendar
  12 
  13     An attempt has been made to allow days marked with attributes denoting Holiday, Marked, Restricted to live with each other
  14 
  15     Dates can be marked, restricted, defined as holidays and have ToolTip notes
  16      Marked and Restricted dates can be defined as a simple date or use more advanced rules for example the 3rd Friday of the month
  17      or every Tuesday of the month. Note: they can be year specific or every year
  18 
  19     Marked dates are marked with a Border, either Square or Oval
  20     Holidays are normally highlighted with a different Foreground/Background colour
  21     Restricted dates are marked using an Italic StrikeThrough font
  22 
  23     Defined Holidays can be year specific or every year on that Month/Day
  24 
  25     Official Holidays rely on the python 'holidays' package being available (pip install --upgrade holidays)
  26         official holidays are automatically entered into the Notes for you
  27     You may add in more than one region's official holidays and they will be denoted by the country code
  28      and region code if appropriate.
  29 
  30     Notes are date specific or every year and can follow the rules for Marked and Restricted dates, namely:
  31             All of a specified week day in a month;
  32             The specified occurrence of a weekday in a month e.g. 3rd Tuesday or last Friday of the month
  33             The last day of the month
  34             The last weekday of the month
  35 
  36     Navigation:
  37      The Escape key will exit the Calendar
  38      The Arrow keys will navigate the calendar
  39      The PageUp/PageDown keys will retreat and advance and the month respectively, as will MouseScrollUp and MouseScrollDown
  40      The Home and End keys jump to the First and Last day of the month, respectively.
  41      A right click on the calendar on a blank day, will display All the notes for the month.
  42      Date ToolTips will be displayed as and when appropriate, depending of the position in the calendar and settings
  43 
  44     MiniDatePicker(parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
  45                  style=wx.BORDER_SIMPLE, name="MiniDatePicker", date=0, formatter='', image=True):
  46 
  47         @param parent:   Parent window. Must not be None.
  48         @param id:       identifier. A value of -1 indicates a default value.
  49         @param pos:      MiniDatePicker position. If the position (-1, -1) is specified
  50                          then a default position is chosen.
  51         @param size:     If the default size (-1, -1) is specified then a default size is calculated.
  52                          Size should be able to accomodate the specified formatter string + image
  53         @param style:    Alignment (Left,Middle,Right).
  54         @param name:     Widget name.
  55         @param date:     Initial date (an invalid date = today)
  56         @param formatter A date formatting string in the form of a lambda function
  57                          The formatter will be called with a wx.DateTime thus we can use .Format()
  58                           the wxPython version of the standard ANSI C strftime
  59                          default lambda dt: dt.FormatISODate()
  60                           = ISO 8601 format "YYYY-MM-DD".
  61                          or a lambda function with a format string e.g.:
  62                             lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
  63                          e.g.:
  64                             format = lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
  65                             format = lambda dt: (f'{dt.Format("%A %d %B %Y")}')
  66                          or
  67                             fmt = "%Y/%m/%d"
  68                             format = lambda dt: (dt.Format(fmt))
  69                             format = lambda dt: (dt.Format("%Y/%m/%d"))
  70                             format = lambda dt: (dt.FormatISODate())
  71                         for those who prefer strftime formatting:
  72                             format = (lambda dt: (f'{wx.wxdate2pydate(dt).strftime("%A %d-%B-%Y")}'))
  73         @param image     Show image or Not (True or False) Default True
  74 
  75     TextCtrl Styles:
  76             wx.RIGHT
  77             wx.LEFT
  78             wx.TE_CENTRE
  79 
  80             wx.BORDER_NONE is always applied to the internal textctrl
  81             wx.BORDER_SIMPLE is the default border for the control itself
  82 
  83     Events: EVT_DATE_CHANGED A date change occurred in the control
  84 
  85     Event Functions:
  86         GetValue()          Returns formatted date in the event as a string
  87 
  88         GetDate()           Returns wxDateTime date in the event, with all of its attendant functions
  89 
  90         GetDateTime()       Returns python datetime of date in the event
  91 
  92         GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected date
  93 
  94     Functions:
  95         GetValue()          Returns formatted date in the event as a string
  96 
  97         GetDate()           Returns wxDateTime date in the control
  98 
  99         GetDateTimeValue()  Returns python datetime of date in the control
 100 
 101         GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected date
 102 
 103         GetLocale()         Returns tuple of current language code and encoding
 104 
 105         SetValue(date)      Sets the date in the control
 106                             expects a wx.DateTime, a python datetime datetime or a timestamp
 107                             Any invalid date defaults to wx.DateTime.Today()
 108 
 109         SetButtonBitmap(bitmap=None)  Set a specified image to used for the Button
 110                                       This is also used for the Focus image unless overridden (see below)
 111                                       You may use a file name or a wx.Bitmap
 112 
 113         SetButtonBitmapFocus(bitmap=None)  Set a specified image to used for the Button focus
 114                                       You may use a file name or a wx.Bitmap
 115 
 116         SetFormatter(formatter) Date format in the form of a lambda
 117             default:    lambda dt: dt.FormatISODate()
 118 
 119 
 120         SetLocale(locale)   Set the locale for Calendar day and month names
 121                              e.g. 'de_DE.UTF-8' German
 122                                   'es_ES.UTF-8' Spanish
 123                              depends on the locale being available on the machine
 124 
 125         SetCalendarStyle(style)
 126             wx.adv.CAL_SUNDAY_FIRST: Show Sunday as the first day in the week
 127             wx.adv.CAL_MONDAY_FIRST: Show Monday as the first day in the week
 128             wx.adv.CAL_SHOW_HOLIDAYS: Highlight holidays in the calendar
 129             wx.adv.CAL_NO_YEAR_CHANGE: Disable the year changing (deprecated, only generic)
 130             wx.adv.CAL_NO_MONTH_CHANGE: Disable the month (and, implicitly, the year) changing
 131             wx.adv.CAL_SHOW_SURROUNDING_WEEKS: Show the neighbouring weeks in the previous and next months
 132             wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION: more compact, style for the month and year selection controls.
 133             wx.adv.CAL_SHOW_WEEK_NUMBERS
 134 
 135         SetCalendarHighlights(colFg, colBg)         Colours to mark the currently selected date
 136 
 137         SetCalendarHolidayColours(colFg, colBg)     Colours to mark Holidays
 138 
 139         SetCalendarHeaders(colFg, colBg)
 140 
 141         SetCalendarFg(colFg)    Calendar ForegroundColour
 142 
 143         SetCalendarBg(colBg)    Calendar & Ctrl BackgroundColour
 144 
 145         SetCalendarFont(font=None)  Set font of the calendar to a wx.Font
 146                 Alter the font family, weight, size, etc
 147 
 148         SetCalendarMarkDates(markdates = {}) Mark dates with a Border
 149                 A dictionary containing year and month tuple as the key and a list of days for the values to be marked
 150                 e.g.
 151                     {
 152                      (2023, 7) : [2,5,7,11,30],
 153                      (2023, 8) : [7,12,13,20,27],
 154                      (2023, 9) : [1,27]
 155                     }
 156 
 157                 Values of less than 0 indicate not a specific date but a day: -1 Monday, -2 Tuesday, ... -7 Sunday
 158                  allowing you to mark all Mondays and Fridays in the month of January e.g {(2023, 1) : [-1, -5]}
 159                  You may include a mixture of negative and positive numbers (days and specific dates)
 160 
 161                 Negative values beyond that indicate the nth weekday, (the indexing is a bit confusing because it's off by 1
 162                  the first digit represents the day and the 2nd digit represents the occurrence i.e.
 163 
 164                 -11, 1st Monday | -12, 2nd Monday | -13, 3rd Monday | -14, 4th Monday | -15, 5th or Last Monday
 165                 -21, 1st Tuesday | -22, 2nd Tuesday | -23, 3rd Tuesday | -24, 4th Tuesday | -25, 5th or Last Tuesday
 166                 ..............................................................................................................
 167                 -71, 1st Sunday | -72, 2nd Sunday | -73, 3rd Sunday | -74, 4th Sunday | -75, 5th or Last Sunday
 168 
 169                 If the 5th occurrence of a weekday doesn't exist, the last occurrence of the weekday is substituted.
 170 
 171                 -99 Stands for the last day of the month
 172                 -98 is for the last weekday of the month
 173 
 174         SetCalendarMarkBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.NullColour)
 175                 Defines the border type to mark dates wx.adv.CAL_BORDER_SQUARE (default)
 176                 and a border colour e.g. wx.NullColour (Default), wx.RED  or a hex value '#800080' etc
 177                 Valid border values are:
 178                     wx.adv.CAL_BORDER_NONE      - 0
 179                     wx.adv.CAL_BORDER_SQUARE    - 1
 180                     wx.adv.CAL_BORDER_ROUND     - 2
 181 
 182 
 183         SetCalendarHolidays(holidays = {})
 184                 A dictionary containing year and month tuple as the key and a list of days for the values e.g.
 185                     {
 186                      (2023, 1) : [1,],
 187                      (2023, 7) : [1,30],
 188                      (2023, 8) : [7,15,27],
 189                      (2023, 12) : [25,26]
 190                     }
 191 
 192                 Holidays can also be 'fixed' Holidays occurring every year on the same day by setting the year to zero in the key
 193                 e.g.
 194                     {
 195                      (0, 1) : [1,],                            # January 1st is a Holiday every year
 196                      (2023, 7) : [1,30],
 197                      (2023, 8) : [7,15,27],
 198                      (0, 12) : [25,26]                         # Christmas Day and Boxing Day are Holidays every year
 199                     }
 200 
 201         SetCalendarNotes(notes = {})
 202                 A dictionary containing a year, month, day tuple as the key and a string for the note e.g.
 203                     {
 204                      (2023, 1, 1) : "New Year's Day",
 205                      (2023, 12, 25) : "Christmas Day"
 206                     }
 207 
 208                 Like Holidays, Notes can be assigned to a specific day every year
 209                     {
 210                      (0, 1, 1) : "New Year's Day",
 211                      (0, 12, 25) : "Christmas Day"
 212                     }
 213 
 214                 To compliment Marked Dates and Restricted Dates, notes can also be assigned a negative day following the
 215                  the same pattern as Marked Dates and Restricted Dates.
 216                 Allowing you to match Notes with Marked Dates and Restricted Dates.
 217 
 218                     {
 219                      (0, 1, -11) : "The 1st Monday of January/the year",
 220                      (0, 1, -35) : "The last Wednesday of January",
 221                      (0, 2, -5)  : "Every Friday in February"
 222                     }
 223 
 224                 If you set Official Holidays, they are enter automatically into the notes, marked with a leading asterix (*).
 225 
 226                 Notes are displayed as a ToolTip, when the day is hovered over or Right clicked
 227                  or if the mouse is over the calendar and the Arrow keys are used to navigate the calendar to that day.
 228 
 229                 A right click on the calendar on a blank day, will display All the notes for the month.
 230 
 231         SetCalendarRestrictDates(rdates = {})
 232                 A dictionary containing a year and month tuple as the key and a list of days, for the days that
 233                  are Not selectable within that year/month i.e. the reverse of Marked Dates
 234                  e.g.
 235                     {
 236                      (2023, 1) : [1,15],
 237                      (2023, 3) : [1,15],
 238                      (2023, 5) : [1,15],
 239                      (2023, 7) : [1,15,23],
 240                      (2023, 9) : [1,15],
 241                      (2023, 11) : [1,15]
 242                     }
 243 
 244                 All dates in the 'restricted' dictionary use an Italic StruckThrough font and cannot be selected
 245 
 246                 See SetCalendarMarkDates for the ability to use negative values to calculate dictionary values to restrict
 247                 more complicated entries like All Mondays or the 2nd and 4th Tuesday for example, by using negative values.
 248 
 249         SetCalendarDateRange(lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime)
 250                 Either 2 wx.DateTime values to restrict the selectable dates
 251                 or just a lower date or just an upper date
 252                 (The oddity of wx.DateTime months from 0 is catered for)
 253                 Returns False if the dates are not wx.DateTime objects
 254                 wx.DefaultDateTime equals no date selected.
 255 
 256                 Dates outside of the range will display an "Out of Range" ToolTip, with the defined range.
 257 
 258         SetCalendarOnlyWeekDays(boolean) Default False
 259                 If set only weekdays are selectable. weekends and holidays use an Italic StruckThrough font and cannot be selected
 260                 Holidays are treated as Not a weekday i.e. no work
 261 
 262         AddOfficialHolidays(country='', subdiv='', language='') Default blank, blank, blank
 263                 Only available if the python 'holidays' module was successfully imported
 264                 Currently supports 134 country codes using country ISO 3166-1 alpha-2 codes and the optional subdivision
 265                  (state, region etc) using ISO 3166-2 codes.
 266                 Language must be an ISO 639-1 (2-letter) language code. If the language translation is not supported
 267                 the original holiday names are returned.
 268 
 269                 For details: https://python-holidays.readthedocs.io/en/latest/
 270                  (or the file 'python-holidays — holidays documentation.html' supplied with this program)
 271                 e.g.
 272                     country='ES'                    Spain
 273                     country='ES' and subdiv='AN'    Spain, Andalucia
 274                     country='UK' and subdiv='ENG'   United Kingdom, England
 275                     country='US' and subdiv='SC'    USA, South Carolina
 276 
 277                 function returns True if successful, an existing country and subdivision (if supplied)
 278                  or False if there was an error
 279 
 280                 This function can be called multiple times, once for each country or region in a country
 281                  that you wish marked on the calendar.
 282                 The first call sets the primary holiday region.
 283                 May be useful if you are operating in more than one geographical area, with differing holidays
 284 
 285     Default Values:
 286         date    -       Today
 287         style   -       READ_ONLY
 288 
 289 Author:     J Healey
 290 Created:    04/12/2022
 291 Copyright:  J Healey - 2022-2023
 292 License:    GPL 3 or any later version
 293 Email:      <rolfofsaxony@gmx.com>
 294 Version     1.5
 295 
 296 A thank you to Richard Townsend (RichardT) for the inspiration of the date dictionaries for some of the functions.
 297 
 298 Changelog:
 299 1.5     Add optional holidays package
 300         A fast, efficient Python library for generating country and subdivision- (e.g. state or province) specific
 301          sets of government-designated holidays on the fly.
 302         Alter the font_family, weight, size etc of the calendar popup
 303         New functions:
 304             AddOfficialHolidays - to add Official holiday zones using 'python holidays package'
 305             SetCalendarFont()   - Change calendar font_family, weight, style, size etc
 306         Fix for MSW, added style wx.PU_CONTAINS_CONTROLS to the Popup, which allows the month choice drop down
 307          to function
 308 
 309 1.4     SetCalendarDateRange
 310          allows the restriction of dates to a range, that can be selected
 311         SetCalendarNotes
 312             Allows for a note to be assigned to individual days in the calendar.
 313             If notes exist for a day, when hovered over the ToolTip will display the note.
 314             Envisaged to be used in conjunction with Holidays and Marked days to provide detail
 315         SetCalendarRestrictDates
 316             Set a dictionary of dates that are Not selectable
 317         SetCalendarOnlyWeekDays
 318             Only weekdays are selectable i.e. weekends and holidays are not
 319 
 320 1.3     New Functions
 321          SetCalendarHolidayColours
 322          SetCalendarHolidays
 323          SetCalendarMarkBorder
 324          SetCalendarMarkDates
 325         Permit the definition of Holidays and key dates to be highlighted and the method of highlighting;
 326          by colour for holidays and border for marked days
 327 
 328 1.2     New function SetButtonBitmap()
 329         Allows a specified image to be used for the button
 330         (also allows bitmap from wx.ArtProvider to be used)
 331 
 332         New function SetButtonBitmapFocus()
 333         Allow a specified image to be used for the button focus
 334 
 335 1.1     subclass changes from wx.Control to wx.Panel to handle tab traversal
 336         The image will indicate Focus
 337         Demonstration colours set to Hex
 338 
 339 Usage example:
 340 
 341 import wx
 342 import minidatepickerbutton as MDB
 343 class Frame(wx.Frame):
 344     def __init__(self, parent):
 345         wx.Frame.__init__(self, parent, -1, "MiniDatePicker Demo")
 346         format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y")}'))
 347         panel = wx.Panel(self)
 348         mdb = MDB.MiniDatePickerButton(panel, -1, pos=(50, 50), style=0, date=0, formatter=format)
 349         self.Show()
 350 
 351 app = wx.App()
 352 frame = Frame(None)
 353 app.MainLoop()
 354 
 355 """
 356 
 357 import wx
 358 import wx.adv
 359 from wx.lib.embeddedimage import PyEmbeddedImage
 360 import datetime
 361 import calendar
 362 import locale
 363 try:
 364     import holidays
 365     holidays_available = True
 366 except ModuleNotFoundError:
 367     holidays_available = False
 368 
 369 img = PyEmbeddedImage(
 370     b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABg2lDQ1BJQ0MgcHJvZmlsZQAA'
 371     b'KJF9kT1Iw0AcxV9TS0UiDu0g4pChOlkQFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOL'
 372     b's64OroIg+AHi6OSk6CIl/i8ptIj14Lgf7+497t4BQqPCdLtnHNANx0onE1I2tyqFXyEihAhi'
 373     b'EBVmm3OynELX8XWPAF/v4jyr+7k/R7+WtxkQkIhnmWk5xBvE05uOyXmfOMpKikZ8Tjxm0QWJ'
 374     b'H7mu+vzGueixwDOjViY9TxwlloodrHYwK1k68RRxTNMNyheyPmuctzjrlRpr3ZO/UMwbK8tc'
 375     b'pzmMJBaxBBkSVNRQRgUO4rQapNhI036ii3/I88vkUslVBiPHAqrQoXh+8D/43a1dmJzwk8QE'
 376     b'EHpx3Y8RILwLNOuu+33sus0TIPgMXBltf7UBzHySXm9rsSNgYBu4uG5r6h5wuQMMPpmKpXhS'
 377     b'kKZQKADvZ/RNOSByC/St+b219nH6AGSoq9QNcHAIjBYpe73Lu3s7e/v3TKu/H0FDcpMpQj/v'
 378     b'AAAACXBIWXMAAAsTAAALEwEAmpwYAAACk0lEQVRIx43W3+unQxQH8Nc83w9tVutHNj9v5M4q'
 379     b'tty7oC2u2Fy58KOV9g/AlRJygQvlRrIlpbhSW7uJorCKlKQoJand7AVJYm3Z7zMu9v18zWfM'
 380     b'J6aeZs7MmTNnznm/zzwFE6p/WunkLczNXEnfyhO2Rza2rLfpP4xPG4zPm2xsNR6NPN/uNuq8'
 381     b'nAa3W4tGaRQrrsZFkX/FIbyO3dG7PHq/RP4d9+NIs/YHTi+OrzKYcS2OZtNpvIqbcQfuyqFv'
 382     b'pH80e45hP26JM1fhYtyDHzGvUGuttZSyB3/hWdyAn3P4KXwYg+dywAfx9mTmf8JH+A5P45Ls'
 383     b'Ox/XUkpJzM/kJofxJ17JjR7GTfgGX2EfHsHZ3PRM5OsynvvEtTCrGR+Ih3fmJvsTgmsSEtE5'
 384     b'lb5N7g7qVgO0LIk/FM9r5N14uUPM3TiY/bVbK5hbDPdEaudq8/VQnBu4Lzo7XJiWJA8Y2reK'
 385     b'5/F4jN6L9/EaHsQFsVewHZtWSfLUeTBqBbfitxi6HTfiisjn1pTPA2daNZ7PDengh8R9b9Cz'
 386     b'L0g6G73bcGUguSv7/1UypkFYlnA9iZfwPR4K+V7EM/H2cIh2JGG7sCszFXW1oZDNeA57Ujre'
 387     b'CkSfSClYpYRcH3Ie7EJU2ySPilnBCXwW4rwdKH4axlYcDwe+xnsbSvlO1vt6Lsa/TGH7OP0X'
 388     b'+Dy6J/BtdD5pbK1BfjVg8aL4QlMIj2btscaBN5s9D3RkW4hWpwaztXmAapfwTdAdEbOF/BoP'
 389     b'JlyamD/VvU6tV/OgrCxzu3BZq7NqFE/iXdzXkW5UOspAZznonaVUY6t9zeRdKMu4YeVUa507'
 390     b'lg51lrXlPS+dh6MwTJibw6dBSdn4J1K6R39ofPDH8L9/c/4GBa36v+mJzSMAAAAASUVORK5C'
 391     b'YII=')
 392 
 393 imgf = PyEmbeddedImage(
 394     b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAA'
 395     b'CXBIWXMAAAsTAAALEwEAmpwYAAADf0lEQVRIx+2V3yusWxjHP2u9o8Y7k92e6DQ1U3KUGiXp'
 396     b'iDZGUepEtF1RdlImuVEucCGk1K7hD9BEfuz2nXBzqIkLdS7VODXDldygEfmVkTBr7at3mjFj'
 397     b'37k7z9X7Pms96/v8+H7XEt/3vms+0CQfbB8OYEv/+fbnNx4fH1FKsXGxgfpX0dvbSyKRQGvN'
 398     b'9fU1QghcLhdaa5xOJ6urq4g6wdc/viKEwOFw8OP4RzZAT0kP7e3tOJ1O3G43gUCA0H8hdnZ2'
 399     b'2NraQilFT08PSinm5uaQUtLW1kYkEiHwV4CpqSni8TgPDw9sbGzw8+RnJsD9/T15eXmMj49z'
 400     b'fHxMYWEhWms8Hg+NjY1IKbHZbCilaGpqQgiB1+tFSklRURF+v5/S0lImJye5u7vLnkEymcQ0'
 401     b'Tc7Ozpifnyc/P5+BgQGcTicLCwtEo1F8Ph8VFRXEYjFCoRB2u51AIIBpmoRCIU5PTzFNEyll'
 402     b'NoBhGAAIITAMg3A4jNfrZXt7G601kUiEeDzO+fk5BwcHAITDYTweD+FwGCFE6tD0b9vbqWut'
 403     b'0VqzuLjIwsICQgi01iQSCQYHB1PBWms2NzdZX1/HMAyEEBlrWRXkWrR8VrAFlp6hlJJkMpny'
 404     b'CyFQSmUDWAenB6ebEILR0VGCwSBSStbW1mhubqavr4/l5WVeXl5QSqG1TrU7o0XpGeQyrTX7'
 405     b'+/sUFBSglGJ3d5fDw0Ourq5QSmGz2X4vNCklWutUecXFxSQSCS4vL/H5fMRiMaLRKHa7Ha01'
 406     b'e3t7XFxccHd3x9PTE1LKDPa8e1VYfZ6enmZoaIiSkhKWlpZwu90MDw8zMTGBzWZjfn6etrY2'
 407     b'+vv7CQaDPD8/k075rAqszIUQSCkZGxvj/v6ex8dHurq6iMfjzMzM4HA4eH19pbe3l5OTE/Ly'
 408     b'8lhfX89oUQYJ3gJY/a6vr6empgbTNOns7MQwDGpra/H7/QghaG1txePxUF5eTktLy7tEyRKa'
 409     b'ZTU1NVRWVuJyuWhoaMDlclFVVUV1dTWGYVBfX09ZWRmVlZXU1dWlGPRboVkc1lozMjKC1hop'
 410     b'JR0dHQghmJ2dTe3t7u5OxaysrGSI7V2hWcNJF9hbYeWib64Ecw759vYW0zSZmppK+dKzsip6'
 411     b'e61YvqenJ25ubjL2iPQ3+eafG46OjjJEl+vqsA7OBe7z+fj096fcM/jc+pkvrV/+f/Qz7BdH'
 412     b'Sp/4DuCblwAAAABJRU5ErkJggg==')
 413 
 414 __version__ = 1.5
 415 
 416 
 417 mdbEVT = wx.NewEventType()
 418 EVT_DATE_CHANGED = wx.PyEventBinder(mdbEVT, 1)
 419 
 420 
 421 class mdbEvent(wx.PyCommandEvent):
 422     def __init__(self, eventType, eventId=1, date=None, value=''):
 423         """
 424         Default class constructor.
 425 
 426         :param `eventType`: the event type;
 427         :param `eventId`: the event identifier.
 428         """
 429         wx.PyCommandEvent.__init__(self, eventType, eventId)
 430         self._eventType = eventType
 431         self.date = date
 432         self.value = value
 433 
 434     def GetDate(self):
 435         """
 436         Retrieve the date value of the control at the time
 437         this event was generated, Returning a wx.DateTime object"""
 438         return self.date
 439 
 440     def GetValue(self):
 441         """
 442         Retrieve the formatted date value of the control at the time
 443         this event was generated, Returning a string"""
 444         return self.value.title()
 445 
 446     def GetDateTime(self):
 447         """
 448         Retrieve the date value of the control at the time
 449         this event was generated, Returning a python datetime object"""
 450         return wx.wxdate2pydate(self.date)
 451 
 452     def GetTimeStamp(self):
 453         """
 454         Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
 455         Returning a integer
 456         """
 457         return int(self.date.GetValue()/1000)
 458 
 459 class MiniDatePickerButton(wx.Panel):
 460     def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
 461                  style=wx.BORDER_SIMPLE, name="MDPickerButton", date=0, formatter='', image=True):
 462 
 463         wx.Control.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
 464         self.parent = parent
 465         self._date = date
 466         if formatter:
 467             format = formatter
 468         else:
 469             format = lambda dt: dt.FormatISODate()
 470         font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
 471         #self.SetWindowStyle(wx.BORDER_NONE)
 472         self._style = style
 473         self._calendar_style = wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SHOW_HOLIDAYS
 474         self._calendar_headercolours = None
 475         self._calendar_highlightcolours = None
 476         self._calendar_holidaycolours = None
 477         self._calendar_Bg = None
 478         self._calendar_Fg = None
 479         self._calendar_Font = None
 480         self._calendar_MarkDates = {}
 481         self._calendar_MarkBorder = (wx.adv.CAL_BORDER_SQUARE, wx.NullColour)
 482         self._calendar_Holidays = {}
 483         self._calendar_RestrictDates = {}
 484         self._calendar_daterange = (wx.DefaultDateTime, wx.DefaultDateTime)
 485         self._calendar_Notes = {}
 486         self._calendar_SetOnlyWeekDays = False
 487         self._calendar_OfficialHolidays = False
 488         self._calendar_AddOfficialHolidays = []
 489 
 490         self._image = image
 491         self._veto = False
 492         self._pop = False
 493         if size == wx.DefaultSize:
 494             dc = wx.ScreenDC()
 495             dc.SetFont(font)
 496             trialdate = format(wx.DateTime(28,9,2022)) # a Wednesday in September = longest names in English
 497             w, h = dc.GetTextExtent(trialdate)
 498             if self._image:
 499                 size = (w+94, -1) # Add image width (24) plus a buffer
 500             else:
 501                 size = (w+60, -1) # Add a buffer
 502             del dc
 503         self.txtstyle = 0
 504 
 505         if style & wx.LEFT or style == wx.LEFT:
 506             self.txtstyle = wx.RIGHT
 507         elif style & wx.RIGHT or style == wx.RIGHT:
 508             self.txtstyle = wx.LEFT
 509 
 510         # MiniDatePickerButton
 511 
 512         self.ctl = wx.Button(self, id, label=str(self._date),
 513                                pos=pos, size=size, style=style, name=name)
 514         if self._image:
 515             if self.txtstyle:
 516                 self.ctl.SetBitmap(img.Bitmap, self.txtstyle)
 517                 self.ctl.SetBitmapFocus(imgf.Bitmap)
 518             else:
 519                 self.ctl.SetBitmap(img.Bitmap)
 520                 self.ctl.SetBitmapFocus(imgf.Bitmap)
 521         self.MinSize = self.GetBestSize()
 522         # End
 523 
 524         # Bind the events
 525         self._formatter = format
 526         self.ctl.Bind(wx.EVT_BUTTON, self.OnCalendar)
 527         self.SetValue(date)
 528 
 529         sizer = wx.BoxSizer(wx.HORIZONTAL)
 530         sizer.Add(self.ctl, 1, wx.EXPAND, 0)
 531         self.SetSizerAndFit(sizer)
 532         self.Show()
 533 
 534     def OnCalendar(self, _event=None):
 535         if self._pop:
 536             return
 537         self._pop = True # controls only one popup at any one time
 538         self.calendar = CalendarPopup(
 539             self, self._date, self.OnDate, self.GetTopLevelParent(), wx.PU_CONTAINS_CONTROLS|wx.SIMPLE_BORDER)
 540         pos = self.ClientToScreen((0, 0))
 541         size = self.GetSize()
 542         self.calendar.Position(pos, (0, size.height))
 543 
 544     def SetFormatter(self, formatter):
 545         '''formatter will be called with a wx.DateTime'''
 546         self._formatter = formatter
 547         self.OnDate(self._date)
 548 
 549     def SetLocale(self, alias):
 550         try:
 551             locale.setlocale(locale.LC_TIME, locale=alias)
 552         except Exception as e:
 553             locale.setlocale(locale.LC_TIME, locale='')
 554         self.SetValue(self._date)
 555 
 556     def SetCalendarStyle(self, style=0):
 557         self._calendar_style = style
 558 
 559     def SetCalendarHeaders(self, colFg=wx.NullColour, colBg=wx.NullColour):
 560         self._calendar_headercolours = colFg, colBg
 561 
 562     def SetCalendarHighlights(self, colFg=wx.NullColour, colBg=wx.NullColour):
 563         self._calendar_highlightcolours = colFg, colBg
 564 
 565     def SetCalendarHolidayColours(self, colFg=wx.NullColour, colBg=wx.NullColour):
 566         self._calendar_holidaycolours = colFg, colBg
 567 
 568     def SetCalendarFg(self, colFg=wx.NullColour):
 569         self._calendar_Fg = colFg
 570 
 571     def SetCalendarBg(self, colBg=wx.NullColour):
 572         self._calendar_Bg = colBg
 573 
 574     def SetCalendarFont(self, font=None):
 575         self._calendar_Font = font
 576 
 577     def SetCalendarMarkDates(self, markdates = {}):
 578         self._calendar_MarkDates = markdates
 579 
 580     def SetCalendarMarkBorder(self, border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.NullColour):
 581         self._calendar_MarkBorder = (border, bcolour)
 582 
 583     def SetCalendarHolidays(self, holidays = {}):
 584         self._calendar_Holidays = holidays
 585 
 586     def SetCalendarRestrictDates(self, rdates = {}):
 587         self._calendar_RestrictDates = rdates
 588 
 589     def AddOfficialHolidays(self, country='', subdiv='', language=''):
 590         if not holidays_available:
 591             return False
 592         country = country.upper()
 593         subdiv = subdiv.upper()
 594         try:
 595             supported = holidays.country_holidays(country=country).subdivisions
 596         except Exception as e:
 597             return False
 598         if subdiv:
 599             if subdiv not in supported:
 600                 return False
 601         if not self._calendar_OfficialHolidays:
 602             self._calendar_OfficialHolidays = (country, subdiv, language)
 603         else:
 604             self._calendar_AddOfficialHolidays.append([country, subdiv, language])
 605         return True
 606 
 607     def SetCalendarDateRange(self, lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime):
 608         if not isinstance(lowerdate, wx.DateTime):
 609             return False
 610         if not isinstance(upperdate, wx.DateTime):
 611             return False
 612         self._calendar_daterange = (lowerdate, upperdate)
 613         return True
 614 
 615     def SetCalendarNotes(self, notes = {}):
 616         self._calendar_Notes = notes
 617 
 618     def SetCalendarOnlyWeekDays(self, wds = False):
 619         self._calendar_SetOnlyWeekDays = wds
 620         if wds:
 621             self._calendar_style = self._calendar_style | wx.adv.CAL_SHOW_HOLIDAYS
 622 
 623     def OnDate(self, date):
 624         self._date = date
 625         self.ctl.SetLabel(self._formatter(date).title())
 626         self.MinSize = self.GetBestSize()
 627         if self._veto:
 628             self._veto = False
 629             return
 630         event = mdbEvent(mdbEVT, self.GetId(), date=date, value=self._formatter(date))
 631         event.SetEventObject(self)
 632         self.GetEventHandler().ProcessEvent(event)
 633 
 634     def GetValue(self):
 635         return self.ctl.GetLabel()
 636 
 637     def GetLabel(self):
 638         return self.ctl.GetLabel()
 639 
 640     def GetDate(self):
 641         return self._date
 642 
 643     def GetDateTimeValue(self):
 644         """
 645         Return a python datetime object"""
 646         return wx.wxdate2pydate(self._date)
 647 
 648     def GetTimeStamp(self):
 649         """
 650         Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
 651         Returning a integer
 652         """
 653         return int(self._date.GetValue()/1000)
 654 
 655     def GetLocale(self):
 656         return locale.getlocale(category=locale.LC_TIME)
 657 
 658     def SetValue(self, date):
 659         if isinstance(date, wx.DateTime):
 660             pass
 661         elif isinstance(date, datetime.date):
 662             date = wx.pydate2wxdate(date)
 663         elif isinstance(date, int) and date > 0:
 664             date = wx.DateTime.FromTimeT(date)
 665         elif isinstance(date, float) and date > 0:
 666             date = wx.DateTime.FromTimeT(int(date))
 667         else:  # Invalid date value default to today's date
 668             date = wx.DateTime.Today()
 669         self._date = date.ResetTime()
 670         self._veto = True
 671         self.SetFormatter(self._formatter)
 672 
 673     def SetButtonBitmap(self, bitmap=None):
 674         if not bitmap:
 675             return
 676         bitmap = wx.Bitmap(bitmap)
 677         if self.txtstyle:
 678             self.ctl.SetBitmap(bitmap, self.txtstyle)
 679             self.ctl.SetBitmapFocus(bitmap)
 680         else:
 681             self.ctl.SetBitmap(bitmap)
 682             self.ctl.SetBitmapFocus(bitmap)
 683 
 684     def SetButtonBitmapFocus(self, bitmap=None):
 685         if not bitmap:
 686             return
 687         bitmap = wx.Bitmap(bitmap)
 688         self.ctl.SetBitmapFocus(bitmap)
 689 
 690 
 691 class CalendarPopup(wx.PopupTransientWindow):
 692     def __init__(self, parent, date, callback, *args, **kwargs):
 693         '''date is the initial date; callback is called with the chosen date'''
 694         super().__init__(*args, **kwargs)
 695         self.parent = parent
 696         self.callback = callback
 697         self.calendar = wx.adv.GenericCalendarCtrl(self, pos=(5, 5), style=parent._calendar_style)
 698         self.calendar.SetDate(date)
 699         self.Holidays = {}
 700         self.OfficialHolidays = {}
 701         self.RestrictedDates = {}
 702         self.MarkDates = {}
 703         self.Notes = {}
 704 
 705         if parent._calendar_headercolours:
 706             self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
 707         if parent._calendar_highlightcolours:
 708             self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
 709         if parent._calendar_holidaycolours:
 710             self.calendar.SetHolidayColours(parent._calendar_holidaycolours[0],parent._calendar_holidaycolours[1])
 711         if parent._calendar_Bg:
 712             self.calendar.SetBackgroundColour(parent._calendar_Bg)
 713             self.SetBackgroundColour(parent._calendar_Bg)
 714         if parent._calendar_Fg:
 715             self.calendar.SetForegroundColour(parent._calendar_Fg)
 716         if parent._calendar_Font:
 717             self.calendar.SetFont(parent._calendar_Font)
 718         self.markborder = parent._calendar_MarkBorder
 719         if parent._calendar_MarkDates:
 720             self.SetMarkDates(parent._calendar_MarkDates)
 721         if parent._calendar_Holidays:
 722             self.SetHolidays(parent._calendar_Holidays)
 723         if parent._calendar_OfficialHolidays:
 724             self.SetOfficialHolidays(parent._calendar_OfficialHolidays)
 725         if parent._calendar_daterange[0].IsValid() or parent._calendar_daterange[1].IsValid():
 726             self.SetDateRange(parent._calendar_daterange[0], parent._calendar_daterange[1])
 727         if parent._calendar_RestrictDates:
 728             self.SetRestrictDates(parent._calendar_RestrictDates)
 729         if parent._calendar_Notes:
 730             self.SetNotes(parent._calendar_Notes)
 731         sizer = wx.BoxSizer(wx.VERTICAL)
 732         sizer.Add(self.calendar, 1, wx.ALL | wx.EXPAND)
 733         self.SetSizerAndFit(sizer)
 734         self.calendar.Bind(wx.adv.EVT_CALENDAR_MONTH, self.OnChange)
 735         self.calendar.Bind(wx.adv.EVT_CALENDAR_YEAR, self.OnChange)
 736         self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnChosen)
 737         self.calendar.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnToolTip)
 738         self.calendar.Bind(wx.EVT_MOTION, self.OnToolTip)
 739         self.calendar.Bind(wx.EVT_RIGHT_DOWN, self.OnToolTip)
 740         self.calendar.Bind(wx.EVT_KEY_DOWN, self.OnKey)
 741         self.Popup()
 742 
 743     def OnChosen(self, _event=None):
 744         ''' Test chosen date for inclusion in restricted dates if set
 745             Test if set to only allow weekdays, test if it is a weekday or a holiday, whicj is treated as not a weekday
 746         '''
 747         d = self.calendar.GetDate()
 748         if self.RestrictedDates:
 749             test = (d.year, d.month+1)
 750             days = self.RestrictedDates.get(test, ())
 751             if not days or d.day not in days:
 752                 pass
 753             else:
 754                 return
 755 
 756         if self.parent._calendar_SetOnlyWeekDays and not d.IsWorkDay(): # Weekend
 757             return
 758         if self.parent._calendar_SetOnlyWeekDays: # Holiday
 759             attr = self.calendar.GetAttr(d.day)
 760             if attr.IsHoliday():
 761                 return
 762 
 763         self.callback(self.calendar.GetDate())
 764         self.parent._pop = False
 765         self.Dismiss()
 766 
 767     def OnChange(self, event):
 768         # If the year changed, recalculate the dictionaries for Marked, Restricted, Official Holidays and Note dates
 769         if event.GetEventType() == wx.adv.EVT_CALENDAR_YEAR.typeId:
 770             self.MarkDates = self.GenerateDates(self.parent._calendar_MarkDates)
 771             self.RestrictedDates = self.GenerateDates(self.parent._calendar_RestrictDates)
 772             self.SetOfficialHolidays(self.parent._calendar_OfficialHolidays)
 773             self.Notes = self.GenerateNotes(self.parent._calendar_Notes)
 774 
 775         date = event.GetDate()
 776         self.OnMonthChange()
 777 
 778     def OnDismiss(self, event=None):
 779         self.parent._pop = False
 780 
 781     def OnKey(self, event):
 782         keycode = event.GetKeyCode()
 783         if keycode == wx.WXK_ESCAPE:
 784             self.parent._pop = False
 785             self.Dismiss()
 786         event.Skip()
 787 
 788     def SetMarkDates(self, markdates):
 789         self.MarkDates = self.GenerateDates(markdates)
 790         self.OnMonthChange()
 791 
 792     def OnMonthChange(self):
 793         font = self.calendar.GetFont()
 794         font.SetStrikethrough(True)
 795         font.MakeItalic()
 796         date = self.calendar.GetDate()
 797         days_in_month = date.GetLastMonthDay().day
 798         mark_days = self.MarkDates.get((date.year, date.month+1), [])    # get dict values or an empty list if none
 799         h_days = self.Holidays.get((date.year, date.month+1), [])
 800         fixed_h_days = self.Holidays.get((0, date.month+1), [])
 801         r_days = self.RestrictedDates.get((date.year, date.month+1), [])
 802         oh_days = self.OfficialHolidays.get((date.year, date.month+1), [])
 803 
 804         if isinstance(mark_days, int): # Allow for people forgetting it must be a tuple, when entering a single day
 805             mark_days = tuple((mark_days,))
 806         if isinstance(h_days, int):
 807             h_days = tuple((h_days,))
 808         if isinstance(fixed_h_days, int):
 809             fixed_h_days = tuple((fixed_h_days,))
 810         if isinstance(r_days, int):
 811             r_days = tuple((r_days,))
 812 
 813         for d in range(1, days_in_month+1):
 814             attr = self.calendar.GetAttr(d)
 815             highlight_attr = wx.adv.CalendarDateAttr()
 816             if d in mark_days:                                                  # Marked Day
 817                 highlight_attr.SetBorder(self.markborder[0])
 818                 highlight_attr.SetBorderColour(self.markborder[1])
 819             if d in h_days:                                                     # Holiday
 820                 highlight_attr.SetHoliday(True)
 821             if d in fixed_h_days:                                               # Fixed Holiday
 822                 highlight_attr.SetHoliday(True)
 823             if d in oh_days:                                                    # Official Holidays
 824                 highlight_attr.SetHoliday(True)
 825             if not wx.DateTime(d, date.month, date.year).IsWorkDay():           # Weekend
 826                 highlight_attr.SetHoliday(True)
 827             if d in r_days:                                                     # Resticted Day (override holiday)
 828                 highlight_attr.SetFont(font)
 829             if highlight_attr.IsHoliday():
 830                 if self.parent._calendar_SetOnlyWeekDays:
 831                     highlight_attr.SetFont(font)
 832             if highlight_attr is not None:
 833                 self.calendar.SetAttr(d, highlight_attr)
 834             else:
 835                 self.calendar.ResetAttr(d)
 836 
 837         self.calendar.Refresh()
 838 
 839     def SetHolidays(self, holidays):
 840         self.Holidays = holidays
 841         self.OnMonthChange()
 842 
 843     def SetOfficialHolidays(self, holiday_codes):
 844         self.OfficialHolidays = {}
 845         if not holiday_codes:                                                   # holiday codes not set
 846             return
 847         country, subdiv, language = holiday_codes
 848         self.country_name = country
 849         for c in holidays.registry.COUNTRIES.values():
 850             if country in c:
 851                 self.country_name = c[0]
 852 
 853         d = self.calendar.GetDate()
 854         for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
 855             existing = self.OfficialHolidays.get((k.year, k.month), [])
 856             if k.day not in existing:
 857                 self.OfficialHolidays[(k.year, k.month)] = existing + [k.day]
 858 
 859         for item in self.parent._calendar_AddOfficialHolidays:
 860             country, subdiv, language = item
 861             for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
 862                 existing = self.OfficialHolidays.get((k.year, k.month), [])
 863                 if k.day not in existing:
 864                     self.OfficialHolidays[(k.year, k.month)] = existing + [k.day]
 865 
 866         self.OnMonthChange()
 867 
 868     def SetDateRange(self, lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime):
 869         if lowerdate.IsValid() or upperdate.IsValid():
 870             if lowerdate.IsValid():
 871                 lowerdate = wx.DateTime(lowerdate.day, lowerdate.month-1, lowerdate.year)
 872             if upperdate.IsValid():
 873                 upperdate =  wx.DateTime(upperdate.day, upperdate.month-1, upperdate.year)
 874             self.calendar.SetDateRange(lowerdate, upperdate)
 875 
 876     def SetNotes(self, notes):
 877         self.Notes = self.GenerateNotes(notes)
 878 
 879     def SetRestrictDates(self, rdates):
 880         self.RestrictedDates = self.GenerateDates(rdates)
 881         self.OnMonthChange()
 882 
 883     def restricted_date_range(self, start, end):
 884         '''
 885             Generate dates between a start and end date
 886         '''
 887         for i in range((end - start).days + 1):
 888             yield start + datetime.timedelta(days = i)
 889 
 890     def day_in_range(self, start, end, day):
 891         '''
 892             Test if date is the required day of the week
 893         '''
 894         for d in self.restricted_date_range(start, end):
 895             if d.isoweekday() == day:
 896                 yield d
 897 
 898     def GenerateDates(self, date_dict):
 899         ''' Generated on start and when the year changes (Marked and Restricted dictionaries)
 900             This routine generates a new dictionary from the one passed in and returns the generated dictionary.
 901             This because the original passed in dictionary may include date codes e.g. -99 for the last day of a month
 902              or -1 all Mondays or -23 the 3rd Tuesday, which need to be calculated for the given month in the given year.
 903             An added complication is that the year may be set to zero, denoting all years, so if the calendar year is
 904              changed, this routine is run again, to ensure that the dates are relevant to the current year.
 905         '''
 906         generated_dict = {}
 907 
 908         for year, month in date_dict:
 909             gen_year = year
 910             if gen_year == 0:               # Zero entry = All years, so generate dates for the currently selected year
 911                 d = self.calendar.GetDate()
 912                 gen_year = d.year
 913             day_map = calendar.monthcalendar(gen_year, month)
 914             for neg in list(date_dict.get((year, month))):
 915                 if neg >= 0:
 916                     existing = generated_dict.get((gen_year, month), [])
 917                     if neg not in existing:
 918                         generated_dict[(gen_year, month)] = existing + [neg]
 919                     continue
 920                 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
 921                 d1 = datetime.datetime(gen_year, month, 1)
 922                 d2 = datetime.datetime(gen_year, month, last_day_no)
 923                 if neg < 0 and neg >= -7:                                       # Every specified weekday
 924                     for i in self.day_in_range(d1, d2, abs(neg)):
 925                         existing = generated_dict.get((gen_year, month), [])
 926                         if i.day not in existing:
 927                             generated_dict[(gen_year, month)] = existing + [i.day]
 928                     continue
 929                 if neg == -99:                                                  # Last day of the month
 930                     first_week_day, last_day_no = calendar.monthrange(gen_year, month)
 931                     existing = generated_dict.get((gen_year, month), [])
 932                     if last_day_no not in existing:
 933                         generated_dict[(gen_year, month)] = existing + [last_day_no]
 934                     continue
 935                 if neg == -98:                                                  # Last weekday of the month
 936                     first_week_day, last_day_no = calendar.monthrange(gen_year, month)
 937                     ld = datetime.date(gen_year, month, last_day_no)
 938                     while ld.isoweekday() > 5:                                  # Last day of month is not a weekday
 939                         ld -= datetime.timedelta(days=1)                        # deduct days to get to Friday
 940                     existing = generated_dict.get((gen_year, month), [])
 941                     if ld.day not in existing:
 942                         generated_dict[(gen_year, month)] = existing + [ld.day]
 943                     continue
 944                 if neg <= -11 and neg >= -75:                                   # Occurrence of a weekday
 945                     if neg <= -11 and neg >= -15:                               # Monday 1-5
 946                         map_idx = 0
 947                         occ = neg + 11
 948                     elif neg <= -21 and neg >= -25:                             # Tuesday 1-5
 949                         map_idx = 1
 950                         occ = neg + 21
 951                     elif neg <= -31 and neg >= -35:                             # Wednesday 1-5
 952                         map_idx = 2
 953                         occ = neg + 31
 954                     elif neg <= -41 and neg >= -45:                             # Thursday 1-5
 955                         map_idx = 3
 956                         occ = neg + 41
 957                     elif neg <= -51 and neg >= -55:                             # Friday 1-5
 958                         map_idx = 4
 959                         occ = neg + 51
 960                     elif neg <= -61 and neg >= -65:                             # Saturday 1-5
 961                         map_idx = 5
 962                         occ = neg + 61
 963                     elif neg <= -71 and neg >= -75:                             # Sunday 1-5
 964                         map_idx = 6
 965                         occ = neg + 71
 966                     else:                                                       # Undefined
 967                         continue
 968                     week_map = [index for (index, item) in enumerate(day_map) if item[map_idx]]
 969                     if abs(occ) >= len(week_map):
 970                         occ = len(week_map) - 1
 971                     week_idx = week_map[abs(occ)]
 972                     map_day = day_map[week_idx][map_idx]
 973                     existing = generated_dict.get((gen_year, month), [])
 974                     if map_day not in existing:
 975                         generated_dict[(gen_year, month)] = existing + [map_day]
 976         return generated_dict
 977 
 978     def GenerateNotes(self, date_dict):
 979         ''' Generated on start and when the year changes
 980             This routine generates a new dictionary of Notes from the one passed in and returns the generated dictionary.
 981             This because the original passed in dictionary may include date codes e.g. -99 for the last of a month
 982              or -1 all Mondays or -23 the 3rd Tuesday, which need to be calculated for the given month in the given year.
 983             An added complication is that the year may be set to zero, denoting all years, so if the calendar year is changed,
 984              this routine is run again, to ensure that the dates are relevant to the current year.
 985             Because some of the notes are calculated, a date may have muliple notes, so the notes are accumulated, to form
 986             a single note entry, seperated by a + sign
 987             If Official Holidays are included, these too are recalculated for the current year.
 988         '''
 989         generated_dict = {}
 990         for year, month, day in date_dict:
 991             gen_year = year
 992             if gen_year == 0:               # Zero entry = All years, so generate dates for the currently selected year
 993                 d = self.calendar.GetDate()
 994                 gen_year = d.year
 995             day_map = calendar.monthcalendar(gen_year, month)
 996             note = date_dict.get((year, month, day))
 997             if day >= 0:
 998                 use_note = generated_dict.get((gen_year, month, day), '')
 999                 if use_note:
1000                     use_note = use_note+"\n  + "+note
1001                 else:
1002                     use_note = note
1003                 generated_dict[(gen_year, month, day)] = use_note
1004                 continue
1005             first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1006             d1 = datetime.datetime(gen_year, month, 1)
1007             d2 = datetime.datetime(gen_year, month, last_day_no)
1008             if day < 0 and day >= -7:                                       # Every specified weekday
1009                 for i in self.day_in_range(d1, d2, abs(day)):
1010                     use_note = generated_dict.get((gen_year, month, i.day), '')
1011                     if use_note:
1012                         use_note = use_note+"\n  + "+note
1013                     else:
1014                         use_note = note
1015                     generated_dict[(gen_year, month, i.day)] = use_note
1016                 continue
1017             if day == -99:                                                  # Last day of the month
1018                 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1019                 use_note = generated_dict.get((gen_year, month, last_day_no), '')
1020                 if use_note:
1021                     use_note = use_note+"\n  + "+note
1022                 else:
1023                     use_note = note
1024                 generated_dict[(gen_year, month, last_day_no)] = use_note
1025                 continue
1026             if day == -98:                                                  # Last weekday of the month
1027                 first_week_day, last_day_no = calendar.monthrange(gen_year, month)
1028                 ld = datetime.date(gen_year, month, last_day_no)
1029                 while ld.isoweekday() > 5:                                  # Last day of month is not a weekday
1030                     ld -= datetime.timedelta(days=1)                        # deduct days to get to Friday
1031                 use_note = generated_dict.get((gen_year, month, ld.day), '')
1032                 if use_note:
1033                     use_note = use_note+"\n  + "+note
1034                 else:
1035                     use_note = note
1036                 generated_dict[(gen_year, month, ld.day)] = use_note
1037                 continue
1038             if day <= -11 and day >= -75:                                   # Occurrence of a weekday
1039                 if day <= -11 and day >= -15:                               # Monday 1-5
1040                     map_idx = 0
1041                     occ = day + 11
1042                 elif day <= -21 and day >= -25:                             # Tuesday 1-5
1043                     map_idx = 1
1044                     occ = day + 21
1045                 elif day <= -31 and day >= -35:                             # Wednesday 1-5
1046                     map_idx = 2
1047                     occ = day + 31
1048                 elif day <= -41 and day >= -45:                             # Thursday 1-5
1049                     map_idx = 3
1050                     occ = day + 41
1051                 elif day <= -51 and day >= -55:                             # Friday 1-5
1052                     map_idx = 4
1053                     occ = day + 51
1054                 elif day <= -61 and day >= -65:                             # Saturday 1-5
1055                     map_idx = 5
1056                     occ = day + 61
1057                 elif day <= -71 and day >= -75:                             # Sunday 1-5
1058                     map_idx = 6
1059                     occ = day + 71
1060                 else:                                                       # Undefined
1061                     continue
1062                 week_map = [index for (index, item) in enumerate(day_map) if item[map_idx]]
1063                 if abs(occ) >= len(week_map):
1064                     occ = len(week_map) - 1
1065                 week_idx = week_map[abs(occ)]
1066                 map_day = day_map[week_idx][map_idx]
1067                 use_note = generated_dict.get((gen_year, month, map_day), '')
1068                 if use_note:
1069                     use_note = use_note+"\n  + "+note
1070                 else:
1071                     use_note = note
1072                 generated_dict[(gen_year, month, map_day)] = use_note
1073 
1074         # If official holidays are available write them into the notes
1075 
1076         if holidays_available and self.parent._calendar_OfficialHolidays:
1077             country, subdiv, language = self.parent._calendar_OfficialHolidays
1078             d = self.calendar.GetDate()
1079             for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
1080                 use_note = generated_dict.get((k.year, k.month, k.day), '')
1081                 if use_note:
1082                     use_note = use_note+"\n  + * "+v
1083                 else:
1084                     use_note = " * "+v
1085                 generated_dict[(k.year, k.month, k.day)] = use_note
1086 
1087             for item in self.parent._calendar_AddOfficialHolidays:
1088                 country, subdiv, language = item
1089                 for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
1090                     use_note = generated_dict.get((k.year, k.month, k.day), '')
1091                     if use_note:
1092                         use_note = use_note+"\n  + *"+' '.join(item)+v
1093                     else:
1094                         use_note = " *"+' '.join(item)+v
1095                     generated_dict[(k.year, k.month, k.day)] = use_note
1096 
1097 
1098         return generated_dict
1099 
1100     def OnToolTip(self, event):
1101         '''
1102         If Right click on a non date area, displays all Notes for the month
1103         Test for date range restrictions.
1104         Generate and display tooltips for each day based on position, if there are:
1105             Notes or Restricted entries for the day
1106         '''
1107         try:
1108             pos = event.GetPosition()
1109             click_code, click_date, click_day = self.calendar.HitTest(pos)
1110 
1111             # Show all holidays for the month in popup
1112             #  if not a valid date position or surrounding week of previous/next month (if shown).
1113             if click_code == 0 or click_code == 5:
1114                 if event.GetEventType() == wx.EVT_RIGHT_DOWN.typeId:
1115                     click_date = self.calendar.GetDate()
1116                     if holidays_available and self.parent._calendar_OfficialHolidays:
1117                         country, subdiv, language = self.parent._calendar_OfficialHolidays
1118                     else:
1119                         country = subdiv = language = ''
1120                     hdr = msg = ''
1121                     if country:
1122                         hdr = "Inc holidays for "+self.country_name
1123                         if subdiv:
1124                             hdr += " region "+subdiv
1125                         hdr += "\n"
1126                     else:
1127                         hdr = ''
1128                     vmax = 200
1129                     for k, v in sorted(self.Notes.items()):
1130                         if k[0] == click_date.year and k[1] == click_date.month + 1:
1131                             msg += "\n"+str(k[2]).zfill(2)+ " "+ v
1132                             vmax = max(vmax, self.GetTextExtent(v)[0]+50)
1133                     vmax = max(vmax, self.GetTextExtent(hdr)[0])
1134                     if msg:
1135                         msg = 'Notes for '+click_date.Format('%B') + '\n' + hdr + msg
1136                         wx.TipWindow(self,msg,maxLength=vmax)
1137                 return
1138             elif click_code != 2:                           # Something other than a valid date or a blank date
1139                 self.calendar.SetToolTip('')
1140                 return
1141         except Exception:
1142             click_date = self.calendar.GetDate()
1143 
1144         self.calendar.SetToolTip('')
1145         range_check, lower, upper = self.calendar.GetDateRange()
1146         if range_check:
1147             if (lower != wx.DefaultDateTime and click_date.IsEarlierThan(lower)) or \
1148                 (upper != wx.DefaultDateTime and click_date.IsLaterThan(upper)):
1149                 msg = str(self.parent._formatter(click_date)).title()+'\n'+"Out of Range\n"
1150                 if lower != wx.DefaultDateTime:
1151                     msg += str(lower.Format("%d-%b-%Y")).title()+' > '
1152                 else:
1153                     msg += "Any date > "
1154                 if upper != wx.DefaultDateTime:
1155                     msg += str(upper.Format("%d-%b-%Y")).title()
1156                 else:
1157                     msg += "Any date"
1158                 self.calendar.SetToolTip(msg)
1159                 return
1160 
1161         restricted = self.RestrictedDates.get((click_date.year, click_date.month + 1), [])
1162         restricted_set = False
1163         if click_date.day in restricted:
1164             restricted_set = True
1165         if self.parent._calendar_SetOnlyWeekDays and not click_date.IsWorkDay():
1166             restricted_set = True
1167         d = (click_date.year, click_date.month + 1, click_date.day)
1168         note = self.Notes.get(d, '')                        # Year/Month/Day specific Note or blank
1169         if restricted_set:
1170             note = "** Restricted **\n\n"+ note
1171         if not note:
1172             return
1173         self.calendar.SetToolTip(str(self.parent._formatter(click_date)).title()+'\n'+note)
1174 
1175 
1176 class DemoFrame(wx.Frame):
1177     '''
1178         This demonstration code attempts to provide at least one example of every option, even if it's commented out
1179         It may offer examples of various options for the same thing, which explains its rather messy look
1180         The bulk of the marked dates, holidays, restrictions and notes are set around August 2023, when the testing
1181         was performed, so feel free to navigate to that month or change the values.
1182     '''
1183     def __init__(self, parent):
1184         wx.Frame.__init__(self, parent, -1, "MiniDatePicker Button")
1185 
1186         #format = (lambda dt:
1187         #    (f'{dt.GetWeekDayName(dt.GetWeekDay())} {str(dt.day).zfill(2)}/{str(dt.month+1).zfill(2)}/{dt.year}')
1188         #    )
1189 
1190         #format = (lambda dt: (f'{dt.Format("%a %d-%m-%Y")}'))
1191 
1192         #Using a strftime format converting wx.DateTime to datetime.datetime
1193         #format = (lambda dt: (f'{wx.wxdate2pydate(dt).strftime("%A %d-%B-%Y")}'))
1194 
1195         format = (lambda dt: (f'{dt.Format("%A %d %B %Y")}'))
1196 
1197         panel = wx.Panel(self)
1198 
1199         self.mdp = MiniDatePickerButton(panel, -1, pos=(50, 50), style=wx.RIGHT,
1200                                    date=0, formatter=format, image=True)
1201 
1202         #self.mdp.SetLocale('es_ES.UTF-8')                                          # Set Locale for Language
1203         #self.mdp.SetFormatter(format)
1204         x=datetime.datetime.now()
1205         #self.mdp.SetValue(x.timestamp())                                            # Set Date
1206         self.mdp.SetValue(0)
1207 
1208         #font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
1209         #font.SetFractionalPointSize(16)
1210         #self.mdp.SetCalendarFont(font)                                              # Set Calendar Font
1211 
1212         self.mdp.SetCalendarStyle(wx.adv.CAL_SHOW_WEEK_NUMBERS|wx.adv.CAL_MONDAY_FIRST)
1213         self.mdp.ctl.SetBackgroundColour('#e1ffe1')                                 # background lightgreen
1214         #self.mdp.SetButtonBitmap('./Off.png')                                      # Specify button bitmap
1215         #self.mdp.SetButtonBitmapFocus('./On.png')                                  # Specify button focus bitmap
1216             # Another option for the bitmap is to use wx.ArtProvider e.g.
1217             # bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, client=wx.ART_BUTTON)
1218             # self.mdp.SetButtonBitmap(bmp)
1219         self.mdp.SetCalendarHeaders(colFg='#ff0000', colBg='#90ee90')               # red/lightgreen
1220         self.mdp.SetCalendarHighlights(colFg='#ffff00', colBg='#0000ff')            # yellow/blue
1221         self.mdp.SetCalendarHolidayColours(colFg='#ff0000', colBg='#ffff00')        # Holidays red/yellow
1222         self.mdp.SetCalendarBg(colBg='#f0ffff')                                     # azure
1223         self.Bind(EVT_DATE_CHANGED, self.OnEvent)
1224         self.mdp.SetCalendarMarkBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour='#800080') # Mark Border Square or Round,  + Colour
1225         #self.mdp.SetCalendarOnlyWeekDays(True)                                     # Only non holiday weekdays are selectable
1226         self.mdp.SetToolTip('Struck Through dates are not selectable')
1227 
1228         self.mdp.SetCalendarMarkDates({
1229                                        (0, 1) : [-11,-23,1],                   # 1st Monday, 3rd Tuesday and the 1st January
1230                                        (0, 2) : [-99,],                        # last day of February holiday, marked, with note
1231                                        (x.year, 7) : [2,5,7,11,30],
1232                                        (x.year, 8) : [7,12,13,20,27],
1233                                        (0, 9) : [1,27,-98]                     # 1st, 27th and the last weekday of September
1234                                       })
1235         self.mdp.SetCalendarHolidays({
1236                                      (0, 1) : [1,],                                 # January 1st every year
1237                                      (0, 2) : [-99,],                               # last day of February holiday, marked, with note
1238                                      (x.year, 8) : [7,15],
1239                                      (0, 12) : [25,26]                              # 25th & 26th December every year
1240                                     })
1241         self.mdp.SetCalendarNotes({
1242                                      (0, 1, 1) : "New Year's Day",                  # January 1st every year
1243                                      (0, 1, -11) : "First Monday of the year",
1244                                      (0, 1, -1) : "A Monday in January",
1245                                      (0, 1, -23) : "3rd Tuesday of the year",
1246                                      (0, 1, -99) : "Last day of January",
1247                                      (0, 1, -35) : "The last Wednesday of January",
1248                                      (0, 2, -5)  : "A Friday in February",
1249                                      (0, 2, -99) : "Note for last day of February",      # last day of February holiday, marked, with note
1250                                      (x.year, 8, 7) : "Marked for no reason whatsoever", # This year only
1251                                      (x.year, 8, 20) : "Marked for reason X",            # this year only
1252                                      (0, 9, -98) : "Last weekday of September",
1253                                      (0, 12, 25) : "Merry Christmas!",
1254                                      (0, 2, -99) : "Last day of February"
1255                                     })
1256 
1257         self.mdp.SetCalendarRestrictDates({
1258                                        (0, 1) : [-1, -2, 5],     # exclude Mondays and Tuesdays, 5th of January All years
1259                                        (0, 2) : [-99,]          # exclude last day of February - All years
1260                                         })
1261 
1262         # Restrict Calendar to a date range (define a lowerdate or an upperdate or both
1263         #self.mdp.SetCalendarDateRange(lowerdate=wx.DateTime(23,8,2023), upperdate=wx.DateTime(23,9,2023))
1264 
1265         # Official Holidays requires the python holidays module (pip install --upgrade holidays)
1266         #self.mdp.AddOfficialHolidays(country="GB", subdiv="ENG", language="")   # Primary region England
1267         #self.mdp.AddOfficialHolidays(country="ES", subdiv="AN", language="")    # Additional region Spain, Andalucia
1268 
1269         #------------------------------ 2nd Calendar ------------------------------#
1270 
1271         self.mdp2 = MiniDatePickerButton(panel, -1, pos=(50, 150), style=wx.LEFT, date=0, formatter=None)
1272 
1273         self.mdp2.SetCalendarRestrictDates({
1274                                        (0, 1) : [-1, -2],        # exclude Mondays and Tuesdays  - All years
1275                                        (x.year, 2) : [-1, -2],   # exclude Mondays and Tuesdays     - This year
1276                                        (0, 3) : [-99, -1, -2, 5],   # exclude Mondays and Tuesdays + last day + 5th - All years
1277                                        (x.year, 4) : [-1, -2],   # exclude Mondays and Tuesdays     - This year
1278                                        (x.year, 5) : [-1, -2],   # exclude Mondays and Tuesdays     - This year
1279                                        (x.year, 6) : [-1, -2]    # exclude Mondays and Tuesdays     - This year
1280                                       })
1281         self.mdp2.SetToolTip("Struck Through dates are not selectable")
1282         self.mdp2.SetCalendarOnlyWeekDays(False)                                     # Weekends and holidays selectable
1283         self.mdp2.SetCalendarStyle(wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION)
1284         font = wx.Font(16, wx.FONTFAMILY_ROMAN,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_SEMIBOLD)
1285         self.mdp2.SetCalendarFont(font)                                              # Set Calendar Font
1286 
1287         y = x + datetime.timedelta(weeks=+4)
1288         self.mdp2.SetValue(y.timestamp())
1289 
1290         self.Bind(EVT_DATE_CHANGED, self.OnEvent)
1291         self.Centre()
1292 
1293     def OnEvent(self, event):
1294         obj = event.GetEventObject()
1295         print("\nevt", event.GetValue())
1296 
1297         x = event.GetDate() # wx.DateTime object
1298         print("evt", x)
1299         print("Day", x.GetDayOfYear(),"Week", x.GetWeekOfYear(), "Name", x.GetWeekDayName(x.GetWeekDay()))
1300 
1301         print("evt", event.GetDateTime()) # datetime.datime object
1302         print("evt", event.GetTimeStamp())
1303         print("func", obj.GetValue())
1304         print("func", obj.GetDate())
1305         print("func", obj.GetDateTimeValue())
1306         print("func", obj.GetTimeStamp())
1307         print("func", obj.GetLocale())
1308 
1309 if __name__ == '__main__':
1310     app = wx.App()
1311     frame = DemoFrame(None)
1312     frame.Show()
1313     app.MainLoop()


Download source

source.zip


Additional Information

Link :

- - - - -

https://wiki.wxpython.org/TitleIndex

https://docs.wxpython.org/

https://discuss.wxpython.org/t/a-wx-datepickerctrl-with-a-customisable-format/36295

https://discuss.wxpython.org/t/datepickerctrlgeneric/36477/5


Thanks to

J. Healey (aka RolfofSaxony), the wxPython community...


About this page

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

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


Comments

- blah, blah, blah....

A wx.DatePickerCtrl with a customisable format - Part 2 (Phoenix) (last edited 2024-08-16 17:55:56 by Ecco)

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