bootstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. bootstack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,930 @@
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.i18n.intl_format import detect_locale
21
+ from bootstack.i18n.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
+
30
+ def set_current_app(app: App) -> None:
31
+ """Set the process-wide current App instance.
32
+
33
+ Intended to be called from App.__init__ for the first app created.
34
+ """
35
+ global _current_app
36
+ _current_app = app
37
+
38
+
39
+ def get_app_settings() -> AppSettings:
40
+ """Return the settings for current App.
41
+
42
+ Returns:
43
+ The AppSettings instance for the current application.
44
+
45
+ Raises:
46
+ RuntimeError: If no active App instance is set.
47
+ """
48
+ return get_current_app().settings
49
+
50
+
51
+ def clear_current_app(app: App) -> None:
52
+ """Clear the current app reference if it matches the given app."""
53
+ global _current_app
54
+ if _current_app is app:
55
+ _current_app = None
56
+
57
+
58
+ def get_current_app() -> App:
59
+ """Return the current App instance.
60
+
61
+ Returns:
62
+ The currently active App instance.
63
+
64
+ Raises:
65
+ RuntimeError: If no App has been registered yet.
66
+ """
67
+ if _current_app is None:
68
+ raise RuntimeError(
69
+ "No current App instance is set. "
70
+ "Create an App first, e.g. `app = App()`."
71
+ )
72
+ return _current_app
73
+
74
+
75
+ def has_current_app() -> bool:
76
+ """Check if a current App instance is registered.
77
+
78
+ Returns:
79
+ True if an App instance exists, False otherwise.
80
+ """
81
+ return _current_app is not None
82
+
83
+
84
+ def get_default_root(what: Optional[str] = None) -> tkinter.Tk:
85
+ """Get the default Tk root window.
86
+
87
+ Returns the default root if it has been created, otherwise
88
+ creates and returns a new instance.
89
+
90
+ Args:
91
+ what: Optional description of the operation requiring the root,
92
+ used in error messages if called too early.
93
+
94
+ Returns:
95
+ The default Tk root window instance.
96
+
97
+ Raises:
98
+ RuntimeError: If tkinter is configured to not support default root,
99
+ or if called too early with a 'what' description.
100
+ """
101
+ if not tkinter._support_default_root:
102
+ raise RuntimeError(
103
+ "No master specified and tkinter is "
104
+ "configured to not support default root")
105
+ if not tkinter._default_root:
106
+ if what:
107
+ raise RuntimeError(f"Too early to {what}: no default root window")
108
+ root = tkinter.Tk()
109
+ assert tkinter._default_root is root
110
+ return tkinter._default_root
111
+
112
+
113
+ def apply_class_bindings(window: tkinter.Widget | App) -> None:
114
+ """Add class level event bindings in application"""
115
+ # Copy TCheckbutton bindings to Toolbutton class
116
+ # This is needed because widgets using class_='Toolbutton' have their
117
+ # bindtags reference 'Toolbutton' instead of 'TCheckbutton', so they
118
+ # need the same mouse/keyboard bindings copied over.
119
+ for event in ('<Button-1>', '<ButtonRelease-1>', '<B1-Leave>', '<B1-Enter>',
120
+ '<Enter>', '<Leave>', '<Key-space>', '<<Invoke>>'):
121
+ binding = window.bind_class('TCheckbutton', event)
122
+ if binding:
123
+ window.bind_class('Toolbutton', event, binding)
124
+ window.bind_class('ButtonGroup', event, binding)
125
+
126
+ for className in ["TEntry", "TSpinbox", "TCombobox", "Text"]:
127
+ window.bind_class(
128
+ className=className,
129
+ sequence="<Configure>",
130
+ func=on_disabled_readonly_state,
131
+ add="+")
132
+
133
+ for sequence in ["<Control-a>", "<Control-A>"]:
134
+ window.bind_class(
135
+ className=className,
136
+ sequence=sequence,
137
+ func=on_select_all)
138
+
139
+ window.unbind_class("TButton", "<Key-space>")
140
+
141
+ def button_default_binding(event: tkinter.Event) -> None:
142
+ """The default keybind on a button when the return or enter key
143
+ is pressed and the button has focus or is the default button."""
144
+ try:
145
+ widget = window.nametowidget(event.widget)
146
+ widget.invoke()
147
+ except KeyError:
148
+ window.tk.call(event.widget, 'invoke')
149
+
150
+ window.bind_class(
151
+ "TButton", "<Key-Return>", button_default_binding,
152
+ add="+")
153
+ window.bind_class("TButton", "<KP_Enter>", button_default_binding, add="+")
154
+
155
+
156
+ def apply_all_bindings(window: tkinter.Widget | App) -> None:
157
+ """Add bindings to all widgets in the application"""
158
+ window.bind_all('<Map>', on_map_child, '+')
159
+ window.bind_all('<Destroy>', lambda e: Publisher.unsubscribe(e.widget))
160
+
161
+
162
+ def on_disabled_readonly_state(event: tkinter.Event) -> None:
163
+ """Change the cursor of entry type widgets to 'arrow' if in a
164
+ disabled or readonly state."""
165
+ try:
166
+ widget = event.widget
167
+ state = str(widget.cget('state'))
168
+ cursor = str(widget.cget('cursor'))
169
+ if state in (DISABLED, READONLY):
170
+ if cursor == 'arrow':
171
+ return
172
+ else:
173
+ widget['cursor'] = 'arrow'
174
+ else:
175
+ if cursor in ('ibeam', ''):
176
+ return
177
+ else:
178
+ widget['cursor'] = None
179
+ except:
180
+ pass
181
+
182
+
183
+ def on_map_child(event: tkinter.Event) -> None:
184
+ """Callback for <Map> event which generates a <<MapChild>> virtual
185
+ event on the parent"""
186
+ widget: tkinter.Widget = event.widget
187
+ try:
188
+ if widget.master is None: # root widget
189
+ return
190
+ else:
191
+ widget.master.event_generate('<<MapChild>>')
192
+ except:
193
+ # not a tkinter widget that I'm handling (ex. Combobox.popdown)
194
+ return
195
+
196
+
197
+ def on_select_all(event: tkinter.Event) -> None:
198
+ """Callback to select all text in Entry or Text widget when Ctrl+A is pressed."""
199
+ widget = event.widget
200
+
201
+ if isinstance(widget, tkinter.Text):
202
+ widget.tag_add(SEL, "1.0", END)
203
+ widget.mark_set(INSERT, END)
204
+ widget.see(INSERT)
205
+ elif isinstance(widget, tkinter.Entry):
206
+ widget.selection_range(0, END)
207
+ widget.icursor(END)
208
+
209
+
210
+ LocalizeMode = Union[bool, Literal['auto']]
211
+
212
+
213
+ @dataclass
214
+ class AppSettings:
215
+ """Internal resolved-configuration holder for an `App`.
216
+
217
+ This is an INTERNAL detail — it is not part of the public API. Users
218
+ configure an app through flat `App(...)` constructor kwargs and read or
219
+ write configuration through `app.*` properties; `App.__init__` assembles
220
+ those kwargs into one of these instances. It holds configuration for
221
+ theming, localization, and application metadata, and is automatically
222
+ populated with sensible locale-based defaults in `__post_init__`.
223
+
224
+ Attributes:
225
+ app_name: The application name displayed in the title bar.
226
+ theme: The current theme name ('light', 'dark', or a specific theme).
227
+ light_theme: The theme to use when `theme='light'`.
228
+ dark_theme: The theme to use when `theme='dark'`.
229
+ follow_system_appearance: If True, automatically switch between
230
+ `light_theme` and `dark_theme` to match the OS appearance and
231
+ track changes at runtime. Currently effective on macOS, where
232
+ Tk fires `<<TkSystemAppearanceChanged>>` and exposes the
233
+ current mode via `tk::unsupported::MacWindowStyle isDark`.
234
+ Defaults to False so existing apps that pin a theme keep
235
+ doing so.
236
+ available_themes: Sequence of available theme names.
237
+ locale: The locale identifier (e.g., 'en_US', 'de_DE'). Auto-detected
238
+ from system if not specified.
239
+ language: The base language code (e.g., 'en', 'de'). Derived from
240
+ locale if not specified.
241
+ date_format: The date format pattern. Derived from locale if not
242
+ specified (e.g., 'M/d/yy' for en_US).
243
+ time_format: The time format pattern. Derived from locale if not
244
+ specified (e.g., 'h:mm a' for en_US).
245
+ number_decimal: The decimal separator character. Derived from locale
246
+ if not specified (e.g., '.' for en_US).
247
+ number_thousands: The thousands separator character. Derived from
248
+ locale if not specified (e.g., ',' for en_US).
249
+ localize_mode: Controls localization behavior. 'auto' enables
250
+ localization based on locale, True always enables, False disables.
251
+ window_style: Windows-only pywinstyles effect for all windows.
252
+ Options include 'mica', 'acrylic', 'aero', 'transparent', 'win7'.
253
+ Defaults to 'mica'. Set to None to disable.
254
+ macos_quit_behavior: How the close button and Cmd+Q behave on macOS.
255
+ 'native' (default) follows Mac convention: clicking the window
256
+ close button or Cmd+H hides the app (withdraws), clicking the
257
+ dock icon reshows it, and Cmd+Q (or Dock → Quit) actually
258
+ destroys it. 'classic' restores the cross-platform behavior
259
+ where the close button destroys the window. No-op on Win/Linux.
260
+ remember_window_state: If True, the App's geometry (size + position)
261
+ is saved on close and restored on next launch, with off-screen
262
+ positions clamped back into a visible monitor. Off by default
263
+ so existing apps that pin a size/position keep doing so.
264
+ state_path: Optional override for where window state is stored.
265
+ When None, defaults to a per-app file under the OS config
266
+ directory (Library/Application Support on macOS, %APPDATA% on
267
+ Windows, $XDG_CONFIG_HOME on Linux). The leaf filename includes
268
+ `app_name` so multiple bootstack apps don't collide.
269
+ """
270
+ # information
271
+ app_name: str | None = None
272
+
273
+ # theme
274
+ theme: str = "light"
275
+ light_theme: str = "bootstrap-light"
276
+ dark_theme: str = "bootstrap-dark"
277
+ follow_system_appearance: bool = False
278
+ available_themes: Sequence[str] = ()
279
+
280
+ # internationalization
281
+ locale: str | None = None
282
+ language: str | None = None
283
+ date_format: str | None = None
284
+ time_format: str | None = None
285
+ number_decimal: str | None = None
286
+ number_thousands: str | None = None
287
+
288
+ # localization behavior
289
+ localize_mode: LocalizeMode = "auto"
290
+
291
+ # platform-specific
292
+ window_style: str | None = 'mica'
293
+ macos_quit_behavior: str = 'native'
294
+
295
+ # window state persistence
296
+ remember_window_state: bool = False
297
+ state_path: str | None = None
298
+
299
+ def __post_init__(self):
300
+ """Populate localization defaults when not explicitly configured."""
301
+ _apply_localization_defaults(self)
302
+
303
+
304
+ DEFAULT_LOCALE = "en_US"
305
+
306
+
307
+ def _apply_localization_defaults(settings: AppSettings) -> None:
308
+ """Ensure locale-based fields always have meaningful defaults."""
309
+ locale_code = settings.locale or detect_locale(DEFAULT_LOCALE)
310
+ settings.locale = locale_code
311
+
312
+ if settings.language is None:
313
+ settings.language = _language_from_locale(locale_code)
314
+
315
+ if settings.date_format is None:
316
+ settings.date_format = _safe_date_format(locale_code)
317
+
318
+ if settings.time_format is None:
319
+ settings.time_format = _safe_time_format(locale_code)
320
+
321
+ if settings.number_decimal is None:
322
+ settings.number_decimal = _safe_decimal_symbol(locale_code)
323
+
324
+ if settings.number_thousands is None:
325
+ settings.number_thousands = _safe_group_symbol(locale_code)
326
+
327
+
328
+ def _language_from_locale(locale_code: str) -> str:
329
+ """Return the base language (e.g., en_US -> en)."""
330
+ base = locale_code.split("_", 1)[0]
331
+ return base.lower() if base else locale_code
332
+
333
+
334
+ def _safe_date_format(locale_code: str) -> str:
335
+ try:
336
+ return str(get_date_format("short", locale=locale_code))
337
+ except (UnknownLocaleError, ValueError):
338
+ return str(get_date_format("short", locale=DEFAULT_LOCALE))
339
+
340
+
341
+ def _safe_time_format(locale_code: str) -> str:
342
+ try:
343
+ return str(get_time_format("short", locale=locale_code))
344
+ except (UnknownLocaleError, ValueError):
345
+ return str(get_time_format("short", locale=DEFAULT_LOCALE))
346
+
347
+
348
+ def _safe_decimal_symbol(locale_code: str) -> str:
349
+ try:
350
+ return get_decimal_symbol(locale_code)
351
+ except (UnknownLocaleError, ValueError):
352
+ return get_decimal_symbol(DEFAULT_LOCALE)
353
+
354
+
355
+ def _safe_group_symbol(locale_code: str) -> str:
356
+ try:
357
+ return get_group_symbol(locale_code)
358
+ except (UnknownLocaleError, ValueError):
359
+ return get_group_symbol(DEFAULT_LOCALE)
360
+
361
+
362
+ class TkKwargs(TypedDict, total=False):
363
+ """The following attributes are available per the Tkinter API (not commonly used).
364
+
365
+ Attributes:
366
+ screenName: Sets the display environment variable (X11 only).
367
+ baseName: Name of the profile file. By default, derived from program name.
368
+ className: Name of the widget class
369
+ useTk: If True, initializes the Tk system.
370
+ sync: If true, executes all X server commands synchronously.
371
+ use: The id of the window in which to embed the application.
372
+ """
373
+ screenName: str
374
+ baseName: str
375
+ className: str
376
+ useTk: bool
377
+ sync: bool
378
+ use: str
379
+
380
+
381
+ class App(BaseWindow, WidgetCapabilitiesMixin, tkinter.Tk):
382
+ """The primary application window and entry point.
383
+
384
+ App adds theming, localization, and platform setup on top of `tkinter.Tk`.
385
+
386
+ The standard widget API (events, scheduling, clipboard, geometry managers,
387
+ winfo, etc.) is documented under bootstack capabilities and is available
388
+ on App via inheritance.
389
+ """
390
+
391
+ def __init__(
392
+ self,
393
+ title: str | None = None,
394
+ *,
395
+ theme: str | None = None,
396
+ icon: tkinter.PhotoImage | str | None = None,
397
+
398
+ # theme
399
+ light_theme: str = "bootstrap-light",
400
+ dark_theme: str = "bootstrap-dark",
401
+ follow_system_appearance: bool = False,
402
+ available_themes: Sequence[str] = (),
403
+
404
+ # localization
405
+ locale: str | None = None,
406
+ localize_mode: LocalizeMode = "auto",
407
+
408
+ # platform
409
+ window_style: str | None = "mica",
410
+ macos_quit_behavior: str = "native",
411
+
412
+ # window-state persistence
413
+ remember_window_state: bool = False,
414
+ state_path: str | None = None,
415
+
416
+ # window settings
417
+ size: tuple[int, int] | None = None,
418
+ position: tuple[int, int] | None = None,
419
+ minsize: tuple[int, int] | None = None,
420
+ maxsize: tuple[int, int] | None = None,
421
+ resizable: tuple[bool, bool] | None = None,
422
+ scaling: float | None = None,
423
+ hdpi: bool = True,
424
+ alpha: float = 1.0,
425
+ transient: object | None = None,
426
+ override_redirect: bool = False,
427
+ center_on_screen: bool = True,
428
+ on_close: Callable | None = None,
429
+ **kwargs: Unpack[TkKwargs],
430
+ ) -> None:
431
+ """Initializes the application window.
432
+
433
+ Args:
434
+ title: The text to display in the window's title bar, and the
435
+ application name used for config-directory and taskbar
436
+ identity. Defaults to `'bootstack'`.
437
+ theme: The name of the theme to use on startup (e.g.
438
+ `'bootstrap-dark'`). Defaults to the built-in light theme.
439
+ icon: A PhotoImage or file path used for the window's icon.
440
+ If None, the default bootstack.png icon is used.
441
+ light_theme: Theme used when following system appearance and the
442
+ OS is in light mode, and as the light end of `toggle_theme`.
443
+ dark_theme: Theme used when following system appearance and the
444
+ OS is in dark mode, and as the dark end of `toggle_theme`.
445
+ follow_system_appearance: If True, switch between `light_theme`
446
+ and `dark_theme` to match the OS and track changes at runtime
447
+ (currently effective on macOS).
448
+ available_themes: Sequence of theme names to expose to theme
449
+ pickers. Empty means all registered themes.
450
+ locale: Locale identifier (e.g. `'en_US'`, `'de_DE'`).
451
+ Auto-detected from the system when not given.
452
+ localize_mode: Controls localization behavior. `'auto'` enables
453
+ localization based on locale, `True` always enables, `False`
454
+ disables.
455
+ window_style: Windows-only effect for all windows (`'mica'`,
456
+ `'acrylic'`, `'aero'`, `'transparent'`, `'win7'`). Defaults
457
+ to `'mica'`; set to None to disable.
458
+ macos_quit_behavior: How close/Cmd+Q behave on macOS, `'native'`
459
+ (default) or `'classic'`. No-op on Win/Linux.
460
+ remember_window_state: If True, the window geometry is saved on
461
+ close and restored on next launch.
462
+ state_path: Optional override for where window state is stored.
463
+ size: A tuple specifying the window's initial width and height.
464
+ position: A tuple specifying the window's initial x and y
465
+ coordinates on the screen.
466
+ minsize: A tuple specifying the window's minimum width and height.
467
+ maxsize: A tuple specifying the window's maximum width and height.
468
+ resizable: A tuple of booleans specifying whether the window can
469
+ be resized horizontally and vertically.
470
+ scaling: The DPI scaling factor for the window. If `None`,
471
+ automatic scaling is used.
472
+ hdpi: If `True`, enables high-DPI awareness for the application.
473
+ alpha: The window's transparency level, from 0.0 (fully
474
+ transparent) to 1.0 (fully opaque).
475
+ transient: The parent window for this window.
476
+ override_redirect: If `True`, creates a window without standard
477
+ decorations (title bar, borders, etc.).
478
+ center_on_screen: If True (default), center the app window on
479
+ the screen when `mainloop()` starts.
480
+ on_close: Callback invoked when the user clicks the close button.
481
+ Return `False` to veto the close; return `None` or `True`
482
+ to allow it. Equivalent to calling `add_close_handler(fn)`
483
+ after construction.
484
+ **kwargs: Additional keyword arguments to pass to the
485
+ underlying `tkinter.Tk` constructor.
486
+ """
487
+ # --- Settings ---------------------------------------------------
488
+ # Flat constructor kwargs are the single configuration path; they are
489
+ # assembled into an internal AppSettings holder (which derives locale
490
+ # formats in __post_init__). There is no public AppSettings / settings=.
491
+ self.settings = AppSettings(
492
+ app_name=title,
493
+ theme=theme if theme is not None else "light",
494
+ light_theme=light_theme,
495
+ dark_theme=dark_theme,
496
+ follow_system_appearance=follow_system_appearance,
497
+ available_themes=available_themes,
498
+ locale=locale,
499
+ localize_mode=localize_mode,
500
+ window_style=window_style,
501
+ macos_quit_behavior=macos_quit_behavior,
502
+ remember_window_state=remember_window_state,
503
+ state_path=state_path,
504
+ )
505
+
506
+ # If no title/app_name was given, fall back to a sensible default.
507
+ if self.settings.app_name is None:
508
+ self.settings.app_name = "bootstack"
509
+
510
+ # --- Window options ---------------------------------------------
511
+ self._size = size
512
+ self._position = position
513
+ self._minsize = minsize
514
+ self._maxsize = maxsize
515
+ self._resizable = resizable
516
+ self._scaling = scaling
517
+ self._hdpi = hdpi
518
+ self._transient = transient
519
+ self._alpha = alpha
520
+ self._override_redirect = override_redirect
521
+ self._center_on_screen = center_on_screen
522
+ self._on_close_kwarg = on_close
523
+
524
+ # Register app
525
+ if not has_current_app():
526
+ set_current_app(self)
527
+
528
+ # Enable HDPI before creating window
529
+ if self._hdpi:
530
+ enable_high_dpi_awareness()
531
+
532
+ # Tell Windows this is a distinct app so the taskbar uses our icon
533
+ # instead of the generic python.exe icon.
534
+ if sys.platform == 'win32':
535
+ try:
536
+ import ctypes
537
+ app_id = f"bootstack.{(self.settings.app_name or 'app').lower().replace(' ', '.')}"
538
+ ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
539
+ except Exception:
540
+ pass
541
+
542
+ # Initialize Tk
543
+ tkinter.Tk.__init__(self, **kwargs)
544
+ self.withdraw() # hide immediately until ready to show.
545
+
546
+ # Setup window system info
547
+ self.winsys: str = self.tk.call('tk', 'windowingsystem')
548
+
549
+ # Apply theme (use resolved settings.theme). If the app opts into
550
+ # following system appearance, override the explicit theme with the
551
+ # mode-appropriate one and bind a listener to track future toggles.
552
+ from bootstack.style.style import set_theme
553
+ initial_theme = self.settings.theme
554
+ if self.settings.follow_system_appearance and self._is_dark_capable_platform():
555
+ initial_theme = (
556
+ self.settings.dark_theme
557
+ if self._system_is_dark()
558
+ else self.settings.light_theme
559
+ )
560
+ set_theme(initial_theme)
561
+
562
+ if self.settings.follow_system_appearance and self._is_dark_capable_platform():
563
+ self._bind_system_appearance_tracking()
564
+
565
+ # Install macOS-native close/quit/hide handlers when requested.
566
+ if self.winsys == 'aqua' and self.settings.macos_quit_behavior == 'native':
567
+ self._install_macos_quit_handlers()
568
+
569
+ # macOS-only polish: sync tk appname so the apple menu's first
570
+ # entry shows the app name (otherwise Tk uses the interpreter's
571
+ # name, typically "Python"), and bind Cmd+W to fire the standard
572
+ # WM_DELETE_WINDOW protocol so the close shortcut behaves like a
573
+ # close-button click.
574
+ if self.winsys == 'aqua':
575
+ if self.settings.app_name:
576
+ try:
577
+ self.tk.call('tk', 'appname', self.settings.app_name)
578
+ except tkinter.TclError:
579
+ pass
580
+ self.bind('<Command-w>', self._trigger_close, add='+')
581
+
582
+ # Initialize the localization bridge so MessageCatalog.translate()
583
+ # and <<LocaleChanged>> are available throughout the app.
584
+ MessageCatalog.init(
585
+ locales_dir=None,
586
+ domain="bootstack",
587
+ default_locale=self.settings.locale or DEFAULT_LOCALE,
588
+ )
589
+
590
+ # Apply HDPI scaling after window creation
591
+ if self._hdpi:
592
+ if self._scaling is None:
593
+ enable_high_dpi_awareness(self, 'auto')
594
+ else:
595
+ enable_high_dpi_awareness(self, self._scaling)
596
+
597
+ # Setup icon
598
+ self._setup_icon(icon, default_icon_enabled=True)
599
+
600
+ # If the app opted into state restoration, override the explicit
601
+ # size/position with whatever was saved last time. The saved geometry
602
+ # is a single 'WxH+X+Y' string applied after _setup_window so it
603
+ # supersedes both kwargs and centering logic.
604
+ saved_geometry = None
605
+ if self.settings.remember_window_state:
606
+ saved_geometry = self._read_saved_geometry()
607
+
608
+ # Setup window using BaseWindow
609
+ self._setup_window(
610
+ title=self.settings.app_name,
611
+ size=self._size,
612
+ position=self._position,
613
+ minsize=self._minsize,
614
+ maxsize=self._maxsize,
615
+ resizable=self._resizable,
616
+ transient=self._transient,
617
+ overrideredirect=self._override_redirect,
618
+ alpha=self._alpha,
619
+ window_style=self.settings.window_style,
620
+ )
621
+
622
+ if saved_geometry is not None:
623
+ self._apply_saved_geometry(saved_geometry)
624
+
625
+ # Apply bootstack-specific bindings
626
+ apply_class_bindings(self)
627
+ apply_all_bindings(self)
628
+
629
+ # Register on_close= kwarg handler after all other setup is complete
630
+ if self._on_close_kwarg is not None:
631
+ self.add_close_handler(self._on_close_kwarg)
632
+
633
+ # A registered intro splash (bs.Splash), or None. Set by the splash on
634
+ # construction; read at reveal time to defer the app's own appearance.
635
+ self._splash: Any = None
636
+
637
+ def mainloop(self, n=0) -> None:
638
+ """Start the application event loop
639
+
640
+ Args:
641
+ n (int): A threshold that keeps the window open if at least n windows is open. This is an archaic c-level
642
+ detail that should not be adjusted unless you have a specific reason.
643
+ """
644
+ if self._center_on_screen:
645
+ self.place_window_center()
646
+ splash = getattr(self, "_splash", None)
647
+ if splash is not None and splash.is_showing:
648
+ # An intro splash is up — defer the app's reveal until it dismisses,
649
+ # then reveal via this callback (see Splash._notify_app_ready).
650
+ splash._notify_app_ready(self.show)
651
+ else:
652
+ self.show()
653
+ super().mainloop(n=n)
654
+
655
+ def _do_close(self) -> None:
656
+ """Default close action for App.
657
+
658
+ On macOS with native quit behavior, hiding the window (withdraw) is the
659
+ correct action when the close button is clicked — the app stays in the
660
+ Dock and can be re-opened. On all other platforms (and macOS without
661
+ native behavior), the window is destroyed.
662
+ """
663
+ if (
664
+ getattr(self, 'winsys', None) == 'aqua'
665
+ and getattr(self.settings, 'macos_quit_behavior', None) == 'native'
666
+ ):
667
+ self.withdraw()
668
+ else:
669
+ try:
670
+ self.destroy()
671
+ except tkinter.TclError:
672
+ pass
673
+
674
+ def close(self) -> None:
675
+ """Close the application window (destroys the Tk root)"""
676
+ clear_current_app(self)
677
+ self.quit()
678
+
679
+ def destroy(self) -> None:
680
+ """Destroys the window and all its children."""
681
+ if self.settings.remember_window_state:
682
+ try:
683
+ if self.winfo_exists():
684
+ self._save_window_state()
685
+ except tkinter.TclError:
686
+ pass
687
+ clear_current_app(self)
688
+ # Drop every process-wide cache that holds a Tk object bound to THIS
689
+ # root's interpreter — the Style singleton, the named/derived font
690
+ # caches, the nine-patch PhotoImage cache, and the visual-focus root
691
+ # ref. This must happen *before* super().destroy(): those cached
692
+ # PhotoImage/TkFont objects run `image delete`/`font delete` in their
693
+ # finalizers, and if they outlive the interpreter that owns them the
694
+ # call lands on a foreign/dead interpreter — a native crash in ttk
695
+ # element_create on the *next* root (the C layer, so Python can't catch
696
+ # it). Releasing + gc.collect() while this root is still alive lets the
697
+ # finalizers run cleanly. Only when no other App is live (single-App is
698
+ # the norm; this also lets the GUI test suite run module-after-module in
699
+ # one process, and fixes reopen-an-app-after-close in production).
700
+ if not has_current_app():
701
+ try:
702
+ import gc
703
+ from bootstack.style.style import reset_style
704
+ from bootstack.style.typography import Typography
705
+ from bootstack._core.images import _ImageService
706
+ from bootstack._runtime.visual_focus import reset_visual_focus_root
707
+ from bootstack.signals.signal import reset_realized_signals
708
+ reset_style()
709
+ Typography.reset()
710
+ _ImageService.clear_cache()
711
+ reset_visual_focus_root()
712
+ reset_realized_signals()
713
+ gc.collect()
714
+ except Exception:
715
+ pass
716
+ super().destroy()
717
+
718
+ # ----- Window state persistence ------------------------------------------
719
+
720
+ def _state_file_path(self):
721
+ """Return the path where this App's window state is persisted."""
722
+ from pathlib import Path
723
+ from bootstack._core.paths import app_config_file
724
+ if self.settings.state_path:
725
+ return Path(self.settings.state_path)
726
+ # Per-platform config dir; leaf includes app_name to avoid collisions
727
+ # between multiple bootstack apps installed on the same machine.
728
+ return app_config_file('window_state.json', self.settings.app_name)
729
+
730
+ def _read_saved_geometry(self):
731
+ """Return the persisted 'WxH+X+Y' string, or None if not present/valid."""
732
+ import json
733
+ path = self._state_file_path()
734
+ try:
735
+ raw = path.read_text()
736
+ except (FileNotFoundError, OSError):
737
+ return None
738
+ try:
739
+ data = json.loads(raw)
740
+ except json.JSONDecodeError:
741
+ return None
742
+ geo = data.get('geometry') if isinstance(data, dict) else None
743
+ return geo if isinstance(geo, str) and geo else None
744
+
745
+ def _apply_saved_geometry(self, geometry: str) -> None:
746
+ """Restore a saved 'WxH+X+Y' string, clamping off-screen positions."""
747
+ try:
748
+ self.geometry(geometry)
749
+ self.update_idletasks()
750
+ except tkinter.TclError:
751
+ return
752
+ # If the saved position is on a now-disconnected monitor, drag it
753
+ # back into a visible region so the window doesn't open invisibly.
754
+ try:
755
+ from bootstack._runtime.window_utilities import WindowPositioning
756
+ x, y = self.winfo_x(), self.winfo_y()
757
+ x, y = WindowPositioning.ensure_on_screen(self, x, y)
758
+ self.geometry(f'+{x}+{y}')
759
+ except Exception:
760
+ pass
761
+
762
+ def _save_window_state(self) -> None:
763
+ """Write the current geometry to the state file."""
764
+ import json
765
+ path = self._state_file_path()
766
+ try:
767
+ path.parent.mkdir(parents=True, exist_ok=True)
768
+ except OSError:
769
+ return
770
+ try:
771
+ geo = self.geometry()
772
+ path.write_text(json.dumps({'geometry': geo}))
773
+ except (OSError, tkinter.TclError):
774
+ pass
775
+
776
+ # ----- System appearance tracking ----------------------------------------
777
+
778
+ def _is_dark_capable_platform(self) -> bool:
779
+ """Return True if the current windowing system reports an appearance.
780
+
781
+ Currently only macOS exposes a Tk-level light/dark signal
782
+ (`<<TkSystemAppearanceChanged>>` and `MacWindowStyle isDark`).
783
+ Win/Linux apps that want to track system theme need their own
784
+ OS-specific hook, which is out of scope for this method.
785
+ """
786
+ return getattr(self, 'winsys', None) == 'aqua'
787
+
788
+ def _system_is_dark(self) -> bool:
789
+ """Return True if the OS is currently in dark mode (macOS)."""
790
+ try:
791
+ return bool(int(self.tk.call(
792
+ '::tk::unsupported::MacWindowStyle', 'isDark', self,
793
+ )))
794
+ except tkinter.TclError:
795
+ return False
796
+
797
+ def _bind_system_appearance_tracking(self) -> None:
798
+ """Switch themes when the OS toggles between light and dark mode."""
799
+ def on_appearance_changed(_event=None):
800
+ if not self.settings.follow_system_appearance:
801
+ return
802
+ from bootstack.style.style import set_theme
803
+ set_theme(
804
+ self.settings.dark_theme
805
+ if self._system_is_dark()
806
+ else self.settings.light_theme
807
+ )
808
+
809
+ try:
810
+ self.bind('<<TkSystemAppearanceChanged>>', on_appearance_changed, add='+')
811
+ except tkinter.TclError:
812
+ pass
813
+
814
+ # ----- macOS Quit/Close conventions --------------------------------------
815
+
816
+ def _install_macos_quit_handlers(self) -> None:
817
+ """Wire macOS-native close/quit/hide gestures.
818
+
819
+ macOS convention: clicking the window close button hides the app
820
+ (it stays in the Dock and Cmd+Tab list) rather than destroying it.
821
+ Cmd+Q (and Dock → Quit) is what actually quits. Cmd+H hides the
822
+ app, and clicking the Dock icon brings the main window back.
823
+
824
+ This method installs the matching Tk handlers so apps that opt
825
+ into `macos_quit_behavior='native'` behave correctly without
826
+ each app duplicating the boilerplate.
827
+ """
828
+ # Close button → _do_close() (which withdraws on macOS native).
829
+ # Initialise the handler chain so that any later add_close_handler()
830
+ # calls stack on top of the dispatch mechanism rather than overwriting
831
+ # the protocol registration.
832
+ self._init_close_handlers()
833
+
834
+ # Cmd+Q / Dock → Quit fire <<AppleQuit>>; that's the real quit signal.
835
+ try:
836
+ self.bind('<<AppleQuit>>', lambda _e: self.destroy(), add='+')
837
+ except tkinter.TclError:
838
+ pass
839
+
840
+ # Cmd+H hides the app.
841
+ try:
842
+ self.bind('<<Apple-Hide>>', lambda _e: self.withdraw(), add='+')
843
+ except tkinter.TclError:
844
+ pass
845
+
846
+ # Clicking the Dock icon when no window is visible fires
847
+ # <<Apple-ReopenApplication>>; bring the main window back.
848
+ try:
849
+ self.bind(
850
+ '<<Apple-ReopenApplication>>',
851
+ lambda _e: self.deiconify(),
852
+ add='+',
853
+ )
854
+ except tkinter.TclError:
855
+ pass
856
+
857
+ def _trigger_close(self, _event=None) -> str:
858
+ """Invoke the registered WM_DELETE_WINDOW handler for this window.
859
+
860
+ Lets `Cmd+W` and any other "close this window" gesture flow
861
+ through the same code path as clicking the close button, so a
862
+ custom `app.on_close(handler)` is honored.
863
+ """
864
+ try:
865
+ handler_script = self.tk.call(
866
+ 'wm', 'protocol', self._w, 'WM_DELETE_WINDOW',
867
+ )
868
+ except tkinter.TclError:
869
+ handler_script = ''
870
+ if handler_script:
871
+ try:
872
+ self.tk.eval(handler_script)
873
+ except tkinter.TclError:
874
+ pass
875
+ else:
876
+ # No handler registered — fall back to the platform-correct
877
+ # default for this app: withdraw on native macOS, destroy
878
+ # otherwise.
879
+ if (
880
+ self.winsys == 'aqua'
881
+ and self.settings.macos_quit_behavior == 'native'
882
+ ):
883
+ self.withdraw()
884
+ else:
885
+ self.destroy()
886
+ return 'break'
887
+
888
+ # ----- macOS apple menu hooks --------------------------------------------
889
+
890
+ def on_about(self, handler: Callable[[], Any]) -> None:
891
+ """Register a handler for the macOS "About <App>" menu item.
892
+
893
+ Tk on Aqua calls `::tk::mac::standardAboutPanel` when the user
894
+ picks About from the application menu. This method overrides
895
+ that proc with the supplied Python callable. No-op on Win/Linux,
896
+ where there's no equivalent system menu.
897
+
898
+ Args:
899
+ handler: Zero-argument callable invoked when the user picks
900
+ About from the apple menu.
901
+ """
902
+ if self.winsys != 'aqua':
903
+ return
904
+ try:
905
+ self.tk.createcommand('::tk::mac::standardAboutPanel', handler)
906
+ except tkinter.TclError:
907
+ pass
908
+
909
+ def on_preferences(self, handler: Callable[[], Any]) -> None:
910
+ """Register a handler for the macOS "Preferences…" menu item.
911
+
912
+ Tk on Aqua calls `::tk::mac::ShowPreferences` when the user picks
913
+ Preferences (Cmd+,) from the application menu. This method
914
+ overrides that proc with the supplied Python callable. No-op on
915
+ Win/Linux.
916
+
917
+ Args:
918
+ handler: Zero-argument callable invoked when the user picks
919
+ Preferences from the apple menu.
920
+ """
921
+ if self.winsys != 'aqua':
922
+ return
923
+ try:
924
+ self.tk.createcommand('::tk::mac::ShowPreferences', handler)
925
+ except tkinter.TclError:
926
+ pass
927
+
928
+
929
+ # Backward compatibility alias
930
+ Window = App