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,387 @@
1
+ from __future__ import annotations
2
+
3
+ from tkinter import StringVar
4
+ from typing import Any, Callable, TypedDict, TYPE_CHECKING
5
+
6
+ from typing_extensions import Unpack
7
+
8
+ from bootstack.events import ChangeEvent
9
+ from bootstack.widgets._core.options import normalize_options, option_display, record_to_dict
10
+ from bootstack.widgets._impl.composites.contextmenu import ContextMenu, ContextMenuItem
11
+ from bootstack.widgets._impl.primitives._menubutton import MenuButton
12
+ from bootstack.widgets._impl.mixins import configure_delegate
13
+ from bootstack.widgets.types import Master, StyledKwargs, CompoundMode, WidgetState, Option
14
+
15
+ if TYPE_CHECKING:
16
+ from bootstack.signals import Signal
17
+
18
+
19
+ class OptionMenuKwargs(StyledKwargs, total=False):
20
+ command: Callable[[], Any] | None
21
+ image: Any
22
+ icon: Any
23
+ icon_only: bool
24
+ compound: CompoundMode
25
+ padding: Any
26
+ width: int
27
+ underline: int
28
+ state: WidgetState
29
+ default: Any
30
+ textvariable: Any
31
+ textsignal: Signal[str]
32
+ localize: Any
33
+ show_dropdown_button: bool
34
+ dropdown_button_icon: str | dict
35
+
36
+
37
+ class OptionMenu(MenuButton):
38
+ """A single-selection dropdown menu backed by a ContextMenu.
39
+
40
+ Renders as a `MenuButton` with a chevron icon. Clicking the button opens
41
+ a popup menu containing the provided options as radiobutton items. Selecting
42
+ an item updates the displayed text and fires a `<<Change>>` event.
43
+
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ master: Master = None,
49
+ value: Any = None,
50
+ options: list[Option] = None,
51
+ **kwargs: Unpack[OptionMenuKwargs],
52
+ ):
53
+ """Create an OptionMenu backed by a ContextMenu.
54
+
55
+ Args:
56
+ master: Parent widget. If None, uses the default root window.
57
+ value: Initial selected value (value-space). Must match one of the
58
+ options unless it is None/empty.
59
+ options: Options to populate the menu — each a string,
60
+ `(text, value)` tuple, or `{'text', 'value'}` dict. The menu
61
+ shows each option's text; selecting one emits its value.
62
+
63
+ Other Parameters:
64
+ command: Callback invoked when the value changes via menu selection.
65
+ image: Tk image to display.
66
+ icon: StyleResolver icon spec for the label content.
67
+ icon_only: Whether to reserve label padding when showing only an icon.
68
+ compound: Placement of image relative to text.
69
+ padding: Extra padding around the menubutton content.
70
+ width: Width of the menubutton.
71
+ underline: Index of underlined character in text.
72
+ state: Widget state ('normal', 'active', 'disabled', 'readonly').
73
+ takefocus: Participation in focus traversal.
74
+ style: Explicit ttk style name (overrides accent/variant).
75
+ textvariable: Existing Tk variable to bind; new StringVar created if omitted.
76
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
77
+ textsignal: Signal bound to the textvariable.
78
+ accent: Accent token for styling, e.g. 'primary', 'danger', 'success'.
79
+ variant: Style variant, e.g. 'solid', 'outline'.
80
+ surface: Surface token for style.
81
+ style_options: Dict forwarded to the style builder (e.g., icon_only, surface).
82
+ show_dropdown_button: Toggle visibility of the dropdown chevron.
83
+ dropdown_button_icon: Icon name for the chevron; defaults to 'caret-down-fill'.
84
+ """
85
+ style_options = kwargs.pop('style_options', {})
86
+ style_options.update(
87
+ self._capture_style_options(
88
+ options=['icon_only', 'icon', 'show_dropdown_button', 'dropdown_button_icon'],
89
+ source=kwargs
90
+ )
91
+ )
92
+
93
+ self._bind_id = None
94
+ # Localization mode for the option labels + button face. None defers to
95
+ # the app's localize_mode; False keeps the raw labels untranslated.
96
+ self._localize = kwargs.pop('localize', None)
97
+ # Normalize options once; keep bidirectional text<->value maps so the
98
+ # menu can display text while the public value stays in value-space.
99
+ self._records = normalize_options(options)
100
+ self._rebuild_option_maps()
101
+
102
+ # Store the textvariable if provided, or create a new one. This variable
103
+ # holds the raw VALUE-KEY text (untranslated): it drives the radio
104
+ # highlight, the public value/selection, and <<Change>> in value-space.
105
+ self._textvariable = kwargs.pop('textvariable', None)
106
+ if self._textvariable is None:
107
+ self._textvariable = StringVar(value=self._resolve_initial_text(value))
108
+
109
+ # A separate variable backs the visible button face, holding the
110
+ # (optionally translated) DISPLAY text — decoupled from the value key so
111
+ # the face can localize while value-space stays raw.
112
+ self._display_var = StringVar(value=self._display_text(self._textvariable.get()))
113
+
114
+ # The mixin must not touch the face (we own its translation), so disable
115
+ # its localization; OptionMenu localizes the face via `_display_var`.
116
+ super().__init__(master, text=self._display_var.get(), localize=False, style_options=style_options, **kwargs)
117
+
118
+ # Bind the textsignal to the value-key variable (drives <<Change>> + the
119
+ # public signal), then point the visible face at the display variable.
120
+ self.configure(textvariable=self._textvariable)
121
+ self._ttk_base.configure(self, textvariable=self._display_var)
122
+
123
+ # Bind signal to change event (also keeps the face display in sync)
124
+ self._bind_id = self._bind_change_event()
125
+ self._sync_display()
126
+ # The locale-change event is generated on the root; bind there (not on
127
+ # self) so the face re-translates live, matching the mixin's pattern.
128
+ self.winfo_toplevel().bind('<<LocaleChanged>>', lambda _e: self._sync_display(), add="+")
129
+
130
+ # Create menu items that update the shared variable
131
+ self._context_menu = self._build_context_menu()
132
+
133
+ # Bind menu display to button events
134
+ self.bind('<Button-1>', lambda _: self.show_menu(), add="+")
135
+ self.bind('<Return>', lambda _: self.show_menu(), add="+")
136
+ self.bind('<KP_Enter>', lambda _: self.show_menu(), add="+")
137
+
138
+ def _effective_localize(self) -> Any:
139
+ """Resolve the active localize mode, deferring to the app when unset."""
140
+ if self._localize is not None:
141
+ return self._localize
142
+ try:
143
+ from bootstack._runtime.app import get_app_settings
144
+ return get_app_settings().localize_mode
145
+ except Exception:
146
+ return 'auto'
147
+
148
+ def _display_text(self, raw: str) -> str:
149
+ """Map a raw value-key label to its display text under the active mode.
150
+
151
+ Returns the label unchanged when localization is off or no translation
152
+ is registered; otherwise the catalog translation.
153
+ """
154
+ if not raw:
155
+ return ''
156
+ if self._effective_localize() is False:
157
+ return raw
158
+ try:
159
+ from bootstack.i18n import MessageCatalog
160
+ return MessageCatalog.translate(raw) or raw
161
+ except Exception:
162
+ return raw
163
+
164
+ def _sync_display(self) -> None:
165
+ """Refresh the visible face from the current value-key text."""
166
+ self._display_var.set(self._display_text(self._textvariable.get()))
167
+
168
+ def _rebuild_option_maps(self) -> None:
169
+ """Rebuild the text<->value lookups from the current records.
170
+
171
+ Duplicate texts keep the first value; duplicate values keep the first
172
+ text. A SelectButton uses a single shared variable keyed on text, so
173
+ duplicate texts with distinct values cannot be told apart — keep option
174
+ texts unique.
175
+ """
176
+ self._value_by_text: dict[str, Any] = {}
177
+ self._text_by_value: dict[Any, str] = {}
178
+ for rec in self._records:
179
+ self._value_by_text.setdefault(rec.text, rec.value)
180
+ try:
181
+ self._text_by_value.setdefault(rec.value, rec.text)
182
+ except TypeError:
183
+ pass # unhashable value — no reverse mapping; display still works
184
+
185
+ def _resolve_initial_text(self, value: Any) -> str:
186
+ """Map an initial value-space value to its display text.
187
+
188
+ Returns the empty string for None/empty; raises if the value matches no
189
+ option.
190
+ """
191
+ if value is None or value == "":
192
+ return ""
193
+ try:
194
+ if value in self._text_by_value:
195
+ return self._text_by_value[value]
196
+ except TypeError:
197
+ pass
198
+ raise ValueError(f"{value!r} is not one of the options")
199
+
200
+ def _bind_change_event(self):
201
+ """(Re)bind textsignal to emit <<Change>> Tk events (in value-space).
202
+
203
+ The same subscription keeps the localized face in sync, since the value
204
+ key changes whenever a selection is made.
205
+ """
206
+ if self._bind_id is not None:
207
+ self._bind_id.cancel()
208
+
209
+ def _on_change(text: str) -> None:
210
+ self._sync_display()
211
+ self.event_generate(
212
+ '<<Change>>',
213
+ data=ChangeEvent(value=self._value_by_text.get(text, text) if text else None),
214
+ )
215
+
216
+ return self.textsignal.subscribe(_on_change)
217
+
218
+ def _build_context_menu(self):
219
+ # Affordance baked into the button image (focus ring + border line in
220
+ # source-px); aligning the menu's left edge to it matches the visible
221
+ # button border the same way the combobox popdown does.
222
+ from bootstack.style.style_builder_base import StyleBuilderBase
223
+ offset_x = StyleBuilderBase.scale_from_source(10)
224
+
225
+ density = self.configure_style_options('density') or 'default'
226
+
227
+ menu_items = []
228
+ for rec in self._records:
229
+ icon, disabled = option_display(rec)
230
+ # A per-option localize key overrides the widget-level mode.
231
+ item_localize = rec.extras.get("localize", self._localize)
232
+ menu_items.append(
233
+ ContextMenuItem(
234
+ type="radiobutton",
235
+ text=rec.text,
236
+ variable=self._textvariable,
237
+ value=rec.text,
238
+ icon=icon,
239
+ disabled=disabled,
240
+ localize=item_localize,
241
+ )
242
+ )
243
+ return ContextMenu(
244
+ self, target=self, items=menu_items,
245
+ anchor="nw", attach="sw",
246
+ offset=(offset_x, 0),
247
+ density=density,
248
+ # OptionMenu drives activation via `show_menu` (left-click,
249
+ # Return/KP_Enter), not ContextMenu's auto-trigger.
250
+ trigger=None,
251
+ )
252
+
253
+ def show_menu(self):
254
+ """Show the dropdown menu unless disabled or readonly."""
255
+ if not self.instate(("!disabled", "!readonly")):
256
+ return
257
+ # Match the menu's minimum width to the visible button width so the
258
+ # dropdown never renders narrower than its trigger.
259
+ from bootstack.style.style_builder_base import StyleBuilderBase
260
+ affordance = StyleBuilderBase.scale_from_source(10)
261
+ target_w = self.winfo_width() - 2 * affordance
262
+ if target_w > 0:
263
+ self._context_menu.configure(minwidth=max(150, target_w))
264
+ self._context_menu.show()
265
+
266
+ @property
267
+ def text(self) -> str:
268
+ """The display label currently shown on the button (translated)."""
269
+ return self._display_var.get()
270
+
271
+ @property
272
+ def selection(self) -> dict | None:
273
+ """The selected option as a full record dict (the data bag), or None."""
274
+ text = self._textvariable.get()
275
+ if text == "":
276
+ return None
277
+ for rec in self._records:
278
+ if rec.text == text:
279
+ return record_to_dict(rec)
280
+ return None
281
+
282
+ def get(self) -> Any:
283
+ """Return the current value (value-space), or `None` if nothing is selected."""
284
+ text = self._textvariable.get()
285
+ if text == "":
286
+ return None
287
+ return self._value_by_text.get(text, text)
288
+
289
+ def set(self, value: Any) -> None:
290
+ """Select the option whose value is `value`, updating the displayed text.
291
+
292
+ Passing None or `''` clears the selection. A value that matches no
293
+ option raises `ValueError`.
294
+ """
295
+ if value is None or value == "":
296
+ self._textvariable.set("")
297
+ return
298
+ try:
299
+ text = self._text_by_value[value]
300
+ except (KeyError, TypeError):
301
+ raise ValueError(f"{value!r} is not one of the options")
302
+ self._textvariable.set(text)
303
+
304
+ @property
305
+ def value(self) -> str:
306
+ """Get or set the current value."""
307
+ return self.get()
308
+
309
+ @value.setter
310
+ def value(self, value: Any) -> None:
311
+ self.set(value)
312
+
313
+ def on_changed(self, callback: Callable) -> str:
314
+ """Register a callback for `<<Change>>` events.
315
+
316
+ Args:
317
+ callback: Called when a menu item is selected. Receives a Tk event
318
+ object; `event.data` is a `ChangeEvent` with the attribute
319
+ `value` (the newly selected value, coerced to string).
320
+
321
+ Returns:
322
+ Bind ID — pass to `off_changed()` to unsubscribe.
323
+ """
324
+ return self.bind('<<Change>>', callback, add="+")
325
+
326
+ def off_changed(self, bind_id: str | None = None) -> None:
327
+ """Unsubscribe from `<<Change>>`.
328
+
329
+ Args:
330
+ bind_id: ID returned by `on_changed()`. If None, removes all.
331
+ """
332
+ self.unbind('<<Change>>', bind_id)
333
+
334
+ @configure_delegate('options')
335
+ def _delegate_options(self, value=None):
336
+ """Get the normalized option records, or set new options."""
337
+ if value is None:
338
+ return list(self._records)
339
+ else:
340
+ self._records = normalize_options(value)
341
+ self._rebuild_option_maps()
342
+ # Reconcile the current selection: if its display text is no longer
343
+ # one of the options, clear it so the button never shows a value the
344
+ # list can't offer (and no menu item stays phantom-selected).
345
+ if self._textvariable.get() not in self._value_by_text:
346
+ self._textvariable.set("")
347
+ if self._context_menu:
348
+ self._context_menu.destroy()
349
+ self._context_menu = self._build_context_menu()
350
+ return None
351
+
352
+ @configure_delegate('value')
353
+ def _delegate_value(self, value):
354
+ """Get or set the current value."""
355
+ if value is None:
356
+ return self.value
357
+ else:
358
+ self.value = value
359
+ return None
360
+
361
+ @configure_delegate('textsignal')
362
+ def _delegate_textsignal(self, value=None):
363
+ """Get or set the textsignal binding."""
364
+ if value is None:
365
+ return super()._delegate_textsignal()
366
+ else:
367
+ super()._delegate_textsignal(value)
368
+ self._bind_change_event()
369
+ return None
370
+
371
+ @configure_delegate('show_dropdown_button')
372
+ def _delegate_show_dropdown_button(self, value=None):
373
+ """Get or set visibility of the dropdown chevron."""
374
+ if value is None:
375
+ return self.configure_style_options('show_dropdown_button')
376
+ else:
377
+ self.configure_style_options(show_dropdown_button=value)
378
+ return self.rebuild_style()
379
+
380
+ @configure_delegate('dropdown_button_icon')
381
+ def _delegate_dropdown_button_icon(self, value):
382
+ """Get or set the dropdown chevron icon name."""
383
+ if value is None:
384
+ return self.configure_style_options('dropdown_button_icon')
385
+ else:
386
+ self.configure_style_options(dropdown_button_icon=value)
387
+ return self.rebuild_style()
@@ -0,0 +1,227 @@
1
+ from __future__ import annotations
2
+
3
+ import tkinter as tk
4
+ from typing import Any
5
+
6
+ from typing_extensions import Unpack
7
+
8
+ from bootstack.widgets._impl.primitives.frame import Frame, FrameKwargs
9
+ from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
10
+ from bootstack.widgets._core.container import normalize_fill
11
+ from bootstack.widgets.types import Anchor, Direction, Fill, Master, Side
12
+
13
+
14
+ class PackFrame(Frame):
15
+ """A Frame with simplified pack-based layout management.
16
+
17
+ PackFrame extends the bootstack Frame with automatic pack-based
18
+ layout management, including support for direction, gap spacing,
19
+ and default fill/expand behavior.
20
+
21
+ Children packed into this frame automatically receive the frame's
22
+ default layout options. Simply use the standard `pack()` method
23
+ on child widgets — no special `add()` method is needed.
24
+
25
+ """
26
+
27
+ SIDE_MAP: dict[Direction, Side] = {
28
+ "vertical": "top",
29
+ "column": "top",
30
+ "column-reverse": "bottom",
31
+ "horizontal": "left",
32
+ "row": "left",
33
+ "row-reverse": "right",
34
+ }
35
+
36
+ def __init__(
37
+ self,
38
+ master: Master = None,
39
+ *,
40
+ direction: Direction = "vertical",
41
+ gap: int = 0,
42
+ fill_items: Fill | None = None,
43
+ expand_items: bool | None = None,
44
+ anchor_items: Anchor | None = None,
45
+ propagate: bool | None = None,
46
+ **kwargs: Unpack[FrameKwargs],
47
+ ) -> None:
48
+ """Create a PackFrame with automatic pack-based layout.
49
+
50
+ Args:
51
+ master: Parent widget. If None, uses the default root window.
52
+ direction: Layout direction — `'vertical'`, `'horizontal'`,
53
+ `'row'`, `'column'`, `'row-reverse'`, or `'column-reverse'`.
54
+ Defaults to `'vertical'`.
55
+ gap: Spacing between children in pixels. Defaults to 0.
56
+ fill_items: Default `fill` option for children
57
+ (`'none'`, `'x'`, `'y'`, or `'both'`). If None, no default fill
58
+ is applied.
59
+ expand_items: Default `expand` option for children. If None, no
60
+ default expand is applied.
61
+ anchor_items: Default `anchor` for children. If None, no default
62
+ anchor is applied.
63
+ propagate: If False, the frame will not resize to fit its contents.
64
+ Defaults to None (Tk default behaviour).
65
+
66
+ Other Parameters:
67
+ **kwargs: Additional keyword arguments forwarded to `Frame`.
68
+ """
69
+ super().__init__(master, **kwargs)
70
+
71
+ self._direction = direction
72
+ self._gap = gap
73
+ self._default_fill = fill_items
74
+ self._default_expand = expand_items
75
+ self._default_anchor = anchor_items
76
+
77
+ # Ordered list of (widget, user_options) tuples for gap tracking
78
+ self._managed: list[tuple[tk.Widget, dict[str, Any]]] = []
79
+
80
+ if propagate is not None:
81
+ self.pack_propagate(propagate)
82
+
83
+ @property
84
+ def _side(self) -> Side:
85
+ """Get the pack side based on direction."""
86
+ return self.SIDE_MAP.get(self._direction, "top")
87
+
88
+ @configure_delegate('direction')
89
+ def _delegate_direction(self, value=None) -> Direction:
90
+ """Get or set the layout direction."""
91
+ if value is None:
92
+ return self._direction
93
+ self._direction = value
94
+ # Repack all widgets with new direction
95
+ self._repack_all()
96
+
97
+ @configure_delegate('gap')
98
+ def _delegate_gap(self, value=None) -> int:
99
+ """Get or set the gap between children."""
100
+ if value is None:
101
+ return self._gap
102
+ self._gap = value
103
+ # Repack all widgets with new gap
104
+ self._repack_all()
105
+
106
+ def _compute_gap(self, index: int) -> dict[str, Any]:
107
+ """Compute padding for gap based on position and direction."""
108
+ if index == 0 or self._gap == 0:
109
+ return {}
110
+
111
+ if self._direction in ("vertical", "column"):
112
+ return {"pady": (self._gap, 0)}
113
+ elif self._direction == "column-reverse":
114
+ return {"pady": (0, self._gap)}
115
+ elif self._direction in ("horizontal", "row"):
116
+ return {"padx": (self._gap, 0)}
117
+ elif self._direction == "row-reverse":
118
+ return {"padx": (0, self._gap)}
119
+ return {}
120
+
121
+ def _build_options(self, index: int, user_options: dict[str, Any]) -> dict[str, Any]:
122
+ """Build final pack options by merging defaults with user options."""
123
+ options: dict[str, Any] = {"in_": self, "side": self._side}
124
+
125
+ # Apply gap based on position
126
+ options.update(self._compute_gap(index))
127
+
128
+ # Apply container-level defaults
129
+ if self._default_fill is not None:
130
+ options["fill"] = self._default_fill
131
+ if self._default_expand is not None:
132
+ options["expand"] = self._default_expand
133
+ if self._default_anchor is not None:
134
+ options["anchor"] = self._default_anchor
135
+
136
+ # User options override everything
137
+ options.update(user_options)
138
+ # Normalize fill aliases so "horizontal"/"vertical" reach tkinter as "x"/"y"
139
+ if "fill" in options:
140
+ options["fill"] = normalize_fill(options["fill"])
141
+ return options
142
+
143
+ def _repack_all(self) -> None:
144
+ """Unpack and repack all widgets to maintain correct order and gaps."""
145
+ # Unpack all
146
+ for widget, _ in self._managed:
147
+ tk.Pack.forget(widget)
148
+
149
+ # Repack in order
150
+ for i, (widget, user_options) in enumerate(self._managed):
151
+ options = self._build_options(i, user_options)
152
+ tk.Pack.configure(widget, **options)
153
+
154
+ def _find_widget_index(self, widget: tk.Widget) -> int:
155
+ """Find index of widget in managed list, return -1 if not found."""
156
+ for i, (w, _) in enumerate(self._managed):
157
+ if w is widget:
158
+ return i
159
+ return -1
160
+
161
+ def _find_insert_index(self, before: tk.Widget = None, after: tk.Widget = None) -> int:
162
+ """Determine insertion index based on before/after options."""
163
+ if before is not None:
164
+ idx = self._find_widget_index(before)
165
+ if idx >= 0:
166
+ return idx
167
+ if after is not None:
168
+ idx = self._find_widget_index(after)
169
+ if idx >= 0:
170
+ return idx + 1
171
+ return len(self._managed)
172
+
173
+ # -------------------------------------------------------------------------
174
+ # Hook methods called by PackMixin
175
+ # -------------------------------------------------------------------------
176
+
177
+ def _on_child_pack(self, widget: tk.Widget, **options: Any) -> None:
178
+ """Hook called when a child widget calls pack().
179
+
180
+ Applies frame defaults, handles gap spacing, and tracks the widget.
181
+ """
182
+ # Check if widget is already managed (reconfigure case)
183
+ existing_idx = self._find_widget_index(widget)
184
+
185
+ # Determine insertion position from before/after
186
+ before = options.pop("before", None)
187
+ after = options.pop("after", None)
188
+
189
+ if existing_idx >= 0:
190
+ # Widget already managed - update its options
191
+ self._managed[existing_idx] = (widget, options)
192
+ self._repack_all()
193
+ else:
194
+ # New widget - find insertion point
195
+ insert_idx = self._find_insert_index(before, after)
196
+
197
+ if insert_idx < len(self._managed):
198
+ # Inserting in the middle - need to repack all
199
+ self._managed.insert(insert_idx, (widget, options))
200
+ self._repack_all()
201
+ else:
202
+ # Appending at the end - just pack it
203
+ pack_options = self._build_options(len(self._managed), options)
204
+ tk.Pack.configure(widget, **pack_options)
205
+ self._managed.append((widget, options))
206
+
207
+ def _on_child_pack_forget(self, widget: tk.Widget) -> None:
208
+ """Hook called when a child widget calls pack_forget().
209
+
210
+ Removes widget from tracking and repacks remaining widgets if needed.
211
+ """
212
+ idx = self._find_widget_index(widget)
213
+ if idx < 0:
214
+ # Not managed by us, just forget it normally
215
+ tk.Pack.forget(widget)
216
+ return
217
+
218
+ # Remove from our tracking
219
+ tk.Pack.forget(widget)
220
+ self._managed.pop(idx)
221
+
222
+ # Only repack if we removed something that affects gaps (not the last
223
+ # item). With no gap, Tk preserves the order of the remaining children
224
+ # on its own, so a full forget/repack is pure churn (it briefly clears
225
+ # the whole row — a visible flash) and is skipped.
226
+ if self._gap and idx < len(self._managed):
227
+ self._repack_all()
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from tkinter import ttk
4
+ from typing import Any
5
+ from typing_extensions import Unpack
6
+
7
+ from bootstack._core.mixins.ttk_state import TtkStateMixin
8
+ from bootstack._core.mixins.widget import WidgetCapabilitiesMixin
9
+ from bootstack.widgets._impl._internal.wrapper_base import TTKWrapperBase
10
+ from bootstack.widgets.types import Master, StyledKwargs, Orient
11
+
12
+
13
+ class PanedWindowKwargs(StyledKwargs, total=False):
14
+ # Standard ttk.Panedwindow options
15
+ orient: Orient
16
+ padding: Any
17
+ width: int
18
+ height: int
19
+
20
+ # bootstack-specific extensions
21
+
22
+
23
+ class PanedWindow(TTKWrapperBase, WidgetCapabilitiesMixin, TtkStateMixin, ttk.PanedWindow):
24
+ """bootstack wrapper for `ttk.Panedwindow` with themed styling support."""
25
+
26
+ _ttk_base = ttk.Panedwindow
27
+
28
+ def __init__(self, master: Master = None, **kwargs: Unpack[PanedWindowKwargs]) -> None:
29
+ """Create a themed bootstack Panedwindow.
30
+
31
+ Args:
32
+ master: Parent widget. If None, uses the default root window.
33
+
34
+ Other Parameters:
35
+ orient: Orientation of panes ('horizontal' or 'vertical').
36
+ padding: Extra internal padding.
37
+ width: Requested width in pixels.
38
+ height: Requested height in pixels.
39
+ style: Explicit ttk style name (overrides accent/variant).
40
+ accent: Accent token for styling, e.g. 'primary', 'secondary'.
41
+ variant: Style variant (if applicable).
42
+ surface: Optional surface token; otherwise inherited.
43
+ style_options: Optional dict forwarded to the style builder.
44
+ """
45
+ super().__init__(master, **kwargs)