bootstack 0.1.0a1__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 (419) hide show
  1. bootstack/__init__.py +249 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/api/__init__.py +127 -0
  4. bootstack/api/app.py +30 -0
  5. bootstack/api/constants.py +3 -0
  6. bootstack/api/data.py +23 -0
  7. bootstack/api/dialogs.py +44 -0
  8. bootstack/api/i18n.py +17 -0
  9. bootstack/api/localization.py +16 -0
  10. bootstack/api/menu.py +7 -0
  11. bootstack/api/style.py +23 -0
  12. bootstack/api/utils.py +24 -0
  13. bootstack/api/widgets.py +137 -0
  14. bootstack/assets/__init__.py +24 -0
  15. bootstack/assets/bootstack-transparent.png +0 -0
  16. bootstack/assets/bootstack.ico +0 -0
  17. bootstack/assets/bootstack.png +0 -0
  18. bootstack/assets/elements/__init__.py +0 -0
  19. bootstack/assets/elements/badge-pill.png +0 -0
  20. bootstack/assets/elements/badge-square.png +0 -0
  21. bootstack/assets/elements/border.png +0 -0
  22. bootstack/assets/elements/button-compact.png +0 -0
  23. bootstack/assets/elements/button-default.png +0 -0
  24. bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
  25. bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
  26. bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
  27. bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
  28. bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
  29. bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
  30. bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
  31. bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
  32. bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
  33. bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
  34. bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
  35. bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
  36. bootstack/assets/elements/checkbox-checked.png +0 -0
  37. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  38. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  39. bootstack/assets/elements/field.png +0 -0
  40. bootstack/assets/elements/input-after-compact.png +0 -0
  41. bootstack/assets/elements/input-after-default.png +0 -0
  42. bootstack/assets/elements/input-before-compact.png +0 -0
  43. bootstack/assets/elements/input-before-default.png +0 -0
  44. bootstack/assets/elements/input-compact.png +0 -0
  45. bootstack/assets/elements/input-default.png +0 -0
  46. bootstack/assets/elements/list-item-separated.png +0 -0
  47. bootstack/assets/elements/list-item.png +0 -0
  48. bootstack/assets/elements/manifest.toml +480 -0
  49. bootstack/assets/elements/menu-item.png +0 -0
  50. bootstack/assets/elements/nav-button-compact.png +0 -0
  51. bootstack/assets/elements/nav-button-default.png +0 -0
  52. bootstack/assets/elements/nav-icon-button-compact.png +0 -0
  53. bootstack/assets/elements/nav-icon-button-default.png +0 -0
  54. bootstack/assets/elements/notebook-client-border.png +0 -0
  55. bootstack/assets/elements/notebook-tab-active.png +0 -0
  56. bootstack/assets/elements/notebook-tab-bar.png +0 -0
  57. bootstack/assets/elements/notebook-tab-normal.png +0 -0
  58. bootstack/assets/elements/notebook-tab-pill.png +0 -0
  59. bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
  60. bootstack/assets/elements/progress-bar-solid.png +0 -0
  61. bootstack/assets/elements/progress-bar-thin.png +0 -0
  62. bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
  63. bootstack/assets/elements/radio-selected.png +0 -0
  64. bootstack/assets/elements/radio-unselected.png +0 -0
  65. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  66. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  67. bootstack/assets/elements/slider-handle-focus.png +0 -0
  68. bootstack/assets/elements/slider-handle.png +0 -0
  69. bootstack/assets/elements/slider-track-horizontal.png +0 -0
  70. bootstack/assets/elements/slider-track-vertical.png +0 -0
  71. bootstack/assets/elements/switch-off.png +0 -0
  72. bootstack/assets/elements/switch-on.png +0 -0
  73. bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
  74. bootstack/assets/elements/tabs-bar-vertical.png +0 -0
  75. bootstack/assets/elements/tabs-pill.png +0 -0
  76. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  77. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
  78. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
  79. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  80. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
  81. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  82. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
  83. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  84. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
  85. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  86. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
  87. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  88. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
  89. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  90. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
  91. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  92. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
  93. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  94. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
  95. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  96. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
  97. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  98. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
  99. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  100. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
  101. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  102. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
  103. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  104. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
  105. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  106. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
  107. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  108. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
  109. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  110. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
  111. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  112. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
  113. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  114. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
  115. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  116. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
  117. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  118. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
  119. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  120. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
  121. bootstack/assets/themes/__init__.py +0 -0
  122. bootstack/assets/themes/amber-dark.json +32 -0
  123. bootstack/assets/themes/amber-light.json +32 -0
  124. bootstack/assets/themes/aurora-dark.json +32 -0
  125. bootstack/assets/themes/aurora-light.json +32 -0
  126. bootstack/assets/themes/bootstrap-dark.json +32 -0
  127. bootstack/assets/themes/bootstrap-light.json +32 -0
  128. bootstack/assets/themes/classic-dark.json +32 -0
  129. bootstack/assets/themes/classic-light.json +32 -0
  130. bootstack/assets/themes/docs-dark.json +32 -0
  131. bootstack/assets/themes/docs-light.json +32 -0
  132. bootstack/assets/themes/forest-dark.json +32 -0
  133. bootstack/assets/themes/forest-light.json +32 -0
  134. bootstack/assets/themes/ocean-dark.json +32 -0
  135. bootstack/assets/themes/ocean-light.json +32 -0
  136. bootstack/assets/themes/rose-dark.json +32 -0
  137. bootstack/assets/themes/rose-light.json +32 -0
  138. bootstack/assets/widgets/__init__.py +0 -0
  139. bootstack/assets/widgets/badge-default.png +0 -0
  140. bootstack/assets/widgets/badge-pill.png +0 -0
  141. bootstack/assets/widgets/border.png +0 -0
  142. bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
  143. bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
  144. bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
  145. bootstack/assets/widgets/button-group-vertical-after.png +0 -0
  146. bootstack/assets/widgets/button-group-vertical-before.png +0 -0
  147. bootstack/assets/widgets/button-group-vertical-center.png +0 -0
  148. bootstack/assets/widgets/button.png +0 -0
  149. bootstack/assets/widgets/checkbox-checked.png +0 -0
  150. bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
  151. bootstack/assets/widgets/checkbox-unchecked.png +0 -0
  152. bootstack/assets/widgets/field.png +0 -0
  153. bootstack/assets/widgets/icon-button.png +0 -0
  154. bootstack/assets/widgets/input-inner.png +0 -0
  155. bootstack/assets/widgets/input-prefix.png +0 -0
  156. bootstack/assets/widgets/input-suffix.png +0 -0
  157. bootstack/assets/widgets/input.png +0 -0
  158. bootstack/assets/widgets/list-item-focus.png +0 -0
  159. bootstack/assets/widgets/list-item-separated.png +0 -0
  160. bootstack/assets/widgets/menu-item-separated.png +0 -0
  161. bootstack/assets/widgets/notebook-client-border.png +0 -0
  162. bootstack/assets/widgets/notebook-pill-active.png +0 -0
  163. bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
  164. bootstack/assets/widgets/notebook-tab-active.png +0 -0
  165. bootstack/assets/widgets/notebook-tab-border.png +0 -0
  166. bootstack/assets/widgets/notebook-tab-normal.png +0 -0
  167. bootstack/assets/widgets/notebook-underline.png +0 -0
  168. bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
  169. bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
  170. bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
  171. bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
  172. bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
  173. bootstack/assets/widgets/progress-trough-vertical.png +0 -0
  174. bootstack/assets/widgets/radio-selected.png +0 -0
  175. bootstack/assets/widgets/radio-unselected.png +0 -0
  176. bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
  177. bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
  178. bootstack/assets/widgets/separator-horizontal.png +0 -0
  179. bootstack/assets/widgets/separator-vertical.png +0 -0
  180. bootstack/assets/widgets/slider-handle-focus.png +0 -0
  181. bootstack/assets/widgets/slider-handle.png +0 -0
  182. bootstack/assets/widgets/slider-track-horizontal.png +0 -0
  183. bootstack/assets/widgets/slider-track-vertical.png +0 -0
  184. bootstack/assets/widgets/switch-off.png +0 -0
  185. bootstack/assets/widgets/switch-on.png +0 -0
  186. bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
  187. bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
  188. bootstack/assets/widgets/tabs-pill.png +0 -0
  189. bootstack/cli/__init__.py +124 -0
  190. bootstack/cli/__main__.py +6 -0
  191. bootstack/cli/add.py +439 -0
  192. bootstack/cli/build.py +115 -0
  193. bootstack/cli/config.py +287 -0
  194. bootstack/cli/demo.py +1267 -0
  195. bootstack/cli/doctor.py +195 -0
  196. bootstack/cli/list_cmd.py +71 -0
  197. bootstack/cli/promote.py +120 -0
  198. bootstack/cli/pyinstaller.py +246 -0
  199. bootstack/cli/run.py +99 -0
  200. bootstack/cli/start.py +105 -0
  201. bootstack/cli/templates/__init__.py +861 -0
  202. bootstack/constants.py +325 -0
  203. bootstack/core/__init__.py +34 -0
  204. bootstack/core/capabilities/__init__.py +45 -0
  205. bootstack/core/capabilities/after.py +103 -0
  206. bootstack/core/capabilities/bind.py +154 -0
  207. bootstack/core/capabilities/bindtags.py +112 -0
  208. bootstack/core/capabilities/busy.py +61 -0
  209. bootstack/core/capabilities/clipboard.py +88 -0
  210. bootstack/core/capabilities/focus.py +118 -0
  211. bootstack/core/capabilities/grab.py +65 -0
  212. bootstack/core/capabilities/grid.py +188 -0
  213. bootstack/core/capabilities/localization.py +231 -0
  214. bootstack/core/capabilities/pack.py +119 -0
  215. bootstack/core/capabilities/place.py +92 -0
  216. bootstack/core/capabilities/selection.py +136 -0
  217. bootstack/core/capabilities/signals.py +242 -0
  218. bootstack/core/capabilities/winfo.py +315 -0
  219. bootstack/core/colorutils.py +234 -0
  220. bootstack/core/exceptions.py +95 -0
  221. bootstack/core/images.py +283 -0
  222. bootstack/core/localization/README.md +90 -0
  223. bootstack/core/localization/__init__.py +13 -0
  224. bootstack/core/localization/intl_format.py +580 -0
  225. bootstack/core/localization/msgcat.py +425 -0
  226. bootstack/core/localization/specs.py +143 -0
  227. bootstack/core/mixins/__init__.py +1 -0
  228. bootstack/core/mixins/ttk_state.py +35 -0
  229. bootstack/core/mixins/widget.py +132 -0
  230. bootstack/core/publisher.py +147 -0
  231. bootstack/core/signals/README.md +112 -0
  232. bootstack/core/signals/__init__.py +8 -0
  233. bootstack/core/signals/integration.py +100 -0
  234. bootstack/core/signals/signal.py +317 -0
  235. bootstack/core/signals/types.py +4 -0
  236. bootstack/core/validation/__init__.py +5 -0
  237. bootstack/core/validation/types.py +13 -0
  238. bootstack/core/validation/validation_result.py +17 -0
  239. bootstack/core/validation/validation_rules.py +112 -0
  240. bootstack/core/variables.py +62 -0
  241. bootstack/datasource/README.md +607 -0
  242. bootstack/datasource/__init__.py +51 -0
  243. bootstack/datasource/base.py +474 -0
  244. bootstack/datasource/file_source.py +541 -0
  245. bootstack/datasource/memory_source.py +482 -0
  246. bootstack/datasource/sqlite_source.py +453 -0
  247. bootstack/datasource/types.py +259 -0
  248. bootstack/dialogs/__init__.py +56 -0
  249. bootstack/dialogs/colorchooser.py +674 -0
  250. bootstack/dialogs/colordropper.py +257 -0
  251. bootstack/dialogs/datedialog.py +404 -0
  252. bootstack/dialogs/dialog.py +514 -0
  253. bootstack/dialogs/filterdialog.py +358 -0
  254. bootstack/dialogs/fontdialog.py +339 -0
  255. bootstack/dialogs/formdialog.py +541 -0
  256. bootstack/dialogs/message.py +489 -0
  257. bootstack/dialogs/query.py +561 -0
  258. bootstack/py.typed +1 -0
  259. bootstack/runtime/__init__.py +3 -0
  260. bootstack/runtime/app.py +879 -0
  261. bootstack/runtime/base_window.py +786 -0
  262. bootstack/runtime/events.py +399 -0
  263. bootstack/runtime/menu.py +510 -0
  264. bootstack/runtime/shortcuts.py +423 -0
  265. bootstack/runtime/tk_patch.py +31 -0
  266. bootstack/runtime/toplevel.py +131 -0
  267. bootstack/runtime/utility.py +371 -0
  268. bootstack/runtime/visual_focus.py +228 -0
  269. bootstack/runtime/window_utilities.py +1043 -0
  270. bootstack/style/__init__.py +5498 -0
  271. bootstack/style/bootstyle.py +507 -0
  272. bootstack/style/bootstyle_builder_base.py +752 -0
  273. bootstack/style/bootstyle_builder_mixed.py +93 -0
  274. bootstack/style/bootstyle_builder_tk.py +109 -0
  275. bootstack/style/bootstyle_builder_ttk.py +354 -0
  276. bootstack/style/builders/__init__.py +51 -0
  277. bootstack/style/builders/badge.py +44 -0
  278. bootstack/style/builders/button.py +453 -0
  279. bootstack/style/builders/buttongroup.py +344 -0
  280. bootstack/style/builders/calendar.py +271 -0
  281. bootstack/style/builders/checkbutton.py +95 -0
  282. bootstack/style/builders/combobox.py +112 -0
  283. bootstack/style/builders/contextmenu.py +268 -0
  284. bootstack/style/builders/entry.py +83 -0
  285. bootstack/style/builders/expander.py +171 -0
  286. bootstack/style/builders/field.py +312 -0
  287. bootstack/style/builders/frame.py +27 -0
  288. bootstack/style/builders/label.py +28 -0
  289. bootstack/style/builders/labelframe.py +41 -0
  290. bootstack/style/builders/listview.py +267 -0
  291. bootstack/style/builders/menubar.py +74 -0
  292. bootstack/style/builders/menubutton.py +408 -0
  293. bootstack/style/builders/notebook.py +316 -0
  294. bootstack/style/builders/panedwindow.py +25 -0
  295. bootstack/style/builders/progressbar.py +71 -0
  296. bootstack/style/builders/radiobutton.py +68 -0
  297. bootstack/style/builders/scale.py +66 -0
  298. bootstack/style/builders/scrollbar.py +360 -0
  299. bootstack/style/builders/separator.py +45 -0
  300. bootstack/style/builders/sidenav.py +313 -0
  301. bootstack/style/builders/sizegrip.py +15 -0
  302. bootstack/style/builders/spinbox.py +119 -0
  303. bootstack/style/builders/switch.py +67 -0
  304. bootstack/style/builders/tabitem.py +205 -0
  305. bootstack/style/builders/toolbutton.py +260 -0
  306. bootstack/style/builders/tooltip.py +26 -0
  307. bootstack/style/builders/treeview.py +269 -0
  308. bootstack/style/builders/utils.py +404 -0
  309. bootstack/style/builders_tk/__init__.py +16 -0
  310. bootstack/style/builders_tk/defaults.py +229 -0
  311. bootstack/style/element.py +173 -0
  312. bootstack/style/style.py +499 -0
  313. bootstack/style/theme_provider.py +449 -0
  314. bootstack/style/tk_patch.py +5 -0
  315. bootstack/style/token_maps.py +42 -0
  316. bootstack/style/types.py +32 -0
  317. bootstack/style/typography.py +527 -0
  318. bootstack/style/utility.py +696 -0
  319. bootstack/themes/__init__.py +12 -0
  320. bootstack/themes/standard.py +415 -0
  321. bootstack/themes/user.py +45 -0
  322. bootstack/widgets/__init__.py +53 -0
  323. bootstack/widgets/composites/__init__.py +38 -0
  324. bootstack/widgets/composites/accordion.py +385 -0
  325. bootstack/widgets/composites/appshell.py +445 -0
  326. bootstack/widgets/composites/buttongroup.py +391 -0
  327. bootstack/widgets/composites/calendar.py +914 -0
  328. bootstack/widgets/composites/compositeframe.py +282 -0
  329. bootstack/widgets/composites/contextmenu.py +1754 -0
  330. bootstack/widgets/composites/dateentry.py +261 -0
  331. bootstack/widgets/composites/dropdownbutton.py +190 -0
  332. bootstack/widgets/composites/expander.py +508 -0
  333. bootstack/widgets/composites/field.py +448 -0
  334. bootstack/widgets/composites/floodgauge.py +434 -0
  335. bootstack/widgets/composites/form.py +983 -0
  336. bootstack/widgets/composites/labeledscale.py +209 -0
  337. bootstack/widgets/composites/list/__init__.py +15 -0
  338. bootstack/widgets/composites/list/listitem.py +733 -0
  339. bootstack/widgets/composites/list/listview.py +1507 -0
  340. bootstack/widgets/composites/menubar.py +303 -0
  341. bootstack/widgets/composites/meter.py +882 -0
  342. bootstack/widgets/composites/numericentry.py +183 -0
  343. bootstack/widgets/composites/pagestack.py +330 -0
  344. bootstack/widgets/composites/passwordentry.py +149 -0
  345. bootstack/widgets/composites/pathentry.py +223 -0
  346. bootstack/widgets/composites/radiogroup.py +466 -0
  347. bootstack/widgets/composites/scrolledtext.py +388 -0
  348. bootstack/widgets/composites/scrolledtext.pyi +186 -0
  349. bootstack/widgets/composites/scrollview.py +675 -0
  350. bootstack/widgets/composites/selectbox.py +544 -0
  351. bootstack/widgets/composites/sidenav/__init__.py +24 -0
  352. bootstack/widgets/composites/sidenav/group.py +485 -0
  353. bootstack/widgets/composites/sidenav/header.py +83 -0
  354. bootstack/widgets/composites/sidenav/item.py +413 -0
  355. bootstack/widgets/composites/sidenav/separator.py +51 -0
  356. bootstack/widgets/composites/sidenav/view.py +919 -0
  357. bootstack/widgets/composites/spinnerentry.py +232 -0
  358. bootstack/widgets/composites/tableview/__init__.py +5 -0
  359. bootstack/widgets/composites/tableview/tableview.py +2254 -0
  360. bootstack/widgets/composites/tableview/types.py +169 -0
  361. bootstack/widgets/composites/tabs/__init__.py +6 -0
  362. bootstack/widgets/composites/tabs/tabitem.py +372 -0
  363. bootstack/widgets/composites/tabs/tabs.py +478 -0
  364. bootstack/widgets/composites/tabs/tabview.py +352 -0
  365. bootstack/widgets/composites/textentry.py +90 -0
  366. bootstack/widgets/composites/timeentry.py +189 -0
  367. bootstack/widgets/composites/toast.py +364 -0
  368. bootstack/widgets/composites/togglegroup.py +382 -0
  369. bootstack/widgets/composites/toolbar.py +393 -0
  370. bootstack/widgets/composites/tooltip.py +404 -0
  371. bootstack/widgets/internal/__init__.py +0 -0
  372. bootstack/widgets/internal/wrapper_base.py +304 -0
  373. bootstack/widgets/mixins/__init__.py +25 -0
  374. bootstack/widgets/mixins/configure_mixin.py +186 -0
  375. bootstack/widgets/mixins/entry_mixin.py +70 -0
  376. bootstack/widgets/mixins/font_mixin.py +346 -0
  377. bootstack/widgets/mixins/icon_mixin.py +38 -0
  378. bootstack/widgets/mixins/localization_mixin.py +255 -0
  379. bootstack/widgets/mixins/signal_mixin.py +272 -0
  380. bootstack/widgets/mixins/validation_mixin.py +204 -0
  381. bootstack/widgets/parts/__init__.py +11 -0
  382. bootstack/widgets/parts/numberentry_part.py +345 -0
  383. bootstack/widgets/parts/spinnerentry_part.py +394 -0
  384. bootstack/widgets/parts/textentry_part.py +344 -0
  385. bootstack/widgets/primitives/__init__.py +55 -0
  386. bootstack/widgets/primitives/badge.py +44 -0
  387. bootstack/widgets/primitives/button.py +89 -0
  388. bootstack/widgets/primitives/card.py +66 -0
  389. bootstack/widgets/primitives/checkbutton.py +124 -0
  390. bootstack/widgets/primitives/checktoggle.py +53 -0
  391. bootstack/widgets/primitives/combobox.py +165 -0
  392. bootstack/widgets/primitives/entry.py +98 -0
  393. bootstack/widgets/primitives/frame.py +206 -0
  394. bootstack/widgets/primitives/gridframe.py +479 -0
  395. bootstack/widgets/primitives/label.py +95 -0
  396. bootstack/widgets/primitives/labelframe.py +63 -0
  397. bootstack/widgets/primitives/menubutton.py +118 -0
  398. bootstack/widgets/primitives/notebook.py +551 -0
  399. bootstack/widgets/primitives/optionmenu.py +248 -0
  400. bootstack/widgets/primitives/packframe.py +228 -0
  401. bootstack/widgets/primitives/panedwindow.py +58 -0
  402. bootstack/widgets/primitives/progressbar.py +95 -0
  403. bootstack/widgets/primitives/radiobutton.py +115 -0
  404. bootstack/widgets/primitives/radiotoggle.py +50 -0
  405. bootstack/widgets/primitives/scale.py +85 -0
  406. bootstack/widgets/primitives/scrollbar.py +56 -0
  407. bootstack/widgets/primitives/separator.py +56 -0
  408. bootstack/widgets/primitives/sizegrip.py +47 -0
  409. bootstack/widgets/primitives/spinbox.py +91 -0
  410. bootstack/widgets/primitives/switch.py +41 -0
  411. bootstack/widgets/primitives/treeview.py +77 -0
  412. bootstack/widgets/types.py +20 -0
  413. bootstack-0.1.0a1.dist-info/METADATA +196 -0
  414. bootstack-0.1.0a1.dist-info/RECORD +419 -0
  415. bootstack-0.1.0a1.dist-info/WHEEL +5 -0
  416. bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
  417. bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
  418. bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
  419. bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,223 @@
