bootstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. bootstack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,972 @@
1
+ """Inline calendar widget supporting single and range date selection."""
2
+ from __future__ import annotations
3
+
4
+ import calendar
5
+ import tkinter
6
+ from datetime import date, datetime, timedelta
7
+ from types import SimpleNamespace
8
+ from tkinter import StringVar
9
+ from typing import Any, Callable, Iterable, Literal, TypedDict
10
+
11
+ from babel import dates
12
+ from babel.core import Locale
13
+ from bootstack.events import DateSelectEvent
14
+ from bootstack.widgets._impl.primitives import Button, CheckToggle, Frame, Label, Separator
15
+ from bootstack.widgets.types import Master
16
+ from bootstack.constants import BOTH, CENTER, LEFT, NSEW, X, Y, YES
17
+ from bootstack.i18n import MessageCatalog
18
+ from bootstack._runtime.utility import bind_right_click
19
+ from bootstack.widgets._impl.composites._dateutils import coerce_date
20
+ from bootstack.widgets._impl.mixins import configure_delegate
21
+
22
+ ttk = SimpleNamespace(
23
+ Button=Button,
24
+ CheckToggle=CheckToggle,
25
+ Frame=Frame,
26
+ Label=Label,
27
+ Separator=Separator,
28
+ StringVar=StringVar,
29
+ )
30
+
31
+ _WEEKDAY_TOKENS = (
32
+ "day.mo",
33
+ "day.tu",
34
+ "day.we",
35
+ "day.th",
36
+ "day.fr",
37
+ "day.sa",
38
+ "day.su",
39
+ )
40
+
41
+ _MONTH_TOKENS = (
42
+ None,
43
+ "month.january",
44
+ "month.february",
45
+ "month.march",
46
+ "month.april",
47
+ "month.may",
48
+ "month.june",
49
+ "month.july",
50
+ "month.august",
51
+ "month.september",
52
+ "month.october",
53
+ "month.november",
54
+ "month.december",
55
+ )
56
+
57
+
58
+ def _localized_month_name(month_index: int) -> str:
59
+ if 1 <= month_index < len(_MONTH_TOKENS):
60
+ token = _MONTH_TOKENS[month_index]
61
+ if token:
62
+ return MessageCatalog.translate(token)
63
+ return calendar.month_name[month_index] if 1 <= month_index <= 12 else ""
64
+
65
+
66
+ def _format_month_year(month_date: date) -> str:
67
+ """Format month/year in the current locale via Babel, falling back to English."""
68
+ locale_code = MessageCatalog.locale().replace("_", "-")
69
+ try:
70
+ return dates.format_skeleton("yMMMM", month_date, locale=locale_code)
71
+ except Exception:
72
+ month_name = _localized_month_name(month_date.month)
73
+ return f"{month_name} {month_date.year}"
74
+
75
+
76
+ def _longest_month_title_length() -> int:
77
+ """Calculate the character length of the longest month/year title for the current locale."""
78
+ max_length = 0
79
+ # Use a 4-digit year for measurement (e.g., 2024)
80
+ sample_year = 2024
81
+ for month in range(1, 13):
82
+ title = _format_month_year(date(sample_year, month, 1))
83
+ if len(title) > max_length:
84
+ max_length = len(title)
85
+ return max_length
86
+
87
+
88
+ class Calendar(ttk.Frame):
89
+ """Inline calendar widget for selecting dates.
90
+
91
+ Supports single or range selection modes with optional disabled dates
92
+ and min/max bounds. Displays one month in single mode or two months
93
+ in range mode.
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ master: Master = None,
99
+ *,
100
+ value: date | datetime | str | None = None,
101
+ start_date: date | datetime | str | None = None,
102
+ end_date: date | datetime | str | None = None,
103
+ disabled_dates: Iterable[date | datetime | str] | None = None,
104
+ selection_mode: Literal['single', 'range'] = "single",
105
+ max_date: date | datetime | str | None = None,
106
+ min_date: date | datetime | str | None = None,
107
+ show_outside_days: bool | None = None,
108
+ show_week_numbers: bool = False,
109
+ first_weekday: int | None = None,
110
+ accent: str = None,
111
+ padding: int | tuple[int, int] | tuple[int, int, int, int] | str | None = None,
112
+ ) -> None:
113
+ """Initialize a Calendar widget.
114
+
115
+ Args:
116
+ master: Parent widget. If None, uses the default root window.
117
+ value: Initial selected date for single selection mode.
118
+ start_date: Range start date. Use `value` instead
119
+ for single selection mode.
120
+ end_date: Range end date. Only used when
121
+ `selection_mode='range'`.
122
+ disabled_dates: Collection of dates that cannot be selected.
123
+ selection_mode: Selection mode - `'single'` for single date or
124
+ `'range'` for date range selection.
125
+ max_date: Maximum selectable date. Dates after
126
+ this are disabled.
127
+ min_date: Minimum selectable date. Dates before
128
+ this are disabled.
129
+ show_outside_days: Whether to show days from adjacent months.
130
+ Defaults to True for single mode, False for range mode.
131
+ show_week_numbers: Whether to display ISO week numbers in the
132
+ leftmost column.
133
+ first_weekday: First day of the week. 0=Monday, 6=Sunday.
134
+ If None, uses the locale default.
135
+ accent: Accent token for selected dates and highlights (e.g., 'primary', 'success').
136
+ padding: Padding around the widget.
137
+ """
138
+ super().__init__(master, padding=padding)
139
+
140
+ self._selection_mode = selection_mode
141
+ # Derive visible months from selection mode: single->1, range->2
142
+ self._display_months = 2 if selection_mode == "range" else 1
143
+ # Default outside-day visibility: True for single, False for range if not provided
144
+ if show_outside_days is None:
145
+ self._show_outside_days = selection_mode != "range"
146
+ else:
147
+ self._show_outside_days = bool(show_outside_days)
148
+ self._show_week_numbers = show_week_numbers
149
+
150
+ # Resolve first_weekday: None -> locale default via Babel
151
+ if first_weekday is None:
152
+ try:
153
+ locale_code = MessageCatalog.locale().replace("-", "_")
154
+ loc = Locale.parse(locale_code)
155
+ first_weekday = loc.first_week_day
156
+ except Exception:
157
+ first_weekday = 0 # fallback to Monday (ISO standard)
158
+ self._first_weekday = first_weekday
159
+ self._accent = accent or 'primary'
160
+ self._calendar = calendar.Calendar(firstweekday=first_weekday)
161
+
162
+ # Allow 'value' as alias for 'start_date' (reads better in single mode)
163
+ if start_date is None and value is not None:
164
+ start_date = value
165
+
166
+ initial = self._coerce_date(start_date) or date.today()
167
+ self._initial_date = initial
168
+ self._display_date = date(initial.year, initial.month, 1)
169
+
170
+ self._range_start: date | None = self._coerce_date(start_date)
171
+ self._range_end: date | None = self._coerce_date(end_date)
172
+ if self._range_start and self._range_end and self._range_end < self._range_start:
173
+ self._range_start, self._range_end = self._range_end, self._range_start
174
+
175
+ self._selected_date: date = self._range_end or self._range_start or initial
176
+
177
+ self._disabled_dates = {
178
+ d for d in (self._coerce_date(x) for x in (disabled_dates or [])) if d is not None
179
+ }
180
+ self._max_date = self._coerce_date(max_date)
181
+ self._min_date = self._coerce_date(min_date)
182
+
183
+ self._title_var = ttk.StringVar()
184
+ self._locked_size: tuple[int, int] | None = None
185
+
186
+ self._header_frame: ttk.Frame | None = None
187
+ self._months_frame: ttk.Frame | None = None
188
+ self._month_frames: list[ttk.Frame] = []
189
+ self._month_separators: list[ttk.Separator] = []
190
+ self._month_views: list[dict[str, Any]] = []
191
+
192
+ self._build_ui()
193
+ self.bind("<<LocaleChanged>>", lambda *_: self._refresh_calendar(), add="+")
194
+
195
+ # --- public API --------------------------------------------------
196
+
197
+ # Value API (v2 standard) -----------------------------------------
198
+ def get(self) -> date | None:
199
+ """Return the currently selected date.
200
+
201
+ Returns:
202
+ The selected date, or None if no date is selected.
203
+ """
204
+ return self._selected_date
205
+
206
+ def set(self, value: date | datetime | str | None) -> None:
207
+ """Set the selected date programmatically.
208
+
209
+ This method does NOT emit `<<DateSelect>>`. Use for programmatic
210
+ updates when you don't want to trigger event handlers.
211
+
212
+ Args:
213
+ value: The date to select. Accepts date, datetime, ISO string,
214
+ or None to clear selection.
215
+ """
216
+ new_date = self._coerce_date(value)
217
+ if new_date is None:
218
+ return
219
+ self._selected_date = new_date
220
+ if self._selection_mode == "single":
221
+ self._range_start = new_date
222
+ self._range_end = None
223
+ else:
224
+ # In range mode, set() sets the start of a new range
225
+ self._range_start = new_date
226
+ self._range_end = None
227
+ self._display_date = date(new_date.year, new_date.month, 1)
228
+ self._refresh_calendar()
229
+
230
+ @property
231
+ def value(self) -> date | None:
232
+ """The currently selected date.
233
+
234
+ This property provides convenient access to `get()` and `set()`.
235
+ """
236
+ return self.get()
237
+
238
+ @value.setter
239
+ def value(self, val: date | datetime | str | None) -> None:
240
+ self.set(val)
241
+
242
+ # Range API -------------------------------------------------------
243
+ def get_range(self) -> tuple[date | None, date | None]:
244
+ """Return the selected date range.
245
+
246
+ Returns:
247
+ A tuple of (start, end) dates. If only a start is selected
248
+ (range in progress), end will be None. If no selection,
249
+ both may be None.
250
+ """
251
+ return (self._range_start, self._range_end)
252
+
253
+ def set_range(
254
+ self,
255
+ start: date | datetime | str | None,
256
+ end: date | datetime | str | None = None,
257
+ ) -> None:
258
+ """Set the selected date range programmatically.
259
+
260
+ This method does NOT emit `<<DateSelect>>`. Use for programmatic
261
+ updates when you don't want to trigger event handlers.
262
+
263
+ If both start and end are provided and end < start, they are
264
+ automatically normalized (swapped) to ensure start <= end.
265
+
266
+ Args:
267
+ start: The range start date. Accepts date, datetime, ISO string.
268
+ end: The range end date. If None, sets a range-in-progress.
269
+ """
270
+ s, e = self._normalize_range(start, end)
271
+ self._range_start = s
272
+ self._range_end = e
273
+ # Update selected_date to the end if complete, else start
274
+ self._selected_date = e if e else (s if s else self._selected_date)
275
+ # Navigate display to show the range
276
+ if s:
277
+ self._display_date = date(s.year, s.month, 1)
278
+ self._refresh_calendar()
279
+
280
+ def set_min_date(self, value: date | datetime | str | None) -> None:
281
+ """Set the earliest selectable date and redraw. None clears the bound."""
282
+ self._min_date = self._coerce_date(value)
283
+ self._request_refresh()
284
+
285
+ def set_max_date(self, value: date | datetime | str | None) -> None:
286
+ """Set the latest selectable date and redraw. None clears the bound."""
287
+ self._max_date = self._coerce_date(value)
288
+ self._request_refresh()
289
+
290
+ def set_disabled_dates(self, dates: Iterable[date | datetime | str] | None) -> None:
291
+ """Replace the set of non-selectable dates and redraw."""
292
+ self._disabled_dates = {
293
+ d for d in (self._coerce_date(x) for x in (dates or [])) if d is not None
294
+ }
295
+ self._request_refresh()
296
+
297
+ def _request_refresh(self) -> None:
298
+ """Coalesce constraint changes into a single redraw.
299
+
300
+ Setting `min_date`, `max_date`, and `disabled_dates` in sequence would
301
+ otherwise redraw the whole grid once per setter. Schedule a single
302
+ idle-time refresh instead so a batch of constraint changes collapses to
303
+ one redraw.
304
+ """
305
+ if getattr(self, "_refresh_pending", False):
306
+ return
307
+ self._refresh_pending = True
308
+
309
+ def _do() -> None:
310
+ self._refresh_pending = False
311
+ self._refresh_calendar()
312
+
313
+ try:
314
+ self.after_idle(_do)
315
+ except tkinter.TclError:
316
+ self._refresh_pending = False
317
+ self._refresh_calendar()
318
+
319
+ @property
320
+ def range(self) -> tuple[date | None, date | None]:
321
+ """The selected date range as (start, end).
322
+
323
+ This property provides convenient access to `get_range()` and
324
+ `set_range()`.
325
+ """
326
+ return self.get_range()
327
+
328
+ @range.setter
329
+ def range(self, val: tuple[date | datetime | str | None, date | datetime | str | None]) -> None:
330
+ if val is None:
331
+ self.set_range(None, None)
332
+ elif isinstance(val, (list, tuple)) and len(val) >= 2:
333
+ self.set_range(val[0], val[1])
334
+ elif isinstance(val, (list, tuple)) and len(val) == 1:
335
+ self.set_range(val[0], None)
336
+ else:
337
+ self.set_range(val, None)
338
+
339
+ # Legacy delegate (for configure() compatibility) -----------------
340
+ @configure_delegate("date")
341
+ def _delegate_date(self, value: date | datetime | str | None = None) -> date | None:
342
+ """Get or set the current selected date via configure()."""
343
+ if value is None:
344
+ return self._selected_date
345
+ self.set(value)
346
+ return None
347
+
348
+ # Event binding ---------------------------------------------------
349
+ def on_date_selected(self, callback: Callable) -> str:
350
+ """Register a callback for `<<DateSelect>>` events.
351
+
352
+ Args:
353
+ callback: Called when a date is selected or a range is updated.
354
+ Receives a Tk event object; `event.data` is a
355
+ `DateSelectEvent` payload with attributes:
356
+ - `date`: the most recently clicked date.
357
+ - `range`: `(start, end)` tuple; `end` is None if only the
358
+ start of a range has been set.
359
+
360
+ Returns:
361
+ Bind ID — pass to `off_date_selected()` to unsubscribe.
362
+ """
363
+ return self.bind("<<DateSelect>>", callback, add=True)
364
+
365
+ def off_date_selected(self, bind_id: str | None = None) -> None:
366
+ """Unsubscribe from `<<DateSelect>>`.
367
+
368
+ Args:
369
+ bind_id: ID returned by `on_date_selected()`. If None, removes all.
370
+ """
371
+ return self.unbind("<<DateSelect>>", bind_id)
372
+
373
+ # --- UI construction --------------------------------------------
374
+ def _build_ui(self) -> None:
375
+ # Single-month header only for non-range mode
376
+ if self._selection_mode != "range":
377
+ self._create_header()
378
+ if not hasattr(self, "_header_separator"):
379
+ self._header_separator = ttk.Separator(self)
380
+ self._header_separator.pack(fill=X)
381
+ self._draw_calendar()
382
+
383
+ def _create_header(self) -> None:
384
+ self._header_frame = ttk.Frame(self)
385
+ self._header_frame.pack(fill=X)
386
+
387
+ for col in range(5):
388
+ self._header_frame.columnconfigure(col, weight=1 if col == 2 else 0)
389
+
390
+ self._prev_year_btn = ttk.Button(
391
+ master=self._header_frame,
392
+ icon='chevron-double-left',
393
+ icon_only=True,
394
+ variant="ghost",
395
+ density='compact',
396
+ command=self._on_prev_year,
397
+ )
398
+ self._prev_year_btn.grid(row=0, column=0)
399
+ bind_right_click(self._prev_year_btn, self._on_prev_year)
400
+
401
+ self._prev_month_btn = ttk.Button(
402
+ master=self._header_frame,
403
+ icon='chevron-left',
404
+ density='compact',
405
+ variant="ghost",
406
+ icon_only=True,
407
+ command=self._on_prev_month,
408
+ )
409
+ self._prev_month_btn.grid(row=0, column=1)
410
+
411
+ self._set_title()
412
+ title_width = _longest_month_title_length()
413
+ title_label = ttk.Label(
414
+ master=self._header_frame,
415
+ textvariable=self._title_var,
416
+ anchor=CENTER,
417
+ accent="muted",
418
+ font='caption[bold]',
419
+ width=title_width,
420
+ )
421
+ title_label.grid(row=0, column=2, sticky="ew")
422
+ title_label.bind("<Button-1>", self._on_reset_date)
423
+
424
+ self._next_month_btn = ttk.Button(
425
+ master=self._header_frame,
426
+ icon='chevron-right',
427
+ variant="ghost",
428
+ density='compact',
429
+ icon_only=True,
430
+ command=self._on_next_month,
431
+ )
432
+ self._next_month_btn.grid(row=0, column=3)
433
+
434
+ self._next_year_btn = ttk.Button(
435
+ master=self._header_frame,
436
+ icon='chevron-double-right',
437
+ icon_only=True,
438
+ variant="ghost",
439
+ density='compact',
440
+ command=self._on_next_year,
441
+ )
442
+ self._next_year_btn.grid(row=0, column=4)
443
+ bind_right_click(self._next_year_btn, self._on_next_year)
444
+
445
+ # Preserve column widths so hiding buttons won't shift the title
446
+ self._header_frame.update_idletasks()
447
+ col_sizes = [
448
+ self._prev_year_btn.winfo_reqwidth(),
449
+ self._prev_month_btn.winfo_reqwidth(),
450
+ title_label.winfo_reqwidth(),
451
+ self._next_month_btn.winfo_reqwidth(),
452
+ self._next_year_btn.winfo_reqwidth(),
453
+ ]
454
+ for idx, size in enumerate(col_sizes):
455
+ self._header_frame.columnconfigure(idx, minsize=size)
456
+
457
+ # --- drawing ------------------------------------------------------
458
+ def _draw_calendar(self) -> None:
459
+ if self._months_frame is None:
460
+ self._months_frame = ttk.Frame(self)
461
+ self._months_frame.pack(fill=BOTH, expand=YES)
462
+
463
+ if self._display_months == 1:
464
+ self._set_title()
465
+
466
+ current = self._display_date
467
+ for idx in range(self._display_months):
468
+ if idx >= len(self._month_frames):
469
+ frame = ttk.Frame(self._months_frame, padding=0)
470
+ self._month_frames.append(frame)
471
+ self._month_views.append({})
472
+ frame.pack(side=LEFT, fill=BOTH, expand=YES, padx=0, pady=0)
473
+ month_frame = self._month_frames[idx]
474
+ view = self._month_views[idx]
475
+ view["frame"] = month_frame
476
+ month_frame.pack_configure(side=LEFT, fill=BOTH, expand=YES, padx=0, pady=0)
477
+ self._draw_month(month_frame, current, view, idx)
478
+ current = self._add_months(current, 1)
479
+
480
+ # Insert vertical separators between months for multi-month display
481
+ if idx < self._display_months - 1:
482
+ if len(self._month_separators) <= idx:
483
+ sep = ttk.Separator(self._months_frame, orient="vertical")
484
+ self._month_separators.append(sep)
485
+ sep = self._month_separators[idx]
486
+ sep.pack(side=LEFT, fill=Y, padx=0)
487
+
488
+ # Hide unused frames
489
+ for extra in self._month_frames[self._display_months:]:
490
+ extra.pack_forget()
491
+ for sep in self._month_separators[self._display_months - 1:]:
492
+ sep.pack_forget()
493
+
494
+ self.after_idle(self._lock_size)
495
+
496
+ def _draw_month(self, parent: ttk.Frame, month_date: date, view: dict[str, Any], idx: int) -> None:
497
+ # Per-month header when in range mode
498
+ if self._selection_mode == "range":
499
+ header = view.get("header_frame")
500
+ title_var = view.get("title_var")
501
+ if header is None:
502
+ header = ttk.Frame(parent)
503
+ header.pack(fill=X)
504
+ for col in range(5):
505
+ header.columnconfigure(col, weight=1 if col == 2 else 0)
506
+ view["header_frame"] = header
507
+ title_var = ttk.StringVar()
508
+ view["title_var"] = title_var
509
+ # Separator between header and weekday row for this month
510
+ sep = ttk.Separator(parent)
511
+ sep.pack(fill=X)
512
+ view["header_separator"] = sep
513
+
514
+ prev_year = ttk.Button(
515
+ master=header,
516
+ icon='chevron-double-left',
517
+ icon_only=True,
518
+ accent="default",
519
+ variant="ghost",
520
+ density='compact',
521
+ command=self._on_prev_year,
522
+ )
523
+ prev_year.grid(row=0, column=0)
524
+ view["prev_year_btn"] = prev_year
525
+
526
+ prev_month = ttk.Button(
527
+ master=header,
528
+ icon='chevron-left',
529
+ accent="default",
530
+ variant="ghost",
531
+ density='compact',
532
+ icon_only=True,
533
+ command=self._on_prev_month,
534
+ )
535
+ prev_month.grid(row=0, column=1)
536
+ view["prev_month_btn"] = prev_month
537
+
538
+ title_width = _longest_month_title_length()
539
+ title_label = ttk.Label(
540
+ master=header,
541
+ textvariable=title_var,
542
+ anchor=CENTER,
543
+ accent="muted",
544
+ font='caption[bold]',
545
+ width=title_width,
546
+ )
547
+ title_label.grid(row=0, column=2, sticky="ew")
548
+
549
+ next_month = ttk.Button(
550
+ master=header,
551
+ icon='chevron-right',
552
+ accent="default",
553
+ variant="ghost",
554
+ density='compact',
555
+ icon_only=True,
556
+ command=self._on_next_month,
557
+ )
558
+ next_month.grid(row=0, column=3)
559
+ view["next_month_btn"] = next_month
560
+
561
+ next_year = ttk.Button(
562
+ master=header,
563
+ icon='chevron-double-right',
564
+ icon_only=True,
565
+ accent="default",
566
+ variant="ghost",
567
+ density='compact',
568
+ command=self._on_next_year,
569
+ )
570
+ next_year.grid(row=0, column=4)
571
+ view["next_year_btn"] = next_year
572
+
573
+ # Capture sizes and create spacers to hold column widths when buttons are hidden
574
+ header.update_idletasks()
575
+ col_sizes = [
576
+ prev_year.winfo_reqwidth(),
577
+ prev_month.winfo_reqwidth(),
578
+ title_label.winfo_reqwidth(),
579
+ next_month.winfo_reqwidth(),
580
+ next_year.winfo_reqwidth(),
581
+ ]
582
+ spacers = [
583
+ ttk.Frame(header, width=col_sizes[0], height=1),
584
+ ttk.Frame(header, width=col_sizes[1], height=1),
585
+ None,
586
+ ttk.Frame(header, width=col_sizes[3], height=1),
587
+ ttk.Frame(header, width=col_sizes[4], height=1),
588
+ ]
589
+ view["col_spacers"] = spacers
590
+ for c_idx, size in enumerate(col_sizes):
591
+ header.columnconfigure(c_idx, minsize=size)
592
+
593
+ # Update title text
594
+ if title_var is None:
595
+ title_var = ttk.StringVar()
596
+ view["title_var"] = title_var
597
+ title_var.set(_format_month_year(month_date))
598
+
599
+ # Show/hide nav buttons depending on column
600
+ prev_year = view.get("prev_year_btn")
601
+ prev_month = view.get("prev_month_btn")
602
+ next_month = view.get("next_month_btn")
603
+ next_year = view.get("next_year_btn")
604
+ spacers = view.get("col_spacers", [None] * 5)
605
+ spacer_left_year, spacer_left_month, _, spacer_right_month, spacer_right_year = spacers
606
+
607
+ def _grid_left():
608
+ if prev_year:
609
+ prev_year.grid(row=0, column=0)
610
+ if prev_month:
611
+ prev_month.grid(row=0, column=1)
612
+ if spacer_right_month:
613
+ spacer_right_month.grid(row=0, column=3)
614
+ if spacer_right_year:
615
+ spacer_right_year.grid(row=0, column=4)
616
+ if next_month:
617
+ next_month.grid_remove()
618
+ if next_year:
619
+ next_year.grid_remove()
620
+ if spacer_left_year:
621
+ spacer_left_year.grid_remove()
622
+ if spacer_left_month:
623
+ spacer_left_month.grid_remove()
624
+
625
+ def _grid_right():
626
+ if spacer_left_year:
627
+ spacer_left_year.grid(row=0, column=0)
628
+ if spacer_left_month:
629
+ spacer_left_month.grid(row=0, column=1)
630
+ if next_month:
631
+ next_month.grid(row=0, column=3)
632
+ if next_year:
633
+ next_year.grid(row=0, column=4)
634
+ if prev_year:
635
+ prev_year.grid_remove()
636
+ if prev_month:
637
+ prev_month.grid_remove()
638
+ if spacer_right_month:
639
+ spacer_right_month.grid_remove()
640
+ if spacer_right_year:
641
+ spacer_right_year.grid_remove()
642
+
643
+ # Ensure layout is stable each draw
644
+ if idx == 0:
645
+ _grid_left()
646
+ else:
647
+ _grid_right()
648
+ else:
649
+ # Ensure any per-month header is hidden when not in range nav mode
650
+ header = view.get("header_frame")
651
+ if header:
652
+ header.pack_forget()
653
+
654
+ # Weekday header per month — labels are created ONCE and reused (text +
655
+ # grid updated on redraw), not destroyed and recreated every draw. The
656
+ # only thing that changes across draws is the column text (locale / week
657
+ # start) and, if it toggles, the week-number column.
658
+ weekdays_frame: ttk.Frame | None = view.get("weekdays")
659
+ if weekdays_frame is None:
660
+ weekdays_frame = ttk.Frame(parent)
661
+ weekdays_frame.pack(fill=X)
662
+ view["weekdays"] = weekdays_frame
663
+ view["weekday_labels"] = []
664
+ view["weeknum_header"] = None
665
+
666
+ col_offset = 1 if self._show_week_numbers else 0
667
+ if self._show_week_numbers:
668
+ wn_header = view.get("weeknum_header")
669
+ if wn_header is None:
670
+ weekdays_frame.columnconfigure(0, weight=1, uniform="cal")
671
+ wn_header = ttk.Label(weekdays_frame, text="#", anchor=CENTER, padding=5,
672
+ surface="background[+1]", font='caption[bold]')
673
+ view["weeknum_header"] = wn_header
674
+ wn_header.grid(row=0, column=0, sticky=NSEW)
675
+ elif view.get("weeknum_header") is not None:
676
+ view["weeknum_header"].grid_remove()
677
+
678
+ header_labels: list[ttk.Label] = view["weekday_labels"]
679
+ for i, col in enumerate(self._header_columns()):
680
+ weekdays_frame.columnconfigure(i + col_offset, weight=1, uniform="cal")
681
+ if i < len(header_labels):
682
+ lbl = header_labels[i]
683
+ else:
684
+ lbl = ttk.Label(
685
+ master=weekdays_frame,
686
+ anchor=CENTER,
687
+ padding=5,
688
+ accent="muted",
689
+ font='caption[bold]',
690
+ )
691
+ header_labels.append(lbl)
692
+ lbl.configure(text=col)
693
+ lbl.grid(row=0, column=i + col_offset, sticky=NSEW)
694
+
695
+ # Grid reused
696
+ grid: ttk.Frame | None = view.get("grid")
697
+ if grid is None:
698
+ grid = ttk.Frame(parent)
699
+ grid.pack(fill=BOTH, expand=YES)
700
+ view["grid"] = grid
701
+ cells: list[list[ttk.Checkbutton]] = []
702
+ cell_vars: list[list[tkinter.BooleanVar]] = []
703
+ week_labels: list[ttk.Label] = []
704
+ week_labels: list[ttk.Label] = []
705
+ if self._show_week_numbers:
706
+ grid.columnconfigure(0, weight=1, uniform="cal")
707
+ for r in range(6):
708
+ if self._show_week_numbers:
709
+ wl = ttk.Label(grid, anchor=CENTER, padding=5, surface="background[+1]", font='caption')
710
+ wl.grid(row=r, column=0, sticky=NSEW)
711
+ week_labels.append(wl)
712
+ row_cells: list[ttk.Checkbutton] = []
713
+ row_vars: list[tkinter.BooleanVar] = []
714
+ for c in range(7):
715
+ col_offset = 1 if self._show_week_numbers else 0
716
+ grid.columnconfigure(c + col_offset, weight=1, uniform="cal")
717
+ var = tkinter.BooleanVar(value=False)
718
+ btn = ttk.CheckToggle(
719
+ grid,
720
+ width=2,
721
+ padding=self._square_button_padding(),
722
+ accent=self._accent,
723
+ variant="calendar-day",
724
+ variable=var,
725
+ onvalue=True,
726
+ offvalue=False,
727
+ takefocus=True,
728
+ )
729
+ btn.grid(row=r, column=c + col_offset, sticky=NSEW)
730
+ row_cells.append(btn)
731
+ row_vars.append(var)
732
+ cells.append(row_cells)
733
+ cell_vars.append(row_vars)
734
+ view["cells"] = cells
735
+ view["cell_vars"] = cell_vars
736
+ view["week_labels"] = week_labels
737
+ else:
738
+ cells = view["cells"]
739
+ cell_vars = view.get("cell_vars", [])
740
+ week_labels = view.get("week_labels", [])
741
+
742
+ # Compute 42 sequential days starting at first cell of month view
743
+ month_dates = self._calendar.monthdatescalendar(year=month_date.year, month=month_date.month)
744
+ start = month_dates[0][0]
745
+ days = [start + timedelta(days=i) for i in range(42)]
746
+
747
+ # Update week numbers; hide rows with no in-month days
748
+ if self._show_week_numbers:
749
+ for r, wl in enumerate(week_labels):
750
+ row_days = days[r * 7:(r + 1) * 7]
751
+ in_month = any(d.month == month_date.month for d in row_days)
752
+ if in_month:
753
+ wl.configure(text=str(row_days[0].isocalendar()[1]))
754
+ wl.grid(row=r, column=0, sticky=NSEW)
755
+ else:
756
+ if self._show_outside_days:
757
+ off_only = all(d.month != month_date.month for d in row_days)
758
+ else:
759
+ off_only = True
760
+ if off_only:
761
+ wl.grid_remove()
762
+ else:
763
+ wl.configure(text=str(row_days[0].isocalendar()[1]))
764
+ wl.grid(row=r, column=0, sticky=NSEW)
765
+
766
+ # Track which rows should be visible
767
+ row_visible = [False] * 6
768
+
769
+ # Update cells without recreating
770
+ for idx, d in enumerate(days):
771
+ r, c = divmod(idx, 7)
772
+ btn = cells[r][c]
773
+ var = cell_vars[r][c]
774
+ in_month = d.month == month_date.month
775
+
776
+ # Mark row visible if it has an in-month day or we are showing outside days
777
+ if in_month or self._show_outside_days:
778
+ row_visible[r] = True
779
+
780
+ # Outside days always use calendar-outside variant
781
+ if not in_month:
782
+ btn.configure(
783
+ text=d.day if self._show_outside_days else "",
784
+ command=lambda d=d: None,
785
+ variant="calendar-outside",
786
+ takefocus=False,
787
+ )
788
+ btn.state(["disabled"])
789
+ var.set(False)
790
+ continue
791
+
792
+ disabled = self._is_disabled(d)
793
+ accent, variant = self._style_for_date(d, in_month, disabled)
794
+ is_selected = self._is_selected(d)
795
+ btn.configure(
796
+ text=d.day,
797
+ accent=accent,
798
+ variant=variant,
799
+ command=(lambda d=d: self._on_date_selected_by_date(d)),
800
+ takefocus=not disabled,
801
+ )
802
+ var.set(is_selected)
803
+ if disabled:
804
+ btn.state(["disabled"])
805
+ else:
806
+ btn.state(["!disabled"])
807
+
808
+ # Hide or show rows (including week numbers) based on visibility
809
+ col_offset = 1 if self._show_week_numbers else 0
810
+ for r in range(6):
811
+ if row_visible[r]:
812
+ for c in range(7):
813
+ cells[r][c].grid(row=r, column=c + col_offset, sticky=NSEW)
814
+ if self._show_week_numbers and r < len(week_labels):
815
+ week_labels[r].grid(row=r, column=0, sticky=NSEW)
816
+ else:
817
+ for c in range(7):
818
+ cells[r][c].grid_remove()
819
+ if self._show_week_numbers and r < len(week_labels):
820
+ week_labels[r].grid_remove()
821
+
822
+ # --- selection/navigation ----------------------------------------
823
+ def _refresh_calendar(self) -> None:
824
+ self._draw_calendar()
825
+
826
+ def _on_next_month(self, *_args) -> None:
827
+ candidate = self._add_months(self._display_date, 1)
828
+ if self._is_month_allowed(candidate):
829
+ self._display_date = candidate
830
+ self._refresh_calendar()
831
+
832
+ def _on_prev_month(self, *_args) -> None:
833
+ candidate = self._add_months(self._display_date, -1)
834
+ if self._is_month_allowed(candidate):
835
+ self._display_date = candidate
836
+ self._refresh_calendar()
837
+
838
+ def _on_next_year(self, *_args) -> None:
839
+ candidate = date(self._display_date.year + 1, self._display_date.month, 1)
840
+ if self._is_month_allowed(candidate):
841
+ self._display_date = candidate
842
+ self._refresh_calendar()
843
+
844
+ def _on_prev_year(self, *_args) -> None:
845
+ candidate = date(self._display_date.year - 1, self._display_date.month, 1)
846
+ if self._is_month_allowed(candidate):
847
+ self._display_date = candidate
848
+ self._refresh_calendar()
849
+
850
+ def _on_reset_date(self, *_args) -> None:
851
+ self._display_date = date(self._initial_date.year, self._initial_date.month, 1)
852
+ self._selected_date = self._initial_date
853
+ self._range_start = self._initial_date
854
+ self._range_end = None
855
+ self._refresh_calendar()
856
+ self.event_generate(
857
+ "<<DateSelect>>", data=DateSelectEvent(date=self._selected_date, range=(self._range_start, self._range_end)))
858
+
859
+ def _on_date_selected_by_date(self, target: date) -> None:
860
+ if self._is_disabled(target):
861
+ return
862
+ if self._selection_mode == "range":
863
+ if self._range_start is None or self._range_end is not None:
864
+ self._range_start = target
865
+ self._range_end = None
866
+ else:
867
+ if target < self._range_start:
868
+ self._range_start, target = target, self._range_start
869
+ self._range_end = target
870
+ self._selected_date = target
871
+ else:
872
+ self._selected_date = target
873
+ self._range_start = target
874
+ self._range_end = None
875
+
876
+ self._draw_calendar()
877
+ self.event_generate(
878
+ "<<DateSelect>>", data=DateSelectEvent(date=self._selected_date, range=(self._range_start, self._range_end)))
879
+
880
+ # --- helpers ------------------------------------------------------
881
+ def _lock_size(self) -> None:
882
+ if self._locked_size is None:
883
+ self.update_idletasks()
884
+ self._locked_size = (self.winfo_width(), self.winfo_height())
885
+ try:
886
+ self.minsize(*self._locked_size)
887
+ except Exception:
888
+ pass
889
+
890
+ def _header_columns(self) -> list[str]:
891
+ localized_weekdays = [MessageCatalog.translate(token) for token in _WEEKDAY_TOKENS]
892
+ return localized_weekdays[self._first_weekday:] + localized_weekdays[: self._first_weekday]
893
+
894
+ def _is_disabled(self, d: date) -> bool:
895
+ if d in self._disabled_dates:
896
+ return True
897
+ if self._min_date and d < self._min_date:
898
+ return True
899
+ if self._max_date and d > self._max_date:
900
+ return True
901
+ return False
902
+
903
+ def _style_for_date(self, d: date, in_month: bool, disabled: bool) -> tuple[str | None, str]:
904
+ """Return (accent, variant) tuple for the given date."""
905
+ if disabled or not in_month:
906
+ return (None, "calendar-outside")
907
+ if self._selection_mode == "range" and self._range_start:
908
+ end = self._range_end
909
+ start = self._range_start
910
+ if end and start:
911
+ if start <= d <= end:
912
+ if start < d < end:
913
+ return (self._accent, "calendar-range")
914
+ return (self._accent, "calendar-date")
915
+ return (self._accent, "calendar-day")
916
+
917
+ def _is_selected(self, d: date) -> bool:
918
+ if self._selection_mode == "range":
919
+ if not self._range_start:
920
+ return False
921
+ if self._range_end:
922
+ return self._range_start <= d <= self._range_end
923
+ return d == self._range_start
924
+ return d == self._selected_date
925
+
926
+ def _is_month_allowed(self, candidate: date) -> bool:
927
+ if self._min_date and candidate < self._min_date.replace(day=1):
928
+ return False
929
+ if self._max_date and candidate > self._max_date.replace(day=1):
930
+ return False
931
+ return True
932
+
933
+ def _set_title(self) -> None:
934
+ self._title_var.set(_format_month_year(self._display_date))
935
+
936
+ def _square_button_padding(self) -> tuple[int, int, int, int]:
937
+ """Calculate padding for square calendar day buttons based on caption font metrics."""
938
+ from tkinter import font
939
+ f = font.nametofont('caption')
940
+ linespace = f.metrics()['linespace']
941
+ text_width = f.measure('00')
942
+ # For square buttons with centered text: width = height
943
+ # width = text_width + 2*h_pad, height = linespace + v_pad
944
+ # Use symmetric h_pad and add v_pad to balance
945
+ diff = linespace - text_width # 15 - 12 = 3
946
+ h_pad = (diff + 1) // 2 # round up: 2
947
+ v_pad = 2 * h_pad - diff # balance: 2*2 - 3 = 1
948
+ # Return (left, top, right, bottom) - add top padding to nudge text down
949
+ return (h_pad, 2, h_pad, v_pad)
950
+
951
+ @staticmethod
952
+ def _add_months(d: date, n: int) -> date:
953
+ year = d.year + (d.month - 1 + n) // 12
954
+ month = (d.month - 1 + n) % 12 + 1
955
+ return date(year, month, 1)
956
+
957
+ _coerce_date = staticmethod(coerce_date)
958
+
959
+ def _normalize_range(
960
+ self,
961
+ start: date | datetime | str | None,
962
+ end: date | datetime | str | None = None,
963
+ ) -> tuple[date | None, date | None]:
964
+ """Normalize a date range, ensuring start <= end if both are present."""
965
+ s = self._coerce_date(start)
966
+ e = self._coerce_date(end)
967
+ if s is not None and e is not None and e < s:
968
+ s, e = e, s
969
+ return (s, e)
970
+
971
+
972
+ __all__ = ["Calendar"]