bootstack 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. bootstack-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1123 @@
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
+ # Per-process cache of the macOS visible frame (work area) per monitor rect,
56
+ # keyed by the full monitor bounds. Computed once via a clamped probe.
57
+ _work_area_cache: dict[Tuple[int, int, int, int], Tuple[int, int, int, int]] = {}
58
+
59
+ @staticmethod
60
+ def get_work_area(
61
+ root: Optional[tkinter.Misc],
62
+ monitor_rect: Tuple[int, int, int, int],
63
+ ) -> Tuple[int, int, int, int]:
64
+ """Return the usable area of a monitor, OS-reserved strips excluded.
65
+
66
+ On macOS the menu bar (top) and Dock sit *inside* the physical monitor
67
+ bounds, so anchoring a window to the raw monitor rect lands it under the
68
+ Dock. Tk exposes no `NSScreen.visibleFrame` accessor, so this clamps a
69
+ throwaway, fully transparent managed Toplevel to the monitor — Aqua snaps
70
+ a managed window to the visible frame — and reads back the realized
71
+ origin and size. The result is cached per monitor rect.
72
+
73
+ Off macOS the full `monitor_rect` is returned unchanged: the X11/Windows
74
+ transient path already clears the taskbar via a topmost overlay.
75
+
76
+ Args:
77
+ root: A live widget used to parent the probe. If None, the full
78
+ `monitor_rect` is returned unchanged.
79
+ monitor_rect: `(x, y, w, h)` full bounds of the target monitor.
80
+
81
+ Returns:
82
+ `(x, y, w, h)` of the usable work area within `monitor_rect`.
83
+ """
84
+ if root is None:
85
+ return monitor_rect
86
+ try:
87
+ if root.tk.call("tk", "windowingsystem") != "aqua":
88
+ return monitor_rect
89
+ except tkinter.TclError:
90
+ return monitor_rect
91
+
92
+ cached = WindowPositioning._work_area_cache.get(monitor_rect)
93
+ if cached is not None:
94
+ return cached
95
+
96
+ mx, my, mw, mh = monitor_rect
97
+ area = monitor_rect
98
+ probe: Optional[tkinter.Toplevel] = None
99
+ try:
100
+ probe = tkinter.Toplevel(root)
101
+ try:
102
+ probe.attributes("-alpha", 0.0) # invisible — no flash
103
+ except tkinter.TclError:
104
+ pass
105
+ # Oversize and place at the monitor origin; a managed Aqua window is
106
+ # clamped to that monitor's visible frame, so the realized geometry
107
+ # is the work area (menu bar + Dock excluded).
108
+ probe.geometry(f"{mw * 2}x{mh * 2}+{mx}+{my}")
109
+ probe.update_idletasks()
110
+ x, y = probe.winfo_rootx(), probe.winfo_rooty()
111
+ w, h = probe.winfo_width(), probe.winfo_height()
112
+ # Guard a degenerate probe (e.g. 1x1 if it never mapped): only trust
113
+ # a result that fills most of the monitor.
114
+ if w >= mw // 2 and h >= mh // 2:
115
+ area = (x, y, w, h)
116
+ except tkinter.TclError:
117
+ pass
118
+ finally:
119
+ if probe is not None:
120
+ try:
121
+ probe.destroy()
122
+ except tkinter.TclError:
123
+ pass
124
+
125
+ WindowPositioning._work_area_cache[monitor_rect] = area
126
+ return area
127
+
128
+ @staticmethod
129
+ def _get_monitor_at_point(x: int, y: int) -> Optional[Tuple[int, int, int, int]]:
130
+ """Find the monitor containing the given point.
131
+
132
+ Args:
133
+ x: X coordinate in screen space.
134
+ y: Y coordinate in screen space.
135
+
136
+ Returns:
137
+ Tuple of (monitor_x, monitor_y, monitor_width, monitor_height) if
138
+ screeninfo is available and a monitor contains the point.
139
+ Returns None if screeninfo is not installed or point is not on any monitor.
140
+ """
141
+ if not HAS_SCREENINFO:
142
+ return None
143
+
144
+ try:
145
+ monitors = get_monitors()
146
+ for monitor in monitors:
147
+ if (monitor.x <= x < monitor.x + monitor.width and
148
+ monitor.y <= y < monitor.y + monitor.height):
149
+ return (monitor.x, monitor.y, monitor.width, monitor.height)
150
+ # Point not on any monitor, return the first monitor as fallback
151
+ if monitors:
152
+ m = monitors[0]
153
+ return (m.x, m.y, m.width, m.height)
154
+ except Exception:
155
+ # If screeninfo fails for any reason, fall back to None
156
+ pass
157
+ return None
158
+
159
+ @staticmethod
160
+ def center_on_screen(window: tkinter.Misc) -> tuple[int, int]:
161
+ """Calculate coordinates to center window on screen.
162
+
163
+ Centers the window on the primary display. For multi-monitor setups,
164
+ this typically centers on the monitor containing the mouse pointer.
165
+
166
+ Args:
167
+ window: The window to center. Must be a tkinter widget with
168
+ geometry info available (call update_idletasks() first).
169
+
170
+ Returns:
171
+ Tuple of (x, y) coordinates representing the top-left position
172
+ that will center the window on screen.
173
+
174
+ Note:
175
+ The window must have been geometry-managed before calling this
176
+ method. Call window.update_idletasks() first to ensure accurate
177
+ dimensions are available.
178
+
179
+ Examples:
180
+ >>> window = tkinter.Tk()
181
+ >>> window.update_idletasks()
182
+ >>> x, y = WindowPositioning.center_on_screen(window)
183
+ >>> window.geometry(f"+{x}+{y}")
184
+ """
185
+ window.update_idletasks()
186
+
187
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
188
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
189
+
190
+ # Try to center on the monitor containing the mouse cursor
191
+ cursor_x = window.winfo_pointerx()
192
+ cursor_y = window.winfo_pointery()
193
+ monitor = WindowPositioning._get_monitor_at_point(cursor_x, cursor_y)
194
+
195
+ if monitor:
196
+ # Center on the specific monitor
197
+ mon_x, mon_y, mon_width, mon_height = monitor
198
+ x = mon_x + (mon_width - w_width) // 2
199
+ y = mon_y + (mon_height - w_height) // 2
200
+ else:
201
+ # Fall back to total screen dimensions (original behavior)
202
+ s_width = window.winfo_screenwidth()
203
+ s_height = window.winfo_screenheight()
204
+ x = (s_width - w_width) // 2
205
+ y = (s_height - w_height) // 2
206
+
207
+ return x, y
208
+
209
+ @staticmethod
210
+ def center_on_parent(window: tkinter.Toplevel, parent: tkinter.Misc) -> tuple[int, int]:
211
+ """Calculate coordinates to center window on parent widget/window.
212
+
213
+ Centers the window relative to its parent window or widget. This is
214
+ commonly used for dialogs to appear centered on their parent window.
215
+
216
+ Args:
217
+ window: The window to center (typically a Toplevel or Dialog).
218
+ parent: The parent window or widget to center on.
219
+
220
+ Returns:
221
+ Tuple of (x, y) screen coordinates that will center the window
222
+ on the parent.
223
+
224
+ Note:
225
+ Both window and parent must have geometry information available.
226
+ The returned coordinates are in screen coordinates, not relative
227
+ to the parent.
228
+
229
+ Examples:
230
+ import bootstack._runtime.toplevel >>> parent = tkinter.Tk()
231
+ >>> dialog = bootstack._runtime.toplevel.Toplevel(parent)
232
+ >>> dialog.update_idletasks()
233
+ >>> parent.update_idletasks()
234
+ >>> x, y = WindowPositioning.center_on_parent(dialog, parent)
235
+ >>> dialog.geometry(f"+{x}+{y}")
236
+ """
237
+ window.update_idletasks()
238
+ parent.update_idletasks()
239
+
240
+ # Use requested size or actual size, whichever is larger
241
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
242
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
243
+
244
+ # Get parent's screen position and size
245
+ p_x = parent.winfo_rootx()
246
+ p_y = parent.winfo_rooty()
247
+ p_width = max(parent.winfo_width(), parent.winfo_reqwidth())
248
+ p_height = max(parent.winfo_height(), parent.winfo_reqheight())
249
+
250
+ # Calculate centered position
251
+ x = p_x + max(0, (p_width - w_width) // 2)
252
+ y = p_y + max(0, (p_height - w_height) // 2)
253
+ return x, y
254
+
255
+ @staticmethod
256
+ def ensure_on_screen(
257
+ window: tkinter.Misc,
258
+ x: int,
259
+ y: int,
260
+ padding: int = 20,
261
+ titlebar_height: int = 60
262
+ ) -> tuple[int, int]:
263
+ """Adjust coordinates to keep window fully visible on screen.
264
+
265
+ Ensures that a window positioned at (x, y) will be fully visible on
266
+ screen. If the coordinates would place any part of the window off-screen,
267
+ they are adjusted to keep the window within screen bounds with padding.
268
+
269
+ This method supports multi-monitor setups by using virtual root
270
+ coordinates, ensuring the window appears on the correct display.
271
+
272
+ Args:
273
+ window: The window to position. Must have geometry info available.
274
+ x: Desired x coordinate (screen coordinates).
275
+ y: Desired y coordinate (screen coordinates).
276
+ padding: Minimum padding from screen edges in pixels. Default is 20.
277
+ titlebar_height: Additional padding for titlebar at top. Default is 60.
278
+
279
+ Returns:
280
+ Tuple of (x, y) coordinates adjusted to keep window on screen.
281
+
282
+ Note:
283
+ The titlebar_height accounts for window manager decorations which
284
+ aren't included in winfo_height(). This prevents the titlebar from
285
+ being positioned off-screen.
286
+
287
+ Examples:
288
+ >>> window = tkinter.Tk()
289
+ >>> window.update_idletasks()
290
+ >>> # Try to position far off screen
291
+ >>> x, y = WindowPositioning.ensure_on_screen(window, 5000, 5000)
292
+ >>> # Returns coordinates that keep window visible
293
+ >>> window.geometry(f"+{x}+{y}")
294
+ """
295
+ window.update_idletasks()
296
+
297
+ w_width = window.winfo_reqwidth()
298
+ w_height = window.winfo_reqheight()
299
+
300
+ # Use the monitor containing the proposed position when screeninfo is
301
+ # available — on Windows, winfo_vroot* only covers the primary monitor,
302
+ # so clamping with it moves windows that legitimately belong on a
303
+ # secondary monitor back onto the primary.
304
+ monitor = WindowPositioning._get_monitor_at_point(x, y)
305
+ if monitor:
306
+ screen_x0, screen_y0, screen_width, screen_height = monitor
307
+ else:
308
+ screen_x0 = window.winfo_vrootx()
309
+ screen_y0 = window.winfo_vrooty()
310
+ screen_width = window.winfo_vrootwidth()
311
+ screen_height = window.winfo_vrootheight()
312
+
313
+ # Calculate screen boundaries
314
+ screen_x1 = screen_x0 + screen_width
315
+ screen_y1 = screen_y0 + screen_height
316
+
317
+ # Constrain to screen bounds with padding
318
+ x = max(screen_x0 + padding, min(x, screen_x1 - w_width - padding))
319
+ y = max(screen_y0 + padding, min(y, screen_y1 - w_height - titlebar_height))
320
+
321
+ return int(x), int(y)
322
+
323
+ @staticmethod
324
+ def position_window(
325
+ window: tkinter.Misc,
326
+ position: Optional[tuple[int, int]] = None,
327
+ parent: Optional[tkinter.Misc] = None,
328
+ center_on_parent: bool = True,
329
+ ensure_visible: bool = True
330
+ ) -> None:
331
+ """Smart window positioning with multiple strategies.
332
+
333
+ Provides a high-level interface for positioning windows using the
334
+ most common strategies:
335
+ - Explicit coordinates (if position is provided)
336
+ - Centered on parent (if parent is provided and center_on_parent=True)
337
+ - Centered on screen (fallback)
338
+
339
+ Optionally ensures the window remains fully visible on screen.
340
+
341
+ Args:
342
+ window: The window to position.
343
+ position: Optional (x, y) coordinates in screen space. If provided,
344
+ positions window at these coordinates.
345
+ parent: Optional parent window. If provided and center_on_parent=True,
346
+ centers window on this parent.
347
+ center_on_parent: Whether to center on parent when parent is provided.
348
+ Ignored if position is explicitly provided.
349
+ ensure_visible: Whether to adjust coordinates to keep window on screen.
350
+ Default is True.
351
+
352
+ Note:
353
+ This method calls window.update_idletasks() internally and applies
354
+ the geometry immediately.
355
+
356
+ Examples:
357
+ >>> # Position at specific coordinates
358
+ >>> WindowPositioning.position_window(window, position=(100, 100))
359
+ >>>
360
+ >>> # Center on parent
361
+ >>> WindowPositioning.position_window(dialog, parent=parent_window)
362
+ >>>
363
+ >>> # Center on screen
364
+ >>> WindowPositioning.position_window(window)
365
+ """
366
+ window.update_idletasks()
367
+
368
+ if position is not None:
369
+ # Explicit coordinates provided
370
+ x, y = position
371
+ if ensure_visible:
372
+ x, y = WindowPositioning.ensure_on_screen(window, int(x), int(y))
373
+ window.geometry(f"+{x}+{y}")
374
+
375
+ elif parent is not None and center_on_parent:
376
+ # Center on parent
377
+ x, y = WindowPositioning.center_on_parent(window, parent)
378
+ if ensure_visible:
379
+ x, y = WindowPositioning.ensure_on_screen(window, x, y)
380
+ window.geometry(f"+{x}+{y}")
381
+
382
+ else:
383
+ # Fallback: center on screen
384
+ x, y = WindowPositioning.center_on_screen(window)
385
+ if ensure_visible:
386
+ x, y = WindowPositioning.ensure_on_screen(window, x, y)
387
+ window.geometry(f"+{x}+{y}")
388
+
389
+ @staticmethod
390
+ def _get_anchor_coordinates(
391
+ widget: tkinter.Misc,
392
+ anchor: AnchorPoint = 'nw',
393
+ use_requested_size: bool = True
394
+ ) -> Tuple[int, int]:
395
+ """Calculate screen coordinates for an anchor point on a widget.
396
+
397
+ Uses tkinter's standard anchor naming convention:
398
+ - 'n', 's', 'e', 'w' for cardinal directions (north, south, east, west)
399
+ - 'ne', 'nw', 'se', 'sw' for corners
400
+ - 'center' for the center point
401
+
402
+ Args:
403
+ widget: Widget to get anchor coordinates for.
404
+ anchor: Which point on the widget to return coordinates for.
405
+ use_requested_size: If True, uses requested size; otherwise actual size.
406
+
407
+ Returns:
408
+ Tuple of (x, y) screen coordinates for the anchor point.
409
+ """
410
+ widget.update_idletasks()
411
+
412
+ # Get widget position
413
+ x = widget.winfo_rootx()
414
+ y = widget.winfo_rooty()
415
+
416
+ # Get widget dimensions
417
+ if use_requested_size:
418
+ width = widget.winfo_reqwidth()
419
+ height = widget.winfo_reqheight()
420
+ else:
421
+ width = widget.winfo_width()
422
+ height = widget.winfo_height()
423
+
424
+ # Calculate anchor position using tkinter convention
425
+ if anchor == 'nw':
426
+ return x, y
427
+ elif anchor == 'n':
428
+ return x + width // 2, y
429
+ elif anchor == 'ne':
430
+ return x + width, y
431
+ elif anchor == 'w':
432
+ return x, y + height // 2
433
+ elif anchor == 'center':
434
+ return x + width // 2, y + height // 2
435
+ elif anchor == 'e':
436
+ return x + width, y + height // 2
437
+ elif anchor == 'sw':
438
+ return x, y + height
439
+ elif anchor == 's':
440
+ return x + width // 2, y + height
441
+ elif anchor == 'se':
442
+ return x + width, y + height
443
+ else:
444
+ return x, y
445
+
446
+ @staticmethod
447
+ def _get_screen_anchor_coordinates(
448
+ window: tkinter.Misc,
449
+ anchor: AnchorPoint = 'center'
450
+ ) -> Tuple[int, int]:
451
+ """Calculate screen coordinates for an anchor point on the screen.
452
+
453
+ For multi-monitor setups, this returns coordinates on the monitor
454
+ containing the mouse cursor.
455
+
456
+ Args:
457
+ window: Window (used to get screen dimensions).
458
+ anchor: Which point on the screen to return coordinates for.
459
+
460
+ Returns:
461
+ Tuple of (x, y) screen coordinates for the anchor point.
462
+ """
463
+ window.update_idletasks()
464
+
465
+ # Try to use the monitor containing the mouse cursor
466
+ cursor_x = window.winfo_pointerx()
467
+ cursor_y = window.winfo_pointery()
468
+ monitor = WindowPositioning._get_monitor_at_point(cursor_x, cursor_y)
469
+
470
+ if monitor:
471
+ screen_x, screen_y, screen_width, screen_height = monitor
472
+ else:
473
+ # Fall back to total screen dimensions
474
+ screen_x, screen_y = 0, 0
475
+ screen_width = window.winfo_screenwidth()
476
+ screen_height = window.winfo_screenheight()
477
+
478
+ # Calculate anchor position on screen/monitor
479
+ if anchor == 'nw':
480
+ return screen_x, screen_y
481
+ elif anchor == 'n':
482
+ return screen_x + screen_width // 2, screen_y
483
+ elif anchor == 'ne':
484
+ return screen_x + screen_width, screen_y
485
+ elif anchor == 'w':
486
+ return screen_x, screen_y + screen_height // 2
487
+ elif anchor == 'center':
488
+ return screen_x + screen_width // 2, screen_y + screen_height // 2
489
+ elif anchor == 'e':
490
+ return screen_x + screen_width, screen_y + screen_height // 2
491
+ elif anchor == 'sw':
492
+ return screen_x, screen_y + screen_height
493
+ elif anchor == 's':
494
+ return screen_x + screen_width // 2, screen_y + screen_height
495
+ elif anchor == 'se':
496
+ return screen_x + screen_width, screen_y + screen_height
497
+ else:
498
+ return screen_x + screen_width // 2, screen_y + screen_height // 2
499
+
500
+ @staticmethod
501
+ def _get_cursor_anchor_coordinates(
502
+ window: tkinter.Misc,
503
+ anchor: AnchorPoint = 'nw'
504
+ ) -> Tuple[int, int]:
505
+ """Calculate screen coordinates for an anchor point relative to cursor.
506
+
507
+ The cursor is treated as a point (no width/height), so all anchor points
508
+ return the same cursor position. The anchor parameter is kept for API
509
+ consistency but doesn't affect the result.
510
+
511
+ Args:
512
+ window: Window (used to get cursor position).
513
+ anchor: Anchor point (ignored, cursor is a point).
514
+
515
+ Returns:
516
+ Tuple of (x, y) screen coordinates of the cursor.
517
+ """
518
+ window.update_idletasks()
519
+
520
+ # Cursor is a point, so all anchors return cursor position
521
+ x = window.winfo_pointerx()
522
+ y = window.winfo_pointery()
523
+
524
+ return x, y
525
+
526
+ @staticmethod
527
+ def _flip_anchor_vertical(anchor: AnchorPoint) -> AnchorPoint:
528
+ """Flip an anchor point vertically (north ↔ south).
529
+
530
+ Args:
531
+ anchor: Anchor point to flip.
532
+
533
+ Returns:
534
+ Vertically flipped anchor point.
535
+ """
536
+ flip_map = {
537
+ 'n': 's', 's': 'n',
538
+ 'ne': 'se', 'se': 'ne',
539
+ 'nw': 'sw', 'sw': 'nw',
540
+ 'e': 'e', 'w': 'w',
541
+ 'center': 'center'
542
+ }
543
+ return flip_map.get(anchor, anchor)
544
+
545
+ @staticmethod
546
+ def _flip_anchor_horizontal(anchor: AnchorPoint) -> AnchorPoint:
547
+ """Flip an anchor point horizontally (east ↔ west).
548
+
549
+ Args:
550
+ anchor: Anchor point to flip.
551
+
552
+ Returns:
553
+ Horizontally flipped anchor point.
554
+ """
555
+ flip_map = {
556
+ 'e': 'w', 'w': 'e',
557
+ 'ne': 'nw', 'nw': 'ne',
558
+ 'se': 'sw', 'sw': 'se',
559
+ 'n': 'n', 's': 's',
560
+ 'center': 'center'
561
+ }
562
+ return flip_map.get(anchor, anchor)
563
+
564
+ @staticmethod
565
+ def _check_offscreen(
566
+ window: tkinter.Misc,
567
+ x: int,
568
+ y: int,
569
+ padding: int = 20
570
+ ) -> Tuple[bool, bool]:
571
+ """Check if a window positioned at (x, y) would be off-screen.
572
+
573
+ For multi-monitor setups, checks against the monitor containing the
574
+ proposed position.
575
+
576
+ Args:
577
+ window: Window to check.
578
+ x: Proposed x coordinate.
579
+ y: Proposed y coordinate.
580
+ padding: Minimum padding from screen edges.
581
+
582
+ Returns:
583
+ Tuple of (vertical_offscreen, horizontal_offscreen) booleans.
584
+ """
585
+ window.update_idletasks()
586
+
587
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
588
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
589
+
590
+ # Try to get the monitor at the proposed position
591
+ monitor = WindowPositioning._get_monitor_at_point(x, y)
592
+
593
+ if monitor:
594
+ screen_x, screen_y, screen_width, screen_height = monitor
595
+ else:
596
+ # Fall back to total screen dimensions
597
+ screen_x, screen_y = 0, 0
598
+ screen_width = window.winfo_screenwidth()
599
+ screen_height = window.winfo_screenheight()
600
+
601
+ # Check vertical (top/bottom off-screen relative to monitor)
602
+ vertical_offscreen = (
603
+ y < screen_y + padding or # Too far up
604
+ y + w_height + padding > screen_y + screen_height # Too far down
605
+ )
606
+
607
+ # Check horizontal (left/right off-screen relative to monitor)
608
+ horizontal_offscreen = (
609
+ x < screen_x + padding or # Too far left
610
+ x + w_width + padding > screen_x + screen_width # Too far right
611
+ )
612
+
613
+ return vertical_offscreen, horizontal_offscreen
614
+
615
+ @staticmethod
616
+ def position_anchored(
617
+ window: tkinter.Misc,
618
+ anchor_to: Union[tkinter.Misc, Literal["screen", "cursor", "parent"]],
619
+ parent: Optional[tkinter.Misc] = None,
620
+ anchor_point: AnchorPoint = 'center',
621
+ window_point: AnchorPoint = 'center',
622
+ offset: Tuple[int, int] = (0, 0),
623
+ auto_flip: AutoFlip = False,
624
+ ensure_visible: bool = True
625
+ ) -> None:
626
+ """Position window using unified anchor-based positioning with auto-flip.
627
+
628
+ This is the new consolidated positioning method that handles:
629
+ - Widget anchoring
630
+ - Screen anchoring (with anchor points)
631
+ - Cursor anchoring
632
+ - Parent anchoring
633
+ - Auto-flip (vertical and/or horizontal)
634
+
635
+ Args:
636
+ window: The window to position.
637
+ anchor_to: Positioning target:
638
+ - Widget: Anchor to a specific widget
639
+ - "screen": Anchor to screen edges/corners
640
+ - "cursor": Anchor to mouse cursor
641
+ - "parent": Anchor to parent window
642
+ parent: Parent window (required if anchor_to="parent").
643
+ anchor_point: Point on the anchor target.
644
+ window_point: Point on the window.
645
+ offset: Additional (x, y) offset in pixels.
646
+ auto_flip: Smart flipping to keep window on screen:
647
+ - False: No flipping
648
+ - True: Flip both vertically and horizontally
649
+ - 'vertical': Only flip up/down
650
+ - 'horizontal': Only flip left/right
651
+ ensure_visible: Whether to adjust position to keep window on screen.
652
+
653
+ Examples:
654
+ >>> # Center on screen
655
+ >>> WindowPositioning.position_anchored(window, anchor_to="screen")
656
+ >>>
657
+ >>> # Top-right corner of screen
658
+ >>> WindowPositioning.position_anchored(
659
+ ... window, anchor_to="screen", anchor_point='ne', window_point='ne'
660
+ ... )
661
+ >>>
662
+ >>> # Dropdown with auto-flip
663
+ >>> WindowPositioning.position_anchored(
664
+ ... window, anchor_to=button,
665
+ ... anchor_point='sw', window_point='nw',
666
+ ... auto_flip='vertical'
667
+ ... )
668
+ """
669
+ window.update_idletasks()
670
+
671
+ # Get anchor coordinates based on anchor_to type
672
+ if anchor_to == "screen":
673
+ anchor_x, anchor_y = WindowPositioning._get_screen_anchor_coordinates(
674
+ window, anchor_point
675
+ )
676
+ elif anchor_to == "cursor":
677
+ anchor_x, anchor_y = WindowPositioning._get_cursor_anchor_coordinates(
678
+ window, anchor_point
679
+ )
680
+ elif anchor_to == "parent":
681
+ if parent is None:
682
+ raise ValueError("parent parameter required when anchor_to='parent'")
683
+ parent.update_idletasks()
684
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
685
+ parent, anchor_point
686
+ )
687
+ else:
688
+ # Assume it's a widget — use actual rendered size, not minimum requested
689
+ anchor_to.update_idletasks()
690
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
691
+ anchor_to, anchor_point, use_requested_size=False
692
+ )
693
+
694
+ # Calculate window position based on window_point
695
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
696
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
697
+
698
+ # Calculate offset based on window anchor point
699
+ x_offset, y_offset = 0, 0
700
+
701
+ if window_point == 'nw':
702
+ x_offset, y_offset = 0, 0
703
+ elif window_point == 'n':
704
+ x_offset, y_offset = -w_width // 2, 0
705
+ elif window_point == 'ne':
706
+ x_offset, y_offset = -w_width, 0
707
+ elif window_point == 'w':
708
+ x_offset, y_offset = 0, -w_height // 2
709
+ elif window_point == 'center':
710
+ x_offset, y_offset = -w_width // 2, -w_height // 2
711
+ elif window_point == 'e':
712
+ x_offset, y_offset = -w_width, -w_height // 2
713
+ elif window_point == 'sw':
714
+ x_offset, y_offset = 0, -w_height
715
+ elif window_point == 's':
716
+ x_offset, y_offset = -w_width // 2, -w_height
717
+ elif window_point == 'se':
718
+ x_offset, y_offset = -w_width, -w_height
719
+
720
+ # Calculate initial position
721
+ x = int(anchor_x + x_offset + offset[0])
722
+ y = int(anchor_y + y_offset + offset[1])
723
+
724
+ # Auto-flip logic
725
+ if auto_flip:
726
+ vertical_offscreen, horizontal_offscreen = WindowPositioning._check_offscreen(
727
+ window, x, y
728
+ )
729
+
730
+ should_flip_vertical = False
731
+ should_flip_horizontal = False
732
+
733
+ if auto_flip is True or auto_flip == 'vertical':
734
+ should_flip_vertical = vertical_offscreen
735
+
736
+ if auto_flip is True or auto_flip == 'horizontal':
737
+ should_flip_horizontal = horizontal_offscreen
738
+
739
+ # Flip if needed
740
+ if should_flip_vertical or should_flip_horizontal:
741
+ flipped_anchor_point = anchor_point
742
+ flipped_window_point = window_point
743
+
744
+ if should_flip_vertical:
745
+ flipped_anchor_point = WindowPositioning._flip_anchor_vertical(
746
+ flipped_anchor_point
747
+ )
748
+ flipped_window_point = WindowPositioning._flip_anchor_vertical(
749
+ flipped_window_point
750
+ )
751
+
752
+ if should_flip_horizontal:
753
+ flipped_anchor_point = WindowPositioning._flip_anchor_horizontal(
754
+ flipped_anchor_point
755
+ )
756
+ flipped_window_point = WindowPositioning._flip_anchor_horizontal(
757
+ flipped_window_point
758
+ )
759
+
760
+ # Recalculate with flipped anchors
761
+ if anchor_to == "screen":
762
+ anchor_x, anchor_y = WindowPositioning._get_screen_anchor_coordinates(
763
+ window, flipped_anchor_point
764
+ )
765
+ elif anchor_to == "cursor":
766
+ anchor_x, anchor_y = WindowPositioning._get_cursor_anchor_coordinates(
767
+ window, flipped_anchor_point
768
+ )
769
+ elif anchor_to == "parent":
770
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
771
+ parent, flipped_anchor_point
772
+ )
773
+ else:
774
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
775
+ anchor_to, flipped_anchor_point
776
+ )
777
+
778
+ # Recalculate offset for flipped window_point
779
+ if flipped_window_point == 'nw':
780
+ x_offset, y_offset = 0, 0
781
+ elif flipped_window_point == 'n':
782
+ x_offset, y_offset = -w_width // 2, 0
783
+ elif flipped_window_point == 'ne':
784
+ x_offset, y_offset = -w_width, 0
785
+ elif flipped_window_point == 'w':
786
+ x_offset, y_offset = 0, -w_height // 2
787
+ elif flipped_window_point == 'center':
788
+ x_offset, y_offset = -w_width // 2, -w_height // 2
789
+ elif flipped_window_point == 'e':
790
+ x_offset, y_offset = -w_width, -w_height // 2
791
+ elif flipped_window_point == 'sw':
792
+ x_offset, y_offset = 0, -w_height
793
+ elif flipped_window_point == 's':
794
+ x_offset, y_offset = -w_width // 2, -w_height
795
+ elif flipped_window_point == 'se':
796
+ x_offset, y_offset = -w_width, -w_height
797
+
798
+ x = int(anchor_x + x_offset + offset[0])
799
+ y = int(anchor_y + y_offset + offset[1])
800
+
801
+ # Final ensure visible check
802
+ if ensure_visible:
803
+ x, y = WindowPositioning.ensure_on_screen(window, x, y)
804
+
805
+ window.geometry(f"+{x}+{y}")
806
+
807
+ @staticmethod
808
+ def position_with_anchor(
809
+ window: tkinter.Misc,
810
+ anchor_to: tkinter.Misc,
811
+ anchor_point: AnchorPoint = 'sw',
812
+ window_point: AnchorPoint = 'nw',
813
+ offset: Tuple[int, int] = (0, 0),
814
+ ensure_visible: bool = True
815
+ ) -> None:
816
+ """Position window relative to another widget using anchor points.
817
+
818
+ This method positions a window by aligning specific points on both
819
+ the window and the anchor widget, with optional offset. This is useful
820
+ for dropdowns, tooltips, context menus, and popovers.
821
+
822
+ Uses tkinter's standard anchor naming:
823
+ - 'n' (north/top), 's' (south/bottom), 'e' (east/right), 'w' (west/left)
824
+ - 'ne', 'nw', 'se', 'sw' for corners
825
+ - 'center' for center point
826
+
827
+ Args:
828
+ window: The window to position.
829
+ anchor_to: The widget to anchor the window to.
830
+ anchor_point: Which point on the anchor widget to use as reference.
831
+ Default 'sw' (bottom-left) is common for dropdowns.
832
+ window_point: Which point on the window to align with the anchor point.
833
+ Default 'nw' (top-left) aligns window's top-left to anchor point.
834
+ offset: Additional (x, y) offset in pixels.
835
+ ensure_visible: Whether to adjust position to keep window on screen.
836
+
837
+ Examples:
838
+ >>> # Show dropdown below button (button's bottom-left -> window's top-left)
839
+ >>> WindowPositioning.position_with_anchor(
840
+ ... window=dropdown,
841
+ ... anchor_to=button,
842
+ ... anchor_point='sw', # button's bottom-left
843
+ ... window_point='nw', # window's top-left
844
+ ... offset=(0, 2)
845
+ ... )
846
+ >>>
847
+ >>> # Show tooltip above widget (widget's top -> tooltip's bottom)
848
+ >>> WindowPositioning.position_with_anchor(
849
+ ... window=tooltip,
850
+ ... anchor_to=widget,
851
+ ... anchor_point='n', # widget's top-center
852
+ ... window_point='s', # tooltip's bottom-center
853
+ ... offset=(0, -5)
854
+ ... )
855
+ """
856
+ window.update_idletasks()
857
+ anchor_to.update_idletasks()
858
+
859
+ # Get anchor point on the reference widget
860
+ anchor_x, anchor_y = WindowPositioning._get_anchor_coordinates(
861
+ anchor_to, anchor_point
862
+ )
863
+
864
+ # Get window dimensions
865
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
866
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
867
+
868
+ # Calculate offset based on window anchor point
869
+ x_offset, y_offset = 0, 0
870
+
871
+ if window_point == 'nw':
872
+ x_offset, y_offset = 0, 0
873
+ elif window_point == 'n':
874
+ x_offset, y_offset = -w_width // 2, 0
875
+ elif window_point == 'ne':
876
+ x_offset, y_offset = -w_width, 0
877
+ elif window_point == 'w':
878
+ x_offset, y_offset = 0, -w_height // 2
879
+ elif window_point == 'center':
880
+ x_offset, y_offset = -w_width // 2, -w_height // 2
881
+ elif window_point == 'e':
882
+ x_offset, y_offset = -w_width, -w_height // 2
883
+ elif window_point == 'sw':
884
+ x_offset, y_offset = 0, -w_height
885
+ elif window_point == 's':
886
+ x_offset, y_offset = -w_width // 2, -w_height
887
+ elif window_point == 'se':
888
+ x_offset, y_offset = -w_width, -w_height
889
+
890
+ # Calculate final position
891
+ x = anchor_x + x_offset + offset[0]
892
+ y = anchor_y + y_offset + offset[1]
893
+
894
+ # Ensure window stays on screen
895
+ if ensure_visible:
896
+ x, y = WindowPositioning.ensure_on_screen(window, int(x), int(y))
897
+
898
+ window.geometry(f"+{int(x)}+{int(y)}")
899
+
900
+ @staticmethod
901
+ def position_at_cursor(
902
+ window: tkinter.Misc,
903
+ offset: Tuple[int, int] = (5, 5),
904
+ ensure_visible: bool = True
905
+ ) -> None:
906
+ """Position window at the current mouse cursor location.
907
+
908
+ Useful for context menus, tooltips that follow the cursor, or
909
+ click-to-show dialogs.
910
+
911
+ Args:
912
+ window: The window to position.
913
+ offset: Additional (x, y) offset from cursor in pixels.
914
+ ensure_visible: Whether to adjust position to keep window on screen.
915
+
916
+ Examples:
917
+ >>> # Show context menu at cursor
918
+ >>> WindowPositioning.position_at_cursor(menu, offset=(2, 2))
919
+ """
920
+ window.update_idletasks()
921
+
922
+ # Get cursor position
923
+ x = window.winfo_pointerx() + offset[0]
924
+ y = window.winfo_pointery() + offset[1]
925
+
926
+ # Ensure window stays on screen
927
+ if ensure_visible:
928
+ x, y = WindowPositioning.ensure_on_screen(window, int(x), int(y))
929
+
930
+ window.geometry(f"+{int(x)}+{int(y)}")
931
+
932
+ @staticmethod
933
+ def position_dropdown(
934
+ window: tkinter.Misc,
935
+ trigger_widget: tkinter.Misc,
936
+ prefer_below: bool = True,
937
+ align: Literal['left', 'right', 'center'] = 'left',
938
+ offset: Tuple[int, int] = (0, 2),
939
+ ensure_visible: bool = True,
940
+ auto_flip: bool = True
941
+ ) -> None:
942
+ """Position window as a dropdown relative to a trigger widget.
943
+
944
+ Smart positioning that automatically flips above/below based on
945
+ available space. Commonly used for combobox dropdowns, autocomplete
946
+ suggestions, and dropdown menus.
947
+
948
+ Args:
949
+ window: The dropdown window to position.
950
+ trigger_widget: The widget that triggers the dropdown (e.g., button).
951
+ prefer_below: If True, tries to show below trigger; else tries above.
952
+ align: Horizontal alignment ('left', 'right', or 'center').
953
+ offset: Additional (x, y) offset in pixels.
954
+ ensure_visible: Whether to adjust position to keep window on screen.
955
+ auto_flip: If True, automatically flips above/below if no room.
956
+
957
+ Examples:
958
+ >>> # Dropdown below button, left-aligned
959
+ >>> WindowPositioning.position_dropdown(
960
+ ... window=dropdown,
961
+ ... trigger_widget=button,
962
+ ... prefer_below=True,
963
+ ... align='left'
964
+ ... )
965
+ """
966
+ window.update_idletasks()
967
+ trigger_widget.update_idletasks()
968
+
969
+ # Get trigger widget position and size
970
+ trigger_x = trigger_widget.winfo_rootx()
971
+ trigger_y = trigger_widget.winfo_rooty()
972
+ trigger_height = trigger_widget.winfo_height()
973
+ trigger_width = trigger_widget.winfo_width()
974
+
975
+ # Get window size
976
+ w_width = max(window.winfo_reqwidth(), window.winfo_width())
977
+ w_height = max(window.winfo_reqheight(), window.winfo_height())
978
+
979
+ # Get screen/monitor boundaries for the trigger widget's location
980
+ monitor = WindowPositioning._get_monitor_at_point(trigger_x, trigger_y)
981
+ if monitor:
982
+ screen_y, screen_height = monitor[1], monitor[3]
983
+ else:
984
+ screen_y = 0
985
+ screen_height = window.winfo_screenheight()
986
+
987
+ # Determine vertical position
988
+ show_below = prefer_below
989
+
990
+ if auto_flip:
991
+ # Check if there's room below (relative to monitor)
992
+ space_below = (screen_y + screen_height) - (trigger_y + trigger_height)
993
+ space_above = trigger_y - screen_y
994
+
995
+ if prefer_below and space_below < w_height and space_above > space_below:
996
+ show_below = False
997
+ elif not prefer_below and space_above < w_height and space_below > space_above:
998
+ show_below = True
999
+
1000
+ # Calculate vertical position
1001
+ if show_below:
1002
+ y = trigger_y + trigger_height + offset[1]
1003
+ else:
1004
+ y = trigger_y - w_height - offset[1]
1005
+
1006
+ # Calculate horizontal position based on alignment
1007
+ if align == 'left':
1008
+ x = trigger_x + offset[0]
1009
+ elif align == 'right':
1010
+ x = trigger_x + trigger_width - w_width + offset[0]
1011
+ elif align == 'center':
1012
+ x = trigger_x + (trigger_width - w_width) // 2 + offset[0]
1013
+ else:
1014
+ x = trigger_x + offset[0]
1015
+
1016
+ # Ensure window stays on screen
1017
+ if ensure_visible:
1018
+ x, y = WindowPositioning.ensure_on_screen(window, int(x), int(y))
1019
+
1020
+ window.geometry(f"+{int(x)}+{int(y)}")
1021
+
1022
+
1023
+ class WindowSizing:
1024
+ """Utilities for window sizing and dimension constraints.
1025
+
1026
+ Provides helper methods for managing window dimensions, including
1027
+ minimum/maximum sizes and calculating appropriate default sizes
1028
+ based on screen dimensions.
1029
+ """
1030
+
1031
+ @staticmethod
1032
+ def get_default_size(
1033
+ window: tkinter.Misc,
1034
+ width_ratio: float = 0.6,
1035
+ height_ratio: float = 0.7,
1036
+ min_width: int = 400,
1037
+ min_height: int = 300,
1038
+ max_width: Optional[int] = None,
1039
+ max_height: Optional[int] = None
1040
+ ) -> tuple[int, int]:
1041
+ """Calculate a reasonable default window size based on screen dimensions.
1042
+
1043
+ Calculates window size as a percentage of screen size, constrained
1044
+ by minimum and optional maximum dimensions. Useful for creating
1045
+ responsive windows that adapt to different screen sizes.
1046
+
1047
+ Args:
1048
+ window: Window to calculate size for (used to get screen dimensions).
1049
+ width_ratio: Proportion of screen width (0.0 to 1.0). Default is 0.6 (60%).
1050
+ height_ratio: Proportion of screen height (0.0 to 1.0). Default is 0.7 (70%).
1051
+ min_width: Minimum window width in pixels. Default is 400.
1052
+ min_height: Minimum window height in pixels. Default is 300.
1053
+ max_width: Optional maximum window width in pixels.
1054
+ max_height: Optional maximum window height in pixels.
1055
+
1056
+ Returns:
1057
+ Tuple of (width, height) in pixels.
1058
+
1059
+ Examples:
1060
+ >>> window = tkinter.Tk()
1061
+ >>> width, height = WindowSizing.get_default_size(window)
1062
+ >>> window.geometry(f"{width}x{height}")
1063
+ """
1064
+ window.update_idletasks()
1065
+
1066
+ # Try to use the monitor containing the mouse cursor
1067
+ cursor_x = window.winfo_pointerx()
1068
+ cursor_y = window.winfo_pointery()
1069
+ monitor = WindowPositioning._get_monitor_at_point(cursor_x, cursor_y)
1070
+
1071
+ if monitor:
1072
+ screen_width, screen_height = monitor[2], monitor[3]
1073
+ else:
1074
+ screen_width = window.winfo_screenwidth()
1075
+ screen_height = window.winfo_screenheight()
1076
+
1077
+ width = int(screen_width * width_ratio)
1078
+ height = int(screen_height * height_ratio)
1079
+
1080
+ # Apply constraints
1081
+ width = max(min_width, width)
1082
+ height = max(min_height, height)
1083
+
1084
+ if max_width is not None:
1085
+ width = min(width, max_width)
1086
+ if max_height is not None:
1087
+ height = min(height, max_height)
1088
+
1089
+ return width, height
1090
+
1091
+ @staticmethod
1092
+ def apply_size_constraints(
1093
+ window: tkinter.Misc,
1094
+ minsize: Optional[tuple[int, int]] = None,
1095
+ maxsize: Optional[tuple[int, int]] = None,
1096
+ resizable: Optional[tuple[bool, bool]] = None
1097
+ ) -> None:
1098
+ """Apply size constraints to a window.
1099
+
1100
+ Convenience method to apply multiple size-related constraints at once.
1101
+
1102
+ Args:
1103
+ window: Window to apply constraints to.
1104
+ minsize: Optional (width, height) minimum size.
1105
+ maxsize: Optional (width, height) maximum size.
1106
+ resizable: Optional (width, height) resizable flags.
1107
+
1108
+ Examples:
1109
+ >>> WindowSizing.apply_size_constraints(
1110
+ ... window,
1111
+ ... minsize=(400, 300),
1112
+ ... maxsize=(1920, 1080),
1113
+ ... resizable=(True, False) # Width resizable, height fixed
1114
+ ... )
1115
+ """
1116
+ if minsize is not None:
1117
+ window.minsize(*minsize)
1118
+
1119
+ if maxsize is not None:
1120
+ window.maxsize(*maxsize)
1121
+
1122
+ if resizable is not None:
1123
+ window.resizable(*resizable)