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,882 @@
1
+ """Internal Chart composite — embeds a matplotlib figure in a themed frame.
2
+
3
+ Two modes, both hosting a matplotlib ``FigureCanvasTkAgg`` inside a bootstack
4
+ `Frame`:
5
+
6
+ - **Figure host** (``figure=``): the caller owns a `Figure`; the composite embeds
7
+ it and recolors its chrome (faces, spines, text) to the active theme — a
8
+ best-effort pass over the existing artists.
9
+ - **Managed render** (``render=``): the composite owns the figure and redraw
10
+ loop. Each redraw clears the axes, applies the theme as matplotlib rcParams
11
+ (including a semantic accent color cycle), calls the caller's ``render(ax,
12
+ data)``, and draws. It re-renders on theme change and whenever a bound
13
+ `Signal` changes (optionally debounced).
14
+
15
+ The public `bootstack.Chart` wrapper drives this; nothing here is public.
16
+ matplotlib is an optional dependency (the ``viz`` extra), imported lazily inside
17
+ the methods so importing bootstack never pulls it in.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import inspect
23
+ from typing import Any, Callable, Sequence
24
+
25
+ from bootstack.widgets._impl.primitives.frame import Frame
26
+ from bootstack.widgets.types import Master
27
+
28
+ # Accent roles used, in order, for the categorical series color cycle.
29
+ _CYCLE_ROLES = ("primary", "success", "info", "warning", "danger", "secondary")
30
+
31
+ # Standard navigation tools: name -> (icon glyph, group). The group index drives
32
+ # the auto-inserted divider between adjacent tools of different groups (history /
33
+ # navigate / io). `toolbar=True` renders them all, in this order.
34
+ _TOOL_ICONS = {
35
+ "home": "house",
36
+ "back": "arrow-left",
37
+ "forward": "arrow-right",
38
+ "pan": "arrows-move",
39
+ "zoom": "zoom-in",
40
+ "save": "download",
41
+ }
42
+ _TOOL_GROUP = {"home": 0, "back": 0, "forward": 0, "pan": 1, "zoom": 1, "save": 2}
43
+ _DEFAULT_TOOLS = ("home", "back", "forward", "pan", "zoom", "save")
44
+
45
+ # family name -> resolved family (or None if matplotlib can't find it). Cached so
46
+ # the theme-rc build does not probe the font manager on every redraw.
47
+ _FONT_CACHE: dict[str, str | None] = {}
48
+
49
+
50
+ def _color(token: str, fallback: str) -> str:
51
+ """Resolve a theme color token to a hex string, falling back on failure."""
52
+ from bootstack.style import get_theme_color
53
+
54
+ try:
55
+ return get_theme_color(token)
56
+ except Exception:
57
+ return fallback
58
+
59
+
60
+ def _resolve_font_family(name: str) -> str | None:
61
+ """Return `name` if matplotlib can resolve that font family, else None."""
62
+ if name in _FONT_CACHE:
63
+ return _FONT_CACHE[name]
64
+ resolved: str | None = None
65
+ try:
66
+ from matplotlib.font_manager import FontProperties, fontManager
67
+
68
+ if fontManager.findfont(FontProperties(family=name), fallback_to_default=False):
69
+ resolved = name
70
+ except Exception:
71
+ resolved = None
72
+ _FONT_CACHE[name] = resolved
73
+ return resolved
74
+
75
+
76
+ def _border_color(surface: str) -> str:
77
+ """The themed border color appropriate for `surface`.
78
+
79
+ Uses the style builder's ``border()`` (a soft stroke derived from the surface
80
+ by blending toward its on-color), so the chart's axis lines adapt to whatever
81
+ surface it sits on — slightly stronger on a tinted `card` than on `content`,
82
+ matching how every other widget borders itself.
83
+ """
84
+ base = _color(surface, _color("background", "#ffffff"))
85
+ try:
86
+ from bootstack.style.style import get_theme_provider
87
+ from bootstack.style.style_builder_ttk import StyleBuilderTtk
88
+
89
+ builder = StyleBuilderTtk(theme_provider=get_theme_provider(), style_instance=None)
90
+ return builder.border(base)
91
+ except Exception:
92
+ return _color("stroke", base)
93
+
94
+
95
+ def _theme_palette(surface: str = "content") -> dict[str, str]:
96
+ """The chart's chrome colors from the active theme (best-effort).
97
+
98
+ `surface` is the chart's inherited surface token, so the figure background
99
+ matches whatever the chart sits on (e.g. a `card`) instead of a fixed token —
100
+ no seam between the chart and its container.
101
+ """
102
+ fg = _color("foreground", "#212529")
103
+ bg = _color(surface, _color("background", "#ffffff"))
104
+ return {
105
+ "figure_bg": bg,
106
+ "axes_bg": bg,
107
+ "fg": fg,
108
+ "stroke": _border_color(surface),
109
+ "grid": _color("stroke_subtle", fg),
110
+ }
111
+
112
+
113
+ def _accent_cycle() -> list[str]:
114
+ """The categorical series colors from the active theme's accent roles."""
115
+ cycle: list[str] = []
116
+ for role in _CYCLE_ROLES:
117
+ hexval = _color(f"{role}[500]", "") or _color(role, "")
118
+ if hexval:
119
+ cycle.append(hexval)
120
+ return cycle
121
+
122
+
123
+ # seaborn runs lower-saturation than matplotlib (its plots are usually
124
+ # area-filled — bars, violins, KDE — where vivid blocks read heavy). We keep the
125
+ # brand hues but soften them to match the seaborn aesthetic, via seaborn's own
126
+ # `desat`. matplotlib line plots keep the full-saturation `[500]` cycle.
127
+ _SEABORN_DESAT = 0.75
128
+
129
+
130
+ def _seed_seaborn(cycle: list[str], desat: float = _SEABORN_DESAT) -> None:
131
+ """If seaborn is already imported, point its palette at the accent cycle.
132
+
133
+ A no-op when seaborn is not in use — the composite never imports it (it stays
134
+ in the optional ``viz-seaborn`` extra, off the import path). When the user has
135
+ imported seaborn, this is what makes a `sns.barplot(..., ax=ax)` (or any
136
+ categorical seaborn plot) pick up the theme's accent colors instead of
137
+ seaborn's own default palette — softened by `desat` to suit seaborn's muted,
138
+ fill-heavy look. Called inside the render `rc_context`, so it governs that
139
+ render and reverts with the rest of the themed rcParams.
140
+ """
141
+ import sys
142
+
143
+ sns = sys.modules.get("seaborn")
144
+ if sns is None or not cycle:
145
+ return
146
+ try:
147
+ sns.set_palette(sns.color_palette(cycle, desat=desat))
148
+ except Exception:
149
+ pass
150
+
151
+
152
+ def _theme_rc(surface: str = "content", with_cycle: bool = True) -> dict[str, Any]:
153
+ """Translate the active theme into a matplotlib rcParams dict.
154
+
155
+ Governs artists created within a ``matplotlib.rc_context(rc)`` block: figure
156
+ and axes faces, spine/tick/label/title colors, grid color, the theme body
157
+ font, and — when `with_cycle` is true — a semantic accent ``prop_cycle`` for
158
+ series. The chrome keys are always present; only the series color cycle is
159
+ optional, so ``themed=False`` keeps the chart fitting the app while leaving
160
+ data colors to the caller. `surface` is the chart's inherited surface token,
161
+ so the figure background matches its container.
162
+ """
163
+ pal = _theme_palette(surface)
164
+ fg = pal["fg"]
165
+ stroke = pal["stroke"]
166
+ rc: dict[str, Any] = {
167
+ "figure.facecolor": pal["figure_bg"],
168
+ "savefig.facecolor": pal["figure_bg"],
169
+ "axes.facecolor": pal["axes_bg"],
170
+ # Drop the heavy box: hide the top/right spines and soften the rest to the
171
+ # theme's border color instead of the near-black foreground.
172
+ "axes.edgecolor": stroke,
173
+ "axes.spines.top": False,
174
+ "axes.spines.right": False,
175
+ "axes.labelcolor": fg,
176
+ "axes.titlecolor": fg,
177
+ "text.color": fg,
178
+ "xtick.color": stroke, # tick marks soft
179
+ "ytick.color": stroke,
180
+ "xtick.labelcolor": fg, # tick labels stay readable
181
+ "ytick.labelcolor": fg,
182
+ "grid.color": pal["grid"],
183
+ "legend.facecolor": pal["axes_bg"],
184
+ "legend.edgecolor": stroke,
185
+ }
186
+
187
+ cycle = _accent_cycle() if with_cycle else []
188
+ if cycle:
189
+ try:
190
+ from cycler import cycler
191
+
192
+ rc["axes.prop_cycle"] = cycler(color=cycle)
193
+ except Exception:
194
+ pass
195
+
196
+ try:
197
+ from bootstack.style.style import get_theme_provider
198
+
199
+ spec = get_theme_provider().typography.body
200
+ rc["font.size"] = spec.size
201
+ family = _resolve_font_family(spec.font)
202
+ if family:
203
+ rc["font.family"] = family
204
+ except Exception:
205
+ pass
206
+
207
+ return rc
208
+
209
+
210
+ def _positional_arity(fn: Callable) -> int:
211
+ """Count the positional parameters of `fn` (``*args`` counts as 2)."""
212
+ try:
213
+ sig = inspect.signature(fn)
214
+ except (TypeError, ValueError):
215
+ return 2
216
+ params = sig.parameters.values()
217
+ if any(p.kind == p.VAR_POSITIONAL for p in params):
218
+ return 2
219
+ return sum(
220
+ 1 for p in params if p.kind in (p.POSITIONAL_ONLY, p.POSITIONAL_OR_KEYWORD)
221
+ )
222
+
223
+
224
+ def _as_artist_list(result: Any) -> list[Any]:
225
+ """Normalize a setup() return (one artist or several) to a list."""
226
+ if result is None:
227
+ return []
228
+ if isinstance(result, (list, tuple)):
229
+ return list(result)
230
+ return [result]
231
+
232
+
233
+ class Chart(Frame):
234
+ """Frame hosting a matplotlib figure — themed, optionally managed/reactive."""
235
+
236
+ def __init__(
237
+ self,
238
+ master: Master = None,
239
+ *,
240
+ figure: Any = None,
241
+ render: Callable | None = None,
242
+ signals: Sequence[Any] | None = None,
243
+ data_source: Any = None,
244
+ debounce: int = 0,
245
+ toolbar: bool | Sequence[str] = False,
246
+ themed: bool = True,
247
+ seaborn_desat: float = _SEABORN_DESAT,
248
+ **kwargs: Any,
249
+ ) -> None:
250
+ super().__init__(master, **kwargs)
251
+
252
+ from matplotlib.figure import Figure
253
+
254
+ from bootstack.scheduling import Schedule
255
+
256
+ self._themed = bool(themed)
257
+ self._seaborn_desat = float(seaborn_desat)
258
+ self._render = render
259
+ self._render_arity = _positional_arity(render) if render else 0
260
+ self._signals = list(signals) if signals else []
261
+ self._data_source = data_source
262
+ self._rows: list[Any] = []
263
+ self._source_handle: Any = None
264
+ self._debounce = max(0, int(debounce))
265
+ self._debounce_job: Any = None
266
+ self._render_pending = False
267
+ self._signal_handles: list[Any] = []
268
+ self._schedule = Schedule(self)
269
+ self._anim: Any = None # matplotlib FuncAnimation when animate() is used
270
+ self._anim_artists: list[Any] = []
271
+ self._anim_wanted = False # user intent: should it be playing
272
+ self._anim_running_now = False # actual timer state we last applied
273
+ self._anim_obscured = False # fully covered by another window
274
+ self._anim_first_draw_cid: Any = None
275
+
276
+ # Navigation toolbar. `_nav` is matplotlib's logic-only
277
+ # NavigationToolbar2Tk; `_navbar` is the themed bootstack bar that drives
278
+ # it. Built per-canvas in `_build_canvas` so a figure swap rebuilds both.
279
+ # `toolbar` selects the standard tools: True = all, a sequence = that
280
+ # subset (in order), False = no bar. An empty sequence builds an empty bar
281
+ # for author-only buttons via `chart.toolbar`.
282
+ if toolbar is False or toolbar is None:
283
+ self._toolbar_tools: list[str] | None = None
284
+ elif toolbar is True:
285
+ self._toolbar_tools = list(_DEFAULT_TOOLS)
286
+ else:
287
+ self._toolbar_tools = list(toolbar)
288
+ if self._toolbar_tools:
289
+ from bootstack.errors import BootstackError
290
+
291
+ unknown = [t for t in self._toolbar_tools if t not in _TOOL_ICONS]
292
+ if unknown:
293
+ raise BootstackError(
294
+ f"Chart(toolbar=) has unknown tool(s) {unknown}; valid tools "
295
+ f"are {list(_TOOL_ICONS)}."
296
+ )
297
+ self._show_toolbar = self._toolbar_tools is not None
298
+ self._canvas: Any = None
299
+ self._widget: Any = None
300
+ self._nav: Any = None
301
+ self._navbar: Any = None
302
+ self._navdiv: Any = None
303
+ self._pan_btn: Any = None
304
+ self._zoom_btn: Any = None
305
+ self._msg_label: Any = None
306
+
307
+ self._figure = figure if figure is not None else Figure()
308
+ # When the chart owns the figure (managed render / imperative drawing),
309
+ # use matplotlib's constrained layout so the axes fill the figure instead
310
+ # of leaving matplotlib's wide default subplot margins. A caller-supplied
311
+ # figure owns its own layout — leave it untouched.
312
+ self._owns_figure = figure is None
313
+ self._apply_owned_layout()
314
+ self._build_canvas()
315
+
316
+ # Bind a data source before the first render so its rows are available.
317
+ if self._data_source is not None:
318
+ self._rows = self._read_rows()
319
+ try:
320
+ self._source_handle = self._data_source.on_change(self._on_source_change)
321
+ except Exception:
322
+ self._source_handle = None
323
+
324
+ if self._render is not None:
325
+ self._render_managed()
326
+ else:
327
+ self._style_figure()
328
+ self._canvas.draw_idle()
329
+
330
+ self._first_paint_done = False
331
+ self._bind_signals()
332
+ # First-paint + visibility gating. A chart is usually built while its
333
+ # window is hidden (inside the `with App()/Workbench()` block), so the
334
+ # initial draw lands on an unmapped, default-sized canvas. If the realized
335
+ # size matches what was laid out while hidden, no <Configure> fires on
336
+ # show and matplotlib never repaints — the chart looks stale or wrong-
337
+ # sized until a manual resize or theme change forces one. Repaint once on
338
+ # first <Map> at the realized size to close that gap. <Map>/<Unmap> also
339
+ # gate animations (paused while hidden); <Visibility> catches a full cover
340
+ # (best-effort, mainly X11).
341
+ self.bind("<Map>", self._on_map, add="+")
342
+ self.bind("<Unmap>", self._on_anim_visibility, add="+")
343
+ self.bind("<Visibility>", self._on_anim_obscured, add="+")
344
+ self.bind("<Destroy>", self._on_destroy, add="+")
345
+
346
+ def _surface_token(self) -> str:
347
+ """The chart's inherited surface token (drives the figure background)."""
348
+ return getattr(self, "_surface", None) or "content"
349
+
350
+ def _apply_owned_layout(self) -> None:
351
+ """Use constrained layout on a figure we own, so the axes fill it."""
352
+ if not self._owns_figure:
353
+ return
354
+ try:
355
+ self._figure.set_layout_engine("constrained")
356
+ except Exception:
357
+ pass
358
+
359
+ # ----- canvas + navigation toolbar --------------------------------------
360
+
361
+ def _build_canvas(self) -> None:
362
+ """Embed the current figure, with the themed nav toolbar when enabled."""
363
+ from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
364
+
365
+ self._canvas = FigureCanvasTkAgg(self._figure, master=self)
366
+ self._widget = self._canvas.get_tk_widget()
367
+ self._widget.configure(highlightthickness=0, bd=0)
368
+ if self._show_toolbar:
369
+ self._build_navbar() # packs at the top, above the canvas
370
+ self._widget.pack(fill="both", expand=True)
371
+
372
+ def _teardown_canvas(self) -> None:
373
+ """Destroy the canvas and toolbar (for a figure swap)."""
374
+ self._pan_btn = self._zoom_btn = self._msg_label = None
375
+ for attr in ("_navbar", "_navdiv", "_nav", "_widget"):
376
+ widget = getattr(self, attr, None)
377
+ if widget is not None:
378
+ try:
379
+ widget.destroy()
380
+ except Exception:
381
+ pass
382
+ setattr(self, attr, None)
383
+ self._canvas = None
384
+
385
+ def _build_navbar(self) -> None:
386
+ """Build a themed bootstack toolbar driving matplotlib's navigation.
387
+
388
+ matplotlib splits navigation *logic* (history, pan, zoom-rectangle, the
389
+ canvas-drawn rubber band) from its raw-Tk button strip. We instantiate
390
+ `NavigationToolbar2Tk` with ``pack_toolbar=False`` so its unstyled tk
391
+ frame is created but never mapped, then wire a compact bootstack bar to
392
+ its methods — reusing the pan/zoom math and rubber band with zero
393
+ reimplementation, and none of the off-theme tk chrome.
394
+ """
395
+ from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk
396
+
397
+ from bootstack.widgets._impl.composites.toolbar import Toolbar as _Toolbar
398
+
399
+ from bootstack.widgets._impl.primitives.label import Label
400
+ from bootstack.widgets._impl.primitives.separator import Separator
401
+
402
+ self._nav = NavigationToolbar2Tk(self._canvas, self, pack_toolbar=False)
403
+ # Route matplotlib's coordinate readout to our themed label.
404
+ self._nav.set_message = self._set_nav_message
405
+
406
+ # The bar blends into the chart surface; a soft hairline below it (the
407
+ # shell chrome-divider strength) separates it from the canvas, instead of
408
+ # a tinted band.
409
+ commands = {
410
+ "home": self._nav.home,
411
+ "back": self._nav.back,
412
+ "forward": self._nav.forward,
413
+ "pan": self._toggle_pan,
414
+ "zoom": self._toggle_zoom,
415
+ "save": self._save_figure,
416
+ }
417
+ bar = _Toolbar(self, density="compact")
418
+ prev_group: int | None = None
419
+ for name in self._toolbar_tools or ():
420
+ # A divider falls between adjacent tools of different groups.
421
+ if prev_group is not None and _TOOL_GROUP[name] != prev_group:
422
+ bar.add_separator()
423
+ btn = bar.add_button(icon=_TOOL_ICONS[name], command=commands[name])
424
+ if name == "pan":
425
+ self._pan_btn = btn
426
+ elif name == "zoom":
427
+ self._zoom_btn = btn
428
+ prev_group = _TOOL_GROUP[name]
429
+ # The coordinate readout anchors to the right edge; author-added buttons
430
+ # pack to the left (after the built-in tools), so the two never collide.
431
+ self._msg_label = Label(bar.content, text="", font="caption")
432
+ self._msg_label.pack(side="right", padx=6)
433
+ bar.pack(side="top", fill="x")
434
+ divider = Separator(self, orient="horizontal", border_strength=0.90)
435
+ divider.pack(side="top", fill="x")
436
+ self._navbar = bar
437
+ self._navdiv = divider
438
+
439
+ def _toggle_pan(self) -> None:
440
+ if self._nav is not None:
441
+ self._nav.pan()
442
+ self._sync_nav_modes()
443
+
444
+ def _toggle_zoom(self) -> None:
445
+ if self._nav is not None:
446
+ self._nav.zoom()
447
+ self._sync_nav_modes()
448
+
449
+ def _sync_nav_modes(self) -> None:
450
+ """Reflect the active pan/zoom mode in the toolbar buttons."""
451
+ mode = str(getattr(self._nav, "mode", ""))
452
+ self._set_button_active(self._pan_btn, mode == "pan/zoom")
453
+ self._set_button_active(self._zoom_btn, mode == "zoom rect")
454
+
455
+ @staticmethod
456
+ def _set_button_active(btn: Any, active: bool) -> None:
457
+ if btn is None:
458
+ return
459
+ try:
460
+ if active:
461
+ btn.configure(variant="solid", accent="primary")
462
+ else:
463
+ btn.configure(variant="ghost", accent="")
464
+ except Exception:
465
+ pass
466
+
467
+ def _set_nav_message(self, text: str) -> None:
468
+ if self._msg_label is not None:
469
+ try:
470
+ self._msg_label.configure(text=text or "")
471
+ except Exception:
472
+ pass
473
+
474
+ def _save_figure(self) -> None:
475
+ """Save the figure through bootstack's themed file dialog."""
476
+ from bootstack.dialogs import ask_save_file
477
+
478
+ path = ask_save_file(
479
+ title="Save chart",
480
+ initial_file="chart.png",
481
+ file_types=[
482
+ ("PNG image", "*.png"),
483
+ ("PDF document", "*.pdf"),
484
+ ("SVG image", "*.svg"),
485
+ ("All files", "*.*"),
486
+ ],
487
+ default_extension=".png",
488
+ parent=self,
489
+ )
490
+ if path:
491
+ try:
492
+ self._figure.savefig(path)
493
+ except Exception:
494
+ pass
495
+
496
+ # ----- reactive wiring --------------------------------------------------
497
+
498
+ def _bind_signals(self) -> None:
499
+ for sig in self._signals:
500
+ try:
501
+ self._signal_handles.append(sig.subscribe(self._on_signal_change))
502
+ except Exception:
503
+ pass
504
+
505
+ def _on_signal_change(self, *_: Any) -> None:
506
+ if self._debounce > 0:
507
+ if self._debounce_job is not None:
508
+ try:
509
+ self._debounce_job.cancel()
510
+ except Exception:
511
+ pass
512
+ self._debounce_job = self._schedule.delay(self._debounce, self._request_render)
513
+ else:
514
+ self._request_render()
515
+
516
+ def _request_render(self) -> None:
517
+ """Coalesce a burst of changes into one render at the next idle frame.
518
+
519
+ A timer or stream that updates a bound signal faster than matplotlib can
520
+ draw would otherwise queue an unbounded backlog of synchronous rebuilds
521
+ and starve the event loop. Collapsing to one render per idle keeps the UI
522
+ responsive and the redraw rate bounded by what the figure can sustain.
523
+ """
524
+ if self._render_pending or not self.winfo_exists():
525
+ return
526
+ self._render_pending = True
527
+ self._schedule.idle(self._flush_render)
528
+
529
+ def _flush_render(self) -> None:
530
+ self._render_pending = False
531
+ self._render_managed()
532
+
533
+ def _current_data(self) -> Any:
534
+ """The data passed to a managed render.
535
+
536
+ The bound data source's rows when one is set; otherwise a signal value
537
+ (or a tuple of values for multiple signals); or None.
538
+ """
539
+ if self._data_source is not None:
540
+ return self._rows
541
+ if not self._signals:
542
+ return None
543
+ values = [sig() for sig in self._signals]
544
+ return values[0] if len(values) == 1 else tuple(values)
545
+
546
+ # ----- data source ------------------------------------------------------
547
+
548
+ def _read_rows(self) -> list[Any]:
549
+ """Read all current records from the source (respecting its filter/sort)."""
550
+ ds = self._data_source
551
+ if ds is None:
552
+ return []
553
+ try:
554
+ n = ds.count
555
+ return list(ds.page_slice(0, n)) if n else []
556
+ except Exception:
557
+ return []
558
+
559
+ def _on_source_change(self, _event: Any = None) -> None:
560
+ self._rows = self._read_rows()
561
+ self._request_render()
562
+
563
+ # ----- managed render ---------------------------------------------------
564
+
565
+ def _invoke_render(self, ax: Any, data: Any) -> None:
566
+ if self._render_arity >= 2:
567
+ self._render(ax, data)
568
+ else:
569
+ self._render(ax)
570
+
571
+ def _render_managed(self) -> None:
572
+ """Clear, apply the theme rc, call render, and draw.
573
+
574
+ The chrome (backgrounds, text, axes) is always themed so the chart fits
575
+ the app. ``themed=False`` only drops the accent series cycle and seaborn
576
+ palette, leaving data colors to the caller; explicit per-series colors win
577
+ either way. Reactivity is unchanged.
578
+ """
579
+ if self._render is None or not self.winfo_exists():
580
+ return
581
+ import matplotlib
582
+
583
+ rc = _theme_rc(self._surface_token(), with_cycle=self._themed)
584
+ fig = self._figure
585
+ fig.clear()
586
+ fig.set_facecolor(rc["figure.facecolor"])
587
+ data = self._current_data()
588
+ with matplotlib.rc_context(rc):
589
+ if self._themed:
590
+ _seed_seaborn(_accent_cycle(), self._seaborn_desat)
591
+ self._invoke_render(fig.add_subplot(111), data)
592
+ self._canvas.draw_idle()
593
+
594
+ # ----- figure-host theming ----------------------------------------------
595
+
596
+ def _style_figure(self) -> None:
597
+ """Recolor an externally-built figure's chrome to the theme.
598
+
599
+ Best-effort pass over the *existing* artists — it does not impose a grid
600
+ or restyle data series, which the figure's author owns. Never raises. The
601
+ chrome always tracks the theme (independent of ``themed=``, which only
602
+ governs whether the managed path imposes the accent series colors).
603
+ """
604
+ pal = _theme_palette(self._surface_token())
605
+ fig = self._figure
606
+ try:
607
+ fig.set_facecolor(pal["figure_bg"])
608
+ for ax in fig.axes:
609
+ self._recolor_axes(ax, pal)
610
+ except Exception:
611
+ pass
612
+
613
+ @staticmethod
614
+ def _recolor_axes(ax: Any, pal: dict[str, str]) -> None:
615
+ """Recolor one axes' chrome (faces, spines, ticks, labels) to the theme."""
616
+ ax.set_facecolor(pal["axes_bg"])
617
+ for spine in ax.spines.values():
618
+ spine.set_color(pal["stroke"])
619
+ ax.tick_params(color=pal["stroke"], labelcolor=pal["fg"], which="both")
620
+ ax.xaxis.label.set_color(pal["fg"])
621
+ ax.yaxis.label.set_color(pal["fg"])
622
+ ax.title.set_color(pal["fg"])
623
+ for label in (*ax.get_xticklabels(), *ax.get_yticklabels()):
624
+ label.set_color(pal["fg"])
625
+ for line in (*ax.get_xgridlines(), *ax.get_ygridlines()):
626
+ line.set_color(pal["grid"])
627
+ legend = ax.get_legend()
628
+ if legend is not None:
629
+ frame = legend.get_frame()
630
+ frame.set_facecolor(pal["axes_bg"])
631
+ frame.set_edgecolor(pal["fg"])
632
+ for text in legend.get_texts():
633
+ text.set_color(pal["fg"])
634
+
635
+ def _bs_apply_theme(self) -> None:
636
+ """Re-resolve theme colors and redraw (called after a theme rebuild).
637
+
638
+ Runs regardless of ``themed=`` — the chrome always follows the theme so
639
+ the chart keeps fitting the app; ``themed=False`` only drops the accent
640
+ series colors, which a re-render naturally honors.
641
+ """
642
+ if self._anim is not None:
643
+ # Recolor the axes chrome, then force the animation to recapture its
644
+ # blit background in the new theme. The blit cache only re-grabs when
645
+ # the axes *view* changes, so a recolor alone would keep restoring the
646
+ # stale background over the new colors — clearing the cache forces a
647
+ # fresh grab on the next frame. Animated artists keep their setup
648
+ # colors. (Falls back to a plain draw if matplotlib internals shift.)
649
+ pal = _theme_palette(self._surface_token())
650
+ try:
651
+ self._figure.set_facecolor(pal["figure_bg"])
652
+ for ax in self._figure.axes:
653
+ self._recolor_axes(ax, pal)
654
+ self._canvas.draw() # render the new-colored background to the buffer
655
+ cache = getattr(self._anim, "_blit_cache", None)
656
+ if cache is not None:
657
+ cache.clear() # next frame re-grabs the background
658
+ except Exception:
659
+ try:
660
+ self._canvas.draw()
661
+ except Exception:
662
+ pass
663
+ elif self._render is not None:
664
+ self._render_managed() # full re-render picks up the new rc + cycle
665
+ else:
666
+ self._style_figure()
667
+ try:
668
+ self._canvas.draw_idle()
669
+ except Exception:
670
+ pass
671
+
672
+ # ----- figure / lifecycle -----------------------------------------------
673
+
674
+ def set_figure(self, figure: Any) -> None:
675
+ """Replace the embedded figure, tearing down the old canvas widget."""
676
+ from matplotlib.figure import Figure
677
+
678
+ owns = figure is None
679
+ if figure is None:
680
+ figure = Figure()
681
+ self._teardown_canvas()
682
+ self._figure = figure
683
+ self._owns_figure = owns
684
+ self._apply_owned_layout()
685
+ self._build_canvas()
686
+ if self._render is not None:
687
+ self._render_managed()
688
+ else:
689
+ self._style_figure()
690
+ self._canvas.draw_idle()
691
+
692
+ def draw(self) -> None:
693
+ """Redraw — re-run the managed render, or refresh the hosted figure."""
694
+ if self._render is not None:
695
+ self._render_managed()
696
+ else:
697
+ try:
698
+ self._canvas.draw_idle()
699
+ except Exception:
700
+ pass
701
+
702
+ # ----- animation (blitting) ---------------------------------------------
703
+
704
+ def animate(
705
+ self,
706
+ setup: Callable,
707
+ update: Callable,
708
+ *,
709
+ interval: int = 30,
710
+ frames: Any = None,
711
+ ) -> Any:
712
+ """Drive a blitting animation, returning the matplotlib animation object.
713
+
714
+ Owns the figure: clears it, builds a themed axes, calls ``setup(ax)``
715
+ once to create the artists, then each frame calls ``update(value,
716
+ artists)`` and redraws only those artists over a cached background.
717
+ `value` is elapsed seconds for a continuous animation (so motion is
718
+ time-based and jitter-free), or the frame value when `frames` is given.
719
+ """
720
+ import itertools
721
+ import time
722
+
723
+ import matplotlib
724
+ from matplotlib.animation import FuncAnimation
725
+
726
+ self._stop_anim()
727
+ update_arity = _positional_arity(update)
728
+ # Continuous animation (no explicit `frames`) is driven by elapsed
729
+ # wall-clock time, not the frame index — so uneven timer delivery does
730
+ # not change the apparent velocity (no speed-up / slow-down jitter).
731
+ timed = frames is None
732
+ start = {"t": None}
733
+
734
+ rc = _theme_rc(self._surface_token(), with_cycle=self._themed)
735
+ fig = self._figure
736
+ fig.clear()
737
+ fig.set_facecolor(rc["figure.facecolor"])
738
+ # Create the artists ONCE. FuncAnimation re-invokes init_func on every
739
+ # full redraw (resize/theme), so init must only RETURN the artists —
740
+ # re-running setup there would accumulate a new artist each redraw.
741
+ with matplotlib.rc_context(rc):
742
+ if self._themed:
743
+ _seed_seaborn(_accent_cycle(), self._seaborn_desat)
744
+ self._anim_artists = _as_artist_list(setup(fig.add_subplot(111)))
745
+
746
+ def init() -> list[Any]:
747
+ return self._anim_artists
748
+
749
+ def func(frame: Any) -> list[Any]:
750
+ artists = self._anim_artists
751
+ arg = artists[0] if len(artists) == 1 else artists
752
+ if timed:
753
+ if start["t"] is None:
754
+ start["t"] = time.perf_counter()
755
+ value: Any = time.perf_counter() - start["t"]
756
+ else:
757
+ value = frame
758
+ if update_arity >= 2:
759
+ update(value, arg)
760
+ else:
761
+ update(value)
762
+ return self._anim_artists
763
+
764
+ self._anim = FuncAnimation(
765
+ fig,
766
+ func,
767
+ init_func=init,
768
+ frames=frames if frames is not None else itertools.count(),
769
+ interval=max(1, int(interval)),
770
+ blit=True,
771
+ cache_frame_data=False,
772
+ )
773
+ self._anim_wanted = True
774
+ self._anim_obscured = False
775
+ # FuncAnimation auto-starts on the first draw_event; assume running, then
776
+ # reconcile against actual visibility once that first draw has happened
777
+ # (covers a chart created on a hidden tab).
778
+ self._anim_running_now = True
779
+
780
+ def _after_first_draw(_event: Any) -> None:
781
+ if self._anim_first_draw_cid is not None:
782
+ try:
783
+ self._canvas.mpl_disconnect(self._anim_first_draw_cid)
784
+ except Exception:
785
+ pass
786
+ self._anim_first_draw_cid = None
787
+ self._apply_anim_state()
788
+
789
+ self._anim_first_draw_cid = self._canvas.mpl_connect("draw_event", _after_first_draw)
790
+ self._canvas.draw_idle()
791
+ return self._anim
792
+
793
+ def _anim_visible(self) -> bool:
794
+ """Whether the chart is actually visible — mapped and not fully covered."""
795
+ try:
796
+ return bool(self.winfo_viewable()) and not self._anim_obscured
797
+ except Exception:
798
+ return not self._anim_obscured
799
+
800
+ def _apply_anim_state(self) -> None:
801
+ """Run the animation only when wanted AND actually visible."""
802
+ anim = self._anim
803
+ if anim is None:
804
+ return
805
+ should = self._anim_wanted and self._anim_visible()
806
+ if should == self._anim_running_now:
807
+ return
808
+ try:
809
+ anim.resume() if should else anim.pause()
810
+ except Exception:
811
+ return
812
+ self._anim_running_now = should
813
+
814
+ def _set_anim_wanted(self, wanted: bool) -> None:
815
+ self._anim_wanted = bool(wanted)
816
+ self._apply_anim_state()
817
+
818
+ def _on_map(self, event: Any = None) -> None:
819
+ if event is not None and getattr(event, "widget", None) is not self:
820
+ return
821
+ # On first show, repaint at the realized size — the construction-time
822
+ # draw happened while hidden and may not have reached the screen.
823
+ if not self._first_paint_done:
824
+ self._first_paint_done = True
825
+ try:
826
+ self.draw()
827
+ except Exception:
828
+ pass
829
+ self._on_anim_visibility(event)
830
+
831
+ def _on_anim_visibility(self, event: Any = None) -> None:
832
+ if event is not None and getattr(event, "widget", None) is not self:
833
+ return
834
+ if self._anim is not None:
835
+ self._apply_anim_state()
836
+
837
+ def _on_anim_obscured(self, event: Any = None) -> None:
838
+ # `<Visibility>` is best-effort (mainly X11); only a FULL cover pauses.
839
+ state = getattr(event, "state", "") if event is not None else ""
840
+ self._anim_obscured = state == "VisibilityFullyObscured"
841
+ if self._anim is not None:
842
+ self._apply_anim_state()
843
+
844
+ def _stop_anim(self) -> None:
845
+ anim = self._anim
846
+ self._anim = None
847
+ self._anim_wanted = False
848
+ self._anim_running_now = False
849
+ if self._anim_first_draw_cid is not None:
850
+ try:
851
+ self._canvas.mpl_disconnect(self._anim_first_draw_cid)
852
+ except Exception:
853
+ pass
854
+ self._anim_first_draw_cid = None
855
+ if anim is not None:
856
+ try:
857
+ anim.event_source.stop()
858
+ except Exception:
859
+ pass
860
+
861
+ def _on_destroy(self, event: Any = None) -> None:
862
+ if event is not None and getattr(event, "widget", None) is not self:
863
+ return
864
+ self._stop_anim()
865
+ for handle in self._signal_handles:
866
+ try:
867
+ handle.cancel()
868
+ except Exception:
869
+ pass
870
+ self._signal_handles = []
871
+ if self._source_handle is not None:
872
+ try:
873
+ self._source_handle.cancel()
874
+ except Exception:
875
+ pass
876
+ self._source_handle = None
877
+ if self._debounce_job is not None:
878
+ try:
879
+ self._debounce_job.cancel()
880
+ except Exception:
881
+ pass
882
+ self._debounce_job = None