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