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,1043 @@
1
+ """Window positioning and sizing utilities for bootstack.
2
+
3
+ This module provides centralized window management utilities used across
4
+ Window (App), Toplevel, and Dialog classes. These utilities handle:
5
+ - Window positioning (screen-centered, parent-centered, custom coords)
6
+ - Screen bounds checking
7
+ - Multi-monitor support
8
+ - Platform-aware positioning
9
+
10
+ The utilities can be used standalone or as part of mixins/base classes.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import tkinter
16
+ from typing import Literal, Optional, Tuple, Union
17
+
18
+ try:
19
+ from screeninfo import get_monitors
20
+ HAS_SCREENINFO = True
21
+ except ImportError:
22
+ HAS_SCREENINFO = False
23
+
24
+ # Type definitions for anchor points (using tkinter convention)
25
+ AnchorPoint = Literal['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw', 'center']
26
+ AutoFlip = Union[bool, Literal['vertical', 'horizontal']]
27
+
28
+
29
+ class WindowPositioning:
30
+ """Centralized window positioning utilities.
31
+
32
+ Provides static methods for calculating and applying window positions
33
+ relative to screen, parent windows, or explicit coordinates. All methods
34
+ handle edge cases like multi-monitor setups and ensure windows remain
35
+ fully visible on screen.
36
+
37
+ This class can be used as:
38
+ - A standalone utility: WindowPositioning.center_on_screen(window)
39
+ - A mixin: class MyWindow(WindowPositioning, tkinter.Tk)
40
+
41
+ Examples:
42
+ >>> # Center window on screen
43
+ >>> x, y = WindowPositioning.center_on_screen(window)
44
+ >>> window.geometry(f"+{x}+{y}")
45
+ >>>
46
+ >>> # Center dialog on parent
47
+ >>> x, y = WindowPositioning.center_on_parent(dialog, parent)
48
+ >>> dialog.geometry(f"+{x}+{y}")
49
+ >>>
50
+ >>> # Ensure coordinates are on screen
51
+ >>> x, y = WindowPositioning.ensure_on_screen(window, 2000, 2000)
52
+ >>> # Returns adjusted coordinates within screen bounds
53
+ """
54
+
55
+ @staticmethod
56
+ def _get_monitor_at_point(x: int, y: int) -> Optional[Tuple[int, int, int, int]]:
57
+ """Find the monitor containing the given point.
58
+
59
+ Args:
60
+ x: X coordinate in screen space.
61
+ y: Y coordinate in screen space.
62
+
63
+ Returns:
64
+ Tuple of (monitor_x, monitor_y, monitor_width, monitor_height) if
65
+ screeninfo is available and a monitor contains the point.
66
+ Returns None if screeninfo is not installed or point is not on any monitor.
67
+ """
68
+ if not HAS_SCREENINFO:
69
+ return None
70
+
71
+ try:
72
+ monitors = get_monitors()
73
+ for monitor in monitors:
74
+ if (monitor.x <= x < monitor.x + monitor.width and
75
+ monitor.y <= y < monitor.y + monitor.height):
76
+ return (monitor.x, monitor.y, monitor.width, monitor.height)
77
+ # Point not on any monitor, return the first monitor as fallback
78
+ if monitors:
79
+ m = monitors[0]
80
+ return (m.x, m.y, m.width, m.height)
81
+ except Exception:
82
+ # If screeninfo fails for any reason, fall back to None
83
+ pass
84
+ return None
85
+
86
+ @staticmethod
87
+ def center_on_screen(window: tkinter.Misc) -> tuple[int, int]:
88
+ """Calculate coordinates to center window on screen.
89
+
90
+ Centers the window on the primary display. For multi-monitor setups,
91
+ this typically centers on the monitor containing the mouse pointer.
92
+
93
+ Args:
94
+ window: The window to center. Must be a tkinter widget with
95
+ geometry info available (call update_idletasks() first).
96
+
97
+ Returns:
98
+ Tuple of (x, y) coordinates representing the top-left position
99
+ that will center the window on screen.
100
+
101
+ Note:
102
+ The window must have been geometry-managed before calling this
103
+ method. Call window.update_idletasks() first to ensure accurate
104
+ dimensions are available.
105
+
106
+ Examples:
107
+ >>> window = tkinter.Tk()
108
+ >>> window.update_idletasks()
109
+ >>> x, y = WindowPositioning.center_on_screen(window)
110
+ >>> window.geometry(f"+{x}+{y}")
111
+ """
112
+ window.update_idletasks()
113
+
114
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
115
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
116
+
117
+ # Try to center on the monitor containing the mouse cursor
118
+ cursor_x = window.winfo_pointerx()
119
+ cursor_y = window.winfo_pointery()
120
+ monitor = WindowPositioning._get_monitor_at_point(cursor_x, cursor_y)
121
+
122
+ if monitor:
123
+ # Center on the specific monitor
124
+ mon_x, mon_y, mon_width, mon_height = monitor
125
+ x = mon_x + (mon_width - w_width) // 2
126
+ y = mon_y + (mon_height - w_height) // 2
127
+ else:
128
+ # Fall back to total screen dimensions (original behavior)
129
+ s_width = window.winfo_screenwidth()
130
+ s_height = window.winfo_screenheight()
131
+ x = (s_width - w_width) // 2
132
+ y = (s_height - w_height) // 2
133
+
134
+ return x, y
135
+
136
+ @staticmethod
137
+ def center_on_parent(window: tkinter.Toplevel, parent: tkinter.Misc) -> tuple[int, int]:
138
+ """Calculate coordinates to center window on parent widget/window.
139
+
140
+ Centers the window relative to its parent window or widget. This is
141
+ commonly used for dialogs to appear centered on their parent window.
142
+
143
+ Args:
144
+ window: The window to center (typically a Toplevel or Dialog).
145
+ parent: The parent window or widget to center on.
146
+
147
+ Returns:
148
+ Tuple of (x, y) screen coordinates that will center the window
149
+ on the parent.
150
+
151
+ Note:
152
+ Both window and parent must have geometry information available.
153
+ The returned coordinates are in screen coordinates, not relative
154
+ to the parent.
155
+
156
+ Examples:
157
+ import bootstack.runtime.toplevel >>> parent = tkinter.Tk()
158
+ >>> dialog = bootstack.runtime.toplevel.Toplevel(parent)
159
+ >>> dialog.update_idletasks()
160
+ >>> parent.update_idletasks()
161
+ >>> x, y = WindowPositioning.center_on_parent(dialog, parent)
162
+ >>> dialog.geometry(f"+{x}+{y}")
163
+ """
164
+ window.update_idletasks()
165
+ parent.update_idletasks()
166
+
167
+ # Use requested size or actual size, whichever is larger
168
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
169
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
170
+
171
+ # Get parent's screen position and size
172
+ p_x = parent.winfo_rootx()
173
+ p_y = parent.winfo_rooty()
174
+ p_width = max(parent.winfo_width(), parent.winfo_reqwidth())
175
+ p_height = max(parent.winfo_height(), parent.winfo_reqheight())
176
+
177
+ # Calculate centered position
178
+ x = p_x + max(0, (p_width - w_width) // 2)
179
+ y = p_y + max(0, (p_height - w_height) // 2)
180
+ return x, y
181
+
182
+ @staticmethod
183
+ def ensure_on_screen(
184
+ window: tkinter.Misc,
185
+ x: int,
186
+ y: int,
187
+ padding: int = 20,
188
+ titlebar_height: int = 60
189
+ ) -> tuple[int, int]:
190
+ """Adjust coordinates to keep window fully visible on screen.
191
+
192
+ Ensures that a window positioned at (x, y) will be fully visible on
193
+ screen. If the coordinates would place any part of the window off-screen,
194
+ they are adjusted to keep the window within screen bounds with padding.
195
+
196
+ This method supports multi-monitor setups by using virtual root
197
+ coordinates, ensuring the window appears on the correct display.
198
+
199
+ Args:
200
+ window: The window to position. Must have geometry info available.
201
+ x: Desired x coordinate (screen coordinates).
202
+ y: Desired y coordinate (screen coordinates).
203
+ padding: Minimum padding from screen edges in pixels. Default is 20.
204
+ titlebar_height: Additional padding for titlebar at top. Default is 60.
205
+
206
+ Returns:
207
+ Tuple of (x, y) coordinates adjusted to keep window on screen.
208
+
209
+ Note:
210
+ The titlebar_height accounts for window manager decorations which
211
+ aren't included in winfo_height(). This prevents the titlebar from
212
+ being positioned off-screen.
213
+
214
+ Examples:
215
+ >>> window = tkinter.Tk()
216
+ >>> window.update_idletasks()
217
+ >>> # Try to position far off screen
218
+ >>> x, y = WindowPositioning.ensure_on_screen(window, 5000, 5000)
219
+ >>> # Returns coordinates that keep window visible
220
+ >>> window.geometry(f"+{x}+{y}")
221
+ """
222
+ window.update_idletasks()
223
+
224
+ w_width = window.winfo_reqwidth()
225
+ w_height = window.winfo_reqheight()
226
+
227
+ # Use virtual root for multi-monitor support
228
+ screen_x0 = window.winfo_vrootx()
229
+ screen_y0 = window.winfo_vrooty()
230
+ screen_width = window.winfo_vrootwidth()
231
+ screen_height = window.winfo_vrootheight()
232
+
233
+ # Calculate screen boundaries
234
+ screen_x1 = screen_x0 + screen_width
235
+ screen_y1 = screen_y0 + screen_height
236
+
237
+ # Constrain to screen bounds with padding
238
+ x = max(screen_x0 + padding, min(x, screen_x1 - w_width - padding))
239
+ y = max(screen_y0 + padding, min(y, screen_y1 - w_height - titlebar_height))
240
+
241
+ return int(x), int(y)
242
+
243
+ @staticmethod
244
+ def position_window(
245
+ window: tkinter.Misc,
246
+ position: Optional[tuple[int, int]] = None,
247
+ parent: Optional[tkinter.Misc] = None,
248
+ center_on_parent: bool = True,
249
+ ensure_visible: bool = True
250
+ ) -> None:
251
+ """Smart window positioning with multiple strategies.
252
+
253
+ Provides a high-level interface for positioning windows using the
254
+ most common strategies:
255
+ - Explicit coordinates (if position is provided)
256
+ - Centered on parent (if parent is provided and center_on_parent=True)
257
+ - Centered on screen (fallback)
258
+
259
+ Optionally ensures the window remains fully visible on screen.
260
+
261
+ Args:
262
+ window: The window to position.
263
+ position: Optional (x, y) coordinates in screen space. If provided,
264
+ positions window at these coordinates.
265
+ parent: Optional parent window. If provided and center_on_parent=True,
266
+ centers window on this parent.
267
+ center_on_parent: Whether to center on parent when parent is provided.
268
+ Ignored if position is explicitly provided.
269
+ ensure_visible: Whether to adjust coordinates to keep window on screen.
270
+ Default is True.
271
+
272
+ Note:
273
+ This method calls window.update_idletasks() internally and applies
274
+ the geometry immediately.
275
+
276
+ Examples:
277
+ >>> # Position at specific coordinates
278
+ >>> WindowPositioning.position_window(window, position=(100, 100))
279
+ >>>
280
+ >>> # Center on parent
281
+ >>> WindowPositioning.position_window(dialog, parent=parent_window)
282
+ >>>
283
+ >>> # Center on screen
284
+ >>> WindowPositioning.position_window(window)
285
+ """
286
+ window.update_idletasks()
287
+
288
+ if position is not None:
289
+ # Explicit coordinates provided
290
+ x, y = position
291
+ if ensure_visible:
292
+ x, y = WindowPositioning.ensure_on_screen(window, int(x), int(y))
293
+ window.geometry(f"+{x}+{y}")
294
+
295
+ elif parent is not None and center_on_parent:
296
+ # Center on parent
297
+ x, y = WindowPositioning.center_on_parent(window, parent)
298
+ if ensure_visible:
299
+ x, y = WindowPositioning.ensure_on_screen(window, x, y)
300
+ window.geometry(f"+{x}+{y}")
301
+
302
+ else:
303
+ # Fallback: center on screen
304
+ x, y = WindowPositioning.center_on_screen(window)
305
+ if ensure_visible:
306
+ x, y = WindowPositioning.ensure_on_screen(window, x, y)
307
+ window.geometry(f"+{x}+{y}")
308
+
309
+ @staticmethod
310
+ def _get_anchor_coordinates(
311
+ widget: tkinter.Misc,
312
+ anchor: AnchorPoint = 'nw',
313
+ use_requested_size: bool = True
314
+ ) -> Tuple[int, int]:
315
+ """Calculate screen coordinates for an anchor point on a widget.
316
+
317
+ Uses tkinter's standard anchor naming convention:
318
+ - 'n', 's', 'e', 'w' for cardinal directions (north, south, east, west)
319
+ - 'ne', 'nw', 'se', 'sw' for corners
320
+ - 'center' for the center point
321
+
322
+ Args:
323
+ widget: Widget to get anchor coordinates for.
324
+ anchor: Which point on the widget to return coordinates for.
325
+ use_requested_size: If True, uses requested size; otherwise actual size.
326
+
327
+ Returns:
328
+ Tuple of (x, y) screen coordinates for the anchor point.
329
+ """
330
+ widget.update_idletasks()
331
+
332
+ # Get widget position
333
+ x = widget.winfo_rootx()
334
+ y = widget.winfo_rooty()
335
+
336
+ # Get widget dimensions
337
+ if use_requested_size:
338
+ width = widget.winfo_reqwidth()
339
+ height = widget.winfo_reqheight()
340
+ else:
341
+ width = widget.winfo_width()
342
+ height = widget.winfo_height()
343
+
344
+ # Calculate anchor position using tkinter convention
345
+ if anchor == 'nw':
346
+ return x, y
347
+ elif anchor == 'n':
348
+ return x + width // 2, y
349
+ elif anchor == 'ne':
350
+ return x + width, y
351
+ elif anchor == 'w':
352
+ return x, y + height // 2
353
+ elif anchor == 'center':
354
+ return x + width // 2, y + height // 2
355
+ elif anchor == 'e':
356
+ return x + width, y + height // 2
357
+ elif anchor == 'sw':
358
+ return x, y + height
359
+ elif anchor == 's':
360
+ return x + width // 2, y + height
361
+ elif anchor == 'se':
362
+ return x + width, y + height
363
+ else:
364
+ return x, y
365
+
366
+ @staticmethod
367
+ def _get_screen_anchor_coordinates(
368
+ window: tkinter.Misc,
369
+ anchor: AnchorPoint = 'center'
370
+ ) -> Tuple[int, int]:
371
+ """Calculate screen coordinates for an anchor point on the screen.
372
+
373
+ For multi-monitor setups, this returns coordinates on the monitor
374
+ containing the mouse cursor.
375
+
376
+ Args:
377
+ window: Window (used to get screen dimensions).
378
+ anchor: Which point on the screen to return coordinates for.
379
+
380
+ Returns:
381
+ Tuple of (x, y) screen coordinates for the anchor point.
382
+ """
383
+ window.update_idletasks()
384
+
385
+ # Try to use the monitor containing the mouse cursor
386
+ cursor_x = window.winfo_pointerx()
387
+ cursor_y = window.winfo_pointery()
388
+ monitor = WindowPositioning._get_monitor_at_point(cursor_x, cursor_y)
389
+
390
+ if monitor:
391
+ screen_x, screen_y, screen_width, screen_height = monitor
392
+ else:
393
+ # Fall back to total screen dimensions
394
+ screen_x, screen_y = 0, 0
395
+ screen_width = window.winfo_screenwidth()
396
+ screen_height = window.winfo_screenheight()
397
+
398
+ # Calculate anchor position on screen/monitor
399
+ if anchor == 'nw':
400
+ return screen_x, screen_y
401
+ elif anchor == 'n':
402
+ return screen_x + screen_width // 2, screen_y
403
+ elif anchor == 'ne':
404
+ return screen_x + screen_width, screen_y
405
+ elif anchor == 'w':
406
+ return screen_x, screen_y + screen_height // 2
407
+ elif anchor == 'center':
408
+ return screen_x + screen_width // 2, screen_y + screen_height // 2
409
+ elif anchor == 'e':
410
+ return screen_x + screen_width, screen_y + screen_height // 2
411
+ elif anchor == 'sw':
412
+ return screen_x, screen_y + screen_height
413
+ elif anchor == 's':
414
+ return screen_x + screen_width // 2, screen_y + screen_height
415
+ elif anchor == 'se':
416
+ return screen_x + screen_width, screen_y + screen_height
417
+ else:
418
+ return screen_x + screen_width // 2, screen_y + screen_height // 2
419
+
420
+ @staticmethod
421
+ def _get_cursor_anchor_coordinates(
422
+ window: tkinter.Misc,
423
+ anchor: AnchorPoint = 'nw'
424
+ ) -> Tuple[int, int]:
425
+ """Calculate screen coordinates for an anchor point relative to cursor.
426
+
427
+ The cursor is treated as a point (no width/height), so all anchor points
428
+ return the same cursor position. The anchor parameter is kept for API
429
+ consistency but doesn't affect the result.
430
+
431
+ Args:
432
+ window: Window (used to get cursor position).
433
+ anchor: Anchor point (ignored, cursor is a point).
434
+
435
+ Returns:
436
+ Tuple of (x, y) screen coordinates of the cursor.
437
+ """
438
+ window.update_idletasks()
439
+
440
+ # Cursor is a point, so all anchors return cursor position
441
+ x = window.winfo_pointerx()
442
+ y = window.winfo_pointery()
443
+
444
+ return x, y
445
+
446
+ @staticmethod
447
+ def _flip_anchor_vertical(anchor: AnchorPoint) -> AnchorPoint:
448
+ """Flip an anchor point vertically (north ↔ south).
449
+
450
+ Args:
451
+ anchor: Anchor point to flip.
452
+
453
+ Returns:
454
+ Vertically flipped anchor point.
455
+ """
456
+ flip_map = {
457
+ 'n': 's', 's': 'n',
458
+ 'ne': 'se', 'se': 'ne',
459
+ 'nw': 'sw', 'sw': 'nw',
460
+ 'e': 'e', 'w': 'w',
461
+ 'center': 'center'
462
+ }
463
+ return flip_map.get(anchor, anchor)
464
+
465
+ @staticmethod
466
+ def _flip_anchor_horizontal(anchor: AnchorPoint) -> AnchorPoint:
467
+ """Flip an anchor point horizontally (east ↔ west).
468
+
469
+ Args:
470
+ anchor: Anchor point to flip.
471
+
472
+ Returns:
473
+ Horizontally flipped anchor point.
474
+ """
475
+ flip_map = {
476
+ 'e': 'w', 'w': 'e',
477
+ 'ne': 'nw', 'nw': 'ne',
478
+ 'se': 'sw', 'sw': 'se',
479
+ 'n': 'n', 's': 's',
480
+ 'center': 'center'
481
+ }
482
+ return flip_map.get(anchor, anchor)
483
+
484
+ @staticmethod
485
+ def _check_offscreen(
486
+ window: tkinter.Misc,
487
+ x: int,
488
+ y: int,
489
+ padding: int = 20
490
+ ) -> Tuple[bool, bool]:
491
+ """Check if a window positioned at (x, y) would be off-screen.
492
+
493
+ For multi-monitor setups, checks against the monitor containing the
494
+ proposed position.
495
+
496
+ Args:
497
+ window: Window to check.
498
+ x: Proposed x coordinate.
499
+ y: Proposed y coordinate.
500
+ padding: Minimum padding from screen edges.
501
+
502
+ Returns:
503
+ Tuple of (vertical_offscreen, horizontal_offscreen) booleans.
504
+ """
505
+ window.update_idletasks()
506
+
507
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
508
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
509
+
510
+ # Try to get the monitor at the proposed position
511
+ monitor = WindowPositioning._get_monitor_at_point(x, y)
512
+
513
+ if monitor:
514
+ screen_x, screen_y, screen_width, screen_height = monitor
515
+ else:
516
+ # Fall back to total screen dimensions
517
+ screen_x, screen_y = 0, 0
518
+ screen_width = window.winfo_screenwidth()
519
+ screen_height = window.winfo_screenheight()
520
+
521
+ # Check vertical (top/bottom off-screen relative to monitor)
522
+ vertical_offscreen = (
523
+ y < screen_y + padding or # Too far up
524
+ y + w_height + padding > screen_y + screen_height # Too far down
525
+ )
526
+
527
+ # Check horizontal (left/right off-screen relative to monitor)
528
+ horizontal_offscreen = (
529
+ x < screen_x + padding or # Too far left
530
+ x + w_width + padding > screen_x + screen_width # Too far right
531
+ )
532
+
533
+ return vertical_offscreen, horizontal_offscreen
534
+
535
+ @staticmethod
536
+ def position_anchored(
537
+ window: tkinter.Misc,
538
+ anchor_to: Union[tkinter.Misc, Literal["screen", "cursor", "parent"]],
539
+ parent: Optional[tkinter.Misc] = None,
540
+ anchor_point: AnchorPoint = 'center',
541
+ window_point: AnchorPoint = 'center',
542
+ offset: Tuple[int, int] = (0, 0),
543
+ auto_flip: AutoFlip = False,
544
+ ensure_visible: bool = True
545
+ ) -> None:
546
+ """Position window using unified anchor-based positioning with auto-flip.
547
+
548
+ This is the new consolidated positioning method that handles:
549
+ - Widget anchoring
550
+ - Screen anchoring (with anchor points)
551
+ - Cursor anchoring
552
+ - Parent anchoring
553
+ - Auto-flip (vertical and/or horizontal)
554
+
555
+ Args:
556
+ window: The window to position.
557
+ anchor_to: Positioning target:
558
+ - Widget: Anchor to a specific widget
559
+ - "screen": Anchor to screen edges/corners
560
+ - "cursor": Anchor to mouse cursor
561
+ - "parent": Anchor to parent window
562
+ parent: Parent window (required if anchor_to="parent").
563
+ anchor_point: Point on the anchor target.
564
+ window_point: Point on the window.
565
+ offset: Additional (x, y) offset in pixels.
566
+ auto_flip: Smart flipping to keep window on screen:
567
+ - False: No flipping
568
+ - True: Flip both vertically and horizontally
569
+ - 'vertical': Only flip up/down
570
+ - 'horizontal': Only flip left/right
571
+ ensure_visible: Whether to adjust position to keep window on screen.
572
+
573
+ Examples:
574
+ >>> # Center on screen
575
+ >>> WindowPositioning.position_anchored(window, anchor_to="screen")
576
+ >>>
577
+ >>> # Top-right corner of screen
578
+ >>> WindowPositioning.position_anchored(
579
+ ... window, anchor_to="screen", anchor_point='ne', window_point='ne'
580
+ ... )
581
+ >>>
582
+ >>> # Dropdown with auto-flip
583
+ >>> WindowPositioning.position_anchored(
584
+ ... window, anchor_to=button,
585
+ ... anchor_point='sw', window_point='nw',
586
+ ... auto_flip='vertical'
587
+ ... )
588
+ """
589
+ window.update_idletasks()
590
+
591
+ # Get anchor coordinates based on anchor_to type
592
+ if anchor_to == "screen":
593
+ anchor_x, anchor_y = WindowPositioning._get_screen_anchor_coordinates(
594
+ window, anchor_point
595
+ )
596
+ elif anchor_to == "cursor":
597
+ anchor_x, anchor_y = WindowPositioning._get_cursor_anchor_coordinates(
598
+ window, anchor_point
599
+ )
600
+ elif anchor_to == "parent":
601
+ if parent is None:
602
+ raise ValueError("parent parameter required when anchor_to='parent'")
603
+ parent.update_idletasks()
604
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
605
+ parent, anchor_point
606
+ )
607
+ else:
608
+ # Assume it's a widget
609
+ anchor_to.update_idletasks()
610
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
611
+ anchor_to, anchor_point
612
+ )
613
+
614
+ # Calculate window position based on window_point
615
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
616
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
617
+
618
+ # Calculate offset based on window anchor point
619
+ x_offset, y_offset = 0, 0
620
+
621
+ if window_point == 'nw':
622
+ x_offset, y_offset = 0, 0
623
+ elif window_point == 'n':
624
+ x_offset, y_offset = -w_width // 2, 0
625
+ elif window_point == 'ne':
626
+ x_offset, y_offset = -w_width, 0
627
+ elif window_point == 'w':
628
+ x_offset, y_offset = 0, -w_height // 2
629
+ elif window_point == 'center':
630
+ x_offset, y_offset = -w_width // 2, -w_height // 2
631
+ elif window_point == 'e':
632
+ x_offset, y_offset = -w_width, -w_height // 2
633
+ elif window_point == 'sw':
634
+ x_offset, y_offset = 0, -w_height
635
+ elif window_point == 's':
636
+ x_offset, y_offset = -w_width // 2, -w_height
637
+ elif window_point == 'se':
638
+ x_offset, y_offset = -w_width, -w_height
639
+
640
+ # Calculate initial position
641
+ x = int(anchor_x + x_offset + offset[0])
642
+ y = int(anchor_y + y_offset + offset[1])
643
+
644
+ # Auto-flip logic
645
+ if auto_flip:
646
+ vertical_offscreen, horizontal_offscreen = WindowPositioning._check_offscreen(
647
+ window, x, y
648
+ )
649
+
650
+ should_flip_vertical = False
651
+ should_flip_horizontal = False
652
+
653
+ if auto_flip is True or auto_flip == 'vertical':
654
+ should_flip_vertical = vertical_offscreen
655
+
656
+ if auto_flip is True or auto_flip == 'horizontal':
657
+ should_flip_horizontal = horizontal_offscreen
658
+
659
+ # Flip if needed
660
+ if should_flip_vertical or should_flip_horizontal:
661
+ flipped_anchor_point = anchor_point
662
+ flipped_window_point = window_point
663
+
664
+ if should_flip_vertical:
665
+ flipped_anchor_point = WindowPositioning._flip_anchor_vertical(
666
+ flipped_anchor_point
667
+ )
668
+ flipped_window_point = WindowPositioning._flip_anchor_vertical(
669
+ flipped_window_point
670
+ )
671
+
672
+ if should_flip_horizontal:
673
+ flipped_anchor_point = WindowPositioning._flip_anchor_horizontal(
674
+ flipped_anchor_point
675
+ )
676
+ flipped_window_point = WindowPositioning._flip_anchor_horizontal(
677
+ flipped_window_point
678
+ )
679
+
680
+ # Recalculate with flipped anchors
681
+ if anchor_to == "screen":
682
+ anchor_x, anchor_y = WindowPositioning._get_screen_anchor_coordinates(
683
+ window, flipped_anchor_point
684
+ )
685
+ elif anchor_to == "cursor":
686
+ anchor_x, anchor_y = WindowPositioning._get_cursor_anchor_coordinates(
687
+ window, flipped_anchor_point
688
+ )
689
+ elif anchor_to == "parent":
690
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
691
+ parent, flipped_anchor_point
692
+ )
693
+ else:
694
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
695
+ anchor_to, flipped_anchor_point
696
+ )
697
+
698
+ # Recalculate offset for flipped window_point
699
+ if flipped_window_point == 'nw':
700
+ x_offset, y_offset = 0, 0
701
+ elif flipped_window_point == 'n':
702
+ x_offset, y_offset = -w_width // 2, 0
703
+ elif flipped_window_point == 'ne':
704
+ x_offset, y_offset = -w_width, 0
705
+ elif flipped_window_point == 'w':
706
+ x_offset, y_offset = 0, -w_height // 2
707
+ elif flipped_window_point == 'center':
708
+ x_offset, y_offset = -w_width // 2, -w_height // 2
709
+ elif flipped_window_point == 'e':
710
+ x_offset, y_offset = -w_width, -w_height // 2
711
+ elif flipped_window_point == 'sw':
712
+ x_offset, y_offset = 0, -w_height
713
+ elif flipped_window_point == 's':
714
+ x_offset, y_offset = -w_width // 2, -w_height
715
+ elif flipped_window_point == 'se':
716
+ x_offset, y_offset = -w_width, -w_height
717
+
718
+ x = int(anchor_x + x_offset + offset[0])
719
+ y = int(anchor_y + y_offset + offset[1])
720
+
721
+ # Final ensure visible check
722
+ if ensure_visible:
723
+ x, y = WindowPositioning.ensure_on_screen(window, x, y)
724
+
725
+ window.geometry(f"+{x}+{y}")
726
+
727
+ @staticmethod
728
+ def position_with_anchor(
729
+ window: tkinter.Misc,
730
+ anchor_to: tkinter.Misc,
731
+ anchor_point: AnchorPoint = 'sw',
732
+ window_point: AnchorPoint = 'nw',
733
+ offset: Tuple[int, int] = (0, 0),
734
+ ensure_visible: bool = True
735
+ ) -> None:
736
+ """Position window relative to another widget using anchor points.
737
+
738
+ This method positions a window by aligning specific points on both
739
+ the window and the anchor widget, with optional offset. This is useful
740
+ for dropdowns, tooltips, context menus, and popovers.
741
+
742
+ Uses tkinter's standard anchor naming:
743
+ - 'n' (north/top), 's' (south/bottom), 'e' (east/right), 'w' (west/left)
744
+ - 'ne', 'nw', 'se', 'sw' for corners
745
+ - 'center' for center point
746
+
747
+ Args:
748
+ window: The window to position.
749
+ anchor_to: The widget to anchor the window to.
750
+ anchor_point: Which point on the anchor widget to use as reference.
751
+ Default 'sw' (bottom-left) is common for dropdowns.
752
+ window_point: Which point on the window to align with the anchor point.
753
+ Default 'nw' (top-left) aligns window's top-left to anchor point.
754
+ offset: Additional (x, y) offset in pixels.
755
+ ensure_visible: Whether to adjust position to keep window on screen.
756
+
757
+ Examples:
758
+ >>> # Show dropdown below button (button's bottom-left -> window's top-left)
759
+ >>> WindowPositioning.position_with_anchor(
760
+ ... window=dropdown,
761
+ ... anchor_to=button,
762
+ ... anchor_point='sw', # button's bottom-left
763
+ ... window_point='nw', # window's top-left
764
+ ... offset=(0, 2)
765
+ ... )
766
+ >>>
767
+ >>> # Show tooltip above widget (widget's top -> tooltip's bottom)
768
+ >>> WindowPositioning.position_with_anchor(
769
+ ... window=tooltip,
770
+ ... anchor_to=widget,
771
+ ... anchor_point='n', # widget's top-center
772
+ ... window_point='s', # tooltip's bottom-center
773
+ ... offset=(0, -5)
774
+ ... )
775
+ """
776
+ window.update_idletasks()
777
+ anchor_to.update_idletasks()
778
+
779
+ # Get anchor point on the reference widget
780
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
781
+ anchor_to, anchor_point
782
+ )
783
+
784
+ # Get window dimensions
785
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
786
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
787
+
788
+ # Calculate offset based on window anchor point
789
+ x_offset, y_offset = 0, 0
790
+
791
+ if window_point == 'nw':
792
+ x_offset, y_offset = 0, 0
793
+ elif window_point == 'n':
794
+ x_offset, y_offset = -w_width // 2, 0
795
+ elif window_point == 'ne':
796
+ x_offset, y_offset = -w_width, 0
797
+ elif window_point == 'w':
798
+ x_offset, y_offset = 0, -w_height // 2
799
+ elif window_point == 'center':
800
+ x_offset, y_offset = -w_width // 2, -w_height // 2
801
+ elif window_point == 'e':
802
+ x_offset, y_offset = -w_width, -w_height // 2
803
+ elif window_point == 'sw':
804
+ x_offset, y_offset = 0, -w_height
805
+ elif window_point == 's':
806
+ x_offset, y_offset = -w_width // 2, -w_height
807
+ elif window_point == 'se':
808
+ x_offset, y_offset = -w_width, -w_height
809
+
810
+ # Calculate final position
811
+ x = anchor_x + x_offset + offset[0]
812
+ y = anchor_y + y_offset + offset[1]
813
+
814
+ # Ensure window stays on screen
815
+ if ensure_visible:
816
+ x, y = WindowPositioning.ensure_on_screen(window, int(x), int(y))
817
+
818
+ window.geometry(f"+{int(x)}+{int(y)}")
819
+
820
+ @staticmethod
821
+ def position_at_cursor(
822
+ window: tkinter.Misc,
823
+ offset: Tuple[int, int] = (5, 5),
824
+ ensure_visible: bool = True
825
+ ) -> None:
826
+ """Position window at the current mouse cursor location.
827
+
828
+ Useful for context menus, tooltips that follow the cursor, or
829
+ click-to-show dialogs.
830
+
831
+ Args:
832
+ window: The window to position.
833
+ offset: Additional (x, y) offset from cursor in pixels.
834
+ ensure_visible: Whether to adjust position to keep window on screen.
835
+
836
+ Examples:
837
+ >>> # Show context menu at cursor
838
+ >>> WindowPositioning.position_at_cursor(menu, offset=(2, 2))
839
+ """
840
+ window.update_idletasks()
841
+
842
+ # Get cursor position
843
+ x = window.winfo_pointerx() + offset[0]
844
+ y = window.winfo_pointery() + offset[1]
845
+
846
+ # Ensure window stays on screen
847
+ if ensure_visible:
848
+ x, y = WindowPositioning.ensure_on_screen(window, int(x), int(y))
849
+
850
+ window.geometry(f"+{int(x)}+{int(y)}")
851
+
852
+ @staticmethod
853
+ def position_dropdown(
854
+ window: tkinter.Misc,
855
+ trigger_widget: tkinter.Misc,
856
+ prefer_below: bool = True,
857
+ align: Literal['left', 'right', 'center'] = 'left',
858
+ offset: Tuple[int, int] = (0, 2),
859
+ ensure_visible: bool = True,
860
+ auto_flip: bool = True
861
+ ) -> None:
862
+ """Position window as a dropdown relative to a trigger widget.
863
+
864
+ Smart positioning that automatically flips above/below based on
865
+ available space. Commonly used for combobox dropdowns, autocomplete
866
+ suggestions, and dropdown menus.
867
+
868
+ Args:
869
+ window: The dropdown window to position.
870
+ trigger_widget: The widget that triggers the dropdown (e.g., button).
871
+ prefer_below: If True, tries to show below trigger; else tries above.
872
+ align: Horizontal alignment ('left', 'right', or 'center').
873
+ offset: Additional (x, y) offset in pixels.
874
+ ensure_visible: Whether to adjust position to keep window on screen.
875
+ auto_flip: If True, automatically flips above/below if no room.
876
+
877
+ Examples:
878
+ >>> # Dropdown below button, left-aligned
879
+ >>> WindowPositioning.position_dropdown(
880
+ ... window=dropdown,
881
+ ... trigger_widget=button,
882
+ ... prefer_below=True,
883
+ ... align='left'
884
+ ... )
885
+ """
886
+ window.update_idletasks()
887
+ trigger_widget.update_idletasks()
888
+
889
+ # Get trigger widget position and size
890
+ trigger_x = trigger_widget.winfo_rootx()
891
+ trigger_y = trigger_widget.winfo_rooty()
892
+ trigger_height = trigger_widget.winfo_height()
893
+ trigger_width = trigger_widget.winfo_width()
894
+
895
+ # Get window size
896
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
897
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
898
+
899
+ # Get screen/monitor boundaries for the trigger widget's location
900
+ monitor = WindowPositioning._get_monitor_at_point(trigger_x, trigger_y)
901
+ if monitor:
902
+ screen_y, screen_height = monitor[1], monitor[3]
903
+ else:
904
+ screen_y = 0
905
+ screen_height = window.winfo_screenheight()
906
+
907
+ # Determine vertical position
908
+ show_below = prefer_below
909
+
910
+ if auto_flip:
911
+ # Check if there's room below (relative to monitor)
912
+ space_below = (screen_y + screen_height) - (trigger_y + trigger_height)
913
+ space_above = trigger_y - screen_y
914
+
915
+ if prefer_below and space_below < w_height and space_above > space_below:
916
+ show_below = False
917
+ elif not prefer_below and space_above < w_height and space_below > space_above:
918
+ show_below = True
919
+
920
+ # Calculate vertical position
921
+ if show_below:
922
+ y = trigger_y + trigger_height + offset[1]
923
+ else:
924
+ y = trigger_y - w_height - offset[1]
925
+
926
+ # Calculate horizontal position based on alignment
927
+ if align == 'left':
928
+ x = trigger_x + offset[0]
929
+ elif align == 'right':
930
+ x = trigger_x + trigger_width - w_width + offset[0]
931
+ elif align == 'center':
932
+ x = trigger_x + (trigger_width - w_width) // 2 + offset[0]
933
+ else:
934
+ x = trigger_x + offset[0]
935
+
936
+ # Ensure window stays on screen
937
+ if ensure_visible:
938
+ x, y = WindowPositioning.ensure_on_screen(window, int(x), int(y))
939
+
940
+ window.geometry(f"+{int(x)}+{int(y)}")
941
+
942
+
943
+ class WindowSizing:
944
+ """Utilities for window sizing and dimension constraints.
945
+
946
+ Provides helper methods for managing window dimensions, including
947
+ minimum/maximum sizes and calculating appropriate default sizes
948
+ based on screen dimensions.
949
+ """
950
+
951
+ @staticmethod
952
+ def get_default_size(
953
+ window: tkinter.Misc,
954
+ width_ratio: float = 0.6,
955
+ height_ratio: float = 0.7,
956
+ min_width: int = 400,
957
+ min_height: int = 300,
958
+ max_width: Optional[int] = None,
959
+ max_height: Optional[int] = None
960
+ ) -> tuple[int, int]:
961
+ """Calculate a reasonable default window size based on screen dimensions.
962
+
963
+ Calculates window size as a percentage of screen size, constrained
964
+ by minimum and optional maximum dimensions. Useful for creating
965
+ responsive windows that adapt to different screen sizes.
966
+
967
+ Args:
968
+ window: Window to calculate size for (used to get screen dimensions).
969
+ width_ratio: Proportion of screen width (0.0 to 1.0). Default is 0.6 (60%).
970
+ height_ratio: Proportion of screen height (0.0 to 1.0). Default is 0.7 (70%).
971
+ min_width: Minimum window width in pixels. Default is 400.
972
+ min_height: Minimum window height in pixels. Default is 300.
973
+ max_width: Optional maximum window width in pixels.
974
+ max_height: Optional maximum window height in pixels.
975
+
976
+ Returns:
977
+ Tuple of (width, height) in pixels.
978
+
979
+ Examples:
980
+ >>> window = tkinter.Tk()
981
+ >>> width, height = WindowSizing.get_default_size(window)
982
+ >>> window.geometry(f"{width}x{height}")
983
+ """
984
+ window.update_idletasks()
985
+
986
+ # Try to use the monitor containing the mouse cursor
987
+ cursor_x = window.winfo_pointerx()
988
+ cursor_y = window.winfo_pointery()
989
+ monitor = WindowPositioning._get_monitor_at_point(cursor_x, cursor_y)
990
+
991
+ if monitor:
992
+ screen_width, screen_height = monitor[2], monitor[3]
993
+ else:
994
+ screen_width = window.winfo_screenwidth()
995
+ screen_height = window.winfo_screenheight()
996
+
997
+ width = int(screen_width * width_ratio)
998
+ height = int(screen_height * height_ratio)
999
+
1000
+ # Apply constraints
1001
+ width = max(min_width, width)
1002
+ height = max(min_height, height)
1003
+
1004
+ if max_width is not None:
1005
+ width = min(width, max_width)
1006
+ if max_height is not None:
1007
+ height = min(height, max_height)
1008
+
1009
+ return width, height
1010
+
1011
+ @staticmethod
1012
+ def apply_size_constraints(
1013
+ window: tkinter.Misc,
1014
+ minsize: Optional[tuple[int, int]] = None,
1015
+ maxsize: Optional[tuple[int, int]] = None,
1016
+ resizable: Optional[tuple[bool, bool]] = None
1017
+ ) -> None:
1018
+ """Apply size constraints to a window.
1019
+
1020
+ Convenience method to apply multiple size-related constraints at once.
1021
+
1022
+ Args:
1023
+ window: Window to apply constraints to.
1024
+ minsize: Optional (width, height) minimum size.
1025
+ maxsize: Optional (width, height) maximum size.
1026
+ resizable: Optional (width, height) resizable flags.
1027
+
1028
+ Examples:
1029
+ >>> WindowSizing.apply_size_constraints(
1030
+ ... window,
1031
+ ... minsize=(400, 300),
1032
+ ... maxsize=(1920, 1080),
1033
+ ... resizable=(True, False) # Width resizable, height fixed
1034
+ ... )
1035
+ """
1036
+ if minsize is not None:
1037
+ window.minsize(*minsize)
1038
+
1039
+ if maxsize is not None:
1040
+ window.maxsize(*maxsize)
1041
+
1042
+ if resizable is not None:
1043
+ window.resizable(*resizable)