bootstack 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. bootstack/__init__.py +249 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/api/__init__.py +127 -0
  4. bootstack/api/app.py +30 -0
  5. bootstack/api/constants.py +3 -0
  6. bootstack/api/data.py +23 -0
  7. bootstack/api/dialogs.py +44 -0
  8. bootstack/api/i18n.py +17 -0
  9. bootstack/api/localization.py +16 -0
  10. bootstack/api/menu.py +7 -0
  11. bootstack/api/style.py +23 -0
  12. bootstack/api/utils.py +24 -0
  13. bootstack/api/widgets.py +137 -0
  14. bootstack/assets/__init__.py +24 -0
  15. bootstack/assets/bootstack-transparent.png +0 -0
  16. bootstack/assets/bootstack.ico +0 -0
  17. bootstack/assets/bootstack.png +0 -0
  18. bootstack/assets/elements/__init__.py +0 -0
  19. bootstack/assets/elements/badge-pill.png +0 -0
  20. bootstack/assets/elements/badge-square.png +0 -0
  21. bootstack/assets/elements/border.png +0 -0
  22. bootstack/assets/elements/button-compact.png +0 -0
  23. bootstack/assets/elements/button-default.png +0 -0
  24. bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
  25. bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
  26. bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
  27. bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
  28. bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
  29. bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
  30. bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
  31. bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
  32. bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
  33. bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
  34. bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
  35. bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
  36. bootstack/assets/elements/checkbox-checked.png +0 -0
  37. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  38. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  39. bootstack/assets/elements/field.png +0 -0
  40. bootstack/assets/elements/input-after-compact.png +0 -0
  41. bootstack/assets/elements/input-after-default.png +0 -0
  42. bootstack/assets/elements/input-before-compact.png +0 -0
  43. bootstack/assets/elements/input-before-default.png +0 -0
  44. bootstack/assets/elements/input-compact.png +0 -0
  45. bootstack/assets/elements/input-default.png +0 -0
  46. bootstack/assets/elements/list-item-separated.png +0 -0
  47. bootstack/assets/elements/list-item.png +0 -0
  48. bootstack/assets/elements/manifest.toml +480 -0
  49. bootstack/assets/elements/menu-item.png +0 -0
  50. bootstack/assets/elements/nav-button-compact.png +0 -0
  51. bootstack/assets/elements/nav-button-default.png +0 -0
  52. bootstack/assets/elements/nav-icon-button-compact.png +0 -0
  53. bootstack/assets/elements/nav-icon-button-default.png +0 -0
  54. bootstack/assets/elements/notebook-client-border.png +0 -0
  55. bootstack/assets/elements/notebook-tab-active.png +0 -0
  56. bootstack/assets/elements/notebook-tab-bar.png +0 -0
  57. bootstack/assets/elements/notebook-tab-normal.png +0 -0
  58. bootstack/assets/elements/notebook-tab-pill.png +0 -0
  59. bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
  60. bootstack/assets/elements/progress-bar-solid.png +0 -0
  61. bootstack/assets/elements/progress-bar-thin.png +0 -0
  62. bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
  63. bootstack/assets/elements/radio-selected.png +0 -0
  64. bootstack/assets/elements/radio-unselected.png +0 -0
  65. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  66. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  67. bootstack/assets/elements/slider-handle-focus.png +0 -0
  68. bootstack/assets/elements/slider-handle.png +0 -0
  69. bootstack/assets/elements/slider-track-horizontal.png +0 -0
  70. bootstack/assets/elements/slider-track-vertical.png +0 -0
  71. bootstack/assets/elements/switch-off.png +0 -0
  72. bootstack/assets/elements/switch-on.png +0 -0
  73. bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
  74. bootstack/assets/elements/tabs-bar-vertical.png +0 -0
  75. bootstack/assets/elements/tabs-pill.png +0 -0
  76. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  77. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
  78. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
  79. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  80. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
  81. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  82. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
  83. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  84. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
  85. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  86. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
  87. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  88. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
  89. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  90. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
  91. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  92. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
  93. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  94. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
  95. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  96. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
  97. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  98. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
  99. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  100. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
  101. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  102. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
  103. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  104. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
  105. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  106. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
  107. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  108. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
  109. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  110. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
  111. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  112. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
  113. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  114. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
  115. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  116. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
  117. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  118. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
  119. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  120. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
  121. bootstack/assets/themes/__init__.py +0 -0
  122. bootstack/assets/themes/amber-dark.json +32 -0
  123. bootstack/assets/themes/amber-light.json +32 -0
  124. bootstack/assets/themes/aurora-dark.json +32 -0
  125. bootstack/assets/themes/aurora-light.json +32 -0
  126. bootstack/assets/themes/bootstrap-dark.json +32 -0
  127. bootstack/assets/themes/bootstrap-light.json +32 -0
  128. bootstack/assets/themes/classic-dark.json +32 -0
  129. bootstack/assets/themes/classic-light.json +32 -0
  130. bootstack/assets/themes/docs-dark.json +32 -0
  131. bootstack/assets/themes/docs-light.json +32 -0
  132. bootstack/assets/themes/forest-dark.json +32 -0
  133. bootstack/assets/themes/forest-light.json +32 -0
  134. bootstack/assets/themes/ocean-dark.json +32 -0
  135. bootstack/assets/themes/ocean-light.json +32 -0
  136. bootstack/assets/themes/rose-dark.json +32 -0
  137. bootstack/assets/themes/rose-light.json +32 -0
  138. bootstack/assets/widgets/__init__.py +0 -0
  139. bootstack/assets/widgets/badge-default.png +0 -0
  140. bootstack/assets/widgets/badge-pill.png +0 -0
  141. bootstack/assets/widgets/border.png +0 -0
  142. bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
  143. bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
  144. bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
  145. bootstack/assets/widgets/button-group-vertical-after.png +0 -0
  146. bootstack/assets/widgets/button-group-vertical-before.png +0 -0
  147. bootstack/assets/widgets/button-group-vertical-center.png +0 -0
  148. bootstack/assets/widgets/button.png +0 -0
  149. bootstack/assets/widgets/checkbox-checked.png +0 -0
  150. bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
  151. bootstack/assets/widgets/checkbox-unchecked.png +0 -0
  152. bootstack/assets/widgets/field.png +0 -0
  153. bootstack/assets/widgets/icon-button.png +0 -0
  154. bootstack/assets/widgets/input-inner.png +0 -0
  155. bootstack/assets/widgets/input-prefix.png +0 -0
  156. bootstack/assets/widgets/input-suffix.png +0 -0
  157. bootstack/assets/widgets/input.png +0 -0
  158. bootstack/assets/widgets/list-item-focus.png +0 -0
  159. bootstack/assets/widgets/list-item-separated.png +0 -0
  160. bootstack/assets/widgets/menu-item-separated.png +0 -0
  161. bootstack/assets/widgets/notebook-client-border.png +0 -0
  162. bootstack/assets/widgets/notebook-pill-active.png +0 -0
  163. bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
  164. bootstack/assets/widgets/notebook-tab-active.png +0 -0
  165. bootstack/assets/widgets/notebook-tab-border.png +0 -0
  166. bootstack/assets/widgets/notebook-tab-normal.png +0 -0
  167. bootstack/assets/widgets/notebook-underline.png +0 -0
  168. bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
  169. bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
  170. bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
  171. bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
  172. bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
  173. bootstack/assets/widgets/progress-trough-vertical.png +0 -0
  174. bootstack/assets/widgets/radio-selected.png +0 -0
  175. bootstack/assets/widgets/radio-unselected.png +0 -0
  176. bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
  177. bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
  178. bootstack/assets/widgets/separator-horizontal.png +0 -0
  179. bootstack/assets/widgets/separator-vertical.png +0 -0
  180. bootstack/assets/widgets/slider-handle-focus.png +0 -0
  181. bootstack/assets/widgets/slider-handle.png +0 -0
  182. bootstack/assets/widgets/slider-track-horizontal.png +0 -0
  183. bootstack/assets/widgets/slider-track-vertical.png +0 -0
  184. bootstack/assets/widgets/switch-off.png +0 -0
  185. bootstack/assets/widgets/switch-on.png +0 -0
  186. bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
  187. bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
  188. bootstack/assets/widgets/tabs-pill.png +0 -0
  189. bootstack/cli/__init__.py +124 -0
  190. bootstack/cli/__main__.py +6 -0
  191. bootstack/cli/add.py +439 -0
  192. bootstack/cli/build.py +115 -0
  193. bootstack/cli/config.py +287 -0
  194. bootstack/cli/demo.py +1267 -0
  195. bootstack/cli/doctor.py +195 -0
  196. bootstack/cli/list_cmd.py +71 -0
  197. bootstack/cli/promote.py +120 -0
  198. bootstack/cli/pyinstaller.py +246 -0
  199. bootstack/cli/run.py +99 -0
  200. bootstack/cli/start.py +105 -0
  201. bootstack/cli/templates/__init__.py +861 -0
  202. bootstack/constants.py +325 -0
  203. bootstack/core/__init__.py +34 -0
  204. bootstack/core/capabilities/__init__.py +45 -0
  205. bootstack/core/capabilities/after.py +103 -0
  206. bootstack/core/capabilities/bind.py +154 -0
  207. bootstack/core/capabilities/bindtags.py +112 -0
  208. bootstack/core/capabilities/busy.py +61 -0
  209. bootstack/core/capabilities/clipboard.py +88 -0
  210. bootstack/core/capabilities/focus.py +118 -0
  211. bootstack/core/capabilities/grab.py +65 -0
  212. bootstack/core/capabilities/grid.py +188 -0
  213. bootstack/core/capabilities/localization.py +231 -0
  214. bootstack/core/capabilities/pack.py +119 -0
  215. bootstack/core/capabilities/place.py +92 -0
  216. bootstack/core/capabilities/selection.py +136 -0
  217. bootstack/core/capabilities/signals.py +242 -0
  218. bootstack/core/capabilities/winfo.py +315 -0
  219. bootstack/core/colorutils.py +234 -0
  220. bootstack/core/exceptions.py +95 -0
  221. bootstack/core/images.py +283 -0
  222. bootstack/core/localization/README.md +90 -0
  223. bootstack/core/localization/__init__.py +13 -0
  224. bootstack/core/localization/intl_format.py +580 -0
  225. bootstack/core/localization/msgcat.py +425 -0
  226. bootstack/core/localization/specs.py +143 -0
  227. bootstack/core/mixins/__init__.py +1 -0
  228. bootstack/core/mixins/ttk_state.py +35 -0
  229. bootstack/core/mixins/widget.py +132 -0
  230. bootstack/core/publisher.py +147 -0
  231. bootstack/core/signals/README.md +112 -0
  232. bootstack/core/signals/__init__.py +8 -0
  233. bootstack/core/signals/integration.py +100 -0
  234. bootstack/core/signals/signal.py +317 -0
  235. bootstack/core/signals/types.py +4 -0
  236. bootstack/core/validation/__init__.py +5 -0
  237. bootstack/core/validation/types.py +13 -0
  238. bootstack/core/validation/validation_result.py +17 -0
  239. bootstack/core/validation/validation_rules.py +112 -0
  240. bootstack/core/variables.py +62 -0
  241. bootstack/datasource/README.md +607 -0
  242. bootstack/datasource/__init__.py +51 -0
  243. bootstack/datasource/base.py +474 -0
  244. bootstack/datasource/file_source.py +541 -0
  245. bootstack/datasource/memory_source.py +482 -0
  246. bootstack/datasource/sqlite_source.py +453 -0
  247. bootstack/datasource/types.py +259 -0
  248. bootstack/dialogs/__init__.py +56 -0
  249. bootstack/dialogs/colorchooser.py +674 -0
  250. bootstack/dialogs/colordropper.py +257 -0
  251. bootstack/dialogs/datedialog.py +404 -0
  252. bootstack/dialogs/dialog.py +514 -0
  253. bootstack/dialogs/filterdialog.py +358 -0
  254. bootstack/dialogs/fontdialog.py +339 -0
  255. bootstack/dialogs/formdialog.py +541 -0
  256. bootstack/dialogs/message.py +489 -0
  257. bootstack/dialogs/query.py +561 -0
  258. bootstack/py.typed +1 -0
  259. bootstack/runtime/__init__.py +3 -0
  260. bootstack/runtime/app.py +879 -0
  261. bootstack/runtime/base_window.py +786 -0
  262. bootstack/runtime/events.py +399 -0
  263. bootstack/runtime/menu.py +510 -0
  264. bootstack/runtime/shortcuts.py +423 -0
  265. bootstack/runtime/tk_patch.py +31 -0
  266. bootstack/runtime/toplevel.py +131 -0
  267. bootstack/runtime/utility.py +371 -0
  268. bootstack/runtime/visual_focus.py +228 -0
  269. bootstack/runtime/window_utilities.py +1043 -0
  270. bootstack/style/__init__.py +5498 -0
  271. bootstack/style/bootstyle.py +507 -0
  272. bootstack/style/bootstyle_builder_base.py +752 -0
  273. bootstack/style/bootstyle_builder_mixed.py +93 -0
  274. bootstack/style/bootstyle_builder_tk.py +109 -0
  275. bootstack/style/bootstyle_builder_ttk.py +354 -0
  276. bootstack/style/builders/__init__.py +51 -0
  277. bootstack/style/builders/badge.py +44 -0
  278. bootstack/style/builders/button.py +453 -0
  279. bootstack/style/builders/buttongroup.py +344 -0
  280. bootstack/style/builders/calendar.py +271 -0
  281. bootstack/style/builders/checkbutton.py +95 -0
  282. bootstack/style/builders/combobox.py +112 -0
  283. bootstack/style/builders/contextmenu.py +268 -0
  284. bootstack/style/builders/entry.py +83 -0
  285. bootstack/style/builders/expander.py +171 -0
  286. bootstack/style/builders/field.py +312 -0
  287. bootstack/style/builders/frame.py +27 -0
  288. bootstack/style/builders/label.py +28 -0
  289. bootstack/style/builders/labelframe.py +41 -0
  290. bootstack/style/builders/listview.py +267 -0
  291. bootstack/style/builders/menubar.py +74 -0
  292. bootstack/style/builders/menubutton.py +408 -0
  293. bootstack/style/builders/notebook.py +316 -0
  294. bootstack/style/builders/panedwindow.py +25 -0
  295. bootstack/style/builders/progressbar.py +71 -0
  296. bootstack/style/builders/radiobutton.py +68 -0
  297. bootstack/style/builders/scale.py +66 -0
  298. bootstack/style/builders/scrollbar.py +360 -0
  299. bootstack/style/builders/separator.py +45 -0
  300. bootstack/style/builders/sidenav.py +313 -0
  301. bootstack/style/builders/sizegrip.py +15 -0
  302. bootstack/style/builders/spinbox.py +119 -0
  303. bootstack/style/builders/switch.py +67 -0
  304. bootstack/style/builders/tabitem.py +205 -0
  305. bootstack/style/builders/toolbutton.py +260 -0
  306. bootstack/style/builders/tooltip.py +26 -0
  307. bootstack/style/builders/treeview.py +269 -0
  308. bootstack/style/builders/utils.py +404 -0
  309. bootstack/style/builders_tk/__init__.py +16 -0
  310. bootstack/style/builders_tk/defaults.py +229 -0
  311. bootstack/style/element.py +173 -0
  312. bootstack/style/style.py +499 -0
  313. bootstack/style/theme_provider.py +449 -0
  314. bootstack/style/tk_patch.py +5 -0
  315. bootstack/style/token_maps.py +42 -0
  316. bootstack/style/types.py +32 -0
  317. bootstack/style/typography.py +527 -0
  318. bootstack/style/utility.py +696 -0
  319. bootstack/themes/__init__.py +12 -0
  320. bootstack/themes/standard.py +415 -0
  321. bootstack/themes/user.py +45 -0
  322. bootstack/widgets/__init__.py +53 -0
  323. bootstack/widgets/composites/__init__.py +38 -0
  324. bootstack/widgets/composites/accordion.py +385 -0
  325. bootstack/widgets/composites/appshell.py +445 -0
  326. bootstack/widgets/composites/buttongroup.py +391 -0
  327. bootstack/widgets/composites/calendar.py +914 -0
  328. bootstack/widgets/composites/compositeframe.py +282 -0
  329. bootstack/widgets/composites/contextmenu.py +1754 -0
  330. bootstack/widgets/composites/dateentry.py +261 -0
  331. bootstack/widgets/composites/dropdownbutton.py +190 -0
  332. bootstack/widgets/composites/expander.py +508 -0
  333. bootstack/widgets/composites/field.py +448 -0
  334. bootstack/widgets/composites/floodgauge.py +434 -0
  335. bootstack/widgets/composites/form.py +983 -0
  336. bootstack/widgets/composites/labeledscale.py +209 -0
  337. bootstack/widgets/composites/list/__init__.py +15 -0
  338. bootstack/widgets/composites/list/listitem.py +733 -0
  339. bootstack/widgets/composites/list/listview.py +1507 -0
  340. bootstack/widgets/composites/menubar.py +303 -0
  341. bootstack/widgets/composites/meter.py +882 -0
  342. bootstack/widgets/composites/numericentry.py +183 -0
  343. bootstack/widgets/composites/pagestack.py +330 -0
  344. bootstack/widgets/composites/passwordentry.py +149 -0
  345. bootstack/widgets/composites/pathentry.py +223 -0
  346. bootstack/widgets/composites/radiogroup.py +466 -0
  347. bootstack/widgets/composites/scrolledtext.py +388 -0
  348. bootstack/widgets/composites/scrolledtext.pyi +186 -0
  349. bootstack/widgets/composites/scrollview.py +675 -0
  350. bootstack/widgets/composites/selectbox.py +544 -0
  351. bootstack/widgets/composites/sidenav/__init__.py +24 -0
  352. bootstack/widgets/composites/sidenav/group.py +485 -0
  353. bootstack/widgets/composites/sidenav/header.py +83 -0
  354. bootstack/widgets/composites/sidenav/item.py +413 -0
  355. bootstack/widgets/composites/sidenav/separator.py +51 -0
  356. bootstack/widgets/composites/sidenav/view.py +919 -0
  357. bootstack/widgets/composites/spinnerentry.py +232 -0
  358. bootstack/widgets/composites/tableview/__init__.py +5 -0
  359. bootstack/widgets/composites/tableview/tableview.py +2254 -0
  360. bootstack/widgets/composites/tableview/types.py +169 -0
  361. bootstack/widgets/composites/tabs/__init__.py +6 -0
  362. bootstack/widgets/composites/tabs/tabitem.py +372 -0
  363. bootstack/widgets/composites/tabs/tabs.py +478 -0
  364. bootstack/widgets/composites/tabs/tabview.py +352 -0
  365. bootstack/widgets/composites/textentry.py +90 -0
  366. bootstack/widgets/composites/timeentry.py +189 -0
  367. bootstack/widgets/composites/toast.py +364 -0
  368. bootstack/widgets/composites/togglegroup.py +382 -0
  369. bootstack/widgets/composites/toolbar.py +393 -0
  370. bootstack/widgets/composites/tooltip.py +404 -0
  371. bootstack/widgets/internal/__init__.py +0 -0
  372. bootstack/widgets/internal/wrapper_base.py +304 -0
  373. bootstack/widgets/mixins/__init__.py +25 -0
  374. bootstack/widgets/mixins/configure_mixin.py +186 -0
  375. bootstack/widgets/mixins/entry_mixin.py +70 -0
  376. bootstack/widgets/mixins/font_mixin.py +346 -0
  377. bootstack/widgets/mixins/icon_mixin.py +38 -0
  378. bootstack/widgets/mixins/localization_mixin.py +255 -0
  379. bootstack/widgets/mixins/signal_mixin.py +272 -0
  380. bootstack/widgets/mixins/validation_mixin.py +204 -0
  381. bootstack/widgets/parts/__init__.py +11 -0
  382. bootstack/widgets/parts/numberentry_part.py +345 -0
  383. bootstack/widgets/parts/spinnerentry_part.py +394 -0
  384. bootstack/widgets/parts/textentry_part.py +344 -0
  385. bootstack/widgets/primitives/__init__.py +55 -0
  386. bootstack/widgets/primitives/badge.py +44 -0
  387. bootstack/widgets/primitives/button.py +89 -0
  388. bootstack/widgets/primitives/card.py +66 -0
  389. bootstack/widgets/primitives/checkbutton.py +124 -0
  390. bootstack/widgets/primitives/checktoggle.py +53 -0
  391. bootstack/widgets/primitives/combobox.py +165 -0
  392. bootstack/widgets/primitives/entry.py +98 -0
  393. bootstack/widgets/primitives/frame.py +206 -0
  394. bootstack/widgets/primitives/gridframe.py +479 -0
  395. bootstack/widgets/primitives/label.py +95 -0
  396. bootstack/widgets/primitives/labelframe.py +63 -0
  397. bootstack/widgets/primitives/menubutton.py +118 -0
  398. bootstack/widgets/primitives/notebook.py +551 -0
  399. bootstack/widgets/primitives/optionmenu.py +248 -0
  400. bootstack/widgets/primitives/packframe.py +228 -0
  401. bootstack/widgets/primitives/panedwindow.py +58 -0
  402. bootstack/widgets/primitives/progressbar.py +95 -0
  403. bootstack/widgets/primitives/radiobutton.py +115 -0
  404. bootstack/widgets/primitives/radiotoggle.py +50 -0
  405. bootstack/widgets/primitives/scale.py +85 -0
  406. bootstack/widgets/primitives/scrollbar.py +56 -0
  407. bootstack/widgets/primitives/separator.py +56 -0
  408. bootstack/widgets/primitives/sizegrip.py +47 -0
  409. bootstack/widgets/primitives/spinbox.py +91 -0
  410. bootstack/widgets/primitives/switch.py +41 -0
  411. bootstack/widgets/primitives/treeview.py +77 -0
  412. bootstack/widgets/types.py +20 -0
  413. bootstack-0.1.0a1.dist-info/METADATA +196 -0
  414. bootstack-0.1.0a1.dist-info/RECORD +419 -0
  415. bootstack-0.1.0a1.dist-info/WHEEL +5 -0
  416. bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
  417. bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
  418. bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
  419. bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,919 @@
