bootstack 0.1.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. bootstack/__init__.py +249 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/api/__init__.py +127 -0
  4. bootstack/api/app.py +30 -0
  5. bootstack/api/constants.py +3 -0
  6. bootstack/api/data.py +23 -0
  7. bootstack/api/dialogs.py +44 -0
  8. bootstack/api/i18n.py +17 -0
  9. bootstack/api/localization.py +16 -0
  10. bootstack/api/menu.py +7 -0
  11. bootstack/api/style.py +23 -0
  12. bootstack/api/utils.py +24 -0
  13. bootstack/api/widgets.py +137 -0
  14. bootstack/assets/__init__.py +24 -0
  15. bootstack/assets/bootstack-transparent.png +0 -0
  16. bootstack/assets/bootstack.ico +0 -0
  17. bootstack/assets/bootstack.png +0 -0
  18. bootstack/assets/elements/__init__.py +0 -0
  19. bootstack/assets/elements/badge-pill.png +0 -0
  20. bootstack/assets/elements/badge-square.png +0 -0
  21. bootstack/assets/elements/border.png +0 -0
  22. bootstack/assets/elements/button-compact.png +0 -0
  23. bootstack/assets/elements/button-default.png +0 -0
  24. bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
  25. bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
  26. bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
  27. bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
  28. bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
  29. bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
  30. bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
  31. bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
  32. bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
  33. bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
  34. bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
  35. bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
  36. bootstack/assets/elements/checkbox-checked.png +0 -0
  37. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  38. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  39. bootstack/assets/elements/field.png +0 -0
  40. bootstack/assets/elements/input-after-compact.png +0 -0
  41. bootstack/assets/elements/input-after-default.png +0 -0
  42. bootstack/assets/elements/input-before-compact.png +0 -0
  43. bootstack/assets/elements/input-before-default.png +0 -0
  44. bootstack/assets/elements/input-compact.png +0 -0
  45. bootstack/assets/elements/input-default.png +0 -0
  46. bootstack/assets/elements/list-item-separated.png +0 -0
  47. bootstack/assets/elements/list-item.png +0 -0
  48. bootstack/assets/elements/manifest.toml +480 -0
  49. bootstack/assets/elements/menu-item.png +0 -0
  50. bootstack/assets/elements/nav-button-compact.png +0 -0
  51. bootstack/assets/elements/nav-button-default.png +0 -0
  52. bootstack/assets/elements/nav-icon-button-compact.png +0 -0
  53. bootstack/assets/elements/nav-icon-button-default.png +0 -0
  54. bootstack/assets/elements/notebook-client-border.png +0 -0
  55. bootstack/assets/elements/notebook-tab-active.png +0 -0
  56. bootstack/assets/elements/notebook-tab-bar.png +0 -0
  57. bootstack/assets/elements/notebook-tab-normal.png +0 -0
  58. bootstack/assets/elements/notebook-tab-pill.png +0 -0
  59. bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
  60. bootstack/assets/elements/progress-bar-solid.png +0 -0
  61. bootstack/assets/elements/progress-bar-thin.png +0 -0
  62. bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
  63. bootstack/assets/elements/radio-selected.png +0 -0
  64. bootstack/assets/elements/radio-unselected.png +0 -0
  65. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  66. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  67. bootstack/assets/elements/slider-handle-focus.png +0 -0
  68. bootstack/assets/elements/slider-handle.png +0 -0
  69. bootstack/assets/elements/slider-track-horizontal.png +0 -0
  70. bootstack/assets/elements/slider-track-vertical.png +0 -0
  71. bootstack/assets/elements/switch-off.png +0 -0
  72. bootstack/assets/elements/switch-on.png +0 -0
  73. bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
  74. bootstack/assets/elements/tabs-bar-vertical.png +0 -0
  75. bootstack/assets/elements/tabs-pill.png +0 -0
  76. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  77. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
  78. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
  79. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  80. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
  81. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  82. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
  83. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  84. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
  85. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  86. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
  87. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  88. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
  89. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  90. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
  91. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  92. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
  93. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  94. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
  95. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  96. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
  97. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  98. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
  99. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  100. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
  101. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  102. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
  103. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  104. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
  105. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  106. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
  107. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  108. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
  109. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  110. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
  111. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  112. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
  113. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  114. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
  115. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  116. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
  117. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  118. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
  119. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  120. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
  121. bootstack/assets/themes/__init__.py +0 -0
  122. bootstack/assets/themes/amber-dark.json +32 -0
  123. bootstack/assets/themes/amber-light.json +32 -0
  124. bootstack/assets/themes/aurora-dark.json +32 -0
  125. bootstack/assets/themes/aurora-light.json +32 -0
  126. bootstack/assets/themes/bootstrap-dark.json +32 -0
  127. bootstack/assets/themes/bootstrap-light.json +32 -0
  128. bootstack/assets/themes/classic-dark.json +32 -0
  129. bootstack/assets/themes/classic-light.json +32 -0
  130. bootstack/assets/themes/docs-dark.json +32 -0
  131. bootstack/assets/themes/docs-light.json +32 -0
  132. bootstack/assets/themes/forest-dark.json +32 -0
  133. bootstack/assets/themes/forest-light.json +32 -0
  134. bootstack/assets/themes/ocean-dark.json +32 -0
  135. bootstack/assets/themes/ocean-light.json +32 -0
  136. bootstack/assets/themes/rose-dark.json +32 -0
  137. bootstack/assets/themes/rose-light.json +32 -0
  138. bootstack/assets/widgets/__init__.py +0 -0
  139. bootstack/assets/widgets/badge-default.png +0 -0
  140. bootstack/assets/widgets/badge-pill.png +0 -0
  141. bootstack/assets/widgets/border.png +0 -0
  142. bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
  143. bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
  144. bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
  145. bootstack/assets/widgets/button-group-vertical-after.png +0 -0
  146. bootstack/assets/widgets/button-group-vertical-before.png +0 -0
  147. bootstack/assets/widgets/button-group-vertical-center.png +0 -0
  148. bootstack/assets/widgets/button.png +0 -0
  149. bootstack/assets/widgets/checkbox-checked.png +0 -0
  150. bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
  151. bootstack/assets/widgets/checkbox-unchecked.png +0 -0
  152. bootstack/assets/widgets/field.png +0 -0
  153. bootstack/assets/widgets/icon-button.png +0 -0
  154. bootstack/assets/widgets/input-inner.png +0 -0
  155. bootstack/assets/widgets/input-prefix.png +0 -0
  156. bootstack/assets/widgets/input-suffix.png +0 -0
  157. bootstack/assets/widgets/input.png +0 -0
  158. bootstack/assets/widgets/list-item-focus.png +0 -0
  159. bootstack/assets/widgets/list-item-separated.png +0 -0
  160. bootstack/assets/widgets/menu-item-separated.png +0 -0
  161. bootstack/assets/widgets/notebook-client-border.png +0 -0
  162. bootstack/assets/widgets/notebook-pill-active.png +0 -0
  163. bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
  164. bootstack/assets/widgets/notebook-tab-active.png +0 -0
  165. bootstack/assets/widgets/notebook-tab-border.png +0 -0
  166. bootstack/assets/widgets/notebook-tab-normal.png +0 -0
  167. bootstack/assets/widgets/notebook-underline.png +0 -0
  168. bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
  169. bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
  170. bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
  171. bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
  172. bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
  173. bootstack/assets/widgets/progress-trough-vertical.png +0 -0
  174. bootstack/assets/widgets/radio-selected.png +0 -0
  175. bootstack/assets/widgets/radio-unselected.png +0 -0
  176. bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
  177. bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
  178. bootstack/assets/widgets/separator-horizontal.png +0 -0
  179. bootstack/assets/widgets/separator-vertical.png +0 -0
  180. bootstack/assets/widgets/slider-handle-focus.png +0 -0
  181. bootstack/assets/widgets/slider-handle.png +0 -0
  182. bootstack/assets/widgets/slider-track-horizontal.png +0 -0
  183. bootstack/assets/widgets/slider-track-vertical.png +0 -0
  184. bootstack/assets/widgets/switch-off.png +0 -0
  185. bootstack/assets/widgets/switch-on.png +0 -0
  186. bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
  187. bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
  188. bootstack/assets/widgets/tabs-pill.png +0 -0
  189. bootstack/cli/__init__.py +124 -0
  190. bootstack/cli/__main__.py +6 -0
  191. bootstack/cli/add.py +439 -0
  192. bootstack/cli/build.py +115 -0
  193. bootstack/cli/config.py +287 -0
  194. bootstack/cli/demo.py +1267 -0
  195. bootstack/cli/doctor.py +195 -0
  196. bootstack/cli/list_cmd.py +71 -0
  197. bootstack/cli/promote.py +120 -0
  198. bootstack/cli/pyinstaller.py +246 -0
  199. bootstack/cli/run.py +99 -0
  200. bootstack/cli/start.py +105 -0
  201. bootstack/cli/templates/__init__.py +861 -0
  202. bootstack/constants.py +325 -0
  203. bootstack/core/__init__.py +34 -0
  204. bootstack/core/capabilities/__init__.py +45 -0
  205. bootstack/core/capabilities/after.py +103 -0
  206. bootstack/core/capabilities/bind.py +154 -0
  207. bootstack/core/capabilities/bindtags.py +112 -0
  208. bootstack/core/capabilities/busy.py +61 -0
  209. bootstack/core/capabilities/clipboard.py +88 -0
  210. bootstack/core/capabilities/focus.py +118 -0
  211. bootstack/core/capabilities/grab.py +65 -0
  212. bootstack/core/capabilities/grid.py +188 -0
  213. bootstack/core/capabilities/localization.py +231 -0
  214. bootstack/core/capabilities/pack.py +119 -0
  215. bootstack/core/capabilities/place.py +92 -0
  216. bootstack/core/capabilities/selection.py +136 -0
  217. bootstack/core/capabilities/signals.py +242 -0
  218. bootstack/core/capabilities/winfo.py +315 -0
  219. bootstack/core/colorutils.py +234 -0
  220. bootstack/core/exceptions.py +95 -0
  221. bootstack/core/images.py +283 -0
  222. bootstack/core/localization/README.md +90 -0
  223. bootstack/core/localization/__init__.py +13 -0
  224. bootstack/core/localization/intl_format.py +580 -0
  225. bootstack/core/localization/msgcat.py +425 -0
  226. bootstack/core/localization/specs.py +143 -0
  227. bootstack/core/mixins/__init__.py +1 -0
  228. bootstack/core/mixins/ttk_state.py +35 -0
  229. bootstack/core/mixins/widget.py +132 -0
  230. bootstack/core/publisher.py +147 -0
  231. bootstack/core/signals/README.md +112 -0
  232. bootstack/core/signals/__init__.py +8 -0
  233. bootstack/core/signals/integration.py +100 -0
  234. bootstack/core/signals/signal.py +317 -0
  235. bootstack/core/signals/types.py +4 -0
  236. bootstack/core/validation/__init__.py +5 -0
  237. bootstack/core/validation/types.py +13 -0
  238. bootstack/core/validation/validation_result.py +17 -0
  239. bootstack/core/validation/validation_rules.py +112 -0
  240. bootstack/core/variables.py +62 -0
  241. bootstack/datasource/README.md +607 -0
  242. bootstack/datasource/__init__.py +51 -0
  243. bootstack/datasource/base.py +474 -0
  244. bootstack/datasource/file_source.py +541 -0
  245. bootstack/datasource/memory_source.py +482 -0
  246. bootstack/datasource/sqlite_source.py +453 -0
  247. bootstack/datasource/types.py +259 -0
  248. bootstack/dialogs/__init__.py +56 -0
  249. bootstack/dialogs/colorchooser.py +674 -0
  250. bootstack/dialogs/colordropper.py +257 -0
  251. bootstack/dialogs/datedialog.py +404 -0
  252. bootstack/dialogs/dialog.py +514 -0
  253. bootstack/dialogs/filterdialog.py +358 -0
  254. bootstack/dialogs/fontdialog.py +339 -0
  255. bootstack/dialogs/formdialog.py +541 -0
  256. bootstack/dialogs/message.py +489 -0
  257. bootstack/dialogs/query.py +561 -0
  258. bootstack/py.typed +1 -0
  259. bootstack/runtime/__init__.py +3 -0
  260. bootstack/runtime/app.py +879 -0
  261. bootstack/runtime/base_window.py +786 -0
  262. bootstack/runtime/events.py +399 -0
  263. bootstack/runtime/menu.py +510 -0
  264. bootstack/runtime/shortcuts.py +423 -0
  265. bootstack/runtime/tk_patch.py +31 -0
  266. bootstack/runtime/toplevel.py +131 -0
  267. bootstack/runtime/utility.py +371 -0
  268. bootstack/runtime/visual_focus.py +228 -0
  269. bootstack/runtime/window_utilities.py +1043 -0
  270. bootstack/style/__init__.py +5498 -0
  271. bootstack/style/bootstyle.py +507 -0
  272. bootstack/style/bootstyle_builder_base.py +752 -0
  273. bootstack/style/bootstyle_builder_mixed.py +93 -0
  274. bootstack/style/bootstyle_builder_tk.py +109 -0
  275. bootstack/style/bootstyle_builder_ttk.py +354 -0
  276. bootstack/style/builders/__init__.py +51 -0
  277. bootstack/style/builders/badge.py +44 -0
  278. bootstack/style/builders/button.py +453 -0
  279. bootstack/style/builders/buttongroup.py +344 -0
  280. bootstack/style/builders/calendar.py +271 -0
  281. bootstack/style/builders/checkbutton.py +95 -0
  282. bootstack/style/builders/combobox.py +112 -0
  283. bootstack/style/builders/contextmenu.py +268 -0
  284. bootstack/style/builders/entry.py +83 -0
  285. bootstack/style/builders/expander.py +171 -0
  286. bootstack/style/builders/field.py +312 -0
  287. bootstack/style/builders/frame.py +27 -0
  288. bootstack/style/builders/label.py +28 -0
  289. bootstack/style/builders/labelframe.py +41 -0
  290. bootstack/style/builders/listview.py +267 -0
  291. bootstack/style/builders/menubar.py +74 -0
  292. bootstack/style/builders/menubutton.py +408 -0
  293. bootstack/style/builders/notebook.py +316 -0
  294. bootstack/style/builders/panedwindow.py +25 -0
  295. bootstack/style/builders/progressbar.py +71 -0
  296. bootstack/style/builders/radiobutton.py +68 -0
  297. bootstack/style/builders/scale.py +66 -0
  298. bootstack/style/builders/scrollbar.py +360 -0
  299. bootstack/style/builders/separator.py +45 -0
  300. bootstack/style/builders/sidenav.py +313 -0
  301. bootstack/style/builders/sizegrip.py +15 -0
  302. bootstack/style/builders/spinbox.py +119 -0
  303. bootstack/style/builders/switch.py +67 -0
  304. bootstack/style/builders/tabitem.py +205 -0
  305. bootstack/style/builders/toolbutton.py +260 -0
  306. bootstack/style/builders/tooltip.py +26 -0
  307. bootstack/style/builders/treeview.py +269 -0
  308. bootstack/style/builders/utils.py +404 -0
  309. bootstack/style/builders_tk/__init__.py +16 -0
  310. bootstack/style/builders_tk/defaults.py +229 -0
  311. bootstack/style/element.py +173 -0
  312. bootstack/style/style.py +499 -0
  313. bootstack/style/theme_provider.py +449 -0
  314. bootstack/style/tk_patch.py +5 -0
  315. bootstack/style/token_maps.py +42 -0
  316. bootstack/style/types.py +32 -0
  317. bootstack/style/typography.py +527 -0
  318. bootstack/style/utility.py +696 -0
  319. bootstack/themes/__init__.py +12 -0
  320. bootstack/themes/standard.py +415 -0
  321. bootstack/themes/user.py +45 -0
  322. bootstack/widgets/__init__.py +53 -0
  323. bootstack/widgets/composites/__init__.py +38 -0
  324. bootstack/widgets/composites/accordion.py +385 -0
  325. bootstack/widgets/composites/appshell.py +445 -0
  326. bootstack/widgets/composites/buttongroup.py +391 -0
  327. bootstack/widgets/composites/calendar.py +914 -0
  328. bootstack/widgets/composites/compositeframe.py +282 -0
  329. bootstack/widgets/composites/contextmenu.py +1754 -0
  330. bootstack/widgets/composites/dateentry.py +261 -0
  331. bootstack/widgets/composites/dropdownbutton.py +190 -0
  332. bootstack/widgets/composites/expander.py +508 -0
  333. bootstack/widgets/composites/field.py +448 -0
  334. bootstack/widgets/composites/floodgauge.py +434 -0
  335. bootstack/widgets/composites/form.py +983 -0
  336. bootstack/widgets/composites/labeledscale.py +209 -0
  337. bootstack/widgets/composites/list/__init__.py +15 -0
  338. bootstack/widgets/composites/list/listitem.py +733 -0
  339. bootstack/widgets/composites/list/listview.py +1507 -0
  340. bootstack/widgets/composites/menubar.py +303 -0
  341. bootstack/widgets/composites/meter.py +882 -0
  342. bootstack/widgets/composites/numericentry.py +183 -0
  343. bootstack/widgets/composites/pagestack.py +330 -0
  344. bootstack/widgets/composites/passwordentry.py +149 -0
  345. bootstack/widgets/composites/pathentry.py +223 -0
  346. bootstack/widgets/composites/radiogroup.py +466 -0
  347. bootstack/widgets/composites/scrolledtext.py +388 -0
  348. bootstack/widgets/composites/scrolledtext.pyi +186 -0
  349. bootstack/widgets/composites/scrollview.py +675 -0
  350. bootstack/widgets/composites/selectbox.py +544 -0
  351. bootstack/widgets/composites/sidenav/__init__.py +24 -0
  352. bootstack/widgets/composites/sidenav/group.py +485 -0
  353. bootstack/widgets/composites/sidenav/header.py +83 -0
  354. bootstack/widgets/composites/sidenav/item.py +413 -0
  355. bootstack/widgets/composites/sidenav/separator.py +51 -0
  356. bootstack/widgets/composites/sidenav/view.py +919 -0
  357. bootstack/widgets/composites/spinnerentry.py +232 -0
  358. bootstack/widgets/composites/tableview/__init__.py +5 -0
  359. bootstack/widgets/composites/tableview/tableview.py +2254 -0
  360. bootstack/widgets/composites/tableview/types.py +169 -0
  361. bootstack/widgets/composites/tabs/__init__.py +6 -0
  362. bootstack/widgets/composites/tabs/tabitem.py +372 -0
  363. bootstack/widgets/composites/tabs/tabs.py +478 -0
  364. bootstack/widgets/composites/tabs/tabview.py +352 -0
  365. bootstack/widgets/composites/textentry.py +90 -0
  366. bootstack/widgets/composites/timeentry.py +189 -0
  367. bootstack/widgets/composites/toast.py +364 -0
  368. bootstack/widgets/composites/togglegroup.py +382 -0
  369. bootstack/widgets/composites/toolbar.py +393 -0
  370. bootstack/widgets/composites/tooltip.py +404 -0
  371. bootstack/widgets/internal/__init__.py +0 -0
  372. bootstack/widgets/internal/wrapper_base.py +304 -0
  373. bootstack/widgets/mixins/__init__.py +25 -0
  374. bootstack/widgets/mixins/configure_mixin.py +186 -0
  375. bootstack/widgets/mixins/entry_mixin.py +70 -0
  376. bootstack/widgets/mixins/font_mixin.py +346 -0
  377. bootstack/widgets/mixins/icon_mixin.py +38 -0
  378. bootstack/widgets/mixins/localization_mixin.py +255 -0
  379. bootstack/widgets/mixins/signal_mixin.py +272 -0
  380. bootstack/widgets/mixins/validation_mixin.py +204 -0
  381. bootstack/widgets/parts/__init__.py +11 -0
  382. bootstack/widgets/parts/numberentry_part.py +345 -0
  383. bootstack/widgets/parts/spinnerentry_part.py +394 -0
  384. bootstack/widgets/parts/textentry_part.py +344 -0
  385. bootstack/widgets/primitives/__init__.py +55 -0
  386. bootstack/widgets/primitives/badge.py +44 -0
  387. bootstack/widgets/primitives/button.py +89 -0
  388. bootstack/widgets/primitives/card.py +66 -0
  389. bootstack/widgets/primitives/checkbutton.py +124 -0
  390. bootstack/widgets/primitives/checktoggle.py +53 -0
  391. bootstack/widgets/primitives/combobox.py +165 -0
  392. bootstack/widgets/primitives/entry.py +98 -0
  393. bootstack/widgets/primitives/frame.py +206 -0
  394. bootstack/widgets/primitives/gridframe.py +479 -0
  395. bootstack/widgets/primitives/label.py +95 -0
  396. bootstack/widgets/primitives/labelframe.py +63 -0
  397. bootstack/widgets/primitives/menubutton.py +118 -0
  398. bootstack/widgets/primitives/notebook.py +551 -0
  399. bootstack/widgets/primitives/optionmenu.py +248 -0
  400. bootstack/widgets/primitives/packframe.py +228 -0
  401. bootstack/widgets/primitives/panedwindow.py +58 -0
  402. bootstack/widgets/primitives/progressbar.py +95 -0
  403. bootstack/widgets/primitives/radiobutton.py +115 -0
  404. bootstack/widgets/primitives/radiotoggle.py +50 -0
  405. bootstack/widgets/primitives/scale.py +85 -0
  406. bootstack/widgets/primitives/scrollbar.py +56 -0
  407. bootstack/widgets/primitives/separator.py +56 -0
  408. bootstack/widgets/primitives/sizegrip.py +47 -0
  409. bootstack/widgets/primitives/spinbox.py +91 -0
  410. bootstack/widgets/primitives/switch.py +41 -0
  411. bootstack/widgets/primitives/treeview.py +77 -0
  412. bootstack/widgets/types.py +20 -0
  413. bootstack-0.1.0a1.dist-info/METADATA +196 -0
  414. bootstack-0.1.0a1.dist-info/RECORD +419 -0
  415. bootstack-0.1.0a1.dist-info/WHEEL +5 -0
  416. bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
  417. bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
  418. bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
  419. bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,983 @@
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.widgets.primitives.button import Button
12
+ from bootstack.widgets.primitives.checkbutton import CheckButton
13
+ from bootstack.widgets.primitives.switch import Switch
14
+ from bootstack.widgets.composites.dateentry import DateEntry
15
+ from bootstack.widgets.composites.field import Field
16
+ from bootstack.widgets.primitives.frame import Frame
17
+ from bootstack.widgets.primitives.label import Label
18
+ from bootstack.widgets.primitives.labelframe import LabelFrame
19
+ from bootstack.widgets.mixins import configure_delegate
20
+ from bootstack.widgets.primitives.notebook import Notebook
21
+ from bootstack.widgets.composites.numericentry import NumericEntry
22
+ from bootstack.widgets.composites.passwordentry import PasswordEntry
23
+ from bootstack.widgets.primitives.scale import Scale
24
+ from bootstack.widgets.composites.selectbox import SelectBox
25
+ from bootstack.widgets.primitives.spinbox import Spinbox
26
+ from bootstack.widgets.composites.textentry import TextEntry
27
+ from bootstack.widgets.mixins.validation_mixin import ValidationMixin
28
+ from bootstack.widgets.types import Master
29
+
30
+ if TYPE_CHECKING:
31
+ from bootstack.dialogs.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
+ EditorType = Literal[
37
+ 'selectbox',
38
+ 'combobox',
39
+ 'spinbox',
40
+ 'text',
41
+ 'textentry',
42
+ 'numericentry',
43
+ 'dateentry',
44
+ 'passwordentry',
45
+ 'toggle',
46
+ 'switch',
47
+ 'checkbutton',
48
+ 'scale',
49
+ ]
50
+
51
+
52
+ @dataclass
53
+ class FieldItem:
54
+ """Field definition used by Form."""
55
+ key: str
56
+ label: str | None = None
57
+ dtype: DType = None
58
+ readonly: bool = False
59
+ visible: bool = True
60
+ column: int | None = None
61
+ row: int | None = None
62
+ columnspan: int = 1
63
+ rowspan: int = 1
64
+ editor: EditorType | None = None
65
+ editor_options: dict[str, Any] = field(default_factory=dict)
66
+ type: Literal['field'] = "field"
67
+
68
+
69
+ @dataclass
70
+ class GroupItem:
71
+ """Grouping of field items laid out in a grid with optional label/padding."""
72
+ items: list[FieldItem | Mapping[str, Any] | GroupItem | TabsItem] = field(default_factory=list)
73
+ label: str | None = None
74
+ col_count: int = 1
75
+ min_col_width: int = DEFAULT_MIN_COL_WIDTH
76
+ width: int | None = None
77
+ height: int | None = None
78
+ column: int | None = None
79
+ row: int | None = None
80
+ columnspan: int = 1
81
+ rowspan: int = 1
82
+ padding: int | str | tuple[int, int] | tuple[int, int, int, int] | None = 8
83
+ type: Literal['group'] = "group"
84
+
85
+
86
+ @dataclass
87
+ class TabItem:
88
+ """Single tab within a TabsItem."""
89
+ label: str
90
+ items: list[FieldItem | Mapping[str, Any] | GroupItem | TabsItem] = field(default_factory=list)
91
+ padding: int | str | tuple[int, int] | tuple[int, int, int, int] | None = 8
92
+
93
+
94
+ @dataclass
95
+ class TabsItem:
96
+ """Notebook container with one or more TabItem entries."""
97
+ tabs: list[TabItem | Mapping[str, Any]] = field(default_factory=list)
98
+ label: str | None = None
99
+ width: int | None = None
100
+ height: int | None = None
101
+ column: int | None = None
102
+ row: int | None = None
103
+ columnspan: int = 1
104
+ rowspan: int = 1
105
+ type: Literal['tabs'] = "tabs"
106
+
107
+
108
+ FormItem = FieldItem | GroupItem | TabsItem
109
+
110
+
111
+ class Form(Frame):
112
+ """A configurable form that can be generated from data or explicit items.
113
+
114
+ Form is a *field manager* that provides a domain-specific API for accessing
115
+ and manipulating form fields and their values.
116
+
117
+ Field Access:
118
+ - `field(key)` — returns the Field widget for a key
119
+ - `fields()` — returns all Field widgets in order
120
+ - `keys()` — returns all field keys in order
121
+
122
+ Value Operations:
123
+ - `get_field_value(key)` — get a single field's value
124
+ - `set_field_value(key, value)` — set a single field's value
125
+ - `get()` / `set(values)` — get/set all values as a dict
126
+ - `value` property — get/set all values as a dict
127
+
128
+ Variable & Signal Access:
129
+ - `field_variable(key)` — get Tk Variable for a field
130
+ - `field_signal(key)` — get Signal for a field's value
131
+ - `field_textsignal(key)` — get Signal for a field's text
132
+
133
+ Attributes:
134
+ data (dict): Current form data (read-only property).
135
+ value (dict): Alias for form data (get/set property).
136
+ result (Any): Result value set by button commands.
137
+
138
+ Args:
139
+ master: Parent widget.
140
+ data: Initial data backing the form. If items are not provided,
141
+ field items are inferred from the keys and value types.
142
+ items: Optional explicit form definition. Accepts dictionaries that
143
+ match the FieldItem/GroupItem/TabsItem shapes or the dataclass
144
+ instances directly.
145
+ col_count: Number of columns at the top level.
146
+ min_col_width: Minimum width for each column in pixels.
147
+ on_data_changed: Optional callback invoked with the updated data dict
148
+ whenever a field value changes.
149
+ width: Requested width for the form container.
150
+ height: Requested height for the form container.
151
+ accent: Accent token for the form container (e.g., 'primary', 'secondary').
152
+ buttons: Optional footer buttons. Accepts plain strings, DialogButton
153
+ instances, or dictionaries that map to DialogButton kwargs.
154
+ **kwargs: Additional Frame configuration options.
155
+ """
156
+
157
+ def __init__(
158
+ self,
159
+ master: Master = None,
160
+ *,
161
+ data: dict[str, Any] | None = None,
162
+ items: Sequence[FormItem | Mapping[str, Any]] | None = None,
163
+ col_count: int = 1,
164
+ min_col_width: int = DEFAULT_MIN_COL_WIDTH,
165
+ on_data_changed: Callable[[dict[str, Any]], Any] | None = None,
166
+ width: int | None = None,
167
+ height: int | None = None,
168
+ accent: str | None = None,
169
+ buttons: Sequence[ButtonInput] | None = None,
170
+ **kwargs: Any,
171
+ ) -> None:
172
+ """Build a configurable form from data or explicit items.
173
+
174
+ Args:
175
+ master: Parent widget.
176
+ data: Initial data backing the form; keys become field names.
177
+ items: Explicit form layout (FieldItem/GroupItem/TabsItem or mappings).
178
+ col_count: Number of columns at the top level.
179
+ min_col_width: Minimum width per column in pixels.
180
+ on_data_changed: Callback invoked with updated data when a field changes.
181
+ width: Requested form width; if None, size naturally.
182
+ height: Requested form height; if None, size naturally.
183
+ accent: Accent token for the form container.
184
+ buttons: Optional footer buttons (DialogButton, mapping, or string).
185
+ **kwargs: Additional Frame configuration options.
186
+ """
187
+ # Support legacy bootstyle parameter
188
+ if 'bootstyle' in kwargs:
189
+ accent = accent or kwargs.pop('bootstyle')
190
+ super().__init__(master=master, width=width, height=height, accent=accent, **kwargs)
191
+
192
+ self._data: dict[str, Any] = dict(data) if data else {}
193
+ self.result: Any = None
194
+ self._on_data_changed = on_data_changed
195
+ self._col_count = col_count
196
+ self._min_col_width = min_col_width
197
+ self._widgets: dict[str, Any] = {}
198
+ self._variables: dict[str, Variable] = {}
199
+ self._signals: dict[str, Any] = {}
200
+ self._textsignals: dict[str, Any] = {}
201
+ self._items_by_key: dict[str, FieldItem] = {}
202
+ self._suspend_sync = False
203
+
204
+ normalized_items = self._normalize_items(items or self._infer_items_from_data(self._data))
205
+
206
+ self.columnconfigure(0, weight=1)
207
+ self.rowconfigure(0, weight=1)
208
+
209
+ container = Frame(self)
210
+ container.grid(row=0, column=0, sticky='nsew')
211
+ self._content_frame = Frame(container)
212
+ self._content_frame.pack(fill='both', expand=True)
213
+
214
+ # Respect explicit width/height by preventing geometry propagation.
215
+ if width or height:
216
+ if width:
217
+ self.configure(width=width)
218
+ container.configure(width=width)
219
+ if height:
220
+ self.configure(height=height)
221
+ container.configure(height=height)
222
+ self.grid_propagate(False)
223
+ self.pack_propagate(False)
224
+ container.grid_propagate(False)
225
+
226
+ self._build_items(
227
+ self._content_frame, normalized_items, col_count=self._col_count, min_col_width=self._min_col_width)
228
+
229
+ if buttons:
230
+ footer = Frame(self)
231
+ footer.grid(row=1, column=0, sticky='ew', pady=(8, 0))
232
+ footer.columnconfigure(0, weight=1)
233
+ self._build_buttons(footer, buttons)
234
+
235
+ @property
236
+ def data(self) -> dict[str, Any]:
237
+ """Current data backing the form."""
238
+ return dict(self._collect_data())
239
+
240
+ def validate(self) -> bool:
241
+ """Run validation rules on all field widgets; returns True if all pass."""
242
+ all_valid = True
243
+ first_invalid_widget = None
244
+
245
+ def _validate_field(widget: Field) -> bool:
246
+ entry = getattr(widget, "_entry", widget)
247
+ rules = getattr(entry, "_rules", [])
248
+ if not rules:
249
+ return True
250
+ value = widget.value
251
+ payload: dict[str, Any] = {"value": value, "is_valid": True, "message": ""}
252
+ is_valid = True
253
+ for rule in rules:
254
+ if rule.trigger not in ("always", "manual"):
255
+ continue
256
+ result = rule.validate(value)
257
+ payload.update(is_valid=result.is_valid, message=result.message)
258
+ if not result.is_valid:
259
+ is_valid = False
260
+ try:
261
+ entry.event_generate(ValidationMixin.EVENT_INVALID, data=payload)
262
+ entry.event_generate(ValidationMixin.EVENT_VALIDATED, data=payload)
263
+ except Exception:
264
+ pass
265
+ break
266
+ if is_valid:
267
+ try:
268
+ entry.event_generate(ValidationMixin.EVENT_VALID, data=payload)
269
+ entry.event_generate(ValidationMixin.EVENT_VALIDATED, data=payload)
270
+ except Exception:
271
+ pass
272
+ return is_valid
273
+
274
+ for widget in self._widgets.values():
275
+ if isinstance(widget, Field):
276
+ ok = _validate_field(widget)
277
+ if not ok and first_invalid_widget is None:
278
+ first_invalid_widget = widget
279
+ all_valid = all_valid and ok
280
+ if first_invalid_widget:
281
+ try:
282
+ first_invalid_widget.focus_set()
283
+ except Exception:
284
+ pass
285
+ return all_valid
286
+
287
+ # --- Field access API (v2) -------------------------------------------
288
+
289
+ def field(self, key: str) -> Field:
290
+ """Return the Field widget for the given key.
291
+
292
+ Args:
293
+ key: The field key.
294
+
295
+ Returns:
296
+ The Field widget instance.
297
+
298
+ Raises:
299
+ KeyError: If no field with the given key exists.
300
+ """
301
+ if key not in self._widgets:
302
+ raise KeyError(f"No field with key '{key}'")
303
+ return self._widgets[key]
304
+
305
+ def fields(self) -> tuple[Field, ...]:
306
+ """Return all field widgets in insertion order.
307
+
308
+ Returns:
309
+ Tuple of Field widget instances.
310
+ """
311
+ return tuple(self._widgets[k] for k in self._items_by_key.keys() if k in self._widgets)
312
+
313
+ def keys(self) -> tuple[str, ...]:
314
+ """Return all field keys in insertion order.
315
+
316
+ Returns:
317
+ Tuple of field key strings.
318
+ """
319
+ return tuple(self._items_by_key.keys())
320
+
321
+ # --- Value helpers (explicit, no overloading) -------------------------
322
+
323
+ def get_field_value(self, key: str) -> Any:
324
+ """Return the current value of the field.
325
+
326
+ Args:
327
+ key: The field key.
328
+
329
+ Returns:
330
+ The current field value.
331
+
332
+ Raises:
333
+ KeyError: If no field with the given key exists.
334
+ """
335
+ if key not in self._widgets:
336
+ raise KeyError(f"No field with key '{key}'")
337
+ return self._read_value_from_widget(key)
338
+
339
+ def set_field_value(self, key: str, value: Any) -> None:
340
+ """Set the value of the field.
341
+
342
+ Args:
343
+ key: The field key.
344
+ value: The new value to set.
345
+
346
+ Raises:
347
+ KeyError: If no field with the given key exists.
348
+ """
349
+ if key not in self._items_by_key:
350
+ raise KeyError(f"No field with key '{key}'")
351
+ item = self._items_by_key[key]
352
+ self._apply_value_to_widget(key, item, value)
353
+ self._data[key] = value
354
+
355
+ # --- Variable & signal accessors (v2 short names) ---------------------
356
+
357
+ def field_variable(self, key: str) -> Variable | None:
358
+ """Return the Tk Variable for the field.
359
+
360
+ Args:
361
+ key: The field key.
362
+
363
+ Returns:
364
+ The Tk Variable, or None if not available.
365
+ """
366
+ return self._variables.get(key)
367
+
368
+ def field_signal(self, key: str):
369
+ """Return the Signal for the field value.
370
+
371
+ Args:
372
+ key: The field key.
373
+
374
+ Returns:
375
+ The Signal, or None if not available.
376
+ """
377
+ return self._signals.get(key)
378
+
379
+ def field_textsignal(self, key: str):
380
+ """Return the Signal for the field text.
381
+
382
+ Args:
383
+ key: The field key.
384
+
385
+ Returns:
386
+ The text Signal, or None if not available.
387
+ """
388
+ return self._textsignals.get(key)
389
+
390
+ # --- Legacy variable/signal accessors (backward compatibility) --------
391
+
392
+ def get_field_variable(self, key: str) -> Variable | None:
393
+ """Return the Tk variable associated with a field key, if any.
394
+
395
+ Deprecated:
396
+ Use `field_variable(key)` instead.
397
+ """
398
+ return self.field_variable(key)
399
+
400
+ def get_field_signal(self, key: str):
401
+ """Return the Signal associated with a field key, if any.
402
+
403
+ Deprecated:
404
+ Use `field_signal(key)` instead.
405
+ """
406
+ return self.field_signal(key)
407
+
408
+ def get_field_textsignal(self, key: str):
409
+ """Return the TextSignal associated with a field key, if any.
410
+
411
+ Deprecated:
412
+ Use `field_textsignal(key)` instead.
413
+ """
414
+ return self.field_textsignal(key)
415
+
416
+ # --- Form-level value API (v2 standardization) ------------------------
417
+
418
+ def get(self) -> dict[str, Any]:
419
+ """Return all field values as a dictionary.
420
+
421
+ Returns:
422
+ Dictionary mapping field keys to their current values.
423
+ """
424
+ return self.data
425
+
426
+ def set(self, values: Mapping[str, Any]) -> None:
427
+ """Set multiple field values from a dictionary.
428
+
429
+ Args:
430
+ values: Dictionary mapping field keys to values.
431
+ """
432
+ self._data = dict(values)
433
+ self._suspend_sync = True
434
+ try:
435
+ for key, item in self._items_by_key.items():
436
+ value = self._data.get(key)
437
+ self._apply_value_to_widget(key, item, value)
438
+ finally:
439
+ self._suspend_sync = False
440
+
441
+ @property
442
+ def value(self) -> dict[str, Any]:
443
+ """Get or set all form field values as a dictionary."""
444
+ return self.get()
445
+
446
+ @value.setter
447
+ def value(self, values: Mapping[str, Any]) -> None:
448
+ self.set(values)
449
+
450
+ @configure_delegate('data')
451
+ def _delegate_data(self, value: Mapping[str, Any] = None):
452
+ if value is None:
453
+ return dict(self._collect_data())
454
+ else:
455
+ self._data = dict(value)
456
+ self._suspend_sync = True
457
+ try:
458
+ for key, item in self._items_by_key.items():
459
+ value = self._data.get(key)
460
+ self._apply_value_to_widget(key, item, value)
461
+ finally:
462
+ self._suspend_sync = False
463
+ return None
464
+
465
+ def _build_items(self, parent: Frame, items: Sequence[FormItem], *, col_count: int, min_col_width: int) -> None:
466
+ for col in range(col_count):
467
+ parent.columnconfigure(col, weight=1, minsize=min_col_width)
468
+
469
+ auto_row = 0
470
+ auto_col = 0
471
+
472
+ for item in items:
473
+ widget = None
474
+ columnspan = 1
475
+ rowspan = 1
476
+
477
+ if isinstance(item, FieldItem):
478
+ widget = self._build_field(parent, item)
479
+ columnspan = item.columnspan
480
+ rowspan = item.rowspan
481
+ elif isinstance(item, GroupItem):
482
+ container = LabelFrame(parent, text=item.label, padding=item.padding) if item.label else Frame(
483
+ parent, padding=item.padding)
484
+ if item.width:
485
+ container.configure(width=item.width)
486
+ if item.height:
487
+ container.configure(height=item.height)
488
+ nested_items = self._normalize_items(item.items)
489
+ self._build_items(
490
+ container,
491
+ nested_items,
492
+ col_count=item.col_count or col_count,
493
+ min_col_width=item.min_col_width or min_col_width,
494
+ )
495
+ widget = container
496
+ columnspan = item.columnspan
497
+ rowspan = item.rowspan
498
+ elif isinstance(item, TabsItem):
499
+ notebook = Notebook(parent, width=item.width, height=item.height)
500
+ for tab in self._normalize_tabs(item.tabs):
501
+ tab_frame = Frame(notebook, padding=tab.padding)
502
+ self._build_items(
503
+ tab_frame, self._normalize_items(tab.items), col_count=col_count, min_col_width=min_col_width)
504
+ notebook.add(tab_frame, text=tab.label)
505
+ widget = notebook
506
+ columnspan = item.columnspan
507
+ rowspan = item.rowspan
508
+
509
+ if widget is None:
510
+ continue
511
+
512
+ row = item.row if isinstance(item, (FieldItem, GroupItem, TabsItem)) and item.row is not None else auto_row
513
+ column = item.column if isinstance(
514
+ item, (FieldItem, GroupItem, TabsItem)) and item.column is not None else auto_col
515
+
516
+ widget.grid(row=row, column=column, columnspan=columnspan, rowspan=rowspan, sticky='nsew', padx=6, pady=4)
517
+
518
+ if isinstance(item, (FieldItem, GroupItem, TabsItem)) and item.row is None and item.column is None:
519
+ auto_col += columnspan
520
+ if auto_col >= col_count:
521
+ auto_col = 0
522
+ auto_row += 1
523
+
524
+ def _build_field(self, parent: Frame, item: FieldItem):
525
+ if not item.visible:
526
+ return None
527
+
528
+ editor = item.editor or self._default_editor_for_dtype(item.dtype, self._data.get(item.key))
529
+ options = dict(item.editor_options or {})
530
+ initial_value = self._data.get(item.key)
531
+ variable = self._variable_for_item(item, initial_value, editor)
532
+ label_text = item.label if item.label is not None else item.key.replace("_", " ").title()
533
+
534
+ container = Frame(parent)
535
+ container.columnconfigure(0, weight=1) # Allow field widgets to expand horizontally
536
+
537
+ # Validation options that should only be passed to widgets supporting ValidationMixin
538
+ validation_options = {'show_message', 'required', 'validator'}
539
+
540
+ field_widget: Any
541
+ if editor == 'textentry':
542
+ field_widget = TextEntry(
543
+ container, value=initial_value or "", label=label_text, textvariable=variable, **options)
544
+ elif editor == 'numericentry':
545
+ numeric_value = initial_value if initial_value is not None else 0
546
+ field_widget = NumericEntry(
547
+ container, value=numeric_value, label=label_text, **options)
548
+ self._bind_numeric_variable(item.key, field_widget, variable)
549
+ elif editor == 'passwordentry':
550
+ field_widget = PasswordEntry(
551
+ container, value=initial_value or "", label=label_text, textvariable=variable, **options)
552
+ elif editor == 'dateentry':
553
+ field_widget = DateEntry(container, value=initial_value, label=label_text, textvariable=variable, **options)
554
+ else:
555
+ # Filter out validation options for widgets that don't support ValidationMixin
556
+ filtered_options = {k: v for k, v in options.items() if k not in validation_options}
557
+
558
+ # Use inline label for checkbutton/toggle/switch, otherwise show a Label widget.
559
+ if editor in ("checkbutton", "toggle", "switch"):
560
+ if not filtered_options.get("text"):
561
+ filtered_options["text"] = label_text
562
+ elif label_text != "" and editor not in ('selectbox', 'combobox'):
563
+ Label(container, text=label_text).pack(anchor='w', pady=(0, 2))
564
+
565
+ if editor in ('selectbox', 'combobox'):
566
+ items = options.pop('items', options.pop('values', None)) or []
567
+ items = [str(i) for i in items]
568
+ field_widget = SelectBox(
569
+ container,
570
+ label=label_text,
571
+ value=initial_value or "",
572
+ items=items,
573
+ textvariable=variable,
574
+ **options
575
+ )
576
+ if initial_value is not None and variable is not None:
577
+ variable.set(initial_value)
578
+ elif editor == 'spinbox':
579
+ field_widget = Spinbox(container, textvariable=variable, **filtered_options)
580
+ if initial_value is not None:
581
+ variable.set(initial_value)
582
+ elif editor == 'text':
583
+ field_widget = Text(container, **filtered_options)
584
+ if initial_value:
585
+ field_widget.insert('1.0', str(initial_value))
586
+ elif editor in ('toggle', 'switch'):
587
+ field_widget = Switch(container, variable=variable, **filtered_options)
588
+ elif editor == 'checkbutton':
589
+ field_widget = CheckButton(container, variable=variable, **filtered_options)
590
+ elif editor == 'scale':
591
+ field_widget = Scale(container, variable=variable, **filtered_options)
592
+ else:
593
+ field_widget = TextEntry(
594
+ container, value=initial_value or "", label=label_text, textvariable=variable, **options)
595
+
596
+ if editor != 'text':
597
+ field_widget.pack(fill='x', expand=True)
598
+ else:
599
+ field_widget.pack(fill='both', expand=True)
600
+
601
+ if isinstance(field_widget, Field):
602
+ field_widget.bind("<<Change>>", lambda _e, k=item.key: self._sync_value_from_widget(k))
603
+ field_widget.pack(fill='both', expand=True)
604
+ if not isinstance(field_widget, NumericEntry):
605
+ traced_var = getattr(field_widget, "variable", None)
606
+ if traced_var is not None:
607
+ self._register_variable(item.key, traced_var)
608
+ elif isinstance(field_widget, Text):
609
+ text_var = variable or StringVar(value=str(initial_value) if initial_value is not None else "")
610
+ self._register_variable(item.key, text_var)
611
+ self._bind_text_change(field_widget, item.key, text_var)
612
+ elif variable is not None:
613
+ self._register_variable(item.key, variable)
614
+
615
+ # record signals if the widget exposes them
616
+ signal_obj = getattr(field_widget, "_signal", None)
617
+ if signal_obj is not None:
618
+ self._signals[item.key] = signal_obj
619
+ text_signal = getattr(field_widget, "_textsignal", None)
620
+ if text_signal is not None:
621
+ self._textsignals[item.key] = text_signal
622
+
623
+ if item.readonly:
624
+ self._set_readonly(field_widget)
625
+
626
+ self._widgets[item.key] = field_widget
627
+ return container
628
+
629
+ def _build_buttons(self, parent: Frame, buttons: Sequence[ButtonInput]) -> None:
630
+ parsed = self._normalize_buttons(buttons)
631
+ for spec in reversed(parsed):
632
+ # Support both color and legacy bootstyle from DialogButton
633
+ btn_color = getattr(spec, 'color', None) or spec.bootstyle
634
+ btn_variant = getattr(spec, 'variant', None)
635
+
636
+ if not btn_color:
637
+ # Get color and variant from role
638
+ btn_color, btn_variant = self._style_for_role(spec.role)
639
+
640
+ btn = Button(parent, text=spec.text, accent=btn_color, variant=btn_variant)
641
+ btn.configure(command=self._make_button_command(spec))
642
+ btn.pack(side='right', padx=(4, 0))
643
+
644
+ # --- normalization --------------------------------------------------
645
+ def _normalize_items(self, items: Iterable[FormItem | Mapping[str, Any]]) -> list[FormItem]:
646
+ normalized: list[FormItem] = []
647
+ for raw in items:
648
+ item: FormItem | None = None
649
+ if isinstance(raw, (FieldItem, GroupItem, TabsItem)):
650
+ item = raw
651
+ elif isinstance(raw, Mapping):
652
+ type_hint = raw.get('type', 'field')
653
+ if type_hint == 'group':
654
+ item = GroupItem(
655
+ items=list(raw.get('items', [])),
656
+ label=raw.get('label'),
657
+ col_count=raw.get('col_count', 1),
658
+ min_col_width=raw.get('min_col_width', DEFAULT_MIN_COL_WIDTH),
659
+ width=raw.get('width'),
660
+ height=raw.get('height'),
661
+ column=raw.get('column'),
662
+ row=raw.get('row'),
663
+ columnspan=raw.get('columnspan', 1),
664
+ rowspan=raw.get('rowspan', 1),
665
+ )
666
+ elif type_hint == 'tabs':
667
+ item = TabsItem(
668
+ tabs=list(raw.get('tabs', [])),
669
+ label=raw.get('label'),
670
+ width=raw.get('width'),
671
+ height=raw.get('height'),
672
+ column=raw.get('column'),
673
+ row=raw.get('row'),
674
+ columnspan=raw.get('columnspan', 1),
675
+ rowspan=raw.get('rowspan', 1),
676
+ )
677
+ else:
678
+ key_value = raw.get('key')
679
+ if key_value is None:
680
+ continue
681
+ item = FieldItem(
682
+ key=str(key_value),
683
+ label=raw.get('label'),
684
+ dtype=raw.get('dtype'),
685
+ readonly=raw.get('readonly', False),
686
+ visible=raw.get('visible', True),
687
+ column=raw.get('column'),
688
+ row=raw.get('row'),
689
+ columnspan=raw.get('columnspan', 1),
690
+ rowspan=raw.get('rowspan', 1),
691
+ editor=raw.get('editor'),
692
+ editor_options=dict(raw.get('editor_options', {}) or {}),
693
+ )
694
+
695
+ if isinstance(item, GroupItem):
696
+ item.items = self._normalize_items(item.items)
697
+ if isinstance(item, TabsItem):
698
+ item.tabs = self._normalize_tabs(item.tabs)
699
+ if isinstance(item, FieldItem):
700
+ self._items_by_key[item.key] = item
701
+
702
+ if item:
703
+ normalized.append(item)
704
+ return normalized
705
+
706
+ def _normalize_tabs(self, tabs: Iterable[TabItem | Mapping[str, Any]]) -> list[TabItem]:
707
+ normalized: list[TabItem] = []
708
+ for raw in tabs:
709
+ if isinstance(raw, TabItem):
710
+ normalized.append(raw)
711
+ elif isinstance(raw, Mapping):
712
+ normalized.append(TabItem(label=str(raw.get('label', 'Tab')), items=list(raw.get('items', []))))
713
+ return normalized
714
+
715
+ def _normalize_buttons(self, buttons: Sequence[ButtonInput]) -> list["DialogButton"]:
716
+ from bootstack.dialogs.dialog import DialogButton # local import to avoid circular init
717
+
718
+ normalized: list[DialogButton] = []
719
+ for raw in buttons:
720
+ if isinstance(raw, DialogButton):
721
+ normalized.append(raw)
722
+ elif isinstance(raw, Mapping):
723
+ normalized.append(DialogButton(**raw)) # type: ignore[arg-type]
724
+ elif isinstance(raw, str):
725
+ normalized.append(DialogButton(text=raw, role="primary" if not normalized else "secondary"))
726
+ return normalized
727
+
728
+ # --- data helpers ---------------------------------------------------
729
+ def _collect_data(self) -> dict[str, Any]:
730
+ current: dict[str, Any] = dict(self._data)
731
+ for key in self._widgets.keys():
732
+ current[key] = self._read_value_from_widget(key)
733
+ return current
734
+
735
+ def _read_value_from_widget(self, key: str) -> Any:
736
+ widget = self._widgets.get(key)
737
+ if widget is None:
738
+ return self._data.get(key)
739
+
740
+ if hasattr(widget, "value"):
741
+ val_attr = getattr(widget, "value")
742
+ value = val_attr() if callable(val_attr) else val_attr
743
+ elif key in self._variables:
744
+ value = self._variables[key].get()
745
+ elif isinstance(widget, Text):
746
+ value = widget.get("1.0", "end-1c")
747
+ else:
748
+ try:
749
+ value = widget.get()
750
+ except Exception:
751
+ value = self._data.get(key)
752
+
753
+ item = self._items_by_key.get(key)
754
+ if item:
755
+ value = self._coerce_value(item.dtype, value)
756
+ return value
757
+
758
+ def _apply_value_to_widget(self, key: str, item: FieldItem, value: Any) -> None:
759
+ widget = self._widgets.get(key)
760
+ if widget is None:
761
+ return
762
+
763
+ if isinstance(widget, Field):
764
+ if hasattr(widget, "_suppress_changed_event"):
765
+ widget._suppress_changed_event = True # type: ignore[attr-defined]
766
+ try:
767
+ widget.value = value
768
+ finally:
769
+ widget._suppress_changed_event = False # type: ignore[attr-defined]
770
+ else:
771
+ widget.value = value
772
+ return
773
+
774
+ if isinstance(widget, Text):
775
+ widget.delete("1.0", "end")
776
+ if value is not None:
777
+ widget.insert("1.0", str(value))
778
+ return
779
+
780
+ if key in self._variables:
781
+ self._variables[key].set("" if value is None else value)
782
+ return
783
+
784
+ try:
785
+ widget.configure(value=value)
786
+ except Exception:
787
+ pass
788
+
789
+ def _sync_value_from_widget(self, key: str) -> None:
790
+ if self._suspend_sync:
791
+ return
792
+ if key not in self._items_by_key:
793
+ return
794
+ new_value = self._read_value_from_widget(key)
795
+ self._data[key] = new_value
796
+ if self._on_data_changed:
797
+ self._on_data_changed(dict(self._data))
798
+
799
+ def _variable_for_item(self, item: FieldItem, initial: Any, editor: EditorType | None) -> Variable | None:
800
+ dtype = item.dtype
801
+ if editor in ('checkbutton', 'toggle'):
802
+ return BooleanVar(value=bool(initial) if initial is not None else False)
803
+ if editor in ('numericentry', 'spinbox', 'scale') or dtype in ('int', int, 'float', float):
804
+ if dtype in ('float', float):
805
+ return DoubleVar(value=float(initial) if initial is not None else 0.0)
806
+ return IntVar(value=int(initial) if initial is not None else 0)
807
+ return StringVar(value="" if initial is None else str(initial))
808
+
809
+ def _default_editor_for_dtype(self, dtype: Any, value: Any) -> EditorType:
810
+ if dtype in ('int', int, 'float', float):
811
+ return 'numericentry'
812
+ if dtype in ('bool', bool):
813
+ return 'checkbutton'
814
+ if dtype in ('date', 'datetime', date, datetime):
815
+ return 'dateentry'
816
+ if dtype in ('password',):
817
+ return 'passwordentry'
818
+ if value is not None:
819
+ if isinstance(value, (int, float)):
820
+ return 'numericentry'
821
+ if isinstance(value, (bool,)):
822
+ return 'checkbutton'
823
+ if isinstance(value, (date, datetime)):
824
+ return 'dateentry'
825
+ return 'textentry'
826
+
827
+ def _coerce_value(self, dtype: Any, value: Any) -> Any:
828
+ if dtype in ('int', int):
829
+ try:
830
+ return int(value)
831
+ except Exception:
832
+ return value
833
+ if dtype in ('float', float):
834
+ try:
835
+ return float(value)
836
+ except Exception:
837
+ return value
838
+ if dtype in ('bool', bool):
839
+ return bool(value)
840
+ if dtype in ('date', 'datetime', date, datetime):
841
+ return value
842
+ return value
843
+
844
+ def _register_variable(self, key: str, var: Variable) -> None:
845
+ self._variables[key] = var
846
+ var.trace_add("write", lambda *_a, k=key: self._sync_value_from_widget(k))
847
+
848
+ def _bind_text_change(self, widget: Text, key: str, var: StringVar | None = None) -> None:
849
+ _updating = {"text": False}
850
+
851
+ def _on_change(_event=None):
852
+ if _updating["text"]:
853
+ return
854
+ _updating["text"] = True
855
+ try:
856
+ widget.edit_modified(False)
857
+ text = widget.get("1.0", "end-1c")
858
+ if var is not None:
859
+ var.set(text)
860
+ self._sync_value_from_widget(key)
861
+ finally:
862
+ _updating["text"] = False
863
+
864
+ def _on_var_change(*_args):
865
+ if _updating["text"] or var is None:
866
+ return
867
+ _updating["text"] = True
868
+ try:
869
+ widget.delete("1.0", "end")
870
+ widget.insert("1.0", var.get())
871
+ finally:
872
+ _updating["text"] = False
873
+
874
+ widget.bind("<<Modified>>", _on_change)
875
+ widget.edit_modified(False)
876
+
877
+ if var is not None:
878
+ var.trace_add("write", _on_var_change)
879
+
880
+ def _bind_numeric_variable(self, key: str, widget: NumericEntry, var: Variable | None) -> None:
881
+ if var is None:
882
+ return
883
+
884
+ self._register_variable(key, var)
885
+
886
+ def _sync_numeric_var(*_args):
887
+ new_value = widget.value
888
+ if new_value is None:
889
+ return
890
+ try:
891
+ current_value = var.get()
892
+ except Exception:
893
+ current_value = None
894
+ if current_value == new_value:
895
+ return
896
+ previous_suspend = self._suspend_sync
897
+ self._suspend_sync = True
898
+ try:
899
+ var.set(new_value)
900
+ finally:
901
+ self._suspend_sync = previous_suspend
902
+
903
+ text_signal = getattr(widget, "signal", None)
904
+ if text_signal is not None:
905
+ text_signal.subscribe(lambda *_: _sync_numeric_var())
906
+ _sync_numeric_var()
907
+
908
+ def _set_readonly(self, widget: Any) -> None:
909
+ if isinstance(widget, Field):
910
+ widget.readonly(True)
911
+ else:
912
+ try:
913
+ widget.state(['disabled'])
914
+ except Exception:
915
+ try:
916
+ widget.configure(state='disabled')
917
+ except Exception:
918
+ pass
919
+
920
+ # --- button helpers -------------------------------------------------
921
+ def _make_button_command(self, spec: DialogButton):
922
+ def command():
923
+ if spec.command:
924
+ spec.command(self) # type: ignore[arg-type]
925
+ self.result = spec.result if spec.result is not None else self.data
926
+
927
+ return command
928
+
929
+ def _style_for_role(self, role: str) -> tuple[str, str | None]:
930
+ """Get color and variant for a button role.
931
+
932
+ Returns:
933
+ Tuple of (color, variant) for the role.
934
+ """
935
+ if role == "primary":
936
+ return ("primary", None)
937
+ if role == "secondary":
938
+ return ("secondary", None)
939
+ if role == "danger":
940
+ return ("danger", None)
941
+ if role == "cancel":
942
+ return ("secondary", "outline")
943
+ if role == "help":
944
+ return ("info", "link")
945
+ return ("secondary", None)
946
+
947
+ # --- inference ------------------------------------------------------
948
+ def _infer_items_from_data(self, data: Mapping[str, Any]) -> list[FieldItem]:
949
+ inferred: list[FieldItem] = []
950
+ for key, value in data.items():
951
+ inferred.append(
952
+ FieldItem(
953
+ key=str(key),
954
+ label=str(key).replace('_', ' ').title(),
955
+ dtype=self._infer_dtype_from_value(value),
956
+ editor=self._default_editor_for_dtype(self._infer_dtype_from_value(value), value),
957
+ editor_options={"show_message": True},
958
+ )
959
+ )
960
+ return inferred
961
+
962
+ @staticmethod
963
+ def _infer_dtype_from_value(value: Any) -> DType:
964
+ if isinstance(value, bool):
965
+ return 'bool'
966
+ if isinstance(value, int) and not isinstance(value, bool):
967
+ return 'int'
968
+ if isinstance(value, float):
969
+ return 'float'
970
+ if isinstance(value, (date, datetime)):
971
+ return 'date'
972
+ return 'str'
973
+
974
+
975
+ __all__ = [
976
+ "Form",
977
+ "FormItem",
978
+ "FieldItem",
979
+ "GroupItem",
980
+ "TabsItem",
981
+ "TabItem",
982
+ "EditorType",
983
+ ]