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,879 @@
1
+ """Application runtime — App window, lifecycle helpers, and settings.
2
+
3
+ Provides the main `App` class (a `Tk` subclass), `AppSettings`, and
4
+ process-wide helpers for accessing the active app instance, reading settings,
5
+ and managing the current locale and theme from anywhere in the application.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ import tkinter
11
+ from dataclasses import dataclass
12
+ from typing import Any, Callable, Literal, Optional, Sequence, TypedDict, Union
13
+
14
+ from babel.core import UnknownLocaleError
15
+ from babel.dates import get_date_format, get_time_format
16
+ from babel.numbers import get_decimal_symbol, get_group_symbol
17
+ from typing_extensions import Unpack
18
+
19
+ from bootstack.constants import *
20
+ from bootstack.core.localization.intl_format import detect_locale
21
+ from bootstack.core.localization.msgcat import MessageCatalog
22
+ from bootstack.core.publisher import Publisher
23
+ from bootstack.core.mixins.widget import WidgetCapabilitiesMixin
24
+ from bootstack.runtime.base_window import BaseWindow
25
+ from bootstack.runtime.utility import enable_high_dpi_awareness
26
+
27
+ _current_app: App | None = None
28
+
29
+ # Sentinel for "use settings default"
30
+ _USE_SETTINGS = object()
31
+
32
+
33
+ def set_current_app(app: App) -> None:
34
+ """Set the process-wide current App instance.
35
+
36
+ Intended to be called from App.__init__ for the first app created.
37
+ """
38
+ global _current_app
39
+ _current_app = app
40
+
41
+
42
+ def get_app_settings() -> AppSettings:
43
+ """Return the settings for current App.
44
+
45
+ Returns:
46
+ The AppSettings instance for the current application.
47
+
48
+ Raises:
49
+ RuntimeError: If no active App instance is set.
50
+ """
51
+ return get_current_app().settings
52
+
53
+
54
+ def clear_current_app(app: App) -> None:
55
+ """Clear the current app reference if it matches the given app."""
56
+ global _current_app
57
+ if _current_app is app:
58
+ _current_app = None
59
+
60
+
61
+ def get_current_app() -> App:
62
+ """Return the current App instance.
63
+
64
+ Returns:
65
+ The currently active App instance.
66
+
67
+ Raises:
68
+ RuntimeError: If no App has been registered yet.
69
+ """
70
+ if _current_app is None:
71
+ raise RuntimeError(
72
+ "No current App instance is set. "
73
+ "Create an App first, e.g. `app = App()`."
74
+ )
75
+ return _current_app
76
+
77
+
78
+ def has_current_app() -> bool:
79
+ """Check if a current App instance is registered.
80
+
81
+ Returns:
82
+ True if an App instance exists, False otherwise.
83
+ """
84
+ return _current_app is not None
85
+
86
+
87
+ def get_default_root(what: Optional[str] = None) -> tkinter.Tk:
88
+ """Get the default Tk root window.
89
+
90
+ Returns the default root if it has been created, otherwise
91
+ creates and returns a new instance.
92
+
93
+ Args:
94
+ what: Optional description of the operation requiring the root,
95
+ used in error messages if called too early.
96
+
97
+ Returns:
98
+ The default Tk root window instance.
99
+
100
+ Raises:
101
+ RuntimeError: If tkinter is configured to not support default root,
102
+ or if called too early with a 'what' description.
103
+ """
104
+ if not tkinter._support_default_root:
105
+ raise RuntimeError(
106
+ "No master specified and tkinter is "
107
+ "configured to not support default root")
108
+ if not tkinter._default_root:
109
+ if what:
110
+ raise RuntimeError(f"Too early to {what}: no default root window")
111
+ root = tkinter.Tk()
112
+ assert tkinter._default_root is root
113
+ return tkinter._default_root
114
+
115
+
116
+ def apply_class_bindings(window: tkinter.Widget | App) -> None:
117
+ """Add class level event bindings in application"""
118
+ # Copy TCheckbutton bindings to Toolbutton class
119
+ # This is needed because widgets using class_='Toolbutton' have their
120
+ # bindtags reference 'Toolbutton' instead of 'TCheckbutton', so they
121
+ # need the same mouse/keyboard bindings copied over.
122
+ for event in ('<Button-1>', '<ButtonRelease-1>', '<B1-Leave>', '<B1-Enter>',
123
+ '<Enter>', '<Leave>', '<Key-space>', '<<Invoke>>'):
124
+ binding = window.bind_class('TCheckbutton', event)
125
+ if binding:
126
+ window.bind_class('Toolbutton', event, binding)
127
+ window.bind_class('ButtonGroup', event, binding)
128
+
129
+ for className in ["TEntry", "TSpinbox", "TCombobox", "Text"]:
130
+ window.bind_class(
131
+ className=className,
132
+ sequence="<Configure>",
133
+ func=on_disabled_readonly_state,
134
+ add="+")
135
+
136
+ for sequence in ["<Control-a>", "<Control-A>"]:
137
+ window.bind_class(
138
+ className=className,
139
+ sequence=sequence,
140
+ func=on_select_all)
141
+
142
+ window.unbind_class("TButton", "<Key-space>")
143
+
144
+ def button_default_binding(event: tkinter.Event) -> None:
145
+ """The default keybind on a button when the return or enter key
146
+ is pressed and the button has focus or is the default button."""
147
+ try:
148
+ widget = window.nametowidget(event.widget)
149
+ widget.invoke()
150
+ except KeyError:
151
+ window.tk.call(event.widget, 'invoke')
152
+
153
+ window.bind_class(
154
+ "TButton", "<Key-Return>", button_default_binding,
155
+ add="+")
156
+ window.bind_class("TButton", "<KP_Enter>", button_default_binding, add="+")
157
+
158
+
159
+ def apply_all_bindings(window: tkinter.Widget | App) -> None:
160
+ """Add bindings to all widgets in the application"""
161
+ window.bind_all('<Map>', on_map_child, '+')
162
+ window.bind_all('<Destroy>', lambda e: Publisher.unsubscribe(e.widget))
163
+
164
+
165
+ def on_disabled_readonly_state(event: tkinter.Event) -> None:
166
+ """Change the cursor of entry type widgets to 'arrow' if in a
167
+ disabled or readonly state."""
168
+ try:
169
+ widget = event.widget
170
+ state = str(widget.cget('state'))
171
+ cursor = str(widget.cget('cursor'))
172
+ if state in (DISABLED, READONLY):
173
+ if cursor == 'arrow':
174
+ return
175
+ else:
176
+ widget['cursor'] = 'arrow'
177
+ else:
178
+ if cursor in ('ibeam', ''):
179
+ return
180
+ else:
181
+ widget['cursor'] = None
182
+ except:
183
+ pass
184
+
185
+
186
+ def on_map_child(event: tkinter.Event) -> None:
187
+ """Callback for <Map> event which generates a <<MapChild>> virtual
188
+ event on the parent"""
189
+ widget: tkinter.Widget = event.widget
190
+ try:
191
+ if widget.master is None: # root widget
192
+ return
193
+ else:
194
+ widget.master.event_generate('<<MapChild>>')
195
+ except:
196
+ # not a tkinter widget that I'm handling (ex. Combobox.popdown)
197
+ return
198
+
199
+
200
+ def on_select_all(event: tkinter.Event) -> None:
201
+ """Callback to select all text in Entry or Text widget when Ctrl+A is pressed."""
202
+ widget = event.widget
203
+
204
+ if isinstance(widget, tkinter.Text):
205
+ widget.tag_add(SEL, "1.0", END)
206
+ widget.mark_set(INSERT, END)
207
+ widget.see(INSERT)
208
+ elif isinstance(widget, tkinter.Entry):
209
+ widget.selection_range(0, END)
210
+ widget.icursor(END)
211
+
212
+
213
+ LocalizeMode = Union[bool, Literal['auto']]
214
+
215
+
216
+ @dataclass
217
+ class AppSettings:
218
+ """Application-wide settings for bootstack applications.
219
+
220
+ This dataclass holds configuration for theming, localization, and
221
+ application metadata. It is automatically populated with sensible
222
+ defaults based on the system locale.
223
+
224
+ Attributes:
225
+ app_name: The application name displayed in the title bar.
226
+ app_author: The application author (used for config paths).
227
+ app_version: The application version string.
228
+ theme: The current theme name ('light', 'dark', or a specific theme).
229
+ light_theme: The theme to use when `theme='light'`.
230
+ dark_theme: The theme to use when `theme='dark'`.
231
+ follow_system_appearance: If True, automatically switch between
232
+ `light_theme` and `dark_theme` to match the OS appearance and
233
+ track changes at runtime. Currently effective on macOS, where
234
+ Tk fires `<<TkSystemAppearanceChanged>>` and exposes the
235
+ current mode via `tk::unsupported::MacWindowStyle isDark`.
236
+ Defaults to False so existing apps that pin a theme keep
237
+ doing so.
238
+ available_themes: Sequence of available theme names.
239
+ inherit_surface_color: If True, child widgets inherit the parent's
240
+ surface color for consistent backgrounds.
241
+ locale: The locale identifier (e.g., 'en_US', 'de_DE'). Auto-detected
242
+ from system if not specified.
243
+ language: The base language code (e.g., 'en', 'de'). Derived from
244
+ locale if not specified.
245
+ date_format: The date format pattern. Derived from locale if not
246
+ specified (e.g., 'M/d/yy' for en_US).
247
+ time_format: The time format pattern. Derived from locale if not
248
+ specified (e.g., 'h:mm a' for en_US).
249
+ number_decimal: The decimal separator character. Derived from locale
250
+ if not specified (e.g., '.' for en_US).
251
+ number_thousands: The thousands separator character. Derived from
252
+ locale if not specified (e.g., ',' for en_US).
253
+ localize_mode: Controls localization behavior. 'auto' enables
254
+ localization based on locale, True always enables, False disables.
255
+ window_style: Windows-only pywinstyles effect for all windows.
256
+ Options include 'mica', 'acrylic', 'aero', 'transparent', 'win7'.
257
+ Defaults to 'mica'. Set to None to disable.
258
+ macos_quit_behavior: How the close button and Cmd+Q behave on macOS.
259
+ 'native' (default) follows Mac convention: clicking the window
260
+ close button or Cmd+H hides the app (withdraws), clicking the
261
+ dock icon reshows it, and Cmd+Q (or Dock → Quit) actually
262
+ destroys it. 'classic' restores the cross-platform behavior
263
+ where the close button destroys the window. No-op on Win/Linux.
264
+ remember_window_state: If True, the App's geometry (size + position)
265
+ is saved on close and restored on next launch, with off-screen
266
+ positions clamped back into a visible monitor. Off by default
267
+ so existing apps that pin a size/position keep doing so.
268
+ state_path: Optional override for where window state is stored.
269
+ When None, defaults to a per-app file under the OS config
270
+ directory (Library/Application Support on macOS, %APPDATA% on
271
+ Windows, $XDG_CONFIG_HOME on Linux). The leaf filename includes
272
+ `app_name` so multiple bootstack apps don't collide.
273
+
274
+ Examples:
275
+ ```python
276
+ # Create app with default settings
277
+ app = App()
278
+
279
+ # Create app with custom settings
280
+ settings = AppSettings(
281
+ app_name="My App",
282
+ theme="dark",
283
+ locale="de_DE"
284
+ )
285
+ app = App(settings=settings)
286
+
287
+ # Access settings
288
+ print(app.settings.locale) # 'de_DE'
289
+ print(app.settings.date_format) # 'd.M.yy'
290
+ ```
291
+ """
292
+ # information
293
+ app_name: str | None = None
294
+ app_author: str | None = None
295
+ app_version: str | None = None
296
+
297
+ # theme
298
+ theme: str = "light"
299
+ light_theme: str = "docs-light"
300
+ dark_theme: str = "docs-dark"
301
+ follow_system_appearance: bool = False
302
+ available_themes: Sequence[str] = ()
303
+ inherit_surface_color: bool = True
304
+
305
+ # internationalization
306
+ locale: str | None = None
307
+ language: str | None = None
308
+ date_format: str | None = None
309
+ time_format: str | None = None
310
+ number_decimal: str | None = None
311
+ number_thousands: str | None = None
312
+
313
+ # localization behavior
314
+ localize_mode: LocalizeMode = "auto"
315
+
316
+ # platform-specific
317
+ window_style: str | None = 'mica'
318
+ macos_quit_behavior: str = 'native'
319
+
320
+ # window state persistence
321
+ remember_window_state: bool = False
322
+ state_path: str | None = None
323
+
324
+ def __post_init__(self):
325
+ """Populate localization defaults when not explicitly configured."""
326
+ _apply_localization_defaults(self)
327
+
328
+
329
+ class AppSettingsKwargs(TypedDict, total=False):
330
+ app_name: str
331
+ app_author: str
332
+ app_version: str
333
+
334
+ # theme
335
+ theme: str
336
+ light_theme: str
337
+ dark_theme: str
338
+ follow_system_appearance: bool
339
+ available_themes: Sequence[str]
340
+ inherit_surface_color: bool
341
+
342
+ # localization
343
+ locale: str
344
+ language: str
345
+ date_format: str
346
+ time_format: str
347
+ number_decimal: str
348
+ number_thousands: str
349
+
350
+ # platform-specific
351
+ window_style: str | None
352
+ macos_quit_behavior: str
353
+
354
+ # window state persistence
355
+ remember_window_state: bool
356
+ state_path: str | None
357
+
358
+
359
+ DEFAULT_LOCALE = "en_US"
360
+
361
+
362
+ def _apply_localization_defaults(settings: AppSettings) -> None:
363
+ """Ensure locale-based fields always have meaningful defaults."""
364
+ locale_code = settings.locale or detect_locale(DEFAULT_LOCALE)
365
+ settings.locale = locale_code
366
+
367
+ if settings.language is None:
368
+ settings.language = _language_from_locale(locale_code)
369
+
370
+ if settings.date_format is None:
371
+ settings.date_format = _safe_date_format(locale_code)
372
+
373
+ if settings.time_format is None:
374
+ settings.time_format = _safe_time_format(locale_code)
375
+
376
+ if settings.number_decimal is None:
377
+ settings.number_decimal = _safe_decimal_symbol(locale_code)
378
+
379
+ if settings.number_thousands is None:
380
+ settings.number_thousands = _safe_group_symbol(locale_code)
381
+
382
+
383
+ def _language_from_locale(locale_code: str) -> str:
384
+ """Return the base language (e.g., en_US -> en)."""
385
+ base = locale_code.split("_", 1)[0]
386
+ return base.lower() if base else locale_code
387
+
388
+
389
+ def _safe_date_format(locale_code: str) -> str:
390
+ try:
391
+ return str(get_date_format("short", locale=locale_code))
392
+ except (UnknownLocaleError, ValueError):
393
+ return str(get_date_format("short", locale=DEFAULT_LOCALE))
394
+
395
+
396
+ def _safe_time_format(locale_code: str) -> str:
397
+ try:
398
+ return str(get_time_format("short", locale=locale_code))
399
+ except (UnknownLocaleError, ValueError):
400
+ return str(get_time_format("short", locale=DEFAULT_LOCALE))
401
+
402
+
403
+ def _safe_decimal_symbol(locale_code: str) -> str:
404
+ try:
405
+ return get_decimal_symbol(locale_code)
406
+ except (UnknownLocaleError, ValueError):
407
+ return get_decimal_symbol(DEFAULT_LOCALE)
408
+
409
+
410
+ def _safe_group_symbol(locale_code: str) -> str:
411
+ try:
412
+ return get_group_symbol(locale_code)
413
+ except (UnknownLocaleError, ValueError):
414
+ return get_group_symbol(DEFAULT_LOCALE)
415
+
416
+
417
+ class TkKwargs(TypedDict, total=False):
418
+ """The following attributes are available per the Tkinter API (not commonly used).
419
+
420
+ Attributes:
421
+ screenName: Sets the display environment variable (X11 only).
422
+ baseName: Name of the profile file. By default, derived from program name.
423
+ className: Name of the widget class
424
+ useTk: If True, initializes the Tk system.
425
+ sync: If true, executes all X server commands synchronously.
426
+ use: The id of the window in which to embed the application.
427
+ """
428
+ screenName: str
429
+ baseName: str
430
+ className: str
431
+ useTk: bool
432
+ sync: bool
433
+ use: str
434
+
435
+
436
+ class App(BaseWindow, WidgetCapabilitiesMixin, tkinter.Tk):
437
+ """The primary application window and entry point.
438
+
439
+ App adds theming, localization, and platform setup on top of `tkinter.Tk`.
440
+
441
+ The standard widget API (events, scheduling, clipboard, geometry managers,
442
+ winfo, etc.) is documented under bootstack capabilities and is available
443
+ on App via inheritance.
444
+ """
445
+
446
+ def __init__(
447
+ self,
448
+ title: str | None = None,
449
+ theme: str | None = None,
450
+ icon: tkinter.PhotoImage | None = None,
451
+
452
+ settings: AppSettings | AppSettingsKwargs | None = None,
453
+ localize: LocalizeMode | None = None,
454
+
455
+ # window settings
456
+ size: tuple[int, int] | None = None,
457
+ position: tuple[int, int] | None = None,
458
+ minsize: tuple[int, int] | None = None,
459
+ maxsize: tuple[int, int] | None = None,
460
+ resizable: tuple[bool, bool] | None = None,
461
+ scaling: float | None = None,
462
+ hdpi: bool = True,
463
+ alpha: float = 1.0,
464
+ transient: object | None = None,
465
+ override_redirect: bool = False,
466
+ window_style: str | None | object = _USE_SETTINGS,
467
+ **kwargs: Unpack[TkKwargs],
468
+ ) -> None:
469
+ """Initializes the application window.
470
+
471
+ Args:
472
+ title: The text to display in the window's title bar. This
473
+ overrides the `app_name` in `settings` if provided.
474
+ theme: The name of the theme to use. This overrides the `theme`
475
+ in `settings` if provided.
476
+ icon: A PhotoImage or file path used for the window's icon.
477
+ If None, the default bootstack.png icon is used.
478
+ settings: A dictionary or `AppSettings` object containing
479
+ application-wide settings. If not provided, default settings
480
+ are used.
481
+ localize: The localization mode for the application. Can be
482
+ 'auto', `True`, or `False`. This overrides the `localize_mode`
483
+ in `settings`.
484
+ size: A tuple specifying the window's initial width and height.
485
+ position: A tuple specifying the window's initial x and y
486
+ coordinates on the screen.
487
+ minsize: A tuple specifying the window's minimum width and height.
488
+ maxsize: A tuple specifying the window's maximum width and height.
489
+ resizable: A tuple of booleans specifying whether the window can
490
+ be resized horizontally and vertically.
491
+ scaling: The DPI scaling factor for the window. If `None`,
492
+ automatic scaling is used.
493
+ hdpi: If `True`, enables high-DPI awareness for the application.
494
+ alpha: The window's transparency level, from 0.0 (fully
495
+ transparent) to 1.0 (fully opaque).
496
+ transient: The parent window for this window.
497
+ override_redirect: If `True`, creates a window without standard
498
+ decorations (title bar, borders, etc.).
499
+ **kwargs: Additional keyword arguments to pass to the
500
+ underlying `tkinter.Tk` constructor.
501
+ """
502
+ # --- Settings ---------------------------------------------------
503
+ if settings is None:
504
+ self.settings = AppSettings()
505
+ elif isinstance(settings, AppSettings):
506
+ self.settings = settings
507
+ else:
508
+ self.settings = AppSettings(**settings)
509
+
510
+ # App-level overrides from ctor
511
+ if theme is not None:
512
+ self.settings.theme = theme
513
+ if title is not None:
514
+ self.settings.app_name = title
515
+
516
+ # If app_name is still None, give it a sensible default
517
+ if self.settings.app_name is None:
518
+ self.settings.app_name = "bootstack"
519
+
520
+ if localize is not None:
521
+ self.settings.localize_mode = localize
522
+
523
+ # --- Window options ---------------------------------------------
524
+ self._size = size
525
+ self._position = position
526
+ self._minsize = minsize
527
+ self._maxsize = maxsize
528
+ self._resizable = resizable
529
+ self._scaling = scaling
530
+ self._hdpi = hdpi
531
+ self._transient = transient
532
+ self._alpha = alpha
533
+ self._override_redirect = override_redirect
534
+
535
+ # Register app
536
+ if not has_current_app():
537
+ set_current_app(self)
538
+
539
+ # Enable HDPI before creating window
540
+ if self._hdpi:
541
+ enable_high_dpi_awareness()
542
+
543
+ # Initialize Tk
544
+ tkinter.Tk.__init__(self, **kwargs)
545
+ self.withdraw() # hide immediately until ready to show.
546
+
547
+ # Setup window system info
548
+ self.winsys: str = self.tk.call('tk', 'windowingsystem')
549
+
550
+ # Apply theme (use resolved settings.theme). If the app opts into
551
+ # following system appearance, override the explicit theme with the
552
+ # mode-appropriate one and bind a listener to track future toggles.
553
+ from bootstack.style.style import set_theme
554
+ initial_theme = self.settings.theme
555
+ if self.settings.follow_system_appearance and self._is_dark_capable_platform():
556
+ initial_theme = (
557
+ self.settings.dark_theme
558
+ if self._system_is_dark()
559
+ else self.settings.light_theme
560
+ )
561
+ set_theme(initial_theme)
562
+
563
+ if self.settings.follow_system_appearance and self._is_dark_capable_platform():
564
+ self._bind_system_appearance_tracking()
565
+
566
+ # Install macOS-native close/quit/hide handlers when requested.
567
+ if self.winsys == 'aqua' and self.settings.macos_quit_behavior == 'native':
568
+ self._install_macos_quit_handlers()
569
+
570
+ # macOS-only polish: sync tk appname so the apple menu's first
571
+ # entry shows the app name (otherwise Tk uses the interpreter's
572
+ # name, typically "Python"), and bind Cmd+W to fire the standard
573
+ # WM_DELETE_WINDOW protocol so the close shortcut behaves like a
574
+ # close-button click.
575
+ if self.winsys == 'aqua':
576
+ if self.settings.app_name:
577
+ try:
578
+ self.tk.call('tk', 'appname', self.settings.app_name)
579
+ except tkinter.TclError:
580
+ pass
581
+ self.bind('<Command-w>', self._trigger_close, add='+')
582
+
583
+ # Initialize the localization bridge so MessageCatalog.translate()
584
+ # and <<LocaleChanged>> are available throughout the app.
585
+ MessageCatalog.init(
586
+ locales_dir=None,
587
+ domain="bootstack",
588
+ default_locale=self.settings.locale or DEFAULT_LOCALE,
589
+ )
590
+
591
+ # Apply HDPI scaling after window creation
592
+ if self._hdpi:
593
+ if self._scaling is None:
594
+ enable_high_dpi_awareness(self, 'auto')
595
+ else:
596
+ enable_high_dpi_awareness(self, self._scaling)
597
+
598
+ # Setup icon
599
+ self._setup_icon(icon, default_icon_enabled=True)
600
+
601
+ # If the app opted into state restoration, override the explicit
602
+ # size/position with whatever was saved last time. The saved geometry
603
+ # is a single 'WxH+X+Y' string applied after _setup_window so it
604
+ # supersedes both kwargs and centering logic.
605
+ saved_geometry = None
606
+ if self.settings.remember_window_state:
607
+ saved_geometry = self._read_saved_geometry()
608
+
609
+ # Setup window using BaseWindow
610
+ # Use window_style from parameter if explicitly provided, otherwise use settings
611
+ _window_style = self.settings.window_style if window_style is _USE_SETTINGS else window_style
612
+ self._setup_window(
613
+ title=self.settings.app_name,
614
+ size=self._size,
615
+ position=self._position,
616
+ minsize=self._minsize,
617
+ maxsize=self._maxsize,
618
+ resizable=self._resizable,
619
+ transient=self._transient,
620
+ overrideredirect=self._override_redirect,
621
+ alpha=self._alpha,
622
+ window_style=_window_style,
623
+ )
624
+
625
+ if saved_geometry is not None:
626
+ self._apply_saved_geometry(saved_geometry)
627
+
628
+ # Apply bootstack-specific bindings
629
+ apply_class_bindings(self)
630
+ apply_all_bindings(self)
631
+
632
+ def mainloop(self, n=0) -> None:
633
+ """Start the application event loop
634
+
635
+ Args:
636
+ n (int): A threshold that keeps the window open if at least n windows is open. This is an archaic c-level
637
+ detail that should not be adjusted unless you have a specific reason.
638
+ """
639
+ self.place_window_center()
640
+ self.show()
641
+ super().mainloop(n=n)
642
+
643
+ def close(self) -> None:
644
+ """Close the application window (destroys the Tk root)"""
645
+ clear_current_app(self)
646
+ self.quit()
647
+
648
+ def destroy(self) -> None:
649
+ """Destroys the window and all its children."""
650
+ if self.settings.remember_window_state:
651
+ try:
652
+ if self.winfo_exists():
653
+ self._save_window_state()
654
+ except tkinter.TclError:
655
+ pass
656
+ clear_current_app(self)
657
+ super().destroy()
658
+
659
+ # ----- Window state persistence ------------------------------------------
660
+
661
+ def _state_file_path(self):
662
+ """Return the path where this App's window state is persisted."""
663
+ from pathlib import Path
664
+ import os
665
+ if self.settings.state_path:
666
+ return Path(self.settings.state_path)
667
+ # Per-platform config dir; leaf includes app_name to avoid collisions
668
+ # between multiple bootstack apps installed on the same machine.
669
+ app_name = self.settings.app_name or 'bootstack'
670
+ if sys.platform == 'darwin':
671
+ base = Path.home() / 'Library' / 'Application Support'
672
+ elif sys.platform == 'win32':
673
+ base = Path(os.environ.get('APPDATA') or (Path.home() / 'AppData' / 'Roaming'))
674
+ else:
675
+ base = Path(os.environ.get('XDG_CONFIG_HOME') or (Path.home() / '.config'))
676
+ return base / app_name / 'window_state.json'
677
+
678
+ def _read_saved_geometry(self):
679
+ """Return the persisted 'WxH+X+Y' string, or None if not present/valid."""
680
+ import json
681
+ path = self._state_file_path()
682
+ try:
683
+ raw = path.read_text()
684
+ except (FileNotFoundError, OSError):
685
+ return None
686
+ try:
687
+ data = json.loads(raw)
688
+ except json.JSONDecodeError:
689
+ return None
690
+ geo = data.get('geometry') if isinstance(data, dict) else None
691
+ return geo if isinstance(geo, str) and geo else None
692
+
693
+ def _apply_saved_geometry(self, geometry: str) -> None:
694
+ """Restore a saved 'WxH+X+Y' string, clamping off-screen positions."""
695
+ try:
696
+ self.geometry(geometry)
697
+ self.update_idletasks()
698
+ except tkinter.TclError:
699
+ return
700
+ # If the saved position is on a now-disconnected monitor, drag it
701
+ # back into a visible region so the window doesn't open invisibly.
702
+ try:
703
+ from bootstack.runtime.window_utilities import WindowPositioning
704
+ x, y = self.winfo_x(), self.winfo_y()
705
+ x, y = WindowPositioning.ensure_on_screen(self, x, y)
706
+ self.geometry(f'+{x}+{y}')
707
+ except Exception:
708
+ pass
709
+
710
+ def _save_window_state(self) -> None:
711
+ """Write the current geometry to the state file."""
712
+ import json
713
+ path = self._state_file_path()
714
+ try:
715
+ path.parent.mkdir(parents=True, exist_ok=True)
716
+ except OSError:
717
+ return
718
+ try:
719
+ geo = self.geometry()
720
+ path.write_text(json.dumps({'geometry': geo}))
721
+ except (OSError, tkinter.TclError):
722
+ pass
723
+
724
+ # ----- System appearance tracking ----------------------------------------
725
+
726
+ def _is_dark_capable_platform(self) -> bool:
727
+ """Return True if the current windowing system reports an appearance.
728
+
729
+ Currently only macOS exposes a Tk-level light/dark signal
730
+ (`<<TkSystemAppearanceChanged>>` and `MacWindowStyle isDark`).
731
+ Win/Linux apps that want to track system theme need their own
732
+ OS-specific hook, which is out of scope for this method.
733
+ """
734
+ return getattr(self, 'winsys', None) == 'aqua'
735
+
736
+ def _system_is_dark(self) -> bool:
737
+ """Return True if the OS is currently in dark mode (macOS)."""
738
+ try:
739
+ return bool(int(self.tk.call(
740
+ '::tk::unsupported::MacWindowStyle', 'isDark', self,
741
+ )))
742
+ except tkinter.TclError:
743
+ return False
744
+
745
+ def _bind_system_appearance_tracking(self) -> None:
746
+ """Switch themes when the OS toggles between light and dark mode."""
747
+ def on_appearance_changed(_event=None):
748
+ if not self.settings.follow_system_appearance:
749
+ return
750
+ from bootstack.style.style import set_theme
751
+ set_theme(
752
+ self.settings.dark_theme
753
+ if self._system_is_dark()
754
+ else self.settings.light_theme
755
+ )
756
+
757
+ try:
758
+ self.bind('<<TkSystemAppearanceChanged>>', on_appearance_changed, add='+')
759
+ except tkinter.TclError:
760
+ pass
761
+
762
+ # ----- macOS Quit/Close conventions --------------------------------------
763
+
764
+ def _install_macos_quit_handlers(self) -> None:
765
+ """Wire macOS-native close/quit/hide gestures.
766
+
767
+ macOS convention: clicking the window close button hides the app
768
+ (it stays in the Dock and Cmd+Tab list) rather than destroying it.
769
+ Cmd+Q (and Dock → Quit) is what actually quits. Cmd+H hides the
770
+ app, and clicking the Dock icon brings the main window back.
771
+
772
+ This method installs the matching Tk handlers so apps that opt
773
+ into `macos_quit_behavior='native'` behave correctly without
774
+ each app duplicating the boilerplate.
775
+ """
776
+ # Close button → withdraw. We replace the protocol unconditionally
777
+ # because the default Tk behavior on close is to destroy, which is
778
+ # wrong on Mac. Apps that want to hook close should call
779
+ # `app.on_close(my_handler)` after construction; that overrides
780
+ # this default.
781
+ self.protocol('WM_DELETE_WINDOW', self.withdraw)
782
+
783
+ # Cmd+Q / Dock → Quit fire <<AppleQuit>>; that's the real quit signal.
784
+ try:
785
+ self.bind('<<AppleQuit>>', lambda _e: self.destroy(), add='+')
786
+ except tkinter.TclError:
787
+ pass
788
+
789
+ # Cmd+H hides the app.
790
+ try:
791
+ self.bind('<<Apple-Hide>>', lambda _e: self.withdraw(), add='+')
792
+ except tkinter.TclError:
793
+ pass
794
+
795
+ # Clicking the Dock icon when no window is visible fires
796
+ # <<Apple-ReopenApplication>>; bring the main window back.
797
+ try:
798
+ self.bind(
799
+ '<<Apple-ReopenApplication>>',
800
+ lambda _e: self.deiconify(),
801
+ add='+',
802
+ )
803
+ except tkinter.TclError:
804
+ pass
805
+
806
+ def _trigger_close(self, _event=None) -> str:
807
+ """Invoke the registered WM_DELETE_WINDOW handler for this window.
808
+
809
+ Lets `Cmd+W` and any other "close this window" gesture flow
810
+ through the same code path as clicking the close button, so a
811
+ custom `app.on_close(handler)` is honored.
812
+ """
813
+ try:
814
+ handler_script = self.tk.call(
815
+ 'wm', 'protocol', self._w, 'WM_DELETE_WINDOW',
816
+ )
817
+ except tkinter.TclError:
818
+ handler_script = ''
819
+ if handler_script:
820
+ try:
821
+ self.tk.eval(handler_script)
822
+ except tkinter.TclError:
823
+ pass
824
+ else:
825
+ # No handler registered — fall back to the platform-correct
826
+ # default for this app: withdraw on native macOS, destroy
827
+ # otherwise.
828
+ if (
829
+ self.winsys == 'aqua'
830
+ and self.settings.macos_quit_behavior == 'native'
831
+ ):
832
+ self.withdraw()
833
+ else:
834
+ self.destroy()
835
+ return 'break'
836
+
837
+ # ----- macOS apple menu hooks --------------------------------------------
838
+
839
+ def on_about(self, handler: Callable[[], Any]) -> None:
840
+ """Register a handler for the macOS "About <App>" menu item.
841
+
842
+ Tk on Aqua calls `::tk::mac::standardAboutPanel` when the user
843
+ picks About from the application menu. This method overrides
844
+ that proc with the supplied Python callable. No-op on Win/Linux,
845
+ where there's no equivalent system menu.
846
+
847
+ Args:
848
+ handler: Zero-argument callable invoked when the user picks
849
+ About from the apple menu.
850
+ """
851
+ if self.winsys != 'aqua':
852
+ return
853
+ try:
854
+ self.tk.createcommand('::tk::mac::standardAboutPanel', handler)
855
+ except tkinter.TclError:
856
+ pass
857
+
858
+ def on_preferences(self, handler: Callable[[], Any]) -> None:
859
+ """Register a handler for the macOS "Preferences…" menu item.
860
+
861
+ Tk on Aqua calls `::tk::mac::ShowPreferences` when the user picks
862
+ Preferences (Cmd+,) from the application menu. This method
863
+ overrides that proc with the supplied Python callable. No-op on
864
+ Win/Linux.
865
+
866
+ Args:
867
+ handler: Zero-argument callable invoked when the user picks
868
+ Preferences from the apple menu.
869
+ """
870
+ if self.winsys != 'aqua':
871
+ return
872
+ try:
873
+ self.tk.createcommand('::tk::mac::ShowPreferences', handler)
874
+ except tkinter.TclError:
875
+ pass
876
+
877
+
878
+ # Backward compatibility alias
879
+ Window = App