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.
- bootstack/__init__.py +249 -0
- bootstack/__main__.py +5 -0
- bootstack/api/__init__.py +127 -0
- bootstack/api/app.py +30 -0
- bootstack/api/constants.py +3 -0
- bootstack/api/data.py +23 -0
- bootstack/api/dialogs.py +44 -0
- bootstack/api/i18n.py +17 -0
- bootstack/api/localization.py +16 -0
- bootstack/api/menu.py +7 -0
- bootstack/api/style.py +23 -0
- bootstack/api/utils.py +24 -0
- bootstack/api/widgets.py +137 -0
- bootstack/assets/__init__.py +24 -0
- bootstack/assets/bootstack-transparent.png +0 -0
- bootstack/assets/bootstack.ico +0 -0
- bootstack/assets/bootstack.png +0 -0
- bootstack/assets/elements/__init__.py +0 -0
- bootstack/assets/elements/badge-pill.png +0 -0
- bootstack/assets/elements/badge-square.png +0 -0
- bootstack/assets/elements/border.png +0 -0
- bootstack/assets/elements/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-default.png +0 -0
- bootstack/assets/elements/checkbox-checked.png +0 -0
- bootstack/assets/elements/checkbox-indeterminate.png +0 -0
- bootstack/assets/elements/checkbox-unchecked.png +0 -0
- bootstack/assets/elements/field.png +0 -0
- bootstack/assets/elements/input-after-compact.png +0 -0
- bootstack/assets/elements/input-after-default.png +0 -0
- bootstack/assets/elements/input-before-compact.png +0 -0
- bootstack/assets/elements/input-before-default.png +0 -0
- bootstack/assets/elements/input-compact.png +0 -0
- bootstack/assets/elements/input-default.png +0 -0
- bootstack/assets/elements/list-item-separated.png +0 -0
- bootstack/assets/elements/list-item.png +0 -0
- bootstack/assets/elements/manifest.toml +480 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/nav-button-compact.png +0 -0
- bootstack/assets/elements/nav-button-default.png +0 -0
- bootstack/assets/elements/nav-icon-button-compact.png +0 -0
- bootstack/assets/elements/nav-icon-button-default.png +0 -0
- bootstack/assets/elements/notebook-client-border.png +0 -0
- bootstack/assets/elements/notebook-tab-active.png +0 -0
- bootstack/assets/elements/notebook-tab-bar.png +0 -0
- bootstack/assets/elements/notebook-tab-normal.png +0 -0
- bootstack/assets/elements/notebook-tab-pill.png +0 -0
- bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/elements/progress-bar-solid.png +0 -0
- bootstack/assets/elements/progress-bar-thin.png +0 -0
- bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/elements/radio-selected.png +0 -0
- bootstack/assets/elements/radio-unselected.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle-focus.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-horizontal.png +0 -0
- bootstack/assets/elements/slider-track-vertical.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
- bootstack/assets/elements/tabs-bar-vertical.png +0 -0
- bootstack/assets/elements/tabs-pill.png +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/themes/__init__.py +0 -0
- bootstack/assets/themes/amber-dark.json +32 -0
- bootstack/assets/themes/amber-light.json +32 -0
- bootstack/assets/themes/aurora-dark.json +32 -0
- bootstack/assets/themes/aurora-light.json +32 -0
- bootstack/assets/themes/bootstrap-dark.json +32 -0
- bootstack/assets/themes/bootstrap-light.json +32 -0
- bootstack/assets/themes/classic-dark.json +32 -0
- bootstack/assets/themes/classic-light.json +32 -0
- bootstack/assets/themes/docs-dark.json +32 -0
- bootstack/assets/themes/docs-light.json +32 -0
- bootstack/assets/themes/forest-dark.json +32 -0
- bootstack/assets/themes/forest-light.json +32 -0
- bootstack/assets/themes/ocean-dark.json +32 -0
- bootstack/assets/themes/ocean-light.json +32 -0
- bootstack/assets/themes/rose-dark.json +32 -0
- bootstack/assets/themes/rose-light.json +32 -0
- bootstack/assets/widgets/__init__.py +0 -0
- bootstack/assets/widgets/badge-default.png +0 -0
- bootstack/assets/widgets/badge-pill.png +0 -0
- bootstack/assets/widgets/border.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
- bootstack/assets/widgets/button-group-vertical-after.png +0 -0
- bootstack/assets/widgets/button-group-vertical-before.png +0 -0
- bootstack/assets/widgets/button-group-vertical-center.png +0 -0
- bootstack/assets/widgets/button.png +0 -0
- bootstack/assets/widgets/checkbox-checked.png +0 -0
- bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
- bootstack/assets/widgets/checkbox-unchecked.png +0 -0
- bootstack/assets/widgets/field.png +0 -0
- bootstack/assets/widgets/icon-button.png +0 -0
- bootstack/assets/widgets/input-inner.png +0 -0
- bootstack/assets/widgets/input-prefix.png +0 -0
- bootstack/assets/widgets/input-suffix.png +0 -0
- bootstack/assets/widgets/input.png +0 -0
- bootstack/assets/widgets/list-item-focus.png +0 -0
- bootstack/assets/widgets/list-item-separated.png +0 -0
- bootstack/assets/widgets/menu-item-separated.png +0 -0
- bootstack/assets/widgets/notebook-client-border.png +0 -0
- bootstack/assets/widgets/notebook-pill-active.png +0 -0
- bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
- bootstack/assets/widgets/notebook-tab-active.png +0 -0
- bootstack/assets/widgets/notebook-tab-border.png +0 -0
- bootstack/assets/widgets/notebook-tab-normal.png +0 -0
- bootstack/assets/widgets/notebook-underline.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
- bootstack/assets/widgets/progress-trough-vertical.png +0 -0
- bootstack/assets/widgets/radio-selected.png +0 -0
- bootstack/assets/widgets/radio-unselected.png +0 -0
- bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
- bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
- bootstack/assets/widgets/separator-horizontal.png +0 -0
- bootstack/assets/widgets/separator-vertical.png +0 -0
- bootstack/assets/widgets/slider-handle-focus.png +0 -0
- bootstack/assets/widgets/slider-handle.png +0 -0
- bootstack/assets/widgets/slider-track-horizontal.png +0 -0
- bootstack/assets/widgets/slider-track-vertical.png +0 -0
- bootstack/assets/widgets/switch-off.png +0 -0
- bootstack/assets/widgets/switch-on.png +0 -0
- bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
- bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
- bootstack/assets/widgets/tabs-pill.png +0 -0
- bootstack/cli/__init__.py +124 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +439 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +287 -0
- bootstack/cli/demo.py +1267 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/list_cmd.py +71 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +246 -0
- bootstack/cli/run.py +99 -0
- bootstack/cli/start.py +105 -0
- bootstack/cli/templates/__init__.py +861 -0
- bootstack/constants.py +325 -0
- bootstack/core/__init__.py +34 -0
- bootstack/core/capabilities/__init__.py +45 -0
- bootstack/core/capabilities/after.py +103 -0
- bootstack/core/capabilities/bind.py +154 -0
- bootstack/core/capabilities/bindtags.py +112 -0
- bootstack/core/capabilities/busy.py +61 -0
- bootstack/core/capabilities/clipboard.py +88 -0
- bootstack/core/capabilities/focus.py +118 -0
- bootstack/core/capabilities/grab.py +65 -0
- bootstack/core/capabilities/grid.py +188 -0
- bootstack/core/capabilities/localization.py +231 -0
- bootstack/core/capabilities/pack.py +119 -0
- bootstack/core/capabilities/place.py +92 -0
- bootstack/core/capabilities/selection.py +136 -0
- bootstack/core/capabilities/signals.py +242 -0
- bootstack/core/capabilities/winfo.py +315 -0
- bootstack/core/colorutils.py +234 -0
- bootstack/core/exceptions.py +95 -0
- bootstack/core/images.py +283 -0
- bootstack/core/localization/README.md +90 -0
- bootstack/core/localization/__init__.py +13 -0
- bootstack/core/localization/intl_format.py +580 -0
- bootstack/core/localization/msgcat.py +425 -0
- bootstack/core/localization/specs.py +143 -0
- bootstack/core/mixins/__init__.py +1 -0
- bootstack/core/mixins/ttk_state.py +35 -0
- bootstack/core/mixins/widget.py +132 -0
- bootstack/core/publisher.py +147 -0
- bootstack/core/signals/README.md +112 -0
- bootstack/core/signals/__init__.py +8 -0
- bootstack/core/signals/integration.py +100 -0
- bootstack/core/signals/signal.py +317 -0
- bootstack/core/signals/types.py +4 -0
- bootstack/core/validation/__init__.py +5 -0
- bootstack/core/validation/types.py +13 -0
- bootstack/core/validation/validation_result.py +17 -0
- bootstack/core/validation/validation_rules.py +112 -0
- bootstack/core/variables.py +62 -0
- bootstack/datasource/README.md +607 -0
- bootstack/datasource/__init__.py +51 -0
- bootstack/datasource/base.py +474 -0
- bootstack/datasource/file_source.py +541 -0
- bootstack/datasource/memory_source.py +482 -0
- bootstack/datasource/sqlite_source.py +453 -0
- bootstack/datasource/types.py +259 -0
- bootstack/dialogs/__init__.py +56 -0
- bootstack/dialogs/colorchooser.py +674 -0
- bootstack/dialogs/colordropper.py +257 -0
- bootstack/dialogs/datedialog.py +404 -0
- bootstack/dialogs/dialog.py +514 -0
- bootstack/dialogs/filterdialog.py +358 -0
- bootstack/dialogs/fontdialog.py +339 -0
- bootstack/dialogs/formdialog.py +541 -0
- bootstack/dialogs/message.py +489 -0
- bootstack/dialogs/query.py +561 -0
- bootstack/py.typed +1 -0
- bootstack/runtime/__init__.py +3 -0
- bootstack/runtime/app.py +879 -0
- bootstack/runtime/base_window.py +786 -0
- bootstack/runtime/events.py +399 -0
- bootstack/runtime/menu.py +510 -0
- bootstack/runtime/shortcuts.py +423 -0
- bootstack/runtime/tk_patch.py +31 -0
- bootstack/runtime/toplevel.py +131 -0
- bootstack/runtime/utility.py +371 -0
- bootstack/runtime/visual_focus.py +228 -0
- bootstack/runtime/window_utilities.py +1043 -0
- bootstack/style/__init__.py +5498 -0
- bootstack/style/bootstyle.py +507 -0
- bootstack/style/bootstyle_builder_base.py +752 -0
- bootstack/style/bootstyle_builder_mixed.py +93 -0
- bootstack/style/bootstyle_builder_tk.py +109 -0
- bootstack/style/bootstyle_builder_ttk.py +354 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +44 -0
- bootstack/style/builders/button.py +453 -0
- bootstack/style/builders/buttongroup.py +344 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +95 -0
- bootstack/style/builders/combobox.py +112 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +83 -0
- bootstack/style/builders/expander.py +171 -0
- bootstack/style/builders/field.py +312 -0
- bootstack/style/builders/frame.py +27 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +41 -0
- bootstack/style/builders/listview.py +267 -0
- bootstack/style/builders/menubar.py +74 -0
- bootstack/style/builders/menubutton.py +408 -0
- bootstack/style/builders/notebook.py +316 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +71 -0
- bootstack/style/builders/radiobutton.py +68 -0
- bootstack/style/builders/scale.py +66 -0
- bootstack/style/builders/scrollbar.py +360 -0
- bootstack/style/builders/separator.py +45 -0
- bootstack/style/builders/sidenav.py +313 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +67 -0
- bootstack/style/builders/tabitem.py +205 -0
- bootstack/style/builders/toolbutton.py +260 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +269 -0
- bootstack/style/builders/utils.py +404 -0
- bootstack/style/builders_tk/__init__.py +16 -0
- bootstack/style/builders_tk/defaults.py +229 -0
- bootstack/style/element.py +173 -0
- bootstack/style/style.py +499 -0
- bootstack/style/theme_provider.py +449 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +42 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +527 -0
- bootstack/style/utility.py +696 -0
- bootstack/themes/__init__.py +12 -0
- bootstack/themes/standard.py +415 -0
- bootstack/themes/user.py +45 -0
- bootstack/widgets/__init__.py +53 -0
- bootstack/widgets/composites/__init__.py +38 -0
- bootstack/widgets/composites/accordion.py +385 -0
- bootstack/widgets/composites/appshell.py +445 -0
- bootstack/widgets/composites/buttongroup.py +391 -0
- bootstack/widgets/composites/calendar.py +914 -0
- bootstack/widgets/composites/compositeframe.py +282 -0
- bootstack/widgets/composites/contextmenu.py +1754 -0
- bootstack/widgets/composites/dateentry.py +261 -0
- bootstack/widgets/composites/dropdownbutton.py +190 -0
- bootstack/widgets/composites/expander.py +508 -0
- bootstack/widgets/composites/field.py +448 -0
- bootstack/widgets/composites/floodgauge.py +434 -0
- bootstack/widgets/composites/form.py +983 -0
- bootstack/widgets/composites/labeledscale.py +209 -0
- bootstack/widgets/composites/list/__init__.py +15 -0
- bootstack/widgets/composites/list/listitem.py +733 -0
- bootstack/widgets/composites/list/listview.py +1507 -0
- bootstack/widgets/composites/menubar.py +303 -0
- bootstack/widgets/composites/meter.py +882 -0
- bootstack/widgets/composites/numericentry.py +183 -0
- bootstack/widgets/composites/pagestack.py +330 -0
- bootstack/widgets/composites/passwordentry.py +149 -0
- bootstack/widgets/composites/pathentry.py +223 -0
- bootstack/widgets/composites/radiogroup.py +466 -0
- bootstack/widgets/composites/scrolledtext.py +388 -0
- bootstack/widgets/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/composites/scrollview.py +675 -0
- bootstack/widgets/composites/selectbox.py +544 -0
- bootstack/widgets/composites/sidenav/__init__.py +24 -0
- bootstack/widgets/composites/sidenav/group.py +485 -0
- bootstack/widgets/composites/sidenav/header.py +83 -0
- bootstack/widgets/composites/sidenav/item.py +413 -0
- bootstack/widgets/composites/sidenav/separator.py +51 -0
- bootstack/widgets/composites/sidenav/view.py +919 -0
- bootstack/widgets/composites/spinnerentry.py +232 -0
- bootstack/widgets/composites/tableview/__init__.py +5 -0
- bootstack/widgets/composites/tableview/tableview.py +2254 -0
- bootstack/widgets/composites/tableview/types.py +169 -0
- bootstack/widgets/composites/tabs/__init__.py +6 -0
- bootstack/widgets/composites/tabs/tabitem.py +372 -0
- bootstack/widgets/composites/tabs/tabs.py +478 -0
- bootstack/widgets/composites/tabs/tabview.py +352 -0
- bootstack/widgets/composites/textentry.py +90 -0
- bootstack/widgets/composites/timeentry.py +189 -0
- bootstack/widgets/composites/toast.py +364 -0
- bootstack/widgets/composites/togglegroup.py +382 -0
- bootstack/widgets/composites/toolbar.py +393 -0
- bootstack/widgets/composites/tooltip.py +404 -0
- bootstack/widgets/internal/__init__.py +0 -0
- bootstack/widgets/internal/wrapper_base.py +304 -0
- bootstack/widgets/mixins/__init__.py +25 -0
- bootstack/widgets/mixins/configure_mixin.py +186 -0
- bootstack/widgets/mixins/entry_mixin.py +70 -0
- bootstack/widgets/mixins/font_mixin.py +346 -0
- bootstack/widgets/mixins/icon_mixin.py +38 -0
- bootstack/widgets/mixins/localization_mixin.py +255 -0
- bootstack/widgets/mixins/signal_mixin.py +272 -0
- bootstack/widgets/mixins/validation_mixin.py +204 -0
- bootstack/widgets/parts/__init__.py +11 -0
- bootstack/widgets/parts/numberentry_part.py +345 -0
- bootstack/widgets/parts/spinnerentry_part.py +394 -0
- bootstack/widgets/parts/textentry_part.py +344 -0
- bootstack/widgets/primitives/__init__.py +55 -0
- bootstack/widgets/primitives/badge.py +44 -0
- bootstack/widgets/primitives/button.py +89 -0
- bootstack/widgets/primitives/card.py +66 -0
- bootstack/widgets/primitives/checkbutton.py +124 -0
- bootstack/widgets/primitives/checktoggle.py +53 -0
- bootstack/widgets/primitives/combobox.py +165 -0
- bootstack/widgets/primitives/entry.py +98 -0
- bootstack/widgets/primitives/frame.py +206 -0
- bootstack/widgets/primitives/gridframe.py +479 -0
- bootstack/widgets/primitives/label.py +95 -0
- bootstack/widgets/primitives/labelframe.py +63 -0
- bootstack/widgets/primitives/menubutton.py +118 -0
- bootstack/widgets/primitives/notebook.py +551 -0
- bootstack/widgets/primitives/optionmenu.py +248 -0
- bootstack/widgets/primitives/packframe.py +228 -0
- bootstack/widgets/primitives/panedwindow.py +58 -0
- bootstack/widgets/primitives/progressbar.py +95 -0
- bootstack/widgets/primitives/radiobutton.py +115 -0
- bootstack/widgets/primitives/radiotoggle.py +50 -0
- bootstack/widgets/primitives/scale.py +85 -0
- bootstack/widgets/primitives/scrollbar.py +56 -0
- bootstack/widgets/primitives/separator.py +56 -0
- bootstack/widgets/primitives/sizegrip.py +47 -0
- bootstack/widgets/primitives/spinbox.py +91 -0
- bootstack/widgets/primitives/switch.py +41 -0
- bootstack/widgets/primitives/treeview.py +77 -0
- bootstack/widgets/types.py +20 -0
- bootstack-0.1.0a1.dist-info/METADATA +196 -0
- bootstack-0.1.0a1.dist-info/RECORD +419 -0
- bootstack-0.1.0a1.dist-info/WHEEL +5 -0
- bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
- bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
- 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)
|