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,541 @@
1
+ """FormDialog - A dialog that embeds a Form widget for data entry.
2
+
3
+ This module provides FormDialog, which combines the Dialog and Form widgets
4
+ to create modal or non-modal dialogs for structured data entry.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from tkinter import Widget
10
+ from typing import Any, Callable, Iterable, Literal, Mapping, Optional, Sequence, Tuple, Union, TYPE_CHECKING
11
+
12
+ if TYPE_CHECKING:
13
+ from bootstack.widgets.composites.form import FormItem
14
+
15
+ from bootstack.dialogs.dialog import Dialog, DialogButton, ButtonSpec
16
+ from bootstack.widgets.primitives.frame import Frame
17
+ from bootstack.widgets.types import Master
18
+ from bootstack.constants import DEFAULT_MIN_COL_WIDTH as FORM_MIN_COL_WIDTH
19
+ from bootstack.runtime.window_utilities import AnchorPoint
20
+
21
+
22
+ class FormDialog:
23
+ """A dialog window that embeds a Form widget for structured data entry.
24
+
25
+ FormDialog combines the Dialog and Form widgets to create modal or non-modal
26
+ dialogs for data entry with automatic field generation or explicit layouts.
27
+
28
+ Attributes:
29
+ result: The form data returned after closing (dict), or None if cancelled.
30
+ form: The embedded Form widget instance, accessible for advanced usage.
31
+
32
+ Args:
33
+ master: Parent widget. If None, uses the default root window.
34
+ title: Dialog window title. Defaults to "Form".
35
+ data: Initial data backing the form. Keys become field names.
36
+ items: Optional explicit form definition using FieldItem/GroupItem/TabsItem.
37
+ If not provided, fields are inferred from data keys and types.
38
+ col_count: Number of columns for form layout. Defaults to 1.
39
+ min_col_width: Minimum width for each column in pixels. Defaults to the
40
+ shared form default.
41
+ on_data_changed: Optional callback invoked when any field value changes.
42
+ Receives the updated data dict as parameter.
43
+ width: Requested width for the form. Defaults to None (auto-size).
44
+ height: Requested height for the form. Defaults to None (auto-size).
45
+ scrollable: Deprecated; FormDialog manages scrolling internally.
46
+ scrollview_options: Additional options passed to ScrollView when scrollable is True.
47
+ buttons: Footer buttons. Can be DialogButton instances, dicts, or strings.
48
+ If not provided, defaults to Cancel and OK buttons.
49
+ First button appears rightmost (Bootstrap convention).
50
+ minsize: Minimum dialog window size as (width, height).
51
+ If None, automatically calculated based on col_count * min_col_width + padding.
52
+ If provided, ensures width is at least the calculated minimum to prevent
53
+ horizontal scrolling. Defaults to None (auto-calculate).
54
+ maxsize: Maximum dialog window size as (width, height). Defaults to None.
55
+ resizable: Allow window resizing as (width, height) bools. Defaults to (True, True).
56
+ alert: If True, plays system alert sound on show. Defaults to False.
57
+ mode: Dialog interaction mode ("modal" or "popover"). Defaults to "modal".
58
+ """
59
+
60
+ def __init__(
61
+ self,
62
+ master: Master = None,
63
+ *,
64
+ title: str = "Form",
65
+ data: dict[str, Any] | None = None,
66
+ items: Sequence[FormItem | Mapping[str, Any]] | None = None,
67
+ col_count: int = 1,
68
+ min_col_width: int | None = None,
69
+ on_data_changed: Callable[[dict[str, Any]], Any] | None = None,
70
+ width: int | None = None,
71
+ height: int | None = None,
72
+ scrollable: bool = True,
73
+ scrollview_options: dict[str, Any] | None = None,
74
+ buttons: Iterable[ButtonSpec | str] | None = None,
75
+ minsize: tuple[int, int] | None = None,
76
+ maxsize: tuple[int, int] | None = None,
77
+ resizable: tuple[bool, bool] | bool | None = False,
78
+ alert: bool = False,
79
+ mode: Literal['modal', 'popover'] = "modal",
80
+ ):
81
+ """Initialize a FormDialog that wraps a Form inside a Dialog.
82
+
83
+ Args:
84
+ master: Parent widget. Defaults to the default root window.
85
+ title: Dialog window title.
86
+ data: Initial data backing the form; keys become field names.
87
+ items: Optional explicit form layout (FieldItem/GroupItem/TabsItem or mappings).
88
+ col_count: Number of columns for the form layout.
89
+ min_col_width: Minimum width per column; defaults to the shared form default.
90
+ on_data_changed: Callback invoked when any field value changes.
91
+ width: Explicit form width; if None, size naturally.
92
+ height: Explicit form height; if None, size naturally.
93
+ scrollable: Whether the form content should be scrollable.
94
+ scrollview_options: Extra options passed to the ScrollView when scrollable.
95
+ buttons: Dialog footer buttons (DialogButton, mapping, or string).
96
+ minsize: Minimum dialog window size (width, height).
97
+ maxsize: Maximum dialog window size (width, height).
98
+ resizable: Bool or (width, height) tuple to allow window resizing.
99
+ alert: Whether to play the system alert sound when shown.
100
+ mode: Dialog interaction mode, "modal" or "popover".
101
+ """
102
+ self._data = data or {}
103
+ self._items = items
104
+ self._col_count = col_count
105
+ # Use the shared form default when not provided to keep layouts consistent
106
+ self._min_col_width = min_col_width if min_col_width is not None else FORM_MIN_COL_WIDTH
107
+ self._on_data_changed = on_data_changed
108
+ self._width = width
109
+ self._height = height
110
+ self._scrollable = scrollable # deprecated; kept for compatibility with callers
111
+
112
+ # Use better default scrollview options - auto-hide when content fits
113
+ default_scrollview_options = {
114
+ # Keep the scrollbar visible to avoid width jumps when it appears/disappears
115
+ 'scrollbar_visibility': 'always',
116
+ 'autohide_delay': 1000,
117
+ }
118
+ if scrollview_options:
119
+ default_scrollview_options.update(scrollview_options)
120
+ self._scrollview_options = default_scrollview_options
121
+
122
+ # Normalize buttons and wrap command callbacks
123
+ self._buttons = self._normalize_buttons(buttons)
124
+ self._wrap_button_commands()
125
+
126
+ # Store minsize for later adjustment
127
+ self._user_minsize = minsize
128
+ self._user_maxsize = maxsize
129
+
130
+ # Normalize resizable flag: bool applies to both axes
131
+ if isinstance(resizable, bool):
132
+ resizable = (resizable, resizable)
133
+
134
+ # Create the dialog with form as content
135
+ # We'll set the proper minsize after measuring the actual form
136
+ self._dialog = Dialog(
137
+ master=master,
138
+ title=title,
139
+ content_builder=self._build_form_content,
140
+ buttons=self._buttons,
141
+ minsize=minsize, # Use user-provided or None initially
142
+ maxsize=maxsize,
143
+ resizable=resizable,
144
+ alert=alert,
145
+ mode=mode,
146
+ )
147
+
148
+ self.form: Any = None # Form widget, imported lazily to avoid circular import
149
+ self.result: Any = None
150
+ self._initial_layout_done = False
151
+ self._scrollview = None
152
+ self._window_id = None
153
+
154
+ def show(
155
+ self,
156
+ position: Optional[Tuple[int, int]] = None,
157
+ modal: Optional[bool] = None,
158
+ *,
159
+ anchor_to: Optional[Union[Widget, Literal["screen", "cursor", "parent"]]] = None,
160
+ anchor_point: AnchorPoint = 'center',
161
+ window_point: AnchorPoint = 'center',
162
+ offset: Tuple[int, int] = (0, 0),
163
+ auto_flip: Union[bool, Literal['vertical', 'horizontal']] = False
164
+ ):
165
+ """Show the form dialog and populate `result` when closed.
166
+
167
+ Args:
168
+ position: Optional (x, y) coordinates to position the dialog.
169
+ If provided, takes precedence over anchor-based positioning.
170
+ modal: Override the mode's default modality.
171
+ - If None, uses mode:
172
+ - "modal": grab_set + wait_window
173
+ - "popover": no grab, but wait_window
174
+ anchor_to: Positioning target. Can be:
175
+ - Widget: Anchor to a specific widget
176
+ - "screen": Anchor to screen edges/corners
177
+ - "cursor": Anchor to mouse cursor location
178
+ - "parent": Anchor to parent window (same as widget)
179
+ - None: Centers on parent (default)
180
+ anchor_point: Point on the anchor target (n, s, e, w, ne, nw, se, sw, center).
181
+ Default 'center'.
182
+ window_point: Point on the dialog window (n, s, e, w, ne, nw, se, sw, center).
183
+ Default 'center'.
184
+ offset: Additional (x, y) offset in pixels from the anchor position.
185
+ auto_flip: Smart positioning to keep window on screen.
186
+ - False: No flipping (default)
187
+ - True: Flip both vertically and horizontally as needed
188
+ - 'vertical': Only flip up/down
189
+ - 'horizontal': Only flip left/right
190
+ """
191
+ # Allow initial layout priming each time the dialog is shown
192
+ self._initial_layout_done = False
193
+
194
+ self._dialog.show(
195
+ position=position,
196
+ modal=modal,
197
+ anchor_to=anchor_to,
198
+ anchor_point=anchor_point,
199
+ window_point=window_point,
200
+ offset=offset,
201
+ auto_flip=auto_flip
202
+ )
203
+
204
+ # Transfer the result from dialog to FormDialog
205
+ if self._dialog.result is not None:
206
+ self.result = self.form.data if self.form else None
207
+ else:
208
+ self.result = None
209
+
210
+
211
+ def _build_form_content(self, parent):
212
+ """Builder callback that creates the Form widget inside the dialog."""
213
+ # Import Form here to avoid circular import
214
+ from bootstack.widgets.composites.form import Form
215
+ from bootstack.widgets.composites.scrollview import ScrollView
216
+
217
+ # Configure parent to allow stretching
218
+ parent.columnconfigure(0, weight=1)
219
+ parent.rowconfigure(0, weight=1)
220
+
221
+ container = parent
222
+ if self._scrollable:
223
+ self._scrollview = ScrollView(parent, scroll_direction='vertical', **self._scrollview_options)
224
+ self._scrollview.grid(row=0, column=0, sticky="nsew")
225
+ self._scrollview.enable_scrolling()
226
+ # add a padding frame inside the scrollview so the form has margins
227
+ padding_frame = Frame(self._scrollview, padding=10)
228
+ padding_frame.columnconfigure(0, weight=1)
229
+ padding_frame.rowconfigure(0, weight=1)
230
+ self._scrollview.add(padding_frame, anchor='nw')
231
+ self._window_id = self._scrollview._window_id
232
+ self._scrollview.bind('<Configure>', self._on_scrollview_configure, add="+")
233
+ # Keep canvas window width in sync with viewport on every resize
234
+ self._scrollview.canvas.bind("<Configure>", self._on_canvas_configure, add="+")
235
+ container = padding_frame
236
+ else:
237
+ self._scrollview = None
238
+
239
+ # Create the form without its own scrolling; scrolling is managed by the dialog
240
+ self.form = Form(
241
+ container,
242
+ data=self._data,
243
+ items=self._items,
244
+ col_count=self._col_count,
245
+ min_col_width=self._min_col_width,
246
+ on_data_changed=self._on_data_changed,
247
+ width=None, # Let form size naturally
248
+ height=None if self._scrollable else self._height,
249
+ buttons=None,
250
+ )
251
+
252
+ # Add the form to the scrollview container or grid directly
253
+ if self._scrollview:
254
+ self.form.grid(row=0, column=0, sticky="nsew")
255
+ else:
256
+ self.form.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
257
+
258
+ # Ensure initial layout runs right after build (for scrollable dialogs)
259
+ self._schedule_initial_layout()
260
+
261
+ # Measure the actual content width after rendering
262
+ measured_width = self._col_count * self._min_col_width
263
+ if self._items:
264
+ nested_width = self._calculate_required_width()
265
+ measured_width = max(measured_width, nested_width)
266
+
267
+ if self._width:
268
+ measured_width = self._width
269
+
270
+ # Calculate dialog size: content + padding (10x2) + dialog chrome (~40)
271
+ dialog_width = measured_width + 60
272
+ dialog_height = 500 # Default height, will adjust after geometry update
273
+
274
+ # Apply user minsize if provided
275
+ if self._user_minsize:
276
+ user_width, user_height = self._user_minsize
277
+ dialog_width = max(user_width, dialog_width)
278
+ dialog_height = max(user_height, dialog_height)
279
+
280
+ if self._height:
281
+ dialog_height = self._height + 100
282
+
283
+ dialog_height = min(dialog_height, 800)
284
+ # Store the intended content width for pre-show sizing of the scrollview
285
+ self._desired_canvas_width = measured_width
286
+
287
+ # Set minsize and geometry BEFORE forcing layout
288
+ if self._dialog.toplevel:
289
+ self._dialog.toplevel.minsize(dialog_width, dialog_height)
290
+ self._dialog.toplevel.geometry(f"{dialog_width}x{dialog_height}")
291
+
292
+ # Force complete geometry calculation while window is still withdrawn
293
+ self._dialog.toplevel.update_idletasks()
294
+
295
+ def _schedule_initial_layout(self):
296
+ """Run layout fixups immediately before showing the window."""
297
+ if not self._scrollable:
298
+ return
299
+ if self._dialog and self._dialog.toplevel:
300
+ # Make sure all pending geometry work is processed while withdrawn.
301
+ # update_idletasks alone flushes layout/redraw; full update() also
302
+ # pumps input/IO events, which can hang on Aqua when the form
303
+ # contains heavy widgets (same pattern as the Dialog/BaseWindow
304
+ # pre-deiconify hang). Gate update() to win32 only.
305
+ top = self._dialog.toplevel
306
+ top.update_idletasks()
307
+ if getattr(top, 'winsys', None) == 'win32':
308
+ top.update()
309
+ # Run once synchronously so sizing is applied before deiconify
310
+ self._fire_initial_configure(blocking=True)
311
+
312
+ def _fire_initial_configure(self, _event=None, attempts: int = 5, blocking: bool = False):
313
+ """Trigger scrollview/layout config once after geometry stabilizes."""
314
+ if self._initial_layout_done:
315
+ return
316
+ if not (self._scrollable and self._scrollview and self.form):
317
+ return
318
+
319
+ canvas = self._scrollview.canvas
320
+ canvas_width = 0
321
+
322
+ # When blocking, loop a few times to give Tk a chance to size the canvas
323
+ loops = attempts if blocking else 1
324
+ for _ in range(max(1, loops)):
325
+ if self._dialog and self._dialog.toplevel:
326
+ self._dialog.toplevel.update_idletasks()
327
+ self._scrollview.update_idletasks()
328
+ canvas_width = canvas.winfo_width()
329
+ if canvas_width > 1:
330
+ break
331
+
332
+ fallback_width = max(
333
+ getattr(self, "_desired_canvas_width", 0),
334
+ self.form.winfo_reqwidth() if self.form else 0,
335
+ self._col_count * self._min_col_width,
336
+ )
337
+ if canvas_width <= 1:
338
+ canvas_width = fallback_width
339
+
340
+ # Apply width and manually call handlers
341
+ if self._window_id:
342
+ canvas.itemconfigure(self._window_id, width=canvas_width)
343
+ self._on_scrollview_configure(None)
344
+ try:
345
+ self._scrollview._on_frame_configure(None)
346
+ except Exception:
347
+ pass
348
+
349
+ # Ensure scrolling bindings are active after layout adjustments
350
+ try:
351
+ if getattr(self._scrollview, "_scrolling_enabled", False):
352
+ self._scrollview.refresh_bindings()
353
+ else:
354
+ self._scrollview.enable_scrolling()
355
+ except Exception:
356
+ pass
357
+
358
+ # Generate configure to mirror user-driven resize
359
+ try:
360
+ canvas.event_generate("<Configure>")
361
+ except Exception:
362
+ pass
363
+
364
+ def _on_scrollview_configure(self, _event=None):
365
+ """Keep scrollview content width in sync with the viewport."""
366
+ if self._scrollview and self._window_id:
367
+ canvas_width = self._scrollview.canvas.winfo_width()
368
+ if canvas_width > 1:
369
+ self._scrollview.canvas.itemconfigure(self._window_id, width=canvas_width)
370
+
371
+ def _on_canvas_configure(self, event=None):
372
+ """Keep the embedded form window width aligned to the canvas viewport."""
373
+ if not (self._scrollview and self._window_id):
374
+ return
375
+ width = event.width if event and hasattr(event, "width") else self._scrollview.canvas.winfo_width()
376
+ if width <= 1:
377
+ return
378
+ self._scrollview.canvas.itemconfigure(self._window_id, width=width)
379
+ try:
380
+ self._scrollview._on_frame_configure(None)
381
+ except Exception:
382
+ pass
383
+
384
+ def _calculate_required_width(self) -> int:
385
+ """Calculate the required minimum width for the dialog based on form structure."""
386
+ try:
387
+ from bootstack.widgets.composites.form import GroupItem, TabsItem
388
+
389
+ # Start by finding the maximum width requirement
390
+ max_content_width = self._find_max_content_width(self._items, self._col_count, self._min_col_width)
391
+
392
+ # Add all the padding layers:
393
+ # 1. Form widget padding (grid padx=10 on each side) = 20px
394
+ # 2. Dialog content frame borders = 10px
395
+ # 3. Window chrome and safety margin = 30px
396
+ total_width = max_content_width + 60
397
+
398
+ return total_width
399
+ except:
400
+ # Fallback to basic calculation
401
+ return (self._col_count * self._min_col_width) + 60
402
+
403
+ def _find_max_content_width(self, items, parent_col_count: int, parent_min_col_width: int) -> int:
404
+ """Find the maximum content width needed, accounting for nested structures."""
405
+ if not items:
406
+ # No items, use parent layout
407
+ # Each column needs: min_col_width + padx (6 on each side = 12)
408
+ return (parent_col_count * (parent_min_col_width + 12))
409
+
410
+ max_width = 0
411
+
412
+ # Calculate width for current level
413
+ current_width = parent_col_count * (parent_min_col_width + 12)
414
+ max_width = max(max_width, current_width)
415
+
416
+ # Check all items for nested layouts
417
+ try:
418
+ from bootstack.widgets.composites.form import GroupItem, TabsItem
419
+
420
+ for item in items:
421
+ if isinstance(item, dict):
422
+ item_type = item.get('type', 'field')
423
+ if item_type == 'group':
424
+ nested_col_count = item.get('col_count', parent_col_count)
425
+ nested_min_col_width = item.get('min_col_width', self._min_col_width)
426
+ nested_items = item.get('items', [])
427
+
428
+ # GroupItem content width
429
+ group_content = self._find_max_content_width(nested_items, nested_col_count, nested_min_col_width)
430
+ # Add LabelFrame padding (8px each side) and borders (~4px) = 24px total
431
+ group_total = group_content + 24
432
+ max_width = max(max_width, group_total)
433
+
434
+ elif item_type == 'tabs':
435
+ tabs = item.get('tabs', [])
436
+ for tab in tabs:
437
+ if isinstance(tab, dict):
438
+ tab_items = tab.get('items', [])
439
+ tab_width = self._find_max_content_width(tab_items, parent_col_count, parent_min_col_width)
440
+ # Add Notebook borders = 20px
441
+ max_width = max(max_width, tab_width + 20)
442
+
443
+ elif isinstance(item, GroupItem):
444
+ nested_col_count = item.col_count if item.col_count else parent_col_count
445
+ nested_min_col_width = item.min_col_width if item.min_col_width else self._min_col_width
446
+
447
+ # GroupItem content width
448
+ group_content = self._find_max_content_width(item.items, nested_col_count, nested_min_col_width)
449
+ # Add LabelFrame padding
450
+ group_total = group_content + 24
451
+ max_width = max(max_width, group_total)
452
+
453
+ elif isinstance(item, TabsItem):
454
+ for tab in item.tabs:
455
+ tab_items = tab.items if hasattr(tab, 'items') else []
456
+ tab_width = self._find_max_content_width(tab_items, parent_col_count, parent_min_col_width)
457
+ # Add Notebook borders
458
+ max_width = max(max_width, tab_width + 20)
459
+ except:
460
+ pass
461
+
462
+ return max_width
463
+
464
+ def _wrap_button_commands(self):
465
+ """Wrap button command callbacks to pass FormDialog instead of Dialog."""
466
+ for button in self._buttons:
467
+ # For non-cancel buttons, handle validation and closing manually
468
+ if button.role != "cancel":
469
+ button.closes = False
470
+ if button.command:
471
+ original_command = button.command
472
+ def wrapped_command(dlg, cmd=original_command, btn=button):
473
+ # Validate form before running custom command
474
+ if self.form and btn.role != "cancel":
475
+ if not self.form.validate():
476
+ return
477
+ result = cmd(self) # Pass FormDialog, not Dialog
478
+ if result is False:
479
+ return
480
+ # Set result and close manually when not cancelled
481
+ if self._dialog:
482
+ self._dialog.result = btn.result if btn.result is not None else (self.form.data if self.form else None)
483
+ if btn.closes is False and self._dialog.toplevel:
484
+ self._dialog.toplevel.destroy()
485
+ return result
486
+ button.command = wrapped_command
487
+ else:
488
+ # No custom command: inject validation and close behavior for non-cancel buttons
489
+ def auto_command(dlg=None, btn=button):
490
+ if btn.role == "cancel":
491
+ # Cancel button: leave result as None, dialog closes via default behavior
492
+ return
493
+ if self.form:
494
+ if not self.form.validate():
495
+ return
496
+ if self._dialog:
497
+ self._dialog.result = btn.result if btn.result is not None else (self.form.data if self.form else None)
498
+ if btn.closes is False and self._dialog.toplevel:
499
+ self._dialog.toplevel.destroy()
500
+ button.command = auto_command
501
+
502
+ def _normalize_buttons(self, buttons: Iterable[ButtonSpec | str] | None) -> list[DialogButton]:
503
+ """Normalize button specifications, providing defaults if none given."""
504
+ if buttons is None:
505
+ # Default buttons: Cancel and OK
506
+ return [
507
+ DialogButton(text="button.cancel", role="cancel", result=None),
508
+ DialogButton(text="button.ok", role="primary", result="ok", default=True),
509
+ ]
510
+
511
+ normalized: list[DialogButton] = []
512
+ for btn in buttons:
513
+ if isinstance(btn, DialogButton):
514
+ normalized.append(btn)
515
+ elif isinstance(btn, str):
516
+ # Simple string becomes a button
517
+ btn_lower = btn.lower()
518
+ if btn_lower == "cancel":
519
+ role = "cancel"
520
+ result = None
521
+ elif btn_lower in ("ok", "submit", "save"):
522
+ role = "primary"
523
+ result = btn_lower
524
+ else:
525
+ role = "primary" if not normalized else "secondary"
526
+ result = None
527
+ normalized.append(DialogButton(text=btn, role=role, result=result))
528
+ elif isinstance(btn, Mapping):
529
+ normalized.append(DialogButton(**btn))
530
+ else:
531
+ raise TypeError(f"Invalid button type: {type(btn)}")
532
+
533
+ return normalized
534
+
535
+ @property
536
+ def toplevel(self):
537
+ """Read-only access to the underlying toplevel window."""
538
+ return self._dialog.toplevel if self._dialog else None
539
+
540
+
541
+ __all__ = ["FormDialog"]