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,974 @@
1
+ """Tabs widget - a tab bar container for TabItem widgets."""
2
+ from __future__ import annotations
3
+
4
+ __all__ = ['Tabs']
5
+
6
+ import tkinter as tk
7
+ from tkinter import Variable
8
+ from typing_extensions import Unpack
9
+ from typing import Any, Callable, Literal, TYPE_CHECKING
10
+
11
+ from bootstack.widgets._impl.primitives.packframe import PackFrame
12
+ from bootstack.widgets._impl.primitives.frame import Frame, FrameKwargs
13
+ from bootstack.widgets._impl.primitives.separator import Separator
14
+ from bootstack.widgets._impl.primitives.button import Button
15
+ from bootstack.widgets._impl.composites.scrollview import ScrollView
16
+ from bootstack.widgets._impl.composites.tabs.tabitem import TabItem
17
+ from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
18
+ from bootstack.widgets.types import Master
19
+ from bootstack._core import NavigationError
20
+
21
+ if TYPE_CHECKING:
22
+ from bootstack.signals import Signal
23
+
24
+
25
+ class Tabs(Frame):
26
+ """A container widget for grouping TabItem widgets.
27
+
28
+ Tabs provides a tab bar with optional divider. It manages the layout,
29
+ orientation, and styling of child TabItems.
30
+
31
+ Attributes:
32
+ orient: The orientation of the tab bar ('horizontal' or 'vertical').
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ master: Master = None,
38
+ orient: Literal['horizontal', 'vertical'] = 'horizontal',
39
+ variant: Literal['bar'] = 'bar',
40
+ show_divider: bool = None,
41
+ compound: Literal['left', 'right', 'top', 'bottom', 'center', 'none'] = 'left',
42
+ tab_width: None | int | Literal['stretch'] = None,
43
+ tab_min_width: int = 80,
44
+ tab_padding: tuple = (12, 8),
45
+ tab_anchor: str = None,
46
+ enable_closing: bool | Literal['hover'] = False,
47
+ enable_adding: bool = False,
48
+ max_tabs: int | None = None,
49
+ variable: Variable = None,
50
+ signal: 'Signal[Any]' = None,
51
+ accent: str = None,
52
+ **kwargs: Unpack[FrameKwargs]
53
+ ):
54
+ """Create a Tabs widget.
55
+
56
+ Args:
57
+ master: Parent widget. If None, uses the default root window.
58
+ orient: Orientation of the tab bar. 'horizontal' places tabs in a row,
59
+ 'vertical' places tabs in a column. Default is 'horizontal'.
60
+ variant: Visual style variant. Only 'bar' is supported.
61
+ show_divider: Whether to show a divider line. If None (default),
62
+ automatically set to True for 'bar' variant, False for others.
63
+ compound: How to position icon relative to text in tabs.
64
+ Passed to all TabItems. Default is 'left'.
65
+ tab_width: Width of tabs. None for auto-sizing, an integer for
66
+ fixed character width, or 'stretch' to expand tabs to fill
67
+ available space (horizontal only). Default is None.
68
+ tab_min_width: Minimum tab width in pixels. Tabs will not shrink
69
+ below this value regardless of label content. Default is 80.
70
+ tab_padding: Padding for all tabs as (horizontal, vertical).
71
+ Default is (12, 8).
72
+ tab_anchor: Anchor for tab text/icon alignment. If None, defaults
73
+ to 'w' for vertical orientation, 'center' for horizontal.
74
+ enable_closing: Default close button visibility for all tabs.
75
+ True=always visible, False=hidden, 'hover'=visible on hover.
76
+ Can be overridden per-tab via `closable` in add_tab().
77
+ enable_adding: If True, shows an "add" button. In horizontal
78
+ orientation, shows a plus icon on the right. In vertical
79
+ orientation, shows "New Tab" at the bottom. Fires `<<TabAdd>>`
80
+ event when clicked.
81
+ max_tabs: Maximum number of tabs allowed. When the tab count
82
+ reaches this value the add button is disabled (and re-enabled
83
+ when a tab is removed). None (default) means no limit.
84
+ variable: Tk variable for tracking selected tab value.
85
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
86
+ signal: Reactive Signal for tracking selected tab value.
87
+ accent: Accent token for styling tabs.
88
+ **kwargs: Additional arguments passed to Frame.
89
+ """
90
+ super().__init__(master=master, **kwargs)
91
+
92
+ self._orient = orient
93
+ self._variant = variant
94
+ self._compound = compound
95
+ self._tab_width = tab_width
96
+ self._tab_min_width = tab_min_width
97
+ self._tab_padding = tab_padding
98
+ self._enable_closing = enable_closing
99
+ self._enable_adding = enable_adding
100
+ self._max_tabs = max_tabs
101
+ self._accent = accent
102
+
103
+ # Handle variable/signal setup
104
+ if signal is not None:
105
+ # Signal provided - extract its variable
106
+ self._signal = signal
107
+ self._variable = signal.var
108
+ elif variable is not None:
109
+ # Variable provided - wrap in Signal
110
+ from bootstack.signals import Signal
111
+ self._variable = variable
112
+ self._signal = Signal.from_variable(variable)
113
+ else:
114
+ # Neither provided - create internal variable and signal
115
+ from bootstack.signals import Signal
116
+ self._variable = tk.StringVar()
117
+ self._signal = Signal.from_variable(self._variable)
118
+
119
+ # Default anchor based on orientation
120
+ if tab_anchor is None:
121
+ self._tab_anchor = 'w' if orient == 'vertical' else 'center'
122
+ else:
123
+ self._tab_anchor = tab_anchor
124
+
125
+ # Determine show_divider default based on variant
126
+ if show_divider is None:
127
+ self._show_divider = variant == 'bar'
128
+ else:
129
+ self._show_divider = show_divider
130
+
131
+ # Set direction based on orient
132
+ direction = 'horizontal' if orient == 'horizontal' else 'vertical'
133
+
134
+ # Gap between tabs
135
+ gap = 0
136
+
137
+ # Internal divider widget
138
+ self._divider: Separator | None = None
139
+
140
+ # Internal add button
141
+ self._add_button: Button | None = None
142
+
143
+ # Tab tracking by key
144
+ self._tabs: dict[str, TabItem] = {}
145
+ self._tab_order: list[str] = []
146
+ self._tab_values: dict[str, Any] = {}
147
+ self._tab_icons: dict[str, Any] = {}
148
+ self._hidden: set[str] = set()
149
+ self._counter = 0 # For auto-generating keys
150
+
151
+ # Tabs scroll when they exceed the strip. 'stretch' always fits, so it
152
+ # uses the plain non-scrolling strip instead.
153
+ self._scrollable = tab_width != 'stretch'
154
+ self._axis = 'x' if orient == 'horizontal' else 'y'
155
+ self._scroll: ScrollView | None = None
156
+ self._strip_row: Frame | None = None
157
+ self._overflow_button: Button | None = None
158
+ self._overflow_menu = None
159
+ self._overflow_visible = False
160
+ self._overflow_after: str | None = None
161
+ self._canvas_cross = 0
162
+ self._wheel_tag = f'TabStripWheel_{id(self)}'
163
+
164
+ # Build the tab strip (scrollable or static) + divider
165
+ if self._scrollable:
166
+ self._build_scrollable_strip(orient, direction, gap)
167
+ else:
168
+ self._build_static_strip(orient, direction, gap)
169
+
170
+ # Create add button if enabled (must be after the strip is built)
171
+ if enable_adding:
172
+ self._create_add_button()
173
+
174
+ # Wire scroll behavior once everything exists
175
+ if self._scrollable:
176
+ self._setup_wheel_bindings()
177
+ self._signal.subscribe(self._on_selection_scroll)
178
+ self.bind('<Destroy>', self._on_overflow_destroy, add='+')
179
+
180
+ def _build_static_strip(self, orient: str, direction: str, gap: int) -> None:
181
+ """Build the plain non-scrolling strip (used when `tab_width='stretch'`)."""
182
+ self._tab_bar = PackFrame(self, direction=direction, gap=gap)
183
+ if orient == 'horizontal':
184
+ self._tab_bar.pack(side='top', fill='x')
185
+ if self._show_divider:
186
+ self._divider = Separator(self, orient='horizontal')
187
+ self._divider.pack(side='top', fill='x')
188
+ else:
189
+ self._tab_bar.pack(side='left', fill='y')
190
+ if self._show_divider:
191
+ self._divider = Separator(self, orient='vertical')
192
+ self._divider.pack(side='left', fill='y')
193
+
194
+ def _build_scrollable_strip(self, orient: str, direction: str, gap: int) -> None:
195
+ """Build a scrollbar-less scrolling strip with a trailing overflow menu.
196
+
197
+ The tab `PackFrame` lives inside a `ScrollView`; the add and overflow
198
+ buttons are pinned outside the scroll region (right edge for horizontal,
199
+ bottom for vertical) so they stay reachable while the tabs scroll.
200
+ """
201
+ if orient == 'horizontal':
202
+ self._strip_row = Frame(self)
203
+ self._strip_row.pack(side='top', fill='x')
204
+ self._scroll = ScrollView(
205
+ self._strip_row,
206
+ scroll_direction='horizontal',
207
+ scrollbar_visibility='never',
208
+ )
209
+ # Small gap between the scrolling tabs and the pinned add/overflow buttons.
210
+ self._scroll.pack(side='left', fill='x', expand=True, padx=(0, 6))
211
+ else:
212
+ self._strip_row = Frame(self)
213
+ self._strip_row.pack(side='left', fill='y')
214
+ self._scroll = ScrollView(
215
+ self._strip_row,
216
+ scroll_direction='vertical',
217
+ scrollbar_visibility='never',
218
+ )
219
+ # Small gap between the scrolling tabs and the pinned add/overflow buttons.
220
+ self._scroll.pack(side='top', fill='y', expand=True, pady=(0, 6))
221
+
222
+ self._tab_bar = PackFrame(self._scroll.canvas, direction=direction, gap=gap)
223
+ self._scroll.add(self._tab_bar)
224
+
225
+ # Match the canvas cross-size to the strip and re-evaluate overflow on
226
+ # any size change (ScrollView's own <Configure> bindings are kept via +).
227
+ self._tab_bar.bind('<Configure>', self._on_tabbar_configure, add='+')
228
+ self._scroll.canvas.bind('<Configure>', self._on_viewport_configure, add='+')
229
+
230
+ if self._show_divider:
231
+ if orient == 'horizontal':
232
+ self._divider = Separator(self, orient='horizontal')
233
+ self._divider.pack(side='top', fill='x')
234
+ else:
235
+ self._divider = Separator(self, orient='vertical')
236
+ self._divider.pack(side='left', fill='y')
237
+
238
+ def _create_divider(self):
239
+ """Create the divider separator widget."""
240
+ if self._divider is not None:
241
+ return
242
+ if self._orient == 'horizontal':
243
+ self._divider = Separator(self, orient='horizontal')
244
+ self._divider.pack(side='top', fill='x')
245
+ else:
246
+ self._divider = Separator(self, orient='vertical')
247
+ self._divider.pack(side='left', fill='y')
248
+
249
+ def _destroy_divider(self):
250
+ """Remove the divider widget."""
251
+ if self._divider is not None:
252
+ self._divider.pack_forget()
253
+ self._divider.destroy()
254
+ self._divider = None
255
+
256
+ def _create_add_button(self):
257
+ """Create the add button widget."""
258
+ if self._add_button is not None:
259
+ return
260
+
261
+ # When scrolling, the add button is pinned outside the scroll region
262
+ # (right edge for horizontal, bottom for vertical) so it stays put.
263
+ parent = self._strip_row if self._scrollable else self._tab_bar
264
+
265
+ if self._orient == 'horizontal':
266
+ self._add_button = Button(
267
+ parent,
268
+ icon={'name': 'plus-lg', 'size': 16},
269
+ icon_only=True,
270
+ variant='ghost',
271
+ command=self._on_add_click,
272
+ )
273
+ if self._scrollable:
274
+ self._add_button.pack(side='right')
275
+ else:
276
+ self._add_button.pack()
277
+ else:
278
+ # "New Tab" with plus icon
279
+ self._add_button = Button(
280
+ parent,
281
+ text='New',
282
+ icon='plus-lg',
283
+ variant='ghost',
284
+ padding=2,
285
+ command=self._on_add_click,
286
+ anchor='w',
287
+ )
288
+ if self._scrollable:
289
+ self._add_button.pack(side='bottom', fill='x')
290
+ else:
291
+ self._add_button.pack(fill='x')
292
+
293
+ self._update_add_button_state()
294
+
295
+ def _destroy_add_button(self):
296
+ """Remove the add button widget."""
297
+ if self._add_button is not None:
298
+ self._add_button.pack_forget()
299
+ self._add_button.destroy()
300
+ self._add_button = None
301
+
302
+ def _update_add_button_state(self):
303
+ """Disable the add button when the tab count reaches `max_tabs`."""
304
+ if self._add_button is None:
305
+ return
306
+ at_limit = self._max_tabs is not None and len(self._tabs) >= self._max_tabs
307
+ if at_limit:
308
+ self._add_button.state(['disabled'])
309
+ else:
310
+ self._add_button.state(['!disabled'])
311
+
312
+ def _on_add_click(self):
313
+ """Handle add button click."""
314
+ # Guard against a queued click that lands after the limit is reached.
315
+ if self._max_tabs is not None and len(self._tabs) >= self._max_tabs:
316
+ return
317
+ self.event_generate('<<TabAdd>>')
318
+
319
+ def on_tab_added(self, callback: Callable) -> str:
320
+ """Register a callback for `<<TabAdd>>` events (fires when the add button is clicked).
321
+
322
+ Args:
323
+ callback: Called when the add button is clicked. `event.data` is None.
324
+
325
+ Returns:
326
+ Bind ID — pass to `off_tab_added()` to unsubscribe.
327
+ """
328
+ return self.bind('<<TabAdd>>', callback, add='+')
329
+
330
+ def off_tab_added(self, bind_id: str | None = None) -> None:
331
+ """Unsubscribe from `<<TabAdd>>`.
332
+
333
+ Args:
334
+ bind_id: ID returned by `on_tab_added()`. If None, removes all.
335
+ """
336
+ self.unbind('<<TabAdd>>', bind_id)
337
+
338
+ def on_tab_closed(self, callback: Callable) -> str:
339
+ """Register a callback for `<<TabClose>>` events (fires when a tab's close button is clicked).
340
+
341
+ Args:
342
+ callback: Receives `event.data = {'value': Any}` — the closed tab's value.
343
+
344
+ Returns:
345
+ Bind ID — pass to `off_tab_closed()` to unsubscribe.
346
+ """
347
+ return self.bind('<<TabClose>>', callback, add='+')
348
+
349
+ def off_tab_closed(self, bind_id: str | None = None) -> None:
350
+ """Unsubscribe from `<<TabClose>>`.
351
+
352
+ Args:
353
+ bind_id: ID returned by `on_tab_closed()`. If None, removes all.
354
+ """
355
+ self.unbind('<<TabClose>>', bind_id)
356
+
357
+ def add(
358
+ self,
359
+ text: str = "",
360
+ *,
361
+ key: str = None,
362
+ icon: str | dict = None,
363
+ value: Any = None,
364
+ closable: bool | Literal['hover'] | None = None,
365
+ close_command: Callable = None,
366
+ command: Callable = None,
367
+ **kwargs
368
+ ) -> TabItem:
369
+ """Add a new tab to the tab bar.
370
+
371
+ Args:
372
+ text: Text to display on the tab.
373
+ key: Unique identifier for the tab. Auto-generated if not provided.
374
+ icon: Icon to display on the tab.
375
+ value: Value associated with this tab for selection tracking.
376
+ If None, defaults to the key.
377
+ closable: Close button visibility (True, False, or 'hover').
378
+ If None, uses the widget's `enable_closing` setting.
379
+ close_command: Callback when close button is clicked.
380
+ command: Callback when tab is selected.
381
+ **kwargs: Additional arguments passed to TabItem.
382
+
383
+ Returns:
384
+ The created TabItem widget.
385
+
386
+ Raises:
387
+ ValueError: If a tab with the same key already exists.
388
+ """
389
+ # Auto-generate key if not provided
390
+ if key is None:
391
+ key = f"tab_{self._counter}"
392
+ self._counter += 1
393
+
394
+ if key in self._tabs:
395
+ raise NavigationError(f"A tab with the key '{key}' already exists.")
396
+
397
+ # Default value to key if not specified
398
+ if value is None:
399
+ value = key
400
+
401
+ # Use widget-level default if not specified
402
+ if closable is None:
403
+ closable = self._enable_closing
404
+
405
+ # Apply container defaults
406
+ tab_kwargs = {
407
+ 'compound': self._compound,
408
+ 'orient': self._orient,
409
+ 'padding': self._tab_padding,
410
+ 'anchor': self._tab_anchor,
411
+ 'min_width': self._tab_min_width,
412
+ }
413
+
414
+ # Apply tab_width if specified (not 'stretch')
415
+ if self._tab_width is not None and self._tab_width != 'stretch':
416
+ tab_kwargs['width'] = self._tab_width
417
+
418
+ # Apply accent if specified
419
+ if self._accent is not None:
420
+ tab_kwargs['accent'] = self._accent
421
+
422
+ # Pass signal to TabItem for selection sync
423
+ tab_kwargs['signal'] = self._signal
424
+
425
+ # User kwargs override defaults
426
+ tab_kwargs.update(kwargs)
427
+
428
+ tab = TabItem(
429
+ self._tab_bar,
430
+ text=text,
431
+ icon=icon,
432
+ value=value,
433
+ closable=closable,
434
+ close_command=close_command,
435
+ command=command,
436
+ **tab_kwargs
437
+ )
438
+
439
+ # Determine pack options
440
+ pack_opts = {'fill': 'x'}
441
+ if self._tab_width == 'stretch' and self._orient == 'horizontal':
442
+ pack_opts['expand'] = True
443
+
444
+ if self._scrollable:
445
+ # Tabs live inside the scroll region; the add/overflow buttons are
446
+ # pinned outside it, so just append in order.
447
+ tab.pack(**pack_opts)
448
+ self._add_wheel_binding(tab)
449
+ self._schedule_overflow_update()
450
+ elif self._add_button is not None:
451
+ tab.pack(before=self._add_button, **pack_opts)
452
+ else:
453
+ tab.pack(**pack_opts)
454
+
455
+ # Track tab by key
456
+ self._tabs[key] = tab
457
+ self._tab_order.append(key)
458
+ self._tab_values[key] = value
459
+ self._tab_icons[key] = icon
460
+
461
+ # Auto-select first tab
462
+ if len(self._tabs) == 1:
463
+ self._variable.set(value)
464
+
465
+ self._update_add_button_state()
466
+ return tab
467
+
468
+ def remove(self, key: str) -> None:
469
+ """Remove a tab by its key.
470
+
471
+ Args:
472
+ key: The key of the tab to remove.
473
+
474
+ Raises:
475
+ KeyError: If no tab with the given key exists.
476
+ """
477
+ if key not in self._tabs:
478
+ raise NavigationError(f"No tab with key '{key}'")
479
+
480
+ tab = self._tabs.pop(key)
481
+ self._tab_order.remove(key)
482
+ self._tab_values.pop(key, None)
483
+ self._tab_icons.pop(key, None)
484
+ tab.pack_forget()
485
+ tab.destroy()
486
+ self._update_add_button_state()
487
+ self._schedule_overflow_update()
488
+
489
+ def hide(self, key: str) -> None:
490
+ """Hide a tab without removing it from the registry.
491
+
492
+ The tab disappears from the bar but can be restored with `show()`.
493
+
494
+ Args:
495
+ key: Key of the tab to hide.
496
+
497
+ Raises:
498
+ KeyError: If no tab with the given key exists.
499
+ """
500
+ if key not in self._tabs:
501
+ raise NavigationError(f"No tab with key '{key}'")
502
+ if key not in self._hidden:
503
+ self._hidden.add(key)
504
+ self._tabs[key].pack_forget()
505
+ self._schedule_overflow_update()
506
+
507
+ def show(self, key: str) -> None:
508
+ """Restore a previously hidden tab to the bar.
509
+
510
+ Args:
511
+ key: Key of the tab to show.
512
+
513
+ Raises:
514
+ KeyError: If no tab with the given key exists.
515
+ """
516
+ if key not in self._tabs:
517
+ raise NavigationError(f"No tab with key '{key}'")
518
+ if key in self._hidden:
519
+ self._hidden.discard(key)
520
+ tab = self._tabs[key]
521
+ pack_opts: dict = {'fill': 'x'}
522
+ if self._tab_width == 'stretch' and self._orient == 'horizontal':
523
+ pack_opts['expand'] = True
524
+ # Re-insert at the correct position relative to visible tabs
525
+ next_visible = self._next_visible_tab_after(key)
526
+ if next_visible is not None:
527
+ tab.pack(before=self._tabs[next_visible], **pack_opts)
528
+ elif self._add_button is not None and not self._scrollable:
529
+ tab.pack(before=self._add_button, **pack_opts)
530
+ else:
531
+ tab.pack(**pack_opts)
532
+ if self._scrollable:
533
+ self._add_wheel_binding(tab)
534
+ self._schedule_overflow_update()
535
+
536
+ def _next_visible_tab_after(self, key: str) -> str | None:
537
+ """Return the key of the next visible tab after *key* in tab order."""
538
+ found = False
539
+ for k in self._tab_order:
540
+ if found and k not in self._hidden:
541
+ return k
542
+ if k == key:
543
+ found = True
544
+ return None
545
+
546
+ # -------------------------------------------------------------------------
547
+ # Overflow scrolling (scrollbar-less strip + trailing overflow menu)
548
+ # -------------------------------------------------------------------------
549
+
550
+ def _on_tabbar_configure(self, _event=None) -> None:
551
+ """Keep the canvas cross-size matched to the strip; recheck overflow."""
552
+ if not self._scrollable:
553
+ return
554
+ if self._axis == 'x':
555
+ cross = self._tab_bar.winfo_reqheight()
556
+ else:
557
+ cross = self._tab_bar.winfo_reqwidth()
558
+ if cross > 1 and cross != self._canvas_cross:
559
+ self._canvas_cross = cross
560
+ if self._axis == 'x':
561
+ self._scroll.canvas.configure(height=cross)
562
+ else:
563
+ self._scroll.canvas.configure(width=cross)
564
+ self._schedule_overflow_update()
565
+
566
+ def _on_viewport_configure(self, _event=None) -> None:
567
+ """Re-evaluate overflow when the visible viewport is resized."""
568
+ self._schedule_overflow_update()
569
+
570
+ def _content_size(self) -> int:
571
+ """Total length of the tab strip along the scroll axis (pixels)."""
572
+ if self._axis == 'x':
573
+ return self._tab_bar.winfo_reqwidth()
574
+ return self._tab_bar.winfo_reqheight()
575
+
576
+ def _viewport_size(self) -> int:
577
+ """Visible length of the scroll viewport along the scroll axis (pixels)."""
578
+ canvas = self._scroll.canvas
579
+ return canvas.winfo_width() if self._axis == 'x' else canvas.winfo_height()
580
+
581
+ def _content_overflows(self) -> bool:
582
+ """Whether the strip is longer than the viewport (tabs are off-screen)."""
583
+ if not self._scrollable:
584
+ return False
585
+ view = self._viewport_size()
586
+ return view > 1 and self._content_size() > view + 1
587
+
588
+ def _view_first(self) -> float:
589
+ """First-visible fraction (0..1) of the scroll axis."""
590
+ view = self._scroll.xview() if self._axis == 'x' else self._scroll.yview()
591
+ return view[0]
592
+
593
+ def _view_moveto(self, fraction: float) -> None:
594
+ """Scroll so *fraction* (0..1) of the content is at the leading edge."""
595
+ if self._axis == 'x':
596
+ self._scroll.xview_moveto(fraction)
597
+ else:
598
+ self._scroll.yview_moveto(fraction)
599
+
600
+ def _tab_start(self, tab: TabItem) -> int:
601
+ """Leading-edge offset of *tab* within the strip along the scroll axis."""
602
+ return tab.winfo_x() if self._axis == 'x' else tab.winfo_y()
603
+
604
+ def _tab_extent(self, tab: TabItem) -> int:
605
+ """Length of *tab* along the scroll axis (pixels)."""
606
+ return tab.winfo_width() if self._axis == 'x' else tab.winfo_height()
607
+
608
+ def _schedule_overflow_update(self) -> None:
609
+ """Coalesce overflow re-evaluation to the next idle moment."""
610
+ if not self._scrollable or self._overflow_after is not None:
611
+ return
612
+ try:
613
+ self._overflow_after = self.after_idle(self._update_overflow)
614
+ except Exception:
615
+ self._overflow_after = None
616
+
617
+ def _update_overflow(self) -> None:
618
+ """Show or hide the trailing overflow button to match overflow state."""
619
+ self._overflow_after = None
620
+ if not self._scrollable or not self.winfo_exists():
621
+ return
622
+ overflow = self._content_overflows()
623
+ if overflow and not self._overflow_visible:
624
+ self._show_overflow_button()
625
+ elif not overflow and self._overflow_visible:
626
+ self._hide_overflow_button()
627
+
628
+ def _show_overflow_button(self) -> None:
629
+ """Create (once) and pin the trailing overflow button.
630
+
631
+ Horizontal uses a compact icon-only chevron; vertical uses a full-width,
632
+ left-aligned `⌄ More` to match the tab rows and the `+ New` button.
633
+ """
634
+ if self._overflow_button is None:
635
+ if self._axis == 'x':
636
+ self._overflow_button = Button(
637
+ self._strip_row,
638
+ icon={'name': 'chevron-down', 'size': 16},
639
+ icon_only=True,
640
+ variant='ghost',
641
+ command=self._on_overflow_click,
642
+ )
643
+ else:
644
+ self._overflow_button = Button(
645
+ self._strip_row,
646
+ text='More',
647
+ icon='chevron-down',
648
+ variant='ghost',
649
+ anchor='w',
650
+ command=self._on_overflow_click,
651
+ )
652
+ if self._axis == 'x':
653
+ self._overflow_button.pack(side='right')
654
+ else:
655
+ self._overflow_button.pack(side='bottom', fill='x')
656
+ self._overflow_visible = True
657
+
658
+ def _hide_overflow_button(self) -> None:
659
+ """Unpin the trailing overflow button (kept for reuse)."""
660
+ if self._overflow_button is not None:
661
+ self._overflow_button.pack_forget()
662
+ self._overflow_visible = False
663
+
664
+ def _offscreen_tab_keys(self) -> list[str]:
665
+ """Keys of visible tabs that are not fully within the viewport."""
666
+ if not self._scrollable:
667
+ return []
668
+ content = self._content_size()
669
+ view = self._viewport_size()
670
+ lead = self._view_first() * content
671
+ trail = lead + view
672
+ keys: list[str] = []
673
+ for key in self._tab_order:
674
+ if key in self._hidden:
675
+ continue
676
+ tab = self._tabs[key]
677
+ start = self._tab_start(tab)
678
+ end = start + self._tab_extent(tab)
679
+ if start < lead - 1 or end > trail + 1:
680
+ keys.append(key)
681
+ return keys
682
+
683
+ def _scroll_into_view(self, key: str, _retries: int = 3) -> None:
684
+ """Scroll the strip so the tab for *key* is fully visible."""
685
+ if not self._scrollable or key not in self._tabs or key in self._hidden:
686
+ return
687
+ if not self.winfo_exists():
688
+ return
689
+ tab = self._tabs[key]
690
+ # A just-added tab may not be laid out yet (winfo_x/y still 0). Re-defer
691
+ # rather than force a synchronous flush, which would paint a half-settled
692
+ # layout and make the page visibly jump. Only the first tab legitimately
693
+ # sits at offset 0.
694
+ if _retries > 0 and key != self._tab_order[0] and self._tab_start(tab) == 0:
695
+ self.after_idle(lambda: self._scroll_into_view(key, _retries - 1))
696
+ return
697
+ content = self._content_size()
698
+ view = self._viewport_size()
699
+ if view <= 1 or content <= view:
700
+ return
701
+ start = self._tab_start(tab)
702
+ end = start + self._tab_extent(tab)
703
+ lead = self._view_first() * content
704
+ trail = lead + view
705
+ if start < lead:
706
+ new_lead = start
707
+ elif end > trail:
708
+ new_lead = end - view
709
+ else:
710
+ return
711
+ new_lead = max(0, min(new_lead, content - view))
712
+ self._view_moveto(new_lead / content)
713
+
714
+ def _on_selection_scroll(self, value: Any) -> None:
715
+ """Auto-scroll the newly selected tab into view (signal subscriber)."""
716
+ if not self._scrollable:
717
+ return
718
+ key = self._key_for_value(value)
719
+ if key is not None:
720
+ self.after_idle(lambda: self._scroll_into_view(key))
721
+
722
+ def _key_for_value(self, value: Any) -> str | None:
723
+ """Reverse-map a selection value back to a tab key."""
724
+ for key, val in self._tab_values.items():
725
+ if val == value:
726
+ return key
727
+ return None
728
+
729
+ def _ensure_overflow_menu(self):
730
+ """Create (once) the dropdown used by the overflow button."""
731
+ if self._overflow_menu is None:
732
+ from bootstack.widgets._impl.composites.contextmenu import ContextMenu
733
+ if self._axis == 'x':
734
+ anchor, attach = 'ne', 'se'
735
+ else:
736
+ anchor, attach = 'sw', 'nw'
737
+ self._overflow_menu = ContextMenu(
738
+ self._overflow_button,
739
+ target=self._overflow_button,
740
+ trigger='manual',
741
+ anchor=anchor,
742
+ attach=attach,
743
+ offset=(0, 2),
744
+ )
745
+ return self._overflow_menu
746
+
747
+ def _on_overflow_click(self) -> None:
748
+ """Populate and show the overflow dropdown with the off-screen tabs."""
749
+ keys = self._offscreen_tab_keys()
750
+ menu = self._ensure_overflow_menu()
751
+ menu.items([]) # clear previous contents
752
+ for key in keys:
753
+ tab = self._tabs[key]
754
+ try:
755
+ label = tab.cget('text') or key
756
+ except Exception:
757
+ label = key
758
+ icon = self._tab_icons.get(key)
759
+ kw = {'text': label, 'command': lambda k=key: self._on_overflow_select(k)}
760
+ if icon:
761
+ kw['icon'] = icon
762
+ menu.add_command(**kw)
763
+ menu.show()
764
+
765
+ def _on_overflow_select(self, key: str) -> None:
766
+ """Select a tab chosen from the overflow menu and reveal it."""
767
+ if key not in self._tab_values:
768
+ return
769
+ self._variable.set(self._tab_values[key])
770
+ # Selection scroll also fires via the signal, but request it explicitly
771
+ # too in case the value did not actually change.
772
+ self.after_idle(lambda: self._scroll_into_view(key))
773
+
774
+ def _setup_wheel_bindings(self) -> None:
775
+ """Bind wheel scrolling along the strip axis via a private bindtag."""
776
+ winsys = self.tk.call('tk', 'windowingsystem')
777
+ if winsys == 'x11':
778
+ self.bind_class(self._wheel_tag, '<Button-4>', self._on_strip_wheel, add='+')
779
+ self.bind_class(self._wheel_tag, '<Button-5>', self._on_strip_wheel, add='+')
780
+ else:
781
+ self.bind_class(self._wheel_tag, '<MouseWheel>', self._on_strip_wheel, add='+')
782
+ self._add_wheel_binding(self._tab_bar)
783
+
784
+ def _add_wheel_binding(self, widget) -> None:
785
+ """Add the wheel bindtag to *widget* and its descendants (idempotent)."""
786
+ try:
787
+ tags = list(widget.bindtags())
788
+ if self._wheel_tag not in tags:
789
+ tags.insert(1 if len(tags) > 1 else len(tags), self._wheel_tag)
790
+ widget.bindtags(tuple(tags))
791
+ except Exception:
792
+ pass
793
+ try:
794
+ children = widget.winfo_children()
795
+ except Exception:
796
+ return
797
+ for child in children:
798
+ self._add_wheel_binding(child)
799
+
800
+ def _on_strip_wheel(self, event) -> str | None:
801
+ """Scroll the strip along its axis in response to the mouse wheel."""
802
+ if not self._scrollable or not self._content_overflows():
803
+ return None
804
+ winsys = self.tk.call('tk', 'windowingsystem')
805
+ if winsys == 'win32':
806
+ notches = -event.delta / 120
807
+ elif winsys == 'aqua':
808
+ notches = -event.delta
809
+ elif getattr(event, 'num', None) == 4:
810
+ notches = -1
811
+ elif getattr(event, 'num', None) == 5:
812
+ notches = 1
813
+ else:
814
+ notches = 0
815
+ if not notches:
816
+ return 'break'
817
+ content = self._content_size()
818
+ view = self._viewport_size()
819
+ if content <= view:
820
+ return 'break'
821
+ step = 60 * notches
822
+ lead = self._view_first() * content
823
+ lead = max(0, min(lead + step, content - view))
824
+ self._view_moveto(lead / content)
825
+ return 'break'
826
+
827
+ def _on_overflow_destroy(self, event) -> None:
828
+ """Release overflow resources when the tab strip is destroyed."""
829
+ if event.widget is not self:
830
+ return
831
+ if self._overflow_after is not None:
832
+ try:
833
+ self.after_cancel(self._overflow_after)
834
+ except Exception:
835
+ pass
836
+ self._overflow_after = None
837
+ if self._overflow_menu is not None:
838
+ try:
839
+ self._overflow_menu.destroy()
840
+ except Exception:
841
+ pass
842
+ self._overflow_menu = None
843
+
844
+ def item(self, key: str) -> TabItem:
845
+ """Get a tab by its key.
846
+
847
+ Args:
848
+ key: The key of the tab to retrieve.
849
+
850
+ Returns:
851
+ The TabItem instance.
852
+
853
+ Raises:
854
+ KeyError: If no tab with the given key exists.
855
+ """
856
+ if key not in self._tabs:
857
+ raise NavigationError(f"No tab with key '{key}'")
858
+ return self._tabs[key]
859
+
860
+ def items(self) -> tuple[TabItem, ...]:
861
+ """Get all tab widgets in order.
862
+
863
+ Returns:
864
+ A tuple of all TabItem instances in the order they were added.
865
+ """
866
+ return tuple(self._tabs[key] for key in self._tab_order)
867
+
868
+ def keys(self) -> tuple[str, ...]:
869
+ """Get all tab keys in order.
870
+
871
+ Returns:
872
+ A tuple of all tab keys in the order they were added.
873
+ """
874
+ return tuple(self._tab_order)
875
+
876
+ def configure_item(self, key: str, option: str = None, **kwargs: Any):
877
+ """Configure a specific tab by its key.
878
+
879
+ Args:
880
+ key: The key of the tab to configure.
881
+ option: If provided, return the value of this option.
882
+ **kwargs: Configuration options to apply to the tab.
883
+
884
+ Returns:
885
+ If option is provided, returns the value of that option.
886
+ """
887
+ tab = self.item(key)
888
+ if option is not None:
889
+ return tab.cget(option)
890
+ tab.configure(**kwargs)
891
+
892
+ @configure_delegate('orient')
893
+ def _delegate_orient(self, value=None):
894
+ """Get orientation (read-only after creation)."""
895
+ if value is None:
896
+ return self._orient
897
+ raise ValueError("orient cannot be changed after creation")
898
+
899
+ @configure_delegate('variant')
900
+ def _delegate_variant(self, value=None):
901
+ """Get variant (read-only after creation)."""
902
+ if value is None:
903
+ return self._variant
904
+ raise ValueError("variant cannot be changed after creation")
905
+
906
+ @property
907
+ def variable(self) -> tk.Variable:
908
+ """Get the underlying tk.Variable for tab selection.
909
+
910
+ See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
911
+ """
912
+ return self._variable
913
+
914
+ @property
915
+ def signal(self) -> 'Signal[Any]':
916
+ """Get the Signal for tab selection."""
917
+ return self._signal
918
+
919
+ @configure_delegate('show_divider')
920
+ def _delegate_show_divider(self, value=None):
921
+ """Get or set whether the divider is shown."""
922
+ if value is None:
923
+ return self._show_divider
924
+ if value != self._show_divider:
925
+ self._show_divider = value
926
+ if value:
927
+ self._create_divider()
928
+ else:
929
+ self._destroy_divider()
930
+
931
+ def get(self) -> str:
932
+ """Return the currently selected tab value."""
933
+ return self._variable.get()
934
+
935
+ def set(self, value: str) -> None:
936
+ """Set the selected tab value."""
937
+ self._variable.set(value)
938
+
939
+ @property
940
+ def value(self) -> str:
941
+ """Get or set the selected tab value."""
942
+ return self.get()
943
+
944
+ @value.setter
945
+ def value(self, value: str) -> None:
946
+ self.set(value)
947
+
948
+ @configure_delegate('value')
949
+ def _delegate_value(self, value=None):
950
+ """Get or set the value via configure."""
951
+ if value is None:
952
+ return self.get()
953
+ self.set(value)
954
+
955
+ def on_tab_changed(self, callback: Callable) -> Any:
956
+ """Register a callback for tab selection changes.
957
+
958
+ Args:
959
+ callback: Called with the new selected tab value (str) when the
960
+ selection changes.
961
+
962
+ Returns:
963
+ Subscription ID — pass to `off_tab_changed()` to unsubscribe.
964
+ """
965
+ return self._signal.subscribe(callback)
966
+
967
+ def off_tab_changed(self, bind_id: Any) -> None:
968
+ """Cancel a subscription created by `on_tab_changed()`.
969
+
970
+ Args:
971
+ bind_id: The handle returned by `on_tab_changed()`.
972
+ """
973
+ if bind_id is not None:
974
+ bind_id.cancel()