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,882 @@
1
+ import math
2
+ from tkinter import Canvas, DoubleVar, IntVar, StringVar, font as tkfont
3
+ from typing import Any, Callable, Literal
4
+ from warnings import warn
5
+
6
+ from PIL import Image, ImageDraw, ImageTk
7
+ from PIL.Image import Resampling
8
+
9
+ from bootstack.core.exceptions import ConfigurationWarning
10
+ from bootstack.widgets.primitives.frame import Frame
11
+ from bootstack.widgets.mixins.configure_mixin import configure_delegate
12
+ from bootstack.widgets.types import Master
13
+
14
+ DEFAULT_IMAGE_SCALE = 6
15
+
16
+
17
+ class Meter(Frame):
18
+ """A circular progress meter widget with customizable appearance and optional text display.
19
+
20
+ The Meter widget displays a value as a circular arc indicator with optional value text,
21
+ prefix/suffix labels, and subtitle. Supports both full circle and semi-circle styles,
22
+ segmented or solid indicators, and interactive mode for user input.
23
+
24
+ The meter value can be accessed and modified using:
25
+
26
+ - `get()` / `set(value)` - Standard value-widget API methods
27
+ - `.value` property - Direct property access
28
+ - `configure(value=x)` - Via the configure interface
29
+
30
+ !!! note "Events"
31
+
32
+ `<<Change>>`: Fired whenever the meter value changes.
33
+ Provides `event.data` with keys: `value`, `prev_value`.
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ master: Master = None,
39
+ accent: str = None,
40
+
41
+ # value parameters
42
+ value: int | float = 0,
43
+ minvalue: int | float = 0,
44
+ maxvalue: int | float = 100,
45
+ value_format: str = "{:.0f}",
46
+ value_prefix: str = None,
47
+ value_suffix: str = None,
48
+ value_font: str = None,
49
+ dtype: type[int] | type[float] = int,
50
+
51
+ # secondary options
52
+ secondary_font: str = None,
53
+ secondary_style: str = None,
54
+ subtitle: str = None,
55
+
56
+ # appearance
57
+ size: int = 200,
58
+ thickness: int = 10,
59
+ indicator_width: int = 0,
60
+ segment_width: int = 0,
61
+ arc_range: int = None,
62
+ arc_offset: int = None,
63
+
64
+ # other
65
+ meter_type: Literal['semi', 'full'] = 'full',
66
+ show_text: bool = True,
67
+ interactive: bool = False,
68
+ step_size: int | float = 1,
69
+ **kwargs: Any
70
+ ):
71
+ """Initialize a Meter widget.
72
+
73
+ Args:
74
+ master: The parent widget.
75
+ accent: Accent token for the meter indicator (e.g., 'primary', 'success').
76
+ bootstyle: DEPRECATED - Use `accent` instead.
77
+
78
+ value: Current meter value.
79
+ minvalue: Minimum value for the meter range.
80
+ maxvalue: Maximum value for the meter range.
81
+ value_format: Format string for displaying the value (e.g., "{:.0f}", "{:.2f}").
82
+ value_prefix: Text to display before the value (e.g., "$", "@").
83
+ value_suffix: Text to display after the value (e.g., "%", "mph").
84
+ value_font: Font specification for the value text (e.g., "-size 36 -weight bold").
85
+ dtype: Data type for the value variable (int or float).
86
+
87
+ secondary_font: Font specification for prefix, suffix, and subtitle text.
88
+ secondary_style: Style name for prefix, suffix, and subtitle color.
89
+ subtitle: Optional subtitle text displayed below the value.
90
+
91
+ size: Width and height of the meter in pixels.
92
+ thickness: Width of the meter arc in pixels.
93
+ indicator_width: Width of the indicator segment when using a wedge-style indicator.
94
+ 0 means fill from start to current value.
95
+ segment_width: Width of each segment for a segmented meter style. 0 means solid.
96
+ arc_range: Total arc range in degrees. None uses defaults (360 for full, 270 for semi).
97
+ arc_offset: Starting angle offset in degrees. None uses defaults (-90 for full, 135 for semi).
98
+
99
+ meter_type: Meter style - 'full' for full circle or 'semi' for semicircle.
100
+ show_text: Whether to display the value text and labels.
101
+ interactive: Whether the meter responds to mouse clicks/drags to change value.
102
+ step_size: Increment step when in interactive mode.
103
+ **kwargs: Additional keyword arguments passed to the Frame parent class.
104
+
105
+ !!! note "Events"
106
+ - `<<Change>>`: Emitted when the value changes (see on_changed()).
107
+ """
108
+ legacy = Meter._coerce_legacy_params(kwargs)
109
+ super().__init__(master, **kwargs)
110
+
111
+ # configuration
112
+ self._dtype = dtype
113
+ self._size = legacy.get('size', size)
114
+ self._thickness = legacy.get('thickness', thickness)
115
+ self._indicator_width = legacy.get('indicator_width', indicator_width)
116
+ self._segment_width = legacy.get('segment_width', segment_width)
117
+ self._arc_range = legacy.get('arc_range', arc_range)
118
+ self._arc_offset = legacy.get('arc_offset', arc_offset)
119
+ self._minvalue = legacy.get('minvalue', minvalue)
120
+ self._maxvalue = legacy.get('maxvalue', maxvalue)
121
+
122
+ self._meter_type = legacy.get('meter_type', meter_type)
123
+ self._show_text = legacy.get('show_text', show_text)
124
+ self._interactive = interactive
125
+ self._step_size = legacy.get('step_size', step_size)
126
+
127
+ self._value_format = legacy.get('value_format', value_format)
128
+ self._value_font = legacy.get('value_font', value_font or '-size 36 -weight bold')
129
+ self._value_prefix = legacy.get('value_prefix', value_prefix)
130
+ self._value_suffix = legacy.get('value_suffix', value_suffix)
131
+ self._accent = accent or 'primary'
132
+
133
+ self._subtitle = legacy.get('subtitle', subtitle)
134
+ self._secondary_font = legacy.get('secondary_font', secondary_font or '-size 9')
135
+ self._secondary_style = legacy.get('secondary_style', secondary_style or 'background[muted]')
136
+
137
+ # color tokens (separate from resolved colors)
138
+ self._surface_token = 'background' # Token name for surface color
139
+
140
+ # state tracking
141
+ self._towards_maximum = True
142
+ self._resolve_arc_range_offset(meter_type, arc_offset, arc_range)
143
+ self._binding = {}
144
+
145
+ # widget variables
146
+ value = legacy.get('value', value)
147
+ self._last_changed_value = value
148
+ self._value_var = self._variable(value)
149
+ self._value_var.trace_add('write', self._update_meter) # Update meter when value changes
150
+ self._value_display_var = StringVar(value=value_format.format(value))
151
+ self._subtitle_var = StringVar(value=self._subtitle)
152
+
153
+ # Resolve styles first to get colors
154
+ self._resolve_meter_styles()
155
+
156
+ # layout - use canvas for both meter and text
157
+ self._canvas = Canvas(
158
+ master=self,
159
+ width=self._size,
160
+ height=self._size,
161
+ highlightthickness=0,
162
+ background=self._surface
163
+ )
164
+
165
+ # Canvas text items (will be created/updated in _draw_meter)
166
+ self._meter_image_id = None
167
+ self._value_text_id = None
168
+ self._prefix_text_id = None
169
+ self._suffix_text_id = None
170
+ self._subtitle_text_id = None
171
+
172
+ # update bindings
173
+ self._binding['<<ThemeChanged>>'] = self.bind('<<ThemeChanged>>', self._handle_theme_changed)
174
+ self._binding['<<Configure>>'] = self.bind('<<Configure>>', self._handle_theme_changed)
175
+ self._bind_interactive_events()
176
+ self._draw_base_meter_images()
177
+ self._draw_meter()
178
+
179
+ # set widget geometry
180
+ self._canvas.pack()
181
+
182
+ # ----- Configuration Delegates -----
183
+
184
+ @staticmethod
185
+ def _coerce_legacy_params(options):
186
+ param_map = dict(
187
+ amountused="value",
188
+ amountmin="minvalue",
189
+ amounttotal="maxvalue",
190
+ amountformat="value_format",
191
+ textleft="value_prefix",
192
+ textright="value_suffix",
193
+ textfont="value_font",
194
+ subtextfont="secondary_font",
195
+ subtextstyle="secondary_style",
196
+ subtext="subtitle",
197
+ metersize="size",
198
+ meterthickness="thickness",
199
+ wedgesize="indicator_width",
200
+ stripethickness="segment_width",
201
+ arcrange="arc_range",
202
+ arcoffset="arc_offset",
203
+ metertype="meter_type",
204
+ showtext="show_text",
205
+ stepsize="step_size"
206
+ )
207
+ legacy_params = dict()
208
+ legacy_keys = set()
209
+
210
+ for k, v in options.items():
211
+ if k in param_map:
212
+ legacy_params[param_map[k]] = v
213
+ legacy_keys.add(k)
214
+
215
+ if legacy_params:
216
+ for k in legacy_keys:
217
+ del options[k]
218
+ warn(
219
+ f'You are using a param signature for Meter which is deprecated. {legacy_keys}. See reference map: {param_map}',
220
+ DeprecationWarning)
221
+ return legacy_params
222
+
223
+ @property
224
+ def value(self):
225
+ return self._value_var.get()
226
+
227
+ @value.setter
228
+ def value(self, value: int | float):
229
+ self._value_var.set(value)
230
+
231
+ @property
232
+ def subtitle(self):
233
+ return self._subtitle_var.get()
234
+
235
+ @subtitle.setter
236
+ def subtitle(self, value: str):
237
+ self._subtitle = value
238
+ self._subtitle_var.set(value)
239
+
240
+ # ------ Value API Methods ------
241
+
242
+ def get(self):
243
+ """Return the current meter value.
244
+
245
+ This is part of the standard value-widget API. It is equivalent
246
+ to accessing the `.value` property.
247
+
248
+ Returns:
249
+ The current meter value (int or float depending on dtype).
250
+ """
251
+ return self.value
252
+
253
+ def set(self, value):
254
+ """Set the meter value.
255
+
256
+ This is part of the standard value-widget API. It is equivalent
257
+ to setting the `.value` property.
258
+
259
+ Args:
260
+ value: The new meter value.
261
+ """
262
+ self.value = value
263
+
264
+ # ------ Configuration Delegates ------
265
+
266
+ @configure_delegate('accent')
267
+ def _delegate_accent(self, value=None):
268
+ if value is None:
269
+ return self._accent
270
+ else:
271
+ self._accent = value
272
+ self._resolve_meter_styles()
273
+ self._draw_meter()
274
+ return None
275
+
276
+ @configure_delegate('value')
277
+ def _delegate_value(self, value=None):
278
+ if value is None:
279
+ return self.value
280
+ else:
281
+ self.value = value
282
+ return None
283
+
284
+ @configure_delegate('minvalue')
285
+ def _delegate_minvalue(self, value=None):
286
+ if value is None:
287
+ return self._minvalue
288
+ else:
289
+ self._minvalue = value
290
+ self._draw_meter()
291
+ return None
292
+
293
+ @configure_delegate('maxvalue')
294
+ def _delegate_maxvalue(self, value=None):
295
+ if value is None:
296
+ return self._maxvalue
297
+ else:
298
+ self._maxvalue = value
299
+ self._draw_meter()
300
+ return None
301
+
302
+ @configure_delegate('value_format')
303
+ def _delegate_value_format(self, value=None):
304
+ if value is None:
305
+ return self._value_format
306
+ else:
307
+ self._value_format = value
308
+ self._draw_meter()
309
+ return None
310
+
311
+ @configure_delegate('value_prefix')
312
+ def _delegate_value_prefix(self, value=None):
313
+ if value is None:
314
+ return self._value_prefix
315
+ else:
316
+ self._value_prefix = value
317
+ self._draw_meter()
318
+ return None
319
+
320
+ @configure_delegate('value_suffix')
321
+ def _delegate_value_suffix(self, value=None):
322
+ if value is None:
323
+ return self._value_suffix
324
+ else:
325
+ self._value_suffix = value
326
+ self._draw_meter()
327
+ return None
328
+
329
+ @configure_delegate('value_font')
330
+ def _delegate_value_font(self, value=None):
331
+ if value is None:
332
+ return self._value_font
333
+ else:
334
+ self._value_font = value
335
+ self._draw_meter()
336
+ return None
337
+
338
+ @configure_delegate('dtype')
339
+ def _delegate_dtype(self, value=None):
340
+ if value is None:
341
+ return self._dtype
342
+ else:
343
+ warn('dtype is only configurable in the widget constructor', ConfigurationWarning)
344
+ return None
345
+
346
+ @configure_delegate('secondary_font')
347
+ def _delegate_secondary_font(self, value=None):
348
+ if value is None:
349
+ return self._secondary_font
350
+ else:
351
+ self._secondary_font = value
352
+ self._draw_meter()
353
+ return None
354
+
355
+ @configure_delegate('secondary_style')
356
+ def _delegate_secondary_style(self, value=None):
357
+ if value is None:
358
+ return self._secondary_style
359
+ else:
360
+ self._secondary_style = value
361
+ self._resolve_meter_styles()
362
+ self._draw_meter()
363
+ return None
364
+
365
+ @configure_delegate('subtitle')
366
+ def _delegate_subtitle(self, value=None):
367
+ if value is None:
368
+ return self.subtitle
369
+ else:
370
+ self.subtitle = value
371
+ self._draw_meter()
372
+ return None
373
+
374
+ @configure_delegate('size')
375
+ def _delegate_size(self, value=None):
376
+ if value is None:
377
+ return self._size
378
+ else:
379
+ self._size = value
380
+ self._canvas.configure(width=value, height=value)
381
+ self._draw_base_meter_images()
382
+ self._draw_meter()
383
+ return None
384
+
385
+ @configure_delegate('thickness')
386
+ def _delegate_thickness(self, value=None):
387
+ if value is None:
388
+ return self._thickness
389
+ else:
390
+ self._thickness = value
391
+ self._draw_base_meter_images()
392
+ self._draw_meter()
393
+ return None
394
+
395
+ @configure_delegate('indicator_width')
396
+ def _delegate_indicator_width(self, value=None):
397
+ if value is None:
398
+ return self._indicator_width
399
+ else:
400
+ self._indicator_width = value
401
+ self._draw_meter()
402
+ return None
403
+
404
+ @configure_delegate('segment_width')
405
+ def _delegate_segment_width(self, value=None):
406
+ if value is None:
407
+ return self._segment_width
408
+ else:
409
+ self._segment_width = value
410
+ self._draw_base_meter_images()
411
+ self._draw_meter()
412
+ return None
413
+
414
+ @configure_delegate('arc_range')
415
+ def _delegate_arc_range(self, value=None):
416
+ if value is None:
417
+ return self._arc_range
418
+ else:
419
+ self._arc_range = value
420
+ self._draw_base_meter_images()
421
+ self._draw_meter()
422
+ return None
423
+
424
+ @configure_delegate('arc_offset')
425
+ def _delegate_arc_offset(self, value=None):
426
+ if value is None:
427
+ return self._arc_offset
428
+ else:
429
+ self._arc_offset = value
430
+ self._draw_base_meter_images()
431
+ self._draw_meter()
432
+ return None
433
+
434
+ @configure_delegate('meter_type')
435
+ def _delegate_meter_type(self, value=None):
436
+ if value is None:
437
+ return self._meter_type
438
+ else:
439
+ self._resolve_arc_range_offset(value, self._arc_offset, self._arc_range)
440
+ self._draw_base_meter_images()
441
+ self._draw_meter()
442
+ return None
443
+
444
+ @configure_delegate('show_text')
445
+ def _delegate_show_text(self, value=None):
446
+ if value is None:
447
+ return self._show_text
448
+ else:
449
+ self._show_text = value
450
+ self._draw_meter()
451
+ return None
452
+
453
+ @configure_delegate('interactive')
454
+ def _delegate_interactive(self, value=None):
455
+ if value is None:
456
+ return self._interactive
457
+ else:
458
+ self._interactive = value
459
+ self._bind_interactive_events()
460
+ return None
461
+
462
+ @configure_delegate('step_size')
463
+ def _delegate_step_size(self, value=None):
464
+ if value is None:
465
+ return self._step_size
466
+ else:
467
+ self._step_size = value
468
+ return None
469
+
470
+ def _variable(self, value: int | float):
471
+ return IntVar(value=value) if self._dtype is int else DoubleVar(value=value)
472
+
473
+ def _update_meter(self, *_: Any):
474
+ """Update meter display when value changes."""
475
+ value = self._value_var.get()
476
+ self._value_display_var.set(self._value_format.format(value))
477
+ self._draw_meter()
478
+ if value != self._last_changed_value:
479
+ prev_value = self._last_changed_value
480
+ self._last_changed_value = value
481
+ self.event_generate('<<Change>>', data={"value": value, "prev_value": prev_value})
482
+
483
+ def _resolve_meter_styles(self):
484
+ """Resolve theme colors for meter indicator, trough, and text."""
485
+ from bootstack.style.style import get_style
486
+ style = get_style()
487
+ b = style.style_builder
488
+
489
+ # Resolve colors from tokens
490
+ accent_token = self._accent or 'primary'
491
+ accent_color = b.color(accent_token)
492
+
493
+ # Use _surface_token to get the token name, resolve to actual color
494
+ surface_token = getattr(self, '_surface_token', 'background')
495
+ surface = b.color(surface_token)
496
+ trough_color = b.border(surface)
497
+
498
+ # Get text colors
499
+ value_text_color = b.color(accent_token)
500
+ secondary_text_color = b.color(self._secondary_style or 'background[muted]')
501
+
502
+ self._image_scale = b.scale(DEFAULT_IMAGE_SCALE)
503
+ self._accent_color = accent_color
504
+ self._surface = surface # Store resolved color
505
+ self._trough_color = trough_color
506
+ self._value_text_color = value_text_color
507
+ self._secondary_text_color = secondary_text_color
508
+
509
+ def _bind_interactive_events(self):
510
+ seq1 = '<B1-Motion>'
511
+ seq2 = '<Button-1>'
512
+
513
+ if self._interactive:
514
+ self._binding[seq1] = self._canvas.bind(seq1, self._handle_interaction)
515
+ self._binding[seq2] = self._canvas.bind(seq2, self._handle_interaction)
516
+ return
517
+
518
+ if seq1 in self._binding:
519
+ self._canvas.unbind(seq1, self._binding[seq1])
520
+ self._canvas.unbind(seq2, self._binding[seq2])
521
+ self._binding.clear()
522
+
523
+ def _resolve_arc_range_offset(self, meter_type: str, arc_offset: int | None, arc_range: int | None):
524
+ """Set default arc parameters based on meter type (full or semi)."""
525
+ if meter_type == 'semi':
526
+ self._arc_offset = 135 if arc_offset is None else arc_offset
527
+ self._arc_range = 270 if arc_range is None else arc_range
528
+ else:
529
+ self._arc_offset = -90 if arc_offset is None else arc_offset
530
+ self._arc_range = 360 if arc_range is None else arc_range
531
+ self._meter_type = meter_type
532
+
533
+ def _draw_meter(self):
534
+ """Draw meter indicator and text on canvas."""
535
+ # Draw meter indicator
536
+ img = self._base_image.copy()
537
+ draw = ImageDraw.Draw(img)
538
+ if self._segment_width > 0:
539
+ self._draw_segment_indicator(draw)
540
+ else:
541
+ self._draw_solid_indicator(draw)
542
+
543
+ self._meter_image = ImageTk.PhotoImage(
544
+ img.resize(
545
+ (self._size, self._size),
546
+ Resampling.BILINEAR
547
+ )
548
+ )
549
+
550
+ # Update or create image on canvas
551
+ if self._meter_image_id is None:
552
+ self._meter_image_id = self._canvas.create_image(
553
+ 0, 0,
554
+ image=self._meter_image,
555
+ anchor='nw'
556
+ )
557
+ else:
558
+ self._canvas.itemconfig(self._meter_image_id, image=self._meter_image)
559
+
560
+ # Draw text if enabled
561
+ if self._show_text:
562
+ self._draw_text_on_canvas()
563
+ else:
564
+ # Hide text elements
565
+ self._hide_text_items()
566
+ # Show subtitle if it exists
567
+ if self._subtitle:
568
+ self._draw_subtitle_centered()
569
+
570
+ def _draw_text_on_canvas(self):
571
+ """Draw value text with optional prefix, suffix, and subtitle on canvas."""
572
+ value_text = self._value_display_var.get()
573
+ center_x = self._size / 2
574
+ center_y = self._size / 2
575
+
576
+ # Create font objects once
577
+ value_font = tkfont.Font(font=self._value_font)
578
+ secondary_font = tkfont.Font(font=self._secondary_font)
579
+
580
+ # Get font metrics
581
+ value_metrics = value_font.metrics()
582
+ secondary_metrics = secondary_font.metrics()
583
+
584
+ value_height = value_metrics['ascent'] + value_metrics['descent']
585
+ secondary_height = secondary_metrics['ascent'] + secondary_metrics['descent']
586
+
587
+ # Calculate max height of value line (value + prefix/suffix)
588
+ max_text_height = max(value_height, secondary_height) if (
589
+ self._value_prefix or self._value_suffix) else value_height
590
+
591
+ # Calculate total block height including subtitle
592
+ subtitle_height = secondary_height if self._subtitle else 0
593
+ subtitle_gap = -4 if self._subtitle else 0
594
+ total_height = max_text_height + subtitle_gap + subtitle_height
595
+
596
+ # Position value text to center the entire block
597
+ block_top = center_y - (total_height / 2)
598
+ value_y = block_top + (max_text_height / 2)
599
+
600
+ # Calculate value text baseline for prefix/suffix alignment
601
+ value_baseline_y = value_y + (value_metrics['ascent'] - value_metrics['descent']) / 2
602
+
603
+ # Draw value text
604
+ self._value_text_id = self._update_or_create_text(
605
+ self._value_text_id, center_x, value_y,
606
+ value_text, self._value_font, self._value_text_color, 'center'
607
+ )
608
+
609
+ # Position and draw prefix/suffix
610
+ if self._value_prefix or self._value_suffix:
611
+ value_width = value_font.measure(value_text)
612
+ horizontal_gap = 4
613
+ value_left_x = center_x - (value_width / 2) - horizontal_gap
614
+ value_right_x = center_x + (value_width / 2) + horizontal_gap - 1
615
+
616
+ # Calculate y position for prefix/suffix (baseline aligned, slightly raised)
617
+ secondary_y = value_baseline_y - (secondary_metrics['ascent'] - secondary_metrics['descent']) / 2 - 4
618
+
619
+ if self._value_prefix:
620
+ self._prefix_text_id = self._update_or_create_text(
621
+ self._prefix_text_id, value_left_x, secondary_y,
622
+ self._value_prefix, self._secondary_font, self._secondary_text_color, 'e'
623
+ )
624
+ elif self._prefix_text_id:
625
+ self._canvas.itemconfig(self._prefix_text_id, state='hidden')
626
+
627
+ if self._value_suffix:
628
+ self._suffix_text_id = self._update_or_create_text(
629
+ self._suffix_text_id, value_right_x, secondary_y,
630
+ self._value_suffix, self._secondary_font, self._secondary_text_color, 'w'
631
+ )
632
+ elif self._suffix_text_id:
633
+ self._canvas.itemconfig(self._suffix_text_id, state='hidden')
634
+
635
+ # Draw subtitle
636
+ if self._subtitle:
637
+ subtitle_y = value_y + (max_text_height / 2) - 4
638
+ self._subtitle_text_id = self._update_or_create_text(
639
+ self._subtitle_text_id, center_x, subtitle_y,
640
+ self._subtitle_var.get(), self._secondary_font, self._secondary_text_color, 'n'
641
+ )
642
+ elif self._subtitle_text_id:
643
+ self._canvas.itemconfig(self._subtitle_text_id, state='hidden')
644
+
645
+ def _update_or_create_text(self, item_id, x, y, text, font, fill, anchor):
646
+ """Update existing canvas text item or create new one.
647
+
648
+ Args:
649
+ item_id: Existing canvas item ID or None to create new.
650
+ x: X coordinate for text position.
651
+ y: Y coordinate for text position.
652
+ text: Text string to display.
653
+ font: Font specification.
654
+ fill: Text color.
655
+ anchor: Text anchor position (e.g., 'center', 'e', 'w').
656
+
657
+ Returns:
658
+ Canvas item ID.
659
+ """
660
+ if item_id is None:
661
+ return self._canvas.create_text(x, y, text=text, font=font, fill=fill, anchor=anchor)
662
+ else:
663
+ self._canvas.itemconfig(item_id, text=text, font=font, fill=fill, state='normal')
664
+ self._canvas.coords(item_id, x, y)
665
+ return item_id
666
+
667
+ def _hide_text_items(self):
668
+ """Hide all text items on canvas."""
669
+ if self._value_text_id:
670
+ self._canvas.itemconfig(self._value_text_id, state='hidden')
671
+ if self._prefix_text_id:
672
+ self._canvas.itemconfig(self._prefix_text_id, state='hidden')
673
+ if self._suffix_text_id:
674
+ self._canvas.itemconfig(self._suffix_text_id, state='hidden')
675
+ if self._subtitle_text_id:
676
+ self._canvas.itemconfig(self._subtitle_text_id, state='hidden')
677
+
678
+ def _draw_subtitle_centered(self):
679
+ """Draw subtitle centered when show_text is False."""
680
+ center_x = self._size // 2
681
+ center_y = self._size // 2
682
+
683
+ if self._subtitle_text_id is None:
684
+ self._subtitle_text_id = self._canvas.create_text(
685
+ center_x, center_y,
686
+ text=self._subtitle_var.get(),
687
+ font=self._secondary_font,
688
+ fill=self._secondary_text_color,
689
+ anchor='center'
690
+ )
691
+ else:
692
+ self._canvas.itemconfig(
693
+ self._subtitle_text_id,
694
+ text=self._subtitle_var.get(),
695
+ font=self._secondary_font,
696
+ fill=self._secondary_text_color,
697
+ state='normal'
698
+ )
699
+ self._canvas.coords(self._subtitle_text_id, center_x, center_y)
700
+
701
+ def _draw_base_meter_images(self):
702
+ """Draw meter background trough at high resolution."""
703
+ self._resolve_meter_styles()
704
+ self._base_image = Image.new(
705
+ mode='RGBA',
706
+ size=(self._size * self._image_scale, self._size * self._image_scale),
707
+ )
708
+ draw = ImageDraw.Draw(self._base_image)
709
+
710
+ # Center the arc with equal margins on all sides
711
+ margin = 10
712
+ x1 = y1 = self._size * self._image_scale - margin
713
+ width = self._thickness * self._image_scale
714
+
715
+ if self._segment_width > 0:
716
+ # segmented meter
717
+ minvalue = self._arc_offset
718
+ maxvalue = self._arc_range + self._arc_offset
719
+ step = 2 if self._segment_width == 1 else self._segment_width
720
+
721
+ for x in range(minvalue, maxvalue, step):
722
+ draw.arc(
723
+ xy=(margin, margin, x1, y1),
724
+ start=x,
725
+ end=x + self._segment_width - 1,
726
+ fill=self._trough_color,
727
+ width=width
728
+ )
729
+ else:
730
+ # default meter
731
+ draw.arc(
732
+ xy=(margin, margin, x1, y1),
733
+ start=self._arc_offset,
734
+ end=self._arc_range + self._arc_offset,
735
+ fill=self._trough_color,
736
+ width=width
737
+ )
738
+
739
+ def _draw_solid_indicator(self, draw):
740
+ """Draw solid arc indicator from start to current value."""
741
+ margin = 10
742
+ x1 = y1 = self._size * self._image_scale - margin
743
+ width = self._thickness * self._image_scale
744
+ value_degrees = self._meter_value_as_degrees()
745
+
746
+ if self._indicator_width > 0:
747
+ draw.arc(
748
+ xy=(margin, margin, x1, y1),
749
+ start=value_degrees - self._indicator_width,
750
+ end=value_degrees + self._indicator_width,
751
+ fill=self._accent_color,
752
+ width=width
753
+ )
754
+ else:
755
+ draw.arc(
756
+ xy=(margin, margin, x1, y1),
757
+ start=self._arc_offset,
758
+ end=value_degrees,
759
+ fill=self._accent_color,
760
+ width=width
761
+ )
762
+
763
+ def _draw_segment_indicator(self, draw):
764
+ """Draw segmented arc indicator from start to current value."""
765
+ value_degrees = self._meter_value_as_degrees()
766
+ margin = 10
767
+ x1 = y1 = self._size * self._image_scale - margin
768
+ width = self._thickness * self._image_scale
769
+
770
+ if self._indicator_width > 0:
771
+ draw.arc(
772
+ xy=(margin, margin, x1, y1),
773
+ start=value_degrees - self._indicator_width,
774
+ end=value_degrees + self._indicator_width,
775
+ fill=self._accent_color,
776
+ width=width
777
+ )
778
+ else:
779
+ # Draw segments from arc start to current value in degrees
780
+ minvalue = self._arc_offset
781
+ maxvalue = value_degrees - 1
782
+ step = self._segment_width
783
+
784
+ for x in range(minvalue, maxvalue, step):
785
+ draw.arc(
786
+ xy=(margin, margin, x1, y1),
787
+ start=x,
788
+ end=x + self._segment_width - 1,
789
+ fill=self._accent_color,
790
+ width=width
791
+ )
792
+
793
+ def _meter_value_as_degrees(self):
794
+ """Convert current meter value to arc degrees.
795
+
796
+ Returns:
797
+ Degree value for meter indicator position.
798
+ """
799
+ minvalue = self._minvalue
800
+ maxvalue = self._maxvalue
801
+ value = self._value_var.get()
802
+
803
+ # normalize to 0-1 range to handle negative values
804
+ range_size = maxvalue - minvalue
805
+ if range_size == 0:
806
+ normalized = 0
807
+ else:
808
+ normalized = (value - minvalue) / range_size
809
+
810
+ return int(normalized * self._arc_range + self._arc_offset)
811
+
812
+ def _handle_theme_changed(self, *_):
813
+ self._resolve_meter_styles()
814
+ self._canvas.configure(background=self._surface)
815
+ self._draw_base_meter_images()
816
+ self._draw_meter()
817
+
818
+ def _handle_interaction(self, e):
819
+ """Handle mouse clicks/drags to update meter value in interactive mode."""
820
+ dx = e.x - self._size // 2
821
+ dy = e.y - self._size // 2
822
+ rads = math.atan2(dy, dx)
823
+ degrees = math.degrees(rads)
824
+
825
+ if degrees > self._arc_offset:
826
+ factor = degrees - self._arc_offset
827
+ else:
828
+ factor = 360 + degrees - self._arc_offset
829
+
830
+ # clamp the value between `minvalue` and `maxvalue`
831
+ minvalue = self._minvalue
832
+ maxvalue = self._maxvalue
833
+ last_value = self._value_var.get()
834
+
835
+ # calculate the value based on the range
836
+ range_size = maxvalue - minvalue
837
+ value = (range_size / self._arc_range * factor) + minvalue
838
+
839
+ # calculate the value given the stepsize.
840
+ if self._step_size > 0:
841
+ # round to the nearest stepsize
842
+ value = round(value / self._step_size) * self._step_size
843
+
844
+ # if the number is the same, then do not redraw
845
+ if last_value == value:
846
+ return
847
+
848
+ # update the value variable
849
+ self._value_var.set(max(min(value, maxvalue), minvalue))
850
+
851
+ def step(self, delta: int | float = 1):
852
+ """Increment or decrement meter value with automatic bounce at limits.
853
+
854
+ Args:
855
+ delta: Amount to step by (default 1).
856
+ """
857
+ value = self._value_var.get()
858
+ minvalue = self._minvalue
859
+ maxvalue = self._maxvalue
860
+
861
+ if self._towards_maximum:
862
+ value_updated = value + delta
863
+ else:
864
+ value_updated = value - delta
865
+
866
+ if value_updated >= maxvalue:
867
+ self._towards_maximum = False
868
+ self._value_var.set(maxvalue - (value_updated - maxvalue))
869
+ elif value_updated < minvalue:
870
+ self._towards_maximum = True
871
+ self._value_var.set(minvalue + (minvalue - value_updated))
872
+ else:
873
+ self._value_var.set(value_updated)
874
+
875
+ def on_changed(self, callback: Callable[[Any], Any]) -> str:
876
+ """Bind a callback to the `<<Change>>` virtual event."""
877
+ return self.bind('<<Change>>', callback, add="+")
878
+
879
+ def off_changed(self, bind_id: str):
880
+ """Remove a previously registered `<<Change>>` callback."""
881
+ self.unbind('<<Change>>', bind_id)
882
+