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