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,457 @@
1
+ """Utility functions for bootstack.
2
+
3
+ This module provides various utility functions for common tasks in
4
+ ttksbootstrap applications, including high-DPI support, screen geometry
5
+ calculations, and color manipulations.
6
+
7
+ Functions:
8
+ enable_high_dpi_awareness: Enable high-DPI scaling on Windows/Linux
9
+ detect_scale_factor: Detect the appropriate scale factor for the display
10
+ scale_size: Scale a size value for high-DPI displays
11
+ get_desktop_geometry: Get the screen dimensions
12
+ get_asset_path: Get the path to an asset file
13
+
14
+ Example:
15
+ ```python
16
+ from bootstack.utility import enable_high_dpi_awareness
17
+ import bootstack as bs
18
+
19
+ # Enable high-DPI before creating window
20
+ enable_high_dpi_awareness()
21
+
22
+ root = bs.Window()
23
+ root.mainloop()
24
+ ```
25
+ """
26
+
27
+
28
+ def _platform_baseline() -> float:
29
+ """Return the default Tk scaling baseline for the current platform.
30
+
31
+ Tk scaling is measured in pixels-per-point (72 points per inch).
32
+ - Windows/Linux default DPI is 96 → baseline = 96/72 ≈ 1.334
33
+ - macOS default DPI is 72 → baseline = 72/72 = 1.0
34
+ (Retina pixel-doubling is handled by the OS, not Tk scaling)
35
+ """
36
+ import platform
37
+ if platform.system() == 'Darwin':
38
+ return 1.0
39
+ return 1.33398982438864281 # 96 DPI / 72
40
+
41
+
42
+ class _ScalingState:
43
+ """Internal class to store global scaling state."""
44
+ _scale_factor: float = 1.0
45
+ _baseline: float = _platform_baseline()
46
+
47
+ @classmethod
48
+ def set_scale_factor(cls, factor: float):
49
+ """Set the global scale factor."""
50
+ cls._scale_factor = factor
51
+
52
+ @classmethod
53
+ def get_scale_factor(cls) -> float:
54
+ """Get the current global scale factor."""
55
+ return cls._scale_factor
56
+
57
+ @classmethod
58
+ def get_ui_scale(cls) -> float:
59
+ """Get the UI scale factor (relative to baseline)."""
60
+ return cls._scale_factor / cls._baseline
61
+
62
+ @classmethod
63
+ def get_image_scale(cls, source_resolution: float = 2.0) -> float:
64
+ """Get the scale factor for images.
65
+
66
+ Args:
67
+ source_resolution: The resolution multiplier of source images.
68
+ For example, 2.0 means images are 2x resolution.
69
+
70
+ Returns:
71
+ The scale factor to apply to images.
72
+ """
73
+ return cls.get_ui_scale() / source_resolution
74
+
75
+
76
+ def enable_high_dpi_awareness(root=None, scaling=None):
77
+ """Enable high dpi awareness.
78
+
79
+ **Windows OS**
80
+ Call the method BEFORE creating the `Tk` object. No parameters
81
+ required. After the root is created, call again with the root
82
+ parameter to apply detected scaling.
83
+
84
+ **Linux OS**
85
+ Must provided the `root` and `scaling` parameters. Call the method
86
+ AFTER creating the `Tk` object. A number between 1.6 and 2.0 is
87
+ usually suffient to scale for high-dpi screen.
88
+
89
+ !!! warning
90
+ If the `root` argument is provided, then `scaling` must also
91
+ be provided. Otherwise, there is no effect.
92
+
93
+ Parameters:
94
+
95
+ root (tk.Tk):
96
+ The root widget
97
+
98
+ scaling (float or 'auto'):
99
+ Sets and queries the current scaling factor used by Tk to
100
+ convert between physical units (for example, points,
101
+ inches, or millimeters) and pixels. The number argument is
102
+ a floating point number that specifies the number of pixels
103
+ per point on window's display. If the window argument is
104
+ omitted, it defaults to the main window. If the number
105
+ argument is omitted, the current value of the scaling
106
+ factor is returned.
107
+
108
+ If set to 'auto', the scale factor will be detected
109
+ automatically from the system DPI settings.
110
+
111
+ A "point" is a unit of measurement equal to 1/72 inch. A
112
+ scaling factor of 1.0 corresponds to 1 pixel per point,
113
+ which is equivalent to a standard 72 dpi monitor. A scaling
114
+ factor of 1.25 would mean 1.25 pixels per point, which is
115
+ the setting for a 90 dpi monitor; setting the scaling factor
116
+ to 1.25 on a 72 dpi monitor would cause everything in the
117
+ application to be displayed 1.25 times as large as normal.
118
+ The initial value for the scaling factor is set when the
119
+ application starts, based on properties of the installed
120
+ monitor, but it can be changed at any time. Measurements
121
+ made after the scaling factor is changed will use the new
122
+ scaling factor, but it is undefined whether existing
123
+ widgets will resize themselves dynamically to accommodate
124
+ the new scaling factor.
125
+
126
+ Returns:
127
+
128
+ float:
129
+ The scaling factor that was applied, or None if no scaling
130
+ was applied.
131
+ """
132
+
133
+ # Enable DPI awareness on Windows
134
+ try:
135
+ from ctypes import windll
136
+ # Use shcore for better DPI awareness (Windows 8.1+)
137
+ try:
138
+ windll.shcore.SetProcessDpiAwareness(1)
139
+ except:
140
+ # Fallback to older API
141
+ windll.user32.SetProcessDPIAware()
142
+ except:
143
+ pass
144
+
145
+ # Apply scaling if root is provided
146
+ if root:
147
+ # Auto-detect scaling if requested
148
+ if scaling == 'auto':
149
+ scaling = detect_scale_factor(root)
150
+
151
+ if scaling:
152
+ try:
153
+ root.tk.call('tk', 'scaling', scaling)
154
+ # Store the scale factor globally
155
+ _ScalingState.set_scale_factor(scaling)
156
+ return scaling
157
+ except:
158
+ pass
159
+
160
+ return None
161
+
162
+
163
+ def detect_scale_factor(root):
164
+ """Detect the appropriate scale factor for the display.
165
+
166
+ This function attempts to detect the system DPI scaling and return
167
+ an appropriate scale factor for Tk. The scale factor is measured in
168
+ "pixels per point" (72 points per inch).
169
+
170
+ Parameters:
171
+
172
+ root (tk.Tk):
173
+ The root window (must be created before calling)
174
+
175
+ Returns:
176
+
177
+ float:
178
+ The detected scale factor in pixels per point
179
+ (e.g., 1.333 for 96 DPI, 2.0 for 144 DPI)
180
+ """
181
+ import platform
182
+
183
+ system = platform.system()
184
+
185
+ try:
186
+ if system == 'Windows':
187
+ # Try to get scale factor from Windows (Windows 8.1+)
188
+ try:
189
+ from ctypes import windll
190
+ # GetScaleFactorForDevice returns percentage (100, 125, 150, 200, etc.)
191
+ scale_percent = windll.shcore.GetScaleFactorForDevice(0)
192
+ # Convert to DPI: 100% = 96 DPI, 125% = 120 DPI, etc.
193
+ dpi = (96 * scale_percent) / 100
194
+ # Convert to tk scaling (pixels per point, 72 points per inch)
195
+ scale_factor = dpi / 72
196
+ return scale_factor
197
+ except:
198
+ pass
199
+
200
+ # Fallback: detect from current DPI
201
+ # This works on Linux, Mac, and older Windows
202
+ current_dpi = root.winfo_fpixels('1i')
203
+ # Tk scaling is pixels per point (72 points per inch)
204
+ scale_factor = current_dpi / 72
205
+ return scale_factor
206
+
207
+ except:
208
+ # If all else fails, return default for 96 DPI
209
+ return 96 / 72 # 1.333...
210
+
211
+
212
+ def get_image_name(image):
213
+ """Extract and return the tcl/tk image name from a PhotoImage
214
+ object.
215
+
216
+ Parameters:
217
+
218
+ image (ImageTk.PhotoImage):
219
+ A photoimage object.
220
+
221
+ Returns:
222
+
223
+ str:
224
+ The tcl/tk name of the photoimage object.
225
+ """
226
+ return image._PhotoImage__photo.name
227
+
228
+
229
+ def scale_padding_floor(base: int) -> int:
230
+ """Scale a padding value up for DPI, never dropping below `base`.
231
+
232
+ Used for the fixed gap between a nine-patch border (whose border slice grows
233
+ with DPI) and the widget packed inside it. If the gap does not grow with the
234
+ border, at high DPI the inner widget overpaints the now-thicker border slice
235
+ and the resting border disappears (#90).
236
+
237
+ Rounds rather than truncates so intermediate scales (e.g. 1.5x) do not lose a
238
+ pixel and clip the rounded corners, and floors at `base` so low DPI keeps its
239
+ tuned spacing.
240
+
241
+ Args:
242
+ base: The baseline padding in pixels at standard DPI.
243
+
244
+ Returns:
245
+ The DPI-scaled padding, at least `base`.
246
+ """
247
+ return max(base, round(base * _ScalingState.get_ui_scale()))
248
+
249
+
250
+ def scale_icon_size(base: int) -> int:
251
+ """Scale a logical icon size to physical pixels for the current DPI.
252
+
253
+ Icons are authored at logical (96-DPI) sizes. On a higher-DPI display the
254
+ surrounding fonts and layout scale up via Tk scaling, so an icon rendered at
255
+ its literal logical size lands too small for its container and gets resampled
256
+ by the display, reading as soft (#267). Rendering at the physical size makes
257
+ the glyph land 1:1 in the scaled layout, so it stays crisp.
258
+
259
+ Floors at `base` (mirrors `scale_padding_floor`) so an uninitialized scale
260
+ state or a sub-baseline display never renders an icon smaller than requested.
261
+
262
+ Args:
263
+ base: The logical icon size in pixels at standard (96-DPI) scaling.
264
+
265
+ Returns:
266
+ The DPI-scaled size, at least `base`.
267
+ """
268
+ return max(base, round(base * _ScalingState.get_ui_scale()))
269
+
270
+
271
+ def scale_size(widget=None, size=None):
272
+ """Scale the size based on the scaling factor of tkinter.
273
+ This is used most frequently to adjust the assets for
274
+ image-based widget layouts and padding values.
275
+
276
+ Can be called in two ways:
277
+ 1. scale_size(widget, size) - Legacy mode, calculates from widget
278
+ 2. scale_size(size) - Uses global scaling state
279
+
280
+ Parameters:
281
+
282
+ widget (Widget, optional):
283
+ The widget object. If provided, scaling is calculated from
284
+ the widget's tk instance. If None, uses global scaling state.
285
+
286
+ size (Union[int, List, Tuple]):
287
+ A single integer or an iterable of integers
288
+
289
+ Returns:
290
+
291
+ Union[int, List]:
292
+ An integer or list of integers representing the new size.
293
+
294
+ Examples:
295
+
296
+ >>> # Using widget (legacy mode)
297
+ >>> scaled = scale_size(my_widget, 10)
298
+
299
+ >>> # Using global state (new mode)
300
+ >>> scaled = scale_size(10)
301
+ """
302
+ # Handle both calling conventions
303
+ if widget is not None and size is None:
304
+ # Called as scale_size(size) - widget is actually the size
305
+ size = widget
306
+ factor = _ScalingState.get_ui_scale()
307
+ elif widget is not None and size is not None:
308
+ # Called as scale_size(widget, size) - legacy mode
309
+ BASELINE = _ScalingState._baseline
310
+ scaling = widget.tk.call('tk', 'scaling')
311
+ factor = scaling / BASELINE
312
+ else:
313
+ # Called as scale_size(size=size) - use global state
314
+ factor = _ScalingState.get_ui_scale()
315
+
316
+ if isinstance(size, int):
317
+ return int(size * factor)
318
+ elif isinstance(size, tuple) or isinstance(size, list):
319
+ return [int(x * factor) for x in size]
320
+ else:
321
+ return size
322
+
323
+
324
+ # --- Debug helpers ---------------------------------------------------------
325
+ def _debug_enabled() -> bool:
326
+ """Return True if debug logging is enabled.
327
+
328
+ Controlled via the environment variable `BOOTSTACK_DEBUG`.
329
+ Accepts: "1", "true", "yes" (case-insensitive) as truthy values.
330
+ """
331
+ import os
332
+ return str(os.environ.get("BOOTSTACK_DEBUG", "")).lower() in {"1", "true", "yes"}
333
+
334
+
335
+ def debug_log_exception(message: str = "") -> None:
336
+ """Print the current exception traceback if debug is enabled.
337
+
338
+ Args:
339
+ message: Optional context message to print before the traceback.
340
+ """
341
+ if not _debug_enabled():
342
+ return
343
+ try:
344
+ import traceback
345
+ if message:
346
+ print(f"bootstack DEBUG: {message}")
347
+ traceback.print_exc()
348
+ except Exception:
349
+ # Never raise from debug logging
350
+ pass
351
+
352
+
353
+ def center_on_parent(win, parent=None):
354
+ """Center `win` on parent or over its master if not given"""
355
+ win.update_idletasks() # ensure geometry
356
+ if parent is None:
357
+ parent = getattr(win, 'master', None) or win # root if no parent
358
+
359
+ # parent geometry
360
+ parent.update_idletasks()
361
+ px, py = parent.winfo_rootx(), parent.winfo_rooty()
362
+ pw, ph = parent.winfo_width(), parent.winfo_height()
363
+ if pw <= 1 or ph <= 1:
364
+ # not yet realized, fallback to requested size
365
+ pw, ph = parent.winfo_reqwidth(), parent.winfo_reqheight()
366
+
367
+ # window geometry
368
+ ww = win.winfo_width() or win.winfo_reqwidth()
369
+ wh = win.winfo_height() or win.winfo_reqheight()
370
+
371
+ x = px + (pw - ww) // 2
372
+ y = py + (ph - wh) // 2
373
+ win.geometry(f"{ww}x{wh}+{x}+{y}")
374
+
375
+
376
+ def bind_right_click(widget, handler, add: str | bool = '+'):
377
+ """Bind a right-click handler portably across Tk windowing systems.
378
+
379
+ On Win/Linux right-click maps to `<Button-3>`. On macOS Tk maps the
380
+ right mouse button (and two-finger trackpad click) to `<Button-2>`,
381
+ and Mac users also expect Ctrl+click as a context-menu trigger. This
382
+ helper binds the appropriate event(s) for the current platform so
383
+ callers don't have to repeat the platform check.
384
+
385
+ Args:
386
+ widget: Any Tk widget with a `.bind` method and a `.tk` attribute.
387
+ handler: Event handler callable (or Tcl command string).
388
+ add: Passed through to `bind`. Defaults to `'+'` so the helper
389
+ never silently replaces an existing binding.
390
+ """
391
+ widget.bind('<Button-3>', handler, add=add)
392
+ try:
393
+ winsys = widget.tk.call('tk', 'windowingsystem')
394
+ except Exception:
395
+ winsys = None
396
+ if winsys == 'aqua':
397
+ widget.bind('<Button-2>', handler, add=add)
398
+ widget.bind('<Control-Button-1>', handler, add=add)
399
+
400
+
401
+ def propagate_target_bindings(target) -> None:
402
+ """Make events on `target`'s descendants also fire `target`'s own bindings.
403
+
404
+ Tk events do not bubble up the widget hierarchy — the widget directly under
405
+ the pointer receives the event — so a gesture (right-click, hover, ...) bound
406
+ to a *container* never fires when the pointer is over one of the container's
407
+ children. This adds the container's own bindtag (its Tk path name, the tag
408
+ that `widget.bind(...)` registers under) to every descendant in the same
409
+ toplevel, so the container's binding fires for events anywhere inside it.
410
+
411
+ The tag is inserted just after each descendant's own path-name tag, so the
412
+ child's own bindings still take precedence. The call is idempotent, so it is
413
+ safe to run again after the container gains new children. Nested toplevels
414
+ (popups, menus, dialogs parented to the target) are skipped.
415
+
416
+ Args:
417
+ target: The container widget whose bindings should cover its descendants.
418
+ """
419
+ try:
420
+ tag = str(target)
421
+ top = str(target.winfo_toplevel())
422
+ except Exception:
423
+ return
424
+
425
+ def visit(widget):
426
+ try:
427
+ children = widget.winfo_children()
428
+ except Exception:
429
+ return
430
+ for child in children:
431
+ try:
432
+ # Don't cross into nested toplevels (the menu/tooltip popup
433
+ # itself is parented to the target and would be walked here).
434
+ if str(child.winfo_toplevel()) != top:
435
+ continue
436
+ tags = list(child.bindtags())
437
+ if tag not in tags:
438
+ child.bindtags([tags[0], tag, *tags[1:]] if tags else [tag])
439
+ except Exception:
440
+ continue
441
+ visit(child)
442
+
443
+ visit(target)
444
+
445
+
446
+ def clamp(value, min_val, max_val):
447
+ """Return a value that is bounded by a minimum and maximum.
448
+
449
+ Args:
450
+ value: The value to evaluate.
451
+ min_val: The minimum allowed value.
452
+ max_val: The maximum allowed value.
453
+
454
+ Returns:
455
+ The value, constrained between `min_val` and `max_val`.
456
+ """
457
+ return min(max(value, min_val), max_val)
@@ -0,0 +1,240 @@
1
+ """Visual focus management for bootstack.
2
+
3
+ This module provides keyboard vs mouse focus distinction by leveraging
4
+ the TTK 'background' state as a "keyboard focus" indicator. This enables
5
+ style maps to show focus rings only for keyboard navigation (Tab), not
6
+ for mouse clicks.
7
+
8
+ The approach:
9
+ - Tab key press → sets 'background' state on newly focused widget
10
+ - FocusOut → removes 'background' state
11
+ - Mouse clicks never add 'background', so click focus is distinguishable
12
+
13
+ Style maps can then use:
14
+ ('background focus', ring_color) # Keyboard focus - show ring
15
+ ('focus', '') # Mouse focus - no ring
16
+
17
+ This matches the CSS :focus-visible behavior that modern browsers implement.
18
+
19
+ Programmatic focus:
20
+ The focus_set() and focus_force() methods accept a `visual_focus` parameter.
21
+ When True, the focus ring is shown as if the widget was focused via keyboard:
22
+
23
+ widget.focus_set(visual_focus=True) # Shows focus ring
24
+
25
+ Note:
26
+ The 'background' TTK state is normally used to indicate an inactive
27
+ window. Since this is rarely styled in practice, it's repurposed here
28
+ for keyboard focus tracking.
29
+ """
30
+
31
+ import tkinter as tk
32
+ from tkinter import TclError
33
+ from typing import Optional
34
+
35
+ _installed = False
36
+ _root_ref: Optional[tk.Misc] = None
37
+
38
+ # Store original focus methods
39
+ _original_focus_set = tk.Misc.focus_set
40
+ _original_focus_force = tk.Misc.focus_force
41
+
42
+
43
+ def _patched_focus_set(self, *, visual_focus: bool = False) -> None:
44
+ """Enhanced focus_set that optionally shows visual focus ring.
45
+
46
+ Args:
47
+ visual_focus: If True, show focus as if focused via keyboard. Default is False.
48
+
49
+ Examples:
50
+ ```python
51
+ # Normal programmatic focus (no ring)
52
+ entry.focus_set()
53
+
54
+ # Focus with visible ring (e.g., after validation error)
55
+ entry.focus_set(visual_focus=True)
56
+ ```
57
+ """
58
+ _original_focus_set(self)
59
+ if visual_focus:
60
+ try:
61
+ self.state(['background'])
62
+ except (TclError, AttributeError):
63
+ pass
64
+
65
+
66
+ def _patched_focus_force(self, *, visual_focus: bool = False) -> None:
67
+ """Enhanced focus_force that optionally shows visual focus ring.
68
+
69
+ Args:
70
+ visual_focus: If True, show focus as if focused via keyboard. Default is False.
71
+
72
+ Examples:
73
+ ```python
74
+ # Normal forced focus (no ring)
75
+ entry.focus_force()
76
+
77
+ # Forced focus with visible ring
78
+ entry.focus_force(visual_focus=True)
79
+ ```
80
+ """
81
+ _original_focus_force(self)
82
+ if visual_focus:
83
+ try:
84
+ self.state(['background'])
85
+ except (TclError, AttributeError):
86
+ pass
87
+
88
+
89
+ def _on_tab_focus(event: tk.Event) -> None:
90
+ """Handle Tab key press by marking the newly focused widget.
91
+
92
+ Uses after_idle to wait for focus to actually move before
93
+ querying focus_get() and setting the background state.
94
+ """
95
+ root = event.widget.winfo_toplevel()
96
+
97
+ def set_keyboard_focus_state():
98
+ widget = root.focus_get()
99
+ if widget is None:
100
+ return
101
+ try:
102
+ widget.state(['background'])
103
+ except (TclError, AttributeError):
104
+ pass # Widget doesn't support state (non-TTK)
105
+
106
+ root.after_idle(set_keyboard_focus_state)
107
+
108
+
109
+ def _on_focus_out(event: tk.Event) -> None:
110
+ """Clear the keyboard focus indicator when widget loses focus."""
111
+ try:
112
+ event.widget.state(['!background'])
113
+ except (TclError, AttributeError):
114
+ pass # Widget doesn't support state (non-TTK)
115
+
116
+
117
+ def install_visual_focus(root: tk.Misc = None) -> None:
118
+ """Install keyboard focus tracking for the application.
119
+
120
+ This function sets up global event bindings that track whether
121
+ focus was acquired via keyboard (Tab) or mouse click, enabling
122
+ style maps to show focus rings only for keyboard navigation.
123
+
124
+ Args:
125
+ root: Optional root widget. If not provided, bindings are
126
+ set up to work with any root via bind_class on Tk.
127
+
128
+ Note:
129
+ This is called automatically when bootstack is imported.
130
+ You typically don't need to call this manually.
131
+
132
+ Examples:
133
+ Style builders can use the 'background' state to distinguish:
134
+
135
+ ```python
136
+ b.map_style(ttk_style,
137
+ focuscolor=[
138
+ ('background focus', ring_color), # Keyboard focus
139
+ ('focus', ''), # Mouse focus
140
+ ('', ''),
141
+ ]
142
+ )
143
+ ```
144
+ """
145
+ global _installed, _root_ref
146
+
147
+ if _installed:
148
+ return
149
+
150
+ # Patch focus_set and focus_force to support visual_focus parameter
151
+ tk.Misc.focus_set = _patched_focus_set
152
+ tk.Misc.focus_force = _patched_focus_force
153
+
154
+ # Bind Tab key globally - works even before any root exists
155
+ # by using bind_class on the base Tk class
156
+ tk.Tk.bind_all = _bind_all_with_focus
157
+
158
+ _installed = True
159
+
160
+
161
+ def _bind_all_with_focus(self, sequence=None, func=None, add=None):
162
+ """Wrapper for bind_all that installs focus tracking on first call."""
163
+ global _root_ref
164
+
165
+ # Install our bindings on first bind_all call (when root exists)
166
+ if _root_ref is None:
167
+ _root_ref = self
168
+ # Bind Tab on root window - the after_idle callback will set state
169
+ # on whatever widget receives focus (forward or backward)
170
+ self.bind('<Tab>', _on_tab_focus, '+')
171
+ # Clear background state on focus out (needs bind_all for all widgets)
172
+ self.bind_all('<FocusOut>', _on_focus_out, '+')
173
+
174
+ # Call original bind_all
175
+ return tk.Misc.bind_all(self, sequence, func, add)
176
+
177
+
178
+ def uninstall_visual_focus() -> None:
179
+ """Remove keyboard focus tracking bindings.
180
+
181
+ This restores the original behavior where focus state doesn't
182
+ distinguish between keyboard and mouse focus.
183
+
184
+ Note:
185
+ After calling this, style maps using 'background focus' will
186
+ no longer show focus rings for keyboard navigation.
187
+ """
188
+ global _installed, _root_ref
189
+
190
+ if not _installed:
191
+ return
192
+
193
+ # Restore original focus methods
194
+ tk.Misc.focus_set = _original_focus_set
195
+ tk.Misc.focus_force = _original_focus_force
196
+
197
+ if _root_ref is not None:
198
+ try:
199
+ _root_ref.unbind('<Tab>')
200
+ _root_ref.unbind_all('<FocusOut>')
201
+ except TclError:
202
+ pass # Root may have been destroyed
203
+
204
+ _root_ref = None
205
+ _installed = False
206
+
207
+
208
+ def reset_visual_focus_root() -> None:
209
+ """Forget the cached root reference without un-patching `bind_all`.
210
+
211
+ `_root_ref` is captured lazily from the first root that binds. After that
212
+ root is destroyed the reference is stale; clearing it lets the next root
213
+ re-bind. The `bind_all` patch (installed once at import) stays in place.
214
+ """
215
+ global _root_ref
216
+ _root_ref = None
217
+
218
+
219
+ def is_keyboard_focus(widget: tk.Misc) -> bool:
220
+ """Check if a widget currently has keyboard-initiated focus.
221
+
222
+ Args:
223
+ widget: The widget to check.
224
+
225
+ Returns:
226
+ True if the widget has focus AND was focused via keyboard.
227
+ """
228
+ try:
229
+ state = widget.state()
230
+ return 'focus' in state and 'background' in state
231
+ except (TclError, AttributeError):
232
+ return False
233
+
234
+
235
+ __all__ = [
236
+ 'install_visual_focus',
237
+ 'uninstall_visual_focus',
238
+ 'reset_visual_focus_root',
239
+ 'is_keyboard_focus',
240
+ ]