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,368 @@
1
+ """Font modifier syntax for inline font customization in bootstack widgets.
2
+
3
+ This module provides a mixin that enables concise font modification syntax for all
4
+ bootstack widgets. The syntax uses bracket notation similar to style modifiers,
5
+ allowing inline font customization without creating custom Font objects.
6
+
7
+ ## Syntax
8
+
9
+ The full modifier syntax follows the pattern: `family[size][weight][style]`
10
+
11
+ All components are optional and can be mixed in any combination. When components are
12
+ omitted, the widget's current font values are preserved.
13
+
14
+ Components:
15
+ - family: Font family name or typography token (e.g., 'helvetica', 'body', 'heading-lg')
16
+ - size: Point size (e.g., '16'), pixel size (e.g., '16px'), or size token (e.g., 'sm', 'lg')
17
+ - weight: 'bold' or 'normal'
18
+ - style: 'italic', 'roman', 'underline', 'overstrike' (comma-separated for multiple)
19
+
20
+ Size Tokens:
21
+ xs=8pt, sm=10pt, md=12pt, lg=14pt, xl=16pt, xxl=18pt
22
+
23
+ Font Tokens:
24
+ body, label, heading-md, heading-lg, heading-xl, display-lg, display-xl,
25
+ code, hyperlink, caption, body-sm, body-lg, body-xl
26
+
27
+ ## Behavior
28
+
29
+ - **At widget creation**: Modifiers are applied to the widget's default style font
30
+ - **At runtime**: Modifiers are applied to the widget's current font
31
+ - **Missing family**: Uses widget's current font family (or 'body' token if none)
32
+ - **Missing size**: Uses widget's current font size (or 'body' token size if none)
33
+
34
+ ## Examples
35
+
36
+ Basic modifications:
37
+
38
+ ```python
39
+ # Use body token, make it bold
40
+ Label(root, text="Title", font="body[bold]")
41
+
42
+ # Change current font to 16pt (preserves family)
43
+ label.configure(font="[16]")
44
+
45
+ # Make current font bold and italic (preserves family and size)
46
+ label.configure(font="[bold,italic]")
47
+ ```
48
+
49
+ Custom font families:
50
+
51
+ ```python
52
+ # Helvetica, 16pt, bold
53
+ Button(root, text="Click", font="helvetica[16][bold]")
54
+
55
+ # Arial, 14 pixels (negative in Tk), bold and italic
56
+ Label(root, text="Text", font="arial[14px][bold,italic]")
57
+ ```
58
+
59
+ Size tokens:
60
+
61
+ ```python
62
+ # Small size (10pt) with bold
63
+ Entry(root, font="[sm][bold]")
64
+
65
+ # Large size (14pt)
66
+ Label(root, font="[lg]")
67
+ ```
68
+
69
+ Font tokens with modifiers:
70
+
71
+ ```python
72
+ # Heading-lg token with italic style
73
+ Label(root, text="Heading", font="heading-lg[italic]")
74
+
75
+ # Label token at custom size
76
+ Button(root, text="Button", font="label[16]")
77
+ ```
78
+
79
+ Multiple style modifiers:
80
+
81
+ ```python
82
+ # Bold, italic, and underlined
83
+ Label(root, text="Emphasis", font="[16][bold,italic,underline]")
84
+ ```
85
+
86
+ ## Integration
87
+
88
+ FontMixin is automatically integrated into all bootstack widgets via TTKWrapperBase.
89
+ No additional setup is required - all widgets supporting the 'font' argument automatically
90
+ gain modifier syntax support.
91
+
92
+ The mixin uses the @configure_delegate pattern to intercept font configuration, parse
93
+ the modifier syntax, and apply the resolved font specification to the underlying ttk widget.
94
+ """
95
+
96
+ from __future__ import annotations
97
+
98
+ import re
99
+ from typing import Any, TYPE_CHECKING
100
+
101
+ if TYPE_CHECKING:
102
+ pass
103
+
104
+ from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
105
+
106
+
107
+ # Size tokens mapping (shortcuts for common sizes)
108
+ SIZE_TOKENS = {
109
+ 'xs': 8,
110
+ 'sm': 10,
111
+ 'md': 12,
112
+ 'lg': 14,
113
+ 'xl': 16,
114
+ 'xxl': 18,
115
+ }
116
+
117
+
118
+ def _get_font_token_names() -> set[str]:
119
+ """Get all valid font token names (lazy to avoid import issues)."""
120
+ from bootstack.style.typography import FontTokenNames
121
+ return {
122
+ name.replace('_', '-')
123
+ for name in dir(FontTokenNames)
124
+ if not name.startswith('_')
125
+ }
126
+
127
+
128
+ def parse_font_modifier(font_spec: str) -> dict[str, Any]:
129
+ """Parse font modifier syntax string into configuration dict.
130
+
131
+ Args:
132
+ font_spec: Font specification string (e.g., "helvetica[16][bold]")
133
+
134
+ Returns:
135
+ Dict with keys: family, size, weight, slant, underline, overstrike
136
+ """
137
+ if not font_spec or not font_spec.strip():
138
+ return {}
139
+
140
+ result = {}
141
+
142
+ # Find all bracketed parts: [content]
143
+ bracket_pattern = r'\[([^\]]+)\]'
144
+ parts = re.findall(bracket_pattern, font_spec)
145
+
146
+ # Get family/token (everything before first bracket, or whole string if no brackets)
147
+ if '[' in font_spec:
148
+ family_part = font_spec[:font_spec.index('[')].strip()
149
+ else:
150
+ family_part = font_spec.strip()
151
+
152
+ if family_part:
153
+ result['family'] = family_part
154
+
155
+ # Process each bracketed part
156
+ for part in parts:
157
+ part = part.strip()
158
+ if not part:
159
+ continue
160
+
161
+ # Check if it's a pixel size (e.g., "16px")
162
+ if part.endswith('px'):
163
+ try:
164
+ # Pixel sizes are negative in Tk
165
+ result['size'] = -int(part[:-2])
166
+ continue
167
+ except ValueError:
168
+ pass
169
+
170
+ # Check if it's a point size (e.g., "16")
171
+ if part.isdigit():
172
+ result['size'] = int(part)
173
+ continue
174
+
175
+ # Check if it's a size token (e.g., "sm", "lg")
176
+ if part in SIZE_TOKENS:
177
+ result['size'] = SIZE_TOKENS[part]
178
+ continue
179
+
180
+ # Otherwise, treat as comma-separated modifiers
181
+ modifiers = [m.strip().lower() for m in part.split(',')]
182
+ for modifier in modifiers:
183
+ if not modifier:
184
+ continue
185
+
186
+ # Weight modifiers
187
+ if modifier in ('bold', 'normal'):
188
+ result['weight'] = modifier
189
+ # Slant modifiers
190
+ elif modifier in ('italic', 'roman'):
191
+ result['slant'] = modifier
192
+ # Boolean modifiers
193
+ elif modifier == 'underline':
194
+ result['underline'] = True
195
+ elif modifier == 'overstrike':
196
+ result['overstrike'] = True
197
+
198
+ return result
199
+
200
+
201
+ def build_font_from_modifier(font_spec: str, base_font: Any = None) -> tuple | str:
202
+ """Build Tk-compatible font tuple from modifier syntax, using base_font for missing values.
203
+
204
+ Args:
205
+ font_spec: Font specification with modifier syntax (e.g., "[16][bold]")
206
+ base_font: Base font to extend (token name or tuple); defaults to 'body' token
207
+
208
+ Returns:
209
+ Font tuple like ('Helvetica', 16, 'bold italic') or token name
210
+ """
211
+ from bootstack.style.typography import Typography
212
+
213
+ parsed = parse_font_modifier(font_spec)
214
+ if not parsed:
215
+ return base_font or 'body'
216
+
217
+ # Start with base font configuration
218
+ config = {}
219
+ family_is_token = False
220
+ font_token_names = _get_font_token_names()
221
+
222
+ # Check if family is a known token
223
+ if 'family' in parsed:
224
+ family_name = parsed['family']
225
+ if family_name in font_token_names:
226
+ # It's a font token - get its spec
227
+ token_spec = Typography.get_token(family_name)
228
+ config['family'] = token_spec.font
229
+ config['size'] = token_spec.size
230
+ config['weight'] = token_spec.weight
231
+ if token_spec.underline:
232
+ config['underline'] = token_spec.underline
233
+ family_is_token = True
234
+ else:
235
+ # It's a font family name
236
+ config['family'] = family_name
237
+ elif base_font:
238
+ # Use base font if provided
239
+ if isinstance(base_font, str) and base_font in font_token_names:
240
+ token_spec = Typography.get_token(base_font)
241
+ config['family'] = token_spec.font
242
+ config['size'] = token_spec.size
243
+ config['weight'] = token_spec.weight
244
+ if token_spec.underline:
245
+ config['underline'] = token_spec.underline
246
+ elif isinstance(base_font, tuple) and len(base_font) >= 2:
247
+ config['family'] = base_font[0]
248
+ config['size'] = base_font[1]
249
+ if len(base_font) >= 3:
250
+ # Parse weight/slant from tuple
251
+ styles = base_font[2].split()
252
+ for style in styles:
253
+ if style in ('bold', 'normal'):
254
+ config['weight'] = style
255
+ elif style in ('italic', 'roman'):
256
+ config['slant'] = style
257
+
258
+ # Apply parsed modifiers (override base)
259
+ if 'size' in parsed:
260
+ config['size'] = parsed['size']
261
+ if 'weight' in parsed:
262
+ config['weight'] = parsed['weight']
263
+ if 'slant' in parsed:
264
+ config['slant'] = parsed['slant']
265
+ if 'underline' in parsed:
266
+ config['underline'] = parsed['underline']
267
+ if 'overstrike' in parsed:
268
+ config['overstrike'] = parsed['overstrike']
269
+
270
+ # Ensure we have family and size - use body token as fallback
271
+ if 'family' not in config or 'size' not in config:
272
+ body_spec = Typography.get_token('body')
273
+ if 'family' not in config:
274
+ config['family'] = body_spec.font
275
+ if 'size' not in config:
276
+ config['size'] = body_spec.size
277
+
278
+ # Build Tk font specification as tuple (family, size, modifiers_string)
279
+ family = config['family']
280
+ size = config['size']
281
+ modifiers = []
282
+
283
+ if config.get('weight') == 'bold':
284
+ modifiers.append('bold')
285
+ if config.get('slant') == 'italic':
286
+ modifiers.append('italic')
287
+ if config.get('underline'):
288
+ modifiers.append('underline')
289
+ if config.get('overstrike'):
290
+ modifiers.append('overstrike')
291
+
292
+ if modifiers:
293
+ return (family, size, ' '.join(modifiers))
294
+ else:
295
+ return (family, size)
296
+
297
+
298
+ class FontMixin:
299
+ """Adds font modifier syntax support to widgets at construction and runtime."""
300
+
301
+ def _init_font_mixin(self, kwargs: dict[str, Any]) -> Any:
302
+ """Extract and pre-process the `font` kwarg before widget construction.
303
+
304
+ Called by `TTKWrapperBase.__init__` before `super().__init__` so the
305
+ font modifier string is resolved to a Tk-compatible tuple early.
306
+
307
+ Args:
308
+ kwargs: The widget constructor kwargs dict (mutated in-place — `font`
309
+ is popped if present).
310
+
311
+ Returns:
312
+ The resolved font value (token name or tuple), or None if `font` was
313
+ not in kwargs.
314
+ """
315
+ if "font" not in kwargs:
316
+ return None
317
+
318
+ value = kwargs.pop("font")
319
+
320
+ if isinstance(value, str):
321
+ font_token_names = _get_font_token_names()
322
+
323
+ if value in font_token_names and "[" not in value:
324
+ return value
325
+
326
+ if "[" in value:
327
+ # Constructor-time: no widget yet, so use a stable base
328
+ return build_font_from_modifier(value, base_font="body")
329
+
330
+ return value
331
+
332
+ @configure_delegate("font")
333
+ def _delegate_font(self, value: Any = None) -> Any:
334
+ """Get or set the widget font, resolving bootstack modifier syntax.
335
+
336
+ Accepts a plain font name, a token name (e.g. `'body'`,
337
+ `'heading-lg'`), a modifier expression (e.g. `'body[bold]'`,
338
+ `'[16][italic]'`), or a Tk font tuple. Modifier expressions are
339
+ merged with the widget's current font so only the specified
340
+ attributes change.
341
+
342
+ Args:
343
+ value: Font specification, or None to query the current font.
344
+
345
+ Returns:
346
+ Current font value when querying, otherwise the result of
347
+ the underlying `configure(font=...)` call.
348
+ """
349
+ if value is None:
350
+ return self._ttk_base.cget(self, "font") # type: ignore[misc]
351
+
352
+ if isinstance(value, str):
353
+ font_token_names = _get_font_token_names()
354
+
355
+ if value in font_token_names and "[" not in value:
356
+ font_value = value
357
+ elif "[" in value:
358
+ try:
359
+ current_font = self._ttk_base.cget(self, "font") # type: ignore[misc]
360
+ except Exception:
361
+ current_font = "body"
362
+ font_value = build_font_from_modifier(value, base_font=current_font)
363
+ else:
364
+ font_value = value
365
+ else:
366
+ font_value = value
367
+
368
+ return self._ttk_base.configure(self, font=font_value) # type: ignore[misc]
@@ -0,0 +1,61 @@
1
+ """Icon mixin for widgets that support theme-aware icons.
2
+
3
+ Provides a `@configure_delegate('icon')` handler that applies an icon through
4
+ the style system and preserves current style tokens, orientation, and
5
+ surface color where applicable.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from typing import Any, Callable
11
+
12
+ from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
13
+
14
+
15
+ class IconMixin:
16
+ """Adds `icon` configuration support via the style engine."""
17
+
18
+ configure_style_options: Callable
19
+ rebuild_style: Callable
20
+
21
+ @configure_delegate("icon")
22
+ def _delegate_icon(self, value: Any = None) -> Any:
23
+ """Get or set the widget icon.
24
+
25
+ Args:
26
+ value: Icon name (str), icon spec dict, or None to query the
27
+ current value.
28
+
29
+ Returns:
30
+ Current icon value when querying (`value` is None), otherwise
31
+ the result of rebuilding the widget style.
32
+ """
33
+ if value is None:
34
+ return self.configure_style_options("icon")
35
+ else:
36
+ self.configure_style_options(icon=value)
37
+ return self.rebuild_style()
38
+
39
+ @configure_delegate("icon_only")
40
+ def _delegate_icon_only(self, value: Any = None) -> Any:
41
+ """Get or set icon-only display mode.
42
+
43
+ When `True`, removes the extra padding reserved for text so the
44
+ icon fills the available space without a label gap.
45
+
46
+ Args:
47
+ value: `True` to enable icon-only mode, `False` to disable,
48
+ or None to query the current value.
49
+
50
+ Returns:
51
+ Current `icon_only` value when querying, otherwise the result
52
+ of rebuilding the widget style.
53
+ """
54
+ if value is None:
55
+ return self.configure_style_options("icon_only")
56
+ else:
57
+ self.configure_style_options(icon_only=value)
58
+ return self.rebuild_style()
59
+
60
+
61
+ __all__ = ["IconMixin"]
@@ -0,0 +1,253 @@
1
+ """Localization mixin for bootstack widgets.
2
+
3
+ Provides automatic text translation and value formatting based on the current
4
+ locale. Widgets using this mixin will automatically update when the locale
5
+ changes.
6
+
7
+ This mixin is a thin glue layer that delegates resolution logic to the core
8
+ localization capability module.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import tkinter
14
+ from typing import Any, Dict
15
+ from tkinter import StringVar, Misc
16
+
17
+ from bootstack._core.capabilities.localization import (
18
+ resolve_text,
19
+ resolve_variable_text,
20
+ apply_spec,
21
+ create_formatted_signal,
22
+ )
23
+ from bootstack.i18n.specs import (
24
+ LocalizedSpec,
25
+ )
26
+ from bootstack._runtime.app import get_app_settings
27
+
28
+
29
+ class LocalizationMixin(Misc):
30
+ """Mixin for widgets that support automatic text and value localization.
31
+
32
+ This mixin enables widgets to automatically localize text and format values
33
+ according to the current locale. It listens for locale change events and
34
+ updates all registered fields accordingly.
35
+
36
+ This mixin delegates resolution logic to the core localization capability.
37
+
38
+ Attributes:
39
+ localize: Controls whether literals are auto-wrapped into localization specs.
40
+ Can be True, False, or "auto".
41
+ value_format: Default IntlFormatter spec for non-string values (e.g., 'currency',
42
+ 'decimal', 'percent').
43
+ """
44
+
45
+ def __init__(self, *args, **kwargs):
46
+ """Initialize the localizable widget mixin.
47
+
48
+ This mixin intercepts localization-related kwargs. It inspects 'text'
49
+ for localization without consuming it, so it can be passed to the
50
+ underlying widget. It consumes 'localize' and 'value_format' as they
51
+ are not standard ttk options.
52
+ """
53
+ # Determine the localization mode. A widget-specific 'localize' argument
54
+ # overrides the global app setting. This argument is consumed from kwargs.
55
+ localize = kwargs.pop('localize', get_app_settings().localize_mode)
56
+ value_format = kwargs.pop('value_format', None)
57
+
58
+ # Get 'text' for localization without removing it from kwargs.
59
+ text_to_localize = kwargs.get('text')
60
+
61
+ # Call the next class in the MRO with the remaining kwargs.
62
+ # 'text' is still in kwargs for the underlying widget to use.
63
+ super().__init__(*args, **kwargs)
64
+
65
+ # --- Post-init setup ---
66
+ self._localized_fields: Dict[str, LocalizedSpec] = {}
67
+ self._localize_mode = localize
68
+ self._default_value_format = value_format
69
+
70
+ root = self.winfo_toplevel() # event is generated on root
71
+ root.bind("<<LocaleChanged>>", self._on_locale_changed, add="+")
72
+
73
+ # Check if widget has a textsignal or textvariable with value_format
74
+ if value_format and self._has_signal_or_variable():
75
+ self._setup_signal_formatting(value_format)
76
+ else:
77
+ # Register the text field for static localization
78
+ self.register_localized_field('text', text_to_localize, value_format=value_format)
79
+
80
+ def _has_signal_or_variable(self) -> bool:
81
+ """Check if the widget has a textsignal, textvariable, or configured textvariable.
82
+
83
+ Returns:
84
+ True if any signal/variable binding exists.
85
+ """
86
+ if hasattr(self, '_textsignal') or hasattr(self, '_textvariable'):
87
+ return True
88
+ try:
89
+ textvariable_name = self.cget('textvariable')
90
+ return bool(textvariable_name)
91
+ except Exception:
92
+ return False
93
+
94
+ def register_localized_field(
95
+ self,
96
+ field_name: str,
97
+ value: Any,
98
+ *,
99
+ value_format: str | None = None,
100
+ localize: bool | str | None = None,
101
+ ) -> None:
102
+ """Register a widget field for automatic localization.
103
+
104
+ Args:
105
+ field_name: The widget field name (e.g., "text", "textvariable").
106
+ value: The value to localize. Can be a literal string/number or a LocalizedSpec.
107
+ value_format: Optional IntlFormatter spec for non-string values (e.g., 'currency').
108
+ localize: Override the widget's default localization mode for this field.
109
+ """
110
+ if value is None:
111
+ return
112
+
113
+ if isinstance(value, str) and value == "":
114
+ return
115
+
116
+ localize_mode = self._localize_mode if localize is None else localize
117
+
118
+ # If already a spec, use it directly
119
+ if isinstance(value, LocalizedSpec):
120
+ self._localized_fields[field_name] = value
121
+ self._apply_spec_now(field_name, value)
122
+ return
123
+
124
+ if localize_mode is False:
125
+ return
126
+
127
+ # Resolve the value to a spec using core capability
128
+ if isinstance(value, str):
129
+ spec = resolve_text(value, localize_mode=localize_mode)
130
+ else:
131
+ spec = resolve_variable_text(
132
+ value,
133
+ value_format=value_format,
134
+ default_format=self._default_value_format or "decimal",
135
+ )
136
+
137
+ if spec is not None:
138
+ self._localized_fields[field_name] = spec
139
+ self._apply_spec_now(field_name, spec)
140
+
141
+ def _apply_spec_now(self, field_name: str, spec: LocalizedSpec) -> None:
142
+ """Resolve a localization spec using the current locale and apply immediately.
143
+
144
+ Args:
145
+ field_name: The widget field name to update.
146
+ spec: The LocalizedSpec to resolve and apply.
147
+ """
148
+ if not spec.enabled:
149
+ return
150
+ value = apply_spec(spec)
151
+ self._apply_localized_value(field_name, value)
152
+
153
+ def _on_locale_changed(self, event: tkinter.Event | None = None) -> None:
154
+ """Handle locale change events by refreshing all localized fields.
155
+
156
+ Args:
157
+ event: The Tkinter event object (unused).
158
+ """
159
+ self._refresh_localized_fields()
160
+
161
+ # If we have signal formatting, trigger a re-format with new locale
162
+ if hasattr(self, '_signal_formatter'):
163
+ value_format, formatter, source_signal = self._signal_formatter
164
+ # Re-format current value from source signal with new locale
165
+ formatter(source_signal())
166
+
167
+ def _refresh_localized_fields(self) -> None:
168
+ """Refresh all registered localized fields with the current locale."""
169
+ for field_name, spec in self._localized_fields.items():
170
+ if not spec.enabled:
171
+ continue
172
+ value = apply_spec(spec)
173
+ self._apply_localized_value(field_name, value)
174
+
175
+ def _setup_signal_formatting(self, value_format: str) -> None:
176
+ """Subscribe to textsignal and format its values.
177
+
178
+ When formatting is enabled, this creates a private textvariable for this widget
179
+ and subscribes to the source signal to format and display values independently.
180
+
181
+ Args:
182
+ value_format: The format spec (e.g., 'currency', 'decimal').
183
+ """
184
+ # Ensure textsignal exists (triggers lazy creation if needed)
185
+ if not hasattr(self, '_textsignal'):
186
+ _ = self.textsignal
187
+
188
+ # Save reference to the source signal (the one we're subscribing to)
189
+ source_signal = self._textsignal
190
+
191
+ # Create formatted signal using core capability
192
+ formatted_signal, formatter = create_formatted_signal(source_signal, value_format)
193
+
194
+ # Update our internal references to use the new private signal/variable
195
+ self._textsignal = formatted_signal
196
+ self._textvariable = formatted_signal.var
197
+
198
+ # Configure widget to use the new private variable
199
+ try:
200
+ self._ttk_base.configure(self, textvariable=self._textvariable) # type: ignore[misc]
201
+ except Exception:
202
+ pass
203
+
204
+ # Store references for locale changes
205
+ self._signal_formatter = (value_format, formatter, source_signal)
206
+
207
+ def _apply_localized_value(self, field_name: str, value: str) -> None:
208
+ """Apply a localized value to the widget field.
209
+
210
+ Checks for associated textvariable/variable first to ensure proper
211
+ integration with textsignal/signal reactive bindings. Falls back to
212
+ direct widget configuration if no variable is found.
213
+
214
+ Args:
215
+ field_name: The widget field name to update.
216
+ value: The localized value to apply.
217
+ """
218
+ # Check if field has a direct attribute (less common)
219
+ var = getattr(self, field_name, None)
220
+ if isinstance(var, StringVar):
221
+ var.set(value)
222
+ return
223
+
224
+ # Check for textvariable (for 'text' field)
225
+ if field_name == 'text':
226
+ # Try cached _textvariable first
227
+ if hasattr(self, '_textvariable') and self._textvariable:
228
+ self._textvariable.set(value)
229
+ return
230
+
231
+ # Try to get textvariable from widget configuration
232
+ try:
233
+ textvariable_name = self.cget('textvariable')
234
+ if textvariable_name:
235
+ # Get the actual variable object via the property
236
+ self.textvariable.set(value)
237
+ return
238
+ except Exception:
239
+ pass
240
+
241
+ # Check for variable (for 'value' or other fields)
242
+ if hasattr(self, '_variable') and self._variable and field_name != 'text':
243
+ self._variable.set(value)
244
+ return
245
+
246
+ # Fallback: configure the widget option directly
247
+ try:
248
+ self.configure({field_name: value})
249
+ except Exception:
250
+ pass
251
+
252
+
253
+ __all__ = ["LocalizationMixin"]