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,1179 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence, overload
4
+
5
+ if TYPE_CHECKING:
6
+ from bootstack.images import AppIcon, Image
7
+ from bootstack.widgets.listview import ListView
8
+ from bootstack.widgets.tree import Tree
9
+
10
+ from bootstack._runtime.app import LocalizeMode
11
+ from bootstack.widgets._impl.composites.shell.shell import Shell as _InternalShell
12
+ from bootstack.widgets._core.app_config import AppConfigMixin, APP_CONFIG_KWARGS
13
+ from bootstack.widgets._core.base import PublicWidgetBase, _RawTkContainer
14
+ from bootstack.widgets._core.events import register_widget_events
15
+ from bootstack.widgets._impl.primitives.flexframe import FlexFrame
16
+ from bootstack.widgets._impl.primitives.gridframe import GridFrame
17
+ from bootstack.widgets._core.container import (
18
+ GRID_KEYS, grid_sticky, place_flex_child, _reject_legacy_child_kwargs,
19
+ _expand_margin,
20
+ )
21
+ from bootstack.widgets._core.context import push_container, pop_container
22
+ from bootstack.widgets._core.window_controls import WindowControlsMixin
23
+ from bootstack.widgets._core.window_menu import ChromeHostMixin
24
+ from bootstack.events import (
25
+ DisplayModeEvent,
26
+ PageChangeEvent,
27
+ PaneToggleEvent,
28
+ Subscription,
29
+ WorkspaceChangeEvent,
30
+ )
31
+ from bootstack.streams import Stream
32
+ from bootstack.widgets.statusbar import StatusBar
33
+ from bootstack.widgets.types import (
34
+ AccentToken, AutoFlow, LayoutKind, Padding, SurfaceToken, WidgetDensity, WindowStyle,
35
+ )
36
+
37
+ SidebarMode = Literal["expanded", "compact", "hidden"]
38
+ NavVariant = Literal["ghost", "solid"]
39
+
40
+
41
+ class Page:
42
+ """Context-manager proxy for a content page or custom sidebar container.
43
+
44
+ A page IS a layout container: it accepts the same layout kwargs as
45
+ `PageStack.add` (`layout` / `padding` / `gap` / `horizontal_items` /
46
+ `vertical_items` / `grow_items` / `columns` / `rows` / `auto_flow`), so
47
+ children placed inside it need no extra `Column` / `Grid` wrapper. Widgets
48
+ created inside `with page:` parent here automatically; `scrollable=True` wraps
49
+ the content in a vertical scroll area.
50
+
51
+ The column default stretches children across the width (page content fills the
52
+ content area), unlike a standalone `Column`.
53
+ """
54
+
55
+ def __init__(
56
+ self,
57
+ tk_frame: Any,
58
+ *,
59
+ scrollable: bool = False,
60
+ layout: LayoutKind = "column",
61
+ padding: Padding | None = None,
62
+ gap: int = 0,
63
+ horizontal_items: str | None = None,
64
+ vertical_items: str | None = None,
65
+ grow_items: bool = False,
66
+ columns: int | list[int | str] | None = None,
67
+ rows: int | list[int | str] | None = None,
68
+ auto_flow: AutoFlow = "row",
69
+ ) -> None:
70
+ self._internal = tk_frame
71
+ self._scroll: Any = None
72
+ host = tk_frame
73
+ if scrollable:
74
+ from bootstack.widgets.scrollview import ScrollView
75
+
76
+ self._scroll = ScrollView(
77
+ parent=_RawTkContainer(tk_frame), scroll_direction="vertical"
78
+ )
79
+ host = self._scroll._child_master()
80
+
81
+ # Page content fills the content area, so a column stretches its children
82
+ # across the width by default (a standalone Column centers them).
83
+ if horizontal_items is None:
84
+ horizontal_items = "stretch" if layout in ("grid", "column") else "left"
85
+ if vertical_items is None:
86
+ vertical_items = "stretch" if layout == "grid" else "center" if layout == "row" else "top"
87
+ self._layout = layout
88
+ self._horizontal_items = horizontal_items
89
+ self._vertical_items = vertical_items
90
+
91
+ if layout in ("column", "row"):
92
+ self._layout_frame: Any = FlexFrame(
93
+ host, direction="vertical" if layout == "column" else "horizontal",
94
+ padding=padding, gap=gap, horizontal_items=horizontal_items,
95
+ vertical_items=vertical_items, grow_items=grow_items,
96
+ )
97
+ elif layout == "grid":
98
+ self._layout_frame = GridFrame(
99
+ host, columns=columns, rows=rows, padding=padding, gap=gap, auto_flow=auto_flow,
100
+ )
101
+ else:
102
+ raise ValueError(f"page layout must be 'column', 'row', or 'grid', got {layout!r}")
103
+ self._layout_frame.pack(fill="both", expand=True)
104
+
105
+ def _child_master(self) -> Any:
106
+ return self._layout_frame
107
+
108
+ def guide_layout(self, child: Any, **layout_kw: Any) -> None:
109
+ if self._layout == "grid":
110
+ _reject_legacy_child_kwargs(layout_kw, "AppShell page")
111
+ _expand_margin(layout_kw)
112
+ options = {k: v for k, v in layout_kw.items() if k in GRID_KEYS}
113
+ h = layout_kw.get("horizontal") or self._horizontal_items
114
+ v = layout_kw.get("vertical") or self._vertical_items
115
+ options["sticky"] = grid_sticky(h, v)
116
+ child._internal.grid(in_=self._child_master(), **options)
117
+ return
118
+ place_flex_child(self._layout_frame, child, layout_kw, "AppShell page")
119
+
120
+ def __enter__(self) -> "Page":
121
+ push_container(self)
122
+ return self
123
+
124
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
125
+ pop_container(self)
126
+
127
+
128
+ class PageNav:
129
+ """Authorable page-list sidebar — the handle returned by `page_nav()`.
130
+
131
+ A page nav is a flat list of authored nav items, each paired with a content
132
+ page. Author it with `add_page()` (and `add_header()` / `add_divider()` to
133
+ chunk the list, `add_footer_page()` to pin an item to the bottom). Use it as a
134
+ context manager for grouping; the items themselves are added via the handle,
135
+ not parented into it.
136
+ """
137
+
138
+ def __init__(self, internal_host: Any) -> None:
139
+ # The internal Shell (single-tier, delegates to its default workspace) or
140
+ # internal Workspace — both expose add_page/add_header/add_divider.
141
+ self._host = internal_host
142
+
143
+ def __enter__(self) -> "PageNav":
144
+ return self
145
+
146
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
147
+ upd = getattr(self._host, "update_idletasks", None)
148
+ if upd is not None:
149
+ try:
150
+ upd()
151
+ except Exception:
152
+ pass
153
+
154
+ def add_page(
155
+ self,
156
+ key: str,
157
+ *,
158
+ text: str = "",
159
+ icon: str | dict | None = None,
160
+ scrollable: bool = False,
161
+ pin_to_footer: bool = False,
162
+ layout: LayoutKind = "column",
163
+ padding: Padding | None = None,
164
+ gap: int = 0,
165
+ horizontal_items: str | None = None,
166
+ vertical_items: str | None = None,
167
+ grow_items: bool = False,
168
+ columns: int | list[int | str] | None = None,
169
+ rows: int | list[int | str] | None = None,
170
+ auto_flow: AutoFlow = "row",
171
+ ) -> Page:
172
+ """Add a nav item and its content page; return a context-manager page.
173
+
174
+ The page IS a layout container — the `layout` / `padding` / `gap` /
175
+ `horizontal_items` / `vertical_items` / `grow_items` / `columns` / `rows` /
176
+ `auto_flow` kwargs configure it directly (same as `PageStack.add`), so
177
+ children inside the `with` block need no extra `Column` / `Grid` wrapper.
178
+
179
+ Args:
180
+ key: Unique identifier for the nav item and page.
181
+ text: Display label in the sidebar.
182
+ icon: Icon name or icon configuration dict.
183
+ scrollable: If `True`, wrap the page in a vertical `ScrollView`.
184
+ pin_to_footer: If `True`, pin the nav item to the bottom of the
185
+ sidebar (the non-scrolling footer zone) instead of the scrolling
186
+ main list — handy for a Settings or Account entry.
187
+ layout: Page layout — `'column'` (default), `'row'`, or `'grid'`.
188
+ padding: Space inside the page around its content.
189
+ gap: Space between the page's children in pixels.
190
+ horizontal_items: Cross/main-axis alignment of children on the x-axis.
191
+ vertical_items: Cross/main-axis alignment of children on the y-axis.
192
+ grow_items: For `'column'`/`'row'`, every child grows to share the axis.
193
+ columns: Column definitions for `'grid'` layout.
194
+ rows: Row definitions for `'grid'` layout.
195
+ auto_flow: Grid auto-flow direction for `'grid'` layout.
196
+ """
197
+ frame = self._host.add_page(key, text=text, icon=icon, footer=pin_to_footer)
198
+ return Page(
199
+ frame, scrollable=scrollable, layout=layout, padding=padding, gap=gap,
200
+ horizontal_items=horizontal_items, vertical_items=vertical_items,
201
+ grow_items=grow_items, columns=columns, rows=rows, auto_flow=auto_flow,
202
+ )
203
+
204
+ def add_header(self, text: str) -> None:
205
+ """Add a plain section-label header that chunks the list (grouped-static)."""
206
+ self._host.add_header(text)
207
+
208
+ def add_divider(self) -> None:
209
+ """Add a divider to the list."""
210
+ self._host.add_divider()
211
+
212
+
213
+ class _SidebarHost:
214
+ """Mixin for anything that owns one sidebar: `AppShell` and `Workspace`.
215
+
216
+ Exposes the four parallel provider front doors — `page_nav()` (authored
217
+ pages), `list_nav()` / `tree_nav()` (data-bound master-detail), and
218
+ `custom_nav()` (hand-built) — plus `detail()` and the `content` escape hatch.
219
+ A sidebar is filled by exactly one provider; the methods are mutually
220
+ exclusive at runtime.
221
+
222
+ Subclasses provide `_sidebar_internal()` (the internal Shell or Workspace that
223
+ carries the provider methods) and `_content_frame()` (the content region).
224
+ """
225
+
226
+ def _sidebar_internal(self) -> Any: # pragma: no cover - overridden
227
+ raise NotImplementedError
228
+
229
+ def _content_frame(self) -> Any: # pragma: no cover - overridden
230
+ raise NotImplementedError
231
+
232
+ def page_nav(self) -> PageNav:
233
+ """Declare the sidebar as an authored page list; return its handle.
234
+
235
+ Returns:
236
+ A `PageNav` handle — author items with `add_page()` / `add_header()` /
237
+ `add_divider()` / `add_footer_page()`.
238
+ """
239
+ internal = self._sidebar_internal()
240
+ internal.page_nav()
241
+ return PageNav(internal)
242
+
243
+ def list_nav(
244
+ self,
245
+ source: Any,
246
+ *,
247
+ separator: bool = False,
248
+ density: WidgetDensity = "default",
249
+ placeholder: str = "Select an item to view",
250
+ chevron: bool = False,
251
+ ) -> "ListView":
252
+ """Declare a data-bound list sidebar from a `DataSource` (flat master-detail).
253
+
254
+ Records render their `icon`/`title`/`text` in a recycling list. Pair it
255
+ with `@detail` to render the body for the selected record. Returns the
256
+ `ListView` driving the sidebar — use it to read `.selection` or drive
257
+ selection (`select_items`).
258
+
259
+ Args:
260
+ source: A `DataSource` (or `rows=`-compatible record list).
261
+ separator: Draw divider lines between rows. Default `False` (flush).
262
+ density: `'default'` (richer two-line rows) or `'compact'` (one-line).
263
+ placeholder: Empty-state message shown when the source has no records.
264
+ chevron: Add a per-row disclosure chevron. Default `False`.
265
+ """
266
+ return self._sidebar_internal().list_nav(
267
+ source, separator=separator, density=density,
268
+ placeholder=placeholder, chevron=chevron,
269
+ ).nav
270
+
271
+ def tree_nav(
272
+ self,
273
+ *,
274
+ nodes: list | None = None,
275
+ source: Any = None,
276
+ parent_field: str = "parent_id",
277
+ label_field: str = "name",
278
+ icon_field: str = "icon",
279
+ density: WidgetDensity = "default",
280
+ placeholder: str = "Select an item to view",
281
+ ) -> "Tree":
282
+ """Declare a data-bound tree sidebar (hierarchical master-detail).
283
+
284
+ Declare the hierarchy inline with `nodes=` or project a flat adjacency-list
285
+ `source=` (each row names its parent via `parent_field`). Pair it with
286
+ `@detail` to render the body for the selected node. Returns the `Tree`
287
+ driving the sidebar — use it to drive the view
288
+ (`expand`/`expand_all`/`collapse`/`select`/`find`).
289
+
290
+ Args:
291
+ nodes: Inline hierarchy as a node list, or `None` to use `source`.
292
+ source: A flat adjacency-list `DataSource`, or `None` to use `nodes`.
293
+ parent_field: Field naming each row's parent (for `source`).
294
+ label_field: Field providing each node's label.
295
+ icon_field: Field providing each node's icon name.
296
+ density: `'default'` or `'compact'`.
297
+ placeholder: Empty-state message shown until a node is picked.
298
+ """
299
+ return self._sidebar_internal().tree_nav(
300
+ nodes=nodes, source=source, parent_field=parent_field,
301
+ label_field=label_field, icon_field=icon_field,
302
+ density=density, placeholder=placeholder,
303
+ ).tree
304
+
305
+ def custom_nav(self) -> Page:
306
+ """Declare a hand-built sidebar; return its container.
307
+
308
+ The escape hatch when none of the providers fit — fill the returned
309
+ container with your own sidebar UI (e.g. a `bs.Accordion`) and drive the
310
+ content region yourself via `content`.
311
+ """
312
+ return Page(self._sidebar_internal().panel())
313
+
314
+ def detail(self, fn: Callable[[dict], Any]) -> Callable[[dict], Any]:
315
+ """Register the detail-body renderer for a data-bound sidebar (decorator).
316
+
317
+ Pairs with `list_nav` / `tree_nav` to form a master-detail view: when the
318
+ selection changes, `fn` runs with the selected record and rebuilds the
319
+ content area. The first row is selected on load, so the detail is never
320
+ empty on open.
321
+
322
+ Args:
323
+ fn: Builder called with the selected record `dict` (for `tree_nav`,
324
+ the node's record `dict`). Its return value is ignored.
325
+
326
+ Returns:
327
+ `fn` unchanged, so it works as a decorator.
328
+ """
329
+ return self._sidebar_internal().detail(fn)
330
+
331
+ @property
332
+ def content(self) -> Page:
333
+ """The content region as a container for hand-driven content.
334
+
335
+ Use with `with host.content:` or `parent=host.content` to place widgets in
336
+ the content area directly — the escape hatch for `custom_nav` mode.
337
+ """
338
+ frame = self._content_frame()
339
+ host = getattr(self, "_content_host", None)
340
+ if host is None or host._internal is not frame:
341
+ host = self._content_host = Page(frame)
342
+ return host
343
+
344
+
345
+ class Workspace(_SidebarHost):
346
+ """One workspace in a `Workbench` (returned by `add_workspace()`).
347
+
348
+ A sidebar host (see `_SidebarHost`) plus its own `navigate()`: declare its
349
+ sidebar with `page_nav()` / `list_nav()` / `tree_nav()` / `custom_nav()`,
350
+ exactly as you would a single-tier `AppShell`.
351
+ """
352
+
353
+ def __init__(self, internal_ws: Any, internal_shell: Any) -> None:
354
+ self._internal = internal_ws
355
+ self._shell_internal = internal_shell
356
+
357
+ @property
358
+ def key(self) -> str:
359
+ """The workspace identifier."""
360
+ return self._internal.key
361
+
362
+ def __enter__(self) -> "Workspace":
363
+ return self
364
+
365
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
366
+ try:
367
+ self._shell_internal.update_idletasks()
368
+ except Exception:
369
+ pass
370
+
371
+ def _sidebar_internal(self) -> Any:
372
+ return self._internal
373
+
374
+ def _content_frame(self) -> Any:
375
+ return self._internal.content
376
+
377
+ def navigate(self, page: str, *, data: dict | None = None) -> None:
378
+ """Navigate to a page in this workspace."""
379
+ self._shell_internal.navigate(self.key, page, data=data)
380
+
381
+ @property
382
+ def current(self) -> str | None:
383
+ """Key of this workspace's active page, or `None`."""
384
+ return self._shell_internal.model.active_page(self.key)
385
+
386
+
387
+ class Rail:
388
+ """The workspace rail (returned by `workbench.rail`).
389
+
390
+ The rail is mostly framework-driven; its public surface switches workspaces
391
+ and observes changes. Methods are no-ops when the rail is not rendered (a
392
+ single-workspace workbench).
393
+ """
394
+
395
+ def __init__(self, shell: "Workbench") -> None:
396
+ self._shell = shell
397
+
398
+ def select(self, key: str) -> None:
399
+ """Switch to the workspace `key` and show its sidebar."""
400
+ model = self._shell._internal.model
401
+ model.select_workspace(key)
402
+ model.show_sidebar()
403
+
404
+ @property
405
+ def current(self) -> str | None:
406
+ """Key of the active workspace, or `None`."""
407
+ return self._shell._internal.current_workspace
408
+
409
+ @overload
410
+ def on_change(self) -> Stream: ...
411
+ @overload
412
+ def on_change(self, handler: Callable[[WorkspaceChangeEvent], Any]) -> Subscription: ...
413
+ def on_change(
414
+ self, handler: Callable[[WorkspaceChangeEvent], Any] | None = None
415
+ ) -> Stream | Subscription:
416
+ """Register a callback fired when the active workspace changes.
417
+
418
+ Args:
419
+ handler: Called with a
420
+ :class:`~bootstack.events.WorkspaceChangeEvent`. Omit to get a
421
+ composable :class:`~bootstack.streams.Stream`.
422
+ """
423
+ return self._shell.on_workspace_change(handler)
424
+
425
+
426
+ class _ShellBase(AppConfigMixin, WindowControlsMixin, ChromeHostMixin, PublicWidgetBase):
427
+ """Shared window/chrome/lifecycle scaffolding for `AppShell` and `Workbench`.
428
+
429
+ Private base — NOT a public type. Holds the parts both shells share: the
430
+ internal-`Shell` construction, configuration (`shell.theme`, etc.), toolbar
431
+ chrome, the status band, sidebar visibility, and the lifecycle. Navigation
432
+ (sidebar hosting vs the workspace rail) lives on the concrete subclasses.
433
+ """
434
+
435
+ _dev_supports_inprocess = True
436
+
437
+ def _init_shell(
438
+ self, init_kwargs: dict[str, Any], *, icon: Any, show_statusbar: bool
439
+ ) -> None:
440
+ self._statusbar: StatusBar | None = None
441
+ self._dev_body = None # set in __enter__ under `bootstack dev`
442
+
443
+ # Under `bootstack dev`, persist window geometry by default so the
444
+ # process-restart fallback re-opens in place (in-process reload preserves
445
+ # everything already). Dev-scoped state file; honors an explicit choice.
446
+ from bootstack.dev._env import DEV_MIN_SIZE, is_dev_mode
447
+
448
+ if is_dev_mode():
449
+ if not init_kwargs.get("remember_window_state"):
450
+ from bootstack._core.paths import app_config_file
451
+
452
+ init_kwargs["remember_window_state"] = True
453
+ if not init_kwargs.get("state_path"):
454
+ init_kwargs["state_path"] = str(
455
+ app_config_file("dev_window_state.json", init_kwargs.get("title") or "bootstack")
456
+ )
457
+ # A usable floor so a reload window starts/stays a sensible size.
458
+ init_kwargs.setdefault("minsize", DEV_MIN_SIZE)
459
+
460
+ self._internal = _InternalShell(**init_kwargs)
461
+
462
+ # Resolve the window icon AFTER the root exists (an AppIcon may resolve
463
+ # theme tokens; a deferred Image renders against the root).
464
+ self._app_icon_photo = None
465
+ if icon is not None:
466
+ from bootstack.widgets._core.image_binding import resolve_window_icon
467
+
468
+ icon_path, icon_image = resolve_window_icon(icon)
469
+ if icon_path is not None:
470
+ self._internal._setup_icon(icon_path)
471
+ elif icon_image is not None:
472
+ self._app_icon_photo = icon_image._materialize()
473
+ self._internal._setup_icon(self._app_icon_photo)
474
+
475
+ # Remember whether the band was forced on, so a dev reload can restore it.
476
+ self._show_statusbar_forced = bool(show_statusbar)
477
+ if show_statusbar:
478
+ # Materialize the band and force it visible now so it is present from
479
+ # the first frame (a content-driven band shows lazily on its first
480
+ # segment; `show_statusbar=True` means "always on").
481
+ _ = self.statusbar
482
+ self._internal.set_statusbar_visible(True)
483
+
484
+ @classmethod
485
+ def from_store(cls, store: Any, **overrides: Any):
486
+ """Construct from a persisted `Store` (or plain dict).
487
+
488
+ Reads configuration from `store`, tolerantly ignoring keys that are not
489
+ valid configuration (so version skew does not raise). Explicit keyword
490
+ `overrides` win over stored values. See `App.from_store`.
491
+ """
492
+ data = store.as_dict() if hasattr(store, "as_dict") else dict(store)
493
+ kwargs = {k: v for k, v in data.items() if k in APP_CONFIG_KWARGS}
494
+ kwargs.update(overrides)
495
+ return cls(**kwargs)
496
+
497
+ def _config_app(self) -> Any:
498
+ return self._internal
499
+
500
+ def __enter__(self):
501
+ from bootstack.dev._env import is_dev_mode
502
+
503
+ if is_dev_mode() and getattr(self, "_dev_body", None) is None:
504
+ import sys as _sys
505
+ from bootstack.dev._capture import capture_from_frame
506
+
507
+ try:
508
+ self._dev_body = capture_from_frame(_sys._getframe(1))
509
+ except Exception:
510
+ self._dev_body = None
511
+ return self
512
+
513
+ def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
514
+ try:
515
+ self._internal.update_idletasks()
516
+ except Exception:
517
+ pass
518
+
519
+ # ----- dev hot-reload hooks (used only under `bootstack dev`) -------------
520
+
521
+ def _dev_reset_region(self) -> None:
522
+ """Tear down the chrome + authored navigation, keeping the shell layout."""
523
+ # Clear chrome toolbars (the stacked-toolbar band is pre-built and stays).
524
+ try:
525
+ for child in list(self._internal.toolbar_stack.winfo_children()):
526
+ child.destroy()
527
+ except Exception:
528
+ pass
529
+ self._chrome_toolbars = []
530
+ self._native_menu_renderer = None
531
+ self._cancel_native_menu_pending()
532
+ try:
533
+ self._internal["menu"] = ""
534
+ except Exception:
535
+ pass
536
+ # Clear any status-band segments so a rebuild doesn't duplicate them.
537
+ try:
538
+ for child in list(self._internal.statusbar.winfo_children()):
539
+ child.destroy()
540
+ except Exception:
541
+ pass
542
+ self._statusbar = None
543
+ # Reset the band to its forced state: hidden unless `show_statusbar=True`
544
+ # was passed (an empty band shouldn't linger if the new body adds no
545
+ # segment; a content-driven band re-shows lazily on the first segment).
546
+ try:
547
+ self._internal.set_statusbar_visible(
548
+ getattr(self, "_show_statusbar_forced", False)
549
+ )
550
+ except Exception:
551
+ pass
552
+ # Tear down + rebuild the navigation substructure.
553
+ self._internal._dev_reset()
554
+ try:
555
+ from bootstack.dev import _registry
556
+
557
+ _registry.reset_mounts()
558
+ except Exception:
559
+ pass
560
+
561
+ def _dev_after_rebuild(self) -> None:
562
+ """Re-apply window chrome that lives outside the body, then flush + grow."""
563
+ self._ensure_default_titlebar()
564
+ try:
565
+ root = self._internal
566
+ root.update_idletasks()
567
+ cur_w, cur_h = root.winfo_width(), root.winfo_height()
568
+ new_w = max(cur_w, root.winfo_reqwidth())
569
+ new_h = max(cur_h, root.winfo_reqheight())
570
+ if new_w > cur_w or new_h > cur_h:
571
+ root.geometry(f"{new_w}x{new_h}+{root.winfo_x()}+{root.winfo_y()}")
572
+ except Exception:
573
+ pass
574
+
575
+ def _dev_capture_route(self) -> Any:
576
+ """Capture the active workspace + per-workspace page, to restore post-reload."""
577
+ model = self._internal._model
578
+ pages = {
579
+ key: model.active_page(key)
580
+ for key in self._internal._workspaces
581
+ if model.active_page(key) is not None
582
+ }
583
+ return {
584
+ "workspace": model.active_workspace,
585
+ "pages": pages,
586
+ "sidebar_mode": model.sidebar_mode,
587
+ }
588
+
589
+ def _dev_restore_route(self, route: Any) -> None:
590
+ """Restore the page/workspace selected before the reload, if still present."""
591
+ if not isinstance(route, dict):
592
+ return
593
+ model = self._internal._model
594
+ for ws_key, page in route.get("pages", {}).items():
595
+ ws = self._internal._workspaces.get(ws_key)
596
+ if ws is not None and page in ws.keys():
597
+ model.select_page(page, workspace=ws_key)
598
+ active_ws = route.get("workspace")
599
+ if isinstance(active_ws, str) and model.has_workspace(active_ws):
600
+ model.select_workspace(active_ws)
601
+ mode = route.get("sidebar_mode")
602
+ if mode in ("hidden", "compact", "expanded"):
603
+ model.set_sidebar_mode(mode)
604
+ # Force the mode's *visual* (compact = icon-only + narrow) onto the
605
+ # freshly-rebuilt nav. set_sidebar_mode above is a no-op when the new
606
+ # model already holds this mode, so the view treatment (normally
607
+ # applied once in mainloop) must be re-applied explicitly here.
608
+ self._internal._apply_sidebar_mode(mode)
609
+
610
+ # ----- Chrome hooks (mount the toolbar stack into the Shell's region) -----
611
+
612
+ def _menu_root(self) -> Any:
613
+ # The Shell is itself a Tk root (subclasses the internal App), so the
614
+ # native menubar and shortcut bindings attach to it.
615
+ return self._internal
616
+
617
+ def _ensure_toolbar_stack(self) -> Any:
618
+ # The shell pre-builds its stacked-toolbar region as a first-class band
619
+ # (above the body); `add_toolbar()` mounts into it rather than the base
620
+ # mixin's content-frame stack.
621
+ return self._internal.toolbar_stack
622
+
623
+ # ----- Sidebar visibility / compaction -----
624
+
625
+ def toggle_sidebar(self) -> None:
626
+ """Toggle the sidebar between hidden and shown (the hamburger action)."""
627
+ self._internal.toggle_sidebar()
628
+
629
+ def show_sidebar(self) -> None:
630
+ """Show the sidebar, restoring its last non-hidden mode."""
631
+ self._internal.show_sidebar()
632
+
633
+ def hide_sidebar(self) -> None:
634
+ """Hide the sidebar entirely."""
635
+ self._internal.hide_sidebar()
636
+
637
+ @property
638
+ def sidebar_mode(self) -> SidebarMode:
639
+ """The sidebar mode (`'hidden'`/`'compact'`/`'expanded'`)."""
640
+ return self._internal.sidebar_mode
641
+
642
+ @sidebar_mode.setter
643
+ def sidebar_mode(self, mode: SidebarMode) -> None:
644
+ self._internal.sidebar_mode = mode
645
+
646
+ # ----- Events shared by both shells -----
647
+
648
+ @overload
649
+ def on_page_change(self) -> Stream: ...
650
+ @overload
651
+ def on_page_change(self, handler: Callable[[PageChangeEvent], Any]) -> Subscription: ...
652
+ def on_page_change(
653
+ self, handler: Callable[[PageChangeEvent], Any] | None = None
654
+ ) -> Stream | Subscription:
655
+ """Register a callback fired when the active page changes.
656
+
657
+ Args:
658
+ handler: Called with a :class:`~bootstack.events.PageChangeEvent`
659
+ (the new and previous page keys, plus any `navigate()` data).
660
+ Omit to get a composable :class:`~bootstack.streams.Stream`.
661
+ """
662
+ return self.on("page_change", handler)
663
+
664
+ @overload
665
+ def on_sidebar_toggle(self) -> Stream: ...
666
+ @overload
667
+ def on_sidebar_toggle(self, handler: Callable[[PaneToggleEvent], Any]) -> Subscription: ...
668
+ def on_sidebar_toggle(
669
+ self, handler: Callable[[PaneToggleEvent], Any] | None = None
670
+ ) -> Stream | Subscription:
671
+ """Register a callback fired when the sidebar is shown or hidden.
672
+
673
+ Args:
674
+ handler: Called with a :class:`~bootstack.events.PaneToggleEvent`.
675
+ Omit to get a composable :class:`~bootstack.streams.Stream`.
676
+ """
677
+ return self.on("sidebar_toggle", handler)
678
+
679
+ @overload
680
+ def on_sidebar_mode_change(self) -> Stream: ...
681
+ @overload
682
+ def on_sidebar_mode_change(
683
+ self, handler: Callable[[DisplayModeEvent], Any]
684
+ ) -> Subscription: ...
685
+ def on_sidebar_mode_change(
686
+ self, handler: Callable[[DisplayModeEvent], Any] | None = None
687
+ ) -> Stream | Subscription:
688
+ """Register a callback fired when the sidebar mode changes.
689
+
690
+ Args:
691
+ handler: Called with a :class:`~bootstack.events.DisplayModeEvent`
692
+ (`compact` ↔ `expanded`). Omit to get a composable
693
+ :class:`~bootstack.streams.Stream`.
694
+ """
695
+ return self.on("sidebar_mode_change", handler)
696
+
697
+ # ----- Lifecycle -----
698
+
699
+ def run(self) -> None:
700
+ """Show the window and start the event loop."""
701
+ self._ensure_default_titlebar()
702
+ from bootstack.dev._env import is_dev_mode
703
+
704
+ if is_dev_mode():
705
+ from bootstack.dev._reloader import install_reloader
706
+
707
+ install_reloader(self)
708
+ self._internal.deiconify()
709
+ self._internal.mainloop()
710
+
711
+ # ----- Region accessors -----
712
+
713
+ @property
714
+ def statusbar(self) -> StatusBar:
715
+ """The bottom status band (passive status). Lazily created on first access.
716
+
717
+ The band renders once a segment is added (or `show_statusbar=True`).
718
+ """
719
+ if self._statusbar is None:
720
+ self._statusbar = StatusBar(
721
+ _toolbar=self._internal.statusbar,
722
+ _show=lambda: self._internal.set_statusbar_visible(True),
723
+ )
724
+ return self._statusbar
725
+
726
+ @property
727
+ def pages(self) -> Any:
728
+ """The active static provider's content page deck (or `None`)."""
729
+ return self._internal.pages
730
+
731
+ @property
732
+ def nav(self) -> Any:
733
+ """The active provider's sidebar navigation panel (or `None`)."""
734
+ return self._internal.nav
735
+
736
+ @property
737
+ def tk(self) -> Any:
738
+ """Underlying `tk.Tk` root window. UNSUPPORTED — escape-hatch use only."""
739
+ return self._internal
740
+
741
+
742
+ def _build_init_kwargs(
743
+ *,
744
+ title: str,
745
+ undecorated: bool,
746
+ light_theme: str,
747
+ dark_theme: str,
748
+ follow_system_appearance: bool,
749
+ available_themes: Sequence[str],
750
+ scaling: float | None,
751
+ hdpi: bool,
752
+ locale: str | None,
753
+ localize_mode: LocalizeMode,
754
+ window_style: WindowStyle | str | None,
755
+ macos_quit_behavior: str,
756
+ remember_window_state: bool,
757
+ state_path: str | None,
758
+ sidebar_mode: SidebarMode,
759
+ collapsible: bool,
760
+ nav_accent: AccentToken | str | None,
761
+ remember_nav_state: bool,
762
+ sidebar_surface: SurfaceToken | str,
763
+ statusbar_surface: SurfaceToken | str,
764
+ size: tuple[int, int] | None,
765
+ theme: str | None,
766
+ position: tuple[int, int] | None,
767
+ min_size: tuple[int, int] | None,
768
+ max_size: tuple[int, int] | None,
769
+ resizable: tuple[bool, bool] | None,
770
+ on_close: Callable[[], bool | None] | None,
771
+ sidebar_width: int | None,
772
+ ) -> dict[str, Any]:
773
+ """Assemble the shared internal-`Shell` kwargs (optionals omitted when None)."""
774
+ kw: dict[str, Any] = {
775
+ "title": title,
776
+ "undecorated": undecorated,
777
+ "light_theme": light_theme,
778
+ "dark_theme": dark_theme,
779
+ "follow_system_appearance": follow_system_appearance,
780
+ "available_themes": available_themes,
781
+ "scaling": scaling,
782
+ "hdpi": hdpi,
783
+ "locale": locale,
784
+ "localize_mode": localize_mode,
785
+ "window_style": window_style,
786
+ "macos_quit_behavior": macos_quit_behavior,
787
+ "remember_window_state": remember_window_state,
788
+ "state_path": state_path,
789
+ "sidebar_mode": sidebar_mode,
790
+ "collapsible": collapsible,
791
+ "nav_accent": nav_accent,
792
+ "remember_nav_state": remember_nav_state,
793
+ "sidebar_surface": sidebar_surface,
794
+ "statusbar_surface": statusbar_surface,
795
+ }
796
+ if size is not None:
797
+ kw["size"] = size
798
+ if theme is not None:
799
+ kw["theme"] = theme
800
+ if position is not None:
801
+ kw["position"] = position
802
+ if min_size is not None:
803
+ kw["minsize"] = min_size
804
+ if max_size is not None:
805
+ kw["maxsize"] = max_size
806
+ if resizable is not None:
807
+ kw["resizable"] = resizable
808
+ if on_close is not None:
809
+ kw["on_close"] = on_close
810
+ if sidebar_width is not None:
811
+ kw["sidebar_width"] = sidebar_width
812
+ return kw
813
+
814
+
815
+ class AppShell(_SidebarHost, _ShellBase):
816
+ """Single-tier application window: one navigation sidebar plus content.
817
+
818
+ The everyday desktop scaffold — an optional toolbar stack across the top, one
819
+ navigation sidebar on the left, a content area that swaps as you navigate, and
820
+ an optional status band along the bottom. For a multi-section app with a
821
+ workspace rail, use :class:`Workbench` instead.
822
+
823
+ Declare the sidebar with exactly one provider front door:
824
+ `page_nav()` (authored pages), `list_nav()` / `tree_nav()` (data-bound
825
+ master-detail), or `custom_nav()` (hand-built). Pages support context-manager
826
+ syntax so widgets inside the `with` block are parented automatically.
827
+
828
+ Like `App`, configuration is a single flat path: pass options as constructor
829
+ kwargs and read or change them through matching `shell.*` properties (e.g.
830
+ `shell.theme`, `shell.locale`, `shell.sidebar_mode`).
831
+
832
+ Args:
833
+ title: Window title and (in undecorated mode) chrome label.
834
+ size: Initial window size as `(width, height)`.
835
+ theme: Theme name to apply on startup (e.g. `'bootstrap-dark'`).
836
+ icon: Title-bar and taskbar icon — an icon file path, an `Image` handle,
837
+ or an `AppIcon`. Defaults to the bootstack icon.
838
+ light_theme: Theme used for the light end of system-appearance tracking
839
+ and `toggle_theme`.
840
+ dark_theme: Theme used for the dark end of system-appearance tracking
841
+ and `toggle_theme`.
842
+ follow_system_appearance: If True, switch between `light_theme` and
843
+ `dark_theme` to match the OS (currently effective on macOS).
844
+ available_themes: Theme names to expose to theme pickers.
845
+ locale: Locale identifier (e.g. `'en_US'`, `'de_DE'`).
846
+ localize_mode: Localization behavior.
847
+ window_style: Windows-only window effect, or None to disable.
848
+ macos_quit_behavior: macOS close / Cmd+Q behavior. No-op on Win/Linux.
849
+ remember_window_state: If True, window geometry is saved and restored.
850
+ state_path: Optional override for the persisted window-state file.
851
+ on_close: Handler invoked when the user clicks the window's close button.
852
+ Return `False` to veto the close; `None` or `True` to allow it.
853
+ position: Initial window position as `(x, y)`.
854
+ min_size: Minimum window size as `(width, height)`.
855
+ max_size: Maximum window size as `(width, height)`.
856
+ resizable: Whether the window can be resized as `(x, y)`.
857
+ scaling: Explicit UI scaling factor. When None, scaling is automatic.
858
+ hdpi: Enable high-DPI awareness for the application. Default `True`.
859
+ sidebar_surface: Surface token for the navigation sidebar.
860
+ statusbar_surface: Surface token for the bottom status band.
861
+ undecorated: Remove OS window decorations and draw a custom border.
862
+ Ignored on macOS. The shell gets a built-in draggable title bar with
863
+ min/max/close; add your own with `add_toolbar(show_window_controls=True)`
864
+ to take over the chrome.
865
+ show_sidebar: Render the sidebar region. Default `True`.
866
+ sidebar_mode: Initial sidebar mode — `'expanded'`/`'compact'`/`'hidden'`.
867
+ `'compact'` (icon-only) applies to an authored `page_nav` sidebar.
868
+ sidebar_width: Expanded sidebar width in pixels.
869
+ collapsible: Allow collapsing the sidebar; binds Ctrl/Cmd-B. Default `True`.
870
+ nav_accent: Accent for the active nav item. Defaults to `'primary'`; set
871
+ another accent token to retint, or `None` for a neutral wash.
872
+ remember_nav_state: Persist the sidebar mode and active page across
873
+ sessions. Default `False`.
874
+ show_statusbar: Force the bottom status band on (otherwise it appears once
875
+ a status segment is added). Default `False`.
876
+ """
877
+
878
+ def __init__(
879
+ self,
880
+ *,
881
+ title: str = "",
882
+ size: tuple[int, int] | None = None,
883
+ theme: str | None = None,
884
+ icon: "str | Image | AppIcon | None" = None,
885
+ light_theme: str = "bootstrap-light",
886
+ dark_theme: str = "bootstrap-dark",
887
+ follow_system_appearance: bool = False,
888
+ available_themes: Sequence[str] = (),
889
+ locale: str | None = None,
890
+ localize_mode: LocalizeMode = "auto",
891
+ window_style: WindowStyle | str | None = "mica",
892
+ macos_quit_behavior: Literal["native", "classic"] = "native",
893
+ remember_window_state: bool = False,
894
+ state_path: str | None = None,
895
+ on_close: Callable[[], bool | None] | None = None,
896
+ position: tuple[int, int] | None = None,
897
+ min_size: tuple[int, int] | None = None,
898
+ max_size: tuple[int, int] | None = None,
899
+ resizable: tuple[bool, bool] | None = None,
900
+ scaling: float | None = None,
901
+ hdpi: bool = True,
902
+ sidebar_surface: SurfaceToken | str = "raised",
903
+ statusbar_surface: SurfaceToken | str = "chrome",
904
+ undecorated: bool = False,
905
+ show_sidebar: bool = True,
906
+ sidebar_mode: SidebarMode = "expanded",
907
+ sidebar_width: int | None = None,
908
+ collapsible: bool = True,
909
+ nav_accent: AccentToken | str | None = "primary",
910
+ remember_nav_state: bool = False,
911
+ show_statusbar: bool = False,
912
+ **kwargs: Any,
913
+ ) -> None:
914
+ # `show_sidebar=False` is the hidden mode (the model is the truth).
915
+ if not show_sidebar:
916
+ sidebar_mode = "hidden"
917
+
918
+ init_kwargs = _build_init_kwargs(
919
+ title=title, undecorated=undecorated, light_theme=light_theme,
920
+ dark_theme=dark_theme, follow_system_appearance=follow_system_appearance,
921
+ available_themes=available_themes, scaling=scaling, hdpi=hdpi,
922
+ locale=locale, localize_mode=localize_mode, window_style=window_style,
923
+ macos_quit_behavior=macos_quit_behavior,
924
+ remember_window_state=remember_window_state, state_path=state_path,
925
+ sidebar_mode=sidebar_mode, collapsible=collapsible, nav_accent=nav_accent,
926
+ remember_nav_state=remember_nav_state, sidebar_surface=sidebar_surface,
927
+ statusbar_surface=statusbar_surface, size=size, theme=theme,
928
+ position=position, min_size=min_size, max_size=max_size,
929
+ resizable=resizable, on_close=on_close, sidebar_width=sidebar_width,
930
+ )
931
+ init_kwargs.update(kwargs)
932
+ self._init_shell(init_kwargs, icon=icon, show_statusbar=show_statusbar)
933
+
934
+ # ----- Sidebar hosting -----
935
+
936
+ def _sidebar_internal(self) -> Any:
937
+ return self._internal
938
+
939
+ def _content_frame(self) -> Any:
940
+ ws = self._internal.workspace
941
+ return ws.content if ws is not None else self._internal.content
942
+
943
+ def page_nav(self, *, variant: NavVariant = "ghost") -> PageNav:
944
+ """Declare the sidebar as an authored page list; return its handle.
945
+
946
+ Args:
947
+ variant: How an accented selection reads — `'ghost'` (default) is a
948
+ subtle accent wash behind full-strength text; `'solid'` fills the
949
+ selected item with the accent and uses on-accent (white) text for
950
+ higher emphasis. Needs `nav_accent`; with `nav_accent=None` it
951
+ falls back to a neutral wash.
952
+
953
+ Returns:
954
+ A `PageNav` handle — author items with `add_page()` / `add_header()` /
955
+ `add_divider()` / `add_footer_page()`.
956
+ """
957
+ self._internal.page_nav(selection=variant)
958
+ return PageNav(self._internal)
959
+
960
+ # ----- Navigation -----
961
+
962
+ def navigate(self, page: str, *, data: dict | None = None) -> None:
963
+ """Navigate to a page; the sidebar selection follows.
964
+
965
+ Args:
966
+ page: The page key to activate.
967
+ data: Optional data dict passed to the page's change event.
968
+ """
969
+ self._internal.navigate(page, data=data)
970
+
971
+ @property
972
+ def current(self) -> str | None:
973
+ """Key of the active page, or `None`."""
974
+ return self._internal.current_page
975
+
976
+
977
+ class Workbench(_ShellBase):
978
+ """Two-tier application window: a workspace rail plus per-workspace sidebars.
979
+
980
+ The advanced, VS Code-style scaffold — a vertical icon **rail** of workspaces
981
+ down the far left, each revealing its own navigation sidebar and content. Add
982
+ workspaces with `add_workspace()`; each is itself a sidebar host, authored with
983
+ the same `page_nav()` / `list_nav()` / `tree_nav()` / `custom_nav()` front
984
+ doors as a single-tier :class:`AppShell`.
985
+
986
+ For a single sidebar with no rail, use :class:`AppShell` instead.
987
+
988
+ Args:
989
+ title: Window title and (in undecorated mode) chrome label.
990
+ size: Initial window size as `(width, height)`.
991
+ theme: Theme name to apply on startup (e.g. `'bootstrap-dark'`).
992
+ icon: Title-bar and taskbar icon — a path, an `Image`, or an `AppIcon`.
993
+ light_theme: Theme used for the light end of system-appearance tracking.
994
+ dark_theme: Theme used for the dark end of system-appearance tracking.
995
+ follow_system_appearance: If True, track the OS appearance (macOS).
996
+ available_themes: Theme names to expose to theme pickers.
997
+ locale: Locale identifier (e.g. `'en_US'`, `'de_DE'`).
998
+ localize_mode: Localization behavior.
999
+ window_style: Windows-only window effect, or None to disable.
1000
+ macos_quit_behavior: macOS close / Cmd+Q behavior. No-op on Win/Linux.
1001
+ remember_window_state: If True, window geometry is saved and restored.
1002
+ state_path: Optional override for the persisted window-state file.
1003
+ on_close: Handler invoked on the window's close button. Return `False` to
1004
+ veto the close; `None` or `True` to allow it.
1005
+ position: Initial window position as `(x, y)`.
1006
+ min_size: Minimum window size as `(width, height)`.
1007
+ max_size: Maximum window size as `(width, height)`.
1008
+ resizable: Whether the window can be resized as `(x, y)`.
1009
+ scaling: Explicit UI scaling factor. When None, scaling is automatic.
1010
+ hdpi: Enable high-DPI awareness for the application. Default `True`.
1011
+ rail_surface: Surface token for the workspace rail.
1012
+ sidebar_surface: Surface token for the per-workspace navigation sidebar.
1013
+ statusbar_surface: Surface token for the bottom status band.
1014
+ undecorated: Remove OS window decorations and draw a custom border.
1015
+ show_sidebar: Render the sidebar region. Default `True`.
1016
+ sidebar_mode: Initial sidebar mode — `'expanded'`/`'hidden'`.
1017
+ sidebar_width: Expanded sidebar width in pixels.
1018
+ rail_width: Workspace-rail width in pixels.
1019
+ rail_labels: Show a caption under each rail icon (widens the rail).
1020
+ collapsible: Allow collapsing the sidebar; binds Ctrl/Cmd-B. Default `True`.
1021
+ nav_accent: Accent for the active nav item and the rail indicator. Defaults
1022
+ to `'primary'`; another token retints, or `None` for a neutral wash.
1023
+ remember_nav_state: Persist the sidebar mode and per-workspace active page
1024
+ across sessions. Default `False`.
1025
+ show_statusbar: Force the bottom status band on. Default `False`.
1026
+ """
1027
+
1028
+ def __init__(
1029
+ self,
1030
+ *,
1031
+ title: str = "",
1032
+ size: tuple[int, int] | None = None,
1033
+ theme: str | None = None,
1034
+ icon: "str | Image | AppIcon | None" = None,
1035
+ light_theme: str = "bootstrap-light",
1036
+ dark_theme: str = "bootstrap-dark",
1037
+ follow_system_appearance: bool = False,
1038
+ available_themes: Sequence[str] = (),
1039
+ locale: str | None = None,
1040
+ localize_mode: LocalizeMode = "auto",
1041
+ window_style: WindowStyle | str | None = "mica",
1042
+ macos_quit_behavior: Literal["native", "classic"] = "native",
1043
+ remember_window_state: bool = False,
1044
+ state_path: str | None = None,
1045
+ on_close: Callable[[], bool | None] | None = None,
1046
+ position: tuple[int, int] | None = None,
1047
+ min_size: tuple[int, int] | None = None,
1048
+ max_size: tuple[int, int] | None = None,
1049
+ resizable: tuple[bool, bool] | None = None,
1050
+ scaling: float | None = None,
1051
+ hdpi: bool = True,
1052
+ rail_surface: SurfaceToken | str = "chrome",
1053
+ sidebar_surface: SurfaceToken | str = "raised",
1054
+ statusbar_surface: SurfaceToken | str = "chrome",
1055
+ undecorated: bool = False,
1056
+ show_sidebar: bool = True,
1057
+ sidebar_mode: SidebarMode = "expanded",
1058
+ sidebar_width: int | None = None,
1059
+ rail_width: int | None = None,
1060
+ rail_labels: bool = False,
1061
+ collapsible: bool = True,
1062
+ nav_accent: AccentToken | str | None = "primary",
1063
+ remember_nav_state: bool = False,
1064
+ show_statusbar: bool = False,
1065
+ **kwargs: Any,
1066
+ ) -> None:
1067
+ self._rail: Rail | None = None
1068
+
1069
+ # `show_sidebar=False` is the hidden mode (the model is the truth).
1070
+ if not show_sidebar:
1071
+ sidebar_mode = "hidden"
1072
+
1073
+ init_kwargs = _build_init_kwargs(
1074
+ title=title, undecorated=undecorated, light_theme=light_theme,
1075
+ dark_theme=dark_theme, follow_system_appearance=follow_system_appearance,
1076
+ available_themes=available_themes, scaling=scaling, hdpi=hdpi,
1077
+ locale=locale, localize_mode=localize_mode, window_style=window_style,
1078
+ macos_quit_behavior=macos_quit_behavior,
1079
+ remember_window_state=remember_window_state, state_path=state_path,
1080
+ sidebar_mode=sidebar_mode, collapsible=collapsible, nav_accent=nav_accent,
1081
+ remember_nav_state=remember_nav_state, sidebar_surface=sidebar_surface,
1082
+ statusbar_surface=statusbar_surface, size=size, theme=theme,
1083
+ position=position, min_size=min_size, max_size=max_size,
1084
+ resizable=resizable, on_close=on_close, sidebar_width=sidebar_width,
1085
+ )
1086
+ # Rail-only chrome (two-tier).
1087
+ init_kwargs["rail_surface"] = rail_surface
1088
+ init_kwargs["rail_labels"] = rail_labels
1089
+ if rail_width is not None:
1090
+ init_kwargs["rail_width"] = rail_width
1091
+ init_kwargs.update(kwargs)
1092
+ self._init_shell(init_kwargs, icon=icon, show_statusbar=show_statusbar)
1093
+
1094
+ # ----- Workspaces -----
1095
+
1096
+ def add_workspace(
1097
+ self,
1098
+ key: str,
1099
+ *,
1100
+ text: str = "",
1101
+ icon: str | dict | None = None,
1102
+ pin_to_footer: bool = False,
1103
+ ) -> Workspace:
1104
+ """Add a workspace — a rail icon revealing its own sidebar + content.
1105
+
1106
+ The rail appears once there is more than one workspace. The returned
1107
+ `Workspace` is a sidebar host: declare its sidebar with `page_nav()` /
1108
+ `list_nav()` / `tree_nav()` / `custom_nav()`.
1109
+
1110
+ Args:
1111
+ key: Unique workspace identifier.
1112
+ text: Tooltip / rail label for the workspace icon.
1113
+ icon: Rail icon name or icon configuration dict.
1114
+ pin_to_footer: If `True`, pin the rail icon to the bottom of the rail
1115
+ (the conventional spot for Settings / Account) instead of the top
1116
+ cluster.
1117
+ """
1118
+ add = self._internal.add_footer_workspace if pin_to_footer else self._internal.add_workspace
1119
+ return Workspace(add(key, text=text, icon=icon), self._internal)
1120
+
1121
+ # ----- Navigation -----
1122
+
1123
+ def navigate(self, workspace: str, page: str, *, data: dict | None = None) -> None:
1124
+ """Switch to `workspace` and select `page` within it.
1125
+
1126
+ Args:
1127
+ workspace: The workspace key to activate.
1128
+ page: The page key to activate within that workspace.
1129
+ data: Optional data dict passed to the page's change event.
1130
+ """
1131
+ self._internal.navigate(workspace, page, data=data)
1132
+
1133
+ @property
1134
+ def current(self) -> str | None:
1135
+ """Key of the active workspace's active page, or `None`."""
1136
+ return self._internal.current_page
1137
+
1138
+ @property
1139
+ def current_workspace(self) -> str | None:
1140
+ """Key of the active workspace, or `None`."""
1141
+ return self._internal.current_workspace
1142
+
1143
+ @property
1144
+ def rail(self) -> Rail:
1145
+ """The workspace switcher. Methods no-op when the rail is not rendered."""
1146
+ if self._rail is None:
1147
+ self._rail = Rail(self)
1148
+ return self._rail
1149
+
1150
+ # ----- Events -----
1151
+
1152
+ @overload
1153
+ def on_workspace_change(self) -> Stream: ...
1154
+ @overload
1155
+ def on_workspace_change(
1156
+ self, handler: Callable[[WorkspaceChangeEvent], Any]
1157
+ ) -> Subscription: ...
1158
+ def on_workspace_change(
1159
+ self, handler: Callable[[WorkspaceChangeEvent], Any] | None = None
1160
+ ) -> Stream | Subscription:
1161
+ """Register a callback fired when the rail switches workspace.
1162
+
1163
+ Args:
1164
+ handler: Called with a
1165
+ :class:`~bootstack.events.WorkspaceChangeEvent`. Omit to get a
1166
+ composable :class:`~bootstack.streams.Stream`.
1167
+ """
1168
+ return self.on("workspace_change", handler)
1169
+
1170
+
1171
+ _SHELL_EVENTS = {
1172
+ "page_change": "<<PageChange>>",
1173
+ "sidebar_toggle": "<<SidebarToggle>>",
1174
+ "sidebar_mode_change": "<<SidebarModeChange>>",
1175
+ }
1176
+ _WORKBENCH_EVENTS = dict(_SHELL_EVENTS, workspace_change="<<WorkspaceChange>>")
1177
+
1178
+ register_widget_events(AppShell, _SHELL_EVENTS)
1179
+ register_widget_events(Workbench, _WORKBENCH_EVENTS)