1
+ """SideNav widget - a sidebar navigation container."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from tkinter import Variable
6
+ from typing import Any, Literal, TYPE_CHECKING
7
+
8
+ from typing_extensions import TypedDict, Unpack
9
+
10
+ from bootstack.widgets.primitives.frame import Frame
11
+ from bootstack.widgets.primitives.gridframe import GridFrame
12
+ from bootstack.widgets.primitives.button import Button
13
+ from bootstack.widgets.primitives.label import Label
14
+ from bootstack.widgets.primitives.separator import Separator
15
+ from bootstack.widgets.composites.scrollview import ScrollView
16
+ from bootstack.widgets.composites.toolbar import Toolbar
17
+ from bootstack.widgets.composites.sidenav.item import SideNavItem
18
+ from bootstack.widgets.composites.sidenav.group import SideNavGroup
19
+ from bootstack.widgets.composites.sidenav.header import SideNavHeader
20
+ from bootstack.widgets.composites.sidenav.separator import SideNavSeparator
21
+ from bootstack.widgets.mixins import configure_delegate
22
+ from bootstack.widgets.types import Master
23
+ from bootstack.core.signals import Signal
24
+
25
+
26
+ DisplayMode = Literal['expanded', 'compact', 'minimal']
27
+
28
+
29
+ class SideNavKwargs(TypedDict, total=False):
30
+ title: str
31
+ show_header: bool
32
+ show_back_button: bool
33
+ collapsible: bool
34
+ display_mode: DisplayMode
35
+ is_pane_open: bool
36
+ pane_width: int
37
+ signal: Any
38
+ variable: Variable
39
+ accent: str
40
+ # Frame options
41
+ padding: Any
42
+ width: int
43
+ height: int
44
+ surface: str
45
+
46
+
47
+ class SideNav(Frame):
48
+ """A sidebar navigation container with header, scrollable items, and footer.
49
+
50
+ SideNav provides a complete navigation solution with:
51
+ - Pane header with optional title and menu button
52
+ - Scrollable navigation items area
53
+ - Groups for organizing related items (expand/collapse in expanded mode,
54
+ popup flyout in compact mode)
55
+ - Footer section for settings and other fixed items
56
+ - Display modes: expanded (full), compact (icons only), minimal (hidden)
57
+
58
+ The pane manages a shared selection variable/signal that all items use
59
+ for radio-group behavior.
60
+
61
+ !!! note "Events"
62
+ - `<<PaneToggled>>`: Fired when pane is opened/closed.
63
+ `event.data = {'is_open': bool}`
64
+ - `<<DisplayModeChanged>>`: Fired when display mode changes.
65
+ `event.data = {'mode': str}`
66
+ - `<<SelectionChanged>>`: Fired when selected item changes.
67
+ `event.data = {'key': str}`
68
+ - `<<BackRequested>>`: Fired when back button is clicked.
69
+
70
+ Example:
71
+ ```python
72
+ nav = SideNav(root, title='My App')
73
+
74
+ # Add root-level items
75
+ nav.add_item('home', text='Home', icon='house')
76
+ nav.add_item('docs', text='Documents', icon='file-earmark-text')
77
+
78
+ # Add a group with items
79
+ nav.add_group('files', text='Files', icon='folder')
80
+ nav.add_item('local', text='Local', icon='hdd', group='files')
81
+ nav.add_item('cloud', text='Cloud', icon='cloud', group='files')
82
+
83
+ # Add section header
84
+ nav.add_header('Favorites')
85
+ nav.add_item('photos', text='Photos', icon='image')
86
+
87
+ # Add footer item
88
+ nav.add_footer_item('settings', text='Settings', icon='gear')
89
+
90
+ # Bind to selection
91
+ nav.on_selection_changed(lambda e: print(f"Selected: {e.data['key']}"))
92
+ ```
93
+ """
94
+
95
+ # Default pane widths
96
+ PANE_WIDTH_EXPANDED = 280
97
+ PANE_WIDTH_COMPACT = 52
98
+
99
+ def __init__(
100
+ self,
101
+ master: Master = None,
102
+ title: str = '',
103
+ show_header: bool = True,
104
+ show_back_button: bool = False,
105
+ collapsible: bool = True,
106
+ display_mode: DisplayMode = 'expanded',
107
+ is_pane_open: bool = True,
108
+ pane_width: int = None,
109
+ signal: 'Signal[str]' = None,
110
+ variable: Variable = None,
111
+ accent: str = 'primary',
112
+ **kwargs: Unpack[SideNavKwargs]
113
+ ):
114
+ """Initialize a SideNav.
115
+
116
+ Args:
117
+ master (Master | None): Parent widget.
118
+ title (str): Title displayed in the pane header.
119
+ show_header (bool): Show internal header with toolbar. Default True.
120
+ Set to False when using an external toolbar.
121
+ show_back_button (bool): Show back button in header. Default False.
122
+ collapsible (bool): Allow pane to collapse. Shows hamburger menu. Default True.
123
+ display_mode (DisplayMode): Initial display mode. Default 'expanded'.
124
+ is_pane_open (bool): Initial pane state. Default True.
125
+ pane_width (int | None): Custom pane width. Uses default based on mode.
126
+ signal (Signal | None): Reactive signal for selection state.
127
+ variable (Variable | None): Tk variable for selection state.
128
+ accent (str): Accent color for selection indicators. Default 'primary'.
129
+ **kwargs: Additional arguments passed to Frame.
130
+ """
131
+ super().__init__(master, **kwargs)
132
+
133
+ self._accent = accent
134
+
135
+ self._title = title
136
+ self._show_header = show_header
137
+ self._show_back_button = show_back_button
138
+ self._collapsible = collapsible
139
+ self._display_mode = display_mode
140
+ self._is_pane_open = is_pane_open
141
+ self._pane_width = pane_width
142
+
143
+ # Selection state - create signal if neither provided
144
+ if signal is not None:
145
+ self._signal = signal
146
+ elif variable is not None:
147
+ self._signal = None
148
+ self._variable = variable
149
+ else:
150
+ # Create internal signal for selection
151
+ self._signal = Signal('')
152
+
153
+ # Get the variable for items (from signal or direct)
154
+ self._selection_var = self._signal._var if self._signal else variable
155
+
156
+ # Track variable changes for efficient selection updates
157
+ self._prev_selection: str | None = None # Track previous selection for efficient updates
158
+ if self._selection_var:
159
+ self._selection_var.trace_add('write', self._on_selection_changed)
160
+
161
+ # Item and group tracking
162
+ self._items: dict[str, SideNavItem] = {} # All items by key
163
+ self._item_to_group: dict[str, str] = {} # item_key -> group_key lookup
164
+ self._groups: dict[str, SideNavGroup] = {} # Groups by key
165
+ self._footer_items: dict[str, SideNavItem] = {}
166
+ self._headers: list[SideNavHeader] = []
167
+ self._separators: list[SideNavSeparator] = []
168
+
169
+ # Track all content widgets in order for proper re-packing
170
+ self._content_widgets: list = []
171
+ self._footer_order: list[str] = []
172
+
173
+ # Widget references
174
+ self._pane_frame: Frame | None = None
175
+ self._header_frame: Frame | None = None
176
+ self._content_scroll: ScrollView | None = None
177
+ self._content_frame: Frame | None = None
178
+ self._footer_frame: Frame | None = None
179
+ self._menu_button: Button | None = None
180
+ self._menu_separator: Separator | None = None
181
+ self._back_button: Button | None = None
182
+ self._title_label: Label | None = None
183
+
184
+ # Build the widget
185
+ self._build_widget()
186
+
187
+ # Initialize previous selection tracker from current value
188
+ if self._selection_var:
189
+ self._prev_selection = self._selection_var.get() or None
190
+
191
+ def _build_widget(self):
192
+ """Build the internal widget structure."""
193
+ # Pane container - uses 'chrome' surface for UI chrome
194
+ pane_width = self._pane_width or self.PANE_WIDTH_EXPANDED
195
+ # padding=(left, top, right, bottom): right=0 so the scrollbar lane in
196
+ # the ScrollView runs flush to the pane edge with no trailing gap.
197
+ self._pane_frame = Frame(self, width=pane_width, padding=(4, 4, 0, 4), surface='chrome')
198
+ self._pane_frame.pack(side='left', fill='y')
199
+ self._pane_frame.pack_propagate(False) # Fixed width
200
+
201
+ # Hamburger menu button at top (if collapsible)
202
+ if self._collapsible:
203
+ self._menu_button = Button(
204
+ self._pane_frame,
205
+ icon='list',
206
+ icon_only=True,
207
+ variant='ghost',
208
+ command=self.toggle_pane,
209
+ padding=(8, 6),
210
+ )
211
+ # Pack based on initial mode - compact stretches, expanded doesn't
212
+ if self._display_mode == 'compact':
213
+ self._menu_button.pack(fill='x')
214
+ else:
215
+ self._menu_button.pack(anchor='w')
216
+
217
+ # Separator after menu button
218
+ self._menu_separator = Separator(self._pane_frame, orient='horizontal')
219
+ self._menu_separator.pack(fill='x', pady=4)
220
+
221
+ # Header section (optional - can be disabled when using external toolbar)
222
+ self._toolbar = None
223
+ if self._show_header:
224
+ self._build_header()
225
+
226
+ # Scrollable content area (vertical only, scrollbar on hover)
227
+ self._content_scroll = ScrollView(
228
+ self._pane_frame,
229
+ scroll_direction='vertical',
230
+ scrollbar_visibility='hover',
231
+ )
232
+ self._content_scroll.pack(fill='both', expand=True)
233
+ self._content_frame = GridFrame(
234
+ self._content_scroll.canvas,
235
+ columns=1,
236
+ gap=(0, 2),
237
+ sticky_items='ew',
238
+ padding=(4, 0),
239
+ )
240
+ self._content_scroll.add(self._content_frame)
241
+
242
+ # Stretch content to fill width when canvas resizes (debounced)
243
+ self._resize_after_id = None
244
+
245
+ def on_canvas_resize(event):
246
+ # Debounce resize events to avoid excessive layout recalculations
247
+ if self._resize_after_id:
248
+ self.after_cancel(self._resize_after_id)
249
+ self._resize_after_id = self.after(16, lambda: self._apply_canvas_width(event.width))
250
+
251
+ self._content_scroll.canvas.bind('<Configure>', on_canvas_resize, add='+')
252
+
253
+ # Footer section
254
+ self._footer_frame = Frame(self._pane_frame, padding=(4, 0))
255
+ self._footer_frame.pack(side='bottom', fill='x')
256
+
257
+ # Separator before footer (shown when footer has items)
258
+ self._footer_separator = Separator(self._pane_frame, orient='horizontal')
259
+
260
+ # Apply initial display mode
261
+ self._apply_display_mode()
262
+
263
+ def _apply_canvas_width(self, width: int):
264
+ """Apply the canvas width after debounce delay."""
265
+ self._resize_after_id = None
266
+ if self._content_scroll and self._content_scroll.winfo_exists():
267
+ self._content_scroll.canvas.itemconfigure(
268
+ self._content_scroll._window_id,
269
+ width=width
270
+ )
271
+
272
+ def _build_header(self):
273
+ """Build the pane header using Toolbar."""
274
+ # Create toolbar for header
275
+ self._toolbar = Toolbar(self._pane_frame, padding=(4, 4))
276
+ self._toolbar.pack(fill='x')
277
+
278
+ # Back button (optional)
279
+ if self._show_back_button:
280
+ self._back_button = self._toolbar.add_button(
281
+ icon={'name': 'arrow-left', 'size': 16},
282
+ command=self._on_back_clicked,
283
+ )
284
+
285
+ # Title
286
+ if self._title:
287
+ self._title_label = self._toolbar.add_label(
288
+ text=self._title,
289
+ font='heading-md',
290
+ )
291
+
292
+ def _apply_display_mode(self):
293
+ """Apply the current display mode to the pane."""
294
+ is_compact = self._display_mode == 'compact'
295
+
296
+ # Update pane width and visibility
297
+ if self._display_mode == 'expanded':
298
+ width = self._pane_width or self.PANE_WIDTH_EXPANDED
299
+ self._pane_frame.configure(width=width, padding=(4, 4, 0, 4))
300
+ if self._is_pane_open:
301
+ self._pane_frame.pack(side='left', fill='y')
302
+ else:
303
+ self._pane_frame.pack_forget()
304
+ elif self._display_mode == 'compact':
305
+ self._pane_frame.configure(width=self.PANE_WIDTH_COMPACT, padding=(2, 2, 0, 2))
306
+ self._pane_frame.pack(side='left', fill='y')
307
+ elif self._display_mode == 'minimal':
308
+ if self._is_pane_open:
309
+ width = self._pane_width or self.PANE_WIDTH_EXPANDED
310
+ self._pane_frame.configure(width=width)
311
+ self._pane_frame.pack(side='left', fill='y')
312
+ else:
313
+ self._pane_frame.pack_forget()
314
+
315
+ # Update menu button pack options based on mode
316
+ if self._collapsible and self._menu_button:
317
+ if is_compact:
318
+ self._menu_button.pack_configure(fill='x', anchor='center')
319
+ else:
320
+ self._menu_button.pack_configure(fill='none', anchor='w')
321
+
322
+ # Hide section headers in compact mode
323
+ for header in self._headers:
324
+ if is_compact:
325
+ header.grid_remove()
326
+ else:
327
+ header.grid()
328
+
329
+ # Hide only the title in compact mode (keep back and menu buttons visible)
330
+ if self._title_label:
331
+ if is_compact:
332
+ self._title_label.pack_forget()
333
+ else:
334
+ self._title_label.pack(side='left', fill='x', expand=True)
335
+
336
+ # In compact mode the pane is icon-only (52 px) and content never
337
+ # overflows vertically, so reserving a scrollbar gutter wastes ~25 % of
338
+ # the pane width. Switch the ScrollView to 'never' visibility to
339
+ # release the gutter; restore 'hover' (with stable gutter) when the
340
+ # pane is expanded again.
341
+ if self._content_scroll:
342
+ if is_compact:
343
+ self._content_scroll.configure(scrollbar_visibility='never')
344
+ else:
345
+ self._content_scroll.configure(scrollbar_visibility='hover')
346
+
347
+ # Set compact mode on all items and groups
348
+ for item in self._items.values():
349
+ # Only set compact on root items (not in groups)
350
+ if not self._get_item_group(item.key):
351
+ item.set_compact(is_compact)
352
+
353
+ for group in self._groups.values():
354
+ group.set_compact(is_compact)
355
+
356
+ for item in self._footer_items.values():
357
+ item.set_compact(is_compact)
358
+
359
+ def _get_item_group(self, key: str) -> str | None:
360
+ """Get the group key for an item, or None if at root.
361
+
362
+ Uses O(1) lookup via _item_to_group cache.
363
+ """
364
+ return self._item_to_group.get(key)
365
+
366
+ def _on_selection_changed(self, *args):
367
+ """Handle selection variable changes.
368
+
369
+ This centralizes selection state updates for better performance.
370
+ Instead of each item tracing the variable (O(n) callbacks), we only
371
+ update the previously-selected and newly-selected items (O(1)).
372
+ """
373
+ if not self._selection_var:
374
+ return
375
+
376
+ new_key = self._selection_var.get()
377
+ old_key = self._prev_selection
378
+
379
+ # Skip if selection hasn't actually changed
380
+ if new_key == old_key:
381
+ return
382
+
383
+ # Update the previously selected item (deselect)
384
+ if old_key:
385
+ old_group = self._get_item_group(old_key)
386
+ if old_key in self._items:
387
+ self._items[old_key].set_selected(False)
388
+ elif old_key in self._footer_items:
389
+ self._footer_items[old_key].set_selected(False)
390
+
391
+ # Update old group's selection state
392
+ if old_group and old_group in self._groups:
393
+ self._groups[old_group].set_child_selected(False)
394
+
395
+ # Update the newly selected item (select)
396
+ new_group = None
397
+ if new_key:
398
+ new_group = self._get_item_group(new_key)
399
+ if new_key in self._items:
400
+ self._items[new_key].set_selected(True)
401
+ elif new_key in self._footer_items:
402
+ self._footer_items[new_key].set_selected(True)
403
+
404
+ # Update new group's selection state
405
+ if new_group and new_group in self._groups:
406
+ self._groups[new_group].set_child_selected(True)
407
+
408
+ # Track current selection for next change
409
+ self._prev_selection = new_key
410
+
411
+ # Fire selection changed event
412
+ self.event_generate('<<SelectionChanged>>', data={'key': new_key})
413
+
414
+ def _on_back_clicked(self):
415
+ """Handle back button click."""
416
+ self.event_generate('<<BackRequested>>')
417
+
418
+ # --- Public API: Groups ---
419
+
420
+ def add_group(
421
+ self,
422
+ key: str,
423
+ text: str = '',
424
+ icon: str | dict = None,
425
+ is_expanded: bool = False,
426
+ **kwargs
427
+ ) -> SideNavGroup:
428
+ """Add a navigation group to the pane.
429
+
430
+ Groups contain related items and can be expanded/collapsed in expanded
431
+ mode. In compact mode, clicking a group shows a popup with its items.
432
+
433
+ Args:
434
+ key (str): Unique identifier for the group.
435
+ text (str): Display text.
436
+ icon (str | dict | None): Icon name or configuration.
437
+ is_expanded (bool): Initial expansion state. Default False.
438
+ **kwargs: Additional arguments passed to SideNavGroup.
439
+
440
+ Returns:
441
+ SideNavGroup: The created group.
442
+
443
+ Raises:
444
+ ValueError: If a group or item with the given key already exists.
445
+ """
446
+ if key in self._groups or key in self._items or key in self._footer_items:
447
+ raise ValueError(f"Key '{key}' already exists")
448
+
449
+ # Use view's accent as default, allow override via kwargs
450
+ group_kwargs = {'accent': self._accent, **kwargs}
451
+
452
+ group = SideNavGroup(
453
+ self._content_frame,
454
+ key=key,
455
+ text=text,
456
+ icon=icon,
457
+ variable=self._selection_var,
458
+ is_expanded=is_expanded,
459
+ **group_kwargs
460
+ )
461
+ group.grid()
462
+ self._groups[key] = group
463
+ self._content_widgets.append(group)
464
+
465
+ # Apply current display mode
466
+ if self._display_mode == 'compact':
467
+ group.set_compact(True)
468
+
469
+ return group
470
+
471
+ # --- Public API: Items ---
472
+
473
+ def add_item(
474
+ self,
475
+ key: str,
476
+ text: str = '',
477
+ icon: str | dict = None,
478
+ group: str = None,
479
+ **kwargs
480
+ ) -> SideNavItem:
481
+ """Add a navigation item to the pane.
482
+
483
+ Args:
484
+ key (str): Unique identifier for the item.
485
+ text (str): Display text.
486
+ icon (str | dict | None): Icon name or configuration.
487
+ group (str | None): Key of the group to add this item to.
488
+ If None, item is added at root level.
489
+ **kwargs: Additional arguments passed to SideNavItem.
490
+
491
+ Returns:
492
+ SideNavItem: The created item.
493
+
494
+ Raises:
495
+ ValueError: If an item with the given key already exists.
496
+ ValueError: If the specified group does not exist.
497
+ """
498
+ if key in self._items or key in self._groups or key in self._footer_items:
499
+ raise ValueError(f"Key '{key}' already exists")
500
+
501
+ # Use view's accent as default, allow override via kwargs
502
+ item_kwargs = {'accent': self._accent, **kwargs}
503
+
504
+ if group is not None:
505
+ # Add to a group
506
+ if group not in self._groups:
507
+ raise ValueError(f"Group '{group}' does not exist")
508
+
509
+ target_group = self._groups[group]
510
+ item = SideNavItem(
511
+ target_group.content_frame,
512
+ key=key,
513
+ text=text,
514
+ icon=icon,
515
+ variable=self._selection_var,
516
+ indent_level=0,
517
+ **item_kwargs
518
+ )
519
+ item.grid()
520
+
521
+ # Register with group and update lookup cache
522
+ target_group._add_item(item)
523
+ self._item_to_group[key] = group
524
+ else:
525
+ # Add at root level
526
+ item = SideNavItem(
527
+ self._content_frame,
528
+ key=key,
529
+ text=text,
530
+ icon=icon,
531
+ variable=self._selection_var,
532
+ **item_kwargs
533
+ )
534
+ item.grid()
535
+ self._content_widgets.append(item)
536
+
537
+ # Apply current display mode
538
+ if self._display_mode == 'compact':
539
+ item.set_compact(True)
540
+
541
+ self._items[key] = item
542
+ return item
543
+
544
+ def add_header(self, text: str, **kwargs) -> SideNavHeader:
545
+ """Add a section header to the pane.
546
+
547
+ Headers are hidden in compact display mode.
548
+
549
+ Args:
550
+ text (str): Header text.
551
+ **kwargs: Additional arguments passed to SideNavHeader.
552
+
553
+ Returns:
554
+ SideNavHeader: The created header.
555
+ """
556
+ header = SideNavHeader(self._content_frame, text=text, **kwargs)
557
+ self._headers.append(header)
558
+ header.grid()
559
+ self._content_widgets.append(header)
560
+
561
+ # Hide if in compact mode
562
+ if self._display_mode == 'compact':
563
+ header.grid_remove()
564
+
565
+ return header
566
+
567
+ def add_separator(self, **kwargs) -> SideNavSeparator:
568
+ """Add a separator to the pane.
569
+
570
+ Args:
571
+ **kwargs: Additional arguments passed to SideNavSeparator.
572
+
573
+ Returns:
574
+ SideNavSeparator: The created separator.
575
+ """
576
+ sep = SideNavSeparator(self._content_frame, **kwargs)
577
+ sep.grid()
578
+ self._content_widgets.append(sep)
579
+ self._separators.append(sep)
580
+ return sep
581
+
582
+ def add_footer_item(
583
+ self,
584
+ key: str,
585
+ text: str = '',
586
+ icon: str | dict = None,
587
+ **kwargs
588
+ ) -> SideNavItem:
589
+ """Add a navigation item to the footer section.
590
+
591
+ Args:
592
+ key (str): Unique identifier for the item.
593
+ text (str): Display text.
594
+ icon (str | dict | None): Icon name or configuration.
595
+ **kwargs: Additional arguments passed to SideNavItem.
596
+
597
+ Returns:
598
+ SideNavItem: The created item.
599
+
600
+ Raises:
601
+ ValueError: If an item with the given key already exists.
602
+ """
603
+ if key in self._items or key in self._groups or key in self._footer_items:
604
+ raise ValueError(f"Key '{key}' already exists")
605
+
606
+ # Show footer separator if this is the first footer item
607
+ if not self._footer_items:
608
+ self._footer_separator.pack(fill='x', pady=4)
609
+
610
+ # Use view's accent as default, allow override via kwargs
611
+ item_kwargs = {'accent': self._accent, **kwargs}
612
+
613
+ item = SideNavItem(
614
+ self._footer_frame,
615
+ key=key,
616
+ text=text,
617
+ icon=icon,
618
+ variable=self._selection_var,
619
+ **item_kwargs
620
+ )
621
+ item.pack(fill='x')
622
+
623
+ self._footer_items[key] = item
624
+ self._footer_order.append(key)
625
+
626
+ # Apply current display mode
627
+ if self._display_mode == 'compact':
628
+ item.set_compact(True)
629
+
630
+ return item
631
+
632
+ def node(self, key: str) -> SideNavItem:
633
+ """Get an item by key.
634
+
635
+ Args:
636
+ key (str): The item key.
637
+
638
+ Returns:
639
+ SideNavItem: The item.
640
+
641
+ Raises:
642
+ KeyError: If no item with the given key exists.
643
+ """
644
+ if key in self._items:
645
+ return self._items[key]
646
+ if key in self._footer_items:
647
+ return self._footer_items[key]
648
+ raise KeyError(f"No item with key '{key}'")
649
+
650
+ def nodes(self) -> tuple[SideNavItem, ...]:
651
+ """Get all items (excluding footer items).
652
+
653
+ Returns:
654
+ A tuple of all SideNavItem instances.
655
+ """
656
+ return tuple(self._items.values())
657
+
658
+ def node_keys(self) -> tuple[str, ...]:
659
+ """Get all item keys (excluding footer items).
660
+
661
+ Returns:
662
+ A tuple of all item keys.
663
+ """
664
+ return tuple(self._items.keys())
665
+
666
+ def group(self, key: str) -> SideNavGroup:
667
+ """Get a group by key.
668
+
669
+ Args:
670
+ key (str): The group key.
671
+
672
+ Returns:
673
+ SideNavGroup: The group.
674
+
675
+ Raises:
676
+ KeyError: If no group with the given key exists.
677
+ """
678
+ if key not in self._groups:
679
+ raise KeyError(f"No group with key '{key}'")
680
+ return self._groups[key]
681
+
682
+ def groups(self) -> tuple[SideNavGroup, ...]:
683
+ """Get all groups.
684
+
685
+ Returns:
686
+ A tuple of all SideNavGroup instances.
687
+ """
688
+ return tuple(self._groups.values())
689
+
690
+ def configure_node(self, key: str, option: str = None, **kwargs: Any):
691
+ """Configure a specific item by its key.
692
+
693
+ Args:
694
+ key: The key of the item to configure.
695
+ option: If provided, return the value of this option.
696
+ **kwargs: Configuration options to apply to the item.
697
+
698
+ Returns:
699
+ If option is provided, returns the value of that option.
700
+ """
701
+ item = self.node(key)
702
+ if option is not None:
703
+ return item.cget(option)
704
+ item.configure(**kwargs)
705
+
706
+ def remove_item(self, key: str) -> None:
707
+ """Remove an item by key.
708
+
709
+ Args:
710
+ key (str): The item key to remove.
711
+ """
712
+ if key in self._items:
713
+ item = self._items.pop(key)
714
+
715
+ # Remove from group if in one (check before removing from lookup)
716
+ group_key = self._item_to_group.pop(key, None)
717
+ if group_key:
718
+ self._groups[group_key]._remove_item(key)
719
+
720
+ # Remove from content widgets if at root
721
+ if item in self._content_widgets:
722
+ self._content_widgets.remove(item)
723
+
724
+ item.destroy()
725
+
726
+ elif key in self._footer_items:
727
+ item = self._footer_items.pop(key)
728
+ self._footer_order.remove(key)
729
+ item.destroy()
730
+ # Hide footer separator if no more footer items
731
+ if not self._footer_items:
732
+ self._footer_separator.pack_forget()
733
+
734
+ def remove_group(self, key: str) -> None:
735
+ """Remove a group and all its items.
736
+
737
+ Args:
738
+ key (str): The group key to remove.
739
+ """
740
+ if key not in self._groups:
741
+ return
742
+
743
+ group = self._groups.pop(key)
744
+
745
+ # Remove all items in the group and clean up lookups
746
+ for item_key in list(group._items.keys()):
747
+ if item_key in self._items:
748
+ del self._items[item_key]
749
+ self._item_to_group.pop(item_key, None)
750
+
751
+ # Remove from content widgets
752
+ if group in self._content_widgets:
753
+ self._content_widgets.remove(group)
754
+
755
+ group.destroy()
756
+
757
+ def select(self, key: str) -> None:
758
+ """Select an item by key.
759
+
760
+ Args:
761
+ key (str): The item key to select.
762
+ """
763
+ if self._selection_var:
764
+ self._selection_var.set(key)
765
+
766
+ # --- Public API: Pane Control ---
767
+
768
+ def toggle_pane(self) -> None:
769
+ """Toggle the pane between expanded and compact modes.
770
+
771
+ In minimal mode, this toggles visibility instead.
772
+ """
773
+ if self._display_mode == 'minimal':
774
+ # In minimal mode, toggle visibility
775
+ self._is_pane_open = not self._is_pane_open
776
+ self._apply_display_mode()
777
+ self.event_generate('<<PaneToggled>>', data={'is_open': self._is_pane_open})
778
+ else:
779
+ # Toggle between expanded and compact
780
+ new_mode = 'compact' if self._display_mode == 'expanded' else 'expanded'
781
+ self.set_display_mode(new_mode)
782
+
783
+ def open_pane(self) -> None:
784
+ """Open the pane."""
785
+ if not self._is_pane_open:
786
+ self._is_pane_open = True
787
+ self._apply_display_mode()
788
+ self.event_generate('<<PaneToggled>>', data={'is_open': True})
789
+
790
+ def close_pane(self) -> None:
791
+ """Close the pane."""
792
+ if self._is_pane_open:
793
+ self._is_pane_open = False
794
+ self._apply_display_mode()
795
+ self.event_generate('<<PaneToggled>>', data={'is_open': False})
796
+
797
+ def set_display_mode(self, mode: DisplayMode) -> None:
798
+ """Set the display mode.
799
+
800
+ Args:
801
+ mode (DisplayMode): 'expanded', 'compact', or 'minimal'.
802
+ """
803
+ if mode != self._display_mode:
804
+ self._display_mode = mode
805
+ self._apply_display_mode()
806
+ self.event_generate('<<DisplayModeChanged>>', data={'mode': mode})
807
+
808
+ # --- Properties ---
809
+
810
+ @property
811
+ def is_pane_open(self) -> bool:
812
+ """Check if the pane is open."""
813
+ return self._is_pane_open
814
+
815
+ @property
816
+ def display_mode(self) -> DisplayMode:
817
+ """Get the current display mode."""
818
+ return self._display_mode
819
+
820
+ @property
821
+ def selected_key(self) -> str | None:
822
+ """Get the currently selected item key."""
823
+ if self._selection_var:
824
+ return self._selection_var.get() or None
825
+ return None
826
+
827
+ def footer_nodes(self) -> tuple[SideNavItem, ...]:
828
+ """Get all footer items in order.
829
+
830
+ Returns:
831
+ A tuple of all footer SideNavItem instances.
832
+ """
833
+ return tuple(self._footer_items[key] for key in self._footer_order)
834
+
835
+ def footer_node_keys(self) -> tuple[str, ...]:
836
+ """Get all footer item keys in order.
837
+
838
+ Returns:
839
+ A tuple of all footer item keys.
840
+ """
841
+ return tuple(self._footer_order)
842
+
843
+ @property
844
+ def signal(self) -> 'Signal[str] | None':
845
+ """Get the selection signal."""
846
+ return self._signal
847
+
848
+ @property
849
+ def variable(self) -> Variable | None:
850
+ """Get the selection variable."""
851
+ return self._selection_var
852
+
853
+ # --- Configuration Delegates ---
854
+
855
+ @configure_delegate('title')
856
+ def _delegate_title(self, value: str = None):
857
+ """Configure the pane title."""
858
+ if value is None:
859
+ return self._title
860
+ self._title = value
861
+ if self._title_label:
862
+ self._title_label.configure(text=value)
863
+ return None
864
+
865
+ @configure_delegate('accent')
866
+ def _delegate_accent(self, value: str = None):
867
+ """Configure the accent color (read-only after creation)."""
868
+ if value is None:
869
+ return self._accent
870
+ # Accent is read-only after creation since changing it would
871
+ # require rebuilding all item styles
872
+ return None
873
+
874
+ # --- Event Binding Helpers ---
875
+
876
+ def on_selection_changed(self, callback) -> str:
877
+ """Bind to `<<SelectionChanged>>`.
878
+
879
+ Args:
880
+ callback: Function to call when selection changes.
881
+
882
+ Returns:
883
+ str: Binding identifier.
884
+ """
885
+ return self.bind('<<SelectionChanged>>', callback, add='+')
886
+
887
+ def off_selection_changed(self, bind_id: str = None) -> None:
888
+ """Unbind from `<<SelectionChanged>>`."""
889
+ self.unbind('<<SelectionChanged>>', bind_id)
890
+
891
+ def on_back_requested(self, callback) -> str:
892
+ """Bind to `<<BackRequested>>`.
893
+
894
+ Args:
895
+ callback: Function to call when back button is clicked.
896
+
897
+ Returns:
898
+ str: Binding identifier.
899
+ """
900
+ return self.bind('<<BackRequested>>', callback, add='+')
901
+
902
+ def off_back_requested(self, bind_id: str = None) -> None:
903
+ """Unbind from `<<BackRequested>>`."""
904
+ self.unbind('<<BackRequested>>', bind_id)
905
+
906
+ def on_pane_toggled(self, callback) -> str:
907
+ """Bind to `<<PaneToggled>>`.
908
+
909
+ Args:
910
+ callback: Function to call when pane is toggled.
911
+
912
+ Returns:
913
+ str: Binding identifier.
914
+ """
915
+ return self.bind('<<PaneToggled>>', callback, add='+')
916
+
917
+ def off_pane_toggled(self, bind_id: str = None) -> None:
918
+ """Unbind from `<<PaneToggled>>`."""
919
+ self.unbind('<<PaneToggled>>', bind_id)