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,581 @@
1
+ """The shell composite — wires `NavModel` to the region layout.
2
+
3
+ `Shell` ties Layer 1 (`ShellLayout` regions) to Layer 2 (`NavModel`) via Layer 3
4
+ workspaces. Every workspace owns one provider mounted into its own sidebar-panel
5
+ frame and content frame; the sidebar and content regions are each a *deck* of
6
+ per-workspace frames, and switching the active workspace swaps which frames show
7
+ (the nested page-stack cascade — rail chooses the panel, the panel chooses the
8
+ content). The `NavModel` is the single source of truth; the view reacts to it.
9
+
10
+ Single-tier is the degenerate case (spec decision #1): shell-level `add_page` /
11
+ `list_nav` / `tree_nav` target an implicit default workspace, so the rail never
12
+ renders. `add_workspace()` creates additional workspaces (revealing the rail in
13
+ step 6b). The two are mutually exclusive — author one style or the other.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, Callable
19
+
20
+ from bootstack.events import (
21
+ DisplayModeEvent,
22
+ PageChangeEvent,
23
+ PaneToggleEvent,
24
+ WorkspaceChangeEvent,
25
+ )
26
+ from bootstack.widgets._core.navmodel import NavChange, NavModel, SidebarMode
27
+ from bootstack.widgets._impl.composites.pagestack import PageStack
28
+ from bootstack.widgets._impl.composites.shell.layout import ShellLayout
29
+ from bootstack.widgets._impl.composites.shell.rail import Rail
30
+ from bootstack.widgets._impl.composites.shell.workspace import Workspace
31
+
32
+
33
+ # Key of the implicit single workspace backing the shell-level page methods.
34
+ _DEFAULT_WORKSPACE = "__shell__"
35
+
36
+
37
+ class Shell(ShellLayout):
38
+ """Application shell with workspace-based navigation.
39
+
40
+ Args:
41
+ title: Window title.
42
+ theme: Theme name, or None for the default.
43
+ size: Initial window size as `(width, height)`.
44
+ undecorated: Remove OS chrome and draw a custom border.
45
+ sidebar_mode: Initial sidebar mode (`'expanded'`/`'compact'`/`'hidden'`).
46
+ `'compact'` (icon-only) applies only to a standalone static sidebar.
47
+ collapsible: Allow collapsing the sidebar; binds Ctrl/Cmd-B to toggle it.
48
+ remember_nav_state: Persist the sidebar mode + per-workspace active page
49
+ across sessions, restoring them on the next launch.
50
+ nav_accent: Accent token for the active nav item.
51
+ nav_selection: Accented-selection treatment for the standalone primary
52
+ nav (the no-rail `nav-pill` default workspace) — `'ghost'` (subtle
53
+ wash) or `'solid'` (filled accent + on-accent text). Under-rail
54
+ workspace navs always use the wash.
55
+ **kwargs: Forwarded to `ShellLayout` / `App` (widths, position, etc.).
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ *,
61
+ title: str = "",
62
+ theme: str | None = None,
63
+ size: tuple[int, int] | None = None,
64
+ undecorated: bool = False,
65
+ sidebar_mode: SidebarMode = "expanded",
66
+ collapsible: bool = True,
67
+ remember_nav_state: bool = False,
68
+ nav_accent: str | None = "primary",
69
+ nav_selection: str = "ghost",
70
+ rail_labels: bool = False,
71
+ **kwargs: Any,
72
+ ) -> None:
73
+ # A labeled rail (caption under each icon) needs a wider strip than the
74
+ # icon-only default; widen unless the caller pinned rail_width explicitly.
75
+ if rail_labels and "rail_width" not in kwargs:
76
+ kwargs["rail_width"] = 72
77
+ super().__init__(
78
+ title=title, theme=theme, size=size, undecorated=undecorated, **kwargs
79
+ )
80
+
81
+ self._model = NavModel(sidebar_mode=sidebar_mode)
82
+ self._nav_accent = nav_accent
83
+ self._nav_selection = nav_selection
84
+ self._rail_labels = rail_labels
85
+ self._collapsible = collapsible
86
+ self._remember_nav_state = remember_nav_state
87
+ self._initial_applied = False
88
+ self._workspaces: dict[str, Workspace] = {}
89
+ self._pending_data: dict | None = None
90
+ self._prev_page: str | None = None
91
+ self._prev_workspace: str | None = None
92
+ # Track sidebar visibility separately so compact<->expanded transitions
93
+ # fire <<SidebarModeChange>> but NOT <<SidebarToggle>> (a pure-visibility
94
+ # event); only a hidden<->shown flip toggles.
95
+ self._prev_sidebar_visible: bool = sidebar_mode != "hidden"
96
+
97
+ # Build the per-workspace decks, rail switcher, and model wiring. Factored
98
+ # out so dev hot reload can tear the authored navigation down and rebuild
99
+ # it against the same (stable) region frames (see _dev_reset).
100
+ self._build_nav_region()
101
+
102
+ # Ctrl/Cmd-B toggles sidebar visibility (the hamburger action).
103
+ if self._collapsible:
104
+ self.bind("<Control-b>", self._on_toggle_shortcut, add="+")
105
+ self.bind("<Command-b>", self._on_toggle_shortcut, add="+")
106
+
107
+ def _build_nav_region(self) -> None:
108
+ """Build the sidebar/content decks and rail switcher, and wire the model.
109
+
110
+ Parents into the stable region frames (`self.sidebar`/`content`/`rail`
111
+ from `ShellLayout`), so it is safe to call again after `_dev_reset` has
112
+ destroyed the previous decks.
113
+ """
114
+ # Sidebar + content are decks of per-workspace frames.
115
+ self._panel_stack = PageStack(self.sidebar)
116
+ self._panel_stack.pack(fill="both", expand=True)
117
+ self._content_stack = PageStack(self.content)
118
+ self._content_stack.pack(fill="both", expand=True)
119
+
120
+ # The tier-1 workspace switcher (hidden until > 1 workspace).
121
+ # NB: must NOT be named `self._rail` — that is ShellLayout's rail *region*
122
+ # frame; shadowing it orphans the region so it never packs into the body.
123
+ self._railnav = Rail(
124
+ self.rail, on_select=self._rail_select, accent=self._nav_accent,
125
+ labels=self._rail_labels, surface=self.rail_surface,
126
+ )
127
+ self._railnav.pack(fill="both", expand=True)
128
+
129
+ self._model.subscribe(self._on_model_change)
130
+
131
+ def _dev_reset(self) -> None:
132
+ """Tear down all authored navigation so the with-body can rebuild it.
133
+
134
+ Dev hot reload only — closes provider subscriptions, destroys the decks
135
+ and rail, and starts a fresh model + nav region against the (stable)
136
+ region frames. Mirrors the navigation half of `__init__`.
137
+ """
138
+ for ws in list(self._workspaces.values()):
139
+ try:
140
+ ws.close()
141
+ except Exception:
142
+ pass
143
+ self._workspaces = {}
144
+ for attr in ("_panel_stack", "_content_stack", "_railnav"):
145
+ widget = getattr(self, attr, None)
146
+ if widget is not None:
147
+ try:
148
+ widget.destroy()
149
+ except Exception:
150
+ pass
151
+ mode = self._model.sidebar_mode
152
+ self._model = NavModel(sidebar_mode=mode)
153
+ self._initial_applied = False
154
+ self._prev_workspace = None
155
+ self._prev_page = None
156
+ self._prev_sidebar_visible = mode != "hidden"
157
+ self._pending_data = None
158
+ self._build_nav_region()
159
+
160
+ # ----- Workspace creation -----
161
+
162
+ def _create_workspace(self, key: str, *, text: str = "", icon=None, is_footer: bool = False) -> Workspace:
163
+ panel = self._panel_stack.add(key)
164
+ content = self._content_stack.add(key)
165
+ # Tier-relative static treatment: the implicit default workspace is the
166
+ # standalone primary nav (no rail) -> prominent pills; a named workspace
167
+ # sits under the rail -> quiet rows. (list_nav/tree keep their own row
168
+ # language regardless; only the static provider reads this.)
169
+ nav_variant = "nav-pill" if key == _DEFAULT_WORKSPACE else "nav-quiet"
170
+ # The `'solid'` (filled-accent) selection is the standalone primary nav's
171
+ # higher-emphasis look ONLY (the no-rail `nav-pill` default workspace). A
172
+ # named workspace's nav sits under the rail as quiet rows — a solid fill
173
+ # there would compete with the rail, so it always uses the subtle wash.
174
+ nav_selection = self._nav_selection if key == _DEFAULT_WORKSPACE else "ghost"
175
+ ws = Workspace(
176
+ key, panel, content,
177
+ nav_accent=self._nav_accent,
178
+ on_select=self._workspace_select,
179
+ on_refresh=self._workspace_refresh,
180
+ on_first_page=self._on_first_page,
181
+ nav_variant=nav_variant,
182
+ nav_surface=self.sidebar_surface,
183
+ nav_selection=nav_selection,
184
+ )
185
+ self._workspaces[key] = ws
186
+ # The implicit default workspace never gets a rail icon (single-tier).
187
+ if key != _DEFAULT_WORKSPACE:
188
+ self._railnav.add_workspace(key, icon=icon, text=text, footer=is_footer)
189
+ self._model.add_workspace(key, text=text, icon=icon, is_footer=is_footer)
190
+ if self._model.active_workspace == key:
191
+ self._panel_stack.navigate(key)
192
+ self._content_stack.navigate(key)
193
+ if key != _DEFAULT_WORKSPACE:
194
+ self._railnav.select(key)
195
+ return ws
196
+
197
+ def _default_workspace(self) -> Workspace:
198
+ if _DEFAULT_WORKSPACE not in self._workspaces:
199
+ if self._workspaces:
200
+ raise RuntimeError(
201
+ "cannot use shell-level pages together with add_workspace(); "
202
+ "author one style or the other"
203
+ )
204
+ self._create_workspace(_DEFAULT_WORKSPACE)
205
+ return self._workspaces[_DEFAULT_WORKSPACE]
206
+
207
+ def add_workspace(self, key: str, *, text: str = "", icon=None) -> Workspace:
208
+ """Add a workspace (a rail icon → its own sidebar panel + content).
209
+
210
+ Adding a second workspace reveals the rail. Mutually exclusive with the
211
+ shell-level page methods.
212
+
213
+ Raises:
214
+ RuntimeError: If shell-level pages already claimed the implicit
215
+ workspace, or the key is already used.
216
+ """
217
+ if _DEFAULT_WORKSPACE in self._workspaces:
218
+ raise RuntimeError("cannot mix add_workspace() with shell-level pages")
219
+ if key in self._workspaces:
220
+ raise RuntimeError(f"duplicate workspace key: {key!r}")
221
+ return self._create_workspace(key, text=text, icon=icon)
222
+
223
+ def add_footer_workspace(self, key: str, *, text: str = "", icon=None) -> Workspace:
224
+ """Add a workspace pinned to the bottom of the rail."""
225
+ if _DEFAULT_WORKSPACE in self._workspaces:
226
+ raise RuntimeError("cannot mix add_workspace() with shell-level pages")
227
+ if key in self._workspaces:
228
+ raise RuntimeError(f"duplicate workspace key: {key!r}")
229
+ return self._create_workspace(key, text=text, icon=icon, is_footer=True)
230
+
231
+ # ----- Shell-level content API (delegates to the default workspace) -----
232
+
233
+ def add_page(self, key: str, *, text: str = "", icon=None, footer: bool = False) -> Any:
234
+ """Add a nav item and its page in the implicit workspace."""
235
+ return self._default_workspace().add_page(key, text=text, icon=icon, footer=footer)
236
+
237
+ def add_footer_page(self, key: str, *, text: str = "", icon=None) -> Any:
238
+ """Add a footer-pinned nav item and its page in the implicit workspace."""
239
+ return self._default_workspace().add_footer_page(key, text=text, icon=icon)
240
+
241
+ def add_header(self, text: str) -> Any:
242
+ """Add a plain section-label header in the implicit workspace."""
243
+ return self._default_workspace().add_header(text)
244
+
245
+ def add_divider(self) -> Any:
246
+ """Add a divider to the implicit workspace."""
247
+ return self._default_workspace().add_divider()
248
+
249
+ def page_nav(self, *, selection: str | None = None) -> Any:
250
+ """Establish the implicit workspace's static page-list provider."""
251
+ return self._default_workspace().page_nav(selection=selection)
252
+
253
+ def panel(self) -> Any:
254
+ """Claim the implicit workspace as a custom panel; return its container."""
255
+ return self._default_workspace().panel()
256
+
257
+ def list_nav(
258
+ self,
259
+ source: Any,
260
+ *,
261
+ separator: bool = False,
262
+ density: str = "default",
263
+ placeholder: str = "Select an item to view",
264
+ chevron: bool = False,
265
+ ) -> Any:
266
+ """Fill the implicit workspace from a `DataSource` (flat master-detail)."""
267
+ return self._default_workspace().list_nav(
268
+ source, separator=separator, density=density,
269
+ placeholder=placeholder, chevron=chevron,
270
+ )
271
+
272
+ def tree_nav(self, **kwargs: Any) -> Any:
273
+ """Fill the implicit workspace from a hierarchy (tree master-detail)."""
274
+ return self._default_workspace().tree_nav(**kwargs)
275
+
276
+ def detail(self, fn: Callable[[dict], Any]) -> Callable[[dict], Any]:
277
+ """Register the implicit workspace's data-bound detail body (decorator)."""
278
+ return self._default_workspace().detail(fn)
279
+
280
+ # ----- Navigation -----
281
+
282
+ def navigate(self, *args: str, data: dict | None = None) -> None:
283
+ """Navigate to a page (single-tier) or a workspace + page (two-tier).
284
+
285
+ `navigate(page)` selects a page in the active workspace;
286
+ `navigate(workspace, page)` switches workspace and selects a page in it.
287
+
288
+ Raises:
289
+ KeyError: If the workspace or page does not exist.
290
+ TypeError: If called with other than one or two positional keys.
291
+ """
292
+ if len(args) == 2:
293
+ ws_key, page = args
294
+ if ws_key not in self._workspaces:
295
+ raise KeyError(ws_key)
296
+ self._model.select_workspace(ws_key)
297
+ self._model.show_sidebar()
298
+ self._select_page(ws_key, page, data=data)
299
+ elif len(args) == 1:
300
+ ws_key = self._model.active_workspace
301
+ if ws_key is None:
302
+ raise RuntimeError("no active workspace to navigate")
303
+ self._select_page(ws_key, args[0], data=data)
304
+ else:
305
+ raise TypeError("navigate() takes 1 (page) or 2 (workspace, page) keys")
306
+
307
+ def _select_page(self, ws_key: str, page_key: str, *, data: dict | None = None) -> None:
308
+ if page_key not in self._workspaces[ws_key].keys():
309
+ raise KeyError(page_key)
310
+ self._pending_data = data
311
+ self._model.select_page(page_key, workspace=ws_key)
312
+
313
+ @property
314
+ def current_page(self) -> str | None:
315
+ """Key of the active workspace's active page, or `None`."""
316
+ return self._model.active_page()
317
+
318
+ @property
319
+ def current_workspace(self) -> str | None:
320
+ """Key of the active workspace, or `None`."""
321
+ return self._model.active_workspace
322
+
323
+ # ----- Sidebar visibility / compaction -----
324
+
325
+ def toggle_sidebar(self) -> None:
326
+ """Toggle the sidebar between hidden and shown (the hamburger action)."""
327
+ self._model.toggle_sidebar()
328
+
329
+ def show_sidebar(self) -> None:
330
+ """Show the sidebar, restoring its last non-hidden mode."""
331
+ self._model.show_sidebar()
332
+
333
+ def hide_sidebar(self) -> None:
334
+ """Hide the sidebar entirely."""
335
+ self._model.hide_sidebar()
336
+
337
+ @property
338
+ def sidebar_mode(self) -> SidebarMode:
339
+ """The sidebar mode (`'hidden'`/`'compact'`/`'expanded'`).
340
+
341
+ Setting `'compact'` only takes visual effect on a standalone static
342
+ sidebar (no rail); otherwise it renders as expanded.
343
+ """
344
+ return self._model.sidebar_mode
345
+
346
+ @sidebar_mode.setter
347
+ def sidebar_mode(self, mode: SidebarMode) -> None:
348
+ self._model.set_sidebar_mode(mode)
349
+
350
+ def _can_compact_active(self) -> bool:
351
+ """Whether the active sidebar can compact (standalone static, no rail)."""
352
+ ws = self.workspace
353
+ return not self._model.rail_visible and ws is not None and ws.supports_compact
354
+
355
+ def _on_toggle_shortcut(self, _event: Any = None) -> str:
356
+ # Collapse the sidebar as far as is useful: to icons when the active
357
+ # sidebar can compact (standalone static — never strands the only nav),
358
+ # otherwise hide it (a data list can't compact; under a rail the rail
359
+ # stays as nav, so hiding is safe). The explicit toggle_sidebar()/
360
+ # show_sidebar()/hide_sidebar() verbs remain pure visibility.
361
+ if self._can_compact_active():
362
+ self.sidebar_mode = (
363
+ "expanded" if self._model.sidebar_mode == "compact" else "compact"
364
+ )
365
+ else:
366
+ self.toggle_sidebar()
367
+ return "break"
368
+
369
+ # ----- Model -> view -----
370
+
371
+ def _rail_select(self, ws_key: str) -> None:
372
+ # The VS Code gesture lives in the model: active icon -> hide sidebar,
373
+ # different icon -> switch workspace + show.
374
+ self._model.activate_rail(ws_key)
375
+
376
+ def _workspace_select(self, ws_key: str, page_key: str) -> None:
377
+ self._model.select_page(page_key, workspace=ws_key)
378
+
379
+ def _on_first_page(self, ws_key: str, page_key: str) -> None:
380
+ if self._model.active_page(ws_key) is None:
381
+ self._model.select_page(page_key, workspace=ws_key)
382
+
383
+ def _workspace_refresh(self, ws_key: str) -> None:
384
+ if ws_key != self._model.active_workspace:
385
+ return
386
+ ws = self._workspaces[ws_key]
387
+ keys = ws.keys()
388
+ if keys and self._model.active_page(ws_key) not in keys:
389
+ self._select_page(ws_key, keys[0])
390
+
391
+ def _on_model_change(self, change: NavChange) -> None:
392
+ if change.facet == "page" and change.page is not None:
393
+ if change.workspace == self._model.active_workspace:
394
+ self._render_page(change.workspace, change.page)
395
+ elif change.facet == "workspace" and change.workspace is not None:
396
+ self._switch_workspace(change.workspace)
397
+ payload = WorkspaceChangeEvent(
398
+ workspace=change.workspace, prev_workspace=self._prev_workspace
399
+ )
400
+ self._prev_workspace = change.workspace
401
+ self.event_generate("<<WorkspaceChange>>", data=payload, when="tail")
402
+ elif change.facet == "sidebar":
403
+ self._apply_sidebar_mode(change.sidebar_mode)
404
+ mode = change.sidebar_mode or self._model.sidebar_mode
405
+ self.event_generate(
406
+ "<<SidebarModeChange>>", data=DisplayModeEvent(mode=mode), when="tail"
407
+ )
408
+ visible = self._model.sidebar_visible
409
+ if visible != self._prev_sidebar_visible:
410
+ self._prev_sidebar_visible = visible
411
+ self.event_generate(
412
+ "<<SidebarToggle>>", data=PaneToggleEvent(is_open=visible), when="tail"
413
+ )
414
+ elif change.facet == "structure":
415
+ self._sync_rail_visibility()
416
+
417
+ def _apply_sidebar_mode(self, mode: SidebarMode | None) -> None:
418
+ """Reflect a sidebar mode in the view.
419
+
420
+ `compact` (icon-only + narrower width) is gated to the standalone static
421
+ case: it applies only when no rail is showing and the active provider
422
+ supports it; otherwise the sidebar renders expanded.
423
+ """
424
+ if mode is None:
425
+ mode = self._model.sidebar_mode
426
+ if mode == "hidden":
427
+ self.set_sidebar_visible(False)
428
+ return
429
+ self.set_sidebar_visible(True)
430
+ ws = self.workspace
431
+ compact = (
432
+ mode == "compact"
433
+ and not self._model.rail_visible
434
+ and ws is not None
435
+ and ws.supports_compact
436
+ )
437
+ # Configure the slot frame directly so the stored expanded-width token is
438
+ # not clobbered (set_sidebar_width mutates the token).
439
+ self.sidebar.configure(width=self._rail_width if compact else self._sidebar_width)
440
+ if ws is not None:
441
+ ws.set_compact(compact)
442
+
443
+ def _render_page(self, ws_key: str, page_key: str) -> None:
444
+ ws = self._workspaces[ws_key]
445
+ data = self._pending_data or {}
446
+ self._pending_data = None
447
+ ws.select_visual(page_key)
448
+ ws.show(page_key, data=data)
449
+ payload = PageChangeEvent(page=page_key, prev_page=self._prev_page, data=dict(data))
450
+ self._prev_page = page_key
451
+ self.event_generate("<<PageChange>>", data=payload, when="tail")
452
+
453
+ def _switch_workspace(self, ws_key: str) -> None:
454
+ self._panel_stack.navigate(ws_key)
455
+ self._content_stack.navigate(ws_key)
456
+ if ws_key in self._railnav.keys():
457
+ self._railnav.select(ws_key)
458
+ page = self._model.active_page(ws_key)
459
+ if page is not None:
460
+ self._render_page(ws_key, page)
461
+
462
+ def _sync_rail_visibility(self) -> None:
463
+ # The rail widget is wired in step 6b; for now reflect model state only.
464
+ self.set_rail_visible(self._model.rail_visible)
465
+
466
+ # ----- Lifecycle -----
467
+
468
+ def mainloop(self, n: int = 0) -> None:
469
+ """Apply the initial (and any persisted) nav state, then run the loop."""
470
+ self._apply_initial_state()
471
+ super().mainloop(n=n)
472
+
473
+ def _apply_initial_state(self) -> None:
474
+ """Restore persisted state (if enabled) and reflect the sidebar mode.
475
+
476
+ Runs once, just before the event loop starts — after all pages have been
477
+ authored — so a restored page/workspace overrides the authored default
478
+ (mirroring window-geometry restore).
479
+ """
480
+ if self._initial_applied:
481
+ return
482
+ self._initial_applied = True
483
+ if self._remember_nav_state:
484
+ self._restore_nav_state()
485
+ self._apply_sidebar_mode(self._model.sidebar_mode)
486
+
487
+ # ----- Nav-state persistence -----
488
+
489
+ def _nav_state_path(self):
490
+ from bootstack._core.paths import app_config_file
491
+ return app_config_file("nav_state.json", self.settings.app_name)
492
+
493
+ def _restore_nav_state(self) -> None:
494
+ import json
495
+ try:
496
+ raw = self._nav_state_path().read_text()
497
+ data = json.loads(raw)
498
+ except (FileNotFoundError, OSError, json.JSONDecodeError):
499
+ return
500
+ if not isinstance(data, dict):
501
+ return
502
+ # Per-workspace remembered pages (only those still present).
503
+ pages = data.get("active_page")
504
+ if isinstance(pages, dict):
505
+ for ws_key, page in pages.items():
506
+ ws = self._workspaces.get(ws_key)
507
+ if ws is not None and page in ws.keys():
508
+ self._model.select_page(page, workspace=ws_key)
509
+ # Active workspace.
510
+ active_ws = data.get("active_workspace")
511
+ if isinstance(active_ws, str) and self._model.has_workspace(active_ws):
512
+ self._model.select_workspace(active_ws)
513
+ # Sidebar mode.
514
+ mode = data.get("sidebar_mode")
515
+ if mode in ("hidden", "compact", "expanded"):
516
+ self._model.set_sidebar_mode(mode)
517
+
518
+ def _save_nav_state(self) -> None:
519
+ import json
520
+ path = self._nav_state_path()
521
+ try:
522
+ path.parent.mkdir(parents=True, exist_ok=True)
523
+ except OSError:
524
+ return
525
+ pages = {
526
+ k: self._model.active_page(k)
527
+ for k in self._workspaces
528
+ if self._model.active_page(k) is not None
529
+ }
530
+ data = {
531
+ "sidebar_mode": self._model.sidebar_mode,
532
+ "active_workspace": self._model.active_workspace,
533
+ "active_page": pages,
534
+ }
535
+ try:
536
+ path.write_text(json.dumps(data))
537
+ except OSError:
538
+ pass
539
+
540
+ def destroy(self) -> None:
541
+ """Persist nav state, close provider subscriptions, then tear down."""
542
+ if getattr(self, "_remember_nav_state", False):
543
+ try:
544
+ self._save_nav_state()
545
+ except Exception:
546
+ pass
547
+ for ws in getattr(self, "_workspaces", {}).values():
548
+ ws.close()
549
+ super().destroy()
550
+
551
+ # ----- Accessors -----
552
+
553
+ @property
554
+ def model(self) -> NavModel:
555
+ """The navigation model (single source of truth)."""
556
+ return self._model
557
+
558
+ @property
559
+ def workspace(self) -> Workspace | None:
560
+ """The active workspace (or `None`)."""
561
+ return self._workspaces.get(self._model.active_workspace)
562
+
563
+ def workspaces(self) -> tuple[Workspace, ...]:
564
+ """All workspaces in insertion order."""
565
+ return tuple(self._workspaces.values())
566
+
567
+ @property
568
+ def provider(self) -> Any:
569
+ """The active workspace's navigation provider (or `None`)."""
570
+ ws = self.workspace
571
+ return ws.provider if ws is not None else None
572
+
573
+ @property
574
+ def pages(self) -> Any:
575
+ """The active static provider's content page deck (or `None`)."""
576
+ return getattr(self.provider, "pages", None)
577
+
578
+ @property
579
+ def nav(self) -> Any:
580
+ """The active provider's sidebar navigation panel (or `None`)."""
581
+ return getattr(self.provider, "nav", None)