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,648 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Iterator, TYPE_CHECKING, overload
4
+
5
+ from bootstack.widgets._impl.composites.tree.treeview import TreeView as _InternalTreeView
6
+ from bootstack.widgets._impl.composites.tree.treenode import TreeNode
7
+ from bootstack.widgets._core.base import PublicWidgetBase
8
+ from bootstack.widgets._core.events import register_widget_events
9
+ from bootstack.events import Subscription, TreeSelectionEvent
10
+ from bootstack.streams import Stream
11
+ from bootstack.widgets.types import AccentToken, WidgetDensity, SelectionMode, ScrollbarVariant
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import Sequence
15
+ from bootstack.data.types import DataSourceProtocol
16
+ from bootstack.data.query import Column, Condition, SortKey
17
+
18
+ __all__ = ["Tree", "TreeNode"]
19
+
20
+
21
+ def _node_key(value: Any) -> Any:
22
+ """Normalize an id for cross-type comparison (mirrors the source binding)."""
23
+ return None if value is None else str(value)
24
+
25
+
26
+ class Tree(PublicWidgetBase):
27
+ """A hierarchical tree for navigation and selection.
28
+
29
+ Tree displays nested data as expandable rows. It is a *hierarchy-first*
30
+ widget — reach for it when the structure is the point (file trees, outlines,
31
+ settings navigation, grouped pickers). For flat records with columns,
32
+ sorting, filtering, and paging, use `DataTable` instead.
33
+
34
+ Nodes are object handles: `add()` returns a `TreeNode` that you hold and
35
+ pass back to `expand()`, `select()`, `remove()`, and so on — there are no
36
+ string ids. Each node carries a `label` and an optional `icon` (with
37
+ optional open/closed variants), plus an open-ended `data` bag for your own
38
+ attributes.
39
+
40
+ Args:
41
+ nodes: Declarative initial tree — a list of node specs, where each spec
42
+ is a label string or a dict like
43
+ `{"label": "src", "icon": "folder", "children": [...]}`. Anything
44
+ not a recognized display key becomes that node's `data`. Mutually
45
+ exclusive with `data_source`.
46
+ data_source: A flat data source to project as a hierarchy. Each record
47
+ carries a `parent_field` pointing at its parent's id (an adjacency
48
+ list); the tree loads one branch at a time, querying a node's
49
+ children only when it is expanded — so a huge hierarchy shows
50
+ instantly. Any data-source-protocol source works. Mutually exclusive
51
+ with `nodes`.
52
+ parent_field: With `data_source`, the record field that holds each row's
53
+ parent id. Defaults to `'parent_id'`.
54
+ root_value: With `data_source`, the `parent_field` value that marks a
55
+ root node. Defaults to `None` (a NULL/absent parent).
56
+ label_field: With `data_source`, the record field used as the node label.
57
+ Defaults to `'name'`. Ignored when `node_builder` is given.
58
+ icon_field: With `data_source`, an optional record field whose value is
59
+ used as the node icon.
60
+ node_builder: With `data_source`, an optional callable taking a record
61
+ and returning a node spec dict (`{'label': ..., 'icon': ...}`) for
62
+ full control over how a record renders. Overrides
63
+ `label_field`/`icon_field`.
64
+ order: With `data_source`, the ordering applied to each sibling group —
65
+ a sort key or sequence of keys (`'name'`, `'-created'`, a `col`,
66
+ or a `SortKey`). Defaults to the source's natural order.
67
+ selection_mode: Node selection behavior — `'single'` keeps one
68
+ highlighted node, `'multi'` is click-to-toggle a set, `'none'` does
69
+ no highlight selection (navigation/display only). Default `'single'`.
70
+ show_selection_controls: If `True`, show a per-node selection control
71
+ (a checkbox in `multi` mode, a radio in `single` mode) as the
72
+ visible affordance for selection — mirroring ListView and DataTable.
73
+ A selected row shows both the control and the highlight wash.
74
+ select_on_click: If `True` (default), clicking a row selects it. Set
75
+ `False` with `show_selection_controls=True` so that only the
76
+ control selects and a row click just focuses (e.g. to drive
77
+ `on_activate` for opening, VS Code style).
78
+ indent: Horizontal indent per depth level, in pixels. Defaults to `16`.
79
+ striped: If `True`, alternate the row background color.
80
+ show_scrollbar: If `True` (default), show the vertical scrollbar.
81
+ Mousewheel scrolling works regardless.
82
+ scrollbar_variant: Scrollbar style — `'thin'` (default) for a slim bar,
83
+ or `'default'` for the standard rounded bar.
84
+ height: Fixed height in pixels. When set, the tree maintains this
85
+ height regardless of its content (so it can scroll without the
86
+ parent layout providing a vertical constraint).
87
+ density: Row height. Default `'default'`.
88
+ accent: Color intent token for the selection highlight. Defaults to the
89
+ theme's default color.
90
+ parent: Override the context-stack parent.
91
+ **kwargs: Layout placement options applied by the parent container —
92
+ `fill`, `expand`, `anchor`, `margin`, `row`, `column`, `sticky`.
93
+ See :doc:`/tasks/layout`.
94
+ """
95
+
96
+ def __init__(
97
+ self,
98
+ *,
99
+ nodes: list[str | dict] | None = None,
100
+ data_source: DataSourceProtocol | None = None,
101
+ parent_field: str = "parent_id",
102
+ root_value: Any = None,
103
+ label_field: str = "name",
104
+ icon_field: str | None = None,
105
+ node_builder: Callable[[dict], dict] | None = None,
106
+ order: str | Column | SortKey | Sequence[str | Column | SortKey] | None = None,
107
+ selection_mode: SelectionMode = "single",
108
+ show_selection_controls: bool = False,
109
+ select_on_click: bool = True,
110
+ indent: int = 16,
111
+ striped: bool = False,
112
+ show_scrollbar: bool = True,
113
+ scrollbar_variant: ScrollbarVariant = "thin",
114
+ height: int | None = None,
115
+ density: WidgetDensity = "default",
116
+ accent: AccentToken | str | None = None,
117
+ accent_selection: bool = True,
118
+ parent: Any = None,
119
+ **kwargs: Any,
120
+ ) -> None:
121
+ self._parent = self._resolve_parent(parent)
122
+ self._selection_mode = selection_mode
123
+ layout_kw = self._split_layout_kwargs(kwargs)
124
+ tk_master = self._parent._child_master() if self._parent else None
125
+
126
+ internal_kwargs: dict[str, Any] = {
127
+ "selection_mode": selection_mode,
128
+ "show_selection_controls": show_selection_controls,
129
+ "select_on_click": select_on_click,
130
+ "indent": indent,
131
+ "striped": striped,
132
+ "scrollbar_visibility": "always" if show_scrollbar else "never",
133
+ "scrollbar_variant": scrollbar_variant,
134
+ "density": density,
135
+ "accent_selection": accent_selection,
136
+ }
137
+ if accent is not None:
138
+ internal_kwargs["accent"] = accent
139
+
140
+ if height is not None and "horizontal" not in layout_kw:
141
+ layout_kw["horizontal"] = "stretch"
142
+
143
+ self._internal = _InternalTreeView(tk_master, **internal_kwargs)
144
+ # Stop the row pool from feeding back into the layout (see ListView).
145
+ self._internal.pack_propagate(False)
146
+ self._attach_to_parent(layout_kw)
147
+
148
+ if height is not None:
149
+ self._internal.configure(height=height)
150
+
151
+ # Data-source backing: project a flat adjacency-list source as a lazy
152
+ # hierarchy. Mutually exclusive with declarative `nodes=`.
153
+ self._binding: Any = None
154
+ if data_source is not None:
155
+ if nodes:
156
+ raise ValueError(
157
+ "Tree: pass either nodes= or data_source=, not both."
158
+ )
159
+ from bootstack.widgets._impl.composites.tree.source_binding import (
160
+ SourceBinding,
161
+ )
162
+
163
+ self._binding = SourceBinding(
164
+ data_source,
165
+ parent_field=parent_field,
166
+ root_value=root_value,
167
+ label_field=label_field,
168
+ icon_field=icon_field,
169
+ node_builder=node_builder,
170
+ order=order,
171
+ )
172
+ self._internal.load(self._binding.roots())
173
+ elif nodes:
174
+ self._internal.load(nodes)
175
+
176
+ # Per-node context menu (set via set_context_menu).
177
+ self._ctx_builder: Callable[[TreeNode, Any], Any] | None = None
178
+ self._ctx_menu: Any = None
179
+ self._ctx_sub: Subscription | None = None
180
+
181
+ # ----- building -----
182
+
183
+ def add(
184
+ self,
185
+ label: str = "",
186
+ *,
187
+ parent: TreeNode | None = None,
188
+ icon: str | None = None,
189
+ open_icon: str | None = None,
190
+ closed_icon: str | None = None,
191
+ expanded: bool = False,
192
+ children: list | None = None,
193
+ loader: Callable[[TreeNode], Any] | None = None,
194
+ data: dict | None = None,
195
+ **extra: Any,
196
+ ) -> TreeNode:
197
+ """Add a node and return its `TreeNode` handle.
198
+
199
+ Args:
200
+ label: The node's display label.
201
+ parent: Parent node, or `None` for a root node.
202
+ icon: Bootstrap icon name shown before the label.
203
+ open_icon: Icon used when the node is expanded (overrides `icon`).
204
+ closed_icon: Icon used when the node is collapsed (overrides `icon`).
205
+ expanded: Whether the node starts expanded.
206
+ children: Optional child specs (same format as `nodes=`).
207
+ loader: Callable invoked on first expand to fetch children lazily.
208
+ Receives the node; returns an iterable of child specs.
209
+ data: Initial data bag for the node.
210
+ **extra: Extra keywords folded into the node's `data` bag.
211
+ """
212
+ return self._internal.add(
213
+ label, parent=parent, icon=icon, open_icon=open_icon,
214
+ closed_icon=closed_icon, expanded=expanded, children=children,
215
+ loader=loader, data=data, **extra,
216
+ )
217
+
218
+ def insert(self, index: int, label: str = "", *, parent: TreeNode | None = None,
219
+ **kwargs: Any) -> TreeNode:
220
+ """Add a node at a specific position among its siblings.
221
+
222
+ Args:
223
+ index: Zero-based position among siblings.
224
+ label: The node's display label.
225
+ parent: Parent node, or `None` for a root node.
226
+ **kwargs: Same as `add()`.
227
+ """
228
+ return self._internal.add(label, parent=parent, index=index, **kwargs)
229
+
230
+ def remove(self, node: TreeNode) -> None:
231
+ """Remove a node (and its descendants).
232
+
233
+ Args:
234
+ node: The node to remove.
235
+ """
236
+ self._internal.remove(node)
237
+
238
+ def move(self, node: TreeNode, parent: TreeNode | None = None,
239
+ index: int | str = "end") -> None:
240
+ """Move a node to a new parent and/or position.
241
+
242
+ Args:
243
+ node: The node to move.
244
+ parent: New parent, or `None` for the root level.
245
+ index: Position among the new siblings — `'end'` or an integer.
246
+ """
247
+ self._internal.move(node, parent, index)
248
+
249
+ def clear(self) -> None:
250
+ """Remove all nodes."""
251
+ self._internal.clear()
252
+
253
+ @property
254
+ def data_source(self) -> DataSourceProtocol | None:
255
+ """The backing data source, or `None` for a declarative tree."""
256
+ return self._binding.source if self._binding is not None else None
257
+
258
+ def refresh(self) -> None:
259
+ """Reload a data-source-backed tree from its source.
260
+
261
+ Re-queries the roots and discards loaded branches, so the tree reflects
262
+ records that were inserted, updated, or deleted in the source. Selection
263
+ and expansion state are reset. A no-op for a declarative tree.
264
+ """
265
+ if self._binding is None:
266
+ return
267
+ self._internal.load(self._binding.roots())
268
+
269
+ # ----- navigation / lookup -----
270
+
271
+ @property
272
+ def roots(self) -> list[TreeNode]:
273
+ """The top-level nodes, in order.
274
+
275
+ Useful for reaching handles after declarative construction
276
+ (`Tree(nodes=...)`), which otherwise returns no `TreeNode` handles.
277
+ """
278
+ return list(self._internal.roots)
279
+
280
+ def walk(self) -> Iterator[TreeNode]:
281
+ """Yield every node depth-first, including collapsed ones."""
282
+ return self._internal._iter_all_nodes()
283
+
284
+ def find(self, matcher: "Callable[[TreeNode], bool] | Condition") -> TreeNode | None:
285
+ """Return the first node matching `matcher`, or `None`.
286
+
287
+ `matcher` is either a predicate `(TreeNode) -> bool` or a `col(...)`
288
+ condition (see :doc:`/reference/data-sources`):
289
+
290
+ - A **predicate** is evaluated over the materialized nodes. For a
291
+ data-source-backed tree that means the currently loaded branches only
292
+ — a Python callable cannot be pushed down to the source.
293
+ - A **condition** is matched against each node's `data`. On a
294
+ declarative tree this scans every node; on a data-source-backed tree
295
+ it is pushed down to the source, so it reaches nodes in unexpanded
296
+ branches, and the path to the match is loaded (without expanding it)
297
+ so a real `TreeNode` handle is returned. Call `reveal()` on the result
298
+ to scroll it into view.
299
+
300
+ Args:
301
+ matcher: A `(TreeNode) -> bool` predicate, or a `col(...)` condition.
302
+ """
303
+ results = self._find(matcher, first=True)
304
+ return results[0] if results else None
305
+
306
+ def find_all(self, matcher: "Callable[[TreeNode], bool] | Condition") -> list[TreeNode]:
307
+ """Return every node matching `matcher`.
308
+
309
+ Accepts the same predicate-or-condition `matcher` as `find()`, with the
310
+ same reach (a predicate sees only materialized nodes; a condition is
311
+ pushed down on a data-source-backed tree). Predicate and declarative
312
+ matches come back in tree order; pushed-down condition matches come back
313
+ in the source's order.
314
+
315
+ Args:
316
+ matcher: A `(TreeNode) -> bool` predicate, or a `col(...)` condition.
317
+ """
318
+ return self._find(matcher, first=False)
319
+
320
+ def _find(self, matcher: Any, *, first: bool) -> list[TreeNode]:
321
+ from bootstack.data.query import Condition
322
+
323
+ # Source-backed tree + condition: push down to reach unloaded branches.
324
+ if isinstance(matcher, Condition) and self._binding is not None:
325
+ records = self._binding.source._query(matcher, self._binding.sort_keys)
326
+ out: list[TreeNode] = []
327
+ for record in records:
328
+ node = self._materialize_path(record)
329
+ if node is not None:
330
+ out.append(node)
331
+ if first:
332
+ break
333
+ return out
334
+
335
+ # Otherwise evaluate over the materialized nodes.
336
+ if isinstance(matcher, Condition):
337
+ predicate: Callable[[TreeNode], bool] = lambda n: matcher.matches(n.data)
338
+ else:
339
+ predicate = matcher
340
+ out = []
341
+ for node in self._internal._iter_all_nodes():
342
+ if predicate(node):
343
+ out.append(node)
344
+ if first:
345
+ break
346
+ return out
347
+
348
+ def _materialize_path(self, record: dict) -> TreeNode | None:
349
+ """Load (without expanding) the path from a root to `record`'s node.
350
+
351
+ Walks `parent_field` up the source to build the root→record id chain,
352
+ then descends the view, loading each level's lazy children as needed,
353
+ and returns the matching `TreeNode` (or `None` if the chain can't be
354
+ resolved — e.g. a broken adjacency link or a custom `node_builder` whose
355
+ `data` omits the id).
356
+ """
357
+ b = self._binding
358
+ src = b.source
359
+
360
+ def ident(rec: Any) -> Any:
361
+ # Normalize identity across raw records (where `_record_id` reads the
362
+ # source's identity column, e.g. SQLite's internal row id) and public
363
+ # node `data` (where the identity is surfaced as `'id'`).
364
+ rid = src._record_id(rec)
365
+ return _node_key(rid if rid is not None else rec.get("id"))
366
+
367
+ # Build the root→record chain by walking parent ids up the source. A
368
+ # row's `parent_field` holds the parent's identity, so fetch each parent
369
+ # by id via `get()` (handles sources whose identity isn't a plain column).
370
+ chain = [record]
371
+ cur = record
372
+ seen: set = set()
373
+ while _node_key(cur.get(b.parent_field)) != _node_key(b.root_value):
374
+ pid = cur.get(b.parent_field)
375
+ key = _node_key(pid)
376
+ if key in seen: # guard a cyclic/broken adjacency list
377
+ break
378
+ seen.add(key)
379
+ parent = src.get(pid)
380
+ if parent is None:
381
+ break
382
+ cur = parent
383
+ chain.append(cur)
384
+ chain.reverse()
385
+
386
+ # Descend the view, loading each level's children as needed.
387
+ level = list(self._internal.roots)
388
+ node: TreeNode | None = None
389
+ for i, rec in enumerate(chain):
390
+ rid = ident(rec)
391
+ node = next((n for n in level if ident(n.data) == rid), None)
392
+ if node is None:
393
+ return None
394
+ if i < len(chain) - 1: # not the target yet — ensure children exist
395
+ if node.loader is not None and not node._loaded:
396
+ self._internal._load_children(node)
397
+ level = node.children
398
+ return node
399
+
400
+ # ----- expansion -----
401
+
402
+ def expand(self, node: TreeNode) -> None:
403
+ """Expand a node to reveal its children."""
404
+ self._internal.expand(node)
405
+
406
+ def collapse(self, node: TreeNode) -> None:
407
+ """Collapse a node to hide its children."""
408
+ self._internal.collapse(node)
409
+
410
+ def toggle(self, node: TreeNode) -> None:
411
+ """Expand the node if collapsed, or collapse it if expanded."""
412
+ self._internal.toggle(node)
413
+
414
+ def expand_all(self) -> None:
415
+ """Expand every node (loading lazy children as needed)."""
416
+ self._internal.expand_all()
417
+
418
+ def collapse_all(self) -> None:
419
+ """Collapse every node."""
420
+ self._internal.collapse_all()
421
+
422
+ def reveal(self, node: TreeNode) -> None:
423
+ """Expand ancestors and scroll so `node` is visible."""
424
+ self._internal.reveal(node)
425
+
426
+ def reload_children(self, node: TreeNode) -> None:
427
+ """Refresh a lazy node's children — drop them and re-fetch via its
428
+ `loader`. If `node` is expanded the children reload immediately;
429
+ otherwise they reload on the next expand. A no-op for non-lazy nodes.
430
+
431
+ Args:
432
+ node: The lazy node (created with `loader=`) to refresh.
433
+ """
434
+ self._internal.reload_children(node)
435
+
436
+ # ----- selection -----
437
+
438
+ @property
439
+ def selection(self) -> TreeNode | list[TreeNode] | None:
440
+ """The selected node(s) — node handles, in tree order.
441
+
442
+ In `'single'` mode, the selected :class:`TreeNode` (or `None` when
443
+ nothing is selected). In `'multi'` mode, a `list` of nodes (empty when
444
+ nothing is selected). Each node's data bag is at `node.data`. Read-only.
445
+ """
446
+ nodes = self._internal.get_selected()
447
+ if self._selection_mode == "multi":
448
+ return nodes
449
+ return nodes[0] if nodes else None
450
+
451
+ def select(self, node: TreeNode) -> None:
452
+ """Select a node (replaces in single mode, adds in multi)."""
453
+ self._internal.select(node)
454
+
455
+ def deselect(self, node: TreeNode) -> None:
456
+ """Remove a node from the selection."""
457
+ self._internal.deselect(node)
458
+
459
+ def select_all(self) -> None:
460
+ """Select every node. Only effective when `selection_mode='multi'`."""
461
+ self._internal.select_all()
462
+
463
+ def clear_selection(self) -> None:
464
+ """Clear the selection."""
465
+ self._internal.clear_selection()
466
+
467
+ # ----- events -----
468
+
469
+ @overload
470
+ def on_select(self) -> Stream: ...
471
+ @overload
472
+ def on_select(self, handler: Callable[[TreeSelectionEvent], Any]) -> Subscription: ...
473
+ def on_select(self, handler: Callable[[TreeSelectionEvent], Any] | None = None) -> Stream | Subscription:
474
+ """Fired when the set of selected nodes changes.
475
+
476
+ Args:
477
+ handler: Called with a :class:`~bootstack.events.TreeSelectionEvent`
478
+ carrying `nodes` (the full selection, in tree order). Omit to get
479
+ a composable :class:`~bootstack.streams.Stream` instead.
480
+
481
+ Returns:
482
+ A cancellable :class:`~bootstack.events.Subscription` when a handler
483
+ is given, otherwise a :class:`~bootstack.streams.Stream`.
484
+ """
485
+ return self.on("select", handler)
486
+
487
+ @overload
488
+ def on_activate(self) -> Stream: ...
489
+ @overload
490
+ def on_activate(self, handler: Callable[[TreeNode], Any]) -> Subscription: ...
491
+ def on_activate(self, handler: Callable[[TreeNode], Any] | None = None) -> Stream | Subscription:
492
+ """Fired when a node is activated (double-click or Enter).
493
+
494
+ Args:
495
+ handler: Called with the activated `TreeNode`. Omit to get a
496
+ composable :class:`~bootstack.streams.Stream` instead.
497
+
498
+ Returns:
499
+ A cancellable :class:`~bootstack.events.Subscription` when a handler
500
+ is given, otherwise a :class:`~bootstack.streams.Stream`.
501
+ """
502
+ return self.on("activate", handler)
503
+
504
+ @overload
505
+ def on_expand(self) -> Stream: ...
506
+ @overload
507
+ def on_expand(self, handler: Callable[[TreeNode], Any]) -> Subscription: ...
508
+ def on_expand(self, handler: Callable[[TreeNode], Any] | None = None) -> Stream | Subscription:
509
+ """Fired when a node is expanded.
510
+
511
+ Args:
512
+ handler: Called with the expanded `TreeNode`. Omit to get a
513
+ composable :class:`~bootstack.streams.Stream` instead.
514
+
515
+ Returns:
516
+ A cancellable :class:`~bootstack.events.Subscription` when a handler
517
+ is given, otherwise a :class:`~bootstack.streams.Stream`.
518
+ """
519
+ return self.on("expand", handler)
520
+
521
+ @overload
522
+ def on_collapse(self) -> Stream: ...
523
+ @overload
524
+ def on_collapse(self, handler: Callable[[TreeNode], Any]) -> Subscription: ...
525
+ def on_collapse(self, handler: Callable[[TreeNode], Any] | None = None) -> Stream | Subscription:
526
+ """Fired when a node is collapsed.
527
+
528
+ Args:
529
+ handler: Called with the collapsed `TreeNode`. Omit to get a
530
+ composable :class:`~bootstack.streams.Stream` instead.
531
+
532
+ Returns:
533
+ A cancellable :class:`~bootstack.events.Subscription` when a handler
534
+ is given, otherwise a :class:`~bootstack.streams.Stream`.
535
+ """
536
+ return self.on("collapse", handler)
537
+
538
+ @overload
539
+ def on_right_click(self) -> Stream: ...
540
+ @overload
541
+ def on_right_click(self, handler: Callable[[dict[str, Any]], Any]) -> Subscription: ...
542
+ def on_right_click(self, handler: Callable[[dict[str, Any]], Any] | None = None) -> Stream | Subscription:
543
+ """Fired on a right-click.
544
+
545
+ Args:
546
+ handler: Called with a `dict` carrying `node`, `x_root`, and
547
+ `y_root` — enough to position a context menu. Omit to get a
548
+ composable :class:`~bootstack.streams.Stream` instead.
549
+
550
+ Returns:
551
+ A cancellable :class:`~bootstack.events.Subscription` when a handler
552
+ is given, otherwise a :class:`~bootstack.streams.Stream`.
553
+ """
554
+ return self.on("right_click", handler)
555
+
556
+ # ----- context menu -----
557
+
558
+ def set_context_menu(
559
+ self,
560
+ builder: Callable[[TreeNode, Any], Any] | None,
561
+ *,
562
+ min_width: int = 150,
563
+ density: WidgetDensity = "default",
564
+ ) -> None:
565
+ """Attach a per-node context menu.
566
+
567
+ On right-click, `builder(node, menu)` is called with the right-clicked
568
+ `TreeNode` and a fresh (emptied) `ContextMenu`; populate it with
569
+ `menu.add_item(...)` / `add_check_item(...)` / `add_separator()` etc.
570
+ (capture `node` in your callbacks). The menu is then shown at the cursor.
571
+ Because it is rebuilt on every right-click, the items can depend on the
572
+ node. If the builder adds no items, nothing is shown. Pass `None` to
573
+ remove the menu.
574
+
575
+ Example::
576
+
577
+ def build(node, menu):
578
+ menu.add_item("Rename", on_click=lambda: rename(node))
579
+ menu.add_item("Delete", icon="trash", on_click=lambda: tree.remove(node))
580
+ tree.set_context_menu(build)
581
+
582
+ Args:
583
+ builder: Callable taking `(node, menu)` and populating `menu`, or
584
+ `None` to detach.
585
+ min_width: Minimum menu width in pixels.
586
+ density: Menu item density — `'default'` or `'compact'`.
587
+ """
588
+ self._ctx_builder = builder
589
+ if builder is None:
590
+ if self._ctx_sub is not None:
591
+ self._ctx_sub.cancel()
592
+ self._ctx_sub = None
593
+ return
594
+ # Menu-level options changed? Drop the cached menu so it's rebuilt.
595
+ opts = (min_width, density)
596
+ if getattr(self, "_ctx_opts", None) != opts:
597
+ self._ctx_opts = opts
598
+ if self._ctx_menu is not None:
599
+ self._ctx_menu.destroy()
600
+ self._ctx_menu = None
601
+ if self._ctx_sub is None:
602
+ self._ctx_sub = self.on_right_click(self._show_context_menu)
603
+
604
+ def _show_context_menu(self, data: dict[str, Any]) -> None:
605
+ node = data.get("node")
606
+ if node is None or self._ctx_builder is None:
607
+ return
608
+ from bootstack.widgets.contextmenu import ContextMenu
609
+ # Reuse one menu, cleared each time, so repeated right-clicks don't
610
+ # accumulate popups.
611
+ if self._ctx_menu is None:
612
+ min_width, density = getattr(self, "_ctx_opts", (150, "default"))
613
+ self._ctx_menu = ContextMenu(
614
+ parent=self, trigger=None, min_width=min_width, density=density
615
+ )
616
+ menu = self._ctx_menu
617
+ for key in list(menu.keys):
618
+ menu.remove_item(key)
619
+ self._ctx_builder(node, menu)
620
+ if menu.keys: # only show if the builder added something
621
+ menu.show((data.get("x_root", 0), data.get("y_root", 0)))
622
+
623
+ # ----- lifecycle -----
624
+
625
+ def destroy(self) -> None:
626
+ """Detach the context menu and destroy the widget (cancels pending
627
+ relayout/type-ahead callbacks on the internal view)."""
628
+ if self._ctx_sub is not None:
629
+ self._ctx_sub.cancel()
630
+ self._ctx_sub = None
631
+ if self._ctx_menu is not None:
632
+ try:
633
+ self._ctx_menu.destroy()
634
+ except Exception:
635
+ pass
636
+ self._ctx_menu = None
637
+ self._internal.destroy()
638
+
639
+
640
+ _TREE_EVENTS: dict[str, str] = {
641
+ "select": "<<TreeSelectionChange>>",
642
+ "activate": "<<TreeActivate>>",
643
+ "expand": "<<TreeExpand>>",
644
+ "collapse": "<<TreeCollapse>>",
645
+ "right_click": "<<TreeRightClick>>",
646
+ }
647
+
648
+ register_widget_events(Tree, _TREE_EVENTS)