bootstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. bootstack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,564 @@
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._impl.composites.form import FormItem
14
+
15
+ from bootstack.dialogs._impl.dialog import Dialog, DialogButton, ButtonSpec
16
+ from bootstack.widgets._impl.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: After closing — the entered form data (dict) for a submit
30
+ button (ok/submit/save), the button's own result value for a custom
31
+ action button (e.g. "delete"), or None if cancelled.
32
+ form: The embedded Form widget instance, accessible for advanced usage.
33
+
34
+ Args:
35
+ parent: Parent widget. If None, uses the default root window.
36
+ title: Dialog window title. Defaults to "Form".
37
+ data: Initial data backing the form. Keys become field names.
38
+ items: Optional explicit form definition using FieldItem/GroupItem/TabsItem.
39
+ If not provided, fields are inferred from data keys and types.
40
+ col_count: Number of columns for form layout. Defaults to 1.
41
+ min_col_width: Minimum width for each column in pixels. Defaults to the
42
+ shared form default.
43
+ on_data_change: Optional callback invoked when any field value changes.
44
+ Receives the updated data dict as parameter.
45
+ width: Requested width for the form. Defaults to None (auto-size).
46
+ height: Requested height for the form. Defaults to None (auto-size).
47
+ scrollable: Deprecated; FormDialog manages scrolling internally.
48
+ scrollview_options: Additional options passed to ScrollView when scrollable is True.
49
+ buttons: Footer buttons. Can be DialogButton instances, dicts, or strings.
50
+ If not provided, defaults to Cancel and OK buttons.
51
+ First button appears rightmost (Bootstrap convention).
52
+ minsize: Minimum dialog window size as (width, height).
53
+ If None, automatically calculated based on col_count * min_col_width + padding.
54
+ If provided, ensures width is at least the calculated minimum to prevent
55
+ horizontal scrolling. Defaults to None (auto-calculate).
56
+ maxsize: Maximum dialog window size as (width, height). Defaults to None.
57
+ resizable: Allow window resizing as (width, height) bools. Defaults to (True, True).
58
+ alert: If True, plays system alert sound on show. Defaults to False.
59
+ mode: Dialog interaction mode ("modal" or "popover"). Defaults to "modal".
60
+ """
61
+
62
+ def __init__(
63
+ self,
64
+ parent: Master = None,
65
+ *,
66
+ title: str = "Form",
67
+ data: dict[str, Any] | None = None,
68
+ items: Sequence[FormItem | Mapping[str, Any]] | None = None,
69
+ col_count: int = 1,
70
+ min_col_width: int | None = None,
71
+ on_data_change: Callable[[dict[str, Any]], Any] | None = None,
72
+ on_close: Callable[[], Any] | None = None,
73
+ width: int | None = None,
74
+ height: int | None = None,
75
+ scrollable: bool = True,
76
+ scrollview_options: dict[str, Any] | None = None,
77
+ buttons: Iterable[ButtonSpec | str] | None = None,
78
+ minsize: tuple[int, int] | None = None,
79
+ maxsize: tuple[int, int] | None = None,
80
+ resizable: tuple[bool, bool] | bool | None = False,
81
+ alert: bool = False,
82
+ mode: Literal['modal', 'popover'] = "modal",
83
+ ):
84
+ """Initialize a FormDialog that wraps a Form inside a Dialog.
85
+
86
+ Args:
87
+ parent: Parent widget. Defaults to the default root window.
88
+ title: Dialog window title.
89
+ data: Initial data backing the form; keys become field names.
90
+ items: Optional explicit form layout (FieldItem/GroupItem/TabsItem or mappings).
91
+ col_count: Number of columns for the form layout.
92
+ min_col_width: Minimum width per column; defaults to the shared form default.
93
+ on_data_change: Callback invoked when any field value changes.
94
+ width: Explicit form width; if None, size naturally.
95
+ height: Explicit form height; if None, size naturally.
96
+ scrollable: Whether the form content should be scrollable.
97
+ scrollview_options: Extra options passed to the ScrollView when scrollable.
98
+ buttons: Dialog footer buttons (DialogButton, mapping, or string).
99
+ minsize: Minimum dialog window size (width, height).
100
+ maxsize: Maximum dialog window size (width, height).
101
+ resizable: Bool or (width, height) tuple to allow window resizing.
102
+ alert: Whether to play the system alert sound when shown.
103
+ mode: Dialog interaction mode, "modal" or "popover".
104
+ """
105
+ self._data = data or {}
106
+ self._items = items
107
+ self._col_count = col_count
108
+ # Use the shared form default when not provided to keep layouts consistent
109
+ self._min_col_width = min_col_width if min_col_width is not None else FORM_MIN_COL_WIDTH
110
+ self._on_data_change = on_data_change
111
+ self._width = width
112
+ self._height = height
113
+ self._scrollable = scrollable # deprecated; kept for compatibility with callers
114
+
115
+ # Use better default scrollview options - auto-hide when content fits
116
+ default_scrollview_options = {
117
+ # Keep the scrollbar visible to avoid width jumps when it appears/disappears
118
+ 'scrollbar_visibility': 'always',
119
+ 'autohide_delay': 1000,
120
+ }
121
+ if scrollview_options:
122
+ default_scrollview_options.update(scrollview_options)
123
+ self._scrollview_options = default_scrollview_options
124
+
125
+ # Normalize buttons and wrap command callbacks
126
+ self._buttons = self._normalize_buttons(buttons)
127
+ self._wrap_button_commands()
128
+
129
+ # Store minsize for later adjustment
130
+ self._user_minsize = minsize
131
+ self._user_maxsize = maxsize
132
+
133
+ # Normalize resizable flag: bool applies to both axes
134
+ if isinstance(resizable, bool):
135
+ resizable = (resizable, resizable)
136
+
137
+ # Create the dialog with form as content
138
+ # We'll set the proper minsize after measuring the actual form
139
+ self._dialog = Dialog(
140
+ title=title,
141
+ content_builder=self._build_form_content,
142
+ _raw_content=True,
143
+ buttons=self._buttons,
144
+ min_size=minsize,
145
+ max_size=maxsize,
146
+ resizable=resizable,
147
+ alert=alert,
148
+ mode=mode,
149
+ on_close=on_close,
150
+ parent=parent,
151
+ )
152
+
153
+ self.form: Any = None # Form widget, imported lazily to avoid circular import
154
+ self.result: Any = None
155
+ self._initial_layout_done = False
156
+ self._scrollview = None
157
+ self._window_id = None
158
+
159
+ def show(
160
+ self,
161
+ *,
162
+ position: Optional[Tuple[int, int]] = None,
163
+ modal: Optional[bool] = None,
164
+ anchor_to: Optional[Union[Widget, Literal["screen", "cursor", "parent"]]] = None,
165
+ anchor_point: AnchorPoint = 'center',
166
+ window_point: AnchorPoint = 'center',
167
+ offset: Tuple[int, int] = (0, 0),
168
+ auto_flip: Union[bool, Literal['vertical', 'horizontal']] = False
169
+ ):
170
+ """Show the form dialog and populate `result` when closed.
171
+
172
+ Args:
173
+ position: Optional (x, y) coordinates to position the dialog.
174
+ If provided, takes precedence over anchor-based positioning.
175
+ modal: Override the mode's default modality.
176
+ - If None, uses mode:
177
+ - "modal": grab_set + wait_window
178
+ - "popover": no grab, but wait_window
179
+ anchor_to: Positioning target. Can be:
180
+ - Widget: Anchor to a specific widget
181
+ - "screen": Anchor to screen edges/corners
182
+ - "cursor": Anchor to mouse cursor location
183
+ - "parent": Anchor to parent window (same as widget)
184
+ - None: Centers on parent (default)
185
+ anchor_point: Point on the anchor target (n, s, e, w, ne, nw, se, sw, center).
186
+ Default 'center'.
187
+ window_point: Point on the dialog window (n, s, e, w, ne, nw, se, sw, center).
188
+ Default 'center'.
189
+ offset: Additional (x, y) offset in pixels from the anchor position.
190
+ auto_flip: Smart positioning to keep window on screen.
191
+ - False: No flipping (default)
192
+ - True: Flip both vertically and horizontally as needed
193
+ - 'vertical': Only flip up/down
194
+ - 'horizontal': Only flip left/right
195
+ """
196
+ # Allow initial layout priming each time the dialog is shown
197
+ self._initial_layout_done = False
198
+
199
+ self._dialog.show(
200
+ position=position,
201
+ modal=modal,
202
+ anchor_to=anchor_to,
203
+ anchor_point=anchor_point,
204
+ window_point=window_point,
205
+ offset=offset,
206
+ auto_flip=auto_flip
207
+ )
208
+
209
+ # Transfer the result from dialog to FormDialog.
210
+ self.result = self._resolve_result(self._dialog.result)
211
+
212
+ # Submit-style button results that mean "return the entered form data".
213
+ _DATA_RESULTS = frozenset({"ok", "submit", "save"})
214
+
215
+ def _resolve_result(self, dialog_result: Any) -> Any:
216
+ """Map the closing button's dialog result to the FormDialog result.
217
+
218
+ A data-submit button (ok / submit / save) yields the entered form data;
219
+ a custom action button (e.g. a `'delete'` result) yields its own result
220
+ value so callers can tell the action apart from the data. A cancel /
221
+ unset result (`None`) yields `None`.
222
+ """
223
+ if dialog_result is None:
224
+ return None
225
+ if isinstance(dialog_result, str) and dialog_result.lower() in self._DATA_RESULTS:
226
+ return self.form.data if self.form else None
227
+ # A non-cancel button with no explicit result already carries the form
228
+ # data (set in `_wrap_button_commands`); anything else is an action token.
229
+ return dialog_result
230
+
231
+
232
+ def _build_form_content(self, parent):
233
+ """Builder callback that creates the Form widget inside the dialog."""
234
+ # Import Form here to avoid circular import
235
+ from bootstack.widgets._impl.composites.form import Form
236
+ from bootstack.widgets._impl.composites.scrollview import ScrollView
237
+
238
+ # Configure parent to allow stretching
239
+ parent.columnconfigure(0, weight=1)
240
+ parent.rowconfigure(0, weight=1)
241
+
242
+ container = parent
243
+ if self._scrollable:
244
+ self._scrollview = ScrollView(parent, scroll_direction='vertical', **self._scrollview_options)
245
+ self._scrollview.grid(row=0, column=0, sticky="nsew")
246
+ self._scrollview.enable_scrolling()
247
+ # add a padding frame inside the scrollview so the form has margins
248
+ padding_frame = Frame(self._scrollview, padding=10)
249
+ padding_frame.columnconfigure(0, weight=1)
250
+ padding_frame.rowconfigure(0, weight=1)
251
+ self._scrollview.add(padding_frame, anchor='nw')
252
+ self._window_id = self._scrollview._window_id
253
+ self._scrollview.bind('<Configure>', self._on_scrollview_configure, add="+")
254
+ # Keep canvas window width in sync with viewport on every resize
255
+ self._scrollview.canvas.bind("<Configure>", self._on_canvas_configure, add="+")
256
+ container = padding_frame
257
+ else:
258
+ self._scrollview = None
259
+
260
+ # Create the form without its own scrolling; scrolling is managed by the dialog
261
+ self.form = Form(
262
+ container,
263
+ data=self._data,
264
+ items=self._items,
265
+ col_count=self._col_count,
266
+ min_col_width=self._min_col_width,
267
+ on_data_change=self._on_data_change,
268
+ width=None, # Let form size naturally
269
+ height=None if self._scrollable else self._height,
270
+ buttons=None,
271
+ )
272
+
273
+ # Add the form to the scrollview container or grid directly
274
+ if self._scrollview:
275
+ self.form.grid(row=0, column=0, sticky="nsew")
276
+ else:
277
+ self.form.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
278
+
279
+ # Ensure initial layout runs right after build (for scrollable dialogs)
280
+ self._schedule_initial_layout()
281
+
282
+ # Measure the actual content width after rendering
283
+ measured_width = self._col_count * self._min_col_width
284
+ if self._items:
285
+ nested_width = self._calculate_required_width()
286
+ measured_width = max(measured_width, nested_width)
287
+
288
+ if self._width:
289
+ measured_width = self._width
290
+
291
+ # Calculate dialog size: content + padding (10x2) + dialog chrome (~40)
292
+ dialog_width = measured_width + 60
293
+
294
+ _MIN_HEIGHT = 150
295
+ _MAX_HEIGHT = 800
296
+
297
+ min_height = _MIN_HEIGHT
298
+ if self._user_minsize:
299
+ user_width, user_height = self._user_minsize
300
+ dialog_width = max(user_width, dialog_width)
301
+ min_height = max(user_height, _MIN_HEIGHT)
302
+
303
+ # Store the intended content width for pre-show sizing of the scrollview
304
+ self._desired_canvas_width = measured_width
305
+
306
+ if self._dialog.toplevel:
307
+ # Seed width and minimum height, then measure natural content height
308
+ self._dialog.toplevel.minsize(dialog_width, min_height)
309
+ self._dialog.toplevel.geometry(f"{dialog_width}x{min_height}")
310
+ self._dialog.toplevel.update_idletasks()
311
+
312
+ if self._height:
313
+ dialog_height = min(self._height + 100, _MAX_HEIGHT)
314
+ else:
315
+ natural = self._dialog.toplevel.winfo_reqheight()
316
+ dialog_height = min(max(natural, min_height), _MAX_HEIGHT)
317
+
318
+ self._dialog.toplevel.geometry(f"{dialog_width}x{dialog_height}")
319
+ self._dialog.toplevel.update_idletasks()
320
+
321
+ def _schedule_initial_layout(self):
322
+ """Run layout fixups immediately before showing the window."""
323
+ if not self._scrollable:
324
+ return
325
+ if self._dialog and self._dialog.toplevel:
326
+ # Make sure all pending geometry work is processed while withdrawn.
327
+ # update_idletasks alone flushes layout/redraw; full update() also
328
+ # pumps input/IO events, which can hang on Aqua when the form
329
+ # contains heavy widgets (same pattern as the Dialog/BaseWindow
330
+ # pre-deiconify hang). Gate update() to win32 only.
331
+ top = self._dialog.toplevel
332
+ top.update_idletasks()
333
+ if getattr(top, 'winsys', None) == 'win32':
334
+ top.update()
335
+ # Run once synchronously so sizing is applied before deiconify
336
+ self._fire_initial_configure(blocking=True)
337
+
338
+ def _fire_initial_configure(self, _event=None, attempts: int = 5, blocking: bool = False):
339
+ """Trigger scrollview/layout config once after geometry stabilizes."""
340
+ if self._initial_layout_done:
341
+ return
342
+ if not (self._scrollable and self._scrollview and self.form):
343
+ return
344
+
345
+ canvas = self._scrollview.canvas
346
+ canvas_width = 0
347
+
348
+ # When blocking, loop a few times to give Tk a chance to size the canvas
349
+ loops = attempts if blocking else 1
350
+ for _ in range(max(1, loops)):
351
+ if self._dialog and self._dialog.toplevel:
352
+ self._dialog.toplevel.update_idletasks()
353
+ self._scrollview.update_idletasks()
354
+ canvas_width = canvas.winfo_width()
355
+ if canvas_width > 1:
356
+ break
357
+
358
+ fallback_width = max(
359
+ getattr(self, "_desired_canvas_width", 0),
360
+ self.form.winfo_reqwidth() if self.form else 0,
361
+ self._col_count * self._min_col_width,
362
+ )
363
+ if canvas_width <= 1:
364
+ canvas_width = fallback_width
365
+
366
+ # Apply width and manually call handlers
367
+ if self._window_id:
368
+ canvas.itemconfigure(self._window_id, width=canvas_width)
369
+ self._on_scrollview_configure(None)
370
+ try:
371
+ self._scrollview._on_frame_configure(None)
372
+ except Exception:
373
+ pass
374
+
375
+ # Ensure scrolling bindings are active after layout adjustments
376
+ try:
377
+ if getattr(self._scrollview, "_scrolling_enabled", False):
378
+ self._scrollview.refresh_bindings()
379
+ else:
380
+ self._scrollview.enable_scrolling()
381
+ except Exception:
382
+ pass
383
+
384
+ # Generate configure to mirror user-driven resize
385
+ try:
386
+ canvas.event_generate("<Configure>")
387
+ except Exception:
388
+ pass
389
+
390
+ def _on_scrollview_configure(self, _event=None):
391
+ """Keep scrollview content width in sync with the viewport."""
392
+ if self._scrollview and self._window_id:
393
+ canvas_width = self._scrollview.canvas.winfo_width()
394
+ if canvas_width > 1:
395
+ self._scrollview.canvas.itemconfigure(self._window_id, width=canvas_width)
396
+
397
+ def _on_canvas_configure(self, event=None):
398
+ """Keep the embedded form window width aligned to the canvas viewport."""
399
+ if not (self._scrollview and self._window_id):
400
+ return
401
+ width = event.width if event and hasattr(event, "width") else self._scrollview.canvas.winfo_width()
402
+ if width <= 1:
403
+ return
404
+ self._scrollview.canvas.itemconfigure(self._window_id, width=width)
405
+ try:
406
+ self._scrollview._on_frame_configure(None)
407
+ except Exception:
408
+ pass
409
+
410
+ def _calculate_required_width(self) -> int:
411
+ """Calculate the required minimum width for the dialog based on form structure."""
412
+ try:
413
+
414
+ # Start by finding the maximum width requirement
415
+ max_content_width = self._find_max_content_width(self._items, self._col_count, self._min_col_width)
416
+
417
+ # Add all the padding layers:
418
+ # 1. Form widget padding (grid padx=10 on each side) = 20px
419
+ # 2. Dialog content frame borders = 10px
420
+ # 3. Window chrome and safety margin = 30px
421
+ total_width = max_content_width + 60
422
+
423
+ return total_width
424
+ except:
425
+ # Fallback to basic calculation
426
+ return (self._col_count * self._min_col_width) + 60
427
+
428
+ def _find_max_content_width(self, items, parent_col_count: int, parent_min_col_width: int) -> int:
429
+ """Find the maximum content width needed, accounting for nested structures."""
430
+ if not items:
431
+ # No items, use parent layout
432
+ # Each column needs: min_col_width + padx (6 on each side = 12)
433
+ return (parent_col_count * (parent_min_col_width + 12))
434
+
435
+ max_width = 0
436
+
437
+ # Calculate width for current level
438
+ current_width = parent_col_count * (parent_min_col_width + 12)
439
+ max_width = max(max_width, current_width)
440
+
441
+ # Check all items for nested layouts
442
+ try:
443
+ from bootstack.widgets._impl.composites.form import GroupItem, TabsItem
444
+
445
+ for item in items:
446
+ if isinstance(item, dict):
447
+ item_type = item.get('type', 'field')
448
+ if item_type == 'group':
449
+ nested_col_count = item.get('col_count', parent_col_count)
450
+ nested_min_col_width = item.get('min_col_width', self._min_col_width)
451
+ nested_items = item.get('items', [])
452
+
453
+ # GroupItem content width
454
+ group_content = self._find_max_content_width(nested_items, nested_col_count, nested_min_col_width)
455
+ # Add LabelFrame padding (8px each side) and borders (~4px) = 24px total
456
+ group_total = group_content + 24
457
+ max_width = max(max_width, group_total)
458
+
459
+ elif item_type == 'tabs':
460
+ tabs = item.get('tabs', [])
461
+ for tab in tabs:
462
+ if isinstance(tab, dict):
463
+ tab_items = tab.get('items', [])
464
+ tab_width = self._find_max_content_width(tab_items, parent_col_count, parent_min_col_width)
465
+ max_width = max(max_width, tab_width + 20)
466
+
467
+ elif isinstance(item, GroupItem):
468
+ nested_col_count = item.col_count if item.col_count else parent_col_count
469
+ nested_min_col_width = item.min_col_width if item.min_col_width else self._min_col_width
470
+
471
+ # GroupItem content width
472
+ group_content = self._find_max_content_width(item.items, nested_col_count, nested_min_col_width)
473
+ # Add LabelFrame padding
474
+ group_total = group_content + 24
475
+ max_width = max(max_width, group_total)
476
+
477
+ elif isinstance(item, TabsItem):
478
+ for tab in item.tabs:
479
+ tab_items = tab.items if hasattr(tab, 'items') else []
480
+ tab_width = self._find_max_content_width(tab_items, parent_col_count, parent_min_col_width)
481
+ max_width = max(max_width, tab_width + 20)
482
+ except:
483
+ pass
484
+
485
+ return max_width
486
+
487
+ def _wrap_button_commands(self):
488
+ """Wrap button command callbacks to pass FormDialog instead of Dialog."""
489
+ for button in self._buttons:
490
+ # For non-cancel buttons, handle validation and closing manually
491
+ if button.role != "cancel":
492
+ button.closes = False
493
+ if button.command:
494
+ original_command = button.command
495
+ def wrapped_command(dlg, cmd=original_command, btn=button):
496
+ # Validate form before running custom command
497
+ if self.form and btn.role != "cancel":
498
+ if not self.form.validate():
499
+ return
500
+ result = cmd(self) # Pass FormDialog, not Dialog
501
+ if result is False:
502
+ return
503
+ # Set result and close manually when not cancelled
504
+ if self._dialog:
505
+ self._dialog.result = btn.result if btn.result is not None else (self.form.data if self.form else None)
506
+ if btn.closes is False and self._dialog.toplevel:
507
+ self._dialog.toplevel.destroy()
508
+ return result
509
+ button.command = wrapped_command
510
+ else:
511
+ # No custom command: inject validation and close behavior for non-cancel buttons
512
+ def auto_command(dlg=None, btn=button):
513
+ if btn.role == "cancel":
514
+ # Cancel button: leave result as None, dialog closes via default behavior
515
+ return
516
+ if self.form:
517
+ if not self.form.validate():
518
+ return
519
+ if self._dialog:
520
+ self._dialog.result = btn.result if btn.result is not None else (self.form.data if self.form else None)
521
+ if btn.closes is False and self._dialog.toplevel:
522
+ self._dialog.toplevel.destroy()
523
+ button.command = auto_command
524
+
525
+ def _normalize_buttons(self, buttons: Iterable[ButtonSpec | str] | None) -> list[DialogButton]:
526
+ """Normalize button specifications, providing defaults if none given."""
527
+ if buttons is None:
528
+ # Default buttons: Cancel and OK
529
+ return [
530
+ DialogButton(text="button.cancel", role="cancel", result=None),
531
+ DialogButton(text="button.ok", role="primary", result="ok", default=True),
532
+ ]
533
+
534
+ normalized: list[DialogButton] = []
535
+ for btn in buttons:
536
+ if isinstance(btn, DialogButton):
537
+ normalized.append(btn)
538
+ elif isinstance(btn, str):
539
+ # Simple string becomes a button
540
+ btn_lower = btn.lower()
541
+ if btn_lower == "cancel":
542
+ role = "cancel"
543
+ result = None
544
+ elif btn_lower in ("ok", "submit", "save"):
545
+ role = "primary"
546
+ result = btn_lower
547
+ else:
548
+ role = "primary" if not normalized else "secondary"
549
+ result = None
550
+ normalized.append(DialogButton(text=btn, role=role, result=result))
551
+ elif isinstance(btn, Mapping):
552
+ normalized.append(DialogButton(**btn))
553
+ else:
554
+ raise TypeError(f"Invalid button type: {type(btn)}")
555
+
556
+ return normalized
557
+
558
+ @property
559
+ def toplevel(self):
560
+ """Read-only access to the underlying toplevel window."""
561
+ return self._dialog.toplevel if self._dialog else None
562
+
563
+
564
+ __all__ = ["FormDialog"]