= A wx.DatePickerCtrl with a customisable format - Part 2 (Phoenix) =
'''Keywords :''' DatePickerCtrl.

 . <<TableOfContents>>

--------
= 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

{{attachment:img_sample_1.jpg}}

{{attachment:img_sample_2.jpg}}

{{{#!python
"""
    MiniDatePickerButton.py

    A custom class that looks like the wx.DatePickerCtrl but with the ability to customise
    the calendar and the output format. (DatePickerCtrl is seemingly stuck with MM/DD/YYYY format)
    Works with wx.DateTime or python datetime values
    With or without a fixed calendar image
    If the image is included, it goes on the opposite side of the text style. wx.LEFT image on the right.
     (If the text_style is wx.TE_CENTRE it defaults left)
    Uses wx.adv.GenericCalendarCtrl
    Uses locale to enable different languages for the calendar

    An attempt has been made to allow days marked with attributes denoting Holiday, Marked, Restricted to live with each other

    Dates can be marked, restricted, defined as holidays and have ToolTip notes
     Marked and Restricted dates can be defined as a simple date or use more advanced rules for example the 3rd Friday of the month
     or every Tuesday of the month. Note: they can be year specific or every year

    Marked dates are marked with a Border, either Square or Oval
    Holidays are normally highlighted with a different Foreground/Background colour
    Restricted dates are marked using an Italic StrikeThrough font

    Defined Holidays can be year specific or every year on that Month/Day

    Official Holidays rely on the python 'holidays' package being available (pip install --upgrade holidays)
        official holidays are automatically entered into the Notes for you
    You may add in more than one region's official holidays and they will be denoted by the country code
     and region code if appropriate.

    Notes are date specific or every year and can follow the rules for Marked and Restricted dates, namely:
            All of a specified week day in a month;
            The specified occurrence of a weekday in a month e.g. 3rd Tuesday or last Friday of the month
            The last day of the month
            The last weekday of the month

    Navigation:
     The Escape key will exit the Calendar
     The Arrow keys will navigate the calendar
     The PageUp/PageDown keys will retreat and advance and the month respectively, as will MouseScrollUp and MouseScrollDown
     The Home and End keys jump to the First and Last day of the month, respectively.
     A right click on the calendar on a blank day, will display All the notes for the month.
     Date ToolTips will be displayed as and when appropriate, depending of the position in the calendar and settings

    MiniDatePicker(parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SIMPLE, name="MiniDatePicker", date=0, formatter='', image=True):

        @param parent:   Parent window. Must not be None.
        @param id:       identifier. A value of -1 indicates a default value.
        @param pos:      MiniDatePicker position. If the position (-1, -1) is specified
                         then a default position is chosen.
        @param size:     If the default size (-1, -1) is specified then a default size is calculated.
                         Size should be able to accomodate the specified formatter string + image
        @param style:    Alignment (Left,Middle,Right).
        @param name:     Widget name.
        @param date:     Initial date (an invalid date = today)
        @param formatter A date formatting string in the form of a lambda function
                         The formatter will be called with a wx.DateTime thus we can use .Format()
                          the wxPython version of the standard ANSI C strftime
                         default lambda dt: dt.FormatISODate()
                          = ISO 8601 format "YYYY-MM-DD".
                         or a lambda function with a format string e.g.:
                            lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
                         e.g.:
                            format = lambda dt: (f'{dt.Format("%a %d-%m-%Y")}')
                            format = lambda dt: (f'{dt.Format("%A %d %B %Y")}')
                         or
                            fmt = "%Y/%m/%d"
                            format = lambda dt: (dt.Format(fmt))
                            format = lambda dt: (dt.Format("%Y/%m/%d"))
                            format = lambda dt: (dt.FormatISODate())
                        for those who prefer strftime formatting:
                            format = (lambda dt: (f'{wx.wxdate2pydate(dt).strftime("%A %d-%B-%Y")}'))
        @param image     Show image or Not (True or False) Default True

    TextCtrl Styles:
            wx.RIGHT
            wx.LEFT
            wx.TE_CENTRE

            wx.BORDER_NONE is always applied to the internal textctrl
            wx.BORDER_SIMPLE is the default border for the control itself

    Events: EVT_DATE_CHANGED A date change occurred in the control

    Event Functions:
        GetValue()          Returns formatted date in the event as a string

        GetDate()           Returns wxDateTime date in the event, with all of its attendant functions

        GetDateTime()       Returns python datetime of date in the event

        GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected date

    Functions:
        GetValue()          Returns formatted date in the event as a string

        GetDate()           Returns wxDateTime date in the control

        GetDateTimeValue()  Returns python datetime of date in the control

        GetTimeStamp()      Returns seconds since Jan 1, 1970 UTC for selected date

        GetLocale()         Returns tuple of current language code and encoding

        SetValue(date)      Sets the date in the control
                            expects a wx.DateTime, a python datetime datetime or a timestamp
                            Any invalid date defaults to wx.DateTime.Today()

        SetButtonBitmap(bitmap=None)  Set a specified image to used for the Button
                                      This is also used for the Focus image unless overridden (see below)
                                      You may use a file name or a wx.Bitmap

        SetButtonBitmapFocus(bitmap=None)  Set a specified image to used for the Button focus
                                      You may use a file name or a wx.Bitmap

        SetFormatter(formatter) Date format in the form of a lambda
            default:    lambda dt: dt.FormatISODate()


        SetLocale(locale)   Set the locale for Calendar day and month names
                             e.g. 'de_DE.UTF-8' German
                                  'es_ES.UTF-8' Spanish
                             depends on the locale being available on the machine

        SetCalendarStyle(style)
            wx.adv.CAL_SUNDAY_FIRST: Show Sunday as the first day in the week
            wx.adv.CAL_MONDAY_FIRST: Show Monday as the first day in the week
            wx.adv.CAL_SHOW_HOLIDAYS: Highlight holidays in the calendar
            wx.adv.CAL_NO_YEAR_CHANGE: Disable the year changing (deprecated, only generic)
            wx.adv.CAL_NO_MONTH_CHANGE: Disable the month (and, implicitly, the year) changing
            wx.adv.CAL_SHOW_SURROUNDING_WEEKS: Show the neighbouring weeks in the previous and next months
            wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION: more compact, style for the month and year selection controls.
            wx.adv.CAL_SHOW_WEEK_NUMBERS

        SetCalendarHighlights(colFg, colBg)         Colours to mark the currently selected date

        SetCalendarHolidayColours(colFg, colBg)     Colours to mark Holidays

        SetCalendarHeaders(colFg, colBg)

        SetCalendarFg(colFg)    Calendar ForegroundColour

        SetCalendarBg(colBg)    Calendar & Ctrl BackgroundColour

        SetCalendarFont(font=None)  Set font of the calendar to a wx.Font
                Alter the font family, weight, size, etc

        SetCalendarMarkDates(markdates = {}) Mark dates with a Border
                A dictionary containing year and month tuple as the key and a list of days for the values to be marked
                e.g.
                    {
                     (2023, 7) : [2,5,7,11,30],
                     (2023, 8) : [7,12,13,20,27],
                     (2023, 9) : [1,27]
                    }

                Values of less than 0 indicate not a specific date but a day: -1 Monday, -2 Tuesday, ... -7 Sunday
                 allowing you to mark all Mondays and Fridays in the month of January e.g {(2023, 1) : [-1, -5]}
                 You may include a mixture of negative and positive numbers (days and specific dates)

                Negative values beyond that indicate the nth weekday, (the indexing is a bit confusing because it's off by 1
                 the first digit represents the day and the 2nd digit represents the occurrence i.e.

                -11, 1st Monday | -12, 2nd Monday | -13, 3rd Monday | -14, 4th Monday | -15, 5th or Last Monday
                -21, 1st Tuesday | -22, 2nd Tuesday | -23, 3rd Tuesday | -24, 4th Tuesday | -25, 5th or Last Tuesday
                ..............................................................................................................
                -71, 1st Sunday | -72, 2nd Sunday | -73, 3rd Sunday | -74, 4th Sunday | -75, 5th or Last Sunday

                If the 5th occurrence of a weekday doesn't exist, the last occurrence of the weekday is substituted.

                -99 Stands for the last day of the month
                -98 is for the last weekday of the month

        SetCalendarMarkBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.NullColour)
                Defines the border type to mark dates wx.adv.CAL_BORDER_SQUARE (default)
                and a border colour e.g. wx.NullColour (Default), wx.RED  or a hex value '#800080' etc
                Valid border values are:
                    wx.adv.CAL_BORDER_NONE      - 0
                    wx.adv.CAL_BORDER_SQUARE    - 1
                    wx.adv.CAL_BORDER_ROUND     - 2


        SetCalendarHolidays(holidays = {})
                A dictionary containing year and month tuple as the key and a list of days for the values e.g.
                    {
                     (2023, 1) : [1,],
                     (2023, 7) : [1,30],
                     (2023, 8) : [7,15,27],
                     (2023, 12) : [25,26]
                    }

                Holidays can also be 'fixed' Holidays occurring every year on the same day by setting the year to zero in the key
                e.g.
                    {
                     (0, 1) : [1,],                            # January 1st is a Holiday every year
                     (2023, 7) : [1,30],
                     (2023, 8) : [7,15,27],
                     (0, 12) : [25,26]                         # Christmas Day and Boxing Day are Holidays every year
                    }

        SetCalendarNotes(notes = {})
                A dictionary containing a year, month, day tuple as the key and a string for the note e.g.
                    {
                     (2023, 1, 1) : "New Year's Day",
                     (2023, 12, 25) : "Christmas Day"
                    }

                Like Holidays, Notes can be assigned to a specific day every year
                    {
                     (0, 1, 1) : "New Year's Day",
                     (0, 12, 25) : "Christmas Day"
                    }

                To compliment Marked Dates and Restricted Dates, notes can also be assigned a negative day following the
                 the same pattern as Marked Dates and Restricted Dates.
                Allowing you to match Notes with Marked Dates and Restricted Dates.

                    {
                     (0, 1, -11) : "The 1st Monday of January/the year",
                     (0, 1, -35) : "The last Wednesday of January",
                     (0, 2, -5)  : "Every Friday in February"
                    }

                If you set Official Holidays, they are enter automatically into the notes, marked with a leading asterix (*).

                Notes are displayed as a ToolTip, when the day is hovered over or Right clicked
                 or if the mouse is over the calendar and the Arrow keys are used to navigate the calendar to that day.

                A right click on the calendar on a blank day, will display All the notes for the month.

        SetCalendarRestrictDates(rdates = {})
                A dictionary containing a year and month tuple as the key and a list of days, for the days that
                 are Not selectable within that year/month i.e. the reverse of Marked Dates
                 e.g.
                    {
                     (2023, 1) : [1,15],
                     (2023, 3) : [1,15],
                     (2023, 5) : [1,15],
                     (2023, 7) : [1,15,23],
                     (2023, 9) : [1,15],
                     (2023, 11) : [1,15]
                    }

                All dates in the 'restricted' dictionary use an Italic StruckThrough font and cannot be selected

                See SetCalendarMarkDates for the ability to use negative values to calculate dictionary values to restrict
                more complicated entries like All Mondays or the 2nd and 4th Tuesday for example, by using negative values.

        SetCalendarDateRange(lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime)
                Either 2 wx.DateTime values to restrict the selectable dates
                or just a lower date or just an upper date
                (The oddity of wx.DateTime months from 0 is catered for)
                Returns False if the dates are not wx.DateTime objects
                wx.DefaultDateTime equals no date selected.

                Dates outside of the range will display an "Out of Range" ToolTip, with the defined range.

        SetCalendarOnlyWeekDays(boolean) Default False
                If set only weekdays are selectable. weekends and holidays use an Italic StruckThrough font and cannot be selected
                Holidays are treated as Not a weekday i.e. no work

        AddOfficialHolidays(country='', subdiv='', language='') Default blank, blank, blank
                Only available if the python 'holidays' module was successfully imported
                Currently supports 134 country codes using country ISO 3166-1 alpha-2 codes and the optional subdivision
                 (state, region etc) using ISO 3166-2 codes.
                Language must be an ISO 639-1 (2-letter) language code. If the language translation is not supported
                the original holiday names are returned.

                For details: https://python-holidays.readthedocs.io/en/latest/
                 (or the file 'python-holidays — holidays documentation.html' supplied with this program)
                e.g.
                    country='ES'                    Spain
                    country='ES' and subdiv='AN'    Spain, Andalucia
                    country='UK' and subdiv='ENG'   United Kingdom, England
                    country='US' and subdiv='SC'    USA, South Carolina

                function returns True if successful, an existing country and subdivision (if supplied)
                 or False if there was an error

                This function can be called multiple times, once for each country or region in a country
                 that you wish marked on the calendar.
                The first call sets the primary holiday region.
                May be useful if you are operating in more than one geographical area, with differing holidays

    Default Values:
        date    -       Today
        style   -       READ_ONLY

Author:     J Healey
Created:    04/12/2022
Copyright:  J Healey - 2022-2023
License:    GPL 3 or any later version
Email:      <rolfofsaxony@gmx.com>
Version     1.5

A thank you to Richard Townsend (RichardT) for the inspiration of the date dictionaries for some of the functions.

Changelog:
1.5     Add optional holidays package
        A fast, efficient Python library for generating country and subdivision- (e.g. state or province) specific
         sets of government-designated holidays on the fly.
        Alter the font_family, weight, size etc of the calendar popup
        New functions:
            AddOfficialHolidays - to add Official holiday zones using 'python holidays package'
            SetCalendarFont()   - Change calendar font_family, weight, style, size etc
        Fix for MSW, added style wx.PU_CONTAINS_CONTROLS to the Popup, which allows the month choice drop down
         to function

1.4     SetCalendarDateRange
         allows the restriction of dates to a range, that can be selected
        SetCalendarNotes
            Allows for a note to be assigned to individual days in the calendar.
            If notes exist for a day, when hovered over the ToolTip will display the note.
            Envisaged to be used in conjunction with Holidays and Marked days to provide detail
        SetCalendarRestrictDates
            Set a dictionary of dates that are Not selectable
        SetCalendarOnlyWeekDays
            Only weekdays are selectable i.e. weekends and holidays are not

1.3     New Functions
         SetCalendarHolidayColours
         SetCalendarHolidays
         SetCalendarMarkBorder
         SetCalendarMarkDates
        Permit the definition of Holidays and key dates to be highlighted and the method of highlighting;
         by colour for holidays and border for marked days

1.2     New function SetButtonBitmap()
        Allows a specified image to be used for the button
        (also allows bitmap from wx.ArtProvider to be used)

        New function SetButtonBitmapFocus()
        Allow a specified image to be used for the button focus

1.1     subclass changes from wx.Control to wx.Panel to handle tab traversal
        The image will indicate Focus
        Demonstration colours set to Hex

Usage example:

import wx
import minidatepickerbutton as MDB
class Frame(wx.Frame):
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDatePicker Demo")
        format = (lambda dt: (f'{dt.Format("%A %d-%m-%Y")}'))
        panel = wx.Panel(self)
        mdb = MDB.MiniDatePickerButton(panel, -1, pos=(50, 50), style=0, date=0, formatter=format)
        self.Show()

app = wx.App()
frame = Frame(None)
app.MainLoop()

"""

import wx
import wx.adv
from wx.lib.embeddedimage import PyEmbeddedImage
import datetime
import calendar
import locale
try:
    import holidays
    holidays_available = True
except ModuleNotFoundError:
    holidays_available = False

img = PyEmbeddedImage(
    b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAABg2lDQ1BJQ0MgcHJvZmlsZQAA'
    b'KJF9kT1Iw0AcxV9TS0UiDu0g4pChOlkQFXHUKhShQqgVWnUwufQLmjQkKS6OgmvBwY/FqoOL'
    b's64OroIg+AHi6OSk6CIl/i8ptIj14Lgf7+497t4BQqPCdLtnHNANx0onE1I2tyqFXyEihAhi'
    b'EBVmm3OynELX8XWPAF/v4jyr+7k/R7+WtxkQkIhnmWk5xBvE05uOyXmfOMpKikZ8Tjxm0QWJ'
    b'H7mu+vzGueixwDOjViY9TxwlloodrHYwK1k68RRxTNMNyheyPmuctzjrlRpr3ZO/UMwbK8tc'
    b'pzmMJBaxBBkSVNRQRgUO4rQapNhI036ii3/I88vkUslVBiPHAqrQoXh+8D/43a1dmJzwk8QE'
    b'EHpx3Y8RILwLNOuu+33sus0TIPgMXBltf7UBzHySXm9rsSNgYBu4uG5r6h5wuQMMPpmKpXhS'
    b'kKZQKADvZ/RNOSByC/St+b219nH6AGSoq9QNcHAIjBYpe73Lu3s7e/v3TKu/H0FDcpMpQj/v'
    b'AAAACXBIWXMAAAsTAAALEwEAmpwYAAACk0lEQVRIx43W3+unQxQH8Nc83w9tVutHNj9v5M4q'
    b'tty7oC2u2Fy58KOV9g/AlRJygQvlRrIlpbhSW7uJorCKlKQoJand7AVJYm3Z7zMu9v18zWfM'
    b'J6aeZs7MmTNnznm/zzwFE6p/WunkLczNXEnfyhO2Rza2rLfpP4xPG4zPm2xsNR6NPN/uNuq8'
    b'nAa3W4tGaRQrrsZFkX/FIbyO3dG7PHq/RP4d9+NIs/YHTi+OrzKYcS2OZtNpvIqbcQfuyqFv'
    b'pH80e45hP26JM1fhYtyDHzGvUGuttZSyB3/hWdyAn3P4KXwYg+dywAfx9mTmf8JH+A5P45Ls'
    b'Ox/XUkpJzM/kJofxJ17JjR7GTfgGX2EfHsHZ3PRM5OsynvvEtTCrGR+Ih3fmJvsTgmsSEtE5'
    b'lb5N7g7qVgO0LIk/FM9r5N14uUPM3TiY/bVbK5hbDPdEaudq8/VQnBu4Lzo7XJiWJA8Y2reK'
    b'5/F4jN6L9/EaHsQFsVewHZtWSfLUeTBqBbfitxi6HTfiisjn1pTPA2daNZ7PDengh8R9b9Cz'
    b'L0g6G73bcGUguSv7/1UypkFYlnA9iZfwPR4K+V7EM/H2cIh2JGG7sCszFXW1oZDNeA57Ujre'
    b'CkSfSClYpYRcH3Ie7EJU2ySPilnBCXwW4rwdKH4axlYcDwe+xnsbSvlO1vt6Lsa/TGH7OP0X'
    b'+Dy6J/BtdD5pbK1BfjVg8aL4QlMIj2btscaBN5s9D3RkW4hWpwaztXmAapfwTdAdEbOF/BoP'
    b'JlyamD/VvU6tV/OgrCxzu3BZq7NqFE/iXdzXkW5UOspAZznonaVUY6t9zeRdKMu4YeVUa507'
    b'lg51lrXlPS+dh6MwTJibw6dBSdn4J1K6R39ofPDH8L9/c/4GBa36v+mJzSMAAAAASUVORK5C'
    b'YII=')

imgf = PyEmbeddedImage(
    b'iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABmJLR0QA/wD/AP+gvaeTAAAA'
    b'CXBIWXMAAAsTAAALEwEAmpwYAAADf0lEQVRIx+2V3yusWxjHP2u9o8Y7k92e6DQ1U3KUGiXp'
    b'iDZGUepEtF1RdlImuVEucCGk1K7hD9BEfuz2nXBzqIkLdS7VODXDldygEfmVkTBr7at3mjFj'
    b'37k7z9X7Pms96/v8+H7XEt/3vms+0CQfbB8OYEv/+fbnNx4fH1FKsXGxgfpX0dvbSyKRQGvN'
    b'9fU1QghcLhdaa5xOJ6urq4g6wdc/viKEwOFw8OP4RzZAT0kP7e3tOJ1O3G43gUCA0H8hdnZ2'
    b'2NraQilFT08PSinm5uaQUtLW1kYkEiHwV4CpqSni8TgPDw9sbGzw8+RnJsD9/T15eXmMj49z'
    b'fHxMYWEhWms8Hg+NjY1IKbHZbCilaGpqQgiB1+tFSklRURF+v5/S0lImJye5u7vLnkEymcQ0'
    b'Tc7Ozpifnyc/P5+BgQGcTicLCwtEo1F8Ph8VFRXEYjFCoRB2u51AIIBpmoRCIU5PTzFNEyll'
    b'NoBhGAAIITAMg3A4jNfrZXt7G601kUiEeDzO+fk5BwcHAITDYTweD+FwGCFE6tD0b9vbqWut'
    b'0VqzuLjIwsICQgi01iQSCQYHB1PBWms2NzdZX1/HMAyEEBlrWRXkWrR8VrAFlp6hlJJkMpny'
    b'CyFQSmUDWAenB6ebEILR0VGCwSBSStbW1mhubqavr4/l5WVeXl5QSqG1TrU7o0XpGeQyrTX7'
    b'+/sUFBSglGJ3d5fDw0Ourq5QSmGz2X4vNCklWutUecXFxSQSCS4vL/H5fMRiMaLRKHa7Ha01'
    b'e3t7XFxccHd3x9PTE1LKDPa8e1VYfZ6enmZoaIiSkhKWlpZwu90MDw8zMTGBzWZjfn6etrY2'
    b'+vv7CQaDPD8/k075rAqszIUQSCkZGxvj/v6ex8dHurq6iMfjzMzM4HA4eH19pbe3l5OTE/Ly'
    b'8lhfX89oUQYJ3gJY/a6vr6empgbTNOns7MQwDGpra/H7/QghaG1txePxUF5eTktLy7tEyRKa'
    b'ZTU1NVRWVuJyuWhoaMDlclFVVUV1dTWGYVBfX09ZWRmVlZXU1dWlGPRboVkc1lozMjKC1hop'
    b'JR0dHQghmJ2dTe3t7u5OxaysrGSI7V2hWcNJF9hbYeWib64Ecw759vYW0zSZmppK+dKzsip6'
    b'e61YvqenJ25ubjL2iPQ3+eafG46OjjJEl+vqsA7OBe7z+fj096fcM/jc+pkvrV/+f/Qz7BdH'
    b'Sp/4DuCblwAAAABJRU5ErkJggg==')

__version__ = 1.5


mdbEVT = wx.NewEventType()
EVT_DATE_CHANGED = wx.PyEventBinder(mdbEVT, 1)


class mdbEvent(wx.PyCommandEvent):
    def __init__(self, eventType, eventId=1, date=None, value=''):
        """
        Default class constructor.

        :param `eventType`: the event type;
        :param `eventId`: the event identifier.
        """
        wx.PyCommandEvent.__init__(self, eventType, eventId)
        self._eventType = eventType
        self.date = date
        self.value = value

    def GetDate(self):
        """
        Retrieve the date value of the control at the time
        this event was generated, Returning a wx.DateTime object"""
        return self.date

    def GetValue(self):
        """
        Retrieve the formatted date value of the control at the time
        this event was generated, Returning a string"""
        return self.value.title()

    def GetDateTime(self):
        """
        Retrieve the date value of the control at the time
        this event was generated, Returning a python datetime object"""
        return wx.wxdate2pydate(self.date)

    def GetTimeStamp(self):
        """
        Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
        Returning a integer
        """
        return int(self.date.GetValue()/1000)

class MiniDatePickerButton(wx.Panel):
    def __init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize,
                 style=wx.BORDER_SIMPLE, name="MDPickerButton", date=0, formatter='', image=True):

        wx.Control.__init__(self, parent, id, pos=pos, size=size, style=style, name=name)
        self.parent = parent
        self._date = date
        if formatter:
            format = formatter
        else:
            format = lambda dt: dt.FormatISODate()
        font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
        #self.SetWindowStyle(wx.BORDER_NONE)
        self._style = style
        self._calendar_style = wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SHOW_HOLIDAYS
        self._calendar_headercolours = None
        self._calendar_highlightcolours = None
        self._calendar_holidaycolours = None
        self._calendar_Bg = None
        self._calendar_Fg = None
        self._calendar_Font = None
        self._calendar_MarkDates = {}
        self._calendar_MarkBorder = (wx.adv.CAL_BORDER_SQUARE, wx.NullColour)
        self._calendar_Holidays = {}
        self._calendar_RestrictDates = {}
        self._calendar_daterange = (wx.DefaultDateTime, wx.DefaultDateTime)
        self._calendar_Notes = {}
        self._calendar_SetOnlyWeekDays = False
        self._calendar_OfficialHolidays = False
        self._calendar_AddOfficialHolidays = []

        self._image = image
        self._veto = False
        self._pop = False
        if size == wx.DefaultSize:
            dc = wx.ScreenDC()
            dc.SetFont(font)
            trialdate = format(wx.DateTime(28,9,2022)) # a Wednesday in September = longest names in English
            w, h = dc.GetTextExtent(trialdate)
            if self._image:
                size = (w+94, -1) # Add image width (24) plus a buffer
            else:
                size = (w+60, -1) # Add a buffer
            del dc
        self.txtstyle = 0

        if style & wx.LEFT or style == wx.LEFT:
            self.txtstyle = wx.RIGHT
        elif style & wx.RIGHT or style == wx.RIGHT:
            self.txtstyle = wx.LEFT

        # MiniDatePickerButton

        self.ctl = wx.Button(self, id, label=str(self._date),
                               pos=pos, size=size, style=style, name=name)
        if self._image:
            if self.txtstyle:
                self.ctl.SetBitmap(img.Bitmap, self.txtstyle)
                self.ctl.SetBitmapFocus(imgf.Bitmap)
            else:
                self.ctl.SetBitmap(img.Bitmap)
                self.ctl.SetBitmapFocus(imgf.Bitmap)
        self.MinSize = self.GetBestSize()
        # End

        # Bind the events
        self._formatter = format
        self.ctl.Bind(wx.EVT_BUTTON, self.OnCalendar)
        self.SetValue(date)

        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.ctl, 1, wx.EXPAND, 0)
        self.SetSizerAndFit(sizer)
        self.Show()

    def OnCalendar(self, _event=None):
        if self._pop:
            return
        self._pop = True # controls only one popup at any one time
        self.calendar = CalendarPopup(
            self, self._date, self.OnDate, self.GetTopLevelParent(), wx.PU_CONTAINS_CONTROLS|wx.SIMPLE_BORDER)
        pos = self.ClientToScreen((0, 0))
        size = self.GetSize()
        self.calendar.Position(pos, (0, size.height))

    def SetFormatter(self, formatter):
        '''formatter will be called with a wx.DateTime'''
        self._formatter = formatter
        self.OnDate(self._date)

    def SetLocale(self, alias):
        try:
            locale.setlocale(locale.LC_TIME, locale=alias)
        except Exception as e:
            locale.setlocale(locale.LC_TIME, locale='')
        self.SetValue(self._date)

    def SetCalendarStyle(self, style=0):
        self._calendar_style = style

    def SetCalendarHeaders(self, colFg=wx.NullColour, colBg=wx.NullColour):
        self._calendar_headercolours = colFg, colBg

    def SetCalendarHighlights(self, colFg=wx.NullColour, colBg=wx.NullColour):
        self._calendar_highlightcolours = colFg, colBg

    def SetCalendarHolidayColours(self, colFg=wx.NullColour, colBg=wx.NullColour):
        self._calendar_holidaycolours = colFg, colBg

    def SetCalendarFg(self, colFg=wx.NullColour):
        self._calendar_Fg = colFg

    def SetCalendarBg(self, colBg=wx.NullColour):
        self._calendar_Bg = colBg

    def SetCalendarFont(self, font=None):
        self._calendar_Font = font

    def SetCalendarMarkDates(self, markdates = {}):
        self._calendar_MarkDates = markdates

    def SetCalendarMarkBorder(self, border=wx.adv.CAL_BORDER_SQUARE, bcolour=wx.NullColour):
        self._calendar_MarkBorder = (border, bcolour)

    def SetCalendarHolidays(self, holidays = {}):
        self._calendar_Holidays = holidays

    def SetCalendarRestrictDates(self, rdates = {}):
        self._calendar_RestrictDates = rdates

    def AddOfficialHolidays(self, country='', subdiv='', language=''):
        if not holidays_available:
            return False
        country = country.upper()
        subdiv = subdiv.upper()
        try:
            supported = holidays.country_holidays(country=country).subdivisions
        except Exception as e:
            return False
        if subdiv:
            if subdiv not in supported:
                return False
        if not self._calendar_OfficialHolidays:
            self._calendar_OfficialHolidays = (country, subdiv, language)
        else:
            self._calendar_AddOfficialHolidays.append([country, subdiv, language])
        return True

    def SetCalendarDateRange(self, lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime):
        if not isinstance(lowerdate, wx.DateTime):
            return False
        if not isinstance(upperdate, wx.DateTime):
            return False
        self._calendar_daterange = (lowerdate, upperdate)
        return True

    def SetCalendarNotes(self, notes = {}):
        self._calendar_Notes = notes

    def SetCalendarOnlyWeekDays(self, wds = False):
        self._calendar_SetOnlyWeekDays = wds
        if wds:
            self._calendar_style = self._calendar_style | wx.adv.CAL_SHOW_HOLIDAYS

    def OnDate(self, date):
        self._date = date
        self.ctl.SetLabel(self._formatter(date).title())
        self.MinSize = self.GetBestSize()
        if self._veto:
            self._veto = False
            return
        event = mdbEvent(mdbEVT, self.GetId(), date=date, value=self._formatter(date))
        event.SetEventObject(self)
        self.GetEventHandler().ProcessEvent(event)

    def GetValue(self):
        return self.ctl.GetLabel()

    def GetLabel(self):
        return self.ctl.GetLabel()

    def GetDate(self):
        return self._date

    def GetDateTimeValue(self):
        """
        Return a python datetime object"""
        return wx.wxdate2pydate(self._date)

    def GetTimeStamp(self):
        """
        Retrieve the date value represented as seconds since Jan 1, 1970 UTC.
        Returning a integer
        """
        return int(self._date.GetValue()/1000)

    def GetLocale(self):
        return locale.getlocale(category=locale.LC_TIME)

    def SetValue(self, date):
        if isinstance(date, wx.DateTime):
            pass
        elif isinstance(date, datetime.date):
            date = wx.pydate2wxdate(date)
        elif isinstance(date, int) and date > 0:
            date = wx.DateTime.FromTimeT(date)
        elif isinstance(date, float) and date > 0:
            date = wx.DateTime.FromTimeT(int(date))
        else:  # Invalid date value default to today's date
            date = wx.DateTime.Today()
        self._date = date.ResetTime()
        self._veto = True
        self.SetFormatter(self._formatter)

    def SetButtonBitmap(self, bitmap=None):
        if not bitmap:
            return
        bitmap = wx.Bitmap(bitmap)
        if self.txtstyle:
            self.ctl.SetBitmap(bitmap, self.txtstyle)
            self.ctl.SetBitmapFocus(bitmap)
        else:
            self.ctl.SetBitmap(bitmap)
            self.ctl.SetBitmapFocus(bitmap)

    def SetButtonBitmapFocus(self, bitmap=None):
        if not bitmap:
            return
        bitmap = wx.Bitmap(bitmap)
        self.ctl.SetBitmapFocus(bitmap)


class CalendarPopup(wx.PopupTransientWindow):
    def __init__(self, parent, date, callback, *args, **kwargs):
        '''date is the initial date; callback is called with the chosen date'''
        super().__init__(*args, **kwargs)
        self.parent = parent
        self.callback = callback
        self.calendar = wx.adv.GenericCalendarCtrl(self, pos=(5, 5), style=parent._calendar_style)
        self.calendar.SetDate(date)
        self.Holidays = {}
        self.OfficialHolidays = {}
        self.RestrictedDates = {}
        self.MarkDates = {}
        self.Notes = {}

        if parent._calendar_headercolours:
            self.calendar.SetHeaderColours(parent._calendar_headercolours[0],parent._calendar_headercolours[1])
        if parent._calendar_highlightcolours:
            self.calendar.SetHighlightColours(parent._calendar_highlightcolours[0],parent._calendar_highlightcolours[1])
        if parent._calendar_holidaycolours:
            self.calendar.SetHolidayColours(parent._calendar_holidaycolours[0],parent._calendar_holidaycolours[1])
        if parent._calendar_Bg:
            self.calendar.SetBackgroundColour(parent._calendar_Bg)
            self.SetBackgroundColour(parent._calendar_Bg)
        if parent._calendar_Fg:
            self.calendar.SetForegroundColour(parent._calendar_Fg)
        if parent._calendar_Font:
            self.calendar.SetFont(parent._calendar_Font)
        self.markborder = parent._calendar_MarkBorder
        if parent._calendar_MarkDates:
            self.SetMarkDates(parent._calendar_MarkDates)
        if parent._calendar_Holidays:
            self.SetHolidays(parent._calendar_Holidays)
        if parent._calendar_OfficialHolidays:
            self.SetOfficialHolidays(parent._calendar_OfficialHolidays)
        if parent._calendar_daterange[0].IsValid() or parent._calendar_daterange[1].IsValid():
            self.SetDateRange(parent._calendar_daterange[0], parent._calendar_daterange[1])
        if parent._calendar_RestrictDates:
            self.SetRestrictDates(parent._calendar_RestrictDates)
        if parent._calendar_Notes:
            self.SetNotes(parent._calendar_Notes)
        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.calendar, 1, wx.ALL | wx.EXPAND)
        self.SetSizerAndFit(sizer)
        self.calendar.Bind(wx.adv.EVT_CALENDAR_MONTH, self.OnChange)
        self.calendar.Bind(wx.adv.EVT_CALENDAR_YEAR, self.OnChange)
        self.calendar.Bind(wx.adv.EVT_CALENDAR, self.OnChosen)
        self.calendar.Bind(wx.adv.EVT_CALENDAR_SEL_CHANGED, self.OnToolTip)
        self.calendar.Bind(wx.EVT_MOTION, self.OnToolTip)
        self.calendar.Bind(wx.EVT_RIGHT_DOWN, self.OnToolTip)
        self.calendar.Bind(wx.EVT_KEY_DOWN, self.OnKey)
        self.Popup()

    def OnChosen(self, _event=None):
        ''' Test chosen date for inclusion in restricted dates if set
            Test if set to only allow weekdays, test if it is a weekday or a holiday, whicj is treated as not a weekday
        '''
        d = self.calendar.GetDate()
        if self.RestrictedDates:
            test = (d.year, d.month+1)
            days = self.RestrictedDates.get(test, ())
            if not days or d.day not in days:
                pass
            else:
                return

        if self.parent._calendar_SetOnlyWeekDays and not d.IsWorkDay(): # Weekend
            return
        if self.parent._calendar_SetOnlyWeekDays: # Holiday
            attr = self.calendar.GetAttr(d.day)
            if attr.IsHoliday():
                return

        self.callback(self.calendar.GetDate())
        self.parent._pop = False
        self.Dismiss()

    def OnChange(self, event):
        # If the year changed, recalculate the dictionaries for Marked, Restricted, Official Holidays and Note dates
        if event.GetEventType() == wx.adv.EVT_CALENDAR_YEAR.typeId:
            self.MarkDates = self.GenerateDates(self.parent._calendar_MarkDates)
            self.RestrictedDates = self.GenerateDates(self.parent._calendar_RestrictDates)
            self.SetOfficialHolidays(self.parent._calendar_OfficialHolidays)
            self.Notes = self.GenerateNotes(self.parent._calendar_Notes)

        date = event.GetDate()
        self.OnMonthChange()

    def OnDismiss(self, event=None):
        self.parent._pop = False

    def OnKey(self, event):
        keycode = event.GetKeyCode()
        if keycode == wx.WXK_ESCAPE:
            self.parent._pop = False
            self.Dismiss()
        event.Skip()

    def SetMarkDates(self, markdates):
        self.MarkDates = self.GenerateDates(markdates)
        self.OnMonthChange()

    def OnMonthChange(self):
        font = self.calendar.GetFont()
        font.SetStrikethrough(True)
        font.MakeItalic()
        date = self.calendar.GetDate()
        days_in_month = date.GetLastMonthDay().day
        mark_days = self.MarkDates.get((date.year, date.month+1), [])    # get dict values or an empty list if none
        h_days = self.Holidays.get((date.year, date.month+1), [])
        fixed_h_days = self.Holidays.get((0, date.month+1), [])
        r_days = self.RestrictedDates.get((date.year, date.month+1), [])
        oh_days = self.OfficialHolidays.get((date.year, date.month+1), [])

        if isinstance(mark_days, int): # Allow for people forgetting it must be a tuple, when entering a single day
            mark_days = tuple((mark_days,))
        if isinstance(h_days, int):
            h_days = tuple((h_days,))
        if isinstance(fixed_h_days, int):
            fixed_h_days = tuple((fixed_h_days,))
        if isinstance(r_days, int):
            r_days = tuple((r_days,))

        for d in range(1, days_in_month+1):
            attr = self.calendar.GetAttr(d)
            highlight_attr = wx.adv.CalendarDateAttr()
            if d in mark_days:                                                  # Marked Day
                highlight_attr.SetBorder(self.markborder[0])
                highlight_attr.SetBorderColour(self.markborder[1])
            if d in h_days:                                                     # Holiday
                highlight_attr.SetHoliday(True)
            if d in fixed_h_days:                                               # Fixed Holiday
                highlight_attr.SetHoliday(True)
            if d in oh_days:                                                    # Official Holidays
                highlight_attr.SetHoliday(True)
            if not wx.DateTime(d, date.month, date.year).IsWorkDay():           # Weekend
                highlight_attr.SetHoliday(True)
            if d in r_days:                                                     # Resticted Day (override holiday)
                highlight_attr.SetFont(font)
            if highlight_attr.IsHoliday():
                if self.parent._calendar_SetOnlyWeekDays:
                    highlight_attr.SetFont(font)
            if highlight_attr is not None:
                self.calendar.SetAttr(d, highlight_attr)
            else:
                self.calendar.ResetAttr(d)

        self.calendar.Refresh()

    def SetHolidays(self, holidays):
        self.Holidays = holidays
        self.OnMonthChange()

    def SetOfficialHolidays(self, holiday_codes):
        self.OfficialHolidays = {}
        if not holiday_codes:                                                   # holiday codes not set
            return
        country, subdiv, language = holiday_codes
        self.country_name = country
        for c in holidays.registry.COUNTRIES.values():
            if country in c:
                self.country_name = c[0]

        d = self.calendar.GetDate()
        for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
            existing = self.OfficialHolidays.get((k.year, k.month), [])
            if k.day not in existing:
                self.OfficialHolidays[(k.year, k.month)] = existing + [k.day]

        for item in self.parent._calendar_AddOfficialHolidays:
            country, subdiv, language = item
            for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
                existing = self.OfficialHolidays.get((k.year, k.month), [])
                if k.day not in existing:
                    self.OfficialHolidays[(k.year, k.month)] = existing + [k.day]

        self.OnMonthChange()

    def SetDateRange(self, lowerdate=wx.DefaultDateTime, upperdate=wx.DefaultDateTime):
        if lowerdate.IsValid() or upperdate.IsValid():
            if lowerdate.IsValid():
                lowerdate = wx.DateTime(lowerdate.day, lowerdate.month-1, lowerdate.year)
            if upperdate.IsValid():
                upperdate =  wx.DateTime(upperdate.day, upperdate.month-1, upperdate.year)
            self.calendar.SetDateRange(lowerdate, upperdate)

    def SetNotes(self, notes):
        self.Notes = self.GenerateNotes(notes)

    def SetRestrictDates(self, rdates):
        self.RestrictedDates = self.GenerateDates(rdates)
        self.OnMonthChange()

    def restricted_date_range(self, start, end):
        '''
            Generate dates between a start and end date
        '''
        for i in range((end - start).days + 1):
            yield start + datetime.timedelta(days = i)

    def day_in_range(self, start, end, day):
        '''
            Test if date is the required day of the week
        '''
        for d in self.restricted_date_range(start, end):
            if d.isoweekday() == day:
                yield d

    def GenerateDates(self, date_dict):
        ''' Generated on start and when the year changes (Marked and Restricted dictionaries)
            This routine generates a new dictionary from the one passed in and returns the generated dictionary.
            This because the original passed in dictionary may include date codes e.g. -99 for the last day of a month
             or -1 all Mondays or -23 the 3rd Tuesday, which need to be calculated for the given month in the given year.
            An added complication is that the year may be set to zero, denoting all years, so if the calendar year is
             changed, this routine is run again, to ensure that the dates are relevant to the current year.
        '''
        generated_dict = {}

        for year, month in date_dict:
            gen_year = year
            if gen_year == 0:               # Zero entry = All years, so generate dates for the currently selected year
                d = self.calendar.GetDate()
                gen_year = d.year
            day_map = calendar.monthcalendar(gen_year, month)
            for neg in list(date_dict.get((year, month))):
                if neg >= 0:
                    existing = generated_dict.get((gen_year, month), [])
                    if neg not in existing:
                        generated_dict[(gen_year, month)] = existing + [neg]
                    continue
                first_week_day, last_day_no = calendar.monthrange(gen_year, month)
                d1 = datetime.datetime(gen_year, month, 1)
                d2 = datetime.datetime(gen_year, month, last_day_no)
                if neg < 0 and neg >= -7:                                       # Every specified weekday
                    for i in self.day_in_range(d1, d2, abs(neg)):
                        existing = generated_dict.get((gen_year, month), [])
                        if i.day not in existing:
                            generated_dict[(gen_year, month)] = existing + [i.day]
                    continue
                if neg == -99:                                                  # Last day of the month
                    first_week_day, last_day_no = calendar.monthrange(gen_year, month)
                    existing = generated_dict.get((gen_year, month), [])
                    if last_day_no not in existing:
                        generated_dict[(gen_year, month)] = existing + [last_day_no]
                    continue
                if neg == -98:                                                  # Last weekday of the month
                    first_week_day, last_day_no = calendar.monthrange(gen_year, month)
                    ld = datetime.date(gen_year, month, last_day_no)
                    while ld.isoweekday() > 5:                                  # Last day of month is not a weekday
                        ld -= datetime.timedelta(days=1)                        # deduct days to get to Friday
                    existing = generated_dict.get((gen_year, month), [])
                    if ld.day not in existing:
                        generated_dict[(gen_year, month)] = existing + [ld.day]
                    continue
                if neg <= -11 and neg >= -75:                                   # Occurrence of a weekday
                    if neg <= -11 and neg >= -15:                               # Monday 1-5
                        map_idx = 0
                        occ = neg + 11
                    elif neg <= -21 and neg >= -25:                             # Tuesday 1-5
                        map_idx = 1
                        occ = neg + 21
                    elif neg <= -31 and neg >= -35:                             # Wednesday 1-5
                        map_idx = 2
                        occ = neg + 31
                    elif neg <= -41 and neg >= -45:                             # Thursday 1-5
                        map_idx = 3
                        occ = neg + 41
                    elif neg <= -51 and neg >= -55:                             # Friday 1-5
                        map_idx = 4
                        occ = neg + 51
                    elif neg <= -61 and neg >= -65:                             # Saturday 1-5
                        map_idx = 5
                        occ = neg + 61
                    elif neg <= -71 and neg >= -75:                             # Sunday 1-5
                        map_idx = 6
                        occ = neg + 71
                    else:                                                       # Undefined
                        continue
                    week_map = [index for (index, item) in enumerate(day_map) if item[map_idx]]
                    if abs(occ) >= len(week_map):
                        occ = len(week_map) - 1
                    week_idx = week_map[abs(occ)]
                    map_day = day_map[week_idx][map_idx]
                    existing = generated_dict.get((gen_year, month), [])
                    if map_day not in existing:
                        generated_dict[(gen_year, month)] = existing + [map_day]
        return generated_dict

    def GenerateNotes(self, date_dict):
        ''' Generated on start and when the year changes
            This routine generates a new dictionary of Notes from the one passed in and returns the generated dictionary.
            This because the original passed in dictionary may include date codes e.g. -99 for the last of a month
             or -1 all Mondays or -23 the 3rd Tuesday, which need to be calculated for the given month in the given year.
            An added complication is that the year may be set to zero, denoting all years, so if the calendar year is changed,
             this routine is run again, to ensure that the dates are relevant to the current year.
            Because some of the notes are calculated, a date may have muliple notes, so the notes are accumulated, to form
            a single note entry, seperated by a + sign
            If Official Holidays are included, these too are recalculated for the current year.
        '''
        generated_dict = {}
        for year, month, day in date_dict:
            gen_year = year
            if gen_year == 0:               # Zero entry = All years, so generate dates for the currently selected year
                d = self.calendar.GetDate()
                gen_year = d.year
            day_map = calendar.monthcalendar(gen_year, month)
            note = date_dict.get((year, month, day))
            if day >= 0:
                use_note = generated_dict.get((gen_year, month, day), '')
                if use_note:
                    use_note = use_note+"\n  + "+note
                else:
                    use_note = note
                generated_dict[(gen_year, month, day)] = use_note
                continue
            first_week_day, last_day_no = calendar.monthrange(gen_year, month)
            d1 = datetime.datetime(gen_year, month, 1)
            d2 = datetime.datetime(gen_year, month, last_day_no)
            if day < 0 and day >= -7:                                       # Every specified weekday
                for i in self.day_in_range(d1, d2, abs(day)):
                    use_note = generated_dict.get((gen_year, month, i.day), '')
                    if use_note:
                        use_note = use_note+"\n  + "+note
                    else:
                        use_note = note
                    generated_dict[(gen_year, month, i.day)] = use_note
                continue
            if day == -99:                                                  # Last day of the month
                first_week_day, last_day_no = calendar.monthrange(gen_year, month)
                use_note = generated_dict.get((gen_year, month, last_day_no), '')
                if use_note:
                    use_note = use_note+"\n  + "+note
                else:
                    use_note = note
                generated_dict[(gen_year, month, last_day_no)] = use_note
                continue
            if day == -98:                                                  # Last weekday of the month
                first_week_day, last_day_no = calendar.monthrange(gen_year, month)
                ld = datetime.date(gen_year, month, last_day_no)
                while ld.isoweekday() > 5:                                  # Last day of month is not a weekday
                    ld -= datetime.timedelta(days=1)                        # deduct days to get to Friday
                use_note = generated_dict.get((gen_year, month, ld.day), '')
                if use_note:
                    use_note = use_note+"\n  + "+note
                else:
                    use_note = note
                generated_dict[(gen_year, month, ld.day)] = use_note
                continue
            if day <= -11 and day >= -75:                                   # Occurrence of a weekday
                if day <= -11 and day >= -15:                               # Monday 1-5
                    map_idx = 0
                    occ = day + 11
                elif day <= -21 and day >= -25:                             # Tuesday 1-5
                    map_idx = 1
                    occ = day + 21
                elif day <= -31 and day >= -35:                             # Wednesday 1-5
                    map_idx = 2
                    occ = day + 31
                elif day <= -41 and day >= -45:                             # Thursday 1-5
                    map_idx = 3
                    occ = day + 41
                elif day <= -51 and day >= -55:                             # Friday 1-5
                    map_idx = 4
                    occ = day + 51
                elif day <= -61 and day >= -65:                             # Saturday 1-5
                    map_idx = 5
                    occ = day + 61
                elif day <= -71 and day >= -75:                             # Sunday 1-5
                    map_idx = 6
                    occ = day + 71
                else:                                                       # Undefined
                    continue
                week_map = [index for (index, item) in enumerate(day_map) if item[map_idx]]
                if abs(occ) >= len(week_map):
                    occ = len(week_map) - 1
                week_idx = week_map[abs(occ)]
                map_day = day_map[week_idx][map_idx]
                use_note = generated_dict.get((gen_year, month, map_day), '')
                if use_note:
                    use_note = use_note+"\n  + "+note
                else:
                    use_note = note
                generated_dict[(gen_year, month, map_day)] = use_note

        # If official holidays are available write them into the notes

        if holidays_available and self.parent._calendar_OfficialHolidays:
            country, subdiv, language = self.parent._calendar_OfficialHolidays
            d = self.calendar.GetDate()
            for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
                use_note = generated_dict.get((k.year, k.month, k.day), '')
                if use_note:
                    use_note = use_note+"\n  + * "+v
                else:
                    use_note = " * "+v
                generated_dict[(k.year, k.month, k.day)] = use_note

            for item in self.parent._calendar_AddOfficialHolidays:
                country, subdiv, language = item
                for k, v in holidays.country_holidays(country=country, subdiv=subdiv, years=d.year, language=language).items():
                    use_note = generated_dict.get((k.year, k.month, k.day), '')
                    if use_note:
                        use_note = use_note+"\n  + *"+' '.join(item)+v
                    else:
                        use_note = " *"+' '.join(item)+v
                    generated_dict[(k.year, k.month, k.day)] = use_note


        return generated_dict

    def OnToolTip(self, event):
        '''
        If Right click on a non date area, displays all Notes for the month
        Test for date range restrictions.
        Generate and display tooltips for each day based on position, if there are:
            Notes or Restricted entries for the day
        '''
        try:
            pos = event.GetPosition()
            click_code, click_date, click_day = self.calendar.HitTest(pos)

            # Show all holidays for the month in popup
            #  if not a valid date position or surrounding week of previous/next month (if shown).
            if click_code == 0 or click_code == 5:
                if event.GetEventType() == wx.EVT_RIGHT_DOWN.typeId:
                    click_date = self.calendar.GetDate()
                    if holidays_available and self.parent._calendar_OfficialHolidays:
                        country, subdiv, language = self.parent._calendar_OfficialHolidays
                    else:
                        country = subdiv = language = ''
                    hdr = msg = ''
                    if country:
                        hdr = "Inc holidays for "+self.country_name
                        if subdiv:
                            hdr += " region "+subdiv
                        hdr += "\n"
                    else:
                        hdr = ''
                    vmax = 200
                    for k, v in sorted(self.Notes.items()):
                        if k[0] == click_date.year and k[1] == click_date.month + 1:
                            msg += "\n"+str(k[2]).zfill(2)+ " "+ v
                            vmax = max(vmax, self.GetTextExtent(v)[0]+50)
                    vmax = max(vmax, self.GetTextExtent(hdr)[0])
                    if msg:
                        msg = 'Notes for '+click_date.Format('%B') + '\n' + hdr + msg
                        wx.TipWindow(self,msg,maxLength=vmax)
                return
            elif click_code != 2:                           # Something other than a valid date or a blank date
                self.calendar.SetToolTip('')
                return
        except Exception:
            click_date = self.calendar.GetDate()

        self.calendar.SetToolTip('')
        range_check, lower, upper = self.calendar.GetDateRange()
        if range_check:
            if (lower != wx.DefaultDateTime and click_date.IsEarlierThan(lower)) or \
                (upper != wx.DefaultDateTime and click_date.IsLaterThan(upper)):
                msg = str(self.parent._formatter(click_date)).title()+'\n'+"Out of Range\n"
                if lower != wx.DefaultDateTime:
                    msg += str(lower.Format("%d-%b-%Y")).title()+' > '
                else:
                    msg += "Any date > "
                if upper != wx.DefaultDateTime:
                    msg += str(upper.Format("%d-%b-%Y")).title()
                else:
                    msg += "Any date"
                self.calendar.SetToolTip(msg)
                return

        restricted = self.RestrictedDates.get((click_date.year, click_date.month + 1), [])
        restricted_set = False
        if click_date.day in restricted:
            restricted_set = True
        if self.parent._calendar_SetOnlyWeekDays and not click_date.IsWorkDay():
            restricted_set = True
        d = (click_date.year, click_date.month + 1, click_date.day)
        note = self.Notes.get(d, '')                        # Year/Month/Day specific Note or blank
        if restricted_set:
            note = "** Restricted **\n\n"+ note
        if not note:
            return
        self.calendar.SetToolTip(str(self.parent._formatter(click_date)).title()+'\n'+note)