1
+ """Path entry field widget with file/directory chooser dialog.
2
+
3
+ Provides an entry field with a button that opens a file or directory chooser
4
+ dialog for selecting paths.
5
+ """
6
+
7
+ from tkinter import filedialog
8
+ from typing import Any, Literal
9
+
10
+ from typing_extensions import Unpack
11
+
12
+ from bootstack.widgets.primitives.button import Button
13
+ from bootstack.widgets.composites.field import Field, FieldOptions
14
+ from bootstack.widgets.mixins import configure_delegate
15
+ from bootstack.widgets.types import Master
16
+
17
+ FileDialogType = Literal[
18
+ 'openfilename', 'openfile', 'directory', 'openfilenames', 'openfiles',
19
+ 'saveasfile', 'saveasfilename'
20
+ ]
21
+ """Type alias for file dialog types.
22
+
23
+ Available dialog types:
24
+ - 'openfilename': Select a single existing file (returns filename)
25
+ - 'openfile': Select a single existing file (returns file object)
26
+ - 'directory': Select a directory
27
+ - 'openfilenames': Select multiple existing files (returns filenames)
28
+ - 'openfiles': Select multiple existing files (returns file objects)
29
+ - 'saveasfile': Select location to save a file (returns file object)
30
+ - 'saveasfilename': Select location to save a file (returns filename)
31
+ """
32
+
33
+
34
+ class PathEntry(Field):
35
+ """A path entry field widget with file/directory chooser dialog button.
36
+
37
+ PathEntry extends the Field widget to provide a specialized input for file
38
+ and directory paths. It includes a button that opens a native file/directory
39
+ chooser dialog, and displays the selected path(s) in the entry field. The
40
+ widget supports various dialog types including single file selection, multiple
41
+ file selection, directory selection, and save file dialogs.
42
+
43
+ !!! note "Events"
44
+
45
+ `<<Change>>`: Fired when a path is selected from the dialog.
46
+ Provides `event.data` with keys: `value`, `prev_value`, `text`, `dialog_result`.
47
+
48
+ `<<Input>>`: Fired when user manually types in the entry.
49
+
50
+ `<<Valid>>`: Fired when validation passes.
51
+
52
+ `<<Invalid>>`: Fired when validation fails.
53
+
54
+ Attributes:
55
+ entry_widget (TextEntryPart): The underlying text entry widget.
56
+ label_widget (Label): The label widget above the entry (from FieldOptions).
57
+ message_widget (Label): The message label widget below the entry.
58
+ addons (dict[str, Widget]): Dictionary of inserted addon widgets by name.
59
+ variable (Variable): Tkinter Variable linked to entry text.
60
+ signal (Signal): Signal object for reactive updates.
61
+ dialog_result (Any): The raw result from the last file dialog operation.
62
+ dialog_button (Button): The button widget that opens the dialog.
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ master: Master = None,
68
+ *,
69
+ value: str | None = None,
70
+ dialog: FileDialogType = "openfilename",
71
+ dialog_options: dict[str, Any] | None = None,
72
+ button_text: str = "Browse",
73
+ label: str = None,
74
+ message: str = None,
75
+ **kwargs: Unpack[FieldOptions]
76
+ ):
77
+ """Initialize a PathEntry widget.
78
+
79
+ Creates a path entry field with a button that opens a native file or
80
+ directory chooser dialog. The selected path(s) are automatically displayed
81
+ in the entry field. The widget supports various dialog types for different
82
+ use cases (single file, multiple files, directory, save file).
83
+
84
+ Args:
85
+ master: Parent widget. If None, uses the default root window.
86
+ value: Initial path value to display in the entry field. Default is None
87
+ (empty field). This is updated when a path is selected from the dialog.
88
+ dialog: Type of file dialog to open. Default is "openfilename".
89
+ Options:
90
+
91
+ - 'openfilename': Single file selection (returns path string)
92
+ - 'openfile': Single file selection (returns file object)
93
+ - 'directory': Directory selection
94
+ - 'openfilenames': Multiple file selection (returns paths)
95
+ - 'openfiles': Multiple file selection (returns file objects)
96
+ - 'saveasfile': Save file dialog (returns file object)
97
+ - 'saveasfilename': Save file dialog (returns path string)
98
+
99
+ dialog_options: Dictionary of options to pass to the file dialog.
100
+ Common options: title, initialdir, initialfile, filetypes,
101
+ defaultextension, multiple.
102
+ button_text: Text to display on the browse button. Default is "Browse".
103
+ Can be changed at runtime via `configure(button_text=...)`.
104
+ label (str): Label text to display above the entry field (from FieldOptions).
105
+ message (str): Message text to display below the field.
106
+
107
+ Other Parameters:
108
+ required (bool): If True, field cannot be empty.
109
+ accent (str): Accent token for the focus ring and active border.
110
+ allow_blank (bool): Allow empty input.
111
+ cursor (str): Cursor style when hovering.
112
+ font (str): Font for text display.
113
+ foreground (str): Text color.
114
+ initial_focus (bool): If True, widget receives focus on creation.
115
+ justify (str): Text alignment.
116
+ show_message (bool): If True, displays message area.
117
+ padding (str): Padding around entry widget.
118
+ takefocus (bool): If True, widget accepts Tab focus.
119
+ textvariable (Variable): Tkinter Variable to link with text.
120
+ textsignal (Signal): Signal object for reactive updates.
121
+ width (int): Width in characters.
122
+
123
+ Note:
124
+ When multiple files are selected (using 'openfilenames' or 'openfiles'),
125
+ the paths are joined with ", " (comma-space) and displayed in the entry.
126
+ The raw dialog result (tuple/list) is available via the `dialog_result`
127
+ property.
128
+ """
129
+ self._dialog = dialog
130
+ self._dialog_options = dialog_options
131
+ self._dialog_result = None
132
+ self._button_text = button_text
133
+ self._prev_value: str | None = value
134
+
135
+ super().__init__(master=master, label=label, message=message, value=value, **kwargs)
136
+
137
+ self.insert_addon(
138
+ Button,
139
+ position="before",
140
+ name="dialog-button",
141
+ text=self._button_text,
142
+ command=self._show_file_chooser
143
+ )
144
+
145
+ @property
146
+ def dialog_button(self):
147
+ """Get the dialog button widget."""
148
+ return self.addons.get('dialog-button')
149
+
150
+ @property
151
+ def dialog_result(self):
152
+ """Get the raw result from the last file dialog operation.
153
+
154
+ For single file selection, this returns the path string.
155
+ For multiple file selection, this returns a tuple/list of paths.
156
+ """
157
+ return self._dialog_result
158
+
159
+ # ------ Configuration Delegates ------
160
+
161
+ @configure_delegate('dialog')
162
+ def _delegate_dialog(self, value: FileDialogType = None):
163
+ if value is None:
164
+ return self._dialog
165
+ else:
166
+ self._dialog = value
167
+ return None
168
+
169
+ @configure_delegate('button_text')
170
+ def _delegate_button_text(self, value: str = None):
171
+ if value is None:
172
+ return self._button_text
173
+ else:
174
+ self._button_text = value
175
+ if self.dialog_button:
176
+ self.dialog_button['text'] = value
177
+ return None
178
+
179
+ @configure_delegate('dialog_options')
180
+ def _delegate_dialog_options(self, value: dict[str, Any] = None):
181
+ if value is None:
182
+ return self._dialog_options
183
+ else:
184
+ self._dialog_options = value
185
+ return None
186
+
187
+ def _show_file_chooser(self):
188
+ """Open the file/directory chooser dialog and update the entry."""
189
+ method_name = f"ask{self._dialog}"
190
+ dialog_func = getattr(filedialog, method_name, None)
191
+
192
+ if dialog_func is None:
193
+ raise ValueError(f"Invalid dialog type `{self._dialog}`")
194
+
195
+ result = dialog_func(**(self._dialog_options or {}))
196
+ self._dialog_result = result
197
+
198
+ # Format display text for multiple selections
199
+ if isinstance(result, (tuple, list)):
200
+ display_text = ", ".join(str(p) for p in result)
201
+ else:
202
+ display_text = result
203
+
204
+ # Only update if a selection was made (result is truthy)
205
+ if result:
206
+ prev_value = self._prev_value
207
+ self._prev_value = display_text
208
+
209
+ # Set the value through the field's standard mechanism
210
+ self.value = display_text
211
+
212
+ # Emit <<Change>> on PathEntry (the composite) with full event data
213
+ self.event_generate(
214
+ '<<Change>>',
215
+ data={
216
+ 'value': display_text,
217
+ 'prev_value': prev_value,
218
+ 'text': display_text,
219
+ 'dialog_result': self._dialog_result,
220
+ },
221
+ when="tail"
222
+ )
223
+
@@ -0,0 +1,466 @@
1
+ from __future__ import annotations
2
+
3
+ from tkinter import StringVar
4
+ from typing import Any, Callable, Literal, TYPE_CHECKING
5
+
6
+ from typing_extensions import TypedDict, Unpack
7
+
8
+ from bootstack.widgets.primitives.radiobutton import RadioButton
9
+ from bootstack.widgets.primitives.label import Label
10
+ from bootstack.widgets.mixins.configure_mixin import configure_delegate
11
+ from bootstack.widgets.primitives import Frame
12
+ from bootstack.widgets.types import Master
13
+
14
+ if TYPE_CHECKING:
15
+ from bootstack.core.signals import Signal
16
+ import tkinter as tk
17
+
18
+
19
+ class RadioGroupKwargs(TypedDict, total=False):
20
+ variable: Any
21
+ signal: Signal[Any]
22
+ value: str
23
+ orient: Literal['horizontal', 'vertical']
24
+ accent: str
25
+ text: str
26
+ labelanchor: Literal['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw']
27
+ state: Literal['normal', 'disabled']
28
+ show_border: bool
29
+ surface: str
30
+ style_options: dict[str, Any]
31
+ # Frame options
32
+ padding: Any
33
+ width: int
34
+ height: int
35
+
36
+
37
+ class RadioGroup(Frame):
38
+ """A group of radio buttons with automatic state tracking and optional label.
39
+
40
+ The RadioGroup widget provides a convenient way to create groups of radio
41
+ buttons with automatic state management. It supports both horizontal and
42
+ vertical orientations, and can display an optional label in various positions.
43
+
44
+ Attributes:
45
+ variable (Variable): The underlying tk.Variable for the selected value.
46
+ signal (Signal): Signal for reactive programming and change subscriptions.
47
+ """
48
+
49
+ def __init__(self, master: Master = None, **kwargs: Unpack[RadioGroupKwargs]):
50
+ """Initialize the RadioGroup.
51
+
52
+ Args:
53
+ master: Parent widget. If None, uses the default root window.
54
+
55
+ Other Parameters:
56
+ orient (str): Layout orientation - 'horizontal' (default) or 'vertical'.
57
+ accent (str): Accent token for styling (e.g., 'primary', 'success', 'danger').
58
+ Defaults to 'primary'.
59
+ bootstyle (str): DEPRECATED - Use `accent` instead.
60
+ text (str): Optional label text to display.
61
+ labelanchor (str): Label position - 'n' (top, default), 's' (bottom),
62
+ 'e' (right), 'w' (left), or combinations like 'nw', 'ne', etc.
63
+ variable (Variable): Optional tk.StringVar for controlling the selected value.
64
+ signal (Signal): Optional Signal instance for reactive programming.
65
+ value (str): Initial selected value.
66
+ state (str): Initial state for all buttons - 'normal' (default) or 'disabled'.
67
+ show_border (bool): If True, draws a border around the group.
68
+ surface (str): Optional surface token; otherwise inherited.
69
+ style_options (dict): Additional style options passed to child buttons.
70
+ padding (int | tuple): Frame padding. Defaults to 1.
71
+ width (int): Requested width in pixels.
72
+ height (int): Requested height in pixels.
73
+ """
74
+ # Extract RadioGroup-specific options before super().__init__
75
+ self._orientation = kwargs.pop('orient', 'horizontal')
76
+ # Support both 'accent' and legacy 'bootstyle'
77
+ self._accent = kwargs.pop('accent', None) or kwargs.pop('bootstyle', 'primary')
78
+ self._labeltext = kwargs.pop('text', None)
79
+ self._labelanchor = kwargs.pop('labelanchor', 'n')
80
+ self._state = kwargs.pop('state', 'normal')
81
+
82
+ # Handle signal/variable/value
83
+ initial_value = kwargs.pop('value', None)
84
+ style_options = kwargs.pop('style_options', {})
85
+
86
+ # Initialize internal state
87
+ self._buttons: dict[str, RadioButton] = {}
88
+ self._label: Label | None = None
89
+ self._button_container: Frame | None = None
90
+
91
+ # Extract signal/variable before Frame init
92
+ signal_value = kwargs.pop('signal', None)
93
+ variable_value = kwargs.pop('variable', None)
94
+
95
+ # Handle padding default
96
+ if 'padding' not in kwargs:
97
+ kwargs['padding'] = 1
98
+
99
+ # Call super().__init__() - just Frame now
100
+ super().__init__(master, style_options=style_options, **kwargs)
101
+
102
+ # Handle variable/signal setup manually
103
+ if signal_value is not None:
104
+ # Signal provided - extract its variable
105
+ self._signal = signal_value
106
+ self._variable = signal_value.var
107
+ elif variable_value is not None:
108
+ # Variable provided - wrap in Signal
109
+ from bootstack.core.signals import Signal
110
+ self._variable = variable_value
111
+ self._signal = Signal.from_variable(variable_value)
112
+ # Set initial value if provided
113
+ if initial_value is not None:
114
+ self._variable.set(initial_value)
115
+ else:
116
+ # Neither provided - create internal variable
117
+ from bootstack.core.signals import Signal
118
+ internal_var = StringVar(value=initial_value or '')
119
+ self._variable = internal_var
120
+ self._signal = Signal.from_variable(internal_var)
121
+
122
+ # Build the UI
123
+ self._build_ui()
124
+
125
+ def _build_ui(self):
126
+ """Construct the widget layout."""
127
+ # Create button container
128
+ self._button_container = Frame(self)
129
+
130
+ # Create label if text provided
131
+ if self._labeltext:
132
+ self._label = Label(self, text=self._labeltext)
133
+
134
+ # Position based on labelanchor
135
+ self._update_layout()
136
+
137
+ def _update_layout(self):
138
+ """Update grid layout based on labelanchor."""
139
+ # Clear existing layout
140
+ for widget in self.winfo_children():
141
+ widget.grid_forget()
142
+
143
+ # Determine anchor direction from labelanchor
144
+ # n/s = top/bottom, e/w = right/left
145
+ anchor = self._labelanchor.lower()
146
+
147
+ # Normalize compound anchors to primary direction
148
+ # Priority: n > s > w > e (vertical placement preferred over horizontal)
149
+ if 'n' in anchor:
150
+ primary = 'n'
151
+ elif 's' in anchor:
152
+ primary = 's'
153
+ elif 'w' in anchor:
154
+ primary = 'w'
155
+ elif 'e' in anchor:
156
+ primary = 'e'
157
+ else:
158
+ # Default to north
159
+ primary = 'n'
160
+
161
+ # Layout based on primary anchor
162
+ if primary == 'n':
163
+ # Label on top
164
+ if self._label:
165
+ self._label.grid(row=0, column=0, sticky='w', pady=(0, 4))
166
+ self._button_container.grid(row=1, column=0, sticky='ew')
167
+ self.grid_columnconfigure(0, weight=1)
168
+ elif primary == 's':
169
+ # Label on bottom
170
+ self._button_container.grid(row=0, column=0, sticky='ew')
171
+ if self._label:
172
+ self._label.grid(row=1, column=0, sticky='w', pady=(4, 0))
173
+ self.grid_columnconfigure(0, weight=1)
174
+ elif primary == 'w':
175
+ # Label on left
176
+ if self._label:
177
+ self._label.grid(row=0, column=0, sticky='w', padx=(0, 8))
178
+ self._button_container.grid(row=0, column=1, sticky='ew')
179
+ self.grid_columnconfigure(1, weight=1)
180
+ else: # 'e'
181
+ # Label on right
182
+ self._button_container.grid(row=0, column=0, sticky='ew')
183
+ if self._label:
184
+ self._label.grid(row=0, column=1, sticky='w', padx=(8, 0))
185
+ self.grid_columnconfigure(0, weight=1)
186
+
187
+ @property
188
+ def variable(self) -> 'tk.Variable':
189
+ """Get the underlying tk.Variable."""
190
+ return self._variable
191
+
192
+ @variable.setter
193
+ def variable(self, value: 'tk.Variable') -> None:
194
+ """Set the variable."""
195
+ from bootstack.core.signals import Signal
196
+ self._variable = value
197
+ self._signal = Signal.from_variable(value)
198
+ # Update all buttons to use new variable
199
+ for button in self._buttons.values():
200
+ button.configure(variable=value)
201
+
202
+ @property
203
+ def signal(self) -> 'Signal[Any]':
204
+ """Get the signal."""
205
+ return self._signal
206
+
207
+ @signal.setter
208
+ def signal(self, value: 'Signal[Any]') -> None:
209
+ """Set the signal."""
210
+ self._signal = value
211
+ self._variable = value.var
212
+ # Update all buttons to use new variable
213
+ for button in self._buttons.values():
214
+ button.configure(variable=value.var)
215
+
216
+ def add(self, text: str = None, value: Any = None, key: str | None = None, **kwargs: Any) -> RadioButton:
217
+ """Add a radio button to the group.
218
+
219
+ Args:
220
+ text: Text to display on the button.
221
+ value: Value this button represents (required).
222
+ key: Unique identifier. Defaults to value.
223
+ **kwargs: Additional arguments passed to RadioButton.
224
+
225
+ Returns:
226
+ The created RadioButton widget.
227
+
228
+ Raises:
229
+ ValueError: If value is None or if a button with the same key already exists.
230
+ """
231
+ if value is None:
232
+ raise ValueError("The 'value' argument is required.")
233
+
234
+ key = key or value
235
+ if key in self._buttons:
236
+ raise ValueError(f"A button with the key '{key}' already exists.")
237
+
238
+ btn_kwargs = kwargs.copy()
239
+ # Use accent for button styling
240
+ if 'accent' not in btn_kwargs and 'bootstyle' not in btn_kwargs:
241
+ btn_kwargs['accent'] = self._accent
242
+ # Apply current state if not explicitly provided
243
+ if 'state' not in btn_kwargs:
244
+ btn_kwargs['state'] = self._state
245
+
246
+ button = RadioButton(
247
+ self._button_container,
248
+ text=text,
249
+ value=value,
250
+ variable=self.variable,
251
+ **btn_kwargs
252
+ )
253
+
254
+ if self._orientation == 'horizontal':
255
+ button.pack(side='left', padx=2)
256
+ else: # vertical
257
+ button.pack(side='top', anchor='w', pady=2)
258
+
259
+ self._buttons[key] = button
260
+ return button
261
+
262
+ def get(self) -> str:
263
+ """Return the currently selected value."""
264
+ return self.variable.get()
265
+
266
+ def set(self, value: str) -> None:
267
+ """Set the selected value.
268
+
269
+ Args:
270
+ value: The value to select (must match a button's value).
271
+
272
+ Raises:
273
+ TypeError: If value is not a string.
274
+ ValueError: If value doesn't exist in the group.
275
+ """
276
+ if not isinstance(value, str):
277
+ raise TypeError(f"RadioGroup requires a string value, got {type(value).__name__}")
278
+
279
+ # Allow empty string (deselection)
280
+ if value and value not in self._buttons:
281
+ valid_values = list(self._buttons.keys())
282
+ raise ValueError(
283
+ f"Value '{value}' not found in group. "
284
+ f"Valid values: {valid_values}"
285
+ )
286
+
287
+ self.variable.set(value)
288
+
289
+ @property
290
+ def value(self) -> str:
291
+ """Get or set the selected value."""
292
+ return self.get()
293
+
294
+ @value.setter
295
+ def value(self, value: str) -> None:
296
+ self.set(value)
297
+
298
+ def remove(self, key: str):
299
+ """Remove a button by its key.
300
+
301
+ Args:
302
+ key: The key of the button to remove.
303
+
304
+ Raises:
305
+ KeyError: If no button with the given key exists.
306
+ """
307
+ if key not in self._buttons:
308
+ raise KeyError(f"No button with key '{key}'")
309
+ button = self._buttons.pop(key)
310
+ button.destroy()
311
+
312
+ def items(self) -> tuple[RadioButton, ...]:
313
+ """Get all button widgets in the group.
314
+
315
+ Returns:
316
+ A tuple of all RadioButton instances in the group.
317
+ """
318
+ return tuple(self._buttons.values())
319
+
320
+ def item(self, key: str) -> RadioButton:
321
+ """Get a button by its key.
322
+
323
+ Args:
324
+ key: The key of the button to retrieve.
325
+
326
+ Returns:
327
+ The RadioButton instance.
328
+
329
+ Raises:
330
+ KeyError: If no button with the given key exists.
331
+ """
332
+ if key not in self._buttons:
333
+ raise KeyError(f"No button with key '{key}'")
334
+ return self._buttons[key]
335
+
336
+ def configure_item(self, key: str, option: str = None, **kwargs: Any):
337
+ """Configure a specific button by its key.
338
+
339
+ Args:
340
+ key: The key of the button to configure.
341
+ option: If provided, return the value of this option.
342
+ **kwargs: Configuration options to apply to the button.
343
+
344
+ Returns:
345
+ If option is provided, returns the value of that option.
346
+ """
347
+ button = self.item(key)
348
+ if option is not None:
349
+ return button.cget(option)
350
+ button.configure(**kwargs)
351
+
352
+ def on_changed(self, callback: Callable) -> Any:
353
+ """Subscribe to value changes. Callback receives `new_value: str` directly."""
354
+ return self._signal.subscribe(callback)
355
+
356
+ def off_changed(self, bind_id: Any) -> None:
357
+ """Unsubscribe from value changes."""
358
+ self._signal.unsubscribe(bind_id)
359
+
360
+ @configure_delegate('accent')
361
+ def _delegate_accent(self, value=None):
362
+ """Get or set the accent. Updates all buttons when changed."""
363
+ if value is None:
364
+ return self._accent
365
+
366
+ self._accent = value
367
+ # Update all buttons with new accent
368
+ for button in self._buttons.values():
369
+ button.configure(accent=value)
370
+
371
+ @configure_delegate('orient')
372
+ def _delegate_orient(self, value=None):
373
+ """Get or set orientation ('horizontal' or 'vertical'). Repacks buttons when changed."""
374
+ if value is None:
375
+ return self._orientation
376
+
377
+ if value not in ('horizontal', 'vertical'):
378
+ raise ValueError("orient must be 'horizontal' or 'vertical'")
379
+
380
+ self._orientation = value
381
+
382
+ # Repack all buttons in new orientation
383
+ for button in self._buttons.values():
384
+ button.pack_forget()
385
+ if self._orientation == 'horizontal':
386
+ button.pack(side='left', padx=2)
387
+ else:
388
+ button.pack(side='top', anchor='w', pady=2)
389
+
390
+ @configure_delegate('labelanchor')
391
+ def _delegate_labelanchor(self, value=None):
392
+ """Get or set label anchor position. Updates layout when changed."""
393
+ if value is None:
394
+ return self._labelanchor
395
+
396
+ valid_anchors = ('n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw')
397
+ if value not in valid_anchors:
398
+ raise ValueError(f"labelanchor must be one of {valid_anchors}")
399
+
400
+ self._labelanchor = value
401
+ self._update_layout()
402
+
403
+ @configure_delegate('text')
404
+ def _delegate_text(self, value=None):
405
+ """Get or set label text. Creates or removes label as needed."""
406
+ if value is None:
407
+ return self._labeltext if self._labeltext else ''
408
+
409
+ self._labeltext = value
410
+ if value and not self._label:
411
+ # Create label if it doesn't exist
412
+ self._label = Label(self, text=value)
413
+ self._update_layout()
414
+ elif self._label:
415
+ if value:
416
+ self._label.configure(text=value)
417
+ else:
418
+ # Remove label if text is empty
419
+ self._label.destroy()
420
+ self._label = None
421
+ self._update_layout()
422
+
423
+ @configure_delegate('value')
424
+ def _delegate_value(self, value=None):
425
+ """Get or set the selected value."""
426
+ if value is None:
427
+ return self.get()
428
+ self.set(value)
429
+
430
+ @configure_delegate('state')
431
+ def _delegate_state(self, value=None):
432
+ """Get or set state for all buttons ('normal' or 'disabled')."""
433
+ if value is None:
434
+ return self._state
435
+
436
+ if value not in ('normal', 'disabled'):
437
+ raise ValueError("state must be 'normal' or 'disabled'")
438
+
439
+ self._state = value
440
+ # Update all buttons with new state
441
+ for button in self._buttons.values():
442
+ button.configure(state=value)
443
+
444
+ def keys(self) -> tuple[str, ...]:
445
+ """Get all button keys.
446
+
447
+ Returns:
448
+ A tuple of all button keys in the group.
449
+ """
450
+ return tuple(self._buttons.keys())
451
+
452
+ def values(self) -> tuple[str, ...]:
453
+ """Get all possible values.
454
+
455
+ Returns:
456
+ A tuple of all values that can be selected.
457
+ """
458
+ return tuple(self._buttons.keys())
459
+
460
+ def __len__(self) -> int:
461
+ """Return the number of buttons in the group."""
462
+ return len(self._buttons)
463
+
464
+ def __contains__(self, key: str) -> bool:
465
+ """Check if a button with the given key exists in the group."""
466
+ return key in self._buttons