bootstack 0.1.0a1__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 (419) hide show
  1. bootstack/__init__.py +249 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/api/__init__.py +127 -0
  4. bootstack/api/app.py +30 -0
  5. bootstack/api/constants.py +3 -0
  6. bootstack/api/data.py +23 -0
  7. bootstack/api/dialogs.py +44 -0
  8. bootstack/api/i18n.py +17 -0
  9. bootstack/api/localization.py +16 -0
  10. bootstack/api/menu.py +7 -0
  11. bootstack/api/style.py +23 -0
  12. bootstack/api/utils.py +24 -0
  13. bootstack/api/widgets.py +137 -0
  14. bootstack/assets/__init__.py +24 -0
  15. bootstack/assets/bootstack-transparent.png +0 -0
  16. bootstack/assets/bootstack.ico +0 -0
  17. bootstack/assets/bootstack.png +0 -0
  18. bootstack/assets/elements/__init__.py +0 -0
  19. bootstack/assets/elements/badge-pill.png +0 -0
  20. bootstack/assets/elements/badge-square.png +0 -0
  21. bootstack/assets/elements/border.png +0 -0
  22. bootstack/assets/elements/button-compact.png +0 -0
  23. bootstack/assets/elements/button-default.png +0 -0
  24. bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
  25. bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
  26. bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
  27. bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
  28. bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
  29. bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
  30. bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
  31. bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
  32. bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
  33. bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
  34. bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
  35. bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
  36. bootstack/assets/elements/checkbox-checked.png +0 -0
  37. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  38. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  39. bootstack/assets/elements/field.png +0 -0
  40. bootstack/assets/elements/input-after-compact.png +0 -0
  41. bootstack/assets/elements/input-after-default.png +0 -0
  42. bootstack/assets/elements/input-before-compact.png +0 -0
  43. bootstack/assets/elements/input-before-default.png +0 -0
  44. bootstack/assets/elements/input-compact.png +0 -0
  45. bootstack/assets/elements/input-default.png +0 -0
  46. bootstack/assets/elements/list-item-separated.png +0 -0
  47. bootstack/assets/elements/list-item.png +0 -0
  48. bootstack/assets/elements/manifest.toml +480 -0
  49. bootstack/assets/elements/menu-item.png +0 -0
  50. bootstack/assets/elements/nav-button-compact.png +0 -0
  51. bootstack/assets/elements/nav-button-default.png +0 -0
  52. bootstack/assets/elements/nav-icon-button-compact.png +0 -0
  53. bootstack/assets/elements/nav-icon-button-default.png +0 -0
  54. bootstack/assets/elements/notebook-client-border.png +0 -0
  55. bootstack/assets/elements/notebook-tab-active.png +0 -0
  56. bootstack/assets/elements/notebook-tab-bar.png +0 -0
  57. bootstack/assets/elements/notebook-tab-normal.png +0 -0
  58. bootstack/assets/elements/notebook-tab-pill.png +0 -0
  59. bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
  60. bootstack/assets/elements/progress-bar-solid.png +0 -0
  61. bootstack/assets/elements/progress-bar-thin.png +0 -0
  62. bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
  63. bootstack/assets/elements/radio-selected.png +0 -0
  64. bootstack/assets/elements/radio-unselected.png +0 -0
  65. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  66. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  67. bootstack/assets/elements/slider-handle-focus.png +0 -0
  68. bootstack/assets/elements/slider-handle.png +0 -0
  69. bootstack/assets/elements/slider-track-horizontal.png +0 -0
  70. bootstack/assets/elements/slider-track-vertical.png +0 -0
  71. bootstack/assets/elements/switch-off.png +0 -0
  72. bootstack/assets/elements/switch-on.png +0 -0
  73. bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
  74. bootstack/assets/elements/tabs-bar-vertical.png +0 -0
  75. bootstack/assets/elements/tabs-pill.png +0 -0
  76. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  77. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
  78. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
  79. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  80. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
  81. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  82. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
  83. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  84. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
  85. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  86. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
  87. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  88. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
  89. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  90. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
  91. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  92. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
  93. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  94. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
  95. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  96. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
  97. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  98. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
  99. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  100. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
  101. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  102. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
  103. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  104. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
  105. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  106. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
  107. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  108. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
  109. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  110. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
  111. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  112. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
  113. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  114. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
  115. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  116. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
  117. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  118. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
  119. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  120. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
  121. bootstack/assets/themes/__init__.py +0 -0
  122. bootstack/assets/themes/amber-dark.json +32 -0
  123. bootstack/assets/themes/amber-light.json +32 -0
  124. bootstack/assets/themes/aurora-dark.json +32 -0
  125. bootstack/assets/themes/aurora-light.json +32 -0
  126. bootstack/assets/themes/bootstrap-dark.json +32 -0
  127. bootstack/assets/themes/bootstrap-light.json +32 -0
  128. bootstack/assets/themes/classic-dark.json +32 -0
  129. bootstack/assets/themes/classic-light.json +32 -0
  130. bootstack/assets/themes/docs-dark.json +32 -0
  131. bootstack/assets/themes/docs-light.json +32 -0
  132. bootstack/assets/themes/forest-dark.json +32 -0
  133. bootstack/assets/themes/forest-light.json +32 -0
  134. bootstack/assets/themes/ocean-dark.json +32 -0
  135. bootstack/assets/themes/ocean-light.json +32 -0
  136. bootstack/assets/themes/rose-dark.json +32 -0
  137. bootstack/assets/themes/rose-light.json +32 -0
  138. bootstack/assets/widgets/__init__.py +0 -0
  139. bootstack/assets/widgets/badge-default.png +0 -0
  140. bootstack/assets/widgets/badge-pill.png +0 -0
  141. bootstack/assets/widgets/border.png +0 -0
  142. bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
  143. bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
  144. bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
  145. bootstack/assets/widgets/button-group-vertical-after.png +0 -0
  146. bootstack/assets/widgets/button-group-vertical-before.png +0 -0
  147. bootstack/assets/widgets/button-group-vertical-center.png +0 -0
  148. bootstack/assets/widgets/button.png +0 -0
  149. bootstack/assets/widgets/checkbox-checked.png +0 -0
  150. bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
  151. bootstack/assets/widgets/checkbox-unchecked.png +0 -0
  152. bootstack/assets/widgets/field.png +0 -0
  153. bootstack/assets/widgets/icon-button.png +0 -0
  154. bootstack/assets/widgets/input-inner.png +0 -0
  155. bootstack/assets/widgets/input-prefix.png +0 -0
  156. bootstack/assets/widgets/input-suffix.png +0 -0
  157. bootstack/assets/widgets/input.png +0 -0
  158. bootstack/assets/widgets/list-item-focus.png +0 -0
  159. bootstack/assets/widgets/list-item-separated.png +0 -0
  160. bootstack/assets/widgets/menu-item-separated.png +0 -0
  161. bootstack/assets/widgets/notebook-client-border.png +0 -0
  162. bootstack/assets/widgets/notebook-pill-active.png +0 -0
  163. bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
  164. bootstack/assets/widgets/notebook-tab-active.png +0 -0
  165. bootstack/assets/widgets/notebook-tab-border.png +0 -0
  166. bootstack/assets/widgets/notebook-tab-normal.png +0 -0
  167. bootstack/assets/widgets/notebook-underline.png +0 -0
  168. bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
  169. bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
  170. bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
  171. bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
  172. bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
  173. bootstack/assets/widgets/progress-trough-vertical.png +0 -0
  174. bootstack/assets/widgets/radio-selected.png +0 -0
  175. bootstack/assets/widgets/radio-unselected.png +0 -0
  176. bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
  177. bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
  178. bootstack/assets/widgets/separator-horizontal.png +0 -0
  179. bootstack/assets/widgets/separator-vertical.png +0 -0
  180. bootstack/assets/widgets/slider-handle-focus.png +0 -0
  181. bootstack/assets/widgets/slider-handle.png +0 -0
  182. bootstack/assets/widgets/slider-track-horizontal.png +0 -0
  183. bootstack/assets/widgets/slider-track-vertical.png +0 -0
  184. bootstack/assets/widgets/switch-off.png +0 -0
  185. bootstack/assets/widgets/switch-on.png +0 -0
  186. bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
  187. bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
  188. bootstack/assets/widgets/tabs-pill.png +0 -0
  189. bootstack/cli/__init__.py +124 -0
  190. bootstack/cli/__main__.py +6 -0
  191. bootstack/cli/add.py +439 -0
  192. bootstack/cli/build.py +115 -0
  193. bootstack/cli/config.py +287 -0
  194. bootstack/cli/demo.py +1267 -0
  195. bootstack/cli/doctor.py +195 -0
  196. bootstack/cli/list_cmd.py +71 -0
  197. bootstack/cli/promote.py +120 -0
  198. bootstack/cli/pyinstaller.py +246 -0
  199. bootstack/cli/run.py +99 -0
  200. bootstack/cli/start.py +105 -0
  201. bootstack/cli/templates/__init__.py +861 -0
  202. bootstack/constants.py +325 -0
  203. bootstack/core/__init__.py +34 -0
  204. bootstack/core/capabilities/__init__.py +45 -0
  205. bootstack/core/capabilities/after.py +103 -0
  206. bootstack/core/capabilities/bind.py +154 -0
  207. bootstack/core/capabilities/bindtags.py +112 -0
  208. bootstack/core/capabilities/busy.py +61 -0
  209. bootstack/core/capabilities/clipboard.py +88 -0
  210. bootstack/core/capabilities/focus.py +118 -0
  211. bootstack/core/capabilities/grab.py +65 -0
  212. bootstack/core/capabilities/grid.py +188 -0
  213. bootstack/core/capabilities/localization.py +231 -0
  214. bootstack/core/capabilities/pack.py +119 -0
  215. bootstack/core/capabilities/place.py +92 -0
  216. bootstack/core/capabilities/selection.py +136 -0
  217. bootstack/core/capabilities/signals.py +242 -0
  218. bootstack/core/capabilities/winfo.py +315 -0
  219. bootstack/core/colorutils.py +234 -0
  220. bootstack/core/exceptions.py +95 -0
  221. bootstack/core/images.py +283 -0
  222. bootstack/core/localization/README.md +90 -0
  223. bootstack/core/localization/__init__.py +13 -0
  224. bootstack/core/localization/intl_format.py +580 -0
  225. bootstack/core/localization/msgcat.py +425 -0
  226. bootstack/core/localization/specs.py +143 -0
  227. bootstack/core/mixins/__init__.py +1 -0
  228. bootstack/core/mixins/ttk_state.py +35 -0
  229. bootstack/core/mixins/widget.py +132 -0
  230. bootstack/core/publisher.py +147 -0
  231. bootstack/core/signals/README.md +112 -0
  232. bootstack/core/signals/__init__.py +8 -0
  233. bootstack/core/signals/integration.py +100 -0
  234. bootstack/core/signals/signal.py +317 -0
  235. bootstack/core/signals/types.py +4 -0
  236. bootstack/core/validation/__init__.py +5 -0
  237. bootstack/core/validation/types.py +13 -0
  238. bootstack/core/validation/validation_result.py +17 -0
  239. bootstack/core/validation/validation_rules.py +112 -0
  240. bootstack/core/variables.py +62 -0
  241. bootstack/datasource/README.md +607 -0
  242. bootstack/datasource/__init__.py +51 -0
  243. bootstack/datasource/base.py +474 -0
  244. bootstack/datasource/file_source.py +541 -0
  245. bootstack/datasource/memory_source.py +482 -0
  246. bootstack/datasource/sqlite_source.py +453 -0
  247. bootstack/datasource/types.py +259 -0
  248. bootstack/dialogs/__init__.py +56 -0
  249. bootstack/dialogs/colorchooser.py +674 -0
  250. bootstack/dialogs/colordropper.py +257 -0
  251. bootstack/dialogs/datedialog.py +404 -0
  252. bootstack/dialogs/dialog.py +514 -0
  253. bootstack/dialogs/filterdialog.py +358 -0
  254. bootstack/dialogs/fontdialog.py +339 -0
  255. bootstack/dialogs/formdialog.py +541 -0
  256. bootstack/dialogs/message.py +489 -0
  257. bootstack/dialogs/query.py +561 -0
  258. bootstack/py.typed +1 -0
  259. bootstack/runtime/__init__.py +3 -0
  260. bootstack/runtime/app.py +879 -0
  261. bootstack/runtime/base_window.py +786 -0
  262. bootstack/runtime/events.py +399 -0
  263. bootstack/runtime/menu.py +510 -0
  264. bootstack/runtime/shortcuts.py +423 -0
  265. bootstack/runtime/tk_patch.py +31 -0
  266. bootstack/runtime/toplevel.py +131 -0
  267. bootstack/runtime/utility.py +371 -0
  268. bootstack/runtime/visual_focus.py +228 -0
  269. bootstack/runtime/window_utilities.py +1043 -0
  270. bootstack/style/__init__.py +5498 -0
  271. bootstack/style/bootstyle.py +507 -0
  272. bootstack/style/bootstyle_builder_base.py +752 -0
  273. bootstack/style/bootstyle_builder_mixed.py +93 -0
  274. bootstack/style/bootstyle_builder_tk.py +109 -0
  275. bootstack/style/bootstyle_builder_ttk.py +354 -0
  276. bootstack/style/builders/__init__.py +51 -0
  277. bootstack/style/builders/badge.py +44 -0
  278. bootstack/style/builders/button.py +453 -0
  279. bootstack/style/builders/buttongroup.py +344 -0
  280. bootstack/style/builders/calendar.py +271 -0
  281. bootstack/style/builders/checkbutton.py +95 -0
  282. bootstack/style/builders/combobox.py +112 -0
  283. bootstack/style/builders/contextmenu.py +268 -0
  284. bootstack/style/builders/entry.py +83 -0
  285. bootstack/style/builders/expander.py +171 -0
  286. bootstack/style/builders/field.py +312 -0
  287. bootstack/style/builders/frame.py +27 -0
  288. bootstack/style/builders/label.py +28 -0
  289. bootstack/style/builders/labelframe.py +41 -0
  290. bootstack/style/builders/listview.py +267 -0
  291. bootstack/style/builders/menubar.py +74 -0
  292. bootstack/style/builders/menubutton.py +408 -0
  293. bootstack/style/builders/notebook.py +316 -0
  294. bootstack/style/builders/panedwindow.py +25 -0
  295. bootstack/style/builders/progressbar.py +71 -0
  296. bootstack/style/builders/radiobutton.py +68 -0
  297. bootstack/style/builders/scale.py +66 -0
  298. bootstack/style/builders/scrollbar.py +360 -0
  299. bootstack/style/builders/separator.py +45 -0
  300. bootstack/style/builders/sidenav.py +313 -0
  301. bootstack/style/builders/sizegrip.py +15 -0
  302. bootstack/style/builders/spinbox.py +119 -0
  303. bootstack/style/builders/switch.py +67 -0
  304. bootstack/style/builders/tabitem.py +205 -0
  305. bootstack/style/builders/toolbutton.py +260 -0
  306. bootstack/style/builders/tooltip.py +26 -0
  307. bootstack/style/builders/treeview.py +269 -0
  308. bootstack/style/builders/utils.py +404 -0
  309. bootstack/style/builders_tk/__init__.py +16 -0
  310. bootstack/style/builders_tk/defaults.py +229 -0
  311. bootstack/style/element.py +173 -0
  312. bootstack/style/style.py +499 -0
  313. bootstack/style/theme_provider.py +449 -0
  314. bootstack/style/tk_patch.py +5 -0
  315. bootstack/style/token_maps.py +42 -0
  316. bootstack/style/types.py +32 -0
  317. bootstack/style/typography.py +527 -0
  318. bootstack/style/utility.py +696 -0
  319. bootstack/themes/__init__.py +12 -0
  320. bootstack/themes/standard.py +415 -0
  321. bootstack/themes/user.py +45 -0
  322. bootstack/widgets/__init__.py +53 -0
  323. bootstack/widgets/composites/__init__.py +38 -0
  324. bootstack/widgets/composites/accordion.py +385 -0
  325. bootstack/widgets/composites/appshell.py +445 -0
  326. bootstack/widgets/composites/buttongroup.py +391 -0
  327. bootstack/widgets/composites/calendar.py +914 -0
  328. bootstack/widgets/composites/compositeframe.py +282 -0
  329. bootstack/widgets/composites/contextmenu.py +1754 -0
  330. bootstack/widgets/composites/dateentry.py +261 -0
  331. bootstack/widgets/composites/dropdownbutton.py +190 -0
  332. bootstack/widgets/composites/expander.py +508 -0
  333. bootstack/widgets/composites/field.py +448 -0
  334. bootstack/widgets/composites/floodgauge.py +434 -0
  335. bootstack/widgets/composites/form.py +983 -0
  336. bootstack/widgets/composites/labeledscale.py +209 -0
  337. bootstack/widgets/composites/list/__init__.py +15 -0
  338. bootstack/widgets/composites/list/listitem.py +733 -0
  339. bootstack/widgets/composites/list/listview.py +1507 -0
  340. bootstack/widgets/composites/menubar.py +303 -0
  341. bootstack/widgets/composites/meter.py +882 -0
  342. bootstack/widgets/composites/numericentry.py +183 -0
  343. bootstack/widgets/composites/pagestack.py +330 -0
  344. bootstack/widgets/composites/passwordentry.py +149 -0
  345. bootstack/widgets/composites/pathentry.py +223 -0
  346. bootstack/widgets/composites/radiogroup.py +466 -0
  347. bootstack/widgets/composites/scrolledtext.py +388 -0
  348. bootstack/widgets/composites/scrolledtext.pyi +186 -0
  349. bootstack/widgets/composites/scrollview.py +675 -0
  350. bootstack/widgets/composites/selectbox.py +544 -0
  351. bootstack/widgets/composites/sidenav/__init__.py +24 -0
  352. bootstack/widgets/composites/sidenav/group.py +485 -0
  353. bootstack/widgets/composites/sidenav/header.py +83 -0
  354. bootstack/widgets/composites/sidenav/item.py +413 -0
  355. bootstack/widgets/composites/sidenav/separator.py +51 -0
  356. bootstack/widgets/composites/sidenav/view.py +919 -0
  357. bootstack/widgets/composites/spinnerentry.py +232 -0
  358. bootstack/widgets/composites/tableview/__init__.py +5 -0
  359. bootstack/widgets/composites/tableview/tableview.py +2254 -0
  360. bootstack/widgets/composites/tableview/types.py +169 -0
  361. bootstack/widgets/composites/tabs/__init__.py +6 -0
  362. bootstack/widgets/composites/tabs/tabitem.py +372 -0
  363. bootstack/widgets/composites/tabs/tabs.py +478 -0
  364. bootstack/widgets/composites/tabs/tabview.py +352 -0
  365. bootstack/widgets/composites/textentry.py +90 -0
  366. bootstack/widgets/composites/timeentry.py +189 -0
  367. bootstack/widgets/composites/toast.py +364 -0
  368. bootstack/widgets/composites/togglegroup.py +382 -0
  369. bootstack/widgets/composites/toolbar.py +393 -0
  370. bootstack/widgets/composites/tooltip.py +404 -0
  371. bootstack/widgets/internal/__init__.py +0 -0
  372. bootstack/widgets/internal/wrapper_base.py +304 -0
  373. bootstack/widgets/mixins/__init__.py +25 -0
  374. bootstack/widgets/mixins/configure_mixin.py +186 -0
  375. bootstack/widgets/mixins/entry_mixin.py +70 -0
  376. bootstack/widgets/mixins/font_mixin.py +346 -0
  377. bootstack/widgets/mixins/icon_mixin.py +38 -0
  378. bootstack/widgets/mixins/localization_mixin.py +255 -0
  379. bootstack/widgets/mixins/signal_mixin.py +272 -0
  380. bootstack/widgets/mixins/validation_mixin.py +204 -0
  381. bootstack/widgets/parts/__init__.py +11 -0
  382. bootstack/widgets/parts/numberentry_part.py +345 -0
  383. bootstack/widgets/parts/spinnerentry_part.py +394 -0
  384. bootstack/widgets/parts/textentry_part.py +344 -0
  385. bootstack/widgets/primitives/__init__.py +55 -0
  386. bootstack/widgets/primitives/badge.py +44 -0
  387. bootstack/widgets/primitives/button.py +89 -0
  388. bootstack/widgets/primitives/card.py +66 -0
  389. bootstack/widgets/primitives/checkbutton.py +124 -0
  390. bootstack/widgets/primitives/checktoggle.py +53 -0
  391. bootstack/widgets/primitives/combobox.py +165 -0
  392. bootstack/widgets/primitives/entry.py +98 -0
  393. bootstack/widgets/primitives/frame.py +206 -0
  394. bootstack/widgets/primitives/gridframe.py +479 -0
  395. bootstack/widgets/primitives/label.py +95 -0
  396. bootstack/widgets/primitives/labelframe.py +63 -0
  397. bootstack/widgets/primitives/menubutton.py +118 -0
  398. bootstack/widgets/primitives/notebook.py +551 -0
  399. bootstack/widgets/primitives/optionmenu.py +248 -0
  400. bootstack/widgets/primitives/packframe.py +228 -0
  401. bootstack/widgets/primitives/panedwindow.py +58 -0
  402. bootstack/widgets/primitives/progressbar.py +95 -0
  403. bootstack/widgets/primitives/radiobutton.py +115 -0
  404. bootstack/widgets/primitives/radiotoggle.py +50 -0
  405. bootstack/widgets/primitives/scale.py +85 -0
  406. bootstack/widgets/primitives/scrollbar.py +56 -0
  407. bootstack/widgets/primitives/separator.py +56 -0
  408. bootstack/widgets/primitives/sizegrip.py +47 -0
  409. bootstack/widgets/primitives/spinbox.py +91 -0
  410. bootstack/widgets/primitives/switch.py +41 -0
  411. bootstack/widgets/primitives/treeview.py +77 -0
  412. bootstack/widgets/types.py +20 -0
  413. bootstack-0.1.0a1.dist-info/METADATA +196 -0
  414. bootstack-0.1.0a1.dist-info/RECORD +419 -0
  415. bootstack-0.1.0a1.dist-info/WHEEL +5 -0
  416. bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
  417. bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
  418. bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
  419. bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,675 @@
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, Optional
5
+
6
+ from bootstack.widgets.primitives.frame import Frame
7
+ from bootstack.widgets.mixins.configure_mixin import configure_delegate
8
+ from bootstack.widgets.primitives.scrollbar import Scrollbar
9
+ from bootstack.widgets.types import Master
10
+
11
+
12
+ class ScrollView(Frame):
13
+ """A canvas-based scrollable container with configurable scrollbar behavior.
14
+
15
+ The ScrollView widget provides a scrollable area for child widgets with
16
+ full mouse wheel support on all descendants. Scrollbars can be configured
17
+ to appear always, never, on hover, or when scrolling, and are only visible
18
+ when the content exceeds the available space.
19
+
20
+ Attributes:
21
+ canvas (Canvas): The underlying tkinter Canvas widget.
22
+ vertical_scrollbar (Scrollbar): The vertical scrollbar widget.
23
+ horizontal_scrollbar (Scrollbar): The horizontal scrollbar widget.
24
+ """
25
+
26
+ def __init__(
27
+ self,
28
+ master: Master = None,
29
+ scroll_direction: Literal['horizontal', 'vertical', 'both'] = 'both',
30
+ scrollbar_visibility: Literal['always', 'never', 'hover', 'scroll'] = 'always',
31
+ autohide_delay: int = 1000, # milliseconds for scroll mode
32
+ scrollbar_variant: str = 'default',
33
+ **kwargs: Any
34
+ ):
35
+ """Initialize a ScrollView widget.
36
+
37
+ Args:
38
+ master: The parent widget.
39
+ scroll_direction: Scroll direction - 'horizontal' for horizontal only,
40
+ 'vertical' for vertical only, or 'both' for bidirectional
41
+ scrolling. Horizontal scrolling uses Shift+MouseWheel.
42
+ scrollbar_visibility: Scrollbar visibility mode:
43
+ - 'always': Scrollbars always visible
44
+ - 'never': Scrollbars hidden (scrolling still works)
45
+ - 'hover': Scrollbars appear when mouse enters the widget
46
+ - 'scroll': Scrollbars appear when scrolling, auto-hide after delay
47
+ autohide_delay: Time in milliseconds before auto-hiding scrollbars
48
+ in 'scroll' mode. Default is 1000ms (1 second).
49
+ scrollbar_variant: The variant to apply to scrollbars (e.g., 'default',
50
+ 'round'). If None, uses the default scrollbar variant.
51
+ **kwargs: Additional keyword arguments passed to the Frame parent class.
52
+
53
+ Note:
54
+ Mouse wheel scrolling is automatically enabled on all child widgets,
55
+ including those added dynamically. For manual refresh of bindings
56
+ after adding many widgets at once, call refresh_bindings().
57
+ """
58
+ super().__init__(master, **kwargs)
59
+
60
+ # configuration
61
+ self._direction = scroll_direction
62
+ self._scrollbar_visibility = scrollbar_visibility
63
+ self._autohide_delay = autohide_delay
64
+ self._scrollbar_variant = scrollbar_variant
65
+
66
+ self._child_widget = None
67
+ self._window_id = None
68
+ self._hide_timer = None
69
+ self._scrolling_enabled = False
70
+ self._hovering = False
71
+
72
+ # Create unique bind tag for this scrollview
73
+ self._scroll_tag = f'ScrollView_{id(self)}'
74
+
75
+ # Detect windowing system
76
+ self.winsys = self.tk.call("tk", "windowingsystem")
77
+
78
+ # Bind scroll events to our custom tag
79
+ self._setup_scroll_tag_bindings()
80
+
81
+ # Create canvas
82
+ self.canvas = Canvas(
83
+ self,
84
+ highlightthickness=0,
85
+ borderwidth=0
86
+ )
87
+ self.canvas.bind("<Configure>", self._on_canvas_configure)
88
+
89
+ # Create scrollbars
90
+ self.vertical_scrollbar = Scrollbar(
91
+ master=self,
92
+ orient='vertical',
93
+ command=self.canvas.yview,
94
+ variant=self._scrollbar_variant
95
+ )
96
+ self.horizontal_scrollbar = Scrollbar(
97
+ master=self,
98
+ orient='horizontal',
99
+ command=self.canvas.xview,
100
+ variant=self._scrollbar_variant
101
+ )
102
+
103
+ # Configure canvas scrolling
104
+ scroll_config = {}
105
+ if scroll_direction in ('vertical', 'both'):
106
+ scroll_config['yscrollcommand'] = self._on_canvas_scroll_y
107
+ if scroll_direction in ('horizontal', 'both'):
108
+ scroll_config['xscrollcommand'] = self._on_canvas_scroll_x
109
+
110
+ self.canvas.configure(**scroll_config)
111
+
112
+ # Layout
113
+ self._layout_widgets()
114
+
115
+ # Bind events for autohide/hover
116
+ self._bind_container_events()
117
+
118
+ # Initial scrollbar visibility
119
+ self._update_scrollbar_visibility()
120
+
121
+ @configure_delegate('scroll_direction')
122
+ def _delegate_scroll_direction(self, value=None):
123
+ if value is None:
124
+ return self._direction
125
+ else:
126
+ old_direction = self._direction
127
+ self._direction = value
128
+
129
+ # Update canvas scroll configuration
130
+ scroll_config = {}
131
+ if value in ('vertical', 'both'):
132
+ scroll_config['yscrollcommand'] = self._on_canvas_scroll_y
133
+ else:
134
+ scroll_config['yscrollcommand'] = None
135
+
136
+ if value in ('horizontal', 'both'):
137
+ scroll_config['xscrollcommand'] = self._on_canvas_scroll_x
138
+ else:
139
+ scroll_config['xscrollcommand'] = None
140
+
141
+ self.canvas.configure(**scroll_config)
142
+
143
+ # Update scrollbar visibility and layout
144
+ self._update_scrollbar_visibility()
145
+ return None
146
+
147
+ @configure_delegate('scrollbar_visibility')
148
+ def _delegate_scrollbar_visibility(self, value=None):
149
+ if value is None:
150
+ return self._scrollbar_visibility
151
+ else:
152
+ old_value = self._scrollbar_visibility
153
+ self._scrollbar_visibility = value
154
+
155
+ # Unbind old events if changing from hover
156
+ if old_value == 'hover':
157
+ self.unbind('<Enter>')
158
+ self.unbind('<Leave>')
159
+ self.canvas.unbind('<Enter>')
160
+ self.canvas.unbind('<Leave>')
161
+ self.vertical_scrollbar.unbind('<Enter>')
162
+ self.vertical_scrollbar.unbind('<Leave>')
163
+ self.horizontal_scrollbar.unbind('<Enter>')
164
+ self.horizontal_scrollbar.unbind('<Leave>')
165
+
166
+ # Sync gutter reservation with the new mode
167
+ if value in ('hover', 'scroll'):
168
+ self._set_scrollbar_gutter(reserve=True)
169
+ else:
170
+ self._set_scrollbar_gutter(reserve=False)
171
+
172
+ # Bind new events and update scrollbar visibility
173
+ self._bind_container_events()
174
+ self._update_scrollbar_visibility()
175
+
176
+ # Update scrolling enabled state
177
+ if value in ('always', 'never', 'scroll'):
178
+ if self._child_widget:
179
+ self.enable_scrolling()
180
+ elif value == 'hover':
181
+ # Scrolling will be enabled on hover
182
+ self.disable_scrolling()
183
+ return None
184
+
185
+ @configure_delegate('autohide_delay')
186
+ def _delegate_autohide_delay(self, value=None):
187
+ if value is None:
188
+ return self._autohide_delay
189
+ else:
190
+ self._autohide_delay = value
191
+ return None
192
+
193
+ @configure_delegate('scrollbar_variant')
194
+ def _delegate_scrollbar_variant(self, value=None):
195
+ if value is None:
196
+ return self._scrollbar_variant
197
+ else:
198
+ self._scrollbar_variant = value
199
+ # Apply the new variant to both scrollbars
200
+ if value:
201
+ self.vertical_scrollbar.configure(variant=value)
202
+ self.horizontal_scrollbar.configure(variant=value)
203
+ return None
204
+
205
+ def _setup_scroll_tag_bindings(self):
206
+ """Setup bindings on our custom bind tag."""
207
+ if self.winsys.lower() == "x11":
208
+ self.bind_class(self._scroll_tag, "<Button-4>", self._on_mousewheel)
209
+ self.bind_class(self._scroll_tag, "<Button-5>", self._on_mousewheel)
210
+ self.bind_class(self._scroll_tag, "<Shift-Button-4>", self._on_shift_mousewheel)
211
+ self.bind_class(self._scroll_tag, "<Shift-Button-5>", self._on_shift_mousewheel)
212
+ else:
213
+ self.bind_class(self._scroll_tag, "<MouseWheel>", self._on_mousewheel)
214
+ self.bind_class(self._scroll_tag, "<Shift-MouseWheel>", self._on_shift_mousewheel)
215
+
216
+ def _layout_widgets(self):
217
+ """Layout the canvas and scrollbars.
218
+
219
+ For 'hover' and 'scroll' visibility modes the grid column and row that
220
+ hold the scrollbars are given a fixed minimum size equal to the
221
+ scrollbar's natural dimensions before the scrollbars are hidden. This
222
+ reserves the gutter permanently so the canvas never resizes when a
223
+ scrollbar is toggled — the same principle as the CSS
224
+ `scrollbar-gutter: stable` property.
225
+
226
+ An overlay approach (lift/lower over the canvas) was considered but
227
+ rejected because the scrollbar then covers content, which was reported
228
+ as confusing by users. Reserving the gutter avoids both the
229
+ layout-shift problem *and* the content-coverage problem.
230
+ """
231
+ self.canvas.grid(row=0, column=0, sticky='nsew')
232
+
233
+ if self._direction in ('vertical', 'both'):
234
+ self.vertical_scrollbar.grid(row=0, column=1, sticky='ns')
235
+
236
+ if self._direction in ('horizontal', 'both'):
237
+ self.horizontal_scrollbar.grid(row=1, column=0, sticky='ew')
238
+
239
+ # Configure grid weights
240
+ self.grid_rowconfigure(0, weight=1)
241
+ self.grid_columnconfigure(0, weight=1)
242
+ # Keep scrollbars above the canvas/content when visible
243
+ self.vertical_scrollbar.lift()
244
+ self.horizontal_scrollbar.lift()
245
+
246
+ # Initially hide scrollbars based on scrollbar_visibility setting
247
+ if self._scrollbar_visibility == 'never':
248
+ self.vertical_scrollbar.grid_remove()
249
+ self.horizontal_scrollbar.grid_remove()
250
+ elif self._scrollbar_visibility in ('hover', 'scroll'):
251
+ # Hide scrollbars now; defer gutter measurement to after_idle so
252
+ # the style system has finished sizing the widgets before we read
253
+ # their dimensions. Measuring too early (before the widget is
254
+ # placed in its parent) can return an inflated reqwidth and leave
255
+ # a visible gap beside the scrollbar when it appears.
256
+ self.vertical_scrollbar.grid_remove()
257
+ self.horizontal_scrollbar.grid_remove()
258
+ self.after_idle(lambda: self._set_scrollbar_gutter(reserve=True))
259
+
260
+ def _set_scrollbar_gutter(self, reserve: bool):
261
+ """Reserve or release the grid gutter used by each scrollbar.
262
+
263
+ When *reserve* is `True` the column (vertical scrollbar) and row
264
+ (horizontal scrollbar) are given a `minsize` equal to the scrollbar's
265
+ natural requested dimensions. The minsize persists even after the
266
+ scrollbar widget is removed from the grid via `grid_remove()`, so the
267
+ canvas occupies a constant area regardless of scrollbar visibility.
268
+
269
+ When *reserve* is `False` the minsize is cleared (set to 0), which
270
+ is appropriate for 'always' and 'never' modes where no gutter needs to
271
+ be held open.
272
+ """
273
+ if reserve:
274
+ if self._direction in ('vertical', 'both'):
275
+ width = self.vertical_scrollbar.winfo_reqwidth()
276
+ if width > 1:
277
+ self.grid_columnconfigure(1, minsize=width)
278
+ if self._direction in ('horizontal', 'both'):
279
+ height = self.horizontal_scrollbar.winfo_reqheight()
280
+ if height > 1:
281
+ self.grid_rowconfigure(1, minsize=height)
282
+ else:
283
+ self.grid_columnconfigure(1, minsize=0)
284
+ self.grid_rowconfigure(1, minsize=0)
285
+
286
+ def _bind_container_events(self):
287
+ """Bind events for the container (enter/leave for autohide)."""
288
+ if self._scrollbar_visibility == 'hover':
289
+ self.bind('<Enter>', self._on_container_enter)
290
+ self.bind('<Leave>', self._on_container_leave)
291
+ self.canvas.bind('<Enter>', self._on_container_enter)
292
+ self.canvas.bind('<Leave>', self._on_container_leave)
293
+ self.vertical_scrollbar.bind('<Enter>', self._on_container_enter)
294
+ self.vertical_scrollbar.bind('<Leave>', self._on_container_leave)
295
+ self.horizontal_scrollbar.bind('<Enter>', self._on_container_enter)
296
+ self.horizontal_scrollbar.bind('<Leave>', self._on_container_leave)
297
+
298
+ def _on_container_enter(self, event):
299
+ """Handle mouse entering the container."""
300
+ self._hovering = True
301
+ self.enable_scrolling()
302
+ if self._scrollbar_visibility == 'hover':
303
+ self._show_scrollbars()
304
+
305
+ def _on_container_leave(self, event):
306
+ """Handle mouse leaving the container."""
307
+ self._hovering = False
308
+ self.disable_scrolling()
309
+ if self._scrollbar_visibility == 'hover':
310
+ self._hide_scrollbars()
311
+
312
+ def _content_fits(self):
313
+ """Return booleans for whether content fits in the viewport (x_fit, y_fit)."""
314
+ if self._window_id:
315
+ bbox = self.canvas.bbox(self._window_id)
316
+ else:
317
+ bbox = self.canvas.bbox('all')
318
+ if not bbox:
319
+ return True, True
320
+ x0, y0, x1, y1 = bbox
321
+ content_w = x1 - x0
322
+ content_h = y1 - y0
323
+ viewport_w = max(1, self.canvas.winfo_width())
324
+ viewport_h = max(1, self.canvas.winfo_height())
325
+ if viewport_w <= 1 or viewport_h <= 1:
326
+ return True, True
327
+ return content_w <= viewport_w, content_h <= viewport_h
328
+
329
+ def _show_scrollbars(self):
330
+ """Show scrollbars only if content overflows the viewport."""
331
+ x_fit, y_fit = self._content_fits()
332
+ if self._direction in ('vertical', 'both') and not y_fit:
333
+ self.vertical_scrollbar.grid()
334
+ else:
335
+ self.vertical_scrollbar.grid_remove()
336
+ if self._direction in ('horizontal', 'both') and not x_fit:
337
+ self.horizontal_scrollbar.grid()
338
+ else:
339
+ self.horizontal_scrollbar.grid_remove()
340
+
341
+ def _hide_scrollbars(self):
342
+ """Hide scrollbars."""
343
+ self.vertical_scrollbar.grid_remove()
344
+ self.horizontal_scrollbar.grid_remove()
345
+
346
+ def _on_canvas_configure(self, event):
347
+ """Update visibility when the viewport size changes."""
348
+ self._update_scrollbar_visibility()
349
+
350
+ def _on_canvas_scroll_y(self, first, last):
351
+ """Update vertical scrollbar position."""
352
+ self.vertical_scrollbar.set(first, last)
353
+ self._update_scrollbar_visibility()
354
+
355
+ def _on_canvas_scroll_x(self, first, last):
356
+ """Update horizontal scrollbar position."""
357
+ self.horizontal_scrollbar.set(first, last)
358
+ self._update_scrollbar_visibility()
359
+
360
+ def _update_scrollbar_visibility(self):
361
+ """Update scrollbar visibility based on current mode."""
362
+ if self._scrollbar_visibility == 'always':
363
+ self._show_scrollbars()
364
+ elif self._scrollbar_visibility == 'never':
365
+ self._hide_scrollbars()
366
+ elif self._scrollbar_visibility == 'hover':
367
+ # Show only while hovering and overflowing
368
+ x_fit, y_fit = self._content_fits()
369
+ if self._hovering and self._direction in ('vertical', 'both') and not y_fit:
370
+ self.vertical_scrollbar.grid()
371
+ else:
372
+ self.vertical_scrollbar.grid_remove()
373
+
374
+ if self._hovering and self._direction in ('horizontal', 'both') and not x_fit:
375
+ self.horizontal_scrollbar.grid()
376
+ else:
377
+ self.horizontal_scrollbar.grid_remove()
378
+ elif self._scrollbar_visibility == 'scroll':
379
+ # Hide if no overflow; otherwise leave current visibility to scroll events
380
+ x_fit, y_fit = self._content_fits()
381
+ if y_fit:
382
+ self.vertical_scrollbar.grid_remove()
383
+ if x_fit:
384
+ self.horizontal_scrollbar.grid_remove()
385
+
386
+ def _on_frame_configure(self, event):
387
+ """Update scroll region and refresh bindings on configure."""
388
+ self.canvas.configure(scrollregion=self.canvas.bbox('all'))
389
+ self._update_scrollbar_visibility()
390
+
391
+ # Refresh bindings for any newly added widgets
392
+ if self._scrolling_enabled and self._child_widget:
393
+ self._add_scroll_binding(self._child_widget)
394
+
395
+ def _on_mousewheel(self, event):
396
+ """Handle vertical mouse wheel scrolling."""
397
+ # Check if vertical scrolling is actually possible
398
+ if self._direction in ('vertical', 'both'):
399
+ try:
400
+ first, last = self.canvas.yview()
401
+ # If first=0.0 and last=1.0, all content is visible, no need to scroll
402
+ if first <= 0.0 and last >= 1.0:
403
+ return # Content fits, don't scroll
404
+ except:
405
+ pass # If we can't check, allow scrolling
406
+
407
+ # Show scrollbar temporarily in scroll mode
408
+ if self._scrollbar_visibility == 'scroll':
409
+ self._show_scrollbars()
410
+ if self._hide_timer:
411
+ self.after_cancel(self._hide_timer)
412
+ self._hide_timer = self.after(self._autohide_delay, self._hide_scrollbars)
413
+
414
+ # Calculate delta based on platform
415
+ delta = 0
416
+ if self.winsys.lower() == "win32":
417
+ delta = -int(event.delta / 120)
418
+ elif self.winsys.lower() == "aqua":
419
+ delta = -event.delta
420
+ elif event.num == 4:
421
+ delta = -10
422
+ elif event.num == 5:
423
+ delta = 10
424
+
425
+ # Scroll vertically
426
+ if self._direction in ('vertical', 'both') and delta != 0:
427
+ self.canvas.yview_scroll(delta, 'units')
428
+
429
+ # Don't return 'break' - allow event to propagate to other handlers if needed
430
+ # But we can return None to continue normal processing
431
+
432
+ def _on_shift_mousewheel(self, event):
433
+ """Handle horizontal mouse wheel scrolling (Shift+MouseWheel)."""
434
+ # Check if horizontal scrolling is actually possible
435
+ if self._direction in ('horizontal', 'both'):
436
+ try:
437
+ first, last = self.canvas.xview()
438
+ # If first=0.0 and last=1.0, all content is visible, no need to scroll
439
+ if first <= 0.0 and last >= 1.0:
440
+ return # Content fits, don't scroll
441
+ except:
442
+ pass # If we can't check, allow scrolling
443
+
444
+ # Show scrollbar temporarily in scroll mode
445
+ if self._scrollbar_visibility == 'scroll':
446
+ self._show_scrollbars()
447
+ if self._hide_timer:
448
+ self.after_cancel(self._hide_timer)
449
+ self._hide_timer = self.after(self._autohide_delay, self._hide_scrollbars)
450
+
451
+ # Calculate delta based on platform
452
+ delta = 0
453
+ if self.winsys.lower() == "win32":
454
+ delta = -int(event.delta / 120)
455
+ elif self.winsys.lower() == "aqua":
456
+ delta = -event.delta
457
+ elif event.num == 4:
458
+ delta = -10
459
+ elif event.num == 5:
460
+ delta = 10
461
+
462
+ # Scroll horizontally
463
+ if self._direction in ('horizontal', 'both') and delta != 0:
464
+ self.canvas.xview_scroll(delta, 'units')
465
+
466
+ # Don't return 'break' - allow event to propagate if needed
467
+
468
+ def _add_scroll_binding(self, widget):
469
+ """Recursively add scroll bind tag to widget and all descendants."""
470
+ try:
471
+ # Get current bindtags
472
+ tags = list(widget.bindtags())
473
+
474
+ # Add our scroll tag if not already present
475
+ if self._scroll_tag not in tags:
476
+ # Insert after the widget name but before the class
477
+ # Typical order: (widget_name, class, toplevel, 'all')
478
+ # We want: (widget_name, scroll_tag, class, toplevel, 'all')
479
+ if len(tags) >= 2:
480
+ tags.insert(1, self._scroll_tag)
481
+ else:
482
+ tags.append(self._scroll_tag)
483
+ widget.bindtags(tuple(tags))
484
+ except:
485
+ # Some widgets may not support bindtags
486
+ pass
487
+
488
+ # Recurse into all children
489
+ for child in widget.winfo_children():
490
+ self._add_scroll_binding(child)
491
+
492
+ def _del_scroll_binding(self, widget):
493
+ """Recursively remove scroll bind tag from widget and all descendants."""
494
+ try:
495
+ tags = list(widget.bindtags())
496
+ if self._scroll_tag in tags:
497
+ tags.remove(self._scroll_tag)
498
+ widget.bindtags(tuple(tags))
499
+ except:
500
+ pass
501
+
502
+ # Recurse into all children
503
+ for child in widget.winfo_children():
504
+ self._del_scroll_binding(child)
505
+
506
+ def enable_scrolling(self):
507
+ """Enable mouse wheel scrolling on canvas and all child widgets."""
508
+ if not self._scrolling_enabled:
509
+ # Add binding to canvas for exposed areas
510
+ self._add_scroll_binding(self.canvas)
511
+
512
+ # Add binding to child widget if it exists
513
+ if self._child_widget:
514
+ self._add_scroll_binding(self._child_widget)
515
+
516
+ self._scrolling_enabled = True
517
+
518
+ def disable_scrolling(self):
519
+ """Disable mouse wheel scrolling on canvas and all child widgets."""
520
+ if self._scrolling_enabled:
521
+ # Remove binding from canvas
522
+ self._del_scroll_binding(self.canvas)
523
+
524
+ # Remove binding from child widget if it exists
525
+ if self._child_widget:
526
+ self._del_scroll_binding(self._child_widget)
527
+
528
+ self._scrolling_enabled = False
529
+
530
+ def add(self, widget: Widget = None, *, anchor: str = 'nw', **kwargs: Any) -> Widget:
531
+ """Add a widget to the scrollable area, or create and return a Frame.
532
+
533
+ Args:
534
+ widget (Widget | None): The widget to add. If None, creates a Frame.
535
+ anchor (str): Anchor position for the widget in the canvas. Default is 'nw'.
536
+ **kwargs: When widget is None, these are passed to Frame (e.g., padding, bootstyle).
537
+
538
+ Returns:
539
+ Widget: The content widget (passed or created).
540
+
541
+ Raises:
542
+ ValueError: If the ScrollView already contains a widget and a new one is provided.
543
+ """
544
+ # If content exists and no widget passed, return existing (idempotent)
545
+ if self._child_widget is not None:
546
+ if widget is not None:
547
+ raise ValueError("ScrollView already contains a widget. Use remove() first.")
548
+ return self._child_widget
549
+
550
+ # Create frame with kwargs if no widget provided
551
+ if widget is None:
552
+ widget = Frame(self.canvas, **kwargs)
553
+
554
+ self._child_widget = widget
555
+
556
+ # Create window in canvas
557
+ self._window_id = self.canvas.create_window(0, 0, anchor=anchor, window=widget)
558
+
559
+ # Keep scrollbars above the canvas/content
560
+ self.vertical_scrollbar.lift()
561
+ self.horizontal_scrollbar.lift()
562
+
563
+ # Bind configure event to update scroll region
564
+ widget.bind('<Configure>', self._on_frame_configure)
565
+ self._update_scrollbar_visibility()
566
+
567
+ # Bind configure event to update scroll region
568
+ widget.bind('<Configure>', self._on_frame_configure)
569
+
570
+ # Enable scrolling based on mode
571
+ if self._scrollbar_visibility in ('always', 'never', 'scroll'):
572
+ # Always enable scrolling for these modes
573
+ self.enable_scrolling()
574
+ # For 'hover' mode, scrolling is enabled on enter
575
+
576
+ # Initial scroll region update
577
+ self.canvas.update_idletasks()
578
+ self.canvas.configure(scrollregion=self.canvas.bbox('all'))
579
+
580
+ return widget
581
+
582
+ def remove(self) -> Optional[Widget]:
583
+ """Remove the current widget from the scrollable area.
584
+
585
+ Returns:
586
+ The removed widget, or None if no widget was present.
587
+ """
588
+ if self._child_widget is None:
589
+ return None
590
+
591
+ widget = self._child_widget
592
+
593
+ # Disable scrolling
594
+ self.disable_scrolling()
595
+
596
+ # Unbind events
597
+ widget.unbind('<Configure>')
598
+
599
+ # Delete canvas window
600
+ if self._window_id:
601
+ self.canvas.delete(self._window_id)
602
+ self._window_id = None
603
+
604
+ self._child_widget = None
605
+ self._scrolling_enabled = False
606
+
607
+ return widget
608
+
609
+ def get_child(self) -> Optional[Widget]:
610
+ """Get the current child widget.
611
+
612
+ Returns:
613
+ The child widget, or None if no widget is present.
614
+ """
615
+ return self._child_widget
616
+
617
+ def refresh_bindings(self):
618
+ """Refresh mouse wheel bindings for all widgets.
619
+
620
+ Call this after dynamically adding many widgets at once to ensure
621
+ mouse wheel scrolling works on all new widgets.
622
+ """
623
+ if self._child_widget and self._scrolling_enabled:
624
+ # Re-enable to refresh bindings
625
+ self.disable_scrolling()
626
+ self.enable_scrolling()
627
+
628
+ def yview(self, *args):
629
+ """Query or command vertical view position."""
630
+ return self.canvas.yview(*args)
631
+
632
+ def xview(self, *args):
633
+ """Query or command horizontal view position."""
634
+ return self.canvas.xview(*args)
635
+
636
+ def yview_moveto(self, fraction: float):
637
+ """Scroll to a specific vertical position.
638
+
639
+ Args:
640
+ fraction: Position from 0.0 (top) to 1.0 (bottom).
641
+ """
642
+ self.canvas.yview_moveto(fraction)
643
+
644
+ def xview_moveto(self, fraction: float):
645
+ """Scroll to a specific horizontal position.
646
+
647
+ Args:
648
+ fraction: Position from 0.0 (left) to 1.0 (right).
649
+ """
650
+ self.canvas.xview_moveto(fraction)
651
+
652
+ def destroy(self):
653
+ """Clean up resources and destroy the widget."""
654
+ # Cancel any pending timer
655
+ if self._hide_timer:
656
+ self.after_cancel(self._hide_timer)
657
+ self._hide_timer = None
658
+
659
+ # Remove scroll bindings from child widget
660
+ if self._child_widget:
661
+ self.disable_scrolling()
662
+
663
+ # Unbind class bindings for the scroll tag
664
+ if self.winsys.lower() == "x11":
665
+ self.unbind_class(self._scroll_tag, "<Button-4>")
666
+ self.unbind_class(self._scroll_tag, "<Button-5>")
667
+ self.unbind_class(self._scroll_tag, "<Shift-Button-4>")
668
+ self.unbind_class(self._scroll_tag, "<Shift-Button-5>")
669
+ else:
670
+ self.unbind_class(self._scroll_tag, "<MouseWheel>")
671
+ self.unbind_class(self._scroll_tag, "<Shift-MouseWheel>")
672
+
673
+ # Call parent destroy
674
+ super().destroy()
675
+