class DemoFrame(wx.Frame):
    '''
        This demonstration code attempts to provide at least one example of every option, even if it's commented out
        It may offer examples of various options for the same thing, which explains its rather messy look
        The bulk of the marked dates, holidays, restrictions and notes are set around August 2023, when the testing
        was performed, so feel free to navigate to that month or change the values.
    '''
    def __init__(self, parent):
        wx.Frame.__init__(self, parent, -1, "MiniDatePicker Button")

        #format = (lambda dt:
        #    (f'{dt.GetWeekDayName(dt.GetWeekDay())} {str(dt.day).zfill(2)}/{str(dt.month+1).zfill(2)}/{dt.year}')
        #    )

        #format = (lambda dt: (f'{dt.Format("%a %d-%m-%Y")}'))

        #Using a strftime format converting wx.DateTime to datetime.datetime
        #format = (lambda dt: (f'{wx.wxdate2pydate(dt).strftime("%A %d-%B-%Y")}'))

        format = (lambda dt: (f'{dt.Format("%A %d %B %Y")}'))

        panel = wx.Panel(self)

        self.mdp = MiniDatePickerButton(panel, -1, pos=(50, 50), style=wx.RIGHT,
                                   date=0, formatter=format, image=True)

        #self.mdp.SetLocale('es_ES.UTF-8')                                          # Set Locale for Language
        #self.mdp.SetFormatter(format)
        x=datetime.datetime.now()
        #self.mdp.SetValue(x.timestamp())                                            # Set Date
        self.mdp.SetValue(0)

        #font = wx.SystemSettings.GetFont(wx.SYS_SYSTEM_FONT)
        #font.SetFractionalPointSize(16)
        #self.mdp.SetCalendarFont(font)                                              # Set Calendar Font

        self.mdp.SetCalendarStyle(wx.adv.CAL_SHOW_WEEK_NUMBERS|wx.adv.CAL_MONDAY_FIRST)
        self.mdp.ctl.SetBackgroundColour('#e1ffe1')                                 # background lightgreen
        #self.mdp.SetButtonBitmap('./Off.png')                                      # Specify button bitmap
        #self.mdp.SetButtonBitmapFocus('./On.png')                                  # Specify button focus bitmap
            # Another option for the bitmap is to use wx.ArtProvider e.g.
            # bmp = wx.ArtProvider.GetBitmap(wx.ART_GO_DOWN, client=wx.ART_BUTTON)
            # self.mdp.SetButtonBitmap(bmp)
        self.mdp.SetCalendarHeaders(colFg='#ff0000', colBg='#90ee90')               # red/lightgreen
        self.mdp.SetCalendarHighlights(colFg='#ffff00', colBg='#0000ff')            # yellow/blue
        self.mdp.SetCalendarHolidayColours(colFg='#ff0000', colBg='#ffff00')        # Holidays red/yellow
        self.mdp.SetCalendarBg(colBg='#f0ffff')                                     # azure
        self.Bind(EVT_DATE_CHANGED, self.OnEvent)
        self.mdp.SetCalendarMarkBorder(border=wx.adv.CAL_BORDER_SQUARE, bcolour='#800080') # Mark Border Square or Round,  + Colour
        #self.mdp.SetCalendarOnlyWeekDays(True)                                     # Only non holiday weekdays are selectable
        self.mdp.SetToolTip('Struck Through dates are not selectable')

        self.mdp.SetCalendarMarkDates({
                                       (0, 1) : [-11,-23,1],                   # 1st Monday, 3rd Tuesday and the 1st January
                                       (0, 2) : [-99,],                        # last day of February holiday, marked, with note
                                       (x.year, 7) : [2,5,7,11,30],
                                       (x.year, 8) : [7,12,13,20,27],
                                       (0, 9) : [1,27,-98]                     # 1st, 27th and the last weekday of September
                                      })
        self.mdp.SetCalendarHolidays({
                                     (0, 1) : [1,],                                 # January 1st every year
                                     (0, 2) : [-99,],                               # last day of February holiday, marked, with note
                                     (x.year, 8) : [7,15],
                                     (0, 12) : [25,26]                              # 25th & 26th December every year
                                    })
        self.mdp.SetCalendarNotes({
                                     (0, 1, 1) : "New Year's Day",                  # January 1st every year
                                     (0, 1, -11) : "First Monday of the year",
                                     (0, 1, -1) : "A Monday in January",
                                     (0, 1, -23) : "3rd Tuesday of the year",
                                     (0, 1, -99) : "Last day of January",
                                     (0, 1, -35) : "The last Wednesday of January",
                                     (0, 2, -5)  : "A Friday in February",
                                     (0, 2, -99) : "Note for last day of February",      # last day of February holiday, marked, with note
                                     (x.year, 8, 7) : "Marked for no reason whatsoever", # This year only
                                     (x.year, 8, 20) : "Marked for reason X",            # this year only
                                     (0, 9, -98) : "Last weekday of September",
                                     (0, 12, 25) : "Merry Christmas!",
                                     (0, 2, -99) : "Last day of February"
                                    })

        self.mdp.SetCalendarRestrictDates({
                                       (0, 1) : [-1, -2, 5],     # exclude Mondays and Tuesdays, 5th of January All years
                                       (0, 2) : [-99,]          # exclude last day of February - All years
                                        })

        # Restrict Calendar to a date range (define a lowerdate or an upperdate or both
        #self.mdp.SetCalendarDateRange(lowerdate=wx.DateTime(23,8,2023), upperdate=wx.DateTime(23,9,2023))

        # Official Holidays requires the python holidays module (pip install --upgrade holidays)
        #self.mdp.AddOfficialHolidays(country="GB", subdiv="ENG", language="")   # Primary region England
        #self.mdp.AddOfficialHolidays(country="ES", subdiv="AN", language="")    # Additional region Spain, Andalucia

        #------------------------------ 2nd Calendar ------------------------------#

        self.mdp2 = MiniDatePickerButton(panel, -1, pos=(50, 150), style=wx.LEFT, date=0, formatter=None)

        self.mdp2.SetCalendarRestrictDates({
                                       (0, 1) : [-1, -2],        # exclude Mondays and Tuesdays  - All years
                                       (x.year, 2) : [-1, -2],   # exclude Mondays and Tuesdays     - This year
                                       (0, 3) : [-99, -1, -2, 5],   # exclude Mondays and Tuesdays + last day + 5th - All years
                                       (x.year, 4) : [-1, -2],   # exclude Mondays and Tuesdays     - This year
                                       (x.year, 5) : [-1, -2],   # exclude Mondays and Tuesdays     - This year
                                       (x.year, 6) : [-1, -2]    # exclude Mondays and Tuesdays     - This year
                                      })
        self.mdp2.SetToolTip("Struck Through dates are not selectable")
        self.mdp2.SetCalendarOnlyWeekDays(False)                                     # Weekends and holidays selectable
        self.mdp2.SetCalendarStyle(wx.adv.CAL_MONDAY_FIRST|wx.adv.CAL_SEQUENTIAL_MONTH_SELECTION)
        font = wx.Font(16, wx.FONTFAMILY_ROMAN,wx.FONTSTYLE_NORMAL,wx.FONTWEIGHT_SEMIBOLD)
        self.mdp2.SetCalendarFont(font)                                              # Set Calendar Font

        y = x + datetime.timedelta(weeks=+4)
        self.mdp2.SetValue(y.timestamp())

        self.Bind(EVT_DATE_CHANGED, self.OnEvent)
        self.Centre()

    def OnEvent(self, event):
        obj = event.GetEventObject()
        print("\nevt", event.GetValue())

        x = event.GetDate() # wx.DateTime object
        print("evt", x)
        print("Day", x.GetDayOfYear(),"Week", x.GetWeekOfYear(), "Name", x.GetWeekDayName(x.GetWeekDay()))

        print("evt", event.GetDateTime()) # datetime.datime object
        print("evt", event.GetTimeStamp())
        print("func", obj.GetValue())
        print("func", obj.GetDate())
        print("func", obj.GetDateTimeValue())
        print("func", obj.GetTimeStamp())
        print("func", obj.GetLocale())

if __name__ == '__main__':
    app = wx.App()
    frame = DemoFrame(None)
    frame.Show()
    app.MainLoop()
}}}

--------
= Download source =
[[attachment: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....