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,257 @@
1
+ """Screen color picker (dropper) dialog for bootstack.
2
+
3
+ This module provides a color dropper tool that allows users to select colors
4
+ directly from anywhere on the screen. It captures a screenshot and displays
5
+ a magnified view to help with precise color selection.
6
+ """
7
+ import tkinter as tk
8
+ from collections import namedtuple
9
+ from types import SimpleNamespace
10
+ from typing import Any, Callable, Optional
11
+
12
+ from tkinter import Canvas, Variable
13
+ from PIL import ImageGrab, ImageTk
14
+ from PIL.Image import Resampling
15
+
16
+ from bootstack.runtime.toplevel import Toplevel
17
+ import bootstack.core.colorutils as colorutils
18
+ import bootstack.runtime.utility as utility
19
+ from bootstack.constants import *
20
+
21
+ ttk = SimpleNamespace(Canvas=Canvas, Toplevel=Toplevel, Variable=Variable)
22
+
23
+ ColorChoice = namedtuple('ColorChoice', 'rgb hsl hex')
24
+
25
+
26
+ class ColorDropperDialog:
27
+ """Screen color picker with zoom preview.
28
+
29
+ Click anywhere on screen to select a color. The selected color is stored in
30
+ `result` as a ColorChoice named tuple with rgb, hsl, and hex values.
31
+
32
+ Note:
33
+ Supported on Windows and Linux. macOS is not supported due to ImageGrab
34
+ limitations. On high-DPI displays, ensure the app runs in high-DPI mode.
35
+ """
36
+
37
+ def __init__(self) -> None:
38
+ """Initialize the ColorDropperDialog and prepare all state attributes."""
39
+ self.zoom_yoffset = None
40
+ self.zoom_xoffset = None
41
+ self.zoom_width = None
42
+ self.zoom_height = None
43
+ self.zoom_image = None
44
+ self.zoom_data = None
45
+ self.zoom_level = None
46
+ self.screenshot_image = None
47
+ self.screenshot_data = None
48
+ self.screenshot_canvas = None
49
+ self.toplevel: Optional[ttk.Toplevel] = None
50
+ self.zoom_toplevel: Optional[ttk.Toplevel] = None
51
+ self.result: ttk.Variable = ttk.Variable()
52
+ self._emitted_result = False
53
+
54
+ def build_screenshot_canvas(self) -> None:
55
+ """Build the screenshot canvas"""
56
+ self.screenshot_canvas: ttk.Canvas = ttk.Canvas(self.toplevel, cursor='tcross')
57
+ self.screenshot_data = ImageGrab.grab()
58
+ self.screenshot_image: ImageTk.PhotoImage = ImageTk.PhotoImage(self.screenshot_data)
59
+ self.screenshot_canvas.create_image(
60
+ 0, 0, image=self.screenshot_image, anchor=NW)
61
+ self.screenshot_canvas.pack(fill=BOTH, expand=YES)
62
+
63
+ def build_zoom_toplevel(self, master) -> None:
64
+ """Build the toplevel widget that shows the zoomed version of
65
+ the pixels underneath the mouse cursor."""
66
+ height = utility.scale_size(self.toplevel, 100)
67
+ width = utility.scale_size(self.toplevel, 100)
68
+ text_xoffset = utility.scale_size(self.toplevel, 50)
69
+ text_yoffset = utility.scale_size(self.toplevel, 50)
70
+ toplevel = ttk.Toplevel(master=master)
71
+ toplevel.transient(master=master)
72
+ if self.toplevel and self.toplevel.winsys == 'x11':
73
+ toplevel.attributes('-type', 'tooltip')
74
+ else:
75
+ toplevel.overrideredirect(True)
76
+ toplevel.geometry(f'{width}x{height}')
77
+ toplevel.lift()
78
+ self.zoom_canvas: ttk.Canvas = ttk.Canvas(
79
+ toplevel, borderwidth=1, height=self.zoom_height, width=self.zoom_width)
80
+ self.zoom_canvas.create_image(0, 0, tags=['image'], anchor=NW)
81
+ self.zoom_canvas.create_text(
82
+ text_xoffset, text_yoffset, text="+", fill="white", tags=['indicator'])
83
+ self.zoom_canvas.pack(fill=BOTH, expand=YES)
84
+ self.zoom_toplevel = toplevel
85
+
86
+ def _cleanup(self) -> None:
87
+ """Destroy zoom and main toplevels."""
88
+ if self.zoom_toplevel and self.zoom_toplevel.winfo_exists():
89
+ try:
90
+ self.zoom_toplevel.destroy()
91
+ except Exception:
92
+ pass
93
+ if self.toplevel and self.toplevel.winfo_exists():
94
+ try:
95
+ self.toplevel.destroy()
96
+ except Exception:
97
+ pass
98
+
99
+ def on_mouse_wheel(self, event: tk.Event) -> None:
100
+ """Zoom in and out on the image underneath the mouse"""
101
+ delta = 0
102
+ if self.toplevel and self.toplevel.winsys.lower() == 'win32':
103
+ delta = -int(event.delta / 120)
104
+ elif self.toplevel and self.toplevel.winsys.lower() == 'aqua':
105
+ delta = -event.delta
106
+ elif event.num == 4:
107
+ delta = -1
108
+ elif event.num == 5:
109
+ delta = 1
110
+ self.zoom_level += delta
111
+ self._on_mouse_motion()
112
+
113
+ def on_left_click(self, _: tk.Event) -> Optional[ColorChoice]:
114
+ """Capture the color underneath the mouse cursor and destroy
115
+ the toplevel widget"""
116
+ # add logic here to capture the image color
117
+ hx = self.get_hover_color()
118
+ hsl = colorutils.color_to_hsl(hx)
119
+ rgb = colorutils.color_to_rgb(hx)
120
+ self.result.set(ColorChoice(rgb, hsl, hx))
121
+ if self.toplevel:
122
+ self.toplevel.grab_release()
123
+ self.toplevel.destroy()
124
+ if self.zoom_toplevel:
125
+ self.zoom_toplevel.destroy()
126
+ return self.result.get()
127
+
128
+ def on_right_click(self, _: tk.Event) -> None:
129
+ """Close the color dropper without saving any color information"""
130
+ if self.zoom_toplevel:
131
+ self.zoom_toplevel.destroy()
132
+ if self.toplevel:
133
+ self.toplevel.grab_release()
134
+ self.toplevel.destroy()
135
+
136
+ def _on_mouse_motion(self, event: Optional[tk.Event] = None) -> None:
137
+ """Callback for mouse motion"""
138
+ if event is None:
139
+ x, y = self.toplevel.winfo_pointerxy() # type: ignore[union-attr]
140
+ else:
141
+ x = event.x
142
+ y = event.y
143
+ # move snip window
144
+ self.zoom_toplevel.geometry(
145
+ f'+{x + self.zoom_xoffset}+{y + self.zoom_yoffset}')
146
+ # update the snip image
147
+ bbox = (x - self.zoom_level, y - self.zoom_level,
148
+ x + self.zoom_level + 1, y + self.zoom_level + 1)
149
+ size = (self.zoom_width, self.zoom_height)
150
+ self.zoom_data = self.screenshot_data.crop(
151
+ bbox).resize(size, Resampling.BOX)
152
+ self.zoom_image: ImageTk.PhotoImage = ImageTk.PhotoImage(self.zoom_data)
153
+ self.zoom_canvas.itemconfig('image', image=self.zoom_image)
154
+ hover_color = self.get_hover_color()
155
+ contrast_color = colorutils.contrast_color(hover_color, 'hex')
156
+ self.zoom_canvas.itemconfig('indicator', fill=contrast_color)
157
+
158
+ def get_hover_color(self) -> str:
159
+ """Get the color that is hovered over by the mouse cursor."""
160
+ x1, y1, x2, y2 = self.zoom_canvas.bbox('indicator')
161
+ x = x1 + (x2 - x1) // 2
162
+ y = y1 + (y2 - y1) // 2
163
+ r, g, b = self.zoom_data.getpixel((x, y))
164
+ hx = colorutils.color_to_hex((r, g, b))
165
+ return hx
166
+
167
+ # event helpers -----------------------------------------------------------
168
+ def on_dialog_result(self, callback: Callable[[Any], None]) -> Optional[str]:
169
+ """Bind a callback fired when the dropper produces a result."""
170
+ target = self.toplevel
171
+ if target is None:
172
+ return None
173
+
174
+ def handler(event):
175
+ callback(getattr(event, "data", None))
176
+
177
+ return target.bind("<<DialogResult>>", handler, add="+")
178
+
179
+ def off_dialog_result(self, funcid: str) -> None:
180
+ """Unbind a previously bound dialog result callback."""
181
+ target = self.toplevel
182
+ if target is None:
183
+ return
184
+ target.unbind("<<DialogResult>>", funcid)
185
+
186
+ def _emit_result(self, confirmed: bool) -> None:
187
+ """Emit the dialog result event once."""
188
+ if self._emitted_result:
189
+ return
190
+ payload = {"result": self.result.get() if hasattr(self.result, "get") else None, "confirmed": confirmed}
191
+ target = self.toplevel
192
+ if not target:
193
+ return
194
+ try:
195
+ target.event_generate("<<DialogResult>>", data=payload)
196
+ except Exception:
197
+ try:
198
+ target.event_generate("<<DialogResult>>")
199
+ except Exception:
200
+ pass
201
+ self._emitted_result = True
202
+
203
+ def show(self) -> None:
204
+ """Show the toplevel window"""
205
+ self._emitted_result = False
206
+ self.toplevel = ttk.Toplevel(alpha=1)
207
+ self.toplevel.wm_attributes('-fullscreen', True)
208
+ self.build_screenshot_canvas()
209
+
210
+ # event binding
211
+ self.toplevel.bind("<Motion>", self._on_mouse_motion, "+")
212
+ self.toplevel.bind("<Button-1>", self._on_left_click, "+")
213
+ utility.bind_right_click(self.toplevel, self._on_right_click)
214
+ self.toplevel.bind("<Escape>", self._on_cancel, "+")
215
+
216
+ if self.toplevel.winsys.lower() == 'x11':
217
+ self.toplevel.bind("<Button-4>", self.on_mouse_wheel, "+")
218
+ self.toplevel.bind("<Button-5>", self.on_mouse_wheel, "+")
219
+ else:
220
+ self.toplevel.bind("<MouseWheel>", self.on_mouse_wheel, "+")
221
+
222
+ # initial snip setup
223
+ self.zoom_level: int = 2
224
+ self.zoom_toplevel: Optional[ttk.Toplevel] = None
225
+ self.zoom_data: Any = None
226
+ self.zoom_image: Optional[ImageTk.PhotoImage] = None
227
+ self.zoom_height: int = utility.scale_size(self.toplevel, 100)
228
+ self.zoom_width: int = utility.scale_size(self.toplevel, 100)
229
+ self.zoom_xoffset: int = utility.scale_size(self.toplevel, 10)
230
+ self.zoom_yoffset: int = utility.scale_size(self.toplevel, 10)
231
+
232
+ self.build_zoom_toplevel(self.toplevel)
233
+ self.toplevel.grab_set()
234
+ self.toplevel.lift('.')
235
+ self.zoom_toplevel.lift(self.toplevel)
236
+
237
+ self._on_mouse_motion()
238
+
239
+ def _on_left_click(self, _: tk.Event) -> Optional[ColorChoice]:
240
+ """Capture the color underneath the mouse cursor and close."""
241
+ hx = self.get_hover_color()
242
+ hsl = colorutils.color_to_hsl(hx)
243
+ rgb = colorutils.color_to_rgb(hx)
244
+ self.result.set(ColorChoice(rgb, hsl, hx))
245
+ self._emit_result(confirmed=True)
246
+ self._cleanup()
247
+ return self.result.get()
248
+
249
+ def _on_right_click(self, _: tk.Event) -> None:
250
+ """Close without saving any color information."""
251
+ self.result.set(None)
252
+ self._emit_result(confirmed=False)
253
+ self._cleanup()
254
+
255
+ def _on_cancel(self, _: tk.Event) -> None:
256
+ """Close without selection (Escape)."""
257
+ self._on_right_click(_)
@@ -0,0 +1,404 @@
1
+ """Dialog wrapper around the Calendar widget.
2
+
3
+ Exposes a chrome-less, popover-capable date picker dialog that can close on
4
+ outside clicks and forwards Calendar options (disabled dates, bounds, etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import tkinter
10
+ from datetime import date, datetime
11
+ from types import SimpleNamespace
12
+ from typing import Any, Callable, Iterable, Literal, Optional, Tuple, Union
13
+ from tkinter import Widget
14
+
15
+ from bootstack.widgets.primitives import Frame
16
+ from bootstack.constants import BOTH, PRIMARY, YES
17
+ from bootstack.dialogs.dialog import Dialog
18
+ from bootstack.runtime.window_utilities import AnchorPoint
19
+ from bootstack.widgets.composites.calendar import Calendar
20
+
21
+ ttk = SimpleNamespace(Frame=Frame)
22
+
23
+ __all__ = ["DateDialog"]
24
+
25
+
26
+ class _ChromeDialog(Dialog):
27
+ """Dialog that can optionally hide window chrome via override-redirect."""
28
+
29
+ def __init__(self, *args: Any, hide_window_chrome: bool = False, **kwargs: Any) -> None:
30
+ self._hide_window_chrome = hide_window_chrome
31
+ self._suppress_focus_out = False
32
+ self._outside_click_binding: str | None = None
33
+ super().__init__(*args, **kwargs)
34
+
35
+ def _create_toplevel(self):
36
+ super()._create_toplevel()
37
+ if self._hide_window_chrome and self._toplevel:
38
+ # Note: overrideredirect is automatically disabled on macOS
39
+ # in BaseWindow.overrideredirect() due to Tk/Cocoa issues
40
+ try:
41
+ self._toplevel.overrideredirect(True)
42
+ except Exception:
43
+ pass
44
+
45
+ def show(
46
+ self,
47
+ position: Optional[Tuple[int, int]] = None,
48
+ modal: Optional[bool] = None,
49
+ *,
50
+ anchor_to: Optional[Union[Widget, Literal["screen", "cursor", "parent"]]] = None,
51
+ anchor_point: AnchorPoint = 'center',
52
+ window_point: AnchorPoint = 'center',
53
+ offset: Tuple[int, int] = (0, 0),
54
+ auto_flip: Union[bool, Literal['vertical', 'horizontal']] = False
55
+ ):
56
+ """Override show to position before deiconify, avoiding placement flash.
57
+
58
+ Args:
59
+ position: Optional (x, y) coordinates to position the dialog.
60
+ modal: Override the mode's default modality.
61
+ anchor_to: Positioning target (Widget, "screen", "cursor", "parent", or None).
62
+ anchor_point: Point on the anchor target (n, s, e, w, ne, nw, se, sw, center).
63
+ window_point: Point on the dialog window (n, s, e, w, ne, nw, se, sw, center).
64
+ offset: Additional (x, y) offset in pixels from the anchor position.
65
+ auto_flip: Smart positioning to keep window on screen.
66
+ """
67
+ if modal is None:
68
+ modal = (self._mode == "modal")
69
+
70
+ self.result = None
71
+ self._create_toplevel()
72
+ if self._hide_window_chrome and self._toplevel:
73
+ self._toplevel.withdraw()
74
+
75
+ self._build_content()
76
+ self._build_footer()
77
+
78
+ self._position_dialog(
79
+ position=position,
80
+ anchor_to=anchor_to,
81
+ anchor_point=anchor_point,
82
+ window_point=window_point,
83
+ offset=offset,
84
+ auto_flip=auto_flip
85
+ )
86
+
87
+ if self._hide_window_chrome and self._toplevel:
88
+ self._toplevel.deiconify()
89
+ try:
90
+ self._toplevel.lift()
91
+ self._toplevel.focus_force()
92
+ except Exception:
93
+ pass
94
+
95
+ if self._alert:
96
+ self._toplevel.bell()
97
+
98
+ if self._mode == "popover":
99
+ self._suppress_focus_out = True
100
+ self._toplevel.bind("<FocusOut>", self._on_focus_out, add="+")
101
+ try:
102
+ self._toplevel.after(50, lambda: setattr(self, "_suppress_focus_out", False))
103
+ except Exception:
104
+ self._suppress_focus_out = False
105
+ self._bind_outside_click()
106
+
107
+ if modal:
108
+ # transient() is already called in Dialog._create_toplevel()
109
+ # Don't call it again here as it breaks mica effect on Windows
110
+ if self._mode == "modal":
111
+ self._toplevel.grab_set()
112
+ self._master.wait_window(self._toplevel)
113
+
114
+ def _on_focus_out(self, event: tkinter.Event):
115
+ if self._suppress_focus_out:
116
+ return
117
+ return super()._on_focus_out(event)
118
+
119
+ def _bind_outside_click(self) -> None:
120
+ """Close popover when clicking outside the toplevel."""
121
+ if not self._toplevel:
122
+ return
123
+
124
+ def handler(event: tkinter.Event) -> None:
125
+ widget = getattr(event, "widget", None)
126
+ if widget is None:
127
+ return
128
+
129
+ # Walk up the widget hierarchy to check if any parent is the toplevel
130
+ current = widget
131
+ toplevel_str = str(self._toplevel)
132
+ while current:
133
+ current_str = str(current)
134
+ if current_str == toplevel_str or current_str.startswith(toplevel_str + "."):
135
+ return
136
+ try:
137
+ current = current.master
138
+ except AttributeError:
139
+ break
140
+
141
+ # Schedule destroy for after current event processing completes
142
+ # This allows button commands inside the dialog to execute first
143
+ try:
144
+ if self._toplevel.winfo_exists():
145
+ self._toplevel.after_idle(lambda: self._destroy_if_exists())
146
+ except Exception:
147
+ pass
148
+
149
+ try:
150
+ self._outside_click_binding = self._toplevel.bind_all("<ButtonPress-1>", handler, add="+")
151
+ self._toplevel.bind("<Destroy>", lambda e: self._unbind_outside_click(), add="+")
152
+ except Exception:
153
+ self._outside_click_binding = None
154
+
155
+ def _destroy_if_exists(self) -> None:
156
+ """Destroy the toplevel if it still exists."""
157
+ try:
158
+ if self._toplevel and self._toplevel.winfo_exists():
159
+ self._toplevel.destroy()
160
+ except Exception:
161
+ pass
162
+
163
+ def _unbind_outside_click(self) -> None:
164
+ if self._toplevel and self._outside_click_binding:
165
+ try:
166
+ self._toplevel.unbind_all("<ButtonPress-1>", self._outside_click_binding)
167
+ except Exception:
168
+ pass
169
+ self._outside_click_binding = None
170
+
171
+
172
+ class _DialogCalendar(Calendar):
173
+ """Calendar variant that records why a selection event fired."""
174
+
175
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
176
+ super().__init__(*args, **kwargs)
177
+ self._last_trigger_reason: str | None = None
178
+
179
+ def _on_reset_date(self, *args: Any) -> None:
180
+ self._last_trigger_reason = "reset"
181
+ super()._on_reset_date(*args)
182
+
183
+ def _on_date_selected_by_date(self, target: date) -> None:
184
+ self._last_trigger_reason = "select"
185
+ super()._on_date_selected_by_date(target)
186
+
187
+
188
+ class DateDialog:
189
+ """Modal dialog that displays a Calendar"""
190
+
191
+ def __init__(
192
+ self,
193
+ master: Optional[tkinter.Misc] = None,
194
+ title: str = " ",
195
+ initial_date: Optional[date] = None,
196
+ first_weekday: int = 6,
197
+ accent: str = None,
198
+ disabled_dates: Optional[Iterable[date | datetime | str]] = None,
199
+ min_date: Optional[date | datetime | str] = None,
200
+ max_date: Optional[date | datetime | str] = None,
201
+ show_outside_days: Optional[bool] = None,
202
+ show_week_numbers: bool = False,
203
+ hide_window_chrome: bool = False,
204
+ close_on_click_outside: bool = False,
205
+ ) -> None:
206
+ """Create a date selection dialog.
207
+
208
+ Args:
209
+ master: Parent widget; positions dialog relative to it when set.
210
+ title: Dialog window title text.
211
+ initial_date: Initial date shown; defaults to `date.today()`.
212
+ first_weekday: First weekday index (0=Monday, 6=Sunday).
213
+ accent: Calendar accent token (e.g., `primary`, `secondary`).
214
+ disabled_dates: Iterable of dates to disable selection.
215
+ min_date: Lower bound for selectable dates.
216
+ max_date: Upper bound for selectable dates.
217
+ show_outside_days: Whether to show outside-month days. Defaults to
218
+ the Calendar behavior (True for single month).
219
+ show_week_numbers: Display ISO week numbers beside each row.
220
+ hide_window_chrome: When True, displays the dialog with no window
221
+ decorations using override-redirect.
222
+ close_on_click_outside: When True, closes the dialog when focus
223
+ moves outside (popover mode).
224
+ """
225
+ self._master = master
226
+ self._first_weekday = first_weekday
227
+ self._initial_date = initial_date or datetime.today().date()
228
+ self._accent = accent or PRIMARY
229
+ self._disabled_dates = disabled_dates
230
+ self._min_date = min_date
231
+ self._max_date = max_date
232
+ self._show_outside_days = show_outside_days
233
+ self._show_week_numbers = show_week_numbers
234
+ self._hide_window_chrome = hide_window_chrome
235
+ self._close_on_click_outside = close_on_click_outside
236
+
237
+ self._picker: Optional[_DialogCalendar] = None
238
+
239
+ self._dialog = _ChromeDialog(
240
+ master=master,
241
+ title=title,
242
+ content_builder=self._create_content,
243
+ buttons=[],
244
+ footer_builder=None,
245
+ hide_window_chrome=self._hide_window_chrome,
246
+ mode="popover" if self._close_on_click_outside else "modal",
247
+ )
248
+
249
+ def _create_content(self, master: tkinter.Widget) -> None:
250
+ """Build the Calendar content inside the dialog."""
251
+ container = ttk.Frame(master, padding=2, show_border=True)
252
+ container.pack(fill=BOTH, expand=YES)
253
+
254
+ self._picker = _DialogCalendar(
255
+ master=container,
256
+ start_date=self._initial_date,
257
+ first_weekday=self._first_weekday,
258
+ accent=self._accent,
259
+ disabled_dates=self._disabled_dates,
260
+ min_date=self._min_date,
261
+ max_date=self._max_date,
262
+ show_outside_days=self._show_outside_days,
263
+ show_week_numbers=self._show_week_numbers,
264
+ padding=0,
265
+ )
266
+ self._picker.pack(fill=BOTH, expand=YES)
267
+ self._picker.on_date_selected(self._on_date_selected)
268
+
269
+ def _on_date_selected(self, event: tkinter.Event) -> None:
270
+ """Handle <<DateSelect>> from the embedded Calendar."""
271
+ if not self._picker:
272
+ return
273
+ trigger_reason = getattr(self._picker, "_last_trigger_reason", None)
274
+ if trigger_reason != "select":
275
+ return
276
+
277
+ payload = getattr(event, "data", None)
278
+ selected = None
279
+ if isinstance(payload, dict):
280
+ selected = payload.get("date") or payload.get("result")
281
+
282
+ selected = selected or self._picker.get()
283
+ if selected is None:
284
+ return
285
+
286
+ self._dialog.result = selected
287
+ self._emit_result(selected, confirmed=True)
288
+ if self._dialog.toplevel:
289
+ top = self._dialog.toplevel
290
+ try:
291
+ top.grab_release()
292
+ except Exception:
293
+ pass
294
+ top.destroy()
295
+
296
+ def show(
297
+ self,
298
+ position: Optional[Tuple[int, int]] = None,
299
+ modal: Optional[bool] = None,
300
+ *,
301
+ anchor_to: Optional[Union[Widget, Literal["screen", "cursor", "parent"]]] = None,
302
+ anchor_point: AnchorPoint = 'center',
303
+ window_point: AnchorPoint = 'center',
304
+ offset: Tuple[int, int] = (0, 0),
305
+ auto_flip: Union[bool, Literal['vertical', 'horizontal']] = False
306
+ ) -> None:
307
+ """Display the dialog and block until closed.
308
+
309
+ Args:
310
+ position: Optional (x, y) coordinates to position the dialog.
311
+ If provided, takes precedence over anchor-based positioning.
312
+ If omitted and anchor_to is not provided, positions at the parent's
313
+ bottom-right when available, otherwise centers.
314
+ modal: Override the mode's default modality.
315
+ If None, uses True for modal mode dialogs.
316
+ anchor_to: Positioning target. Can be:
317
+ - Widget: Anchor to a specific widget
318
+ - "screen": Anchor to screen edges/corners
319
+ - "cursor": Anchor to mouse cursor location
320
+ - "parent": Anchor to parent window (same as widget)
321
+ - None: Uses default positioning behavior
322
+ anchor_point: Point on the anchor target (n, s, e, w, ne, nw, se, sw, center).
323
+ Default 'center'.
324
+ window_point: Point on the dialog window (n, s, e, w, ne, nw, se, sw, center).
325
+ Default 'center'.
326
+ offset: Additional (x, y) offset in pixels from the anchor position.
327
+ auto_flip: Smart positioning to keep window on screen.
328
+ - False: No flipping (default)
329
+ - True: Flip both vertically and horizontally as needed
330
+ - 'vertical': Only flip up/down
331
+ - 'horizontal': Only flip left/right
332
+ """
333
+ # Default positioning: bottom-right of parent if no positioning options provided
334
+ if position is None and anchor_to is None and self._master:
335
+ try:
336
+ x = self._master.winfo_rootx() + self._master.winfo_width()
337
+ y = self._master.winfo_rooty() + self._master.winfo_height()
338
+ position = (x, y)
339
+ except Exception:
340
+ pass
341
+
342
+ # Default modal to True if not specified
343
+ if modal is None:
344
+ modal = True
345
+
346
+ self._dialog.show(
347
+ position=position,
348
+ modal=modal,
349
+ anchor_to=anchor_to,
350
+ anchor_point=anchor_point,
351
+ window_point=window_point,
352
+ offset=offset,
353
+ auto_flip=auto_flip
354
+ )
355
+
356
+ @property
357
+ def result(self) -> Optional[date]:
358
+ """The selected date, or None if cancelled."""
359
+ return self._dialog.result
360
+
361
+ def on_result(self, callback: Callable[[date], None]) -> Optional[str]:
362
+ """Bind a callback fired when a result is produced.
363
+
364
+ The callback receives `event.data["result"]` (a `datetime.date`).
365
+
366
+ Args:
367
+ callback: Callable that receives the selected `datetime.date`.
368
+
369
+ Returns:
370
+ The Tk binding identifier, which can be used with `off_result`.
371
+ """
372
+ target = self._dialog.toplevel or self._master
373
+ if target is None:
374
+ return None
375
+
376
+ def handler(event: tkinter.Event) -> None:
377
+ callback(getattr(event, "data", None))
378
+
379
+ return target.bind("<<DialogResult>>", handler, add="+")
380
+
381
+ def off_result(self, funcid: str) -> None:
382
+ """Unbind a previously bound `on_result` callback.
383
+
384
+ Args:
385
+ funcid: Binding identifier returned by `on_result`.
386
+ """
387
+ target = self._dialog.toplevel or self._master
388
+ if target is None:
389
+ return
390
+ target.unbind("<<DialogResult>>", funcid)
391
+
392
+ def _emit_result(self, value: date, confirmed: bool) -> None:
393
+ """Emit a virtual Tk event with the dialog result."""
394
+ target = self._dialog.toplevel or self._master
395
+ if not target:
396
+ return
397
+ payload = {"result": value, "confirmed": confirmed}
398
+ try:
399
+ target.event_generate("<<DialogResult>>", data=payload)
400
+ except Exception:
401
+ try:
402
+ target.event_generate("<<DialogResult>>")
403
+ except Exception:
404
+ pass