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,491 @@
1
+ import tkinter as tk
2
+ from tkinter import Misc
3
+ from types import SimpleNamespace
4
+ from typing import Any, Literal
5
+
6
+ from bootstack.constants import *
7
+ from bootstack._runtime.toplevel import Toplevel
8
+ from bootstack._runtime.utility import scale_size
9
+ from bootstack._runtime.window_utilities import WindowPositioning, AnchorPoint
10
+ from bootstack.widgets._impl.primitives import Button, Frame, Label
11
+
12
+ ttk = SimpleNamespace(
13
+ Button=Button,
14
+ Frame=Frame,
15
+ Label=Label,
16
+ window=SimpleNamespace(Toplevel=Toplevel),
17
+ )
18
+
19
+
20
+ class ToolTip:
21
+ # Position offset from mouse pointer
22
+ _MOUSE_OFFSET_X = 25
23
+ _MOUSE_OFFSET_Y = 10
24
+
25
+ # Spacing between tooltip and widget when anchored
26
+ _WIDGET_SPACING = 1
27
+
28
+ # Fallback dimensions for tooltip sizing
29
+ _FALLBACK_WIDTH = 200
30
+ _FALLBACK_HEIGHT = 50
31
+
32
+ def __init__(
33
+ self,
34
+ widget: Misc,
35
+ text: str = "widget info",
36
+ padding: int = 10,
37
+ justify: Literal["left", "center", "right"] = "left",
38
+ accent: str | None = None,
39
+ wraplength: int | None = None,
40
+ delay: int = 250, # milliseconds
41
+ image: Any = None,
42
+ anchor_point: AnchorPoint | None = None,
43
+ window_point: AnchorPoint | None = None,
44
+ auto_flip: bool | Literal['vertical', 'horizontal'] = True,
45
+ **kwargs: Any,
46
+ ) -> None:
47
+ """Initialize a ToolTip instance for the specified widget.
48
+
49
+ Creates a tooltip that appears after a configurable delay when the mouse
50
+ enters the widget and disappears when the mouse leaves or on button press.
51
+ The tooltip can either follow the mouse pointer or be anchored to a specific
52
+ position relative to the widget.
53
+
54
+ The tooltip window is created with semi-transparency (alpha=0.95 by default)
55
+ and uses the Bootstrap styling system for consistent theming. Text automatically
56
+ wraps based on the specified wraplength, and optional images can be displayed
57
+ alongside the text.
58
+
59
+ Args:
60
+ widget: The tkinter widget to attach this tooltip to. The tooltip will
61
+ appear when hovering over this widget.
62
+ text: The text content to display in the tooltip. Supports multi-line
63
+ text that will wrap according to wraplength. Defaults to "widget info".
64
+ padding: The internal padding in pixels between the tooltip text and the
65
+ tooltip border. Defaults to 10.
66
+ justify: Text alignment within the tooltip. Valid options are "left",
67
+ "center", or "right". Defaults to "left".
68
+ accent: Accent token for the tooltip frame (e.g., "danger", "info").
69
+ If None, uses default elevated background styling.
70
+ to the tooltip frame.
71
+ wraplength: Maximum width in screen units before text wraps to a new line.
72
+ If None, defaults to a scaled value of 300 based on the widget's display.
73
+ delay: Time in milliseconds to wait before showing the tooltip after mouse
74
+ enters the widget. Defaults to 250ms.
75
+ image: Optional image to display in the tooltip below the text. Should be
76
+ a PhotoImage or compatible tkinter image object.
77
+ anchor_point: Point on the widget to anchor to (n, s, e, w, ne, nw, se, sw, center).
78
+ If None, tooltip follows the mouse pointer.
79
+ window_point: Point on the tooltip window to align with anchor_point
80
+ (n, s, e, w, ne, nw, se, sw, center). If None, auto-defaults to the
81
+ opposite of anchor_point for natural positioning.
82
+ auto_flip: Smart positioning to keep tooltip on screen. Defaults to True.
83
+ - False: No flipping
84
+ - True: Flip both vertically and horizontally as needed
85
+ - 'vertical': Only flip up/down
86
+ - 'horizontal': Only flip left/right
87
+ **kwargs: Additional keyword arguments passed to the Toplevel window
88
+ constructor. Common options include alpha, topmost, etc. The arguments
89
+ overrideredirect, master, and windowtype are set automatically.
90
+ """
91
+ # Configuration
92
+ self._widget = widget
93
+ self._text = text
94
+ self._padding = padding
95
+ self._justify = justify
96
+ self._accent = accent
97
+ self._wraplength = wraplength if wraplength is not None else scale_size(self._widget, 300)
98
+ self._delay = delay
99
+ self._image = image
100
+ self._anchor_point = anchor_point
101
+ self._window_point = window_point
102
+ self._auto_flip = auto_flip
103
+
104
+ self._toplevel = None
105
+ self._label = None
106
+ self._id = None
107
+
108
+ # Set keyword arguments (create copy to avoid mutating caller's dict)
109
+ self.toplevel_kwargs = kwargs.copy()
110
+ self.toplevel_kwargs["overrideredirect"] = True
111
+ self.toplevel_kwargs["master"] = self._widget
112
+ self.toplevel_kwargs["windowtype"] = "tooltip"
113
+ if "alpha" not in self.toplevel_kwargs:
114
+ self.toplevel_kwargs["alpha"] = 0.95
115
+
116
+ # Event binding. add="+" so we never replace a handler the target
117
+ # already has; keep the bind ids so destroy() removes only our own.
118
+ self._bind_ids = {
119
+ "<Enter>": self._widget.bind("<Enter>", self._on_enter, add="+"),
120
+ "<Leave>": self._widget.bind("<Leave>", self._on_leave, add="+"),
121
+ "<Motion>": self._widget.bind("<Motion>", self._move_tip, add="+"),
122
+ "<ButtonPress>": self._widget.bind("<ButtonPress>", self._on_button_press, add="+"),
123
+ }
124
+ # Self-release when the target is destroyed, so a pending timer or a
125
+ # visible popup can't outlive it (and a later destroy() can't crash on a
126
+ # dead widget). <Destroy> propagates up from descendants that carry the
127
+ # target's bindtag, so the handler guards on the target itself.
128
+ self._destroy_id = self._widget.bind("<Destroy>", self._on_target_destroy, add="+")
129
+
130
+ # Tk events don't bubble, so hovering a child of a container target
131
+ # would otherwise never reach these bindings. Extend them across the
132
+ # subtree so the tip shows anywhere inside the container.
133
+ self.refresh_bindings()
134
+
135
+ def refresh_bindings(self) -> None:
136
+ """Re-cover the target's subtree after children were added post-attach.
137
+
138
+ `propagate_target_bindings` only tags the descendants present when it
139
+ runs, so children added to a container target after the tooltip was
140
+ created are not covered until this is called again. The call is
141
+ idempotent — already-tagged descendants are left untouched.
142
+ """
143
+ from bootstack._runtime.utility import propagate_target_bindings
144
+ try:
145
+ if self._widget.winfo_exists():
146
+ propagate_target_bindings(self._widget)
147
+ except tk.TclError:
148
+ pass
149
+
150
+ def destroy(self) -> None:
151
+ """Cleanup tooltip resources and unbind all event handlers.
152
+
153
+ This method should be called when the tooltip is no longer needed to prevent
154
+ memory leaks. It cancels any pending tooltip display, hides any visible tooltip,
155
+ and removes all event bindings from the widget.
156
+ """
157
+ self._unschedule()
158
+ self._hide_tip()
159
+ # The target may already be gone (e.g. destroyed before destroy() is
160
+ # called); unbinding a dead widget path raises a TclError, so guard it.
161
+ try:
162
+ if self._widget.winfo_exists():
163
+ for sequence, bind_id in self._bind_ids.items():
164
+ self._widget.unbind(sequence, bind_id)
165
+ if self._destroy_id:
166
+ self._widget.unbind("<Destroy>", self._destroy_id)
167
+ except tk.TclError:
168
+ pass
169
+ self._bind_ids = {}
170
+ self._destroy_id = None
171
+
172
+ @property
173
+ def text(self) -> str:
174
+ """The tooltip text. Assigning updates a visible popup live."""
175
+ return self._text
176
+
177
+ @text.setter
178
+ def text(self, value: str) -> None:
179
+ self._text = value
180
+ if self._toplevel is not None and self._label is not None:
181
+ try:
182
+ self._label.configure(text=value)
183
+ except tk.TclError:
184
+ pass
185
+
186
+ def _on_target_destroy(self, event: Any) -> None:
187
+ """Release the timer and popup when the target widget is destroyed."""
188
+ if event.widget is not self._widget:
189
+ return # a descendant was destroyed, not the target itself
190
+ self._unschedule()
191
+ self._hide_tip()
192
+
193
+ def _on_enter(self, _) -> None:
194
+ """Handle mouse enter event by scheduling tooltip display."""
195
+ self._schedule()
196
+
197
+ def _on_leave(self, _) -> None:
198
+ """Handle mouse leave event by canceling and hiding tooltip.
199
+
200
+ When the target is a container, crossing from the container onto one of
201
+ its children fires a `<Leave>` on the container even though the pointer
202
+ is still inside it. Keep the tip in that case so it doesn't flicker as
203
+ the pointer moves over child widgets.
204
+ """
205
+ if self._pointer_within_target():
206
+ return
207
+ self._unschedule()
208
+ self._hide_tip()
209
+
210
+ def _on_button_press(self, _) -> None:
211
+ """Hide the tooltip on any button press inside the target subtree."""
212
+ self._unschedule()
213
+ self._hide_tip()
214
+
215
+ def _pointer_within_target(self) -> bool:
216
+ """Return True if the pointer is over the target or one of its children."""
217
+ try:
218
+ x = self._widget.winfo_pointerx()
219
+ y = self._widget.winfo_pointery()
220
+ under = self._widget.winfo_containing(x, y)
221
+ except tk.TclError:
222
+ return False
223
+ if under is None:
224
+ return False
225
+ target_path = str(self._widget)
226
+ under_path = str(under)
227
+ return under_path == target_path or under_path.startswith(target_path + ".")
228
+
229
+ def _schedule(self) -> None:
230
+ """Schedule the tooltip to appear after the configured delay."""
231
+ self._unschedule()
232
+ self._id = self._widget.after(self._delay, self._show_tip)
233
+
234
+ def _unschedule(self) -> None:
235
+ """Cancel any pending scheduled tooltip display."""
236
+ _id = self._id
237
+ self._id = None
238
+ if _id:
239
+ self._widget.after_cancel(_id)
240
+
241
+ def _show_tip(self, *_: Any) -> None:
242
+ """Create and display the tooltip window at the appropriate position."""
243
+ if self._toplevel:
244
+ return
245
+
246
+ # Check if widget still exists before showing tooltip
247
+ try:
248
+ if not self._widget.winfo_exists():
249
+ return
250
+ except tk.TclError:
251
+ return
252
+
253
+ # Create the tooltip window (position will be set after content is built)
254
+ self._toplevel = Toplevel(**self.toplevel_kwargs)
255
+ # Use accent with tooltip variant
256
+ accent = 'background[+1]' if self._accent is None else self._accent
257
+ frame = ttk.Frame(
258
+ self._toplevel,
259
+ accent=accent,
260
+ variant='tooltip',
261
+ padding=self._padding
262
+ )
263
+ frame.pack(fill=BOTH, expand=YES)
264
+
265
+ lbl = ttk.Label(
266
+ master=frame,
267
+ text=self._text,
268
+ image=self._image,
269
+ compound='bottom',
270
+ justify=self._justify,
271
+ font="caption",
272
+ wraplength=self._wraplength,
273
+ )
274
+ lbl.pack(fill=BOTH, expand=YES)
275
+ self._label = lbl
276
+
277
+ # Wait until size is known, then position
278
+ self._toplevel.update_idletasks()
279
+
280
+ if self._anchor_point:
281
+ # Use WindowPositioning for anchored tooltips (sets geometry directly)
282
+ self._position_anchored()
283
+ else:
284
+ # Mouse-following tooltip
285
+ x, y = self._get_mouse_position()
286
+ self._toplevel.geometry(f"+{x}+{y}")
287
+
288
+ # Ensure the tooltip is visible
289
+ self._toplevel.deiconify()
290
+
291
+ def _move_tip(self, *_: Any) -> None:
292
+ """Update the tooltip position based on mouse or anchor position."""
293
+ if self._toplevel:
294
+ if self._anchor_point:
295
+ # Anchored tooltips don't move
296
+ pass
297
+ else:
298
+ # Update mouse-following tooltip
299
+ x, y = self._get_mouse_position()
300
+ self._toplevel.geometry(f"+{x}+{y}")
301
+
302
+ def _hide_tip(self, *_: Any) -> None:
303
+ """Hide and destroy the tooltip window."""
304
+ if self._toplevel:
305
+ self._toplevel.destroy()
306
+ self._toplevel = None
307
+ self._label = None
308
+
309
+ def _get_mouse_position(self) -> tuple[int, int]:
310
+ """Get tooltip position offset from the current mouse pointer.
311
+
312
+ Uses WindowPositioning to ensure the tooltip stays on screen.
313
+
314
+ Returns:
315
+ Tuple of (x, y) screen coordinates for the tooltip.
316
+ """
317
+ x = self._widget.winfo_pointerx() + self._MOUSE_OFFSET_X
318
+ y = self._widget.winfo_pointery() + self._MOUSE_OFFSET_Y
319
+
320
+ # Ensure tooltip stays on screen (no titlebar since overrideredirect=True)
321
+ if self._toplevel:
322
+ x, y = WindowPositioning.ensure_on_screen(
323
+ self._toplevel, x, y, padding=5, titlebar_height=0
324
+ )
325
+
326
+ return x, y
327
+
328
+ def _position_anchored(self) -> None:
329
+ """Position tooltip using WindowPositioning for intelligent anchor-based positioning.
330
+
331
+ Uses anchor_point and window_point attributes and applies auto-flip
332
+ to keep tooltip on screen.
333
+ """
334
+ if not self._toplevel or not self._anchor_point:
335
+ return
336
+
337
+ # Ensure the widget is fully laid out before calculating position
338
+ self._widget.update_idletasks()
339
+
340
+ # Default window_point based on anchor_point if not specified
341
+ window_point = self._window_point
342
+ if window_point is None:
343
+ # Auto-determine opposite anchor point for natural positioning
344
+ opposite = {
345
+ 'n': 's', 's': 'n', 'e': 'w', 'w': 'e',
346
+ 'ne': 'sw', 'nw': 'se', 'se': 'nw', 'sw': 'ne',
347
+ 'center': 'center'
348
+ }
349
+ window_point = opposite.get(self._anchor_point, 's')
350
+
351
+ # Get widget dimensions using max of requested and actual size
352
+ widget_x = self._widget.winfo_rootx()
353
+ widget_y = self._widget.winfo_rooty()
354
+ widget_w = max(self._widget.winfo_reqwidth(), self._widget.winfo_width())
355
+ widget_h = max(self._widget.winfo_reqheight(), self._widget.winfo_height())
356
+
357
+ # Calculate anchor coordinates based on anchor_point
358
+ if self._anchor_point == 'nw':
359
+ anchor_x, anchor_y = widget_x, widget_y
360
+ elif self._anchor_point == 'n':
361
+ anchor_x, anchor_y = widget_x + widget_w // 2, widget_y
362
+ elif self._anchor_point == 'ne':
363
+ anchor_x, anchor_y = widget_x + widget_w, widget_y
364
+ elif self._anchor_point == 'w':
365
+ anchor_x, anchor_y = widget_x, widget_y + widget_h // 2
366
+ elif self._anchor_point == 'center':
367
+ anchor_x, anchor_y = widget_x + widget_w // 2, widget_y + widget_h // 2
368
+ elif self._anchor_point == 'e':
369
+ anchor_x, anchor_y = widget_x + widget_w, widget_y + widget_h // 2
370
+ elif self._anchor_point == 'sw':
371
+ anchor_x, anchor_y = widget_x, widget_y + widget_h
372
+ elif self._anchor_point == 's':
373
+ anchor_x, anchor_y = widget_x + widget_w // 2, widget_y + widget_h
374
+ elif self._anchor_point == 'se':
375
+ anchor_x, anchor_y = widget_x + widget_w, widget_y + widget_h
376
+ else:
377
+ anchor_x, anchor_y = widget_x, widget_y
378
+
379
+ # Use req dimensions — the Toplevel is freshly created and withdrawn;
380
+ # winfo_width/height return the default 200×200 placeholder, not content size.
381
+ w_width = self._toplevel.winfo_reqwidth()
382
+ w_height = self._toplevel.winfo_reqheight()
383
+
384
+ x_offset, y_offset = 0, 0
385
+ if window_point == 'nw':
386
+ x_offset, y_offset = 0, 0
387
+ elif window_point == 'n':
388
+ x_offset, y_offset = -w_width // 2, 0
389
+ elif window_point == 'ne':
390
+ x_offset, y_offset = -w_width, 0
391
+ elif window_point == 'w':
392
+ x_offset, y_offset = 0, -w_height // 2
393
+ elif window_point == 'center':
394
+ x_offset, y_offset = -w_width // 2, -w_height // 2
395
+ elif window_point == 'e':
396
+ x_offset, y_offset = -w_width, -w_height // 2
397
+ elif window_point == 'sw':
398
+ x_offset, y_offset = 0, -w_height
399
+ elif window_point == 's':
400
+ x_offset, y_offset = -w_width // 2, -w_height
401
+ elif window_point == 'se':
402
+ x_offset, y_offset = -w_width, -w_height
403
+
404
+ # Calculate position with offset
405
+ x = int(anchor_x + x_offset + self._WIDGET_SPACING)
406
+ y = int(anchor_y + y_offset + self._WIDGET_SPACING)
407
+
408
+ # Auto-flip logic
409
+ if self._auto_flip:
410
+ vertical_offscreen, horizontal_offscreen = WindowPositioning._check_offscreen(
411
+ self._toplevel, x, y, padding=5
412
+ )
413
+
414
+ should_flip_vertical = False
415
+ should_flip_horizontal = False
416
+
417
+ if self._auto_flip is True or self._auto_flip == 'vertical':
418
+ should_flip_vertical = vertical_offscreen
419
+
420
+ if self._auto_flip is True or self._auto_flip == 'horizontal':
421
+ should_flip_horizontal = horizontal_offscreen
422
+
423
+ # Flip if needed
424
+ if should_flip_vertical or should_flip_horizontal:
425
+ flipped_anchor_point = self._anchor_point
426
+ flipped_window_point = window_point
427
+
428
+ if should_flip_vertical:
429
+ flipped_anchor_point = WindowPositioning._flip_anchor_vertical(flipped_anchor_point)
430
+ flipped_window_point = WindowPositioning._flip_anchor_vertical(flipped_window_point)
431
+
432
+ if should_flip_horizontal:
433
+ flipped_anchor_point = WindowPositioning._flip_anchor_horizontal(flipped_anchor_point)
434
+ flipped_window_point = WindowPositioning._flip_anchor_horizontal(flipped_window_point)
435
+
436
+ # Recalculate anchor coordinates with flipped anchor point
437
+ if flipped_anchor_point == 'nw':
438
+ anchor_x, anchor_y = widget_x, widget_y
439
+ elif flipped_anchor_point == 'n':
440
+ anchor_x, anchor_y = widget_x + widget_w // 2, widget_y
441
+ elif flipped_anchor_point == 'ne':
442
+ anchor_x, anchor_y = widget_x + widget_w, widget_y
443
+ elif flipped_anchor_point == 'w':
444
+ anchor_x, anchor_y = widget_x, widget_y + widget_h // 2
445
+ elif flipped_anchor_point == 'center':
446
+ anchor_x, anchor_y = widget_x + widget_w // 2, widget_y + widget_h // 2
447
+ elif flipped_anchor_point == 'e':
448
+ anchor_x, anchor_y = widget_x + widget_w, widget_y + widget_h // 2
449
+ elif flipped_anchor_point == 'sw':
450
+ anchor_x, anchor_y = widget_x, widget_y + widget_h
451
+ elif flipped_anchor_point == 's':
452
+ anchor_x, anchor_y = widget_x + widget_w // 2, widget_y + widget_h
453
+ elif flipped_anchor_point == 'se':
454
+ anchor_x, anchor_y = widget_x + widget_w, widget_y + widget_h
455
+ else:
456
+ anchor_x, anchor_y = widget_x, widget_y
457
+
458
+ # Recalculate window offset with flipped window point
459
+ if flipped_window_point == 'nw':
460
+ x_offset, y_offset = 0, 0
461
+ elif flipped_window_point == 'n':
462
+ x_offset, y_offset = -w_width // 2, 0
463
+ elif flipped_window_point == 'ne':
464
+ x_offset, y_offset = -w_width, 0
465
+ elif flipped_window_point == 'w':
466
+ x_offset, y_offset = 0, -w_height // 2
467
+ elif flipped_window_point == 'center':
468
+ x_offset, y_offset = -w_width // 2, -w_height // 2
469
+ elif flipped_window_point == 'e':
470
+ x_offset, y_offset = -w_width, -w_height // 2
471
+ elif flipped_window_point == 'sw':
472
+ x_offset, y_offset = 0, -w_height
473
+ elif flipped_window_point == 's':
474
+ x_offset, y_offset = -w_width // 2, -w_height
475
+ elif flipped_window_point == 'se':
476
+ x_offset, y_offset = -w_width, -w_height
477
+
478
+ # Recalculate position with flipped anchors
479
+ x = int(anchor_x + x_offset + self._WIDGET_SPACING)
480
+ y = int(anchor_y + y_offset + self._WIDGET_SPACING)
481
+
482
+ # Ensure on screen (final safety check)
483
+ try:
484
+ x, y = WindowPositioning.ensure_on_screen(
485
+ self._toplevel, x, y, padding=5, titlebar_height=0
486
+ )
487
+ except Exception:
488
+ pass
489
+
490
+ # Set geometry
491
+ self._toplevel.geometry(f"+{x}+{y}")
@@ -0,0 +1,7 @@
1
+ """Internal recycle-view Tree composite (engine behind the public Tree)."""
2
+
3
+ from bootstack.widgets._impl.composites.tree.treenode import TreeNode
4
+ from bootstack.widgets._impl.composites.tree.treeitem import TreeItem
5
+ from bootstack.widgets._impl.composites.tree.treeview import TreeView
6
+
7
+ __all__ = ["TreeNode", "TreeItem", "TreeView"]
@@ -0,0 +1,136 @@
1
+ """Project a flat adjacency-list data source as a lazy Tree hierarchy.
2
+
3
+ A data-source-backed tree is "auto-generate the loader from a source": the
4
+ source stores flat records that each carry a `parent_field` pointing at their
5
+ parent's id (an adjacency list), and the binding projects that flat shape as a
6
+ hierarchy by issuing one indexed query per expanded node. Nothing about the flat
7
+ `DataSourceProtocol` changes — the hierarchy lives entirely in this binding, so
8
+ any source (SQLite, memory, a custom adapter) can back a tree.
9
+
10
+ Scale comes for free: only the children of *expanded* nodes are ever queried
11
+ (one `WHERE parent_field = ?` each), and a single batched existence query per
12
+ expand decides which of those children get a chevron.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Callable, List, Sequence
18
+
19
+ from bootstack.data.query import Condition, SortKey, col, normalize_sort_keys
20
+
21
+ __all__ = ["SourceBinding"]
22
+
23
+
24
+ def _key(value: Any) -> Any:
25
+ """Normalize an identifier for cross-column comparison.
26
+
27
+ A record's id and another record's `parent_field` may be stored with
28
+ different types depending on the source — for instance a column whose sampled
29
+ values are all NULL gets TEXT affinity, so its ids can come back as `'1'`
30
+ while a sibling id column returns `1`. They are the same logical key, so
31
+ compare them as strings (NULL stays distinct).
32
+ """
33
+ return None if value is None else str(value)
34
+
35
+
36
+ def _child_condition(parent_field: str, value: Any) -> Condition:
37
+ """Match the rows whose `parent_field` equals `value`.
38
+
39
+ A `value` of `None` means "the roots" and matches a NULL/absent parent.
40
+ """
41
+ column = col(parent_field)
42
+ if value is None:
43
+ return column.is_null()
44
+ return column == value
45
+
46
+
47
+ class SourceBinding:
48
+ """Binds a flat adjacency-list data source to a `Tree`.
49
+
50
+ Holds the projection config and turns records into node specs, wiring each
51
+ node that has children with a `loader` closure that re-queries on expand.
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ source: Any,
57
+ *,
58
+ parent_field: str,
59
+ root_value: Any,
60
+ label_field: str,
61
+ icon_field: str | None,
62
+ node_builder: Callable[[dict], dict] | None,
63
+ order: Any = None,
64
+ ) -> None:
65
+ self.source = source
66
+ self.parent_field = parent_field
67
+ self.root_value = root_value
68
+ self.label_field = label_field
69
+ self.icon_field = icon_field
70
+ self.node_builder = node_builder
71
+ self.sort_keys: List[SortKey] = self._normalize_order(order)
72
+
73
+ @staticmethod
74
+ def _normalize_order(order: Any) -> List[SortKey]:
75
+ if order is None:
76
+ return []
77
+ keys: Sequence[Any]
78
+ if isinstance(order, (list, tuple)):
79
+ keys = order
80
+ else:
81
+ keys = (order,)
82
+ return normalize_sort_keys(keys)
83
+
84
+ # ----- spec building -----
85
+
86
+ def _spec(self, record: Any, has_children: bool) -> dict:
87
+ """Turn one raw record into a node spec dict."""
88
+ public = self.source._public_record(record)
89
+ if self.node_builder is not None:
90
+ spec = dict(self.node_builder(public))
91
+ else:
92
+ if self.label_field not in public:
93
+ raise KeyError(
94
+ f"Tree(data_source=...): no field {self.label_field!r} in the "
95
+ f"record {dict(public)!r}. Set label_field= to the column that "
96
+ f"holds the node label, or pass node_builder=."
97
+ )
98
+ spec = {"label": str(public.get(self.label_field, ""))}
99
+ if self.icon_field and public.get(self.icon_field):
100
+ spec["icon"] = public[self.icon_field]
101
+ spec.setdefault("data", dict(public))
102
+ if has_children:
103
+ record_id = self.source._record_id(record)
104
+ spec["loader"] = lambda _node, _pid=record_id: self._fetch(_pid)
105
+ return spec
106
+
107
+ def _fetch(self, parent_value: Any) -> List[dict]:
108
+ """Fetch and build the child specs for one parent value."""
109
+ records = self.source._query(
110
+ _child_condition(self.parent_field, parent_value), self.sort_keys
111
+ )
112
+ if not records:
113
+ return []
114
+ ids = [self.source._record_id(r) for r in records]
115
+ with_children = self._records_with_children(ids)
116
+ return [
117
+ self._spec(r, _key(self.source._record_id(r)) in with_children)
118
+ for r in records
119
+ ]
120
+
121
+ def _records_with_children(self, ids: Sequence[Any]) -> set:
122
+ """Return the subset of `ids` that are some row's `parent_field` value.
123
+
124
+ One batched existence query decides chevrons for a whole sibling group,
125
+ so the cost is O(1 query) per expand regardless of how many children the
126
+ expanded node has.
127
+ """
128
+ ids = [i for i in ids if i is not None]
129
+ if not ids:
130
+ return set()
131
+ rows = self.source._query(col(self.parent_field).is_in(ids), [])
132
+ return {_key(r.get(self.parent_field)) for r in rows}
133
+
134
+ def roots(self) -> List[dict]:
135
+ """Build the specs for the top-level (root) nodes."""
136
+ return self._fetch(self.root_value)