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,201 @@
|
|
|
1
|
+
"""Numeric entry field widget with spin buttons.
|
|
2
|
+
|
|
3
|
+
Provides a specialized entry field for numeric input with increment/decrement
|
|
4
|
+
buttons and keyboard/mouse wheel support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Callable
|
|
8
|
+
from tkinter import TclError
|
|
9
|
+
from typing_extensions import Unpack
|
|
10
|
+
|
|
11
|
+
from bootstack.widgets._impl.primitives.button import Button
|
|
12
|
+
from bootstack.widgets._impl.composites.field import Field, FieldOptions
|
|
13
|
+
from bootstack.widgets._impl.mixins import configure_delegate
|
|
14
|
+
from bootstack.widgets.types import Master
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NumericEntry(Field):
|
|
18
|
+
"""A numeric entry field widget with increment/decrement spin buttons.
|
|
19
|
+
|
|
20
|
+
Extends Field to provide numeric input with spin buttons, bounds validation,
|
|
21
|
+
keyboard stepping (Up/Down arrows), mouse wheel support, and optional wrapping.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
master: Master = None,
|
|
27
|
+
value: int | float = 0,
|
|
28
|
+
label: str = None,
|
|
29
|
+
message: str = None,
|
|
30
|
+
show_spin_buttons: bool = True,
|
|
31
|
+
minvalue: int | float | None = None,
|
|
32
|
+
maxvalue: int | float | None = None,
|
|
33
|
+
increment: int | float = 1,
|
|
34
|
+
**kwargs: Unpack[FieldOptions]
|
|
35
|
+
):
|
|
36
|
+
"""Args:
|
|
37
|
+
master: Parent widget. If None, uses the default root window.
|
|
38
|
+
value: Initial numeric value to display.
|
|
39
|
+
label: Optional label text to display above the entry field.
|
|
40
|
+
If `required=True`, an asterisk (*) is automatically appended.
|
|
41
|
+
message: Optional message text to display below the entry field.
|
|
42
|
+
Used for hints or help text. Replaced by validation errors when
|
|
43
|
+
validation fails.
|
|
44
|
+
show_spin_buttons: If True, displays increment/decrement buttons
|
|
45
|
+
(plus and minus icons) to the right of the entry.
|
|
46
|
+
minvalue: Minimum allowed value (inclusive). Values below
|
|
47
|
+
this will be clamped or wrapped depending on the wrap setting.
|
|
48
|
+
maxvalue: Maximum allowed value (inclusive). Values above
|
|
49
|
+
this will be clamped or wrapped depending on the wrap setting.
|
|
50
|
+
increment: Step size for increment/decrement operations.
|
|
51
|
+
|
|
52
|
+
Other Parameters:
|
|
53
|
+
wrap: If True, values wrap around at min/max boundaries.
|
|
54
|
+
value_format: Number format — a `NumberPreset` string
|
|
55
|
+
(e.g., `'decimal'`, `'percent'`, `'currency'`) or a custom ICU
|
|
56
|
+
pattern (e.g., `'#,##0.00'`).
|
|
57
|
+
locale: Locale identifier for number formatting (e.g., `'en_US'`).
|
|
58
|
+
required: If True, field cannot be empty.
|
|
59
|
+
accent: Accent token for the focus ring and active border.
|
|
60
|
+
allow_blank: If True, empty input is allowed (sets value to None).
|
|
61
|
+
cursor: Cursor style when hovering.
|
|
62
|
+
exportselection: Export selection to clipboard.
|
|
63
|
+
font: Font for text display.
|
|
64
|
+
foreground: Text color.
|
|
65
|
+
initial_focus: If True, widget receives focus on creation.
|
|
66
|
+
justify: Text alignment (`'left'`, `'center'`, `'right'`).
|
|
67
|
+
show_message: If True, displays message area.
|
|
68
|
+
padding: Padding around entry widget.
|
|
69
|
+
takefocus: If True, widget accepts Tab focus.
|
|
70
|
+
textvariable: Tkinter Variable to link with text.
|
|
71
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
72
|
+
textsignal: Signal object for reactive updates.
|
|
73
|
+
width: Width in characters.
|
|
74
|
+
xscrollcommand: Callback for horizontal scrolling.
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(
|
|
77
|
+
master, value=value, label=label, message=message, minvalue=minvalue, maxvalue=maxvalue, kind="numeric",
|
|
78
|
+
increment=increment, **kwargs)
|
|
79
|
+
|
|
80
|
+
self._show_spin_buttons = show_spin_buttons
|
|
81
|
+
|
|
82
|
+
# pack info
|
|
83
|
+
self._increment_pack_info = {}
|
|
84
|
+
self._decrement_pack_info = {}
|
|
85
|
+
|
|
86
|
+
# buttons
|
|
87
|
+
self.insert_addon(
|
|
88
|
+
Button, position="after", name="decrement", icon="dash-lg", command=self.decrement, icon_only=True)
|
|
89
|
+
self.insert_addon(
|
|
90
|
+
Button, position="after", name="increment", icon="plus-lg", command=self.increment, icon_only=True)
|
|
91
|
+
self._delegate_show_spin_buttons(show_spin_buttons)
|
|
92
|
+
|
|
93
|
+
self.entry_widget.bind('<<StateChanged>>', self._on_entry_state_changed, add=True)
|
|
94
|
+
self._update_spin_button_states()
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def increment_widget(self) -> Button:
|
|
98
|
+
"""Get the increment spin button widget."""
|
|
99
|
+
return self.addons['increment']
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def decrement_widget(self) -> Button:
|
|
103
|
+
"""Get the decrement spin button widget."""
|
|
104
|
+
return self.addons['decrement']
|
|
105
|
+
|
|
106
|
+
def increment(self) -> None:
|
|
107
|
+
"""Increment the numeric value by one step."""
|
|
108
|
+
if not self._entry_is_interactive():
|
|
109
|
+
return
|
|
110
|
+
self.entry_widget.event_generate("<<Increment>>")
|
|
111
|
+
|
|
112
|
+
def decrement(self) -> None:
|
|
113
|
+
"""Decrement the numeric value by one step."""
|
|
114
|
+
if not self._entry_is_interactive():
|
|
115
|
+
return
|
|
116
|
+
self.entry_widget.event_generate("<<Decrement>>")
|
|
117
|
+
|
|
118
|
+
def step(self, n: int = 1) -> None:
|
|
119
|
+
"""Step the value by `n` increments (positive) or decrements (negative).
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
n: Number of steps. Positive steps up, negative steps down.
|
|
123
|
+
"""
|
|
124
|
+
self.entry_widget.step(n)
|
|
125
|
+
|
|
126
|
+
def on_increment(self, callback: Callable) -> str:
|
|
127
|
+
"""Register a callback for `<<Increment>>` events (fires when value steps up).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
callback: Receives a Tkinter `Event` object with `event.data['value']`
|
|
131
|
+
set to the new numeric value.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Bind ID — pass to `off_increment()` to unsubscribe.
|
|
135
|
+
"""
|
|
136
|
+
return self.entry_widget.on_increment(callback)
|
|
137
|
+
|
|
138
|
+
def off_increment(self, bind_id: str | None = None) -> None:
|
|
139
|
+
"""Unsubscribe from `<<Increment>>`.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
bind_id: ID returned by `on_increment()`.
|
|
143
|
+
"""
|
|
144
|
+
self.entry_widget.off_increment(bind_id)
|
|
145
|
+
|
|
146
|
+
def on_decrement(self, callback: Callable) -> str:
|
|
147
|
+
"""Register a callback for `<<Decrement>>` events (fires when value steps down).
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
callback: Receives a Tkinter `Event` object with `event.data['value']`
|
|
151
|
+
set to the new numeric value.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Bind ID — pass to `off_decrement()` to unsubscribe.
|
|
155
|
+
"""
|
|
156
|
+
return self.entry_widget.on_decrement(callback)
|
|
157
|
+
|
|
158
|
+
def off_decrement(self, bind_id: str | None = None) -> None:
|
|
159
|
+
"""Unsubscribe from `<<Decrement>>`.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
bind_id: ID returned by `on_decrement()`.
|
|
163
|
+
"""
|
|
164
|
+
self.entry_widget.off_decrement(bind_id)
|
|
165
|
+
|
|
166
|
+
def _entry_is_interactive(self) -> bool:
|
|
167
|
+
"""Return True if the entry widget is not disabled or readonly."""
|
|
168
|
+
state = self.entry_widget.state()
|
|
169
|
+
return 'disabled' not in state and 'readonly' not in state
|
|
170
|
+
|
|
171
|
+
def _on_entry_state_changed(self, _: Any = None):
|
|
172
|
+
"""Sync spin buttons whenever the entry state mutates."""
|
|
173
|
+
self._update_spin_button_states()
|
|
174
|
+
|
|
175
|
+
def _update_spin_button_states(self):
|
|
176
|
+
"""Enable or disable spin buttons based on entry interactivity."""
|
|
177
|
+
state_value = 'disabled' if not self._entry_is_interactive() else '!disabled'
|
|
178
|
+
for control in (self.increment_widget, self.decrement_widget):
|
|
179
|
+
try:
|
|
180
|
+
control.configure(state=state_value)
|
|
181
|
+
except TclError:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
@configure_delegate('show_spin_buttons')
|
|
185
|
+
def _delegate_show_spin_buttons(self, value: bool = None):
|
|
186
|
+
"""Get or set the visibility of spin buttons."""
|
|
187
|
+
if value is None:
|
|
188
|
+
return self._show_spin_buttons
|
|
189
|
+
else:
|
|
190
|
+
self._show_spin_buttons = value
|
|
191
|
+
if value:
|
|
192
|
+
if not self.increment_widget.winfo_ismapped():
|
|
193
|
+
self.increment_widget.pack(**self._increment_pack_info)
|
|
194
|
+
if not self.decrement_widget.winfo_ismapped():
|
|
195
|
+
self.decrement_widget.pack(**self._decrement_pack_info)
|
|
196
|
+
else:
|
|
197
|
+
self._increment_pack_info = self.increment_widget.pack_info()
|
|
198
|
+
self._decrement_pack_info = self.decrement_widget.pack_info()
|
|
199
|
+
self.increment_widget.pack_forget()
|
|
200
|
+
self.decrement_widget.pack_forget()
|
|
201
|
+
return None
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import tkinter
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
|
|
4
|
+
from typing_extensions import TypedDict, Unpack
|
|
5
|
+
|
|
6
|
+
from bootstack.events import PageChangeEvent
|
|
7
|
+
from bootstack.widgets._impl.primitives import Frame
|
|
8
|
+
from bootstack.widgets.types import Master, AccentToken, SurfaceToken
|
|
9
|
+
from bootstack._core import NavigationError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PageOptions(TypedDict, total=False):
|
|
13
|
+
padding: Any
|
|
14
|
+
width: int
|
|
15
|
+
height: int
|
|
16
|
+
style: str
|
|
17
|
+
cursor: str
|
|
18
|
+
show_border: bool
|
|
19
|
+
accent: AccentToken | str
|
|
20
|
+
variant: str
|
|
21
|
+
surface: SurfaceToken | str
|
|
22
|
+
style_options: dict[str, Any]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PageStackKwargs(TypedDict, total=False):
|
|
26
|
+
takefocus: bool
|
|
27
|
+
width: int
|
|
28
|
+
height: int
|
|
29
|
+
padding: Any
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PageStack(Frame):
|
|
33
|
+
"""A navigation container widget for managing multiple pages with history support.
|
|
34
|
+
|
|
35
|
+
PageStack provides a stack-based navigation system where only one page is
|
|
36
|
+
visible at a time. It maintains a navigation history, allowing users to move
|
|
37
|
+
backward and forward through pages similar to a web browser.
|
|
38
|
+
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, master: Master = None, **kwargs: Unpack[PageStackKwargs]):
|
|
42
|
+
"""Initialize a new PageStack instance.
|
|
43
|
+
|
|
44
|
+
Creates an empty PageStack with no pages and no navigation history.
|
|
45
|
+
The widget inherits from Frame and supports all standard Frame
|
|
46
|
+
configuration options.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
master: Parent widget. If None, uses the default root window.
|
|
50
|
+
**kwargs: Additional arguments passed to Frame (e.g., `takefocus`,
|
|
51
|
+
`width`, `height`, `padding`).
|
|
52
|
+
"""
|
|
53
|
+
super().__init__(master, **kwargs)
|
|
54
|
+
self._pages: dict[str, tkinter.Widget] = {}
|
|
55
|
+
self._current: str | None = None
|
|
56
|
+
self._history: list[tuple[str, dict]] = []
|
|
57
|
+
self._index: int = -1
|
|
58
|
+
# macOS (Aqua) only: re-mapping a previously-unmapped page paints blank
|
|
59
|
+
# for one display cycle, so swapping pages with pack_forget/pack flashes
|
|
60
|
+
# white. There we instead keep every visited page mapped, stacked in a
|
|
61
|
+
# single grid cell, and switch with lift()/lower() — no remap, no flash.
|
|
62
|
+
# A page must stay *full size* while hidden for this to hold (a shrunk
|
|
63
|
+
# page re-expands with the same blank flash), so each mapped page does
|
|
64
|
+
# reflow on a window resize; that cost is the deliberate trade. Other
|
|
65
|
+
# platforms don't have the flash and keep the cheaper one-page-mapped
|
|
66
|
+
# pack path unchanged.
|
|
67
|
+
try:
|
|
68
|
+
self._stacked: bool = self.tk.call('tk', 'windowingsystem') == 'aqua'
|
|
69
|
+
except tkinter.TclError:
|
|
70
|
+
self._stacked = False
|
|
71
|
+
|
|
72
|
+
def add(self, key: str, page: tkinter.Widget = None, **kwargs) -> tkinter.Widget:
|
|
73
|
+
"""Add a page to the stack, optionally creating a Frame.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
key: Unique identifier for the page (required for navigation).
|
|
77
|
+
page: The widget to add. If None, creates a Frame.
|
|
78
|
+
**kwargs: When page is None, these are passed to Frame (e.g., padding, accent, variant).
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Widget: The page widget (passed or created Frame).
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
NavigationError: If a page with the given key already exists.
|
|
85
|
+
ValueError: If key is an empty string.
|
|
86
|
+
"""
|
|
87
|
+
if not key:
|
|
88
|
+
raise ValueError("Page key cannot be an empty string")
|
|
89
|
+
if key in self._pages:
|
|
90
|
+
raise NavigationError(f"Page {key} already exists")
|
|
91
|
+
|
|
92
|
+
if page is None:
|
|
93
|
+
page = Frame(self, **kwargs)
|
|
94
|
+
|
|
95
|
+
self._pages[key] = page
|
|
96
|
+
if self._stacked:
|
|
97
|
+
# Stacked layout: every page shares grid cell (0, 0); navigate()
|
|
98
|
+
# maps/lifts the active one. Left unmapped here — it maps on first
|
|
99
|
+
# show (the initial render always paints fresh, flash or not).
|
|
100
|
+
self.grid_rowconfigure(0, weight=1)
|
|
101
|
+
self.grid_columnconfigure(0, weight=1)
|
|
102
|
+
page._bs_nav_hidden = True
|
|
103
|
+
else:
|
|
104
|
+
page.pack(fill='both', expand=True)
|
|
105
|
+
page.pack_forget()
|
|
106
|
+
return page
|
|
107
|
+
|
|
108
|
+
def remove(self, key: str) -> None:
|
|
109
|
+
"""Remove a page from the stack and destroy its widget.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
key: The identifier of the page to remove
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
NavigationError: If no page with the given key exists.
|
|
116
|
+
ValueError: If key is an empty string.
|
|
117
|
+
|
|
118
|
+
Note:
|
|
119
|
+
If the removed page is currently displayed, the current page
|
|
120
|
+
will be set to None without navigating to another page.
|
|
121
|
+
"""
|
|
122
|
+
if not key:
|
|
123
|
+
raise ValueError("Page key cannot be an empty string")
|
|
124
|
+
if key not in self._pages:
|
|
125
|
+
raise NavigationError(f"Page {key} does not exist")
|
|
126
|
+
page = self._pages.pop(key)
|
|
127
|
+
page.destroy()
|
|
128
|
+
if self._current == key:
|
|
129
|
+
self._current = None
|
|
130
|
+
|
|
131
|
+
def navigate(
|
|
132
|
+
self,
|
|
133
|
+
key: str,
|
|
134
|
+
data: dict | None = None,
|
|
135
|
+
replace: bool = False,
|
|
136
|
+
_nav: str = 'push',
|
|
137
|
+
_prev: tuple[str, dict] | None = None
|
|
138
|
+
) -> None:
|
|
139
|
+
"""Navigate to the page with the given key.
|
|
140
|
+
|
|
141
|
+
This method handles page transitions, manages navigation history,
|
|
142
|
+
and triggers lifecycle events (`<<PageUnmount>>`, `<<PageWillMount>>`,
|
|
143
|
+
`<<PageMount>>`, `<<PageChange>>`).
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
key: The identifier of the page to navigate to
|
|
147
|
+
data: Optional data to pass to the page and include in event payloads
|
|
148
|
+
replace: If True, replace the current history entry instead of adding a new one
|
|
149
|
+
_nav: Internal parameter indicating navigation type ('push', 'back', 'forward')
|
|
150
|
+
_prev: Internal parameter for preserving previous page state
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
NavigationError: If the page with the given key does not exist
|
|
154
|
+
ValueError: If key is an empty string
|
|
155
|
+
|
|
156
|
+
Note:
|
|
157
|
+
Event payloads include: page, prev_page, prev_data, nav, index,
|
|
158
|
+
length, can_back, and can_forward.
|
|
159
|
+
"""
|
|
160
|
+
if not key:
|
|
161
|
+
raise ValueError("Page key cannot be an empty string")
|
|
162
|
+
if key not in self._pages:
|
|
163
|
+
raise NavigationError(f"Page {key} does not exist")
|
|
164
|
+
|
|
165
|
+
# Normalize data to empty dict if None
|
|
166
|
+
if data is None:
|
|
167
|
+
data = {}
|
|
168
|
+
|
|
169
|
+
# Snapshot "previous" BEFORE mutating history
|
|
170
|
+
if _prev is None:
|
|
171
|
+
prev_key = self._current
|
|
172
|
+
prev_data = self._history[self._index][1] if self._index >= 0 else {}
|
|
173
|
+
else:
|
|
174
|
+
prev_key, prev_data = _prev
|
|
175
|
+
|
|
176
|
+
# Mutate history
|
|
177
|
+
if replace and 0 <= self._index < len(self._history):
|
|
178
|
+
self._history[self._index] = (key, data)
|
|
179
|
+
else:
|
|
180
|
+
if self._index < len(self._history) - 1:
|
|
181
|
+
self._history = self._history[:self._index + 1]
|
|
182
|
+
self._history.append((key, data))
|
|
183
|
+
self._index += 1
|
|
184
|
+
|
|
185
|
+
# Unmount the previous page. On Aqua keep it mapped (stacked behind the
|
|
186
|
+
# incoming page) so returning to it doesn't remap-flash; elsewhere unmap
|
|
187
|
+
# it for the cheaper one-page-mapped path.
|
|
188
|
+
if self._stacked:
|
|
189
|
+
if self._current is not None and self._current != key:
|
|
190
|
+
prev_page = self._pages[self._current]
|
|
191
|
+
prev_page.event_generate('<<PageUnmount>>', when="tail")
|
|
192
|
+
prev_page._bs_nav_hidden = True
|
|
193
|
+
prev_page.lower()
|
|
194
|
+
elif self._current is not None:
|
|
195
|
+
# Pre-size the target page while the current one is still mounted, so
|
|
196
|
+
# the swap doesn't briefly collapse the content area (a freshly built
|
|
197
|
+
# page is otherwise unsized at mount time and the view jumps when it
|
|
198
|
+
# settles). Only needed for a real swap — the initial mount has
|
|
199
|
+
# nothing to collapse from, and forcing layout there would serialize
|
|
200
|
+
# the first render (a visible "slow-motion" build of the page).
|
|
201
|
+
self._pages[key].update_idletasks()
|
|
202
|
+
self._pages[self._current].event_generate('<<PageUnmount>>', when="tail")
|
|
203
|
+
self._pages[self._current].pack_forget()
|
|
204
|
+
|
|
205
|
+
# Normalized payload (self-contained snapshot)
|
|
206
|
+
payload = PageChangeEvent(
|
|
207
|
+
page=key,
|
|
208
|
+
prev_page=prev_key,
|
|
209
|
+
nav=_nav,
|
|
210
|
+
data=dict(data),
|
|
211
|
+
prev_data=prev_data,
|
|
212
|
+
index=self._index,
|
|
213
|
+
length=len(self._history),
|
|
214
|
+
can_back=self._index > 0,
|
|
215
|
+
can_forward=self._index < len(self._history) - 1,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Mount and notify
|
|
219
|
+
page = self._pages[key]
|
|
220
|
+
page.event_generate('<<PageWillMount>>', data=payload, when="tail")
|
|
221
|
+
if self._stacked:
|
|
222
|
+
page._bs_nav_hidden = False
|
|
223
|
+
# Map into the shared cell (the first show maps it; later shows are a
|
|
224
|
+
# no-op reconfigure of an already-mapped page) and lift it above the
|
|
225
|
+
# stack. Pages are never unmapped again, so a return never remaps.
|
|
226
|
+
page.grid(row=0, column=0, sticky='nsew')
|
|
227
|
+
page.lift()
|
|
228
|
+
else:
|
|
229
|
+
page.pack(fill='both', expand=True)
|
|
230
|
+
self._current = key
|
|
231
|
+
# A theme change that arrived while this page was hidden left its widgets
|
|
232
|
+
# stale (the walk skips off-screen widgets); recolor the now-visible page.
|
|
233
|
+
# Deferred to idle so the page is mapped first.
|
|
234
|
+
def _recolor_shown(p=page):
|
|
235
|
+
from bootstack.style.style import get_style
|
|
236
|
+
get_style().apply_theme_walk(p, only_stale=True)
|
|
237
|
+
self.after_idle(_recolor_shown)
|
|
238
|
+
self.event_generate('<<PageMount>>', data=payload, when="tail")
|
|
239
|
+
self.event_generate('<<PageChange>>', data=payload, when="tail")
|
|
240
|
+
|
|
241
|
+
def back(self) -> None:
|
|
242
|
+
"""Navigate to the previous page in the navigation history.
|
|
243
|
+
|
|
244
|
+
This method moves backward in the history stack if possible.
|
|
245
|
+
Does nothing if already at the first page.
|
|
246
|
+
"""
|
|
247
|
+
if self._index > 0:
|
|
248
|
+
# Snapshot BEFORE changing index
|
|
249
|
+
prev = (self._current, self._history[self._index][1] if self._index >= 0 else {})
|
|
250
|
+
self._index -= 1
|
|
251
|
+
key, data = self._history[self._index]
|
|
252
|
+
# use replace=True to avoid pushing a new entry
|
|
253
|
+
# pass _prev to preserve the correct 'previous' snapshot
|
|
254
|
+
self.navigate(key, data=data, replace=True, _nav='back', _prev=prev)
|
|
255
|
+
|
|
256
|
+
def forward(self) -> None:
|
|
257
|
+
"""Navigate to the next page in the navigation history.
|
|
258
|
+
|
|
259
|
+
This method moves forward in the history stack if possible.
|
|
260
|
+
Does nothing if already at the most recent page.
|
|
261
|
+
"""
|
|
262
|
+
if self._index < len(self._history) - 1:
|
|
263
|
+
prev = (self._current, self._history[self._index][1] if self._index >= 0 else {})
|
|
264
|
+
self._index += 1
|
|
265
|
+
key, data = self._history[self._index]
|
|
266
|
+
self.navigate(key, data=data, replace=True, _nav='forward', _prev=prev)
|
|
267
|
+
|
|
268
|
+
def can_back(self) -> bool:
|
|
269
|
+
"""Check if backward navigation is possible.
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
True if there is a previous page in the history to navigate back to,
|
|
273
|
+
False otherwise.
|
|
274
|
+
"""
|
|
275
|
+
return self._index > 0
|
|
276
|
+
|
|
277
|
+
def can_forward(self) -> bool:
|
|
278
|
+
"""Check if forward navigation is possible.
|
|
279
|
+
|
|
280
|
+
Returns:
|
|
281
|
+
True if there is a next page in the history to navigate forward to,
|
|
282
|
+
False otherwise.
|
|
283
|
+
"""
|
|
284
|
+
return self._index < len(self._history) - 1
|
|
285
|
+
|
|
286
|
+
def current(self) -> tuple[str, dict] | None:
|
|
287
|
+
"""Return the current page key and its navigation data.
|
|
288
|
+
|
|
289
|
+
Returns:
|
|
290
|
+
A tuple of (page_key, data_dict) if a page is currently displayed,
|
|
291
|
+
None if no page is currently displayed.
|
|
292
|
+
"""
|
|
293
|
+
if self._current is None:
|
|
294
|
+
return None
|
|
295
|
+
return self._current, (self._history[self._index][1] if self._index >= 0 else {})
|
|
296
|
+
|
|
297
|
+
def item(self, key: str) -> tkinter.Widget:
|
|
298
|
+
"""Get a page by its key.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
key: The identifier of the page to retrieve.
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
The page widget.
|
|
305
|
+
|
|
306
|
+
Raises:
|
|
307
|
+
NavigationError: If no page with the given key exists.
|
|
308
|
+
ValueError: If key is an empty string.
|
|
309
|
+
"""
|
|
310
|
+
if not key:
|
|
311
|
+
raise ValueError("Page key cannot be an empty string")
|
|
312
|
+
if key not in self._pages:
|
|
313
|
+
raise NavigationError(f"Page {key} does not exist")
|
|
314
|
+
return self._pages[key]
|
|
315
|
+
|
|
316
|
+
def items(self) -> tuple[tkinter.Widget, ...]:
|
|
317
|
+
"""Get all page widgets in the stack.
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
A tuple of all page widgets managed by this PageStack.
|
|
321
|
+
"""
|
|
322
|
+
return tuple(self._pages.values())
|
|
323
|
+
|
|
324
|
+
def keys(self) -> tuple[str, ...]:
|
|
325
|
+
"""Get all page keys.
|
|
326
|
+
|
|
327
|
+
Returns:
|
|
328
|
+
A tuple of all page keys in the stack.
|
|
329
|
+
"""
|
|
330
|
+
return tuple(self._pages.keys())
|
|
331
|
+
|
|
332
|
+
def configure_item(self, key: str, option: Any = None, **kwargs: Any) -> Any:
|
|
333
|
+
"""Query or configure the page configuration.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
key: The identifier of the page to configure.
|
|
337
|
+
option: Optional configuration option to query.
|
|
338
|
+
**kwargs: Configuration options to set on the page widget.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
If option is provided, returns the value of that option.
|
|
342
|
+
Otherwise returns the result of configure() if kwargs are provided.
|
|
343
|
+
|
|
344
|
+
Raises:
|
|
345
|
+
NavigationError: If no page with the given key exists.
|
|
346
|
+
ValueError: If key is an empty string.
|
|
347
|
+
"""
|
|
348
|
+
if not key:
|
|
349
|
+
raise ValueError("Page key cannot be an empty string")
|
|
350
|
+
if key not in self._pages:
|
|
351
|
+
raise NavigationError(f"Page {key} does not exist")
|
|
352
|
+
if option is not None:
|
|
353
|
+
return self._pages[key].cget(option)
|
|
354
|
+
else:
|
|
355
|
+
return self._pages[key].configure(**kwargs)
|
|
356
|
+
|
|
357
|
+
def on_page_changed(self, callback: Callable) -> str:
|
|
358
|
+
"""Register a callback for `<<PageChange>>` events (fires after every navigation).
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
callback: Receives `event.data` with navigation info: `page`, `prev_page`,
|
|
362
|
+
`nav`, `index`, `length`, `can_back`, `can_forward`.
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
Bind ID — pass to `off_page_changed()` to unsubscribe.
|
|
366
|
+
"""
|
|
367
|
+
return self.bind('<<PageChange>>', callback, add="+")
|
|
368
|
+
|
|
369
|
+
def off_page_changed(self, bind_id: str | None = None) -> None:
|
|
370
|
+
"""Unsubscribe from `<<PageChange>>`.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
bind_id: ID returned by `on_page_changed()`. If None, removes all.
|
|
374
|
+
"""
|
|
375
|
+
self.unbind("<<PageChange>>", bind_id)
|
|
376
|
+
|
|
377
|
+
def on_page_mount(self, callback: Callable) -> str:
|
|
378
|
+
"""Register a callback for `<<PageMount>>` events (fires after a new page is displayed).
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
callback: Receives `event.data` with navigation info: `page`,
|
|
382
|
+
`prev_page`, `nav`, `index`, `length`, `can_back`, `can_forward`.
|
|
383
|
+
|
|
384
|
+
Returns:
|
|
385
|
+
Bind ID — pass to `off_page_mount()` to unsubscribe.
|
|
386
|
+
"""
|
|
387
|
+
return self.bind('<<PageMount>>', callback, add='+')
|
|
388
|
+
|
|
389
|
+
def off_page_mount(self, bind_id: str | None = None) -> None:
|
|
390
|
+
"""Unsubscribe from `<<PageMount>>`.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
bind_id: ID returned by `on_page_mount()`. If None, removes all.
|
|
394
|
+
"""
|
|
395
|
+
self.unbind('<<PageMount>>', bind_id)
|