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,268 @@
1
+ """Signal integration mixins for bootstack widgets.
2
+
3
+ Provides seamless integration between tkinter Variables and reactive Signals,
4
+ exposing both as properties on widgets that support `textvariable` and `variable`
5
+ options. The mixins maintain bidirectional synchronization between Variables and
6
+ Signals automatically.
7
+
8
+ These mixins are thin glue layers that delegate to the core signal capability
9
+ module for normalization and binding logic.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import tkinter as tk
15
+ from typing import Any, TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from bootstack.signals import Signal
19
+
20
+ from bootstack._core.capabilities.signals import (
21
+ normalize_signal,
22
+ create_signal,
23
+ infer_default_value_for_widget,
24
+ query_binding,
25
+ )
26
+ from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
27
+
28
+
29
+ class TextSignalMixin:
30
+ """Mixin providing `.textvariable` and `.textsignal` properties for text-based widgets.
31
+
32
+ For widgets that support the `textvariable` option (Entry, Label, Button, etc.),
33
+ this mixin exposes both the underlying tk.Variable and a reactive Signal as properties,
34
+ maintaining bidirectional synchronization between them.
35
+
36
+ This mixin delegates normalization and binding logic to the core signals capability.
37
+
38
+ """
39
+
40
+ def __init__(self, *args, **kwargs):
41
+ """Initialize mixin and extract textsignal/textvariable parameters before tkinter sees them."""
42
+ # Extract textsignal and textvariable before passing kwargs to tkinter
43
+ textsignal_value = kwargs.pop('textsignal', None)
44
+ textvariable_value = kwargs.pop('textvariable', None)
45
+
46
+ # Call parent __init__
47
+ super().__init__(*args, **kwargs)
48
+
49
+ # Apply textsignal or textvariable after widget construction
50
+ # Prefer textsignal if both are provided
51
+ if textsignal_value is not None:
52
+ self._config_delegate_set('textsignal', textsignal_value)
53
+ elif textvariable_value is not None:
54
+ self._config_delegate_set('textvariable', textvariable_value)
55
+
56
+ @configure_delegate("textvariable", "textsignal")
57
+ def _delegate_textsignal(self, value: Any = None):
58
+ """Handle textvariable and textsignal configuration.
59
+
60
+ Args:
61
+ value: Signal, tk.Variable, or None for query.
62
+
63
+ Returns:
64
+ Current value for query path, None for set path.
65
+ """
66
+ # Query path - return stored value
67
+ if value is None:
68
+ return query_binding(
69
+ getattr(self, '_textsignal', None),
70
+ getattr(self, '_textvariable', None),
71
+ self._ttk_base, # type: ignore[misc]
72
+ self,
73
+ 'textvariable',
74
+ )
75
+
76
+ # Set path - normalize and apply
77
+ binding = normalize_signal(value, default_value="")
78
+ if binding is not None:
79
+ self._textsignal = binding.signal
80
+ self._textvariable = binding.variable
81
+ var_to_set = binding.tk_value
82
+ else:
83
+ # String (Tcl variable name) or other - pass through
84
+ var_to_set = value
85
+ if value:
86
+ self._textvariable = value
87
+
88
+ # Apply via base ttk widget to avoid recursion
89
+ return self._ttk_base.configure(self, textvariable=var_to_set) # type: ignore[misc]
90
+
91
+ @property
92
+ def textsignal(self) -> Signal[str]:
93
+ """Get or lazily create the textsignal.
94
+
95
+ If no textvariable has been set, creates a new Signal with empty string.
96
+ If textvariable exists, wraps it in a Signal.
97
+
98
+ Returns:
99
+ The reactive Signal for this widget's text.
100
+ """
101
+ if not hasattr(self, '_textsignal'):
102
+ # Create fresh signal
103
+ binding = create_signal("")
104
+ self._textsignal = binding.signal
105
+ self._textvariable = binding.variable
106
+ try:
107
+ self._ttk_base.configure(self, textvariable=binding.tk_value) # type: ignore[misc]
108
+ except Exception:
109
+ pass
110
+ return self._textsignal
111
+
112
+ @textsignal.setter
113
+ def textsignal(self, value: Signal[str]) -> None:
114
+ """Set the textsignal, extracting and configuring its underlying variable.
115
+
116
+ Args:
117
+ value: Signal to use for this widget's text.
118
+ """
119
+ self._delegate_textsignal(value)
120
+
121
+ @property
122
+ def textvariable(self) -> tk.Variable:
123
+ """Get the underlying tk.Variable.
124
+
125
+ If not yet created, accessing this property will trigger lazy creation
126
+ via the textsignal property.
127
+
128
+ Returns:
129
+ The tk.Variable (usually StringVar) for this widget's text.
130
+
131
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
132
+ """
133
+ if not hasattr(self, '_textvariable'):
134
+ # Trigger lazy creation via textsignal
135
+ _ = self.textsignal
136
+ return self._textvariable
137
+
138
+ @textvariable.setter
139
+ def textvariable(self, value: tk.Variable) -> None:
140
+ """Set the textvariable, creating a synced Signal automatically.
141
+
142
+ Args:
143
+ value: tk.Variable to use for this widget's text.
144
+ """
145
+ self._delegate_textsignal(value)
146
+
147
+
148
+ class SignalMixin:
149
+ """Mixin providing `.variable` and `.signal` properties for value-based widgets.
150
+
151
+ For widgets that support the `variable` option (Checkbutton, Radiobutton, Scale, etc.),
152
+ this mixin exposes both the underlying tk.Variable and a reactive Signal as properties,
153
+ maintaining bidirectional synchronization between them.
154
+
155
+ This mixin delegates normalization and binding logic to the core signals capability.
156
+
157
+ """
158
+
159
+ def __init__(self, *args, **kwargs):
160
+ """Initialize mixin and extract signal/variable parameters before tkinter sees them."""
161
+ # Extract signal and variable before passing kwargs to tkinter
162
+ signal_value = kwargs.pop('signal', None)
163
+ variable_value = kwargs.pop('variable', None)
164
+
165
+ # Call parent __init__
166
+ super().__init__(*args, **kwargs)
167
+
168
+ # Apply signal or variable after widget construction
169
+ # Prefer signal if both are provided
170
+ if signal_value is not None:
171
+ self._config_delegate_set('signal', signal_value)
172
+ elif variable_value is not None:
173
+ self._config_delegate_set('variable', variable_value)
174
+
175
+ @configure_delegate("variable", "signal")
176
+ def _delegate_signal(self, value: Any = None):
177
+ """Handle variable and signal configuration.
178
+
179
+ Args:
180
+ value: Signal, tk.Variable, or None for query.
181
+
182
+ Returns:
183
+ Current value for query path, None for set path.
184
+ """
185
+ # Query path - return stored value
186
+ if value is None:
187
+ return query_binding(
188
+ getattr(self, '_signal', None),
189
+ getattr(self, '_variable', None),
190
+ self._ttk_base, # type: ignore[misc]
191
+ self,
192
+ 'variable',
193
+ )
194
+
195
+ # Set path - normalize and apply
196
+ binding = normalize_signal(value)
197
+ if binding is not None:
198
+ self._signal = binding.signal
199
+ self._variable = binding.variable
200
+ var_to_set = binding.tk_value
201
+ else:
202
+ # String (Tcl variable name) or other - pass through
203
+ var_to_set = value
204
+ if value:
205
+ self._variable = value
206
+
207
+ # Apply via base ttk widget to avoid recursion
208
+ return self._ttk_base.configure(self, variable=var_to_set) # type: ignore[misc]
209
+
210
+ @property
211
+ def signal(self) -> Signal[Any]:
212
+ """Get or lazily create the signal.
213
+
214
+ If no variable has been set, creates a new Signal with appropriate default
215
+ (False for Checkbutton, 0 for Scale, etc.). If variable exists, wraps it in a Signal.
216
+
217
+ Returns:
218
+ The reactive Signal for this widget's value.
219
+ """
220
+ if not hasattr(self, '_signal'):
221
+ # Infer default value based on widget type
222
+ default_value = infer_default_value_for_widget(self.winfo_class())
223
+ binding = create_signal(default_value)
224
+ self._signal = binding.signal
225
+ self._variable = binding.variable
226
+ try:
227
+ self._ttk_base.configure(self, variable=binding.tk_value) # type: ignore[misc]
228
+ except Exception:
229
+ pass
230
+ return self._signal
231
+
232
+ @signal.setter
233
+ def signal(self, value: Signal[Any]) -> None:
234
+ """Set the signal, extracting and configuring its underlying variable.
235
+
236
+ Args:
237
+ value: Signal to use for this widget's value.
238
+ """
239
+ self._delegate_signal(value)
240
+
241
+ @property
242
+ def variable(self) -> tk.Variable:
243
+ """Get the underlying tk.Variable.
244
+
245
+ If not yet created, accessing this property will trigger lazy creation
246
+ via the signal property.
247
+
248
+ Returns:
249
+ The tk.Variable (IntVar, BooleanVar, DoubleVar, etc.) for this widget's value.
250
+
251
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
252
+ """
253
+ if not hasattr(self, '_variable'):
254
+ # Trigger lazy creation via signal
255
+ _ = self.signal
256
+ return self._variable
257
+
258
+ @variable.setter
259
+ def variable(self, value: tk.Variable) -> None:
260
+ """Set the variable, creating a synced Signal automatically.
261
+
262
+ Args:
263
+ value: tk.Variable to use for this widget's value.
264
+ """
265
+ self._delegate_signal(value)
266
+
267
+
268
+ __all__ = ["TextSignalMixin", "SignalMixin"]
@@ -0,0 +1,226 @@
1
+ """Validation mixin for widgets with enhanced event system.
2
+
3
+ This mixin provides validation functionality using the enhanced event system
4
+ that allows passing data directly through virtual events.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import tkinter
10
+ from tkinter import TclError
11
+ from tkinter.ttk import Widget
12
+ from typing import Any, Callable
13
+
14
+ from bootstack.events import ValidationEvent
15
+ from bootstack.signals import Signal
16
+ from bootstack.validation import ValidationRule
17
+ from bootstack.validation.types import RuleTriggerType, RuleType, ValidationOptions
18
+
19
+
20
+ class ValidationMixin(Widget):
21
+ """Pure-Tkinter validation mixin for bound widgets.
22
+
23
+ Provides debounced auto-validation on key/blur with virtual event emission.
24
+ Event data is accessible via `event.data` in handlers.
25
+
26
+ """
27
+
28
+ EVENT_VALID = '<<Valid>>'
29
+ EVENT_INVALID = '<<Invalid>>'
30
+ EVENT_VALIDATED = '<<Validate>>'
31
+ SEQ_KEYUP = '<KeyRelease>'
32
+ SEQ_BLUR = '<FocusOut>'
33
+
34
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
35
+ """Set up validation state before the widget is fully initialized.
36
+
37
+ Initialises the rule list, debounce tracking, and optional convenience
38
+ callbacks. Binds `<<KeyRelease>>` and `<<FocusOut>>` to trigger
39
+ debounced validation automatically.
40
+ """
41
+ # Validation rules
42
+ self._rules: list[ValidationRule] = []
43
+
44
+ # Debounce tracking
45
+ self._debounce_ids: dict[str, str] = {}
46
+
47
+ # Optional convenience callbacks
48
+ self._on_invalid_command: Callable | None = None
49
+ self._on_valid_command: Callable | None = None
50
+ self._on_validated_command: Callable | None = None
51
+
52
+ super().__init__(*args, **kwargs) # next in MRO must be a Tk/ttk widget
53
+
54
+ # Reactive validity state — the source of truth for the field's display
55
+ # and for form-level aggregation. Updated on every validation run.
56
+ self._valid_signal: Signal = Signal(True)
57
+ self._error_signal: Signal = Signal("")
58
+
59
+ self._setup_validation_binds()
60
+
61
+ def _set_validity(self, is_valid: bool, message: str) -> None:
62
+ """Update the reactive validity signals (the source of truth)."""
63
+ self._error_signal.set("" if is_valid else message)
64
+ self._valid_signal.set(is_valid)
65
+
66
+ # ---------------- Public API ----------------
67
+
68
+ def value(self) -> Any:
69
+ """Default value accessor; override for non-Entry widgets.
70
+
71
+ Returns:
72
+ Current widget value
73
+ """
74
+ if hasattr(self, 'get'):
75
+ try:
76
+ return self.get()
77
+ except AttributeError:
78
+ return None
79
+ return None
80
+
81
+ def add_validation_rule(self, rule_type: RuleType, **kwargs: ValidationOptions) -> None:
82
+ """Add a single validation rule.
83
+
84
+ Args:
85
+ rule_type: Type of validation rule (e.g., 'required', 'min_length')
86
+ **kwargs: Rule-specific options (e.g., min_length=5, message='...')
87
+ """
88
+ self._rules.append(ValidationRule(rule_type, **kwargs))
89
+
90
+ def add_validation_rules(self, rules: list[ValidationRule]) -> None:
91
+ """Replace all validation rules.
92
+
93
+ Args:
94
+ rules: List of ValidationRule objects
95
+ """
96
+ self._rules = list(rules)
97
+
98
+ def validate(self, value: Any, trigger: RuleTriggerType = "manual") -> bool:
99
+ """Run validation rules against a value.
100
+
101
+ Emits `<<Valid>>`, `<<Invalid>>`, and `<<Validate>>` events
102
+ with data payload containing validation results.
103
+
104
+ Args:
105
+ value: Value to validate
106
+ trigger: Trigger type ('manual', 'key', 'blur', or 'always')
107
+
108
+ Returns:
109
+ True if every applicable rule passed (vacuously true when no rule
110
+ applies for this trigger), False on the first failure.
111
+ """
112
+ ran_rule = False
113
+
114
+ for rule in self._rules:
115
+ if trigger != "manual" and rule.trigger not in ("always", trigger):
116
+ continue
117
+
118
+ ran_rule = True
119
+ result = rule.validate(value)
120
+
121
+ if not result.is_valid:
122
+ # Update validity state, then emit invalid + validated events.
123
+ self._set_validity(False, result.message)
124
+ payload = ValidationEvent(value=value, is_valid=False, message=result.message)
125
+ self.event_generate(self.EVENT_INVALID, data=payload)
126
+ self.event_generate(self.EVENT_VALIDATED, data=payload)
127
+ return False
128
+
129
+ if ran_rule:
130
+ # Update validity state, then emit valid + validated events.
131
+ self._set_validity(True, "")
132
+ payload = ValidationEvent(value=value, is_valid=True, message="")
133
+ self.event_generate(self.EVENT_VALID, data=payload)
134
+ self.event_generate(self.EVENT_VALIDATED, data=payload)
135
+
136
+ # Nothing failed — pass (vacuously when no rule ran for this trigger).
137
+ return True
138
+
139
+ # Optional: ergonomic callback registration
140
+ def on_invalid(self, func: Callable) -> None:
141
+ """Register callback for `<<Invalid>>`. Callback receives a `ValidationEvent` payload (value, is_valid, message)."""
142
+ self._on_invalid_command = func
143
+
144
+ def off_invalid(self, bind_id: str | None = None) -> None:
145
+ """Unsubscribe from `<<Invalid>>` events.
146
+
147
+ Args:
148
+ bind_id: ID returned by `on_invalid()`. If None, removes all bindings.
149
+ """
150
+ self.unbind('<<Invalid>>', bind_id)
151
+
152
+ def on_valid(self, func: Callable) -> None:
153
+ """Register callback for `<<Valid>>`. Callback receives a `ValidationEvent` payload (value, is_valid, message)."""
154
+ self._on_valid_command = func
155
+
156
+ def off_valid(self, bind_id: str | None = None) -> None:
157
+ """Unsubscribe from `<<Valid>>` events.
158
+
159
+ Args:
160
+ bind_id: ID returned by `on_valid()`. If None, removes all bindings.
161
+ """
162
+ self.unbind('<<Valid>>', bind_id)
163
+
164
+ def on_validated(self, func: Callable) -> None:
165
+ """Register callback for `<<Validate>>`. Callback receives a `ValidationEvent` payload (value, is_valid, message)."""
166
+ self._on_validated_command = func
167
+
168
+ def off_validated(self, bind_id: str | None = None) -> None:
169
+ """Unsubscribe from `<<Validate>>` events.
170
+
171
+ Args:
172
+ bind_id: ID returned by `on_validated()`. If None, removes all bindings.
173
+ """
174
+ self.unbind('<<Validate>>', bind_id)
175
+
176
+ # ---------------- Internals ----------------
177
+
178
+ def _get_validation_value(self) -> Any:
179
+ """Get the current value for validation purposes.
180
+
181
+ For entry widgets, this should return the current text being typed,
182
+ not the committed value. Prefers get() method over value() method.
183
+ """
184
+ if hasattr(self, 'get'):
185
+ try:
186
+ return self.get()
187
+ except (AttributeError, TclError):
188
+ pass
189
+ return self.value()
190
+
191
+ def _setup_validation_binds(self, keyup_delay_ms: int = 50, blur_delay_ms: int = 50) -> None:
192
+ """Set up automatic validation bindings with debouncing."""
193
+ # Auto-validate (debounced)
194
+ self.bind(self.SEQ_KEYUP, lambda e: self._debounced("key", keyup_delay_ms), add=True)
195
+ self.bind(self.SEQ_BLUR, lambda e: self._debounced("blur", blur_delay_ms), add=True)
196
+
197
+ # Wire optional convenience callbacks
198
+ self.bind(self.EVENT_VALIDATED, self._dispatch_validated, add=True)
199
+ self.bind(self.EVENT_VALID, self._dispatch_valid, add=True)
200
+ self.bind(self.EVENT_INVALID, self._dispatch_invalid, add=True)
201
+
202
+ def _debounced(self, trigger: RuleTriggerType, ms: int) -> None:
203
+ """Debounce validation to avoid excessive checks during typing."""
204
+ key = f"debounce:{trigger}"
205
+ aid: str | None = self._debounce_ids.get(key)
206
+ if aid:
207
+ try:
208
+ self.after_cancel(aid)
209
+ except TclError:
210
+ pass
211
+ # defer reading value until the timer fires
212
+ self._debounce_ids[key] = self.after(ms, lambda: self.validate(self._get_validation_value(), trigger))
213
+
214
+ # ----- optional dispatchers for on_* convenience -----
215
+
216
+ def _dispatch_validated(self, event: tkinter.Event) -> None:
217
+ if self._on_validated_command:
218
+ self._on_validated_command(event)
219
+
220
+ def _dispatch_valid(self, event: tkinter.Event) -> None:
221
+ if self._on_valid_command:
222
+ self._on_valid_command(event)
223
+
224
+ def _dispatch_invalid(self, event: tkinter.Event) -> None:
225
+ if self._on_invalid_command:
226
+ self._on_invalid_command(event)
@@ -0,0 +1,49 @@
1
+ """Primitive bootstack widgets."""
2
+
3
+ from .badge import Badge
4
+ from .button import Button
5
+ from .card import Card
6
+ from .checkbutton import CheckButton
7
+ from .checktoggle import CheckToggle
8
+ from .combobox import Combobox
9
+ from .entry import Entry
10
+ from .frame import Frame
11
+ from .gridframe import GridFrame
12
+ from .label import Label
13
+ from .labelframe import LabelFrame
14
+ from .packframe import PackFrame
15
+ from .panedwindow import PanedWindow
16
+ from .progressbar import Progressbar
17
+ from .radiobutton import RadioButton
18
+ from .radiotoggle import RadioToggle
19
+ from .scrollbar import Scrollbar
20
+ from .separator import Separator
21
+ from .sizegrip import SizeGrip
22
+ from .spinbox import Spinbox
23
+ from .switch import Switch
24
+ from .treeview import TreeView
25
+
26
+ __all__ = [
27
+ "Badge",
28
+ "Button",
29
+ "Card",
30
+ "CheckButton",
31
+ "CheckToggle",
32
+ "Combobox",
33
+ "Entry",
34
+ "Frame",
35
+ "GridFrame",
36
+ "Label",
37
+ "LabelFrame",
38
+ "PackFrame",
39
+ "PanedWindow",
40
+ "Progressbar",
41
+ "RadioButton",
42
+ "RadioToggle",
43
+ "Scrollbar",
44
+ "Separator",
45
+ "SizeGrip",
46
+ "Spinbox",
47
+ "Switch",
48
+ "TreeView",
49
+ ]
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from tkinter import ttk
4
+ from typing import Any, Literal, TYPE_CHECKING
5
+
6
+ from typing_extensions import Unpack
7
+
8
+ from bootstack._core.mixins.ttk_state import TtkStateMixin
9
+ from bootstack._core.mixins.widget import WidgetCapabilitiesMixin
10
+ from bootstack.widgets._impl._internal.wrapper_base import TTKWrapperBase
11
+ from bootstack.widgets._impl.mixins import IconMixin, LocalizationMixin, TextSignalMixin
12
+ from bootstack.widgets.types import Master, StyledKwargs, CompoundMode, WidgetState, WidgetDensity
13
+
14
+ if TYPE_CHECKING:
15
+ from bootstack.signals import Signal
16
+
17
+
18
+ class MenuButtonKwargs(StyledKwargs, total=False):
19
+ # Standard ttk.Menubutton options
20
+ text: Any
21
+ image: Any
22
+ icon: Any
23
+ icon_only: bool
24
+ compound: CompoundMode
25
+ direction: Any
26
+ menu: Any
27
+ padding: Any
28
+ state: WidgetState
29
+ textvariable: Any
30
+ textsignal: Signal[Any]
31
+
32
+ # bootstack-specific extensions
33
+ density: WidgetDensity
34
+ localize: bool | Literal['auto']
35
+
36
+
37
+ class MenuButton(LocalizationMixin, TextSignalMixin, IconMixin, TTKWrapperBase, WidgetCapabilitiesMixin, TtkStateMixin, ttk.Menubutton):
38
+ """bootstack wrapper for `ttk.Menubutton` with themed styling and icon support."""
39
+
40
+ _ttk_base = ttk.Menubutton
41
+
42
+ def __init__(self, master: Master = None, **kwargs: Unpack[MenuButtonKwargs]) -> None:
43
+ """Create a themed bootstack Menubutton.
44
+
45
+ Args:
46
+ master: Parent widget. If None, uses the default root window.
47
+
48
+ Other Parameters:
49
+ text: Text to display in the menubutton.
50
+ image: Image to display.
51
+ icon: Theme-aware icon spec handled by the style system.
52
+ icon_only: If True, removes the additional padding reserved for label text.
53
+ compound: Placement of the image relative to text.
54
+ direction: Direction for the menu to appear.
55
+ menu: Associated tk.Menu instance.
56
+ padding: Extra space around the content.
57
+ density: Widget density — 'default' or 'compact'.
58
+ state: Widget state — 'normal', 'active', 'disabled', or 'readonly'.
59
+ takefocus: Whether the widget participates in focus traversal.
60
+ textvariable: Tk variable linked to the text.
61
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
62
+ textsignal: Reactive Signal linked to the text (auto-synced with textvariable).
63
+ style: Explicit ttk style name (overrides accent/variant).
64
+ accent: Accent token for styling, e.g. 'primary', 'danger', 'success'.
65
+ variant: Style variant, e.g. 'solid', 'outline', 'ghost'.
66
+ surface: Optional surface token; otherwise inherited.
67
+ style_options: Optional dict forwarded to the style builder.
68
+ localize: Determines the widget's localization mode.
69
+ """
70
+ kwargs.update(style_options=self._capture_style_options(['icon_only', 'icon', 'density'], kwargs))
71
+ super().__init__(master, **kwargs)
72
+
73
+ # When a native tk.Menu is attached, post it ourselves with the
74
+ # focus-ring affordance offset so it aligns with the visible button
75
+ # border. Subclasses (OptionMenu, DropdownButton) use a ContextMenu
76
+ # and override show_menu, so this only affects the native path.
77
+ # On Aqua, Tk's native Menubutton class binding drives an NSMenu
78
+ # state machine that manages mouse/focus capture; bypassing it with
79
+ # a manual `menu post` strands focus after item selection, so we
80
+ # skip the offset path on Mac and let the default class binding run.
81
+ if self.tk.call('tk', 'windowingsystem') != 'aqua':
82
+ self.bind("<Button-1>", self._on_button_press, add="+")
83
+
84
+ def _on_button_press(self, _event):
85
+ """Focus self, and if a native menu is attached, post it with offset."""
86
+ self.focus_set()
87
+ try:
88
+ menu_path = self.cget('menu')
89
+ except Exception:
90
+ menu_path = None
91
+ if not menu_path:
92
+ return None
93
+ try:
94
+ # If the menu is already visible, unpost so a second click closes it.
95
+ if int(self.tk.eval(f"winfo viewable {menu_path}")):
96
+ self.tk.call(menu_path, 'unpost')
97
+ return 'break'
98
+ from bootstack.style.style_builder_base import StyleBuilderBase
99
+ offset = StyleBuilderBase.scale_from_source(10)
100
+ x = self.winfo_rootx() + offset
101
+ y = self.winfo_rooty() + self.winfo_height()
102
+ self.tk.call(menu_path, 'post', x, y)
103
+ except Exception:
104
+ return None
105
+ # Suppress Tk's default class binding so it doesn't also post the
106
+ # menu at its un-offset position.
107
+ return 'break'