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,353 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Callable, Literal, TYPE_CHECKING
4
+
5
+ from bootstack.errors import BootstackError
6
+ from bootstack.validation import RuleType
7
+ from bootstack.validation.validation_rules import rule_applies_to_kind
8
+ from bootstack.widgets.types import AccentToken
9
+
10
+ if TYPE_CHECKING:
11
+ from bootstack.signals import Signal
12
+
13
+
14
+ class FieldAddonMixin:
15
+ """Mixin that exposes `insert_addon` and `addons` on public field wrappers.
16
+
17
+ Requires the inheriting class to have `self._internal` pointing to a
18
+ `Field`-based composite that implements `insert_addon` / `addons`.
19
+ """
20
+
21
+ #: The kind of value this field holds, used to reject inapplicable
22
+ #: validation rules at attach time. Text fields keep the default; typed
23
+ #: field wrappers override it (`'number'`, `'date'`, `'time'`).
24
+ _VALIDATION_KIND: str = "text"
25
+
26
+ _ADDON_TYPES = {
27
+ "button": "bootstack.widgets._impl.primitives.button::Button",
28
+ "label": "bootstack.widgets._impl.primitives.label::Label",
29
+ "toggle": "bootstack.widgets._impl.primitives.checktoggle::CheckToggle",
30
+ }
31
+
32
+ def insert_addon(
33
+ self,
34
+ widget: Literal["button", "label", "toggle"],
35
+ position: Literal["before", "after"],
36
+ *,
37
+ name: str | None = None,
38
+ text: str | None = None,
39
+ icon: str | None = None,
40
+ accent: AccentToken | str | None = None,
41
+ on_click: Callable[[], Any] | None = None,
42
+ signal: "Signal | None" = None,
43
+ active_when_readonly: bool = False,
44
+ ) -> Any:
45
+ """Insert a small widget inside the field border, before or after the input.
46
+
47
+ Use this for affordances such as a clear button, a search icon, a unit
48
+ suffix label, or an on/off toggle.
49
+
50
+ Args:
51
+ widget: Addon type — `'button'` (clickable), `'label'` (static
52
+ text or icon), or `'toggle'` (on/off control).
53
+ position: `'before'` (left of the input) or `'after'` (right).
54
+ name: Key to retrieve the addon later via `addons`. Auto-generated
55
+ if omitted.
56
+ text: Text shown on the addon. Applies to any addon type.
57
+ icon: Bootstrap Icons name shown on the addon (e.g. `'search'`,
58
+ `'x-lg'`). An icon-only addon is rendered when `icon` is given
59
+ without `text`.
60
+ accent: Accent token for the addon. Prefer an accent for a
61
+ text-only button.
62
+ on_click: Called with no arguments when a `'button'` or `'toggle'`
63
+ addon is activated.
64
+ signal: Reactive `Signal[bool]` bound to a `'toggle'` addon's
65
+ on/off state.
66
+ active_when_readonly: Keep the addon interactive while the field is
67
+ read-only. Off by default, so addons follow the field's
68
+ read-only state — appropriate since most act on the value (a
69
+ clear button, the spin buttons). Set `True` only for a
70
+ read-only-safe action such as a copy or reveal button; it still
71
+ dims when the field is fully disabled.
72
+
73
+ Returns:
74
+ The created addon widget instance.
75
+ """
76
+ if widget not in self._ADDON_TYPES:
77
+ raise ValueError(f"widget must be 'button', 'label', or 'toggle'; got {widget!r}")
78
+ if on_click is not None and widget == "label":
79
+ raise ValueError("on_click is not valid for a 'label' addon")
80
+ if signal is not None and widget != "toggle":
81
+ raise ValueError("signal is only valid for a 'toggle' addon")
82
+
83
+ module_path, cls_name = self._ADDON_TYPES[widget].split("::")
84
+ import importlib
85
+ cls = getattr(importlib.import_module(module_path), cls_name)
86
+
87
+ if name is None:
88
+ seq = getattr(self, "_addon_seq", 0)
89
+ self._addon_seq = seq + 1
90
+ name = f"addon_{seq}"
91
+
92
+ addon_kwargs: dict[str, Any] = {}
93
+ if text is not None:
94
+ addon_kwargs["text"] = text
95
+ if icon is not None:
96
+ addon_kwargs["icon"] = icon
97
+ if on_click is not None:
98
+ addon_kwargs["command"] = on_click
99
+ if signal is not None:
100
+ addon_kwargs["signal"] = signal
101
+
102
+ return self._internal.insert_addon(
103
+ cls, position, name=name, accent=accent,
104
+ active_when_readonly=active_when_readonly, **addon_kwargs,
105
+ )
106
+
107
+ def update_addon(
108
+ self,
109
+ name: str,
110
+ *,
111
+ text: str | None = None,
112
+ icon: str | None = None,
113
+ accent: AccentToken | str | None = None,
114
+ on_click: Callable[[], Any] | None = None,
115
+ ) -> None:
116
+ """Reconfigure an existing addon in place.
117
+
118
+ Only the options you pass are changed. Use this, for example, to flip a
119
+ toggle addon's label between two units, or swap a button's icon.
120
+
121
+ Args:
122
+ name: The addon's name (as passed to `insert_addon`).
123
+ text: New text for the addon.
124
+ icon: New Bootstrap Icons name for the addon.
125
+ accent: New accent token for the addon.
126
+ on_click: New click handler for a `'button'` or `'toggle'` addon.
127
+
128
+ Raises:
129
+ KeyError: If no addon with that name exists.
130
+ """
131
+ try:
132
+ addon = self._internal.addons[name]
133
+ except KeyError:
134
+ raise KeyError(f"no addon named {name!r}") from None
135
+ config: dict[str, Any] = {}
136
+ if text is not None:
137
+ config["text"] = text
138
+ if icon is not None:
139
+ config["icon"] = icon
140
+ if accent is not None:
141
+ config["accent"] = accent
142
+ if on_click is not None:
143
+ config["command"] = on_click
144
+ if config:
145
+ addon.configure(**config)
146
+
147
+ def remove_addon(self, name: str) -> None:
148
+ """Remove an addon inserted with `insert_addon()`.
149
+
150
+ Args:
151
+ name: The addon's name (as passed to `insert_addon`).
152
+
153
+ Raises:
154
+ KeyError: If no addon with that name exists.
155
+ """
156
+ self._internal.remove_addon(name)
157
+
158
+ @property
159
+ def addons(self) -> dict[str, Any]:
160
+ """Named addon widgets inserted via `insert_addon()`."""
161
+ return self._internal.addons
162
+
163
+ @property
164
+ def text(self) -> str:
165
+ """The current display text shown in the field.
166
+
167
+ This is the formatted string the user sees, as opposed to `value`, which
168
+ is the raw parsed datum (e.g. for a `DateField`, `text` is `'Jan 2, 2024'`
169
+ while `value` is a `date`). Read-only — assign to `value` to change it.
170
+ """
171
+ return self._internal.get()
172
+
173
+ @property
174
+ def valid(self) -> "Signal":
175
+ """Reactive `Signal[bool]` — whether the field currently passes validation.
176
+
177
+ Call it to read the current state (`field.valid()`), `subscribe` to
178
+ react, or bind it. Updated on every validation run — a blur, a keystroke
179
+ (for key-triggered rules), or a manual `validate()`.
180
+ """
181
+ return self._internal._entry._valid_signal
182
+
183
+ @property
184
+ def error(self) -> "Signal":
185
+ """Reactive `Signal[str]` — the current validation error, `''` when valid.
186
+
187
+ Bind it to surface the message anywhere, for example
188
+ `bs.Label(textsignal=field.error)`.
189
+ """
190
+ return self._internal._entry._error_signal
191
+
192
+ def add_validation_rule(self, rule_type: RuleType, **kwargs: Any) -> None:
193
+ """Add a validation rule to the field.
194
+
195
+ Rules run automatically on blur or key events depending on the rule
196
+ type, or manually via `validate()`. Multiple rules can be added;
197
+ they are evaluated in order and stop at the first failure.
198
+
199
+ Rules validate the field's **typed value** — a `custom` rule on a
200
+ numeric field receives a number, on a date field a `date`. Text rules
201
+ (`stringLength`, `pattern`, `email`) apply only to text fields; `range`
202
+ applies only to numeric/date/time fields. Attaching a rule to a field
203
+ whose value kind it cannot validate raises `BootstackError`.
204
+
205
+ Args:
206
+ rule_type: The kind of validation rule to apply.
207
+ **kwargs: Rule-specific options:
208
+
209
+ - `message` *(all rules)* — override the default error message.
210
+ - `trigger` *(all rules)* — when to run: `'always'` (key and
211
+ blur), `'key'`, `'blur'`, or `'manual'`. Each rule type
212
+ has its own sensible default (see the Validation reference).
213
+ - `min`, `max` *(stringLength)* — minimum/maximum character count.
214
+ - `min`, `max` *(range)* — lower/upper bound for the typed value
215
+ (numbers, or `date`/`time` objects).
216
+ - `pattern` *(pattern)* — regex string the value must match.
217
+ - `other_field` *(compare)* — field whose value must match.
218
+ - `func` *(custom)* — callable `(value) -> bool` receiving the
219
+ field's typed value.
220
+
221
+ Example::
222
+
223
+ field.add_validation_rule("required")
224
+ field.add_validation_rule("stringLength", min=3, max=50)
225
+ field.add_validation_rule("email", message="Enter a valid email.")
226
+ field.add_validation_rule("custom", func=lambda v: v.isdigit())
227
+
228
+ age.add_validation_rule("range", min=18, message="Must be 18+.")
229
+ """
230
+ if not isinstance(rule_type, str):
231
+ raise TypeError(
232
+ f"add_validation_rule() takes a rule-type string (e.g. "
233
+ f"'stringLength'), not a {type(rule_type).__name__} object. Pass "
234
+ f"the rule's options as keyword arguments: "
235
+ f"field.add_validation_rule('stringLength', min=3)."
236
+ )
237
+ kind = self._VALIDATION_KIND
238
+ if not rule_applies_to_kind(rule_type, kind):
239
+ if kind == "text":
240
+ hint = "'stringLength' bounds text length."
241
+ else:
242
+ hint = f"'range' or 'custom' validates {kind} values."
243
+ raise BootstackError(
244
+ f"the {rule_type!r} rule does not apply to a {kind} field "
245
+ f"({type(self).__name__}). {hint}"
246
+ )
247
+ self._internal.add_validation_rule(rule_type, **kwargs)
248
+
249
+
250
+ class ValueSignalMixin:
251
+ """Two-way binds a field's typed `.value` to a public `signal`.
252
+
253
+ The signal carries the field's **value** (a number, date, time, …) — not its
254
+ text. The inheriting wrapper must expose a `value` property (get and set) and
255
+ an `on_change` event; this mixin keeps the two in sync, seeding the field from
256
+ the signal and updating the signal when the field commits a new value.
257
+ """
258
+
259
+ value: Any
260
+ on_change: Callable
261
+
262
+ def _bind_value_signal(self, signal: "Signal") -> None:
263
+ """Establish the two-way binding between `self.value` and `signal`."""
264
+ self._value_signal = signal
265
+ self._value_syncing = False
266
+
267
+ current = signal()
268
+ if current is not None:
269
+ self.value = current
270
+ else:
271
+ self._push_to_signal(self.value)
272
+
273
+ def _from_signal(v: Any) -> None:
274
+ if self._value_syncing or v is None:
275
+ return
276
+ self._value_syncing = True
277
+ try:
278
+ self.value = v
279
+ finally:
280
+ self._value_syncing = False
281
+
282
+ def _to_signal(_event: Any) -> None:
283
+ if self._value_syncing:
284
+ return
285
+ v = self.value
286
+ if v is None:
287
+ return
288
+ self._value_syncing = True
289
+ try:
290
+ self._push_to_signal(v)
291
+ finally:
292
+ self._value_syncing = False
293
+
294
+ # Hold the subscription id so it can be released on destroy. A Signal
295
+ # usually outlives the widgets bound to it, so a dangling subscription
296
+ # would pin a destroyed field (and all it references) in memory.
297
+ self._value_sub = signal.subscribe(_from_signal)
298
+ self.on_change(_to_signal)
299
+ self.on_destroy(lambda _e: self._release_value_signal())
300
+
301
+ def _sync_value_set(self, value: Any) -> None:
302
+ """Push a programmatic `field.value = …` to the bound signal.
303
+
304
+ The `on_change`-driven sync only fires on a user commit, so a
305
+ programmatic value set would otherwise leave the signal stale. Call this
306
+ from the wrapper's `value` setter. A no-op when no signal is bound, while
307
+ a signal↔field sync is in flight, or for an empty value.
308
+ """
309
+ if getattr(self, "_value_signal", None) is None:
310
+ return
311
+ if getattr(self, "_value_syncing", False) or value is None:
312
+ return
313
+ self._value_syncing = True
314
+ try:
315
+ self._push_to_signal(self.value)
316
+ finally:
317
+ self._value_syncing = False
318
+
319
+ def _push_to_signal(self, value: Any) -> None:
320
+ """Write `value` to the bound signal, reconciling numeric types.
321
+
322
+ `Signal` is strict about value type (it only widens `int` into a
323
+ `float`-typed signal). A numeric field can produce either an `int` or a
324
+ `float`, so reconcile the two here. If the signal's type is genuinely
325
+ incompatible (for example an `int`-typed signal and a fractional value),
326
+ leave the signal unchanged rather than raising into the Tk event loop.
327
+ """
328
+ signal = self._value_signal
329
+ target = getattr(signal, "_type", None)
330
+ if target is not None and type(value) is not target:
331
+ if target is float and type(value) is int:
332
+ value = float(value)
333
+ elif target is int and type(value) is float and value.is_integer():
334
+ value = int(value)
335
+ try:
336
+ signal.set(value)
337
+ except TypeError:
338
+ pass
339
+
340
+ def _release_value_signal(self) -> None:
341
+ """Drop the value-signal subscription (called on widget destroy)."""
342
+ sub = getattr(self, "_value_sub", None)
343
+ if sub is not None:
344
+ try:
345
+ sub.cancel()
346
+ except Exception:
347
+ pass
348
+ self._value_sub = None
349
+
350
+ @property
351
+ def signal(self) -> "Signal | None":
352
+ """The reactive `Signal` bound to this field's value, or `None`."""
353
+ return getattr(self, "_value_signal", None)
@@ -0,0 +1,72 @@
1
+ """Reusable `icon` / `image` property mixins for widgets that accept them.
2
+
3
+ Widgets in the button family take a widget-level `icon=` (a Bootstrap icon name)
4
+ and/or `image=` (an `Image` handle) at construction. These mixins expose those as
5
+ live, settable properties so they can be changed after construction — `image`
6
+ updates in place (no rebuild, no flicker).
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from bootstack.widgets.types import IconSpec
15
+
16
+
17
+ class IconProperty:
18
+ """Adds a live `icon` property to a widget whose internal accepts `icon=`."""
19
+
20
+ @property
21
+ def icon(self) -> "str | IconSpec | None":
22
+ """The icon shown on the widget, or `None` if none is set.
23
+
24
+ A Bootstrap Icons name, or an `IconSpec` mapping (`{'name', 'size',
25
+ 'color'}`) for control over size and color.
26
+ """
27
+ return self._internal.configure_style_options("icon")
28
+
29
+ @icon.setter
30
+ def icon(self, value: "str | IconSpec | None") -> None:
31
+ self._internal.configure(icon=value)
32
+
33
+
34
+ class ImageProperty:
35
+ """Adds a live `image` property to a widget whose internal accepts `image=`."""
36
+
37
+ @property
38
+ def image(self) -> Any:
39
+ """The displayed image handle, or `None`.
40
+
41
+ Assigning an `Image` (from `bootstack.images`) updates the picture in
42
+ place — no rebuild — which avoids the flicker of recreating the widget.
43
+ """
44
+ return getattr(self, "_image_handle", None)
45
+
46
+ @image.setter
47
+ def image(self, value: Any) -> None:
48
+ from bootstack.widgets._core.image_binding import (
49
+ _release_theme_binding,
50
+ bind_image,
51
+ )
52
+
53
+ if value is None:
54
+ # Drop any theme-following binding from a previous Image, otherwise it
55
+ # keeps re-applying a now-removed image on theme changes (and pins it).
56
+ _release_theme_binding(self)
57
+ self._internal.configure(image="")
58
+ self._image_handle = None
59
+ self._image_photo = None
60
+ return
61
+ from bootstack.images import Image as _ImageHandle
62
+
63
+ if isinstance(value, _ImageHandle):
64
+ bind_image(self, self._internal, value)
65
+ else:
66
+ _release_theme_binding(self)
67
+ self._internal.configure(image=value)
68
+ self._image_handle = None
69
+ self._image_photo = value
70
+
71
+
72
+ __all__ = ["IconProperty", "ImageProperty"]
@@ -0,0 +1,93 @@
1
+ """Bind a public image handle onto an internal widget.
2
+
3
+ The `image=` slot on text widgets accepts a public `bootstack.images.Image`
4
+ handle. Because a handle holds no display resource until it is used, this helper
5
+ renders it against the target widget and, when the handle follows the theme (a
6
+ token-colored icon), re-renders it whenever the theme changes — releasing the
7
+ binding when the widget is destroyed.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import tkinter as tk
13
+ from typing import TYPE_CHECKING
14
+
15
+ if TYPE_CHECKING:
16
+ from bootstack.images import Image
17
+
18
+
19
+ def bind_image(public_widget, internal_widget, image: "Image") -> None:
20
+ """Render `image` onto `internal_widget`, following the theme if needed.
21
+
22
+ Args:
23
+ public_widget: The public wrapper; strong references to the handle and
24
+ its rendered image are stored on it so they are not collected.
25
+ internal_widget: The underlying widget to display the image on.
26
+ image: The image handle to render and bind.
27
+ """
28
+ # Calling this again on the same widget (a live `image=` update) must not
29
+ # stack theme bindings — release any prior one first.
30
+ _release_theme_binding(public_widget)
31
+
32
+ photo = image._materialize(internal_widget)
33
+ internal_widget.configure(image=photo)
34
+ public_widget._image_handle = image
35
+ public_widget._image_photo = photo
36
+
37
+ if not image._is_theme_following:
38
+ return
39
+
40
+ root = internal_widget.winfo_toplevel()
41
+
42
+ def _reapply(_event=None) -> None:
43
+ try:
44
+ image._invalidate()
45
+ new_photo = image._materialize(internal_widget)
46
+ internal_widget.configure(image=new_photo)
47
+ public_widget._image_photo = new_photo
48
+ except tk.TclError:
49
+ pass
50
+
51
+ bind_id = root.bind("<<BsThemeChanged>>", _reapply, add="+")
52
+ public_widget._image_theme_binding = (root, bind_id)
53
+
54
+ def _release(event) -> None:
55
+ if event.widget is not internal_widget:
56
+ return
57
+ _release_theme_binding(public_widget)
58
+
59
+ internal_widget.bind("<Destroy>", _release, add="+")
60
+
61
+
62
+ def _release_theme_binding(public_widget) -> None:
63
+ """Unbind a previously installed `<<BsThemeChanged>>` handler, if any."""
64
+ prev = getattr(public_widget, "_image_theme_binding", None)
65
+ if prev is None:
66
+ return
67
+ root, bind_id = prev
68
+ try:
69
+ root.unbind("<<BsThemeChanged>>", bind_id)
70
+ except tk.TclError:
71
+ pass
72
+ public_widget._image_theme_binding = None
73
+
74
+
75
+ def resolve_window_icon(icon):
76
+ """Normalize a window `icon=` argument to `(path, deferred_image)`.
77
+
78
+ Exactly one of the returned values is non-`None` (or both are, for `None`
79
+ input). A `path` can be handed to the window at construction; a deferred
80
+ image must be rendered and applied after the window's root exists.
81
+
82
+ Args:
83
+ icon: An icon file path, an `Image` handle, or an `AppIcon`.
84
+ """
85
+ if icon is None:
86
+ return None, None
87
+ from bootstack.images import AppIcon, Image
88
+
89
+ if isinstance(icon, AppIcon):
90
+ return icon._icon_path(), None
91
+ if isinstance(icon, Image):
92
+ return None, icon
93
+ return str(icon), None