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

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