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,1066 @@
1
+ """Dynamic form widget for building data entry layouts quickly."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from datetime import date, datetime
7
+ from tkinter import BooleanVar, DoubleVar, IntVar, StringVar, Text, Variable
8
+ from typing import Any, Callable, Iterable, Literal, Mapping, Sequence, TYPE_CHECKING
9
+
10
+ from bootstack.constants import DEFAULT_MIN_COL_WIDTH
11
+ from bootstack.signals import Signal
12
+ from bootstack.widgets._impl.primitives.button import Button
13
+ from bootstack.widgets._impl.primitives.checkbutton import CheckButton
14
+ from bootstack.widgets._impl.primitives.switch import Switch
15
+ from bootstack.widgets._impl.composites.dateentry import DateEntry
16
+ from bootstack.widgets._impl.composites.field import Field
17
+ from bootstack.widgets._impl.primitives.frame import Frame
18
+ from bootstack.widgets._impl.primitives.label import Label
19
+ from bootstack.widgets._impl.primitives.labelframe import LabelFrame
20
+ from bootstack.widgets._impl.mixins import configure_delegate
21
+ from bootstack.widgets._impl.composites.numericentry import NumericEntry
22
+ from bootstack.widgets._impl.composites.tabs.tabview import TabView
23
+ from bootstack.widgets._impl.composites.passwordentry import PasswordEntry
24
+ from bootstack.widgets._impl.composites.slider.slider import Slider
25
+ from bootstack.widgets._impl.composites.selectbox import SelectBox
26
+ from bootstack.widgets._impl.primitives.spinbox import Spinbox
27
+ from bootstack.widgets._impl.composites.textentry import TextEntry
28
+ from bootstack.widgets.types import EditorType, Master
29
+
30
+ if TYPE_CHECKING:
31
+ from bootstack.dialogs._impl.dialog import DialogButton
32
+ ButtonInput = str | Mapping[str, Any] | DialogButton
33
+
34
+ DType = Literal['int', 'float', 'bool', 'date', 'datetime', 'password', 'str'] | type | None
35
+
36
+
37
+ @dataclass
38
+ class FieldItem:
39
+ """A single field in a `Form`, addressed by its `key`."""
40
+
41
+ key: str
42
+ """Unique field identifier — used to read and write the field value."""
43
+ label: str | None = None
44
+ """Display label shown above the field. Defaults to the capitalized `key`."""
45
+ dtype: DType = None
46
+ """Type hint controlling the default editor — `'str'`, `'int'`, `'float'`,
47
+ `'bool'`, `'date'`, `'datetime'`, or `'password'`. `None` infers the type from
48
+ the initial value."""
49
+ readonly: bool = False
50
+ """Render the field read-only. Default `False`."""
51
+ required: bool = False
52
+ """Mark the field as required — adds a `'required'` validation rule and appends
53
+ an asterisk to the label. Honored by the text, number, password, date, and
54
+ select editors; has no effect on boolean or slider editors. Default `False`."""
55
+ visible: bool = True
56
+ """Show the field. Set `False` to hide it. Default `True`."""
57
+ column: int | None = None
58
+ """Zero-based grid column. Auto-placed when `None`."""
59
+ row: int | None = None
60
+ """Zero-based grid row. Auto-placed when `None`."""
61
+ columnspan: int = 1
62
+ """Number of grid columns to span. Default `1`."""
63
+ rowspan: int = 1
64
+ """Number of grid rows to span. Default `1`."""
65
+ editor: EditorType | None = None
66
+ """Force a specific editor widget (see `EditorType`). `None` infers it from
67
+ `dtype`."""
68
+ editor_options: dict[str, Any] = field(default_factory=dict)
69
+ """Extra keyword arguments forwarded to the editor widget. For `'select'`,
70
+ pass `{"values": ["A", "B", "C"]}`; add `{"allow_custom_values": True}` for an
71
+ editable combobox."""
72
+ type: Literal['field'] = "field"
73
+ """Item-type discriminator. Set automatically; only needed when building items
74
+ as plain dicts."""
75
+
76
+
77
+ @dataclass
78
+ class GroupItem:
79
+ """A labeled group of fields with its own column layout, placed in a `Form`."""
80
+
81
+ items: list[FieldItem | Mapping[str, Any] | GroupItem | TabsItem] = field(default_factory=list)
82
+ """Child `FieldItem`, `GroupItem`, or `TabsItem` instances (or equivalent dicts)."""
83
+ label: str | None = None
84
+ """Section heading shown above the group border. No border is drawn when `None`."""
85
+ col_count: int = 1
86
+ """Number of columns within the group. Default `1`."""
87
+ min_col_width: int = DEFAULT_MIN_COL_WIDTH
88
+ """Minimum column width in pixels."""
89
+ width: int | None = None
90
+ """Fixed width for the group container."""
91
+ height: int | None = None
92
+ """Fixed height for the group container."""
93
+ column: int | None = None
94
+ """Zero-based grid column in the parent form. Auto-placed when `None`."""
95
+ row: int | None = None
96
+ """Zero-based grid row in the parent form. Auto-placed when `None`."""
97
+ columnspan: int = 1
98
+ """Columns to span in the parent grid. Default `1`."""
99
+ rowspan: int = 1
100
+ """Rows to span in the parent grid. Default `1`."""
101
+ padding: int | str | tuple[int, int] | tuple[int, int, int, int] | None = 8
102
+ """Internal padding inside the group border. Default `8`."""
103
+ type: Literal['group'] = "group"
104
+ """Item-type discriminator. Set automatically; only needed when building items
105
+ as plain dicts."""
106
+
107
+
108
+ @dataclass
109
+ class TabItem:
110
+ """A single tab within a `TabsItem`."""
111
+
112
+ label: str
113
+ """Tab button label."""
114
+ items: list[FieldItem | Mapping[str, Any] | GroupItem | TabsItem] = field(default_factory=list)
115
+ """`FieldItem`, `GroupItem`, or `TabsItem` instances for this tab's content."""
116
+ padding: int | str | tuple[int, int] | tuple[int, int, int, int] | None = 8
117
+ """Internal padding inside the tab body. Default `8`."""
118
+
119
+
120
+ @dataclass
121
+ class TabsItem:
122
+ """A tab container holding one or more `TabItem` entries, placed in a `Form`."""
123
+
124
+ tabs: list[TabItem | Mapping[str, Any]] = field(default_factory=list)
125
+ """`TabItem` instances (or equivalent dicts) defining each tab."""
126
+ label: str | None = None
127
+ """Optional heading shown above the tab bar."""
128
+ width: int | None = None
129
+ """Fixed width for the tab container."""
130
+ height: int | None = None
131
+ """Fixed height for the tab container."""
132
+ column: int | None = None
133
+ """Zero-based grid column in the parent form. Auto-placed when `None`."""
134
+ row: int | None = None
135
+ """Zero-based grid row in the parent form. Auto-placed when `None`."""
136
+ columnspan: int = 1
137
+ """Columns to span in the parent grid. Default `1`."""
138
+ rowspan: int = 1
139
+ """Rows to span in the parent grid. Default `1`."""
140
+ type: Literal['tabs'] = "tabs"
141
+ """Item-type discriminator. Set automatically; only needed when building items
142
+ as plain dicts."""
143
+
144
+
145
+ FormItem = FieldItem | GroupItem | TabsItem
146
+
147
+
148
+ class Form(Frame):
149
+ """A configurable form that can be generated from data or explicit items.
150
+
151
+ Form is a *field manager* that provides a domain-specific API for accessing
152
+ and manipulating form fields and their values.
153
+
154
+ Field Access:
155
+ - `field(key)` — returns the Field widget for a key
156
+ - `fields()` — returns all Field widgets in order
157
+ - `keys()` — returns all field keys in order
158
+
159
+ Value Operations:
160
+ - `get_field_value(key)` — get a single field's value
161
+ - `set_field_value(key, value)` — set a single field's value
162
+ - `get()` / `set(values)` — get/set all values as a dict
163
+ - `value` property — get/set all values as a dict
164
+
165
+ Variable & Signal Access:
166
+ - `field_variable(key)` — get Tk Variable for a field
167
+ - `field_signal(key)` — get Signal for a field's value
168
+ - `field_textsignal(key)` — get Signal for a field's text
169
+
170
+ Attributes:
171
+ result: Result value set by button commands. None until a button with a
172
+ `result` or `command` is pressed.
173
+
174
+ Args:
175
+ master: Parent widget.
176
+ data: Initial data backing the form. If items are not provided,
177
+ field items are inferred from the keys and value types.
178
+ items: Optional explicit form definition. Accepts dictionaries that
179
+ match the FieldItem/GroupItem/TabsItem shapes or the dataclass
180
+ instances directly.
181
+ col_count: Number of columns at the top level.
182
+ min_col_width: Minimum width for each column in pixels.
183
+ on_data_change: Optional callback invoked with the updated data dict
184
+ whenever a field value changes.
185
+ width: Requested width for the form container.
186
+ height: Requested height for the form container.
187
+ accent: Accent token for the form container (e.g., 'primary', 'secondary').
188
+ buttons: Optional footer buttons. Accepts plain strings, DialogButton
189
+ instances, or dictionaries that map to DialogButton kwargs.
190
+ **kwargs: Additional Frame configuration options.
191
+ """
192
+
193
+ def __init__(
194
+ self,
195
+ master: Master = None,
196
+ *,
197
+ data: dict[str, Any] | None = None,
198
+ items: Sequence[FormItem | Mapping[str, Any]] | None = None,
199
+ col_count: int = 1,
200
+ min_col_width: int = DEFAULT_MIN_COL_WIDTH,
201
+ on_data_change: Callable[[dict[str, Any]], Any] | None = None,
202
+ width: int | None = None,
203
+ height: int | None = None,
204
+ accent: str | None = None,
205
+ buttons: Sequence[ButtonInput] | None = None,
206
+ **kwargs: Any,
207
+ ) -> None:
208
+ """Build a configurable form from data or explicit items.
209
+
210
+ Args:
211
+ master: Parent widget.
212
+ data: Initial data backing the form; keys become field names.
213
+ items: Explicit form layout (FieldItem/GroupItem/TabsItem or mappings).
214
+ col_count: Number of columns at the top level.
215
+ min_col_width: Minimum width per column in pixels.
216
+ on_data_change: Callback invoked with updated data when a field changes.
217
+ width: Requested form width; if None, size naturally.
218
+ height: Requested form height; if None, size naturally.
219
+ accent: Accent token for the form container.
220
+ buttons: Optional footer buttons (DialogButton, mapping, or string).
221
+ **kwargs: Additional Frame configuration options.
222
+ """
223
+ super().__init__(master=master, width=width, height=height, accent=accent, **kwargs)
224
+
225
+ self._data: dict[str, Any] = dict(data) if data else {}
226
+ self.result: Any = None
227
+ self._on_data_change = on_data_change
228
+ self._col_count = col_count
229
+ self._min_col_width = min_col_width
230
+ self._widgets: dict[str, Any] = {}
231
+ self._variables: dict[str, Variable] = {}
232
+ self._signals: dict[str, Any] = {}
233
+ self._textsignals: dict[str, Any] = {}
234
+ self._items_by_key: dict[str, FieldItem] = {}
235
+ self._suspend_sync = False
236
+
237
+ normalized_items = self._normalize_items(items or self._infer_items_from_data(self._data))
238
+
239
+ self.columnconfigure(0, weight=1)
240
+ self.rowconfigure(0, weight=1)
241
+
242
+ container = Frame(self)
243
+ container.grid(row=0, column=0, sticky='nsew')
244
+ self._content_frame = Frame(container)
245
+ self._content_frame.pack(fill='both', expand=True)
246
+
247
+ # Respect explicit width/height by preventing geometry propagation.
248
+ if width or height:
249
+ if width:
250
+ self.configure(width=width)
251
+ container.configure(width=width)
252
+ if height:
253
+ self.configure(height=height)
254
+ container.configure(height=height)
255
+ self.grid_propagate(False)
256
+ self.pack_propagate(False)
257
+ container.grid_propagate(False)
258
+
259
+ self._build_items(
260
+ self._content_frame, normalized_items, col_count=self._col_count, min_col_width=self._min_col_width)
261
+
262
+ if buttons:
263
+ footer = Frame(self)
264
+ footer.grid(row=1, column=0, sticky='ew', pady=(8, 0))
265
+ footer.columnconfigure(0, weight=1)
266
+ self._build_buttons(footer, buttons)
267
+
268
+ # Reactive form-level validity aggregate. AND of the member fields'
269
+ # `valid` signals; recomputed whenever any field's validity changes.
270
+ self._valid_signal: Signal = Signal(True)
271
+ self._validity_entries: list[Any] = []
272
+ self._register_validity_aggregate()
273
+
274
+ @property
275
+ def data(self) -> dict[str, Any]:
276
+ """Current data backing the form."""
277
+ return dict(self._collect_data())
278
+
279
+ @property
280
+ def valid(self) -> Signal:
281
+ """Reactive `Signal[bool]` — True when every field passes validation.
282
+
283
+ AND-ed over the member fields' reactive `valid` signals and recomputed
284
+ whenever any field's validity changes (a blur, a key for key-triggered
285
+ rules, or a manual validate). A field is valid until proven otherwise,
286
+ so this reads `True` before any rule has run — call `validate()` to force
287
+ a full pass (for example, to gate a submit button's initial state).
288
+ """
289
+ return self._valid_signal
290
+
291
+ @property
292
+ def errors(self) -> dict[str, str]:
293
+ """Current validation errors keyed by field key; empty when all valid.
294
+
295
+ Read live from the fields' reactive `error` signals, so it always
296
+ reflects the latest validation run.
297
+ """
298
+ out: dict[str, str] = {}
299
+ for key, widget in self._widgets.items():
300
+ entry = self._validity_entry(widget)
301
+ if entry is not None and not entry._valid_signal():
302
+ out[key] = entry._error_signal()
303
+ return out
304
+
305
+ @staticmethod
306
+ def _validity_entry(widget: Any) -> Any | None:
307
+ """Return a field's validation-bearing entry, or None if it has none."""
308
+ if not isinstance(widget, Field):
309
+ return None
310
+ entry = getattr(widget, "_entry", None)
311
+ if entry is not None and hasattr(entry, "_valid_signal"):
312
+ return entry
313
+ return None
314
+
315
+ def _register_validity_aggregate(self) -> None:
316
+ """Subscribe each field's validity signal into the form `valid` aggregate."""
317
+ for widget in self._widgets.values():
318
+ entry = self._validity_entry(widget)
319
+ if entry is not None:
320
+ self._validity_entries.append(entry)
321
+ entry._valid_signal.subscribe(lambda *_: self._recompute_validity())
322
+ self._recompute_validity()
323
+
324
+ def _recompute_validity(self) -> None:
325
+ """Recompute the form `valid` aggregate from its fields' signals."""
326
+ # Signal.set dedupes unchanged values, so this won't re-notify needlessly.
327
+ self._valid_signal.set(all(e._valid_signal() for e in self._validity_entries))
328
+
329
+ def validate(self) -> bool:
330
+ """Run validation rules on all field widgets; returns True if all pass."""
331
+ all_valid = True
332
+ first_invalid_widget = None
333
+
334
+ def _validate_field(widget: Field) -> bool:
335
+ entry = getattr(widget, "_entry", widget)
336
+ if not getattr(entry, "_rules", None):
337
+ return True
338
+ # Delegate to the entry's own validator. An explicit form.validate()
339
+ # is a manual action, so the "manual" trigger runs every rule; the
340
+ # entry validates its typed value, updates the field's reactive
341
+ # validity signals, and emits the validation events.
342
+ return entry.validate(entry._get_validation_value(), trigger="manual")
343
+
344
+ for widget in self._widgets.values():
345
+ if isinstance(widget, Field):
346
+ ok = _validate_field(widget)
347
+ if not ok and first_invalid_widget is None:
348
+ first_invalid_widget = widget
349
+ all_valid = all_valid and ok
350
+ if first_invalid_widget:
351
+ try:
352
+ first_invalid_widget.focus_set()
353
+ except Exception:
354
+ pass
355
+ return all_valid
356
+
357
+ # --- Field access API (v2) -------------------------------------------
358
+
359
+ def field(self, key: str) -> Field:
360
+ """Return the Field widget for the given key.
361
+
362
+ Args:
363
+ key: The field key.
364
+
365
+ Returns:
366
+ The Field widget instance.
367
+
368
+ Raises:
369
+ KeyError: If no field with the given key exists.
370
+ """
371
+ if key not in self._widgets:
372
+ raise KeyError(f"No field with key '{key}'")
373
+ return self._widgets[key]
374
+
375
+ def fields(self) -> tuple[Field, ...]:
376
+ """Return all field widgets in insertion order.
377
+
378
+ Returns:
379
+ Tuple of Field widget instances.
380
+ """
381
+ return tuple(self._widgets[k] for k in self._items_by_key.keys() if k in self._widgets)
382
+
383
+ def keys(self) -> tuple[str, ...]:
384
+ """Return all field keys in insertion order.
385
+
386
+ Returns:
387
+ Tuple of field key strings.
388
+ """
389
+ return tuple(self._items_by_key.keys())
390
+
391
+ # --- Value helpers (explicit, no overloading) -------------------------
392
+
393
+ def get_field_value(self, key: str) -> Any:
394
+ """Return the current value of the field.
395
+
396
+ Args:
397
+ key: The field key.
398
+
399
+ Returns:
400
+ The current field value.
401
+
402
+ Raises:
403
+ KeyError: If no field with the given key exists.
404
+ """
405
+ if key not in self._widgets:
406
+ raise KeyError(f"No field with key '{key}'")
407
+ return self._read_value_from_widget(key)
408
+
409
+ def set_field_value(self, key: str, value: Any) -> None:
410
+ """Set the value of the field.
411
+
412
+ Args:
413
+ key: The field key.
414
+ value: The new value to set.
415
+
416
+ Raises:
417
+ KeyError: If no field with the given key exists.
418
+ """
419
+ if key not in self._items_by_key:
420
+ raise KeyError(f"No field with key '{key}'")
421
+ item = self._items_by_key[key]
422
+ self._apply_value_to_widget(key, item, value)
423
+ self._data[key] = value
424
+
425
+ # --- Variable & signal accessors (v2 short names) ---------------------
426
+
427
+ def field_variable(self, key: str) -> Variable | None:
428
+ """Return the Tk Variable for the field.
429
+
430
+ Args:
431
+ key: The field key.
432
+
433
+ Returns:
434
+ The Tk Variable, or None if not available.
435
+ """
436
+ return self._variables.get(key)
437
+
438
+ def field_signal(self, key: str):
439
+ """Return the Signal for the field value.
440
+
441
+ Args:
442
+ key: The field key.
443
+
444
+ Returns:
445
+ The Signal, or None if not available.
446
+ """
447
+ return self._signals.get(key)
448
+
449
+ def field_textsignal(self, key: str):
450
+ """Return the Signal for the field text.
451
+
452
+ Args:
453
+ key: The field key.
454
+
455
+ Returns:
456
+ The text Signal, or None if not available.
457
+ """
458
+ return self._textsignals.get(key)
459
+
460
+ # --- Legacy variable/signal accessors (backward compatibility) --------
461
+
462
+ def get_field_variable(self, key: str) -> Variable | None:
463
+ """Return the Tk variable associated with a field key, if any.
464
+
465
+ Deprecated:
466
+ Use `field_variable(key)` instead.
467
+ """
468
+ return self.field_variable(key)
469
+
470
+ def get_field_signal(self, key: str):
471
+ """Return the Signal associated with a field key, if any.
472
+
473
+ Deprecated:
474
+ Use `field_signal(key)` instead.
475
+ """
476
+ return self.field_signal(key)
477
+
478
+ def get_field_textsignal(self, key: str):
479
+ """Return the TextSignal associated with a field key, if any.
480
+
481
+ Deprecated:
482
+ Use `field_textsignal(key)` instead.
483
+ """
484
+ return self.field_textsignal(key)
485
+
486
+ # --- Form-level value API (v2 standardization) ------------------------
487
+
488
+ def get(self) -> dict[str, Any]:
489
+ """Return all field values as a dictionary.
490
+
491
+ Returns:
492
+ Dictionary mapping field keys to their current values.
493
+ """
494
+ return self.data
495
+
496
+ def set(self, values: Mapping[str, Any]) -> None:
497
+ """Set multiple field values from a dictionary.
498
+
499
+ Args:
500
+ values: Dictionary mapping field keys to values.
501
+ """
502
+ self._data = dict(values)
503
+ self._suspend_sync = True
504
+ try:
505
+ for key, item in self._items_by_key.items():
506
+ value = self._data.get(key)
507
+ self._apply_value_to_widget(key, item, value)
508
+ finally:
509
+ self._suspend_sync = False
510
+
511
+ @property
512
+ def value(self) -> dict[str, Any]:
513
+ """Get or set all form field values as a dictionary."""
514
+ return self.get()
515
+
516
+ @value.setter
517
+ def value(self, values: Mapping[str, Any]) -> None:
518
+ self.set(values)
519
+
520
+ @configure_delegate('data')
521
+ def _delegate_data(self, value: Mapping[str, Any] = None):
522
+ if value is None:
523
+ return dict(self._collect_data())
524
+ else:
525
+ self._data = dict(value)
526
+ self._suspend_sync = True
527
+ try:
528
+ for key, item in self._items_by_key.items():
529
+ value = self._data.get(key)
530
+ self._apply_value_to_widget(key, item, value)
531
+ finally:
532
+ self._suspend_sync = False
533
+ return None
534
+
535
+ def _build_items(self, parent: Frame, items: Sequence[FormItem], *, col_count: int, min_col_width: int) -> None:
536
+ for col in range(col_count):
537
+ parent.columnconfigure(col, weight=1, minsize=min_col_width)
538
+
539
+ auto_row = 0
540
+ auto_col = 0
541
+
542
+ for item in items:
543
+ widget = None
544
+ columnspan = 1
545
+ rowspan = 1
546
+
547
+ if isinstance(item, FieldItem):
548
+ widget = self._build_field(parent, item)
549
+ columnspan = item.columnspan
550
+ rowspan = item.rowspan
551
+ elif isinstance(item, GroupItem):
552
+ container = LabelFrame(parent, text=item.label, padding=item.padding) if item.label else Frame(
553
+ parent, padding=item.padding)
554
+ if item.width:
555
+ container.configure(width=item.width)
556
+ if item.height:
557
+ container.configure(height=item.height)
558
+ nested_items = self._normalize_items(item.items)
559
+ self._build_items(
560
+ container,
561
+ nested_items,
562
+ col_count=item.col_count or col_count,
563
+ min_col_width=item.min_col_width or min_col_width,
564
+ )
565
+ widget = container
566
+ columnspan = item.columnspan
567
+ rowspan = item.rowspan
568
+ elif isinstance(item, TabsItem):
569
+ tv_kwargs = {}
570
+ if item.width is not None:
571
+ tv_kwargs['width'] = item.width
572
+ if item.height is not None:
573
+ tv_kwargs['height'] = item.height
574
+ tabview = TabView(parent, **tv_kwargs)
575
+ for i, tab in enumerate(self._normalize_tabs(item.tabs)):
576
+ page = tabview.add(f'tab_{i}', text=tab.label)
577
+ if tab.padding is not None:
578
+ page.configure(padding=tab.padding)
579
+ self._build_items(
580
+ page, self._normalize_items(tab.items), col_count=col_count, min_col_width=min_col_width)
581
+ widget = tabview
582
+ columnspan = item.columnspan
583
+ rowspan = item.rowspan
584
+
585
+ if widget is None:
586
+ continue
587
+
588
+ row = item.row if isinstance(item, (FieldItem, GroupItem, TabsItem)) and item.row is not None else auto_row
589
+ column = item.column if isinstance(
590
+ item, (FieldItem, GroupItem, TabsItem)) and item.column is not None else auto_col
591
+
592
+ widget.grid(row=row, column=column, columnspan=columnspan, rowspan=rowspan, sticky='nsew', padx=6, pady=4)
593
+
594
+ if isinstance(item, (FieldItem, GroupItem, TabsItem)) and item.row is None and item.column is None:
595
+ auto_col += columnspan
596
+ if auto_col >= col_count:
597
+ auto_col = 0
598
+ auto_row += 1
599
+
600
+ def _build_field(self, parent: Frame, item: FieldItem):
601
+ if not item.visible:
602
+ return None
603
+
604
+ editor = item.editor or self._default_editor_for_dtype(item.dtype, self._data.get(item.key))
605
+ options = dict(item.editor_options or {})
606
+ if item.required:
607
+ options["required"] = True
608
+ initial_value = self._data.get(item.key)
609
+ variable = self._variable_for_item(item, initial_value, editor)
610
+ label_text = item.label if item.label is not None else item.key.replace("_", " ").title()
611
+
612
+ container = Frame(parent)
613
+ container.columnconfigure(0, weight=1) # Allow field widgets to expand horizontally
614
+
615
+ # Validation options that should only be passed to widgets supporting ValidationMixin
616
+ validation_options = {'show_message', 'required', 'validator'}
617
+
618
+ field_widget: Any
619
+ if editor == 'textfield':
620
+ field_widget = TextEntry(
621
+ container, value=initial_value or "", label=label_text, textvariable=variable, **options)
622
+ elif editor == 'numberfield':
623
+ numeric_value = initial_value if initial_value is not None else 0
624
+ field_widget = NumericEntry(
625
+ container, value=numeric_value, label=label_text, **options)
626
+ self._bind_numeric_variable(item.key, field_widget, variable)
627
+ elif editor == 'passwordfield':
628
+ field_widget = PasswordEntry(
629
+ container, value=initial_value or "", label=label_text, textvariable=variable, **options)
630
+ elif editor == 'datefield':
631
+ field_widget = DateEntry(container, value=initial_value, label=label_text, textvariable=variable, **options)
632
+ else:
633
+ # Filter out validation options for widgets that don't support ValidationMixin
634
+ filtered_options = {k: v for k, v in options.items() if k not in validation_options}
635
+
636
+ # Checkbox and switch use an inline label; select handles its own label.
637
+ if editor in ('checkbox', 'switch'):
638
+ if not filtered_options.get("text"):
639
+ filtered_options["text"] = label_text
640
+ elif label_text != "" and editor != 'select':
641
+ Label(container, text=label_text).pack(anchor='w', pady=(0, 2))
642
+
643
+ if editor == 'select':
644
+ items = options.pop('items', options.pop('values', None)) or []
645
+ items = [str(i) for i in items]
646
+ field_widget = SelectBox(
647
+ container,
648
+ label=label_text,
649
+ value=initial_value or "",
650
+ items=items,
651
+ textvariable=variable,
652
+ **options
653
+ )
654
+ if initial_value is not None and variable is not None:
655
+ variable.set(initial_value)
656
+ elif editor == 'spinnerfield':
657
+ field_widget = Spinbox(container, textvariable=variable, **filtered_options)
658
+ if initial_value is not None:
659
+ variable.set(initial_value)
660
+ elif editor == 'textarea':
661
+ field_widget = Text(container, **filtered_options)
662
+ if initial_value:
663
+ field_widget.insert('1.0', str(initial_value))
664
+ elif editor == 'switch':
665
+ field_widget = Switch(container, variable=variable, **filtered_options)
666
+ elif editor == 'checkbox':
667
+ field_widget = CheckButton(container, variable=variable, **filtered_options)
668
+ elif editor == 'slider':
669
+ slider_opts = dict(filtered_options)
670
+ if 'from_' in slider_opts:
671
+ slider_opts['minvalue'] = slider_opts.pop('from_')
672
+ if 'to' in slider_opts:
673
+ slider_opts['maxvalue'] = slider_opts.pop('to')
674
+ field_widget = Slider(container, variable=variable, **slider_opts)
675
+ else:
676
+ field_widget = TextEntry(
677
+ container, value=initial_value or "", label=label_text, textvariable=variable, **options)
678
+
679
+ if editor != 'textarea':
680
+ field_widget.pack(fill='x', expand=True)
681
+ else:
682
+ field_widget.pack(fill='both', expand=True)
683
+
684
+ if isinstance(field_widget, Field):
685
+ field_widget.bind("<<Change>>", lambda _e, k=item.key: self._sync_value_from_widget(k))
686
+ field_widget.pack(fill='both', expand=True)
687
+ if not isinstance(field_widget, NumericEntry):
688
+ traced_var = getattr(field_widget, "variable", None)
689
+ if traced_var is not None:
690
+ self._register_variable(item.key, traced_var)
691
+ elif isinstance(field_widget, Text):
692
+ text_var = variable or StringVar(value=str(initial_value) if initial_value is not None else "")
693
+ self._register_variable(item.key, text_var)
694
+ self._bind_text_change(field_widget, item.key, text_var)
695
+ elif variable is not None:
696
+ self._register_variable(item.key, variable)
697
+
698
+ # record signals if the widget exposes them
699
+ signal_obj = getattr(field_widget, "_signal", None)
700
+ if signal_obj is not None:
701
+ self._signals[item.key] = signal_obj
702
+ text_signal = getattr(field_widget, "_textsignal", None)
703
+ if text_signal is not None:
704
+ self._textsignals[item.key] = text_signal
705
+
706
+ if item.readonly:
707
+ self._set_readonly(field_widget)
708
+
709
+ self._widgets[item.key] = field_widget
710
+ return container
711
+
712
+ def _build_buttons(self, parent: Frame, buttons: Sequence[ButtonInput]) -> None:
713
+ parsed = self._normalize_buttons(buttons)
714
+ for spec in reversed(parsed):
715
+ btn_color = getattr(spec, 'accent', None) or getattr(spec, 'color', None)
716
+ btn_variant = getattr(spec, 'variant', None)
717
+
718
+ if not btn_color:
719
+ # Get color and variant from role
720
+ btn_color, btn_variant = self._style_for_role(spec.role)
721
+
722
+ btn = Button(parent, text=spec.text, accent=btn_color, variant=btn_variant)
723
+ btn.configure(command=self._make_button_command(spec))
724
+ btn.pack(side='right', padx=(4, 0))
725
+
726
+ # --- normalization --------------------------------------------------
727
+ def _normalize_items(self, items: Iterable[FormItem | Mapping[str, Any]]) -> list[FormItem]:
728
+ normalized: list[FormItem] = []
729
+ for raw in items:
730
+ item: FormItem | None = None
731
+ if isinstance(raw, (FieldItem, GroupItem, TabsItem)):
732
+ item = raw
733
+ elif isinstance(raw, Mapping):
734
+ type_hint = raw.get('type', 'field')
735
+ if type_hint == 'group':
736
+ item = GroupItem(
737
+ items=list(raw.get('items', [])),
738
+ label=raw.get('label'),
739
+ col_count=raw.get('col_count', 1),
740
+ min_col_width=raw.get('min_col_width', DEFAULT_MIN_COL_WIDTH),
741
+ width=raw.get('width'),
742
+ height=raw.get('height'),
743
+ column=raw.get('column'),
744
+ row=raw.get('row'),
745
+ columnspan=raw.get('columnspan', 1),
746
+ rowspan=raw.get('rowspan', 1),
747
+ )
748
+ elif type_hint == 'tabs':
749
+ item = TabsItem(
750
+ tabs=list(raw.get('tabs', [])),
751
+ label=raw.get('label'),
752
+ width=raw.get('width'),
753
+ height=raw.get('height'),
754
+ column=raw.get('column'),
755
+ row=raw.get('row'),
756
+ columnspan=raw.get('columnspan', 1),
757
+ rowspan=raw.get('rowspan', 1),
758
+ )
759
+ else:
760
+ key_value = raw.get('key')
761
+ if key_value is None:
762
+ continue
763
+ item = FieldItem(
764
+ key=str(key_value),
765
+ label=raw.get('label'),
766
+ dtype=raw.get('dtype'),
767
+ readonly=raw.get('readonly', False),
768
+ required=raw.get('required', False),
769
+ visible=raw.get('visible', True),
770
+ column=raw.get('column'),
771
+ row=raw.get('row'),
772
+ columnspan=raw.get('columnspan', 1),
773
+ rowspan=raw.get('rowspan', 1),
774
+ editor=raw.get('editor'),
775
+ editor_options=dict(raw.get('editor_options', {}) or {}),
776
+ )
777
+
778
+ if isinstance(item, GroupItem):
779
+ item.items = self._normalize_items(item.items)
780
+ if isinstance(item, TabsItem):
781
+ item.tabs = self._normalize_tabs(item.tabs)
782
+ if isinstance(item, FieldItem):
783
+ self._items_by_key[item.key] = item
784
+
785
+ if item:
786
+ normalized.append(item)
787
+ return normalized
788
+
789
+ def _normalize_tabs(self, tabs: Iterable[TabItem | Mapping[str, Any]]) -> list[TabItem]:
790
+ normalized: list[TabItem] = []
791
+ for raw in tabs:
792
+ if isinstance(raw, TabItem):
793
+ normalized.append(raw)
794
+ elif isinstance(raw, Mapping):
795
+ normalized.append(TabItem(label=str(raw.get('label', 'Tab')), items=list(raw.get('items', []))))
796
+ return normalized
797
+
798
+ def _normalize_buttons(self, buttons: Sequence[ButtonInput]) -> list["DialogButton"]:
799
+ from bootstack.dialogs._impl.dialog import DialogButton # local import to avoid circular init
800
+
801
+ normalized: list[DialogButton] = []
802
+ for raw in buttons:
803
+ if isinstance(raw, DialogButton):
804
+ normalized.append(raw)
805
+ elif isinstance(raw, Mapping):
806
+ normalized.append(DialogButton(**raw)) # type: ignore[arg-type]
807
+ elif isinstance(raw, str):
808
+ normalized.append(DialogButton(text=raw))
809
+ return normalized
810
+
811
+ # --- data helpers ---------------------------------------------------
812
+ def _collect_data(self) -> dict[str, Any]:
813
+ current: dict[str, Any] = dict(self._data)
814
+ for key in self._widgets.keys():
815
+ current[key] = self._read_value_from_widget(key)
816
+ return current
817
+
818
+ def _read_value_from_widget(self, key: str) -> Any:
819
+ widget = self._widgets.get(key)
820
+ if widget is None:
821
+ return self._data.get(key)
822
+
823
+ if hasattr(widget, "value"):
824
+ val_attr = getattr(widget, "value")
825
+ value = val_attr() if callable(val_attr) else val_attr
826
+ elif key in self._variables:
827
+ value = self._variables[key].get()
828
+ elif isinstance(widget, Text):
829
+ value = widget.get("1.0", "end-1c")
830
+ else:
831
+ try:
832
+ value = widget.get()
833
+ except Exception:
834
+ value = self._data.get(key)
835
+
836
+ item = self._items_by_key.get(key)
837
+ if item:
838
+ value = self._coerce_value(item.dtype, value)
839
+ return value
840
+
841
+ def _apply_value_to_widget(self, key: str, item: FieldItem, value: Any) -> None:
842
+ widget = self._widgets.get(key)
843
+ if widget is None:
844
+ return
845
+
846
+ if isinstance(widget, Field):
847
+ if hasattr(widget, "_suppress_changed_event"):
848
+ widget._suppress_changed_event = True # type: ignore[attr-defined]
849
+ try:
850
+ widget.value = value
851
+ finally:
852
+ widget._suppress_changed_event = False # type: ignore[attr-defined]
853
+ else:
854
+ widget.value = value
855
+ return
856
+
857
+ if isinstance(widget, Text):
858
+ widget.delete("1.0", "end")
859
+ if value is not None:
860
+ widget.insert("1.0", str(value))
861
+ return
862
+
863
+ if key in self._variables:
864
+ self._variables[key].set("" if value is None else value)
865
+ return
866
+
867
+ try:
868
+ widget.configure(value=value)
869
+ except Exception:
870
+ pass
871
+
872
+ def _sync_value_from_widget(self, key: str) -> None:
873
+ if self._suspend_sync:
874
+ return
875
+ if key not in self._items_by_key:
876
+ return
877
+ new_value = self._read_value_from_widget(key)
878
+ self._data[key] = new_value
879
+ if self._on_data_change:
880
+ self._on_data_change(dict(self._data))
881
+ self.event_generate("<<BsDataChange>>")
882
+
883
+ def _variable_for_item(self, item: FieldItem, initial: Any, editor: EditorType | None) -> Variable | None:
884
+ dtype = item.dtype
885
+ if editor in ('checkbox', 'switch'):
886
+ return BooleanVar(value=bool(initial) if initial is not None else False)
887
+ if editor in ('numberfield', 'spinnerfield', 'slider') or dtype in ('int', int, 'float', float):
888
+ if dtype in ('float', float):
889
+ return DoubleVar(value=float(initial) if initial is not None else 0.0)
890
+ return IntVar(value=int(initial) if initial is not None else 0)
891
+ return StringVar(value="" if initial is None else str(initial))
892
+
893
+ def _default_editor_for_dtype(self, dtype: Any, value: Any) -> EditorType:
894
+ if dtype in ('int', int, 'float', float):
895
+ return 'numberfield'
896
+ if dtype in ('bool', bool):
897
+ return 'checkbox'
898
+ if dtype in ('date', 'datetime', date, datetime):
899
+ return 'datefield'
900
+ if dtype in ('password',):
901
+ return 'passwordfield'
902
+ if value is not None:
903
+ if isinstance(value, (int, float)):
904
+ return 'numberfield'
905
+ if isinstance(value, (bool,)):
906
+ return 'checkbox'
907
+ if isinstance(value, (date, datetime)):
908
+ return 'datefield'
909
+ return 'textfield'
910
+
911
+ def _coerce_value(self, dtype: Any, value: Any) -> Any:
912
+ if dtype in ('int', int):
913
+ try:
914
+ return int(value)
915
+ except Exception:
916
+ return value
917
+ if dtype in ('float', float):
918
+ try:
919
+ return float(value)
920
+ except Exception:
921
+ return value
922
+ if dtype in ('bool', bool):
923
+ return bool(value)
924
+ if dtype in ('date', 'datetime', date, datetime):
925
+ return value
926
+ return value
927
+
928
+ def _register_variable(self, key: str, var: Variable) -> None:
929
+ self._variables[key] = var
930
+ var.trace_add("write", lambda *_a, k=key: self._sync_value_from_widget(k))
931
+
932
+ def _bind_text_change(self, widget: Text, key: str, var: StringVar | None = None) -> None:
933
+ _updating = {"text": False}
934
+
935
+ def _on_change(_event=None):
936
+ if _updating["text"]:
937
+ return
938
+ _updating["text"] = True
939
+ try:
940
+ widget.edit_modified(False)
941
+ text = widget.get("1.0", "end-1c")
942
+ if var is not None:
943
+ var.set(text)
944
+ self._sync_value_from_widget(key)
945
+ finally:
946
+ _updating["text"] = False
947
+
948
+ def _on_var_change(*_args):
949
+ if _updating["text"] or var is None:
950
+ return
951
+ _updating["text"] = True
952
+ try:
953
+ widget.delete("1.0", "end")
954
+ widget.insert("1.0", var.get())
955
+ finally:
956
+ _updating["text"] = False
957
+
958
+ widget.bind("<<Modified>>", _on_change)
959
+ widget.edit_modified(False)
960
+
961
+ if var is not None:
962
+ var.trace_add("write", _on_var_change)
963
+
964
+ def _bind_numeric_variable(self, key: str, widget: NumericEntry, var: Variable | None) -> None:
965
+ if var is None:
966
+ return
967
+
968
+ self._register_variable(key, var)
969
+
970
+ def _sync_numeric_var(*_args):
971
+ new_value = widget.value
972
+ if new_value is None:
973
+ return
974
+ try:
975
+ current_value = var.get()
976
+ except Exception:
977
+ current_value = None
978
+ if current_value == new_value:
979
+ return
980
+ previous_suspend = self._suspend_sync
981
+ self._suspend_sync = True
982
+ try:
983
+ var.set(new_value)
984
+ finally:
985
+ self._suspend_sync = previous_suspend
986
+
987
+ text_signal = getattr(widget, "signal", None)
988
+ if text_signal is not None:
989
+ text_signal.subscribe(lambda *_: _sync_numeric_var())
990
+ _sync_numeric_var()
991
+
992
+ def _set_readonly(self, widget: Any) -> None:
993
+ if isinstance(widget, Field):
994
+ widget.readonly(True)
995
+ else:
996
+ try:
997
+ widget.state(['disabled'])
998
+ except Exception:
999
+ try:
1000
+ widget.configure(state='disabled')
1001
+ except Exception:
1002
+ pass
1003
+
1004
+ # --- button helpers -------------------------------------------------
1005
+ def _make_button_command(self, spec: DialogButton):
1006
+ def command():
1007
+ if spec.command:
1008
+ spec.command(self) # type: ignore[arg-type]
1009
+ self.result = spec.result if spec.result is not None else self.data
1010
+
1011
+ return command
1012
+
1013
+ def _style_for_role(self, role: str) -> tuple[str, str | None]:
1014
+ """Get color and variant for a button role.
1015
+
1016
+ Returns:
1017
+ Tuple of (color, variant) for the role.
1018
+ """
1019
+ if role == "primary":
1020
+ return ("primary", None)
1021
+ if role == "secondary":
1022
+ return ("default", None)
1023
+ if role == "danger":
1024
+ return ("danger", None)
1025
+ if role == "cancel":
1026
+ return ("default", "outline")
1027
+ if role == "help":
1028
+ return ("primary", "link")
1029
+ return ("default", None)
1030
+
1031
+ # --- inference ------------------------------------------------------
1032
+ def _infer_items_from_data(self, data: Mapping[str, Any]) -> list[FieldItem]:
1033
+ inferred: list[FieldItem] = []
1034
+ for key, value in data.items():
1035
+ inferred.append(
1036
+ FieldItem(
1037
+ key=str(key),
1038
+ label=str(key).replace('_', ' ').title(),
1039
+ dtype=self._infer_dtype_from_value(value),
1040
+ editor=self._default_editor_for_dtype(self._infer_dtype_from_value(value), value),
1041
+ editor_options={"show_message": True},
1042
+ )
1043
+ )
1044
+ return inferred
1045
+
1046
+ @staticmethod
1047
+ def _infer_dtype_from_value(value: Any) -> DType:
1048
+ if isinstance(value, bool):
1049
+ return 'bool'
1050
+ if isinstance(value, int) and not isinstance(value, bool):
1051
+ return 'int'
1052
+ if isinstance(value, float):
1053
+ return 'float'
1054
+ if isinstance(value, (date, datetime)):
1055
+ return 'date'
1056
+ return 'str'
1057
+
1058
+
1059
+ __all__ = [
1060
+ "Form",
1061
+ "FormItem",
1062
+ "FieldItem",
1063
+ "GroupItem",
1064
+ "TabsItem",
1065
+ "TabItem",
1066
+ ]