bootstack 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bootstack/__init__.py +157 -0
- bootstack/__main__.py +5 -0
- bootstack/_core/__init__.py +21 -0
- bootstack/_core/capabilities/__init__.py +45 -0
- bootstack/_core/capabilities/after.py +103 -0
- bootstack/_core/capabilities/bind.py +192 -0
- bootstack/_core/capabilities/bindtags.py +112 -0
- bootstack/_core/capabilities/busy.py +72 -0
- bootstack/_core/capabilities/clipboard.py +89 -0
- bootstack/_core/capabilities/focus.py +118 -0
- bootstack/_core/capabilities/grab.py +65 -0
- bootstack/_core/capabilities/grid.py +211 -0
- bootstack/_core/capabilities/localization.py +231 -0
- bootstack/_core/capabilities/pack.py +140 -0
- bootstack/_core/capabilities/place.py +113 -0
- bootstack/_core/capabilities/selection.py +136 -0
- bootstack/_core/capabilities/signals.py +244 -0
- bootstack/_core/capabilities/winfo.py +315 -0
- bootstack/_core/colorutils.py +234 -0
- bootstack/_core/exceptions.py +25 -0
- bootstack/_core/images.py +463 -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/paths.py +49 -0
- bootstack/_core/publisher.py +149 -0
- bootstack/_core/variables.py +62 -0
- bootstack/_runtime/__init__.py +3 -0
- bootstack/_runtime/app.py +930 -0
- bootstack/_runtime/base_window.py +945 -0
- bootstack/_runtime/events.py +399 -0
- bootstack/_runtime/shortcuts.py +496 -0
- bootstack/_runtime/tk_patch.py +43 -0
- bootstack/_runtime/toplevel.py +276 -0
- bootstack/_runtime/utility.py +457 -0
- bootstack/_runtime/visual_focus.py +240 -0
- bootstack/_runtime/window_utilities.py +1123 -0
- bootstack/assets/__init__.py +21 -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/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
- bootstack/assets/elements/card.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-addon-compact.png +0 -0
- bootstack/assets/elements/input-addon-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/listrow-compact.png +0 -0
- bootstack/assets/elements/listrow-default.png +0 -0
- bootstack/assets/elements/manifest.toml +361 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/navitem-compact.png +0 -0
- bootstack/assets/elements/navitem-default.png +0 -0
- bootstack/assets/elements/progressbar-h-compact.png +0 -0
- bootstack/assets/elements/progressbar-h-default.png +0 -0
- bootstack/assets/elements/progressbar-v-compact.png +0 -0
- bootstack/assets/elements/progressbar-v-default.png +0 -0
- bootstack/assets/elements/radiobutton.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-h.png +0 -0
- bootstack/assets/elements/slider-track-v.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tab-h.png +0 -0
- bootstack/assets/elements/tab-v.png +0 -0
- bootstack/assets/icons/bootstrap.ttf +0 -0
- bootstack/assets/icons/glyphmap.json +2080 -0
- bootstack/assets/icons/icon_metrics.json +12470 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
- bootstack/cli/__init__.py +133 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +395 -0
- bootstack/cli/appicon.py +285 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +313 -0
- bootstack/cli/demo.py +564 -0
- bootstack/cli/dev.py +153 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/icons.py +98 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +268 -0
- bootstack/cli/run.py +95 -0
- bootstack/cli/start.py +117 -0
- bootstack/cli/templates/__init__.py +931 -0
- bootstack/clipboard.py +48 -0
- bootstack/constants.py +318 -0
- bootstack/data/README.md +615 -0
- bootstack/data/__init__.py +78 -0
- bootstack/data/_observable.py +276 -0
- bootstack/data/base.py +780 -0
- bootstack/data/file_source.py +367 -0
- bootstack/data/memory_source.py +450 -0
- bootstack/data/query.py +367 -0
- bootstack/data/readers.py +289 -0
- bootstack/data/sqlite_source.py +869 -0
- bootstack/data/types.py +354 -0
- bootstack/data/writers.py +232 -0
- bootstack/dev/__init__.py +36 -0
- bootstack/dev/_capture.py +141 -0
- bootstack/dev/_env.py +43 -0
- bootstack/dev/_registry.py +140 -0
- bootstack/dev/_reloader.py +351 -0
- bootstack/dev/_reset.py +50 -0
- bootstack/dev/_watcher.py +91 -0
- bootstack/dialogs/__init__.py +948 -0
- bootstack/dialogs/_impl/__init__.py +47 -0
- bootstack/dialogs/_impl/colorchooser.py +588 -0
- bootstack/dialogs/_impl/datedialog.py +450 -0
- bootstack/dialogs/_impl/dialog.py +594 -0
- bootstack/dialogs/_impl/filterdialog.py +358 -0
- bootstack/dialogs/_impl/fontdialog.py +364 -0
- bootstack/dialogs/_impl/formdialog.py +564 -0
- bootstack/dialogs/_impl/message.py +486 -0
- bootstack/dialogs/_impl/query.py +570 -0
- bootstack/errors.py +67 -0
- bootstack/events/__init__.py +111 -0
- bootstack/events/_event.py +135 -0
- bootstack/events/_payloads.py +539 -0
- bootstack/events/_subscription.py +38 -0
- bootstack/i18n/README.md +77 -0
- bootstack/i18n/__init__.py +23 -0
- bootstack/i18n/catalog.py +121 -0
- bootstack/i18n/intl_format.py +584 -0
- bootstack/i18n/msgcat.py +425 -0
- bootstack/i18n/specs.py +156 -0
- bootstack/images.py +563 -0
- bootstack/py.typed +1 -0
- bootstack/scheduling/__init__.py +11 -0
- bootstack/scheduling/_schedule.py +218 -0
- bootstack/shortcuts.py +21 -0
- bootstack/signals/README.md +98 -0
- bootstack/signals/__init__.py +10 -0
- bootstack/signals/integration.py +100 -0
- bootstack/signals/signal.py +353 -0
- bootstack/signals/types.py +6 -0
- bootstack/store.py +286 -0
- bootstack/streams/__init__.py +12 -0
- bootstack/streams/_stream.py +321 -0
- bootstack/style/__init__.py +38 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +46 -0
- bootstack/style/builders/button.py +339 -0
- bootstack/style/builders/buttongroup.py +311 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +110 -0
- bootstack/style/builders/combobox.py +113 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +82 -0
- bootstack/style/builders/expander.py +148 -0
- bootstack/style/builders/field.py +335 -0
- bootstack/style/builders/frame.py +50 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +34 -0
- bootstack/style/builders/listview.py +369 -0
- bootstack/style/builders/menubar.py +91 -0
- bootstack/style/builders/menubutton.py +359 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +67 -0
- bootstack/style/builders/radiobutton.py +99 -0
- bootstack/style/builders/scale.py +64 -0
- bootstack/style/builders/scrollbar.py +225 -0
- bootstack/style/builders/separator.py +49 -0
- bootstack/style/builders/sidenav.py +643 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +70 -0
- bootstack/style/builders/tabitem.py +204 -0
- bootstack/style/builders/togglegroup.py +294 -0
- bootstack/style/builders/toolbutton.py +275 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +193 -0
- bootstack/style/builders/utils.py +455 -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/fonts.py +123 -0
- bootstack/style/style.py +609 -0
- bootstack/style/style_builder_base.py +716 -0
- bootstack/style/style_builder_mixed.py +93 -0
- bootstack/style/style_builder_tk.py +109 -0
- bootstack/style/style_builder_ttk.py +353 -0
- bootstack/style/style_resolver.py +447 -0
- bootstack/style/theme.py +245 -0
- bootstack/style/theme_provider.py +471 -0
- bootstack/style/themes/__init__.py +128 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +41 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +523 -0
- bootstack/style/utility.py +746 -0
- bootstack/types.py +39 -0
- bootstack/validation/__init__.py +6 -0
- bootstack/validation/types.py +15 -0
- bootstack/validation/validation_result.py +17 -0
- bootstack/validation/validation_rules.py +205 -0
- bootstack/widgets/__init__.py +74 -0
- bootstack/widgets/_core/__init__.py +31 -0
- bootstack/widgets/_core/app_config.py +266 -0
- bootstack/widgets/_core/base.py +461 -0
- bootstack/widgets/_core/container.py +334 -0
- bootstack/widgets/_core/context.py +35 -0
- bootstack/widgets/_core/events.py +130 -0
- bootstack/widgets/_core/field_mixin.py +353 -0
- bootstack/widgets/_core/icon_image_props.py +72 -0
- bootstack/widgets/_core/image_binding.py +93 -0
- bootstack/widgets/_core/navmodel.py +345 -0
- bootstack/widgets/_core/options.py +210 -0
- bootstack/widgets/_core/selection_group.py +130 -0
- bootstack/widgets/_core/window_controls.py +96 -0
- bootstack/widgets/_core/window_menu.py +246 -0
- bootstack/widgets/_impl/__init__.py +1 -0
- bootstack/widgets/_impl/_internal/__init__.py +0 -0
- bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
- bootstack/widgets/_impl/_parts/__init__.py +11 -0
- bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
- bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
- bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
- bootstack/widgets/_impl/composites/__init__.py +33 -0
- bootstack/widgets/_impl/composites/_dateutils.py +33 -0
- bootstack/widgets/_impl/composites/_image_fit.py +105 -0
- bootstack/widgets/_impl/composites/accordion.py +381 -0
- bootstack/widgets/_impl/composites/avatar.py +191 -0
- bootstack/widgets/_impl/composites/buttongroup.py +371 -0
- bootstack/widgets/_impl/composites/calendar.py +972 -0
- bootstack/widgets/_impl/composites/carousel.py +567 -0
- bootstack/widgets/_impl/composites/chart.py +882 -0
- bootstack/widgets/_impl/composites/compositeframe.py +298 -0
- bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
- bootstack/widgets/_impl/composites/dateentry.py +404 -0
- bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
- bootstack/widgets/_impl/composites/expander.py +515 -0
- bootstack/widgets/_impl/composites/field.py +670 -0
- bootstack/widgets/_impl/composites/form.py +1066 -0
- bootstack/widgets/_impl/composites/gallery.py +551 -0
- bootstack/widgets/_impl/composites/list/__init__.py +15 -0
- bootstack/widgets/_impl/composites/list/listitem.py +802 -0
- bootstack/widgets/_impl/composites/list/listview.py +1433 -0
- bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
- bootstack/widgets/_impl/composites/menu/model.py +358 -0
- bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
- bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
- bootstack/widgets/_impl/composites/meter.py +860 -0
- bootstack/widgets/_impl/composites/numericentry.py +201 -0
- bootstack/widgets/_impl/composites/pagestack.py +395 -0
- bootstack/widgets/_impl/composites/passwordentry.py +142 -0
- bootstack/widgets/_impl/composites/pathentry.py +168 -0
- bootstack/widgets/_impl/composites/picture.py +289 -0
- bootstack/widgets/_impl/composites/radiogroup.py +511 -0
- bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
- bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/_impl/composites/scrollview.py +764 -0
- bootstack/widgets/_impl/composites/selectbox.py +1026 -0
- bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
- bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
- bootstack/widgets/_impl/composites/shell/layout.py +335 -0
- bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
- bootstack/widgets/_impl/composites/shell/providers.py +558 -0
- bootstack/widgets/_impl/composites/shell/rail.py +117 -0
- bootstack/widgets/_impl/composites/shell/shell.py +581 -0
- bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
- bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
- bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
- bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
- bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
- bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
- bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
- bootstack/widgets/_impl/composites/slider/slider.py +851 -0
- bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
- bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
- bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
- bootstack/widgets/_impl/composites/tableview/types.py +169 -0
- bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
- bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
- bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
- bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
- bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
- bootstack/widgets/_impl/composites/textarea/change.py +46 -0
- bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
- bootstack/widgets/_impl/composites/textarea/core.py +495 -0
- bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
- bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
- bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
- bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
- bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
- bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
- bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
- bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
- bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
- bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
- bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
- bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
- bootstack/widgets/_impl/composites/textentry.py +57 -0
- bootstack/widgets/_impl/composites/timeentry.py +176 -0
- bootstack/widgets/_impl/composites/toast.py +390 -0
- bootstack/widgets/_impl/composites/toast_stack.py +156 -0
- bootstack/widgets/_impl/composites/togglegroup.py +418 -0
- bootstack/widgets/_impl/composites/toolbar.py +608 -0
- bootstack/widgets/_impl/composites/tooltip.py +491 -0
- bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
- bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
- bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
- bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
- bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
- bootstack/widgets/_impl/mixins/__init__.py +24 -0
- bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
- bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
- bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
- bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
- bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
- bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
- bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
- bootstack/widgets/_impl/primitives/__init__.py +49 -0
- bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
- bootstack/widgets/_impl/primitives/badge.py +45 -0
- bootstack/widgets/_impl/primitives/button.py +76 -0
- bootstack/widgets/_impl/primitives/card.py +45 -0
- bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
- bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
- bootstack/widgets/_impl/primitives/combobox.py +156 -0
- bootstack/widgets/_impl/primitives/entry.py +87 -0
- bootstack/widgets/_impl/primitives/flexframe.py +448 -0
- bootstack/widgets/_impl/primitives/frame.py +185 -0
- bootstack/widgets/_impl/primitives/gridframe.py +546 -0
- bootstack/widgets/_impl/primitives/label.py +84 -0
- bootstack/widgets/_impl/primitives/labelframe.py +54 -0
- bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
- bootstack/widgets/_impl/primitives/packframe.py +227 -0
- bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
- bootstack/widgets/_impl/primitives/progressbar.py +83 -0
- bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
- bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
- bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
- bootstack/widgets/_impl/primitives/separator.py +43 -0
- bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
- bootstack/widgets/_impl/primitives/spinbox.py +95 -0
- bootstack/widgets/_impl/primitives/switch.py +44 -0
- bootstack/widgets/_impl/primitives/treeview.py +69 -0
- bootstack/widgets/app.py +371 -0
- bootstack/widgets/appshell.py +1179 -0
- bootstack/widgets/avatar.py +140 -0
- bootstack/widgets/boolean_controls.py +455 -0
- bootstack/widgets/button.py +224 -0
- bootstack/widgets/buttongroup.py +239 -0
- bootstack/widgets/calendar.py +195 -0
- bootstack/widgets/card.py +159 -0
- bootstack/widgets/carousel.py +241 -0
- bootstack/widgets/chart.py +302 -0
- bootstack/widgets/codeeditor.py +675 -0
- bootstack/widgets/contextmenu.py +371 -0
- bootstack/widgets/datatable.py +688 -0
- bootstack/widgets/datefield.py +395 -0
- bootstack/widgets/divider.py +60 -0
- bootstack/widgets/expander.py +579 -0
- bootstack/widgets/form.py +200 -0
- bootstack/widgets/gallery.py +250 -0
- bootstack/widgets/gauge.py +168 -0
- bootstack/widgets/grid.py +121 -0
- bootstack/widgets/groupbox.py +162 -0
- bootstack/widgets/label.py +229 -0
- bootstack/widgets/listview.py +354 -0
- bootstack/widgets/menubutton.py +419 -0
- bootstack/widgets/numberfield.py +431 -0
- bootstack/widgets/pagestack.py +345 -0
- bootstack/widgets/passwordfield.py +383 -0
- bootstack/widgets/pathfield.py +454 -0
- bootstack/widgets/picture.py +251 -0
- bootstack/widgets/progressbar.py +106 -0
- bootstack/widgets/radio_variants.py +271 -0
- bootstack/widgets/radiogroup.py +228 -0
- bootstack/widgets/scrollbar.py +64 -0
- bootstack/widgets/scrollview.py +186 -0
- bootstack/widgets/select.py +302 -0
- bootstack/widgets/selectbutton.py +182 -0
- bootstack/widgets/sidebar_toggle.py +130 -0
- bootstack/widgets/sizegrip.py +42 -0
- bootstack/widgets/slider.py +408 -0
- bootstack/widgets/spinbox.py +147 -0
- bootstack/widgets/spinnerfield.py +413 -0
- bootstack/widgets/splash.py +367 -0
- bootstack/widgets/splitview.py +482 -0
- bootstack/widgets/stacks.py +249 -0
- bootstack/widgets/statusbar.py +189 -0
- bootstack/widgets/tabs.py +425 -0
- bootstack/widgets/textarea.py +459 -0
- bootstack/widgets/textfield.py +410 -0
- bootstack/widgets/theme_toggle.py +92 -0
- bootstack/widgets/timefield.py +340 -0
- bootstack/widgets/toast.py +314 -0
- bootstack/widgets/togglegroup.py +231 -0
- bootstack/widgets/toolbar.py +347 -0
- bootstack/widgets/tooltip.py +94 -0
- bootstack/widgets/tree.py +648 -0
- bootstack/widgets/types.py +305 -0
- bootstack/widgets/window.py +307 -0
- bootstack-0.1.0.dist-info/METADATA +301 -0
- bootstack-0.1.0.dist-info/RECORD +471 -0
- bootstack-0.1.0.dist-info/WHEEL +5 -0
- bootstack-0.1.0.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
- bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
|
|
2
|
+
from bootstack._runtime.toplevel import Toplevel
|
|
3
|
+
from typing import Any, Callable, Sequence
|
|
4
|
+
|
|
5
|
+
from typing_extensions import TypedDict, Unpack
|
|
6
|
+
|
|
7
|
+
from bootstack._runtime.window_utilities import WindowPositioning
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class IconSpec(TypedDict, total=False):
|
|
11
|
+
"""Icon configuration for toast."""
|
|
12
|
+
name: str
|
|
13
|
+
size: int | None
|
|
14
|
+
color: str | None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
from bootstack.widgets._impl.primitives.frame import Frame as _Frame
|
|
18
|
+
from bootstack.widgets._impl.primitives.label import Label as _Label
|
|
19
|
+
from bootstack.widgets._impl.primitives.button import Button as _Button
|
|
20
|
+
|
|
21
|
+
class ToastConfig(TypedDict, total=False):
|
|
22
|
+
"""Configuration options for Toast widget."""
|
|
23
|
+
title: str | None
|
|
24
|
+
icon: str | IconSpec | None
|
|
25
|
+
message: str | None
|
|
26
|
+
memo: str | None
|
|
27
|
+
duration: int | None
|
|
28
|
+
buttons: Sequence[dict[str, Any]] | None
|
|
29
|
+
show_close_button: bool
|
|
30
|
+
accent: str | None
|
|
31
|
+
position: str | None
|
|
32
|
+
alert: bool
|
|
33
|
+
on_dismissed: Callable[[Any], Any] | None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Toast:
|
|
37
|
+
"""A notification toast widget that displays temporary messages.
|
|
38
|
+
|
|
39
|
+
Toast notifications appear in a small window, typically in the corner of the screen,
|
|
40
|
+
and can display a title, message, icon, buttons, and optional metadata. They can be
|
|
41
|
+
configured to auto-dismiss after a duration or remain visible until manually closed.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
title: str | None = None,
|
|
48
|
+
icon: str | IconSpec | None = None,
|
|
49
|
+
message: str | None = None,
|
|
50
|
+
memo: str | None = None,
|
|
51
|
+
duration: int | None = None,
|
|
52
|
+
buttons: Sequence[dict[str, Any]] | None = None,
|
|
53
|
+
show_close_button: bool = True,
|
|
54
|
+
accent: str | None = None,
|
|
55
|
+
position: str | None = None,
|
|
56
|
+
alert: bool = False,
|
|
57
|
+
on_dismissed: Callable[[Any], Any] | None = None,
|
|
58
|
+
place: Callable[[Any], None] | None = None,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Initialize a Toast notification.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
title: Title text. Displayed with a larger font when no message is
|
|
64
|
+
provided; otherwise appears as the header with message below.
|
|
65
|
+
icon: Icon name string or `IconSpec` dict with `name`, `size`, and
|
|
66
|
+
`color` keys.
|
|
67
|
+
message: Main message text. Displayed in header when no title; shown
|
|
68
|
+
below the title separator when both are provided.
|
|
69
|
+
memo: Small metadata text in the header (e.g., "5 mins ago"). Muted styling.
|
|
70
|
+
duration: Auto-dismiss delay in milliseconds. If None, stays until closed.
|
|
71
|
+
buttons: Sequence of button option dicts. Each dict accepts any bootstack
|
|
72
|
+
Button kwargs. Button press triggers `on_dismissed` before closing.
|
|
73
|
+
show_close_button: Whether to show the close (×) button. Default True.
|
|
74
|
+
accent: Accent token for the container (e.g., 'primary', 'danger'). If
|
|
75
|
+
None, uses the default background.
|
|
76
|
+
position: Tkinter geometry string (e.g., '-25-75'). If None, uses
|
|
77
|
+
platform defaults (bottom-right on Windows/macOS, top-right on X11).
|
|
78
|
+
alert: If True, plays a system bell when shown.
|
|
79
|
+
on_dismissed: Callback invoked on dismiss. Receives the button options
|
|
80
|
+
dict when dismissed via a button, or None otherwise.
|
|
81
|
+
"""
|
|
82
|
+
self._config_keys = {'title', 'icon', 'message', 'memo', 'duration', 'buttons', 'show_close_button',
|
|
83
|
+
'accent', 'position', 'alert', 'on_dismissed'}
|
|
84
|
+
|
|
85
|
+
# initialized configuration
|
|
86
|
+
self._title = title
|
|
87
|
+
self._icon = icon
|
|
88
|
+
self._message = message
|
|
89
|
+
self._memo = memo
|
|
90
|
+
self._duration = duration
|
|
91
|
+
self._buttons = buttons
|
|
92
|
+
self._show_close_button = show_close_button
|
|
93
|
+
self._accent = accent
|
|
94
|
+
self._position = position
|
|
95
|
+
self._alert = alert
|
|
96
|
+
self._on_dismissed = on_dismissed
|
|
97
|
+
# Optional positioning hook: a callable given the built (off-screen,
|
|
98
|
+
# measured) Toplevel that sets its final geometry. Lets a facade own
|
|
99
|
+
# corner/window placement and stacking; bypasses the default logic.
|
|
100
|
+
self._place = place
|
|
101
|
+
|
|
102
|
+
# top level widget
|
|
103
|
+
self._toplevel: Toplevel | None = None
|
|
104
|
+
|
|
105
|
+
def __setitem__(self, key: str, value: Any) -> None:
|
|
106
|
+
"""Set a configuration option using dictionary syntax."""
|
|
107
|
+
self.configure(**{key: value})
|
|
108
|
+
|
|
109
|
+
def __getitem__(self, key: str) -> Any:
|
|
110
|
+
"""Get a configuration option using dictionary syntax."""
|
|
111
|
+
return self.cget(key)
|
|
112
|
+
|
|
113
|
+
def _handle_on_dismissed(self, data: Any = None) -> None:
|
|
114
|
+
"""Invoke the on_dismissed callback if configured."""
|
|
115
|
+
if self._on_dismissed:
|
|
116
|
+
self._on_dismissed(data)
|
|
117
|
+
|
|
118
|
+
def configure(
|
|
119
|
+
self,
|
|
120
|
+
option: str | None = None,
|
|
121
|
+
**kwargs: Unpack[ToastConfig]
|
|
122
|
+
) -> tuple[str, str, str, None, Any] | None:
|
|
123
|
+
"""Configure toast options.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
option: If provided, returns the configuration for this option.
|
|
127
|
+
**kwargs: Configuration options to set.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Configuration tuple if option provided, otherwise None.
|
|
131
|
+
"""
|
|
132
|
+
if option is not None:
|
|
133
|
+
if option in self._config_keys:
|
|
134
|
+
attr = f"_{option}"
|
|
135
|
+
value = getattr(self, attr)
|
|
136
|
+
# returns in the expected tkinter.configure format
|
|
137
|
+
return option, option, option.capitalize(), None, value
|
|
138
|
+
else:
|
|
139
|
+
raise AttributeError(f"'{option}' is not a valid option")
|
|
140
|
+
|
|
141
|
+
if kwargs:
|
|
142
|
+
for key, value in kwargs.items():
|
|
143
|
+
if key in self._config_keys:
|
|
144
|
+
attr = f"_{key}"
|
|
145
|
+
setattr(self, attr, value)
|
|
146
|
+
else:
|
|
147
|
+
raise AttributeError(f"'{key}' is not a valid option")
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
def cget(self, option: str) -> Any:
|
|
151
|
+
"""Get the value of a configuration option.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
option: The configuration option name.
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
The value of the configuration option.
|
|
158
|
+
"""
|
|
159
|
+
if option in self._config_keys:
|
|
160
|
+
attr = f"_{option}"
|
|
161
|
+
return getattr(self, attr)
|
|
162
|
+
else:
|
|
163
|
+
raise AttributeError(f"'{option}' is not a valid option")
|
|
164
|
+
|
|
165
|
+
def show(self, merge: bool = True, **options: Unpack[ToastConfig]) -> None:
|
|
166
|
+
"""Display the toast.
|
|
167
|
+
|
|
168
|
+
If options are provided, they are merged with the existing toast configuration. If you do not want this
|
|
169
|
+
behavior, set the merge flag to False, or create a new toast instance.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
merge: If True, merge options with existing configuration. If False, clear existing options first.
|
|
173
|
+
**options: Configuration options to set before showing.
|
|
174
|
+
"""
|
|
175
|
+
if not merge:
|
|
176
|
+
self._clear_options()
|
|
177
|
+
|
|
178
|
+
self.configure(**options)
|
|
179
|
+
self._build_toast()
|
|
180
|
+
if self._toplevel:
|
|
181
|
+
self._toplevel.deiconify()
|
|
182
|
+
|
|
183
|
+
def _clear_options(self) -> None:
|
|
184
|
+
"""Clear all configuration options to their default values."""
|
|
185
|
+
self._title = None
|
|
186
|
+
self._icon = None
|
|
187
|
+
self._message = None
|
|
188
|
+
self._memo = None
|
|
189
|
+
self._duration = None
|
|
190
|
+
self._buttons = None
|
|
191
|
+
self._show_close_button = True
|
|
192
|
+
self._accent = None
|
|
193
|
+
self._position = None
|
|
194
|
+
self._alert = False
|
|
195
|
+
self._on_dismissed = None
|
|
196
|
+
|
|
197
|
+
def hide(self) -> None:
|
|
198
|
+
"""Hide the toast and trigger the on_dismissed callback."""
|
|
199
|
+
if self._toplevel:
|
|
200
|
+
self._toplevel.destroy()
|
|
201
|
+
self._handle_on_dismissed(None)
|
|
202
|
+
|
|
203
|
+
def destroy(self) -> None:
|
|
204
|
+
"""Destroy the toast widget and cleanup resources."""
|
|
205
|
+
if hasattr(self, '_toplevel') and self._toplevel:
|
|
206
|
+
self._toplevel.destroy()
|
|
207
|
+
self._toplevel = None
|
|
208
|
+
|
|
209
|
+
def _build_toast(self) -> None:
|
|
210
|
+
# ----- Configuration Options -------
|
|
211
|
+
|
|
212
|
+
has_title = self._title is not None
|
|
213
|
+
has_title_and_message = has_title and self._message is not None
|
|
214
|
+
resolved_title_font = "label" if has_title else "body"
|
|
215
|
+
subtle_accent = f"{self._accent}[subtle]" if self._accent else None
|
|
216
|
+
# Text uses the accent's *emphasis* shade — a dark, readable, accent-
|
|
217
|
+
# tinted color (matching Bootstrap's text-emphasis), not the bright base
|
|
218
|
+
# accent, which is low-contrast and harsh as text on the subtle wash.
|
|
219
|
+
# No accent -> None, so the label falls back to the on-surface foreground.
|
|
220
|
+
text_accent = f"{self._accent}_emphasis" if self._accent else None
|
|
221
|
+
# Icon is opt-in (no forced default): a toast is its message, optionally
|
|
222
|
+
# preceded by an icon. Frameworks don't agree on a default, so render one
|
|
223
|
+
# only when asked.
|
|
224
|
+
icon_spec = None
|
|
225
|
+
if self._icon:
|
|
226
|
+
icon_spec = self._icon if isinstance(self._icon, dict) else {"name": self._icon, "size": 20}
|
|
227
|
+
|
|
228
|
+
# ------ Toplevel setup ------
|
|
229
|
+
|
|
230
|
+
# bootstack.Toplevel applies MacWindowStyle 'help none' on Aqua
|
|
231
|
+
# via windowtype='tooltip', so toasts render chromeless on Mac
|
|
232
|
+
# (overrideredirect alone is silently skipped on Aqua per the
|
|
233
|
+
# project's BaseWindow guard). Win/Linux keep the overrideredirect
|
|
234
|
+
# path. Topmost + alpha are passed as kwargs so the Toplevel sets
|
|
235
|
+
# them at the right moment in its setup pipeline.
|
|
236
|
+
top = Toplevel(
|
|
237
|
+
overrideredirect=True,
|
|
238
|
+
topmost=True,
|
|
239
|
+
alpha=0.97,
|
|
240
|
+
windowtype='tooltip',
|
|
241
|
+
)
|
|
242
|
+
top.minsize(400, 30)
|
|
243
|
+
|
|
244
|
+
# ------ Toast Layout ------
|
|
245
|
+
|
|
246
|
+
# The container carries the subtle accent wash; the border stays neutral
|
|
247
|
+
# (no accent) so the card has a quiet edge rather than a bright outline.
|
|
248
|
+
container = _Frame(top, padding=4, show_border=True,
|
|
249
|
+
surface=subtle_accent or 'overlay')
|
|
250
|
+
container.pack(fill='both', expand=True)
|
|
251
|
+
|
|
252
|
+
# Tight gap below the title so it reads as one unit with the message;
|
|
253
|
+
# the breathing room lives on the outside (container padding + message
|
|
254
|
+
# bottom). A lone title (no message) keeps a balanced bottom instead.
|
|
255
|
+
header_bottom = 8 if not has_title_and_message else 2
|
|
256
|
+
header = _Frame(container, padding=(8, 8, 8, header_bottom))
|
|
257
|
+
header.pack(side='top', fill='x')
|
|
258
|
+
|
|
259
|
+
# icon — the emphasis shade, like the text, so it stays legible on the
|
|
260
|
+
# subtle wash (the bright base accent — e.g. warning's yellow — washes
|
|
261
|
+
# out against its own pale tint).
|
|
262
|
+
if icon_spec:
|
|
263
|
+
_Label(header, icon=icon_spec, accent=text_accent).pack(side='left', padx=(0, 8))
|
|
264
|
+
|
|
265
|
+
# title — emphasis (dark, readable accent-tinted) text
|
|
266
|
+
_Label(
|
|
267
|
+
header,
|
|
268
|
+
text=self._title if has_title else self._message,
|
|
269
|
+
font=resolved_title_font,
|
|
270
|
+
wraplength=380,
|
|
271
|
+
justify='left',
|
|
272
|
+
accent=text_accent,
|
|
273
|
+
).pack(side='left', fill='x')
|
|
274
|
+
|
|
275
|
+
# close — a clickable x icon, not a Button: a compact button still floors
|
|
276
|
+
# ~12px above the title row and would inflate the header. A Label icon
|
|
277
|
+
# matches the title/icon height, so the row stays content-sized.
|
|
278
|
+
if self._show_close_button:
|
|
279
|
+
close = _Label(header, icon={"name": "x-lg", "size": 14}, accent="secondary")
|
|
280
|
+
close.configure(cursor="hand2")
|
|
281
|
+
close.bind("<Button-1>", lambda _e: self.hide())
|
|
282
|
+
close.pack(side='right', padx=(4, 0))
|
|
283
|
+
|
|
284
|
+
# memo — muted metadata (no extra vertical padding; the header owns it)
|
|
285
|
+
if self._memo:
|
|
286
|
+
_Label(
|
|
287
|
+
header,
|
|
288
|
+
text=self._memo,
|
|
289
|
+
font="caption",
|
|
290
|
+
accent="secondary",
|
|
291
|
+
).pack(side='right', padx=(0, 4 if self._show_close_button else 12))
|
|
292
|
+
|
|
293
|
+
# message — emphasis text
|
|
294
|
+
if has_title_and_message:
|
|
295
|
+
_Label(
|
|
296
|
+
container,
|
|
297
|
+
text=self._message,
|
|
298
|
+
wraplength=400,
|
|
299
|
+
justify='left',
|
|
300
|
+
accent=text_accent,
|
|
301
|
+
).pack(side='top', fill='x', pady=(0, 8), padx=8)
|
|
302
|
+
|
|
303
|
+
# buttons
|
|
304
|
+
if self._buttons:
|
|
305
|
+
def execute_command(options: dict[str, Any], fn: Callable[[], None] | None = None) -> Callable[[], None]:
|
|
306
|
+
def inner() -> None:
|
|
307
|
+
if fn:
|
|
308
|
+
fn()
|
|
309
|
+
self._handle_on_dismissed(options)
|
|
310
|
+
top.destroy()
|
|
311
|
+
|
|
312
|
+
return inner
|
|
313
|
+
|
|
314
|
+
button_frame = _Frame(container)
|
|
315
|
+
button_frame.pack(side='top', anchor='e', padx=4, pady=(0, 4))
|
|
316
|
+
|
|
317
|
+
for i, button_options in enumerate(self._buttons):
|
|
318
|
+
func = button_options.get('command', None)
|
|
319
|
+
button_opts = {k: v for k, v in button_options.items() if k != 'command'}
|
|
320
|
+
_Button(
|
|
321
|
+
button_frame,
|
|
322
|
+
**button_opts,
|
|
323
|
+
command=execute_command(button_options, func)
|
|
324
|
+
).grid(column=i, row=0, padx=(4 if i > 0 else 0, 0))
|
|
325
|
+
|
|
326
|
+
# ------ Positioning -------
|
|
327
|
+
|
|
328
|
+
# Deiconify off-screen first so the geometry manager runs a full
|
|
329
|
+
# layout pass on a mapped window. winfo_height() returns 1 on a
|
|
330
|
+
# withdrawn window, causing the toast to be placed with ~0 height
|
|
331
|
+
# and therefore positioned flush against the screen edge.
|
|
332
|
+
top.deiconify()
|
|
333
|
+
top.geometry(
|
|
334
|
+
f"+{top.winfo_screenwidth() * 2}+{top.winfo_screenheight() * 2}"
|
|
335
|
+
)
|
|
336
|
+
top.update_idletasks()
|
|
337
|
+
|
|
338
|
+
# A facade-supplied placement hook owns corner/window positioning and
|
|
339
|
+
# stacking; it runs while the window is still off-screen, so there is no
|
|
340
|
+
# reposition flash.
|
|
341
|
+
if self._place is not None:
|
|
342
|
+
self._place(top)
|
|
343
|
+
# Apply positioning using WindowPositioning utilities
|
|
344
|
+
elif self._position:
|
|
345
|
+
# Support legacy geometry strings (e.g., "-25-75")
|
|
346
|
+
if self._position.startswith(('+', '-')):
|
|
347
|
+
# Legacy geometry string - use directly
|
|
348
|
+
top.geometry(self._position)
|
|
349
|
+
else:
|
|
350
|
+
# New positioning - assume it's a corner specification
|
|
351
|
+
# (future enhancement: could support more complex positioning)
|
|
352
|
+
top.geometry(self._position)
|
|
353
|
+
else:
|
|
354
|
+
# Default positioning based on platform
|
|
355
|
+
winsys = top.tk.call('tk', 'windowingsystem')
|
|
356
|
+
if winsys in ['win32', 'aqua']:
|
|
357
|
+
# Bottom-right corner
|
|
358
|
+
WindowPositioning.position_anchored(
|
|
359
|
+
window=top,
|
|
360
|
+
anchor_to="screen",
|
|
361
|
+
parent=None,
|
|
362
|
+
anchor_point="se",
|
|
363
|
+
window_point="se",
|
|
364
|
+
offset=(-25, -75),
|
|
365
|
+
auto_flip=False,
|
|
366
|
+
ensure_visible=True
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
# Top-right corner for X11
|
|
370
|
+
WindowPositioning.position_anchored(
|
|
371
|
+
window=top,
|
|
372
|
+
anchor_to="screen",
|
|
373
|
+
parent=None,
|
|
374
|
+
anchor_point="ne",
|
|
375
|
+
window_point="ne",
|
|
376
|
+
offset=(-25, 25),
|
|
377
|
+
auto_flip=False,
|
|
378
|
+
ensure_visible=True
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# ------ Other setup -------
|
|
382
|
+
|
|
383
|
+
if self._duration:
|
|
384
|
+
top.after(self._duration, self.hide)
|
|
385
|
+
|
|
386
|
+
if self._alert:
|
|
387
|
+
top.bell()
|
|
388
|
+
|
|
389
|
+
self._toplevel = top
|
|
390
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Positioning and stacking for the toast family.
|
|
2
|
+
|
|
3
|
+
The public `toast()` / `Notification` / `Snackbar` facades share one process-wide
|
|
4
|
+
manager that owns *where* a transient window lands and how several of them stack:
|
|
5
|
+
|
|
6
|
+
- **Corner stacks** (toast, notification) float in a corner of the monitor the
|
|
7
|
+
app window currently sits on, newest nearest the corner, older ones offset
|
|
8
|
+
away. The stack re-flows when any member dismisses.
|
|
9
|
+
- **Snackbar** anchors to the app window's own bottom edge, aligned left,
|
|
10
|
+
center, or right. Only one snackbar shows at a time; a new one replaces it.
|
|
11
|
+
|
|
12
|
+
Geometry is computed by hand (no reliance on the engine's default placement) and
|
|
13
|
+
applied through the engine's `place=` hook, so positioning happens while the
|
|
14
|
+
window is still off-screen — no reposition flash.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import tkinter
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
from bootstack._runtime.app import get_default_root
|
|
22
|
+
from bootstack._runtime.window_utilities import WindowPositioning
|
|
23
|
+
|
|
24
|
+
Corner = Literal["top-left", "top-right", "bottom-left", "bottom-right"]
|
|
25
|
+
Align = Literal["left", "center", "right"]
|
|
26
|
+
|
|
27
|
+
CORNERS: tuple[Corner, ...] = ("top-left", "top-right", "bottom-left", "bottom-right")
|
|
28
|
+
|
|
29
|
+
_MARGIN = 24 # px gap from the monitor / window edge
|
|
30
|
+
_GAP = 12 # px gap between stacked windows
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _alive(top: tkinter.Toplevel) -> bool:
|
|
34
|
+
"""Whether a toast Toplevel still exists."""
|
|
35
|
+
try:
|
|
36
|
+
return bool(top.winfo_exists())
|
|
37
|
+
except tkinter.TclError:
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _monitor_rect() -> tuple[int, int, int, int]:
|
|
42
|
+
"""`(x, y, w, h)` usable work area of the monitor the app window is on.
|
|
43
|
+
|
|
44
|
+
On macOS this is the visible frame (menu bar + Dock excluded) so a corner
|
|
45
|
+
toast never lands under the Dock; elsewhere it is the full monitor bounds.
|
|
46
|
+
Falls back to the full screen, then a fixed default.
|
|
47
|
+
"""
|
|
48
|
+
root = get_default_root()
|
|
49
|
+
if root is not None:
|
|
50
|
+
try:
|
|
51
|
+
root.update_idletasks()
|
|
52
|
+
cx = root.winfo_rootx() + root.winfo_width() // 2
|
|
53
|
+
cy = root.winfo_rooty() + root.winfo_height() // 2
|
|
54
|
+
rect = WindowPositioning._get_monitor_at_point(cx, cy)
|
|
55
|
+
if rect is None:
|
|
56
|
+
rect = (0, 0, root.winfo_screenwidth(), root.winfo_screenheight())
|
|
57
|
+
return WindowPositioning.get_work_area(root, rect)
|
|
58
|
+
except tkinter.TclError:
|
|
59
|
+
pass
|
|
60
|
+
return (0, 0, 1920, 1080)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class _ToastManager:
|
|
64
|
+
"""Process-wide owner of corner stacks and the single active snackbar."""
|
|
65
|
+
|
|
66
|
+
def __init__(self) -> None:
|
|
67
|
+
self._corners: dict[Corner, list[tkinter.Toplevel]] = {c: [] for c in CORNERS}
|
|
68
|
+
self._snackbar: tkinter.Toplevel | None = None
|
|
69
|
+
|
|
70
|
+
# ----- corner stacks (toast / notification) --------------------------
|
|
71
|
+
|
|
72
|
+
def corner_placer(self, corner: Corner):
|
|
73
|
+
"""Return a `place(top)` hook that slots `top` into `corner`'s stack."""
|
|
74
|
+
def _place(top: tkinter.Toplevel) -> None:
|
|
75
|
+
self._corners[corner].insert(0, top) # newest nearest the corner
|
|
76
|
+
self._reflow(corner)
|
|
77
|
+
return _place
|
|
78
|
+
|
|
79
|
+
def remove(self, corner: Corner, top: tkinter.Toplevel) -> None:
|
|
80
|
+
"""Drop `top` from its stack (on dismiss) and re-flow the rest."""
|
|
81
|
+
stack = self._corners.get(corner)
|
|
82
|
+
if stack is None:
|
|
83
|
+
return
|
|
84
|
+
self._corners[corner] = [t for t in stack if t is not top]
|
|
85
|
+
self._reflow(corner)
|
|
86
|
+
|
|
87
|
+
def _reflow(self, corner: Corner) -> None:
|
|
88
|
+
"""Re-position every live window in a corner stack, in order."""
|
|
89
|
+
mx, my, mw, mh = _monitor_rect()
|
|
90
|
+
at_top = "top" in corner
|
|
91
|
+
at_right = "right" in corner
|
|
92
|
+
offset = _MARGIN
|
|
93
|
+
live: list[tkinter.Toplevel] = []
|
|
94
|
+
for top in self._corners[corner]:
|
|
95
|
+
if not _alive(top):
|
|
96
|
+
continue
|
|
97
|
+
live.append(top)
|
|
98
|
+
try:
|
|
99
|
+
top.update_idletasks()
|
|
100
|
+
w, h = top.winfo_width(), top.winfo_height()
|
|
101
|
+
x = (mx + mw - w - _MARGIN) if at_right else (mx + _MARGIN)
|
|
102
|
+
y = (my + offset) if at_top else (my + mh - h - offset)
|
|
103
|
+
top.geometry(f"+{int(x)}+{int(y)}")
|
|
104
|
+
offset += h + _GAP
|
|
105
|
+
except tkinter.TclError:
|
|
106
|
+
pass
|
|
107
|
+
self._corners[corner] = live
|
|
108
|
+
|
|
109
|
+
# ----- snackbar (single, app-window bottom edge) ---------------------
|
|
110
|
+
|
|
111
|
+
def snackbar_placer(self, align: Align):
|
|
112
|
+
"""Return a `place(top)` hook that anchors `top` to the window bottom."""
|
|
113
|
+
def _place(top: tkinter.Toplevel) -> None:
|
|
114
|
+
if self._snackbar is not None and _alive(self._snackbar) and self._snackbar is not top:
|
|
115
|
+
try:
|
|
116
|
+
self._snackbar.destroy()
|
|
117
|
+
except tkinter.TclError:
|
|
118
|
+
pass
|
|
119
|
+
self._snackbar = top
|
|
120
|
+
self._position_snackbar(top, align)
|
|
121
|
+
return _place
|
|
122
|
+
|
|
123
|
+
def clear_snackbar(self, top: tkinter.Toplevel) -> None:
|
|
124
|
+
"""Forget the active snackbar if it is `top` (on dismiss)."""
|
|
125
|
+
if self._snackbar is top:
|
|
126
|
+
self._snackbar = None
|
|
127
|
+
|
|
128
|
+
def _position_snackbar(self, top: tkinter.Toplevel, align: Align) -> None:
|
|
129
|
+
root = get_default_root()
|
|
130
|
+
if root is None:
|
|
131
|
+
return
|
|
132
|
+
try:
|
|
133
|
+
root.update_idletasks()
|
|
134
|
+
top.update_idletasks()
|
|
135
|
+
rx, ry = root.winfo_rootx(), root.winfo_rooty()
|
|
136
|
+
rw, rh = root.winfo_width(), root.winfo_height()
|
|
137
|
+
w, h = top.winfo_width(), top.winfo_height()
|
|
138
|
+
y = ry + rh - h - _MARGIN
|
|
139
|
+
if align == "left":
|
|
140
|
+
x = rx + _MARGIN
|
|
141
|
+
elif align == "right":
|
|
142
|
+
x = rx + rw - w - _MARGIN
|
|
143
|
+
else: # center
|
|
144
|
+
x = rx + (rw - w) // 2
|
|
145
|
+
top.geometry(f"+{int(x)}+{int(y)}")
|
|
146
|
+
except tkinter.TclError:
|
|
147
|
+
pass
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# One manager for the whole process — toasts are app-global.
|
|
151
|
+
_manager = _ToastManager()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def get_toast_manager() -> _ToastManager:
|
|
155
|
+
"""Return the process-wide toast manager."""
|
|
156
|
+
return _manager
|