calendar_.py 68 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614
  1. # -*- coding: utf-8 -*-
  2. """
  3. tkcalendar - Calendar and DateEntry widgets for Tkinter
  4. Copyright 2017-2019 Juliette Monsel <j_4321@protonmail.com>
  5. with contributions from:
  6. - Neal Probert (https://github.com/nprobert)
  7. - arahorn28 (https://github.com/arahorn28)
  8. tkcalendar is free software: you can redistribute it and/or modify
  9. it under the terms of the GNU General Public License as published by
  10. the Free Software Foundation, either version 3 of the License, or
  11. (at your option) any later version.
  12. tkcalendar is distributed in the hope that it will be useful,
  13. but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. GNU General Public License for more details.
  16. You should have received a copy of the GNU General Public License
  17. along with this program. If not, see <http://www.gnu.org/licenses/>.
  18. Calendar widget
  19. """
  20. import calendar
  21. try:
  22. from tkinter import ttk
  23. from tkinter.font import Font
  24. except ImportError:
  25. import ttk
  26. from tkFont import Font
  27. from babel import default_locale
  28. from babel.dates import format_date, parse_date, get_day_names, get_month_names, get_date_format
  29. from tkcalendar.tooltip import TooltipWrapper
  30. import re
  31. class Calendar(ttk.Frame):
  32. """Calendar widget."""
  33. date = calendar.datetime.date
  34. datetime = calendar.datetime.datetime
  35. timedelta = calendar.datetime.timedelta
  36. strptime = calendar.datetime.datetime.strptime
  37. strftime = calendar.datetime.datetime.strftime
  38. def __init__(self, master=None, **kw):
  39. """
  40. Construct a Calendar with parent master.
  41. Standard Options
  42. ----------------
  43. cursor, font, borderwidth, state
  44. Widget-specific Options
  45. -----------------------
  46. year : int
  47. intinitially displayed year, default is current year.
  48. month : int
  49. initially displayed month, default is current month.
  50. day : int
  51. initially selected day, if month or year is given but not day, no initial selection, otherwise, default is today.
  52. firstweekday : "monday" (default) or "sunday"
  53. first day of the week
  54. weekenddays : list
  55. days to be displayed as week-end days given as a list of integers corresponding to the number of the day in the week
  56. (e.g. [6, 7] for the last two days of the week).
  57. mindate : None (default), datetime.date or datetime.datetime
  58. minimum allowed date
  59. maxdate : None (default), datetime.date or datetime.datetime
  60. maximum allowed date
  61. showweeknumbers : bool (default is True)
  62. whether to display week numbers.
  63. showothermonthdays : bool (default is True)
  64. whether to display the last days of the previous month and the first of the next month.
  65. locale : str
  66. locale to use, e.g. 'en_US'
  67. date_pattern : str
  68. date pattern used to format the date as a string. The default
  69. pattern used is babel's short date format in the Calendar's locale.
  70. A valid pattern is a combination of 'y', 'm' and 'd' separated by
  71. non letter characters to indicate how and in which order the
  72. year, month and day should be displayed.
  73. y: 'yy' for the last two digits of the year, any other number of 'y's for
  74. the full year with an extra padding of zero if it has less
  75. digits than the number of 'y's.
  76. m: 'm' for the month number without padding, 'mm' for a
  77. two-digit month
  78. d: 'd' for the day of month number without padding, 'dd' for a
  79. two-digit day
  80. Examples for datetime.date(2019, 7, 1):
  81. 'y-mm-dd' → '2019-07-01'
  82. 'm/d/yy' → '7/1/19'
  83. selectmode : "none" or "day" (default)
  84. whether the user can change the selected day with a mouse click.
  85. textvariable : StringVar
  86. connect the currently selected date to the variable.
  87. Style Options
  88. -------------
  89. background : str
  90. background color of calendar border and month/year name
  91. foreground : str
  92. foreground color of month/year name
  93. disabledbackground : str
  94. background color of calendar border and month/year name in disabled state
  95. disabledforeground : str
  96. foreground color of month/year name in disabled state
  97. bordercolor : str
  98. day border color
  99. headersbackground : str
  100. background color of day names and week numbers
  101. headersforeground : str
  102. foreground color of day names and week numbers
  103. selectbackground : str
  104. background color of selected day
  105. selectforeground : str
  106. foreground color of selected day
  107. disabledselectbackground : str
  108. background color of selected day in disabled state
  109. disabledselectforeground : str
  110. foreground color of selected day in disabled state
  111. normalbackground : str
  112. background color of normal week days
  113. normalforeground : str
  114. foreground color of normal week days
  115. weekendbackground : str
  116. background color of week-end days
  117. weekendforeground : str
  118. foreground color of week-end days
  119. othermonthforeground : str
  120. foreground color of normal week days belonging to the previous/next month
  121. othermonthbackground : str
  122. background color of normal week days belonging to the previous/next month
  123. othermonthweforeground : str
  124. foreground color of week-end days belonging to the previous/next month
  125. othermonthwebackground : str
  126. background color of week-end days belonging to the previous/next month
  127. disableddaybackground : str
  128. background color of days in disabled state
  129. disableddayforeground : str
  130. foreground color of days in disabled state
  131. Tooltip Options (for calevents)
  132. -------------------------------
  133. tooltipforeground : str
  134. tooltip text color
  135. tooltipbackground : str
  136. tooltip background color
  137. tooltipalpha : float
  138. tooltip opacity between 0 and 1
  139. tooltipdelay : int
  140. delay in ms before displaying the tooltip
  141. Virtual Event
  142. -------------
  143. A ``<<CalendarSelected>>`` event is generated each time the user
  144. selects a day with the mouse.
  145. A ``<<CalendarMonthChanged>>`` event is generated each time the user
  146. changes the displayed month.
  147. Calendar Events
  148. ---------------
  149. Special events (e.g. birthdays, ..) can be managed using the
  150. ``calevent_..`` methods. The way they are displayed in the calendar is
  151. determined with tags. An id is attributed to each event upon creation
  152. and can be used to edit the event (ev_id argument).
  153. """
  154. curs = kw.pop("cursor", "")
  155. font = kw.pop("font", "Liberation\ Sans 9")
  156. classname = kw.pop('class_', "Calendar")
  157. name = kw.pop('name', None)
  158. ttk.Frame.__init__(self, master, class_=classname, cursor=curs, name=name)
  159. self._style_prefixe = str(self)
  160. ttk.Frame.configure(self, style='main.%s.TFrame' % self._style_prefixe)
  161. self._textvariable = kw.pop("textvariable", None)
  162. self._font = Font(self, font)
  163. prop = self._font.actual()
  164. prop["size"] += 1
  165. self._header_font = Font(self, **prop)
  166. # state
  167. state = kw.get('state', 'normal')
  168. try:
  169. bd = int(kw.pop('borderwidth', 2))
  170. except ValueError:
  171. raise ValueError("expected integer for the 'borderwidth' option.")
  172. firstweekday = kw.pop('firstweekday', 'monday')
  173. if firstweekday not in ["monday", "sunday"]:
  174. raise ValueError("'firstweekday' option should be 'monday' or 'sunday'.")
  175. self._cal = calendar.TextCalendar((firstweekday == 'sunday') * 6)
  176. weekenddays = kw.pop("weekenddays", None)
  177. if not weekenddays:
  178. l = list(self._cal.iterweekdays())
  179. weekenddays = [l.index(5) + 1, l.index(6) + 1] # saturday and sunday
  180. self._check_weekenddays(weekenddays)
  181. # --- locale
  182. locale = kw.pop("locale", default_locale())
  183. if locale is None:
  184. locale = 'en'
  185. self._day_names = get_day_names('abbreviated', locale=locale)
  186. self._month_names = get_month_names('wide', locale=locale)
  187. date_pattern = self._get_date_pattern(kw.pop("date_pattern", "short"), locale)
  188. # --- date
  189. today = self.date.today()
  190. if self._textvariable is not None:
  191. # the variable overrides day, month and year keywords
  192. try:
  193. self._sel_date = parse_date(self._textvariable.get(), locale)
  194. month = self._sel_date.month
  195. year = self._sel_date.year
  196. except IndexError:
  197. self._sel_date = None
  198. self._textvariable.set('')
  199. month = kw.pop("month", today.month)
  200. year = kw.pop('year', today.year)
  201. else:
  202. if (("month" in kw) or ("year" in kw)) and ("day" not in kw):
  203. month = kw.pop("month", today.month)
  204. year = kw.pop('year', today.year)
  205. self._sel_date = None # selected day
  206. else:
  207. day = kw.pop('day', today.day)
  208. month = kw.pop("month", today.month)
  209. year = kw.pop('year', today.year)
  210. try:
  211. self._sel_date = self.date(year, month, day) # selected day
  212. except ValueError:
  213. self._sel_date = None
  214. self._date = self.date(year, month, 1) # (year, month) displayed by the calendar
  215. # --- date limits
  216. maxdate = kw.pop('maxdate', None)
  217. mindate = kw.pop('mindate', None)
  218. if maxdate is not None:
  219. if isinstance(maxdate, self.datetime):
  220. maxdate = maxdate.date()
  221. elif not isinstance(maxdate, self.date):
  222. raise TypeError("expected %s for the 'maxdate' option." % self.date)
  223. if mindate is not None:
  224. if isinstance(mindate, self.datetime):
  225. mindate = mindate.date()
  226. elif not isinstance(mindate, self.date):
  227. raise TypeError("expected %s for the 'mindate' option." % self.date)
  228. if (mindate is not None) and (maxdate is not None) and (mindate > maxdate):
  229. raise ValueError("mindate should be smaller than maxdate.")
  230. # --- selectmode
  231. selectmode = kw.pop("selectmode", "day")
  232. if selectmode not in ("none", "day"):
  233. raise ValueError("'selectmode' option should be 'none' or 'day'.")
  234. # --- show week numbers
  235. showweeknumbers = kw.pop('showweeknumbers', True)
  236. # --- style
  237. self.style = ttk.Style(self)
  238. active_bg = self.style.lookup('TEntry', 'selectbackground', ('focus',))
  239. dis_active_bg = self.style.lookup('TEntry', 'selectbackground', ('disabled',))
  240. dis_bg = self.style.lookup('TLabel', 'background', ('disabled',))
  241. dis_fg = self.style.lookup('TLabel', 'foreground', ('disabled',))
  242. # --- properties
  243. options = ['cursor',
  244. 'font',
  245. 'borderwidth',
  246. 'state',
  247. 'selectmode',
  248. 'textvariable',
  249. 'locale',
  250. 'date_pattern',
  251. 'maxdate',
  252. 'mindate',
  253. 'showweeknumbers',
  254. 'showothermonthdays',
  255. 'firstweekday',
  256. 'weekenddays',
  257. 'selectbackground',
  258. 'selectforeground',
  259. 'disabledselectbackground',
  260. 'disabledselectforeground',
  261. 'normalbackground',
  262. 'normalforeground',
  263. 'background',
  264. 'foreground',
  265. 'disabledbackground',
  266. 'disabledforeground',
  267. 'bordercolor',
  268. 'othermonthforeground',
  269. 'othermonthbackground',
  270. 'othermonthweforeground',
  271. 'othermonthwebackground',
  272. 'weekendbackground',
  273. 'weekendforeground',
  274. 'headersbackground',
  275. 'headersforeground',
  276. 'disableddaybackground',
  277. 'disableddayforeground',
  278. 'tooltipforeground',
  279. 'tooltipbackground',
  280. 'tooltipalpha',
  281. 'tooltipdelay']
  282. keys = list(kw.keys())
  283. for option in keys:
  284. if option not in options:
  285. del(kw[option])
  286. self._properties = {"cursor": curs,
  287. "font": font,
  288. "borderwidth": bd,
  289. "state": state,
  290. "locale": locale,
  291. "date_pattern": date_pattern,
  292. "selectmode": selectmode,
  293. 'textvariable': self._textvariable,
  294. 'firstweekday': firstweekday,
  295. 'weekenddays': weekenddays,
  296. 'mindate': mindate,
  297. 'maxdate': maxdate,
  298. 'showweeknumbers': showweeknumbers,
  299. 'showothermonthdays': kw.pop('showothermonthdays', True),
  300. 'selectbackground': active_bg,
  301. 'selectforeground': 'white',
  302. 'disabledselectbackground': dis_active_bg,
  303. 'disabledselectforeground': 'white',
  304. 'normalbackground': 'white',
  305. 'normalforeground': 'black',
  306. 'background': 'gray30',
  307. 'foreground': 'white',
  308. 'disabledbackground': 'gray30',
  309. 'disabledforeground': 'gray70',
  310. 'bordercolor': 'gray70',
  311. 'othermonthforeground': 'gray45',
  312. 'othermonthbackground': 'gray93',
  313. 'othermonthweforeground': 'gray45',
  314. 'othermonthwebackground': 'gray75',
  315. 'weekendbackground': 'gray80',
  316. 'weekendforeground': 'gray30',
  317. 'headersbackground': 'gray70',
  318. 'headersforeground': 'black',
  319. 'disableddaybackground': dis_bg,
  320. 'disableddayforeground': dis_fg,
  321. 'tooltipforeground': 'gray90',
  322. 'tooltipbackground': 'black',
  323. 'tooltipalpha': 0.8,
  324. 'tooltipdelay': 2000}
  325. self._properties.update(kw)
  326. # --- calevents
  327. self.calevents = {} # special events displayed in colors and with tooltips to show content
  328. self._calevent_dates = {} # list of event ids for each date
  329. self._tags = {} # tags to format event display
  330. self.tooltip_wrapper = TooltipWrapper(self,
  331. alpha=self._properties['tooltipalpha'],
  332. style=self._style_prefixe + '.tooltip.TLabel',
  333. delay=self._properties['tooltipdelay'])
  334. # --- init calendar
  335. # --- *-- header: month - year
  336. self._header = ttk.Frame(self, style='main.%s.TFrame' % self._style_prefixe)
  337. f_month = ttk.Frame(self._header,
  338. style='main.%s.TFrame' % self._style_prefixe)
  339. self._l_month = ttk.Button(f_month,
  340. style='L.%s.TButton' % self._style_prefixe,
  341. command=self._prev_month)
  342. self._header_month = ttk.Label(f_month, width=10, anchor='center',
  343. style='main.%s.TLabel' % self._style_prefixe, font=self._header_font)
  344. self._r_month = ttk.Button(f_month,
  345. style='R.%s.TButton' % self._style_prefixe,
  346. command=self._next_month)
  347. self._l_month.pack(side='left', fill="y")
  348. self._header_month.pack(side='left', padx=4)
  349. self._r_month.pack(side='left', fill="y")
  350. f_year = ttk.Frame(self._header, style='main.%s.TFrame' % self._style_prefixe)
  351. self._l_year = ttk.Button(f_year, style='L.%s.TButton' % self._style_prefixe,
  352. command=self._prev_year)
  353. self._header_year = ttk.Label(f_year, width=4, anchor='center',
  354. style='main.%s.TLabel' % self._style_prefixe, font=self._header_font)
  355. self._r_year = ttk.Button(f_year, style='R.%s.TButton' % self._style_prefixe,
  356. command=self._next_year)
  357. self._l_year.pack(side='left', fill="y")
  358. self._header_year.pack(side='left', padx=4)
  359. self._r_year.pack(side='left', fill="y")
  360. f_month.pack(side='left', fill='x')
  361. f_year.pack(side='right')
  362. # --- *-- calendar
  363. self._cal_frame = ttk.Frame(self,
  364. style='cal.%s.TFrame' % self._style_prefixe)
  365. ttk.Label(self._cal_frame,
  366. style='headers.%s.TLabel' % self._style_prefixe).grid(row=0,
  367. column=0,
  368. sticky="eswn")
  369. # week day names
  370. self._week_days = []
  371. for i, day in enumerate(self._cal.iterweekdays()):
  372. d = self._day_names[day % 7]
  373. self._cal_frame.columnconfigure(i + 1, weight=1)
  374. self._week_days.append(ttk.Label(self._cal_frame,
  375. font=self._font,
  376. style='headers.%s.TLabel' % self._style_prefixe,
  377. anchor="center",
  378. text=d, width=4))
  379. self._week_days[-1].grid(row=0, column=i + 1, sticky="ew", pady=(0, 1))
  380. self._week_nbs = [] # week numbers
  381. self._calendar = [] # days
  382. for i in range(1, 7):
  383. self._cal_frame.rowconfigure(i, weight=1)
  384. wlabel = ttk.Label(self._cal_frame, style='headers.%s.TLabel' % self._style_prefixe,
  385. font=self._font, padding=2,
  386. anchor="e", width=2)
  387. self._week_nbs.append(wlabel)
  388. wlabel.grid(row=i, column=0, sticky="esnw", padx=(0, 1))
  389. if not showweeknumbers:
  390. wlabel.grid_remove()
  391. self._calendar.append([])
  392. for j in range(1, 8):
  393. label = ttk.Label(self._cal_frame, style='normal.%s.TLabel' % self._style_prefixe,
  394. font=self._font, anchor="center")
  395. self._calendar[-1].append(label)
  396. label.grid(row=i, column=j, padx=(0, 1), pady=(0, 1), sticky="nsew")
  397. if selectmode == "day":
  398. label.bind("<1>", self._on_click)
  399. # --- *-- pack main elements
  400. self._header.pack(fill="x", padx=2, pady=2)
  401. self._cal_frame.pack(fill="both", expand=True, padx=bd, pady=bd)
  402. self.config(state=state)
  403. # --- bindings
  404. self.bind('<<ThemeChanged>>', self._setup_style)
  405. self._setup_style()
  406. self._display_calendar()
  407. self._btns_date_range()
  408. self._check_sel_date()
  409. if self._textvariable is not None:
  410. try:
  411. self._textvariable_trace_id = self._textvariable.trace_add('write', self._textvariable_trace)
  412. except AttributeError:
  413. self._textvariable_trace_id = self._textvariable.trace('w', self._textvariable_trace)
  414. def __getitem__(self, key):
  415. """Return the resource value for a KEY given as string."""
  416. try:
  417. return self._properties[key]
  418. except KeyError:
  419. raise AttributeError("Calendar object has no attribute %s." % key)
  420. def __setitem__(self, key, value):
  421. if key not in self._properties:
  422. raise AttributeError("Calendar object has no attribute %s." % key)
  423. elif key == 'date_pattern':
  424. date_pattern = self._get_date_pattern(value)
  425. self._properties[key] = date_pattern
  426. else:
  427. if key == "selectmode":
  428. if value == "none":
  429. for week in self._calendar:
  430. for day in week:
  431. day.unbind("<1>")
  432. elif value == "day":
  433. for week in self._calendar:
  434. for day in week:
  435. day.bind("<1>", self._on_click)
  436. else:
  437. raise ValueError("'selectmode' option should be 'none' or 'day'.")
  438. elif key == "locale":
  439. self._day_names = get_day_names('abbreviated', locale=value)
  440. self._month_names = get_month_names('wide', locale=value)
  441. self._properties['date_pattern'] = self._get_date_pattern("short", value)
  442. for i, l in enumerate(self._week_days):
  443. l.configure(text=self._day_names[i])
  444. self._header_month.configure(text=self._month_names[self._date.month].title())
  445. elif key == 'textvariable':
  446. try:
  447. if self._textvariable is not None:
  448. self._textvariable.trace_remove('write', self._textvariable_trace_id)
  449. if value is not None:
  450. self._textvariable_trace_id = value.trace_add('write', self._textvariable_trace)
  451. except AttributeError:
  452. if self._textvariable is not None:
  453. self._textvariable.trace_vdelete('w', self._textvariable_trace_id)
  454. if value is not None:
  455. value.trace('w', self._textvariable_trace)
  456. self._textvariable = value
  457. value.set(value.get())
  458. elif key == 'showweeknumbers':
  459. if value:
  460. for wlabel in self._week_nbs:
  461. wlabel.grid()
  462. else:
  463. for wlabel in self._week_nbs:
  464. wlabel.grid_remove()
  465. elif key == 'firstweekday':
  466. if value not in ["monday", "sunday"]:
  467. raise ValueError("'firstweekday' option should be 'monday' or 'sunday'.")
  468. self._cal.firstweekday = (value == 'sunday') * 6
  469. for label, day in zip(self._week_days, self._cal.iterweekdays()):
  470. label.configure(text=self._day_names[day % 7])
  471. elif key == 'weekenddays':
  472. self._check_weekenddays(value)
  473. elif key == 'borderwidth':
  474. try:
  475. bd = int(value)
  476. self._cal_frame.pack_configure(padx=bd, pady=bd)
  477. except ValueError:
  478. raise ValueError('expected integer for the borderwidth option.')
  479. elif key == 'state':
  480. if value not in ['normal', 'disabled']:
  481. raise ValueError("bad state '%s': must be disabled or normal" % value)
  482. else:
  483. state = '!' * (value == 'normal') + 'disabled'
  484. self.state((state,))
  485. self._header.state((state,))
  486. for child in self._header.children.values():
  487. child.state((state,))
  488. self._header_month.state((state,))
  489. self._header_year.state((state,))
  490. self._l_year.state((state,))
  491. self._r_year.state((state,))
  492. self._l_month.state((state,))
  493. self._r_month.state((state,))
  494. for child in self._cal_frame.children.values():
  495. child.state((state,))
  496. elif key == "maxdate":
  497. if value is not None:
  498. if isinstance(value, self.datetime):
  499. value = value.date()
  500. elif not isinstance(value, self.date):
  501. raise TypeError("expected %s for the 'maxdate' option." % self.date)
  502. mindate = self['mindate']
  503. if mindate is not None and mindate > value:
  504. self._properties['mindate'] = value
  505. self._date = self._date.replace(year=value.year, month=value.month)
  506. elif self._date > value:
  507. self._date = self._date.replace(year=value.year, month=value.month)
  508. self._r_month.state(['!disabled'])
  509. self._r_year.state(['!disabled'])
  510. self._l_month.state(['!disabled'])
  511. self._l_year.state(['!disabled'])
  512. elif key == "mindate":
  513. if value is not None:
  514. if isinstance(value, self.datetime):
  515. value = value.date()
  516. elif not isinstance(value, self.date):
  517. raise TypeError("expected %s for the 'mindate' option." % self.date)
  518. maxdate = self['maxdate']
  519. if maxdate is not None and maxdate < value:
  520. self._properties['maxdate'] = value
  521. self._date = self._date.replace(year=value.year, month=value.month)
  522. elif self._date < value:
  523. self._date = self._date.replace(year=value.year, month=value.month)
  524. self._r_month.state(['!disabled'])
  525. self._r_year.state(['!disabled'])
  526. self._l_month.state(['!disabled'])
  527. self._l_year.state(['!disabled'])
  528. elif key == "font":
  529. font = Font(self, value)
  530. prop = font.actual()
  531. self._font.configure(**prop)
  532. prop["size"] += 1
  533. self._header_font.configure(**prop)
  534. size = max(prop["size"], 10)
  535. self.style.configure('R.%s.TButton' % self._style_prefixe, arrowsize=size)
  536. self.style.configure('L.%s.TButton' % self._style_prefixe, arrowsize=size)
  537. elif key == "normalbackground":
  538. self.style.configure('cal.%s.TFrame' % self._style_prefixe, background=value)
  539. self.style.configure('normal.%s.TLabel' % self._style_prefixe, background=value)
  540. self.style.configure('normal_om.%s.TLabel' % self._style_prefixe, background=value)
  541. elif key == "normalforeground":
  542. self.style.configure('normal.%s.TLabel' % self._style_prefixe, foreground=value)
  543. elif key == "bordercolor":
  544. self.style.configure('cal.%s.TFrame' % self._style_prefixe, background=value)
  545. elif key == "othermonthforeground":
  546. self.style.configure('normal_om.%s.TLabel' % self._style_prefixe, foreground=value)
  547. elif key == "othermonthbackground":
  548. self.style.configure('normal_om.%s.TLabel' % self._style_prefixe, background=value)
  549. elif key == "othermonthweforeground":
  550. self.style.configure('we_om.%s.TLabel' % self._style_prefixe, foreground=value)
  551. elif key == "othermonthwebackground":
  552. self.style.configure('we_om.%s.TLabel' % self._style_prefixe, background=value)
  553. elif key == "selectbackground":
  554. self.style.configure('sel.%s.TLabel' % self._style_prefixe, background=value)
  555. elif key == "selectforeground":
  556. self.style.configure('sel.%s.TLabel' % self._style_prefixe, foreground=value)
  557. elif key == "disabledselectbackground":
  558. self.style.map('sel.%s.TLabel' % self._style_prefixe, background=[('disabled', value)])
  559. elif key == "disabledselectforeground":
  560. self.style.map('sel.%s.TLabel' % self._style_prefixe, foreground=[('disabled', value)])
  561. elif key == "disableddaybackground":
  562. self.style.map('%s.TLabel' % self._style_prefixe, background=[('disabled', value)])
  563. elif key == "disableddayforeground":
  564. self.style.map('%s.TLabel' % self._style_prefixe, foreground=[('disabled', value)])
  565. elif key == "weekendbackground":
  566. self.style.configure('we.%s.TLabel' % self._style_prefixe, background=value)
  567. self.style.configure('we_om.%s.TLabel' % self._style_prefixe, background=value)
  568. elif key == "weekendforeground":
  569. self.style.configure('we.%s.TLabel' % self._style_prefixe, foreground=value)
  570. elif key == "headersbackground":
  571. self.style.configure('headers.%s.TLabel' % self._style_prefixe, background=value)
  572. elif key == "headersforeground":
  573. self.style.configure('headers.%s.TLabel' % self._style_prefixe, foreground=value)
  574. elif key == "background":
  575. self.style.configure('main.%s.TFrame' % self._style_prefixe, background=value)
  576. self.style.configure('main.%s.TLabel' % self._style_prefixe, background=value)
  577. self.style.configure('R.%s.TButton' % self._style_prefixe, background=value,
  578. bordercolor=value,
  579. lightcolor=value, darkcolor=value)
  580. self.style.configure('L.%s.TButton' % self._style_prefixe, background=value,
  581. bordercolor=value,
  582. lightcolor=value, darkcolor=value)
  583. elif key == "foreground":
  584. self.style.configure('R.%s.TButton' % self._style_prefixe, arrowcolor=value)
  585. self.style.configure('L.%s.TButton' % self._style_prefixe, arrowcolor=value)
  586. self.style.configure('main.%s.TLabel' % self._style_prefixe, foreground=value)
  587. elif key == "disabledbackground":
  588. self.style.map('%s.TButton' % self._style_prefixe,
  589. background=[('active', '!disabled', self.style.lookup('TEntry', 'selectbackground', ('focus',))),
  590. ('disabled', value)],)
  591. self.style.map('main.%s.TFrame' % self._style_prefixe,
  592. background=[('disabled', value)])
  593. self.style.map('main.%s.TLabel' % self._style_prefixe,
  594. background=[('disabled', value)])
  595. elif key == "disabledforeground":
  596. self.style.map('%s.TButton' % self._style_prefixe,
  597. arrowcolor=[('disabled', value)])
  598. self.style.map('main.%s.TLabel' % self._style_prefixe,
  599. foreground=[('disabled', value)])
  600. elif key == "cursor":
  601. ttk.Frame.configure(self, cursor=value)
  602. elif key == "tooltipbackground":
  603. self.style.configure('%s.tooltip.TLabel' % self._style_prefixe,
  604. background=value)
  605. elif key == "tooltipforeground":
  606. self.style.configure('%s.tooltip.TLabel' % self._style_prefixe,
  607. foreground=value)
  608. elif key == "tooltipalpha":
  609. self.tooltip_wrapper.configure(alpha=value)
  610. elif key == "tooltipdelay":
  611. self.tooltip_wrapper.configure(delay=value)
  612. self._properties[key] = value
  613. if key in ['showothermonthdays', 'firstweekday', 'weekenddays',
  614. 'maxdate', 'mindate']:
  615. self._display_calendar()
  616. self._check_sel_date()
  617. self._btns_date_range()
  618. @staticmethod
  619. def _check_weekenddays(weekenddays):
  620. try:
  621. if len(weekenddays) != 2:
  622. raise ValueError("weekenddays should be a list of two days.")
  623. else:
  624. for d in weekenddays:
  625. if d not in range(1, 8):
  626. raise ValueError("weekenddays should contain integers between 1 and 7.")
  627. except TypeError:
  628. raise TypeError("weekenddays should be a list of two days.")
  629. def _textvariable_trace(self, *args):
  630. """Connect StringVar value with selected date."""
  631. if self._properties.get("selectmode") == "day":
  632. date = self._textvariable.get()
  633. if not date:
  634. self._remove_selection()
  635. self._sel_date = None
  636. else:
  637. try:
  638. self._sel_date = self.parse_date(date)
  639. except Exception:
  640. if self._sel_date is None:
  641. self._textvariable.set('')
  642. else:
  643. self._textvariable.set(self.format_date(self._sel_date))
  644. raise ValueError("%r is not a valid date." % date)
  645. else:
  646. self._date = self._sel_date.replace(day=1)
  647. self._display_calendar()
  648. self._display_selection()
  649. def _setup_style(self, event=None):
  650. """Configure style."""
  651. self.style.layout('L.%s.TButton' % self._style_prefixe,
  652. [('Button.focus',
  653. {'children': [('Button.leftarrow', None)]})])
  654. self.style.layout('R.%s.TButton' % self._style_prefixe,
  655. [('Button.focus',
  656. {'children': [('Button.rightarrow', None)]})])
  657. active_bg = self.style.lookup('TEntry', 'selectbackground', ('focus',))
  658. sel_bg = self._properties.get('selectbackground')
  659. sel_fg = self._properties.get('selectforeground')
  660. dis_sel_bg = self._properties.get('disabledselectbackground')
  661. dis_sel_fg = self._properties.get('disabledselectforeground')
  662. dis_day_bg = self._properties.get('disableddaybackground')
  663. dis_day_fg = self._properties.get('disableddayforeground')
  664. cal_bg = self._properties.get('normalbackground')
  665. cal_fg = self._properties.get('normalforeground')
  666. hd_bg = self._properties.get("headersbackground")
  667. hd_fg = self._properties.get("headersforeground")
  668. bg = self._properties.get('background')
  669. fg = self._properties.get('foreground')
  670. dis_bg = self._properties.get('disabledbackground')
  671. dis_fg = self._properties.get('disabledforeground')
  672. bc = self._properties.get('bordercolor')
  673. om_fg = self._properties.get('othermonthforeground')
  674. om_bg = self._properties.get('othermonthbackground')
  675. omwe_fg = self._properties.get('othermonthweforeground')
  676. omwe_bg = self._properties.get('othermonthwebackground')
  677. we_bg = self._properties.get('weekendbackground')
  678. we_fg = self._properties.get('weekendforeground')
  679. self.style.configure('main.%s.TFrame' % self._style_prefixe, background=bg)
  680. self.style.configure('cal.%s.TFrame' % self._style_prefixe, background=bc)
  681. self.style.configure('main.%s.TLabel' % self._style_prefixe, background=bg, foreground=fg)
  682. self.style.configure('headers.%s.TLabel' % self._style_prefixe, background=hd_bg,
  683. foreground=hd_fg)
  684. self.style.configure('normal.%s.TLabel' % self._style_prefixe, background=cal_bg,
  685. foreground=cal_fg)
  686. self.style.configure('normal_om.%s.TLabel' % self._style_prefixe, background=om_bg,
  687. foreground=om_fg)
  688. self.style.configure('we_om.%s.TLabel' % self._style_prefixe, background=omwe_bg,
  689. foreground=omwe_fg)
  690. self.style.configure('sel.%s.TLabel' % self._style_prefixe, background=sel_bg,
  691. foreground=sel_fg)
  692. self.style.configure('we.%s.TLabel' % self._style_prefixe, background=we_bg,
  693. foreground=we_fg)
  694. size = max(self._header_font.actual()["size"], 10)
  695. self.style.configure('%s.TButton' % self._style_prefixe, background=bg,
  696. arrowcolor=fg, arrowsize=size, bordercolor=bg,
  697. relief="flat", lightcolor=bg, darkcolor=bg)
  698. self.style.configure('%s.tooltip.TLabel' % self._style_prefixe,
  699. background=self._properties['tooltipbackground'],
  700. foreground=self._properties['tooltipforeground'])
  701. self.style.map('%s.TButton' % self._style_prefixe,
  702. background=[('active', '!disabled', active_bg), ('disabled', dis_bg)],
  703. bordercolor=[('active', active_bg)],
  704. relief=[('active', 'flat')],
  705. arrowcolor=[('disabled', dis_fg)],
  706. darkcolor=[('active', active_bg)],
  707. lightcolor=[('active', active_bg)])
  708. self.style.map('main.%s.TFrame' % self._style_prefixe,
  709. background=[('disabled', dis_bg)])
  710. self.style.map('main.%s.TLabel' % self._style_prefixe,
  711. background=[('disabled', dis_bg)],
  712. foreground=[('disabled', dis_fg)])
  713. self.style.map('sel.%s.TLabel' % self._style_prefixe,
  714. background=[('disabled', dis_sel_bg)],
  715. foreground=[('disabled', dis_sel_fg)])
  716. self.style.map(self._style_prefixe + '.TLabel',
  717. background=[('disabled', dis_day_bg)],
  718. foreground=[('disabled', dis_day_fg)])
  719. # --- display
  720. def _display_calendar(self):
  721. """Display the days of the current month (the one in self._date)."""
  722. year, month = self._date.year, self._date.month
  723. # update header text (Month, Year)
  724. header = self._month_names[month]
  725. self._header_month.configure(text=header.title())
  726. self._header_year.configure(text=str(year))
  727. # remove previous tooltips
  728. self.tooltip_wrapper.remove_all()
  729. # update calendar shown dates
  730. if self['showothermonthdays']:
  731. self._display_days_with_othermonthdays()
  732. else:
  733. self._display_days_without_othermonthdays()
  734. self._display_selection()
  735. maxdate = self['maxdate']
  736. mindate = self['mindate']
  737. if maxdate is not None:
  738. mi, mj = self._get_day_coords(maxdate)
  739. if mi is not None:
  740. for j in range(mj + 1, 7):
  741. self._calendar[mi][j].state(['disabled'])
  742. for i in range(mi + 1, 6):
  743. for j in range(7):
  744. self._calendar[i][j].state(['disabled'])
  745. if mindate is not None:
  746. mi, mj = self._get_day_coords(mindate)
  747. if mi is not None:
  748. for j in range(mj):
  749. self._calendar[mi][j].state(['disabled'])
  750. for i in range(mi):
  751. for j in range(7):
  752. self._calendar[i][j].state(['disabled'])
  753. def _display_days_without_othermonthdays(self):
  754. year, month = self._date.year, self._date.month
  755. cal = self._cal.monthdays2calendar(year, month)
  756. while len(cal) < 6:
  757. cal.append([(0, i) for i in range(7)])
  758. week_days = {i: 'normal.%s.TLabel' % self._style_prefixe for i in range(7)} # style names depending on the type of day
  759. week_days[self['weekenddays'][0] - 1] = 'we.%s.TLabel' % self._style_prefixe
  760. week_days[self['weekenddays'][1] - 1] = 'we.%s.TLabel' % self._style_prefixe
  761. _, week_nb, d = self._date.isocalendar()
  762. if d == 7 and self['firstweekday'] == 'sunday':
  763. week_nb += 1
  764. modulo = max(week_nb, 52)
  765. for i_week in range(6):
  766. if i_week == 0 or cal[i_week][0][0]:
  767. self._week_nbs[i_week].configure(text=str((week_nb + i_week - 1) % modulo + 1))
  768. else:
  769. self._week_nbs[i_week].configure(text='')
  770. for i_day in range(7):
  771. day_number, week_day = cal[i_week][i_day]
  772. style = week_days[i_day]
  773. label = self._calendar[i_week][i_day]
  774. label.state(['!disabled'])
  775. if day_number:
  776. txt = str(day_number)
  777. label.configure(text=txt, style=style)
  778. date = self.date(year, month, day_number)
  779. if date in self._calevent_dates:
  780. ev_ids = self._calevent_dates[date]
  781. i = len(ev_ids) - 1
  782. while i >= 0 and not self.calevents[ev_ids[i]]['tags']:
  783. i -= 1
  784. if i >= 0:
  785. tag = self.calevents[ev_ids[i]]['tags'][-1]
  786. label.configure(style='tag_%s.%s.TLabel' % (tag, self._style_prefixe))
  787. text = '\n'.join(['➢ {}'.format(self.calevents[ev]['text']) for ev in ev_ids])
  788. self.tooltip_wrapper.add_tooltip(label, text)
  789. else:
  790. label.configure(text='', style=style)
  791. def _display_days_with_othermonthdays(self):
  792. year, month = self._date.year, self._date.month
  793. cal = self._cal.monthdatescalendar(year, month)
  794. next_m = month + 1
  795. y = year
  796. if next_m == 13:
  797. next_m = 1
  798. y += 1
  799. if len(cal) < 6:
  800. if cal[-1][-1].month == month:
  801. i = 0
  802. else:
  803. i = 1
  804. cal.append(self._cal.monthdatescalendar(y, next_m)[i])
  805. if len(cal) < 6:
  806. cal.append(self._cal.monthdatescalendar(y, next_m)[i + 1])
  807. week_days = {i: 'normal' for i in range(7)} # style names depending on the type of day
  808. week_days[self['weekenddays'][0] - 1] = 'we'
  809. week_days[self['weekenddays'][1] - 1] = 'we'
  810. prev_m = (month - 2) % 12 + 1
  811. months = {month: '.%s.TLabel' % self._style_prefixe,
  812. next_m: '_om.%s.TLabel' % self._style_prefixe,
  813. prev_m: '_om.%s.TLabel' % self._style_prefixe}
  814. week_nb = cal[0][1].isocalendar()[1]
  815. modulo = max(week_nb, 52)
  816. for i_week in range(6):
  817. self._week_nbs[i_week].configure(text=str((week_nb + i_week - 1) % modulo + 1))
  818. for i_day in range(7):
  819. style = week_days[i_day] + months[cal[i_week][i_day].month]
  820. label = self._calendar[i_week][i_day]
  821. label.state(['!disabled'])
  822. txt = str(cal[i_week][i_day].day)
  823. label.configure(text=txt, style=style)
  824. if cal[i_week][i_day] in self._calevent_dates:
  825. date = cal[i_week][i_day]
  826. ev_ids = self._calevent_dates[date]
  827. i = len(ev_ids) - 1
  828. while i >= 0 and not self.calevents[ev_ids[i]]['tags']:
  829. i -= 1
  830. if i >= 0:
  831. tag = self.calevents[ev_ids[i]]['tags'][-1]
  832. label.configure(style='tag_%s.%s.TLabel' % (tag, self._style_prefixe))
  833. text = '\n'.join(['➢ {}'.format(self.calevents[ev]['text']) for ev in ev_ids])
  834. self.tooltip_wrapper.add_tooltip(label, text)
  835. def _get_day_coords(self, date):
  836. y1, y2 = date.year, self._date.year
  837. m1, m2 = date.month, self._date.month
  838. if y1 == y2 or (y1 - y2 == 1 and m1 == 1 and m2 == 12) or (y2 - y1 == 1 and m2 == 1 and m1 == 12):
  839. _, w, d = date.isocalendar()
  840. _, wn, dn = self._date.isocalendar()
  841. if self['firstweekday'] == 'sunday':
  842. d %= 7
  843. if d == 0:
  844. w += 1
  845. if dn == 7:
  846. wn += 1
  847. else:
  848. d -= 1
  849. w -= wn
  850. w %= max(52, wn)
  851. if 0 <= w < 6:
  852. return w, d
  853. else:
  854. return None, None
  855. else:
  856. return None, None
  857. def _display_selection(self):
  858. """Highlight selected day."""
  859. if self._sel_date is not None:
  860. w, d = self._get_day_coords(self._sel_date)
  861. if w is not None:
  862. label = self._calendar[w][d]
  863. if label.cget('text'):
  864. label.configure(style='sel.%s.TLabel' % self._style_prefixe)
  865. def _reset_day(self, date):
  866. """Restore usual week day colors."""
  867. month = date.month
  868. w, d = self._get_day_coords(date)
  869. if w is not None:
  870. self.tooltip_wrapper.remove_tooltip(self._calendar[w][d])
  871. week_end = [0, 6] if self['firstweekday'] == 'sunday' else [5, 6]
  872. if month == date.month:
  873. if d in week_end:
  874. self._calendar[w][d].configure(style='we.%s.TLabel' % self._style_prefixe)
  875. else:
  876. self._calendar[w][d].configure(style='normal.%s.TLabel' % self._style_prefixe)
  877. else:
  878. if d in week_end:
  879. self._calendar[w][d].configure(style='we_om.%s.TLabel' % self._style_prefixe)
  880. else:
  881. self._calendar[w][d].configure(style='normal_om.%s.TLabel' % self._style_prefixe)
  882. def _remove_selection(self):
  883. """Remove highlight of selected day."""
  884. if self._sel_date is not None:
  885. if self._sel_date in self._calevent_dates:
  886. self._show_event(self._sel_date)
  887. else:
  888. w, d = self._get_day_coords(self._sel_date)
  889. if w is not None:
  890. week_end = [0, 6] if self['firstweekday'] == 'sunday' else [5, 6]
  891. if self._sel_date.month == self._date.month:
  892. if d in week_end:
  893. self._calendar[w][d].configure(style='we.%s.TLabel' % self._style_prefixe)
  894. else:
  895. self._calendar[w][d].configure(style='normal.%s.TLabel' % self._style_prefixe)
  896. else:
  897. if d in week_end:
  898. self._calendar[w][d].configure(style='we_om.%s.TLabel' % self._style_prefixe)
  899. else:
  900. self._calendar[w][d].configure(style='normal_om.%s.TLabel' % self._style_prefixe)
  901. def _show_event(self, date):
  902. """Display events on date if visible."""
  903. w, d = self._get_day_coords(date)
  904. if w is not None:
  905. label = self._calendar[w][d]
  906. if not label.cget('text'):
  907. # this is an other month's day and showothermonth is False
  908. return
  909. ev_ids = self._calevent_dates[date]
  910. i = len(ev_ids) - 1
  911. while i >= 0 and not self.calevents[ev_ids[i]]['tags']:
  912. i -= 1
  913. if i >= 0:
  914. tag = self.calevents[ev_ids[i]]['tags'][-1]
  915. label.configure(style='tag_%s.%s.TLabel' % (tag, self._style_prefixe))
  916. text = '\n'.join(['➢ {}'.format(self.calevents[ev]['text']) for ev in ev_ids])
  917. self.tooltip_wrapper.remove_tooltip(label)
  918. self.tooltip_wrapper.add_tooltip(label, text)
  919. def check_date_range(self, date):
  920. """
  921. Ensure that date is in the allowed date range.
  922. date : datetime.date or datetime.datetime
  923. Return date if date is in the allowed date range, return the closest
  924. bound otherwise.
  925. """
  926. maxdate = self['maxdate']
  927. mindate = self['mindate']
  928. if maxdate is not None and date > maxdate:
  929. return maxdate
  930. elif mindate is not None and date < mindate:
  931. return mindate
  932. else:
  933. return date
  934. def _check_sel_date(self):
  935. if self._sel_date is not None:
  936. maxdate = self['maxdate']
  937. mindate = self['mindate']
  938. if maxdate is not None and self._sel_date > maxdate:
  939. self._sel_date = maxdate
  940. self._display_selection()
  941. elif mindate is not None and self._sel_date < mindate:
  942. self._sel_date = mindate
  943. self._display_selection()
  944. def _btns_date_range(self):
  945. """Disable/enable buttons depending on allowed date range."""
  946. maxdate = self['maxdate']
  947. mindate = self['mindate']
  948. if maxdate is not None:
  949. max_year, max_month = maxdate.year, maxdate.month
  950. if self._date > maxdate:
  951. self._date = self._date.replace(year=max_year, month=max_month)
  952. self._display_calendar()
  953. dy = max_year - self._date.year
  954. if dy == 0:
  955. self._r_year.state(['disabled'])
  956. if self._date.month == max_month:
  957. self._r_month.state(['disabled'])
  958. else:
  959. self._r_month.state(['!disabled'])
  960. elif dy == 1:
  961. if self._date.month > max_month:
  962. self._r_year.state(['disabled'])
  963. else:
  964. self._r_year.state(['!disabled'])
  965. self._r_month.state(['!disabled'])
  966. else: # dy > 1
  967. self._r_year.state(['!disabled'])
  968. self._r_month.state(['!disabled'])
  969. if mindate is not None:
  970. min_year, min_month = mindate.year, mindate.month
  971. if self._date < mindate:
  972. self._date = self._date.replace(year=min_year, month=min_month)
  973. self._display_calendar()
  974. dy = self._date.year - min_year
  975. if dy == 0:
  976. self._l_year.state(['disabled'])
  977. if self._date.month == min_month:
  978. self._l_month.state(['disabled'])
  979. else:
  980. self._l_month.state(['!disabled'])
  981. elif dy == 1:
  982. if self._date.month >= min_month:
  983. self._l_year.state(['!disabled'])
  984. self._l_month.state(['!disabled'])
  985. else:
  986. self._l_year.state(['disabled'])
  987. else: # dy > 1
  988. self._l_year.state(['!disabled'])
  989. self._l_month.state(['!disabled'])
  990. # --- callbacks
  991. def _next_month(self):
  992. """Display the next month."""
  993. year, month = self._date.year, self._date.month
  994. self._date = self._date + \
  995. self.timedelta(days=calendar.monthrange(year, month)[1])
  996. self._display_calendar()
  997. self.event_generate('<<CalendarMonthChanged>>')
  998. self._btns_date_range()
  999. def _prev_month(self):
  1000. """Display the previous month."""
  1001. self._date = self._date - self.timedelta(days=1)
  1002. self._date = self._date.replace(day=1)
  1003. self._display_calendar()
  1004. self.event_generate('<<CalendarMonthChanged>>')
  1005. self._btns_date_range()
  1006. def _next_year(self):
  1007. """Display the next year."""
  1008. year = self._date.year
  1009. self._date = self._date.replace(year=year + 1)
  1010. self._display_calendar()
  1011. self.event_generate('<<CalendarMonthChanged>>')
  1012. self._btns_date_range()
  1013. def _prev_year(self):
  1014. """Display the previous year."""
  1015. year = self._date.year
  1016. self._date = self._date.replace(year=year - 1)
  1017. self._display_calendar()
  1018. self.event_generate('<<CalendarMonthChanged>>')
  1019. self._btns_date_range()
  1020. # --- bindings
  1021. def _on_click(self, event):
  1022. """Select the day on which the user clicked."""
  1023. if self._properties['state'] == 'normal':
  1024. label = event.widget
  1025. if "disabled" not in label.state():
  1026. day = label.cget("text")
  1027. style = label.cget("style")
  1028. if style in ['normal_om.%s.TLabel' % self._style_prefixe, 'we_om.%s.TLabel' % self._style_prefixe]:
  1029. if label in self._calendar[0]:
  1030. self._prev_month()
  1031. else:
  1032. self._next_month()
  1033. if day:
  1034. day = int(day)
  1035. year, month = self._date.year, self._date.month
  1036. self._remove_selection()
  1037. self._sel_date = self.date(year, month, day)
  1038. self._display_selection()
  1039. if self._textvariable is not None:
  1040. self._textvariable.set(self.format_date(self._sel_date))
  1041. self.event_generate("<<CalendarSelected>>")
  1042. def _get_date_pattern(self, date_pattern, locale=None):
  1043. """
  1044. Return the babel pattern corresponding to date_pattern.
  1045. If date_pattern is "short", return the pattern correpsonding to the
  1046. locale, else return date_pattern if valid.
  1047. A valid pattern is a sequence of y, m and d
  1048. separated by non letter characters, e.g. yyyy-mm-dd or d/m/yy
  1049. """
  1050. if locale is None:
  1051. locale = self._properties["locale"]
  1052. if date_pattern == "short":
  1053. return get_date_format("short", locale).pattern
  1054. pattern = date_pattern.lower()
  1055. ymd = r"^y+[^a-zA-Z]*m{1,2}[^a-z]*d{1,2}[^mdy]*$"
  1056. mdy = r"^m{1,2}[^a-zA-Z]*d{1,2}[^a-z]*y+[^mdy]*$"
  1057. dmy = r"^d{1,2}[^a-zA-Z]*m{1,2}[^a-z]*y+[^mdy]*$"
  1058. res = ((re.search(ymd, pattern) is not None)
  1059. or (re.search(mdy, pattern) is not None)
  1060. or (re.search(dmy, pattern) is not None))
  1061. if res:
  1062. return pattern.replace('m', 'M')
  1063. raise ValueError("%r is not a valid date pattern" % date_pattern)
  1064. def format_date(self, date=None):
  1065. """Convert date (datetime.date) to a string in the locale."""
  1066. return format_date(date, self._properties['date_pattern'], self._properties['locale'])
  1067. def parse_date(self, date):
  1068. """Parse string date in the locale format and return the corresponding datetime.date."""
  1069. date_format = self._properties['date_pattern'].lower()
  1070. year_idx = date_format.index('y')
  1071. month_idx = date_format.index('m')
  1072. day_idx = date_format.index('d')
  1073. indexes = [(year_idx, 'Y'), (month_idx, 'M'), (day_idx, 'D')]
  1074. indexes.sort()
  1075. indexes = dict([(item[1], idx) for idx, item in enumerate(indexes)])
  1076. numbers = re.findall(r'(\d+)', date)
  1077. year = numbers[indexes['Y']]
  1078. if len(year) == 2:
  1079. year = 2000 + int(year)
  1080. else:
  1081. year = int(year)
  1082. month = int(numbers[indexes['M']])
  1083. day = int(numbers[indexes['D']])
  1084. if month > 12:
  1085. month, day = day, month
  1086. return self.date(year, month, day)
  1087. def see(self, date):
  1088. """
  1089. Display the month in which date is.
  1090. date : datetime.date or datetime.datetime
  1091. date to be made visible
  1092. """
  1093. if isinstance(date, self.datetime):
  1094. date = date.date()
  1095. elif not isinstance(date, self.date):
  1096. raise TypeError("expected %s for the 'date' argument." % self.date)
  1097. self._date = self._date.replace(month=date.month, year=date.year)
  1098. self._display_calendar()
  1099. self._btns_date_range()
  1100. # --- selection handling
  1101. def selection_clear(self):
  1102. """Clear the selection."""
  1103. self._remove_selection()
  1104. self._sel_date = None
  1105. if self._textvariable is not None:
  1106. self._textvariable.set('')
  1107. def selection_get(self):
  1108. """
  1109. Return currently selected date (datetime.date instance).
  1110. Always return None if selectmode == "none".
  1111. """
  1112. if self._properties.get("selectmode") == "day":
  1113. return self._sel_date
  1114. else:
  1115. return None
  1116. def selection_set(self, date):
  1117. """
  1118. Set the selection to date.
  1119. date : datetime.date, datetime.datetime or str
  1120. date to be made visible. If given as a string, it should be
  1121. in the format corresponding to the calendar locale.
  1122. Do nothing if selectmode == "none".
  1123. """
  1124. if self._properties.get("selectmode") == "day" and self._properties['state'] == 'normal':
  1125. if date is None:
  1126. self.selection_clear()
  1127. else:
  1128. if isinstance(date, self.datetime):
  1129. self._sel_date = date.date()
  1130. elif isinstance(date, self.date):
  1131. self._sel_date = date
  1132. else:
  1133. try:
  1134. self._sel_date = self.parse_date(date)
  1135. except Exception as e:
  1136. raise ValueError("%r is not a valid date." % date)
  1137. if self['mindate'] is not None and self._sel_date < self['mindate']:
  1138. self._sel_date = self['mindate']
  1139. elif self['maxdate'] is not None and self._sel_date > self['maxdate']:
  1140. self._sel_date = self['maxdate']
  1141. if self._textvariable is not None:
  1142. self._textvariable.set(self.format_date(self._sel_date))
  1143. self._date = self._sel_date.replace(day=1)
  1144. self._display_calendar()
  1145. self._display_selection()
  1146. self._btns_date_range()
  1147. def get_displayed_month(self):
  1148. """Return the currently displayed month in the form of a (month, year) tuple."""
  1149. return self._date.month, self._date.year
  1150. def get_date(self):
  1151. """Return selected date as string."""
  1152. if self._sel_date is not None:
  1153. return self.format_date(self._sel_date)
  1154. else:
  1155. return ""
  1156. # --- events
  1157. def calevent_create(self, date, text, tags=[]):
  1158. """
  1159. Add new event in calendar and return event id.
  1160. Options:
  1161. date : datetime.date or datetime.datetime
  1162. event date
  1163. text : str
  1164. text to put in the tooltip associated to date.
  1165. tags : list
  1166. list of tags to apply to the event. The last tag determines
  1167. the way the event is displayed. If there are several events on
  1168. the same day, the lowest one (on the tooltip list) which has
  1169. tags determines the colors of the day.
  1170. """
  1171. if isinstance(date, Calendar.datetime):
  1172. date = date.date()
  1173. if not isinstance(date, Calendar.date):
  1174. raise TypeError("date option should be a %s instance" % (Calendar.date))
  1175. if self.calevents:
  1176. ev_id = max(self.calevents) + 1
  1177. else:
  1178. ev_id = 0
  1179. if isinstance(tags, str):
  1180. tags_ = [tags]
  1181. else:
  1182. tags_ = list(tags)
  1183. self.calevents[ev_id] = {'date': date, 'text': text, 'tags': tags_}
  1184. for tag in tags_:
  1185. if tag not in self._tags:
  1186. self._tag_initialize(tag)
  1187. if date not in self._calevent_dates:
  1188. self._calevent_dates[date] = [ev_id]
  1189. else:
  1190. self._calevent_dates[date].append(ev_id)
  1191. self._show_event(date)
  1192. return ev_id
  1193. def _calevent_remove(self, ev_id):
  1194. """Remove event ev_id."""
  1195. try:
  1196. date = self.calevents[ev_id]['date']
  1197. except KeyError:
  1198. ValueError("event %s does not exists" % ev_id)
  1199. else:
  1200. del self.calevents[ev_id]
  1201. self._calevent_dates[date].remove(ev_id)
  1202. if not self._calevent_dates[date]:
  1203. del self._calevent_dates[date]
  1204. self._reset_day(date)
  1205. else:
  1206. self._show_event(date)
  1207. def calevent_remove(self, *ev_ids, **kw):
  1208. """
  1209. Remove events from calendar.
  1210. Arguments: event ids to remove or 'all' to remove them all.
  1211. Keyword arguments: tag, date.
  1212. They are taken into account only if no id is given. Remove all events
  1213. with given tag on given date. If only date is given, remove all events
  1214. on date and if only tag is given, remove all events with tag.
  1215. """
  1216. if ev_ids:
  1217. if 'all' in ev_ids:
  1218. ev_ids = self.get_calevents()
  1219. for ev_id in ev_ids:
  1220. self._calevent_remove(ev_id)
  1221. else:
  1222. date = kw.get('date')
  1223. tag = kw.get('tag')
  1224. evs = self.get_calevents(tag=tag, date=date)
  1225. for ev_id in evs:
  1226. self._calevent_remove(ev_id)
  1227. def calevent_cget(self, ev_id, option):
  1228. """Return value of given option for the event ev_id."""
  1229. try:
  1230. ev = self.calevents[ev_id]
  1231. except KeyError:
  1232. raise ValueError("event %s does not exists" % ev_id)
  1233. else:
  1234. try:
  1235. return ev[option]
  1236. except KeyError:
  1237. raise ValueError('unknown option "%s"' % option)
  1238. def calevent_configure(self, ev_id, **kw):
  1239. """
  1240. Configure the event ev_id.
  1241. Keyword options: date, text, tags (see calevent_create options).
  1242. """
  1243. try:
  1244. ev = self.calevents[ev_id]
  1245. except KeyError:
  1246. raise ValueError("event %s does not exists" % ev_id)
  1247. else:
  1248. text = kw.pop('text', None)
  1249. tags = kw.pop('tags', None)
  1250. date = kw.pop('date', None)
  1251. if kw:
  1252. raise KeyError('Invalid keyword option(s) %s, valid options are "text", "tags" and "date".' % (kw.keys(),))
  1253. else:
  1254. if text is not None:
  1255. ev['text'] = str(text)
  1256. if tags is not None:
  1257. if isinstance(tags, str):
  1258. tags_ = [tags]
  1259. else:
  1260. tags_ = list(tags)
  1261. for tag in tags_:
  1262. if tag not in self._tags:
  1263. self._tag_initialize(tag)
  1264. ev['tags'] = tags_
  1265. if date is not None:
  1266. if isinstance(date, Calendar.datetime):
  1267. date = date.date()
  1268. if not isinstance(date, Calendar.date):
  1269. raise TypeError("date option should be a %s instance" % (Calendar.date))
  1270. old_date = ev['date']
  1271. self._calevent_dates[old_date].remove(ev_id)
  1272. if not self._calevent_dates[old_date]:
  1273. self._reset_day(old_date)
  1274. else:
  1275. self._show_event(old_date)
  1276. ev['date'] = date
  1277. if date not in self._calevent_dates:
  1278. self._calevent_dates[date] = [ev_id]
  1279. else:
  1280. self._calevent_dates[date].append(ev_id)
  1281. self._show_event(ev['date'])
  1282. def calevent_raise(self, ev_id, above=None):
  1283. """
  1284. Raise event ev_id in tooltip event list.
  1285. above : str
  1286. put ev_id above given one, if above is None, put it on top
  1287. of tooltip event list.
  1288. The day's colors are determined by the last tag of the lowest event
  1289. which has tags.
  1290. """
  1291. try:
  1292. date = self.calevents[ev_id]['date']
  1293. except KeyError:
  1294. raise ValueError("event %s does not exists" % ev_id)
  1295. else:
  1296. evs = self._calevent_dates[date]
  1297. if above is None:
  1298. evs.remove(ev_id)
  1299. evs.insert(0, ev_id)
  1300. else:
  1301. if above not in evs:
  1302. raise ValueError("event %s does not exists on %s" % (above, date))
  1303. else:
  1304. evs.remove(ev_id)
  1305. index = evs.index(above)
  1306. evs.insert(index, ev_id)
  1307. self._show_event(date)
  1308. def calevent_lower(self, ev_id, below=None):
  1309. """
  1310. Lower event ev_id in tooltip event list.
  1311. below : str
  1312. put ev_id below given one, if below is None, put it at the
  1313. bottom of tooltip event list.
  1314. The day's colors are determined by the last tag of the lowest event
  1315. which has tags.
  1316. """
  1317. try:
  1318. date = self.calevents[ev_id]['date']
  1319. except KeyError:
  1320. raise ValueError("event %s does not exists" % ev_id)
  1321. else:
  1322. evs = self._calevent_dates[date]
  1323. if below is None:
  1324. evs.remove(ev_id)
  1325. evs.append(ev_id)
  1326. else:
  1327. if below not in evs:
  1328. raise ValueError("event %s does not exists on %s" % (below, date))
  1329. else:
  1330. evs.remove(ev_id)
  1331. index = evs.index(below) + 1
  1332. evs.insert(index, ev_id)
  1333. self._show_event(date)
  1334. def get_calevents(self, date=None, tag=None):
  1335. """
  1336. Return event ids of events with given tag and on given date.
  1337. If only date is given, return event ids of all events on date.
  1338. If only tag is given, return event ids of all events with tag.
  1339. If both options are None, return all event ids.
  1340. """
  1341. if date is not None:
  1342. if isinstance(date, Calendar.datetime):
  1343. date = date.date()
  1344. if not isinstance(date, Calendar.date):
  1345. raise TypeError("date option should be a %s instance" % (Calendar.date))
  1346. try:
  1347. if tag is not None:
  1348. return tuple(ev_id for ev_id in self._calevent_dates[date] if tag in self.calevents[ev_id]['tags'])
  1349. else:
  1350. return tuple(self._calevent_dates[date])
  1351. except KeyError:
  1352. return ()
  1353. elif tag is not None:
  1354. return tuple(ev_id for ev_id, prop in self.calevents.items() if tag in prop['tags'])
  1355. else:
  1356. return tuple(self.calevents.keys())
  1357. def _tag_initialize(self, tag):
  1358. props = dict(foreground='white', background='royal blue')
  1359. self._tags[tag] = props
  1360. self.style.configure('tag_%s.%s.TLabel' % (tag, self._style_prefixe), **props)
  1361. def tag_config(self, tag, **kw):
  1362. """
  1363. Configure tag.
  1364. Keyword options: foreground, background (of the day in the calendar)
  1365. """
  1366. if tag not in self._tags:
  1367. self._tags[tag] = {}
  1368. props = dict(foreground='white', background='royal blue') # default
  1369. props.update(self._tags[tag])
  1370. props.update(kw)
  1371. self.style.configure('tag_%s.%s.TLabel' % (tag, self._style_prefixe), **props)
  1372. self._tags[tag] = props
  1373. def tag_cget(self, tag, option):
  1374. """Return the value of the tag's option."""
  1375. try:
  1376. prop = self._tags[tag]
  1377. except KeyError:
  1378. raise ValueError('unknow tag "%s"' % tag)
  1379. else:
  1380. try:
  1381. return prop[option]
  1382. except KeyError:
  1383. raise ValueError('unknow option "%s"' % option)
  1384. def tag_names(self):
  1385. """Return tuple of existing tags."""
  1386. return tuple(self._tags.keys())
  1387. def tag_delete(self, tag):
  1388. """
  1389. Delete given tag.
  1390. Delete tag properties and remove tag from all events.
  1391. """
  1392. try:
  1393. del self._tags[tag]
  1394. except KeyError:
  1395. raise ValueError('tag "%s" does not exists' % tag)
  1396. else:
  1397. for props in self.calevents.values():
  1398. if tag in props['tags']:
  1399. props['tags'].remove(tag)
  1400. self._display_calendar()
  1401. # --- other methods
  1402. def keys(self):
  1403. """Return a list of all resource names of this widget."""
  1404. return list(self._properties.keys())
  1405. def cget(self, key):
  1406. """Return the resource value for a KEY given as string."""
  1407. return self[key]
  1408. def configure(self, cnf={}, **kw):
  1409. """
  1410. Configure resources of a widget.
  1411. The values for resources are specified as keyword
  1412. arguments. To get an overview about
  1413. the allowed keyword arguments call the method :meth:`~Calendar.keys`.
  1414. """
  1415. if not isinstance(cnf, dict):
  1416. raise TypeError("Expected a dictionary or keyword arguments.")
  1417. kwargs = cnf.copy()
  1418. kwargs.update(kw)
  1419. for item, value in kwargs.items():
  1420. self[item] = value
  1421. config = configure