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,393 @@
1
+ """TreeItem — a single recycled row in the Tree's virtual view.
2
+
3
+ Mirrors `ListItem` (a `CompositeFrame` composing on-demand child widgets with
4
+ synchronized hover/focus/selected state) and adds the two things a tree row
5
+ needs that a flat list row does not: a depth indent and an expander chevron.
6
+
7
+ A row is recycled: `update_data(record)` re-points it at whichever visible node
8
+ currently occupies its slot. The record is built by the internal `TreeView`.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from tkinter import TclError
14
+ from typing import Any, Optional
15
+
16
+ from bootstack.widgets._impl.composites.compositeframe import CompositeFrame
17
+ from bootstack.widgets._impl.primitives.button import Button
18
+ from bootstack.widgets._impl.primitives.frame import Frame
19
+ from bootstack.widgets._impl.primitives.label import Label
20
+ from bootstack.widgets._impl.primitives.separator import Separator
21
+ from bootstack.widgets.types import Master
22
+
23
+
24
+ # Empty-slot sentinel (a record with this key hides the row).
25
+ EMPTY = {"__empty__": True}
26
+
27
+
28
+ class TreeItem(CompositeFrame):
29
+ """A recycled tree row: indent, chevron, selection control, icon, label."""
30
+
31
+ def __init__(
32
+ self,
33
+ master: Master = None,
34
+ *,
35
+ indent: int = 16,
36
+ chevron_width: int = 26,
37
+ focusable: bool = True,
38
+ hoverable: bool = True,
39
+ selection_mode: str = "single",
40
+ show_selection_controls: bool = False,
41
+ select_on_click: bool = True,
42
+ show_separator: bool = False,
43
+ density: str = "default",
44
+ accent: Optional[str] = None,
45
+ accent_selection: bool = False,
46
+ ) -> None:
47
+ self._data: dict = {}
48
+ self._state: dict = {}
49
+ self._node = None
50
+ self._item_index = 0
51
+
52
+ self._indent = indent
53
+ self._chevron_width = chevron_width
54
+ self._focusable = focusable
55
+ self._hoverable = hoverable
56
+ self._selection_mode = selection_mode
57
+ self._show_selection_controls = show_selection_controls
58
+ self._select_on_click = select_on_click
59
+ self._show_separator = show_separator
60
+ self._density = density
61
+ self._accent = accent or "primary"
62
+ # Selected rows always show the wash, even alongside a selection control
63
+ # (checkbox/radio): wash + control, consistent with DataTable.
64
+ self._wash = True
65
+ # Neutral selection wash by default; opt in to an accent-tinted wash.
66
+ self._accent_selection = accent_selection
67
+
68
+ item_padding = (6, 3) if density == "compact" else (8, 4)
69
+
70
+ super().__init__(
71
+ master=master,
72
+ select_on_click=False,
73
+ variant="separated_item" if show_separator else "item",
74
+ takefocus=focusable,
75
+ ttk_class="ListView.TFrame",
76
+ padding=item_padding,
77
+ accent=self._accent,
78
+ style_options=dict(hoverable=hoverable, density=density, wash=self._wash,
79
+ accent_selection=accent_selection),
80
+ )
81
+
82
+ style_opts = dict(hoverable=hoverable, density=density, wash=self._wash,
83
+ accent_selection=accent_selection)
84
+
85
+ # Left region: [<indent padx> chevron-slot][checkbox?][icon]
86
+ self._left_frame = Frame(
87
+ self, variant="list", ttk_class="ListView.TFrame",
88
+ takefocus=False, accent=self._accent, style_options=style_opts,
89
+ )
90
+ self._left_frame.pack(side="left", fill="y")
91
+
92
+ self._center_frame = Frame(
93
+ self, variant="list", ttk_class="ListView.TFrame",
94
+ takefocus=False, accent=self._accent, style_options=style_opts,
95
+ )
96
+ self._center_frame.pack(side="left", fill="x", expand=True)
97
+
98
+ # Fixed-width chevron slot. The chevron button is PLACED inside it (place
99
+ # children don't influence the slot's requested width), so the gutter is
100
+ # exactly `chevron_width` whether or not a chevron is shown — leaf and
101
+ # parent rows at the same depth line up perfectly. The depth indent is
102
+ # applied as LEFT PADDING on this slot via pack_configure: a ttk frame
103
+ # won't shrink its -width back down once set, so a width-resized spacer
104
+ # goes stale on recycle; padx is reliable and the gap shows the left
105
+ # frame's background (which already follows the row's wash).
106
+ self._chevron_slot = Frame(
107
+ self._left_frame, variant="list", ttk_class="ListView.TFrame",
108
+ takefocus=False, accent=self._accent, style_options=style_opts,
109
+ width=chevron_width,
110
+ )
111
+ self._chevron_slot.pack(side="left", fill="y")
112
+ self._chevron_btn: Optional[Button] = None
113
+
114
+ self.register_composite(self._chevron_slot)
115
+
116
+ self._select_ctrl: Optional[Label] = None
117
+ self._icon_widget: Optional[Label] = None
118
+ self._label_widget: Optional[Label] = None
119
+
120
+ self._separator = Separator(self)
121
+
122
+ for w in (self._left_frame, self._center_frame):
123
+ self.register_composite(w)
124
+
125
+ # Selection control (checkbox for multi, radio for single) — the visible
126
+ # affordance for selection, the same 'selection' variant ListView uses.
127
+ # Its fill is driven by the row's 'selected' state via the coordinator;
128
+ # the 'alternate' state shows a partially-selected (mixed) parent.
129
+ if self._show_selection_controls and self._selection_mode != "none":
130
+ if self._selection_mode == "multi":
131
+ glyph = dict(name="square", state=[
132
+ ("selected", "check-square-fill"),
133
+ ("alternate", "dash-square-fill"),
134
+ ])
135
+ else:
136
+ glyph = dict(name="circle", state=[("selected", "check-circle-fill")])
137
+ self._select_ctrl = Label(
138
+ self._left_frame, icon=glyph, variant="selection",
139
+ ttk_class="ListView.TLabel", icon_only=True, accent=self._accent,
140
+ takefocus=False, style_options=self._style_opts(),
141
+ )
142
+ self._select_ctrl.pack(side="left", after=self._chevron_slot, padx=(0, 4))
143
+ self.register_composite(self._select_ctrl)
144
+ # When the row itself doesn't select on click, the control still must
145
+ # — give it its own click path (the row's invoke is gated below).
146
+ if not self._select_on_click:
147
+ self._select_ctrl.bind("<Button-1>", self._on_ctrl_click, add="+")
148
+
149
+ # Row interactions. Double-/right-click must be bound on the leaf widgets
150
+ # too — a click lands on the label/icon, not the row frame, and Tk does
151
+ # not propagate it to the parent's bindings.
152
+ if self._focusable:
153
+ self.on_invoked(self._on_invoke)
154
+ self.bind("<FocusIn>", self._on_focus_in, add="+")
155
+ for w in (self, self._left_frame, self._center_frame,
156
+ self._chevron_slot):
157
+ self._bind_row_mouse(w)
158
+
159
+ # ----- helpers -----
160
+
161
+ def _style_opts(self) -> dict:
162
+ return dict(hoverable=self._hoverable, density=self._density, wash=self._wash,
163
+ accent_selection=self._accent_selection)
164
+
165
+ def _bind_row_mouse(self, widget) -> None:
166
+ """Bind double-click (activate) and right-click on a row-level widget."""
167
+ widget.bind("<Double-Button-1>", self._on_activate, add="+")
168
+ widget.bind("<Button-3>", self._on_right_click, add="+")
169
+ widget.bind("<Button-2>", self._on_right_click, add="+") # mac
170
+
171
+ @property
172
+ def node(self):
173
+ """The TreeNode currently rendered in this row (or None)."""
174
+ return self._node
175
+
176
+ # ----- row -> container events -----
177
+
178
+ def _emit(self, sequence: str, data: Any) -> None:
179
+ try:
180
+ self.master.event_generate(sequence, data=data)
181
+ except TclError:
182
+ pass
183
+
184
+ def _on_invoke(self, event) -> None:
185
+ if self._node is None:
186
+ return
187
+ if self._focusable:
188
+ self.focus()
189
+ # When select_on_click is off, a row click only focuses (the selection
190
+ # control is the sole way to select). The control routes through
191
+ # _on_ctrl_click in that case.
192
+ if self._select_on_click:
193
+ self._emit("<<TreeItemSelect>>", self._node)
194
+
195
+ def _on_ctrl_click(self, event=None) -> str:
196
+ if self._node is not None:
197
+ if self._focusable:
198
+ self.focus()
199
+ self._emit("<<TreeItemSelect>>", self._node)
200
+ return "break"
201
+
202
+ def _on_focus_in(self, event) -> None:
203
+ if self._node is not None:
204
+ self._emit("<<TreeItemFocus>>", self._node)
205
+
206
+ def _on_activate(self, event=None) -> str:
207
+ if self._node is not None:
208
+ self._emit("<<TreeItemActivate>>", self._node)
209
+ return "break"
210
+
211
+ def _on_right_click(self, event) -> str:
212
+ if self._node is not None:
213
+ self._emit(
214
+ "<<TreeItemRightClick>>",
215
+ {"node": self._node, "x_root": event.x_root, "y_root": event.y_root},
216
+ )
217
+ return "break"
218
+
219
+ def _on_chevron(self) -> None:
220
+ if self._node is not None:
221
+ self._emit("<<TreeItemToggle>>", self._node)
222
+
223
+ # ----- widget updates -----
224
+
225
+ def _update_chevron(self, expandable: bool, expanded: bool) -> None:
226
+ if expandable:
227
+ icon = "chevron-down" if expanded else "chevron-right"
228
+ if self._chevron_btn is None:
229
+ self._chevron_btn = Button(
230
+ self._chevron_slot, icon=icon, icon_only=True, variant="icon",
231
+ ttk_class="ListView.TButton", takefocus=False,
232
+ accent=self._accent, command=self._on_chevron,
233
+ style_options=self._style_opts(),
234
+ )
235
+ self.register_composite(self._chevron_btn)
236
+ else:
237
+ self._chevron_btn.configure(icon=icon)
238
+ if not self._chevron_btn.winfo_manager():
239
+ self._chevron_btn.place(relx=0.5, rely=0.5, anchor="center")
240
+ else:
241
+ if self._chevron_btn is not None:
242
+ self._chevron_btn.place_forget()
243
+
244
+ def set_mixed(self, mixed: bool) -> None:
245
+ """Mark the selection control as a partially-selected (mixed) parent.
246
+
247
+ Driven explicitly (not by the coordinator) so it survives selection
248
+ state updates. Used by tri-state cascade.
249
+ """
250
+ if self._select_ctrl is not None:
251
+ self._select_ctrl.state(("alternate",) if mixed else ("!alternate",))
252
+
253
+ def _update_icon(self, icon: Optional[str]) -> None:
254
+ if icon:
255
+ if self._icon_widget is None:
256
+ self._icon_widget = Label(
257
+ self._left_frame, icon=icon, variant="icon",
258
+ ttk_class="ListView.TLabel", takefocus=False, icon_only=True,
259
+ accent=self._accent, style_options=self._style_opts(),
260
+ )
261
+ self._icon_widget.pack(side="left", padx=(0, 5))
262
+ self.register_composite(self._icon_widget)
263
+ self._bind_row_mouse(self._icon_widget)
264
+ else:
265
+ self._icon_widget.configure(icon=icon)
266
+ elif self._icon_widget is not None:
267
+ self._destroy_widget("_icon_widget")
268
+
269
+ def _update_label(self, text: Optional[str]) -> None:
270
+ font = "caption" if self._density == "compact" else "body"
271
+ if text is not None:
272
+ if self._label_widget is None:
273
+ self._label_widget = Label(
274
+ self._center_frame, text=text, font=font, variant="list",
275
+ ttk_class="ListView.TLabel", takefocus=False,
276
+ accent=self._accent, style_options=self._style_opts(),
277
+ )
278
+ self._label_widget.pack(side="left", anchor="w")
279
+ self.register_composite(self._label_widget)
280
+ self._bind_row_mouse(self._label_widget)
281
+ else:
282
+ self._label_widget.configure(text=text)
283
+ elif self._label_widget is not None:
284
+ self._destroy_widget("_label_widget")
285
+
286
+ def _destroy_widget(self, attr: str) -> None:
287
+ w = getattr(self, attr, None)
288
+ if w is not None:
289
+ # Unregister BEFORE destroy so a later hover/state update doesn't
290
+ # call .state() on a dead widget.
291
+ try:
292
+ self.unregister_composite(w)
293
+ except Exception:
294
+ pass
295
+ try:
296
+ w.pack_forget()
297
+ w.destroy()
298
+ except TclError:
299
+ pass
300
+ setattr(self, attr, None)
301
+
302
+ # ----- surface (striping) -----
303
+
304
+ def set_surface(self, surface: str) -> None:
305
+ """Set the row + container surface color (used for striped rows).
306
+
307
+ Idempotent: a no-op when the surface is unchanged, so it is safe (and
308
+ cheap) to call on every recycle — that is what keeps striping correct as
309
+ rows are repointed at different data indices.
310
+ """
311
+ if getattr(self, "_applied_surface", None) == surface:
312
+ return
313
+ self._applied_surface = surface
314
+ self.configure_style_options(surface=surface)
315
+ self.rebuild_style()
316
+ targets = [self._left_frame, self._center_frame, self._chevron_slot]
317
+ if self._select_ctrl is not None:
318
+ targets.append(self._select_ctrl)
319
+ for frame in targets:
320
+ try:
321
+ frame.configure_style_options(surface=surface)
322
+ frame.rebuild_style()
323
+ except Exception:
324
+ continue
325
+
326
+ # ----- recycle entry point -----
327
+
328
+ def update_data(self, record: Optional[dict]) -> None:
329
+ """Re-point this row at the node in `record`, or hide it if empty."""
330
+ if record is None or "__empty__" in record:
331
+ self._node = None
332
+ self.pack_forget()
333
+ return
334
+
335
+ if not self.winfo_manager():
336
+ self.pack(fill="x")
337
+
338
+ self._data = record
339
+ self._node = record.get("node")
340
+ self._item_index = record.get("item_index", 0)
341
+
342
+ # Indent reflects depth, applied as left padding on the chevron slot.
343
+ depth = record.get("depth", 0)
344
+ if self._state.get("depth") != depth:
345
+ self._chevron_slot.pack_configure(padx=(max(0, depth * self._indent), 0))
346
+ self._state["depth"] = depth
347
+
348
+ # Focus. (The keyboard-focus ring rides the 'background' state, set by
349
+ # the keyboard nav via focus_set(visual_focus=True). When a row stops
350
+ # being the focused node, drop that state so a recycled/relocated row
351
+ # doesn't keep a stale ring.)
352
+ focused = bool(record.get("focused", False))
353
+ if self._state.get("focused") != focused:
354
+ if focused and self._focusable:
355
+ try:
356
+ self.focus_set()
357
+ except TclError:
358
+ pass
359
+ elif not focused:
360
+ try:
361
+ self.state(["!background"])
362
+ except TclError:
363
+ pass
364
+ self._state["focused"] = focused
365
+
366
+ # Chevron (re-pack only when the expandable/expanded pair changes).
367
+ chev = (bool(record.get("expandable", False)), bool(record.get("expanded", False)))
368
+ if self._state.get("chevron") != chev:
369
+ self._update_chevron(*chev)
370
+ self._state["chevron"] = chev
371
+
372
+ # Content widgets (diffed against last render).
373
+ for field, updater in (
374
+ ("icon", self._update_icon),
375
+ ("label", self._update_label),
376
+ ):
377
+ value = record.get(field)
378
+ if self._state.get(field) != value:
379
+ updater(value)
380
+ self._state[field] = value
381
+
382
+ # Selection highlight — drives the row wash AND the selection control's
383
+ # fill (the control follows the coordinator's 'selected' state).
384
+ selected = bool(record.get("selected", False))
385
+ if self._state.get("selected") != selected:
386
+ self.set_selected(selected)
387
+ self._state["selected"] = selected
388
+
389
+ # Mixed (partially-selected parent) marker on the selection control.
390
+ mixed = bool(record.get("mixed", False))
391
+ if self._state.get("mixed") != mixed:
392
+ self.set_mixed(mixed)
393
+ self._state["mixed"] = mixed
@@ -0,0 +1,174 @@
1
+ """TreeNode — the object handle for nodes in the Tree widget.
2
+
3
+ A `TreeNode` is a plain Python object (not a Tk widget) that the user holds and
4
+ passes back to the tree. It carries display configuration, an opaque data bag,
5
+ and its place in the hierarchy. Structural changes route back to the owning tree
6
+ so the view re-renders.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, Callable, Iterator, Optional
12
+
13
+ # Sentinel separating recognized display parameters from the user data bag.
14
+ _DISPLAY_PARAMS = frozenset(
15
+ {"label", "icon", "open_icon", "closed_icon", "expanded", "children", "loader"}
16
+ )
17
+
18
+
19
+ class TreeNode:
20
+ """A single node in a `Tree`.
21
+
22
+ Nodes are identity-based handles: hold the object returned by `Tree.add()`
23
+ and pass it back to `expand()`, `select()`, `remove()`, and friends. Two
24
+ nodes are never equal unless they are the same object.
25
+
26
+ Display attributes (`label`, `icon`, `open_icon`, `closed_icon`, ...) are a
27
+ non-destructive view over the node. Any keyword not recognized as a display
28
+ parameter is folded into `data`, so a handler always gets the user's domain
29
+ data back.
30
+ """
31
+
32
+ __slots__ = (
33
+ "label", "icon", "open_icon", "closed_icon",
34
+ "expanded", "children", "parent", "data", "loader",
35
+ "_tree", "_loaded",
36
+ )
37
+
38
+ label: str
39
+ """The node's display label."""
40
+
41
+ icon: str | None
42
+ """Bootstrap icon name shown before the label, or `None` for no icon."""
43
+
44
+ open_icon: str | None
45
+ """Icon shown while the node is expanded, overriding `icon` when set."""
46
+
47
+ closed_icon: str | None
48
+ """Icon shown while the node is collapsed, overriding `icon` when set."""
49
+
50
+ expanded: bool
51
+ """Whether the node is currently expanded — kept in sync as it expands and
52
+ collapses."""
53
+
54
+ children: list["TreeNode"]
55
+ """The child nodes, in order. Empty for a leaf, and empty for a lazy node
56
+ until its `loader` has run on first expand."""
57
+
58
+ parent: "TreeNode | None"
59
+ """The parent node, or `None` for a root node."""
60
+
61
+ data: dict[str, Any]
62
+ """Open-ended data bag for your own attributes. Holds any keyword passed to
63
+ `add()` that is not a recognized display parameter, and — for a
64
+ data-source-backed tree — the node's source record."""
65
+
66
+ loader: Callable[["TreeNode"], Any] | None
67
+ """Callable invoked on first expand to fetch children lazily, or `None` for
68
+ an eagerly-populated node. Receives the node; returns an iterable of child
69
+ specs."""
70
+
71
+ def __init__(
72
+ self,
73
+ label: str = "",
74
+ *,
75
+ icon: Optional[str] = None,
76
+ open_icon: Optional[str] = None,
77
+ closed_icon: Optional[str] = None,
78
+ expanded: bool = False,
79
+ loader: Optional[Callable[["TreeNode"], Any]] = None,
80
+ data: Optional[dict] = None,
81
+ **extra: Any,
82
+ ) -> None:
83
+ self.label = label
84
+ self.icon = icon
85
+ self.open_icon = open_icon
86
+ self.closed_icon = closed_icon
87
+ self.expanded = expanded
88
+ self.loader = loader
89
+
90
+ self.children: list[TreeNode] = []
91
+ self.parent: Optional[TreeNode] = None
92
+
93
+ # The data bag: explicit data= merged with any overflow kwargs. Nothing
94
+ # consumed by a display parameter ever lands here; nothing else is lost.
95
+ self.data: dict = dict(data or {})
96
+ self.data.update(extra)
97
+
98
+ # Owning internal TreeView (set when attached); used to request renders.
99
+ self._tree = None
100
+ # Lazy-loading state: a node with a loader is "expandable" before its
101
+ # children exist; _loaded flips True after the loader runs once.
102
+ self._loaded = loader is None
103
+
104
+ # ----- structure -----
105
+
106
+ @property
107
+ def is_leaf(self) -> bool:
108
+ """Whether this node has no children and cannot lazily load any."""
109
+ return not self.children and self.loader is None
110
+
111
+ @property
112
+ def expandable(self) -> bool:
113
+ """Whether this node can be expanded (has children or a lazy loader)."""
114
+ return bool(self.children) or (self.loader is not None and not self._loaded)
115
+
116
+ @property
117
+ def depth(self) -> int:
118
+ """Distance from the root (root nodes are depth 0)."""
119
+ d = 0
120
+ p = self.parent
121
+ while p is not None:
122
+ d += 1
123
+ p = p.parent
124
+ return d
125
+
126
+ def ancestors(self) -> Iterator["TreeNode"]:
127
+ """Yield this node's parent, grandparent, ... up to the root."""
128
+ p = self.parent
129
+ while p is not None:
130
+ yield p
131
+ p = p.parent
132
+
133
+ def descendants(self) -> Iterator["TreeNode"]:
134
+ """Yield every node beneath this one, depth-first."""
135
+ for child in self.children:
136
+ yield child
137
+ yield from child.descendants()
138
+
139
+ # ----- convenience mutation (routes to the owning tree) -----
140
+
141
+ def add(self, label: str = "", **kwargs: Any) -> "TreeNode":
142
+ """Add a child node under this node and return it.
143
+
144
+ Args:
145
+ label: The child's display label.
146
+ **kwargs: Forwarded to `Tree.add` (icon, open_icon, closed_icon,
147
+ expanded, children, loader, data, and overflow data kwargs).
148
+ """
149
+ if self._tree is None:
150
+ raise RuntimeError("node is not attached to a tree")
151
+ return self._tree.add(label, parent=self, **kwargs)
152
+
153
+ def remove(self) -> None:
154
+ """Remove this node (and its descendants) from the tree."""
155
+ if self._tree is not None:
156
+ self._tree.remove(self)
157
+
158
+ def expand(self) -> None:
159
+ """Expand this node to reveal its children."""
160
+ if self._tree is not None:
161
+ self._tree.expand(self)
162
+
163
+ def collapse(self) -> None:
164
+ """Collapse this node to hide its children."""
165
+ if self._tree is not None:
166
+ self._tree.collapse(self)
167
+
168
+ def reload_children(self) -> None:
169
+ """Refresh a lazy node's children (drop and re-fetch via its loader)."""
170
+ if self._tree is not None:
171
+ self._tree.reload_children(self)
172
+
173
+ def __repr__(self) -> str: # pragma: no cover - debug aid
174
+ return f"TreeNode(label={self.label!r}, children={len(self.children)})"