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,340 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import datetime
|
|
4
|
+
import tkinter
|
|
5
|
+
from typing import overload, Any, Callable, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from bootstack.widgets._impl.composites.timeentry import TimeEntry as _InternalTimeEntry
|
|
8
|
+
from bootstack.widgets._core.base import PublicWidgetBase, adapt_handler
|
|
9
|
+
from bootstack.widgets._core.events import resolve_event, register_widget_events
|
|
10
|
+
from bootstack.widgets._core.field_mixin import FieldAddonMixin, ValueSignalMixin
|
|
11
|
+
from bootstack.events import ChangeEvent, Subscription, ValidationEvent
|
|
12
|
+
from bootstack.streams import Stream
|
|
13
|
+
from bootstack.widgets.textfield import _INNER_ENTRY_SEQUENCES
|
|
14
|
+
from bootstack.widgets.types import AccentToken, Event, WidgetDensity
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from bootstack.signals import Signal
|
|
18
|
+
|
|
19
|
+
_TIME_FIELD_EVENTS: dict[str, str] = {
|
|
20
|
+
"change": "<<Change>>",
|
|
21
|
+
"validate": "<<Validate>>",
|
|
22
|
+
"valid": "<<Valid>>",
|
|
23
|
+
"invalid": "<<Invalid>>",
|
|
24
|
+
"submit": "<Return>",
|
|
25
|
+
"focus": "<FocusIn>",
|
|
26
|
+
"blur": "<FocusOut>",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TimeField(ValueSignalMixin, FieldAddonMixin, PublicWidgetBase):
|
|
31
|
+
"""A time-input field with a searchable dropdown of time intervals.
|
|
32
|
+
|
|
33
|
+
Displays a formatted time value and shows a dropdown list of times at
|
|
34
|
+
the specified `interval`. The user can type a custom time or pick from
|
|
35
|
+
the list.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
value: Initial time value — a `datetime.time` object or a time
|
|
39
|
+
string (e.g. `'14:30'` or `'2:30 PM'`). Empty by default; pass a
|
|
40
|
+
value to seed a starting time.
|
|
41
|
+
value_format: Format applied to the displayed time — a named preset
|
|
42
|
+
(e.g. `'shortTime'`, `'longTime'`) or a custom pattern (e.g.
|
|
43
|
+
`'HH:mm'`, `'h:mm a'`). Default `'shortTime'`. See
|
|
44
|
+
:ref:`format specs <value-formats>`.
|
|
45
|
+
interval: Minute interval for dropdown entries. Default `30`.
|
|
46
|
+
min_time: Earliest time shown in the dropdown.
|
|
47
|
+
max_time: Latest time shown in the dropdown.
|
|
48
|
+
label: Label displayed above the field.
|
|
49
|
+
message: Hint text displayed below the field.
|
|
50
|
+
signal: Reactive `Signal` two-way bound to the field's `time` value (not
|
|
51
|
+
its text). When given, it seeds the initial value. This is the usual
|
|
52
|
+
way to bind a time field.
|
|
53
|
+
required: If `True`, field cannot be left empty.
|
|
54
|
+
disabled: If `True`, field is non-interactive.
|
|
55
|
+
read_only: If `True`, free-text entry is blocked; user must pick
|
|
56
|
+
from the dropdown.
|
|
57
|
+
width: Width in character cells.
|
|
58
|
+
accent: Accent color applied to the focus ring. Default `'primary'`.
|
|
59
|
+
density: Widget density.
|
|
60
|
+
parent: Override the context-stack parent.
|
|
61
|
+
**kwargs: Layout placement options applied by the parent container —
|
|
62
|
+
`fill`, `expand`, `anchor`, `margin`, `row`, `column`, `sticky`.
|
|
63
|
+
See :doc:`/tasks/layout`.
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
_VALIDATION_KIND = "time"
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
value: datetime.time | str | None = None,
|
|
71
|
+
*,
|
|
72
|
+
signal: "Signal | None" = None,
|
|
73
|
+
value_format: str = "shortTime",
|
|
74
|
+
interval: int = 30,
|
|
75
|
+
min_time: datetime.time | str | None = None,
|
|
76
|
+
max_time: datetime.time | str | None = None,
|
|
77
|
+
label: str | None = None,
|
|
78
|
+
message: str | None = None,
|
|
79
|
+
required: bool = False,
|
|
80
|
+
disabled: bool = False,
|
|
81
|
+
read_only: bool = False,
|
|
82
|
+
width: int | None = None,
|
|
83
|
+
accent: AccentToken | str | None = None,
|
|
84
|
+
density: WidgetDensity | None = None,
|
|
85
|
+
parent: Any = None,
|
|
86
|
+
**kwargs: Any,
|
|
87
|
+
) -> None:
|
|
88
|
+
self._parent = self._resolve_parent(parent)
|
|
89
|
+
layout_kw = self._split_layout_kwargs(kwargs)
|
|
90
|
+
if "textsignal" in kwargs:
|
|
91
|
+
raise TypeError(
|
|
92
|
+
"TimeField does not accept 'textsignal=' — a time field binds its "
|
|
93
|
+
"time value. Use signal= with a time-typed Signal "
|
|
94
|
+
"(e.g. Signal(time(9, 0)))."
|
|
95
|
+
)
|
|
96
|
+
tk_master = self._parent._child_master() if self._parent else None
|
|
97
|
+
|
|
98
|
+
internal_kwargs: dict[str, Any] = {
|
|
99
|
+
"value_format": value_format,
|
|
100
|
+
"interval": interval,
|
|
101
|
+
}
|
|
102
|
+
if value is not None:
|
|
103
|
+
internal_kwargs["value"] = value
|
|
104
|
+
if min_time is not None:
|
|
105
|
+
internal_kwargs["min_time"] = min_time
|
|
106
|
+
if max_time is not None:
|
|
107
|
+
internal_kwargs["max_time"] = max_time
|
|
108
|
+
if label is not None:
|
|
109
|
+
internal_kwargs["label"] = label
|
|
110
|
+
if message is not None:
|
|
111
|
+
internal_kwargs["message"] = message
|
|
112
|
+
if required:
|
|
113
|
+
internal_kwargs["required"] = True
|
|
114
|
+
if disabled:
|
|
115
|
+
internal_kwargs["state"] = "disabled"
|
|
116
|
+
elif read_only:
|
|
117
|
+
internal_kwargs["state"] = "readonly"
|
|
118
|
+
if width is not None:
|
|
119
|
+
internal_kwargs["width"] = width
|
|
120
|
+
if accent is not None:
|
|
121
|
+
internal_kwargs["accent"] = accent
|
|
122
|
+
if density is not None:
|
|
123
|
+
internal_kwargs["density"] = density
|
|
124
|
+
|
|
125
|
+
self._internal = _InternalTimeEntry(tk_master, **internal_kwargs)
|
|
126
|
+
self._attach_to_parent(layout_kw)
|
|
127
|
+
|
|
128
|
+
if signal is not None:
|
|
129
|
+
self._bind_value_signal(signal)
|
|
130
|
+
|
|
131
|
+
# ----- Event routing -----
|
|
132
|
+
|
|
133
|
+
def _entry_widget(self) -> tkinter.Misc:
|
|
134
|
+
return self._internal._entry
|
|
135
|
+
|
|
136
|
+
@overload
|
|
137
|
+
def on(self, event: str) -> Stream: ...
|
|
138
|
+
@overload
|
|
139
|
+
def on(self, event: str, handler: Callable[[Event], Any]) -> Subscription: ...
|
|
140
|
+
def on(self, event: str, handler: Callable[[Event], Any] | None = None) -> Stream | Subscription:
|
|
141
|
+
"""Register a callback for an event by name.
|
|
142
|
+
|
|
143
|
+
A generic, string-keyed escape hatch — prefer the typed `on_*`
|
|
144
|
+
shorthands (e.g. `on_change`), which carry the precise payload type.
|
|
145
|
+
Called with no handler, returns a composable `Stream`; with a handler,
|
|
146
|
+
binds it and returns a `Subscription`.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
event: Event name (for example `'change'` or `'focus'`).
|
|
150
|
+
handler: Called with the event payload. Omit to get a composable
|
|
151
|
+
:class:`~bootstack.streams.Stream` instead.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A cancellable :class:`~bootstack.events.Subscription` when a handler
|
|
155
|
+
is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
156
|
+
"""
|
|
157
|
+
sequence = resolve_event(self, str(event))
|
|
158
|
+
widget = self._entry_widget() if sequence in _INNER_ENTRY_SEQUENCES else self._internal
|
|
159
|
+
if handler is None:
|
|
160
|
+
from bootstack.streams import Stream as _Stream
|
|
161
|
+
def _source(h):
|
|
162
|
+
_w = self._entry_widget() if sequence in _INNER_ENTRY_SEQUENCES else self._internal
|
|
163
|
+
_bid = _w.bind(sequence, adapt_handler(h), add="+")
|
|
164
|
+
return Subscription(_w, sequence, _bid)
|
|
165
|
+
return _Stream(self._internal, _source=_source)
|
|
166
|
+
bind_id = widget.bind(sequence, adapt_handler(handler), add="+")
|
|
167
|
+
return Subscription(widget, sequence, bind_id)
|
|
168
|
+
|
|
169
|
+
# ----- Properties -----
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def value(self) -> "datetime.time | None":
|
|
173
|
+
"""Current time value, or `None` if the field is empty."""
|
|
174
|
+
return self._internal.value
|
|
175
|
+
|
|
176
|
+
@value.setter
|
|
177
|
+
def value(self, v: "datetime.time | str | None") -> None:
|
|
178
|
+
self._internal.value = v
|
|
179
|
+
self._sync_value_set(self._internal.value)
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def disabled(self) -> bool:
|
|
183
|
+
"""Whether the field is disabled (non-interactive and greyed out)."""
|
|
184
|
+
return self._internal._entry.instate(("disabled",))
|
|
185
|
+
|
|
186
|
+
@disabled.setter
|
|
187
|
+
def disabled(self, v: bool) -> None:
|
|
188
|
+
self._internal.configure(state="disabled" if v else "normal")
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def read_only(self) -> bool:
|
|
192
|
+
"""Whether the field is read-only (selectable but not editable)."""
|
|
193
|
+
return self._internal._entry.instate(("readonly",))
|
|
194
|
+
|
|
195
|
+
@read_only.setter
|
|
196
|
+
def read_only(self, v: bool) -> None:
|
|
197
|
+
self._internal.configure(state="readonly" if v else "normal")
|
|
198
|
+
|
|
199
|
+
# ----- Methods -----
|
|
200
|
+
|
|
201
|
+
def validate(self) -> bool:
|
|
202
|
+
"""Run validation rules against the current value.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
`True` if all rules pass, `False` otherwise.
|
|
206
|
+
"""
|
|
207
|
+
entry = self._internal._entry
|
|
208
|
+
return entry.validate(entry._get_validation_value(), trigger="manual")
|
|
209
|
+
|
|
210
|
+
def focus(self) -> None:
|
|
211
|
+
"""Give keyboard focus to this field."""
|
|
212
|
+
self._entry_widget().focus_set()
|
|
213
|
+
|
|
214
|
+
def clear(self) -> None:
|
|
215
|
+
"""Clear the field, setting the value to `None`."""
|
|
216
|
+
self._internal.value = None
|
|
217
|
+
|
|
218
|
+
# ----- Event shorthands -----
|
|
219
|
+
|
|
220
|
+
@overload
|
|
221
|
+
def on_change(self) -> Stream: ...
|
|
222
|
+
@overload
|
|
223
|
+
def on_change(self, handler: Callable[[ChangeEvent], Any]) -> Subscription: ...
|
|
224
|
+
def on_change(self, handler: Callable[[ChangeEvent], Any] | None = None) -> Stream | Subscription:
|
|
225
|
+
"""Register a callback fired when the time value changes.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
handler: Called with a :class:`~bootstack.events.ChangeEvent`. Omit to
|
|
229
|
+
get a composable :class:`~bootstack.streams.Stream` instead.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
A cancellable :class:`~bootstack.events.Subscription` when a
|
|
233
|
+
handler is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
234
|
+
"""
|
|
235
|
+
return self.on("change", handler)
|
|
236
|
+
|
|
237
|
+
@overload
|
|
238
|
+
def on_submit(self) -> Stream: ...
|
|
239
|
+
@overload
|
|
240
|
+
def on_submit(self, handler: Callable[[Event], Any]) -> Subscription: ...
|
|
241
|
+
def on_submit(self, handler: Callable[[Event], Any] | None = None) -> Stream | Subscription:
|
|
242
|
+
"""Register a callback fired when the user presses Return to confirm input.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
handler: Called with an :class:`~bootstack.events.Event`. Omit to
|
|
246
|
+
get a composable :class:`~bootstack.streams.Stream` instead.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
A cancellable :class:`~bootstack.events.Subscription` when a
|
|
250
|
+
handler is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
251
|
+
"""
|
|
252
|
+
return self.on("submit", handler)
|
|
253
|
+
|
|
254
|
+
@overload
|
|
255
|
+
def on_valid(self) -> Stream: ...
|
|
256
|
+
@overload
|
|
257
|
+
def on_valid(self, handler: Callable[[ValidationEvent], Any]) -> Subscription: ...
|
|
258
|
+
def on_valid(self, handler: Callable[[ValidationEvent], Any] | None = None) -> Stream | Subscription:
|
|
259
|
+
"""Register a callback fired when validation passes.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
handler: Called with a :class:`~bootstack.events.ValidationEvent`. Omit to
|
|
263
|
+
get a composable :class:`~bootstack.streams.Stream` instead.
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
A cancellable :class:`~bootstack.events.Subscription` when a
|
|
267
|
+
handler is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
268
|
+
"""
|
|
269
|
+
return self.on("valid", handler)
|
|
270
|
+
|
|
271
|
+
@overload
|
|
272
|
+
def on_invalid(self) -> Stream: ...
|
|
273
|
+
@overload
|
|
274
|
+
def on_invalid(self, handler: Callable[[ValidationEvent], Any]) -> Subscription: ...
|
|
275
|
+
def on_invalid(self, handler: Callable[[ValidationEvent], Any] | None = None) -> Stream | Subscription:
|
|
276
|
+
"""Register a callback fired when validation fails.
|
|
277
|
+
|
|
278
|
+
Args:
|
|
279
|
+
handler: Called with a :class:`~bootstack.events.ValidationEvent`. Omit to
|
|
280
|
+
get a composable :class:`~bootstack.streams.Stream` instead.
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
A cancellable :class:`~bootstack.events.Subscription` when a
|
|
284
|
+
handler is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
285
|
+
"""
|
|
286
|
+
return self.on("invalid", handler)
|
|
287
|
+
|
|
288
|
+
@overload
|
|
289
|
+
def on_validate(self) -> Stream: ...
|
|
290
|
+
@overload
|
|
291
|
+
def on_validate(self, handler: Callable[[ValidationEvent], Any]) -> Subscription: ...
|
|
292
|
+
def on_validate(self, handler: Callable[[ValidationEvent], Any] | None = None) -> Stream | Subscription:
|
|
293
|
+
"""Register a callback fired when a validation check runs.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
handler: Called with a :class:`~bootstack.events.ValidationEvent`. Omit to
|
|
297
|
+
get a composable :class:`~bootstack.streams.Stream` instead.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
A cancellable :class:`~bootstack.events.Subscription` when a
|
|
301
|
+
handler is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
302
|
+
"""
|
|
303
|
+
return self.on("validate", handler)
|
|
304
|
+
|
|
305
|
+
@overload
|
|
306
|
+
def on_focus(self) -> Stream: ...
|
|
307
|
+
@overload
|
|
308
|
+
def on_focus(self, handler: Callable[[Event], Any]) -> Subscription: ...
|
|
309
|
+
def on_focus(self, handler: Callable[[Event], Any] | None = None) -> Stream | Subscription:
|
|
310
|
+
"""Register a callback fired when the field gains keyboard focus.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
handler: Called with an :class:`~bootstack.events.Event`. Omit to
|
|
314
|
+
get a composable :class:`~bootstack.streams.Stream` instead.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
A cancellable :class:`~bootstack.events.Subscription` when a
|
|
318
|
+
handler is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
319
|
+
"""
|
|
320
|
+
return self.on("focus", handler)
|
|
321
|
+
|
|
322
|
+
@overload
|
|
323
|
+
def on_blur(self) -> Stream: ...
|
|
324
|
+
@overload
|
|
325
|
+
def on_blur(self, handler: Callable[[Event], Any]) -> Subscription: ...
|
|
326
|
+
def on_blur(self, handler: Callable[[Event], Any] | None = None) -> Stream | Subscription:
|
|
327
|
+
"""Register a callback fired when the field loses keyboard focus.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
handler: Called with an :class:`~bootstack.events.Event`. Omit to
|
|
331
|
+
get a composable :class:`~bootstack.streams.Stream` instead.
|
|
332
|
+
|
|
333
|
+
Returns:
|
|
334
|
+
A cancellable :class:`~bootstack.events.Subscription` when a
|
|
335
|
+
handler is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
336
|
+
"""
|
|
337
|
+
return self.on("blur", handler)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
register_widget_events(TimeField, _TIME_FIELD_EVENTS)
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Transient message surfaces: toast, notification, and snackbar.
|
|
2
|
+
|
|
3
|
+
Three opinionated, single-purpose facades over the shared internal toast engine:
|
|
4
|
+
|
|
5
|
+
- `toast()` — a passive, auto-dismissing message that floats in a screen corner
|
|
6
|
+
and stacks. Fire-and-forget.
|
|
7
|
+
- `Notification` — a persistent corner message with a close button; the user
|
|
8
|
+
decides when it goes away. Stacks alongside toasts.
|
|
9
|
+
- `Snackbar` / `snackbar()` — a transient message bound to the app window's
|
|
10
|
+
bottom edge with an optional single action (e.g. *Undo*). Only one shows at a
|
|
11
|
+
time; a new one replaces it.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any, Callable
|
|
16
|
+
|
|
17
|
+
from bootstack.widgets._impl.composites.toast import Toast as _InternalToast
|
|
18
|
+
from bootstack.widgets._impl.composites.toast_stack import get_toast_manager
|
|
19
|
+
from bootstack.widgets.types import AccentToken, Icon, ToastCorner, SnackbarAlign
|
|
20
|
+
|
|
21
|
+
_TOAST_DEFAULT_DURATION = 4000
|
|
22
|
+
_SNACKBAR_DEFAULT_DURATION = 5000
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _windowing_system() -> str:
|
|
26
|
+
"""The active Tk windowing system (`'win32'`, `'aqua'`, or `'x11'`)."""
|
|
27
|
+
from bootstack._runtime.app import get_default_root
|
|
28
|
+
|
|
29
|
+
root = get_default_root()
|
|
30
|
+
if root is not None:
|
|
31
|
+
try:
|
|
32
|
+
return root.tk.call("tk", "windowingsystem")
|
|
33
|
+
except Exception:
|
|
34
|
+
pass
|
|
35
|
+
return "win32"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _resolve_corner(corner: ToastCorner | None, *, persistent: bool) -> ToastCorner:
|
|
39
|
+
"""Resolve the default corner for a toast or notification.
|
|
40
|
+
|
|
41
|
+
Notifications default to the top-right everywhere. Passive toasts default to
|
|
42
|
+
the bottom-right on Windows and macOS, and the top-right on Linux (X11),
|
|
43
|
+
matching each platform's native notification placement.
|
|
44
|
+
"""
|
|
45
|
+
if corner is not None:
|
|
46
|
+
return corner
|
|
47
|
+
if persistent:
|
|
48
|
+
return "top-right"
|
|
49
|
+
return "bottom-right" if _windowing_system() in ("win32", "aqua") else "top-right"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# toast() — passive, auto-dismiss, stacks in a screen corner
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
def toast(
|
|
57
|
+
message: str,
|
|
58
|
+
*,
|
|
59
|
+
title: str | None = None,
|
|
60
|
+
corner: ToastCorner | None = None,
|
|
61
|
+
duration: int = _TOAST_DEFAULT_DURATION,
|
|
62
|
+
accent: AccentToken | str | None = None,
|
|
63
|
+
icon: Icon | None = None,
|
|
64
|
+
on_dismiss: Callable[[], Any] | None = None,
|
|
65
|
+
) -> None:
|
|
66
|
+
"""Show a passive toast that dismisses itself, and return immediately.
|
|
67
|
+
|
|
68
|
+
A toast is the lightest notification — "the app telling you something" with
|
|
69
|
+
no demand for a response. It floats in a corner of the monitor the app
|
|
70
|
+
window is on, stacks below any toasts already there, and fades after
|
|
71
|
+
`duration`. For a message the user must dismiss, use `Notification`; for one
|
|
72
|
+
tied to the window with an action, use `Snackbar`.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
message: Body text of the toast.
|
|
76
|
+
title: Optional header shown above the message.
|
|
77
|
+
corner: Screen corner to anchor to — `'top-left'`, `'top-right'`,
|
|
78
|
+
`'bottom-left'`, or `'bottom-right'`. Defaults to the bottom-right
|
|
79
|
+
(top-right on Linux).
|
|
80
|
+
duration: Time on screen in milliseconds before it auto-dismisses.
|
|
81
|
+
Defaults to `4000`.
|
|
82
|
+
accent: Semantic color accent (e.g. `'success'`, `'danger'`). Defaults
|
|
83
|
+
to a neutral surface.
|
|
84
|
+
icon: Optional Bootstrap Icons name (e.g. `'check-circle'`) or an icon
|
|
85
|
+
spec dict with `name`, `size`, and `color`, shown before the message.
|
|
86
|
+
No icon is shown unless given.
|
|
87
|
+
on_dismiss: Called with no arguments when the toast dismisses.
|
|
88
|
+
"""
|
|
89
|
+
manager = get_toast_manager()
|
|
90
|
+
resolved = _resolve_corner(corner, persistent=False)
|
|
91
|
+
|
|
92
|
+
engine = _InternalToast(
|
|
93
|
+
title=title,
|
|
94
|
+
message=message,
|
|
95
|
+
icon=icon,
|
|
96
|
+
accent=accent,
|
|
97
|
+
duration=duration,
|
|
98
|
+
show_close_button=False,
|
|
99
|
+
place=manager.corner_placer(resolved),
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def _on_dismissed(_data: Any) -> None:
|
|
103
|
+
manager.remove(resolved, engine._toplevel)
|
|
104
|
+
if on_dismiss is not None:
|
|
105
|
+
on_dismiss()
|
|
106
|
+
|
|
107
|
+
engine.configure(on_dismissed=_on_dismissed)
|
|
108
|
+
engine.show()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ---------------------------------------------------------------------------
|
|
112
|
+
# Notification — persistent, close button, stacks in a screen corner
|
|
113
|
+
# ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
class Notification:
|
|
116
|
+
"""A persistent corner message the user closes themselves.
|
|
117
|
+
|
|
118
|
+
Like a toast, a notification floats in a screen corner and stacks — but it
|
|
119
|
+
does not auto-dismiss. It carries a close button and stays until the user
|
|
120
|
+
closes it or your code calls `dismiss()`. Use it for information that should
|
|
121
|
+
persist (a finished background job, an available update) rather than flash
|
|
122
|
+
by. Build it, then call `show()`.
|
|
123
|
+
|
|
124
|
+
Notifications are message-only — for a response, use a `Snackbar` action or
|
|
125
|
+
a dialog.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
title: Header text — the notification's headline.
|
|
129
|
+
message: Body text shown below the title.
|
|
130
|
+
detail: Small muted metadata shown in the header (e.g. `'just now'`).
|
|
131
|
+
corner: Screen corner to anchor to — `'top-left'`, `'top-right'`,
|
|
132
|
+
`'bottom-left'`, or `'bottom-right'`. Defaults to the top-right.
|
|
133
|
+
accent: Semantic color accent. Defaults to a neutral surface.
|
|
134
|
+
icon: Optional Bootstrap Icons name or an icon spec dict, shown before
|
|
135
|
+
the title. No icon is shown unless given.
|
|
136
|
+
on_dismiss: Called with no arguments when the notification closes.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(
|
|
140
|
+
self,
|
|
141
|
+
title: str | None = None,
|
|
142
|
+
*,
|
|
143
|
+
message: str | None = None,
|
|
144
|
+
detail: str | None = None,
|
|
145
|
+
corner: ToastCorner | None = None,
|
|
146
|
+
accent: AccentToken | str | None = None,
|
|
147
|
+
icon: Icon | None = None,
|
|
148
|
+
on_dismiss: Callable[[], Any] | None = None,
|
|
149
|
+
) -> None:
|
|
150
|
+
self._title = title
|
|
151
|
+
self._message = message
|
|
152
|
+
self._detail = detail
|
|
153
|
+
self._corner = corner
|
|
154
|
+
self._accent = accent
|
|
155
|
+
self._icon = icon
|
|
156
|
+
self._on_dismiss = on_dismiss
|
|
157
|
+
self._engine: _InternalToast | None = None
|
|
158
|
+
|
|
159
|
+
def show(self) -> "Notification":
|
|
160
|
+
"""Display the notification. Returns `self` for chaining."""
|
|
161
|
+
manager = get_toast_manager()
|
|
162
|
+
resolved = _resolve_corner(self._corner, persistent=True)
|
|
163
|
+
|
|
164
|
+
engine = _InternalToast(
|
|
165
|
+
title=self._title,
|
|
166
|
+
message=self._message,
|
|
167
|
+
memo=self._detail,
|
|
168
|
+
icon=self._icon,
|
|
169
|
+
accent=self._accent,
|
|
170
|
+
duration=None,
|
|
171
|
+
show_close_button=True,
|
|
172
|
+
place=manager.corner_placer(resolved),
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
def _on_dismissed(_data: Any) -> None:
|
|
176
|
+
manager.remove(resolved, engine._toplevel)
|
|
177
|
+
if self._on_dismiss is not None:
|
|
178
|
+
self._on_dismiss()
|
|
179
|
+
|
|
180
|
+
engine.configure(on_dismissed=_on_dismissed)
|
|
181
|
+
self._engine = engine
|
|
182
|
+
engine.show()
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
def dismiss(self) -> None:
|
|
186
|
+
"""Close the notification immediately."""
|
|
187
|
+
if self._engine is not None:
|
|
188
|
+
self._engine.hide()
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ---------------------------------------------------------------------------
|
|
192
|
+
# Snackbar — transient, one action, anchored to the window bottom edge
|
|
193
|
+
# ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
class Snackbar:
|
|
196
|
+
"""A transient message at the app window's bottom edge with one action.
|
|
197
|
+
|
|
198
|
+
A snackbar belongs to the app, not the screen: it slides up from the bottom
|
|
199
|
+
of the window and moves with it. It is for brief, in-context feedback that
|
|
200
|
+
may offer a single response — the classic *"Message archived. [Undo]"*. Only
|
|
201
|
+
one snackbar shows at a time; showing another replaces it. Build it, then
|
|
202
|
+
call `show()`.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
message: Body text of the snackbar.
|
|
206
|
+
action: Label for the optional action button (e.g. `'Undo'`). Omit for
|
|
207
|
+
a message with no action.
|
|
208
|
+
on_action: Called with no arguments when the action button is pressed.
|
|
209
|
+
The snackbar dismisses afterward.
|
|
210
|
+
align: Horizontal placement along the window's bottom edge — `'left'`,
|
|
211
|
+
`'center'`, or `'right'`. Defaults to `'center'`.
|
|
212
|
+
duration: Time on screen in milliseconds before it auto-dismisses.
|
|
213
|
+
Defaults to `5000`.
|
|
214
|
+
accent: Semantic color accent for the action button (the surface stays
|
|
215
|
+
neutral — severity coloring belongs to `toast`/`Notification`).
|
|
216
|
+
Defaults to `'primary'`. No effect without an `action`.
|
|
217
|
+
on_dismiss: Called with no arguments when the snackbar dismisses (whether
|
|
218
|
+
by timeout, the action, or `dismiss()`).
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
message: str,
|
|
224
|
+
*,
|
|
225
|
+
action: str | None = None,
|
|
226
|
+
on_action: Callable[[], Any] | None = None,
|
|
227
|
+
align: SnackbarAlign = "center",
|
|
228
|
+
duration: int = _SNACKBAR_DEFAULT_DURATION,
|
|
229
|
+
accent: AccentToken | str | None = None,
|
|
230
|
+
on_dismiss: Callable[[], Any] | None = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
self._message = message
|
|
233
|
+
self._action = action
|
|
234
|
+
self._on_action = on_action
|
|
235
|
+
self._align = align
|
|
236
|
+
self._duration = duration
|
|
237
|
+
self._accent = accent
|
|
238
|
+
self._on_dismiss = on_dismiss
|
|
239
|
+
self._engine: _InternalToast | None = None
|
|
240
|
+
|
|
241
|
+
def show(self) -> "Snackbar":
|
|
242
|
+
"""Display the snackbar, replacing any current one. Returns `self`."""
|
|
243
|
+
manager = get_toast_manager()
|
|
244
|
+
|
|
245
|
+
buttons = None
|
|
246
|
+
if self._action is not None:
|
|
247
|
+
buttons = [{
|
|
248
|
+
"text": self._action,
|
|
249
|
+
"command": self._on_action,
|
|
250
|
+
"variant": "ghost",
|
|
251
|
+
"accent": self._accent or "primary",
|
|
252
|
+
}]
|
|
253
|
+
|
|
254
|
+
# Neutral surface (no accent) and no icon — a snackbar is in-app action
|
|
255
|
+
# feedback, not a severity message. The accent rides only on the action.
|
|
256
|
+
engine = _InternalToast(
|
|
257
|
+
message=self._message,
|
|
258
|
+
duration=self._duration,
|
|
259
|
+
show_close_button=False,
|
|
260
|
+
buttons=buttons,
|
|
261
|
+
place=manager.snackbar_placer(self._align),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
def _on_dismissed(_data: Any) -> None:
|
|
265
|
+
manager.clear_snackbar(engine._toplevel)
|
|
266
|
+
if self._on_dismiss is not None:
|
|
267
|
+
self._on_dismiss()
|
|
268
|
+
|
|
269
|
+
engine.configure(on_dismissed=_on_dismissed)
|
|
270
|
+
self._engine = engine
|
|
271
|
+
engine.show()
|
|
272
|
+
return self
|
|
273
|
+
|
|
274
|
+
def dismiss(self) -> None:
|
|
275
|
+
"""Dismiss the snackbar immediately."""
|
|
276
|
+
if self._engine is not None:
|
|
277
|
+
self._engine.hide()
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def snackbar(
|
|
281
|
+
message: str,
|
|
282
|
+
*,
|
|
283
|
+
action: str | None = None,
|
|
284
|
+
on_action: Callable[[], Any] | None = None,
|
|
285
|
+
align: SnackbarAlign = "center",
|
|
286
|
+
duration: int = _SNACKBAR_DEFAULT_DURATION,
|
|
287
|
+
accent: AccentToken | str | None = None,
|
|
288
|
+
on_dismiss: Callable[[], Any] | None = None,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Show a snackbar at the window's bottom edge, and return immediately.
|
|
291
|
+
|
|
292
|
+
The one-line form of `Snackbar` — see it for the full description. Use the
|
|
293
|
+
class when you need a handle to `dismiss()` early.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
message: Body text of the snackbar.
|
|
297
|
+
action: Label for the optional action button (e.g. `'Undo'`).
|
|
298
|
+
on_action: Called with no arguments when the action is pressed.
|
|
299
|
+
align: Bottom-edge alignment — `'left'`, `'center'`, or `'right'`.
|
|
300
|
+
Defaults to `'center'`.
|
|
301
|
+
duration: Time on screen in milliseconds. Defaults to `5000`.
|
|
302
|
+
accent: Semantic color accent for the action button (the surface stays
|
|
303
|
+
neutral). No effect without an `action`.
|
|
304
|
+
on_dismiss: Called with no arguments when the snackbar dismisses.
|
|
305
|
+
"""
|
|
306
|
+
Snackbar(
|
|
307
|
+
message,
|
|
308
|
+
action=action,
|
|
309
|
+
on_action=on_action,
|
|
310
|
+
align=align,
|
|
311
|
+
duration=duration,
|
|
312
|
+
accent=accent,
|
|
313
|
+
on_dismiss=on_dismiss,
|
|
314
|
+
).show()
|