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,257 @@
|
|
|
1
|
+
"""Screen color picker (dropper) dialog for bootstack.
|
|
2
|
+
|
|
3
|
+
This module provides a color dropper tool that allows users to select colors
|
|
4
|
+
directly from anywhere on the screen. It captures a screenshot and displays
|
|
5
|
+
a magnified view to help with precise color selection.
|
|
6
|
+
"""
|
|
7
|
+
import tkinter as tk
|
|
8
|
+
from collections import namedtuple
|
|
9
|
+
from types import SimpleNamespace
|
|
10
|
+
from typing import Any, Callable, Optional
|
|
11
|
+
|
|
12
|
+
from tkinter import Canvas, Variable
|
|
13
|
+
from PIL import ImageGrab, ImageTk
|
|
14
|
+
from PIL.Image import Resampling
|
|
15
|
+
|
|
16
|
+
from bootstack.runtime.toplevel import Toplevel
|
|
17
|
+
import bootstack.core.colorutils as colorutils
|
|
18
|
+
import bootstack.runtime.utility as utility
|
|
19
|
+
from bootstack.constants import *
|
|
20
|
+
|
|
21
|
+
ttk = SimpleNamespace(Canvas=Canvas, Toplevel=Toplevel, Variable=Variable)
|
|
22
|
+
|
|
23
|
+
ColorChoice = namedtuple('ColorChoice', 'rgb hsl hex')
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ColorDropperDialog:
|
|
27
|
+
"""Screen color picker with zoom preview.
|
|
28
|
+
|
|
29
|
+
Click anywhere on screen to select a color. The selected color is stored in
|
|
30
|
+
`result` as a ColorChoice named tuple with rgb, hsl, and hex values.
|
|
31
|
+
|
|
32
|
+
Note:
|
|
33
|
+
Supported on Windows and Linux. macOS is not supported due to ImageGrab
|
|
34
|
+
limitations. On high-DPI displays, ensure the app runs in high-DPI mode.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self) -> None:
|
|
38
|
+
"""Initialize the ColorDropperDialog and prepare all state attributes."""
|
|
39
|
+
self.zoom_yoffset = None
|
|
40
|
+
self.zoom_xoffset = None
|
|
41
|
+
self.zoom_width = None
|
|
42
|
+
self.zoom_height = None
|
|
43
|
+
self.zoom_image = None
|
|
44
|
+
self.zoom_data = None
|
|
45
|
+
self.zoom_level = None
|
|
46
|
+
self.screenshot_image = None
|
|
47
|
+
self.screenshot_data = None
|
|
48
|
+
self.screenshot_canvas = None
|
|
49
|
+
self.toplevel: Optional[ttk.Toplevel] = None
|
|
50
|
+
self.zoom_toplevel: Optional[ttk.Toplevel] = None
|
|
51
|
+
self.result: ttk.Variable = ttk.Variable()
|
|
52
|
+
self._emitted_result = False
|
|
53
|
+
|
|
54
|
+
def build_screenshot_canvas(self) -> None:
|
|
55
|
+
"""Build the screenshot canvas"""
|
|
56
|
+
self.screenshot_canvas: ttk.Canvas = ttk.Canvas(self.toplevel, cursor='tcross')
|
|
57
|
+
self.screenshot_data = ImageGrab.grab()
|
|
58
|
+
self.screenshot_image: ImageTk.PhotoImage = ImageTk.PhotoImage(self.screenshot_data)
|
|
59
|
+
self.screenshot_canvas.create_image(
|
|
60
|
+
0, 0, image=self.screenshot_image, anchor=NW)
|
|
61
|
+
self.screenshot_canvas.pack(fill=BOTH, expand=YES)
|
|
62
|
+
|
|
63
|
+
def build_zoom_toplevel(self, master) -> None:
|
|
64
|
+
"""Build the toplevel widget that shows the zoomed version of
|
|
65
|
+
the pixels underneath the mouse cursor."""
|
|
66
|
+
height = utility.scale_size(self.toplevel, 100)
|
|
67
|
+
width = utility.scale_size(self.toplevel, 100)
|
|
68
|
+
text_xoffset = utility.scale_size(self.toplevel, 50)
|
|
69
|
+
text_yoffset = utility.scale_size(self.toplevel, 50)
|
|
70
|
+
toplevel = ttk.Toplevel(master=master)
|
|
71
|
+
toplevel.transient(master=master)
|
|
72
|
+
if self.toplevel and self.toplevel.winsys == 'x11':
|
|
73
|
+
toplevel.attributes('-type', 'tooltip')
|
|
74
|
+
else:
|
|
75
|
+
toplevel.overrideredirect(True)
|
|
76
|
+
toplevel.geometry(f'{width}x{height}')
|
|
77
|
+
toplevel.lift()
|
|
78
|
+
self.zoom_canvas: ttk.Canvas = ttk.Canvas(
|
|
79
|
+
toplevel, borderwidth=1, height=self.zoom_height, width=self.zoom_width)
|
|
80
|
+
self.zoom_canvas.create_image(0, 0, tags=['image'], anchor=NW)
|
|
81
|
+
self.zoom_canvas.create_text(
|
|
82
|
+
text_xoffset, text_yoffset, text="+", fill="white", tags=['indicator'])
|
|
83
|
+
self.zoom_canvas.pack(fill=BOTH, expand=YES)
|
|
84
|
+
self.zoom_toplevel = toplevel
|
|
85
|
+
|
|
86
|
+
def _cleanup(self) -> None:
|
|
87
|
+
"""Destroy zoom and main toplevels."""
|
|
88
|
+
if self.zoom_toplevel and self.zoom_toplevel.winfo_exists():
|
|
89
|
+
try:
|
|
90
|
+
self.zoom_toplevel.destroy()
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
if self.toplevel and self.toplevel.winfo_exists():
|
|
94
|
+
try:
|
|
95
|
+
self.toplevel.destroy()
|
|
96
|
+
except Exception:
|
|
97
|
+
pass
|
|
98
|
+
|
|
99
|
+
def on_mouse_wheel(self, event: tk.Event) -> None:
|
|
100
|
+
"""Zoom in and out on the image underneath the mouse"""
|
|
101
|
+
delta = 0
|
|
102
|
+
if self.toplevel and self.toplevel.winsys.lower() == 'win32':
|
|
103
|
+
delta = -int(event.delta / 120)
|
|
104
|
+
elif self.toplevel and self.toplevel.winsys.lower() == 'aqua':
|
|
105
|
+
delta = -event.delta
|
|
106
|
+
elif event.num == 4:
|
|
107
|
+
delta = -1
|
|
108
|
+
elif event.num == 5:
|
|
109
|
+
delta = 1
|
|
110
|
+
self.zoom_level += delta
|
|
111
|
+
self._on_mouse_motion()
|
|
112
|
+
|
|
113
|
+
def on_left_click(self, _: tk.Event) -> Optional[ColorChoice]:
|
|
114
|
+
"""Capture the color underneath the mouse cursor and destroy
|
|
115
|
+
the toplevel widget"""
|
|
116
|
+
# add logic here to capture the image color
|
|
117
|
+
hx = self.get_hover_color()
|
|
118
|
+
hsl = colorutils.color_to_hsl(hx)
|
|
119
|
+
rgb = colorutils.color_to_rgb(hx)
|
|
120
|
+
self.result.set(ColorChoice(rgb, hsl, hx))
|
|
121
|
+
if self.toplevel:
|
|
122
|
+
self.toplevel.grab_release()
|
|
123
|
+
self.toplevel.destroy()
|
|
124
|
+
if self.zoom_toplevel:
|
|
125
|
+
self.zoom_toplevel.destroy()
|
|
126
|
+
return self.result.get()
|
|
127
|
+
|
|
128
|
+
def on_right_click(self, _: tk.Event) -> None:
|
|
129
|
+
"""Close the color dropper without saving any color information"""
|
|
130
|
+
if self.zoom_toplevel:
|
|
131
|
+
self.zoom_toplevel.destroy()
|
|
132
|
+
if self.toplevel:
|
|
133
|
+
self.toplevel.grab_release()
|
|
134
|
+
self.toplevel.destroy()
|
|
135
|
+
|
|
136
|
+
def _on_mouse_motion(self, event: Optional[tk.Event] = None) -> None:
|
|
137
|
+
"""Callback for mouse motion"""
|
|
138
|
+
if event is None:
|
|
139
|
+
x, y = self.toplevel.winfo_pointerxy() # type: ignore[union-attr]
|
|
140
|
+
else:
|
|
141
|
+
x = event.x
|
|
142
|
+
y = event.y
|
|
143
|
+
# move snip window
|
|
144
|
+
self.zoom_toplevel.geometry(
|
|
145
|
+
f'+{x + self.zoom_xoffset}+{y + self.zoom_yoffset}')
|
|
146
|
+
# update the snip image
|
|
147
|
+
bbox = (x - self.zoom_level, y - self.zoom_level,
|
|
148
|
+
x + self.zoom_level + 1, y + self.zoom_level + 1)
|
|
149
|
+
size = (self.zoom_width, self.zoom_height)
|
|
150
|
+
self.zoom_data = self.screenshot_data.crop(
|
|
151
|
+
bbox).resize(size, Resampling.BOX)
|
|
152
|
+
self.zoom_image: ImageTk.PhotoImage = ImageTk.PhotoImage(self.zoom_data)
|
|
153
|
+
self.zoom_canvas.itemconfig('image', image=self.zoom_image)
|
|
154
|
+
hover_color = self.get_hover_color()
|
|
155
|
+
contrast_color = colorutils.contrast_color(hover_color, 'hex')
|
|
156
|
+
self.zoom_canvas.itemconfig('indicator', fill=contrast_color)
|
|
157
|
+
|
|
158
|
+
def get_hover_color(self) -> str:
|
|
159
|
+
"""Get the color that is hovered over by the mouse cursor."""
|
|
160
|
+
x1, y1, x2, y2 = self.zoom_canvas.bbox('indicator')
|
|
161
|
+
x = x1 + (x2 - x1) // 2
|
|
162
|
+
y = y1 + (y2 - y1) // 2
|
|
163
|
+
r, g, b = self.zoom_data.getpixel((x, y))
|
|
164
|
+
hx = colorutils.color_to_hex((r, g, b))
|
|
165
|
+
return hx
|
|
166
|
+
|
|
167
|
+
# event helpers -----------------------------------------------------------
|
|
168
|
+
def on_dialog_result(self, callback: Callable[[Any], None]) -> Optional[str]:
|
|
169
|
+
"""Bind a callback fired when the dropper produces a result."""
|
|
170
|
+
target = self.toplevel
|
|
171
|
+
if target is None:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
def handler(event):
|
|
175
|
+
callback(getattr(event, "data", None))
|
|
176
|
+
|
|
177
|
+
return target.bind("<<DialogResult>>", handler, add="+")
|
|
178
|
+
|
|
179
|
+
def off_dialog_result(self, funcid: str) -> None:
|
|
180
|
+
"""Unbind a previously bound dialog result callback."""
|
|
181
|
+
target = self.toplevel
|
|
182
|
+
if target is None:
|
|
183
|
+
return
|
|
184
|
+
target.unbind("<<DialogResult>>", funcid)
|
|
185
|
+
|
|
186
|
+
def _emit_result(self, confirmed: bool) -> None:
|
|
187
|
+
"""Emit the dialog result event once."""
|
|
188
|
+
if self._emitted_result:
|
|
189
|
+
return
|
|
190
|
+
payload = {"result": self.result.get() if hasattr(self.result, "get") else None, "confirmed": confirmed}
|
|
191
|
+
target = self.toplevel
|
|
192
|
+
if not target:
|
|
193
|
+
return
|
|
194
|
+
try:
|
|
195
|
+
target.event_generate("<<DialogResult>>", data=payload)
|
|
196
|
+
except Exception:
|
|
197
|
+
try:
|
|
198
|
+
target.event_generate("<<DialogResult>>")
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
self._emitted_result = True
|
|
202
|
+
|
|
203
|
+
def show(self) -> None:
|
|
204
|
+
"""Show the toplevel window"""
|
|
205
|
+
self._emitted_result = False
|
|
206
|
+
self.toplevel = ttk.Toplevel(alpha=1)
|
|
207
|
+
self.toplevel.wm_attributes('-fullscreen', True)
|
|
208
|
+
self.build_screenshot_canvas()
|
|
209
|
+
|
|
210
|
+
# event binding
|
|
211
|
+
self.toplevel.bind("<Motion>", self._on_mouse_motion, "+")
|
|
212
|
+
self.toplevel.bind("<Button-1>", self._on_left_click, "+")
|
|
213
|
+
utility.bind_right_click(self.toplevel, self._on_right_click)
|
|
214
|
+
self.toplevel.bind("<Escape>", self._on_cancel, "+")
|
|
215
|
+
|
|
216
|
+
if self.toplevel.winsys.lower() == 'x11':
|
|
217
|
+
self.toplevel.bind("<Button-4>", self.on_mouse_wheel, "+")
|
|
218
|
+
self.toplevel.bind("<Button-5>", self.on_mouse_wheel, "+")
|
|
219
|
+
else:
|
|
220
|
+
self.toplevel.bind("<MouseWheel>", self.on_mouse_wheel, "+")
|
|
221
|
+
|
|
222
|
+
# initial snip setup
|
|
223
|
+
self.zoom_level: int = 2
|
|
224
|
+
self.zoom_toplevel: Optional[ttk.Toplevel] = None
|
|
225
|
+
self.zoom_data: Any = None
|
|
226
|
+
self.zoom_image: Optional[ImageTk.PhotoImage] = None
|
|
227
|
+
self.zoom_height: int = utility.scale_size(self.toplevel, 100)
|
|
228
|
+
self.zoom_width: int = utility.scale_size(self.toplevel, 100)
|
|
229
|
+
self.zoom_xoffset: int = utility.scale_size(self.toplevel, 10)
|
|
230
|
+
self.zoom_yoffset: int = utility.scale_size(self.toplevel, 10)
|
|
231
|
+
|
|
232
|
+
self.build_zoom_toplevel(self.toplevel)
|
|
233
|
+
self.toplevel.grab_set()
|
|
234
|
+
self.toplevel.lift('.')
|
|
235
|
+
self.zoom_toplevel.lift(self.toplevel)
|
|
236
|
+
|
|
237
|
+
self._on_mouse_motion()
|
|
238
|
+
|
|
239
|
+
def _on_left_click(self, _: tk.Event) -> Optional[ColorChoice]:
|
|
240
|
+
"""Capture the color underneath the mouse cursor and close."""
|
|
241
|
+
hx = self.get_hover_color()
|
|
242
|
+
hsl = colorutils.color_to_hsl(hx)
|
|
243
|
+
rgb = colorutils.color_to_rgb(hx)
|
|
244
|
+
self.result.set(ColorChoice(rgb, hsl, hx))
|
|
245
|
+
self._emit_result(confirmed=True)
|
|
246
|
+
self._cleanup()
|
|
247
|
+
return self.result.get()
|
|
248
|
+
|
|
249
|
+
def _on_right_click(self, _: tk.Event) -> None:
|
|
250
|
+
"""Close without saving any color information."""
|
|
251
|
+
self.result.set(None)
|
|
252
|
+
self._emit_result(confirmed=False)
|
|
253
|
+
self._cleanup()
|
|
254
|
+
|
|
255
|
+
def _on_cancel(self, _: tk.Event) -> None:
|
|
256
|
+
"""Close without selection (Escape)."""
|
|
257
|
+
self._on_right_click(_)
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"""Dialog wrapper around the Calendar widget.
|
|
2
|
+
|
|
3
|
+
Exposes a chrome-less, popover-capable date picker dialog that can close on
|
|
4
|
+
outside clicks and forwards Calendar options (disabled dates, bounds, etc.).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import tkinter
|
|
10
|
+
from datetime import date, datetime
|
|
11
|
+
from types import SimpleNamespace
|
|
12
|
+
from typing import Any, Callable, Iterable, Literal, Optional, Tuple, Union
|
|
13
|
+
from tkinter import Widget
|
|
14
|
+
|
|
15
|
+
from bootstack.widgets.primitives import Frame
|
|
16
|
+
from bootstack.constants import BOTH, PRIMARY, YES
|
|
17
|
+
from bootstack.dialogs.dialog import Dialog
|
|
18
|
+
from bootstack.runtime.window_utilities import AnchorPoint
|
|
19
|
+
from bootstack.widgets.composites.calendar import Calendar
|
|
20
|
+
|
|
21
|
+
ttk = SimpleNamespace(Frame=Frame)
|
|
22
|
+
|
|
23
|
+
__all__ = ["DateDialog"]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class _ChromeDialog(Dialog):
|
|
27
|
+
"""Dialog that can optionally hide window chrome via override-redirect."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, *args: Any, hide_window_chrome: bool = False, **kwargs: Any) -> None:
|
|
30
|
+
self._hide_window_chrome = hide_window_chrome
|
|
31
|
+
self._suppress_focus_out = False
|
|
32
|
+
self._outside_click_binding: str | None = None
|
|
33
|
+
super().__init__(*args, **kwargs)
|
|
34
|
+
|
|
35
|
+
def _create_toplevel(self):
|
|
36
|
+
super()._create_toplevel()
|
|
37
|
+
if self._hide_window_chrome and self._toplevel:
|
|
38
|
+
# Note: overrideredirect is automatically disabled on macOS
|
|
39
|
+
# in BaseWindow.overrideredirect() due to Tk/Cocoa issues
|
|
40
|
+
try:
|
|
41
|
+
self._toplevel.overrideredirect(True)
|
|
42
|
+
except Exception:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def show(
|
|
46
|
+
self,
|
|
47
|
+
position: Optional[Tuple[int, int]] = None,
|
|
48
|
+
modal: Optional[bool] = None,
|
|
49
|
+
*,
|
|
50
|
+
anchor_to: Optional[Union[Widget, Literal["screen", "cursor", "parent"]]] = None,
|
|
51
|
+
anchor_point: AnchorPoint = 'center',
|
|
52
|
+
window_point: AnchorPoint = 'center',
|
|
53
|
+
offset: Tuple[int, int] = (0, 0),
|
|
54
|
+
auto_flip: Union[bool, Literal['vertical', 'horizontal']] = False
|
|
55
|
+
):
|
|
56
|
+
"""Override show to position before deiconify, avoiding placement flash.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
position: Optional (x, y) coordinates to position the dialog.
|
|
60
|
+
modal: Override the mode's default modality.
|
|
61
|
+
anchor_to: Positioning target (Widget, "screen", "cursor", "parent", or None).
|
|
62
|
+
anchor_point: Point on the anchor target (n, s, e, w, ne, nw, se, sw, center).
|
|
63
|
+
window_point: Point on the dialog window (n, s, e, w, ne, nw, se, sw, center).
|
|
64
|
+
offset: Additional (x, y) offset in pixels from the anchor position.
|
|
65
|
+
auto_flip: Smart positioning to keep window on screen.
|
|
66
|
+
"""
|
|
67
|
+
if modal is None:
|
|
68
|
+
modal = (self._mode == "modal")
|
|
69
|
+
|
|
70
|
+
self.result = None
|
|
71
|
+
self._create_toplevel()
|
|
72
|
+
if self._hide_window_chrome and self._toplevel:
|
|
73
|
+
self._toplevel.withdraw()
|
|
74
|
+
|
|
75
|
+
self._build_content()
|
|
76
|
+
self._build_footer()
|
|
77
|
+
|
|
78
|
+
self._position_dialog(
|
|
79
|
+
position=position,
|
|
80
|
+
anchor_to=anchor_to,
|
|
81
|
+
anchor_point=anchor_point,
|
|
82
|
+
window_point=window_point,
|
|
83
|
+
offset=offset,
|
|
84
|
+
auto_flip=auto_flip
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if self._hide_window_chrome and self._toplevel:
|
|
88
|
+
self._toplevel.deiconify()
|
|
89
|
+
try:
|
|
90
|
+
self._toplevel.lift()
|
|
91
|
+
self._toplevel.focus_force()
|
|
92
|
+
except Exception:
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
if self._alert:
|
|
96
|
+
self._toplevel.bell()
|
|
97
|
+
|
|
98
|
+
if self._mode == "popover":
|
|
99
|
+
self._suppress_focus_out = True
|
|
100
|
+
self._toplevel.bind("<FocusOut>", self._on_focus_out, add="+")
|
|
101
|
+
try:
|
|
102
|
+
self._toplevel.after(50, lambda: setattr(self, "_suppress_focus_out", False))
|
|
103
|
+
except Exception:
|
|
104
|
+
self._suppress_focus_out = False
|
|
105
|
+
self._bind_outside_click()
|
|
106
|
+
|
|
107
|
+
if modal:
|
|
108
|
+
# transient() is already called in Dialog._create_toplevel()
|
|
109
|
+
# Don't call it again here as it breaks mica effect on Windows
|
|
110
|
+
if self._mode == "modal":
|
|
111
|
+
self._toplevel.grab_set()
|
|
112
|
+
self._master.wait_window(self._toplevel)
|
|
113
|
+
|
|
114
|
+
def _on_focus_out(self, event: tkinter.Event):
|
|
115
|
+
if self._suppress_focus_out:
|
|
116
|
+
return
|
|
117
|
+
return super()._on_focus_out(event)
|
|
118
|
+
|
|
119
|
+
def _bind_outside_click(self) -> None:
|
|
120
|
+
"""Close popover when clicking outside the toplevel."""
|
|
121
|
+
if not self._toplevel:
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
def handler(event: tkinter.Event) -> None:
|
|
125
|
+
widget = getattr(event, "widget", None)
|
|
126
|
+
if widget is None:
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Walk up the widget hierarchy to check if any parent is the toplevel
|
|
130
|
+
current = widget
|
|
131
|
+
toplevel_str = str(self._toplevel)
|
|
132
|
+
while current:
|
|
133
|
+
current_str = str(current)
|
|
134
|
+
if current_str == toplevel_str or current_str.startswith(toplevel_str + "."):
|
|
135
|
+
return
|
|
136
|
+
try:
|
|
137
|
+
current = current.master
|
|
138
|
+
except AttributeError:
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
# Schedule destroy for after current event processing completes
|
|
142
|
+
# This allows button commands inside the dialog to execute first
|
|
143
|
+
try:
|
|
144
|
+
if self._toplevel.winfo_exists():
|
|
145
|
+
self._toplevel.after_idle(lambda: self._destroy_if_exists())
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
self._outside_click_binding = self._toplevel.bind_all("<ButtonPress-1>", handler, add="+")
|
|
151
|
+
self._toplevel.bind("<Destroy>", lambda e: self._unbind_outside_click(), add="+")
|
|
152
|
+
except Exception:
|
|
153
|
+
self._outside_click_binding = None
|
|
154
|
+
|
|
155
|
+
def _destroy_if_exists(self) -> None:
|
|
156
|
+
"""Destroy the toplevel if it still exists."""
|
|
157
|
+
try:
|
|
158
|
+
if self._toplevel and self._toplevel.winfo_exists():
|
|
159
|
+
self._toplevel.destroy()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
def _unbind_outside_click(self) -> None:
|
|
164
|
+
if self._toplevel and self._outside_click_binding:
|
|
165
|
+
try:
|
|
166
|
+
self._toplevel.unbind_all("<ButtonPress-1>", self._outside_click_binding)
|
|
167
|
+
except Exception:
|
|
168
|
+
pass
|
|
169
|
+
self._outside_click_binding = None
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _DialogCalendar(Calendar):
|
|
173
|
+
"""Calendar variant that records why a selection event fired."""
|
|
174
|
+
|
|
175
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
176
|
+
super().__init__(*args, **kwargs)
|
|
177
|
+
self._last_trigger_reason: str | None = None
|
|
178
|
+
|
|
179
|
+
def _on_reset_date(self, *args: Any) -> None:
|
|
180
|
+
self._last_trigger_reason = "reset"
|
|
181
|
+
super()._on_reset_date(*args)
|
|
182
|
+
|
|
183
|
+
def _on_date_selected_by_date(self, target: date) -> None:
|
|
184
|
+
self._last_trigger_reason = "select"
|
|
185
|
+
super()._on_date_selected_by_date(target)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
class DateDialog:
|
|
189
|
+
"""Modal dialog that displays a Calendar"""
|
|
190
|
+
|
|
191
|
+
def __init__(
|
|
192
|
+
self,
|
|
193
|
+
master: Optional[tkinter.Misc] = None,
|
|
194
|
+
title: str = " ",
|
|
195
|
+
initial_date: Optional[date] = None,
|
|
196
|
+
first_weekday: int = 6,
|
|
197
|
+
accent: str = None,
|
|
198
|
+
disabled_dates: Optional[Iterable[date | datetime | str]] = None,
|
|
199
|
+
min_date: Optional[date | datetime | str] = None,
|
|
200
|
+
max_date: Optional[date | datetime | str] = None,
|
|
201
|
+
show_outside_days: Optional[bool] = None,
|
|
202
|
+
show_week_numbers: bool = False,
|
|
203
|
+
hide_window_chrome: bool = False,
|
|
204
|
+
close_on_click_outside: bool = False,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""Create a date selection dialog.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
master: Parent widget; positions dialog relative to it when set.
|
|
210
|
+
title: Dialog window title text.
|
|
211
|
+
initial_date: Initial date shown; defaults to `date.today()`.
|
|
212
|
+
first_weekday: First weekday index (0=Monday, 6=Sunday).
|
|
213
|
+
accent: Calendar accent token (e.g., `primary`, `secondary`).
|
|
214
|
+
disabled_dates: Iterable of dates to disable selection.
|
|
215
|
+
min_date: Lower bound for selectable dates.
|
|
216
|
+
max_date: Upper bound for selectable dates.
|
|
217
|
+
show_outside_days: Whether to show outside-month days. Defaults to
|
|
218
|
+
the Calendar behavior (True for single month).
|
|
219
|
+
show_week_numbers: Display ISO week numbers beside each row.
|
|
220
|
+
hide_window_chrome: When True, displays the dialog with no window
|
|
221
|
+
decorations using override-redirect.
|
|
222
|
+
close_on_click_outside: When True, closes the dialog when focus
|
|
223
|
+
moves outside (popover mode).
|
|
224
|
+
"""
|
|
225
|
+
self._master = master
|
|
226
|
+
self._first_weekday = first_weekday
|
|
227
|
+
self._initial_date = initial_date or datetime.today().date()
|
|
228
|
+
self._accent = accent or PRIMARY
|
|
229
|
+
self._disabled_dates = disabled_dates
|
|
230
|
+
self._min_date = min_date
|
|
231
|
+
self._max_date = max_date
|
|
232
|
+
self._show_outside_days = show_outside_days
|
|
233
|
+
self._show_week_numbers = show_week_numbers
|
|
234
|
+
self._hide_window_chrome = hide_window_chrome
|
|
235
|
+
self._close_on_click_outside = close_on_click_outside
|
|
236
|
+
|
|
237
|
+
self._picker: Optional[_DialogCalendar] = None
|
|
238
|
+
|
|
239
|
+
self._dialog = _ChromeDialog(
|
|
240
|
+
master=master,
|
|
241
|
+
title=title,
|
|
242
|
+
content_builder=self._create_content,
|
|
243
|
+
buttons=[],
|
|
244
|
+
footer_builder=None,
|
|
245
|
+
hide_window_chrome=self._hide_window_chrome,
|
|
246
|
+
mode="popover" if self._close_on_click_outside else "modal",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
def _create_content(self, master: tkinter.Widget) -> None:
|
|
250
|
+
"""Build the Calendar content inside the dialog."""
|
|
251
|
+
container = ttk.Frame(master, padding=2, show_border=True)
|
|
252
|
+
container.pack(fill=BOTH, expand=YES)
|
|
253
|
+
|
|
254
|
+
self._picker = _DialogCalendar(
|
|
255
|
+
master=container,
|
|
256
|
+
start_date=self._initial_date,
|
|
257
|
+
first_weekday=self._first_weekday,
|
|
258
|
+
accent=self._accent,
|
|
259
|
+
disabled_dates=self._disabled_dates,
|
|
260
|
+
min_date=self._min_date,
|
|
261
|
+
max_date=self._max_date,
|
|
262
|
+
show_outside_days=self._show_outside_days,
|
|
263
|
+
show_week_numbers=self._show_week_numbers,
|
|
264
|
+
padding=0,
|
|
265
|
+
)
|
|
266
|
+
self._picker.pack(fill=BOTH, expand=YES)
|
|
267
|
+
self._picker.on_date_selected(self._on_date_selected)
|
|
268
|
+
|
|
269
|
+
def _on_date_selected(self, event: tkinter.Event) -> None:
|
|
270
|
+
"""Handle <<DateSelect>> from the embedded Calendar."""
|
|
271
|
+
if not self._picker:
|
|
272
|
+
return
|
|
273
|
+
trigger_reason = getattr(self._picker, "_last_trigger_reason", None)
|
|
274
|
+
if trigger_reason != "select":
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
payload = getattr(event, "data", None)
|
|
278
|
+
selected = None
|
|
279
|
+
if isinstance(payload, dict):
|
|
280
|
+
selected = payload.get("date") or payload.get("result")
|
|
281
|
+
|
|
282
|
+
selected = selected or self._picker.get()
|
|
283
|
+
if selected is None:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
self._dialog.result = selected
|
|
287
|
+
self._emit_result(selected, confirmed=True)
|
|
288
|
+
if self._dialog.toplevel:
|
|
289
|
+
top = self._dialog.toplevel
|
|
290
|
+
try:
|
|
291
|
+
top.grab_release()
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
top.destroy()
|
|
295
|
+
|
|
296
|
+
def show(
|
|
297
|
+
self,
|
|
298
|
+
position: Optional[Tuple[int, int]] = None,
|
|
299
|
+
modal: Optional[bool] = None,
|
|
300
|
+
*,
|
|
301
|
+
anchor_to: Optional[Union[Widget, Literal["screen", "cursor", "parent"]]] = None,
|
|
302
|
+
anchor_point: AnchorPoint = 'center',
|
|
303
|
+
window_point: AnchorPoint = 'center',
|
|
304
|
+
offset: Tuple[int, int] = (0, 0),
|
|
305
|
+
auto_flip: Union[bool, Literal['vertical', 'horizontal']] = False
|
|
306
|
+
) -> None:
|
|
307
|
+
"""Display the dialog and block until closed.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
position: Optional (x, y) coordinates to position the dialog.
|
|
311
|
+
If provided, takes precedence over anchor-based positioning.
|
|
312
|
+
If omitted and anchor_to is not provided, positions at the parent's
|
|
313
|
+
bottom-right when available, otherwise centers.
|
|
314
|
+
modal: Override the mode's default modality.
|
|
315
|
+
If None, uses True for modal mode dialogs.
|
|
316
|
+
anchor_to: Positioning target. Can be:
|
|
317
|
+
- Widget: Anchor to a specific widget
|
|
318
|
+
- "screen": Anchor to screen edges/corners
|
|
319
|
+
- "cursor": Anchor to mouse cursor location
|
|
320
|
+
- "parent": Anchor to parent window (same as widget)
|
|
321
|
+
- None: Uses default positioning behavior
|
|
322
|
+
anchor_point: Point on the anchor target (n, s, e, w, ne, nw, se, sw, center).
|
|
323
|
+
Default 'center'.
|
|
324
|
+
window_point: Point on the dialog window (n, s, e, w, ne, nw, se, sw, center).
|
|
325
|
+
Default 'center'.
|
|
326
|
+
offset: Additional (x, y) offset in pixels from the anchor position.
|
|
327
|
+
auto_flip: Smart positioning to keep window on screen.
|
|
328
|
+
- False: No flipping (default)
|
|
329
|
+
- True: Flip both vertically and horizontally as needed
|
|
330
|
+
- 'vertical': Only flip up/down
|
|
331
|
+
- 'horizontal': Only flip left/right
|
|
332
|
+
"""
|
|
333
|
+
# Default positioning: bottom-right of parent if no positioning options provided
|
|
334
|
+
if position is None and anchor_to is None and self._master:
|
|
335
|
+
try:
|
|
336
|
+
x = self._master.winfo_rootx() + self._master.winfo_width()
|
|
337
|
+
y = self._master.winfo_rooty() + self._master.winfo_height()
|
|
338
|
+
position = (x, y)
|
|
339
|
+
except Exception:
|
|
340
|
+
pass
|
|
341
|
+
|
|
342
|
+
# Default modal to True if not specified
|
|
343
|
+
if modal is None:
|
|
344
|
+
modal = True
|
|
345
|
+
|
|
346
|
+
self._dialog.show(
|
|
347
|
+
position=position,
|
|
348
|
+
modal=modal,
|
|
349
|
+
anchor_to=anchor_to,
|
|
350
|
+
anchor_point=anchor_point,
|
|
351
|
+
window_point=window_point,
|
|
352
|
+
offset=offset,
|
|
353
|
+
auto_flip=auto_flip
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def result(self) -> Optional[date]:
|
|
358
|
+
"""The selected date, or None if cancelled."""
|
|
359
|
+
return self._dialog.result
|
|
360
|
+
|
|
361
|
+
def on_result(self, callback: Callable[[date], None]) -> Optional[str]:
|
|
362
|
+
"""Bind a callback fired when a result is produced.
|
|
363
|
+
|
|
364
|
+
The callback receives `event.data["result"]` (a `datetime.date`).
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
callback: Callable that receives the selected `datetime.date`.
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
The Tk binding identifier, which can be used with `off_result`.
|
|
371
|
+
"""
|
|
372
|
+
target = self._dialog.toplevel or self._master
|
|
373
|
+
if target is None:
|
|
374
|
+
return None
|
|
375
|
+
|
|
376
|
+
def handler(event: tkinter.Event) -> None:
|
|
377
|
+
callback(getattr(event, "data", None))
|
|
378
|
+
|
|
379
|
+
return target.bind("<<DialogResult>>", handler, add="+")
|
|
380
|
+
|
|
381
|
+
def off_result(self, funcid: str) -> None:
|
|
382
|
+
"""Unbind a previously bound `on_result` callback.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
funcid: Binding identifier returned by `on_result`.
|
|
386
|
+
"""
|
|
387
|
+
target = self._dialog.toplevel or self._master
|
|
388
|
+
if target is None:
|
|
389
|
+
return
|
|
390
|
+
target.unbind("<<DialogResult>>", funcid)
|
|
391
|
+
|
|
392
|
+
def _emit_result(self, value: date, confirmed: bool) -> None:
|
|
393
|
+
"""Emit a virtual Tk event with the dialog result."""
|
|
394
|
+
target = self._dialog.toplevel or self._master
|
|
395
|
+
if not target:
|
|
396
|
+
return
|
|
397
|
+
payload = {"result": value, "confirmed": confirmed}
|
|
398
|
+
try:
|
|
399
|
+
target.event_generate("<<DialogResult>>", data=payload)
|
|
400
|
+
except Exception:
|
|
401
|
+
try:
|
|
402
|
+
target.event_generate("<<DialogResult>>")
|
|
403
|
+
except Exception:
|
|
404
|
+
pass
|