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,608 @@
1
+ """Toolbar widget with customizable content and optional window controls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from tkinter import Widget
7
+ from typing import TYPE_CHECKING, Any, Callable, Literal
8
+
9
+ from typing_extensions import TypedDict, Unpack
10
+
11
+ from bootstack.widgets._impl.primitives.frame import Frame
12
+ from bootstack.widgets._impl.primitives.button import Button
13
+ from bootstack.widgets._impl.primitives.label import Label
14
+ from bootstack.widgets._impl.primitives.separator import Separator
15
+ from bootstack.widgets._impl.mixins import configure_delegate
16
+ from bootstack.widgets.types import Master, SurfaceToken, WidgetDensity
17
+
18
+ if TYPE_CHECKING:
19
+ from tkinter import StringVar
20
+
21
+ from bootstack.widgets._impl.composites.dropdownbutton import DropdownButton
22
+ from bootstack.widgets._impl.composites.menu.model import MenuGroup, MenuModel
23
+
24
+
25
+ class ToolbarKwargs(TypedDict, total=False):
26
+ show_window_controls: bool
27
+ draggable: bool
28
+ button_variant: str
29
+ density: WidgetDensity
30
+ padding: Any
31
+ # Frame options
32
+ width: int
33
+ height: int
34
+ surface: SurfaceToken | str
35
+ show_border: bool
36
+
37
+
38
+ class Toolbar(Frame):
39
+ """A horizontal toolbar with customizable content and optional window controls.
40
+
41
+ Toolbar provides a container for icon buttons, labels, and other widgets
42
+ arranged horizontally. It optionally supports window control buttons
43
+ (minimize, maximize, close) and window dragging for custom titlebars.
44
+
45
+ Items are added from left to right. Use `add_spacer()` to push
46
+ subsequent items to the right side.
47
+
48
+ """
49
+
50
+ def __init__(
51
+ self,
52
+ master: Master = None,
53
+ show_window_controls: bool = False,
54
+ draggable: bool = False,
55
+ button_variant: str = 'ghost',
56
+ density: Literal['default', 'compact'] = 'default',
57
+ padding: int | tuple = None,
58
+ **kwargs: Unpack[ToolbarKwargs]
59
+ ):
60
+ """Initialize a Toolbar.
61
+
62
+ Args:
63
+ master: Parent widget.
64
+ show_window_controls: Show minimize/maximize/close buttons. Default False.
65
+ draggable: Enable window dragging by clicking and dragging the toolbar.
66
+ Default False.
67
+ button_variant: Default variant for toolbar buttons. Default 'ghost'.
68
+ density: Button density for toolbar items. 'compact' for smaller buttons,
69
+ 'default' for standard size. Default 'default'.
70
+ padding: Toolbar padding. If None, uses a uniform density-based
71
+ default (2 for compact, 3 for default) so items have even
72
+ breathing room on all sides.
73
+ **kwargs: Additional arguments passed to Frame.
74
+ """
75
+ if padding is None:
76
+ padding = 2 if density == 'compact' else 3
77
+ super().__init__(master, padding=padding, **kwargs)
78
+
79
+ self._show_window_controls = show_window_controls
80
+ self._draggable = draggable or show_window_controls # Auto-enable drag with window controls
81
+ self._button_variant = button_variant
82
+ self._density = density
83
+
84
+ # Content container (left side)
85
+ self._content_frame = Frame(self)
86
+ self._content_frame.pack(side='left', fill='both', expand=True)
87
+
88
+ # Window controls container (right side)
89
+ self._controls_frame: Frame | None = None
90
+ if show_window_controls:
91
+ self._build_window_controls()
92
+
93
+ # Drag state
94
+ self._drag_start_x = 0
95
+ self._drag_start_y = 0
96
+
97
+ if self._draggable:
98
+ self._setup_drag()
99
+
100
+ # Menu state — populated lazily by add_menu(). The model is the single
101
+ # source of truth for this toolbar's menus (so a host can aggregate them
102
+ # for the macOS native bar); the triggers are the in-window dropdowns.
103
+ self._menu_model: MenuModel | None = None
104
+ self._menu_triggers: dict[str, DropdownButton] = {}
105
+ self._menu_radio_vars: dict[str, StringVar] = {}
106
+ self._menu_rebind_pending: Any = None
107
+ # Optional host hook fired when this toolbar's menus change, so a window
108
+ # can rebuild the aggregated macOS native menu bar.
109
+ self._on_menu_change: Callable[[], Any] | None = None
110
+
111
+ # Cancel a pending menu-shortcut rebind if the bar is destroyed first.
112
+ self.bind("<Destroy>", self._on_destroy, add="+")
113
+
114
+ @staticmethod
115
+ def _control_glyph(name: str) -> dict:
116
+ """A title-bar control glyph spec — a small (~14px) glyph regardless of
117
+ the toolbar density, for a tight native-style control rather than a
118
+ chunky compact-icon button (the 17px compact-icon default)."""
119
+ return {'name': name, 'size': 14}
120
+
121
+ def _build_window_controls(self):
122
+ """Build window control buttons (minimize, maximize, close)."""
123
+ self._controls_frame = Frame(self)
124
+ self._controls_frame.pack(side='right', fill='y', padx=(8, 0))
125
+
126
+ # Minimize button
127
+ self._minimize_btn = Button(
128
+ self._controls_frame,
129
+ icon=self._control_glyph('dash-lg'),
130
+ icon_only=True,
131
+ variant='ghost',
132
+ density='compact',
133
+ surface=self._surface,
134
+ command=self._on_minimize,
135
+ )
136
+ self._minimize_btn.pack(side='left', padx=2)
137
+
138
+ # Maximize/Restore button
139
+ self._maximize_btn = Button(
140
+ self._controls_frame,
141
+ icon=self._control_glyph('fullscreen'),
142
+ icon_only=True,
143
+ variant='ghost',
144
+ density='compact',
145
+ surface=self._surface,
146
+ command=self._on_maximize,
147
+ )
148
+ self._maximize_btn.pack(side='left', padx=2)
149
+
150
+ # Close button — danger accent so it hovers red (the native convention).
151
+ self._close_btn = Button(
152
+ self._controls_frame,
153
+ icon=self._control_glyph('x-lg'),
154
+ icon_only=True,
155
+ variant='ghost',
156
+ accent='danger',
157
+ density='compact',
158
+ surface=self._surface,
159
+ command=self._on_close,
160
+ )
161
+ self._close_btn.pack(side='left', padx=2)
162
+
163
+ # Disable maximize if window is not resizable (checked after idle so
164
+ # the window geometry is finalised).
165
+ self.after_idle(self._sync_maximize_state)
166
+
167
+ def _sync_maximize_state(self) -> None:
168
+ """Hide the maximize button when the window is not resizable."""
169
+ try:
170
+ w, h = self.winfo_toplevel().resizable()
171
+ if not w and not h:
172
+ self._maximize_btn.pack_forget()
173
+ except Exception:
174
+ pass
175
+
176
+ def _on_minimize(self):
177
+ """Minimize the window, keeping it recoverable from the taskbar."""
178
+ window = self.winfo_toplevel()
179
+ if not window.overrideredirect():
180
+ window.iconify()
181
+ return
182
+ # A borderless (override_redirect) window has no taskbar button by
183
+ # default, so a plain iconify() would leave it with no way back. On
184
+ # Windows, give it the APPWINDOW taskbar style and minimize via the Win32
185
+ # API — it stays borderless and the taskbar button restores it. Elsewhere,
186
+ # fall back to briefly restoring native decorations (which grant a taskbar
187
+ # button), iconify, then re-hide them once the user restores it.
188
+ if sys.platform == 'win32' and self._win32_minimize(window):
189
+ return
190
+ window.update_idletasks()
191
+ window.overrideredirect(False)
192
+ window.update_idletasks()
193
+ window.iconify()
194
+ window.bind('<Map>', self._on_restore_override_redirect, add='+')
195
+
196
+ def _win32_minimize(self, window) -> bool:
197
+ """Minimize a borderless window via Win32, with a taskbar button.
198
+
199
+ Adds `WS_EX_APPWINDOW` (and drops `WS_EX_TOOLWINDOW`) so the borderless
200
+ window gets a taskbar button, then minimizes it. Returns True on success.
201
+ """
202
+ try:
203
+ from ctypes import windll
204
+
205
+ GWL_EXSTYLE = -20
206
+ WS_EX_APPWINDOW = 0x00040000
207
+ WS_EX_TOOLWINDOW = 0x00000080
208
+ SW_HIDE, SW_SHOW, SW_MINIMIZE = 0, 5, 6
209
+
210
+ hwnd = windll.user32.GetParent(window.winfo_id()) or window.winfo_id()
211
+ style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
212
+ new_style = (style & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW
213
+ if new_style != style:
214
+ windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, new_style)
215
+ # The taskbar only re-reads the style on a re-show, so cycle the
216
+ # window once (a brief flash) the first time the style changes.
217
+ windll.user32.ShowWindow(hwnd, SW_HIDE)
218
+ windll.user32.ShowWindow(hwnd, SW_SHOW)
219
+ windll.user32.ShowWindow(hwnd, SW_MINIMIZE)
220
+ return True
221
+ except Exception:
222
+ return False
223
+
224
+ def _on_restore_override_redirect(self, event=None) -> None:
225
+ """Restore override_redirect after the window is deiconified."""
226
+ window = self.winfo_toplevel()
227
+ if event is not None and getattr(event, 'widget', window) is not window:
228
+ return
229
+ window.unbind('<Map>')
230
+ window.overrideredirect(True)
231
+
232
+ def _on_maximize(self):
233
+ """Handle maximize/restore button click."""
234
+ window = self.winfo_toplevel()
235
+ w, h = window.resizable()
236
+ if not w and not h:
237
+ return
238
+ if window.state() == 'zoomed':
239
+ window.state('normal')
240
+ self._maximize_btn.configure(icon=self._control_glyph('fullscreen'))
241
+ else:
242
+ window.state('zoomed')
243
+ self._maximize_btn.configure(icon=self._control_glyph('fullscreen-exit'))
244
+
245
+ def _on_close(self):
246
+ """Handle close button click."""
247
+ window = self.winfo_toplevel()
248
+ window.destroy()
249
+
250
+ def _setup_drag(self):
251
+ """Set up window dragging (and double-click-to-maximize) bindings."""
252
+ for widget in (self, self._content_frame):
253
+ widget.bind('<Button-1>', self._on_drag_start, add='+')
254
+ widget.bind('<B1-Motion>', self._on_drag_motion, add='+')
255
+ widget.bind('<Double-Button-1>', self._on_drag_double, add='+')
256
+
257
+ def _on_drag_start(self, event):
258
+ """Record drag start position."""
259
+ self._drag_start_x = event.x_root
260
+ self._drag_start_y = event.y_root
261
+
262
+ def _on_drag_motion(self, event):
263
+ """Handle drag motion to move window."""
264
+ window = self.winfo_toplevel()
265
+
266
+ # A maximized window snaps back to its normal size before it can move.
267
+ # The grab offset was captured against the *maximized* width, so simply
268
+ # restoring would leave the window offset from the cursor (#165). Keep the
269
+ # pointer on the title bar: capture where the cursor sits over the
270
+ # maximized window (its horizontal fraction + vertical offset), restore,
271
+ # then reposition the smaller window so the cursor lands at the same spot.
272
+ if window.state() == 'zoomed':
273
+ max_w = max(window.winfo_width(), 1)
274
+ frac_x = (event.x_root - window.winfo_rootx()) / max_w
275
+ offset_y = event.y_root - window.winfo_rooty()
276
+ try:
277
+ window.state('normal')
278
+ except Exception:
279
+ pass
280
+ window.update_idletasks()
281
+ new_x = int(event.x_root - frac_x * window.winfo_width())
282
+ new_y = int(event.y_root - offset_y)
283
+ window.geometry(f'+{new_x}+{new_y}')
284
+ self._sync_maximize_icon('fullscreen')
285
+ self._drag_start_x = event.x_root
286
+ self._drag_start_y = event.y_root
287
+ return
288
+
289
+ # Calculate delta
290
+ dx = event.x_root - self._drag_start_x
291
+ dy = event.y_root - self._drag_start_y
292
+
293
+ # Get current position
294
+ x = window.winfo_x() + dx
295
+ y = window.winfo_y() + dy
296
+
297
+ # Move window
298
+ window.geometry(f'+{x}+{y}')
299
+
300
+ # Update drag start for next motion
301
+ self._drag_start_x = event.x_root
302
+ self._drag_start_y = event.y_root
303
+
304
+ def _on_drag_double(self, _event):
305
+ """Double-clicking the bar toggles the window's maximized state."""
306
+ self._on_maximize()
307
+ return 'break'
308
+
309
+ def _sync_maximize_icon(self, name: str) -> None:
310
+ """Keep the maximize button's icon in sync with the window state."""
311
+ btn = getattr(self, '_maximize_btn', None)
312
+ if btn is not None:
313
+ try:
314
+ btn.configure(icon=self._control_glyph(name))
315
+ except Exception:
316
+ pass
317
+
318
+ # --- Public API: Adding Items ---
319
+
320
+ def add_button(
321
+ self,
322
+ icon: str | dict = None,
323
+ text: str = None,
324
+ command: Callable = None,
325
+ accent: str = None,
326
+ variant: str = None,
327
+ **kwargs
328
+ ) -> Button:
329
+ """Add a button to the toolbar.
330
+
331
+ Args:
332
+ icon: Icon name or configuration.
333
+ text: Button text. If None and icon provided, creates icon-only button.
334
+ command: Button click callback.
335
+ accent: Button accent token.
336
+ variant: Button variant. Uses toolbar default if None.
337
+ **kwargs: Additional arguments passed to Button.
338
+
339
+ Returns:
340
+ Button: The created button.
341
+ """
342
+ btn = Button(
343
+ self._content_frame,
344
+ icon=icon,
345
+ text=text,
346
+ icon_only=(icon is not None and text is None),
347
+ command=command,
348
+ accent=accent,
349
+ variant=variant or self._button_variant,
350
+ density=kwargs.pop('density', self._density),
351
+ surface=kwargs.pop('surface', self._surface),
352
+ **kwargs
353
+ )
354
+ btn.pack(side='left')
355
+ return btn
356
+
357
+ def add_menu(self, text: str, *, key: str | None = None) -> "MenuGroup":
358
+ """Add a dropdown menu (File / Edit / …) as a toolbar item.
359
+
360
+ Returns the menu's `MenuGroup` builder — add items with
361
+ `add_action` / `add_check` / `add_radio` / `add_divider`, ideally in a
362
+ `with` block. On Windows/Linux the menu renders as an in-window dropdown
363
+ trigger in the toolbar; the menu also feeds this toolbar's menu model so a
364
+ host can surface it in the macOS native menu bar.
365
+
366
+ Args:
367
+ text: The menu's label (e.g. `'File'`).
368
+ key: Optional stable identifier; defaults to `text`.
369
+
370
+ Returns:
371
+ The `MenuGroup` for this menu.
372
+ """
373
+ from bootstack.widgets._impl.composites.dropdownbutton import DropdownButton
374
+ from bootstack.widgets._impl.composites.menu.model import MenuModel
375
+
376
+ if self._menu_model is None:
377
+ self._menu_model = MenuModel()
378
+ group = self._menu_model.add_menu(text, key=key)
379
+
380
+ # A flat menu-bar-style trigger whose popdown is the themed context menu.
381
+ trigger = DropdownButton(
382
+ self._content_frame,
383
+ text=text,
384
+ items=[],
385
+ variant='menubar-item',
386
+ show_dropdown_button=False,
387
+ density=self._density,
388
+ surface=self._surface,
389
+ )
390
+ trigger.pack(side='left')
391
+ self._menu_triggers[group.key] = trigger
392
+
393
+ # Items are appended to the trigger as they are added to the group (the
394
+ # build-up case), so the `with tb.add_menu(...)` block renders live; each
395
+ # change also (coalesced) rebinds the model's keyboard shortcuts.
396
+ group.on_change = lambda g=group, t=trigger: self._on_menu_item_added(g, t)
397
+ return group
398
+
399
+ def _on_menu_item_added(self, group: "MenuGroup", trigger: "DropdownButton") -> None:
400
+ from bootstack.widgets._impl.composites.menu.render_themed import (
401
+ menu_item_to_context_item,
402
+ )
403
+
404
+ item = group.items[-1]
405
+ trigger.add_items([menu_item_to_context_item(item, self._menu_radio_vars)])
406
+ self._schedule_menu_rebind()
407
+ if self._on_menu_change is not None:
408
+ self._on_menu_change()
409
+
410
+ def _schedule_menu_rebind(self) -> None:
411
+ """Coalesce shortcut (re)binding to idle so a built menu binds once."""
412
+ if self._menu_rebind_pending is not None:
413
+ try:
414
+ self.after_cancel(self._menu_rebind_pending)
415
+ except Exception:
416
+ pass
417
+ try:
418
+ self._menu_rebind_pending = self.after_idle(self._rebind_menu_shortcuts)
419
+ except Exception:
420
+ self._rebind_menu_shortcuts()
421
+
422
+ def _rebind_menu_shortcuts(self) -> None:
423
+ self._menu_rebind_pending = None
424
+ if self._menu_model is not None:
425
+ try:
426
+ self._menu_model.bind_shortcuts(self.winfo_toplevel())
427
+ except Exception:
428
+ pass
429
+
430
+ def _on_destroy(self, event: Any) -> None:
431
+ """Cancel a pending menu-shortcut rebind when the toolbar is destroyed.
432
+
433
+ `<Destroy>` propagates up from descendants, so act only for the bar itself.
434
+ """
435
+ if event.widget is not self:
436
+ return
437
+ if self._menu_rebind_pending is not None:
438
+ try:
439
+ self.after_cancel(self._menu_rebind_pending)
440
+ except Exception:
441
+ pass
442
+ self._menu_rebind_pending = None
443
+
444
+ @property
445
+ def menu_model(self) -> "MenuModel | None":
446
+ """This toolbar's menu model (or `None` if no menus were added)."""
447
+ return self._menu_model
448
+
449
+ def add_label(
450
+ self,
451
+ text: str = '',
452
+ icon: str | dict = None,
453
+ font: str = None,
454
+ **kwargs
455
+ ) -> Label:
456
+ """Add a label to the toolbar.
457
+
458
+ Args:
459
+ text: Label text.
460
+ icon: Optional icon name or configuration.
461
+ font: Font specification.
462
+ **kwargs: Additional arguments passed to Label.
463
+
464
+ Returns:
465
+ Label: The created label.
466
+ """
467
+ # Show icon AND text when both are given. The internal Label primitive
468
+ # defaults to icon-only (no compound) — the public Label wrapper's
469
+ # compound logic doesn't run on this path, so set it here.
470
+ has_text = bool(text) or kwargs.get('textsignal') is not None
471
+ if icon is not None and has_text and 'compound' not in kwargs:
472
+ kwargs['compound'] = 'left'
473
+ lbl = Label(
474
+ self._content_frame,
475
+ text=text,
476
+ icon=icon,
477
+ font=font,
478
+ surface=kwargs.pop('surface', self._surface),
479
+ **kwargs
480
+ )
481
+ lbl.pack(side='left', padx=4)
482
+ self._attach_drag(lbl)
483
+ return lbl
484
+
485
+ def _attach_drag(self, widget) -> None:
486
+ """Wire window-drag bindings onto a child widget so the bar can be
487
+ grabbed there (used for a draggable titlebar). No-op when the bar is not
488
+ draggable. Buttons are intentionally excluded — they are click targets."""
489
+ if not self._draggable:
490
+ return
491
+ widget.bind('<Button-1>', self._on_drag_start, add='+')
492
+ widget.bind('<B1-Motion>', self._on_drag_motion, add='+')
493
+ widget.bind('<Double-Button-1>', self._on_drag_double, add='+')
494
+
495
+ def add_separator(self, length: int = 16, **kwargs) -> Separator:
496
+ """Add a vertical separator to the toolbar.
497
+
498
+ Args:
499
+ length: Fixed length in pixels. If None, stretches to fill the
500
+ toolbar height.
501
+ **kwargs: Additional arguments passed to Separator.
502
+
503
+ Returns:
504
+ Separator: The created separator.
505
+ """
506
+ sep = Separator(
507
+ self._content_frame,
508
+ orient='vertical',
509
+ length=length,
510
+ **kwargs
511
+ )
512
+ # Only fill='y' if no fixed length specified
513
+ if length:
514
+ sep.pack(side='left', padx=4)
515
+ else:
516
+ sep.pack(side='left', fill='y', padx=4)
517
+ return sep
518
+
519
+ def add_spacer(self) -> Frame:
520
+ """Add a flexible spacer that pushes subsequent items to the right.
521
+
522
+ Returns:
523
+ Frame: The spacer frame.
524
+ """
525
+ spacer = Frame(self._content_frame)
526
+ spacer.pack(side='left', fill='both', expand=True)
527
+ self._attach_drag(spacer)
528
+ return spacer
529
+
530
+ def add_widget(self, widget: Widget, **pack_kwargs) -> Widget:
531
+ """Add a custom widget to the toolbar.
532
+
533
+ The widget must already be created with the toolbar's content frame
534
+ as its parent. Use `toolbar.content` to get the parent frame.
535
+
536
+ Args:
537
+ widget: The widget to add.
538
+ **pack_kwargs: Arguments passed to pack(). Defaults to side='left'.
539
+
540
+ Returns:
541
+ Widget: The added widget.
542
+ """
543
+ pack_kwargs.setdefault('side', 'left')
544
+ pack_kwargs.setdefault('padx', 2)
545
+ widget.pack(**pack_kwargs)
546
+ return widget
547
+
548
+ # --- Properties ---
549
+
550
+ @property
551
+ def content(self) -> Frame:
552
+ """Get the content frame for adding custom widgets."""
553
+ return self._content_frame
554
+
555
+ @property
556
+ def show_window_controls(self) -> bool:
557
+ """Check if window controls are shown."""
558
+ return self._show_window_controls
559
+
560
+ @property
561
+ def draggable(self) -> bool:
562
+ """Check if toolbar is draggable."""
563
+ return self._draggable
564
+
565
+ @property
566
+ def density(self) -> str:
567
+ """Get the toolbar's button density."""
568
+ return self._density
569
+
570
+ @property
571
+ def surface(self) -> str:
572
+ """The surface token applied to this toolbar."""
573
+ return getattr(self, "_surface", "chrome")
574
+
575
+ # --- Window Control Access ---
576
+
577
+ @property
578
+ def minimize_button(self) -> Button | None:
579
+ """Get the minimize button (if window controls are shown)."""
580
+ return getattr(self, '_minimize_btn', None)
581
+
582
+ @property
583
+ def maximize_button(self) -> Button | None:
584
+ """Get the maximize button (if window controls are shown)."""
585
+ return getattr(self, '_maximize_btn', None)
586
+
587
+ @property
588
+ def close_button(self) -> Button | None:
589
+ """Get the close button (if window controls are shown)."""
590
+ return getattr(self, '_close_btn', None)
591
+
592
+ # --- Configuration Delegates ---
593
+
594
+ @configure_delegate('density')
595
+ def _delegate_density(self, value: str = None):
596
+ """Configure the toolbar's default button density."""
597
+ if value is None:
598
+ return self._density
599
+ self._density = value
600
+ return None
601
+
602
+ @configure_delegate('button_variant')
603
+ def _delegate_button_variant(self, value: str = None):
604
+ """Configure the toolbar's default button variant."""
605
+ if value is None:
606
+ return self._button_variant
607
+ self._button_variant = value
608
+ return None