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,18 @@
1
+ """Cross-platform menu bar.
2
+
3
+ A single platform-neutral menu *model* (`MenuModel` / `MenuGroup` / `MenuItem`)
4
+ is the source of truth; platform renderers consume it — a themed in-window strip
5
+ on Windows/Linux and the native global menu bar on macOS.
6
+
7
+ See `docs/_dev/menu-redesign.md` for the design.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ from bootstack.widgets._impl.composites.menu.model import (
12
+ MenuGroup,
13
+ MenuItem,
14
+ MenuItemType,
15
+ MenuModel,
16
+ )
17
+
18
+ __all__ = ["MenuModel", "MenuGroup", "MenuItem", "MenuItemType"]
@@ -0,0 +1,358 @@
1
+ """Platform-neutral menu model.
2
+
3
+ The menu bar is built up as data — an ordered list of `MenuGroup`s (File,
4
+ Edit, …), each holding a flat list of `MenuItem`s. This is the single source of
5
+ truth the platform renderers consume. The model is **single layer**: a group
6
+ holds items, and items never hold sub-items (no sub-submenus).
7
+
8
+ The model is pure data plus shortcut wiring:
9
+
10
+ - **Display** — every item resolves its `shortcut` to a platform-appropriate
11
+ accelerator string (`shortcut_display`) via the `Shortcuts` service.
12
+ - **Binding** — `bind_shortcuts()` registers *pattern* shortcuts
13
+ (e.g. `'Mod+S'`) with the `Shortcuts` service and binds them to a window so
14
+ the keypress actually fires. *Registered keys* (e.g. `'save'`) are treated as
15
+ display-only (their keypress is bound wherever they were registered). This is
16
+ renderer-independent: `tk.Menu` accelerators never bind on their own, and the
17
+ themed strip doesn't either.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import itertools
22
+ from dataclasses import dataclass, field
23
+ from typing import Any, Callable, Literal
24
+
25
+ from bootstack._runtime.shortcuts import (
26
+ classify_shortcut,
27
+ format_shortcut,
28
+ get_shortcuts,
29
+ )
30
+
31
+ MenuItemType = Literal["action", "check", "radio", "separator"]
32
+ """Kind of a menu item.
33
+
34
+ - `'action'` — a command that fires `on_click` when chosen.
35
+ - `'check'` — a toggle with an on/off state.
36
+ - `'radio'` — one choice within a named group.
37
+ - `'separator'` — a horizontal divider (carries no other fields).
38
+ """
39
+
40
+ # Process-wide counter so auto-registered pattern shortcuts get unique keys.
41
+ _shortcut_seq = itertools.count()
42
+
43
+
44
+ @dataclass
45
+ class MenuItem:
46
+ """A single entry in a menu group.
47
+
48
+ Only the fields relevant to `type` are meaningful; the rest stay at their
49
+ defaults. `icon` is honored by the Windows/Linux renderer and dropped on
50
+ macOS (native menus are text-only by convention).
51
+ """
52
+
53
+ type: MenuItemType = "action"
54
+ """The kind of item — see `MenuItemType`."""
55
+ text: str | None = None
56
+ """Display label. `None` only for separators."""
57
+ icon: str | None = None
58
+ """Icon name (Windows/Linux only; ignored on macOS)."""
59
+ on_click: Callable[[], Any] | None = None
60
+ """Called when the item is chosen (or its shortcut fires)."""
61
+ shortcut: str | None = None
62
+ """A registered shortcut key (display-only) or a pattern (`'Mod+S'`)."""
63
+ disabled: bool = False
64
+ """If `True`, the item is shown dimmed and cannot be chosen."""
65
+ checked: bool = False
66
+ """Initial on/off state for a `'check'` item."""
67
+ value: Any = None
68
+ """The value a `'radio'` item selects within its group."""
69
+ group: str | None = None
70
+ """Radio group name — items sharing a name are mutually exclusive."""
71
+ key: str | None = None
72
+ """Stable identifier for lookup; auto-derived from position if omitted."""
73
+
74
+ shortcut_display: str | None = field(default=None, init=False)
75
+ """Resolved accelerator text for display (set in `__post_init__`)."""
76
+
77
+ def __post_init__(self) -> None:
78
+ self.shortcut_display = format_shortcut(self.shortcut) or None
79
+
80
+
81
+ class MenuGroup:
82
+ """A top-level menu (e.g. File) holding a flat list of items.
83
+
84
+ Usable as a context manager so the builder reads naturally::
85
+
86
+ with model.add_menu("File") as file:
87
+ file.add_action("Open", shortcut="Mod+O", on_click=open_file)
88
+ """
89
+
90
+ def __init__(self, text: str, *, key: str | None = None) -> None:
91
+ self.text = text
92
+ self.key = key or text
93
+ self.items: list[MenuItem] = []
94
+ self.on_change: Callable[[], Any] | None = None
95
+ """Optional callback fired after any item is added (set by the host
96
+ facade to trigger a coalesced re-render)."""
97
+
98
+ # ----- builders -----
99
+
100
+ def add_action(
101
+ self,
102
+ text: str,
103
+ *,
104
+ icon: str | None = None,
105
+ on_click: Callable[[], Any] | None = None,
106
+ shortcut: str | None = None,
107
+ disabled: bool = False,
108
+ key: str | None = None,
109
+ ) -> MenuItem:
110
+ """Add a command item that fires `on_click` when chosen."""
111
+ return self._append(
112
+ MenuItem(
113
+ type="action",
114
+ text=text,
115
+ icon=icon,
116
+ on_click=on_click,
117
+ shortcut=shortcut,
118
+ disabled=disabled,
119
+ key=key,
120
+ )
121
+ )
122
+
123
+ def add_check(
124
+ self,
125
+ text: str,
126
+ *,
127
+ checked: bool = False,
128
+ on_click: Callable[[], Any] | None = None,
129
+ shortcut: str | None = None,
130
+ disabled: bool = False,
131
+ key: str | None = None,
132
+ ) -> MenuItem:
133
+ """Add a toggle item with an on/off state."""
134
+ return self._append(
135
+ MenuItem(
136
+ type="check",
137
+ text=text,
138
+ checked=checked,
139
+ on_click=on_click,
140
+ shortcut=shortcut,
141
+ disabled=disabled,
142
+ key=key,
143
+ )
144
+ )
145
+
146
+ def add_radio(
147
+ self,
148
+ text: str,
149
+ *,
150
+ value: Any = None,
151
+ group: str | None = None,
152
+ on_click: Callable[[], Any] | None = None,
153
+ disabled: bool = False,
154
+ key: str | None = None,
155
+ ) -> MenuItem:
156
+ """Add a radio item — one choice within `group`."""
157
+ return self._append(
158
+ MenuItem(
159
+ type="radio",
160
+ text=text,
161
+ value=value if value is not None else text,
162
+ group=group or self.key,
163
+ on_click=on_click,
164
+ disabled=disabled,
165
+ key=key,
166
+ )
167
+ )
168
+
169
+ def add_divider(self, *, key: str | None = None) -> MenuItem:
170
+ """Add a horizontal divider."""
171
+ return self._append(MenuItem(type="separator", key=key))
172
+
173
+ # ----- internals -----
174
+
175
+ def _append(self, item: MenuItem) -> MenuItem:
176
+ if item.key is None:
177
+ item.key = f"{self.key}.{len(self.items)}"
178
+ self.items.append(item)
179
+ if self.on_change is not None:
180
+ self.on_change()
181
+ return item
182
+
183
+ def __enter__(self) -> "MenuGroup":
184
+ return self
185
+
186
+ def __exit__(self, *exc: Any) -> bool:
187
+ return False
188
+
189
+ def __len__(self) -> int:
190
+ return len(self.items)
191
+
192
+ def __repr__(self) -> str:
193
+ return f"MenuGroup(text={self.text!r}, items={len(self.items)})"
194
+
195
+
196
+ # Item dict keys recognized by the declarative `load()` form.
197
+ _ITEM_KEYS = frozenset(
198
+ {"type", "text", "icon", "on_click", "shortcut", "disabled", "checked",
199
+ "value", "group", "key"}
200
+ )
201
+
202
+
203
+ class MenuModel:
204
+ """The whole menu bar — an ordered list of `MenuGroup`s.
205
+
206
+ Build imperatively with `add_menu()`, or declaratively with `load()`; both
207
+ produce the same structure.
208
+ """
209
+
210
+ def __init__(self) -> None:
211
+ self.groups: list[MenuGroup] = []
212
+ self._bound_window: Any = None
213
+ self._registered_keys: list[str] = []
214
+
215
+ # ----- building -----
216
+
217
+ def add_menu(self, text: str, *, key: str | None = None) -> MenuGroup:
218
+ """Append a top-level menu and return it (context-manager capable)."""
219
+ group = MenuGroup(text, key=key)
220
+ self.groups.append(group)
221
+ return group
222
+
223
+ def load(self, spec: list[dict]) -> None:
224
+ """Build the model from a declarative spec, replacing any contents.
225
+
226
+ Each top-level dict is a menu group: `{"text": ..., "items": [...]}`.
227
+ Each item dict carries `type` (default `'action'`) plus the fields for
228
+ that type. Items must NOT contain a nested `items` key — the model is
229
+ single layer.
230
+
231
+ Args:
232
+ spec: List of group dicts.
233
+
234
+ Raises:
235
+ ValueError: If an item nests its own `items` (no sub-submenus), or
236
+ an unknown item field is supplied.
237
+ """
238
+ self.clear()
239
+ for raw_group in spec:
240
+ group = self.add_menu(raw_group["text"], key=raw_group.get("key"))
241
+ for raw_item in raw_group.get("items", []):
242
+ self._load_item(group, raw_item)
243
+
244
+ @staticmethod
245
+ def _load_item(group: MenuGroup, raw: dict) -> None:
246
+ if "items" in raw:
247
+ raise ValueError(
248
+ "Menu items cannot contain nested 'items' — the menu bar is "
249
+ "single layer (no sub-submenus)."
250
+ )
251
+ unknown = set(raw) - _ITEM_KEYS
252
+ if unknown:
253
+ raise ValueError(
254
+ f"Unknown menu item field(s): {', '.join(sorted(unknown))}. "
255
+ f"Allowed: {', '.join(sorted(_ITEM_KEYS))}."
256
+ )
257
+ item_type = raw.get("type", "action")
258
+ if item_type in ("divider", "separator"):
259
+ group.add_divider(key=raw.get("key"))
260
+ elif item_type == "check":
261
+ group.add_check(
262
+ raw.get("text"),
263
+ checked=raw.get("checked", False),
264
+ on_click=raw.get("on_click"),
265
+ shortcut=raw.get("shortcut"),
266
+ disabled=raw.get("disabled", False),
267
+ key=raw.get("key"),
268
+ )
269
+ elif item_type == "radio":
270
+ group.add_radio(
271
+ raw.get("text"),
272
+ value=raw.get("value"),
273
+ group=raw.get("group"),
274
+ on_click=raw.get("on_click"),
275
+ disabled=raw.get("disabled", False),
276
+ key=raw.get("key"),
277
+ )
278
+ elif item_type == "action":
279
+ group.add_action(
280
+ raw.get("text"),
281
+ icon=raw.get("icon"),
282
+ on_click=raw.get("on_click"),
283
+ shortcut=raw.get("shortcut"),
284
+ disabled=raw.get("disabled", False),
285
+ key=raw.get("key"),
286
+ )
287
+ else:
288
+ raise ValueError(
289
+ f"Unknown menu item type {item_type!r}. Use 'action', 'check', "
290
+ f"'radio', or 'divider'."
291
+ )
292
+
293
+ def clear(self) -> None:
294
+ """Remove all groups and release any registered shortcuts."""
295
+ self.unbind_shortcuts()
296
+ self.groups = []
297
+
298
+ # ----- iteration -----
299
+
300
+ def iter_items(self):
301
+ """Yield every `MenuItem` across all groups, in order."""
302
+ for group in self.groups:
303
+ yield from group.items
304
+
305
+ def __len__(self) -> int:
306
+ return len(self.groups)
307
+
308
+ def __iter__(self):
309
+ return iter(self.groups)
310
+
311
+ # ----- shortcuts -----
312
+
313
+ def bind_shortcuts(self, window: Any = None) -> None:
314
+ """Register pattern shortcuts and bind them so keypresses fire.
315
+
316
+ Idempotent: previously registered pattern shortcuts are released
317
+ first, then re-registered from the current model. Items whose
318
+ `shortcut` is a *registered key* or a *literal* are left alone (their
319
+ keypress is bound elsewhere, or the text is display-only). Items
320
+ without an `on_click` are skipped — there would be nothing to fire.
321
+
322
+ Args:
323
+ window: Window (App/Toplevel) to bind the shortcuts to. If omitted,
324
+ the last window passed to `bind_shortcuts` is reused, if any.
325
+ Shortcuts still register without a window; they bind once one
326
+ is known.
327
+ """
328
+ self.unbind_shortcuts()
329
+ if window is not None:
330
+ self._bound_window = window
331
+
332
+ shortcuts = get_shortcuts()
333
+ for item in self.iter_items():
334
+ if not item.shortcut or item.on_click is None:
335
+ continue
336
+ if classify_shortcut(item.shortcut) != "pattern":
337
+ continue # registered key or literal — bind handled elsewhere
338
+ reg_key = f"__menu_{next(_shortcut_seq)}"
339
+ shortcuts.register(reg_key, item.shortcut, item.on_click)
340
+ self._registered_keys.append(reg_key)
341
+
342
+ if self._bound_window is not None:
343
+ shortcuts.bind_to(self._bound_window)
344
+
345
+ def unbind_shortcuts(self) -> None:
346
+ """Release every pattern shortcut this model registered."""
347
+ if not self._registered_keys:
348
+ return
349
+ shortcuts = get_shortcuts()
350
+ for reg_key in self._registered_keys:
351
+ try:
352
+ shortcuts.unregister(reg_key)
353
+ except KeyError:
354
+ pass
355
+ self._registered_keys = []
356
+
357
+ def __repr__(self) -> str:
358
+ return f"MenuModel(groups={[g.text for g in self.groups]})"
@@ -0,0 +1,134 @@
1
+ """macOS renderer — the native global menu bar.
2
+
3
+ Builds a flat `tk.Menu` cascade structure from a `MenuModel` and assigns it as
4
+ the window's menubar (`root['menu']`). On macOS this materializes as the native
5
+ global menu bar at the top of the screen.
6
+
7
+ Deliberately thin (this is why `MenuManager` is retired): single layer, **no
8
+ icons** (native Mac menus are text-only by convention), label translation for
9
+ semantic keys, and accelerator text for display. Actual key binding is owned by
10
+ `MenuModel.bind_shortcuts`, not the menu — `tk.Menu` accelerators never fire on
11
+ their own.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import tkinter as tk
16
+ from typing import Any
17
+
18
+ from bootstack._runtime.shortcuts import tk_aqua_accelerator
19
+ from bootstack.widgets._impl.composites.menu.model import MenuItem, MenuModel
20
+
21
+
22
+ def _translate(text: str | None) -> str:
23
+ """Resolve a semantic message key; pass plain text through unchanged."""
24
+ if not text:
25
+ return ""
26
+ try:
27
+ from bootstack.i18n import MessageCatalog
28
+
29
+ return MessageCatalog.translate(text) or text
30
+ except Exception:
31
+ return text
32
+
33
+
34
+ class NativeMenuBar:
35
+ """Native menubar (macOS) built from a `MenuModel` and attached to a window.
36
+
37
+ Call `rebuild()` to re-render after the model changes; `destroy()` detaches
38
+ the menubar from the window.
39
+ """
40
+
41
+ def __init__(self, root: Any, model: MenuModel) -> None:
42
+ self._root = root
43
+ self._model = model
44
+ self._menubar: tk.Menu | None = None
45
+ # Strong refs so Tk variables backing check/radio items aren't GC'd
46
+ # while the menu holds them.
47
+ self._var_refs: list[Any] = []
48
+ self.rebuild()
49
+
50
+ @property
51
+ def model(self) -> MenuModel:
52
+ """The menu model this menubar renders."""
53
+ return self._model
54
+
55
+ def set_model(self, model: MenuModel) -> None:
56
+ """Swap in a new model and re-render."""
57
+ self._model = model
58
+ self.rebuild()
59
+
60
+ def rebuild(self) -> None:
61
+ """Rebuild the cascade structure and (re)attach it to the window."""
62
+ menubar = tk.Menu(self._root, tearoff=0)
63
+ self._var_refs = []
64
+ radio_vars: dict[str, tk.StringVar] = {}
65
+
66
+ for group in self._model:
67
+ submenu = tk.Menu(menubar, tearoff=0)
68
+ for item in group.items:
69
+ self._add_item(submenu, item, radio_vars)
70
+ menubar.add_cascade(label=_translate(group.text), menu=submenu)
71
+
72
+ self._root["menu"] = menubar
73
+ self._menubar = menubar
74
+
75
+ def _add_item(
76
+ self,
77
+ submenu: tk.Menu,
78
+ item: MenuItem,
79
+ radio_vars: dict[str, tk.StringVar],
80
+ ) -> None:
81
+ if item.type == "separator":
82
+ submenu.add_separator()
83
+ return
84
+
85
+ if item.type == "check":
86
+ var = tk.BooleanVar(value=item.checked)
87
+ self._var_refs.append(var)
88
+ submenu.add_checkbutton(
89
+ label=_translate(item.text),
90
+ variable=var,
91
+ command=item.on_click,
92
+ state="disabled" if item.disabled else "normal",
93
+ )
94
+ return
95
+
96
+ if item.type == "radio":
97
+ var = radio_vars.setdefault(item.group or "", tk.StringVar())
98
+ if var not in self._var_refs:
99
+ self._var_refs.append(var)
100
+ submenu.add_radiobutton(
101
+ label=_translate(item.text),
102
+ variable=var,
103
+ value=item.value,
104
+ command=item.on_click,
105
+ state="disabled" if item.disabled else "normal",
106
+ )
107
+ return
108
+
109
+ # action
110
+ opts: dict[str, Any] = {
111
+ "label": _translate(item.text),
112
+ "command": item.on_click,
113
+ "state": "disabled" if item.disabled else "normal",
114
+ }
115
+ # Native menus need the word-form accelerator (Tk renders the glyphs);
116
+ # the pre-symbolized display would show the modifier but drop the key.
117
+ acc = tk_aqua_accelerator(item.shortcut)
118
+ if acc:
119
+ opts["accelerator"] = acc
120
+ submenu.add_command(**opts)
121
+
122
+ def destroy(self) -> None:
123
+ """Detach the menubar from the window and drop it."""
124
+ try:
125
+ self._root["menu"] = ""
126
+ except Exception:
127
+ pass
128
+ if self._menubar is not None:
129
+ try:
130
+ self._menubar.destroy()
131
+ except Exception:
132
+ pass
133
+ self._menubar = None
134
+ self._var_refs = []
@@ -0,0 +1,134 @@
1
+ """Windows/Linux renderer — a themed in-window menu-bar strip.
2
+
3
+ Builds a horizontal strip of trigger buttons (one per `MenuGroup`) from a
4
+ `MenuModel`. Each trigger is a `DropdownButton` whose popdown is the themed
5
+ `_ToplevelContextMenu` backend, so theme tokens, density, and icons all apply.
6
+ macOS uses the native renderer instead (`render_native`).
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from tkinter import StringVar
11
+ from typing import Any
12
+
13
+ from bootstack.widgets._impl.composites.contextmenu import ContextMenuItem
14
+ from bootstack.widgets._impl.composites.dropdownbutton import DropdownButton
15
+ from bootstack.widgets._impl.primitives.packframe import PackFrame
16
+ from bootstack.widgets._impl.composites.menu.model import MenuItem, MenuModel
17
+ from bootstack.widgets.types import Master, WidgetDensity
18
+
19
+
20
+ def menu_item_to_context_item(
21
+ item: MenuItem, radio_vars: dict[str, StringVar]
22
+ ) -> ContextMenuItem:
23
+ """Translate a model `MenuItem` into a `ContextMenuItem`.
24
+
25
+ Maps the public type names to the ContextMenu backend's Tk-derived names
26
+ (`action`->`command`, `check`->`checkbutton`, `radio`->`radiobutton`). The
27
+ raw `shortcut` is forwarded for display only — actual key binding is owned by
28
+ `MenuModel.bind_shortcuts`. `radio_vars` holds one shared `StringVar` per
29
+ radio-group name so items in a group are mutually exclusive; it is mutated in
30
+ place as new groups are seen.
31
+ """
32
+ if item.type == "separator":
33
+ return ContextMenuItem("separator", key=item.key)
34
+
35
+ if item.type == "check":
36
+ return ContextMenuItem(
37
+ "checkbutton",
38
+ text=item.text,
39
+ value=item.checked,
40
+ command=item.on_click,
41
+ key=item.key,
42
+ )
43
+
44
+ if item.type == "radio":
45
+ var = radio_vars.setdefault(item.group or "", StringVar())
46
+ return ContextMenuItem(
47
+ "radiobutton",
48
+ text=item.text,
49
+ value=item.value,
50
+ variable=var,
51
+ command=item.on_click,
52
+ disabled=item.disabled,
53
+ key=item.key,
54
+ )
55
+
56
+ # action
57
+ kwargs: dict[str, Any] = {
58
+ "text": item.text,
59
+ "command": item.on_click,
60
+ "disabled": item.disabled,
61
+ "key": item.key,
62
+ }
63
+ if item.icon:
64
+ kwargs["icon"] = item.icon
65
+ if item.shortcut:
66
+ kwargs["shortcut"] = item.shortcut
67
+ return ContextMenuItem("command", **kwargs)
68
+
69
+
70
+ class ThemedMenuBar(PackFrame):
71
+ """Themed menu-bar strip (Windows/Linux) rendered from a `MenuModel`.
72
+
73
+ The strip is a horizontal row of dropdown triggers. Call `rebuild()` to
74
+ re-render after the model changes.
75
+ """
76
+
77
+ def __init__(
78
+ self,
79
+ master: Master = None,
80
+ model: MenuModel | None = None,
81
+ *,
82
+ density: WidgetDensity = "compact",
83
+ surface: str = "chrome",
84
+ **kwargs: Any,
85
+ ) -> None:
86
+ kwargs.setdefault("surface", surface)
87
+ super().__init__(master, direction="horizontal", fill_items="y", **kwargs)
88
+ self._model = model if model is not None else MenuModel()
89
+ self._density = density
90
+ # Threaded into the menubar-item triggers so they paint this surface
91
+ # (the builders paint the normal bg with the surface color, not parent).
92
+ self._surface = surface
93
+ self._triggers: list[DropdownButton] = []
94
+ # One shared Tk variable per radio group name so radio items in the
95
+ # same group are mutually exclusive.
96
+ self._radio_vars: dict[str, StringVar] = {}
97
+ self.rebuild()
98
+
99
+ @property
100
+ def model(self) -> MenuModel:
101
+ """The menu model this strip renders."""
102
+ return self._model
103
+
104
+ def set_model(self, model: MenuModel) -> None:
105
+ """Swap in a new model and re-render."""
106
+ self._model = model
107
+ self.rebuild()
108
+
109
+ def rebuild(self) -> None:
110
+ """Tear down and re-render every trigger from the current model."""
111
+ for trigger in self._triggers:
112
+ try:
113
+ trigger.destroy()
114
+ except Exception:
115
+ pass
116
+ self._triggers = []
117
+ self._radio_vars = {}
118
+
119
+ for group in self._model:
120
+ items = [
121
+ menu_item_to_context_item(item, self._radio_vars)
122
+ for item in group.items
123
+ ]
124
+ trigger = DropdownButton(
125
+ self,
126
+ text=group.text,
127
+ items=items,
128
+ variant="menubar-item",
129
+ show_dropdown_button=False,
130
+ density=self._density,
131
+ surface=self._surface,
132
+ )
133
+ trigger.pack(side="left")
134
+ self._triggers.append(trigger)