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,201 @@
1
+ """Numeric entry field widget with spin buttons.
2
+
3
+ Provides a specialized entry field for numeric input with increment/decrement
4
+ buttons and keyboard/mouse wheel support.
5
+ """
6
+
7
+ from typing import Any, Callable
8
+ from tkinter import TclError
9
+ from typing_extensions import Unpack
10
+
11
+ from bootstack.widgets._impl.primitives.button import Button
12
+ from bootstack.widgets._impl.composites.field import Field, FieldOptions
13
+ from bootstack.widgets._impl.mixins import configure_delegate
14
+ from bootstack.widgets.types import Master
15
+
16
+
17
+ class NumericEntry(Field):
18
+ """A numeric entry field widget with increment/decrement spin buttons.
19
+
20
+ Extends Field to provide numeric input with spin buttons, bounds validation,
21
+ keyboard stepping (Up/Down arrows), mouse wheel support, and optional wrapping.
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ master: Master = None,
27
+ value: int | float = 0,
28
+ label: str = None,
29
+ message: str = None,
30
+ show_spin_buttons: bool = True,
31
+ minvalue: int | float | None = None,
32
+ maxvalue: int | float | None = None,
33
+ increment: int | float = 1,
34
+ **kwargs: Unpack[FieldOptions]
35
+ ):
36
+ """Args:
37
+ master: Parent widget. If None, uses the default root window.
38
+ value: Initial numeric value to display.
39
+ label: Optional label text to display above the entry field.
40
+ If `required=True`, an asterisk (*) is automatically appended.
41
+ message: Optional message text to display below the entry field.
42
+ Used for hints or help text. Replaced by validation errors when
43
+ validation fails.
44
+ show_spin_buttons: If True, displays increment/decrement buttons
45
+ (plus and minus icons) to the right of the entry.
46
+ minvalue: Minimum allowed value (inclusive). Values below
47
+ this will be clamped or wrapped depending on the wrap setting.
48
+ maxvalue: Maximum allowed value (inclusive). Values above
49
+ this will be clamped or wrapped depending on the wrap setting.
50
+ increment: Step size for increment/decrement operations.
51
+
52
+ Other Parameters:
53
+ wrap: If True, values wrap around at min/max boundaries.
54
+ value_format: Number format — a `NumberPreset` string
55
+ (e.g., `'decimal'`, `'percent'`, `'currency'`) or a custom ICU
56
+ pattern (e.g., `'#,##0.00'`).
57
+ locale: Locale identifier for number formatting (e.g., `'en_US'`).
58
+ required: If True, field cannot be empty.
59
+ accent: Accent token for the focus ring and active border.
60
+ allow_blank: If True, empty input is allowed (sets value to None).
61
+ cursor: Cursor style when hovering.
62
+ exportselection: Export selection to clipboard.
63
+ font: Font for text display.
64
+ foreground: Text color.
65
+ initial_focus: If True, widget receives focus on creation.
66
+ justify: Text alignment (`'left'`, `'center'`, `'right'`).
67
+ show_message: If True, displays message area.
68
+ padding: Padding around entry widget.
69
+ takefocus: If True, widget accepts Tab focus.
70
+ textvariable: Tkinter Variable to link with text.
71
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
72
+ textsignal: Signal object for reactive updates.
73
+ width: Width in characters.
74
+ xscrollcommand: Callback for horizontal scrolling.
75
+ """
76
+ super().__init__(
77
+ master, value=value, label=label, message=message, minvalue=minvalue, maxvalue=maxvalue, kind="numeric",
78
+ increment=increment, **kwargs)
79
+
80
+ self._show_spin_buttons = show_spin_buttons
81
+
82
+ # pack info
83
+ self._increment_pack_info = {}
84
+ self._decrement_pack_info = {}
85
+
86
+ # buttons
87
+ self.insert_addon(
88
+ Button, position="after", name="decrement", icon="dash-lg", command=self.decrement, icon_only=True)
89
+ self.insert_addon(
90
+ Button, position="after", name="increment", icon="plus-lg", command=self.increment, icon_only=True)
91
+ self._delegate_show_spin_buttons(show_spin_buttons)
92
+
93
+ self.entry_widget.bind('<<StateChanged>>', self._on_entry_state_changed, add=True)
94
+ self._update_spin_button_states()
95
+
96
+ @property
97
+ def increment_widget(self) -> Button:
98
+ """Get the increment spin button widget."""
99
+ return self.addons['increment']
100
+
101
+ @property
102
+ def decrement_widget(self) -> Button:
103
+ """Get the decrement spin button widget."""
104
+ return self.addons['decrement']
105
+
106
+ def increment(self) -> None:
107
+ """Increment the numeric value by one step."""
108
+ if not self._entry_is_interactive():
109
+ return
110
+ self.entry_widget.event_generate("<<Increment>>")
111
+
112
+ def decrement(self) -> None:
113
+ """Decrement the numeric value by one step."""
114
+ if not self._entry_is_interactive():
115
+ return
116
+ self.entry_widget.event_generate("<<Decrement>>")
117
+
118
+ def step(self, n: int = 1) -> None:
119
+ """Step the value by `n` increments (positive) or decrements (negative).
120
+
121
+ Args:
122
+ n: Number of steps. Positive steps up, negative steps down.
123
+ """
124
+ self.entry_widget.step(n)
125
+
126
+ def on_increment(self, callback: Callable) -> str:
127
+ """Register a callback for `<<Increment>>` events (fires when value steps up).
128
+
129
+ Args:
130
+ callback: Receives a Tkinter `Event` object with `event.data['value']`
131
+ set to the new numeric value.
132
+
133
+ Returns:
134
+ Bind ID — pass to `off_increment()` to unsubscribe.
135
+ """
136
+ return self.entry_widget.on_increment(callback)
137
+
138
+ def off_increment(self, bind_id: str | None = None) -> None:
139
+ """Unsubscribe from `<<Increment>>`.
140
+
141
+ Args:
142
+ bind_id: ID returned by `on_increment()`.
143
+ """
144
+ self.entry_widget.off_increment(bind_id)
145
+
146
+ def on_decrement(self, callback: Callable) -> str:
147
+ """Register a callback for `<<Decrement>>` events (fires when value steps down).
148
+
149
+ Args:
150
+ callback: Receives a Tkinter `Event` object with `event.data['value']`
151
+ set to the new numeric value.
152
+
153
+ Returns:
154
+ Bind ID — pass to `off_decrement()` to unsubscribe.
155
+ """
156
+ return self.entry_widget.on_decrement(callback)
157
+
158
+ def off_decrement(self, bind_id: str | None = None) -> None:
159
+ """Unsubscribe from `<<Decrement>>`.
160
+
161
+ Args:
162
+ bind_id: ID returned by `on_decrement()`.
163
+ """
164
+ self.entry_widget.off_decrement(bind_id)
165
+
166
+ def _entry_is_interactive(self) -> bool:
167
+ """Return True if the entry widget is not disabled or readonly."""
168
+ state = self.entry_widget.state()
169
+ return 'disabled' not in state and 'readonly' not in state
170
+
171
+ def _on_entry_state_changed(self, _: Any = None):
172
+ """Sync spin buttons whenever the entry state mutates."""
173
+ self._update_spin_button_states()
174
+
175
+ def _update_spin_button_states(self):
176
+ """Enable or disable spin buttons based on entry interactivity."""
177
+ state_value = 'disabled' if not self._entry_is_interactive() else '!disabled'
178
+ for control in (self.increment_widget, self.decrement_widget):
179
+ try:
180
+ control.configure(state=state_value)
181
+ except TclError:
182
+ pass
183
+
184
+ @configure_delegate('show_spin_buttons')
185
+ def _delegate_show_spin_buttons(self, value: bool = None):
186
+ """Get or set the visibility of spin buttons."""
187
+ if value is None:
188
+ return self._show_spin_buttons
189
+ else:
190
+ self._show_spin_buttons = value
191
+ if value:
192
+ if not self.increment_widget.winfo_ismapped():
193
+ self.increment_widget.pack(**self._increment_pack_info)
194
+ if not self.decrement_widget.winfo_ismapped():
195
+ self.decrement_widget.pack(**self._decrement_pack_info)
196
+ else:
197
+ self._increment_pack_info = self.increment_widget.pack_info()
198
+ self._decrement_pack_info = self.decrement_widget.pack_info()
199
+ self.increment_widget.pack_forget()
200
+ self.decrement_widget.pack_forget()
201
+ return None
@@ -0,0 +1,395 @@
1
+ import tkinter
2
+ from typing import Any, Callable
3
+
4
+ from typing_extensions import TypedDict, Unpack
5
+
6
+ from bootstack.events import PageChangeEvent
7
+ from bootstack.widgets._impl.primitives import Frame
8
+ from bootstack.widgets.types import Master, AccentToken, SurfaceToken
9
+ from bootstack._core import NavigationError
10
+
11
+
12
+ class PageOptions(TypedDict, total=False):
13
+ padding: Any
14
+ width: int
15
+ height: int
16
+ style: str
17
+ cursor: str
18
+ show_border: bool
19
+ accent: AccentToken | str
20
+ variant: str
21
+ surface: SurfaceToken | str
22
+ style_options: dict[str, Any]
23
+
24
+
25
+ class PageStackKwargs(TypedDict, total=False):
26
+ takefocus: bool
27
+ width: int
28
+ height: int
29
+ padding: Any
30
+
31
+
32
+ class PageStack(Frame):
33
+ """A navigation container widget for managing multiple pages with history support.
34
+
35
+ PageStack provides a stack-based navigation system where only one page is
36
+ visible at a time. It maintains a navigation history, allowing users to move
37
+ backward and forward through pages similar to a web browser.
38
+
39
+ """
40
+
41
+ def __init__(self, master: Master = None, **kwargs: Unpack[PageStackKwargs]):
42
+ """Initialize a new PageStack instance.
43
+
44
+ Creates an empty PageStack with no pages and no navigation history.
45
+ The widget inherits from Frame and supports all standard Frame
46
+ configuration options.
47
+
48
+ Args:
49
+ master: Parent widget. If None, uses the default root window.
50
+ **kwargs: Additional arguments passed to Frame (e.g., `takefocus`,
51
+ `width`, `height`, `padding`).
52
+ """
53
+ super().__init__(master, **kwargs)
54
+ self._pages: dict[str, tkinter.Widget] = {}
55
+ self._current: str | None = None
56
+ self._history: list[tuple[str, dict]] = []
57
+ self._index: int = -1
58
+ # macOS (Aqua) only: re-mapping a previously-unmapped page paints blank
59
+ # for one display cycle, so swapping pages with pack_forget/pack flashes
60
+ # white. There we instead keep every visited page mapped, stacked in a
61
+ # single grid cell, and switch with lift()/lower() — no remap, no flash.
62
+ # A page must stay *full size* while hidden for this to hold (a shrunk
63
+ # page re-expands with the same blank flash), so each mapped page does
64
+ # reflow on a window resize; that cost is the deliberate trade. Other
65
+ # platforms don't have the flash and keep the cheaper one-page-mapped
66
+ # pack path unchanged.
67
+ try:
68
+ self._stacked: bool = self.tk.call('tk', 'windowingsystem') == 'aqua'
69
+ except tkinter.TclError:
70
+ self._stacked = False
71
+
72
+ def add(self, key: str, page: tkinter.Widget = None, **kwargs) -> tkinter.Widget:
73
+ """Add a page to the stack, optionally creating a Frame.
74
+
75
+ Args:
76
+ key: Unique identifier for the page (required for navigation).
77
+ page: The widget to add. If None, creates a Frame.
78
+ **kwargs: When page is None, these are passed to Frame (e.g., padding, accent, variant).
79
+
80
+ Returns:
81
+ Widget: The page widget (passed or created Frame).
82
+
83
+ Raises:
84
+ NavigationError: If a page with the given key already exists.
85
+ ValueError: If key is an empty string.
86
+ """
87
+ if not key:
88
+ raise ValueError("Page key cannot be an empty string")
89
+ if key in self._pages:
90
+ raise NavigationError(f"Page {key} already exists")
91
+
92
+ if page is None:
93
+ page = Frame(self, **kwargs)
94
+
95
+ self._pages[key] = page
96
+ if self._stacked:
97
+ # Stacked layout: every page shares grid cell (0, 0); navigate()
98
+ # maps/lifts the active one. Left unmapped here — it maps on first
99
+ # show (the initial render always paints fresh, flash or not).
100
+ self.grid_rowconfigure(0, weight=1)
101
+ self.grid_columnconfigure(0, weight=1)
102
+ page._bs_nav_hidden = True
103
+ else:
104
+ page.pack(fill='both', expand=True)
105
+ page.pack_forget()
106
+ return page
107
+
108
+ def remove(self, key: str) -> None:
109
+ """Remove a page from the stack and destroy its widget.
110
+
111
+ Args:
112
+ key: The identifier of the page to remove
113
+
114
+ Raises:
115
+ NavigationError: If no page with the given key exists.
116
+ ValueError: If key is an empty string.
117
+
118
+ Note:
119
+ If the removed page is currently displayed, the current page
120
+ will be set to None without navigating to another page.
121
+ """
122
+ if not key:
123
+ raise ValueError("Page key cannot be an empty string")
124
+ if key not in self._pages:
125
+ raise NavigationError(f"Page {key} does not exist")
126
+ page = self._pages.pop(key)
127
+ page.destroy()
128
+ if self._current == key:
129
+ self._current = None
130
+
131
+ def navigate(
132
+ self,
133
+ key: str,
134
+ data: dict | None = None,
135
+ replace: bool = False,
136
+ _nav: str = 'push',
137
+ _prev: tuple[str, dict] | None = None
138
+ ) -> None:
139
+ """Navigate to the page with the given key.
140
+
141
+ This method handles page transitions, manages navigation history,
142
+ and triggers lifecycle events (`<<PageUnmount>>`, `<<PageWillMount>>`,
143
+ `<<PageMount>>`, `<<PageChange>>`).
144
+
145
+ Args:
146
+ key: The identifier of the page to navigate to
147
+ data: Optional data to pass to the page and include in event payloads
148
+ replace: If True, replace the current history entry instead of adding a new one
149
+ _nav: Internal parameter indicating navigation type ('push', 'back', 'forward')
150
+ _prev: Internal parameter for preserving previous page state
151
+
152
+ Raises:
153
+ NavigationError: If the page with the given key does not exist
154
+ ValueError: If key is an empty string
155
+
156
+ Note:
157
+ Event payloads include: page, prev_page, prev_data, nav, index,
158
+ length, can_back, and can_forward.
159
+ """
160
+ if not key:
161
+ raise ValueError("Page key cannot be an empty string")
162
+ if key not in self._pages:
163
+ raise NavigationError(f"Page {key} does not exist")
164
+
165
+ # Normalize data to empty dict if None
166
+ if data is None:
167
+ data = {}
168
+
169
+ # Snapshot "previous" BEFORE mutating history
170
+ if _prev is None:
171
+ prev_key = self._current
172
+ prev_data = self._history[self._index][1] if self._index >= 0 else {}
173
+ else:
174
+ prev_key, prev_data = _prev
175
+
176
+ # Mutate history
177
+ if replace and 0 <= self._index < len(self._history):
178
+ self._history[self._index] = (key, data)
179
+ else:
180
+ if self._index < len(self._history) - 1:
181
+ self._history = self._history[:self._index + 1]
182
+ self._history.append((key, data))
183
+ self._index += 1
184
+
185
+ # Unmount the previous page. On Aqua keep it mapped (stacked behind the
186
+ # incoming page) so returning to it doesn't remap-flash; elsewhere unmap
187
+ # it for the cheaper one-page-mapped path.
188
+ if self._stacked:
189
+ if self._current is not None and self._current != key:
190
+ prev_page = self._pages[self._current]
191
+ prev_page.event_generate('<<PageUnmount>>', when="tail")
192
+ prev_page._bs_nav_hidden = True
193
+ prev_page.lower()
194
+ elif self._current is not None:
195
+ # Pre-size the target page while the current one is still mounted, so
196
+ # the swap doesn't briefly collapse the content area (a freshly built
197
+ # page is otherwise unsized at mount time and the view jumps when it
198
+ # settles). Only needed for a real swap — the initial mount has
199
+ # nothing to collapse from, and forcing layout there would serialize
200
+ # the first render (a visible "slow-motion" build of the page).
201
+ self._pages[key].update_idletasks()
202
+ self._pages[self._current].event_generate('<<PageUnmount>>', when="tail")
203
+ self._pages[self._current].pack_forget()
204
+
205
+ # Normalized payload (self-contained snapshot)
206
+ payload = PageChangeEvent(
207
+ page=key,
208
+ prev_page=prev_key,
209
+ nav=_nav,
210
+ data=dict(data),
211
+ prev_data=prev_data,
212
+ index=self._index,
213
+ length=len(self._history),
214
+ can_back=self._index > 0,
215
+ can_forward=self._index < len(self._history) - 1,
216
+ )
217
+
218
+ # Mount and notify
219
+ page = self._pages[key]
220
+ page.event_generate('<<PageWillMount>>', data=payload, when="tail")
221
+ if self._stacked:
222
+ page._bs_nav_hidden = False
223
+ # Map into the shared cell (the first show maps it; later shows are a
224
+ # no-op reconfigure of an already-mapped page) and lift it above the
225
+ # stack. Pages are never unmapped again, so a return never remaps.
226
+ page.grid(row=0, column=0, sticky='nsew')
227
+ page.lift()
228
+ else:
229
+ page.pack(fill='both', expand=True)
230
+ self._current = key
231
+ # A theme change that arrived while this page was hidden left its widgets
232
+ # stale (the walk skips off-screen widgets); recolor the now-visible page.
233
+ # Deferred to idle so the page is mapped first.
234
+ def _recolor_shown(p=page):
235
+ from bootstack.style.style import get_style
236
+ get_style().apply_theme_walk(p, only_stale=True)
237
+ self.after_idle(_recolor_shown)
238
+ self.event_generate('<<PageMount>>', data=payload, when="tail")
239
+ self.event_generate('<<PageChange>>', data=payload, when="tail")
240
+
241
+ def back(self) -> None:
242
+ """Navigate to the previous page in the navigation history.
243
+
244
+ This method moves backward in the history stack if possible.
245
+ Does nothing if already at the first page.
246
+ """
247
+ if self._index > 0:
248
+ # Snapshot BEFORE changing index
249
+ prev = (self._current, self._history[self._index][1] if self._index >= 0 else {})
250
+ self._index -= 1
251
+ key, data = self._history[self._index]
252
+ # use replace=True to avoid pushing a new entry
253
+ # pass _prev to preserve the correct 'previous' snapshot
254
+ self.navigate(key, data=data, replace=True, _nav='back', _prev=prev)
255
+
256
+ def forward(self) -> None:
257
+ """Navigate to the next page in the navigation history.
258
+
259
+ This method moves forward in the history stack if possible.
260
+ Does nothing if already at the most recent page.
261
+ """
262
+ if self._index < len(self._history) - 1:
263
+ prev = (self._current, self._history[self._index][1] if self._index >= 0 else {})
264
+ self._index += 1
265
+ key, data = self._history[self._index]
266
+ self.navigate(key, data=data, replace=True, _nav='forward', _prev=prev)
267
+
268
+ def can_back(self) -> bool:
269
+ """Check if backward navigation is possible.
270
+
271
+ Returns:
272
+ True if there is a previous page in the history to navigate back to,
273
+ False otherwise.
274
+ """
275
+ return self._index > 0
276
+
277
+ def can_forward(self) -> bool:
278
+ """Check if forward navigation is possible.
279
+
280
+ Returns:
281
+ True if there is a next page in the history to navigate forward to,
282
+ False otherwise.
283
+ """
284
+ return self._index < len(self._history) - 1
285
+
286
+ def current(self) -> tuple[str, dict] | None:
287
+ """Return the current page key and its navigation data.
288
+
289
+ Returns:
290
+ A tuple of (page_key, data_dict) if a page is currently displayed,
291
+ None if no page is currently displayed.
292
+ """
293
+ if self._current is None:
294
+ return None
295
+ return self._current, (self._history[self._index][1] if self._index >= 0 else {})
296
+
297
+ def item(self, key: str) -> tkinter.Widget:
298
+ """Get a page by its key.
299
+
300
+ Args:
301
+ key: The identifier of the page to retrieve.
302
+
303
+ Returns:
304
+ The page widget.
305
+
306
+ Raises:
307
+ NavigationError: If no page with the given key exists.
308
+ ValueError: If key is an empty string.
309
+ """
310
+ if not key:
311
+ raise ValueError("Page key cannot be an empty string")
312
+ if key not in self._pages:
313
+ raise NavigationError(f"Page {key} does not exist")
314
+ return self._pages[key]
315
+
316
+ def items(self) -> tuple[tkinter.Widget, ...]:
317
+ """Get all page widgets in the stack.
318
+
319
+ Returns:
320
+ A tuple of all page widgets managed by this PageStack.
321
+ """
322
+ return tuple(self._pages.values())
323
+
324
+ def keys(self) -> tuple[str, ...]:
325
+ """Get all page keys.
326
+
327
+ Returns:
328
+ A tuple of all page keys in the stack.
329
+ """
330
+ return tuple(self._pages.keys())
331
+
332
+ def configure_item(self, key: str, option: Any = None, **kwargs: Any) -> Any:
333
+ """Query or configure the page configuration.
334
+
335
+ Args:
336
+ key: The identifier of the page to configure.
337
+ option: Optional configuration option to query.
338
+ **kwargs: Configuration options to set on the page widget.
339
+
340
+ Returns:
341
+ If option is provided, returns the value of that option.
342
+ Otherwise returns the result of configure() if kwargs are provided.
343
+
344
+ Raises:
345
+ NavigationError: If no page with the given key exists.
346
+ ValueError: If key is an empty string.
347
+ """
348
+ if not key:
349
+ raise ValueError("Page key cannot be an empty string")
350
+ if key not in self._pages:
351
+ raise NavigationError(f"Page {key} does not exist")
352
+ if option is not None:
353
+ return self._pages[key].cget(option)
354
+ else:
355
+ return self._pages[key].configure(**kwargs)
356
+
357
+ def on_page_changed(self, callback: Callable) -> str:
358
+ """Register a callback for `<<PageChange>>` events (fires after every navigation).
359
+
360
+ Args:
361
+ callback: Receives `event.data` with navigation info: `page`, `prev_page`,
362
+ `nav`, `index`, `length`, `can_back`, `can_forward`.
363
+
364
+ Returns:
365
+ Bind ID — pass to `off_page_changed()` to unsubscribe.
366
+ """
367
+ return self.bind('<<PageChange>>', callback, add="+")
368
+
369
+ def off_page_changed(self, bind_id: str | None = None) -> None:
370
+ """Unsubscribe from `<<PageChange>>`.
371
+
372
+ Args:
373
+ bind_id: ID returned by `on_page_changed()`. If None, removes all.
374
+ """
375
+ self.unbind("<<PageChange>>", bind_id)
376
+
377
+ def on_page_mount(self, callback: Callable) -> str:
378
+ """Register a callback for `<<PageMount>>` events (fires after a new page is displayed).
379
+
380
+ Args:
381
+ callback: Receives `event.data` with navigation info: `page`,
382
+ `prev_page`, `nav`, `index`, `length`, `can_back`, `can_forward`.
383
+
384
+ Returns:
385
+ Bind ID — pass to `off_page_mount()` to unsubscribe.
386
+ """
387
+ return self.bind('<<PageMount>>', callback, add='+')
388
+
389
+ def off_page_mount(self, bind_id: str | None = None) -> None:
390
+ """Unsubscribe from `<<PageMount>>`.
391
+
392
+ Args:
393
+ bind_id: ID returned by `on_page_mount()`. If None, removes all.
394
+ """
395
+ self.unbind('<<PageMount>>', bind_id)