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,670 @@
1
+ """Field widget module.
2
+
3
+ Provides a flexible generic entry field composite widget used as the foundation
4
+ for creating specialized entry widgets like TextEntry, PasswordEntry, NumberEntry, etc.
5
+ """
6
+
7
+ from tkinter import TclError, Variable
8
+ from typing import Any, Callable, Literal, Type, TypedDict, cast
9
+
10
+ from bootstack.signals import Signal
11
+ from bootstack.validation.types import RuleType
12
+ from bootstack.widgets._impl.primitives.button import Button
13
+ from bootstack.widgets._impl.primitives.frame import Frame
14
+ from bootstack.widgets._impl.primitives.label import Label
15
+ from bootstack.widgets._impl.primitives.checkbutton import CheckButton
16
+ from bootstack.widgets._impl.primitives.checktoggle import CheckToggle
17
+ from bootstack.widgets._impl.mixins import configure_delegate
18
+ from bootstack.widgets._impl.mixins.entry_mixin import EntryMixin
19
+ from bootstack._runtime.utility import scale_padding_floor
20
+ from bootstack.widgets._impl._parts.numberentry_part import NumberEntryPart
21
+ from bootstack.widgets._impl._parts.textentry_part import TextEntryPart
22
+ from bootstack.widgets._impl._parts.spinnerentry_part import SpinnerEntryPart
23
+ from bootstack.widgets.types import Master, AccentToken, Justify, WidgetDensity, WidgetState
24
+
25
+ FieldKind = Literal['text', 'numeric', 'spinbox']
26
+
27
+ EntryWidget = TextEntryPart | NumberEntryPart | SpinnerEntryPart
28
+ """The internal entry widget used by a Field — one of the three entry part types."""
29
+
30
+ FieldAddonWidget = Button | Label | CheckToggle
31
+ """Widget types supported by `Field.insert_addon`."""
32
+ """Type alias for field kind specification.
33
+
34
+ Determines which entry part widget to use:
35
+ - 'text': Uses TextEntryPart for text input with formatting support
36
+ - 'numeric': Uses NumberEntryPart for numeric input with bounds and stepping
37
+ - 'spinbox': Uses SpinnerEntryPart for spinner input (supports text or numeric values)
38
+ """
39
+
40
+
41
+ class FieldOptions(TypedDict, total=False):
42
+ """Type hints for Field widget configuration options.
43
+
44
+ Attributes:
45
+ allow_blank: If True, empty input is allowed. If False, empty input preserves previous value.
46
+ accent: Accent token for the focus ring and active border of the input.
47
+ density: Widget density. 'default' for normal size, 'compact' for smaller size.
48
+ variant: Style variant (if applicable).
49
+ cursor: Cursor to display when hovering over the widget.
50
+ value_format: ICU format pattern for parsing/formatting (e.g., '$#,##0.00' for currency).
51
+ exportselection: If True, selected text is exported to X selection.
52
+ font: Font to use for text display.
53
+ foreground: Text color.
54
+ initial_focus: If True, widget receives focus when created.
55
+ justify: Text justification ('left', 'center', 'right').
56
+ show_message: If True, displays message text below the field.
57
+ padding: Padding around the entry widget.
58
+ show: Character to display instead of typed characters (for password fields).
59
+ state: The widget state. One of 'normal', 'disabled', or 'readonly'.
60
+ takefocus: If True, widget can receive focus via Tab key.
61
+ textvariable: Tkinter Variable to link with the entry text.
62
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
63
+ textsignal: Signal object for reactive text updates.
64
+ width: Width of the entry in characters.
65
+ required: If True, field cannot be empty (adds validation rule).
66
+ xscrollcommand: Callback for horizontal scrolling.
67
+ localize: Determines the field label localization mode. 'auto', True, False.
68
+ """
69
+ allow_blank: bool
70
+ accent: AccentToken | str
71
+ density: WidgetDensity
72
+ variant: str
73
+ cursor: str
74
+ value_format: str
75
+ exportselection: bool
76
+ font: str
77
+ foreground: str
78
+ initial_focus: bool
79
+ justify: Justify
80
+ show_message: bool
81
+ padding: str
82
+ show: str
83
+ state: WidgetState
84
+ takefocus: bool
85
+ textvariable: Variable
86
+ textsignal: Signal
87
+ width: int
88
+ required: bool
89
+ placeholder: str
90
+ xscrollcommand: Callable[[int, int], None]
91
+ localize: bool | Literal['auto']
92
+
93
+
94
+ class Field(EntryMixin, Frame):
95
+ """A flexible generic composite entry field widget.
96
+
97
+ Field is a base composite widget that combines a label, entry input, and
98
+ message area into a complete input field component. It serves as the foundation
99
+ for creating specialized entry widgets like TextEntry, PasswordEntry, NumberEntry,
100
+ and other custom entry types.
101
+
102
+ The widget automatically handles layout, focus states, validation feedback, and
103
+ provides a consistent API for all entry-based components. It supports both text
104
+ and numeric input types through the `kind` parameter.
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ master: Master = None,
110
+ *,
111
+ value: str | int | float = None,
112
+ label: str = None,
113
+ message: str = None,
114
+ show_message: bool = False,
115
+ required: bool = False,
116
+ kind: FieldKind = "text",
117
+ **kwargs: Any
118
+ ):
119
+ """Args:
120
+ master: Parent widget. If None, uses the default root window.
121
+ value: Initial value to display. Can be str, int, or float depending
122
+ on the field kind. For 'text' kind, should be a string. For
123
+ 'numeric' kind, can be int or float. Default is None.
124
+ label: Optional label text to display above the entry field.
125
+ If required=True, an asterisk (*) is automatically appended to
126
+ indicate the field is mandatory.
127
+ message: Optional message text to display below the entry field.
128
+ Used for hints, instructions, or help text. This text is replaced
129
+ by validation error messages when validation fails, and restored
130
+ when validation passes. Default is None (no message).
131
+ show_message: If True, displays the message area below the field.
132
+ If False, hides the message area entirely (validation errors
133
+ won't be shown). Defaults to False, but auto-enables when
134
+ `message=` or `required=True` is set.
135
+ required: If True, marks the field as required and automatically adds
136
+ a 'required' validation rule. An asterisk (*) is appended to the
137
+ label. The field cannot be left empty. Default is False.
138
+ kind: Type of entry field to create. Either 'text' for text input
139
+ (uses TextEntryPart) or 'numeric' for numeric input (uses
140
+ NumberEntryPart). Default is 'text'.
141
+
142
+ Other Parameters:
143
+ accent: Accent token for the focus ring and active border.
144
+ density: Widget density — 'default' or 'compact'.
145
+ state: Initial widget state — 'normal', 'disabled', or 'readonly'.
146
+ value_format: ICU format pattern for parsing/formatting.
147
+ allow_blank: Allow empty input.
148
+ initial_focus: Focus on creation.
149
+ show: Character to mask input (e.g., '*' for passwords).
150
+ width: Width in characters.
151
+ font: Font specification.
152
+ justify: Text alignment ('left', 'center', 'right').
153
+ textvariable: Tkinter Variable linked to entry text.
154
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
155
+ textsignal: Signal for reactive text updates.
156
+ minvalue: Minimum allowed value (numeric kind only).
157
+ maxvalue: Maximum allowed value (numeric kind only).
158
+ increment: Step size for up/down arrows (numeric kind only).
159
+ wrap: Wrap around at boundaries (numeric kind only).
160
+ """
161
+ # Accept legacy parameter name and prevent it from reaching the Tk widget.
162
+ if 'show_messages' in kwargs:
163
+ show_message = kwargs.pop('show_messages')
164
+ # Track if user explicitly provided show_message
165
+ show_message_explicit = 'show_message' in kwargs
166
+ show_message = kwargs.pop('show_message', show_message)
167
+
168
+ # Auto-enable show_message if there is any chance a message will be shown
169
+ if (message or required) and not show_message_explicit:
170
+ show_message = True
171
+
172
+ accent = kwargs.pop('accent', None)
173
+ self._density = kwargs.pop('density', 'default')
174
+ self._localize = cast(bool | Literal['auto'], kwargs.pop('localize', 'auto'))
175
+ placeholder = kwargs.pop('placeholder', None)
176
+
177
+ # Field itself (outer Frame) doesn't need styling - only pass master
178
+ super().__init__(master)
179
+
180
+ # Set accent AFTER super().__init__ to avoid being overwritten by wrapper
181
+ self._accent = accent
182
+
183
+ # configuration
184
+ self._message_text = message
185
+ self._show_messages = show_message
186
+ self._addons: dict[str, Button | Label | CheckToggle] = {}
187
+ self._required = required
188
+ self._kind = kind
189
+ self._label_text = label
190
+ self._value = value
191
+
192
+ self._entry: EntryWidget
193
+ self._addons: dict[str, Button | Label | CheckToggle] = {}
194
+ # Addon keys that stay interactive while the field is read-only (opt-in
195
+ # via insert_addon(active_when_readonly=True)) — for read-only-safe
196
+ # actions like copy or reveal. They still dim when the field is disabled.
197
+ self._readonly_active: set[str] = set()
198
+
199
+ # layout
200
+ label_text = self._label_text or ''
201
+ self._label_lbl = Label(
202
+ self,
203
+ localize=self._localize,
204
+ text=f"{label_text}*" if required else label_text,
205
+ font="label[normal]"
206
+ )
207
+ self._message_lbl = Label(self, localize=self._localize, text=message or '', font="caption", accent="secondary")
208
+
209
+ # The TField frame draws the visible border via a nine-patch whose border
210
+ # slice scales with DPI; this gap must grow with it, or at high DPI the
211
+ # entry overpaints the border slice and the resting border vanishes (#90).
212
+ field_padding = scale_padding_floor(5)
213
+ self._field = Frame(self, accent=self._accent, padding=field_padding, ttk_class="TField", style_options={'density': self._density})
214
+
215
+ if kind == "numeric":
216
+ self._entry = NumberEntryPart(self._field, value=value, density=self._density, **kwargs)
217
+ elif kind == "spinbox":
218
+ self._entry = SpinnerEntryPart(self._field, value=value, density=self._density, **kwargs)
219
+ else:
220
+ self._entry = TextEntryPart(self._field, value=value, density=self._density, placeholder=placeholder, **kwargs)
221
+
222
+ # attach widgets
223
+ if label:
224
+ self._label_lbl.pack(side='top', fill='x', padx=(4, 0))
225
+
226
+ self._field.pack(side='top', fill='x', expand=True)
227
+ self._entry.pack(side='left', fill='x', expand=True, padx=(0, 6) if kind == "spinbox" else 0, pady=0)
228
+
229
+ self._entry.bind('<<StateChanged>>', self._sync_addon_state, add=True)
230
+ self._sync_addon_state()
231
+
232
+ if self._show_messages:
233
+ self._message_lbl.pack(side='top', fill='x', padx=4)
234
+
235
+ # The message label follows the entry's reactive error signal: a
236
+ # non-empty error shows in danger, an empty one restores the static
237
+ # helper message.
238
+ self._entry._error_signal.subscribe(self._on_validation_message)
239
+
240
+ # bind focus styling to the field frame
241
+ self._entry.bind('<FocusIn>', lambda _: self._field.state(['focus']), add=True)
242
+ self._entry.bind('<FocusOut>', lambda _: self._field.state(['!focus']), add=True)
243
+
244
+ # add required validation
245
+ if required:
246
+ self._entry.add_validation_rule("required")
247
+
248
+
249
+ self.validation = self._entry.validate
250
+
251
+ # Copy Field's delegate handlers to entry for configuration forwarding
252
+ for key, method_name in self._configure_delegate_map.items():
253
+ if hasattr(self, method_name):
254
+ # Attach the Field's handler method to the entry instance
255
+ setattr(self._entry, method_name, getattr(self, method_name))
256
+ # Add to entry's delegate map
257
+ self._entry._configure_delegate_map[key] = method_name
258
+
259
+ # Forward configuration methods to entry widget
260
+ self.configure = self._entry.configure
261
+ self.config = self._entry.config
262
+ self.cget = self._entry.cget
263
+ self.__getitem__ = self._entry.__getitem__
264
+ self.__setitem__ = self._entry.__setitem__
265
+
266
+ @property
267
+ def value(self) -> Any:
268
+ """Get or set the parsed value via the underlying entry widget."""
269
+ return self._entry.value()
270
+
271
+ @value.setter
272
+ def value(self, value: Any) -> None:
273
+ self._entry.value(value)
274
+
275
+ def get(self) -> str:
276
+ """Return the raw text from the underlying entry widget."""
277
+ return self._entry.get()
278
+
279
+ @property
280
+ def entry_widget(self) -> EntryWidget:
281
+ """Get the underlying entry widget."""
282
+ return self._entry
283
+
284
+ @property
285
+ def label_widget(self) -> Label:
286
+ """Get the label widget."""
287
+ return self._label_lbl
288
+
289
+ @property
290
+ def message_widget(self) -> Label:
291
+ """Get the message widget."""
292
+ return self._message_lbl
293
+
294
+ @property
295
+ def addons(self) -> dict[str, FieldAddonWidget]:
296
+ """Get the dictionary of inserted addon widgets."""
297
+ return self._addons
298
+
299
+ @property
300
+ def variable(self) -> Variable:
301
+ """The `StringVar` linked to the entry text.
302
+
303
+ Use when bridging to a plain tkinter widget that requires `textvariable=`,
304
+ or for direct read/write access to the raw string value. For reactive
305
+ bindings between bootstack widgets, prefer `signal` instead.
306
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
307
+ """
308
+ return self._entry.textvariable
309
+
310
+ @property
311
+ def signal(self) -> Signal:
312
+ """The `Signal[str]` linked to the entry text.
313
+
314
+ Pass as `textsignal=` to any other bootstack widget to keep its text
315
+ in sync with this field's value, or call `signal.subscribe(callback)`
316
+ to observe changes programmatically. See the
317
+ [Reactivity guide](../../guides/reactivity.md) for patterns.
318
+ """
319
+ return self._entry.textsignal
320
+
321
+ @configure_delegate
322
+ def _config_accent(self, value=None):
323
+ if value is None:
324
+ return self._accent
325
+ else:
326
+ self._accent = value
327
+ self._field['accent'] = value
328
+ return None
329
+
330
+
331
+ # ------ Event registration ------
332
+
333
+ def on_input(self, callback: Callable) -> str:
334
+ """Register a callback for `<<Input>>` events (fires on each keystroke).
335
+
336
+ Args:
337
+ callback: Receives a Tkinter `Event` object whose `event.data` is an
338
+ `InputEvent` payload with `text` (current raw text).
339
+
340
+ Returns:
341
+ Bind ID — pass to `off_input()` to unsubscribe.
342
+ """
343
+ return self._entry.on_input(callback)
344
+
345
+ def off_input(self, bind_id: str | None = None) -> None:
346
+ """Unsubscribe from `<<Input>>`.
347
+
348
+ Args:
349
+ bind_id: ID returned by `on_input()`.
350
+ """
351
+ self._entry.off_input(bind_id)
352
+
353
+ def on_changed(self, callback: Callable) -> str:
354
+ """Register a callback for `<<Change>>` events (fires on commit).
355
+
356
+ Args:
357
+ callback: Receives a Tkinter `Event` object whose `event.data` is a
358
+ `ChangeEvent` payload with `value` (committed value),
359
+ `prev_value` (previous value), and `text` (raw display string).
360
+
361
+ Returns:
362
+ Bind ID — pass to `off_changed()` to unsubscribe.
363
+ """
364
+ return self._entry.on_changed(callback)
365
+
366
+ def off_changed(self, bind_id: str | None = None) -> None:
367
+ """Unsubscribe from `<<Change>>`.
368
+
369
+ Args:
370
+ bind_id: ID returned by `on_changed()`.
371
+ """
372
+ self._entry.off_changed(bind_id)
373
+
374
+ def on_enter(self, callback: Callable) -> str:
375
+ """Register a callback for `<Return>` key events.
376
+
377
+ Args:
378
+ callback: Receives a Tkinter `Event` object whose `event.data` is an
379
+ the curated `Event` for the Return key press.
380
+
381
+ Returns:
382
+ Bind ID — pass to `off_enter()` to unsubscribe.
383
+ """
384
+ return self._entry.on_enter(callback)
385
+
386
+ def off_enter(self, bind_id: str | None = None) -> None:
387
+ """Unsubscribe from `<Return>`.
388
+
389
+ Args:
390
+ bind_id: ID returned by `on_enter()`.
391
+ """
392
+ self._entry.off_enter(bind_id)
393
+
394
+ def on_valid(self, callback: Callable) -> None:
395
+ """Register a callback for `<<Valid>>` events (fires when validation passes).
396
+
397
+ Args:
398
+ callback: Receives the event; `event.data` is a `ValidationEvent`
399
+ payload with `value`, `is_valid` (`True`), and `message`.
400
+ """
401
+ self._entry.on_valid(callback)
402
+
403
+ def off_valid(self, bind_id: str | None = None) -> None:
404
+ """Unsubscribe from `<<Valid>>`.
405
+
406
+ Args:
407
+ bind_id: Bind ID from a direct `widget.bind()` call, or `None`
408
+ to remove all `<<Valid>>` bindings.
409
+ """
410
+ self._entry.off_valid(bind_id)
411
+
412
+ def on_invalid(self, callback: Callable) -> None:
413
+ """Register a callback for `<<Invalid>>` events (fires when validation fails).
414
+
415
+ Args:
416
+ callback: Receives the event; `event.data` is a `ValidationEvent`
417
+ payload with `value`, `is_valid` (`False`), and `message`.
418
+ """
419
+ self._entry.on_invalid(callback)
420
+
421
+ def off_invalid(self, bind_id: str | None = None) -> None:
422
+ """Unsubscribe from `<<Invalid>>`.
423
+
424
+ Args:
425
+ bind_id: Bind ID from a direct `widget.bind()` call, or `None`
426
+ to remove all `<<Invalid>>` bindings.
427
+ """
428
+ self._entry.off_invalid(bind_id)
429
+
430
+ def on_validated(self, callback: Callable) -> None:
431
+ """Register a callback for `<<Validate>>` events (fires after any validation).
432
+
433
+ Args:
434
+ callback: Receives the event; `event.data` is a `ValidationEvent`
435
+ payload with `value`, `is_valid` (bool), and `message`.
436
+ """
437
+ self._entry.on_validated(callback)
438
+
439
+ def off_validated(self, bind_id: str | None = None) -> None:
440
+ """Unsubscribe from `<<Validate>>`.
441
+
442
+ Args:
443
+ bind_id: Bind ID from a direct `widget.bind()` call, or `None`
444
+ to remove all `<<Validate>>` bindings.
445
+ """
446
+ self._entry.off_validated(bind_id)
447
+
448
+ # ------ Validation ------
449
+
450
+ def add_validation_rule(
451
+ self,
452
+ rule_type: RuleType,
453
+ **kwargs,
454
+ ) -> None:
455
+ """Add a validation rule to the field.
456
+
457
+ Rules are evaluated on blur and on Enter. When a rule fails the field
458
+ emits `<<Invalid>>`; when all rules pass it emits `<<Valid>>`. Both
459
+ carry a `ValidationEvent` payload.
460
+
461
+ Args:
462
+ rule_type: Rule type. One of:
463
+
464
+ - `"required"` — field must not be empty.
465
+ - `"email"` — value must be a valid email address.
466
+ - `"pattern"` — value must match a regex. Pass `pattern=`.
467
+ - `"stringLength"` — length bounds. Pass `min=` and/or `max=`.
468
+ - `"compare"` — must match another field's value. Pass `other_field=`.
469
+ - `"custom"` — arbitrary logic. Pass `func=`, a callable that
470
+ receives the value and returns `bool` or `(bool, message)`.
471
+
472
+ **kwargs: Rule-specific options. `message=` is accepted by all rule
473
+ types to override the default failure message.
474
+ """
475
+ self._reserve_message_space()
476
+ self._entry.add_validation_rule(rule_type, **kwargs)
477
+
478
+ def disable(self):
479
+ """Disable the field, preventing user input."""
480
+ self._entry.state(['disabled !readonly'])
481
+ self._field.state(['disabled'])
482
+ self._set_addons_state(True)
483
+
484
+ def enable(self):
485
+ """Enable the field, allowing user input."""
486
+ self._entry.state(['!disabled !readonly'])
487
+ self._field.state(['!disabled'])
488
+ self._set_addons_state(False)
489
+
490
+ def readonly(self, value: bool = None):
491
+ """Set or toggle the readonly state of the field."""
492
+ if value == False:
493
+ self._field.state(['disabled'])
494
+ self._entry.state(['readonly'])
495
+ elif value:
496
+ self._field.state(['!disabled'])
497
+ self._entry.state(['readonly'])
498
+ else:
499
+ self._entry.state(['readonly !disabled'])
500
+ self._field.state(['disabled'])
501
+ self._sync_addon_state()
502
+
503
+ @configure_delegate('state')
504
+ def _delegate_state(self, value=None):
505
+ if value is None:
506
+ states = self._entry.state()
507
+ if 'disabled' in states:
508
+ return 'disabled'
509
+ if 'readonly' in states:
510
+ return 'readonly'
511
+ return 'normal'
512
+ if value == 'disabled':
513
+ self._entry.state(['disabled', '!readonly'])
514
+ self._field.state(['disabled'])
515
+ elif value == 'readonly':
516
+ self._entry.state(['readonly', '!disabled'])
517
+ self._field.state(['!disabled'])
518
+ else:
519
+ self._entry.state(['!disabled', '!readonly'])
520
+ self._field.state(['!disabled'])
521
+ self._sync_addon_state()
522
+ self._entry.event_generate('<<StateChanged>>')
523
+ return None
524
+
525
+ def insert_addon(
526
+ self,
527
+ widget: Type[FieldAddonWidget],
528
+ position: Literal['before', 'after'],
529
+ name: str | None = None,
530
+ accent: str | None = None,
531
+ pack_options: dict[str, Any] = None,
532
+ active_when_readonly: bool = False,
533
+ **kwargs: Any
534
+ ) -> FieldAddonWidget:
535
+ """Insert a widget addon before or after the entry input.
536
+
537
+ Addons are Button, Label, or CheckToggle widgets positioned inside the field container,
538
+ either before (left of) or after (right of) the entry input. Common use
539
+ cases include search buttons, icons, clear buttons, or status indicators.
540
+
541
+ The addon widget automatically:
542
+ - Inherits the field's disabled state
543
+ - Participates in focus state styling (highlights field on addon focus)
544
+ - Is stored in the addons dictionary for later reference
545
+
546
+ Args:
547
+ widget: Widget class to instantiate. Must be Button, Label, or CheckToggle.
548
+ position: Position relative to the entry input:
549
+
550
+ - 'before': Insert to the left of the entry (prefix)
551
+ - 'after': Insert to the right of the entry (suffix)
552
+ name: Optional name for the addon. If provided, the addon can be
553
+ retrieved from the addons dictionary using this name. If None,
554
+ the widget's string representation is used as the key.
555
+ accent: Optional accent color for the addon. Default to surface color. Prefer to use an accent if using
556
+ a text-only button.
557
+ pack_options: Optional dictionary of additional pack() options to apply when placing the addon widget.
558
+ Common options include padx, pady, etc. The side and after/before options are set automatically based
559
+ on position.
560
+ active_when_readonly: If True, the addon stays interactive while the
561
+ field is read-only (for read-only-safe actions like copy or
562
+ reveal). It still dims when the field is fully disabled. Default
563
+ False — the addon follows the field's read-only state.
564
+ **kwargs: Additional keyword arguments passed to the widget constructor.
565
+ For Button: text, command, icon, accent, variant, etc.
566
+ For Label: text, icon, image, accent, etc.
567
+ For CheckToggle: text, icon, signal, command, accent, etc.
568
+ Note: variant and takefocus are set automatically but can be
569
+ overridden.
570
+ """
571
+ variant = "suffix" if position == "after" else "prefix"
572
+ kwargs.setdefault('ttk_class', 'TField')
573
+ kwargs.setdefault('variant', variant)
574
+ kwargs.setdefault('takefocus', False)
575
+ kwargs.setdefault('density', self._density)
576
+ kwargs.setdefault('anchor', 'center')
577
+ if 'icon' in kwargs and 'text' not in kwargs:
578
+ kwargs.setdefault('compound', 'image')
579
+ kwargs.setdefault('icon_only', True)
580
+
581
+ if issubclass(widget, (Button, CheckToggle)):
582
+ icon_only = kwargs.get('icon_only', False)
583
+ if 'style_options' in kwargs:
584
+ kwargs['style_options'].update(use_active_states=True, density=self._density, icon_only=icon_only)
585
+ else:
586
+ kwargs['style_options'] = dict(use_active_states=True, density=self._density, icon_only=icon_only)
587
+ instance = widget(master=self._field, accent=accent, **kwargs)
588
+ key = name or str(instance)
589
+ self._addons[key] = instance
590
+ if active_when_readonly:
591
+ self._readonly_active.add(key)
592
+
593
+ # configure layout
594
+ options = pack_options or {}
595
+ if position == "after":
596
+ options.update(side="right", after=self._entry)
597
+ else:
598
+ options.update(side="left", before=self._entry)
599
+ instance.pack(**options)
600
+
601
+ # match parent disabled state
602
+ self._sync_addon_state()
603
+
604
+ # bind focus events to field frame
605
+ instance.bind('<FocusIn>', lambda _: self._field.state(['focus']), add=True)
606
+ instance.bind('<FocusOut>', lambda _: self._field.state(['!focus']), add=True)
607
+
608
+ return instance
609
+
610
+ def remove_addon(self, key: str) -> None:
611
+ """Remove a previously-inserted addon by its key.
612
+
613
+ Args:
614
+ key: The addon's name (as passed to or returned by `insert_addon`).
615
+
616
+ Raises:
617
+ KeyError: If no addon with that key exists.
618
+ """
619
+ instance = self._addons.pop(key)
620
+ self._readonly_active.discard(key)
621
+ instance.destroy()
622
+
623
+ def _reserve_message_space(self) -> None:
624
+ if not self._show_messages:
625
+ self._show_messages = True
626
+ self._message_lbl.pack(side='top', fill='x', padx=4)
627
+
628
+ def _on_validation_message(self, message: str) -> None:
629
+ """React to the entry's error signal — show the error or restore helper text.
630
+
631
+ Args:
632
+ message: The current validation error, or an empty string when the
633
+ field is valid.
634
+ """
635
+ if message:
636
+ self._message_lbl['text'] = message
637
+ self._message_lbl['accent'] = "danger"
638
+ self._message_lbl.pack(side='top', after=self._field, padx=4)
639
+ else:
640
+ self._message_lbl['text'] = self._message_text or ''
641
+ self._message_lbl['accent'] = "secondary"
642
+
643
+ def _set_addons_state(self, disabled: bool) -> None:
644
+ """Configure addon widgets based on whether the entry is interactive."""
645
+ state_value = 'disabled' if disabled else '!disabled'
646
+ for item in self._addons.values():
647
+ try:
648
+ item.configure(state=state_value)
649
+ except TclError:
650
+ pass
651
+
652
+ def _sync_addon_state(self, event: Any = None) -> None:
653
+ """Match each addon to the field's interactivity state.
654
+
655
+ A `disabled` field dims every addon. A `readonly` field dims its addons
656
+ too — most act on the value (clear, the spin buttons), so leaving them
657
+ live would let the user mutate a field that is supposed to be
658
+ uneditable. Addons inserted with `active_when_readonly=True` (read-only
659
+ safe actions, e.g. copy or reveal) are the exception: they stay live
660
+ while read-only, but still dim when the field is fully disabled.
661
+ """
662
+ states = self._entry.state()
663
+ fully_disabled = 'disabled' in states
664
+ readonly = 'readonly' in states
665
+ for key, item in self._addons.items():
666
+ off = fully_disabled or (readonly and key not in self._readonly_active)
667
+ try:
668
+ item.configure(state='disabled' if off else '!disabled')
669
+ except TclError:
670
+ pass