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,764 @@
1
+ """Canvas-based scrollable container widget with mouse wheel support."""
2
+ from tkinter import Canvas
3
+ from tkinter.ttk import Widget
4
+ from typing import Any, Literal
5
+
6
+ from bootstack.widgets._impl.primitives.frame import Frame
7
+ from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
8
+ from bootstack.widgets._impl.primitives.scrollbar import Scrollbar
9
+ from bootstack.widgets.types import Master
10
+ from bootstack.events import ScrollEvent
11
+
12
+
13
+ class ScrollView(Frame):
14
+ """A canvas-based scrollable container with configurable scrollbar behavior.
15
+
16
+ The ScrollView widget provides a scrollable area for child widgets with
17
+ full mouse wheel support on all descendants. Scrollbars can be configured
18
+ to appear always, never, on hover, or when scrolling, and are only visible
19
+ when the content exceeds the available space.
20
+
21
+ Attributes:
22
+ canvas: The underlying tkinter Canvas widget.
23
+ vertical_scrollbar: The vertical scrollbar widget.
24
+ horizontal_scrollbar: The horizontal scrollbar widget.
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ master: Master = None,
30
+ scroll_direction: Literal['horizontal', 'vertical', 'both'] = 'both',
31
+ scrollbar_visibility: Literal['always', 'never', 'hover', 'scroll'] = 'always',
32
+ autohide_delay: int = 1000, # milliseconds for scroll mode
33
+ scrollbar_variant: str = 'default',
34
+ **kwargs: Any
35
+ ):
36
+ """Initialize a ScrollView widget.
37
+
38
+ Args:
39
+ master: The parent widget.
40
+ scroll_direction: Scroll direction - 'horizontal' for horizontal only,
41
+ 'vertical' for vertical only, or 'both' for bidirectional
42
+ scrolling. Horizontal scrolling uses Shift+MouseWheel.
43
+ scrollbar_visibility: Scrollbar visibility mode:
44
+ - 'always': Scrollbars always visible
45
+ - 'never': Scrollbars hidden (scrolling still works)
46
+ - 'hover': Scrollbars appear when mouse enters the widget
47
+ - 'scroll': Scrollbars appear when scrolling, auto-hide after delay
48
+ autohide_delay: Time in milliseconds before auto-hiding scrollbars
49
+ in 'scroll' mode. Default is 1000ms (1 second).
50
+ scrollbar_variant: The variant to apply to scrollbars (e.g., 'default',
51
+ 'round'). If None, uses the default scrollbar variant.
52
+ **kwargs: Additional keyword arguments passed to the Frame parent class.
53
+
54
+ Note:
55
+ Mouse wheel scrolling is automatically enabled on all child widgets,
56
+ including those added dynamically. For manual refresh of bindings
57
+ after adding many widgets at once, call refresh_bindings().
58
+ """
59
+ canvas_height = kwargs.pop('height', None)
60
+ canvas_width = kwargs.pop('width', None)
61
+ super().__init__(master, **kwargs)
62
+
63
+ # configuration
64
+ self._direction = scroll_direction
65
+ self._scrollbar_visibility = scrollbar_visibility
66
+ self._autohide_delay = autohide_delay
67
+ self._scrollbar_variant = scrollbar_variant
68
+
69
+ self._child_widget = None
70
+ self._window_id = None
71
+ self._hide_timer = None
72
+ self._scrolling_enabled = False
73
+ self._hovering = False
74
+
75
+ # Create unique bind tag for this scrollview
76
+ self._scroll_tag = f'ScrollView_{id(self)}'
77
+
78
+ # Detect windowing system
79
+ self.winsys = self.tk.call("tk", "windowingsystem")
80
+
81
+ # Bind scroll events to our custom tag
82
+ self._setup_scroll_tag_bindings()
83
+
84
+ # Create canvas
85
+ canvas_kw: dict[str, Any] = {"highlightthickness": 0, "borderwidth": 0}
86
+ if canvas_height is not None:
87
+ canvas_kw["height"] = canvas_height
88
+ if canvas_width is not None:
89
+ canvas_kw["width"] = canvas_width
90
+ self.canvas = Canvas(self, **canvas_kw)
91
+ self.canvas.bind("<Configure>", self._on_canvas_configure)
92
+ # Focus canvas on click so keyboard scrolling works.
93
+ self.canvas.bind("<Button-1>", lambda e: self.canvas.focus_set(), add="+")
94
+ self._setup_keyboard_bindings()
95
+
96
+ # Create scrollbars. Thread our own surface through so the scrollbar track
97
+ # matches the container (an auto-hiding bar then leaves no tinted gutter).
98
+ sb_surface = getattr(self, "_surface", None)
99
+ sb_kw: dict[str, Any] = {"surface": sb_surface} if sb_surface else {}
100
+ self.vertical_scrollbar = Scrollbar(
101
+ master=self,
102
+ orient='vertical',
103
+ command=self.canvas.yview,
104
+ variant=self._scrollbar_variant,
105
+ **sb_kw,
106
+ )
107
+ self.horizontal_scrollbar = Scrollbar(
108
+ master=self,
109
+ orient='horizontal',
110
+ command=self.canvas.xview,
111
+ variant=self._scrollbar_variant,
112
+ **sb_kw,
113
+ )
114
+
115
+ # Configure canvas scrolling
116
+ scroll_config = {}
117
+ if scroll_direction in ('vertical', 'both'):
118
+ scroll_config['yscrollcommand'] = self._on_canvas_scroll_y
119
+ if scroll_direction in ('horizontal', 'both'):
120
+ scroll_config['xscrollcommand'] = self._on_canvas_scroll_x
121
+
122
+ self.canvas.configure(**scroll_config)
123
+
124
+ # Layout
125
+ self._layout_widgets()
126
+
127
+ # Bind events for autohide/hover
128
+ self._bind_container_events()
129
+
130
+ # Initial scrollbar visibility
131
+ self._update_scrollbar_visibility()
132
+
133
+ # Finalize content sizing on first show. A ScrollView is usually built
134
+ # inside a hidden window (the `with App()/Workbench()` block), so the
135
+ # canvas <Configure> that pins the content width to the viewport may fire
136
+ # at a transient size — or never at the final size — leaving the content
137
+ # at its natural width (inflated by wide children, e.g. a chart's default
138
+ # request). It then overflows until a user resize or theme change triggers
139
+ # a fresh <Configure>. Re-pin the width and recompute the scroll region
140
+ # once we are actually mapped.
141
+ self._first_map_done = False
142
+ self.bind('<Map>', self._on_first_map, add='+')
143
+
144
+ @configure_delegate('scroll_direction')
145
+ def _delegate_scroll_direction(self, value=None):
146
+ """Get or set the scroll direction."""
147
+ if value is None:
148
+ return self._direction
149
+ else:
150
+ old_direction = self._direction
151
+ self._direction = value
152
+
153
+ # Update canvas scroll configuration
154
+ scroll_config = {}
155
+ if value in ('vertical', 'both'):
156
+ scroll_config['yscrollcommand'] = self._on_canvas_scroll_y
157
+ else:
158
+ scroll_config['yscrollcommand'] = None
159
+
160
+ if value in ('horizontal', 'both'):
161
+ scroll_config['xscrollcommand'] = self._on_canvas_scroll_x
162
+ else:
163
+ scroll_config['xscrollcommand'] = None
164
+
165
+ self.canvas.configure(**scroll_config)
166
+
167
+ # Update scrollbar visibility and layout
168
+ self._update_scrollbar_visibility()
169
+ return None
170
+
171
+ @configure_delegate('scrollbar_visibility')
172
+ def _delegate_scrollbar_visibility(self, value=None):
173
+ """Get or set the scrollbar visibility mode."""
174
+ if value is None:
175
+ return self._scrollbar_visibility
176
+ else:
177
+ old_value = self._scrollbar_visibility
178
+ self._scrollbar_visibility = value
179
+
180
+ # Unbind old events if changing from hover
181
+ if old_value == 'hover':
182
+ self.unbind('<Enter>')
183
+ self.unbind('<Leave>')
184
+ self.canvas.unbind('<Enter>')
185
+ self.canvas.unbind('<Leave>')
186
+ self.vertical_scrollbar.unbind('<Enter>')
187
+ self.vertical_scrollbar.unbind('<Leave>')
188
+ self.horizontal_scrollbar.unbind('<Enter>')
189
+ self.horizontal_scrollbar.unbind('<Leave>')
190
+
191
+ # Sync gutter reservation with the new mode
192
+ if value in ('hover', 'scroll'):
193
+ self._set_scrollbar_gutter(reserve=True)
194
+ else:
195
+ self._set_scrollbar_gutter(reserve=False)
196
+
197
+ # Bind new events and update scrollbar visibility
198
+ self._bind_container_events()
199
+ self._update_scrollbar_visibility()
200
+
201
+ # Update scrolling enabled state
202
+ if value in ('always', 'never', 'scroll'):
203
+ if self._child_widget:
204
+ self.enable_scrolling()
205
+ elif value == 'hover':
206
+ # Scrolling will be enabled on hover
207
+ self.disable_scrolling()
208
+ return None
209
+
210
+ @configure_delegate('autohide_delay')
211
+ def _delegate_autohide_delay(self, value=None):
212
+ """Get or set the autohide delay in milliseconds."""
213
+ if value is None:
214
+ return self._autohide_delay
215
+ else:
216
+ self._autohide_delay = value
217
+ return None
218
+
219
+ @configure_delegate('scrollbar_variant')
220
+ def _delegate_scrollbar_variant(self, value=None):
221
+ """Get or set the scrollbar variant."""
222
+ if value is None:
223
+ return self._scrollbar_variant
224
+ else:
225
+ self._scrollbar_variant = value
226
+ # Apply the new variant to both scrollbars
227
+ if value:
228
+ self.vertical_scrollbar.configure(variant=value)
229
+ self.horizontal_scrollbar.configure(variant=value)
230
+ return None
231
+
232
+ def _setup_scroll_tag_bindings(self):
233
+ """Setup bindings on our custom bind tag."""
234
+ if self.winsys.lower() == "x11":
235
+ self.bind_class(self._scroll_tag, "<Button-4>", self._on_mousewheel)
236
+ self.bind_class(self._scroll_tag, "<Button-5>", self._on_mousewheel)
237
+ self.bind_class(self._scroll_tag, "<Shift-Button-4>", self._on_shift_mousewheel)
238
+ self.bind_class(self._scroll_tag, "<Shift-Button-5>", self._on_shift_mousewheel)
239
+ else:
240
+ self.bind_class(self._scroll_tag, "<MouseWheel>", self._on_mousewheel)
241
+ self.bind_class(self._scroll_tag, "<Shift-MouseWheel>", self._on_shift_mousewheel)
242
+
243
+ def _setup_keyboard_bindings(self) -> None:
244
+ """Bind arrow / page / home / end keys to the canvas for keyboard scroll."""
245
+ c = self.canvas
246
+ # Vertical
247
+ c.bind("<Up>", lambda e: self.canvas.yview_scroll(-1, "units") or "break", add="+")
248
+ c.bind("<Down>", lambda e: self.canvas.yview_scroll( 1, "units") or "break", add="+")
249
+ c.bind("<Prior>", lambda e: self.canvas.yview_scroll(-1, "pages") or "break", add="+")
250
+ c.bind("<Next>", lambda e: self.canvas.yview_scroll( 1, "pages") or "break", add="+")
251
+ c.bind("<Home>", lambda e: self.canvas.yview_moveto(0.0) or "break", add="+")
252
+ c.bind("<End>", lambda e: self.canvas.yview_moveto(1.0) or "break", add="+")
253
+ # Horizontal
254
+ c.bind("<Left>", lambda e: self.canvas.xview_scroll(-1, "units") or "break", add="+")
255
+ c.bind("<Right>", lambda e: self.canvas.xview_scroll( 1, "units") or "break", add="+")
256
+
257
+ def _layout_widgets(self):
258
+ """Layout the canvas and scrollbars.
259
+
260
+ For 'hover' and 'scroll' visibility modes the grid column and row that
261
+ hold the scrollbars are given a fixed minimum size equal to the
262
+ scrollbar's natural dimensions before the scrollbars are hidden. This
263
+ reserves the gutter permanently so the canvas never resizes when a
264
+ scrollbar is toggled — the same principle as the CSS
265
+ `scrollbar-gutter: stable` property.
266
+
267
+ An overlay approach (lift/lower over the canvas) was considered but
268
+ rejected because the scrollbar then covers content, which was reported
269
+ as confusing by users. Reserving the gutter avoids both the
270
+ layout-shift problem *and* the content-coverage problem.
271
+
272
+ For 'never' mode the scrollbars are never placed in the grid at all.
273
+ Gridding them and immediately calling grid_remove() leaves Tkinter's
274
+ geometry manager holding the column's natural width until the next idle
275
+ cycle, which produces a visible gap beside the content area.
276
+ """
277
+ self.canvas.grid(row=0, column=0, sticky='nsew')
278
+
279
+ # Configure grid weights
280
+ self.grid_rowconfigure(0, weight=1)
281
+ self.grid_columnconfigure(0, weight=1)
282
+
283
+ if self._scrollbar_visibility == 'never':
284
+ # Don't touch the grid for the scrollbar columns/rows — column 1
285
+ # and row 1 stay at zero width so no space is reserved.
286
+ return
287
+
288
+ if self._direction in ('vertical', 'both'):
289
+ self.vertical_scrollbar.grid(row=0, column=1, sticky='ns')
290
+
291
+ if self._direction in ('horizontal', 'both'):
292
+ self.horizontal_scrollbar.grid(row=1, column=0, sticky='ew')
293
+
294
+ # Keep scrollbars above the canvas/content when visible
295
+ self.vertical_scrollbar.lift()
296
+ self.horizontal_scrollbar.lift()
297
+
298
+ if self._scrollbar_visibility in ('hover', 'scroll'):
299
+ # Hide scrollbars now; defer gutter measurement to after_idle so
300
+ # the style system has finished sizing the widgets before we read
301
+ # their dimensions. Measuring too early (before the widget is
302
+ # placed in its parent) can return an inflated reqwidth and leave
303
+ # a visible gap beside the scrollbar when it appears.
304
+ self.vertical_scrollbar.grid_remove()
305
+ self.horizontal_scrollbar.grid_remove()
306
+ self.after_idle(lambda: self._set_scrollbar_gutter(reserve=True))
307
+
308
+ def _set_scrollbar_gutter(self, reserve: bool):
309
+ """Reserve or release the grid gutter used by each scrollbar.
310
+
311
+ When *reserve* is `True` the column (vertical scrollbar) and row
312
+ (horizontal scrollbar) are given a `minsize` equal to the scrollbar's
313
+ natural requested dimensions. The minsize persists even after the
314
+ scrollbar widget is removed from the grid via `grid_remove()`, so the
315
+ canvas occupies a constant area regardless of scrollbar visibility.
316
+
317
+ When *reserve* is `False` the minsize is cleared (set to 0), which
318
+ is appropriate for 'always' and 'never' modes where no gutter needs to
319
+ be held open.
320
+ """
321
+ if reserve:
322
+ if self._direction in ('vertical', 'both'):
323
+ width = self.vertical_scrollbar.winfo_reqwidth()
324
+ if width > 1:
325
+ self.grid_columnconfigure(1, minsize=width)
326
+ if self._direction in ('horizontal', 'both'):
327
+ height = self.horizontal_scrollbar.winfo_reqheight()
328
+ if height > 1:
329
+ self.grid_rowconfigure(1, minsize=height)
330
+ else:
331
+ self.grid_columnconfigure(1, minsize=0)
332
+ self.grid_rowconfigure(1, minsize=0)
333
+
334
+ def _bind_container_events(self):
335
+ """Bind events for the container (enter/leave for autohide)."""
336
+ if self._scrollbar_visibility == 'hover':
337
+ self.bind('<Enter>', self._on_container_enter)
338
+ self.bind('<Leave>', self._on_container_leave)
339
+ self.canvas.bind('<Enter>', self._on_container_enter)
340
+ self.canvas.bind('<Leave>', self._on_container_leave)
341
+ self.vertical_scrollbar.bind('<Enter>', self._on_container_enter)
342
+ self.vertical_scrollbar.bind('<Leave>', self._on_container_leave)
343
+ self.horizontal_scrollbar.bind('<Enter>', self._on_container_enter)
344
+ self.horizontal_scrollbar.bind('<Leave>', self._on_container_leave)
345
+
346
+ def _on_container_enter(self, event):
347
+ """Handle mouse entering the container."""
348
+ self._hovering = True
349
+ self.enable_scrolling()
350
+ if self._scrollbar_visibility == 'hover':
351
+ self._show_scrollbars()
352
+
353
+ def _on_container_leave(self, event):
354
+ """Handle mouse leaving the container."""
355
+ self._hovering = False
356
+ self.disable_scrolling()
357
+ if self._scrollbar_visibility == 'hover':
358
+ self._hide_scrollbars()
359
+
360
+ def _content_fits(self):
361
+ """Return booleans for whether content fits in the viewport (x_fit, y_fit)."""
362
+ if self._window_id:
363
+ bbox = self.canvas.bbox(self._window_id)
364
+ else:
365
+ bbox = self.canvas.bbox('all')
366
+ if not bbox:
367
+ return True, True
368
+ x0, y0, x1, y1 = bbox
369
+ content_w = x1 - x0
370
+ content_h = y1 - y0
371
+ viewport_w = max(1, self.canvas.winfo_width())
372
+ viewport_h = max(1, self.canvas.winfo_height())
373
+ if viewport_w <= 1 or viewport_h <= 1:
374
+ return True, True
375
+ return content_w <= viewport_w, content_h <= viewport_h
376
+
377
+ def _show_scrollbars(self):
378
+ """Show scrollbars only if content overflows the viewport."""
379
+ x_fit, y_fit = self._content_fits()
380
+ if self._direction in ('vertical', 'both') and not y_fit:
381
+ self.vertical_scrollbar.grid(row=0, column=1, sticky='ns')
382
+ self.vertical_scrollbar.lift()
383
+ else:
384
+ self.vertical_scrollbar.grid_remove()
385
+ if self._direction in ('horizontal', 'both') and not x_fit:
386
+ self.horizontal_scrollbar.grid(row=1, column=0, sticky='ew')
387
+ self.horizontal_scrollbar.lift()
388
+ else:
389
+ self.horizontal_scrollbar.grid_remove()
390
+
391
+ def _hide_scrollbars(self):
392
+ """Hide scrollbars."""
393
+ self.vertical_scrollbar.grid_remove()
394
+ self.horizontal_scrollbar.grid_remove()
395
+
396
+ def _on_canvas_configure(self, event):
397
+ """Pin content width to viewport and update visibility."""
398
+ if self._window_id and self._direction in ('vertical', 'both'):
399
+ self.canvas.itemconfig(self._window_id, width=event.width)
400
+ self._update_scrollbar_visibility()
401
+
402
+ def _on_first_map(self, event=None):
403
+ """On the first show, finalize content sizing (see __init__)."""
404
+ if event is not None and event.widget is not self:
405
+ return
406
+ if self._first_map_done:
407
+ return
408
+ self._first_map_done = True
409
+ # Defer to idle so the geometry manager has assigned the canvas its real
410
+ # viewport size before we read it.
411
+ self.after_idle(self._finalize_layout)
412
+
413
+ def _finalize_layout(self):
414
+ """Pin the content width to the realized viewport and recompute scroll."""
415
+ try:
416
+ if not self.winfo_exists():
417
+ return
418
+ self.canvas.update_idletasks()
419
+ width = self.canvas.winfo_width()
420
+ if (self._window_id and width > 1
421
+ and self._direction in ('vertical', 'both')):
422
+ self.canvas.itemconfig(self._window_id, width=width)
423
+ self.canvas.configure(scrollregion=self.canvas.bbox('all'))
424
+ self._update_scrollbar_visibility()
425
+ except Exception:
426
+ pass
427
+
428
+ def _on_canvas_scroll_y(self, first, last):
429
+ """Update vertical scrollbar position."""
430
+ self.vertical_scrollbar.set(first, last)
431
+ self._update_scrollbar_visibility()
432
+ self._emit_scroll_event()
433
+
434
+ def _on_canvas_scroll_x(self, first, last):
435
+ """Update horizontal scrollbar position."""
436
+ self.horizontal_scrollbar.set(first, last)
437
+ self._update_scrollbar_visibility()
438
+ self._emit_scroll_event()
439
+
440
+ def _emit_scroll_event(self) -> None:
441
+ y = float(self.canvas.yview()[0])
442
+ x = float(self.canvas.xview()[0])
443
+ self.event_generate("<<BsScroll>>", data=ScrollEvent(y=y, x=x))
444
+
445
+ @property
446
+ def scroll_position(self) -> tuple[float, float]:
447
+ """Current scroll position as `(y, x)` fractions in `[0.0, 1.0]`."""
448
+ return float(self.canvas.yview()[0]), float(self.canvas.xview()[0])
449
+
450
+ def _update_scrollbar_visibility(self):
451
+ """Update scrollbar visibility based on current mode."""
452
+ if self._scrollbar_visibility == 'always':
453
+ self._show_scrollbars()
454
+ elif self._scrollbar_visibility == 'never':
455
+ self._hide_scrollbars()
456
+ elif self._scrollbar_visibility == 'hover':
457
+ # Show only while hovering and overflowing
458
+ x_fit, y_fit = self._content_fits()
459
+ if self._hovering and self._direction in ('vertical', 'both') and not y_fit:
460
+ self.vertical_scrollbar.grid()
461
+ else:
462
+ self.vertical_scrollbar.grid_remove()
463
+
464
+ if self._hovering and self._direction in ('horizontal', 'both') and not x_fit:
465
+ self.horizontal_scrollbar.grid()
466
+ else:
467
+ self.horizontal_scrollbar.grid_remove()
468
+ elif self._scrollbar_visibility == 'scroll':
469
+ # Hide if no overflow; otherwise leave current visibility to scroll events
470
+ x_fit, y_fit = self._content_fits()
471
+ if y_fit:
472
+ self.vertical_scrollbar.grid_remove()
473
+ if x_fit:
474
+ self.horizontal_scrollbar.grid_remove()
475
+
476
+ def _on_frame_configure(self, event):
477
+ """Update scroll region and refresh bindings on configure."""
478
+ self.canvas.configure(scrollregion=self.canvas.bbox('all'))
479
+ self._update_scrollbar_visibility()
480
+
481
+ # Refresh bindings for any newly added widgets
482
+ if self._scrolling_enabled and self._child_widget:
483
+ self._add_scroll_binding(self._child_widget)
484
+
485
+ def _on_mousewheel(self, event):
486
+ """Handle vertical mouse wheel scrolling."""
487
+ # Check if vertical scrolling is actually possible
488
+ if self._direction in ('vertical', 'both'):
489
+ try:
490
+ first, last = self.canvas.yview()
491
+ # If first=0.0 and last=1.0, all content is visible, no need to scroll
492
+ if first <= 0.0 and last >= 1.0:
493
+ return # Content fits, don't scroll
494
+ except:
495
+ pass # If we can't check, allow scrolling
496
+
497
+ # Show scrollbar temporarily in scroll mode
498
+ if self._scrollbar_visibility == 'scroll':
499
+ self._show_scrollbars()
500
+ if self._hide_timer:
501
+ self.after_cancel(self._hide_timer)
502
+ self._hide_timer = self.after(self._autohide_delay, self._hide_scrollbars)
503
+
504
+ # Calculate delta based on platform
505
+ delta = 0
506
+ if self.winsys.lower() == "win32":
507
+ delta = -int(event.delta / 120)
508
+ elif self.winsys.lower() == "aqua":
509
+ delta = -event.delta
510
+ elif event.num == 4:
511
+ delta = -10
512
+ elif event.num == 5:
513
+ delta = 10
514
+
515
+ # Scroll vertically
516
+ if self._direction in ('vertical', 'both') and delta != 0:
517
+ self.canvas.yview_scroll(delta, 'units')
518
+
519
+ # Don't return 'break' - allow event to propagate to other handlers if needed
520
+ # But we can return None to continue normal processing
521
+
522
+ def _on_shift_mousewheel(self, event):
523
+ """Handle horizontal mouse wheel scrolling (Shift+MouseWheel)."""
524
+ # Check if horizontal scrolling is actually possible
525
+ if self._direction in ('horizontal', 'both'):
526
+ try:
527
+ first, last = self.canvas.xview()
528
+ # If first=0.0 and last=1.0, all content is visible, no need to scroll
529
+ if first <= 0.0 and last >= 1.0:
530
+ return # Content fits, don't scroll
531
+ except:
532
+ pass # If we can't check, allow scrolling
533
+
534
+ # Show scrollbar temporarily in scroll mode
535
+ if self._scrollbar_visibility == 'scroll':
536
+ self._show_scrollbars()
537
+ if self._hide_timer:
538
+ self.after_cancel(self._hide_timer)
539
+ self._hide_timer = self.after(self._autohide_delay, self._hide_scrollbars)
540
+
541
+ # Calculate delta based on platform
542
+ delta = 0
543
+ if self.winsys.lower() == "win32":
544
+ delta = -int(event.delta / 120)
545
+ elif self.winsys.lower() == "aqua":
546
+ delta = -event.delta
547
+ elif event.num == 4:
548
+ delta = -10
549
+ elif event.num == 5:
550
+ delta = 10
551
+
552
+ # Scroll horizontally
553
+ if self._direction in ('horizontal', 'both') and delta != 0:
554
+ self.canvas.xview_scroll(delta, 'units')
555
+
556
+ # Don't return 'break' - allow event to propagate if needed
557
+
558
+ def _add_scroll_binding(self, widget):
559
+ """Recursively add scroll bind tag to widget and all descendants."""
560
+ try:
561
+ # Get current bindtags
562
+ tags = list(widget.bindtags())
563
+
564
+ # Add our scroll tag if not already present
565
+ if self._scroll_tag not in tags:
566
+ # Insert after the widget name but before the class
567
+ # Typical order: (widget_name, class, toplevel, 'all')
568
+ # We want: (widget_name, scroll_tag, class, toplevel, 'all')
569
+ if len(tags) >= 2:
570
+ tags.insert(1, self._scroll_tag)
571
+ else:
572
+ tags.append(self._scroll_tag)
573
+ widget.bindtags(tuple(tags))
574
+ except:
575
+ # Some widgets may not support bindtags
576
+ pass
577
+
578
+ # Recurse into all children
579
+ for child in widget.winfo_children():
580
+ self._add_scroll_binding(child)
581
+
582
+ def _del_scroll_binding(self, widget):
583
+ """Recursively remove scroll bind tag from widget and all descendants."""
584
+ try:
585
+ tags = list(widget.bindtags())
586
+ if self._scroll_tag in tags:
587
+ tags.remove(self._scroll_tag)
588
+ widget.bindtags(tuple(tags))
589
+ except:
590
+ pass
591
+
592
+ # Recurse into all children
593
+ for child in widget.winfo_children():
594
+ self._del_scroll_binding(child)
595
+
596
+ def enable_scrolling(self):
597
+ """Enable mouse wheel scrolling on canvas and all child widgets."""
598
+ if not self._scrolling_enabled:
599
+ # Add binding to canvas for exposed areas
600
+ self._add_scroll_binding(self.canvas)
601
+
602
+ # Add binding to child widget if it exists
603
+ if self._child_widget:
604
+ self._add_scroll_binding(self._child_widget)
605
+
606
+ self._scrolling_enabled = True
607
+
608
+ def disable_scrolling(self):
609
+ """Disable mouse wheel scrolling on canvas and all child widgets."""
610
+ if self._scrolling_enabled:
611
+ # Remove binding from canvas
612
+ self._del_scroll_binding(self.canvas)
613
+
614
+ # Remove binding from child widget if it exists
615
+ if self._child_widget:
616
+ self._del_scroll_binding(self._child_widget)
617
+
618
+ self._scrolling_enabled = False
619
+
620
+ def add(self, widget: Widget = None, *, anchor: str = 'nw', **kwargs: Any) -> Widget:
621
+ """Add a widget to the scrollable area, or create and return a Frame.
622
+
623
+ Args:
624
+ widget: The widget to add. If None, creates a Frame.
625
+ anchor: Anchor position for the widget in the canvas. Default is 'nw'.
626
+ **kwargs: When widget is None, these are passed to Frame (e.g., padding, accent).
627
+
628
+ Returns:
629
+ Widget: The content widget (passed or created).
630
+
631
+ Raises:
632
+ ValueError: If the ScrollView already contains a widget and a new one is provided.
633
+ """
634
+ # If content exists and no widget passed, return existing (idempotent)
635
+ if self._child_widget is not None:
636
+ if widget is not None:
637
+ raise ValueError("ScrollView already contains a widget. Use remove() first.")
638
+ return self._child_widget
639
+
640
+ # Create frame with kwargs if no widget provided
641
+ if widget is None:
642
+ widget = Frame(self.canvas, **kwargs)
643
+
644
+ self._child_widget = widget
645
+
646
+ # Create window in canvas
647
+ self._window_id = self.canvas.create_window(0, 0, anchor=anchor, window=widget)
648
+
649
+ # Keep scrollbars above the canvas/content
650
+ self.vertical_scrollbar.lift()
651
+ self.horizontal_scrollbar.lift()
652
+
653
+ # Bind configure event to update scroll region. (bind() auto-adds here,
654
+ # so this must run exactly once — a second identical bind double-fires
655
+ # _on_frame_configure on every resize, doubling the recursive subtree walk.)
656
+ widget.bind('<Configure>', self._on_frame_configure)
657
+ self._update_scrollbar_visibility()
658
+
659
+ # Enable scrolling based on mode
660
+ if self._scrollbar_visibility in ('always', 'never', 'scroll'):
661
+ # Always enable scrolling for these modes
662
+ self.enable_scrolling()
663
+ # For 'hover' mode, scrolling is enabled on enter
664
+
665
+ # Initial scroll region update
666
+ self.canvas.update_idletasks()
667
+ self.canvas.configure(scrollregion=self.canvas.bbox('all'))
668
+
669
+ return widget
670
+
671
+ def remove(self) -> Widget | None:
672
+ """Remove the current widget from the scrollable area.
673
+
674
+ Returns:
675
+ The removed widget, or None if no widget was present.
676
+ """
677
+ if self._child_widget is None:
678
+ return None
679
+
680
+ widget = self._child_widget
681
+
682
+ # Disable scrolling
683
+ self.disable_scrolling()
684
+
685
+ # Unbind events
686
+ widget.unbind('<Configure>')
687
+
688
+ # Delete canvas window
689
+ if self._window_id:
690
+ self.canvas.delete(self._window_id)
691
+ self._window_id = None
692
+
693
+ self._child_widget = None
694
+ self._scrolling_enabled = False
695
+
696
+ return widget
697
+
698
+ def get_child(self) -> Widget | None:
699
+ """Get the current child widget.
700
+
701
+ Returns:
702
+ The child widget, or None if no widget is present.
703
+ """
704
+ return self._child_widget
705
+
706
+ def refresh_bindings(self):
707
+ """Refresh mouse wheel bindings for all widgets.
708
+
709
+ Call this after dynamically adding many widgets at once to ensure
710
+ mouse wheel scrolling works on all new widgets.
711
+ """
712
+ if self._child_widget and self._scrolling_enabled:
713
+ # Re-enable to refresh bindings
714
+ self.disable_scrolling()
715
+ self.enable_scrolling()
716
+
717
+ def yview(self, *args):
718
+ """Query or command vertical view position."""
719
+ return self.canvas.yview(*args)
720
+
721
+ def xview(self, *args):
722
+ """Query or command horizontal view position."""
723
+ return self.canvas.xview(*args)
724
+
725
+ def yview_moveto(self, fraction: float):
726
+ """Scroll to a specific vertical position.
727
+
728
+ Args:
729
+ fraction: Position from 0.0 (top) to 1.0 (bottom).
730
+ """
731
+ self.canvas.yview_moveto(fraction)
732
+
733
+ def xview_moveto(self, fraction: float):
734
+ """Scroll to a specific horizontal position.
735
+
736
+ Args:
737
+ fraction: Position from 0.0 (left) to 1.0 (right).
738
+ """
739
+ self.canvas.xview_moveto(fraction)
740
+
741
+ def destroy(self):
742
+ """Clean up resources and destroy the widget."""
743
+ # Cancel any pending timer
744
+ if self._hide_timer:
745
+ self.after_cancel(self._hide_timer)
746
+ self._hide_timer = None
747
+
748
+ # Remove scroll bindings from child widget
749
+ if self._child_widget:
750
+ self.disable_scrolling()
751
+
752
+ # Unbind class bindings for the scroll tag
753
+ if self.winsys.lower() == "x11":
754
+ self.unbind_class(self._scroll_tag, "<Button-4>")
755
+ self.unbind_class(self._scroll_tag, "<Button-5>")
756
+ self.unbind_class(self._scroll_tag, "<Shift-Button-4>")
757
+ self.unbind_class(self._scroll_tag, "<Shift-Button-5>")
758
+ else:
759
+ self.unbind_class(self._scroll_tag, "<MouseWheel>")
760
+ self.unbind_class(self._scroll_tag, "<Shift-MouseWheel>")
761
+
762
+ # Call parent destroy
763
+ super().destroy()
764
+