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,1179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Any, Callable, Literal, Sequence, overload
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from bootstack.images import AppIcon, Image
|
|
7
|
+
from bootstack.widgets.listview import ListView
|
|
8
|
+
from bootstack.widgets.tree import Tree
|
|
9
|
+
|
|
10
|
+
from bootstack._runtime.app import LocalizeMode
|
|
11
|
+
from bootstack.widgets._impl.composites.shell.shell import Shell as _InternalShell
|
|
12
|
+
from bootstack.widgets._core.app_config import AppConfigMixin, APP_CONFIG_KWARGS
|
|
13
|
+
from bootstack.widgets._core.base import PublicWidgetBase, _RawTkContainer
|
|
14
|
+
from bootstack.widgets._core.events import register_widget_events
|
|
15
|
+
from bootstack.widgets._impl.primitives.flexframe import FlexFrame
|
|
16
|
+
from bootstack.widgets._impl.primitives.gridframe import GridFrame
|
|
17
|
+
from bootstack.widgets._core.container import (
|
|
18
|
+
GRID_KEYS, grid_sticky, place_flex_child, _reject_legacy_child_kwargs,
|
|
19
|
+
_expand_margin,
|
|
20
|
+
)
|
|
21
|
+
from bootstack.widgets._core.context import push_container, pop_container
|
|
22
|
+
from bootstack.widgets._core.window_controls import WindowControlsMixin
|
|
23
|
+
from bootstack.widgets._core.window_menu import ChromeHostMixin
|
|
24
|
+
from bootstack.events import (
|
|
25
|
+
DisplayModeEvent,
|
|
26
|
+
PageChangeEvent,
|
|
27
|
+
PaneToggleEvent,
|
|
28
|
+
Subscription,
|
|
29
|
+
WorkspaceChangeEvent,
|
|
30
|
+
)
|
|
31
|
+
from bootstack.streams import Stream
|
|
32
|
+
from bootstack.widgets.statusbar import StatusBar
|
|
33
|
+
from bootstack.widgets.types import (
|
|
34
|
+
AccentToken, AutoFlow, LayoutKind, Padding, SurfaceToken, WidgetDensity, WindowStyle,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
SidebarMode = Literal["expanded", "compact", "hidden"]
|
|
38
|
+
NavVariant = Literal["ghost", "solid"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Page:
|
|
42
|
+
"""Context-manager proxy for a content page or custom sidebar container.
|
|
43
|
+
|
|
44
|
+
A page IS a layout container: it accepts the same layout kwargs as
|
|
45
|
+
`PageStack.add` (`layout` / `padding` / `gap` / `horizontal_items` /
|
|
46
|
+
`vertical_items` / `grow_items` / `columns` / `rows` / `auto_flow`), so
|
|
47
|
+
children placed inside it need no extra `Column` / `Grid` wrapper. Widgets
|
|
48
|
+
created inside `with page:` parent here automatically; `scrollable=True` wraps
|
|
49
|
+
the content in a vertical scroll area.
|
|
50
|
+
|
|
51
|
+
The column default stretches children across the width (page content fills the
|
|
52
|
+
content area), unlike a standalone `Column`.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def __init__(
|
|
56
|
+
self,
|
|
57
|
+
tk_frame: Any,
|
|
58
|
+
*,
|
|
59
|
+
scrollable: bool = False,
|
|
60
|
+
layout: LayoutKind = "column",
|
|
61
|
+
padding: Padding | None = None,
|
|
62
|
+
gap: int = 0,
|
|
63
|
+
horizontal_items: str | None = None,
|
|
64
|
+
vertical_items: str | None = None,
|
|
65
|
+
grow_items: bool = False,
|
|
66
|
+
columns: int | list[int | str] | None = None,
|
|
67
|
+
rows: int | list[int | str] | None = None,
|
|
68
|
+
auto_flow: AutoFlow = "row",
|
|
69
|
+
) -> None:
|
|
70
|
+
self._internal = tk_frame
|
|
71
|
+
self._scroll: Any = None
|
|
72
|
+
host = tk_frame
|
|
73
|
+
if scrollable:
|
|
74
|
+
from bootstack.widgets.scrollview import ScrollView
|
|
75
|
+
|
|
76
|
+
self._scroll = ScrollView(
|
|
77
|
+
parent=_RawTkContainer(tk_frame), scroll_direction="vertical"
|
|
78
|
+
)
|
|
79
|
+
host = self._scroll._child_master()
|
|
80
|
+
|
|
81
|
+
# Page content fills the content area, so a column stretches its children
|
|
82
|
+
# across the width by default (a standalone Column centers them).
|
|
83
|
+
if horizontal_items is None:
|
|
84
|
+
horizontal_items = "stretch" if layout in ("grid", "column") else "left"
|
|
85
|
+
if vertical_items is None:
|
|
86
|
+
vertical_items = "stretch" if layout == "grid" else "center" if layout == "row" else "top"
|
|
87
|
+
self._layout = layout
|
|
88
|
+
self._horizontal_items = horizontal_items
|
|
89
|
+
self._vertical_items = vertical_items
|
|
90
|
+
|
|
91
|
+
if layout in ("column", "row"):
|
|
92
|
+
self._layout_frame: Any = FlexFrame(
|
|
93
|
+
host, direction="vertical" if layout == "column" else "horizontal",
|
|
94
|
+
padding=padding, gap=gap, horizontal_items=horizontal_items,
|
|
95
|
+
vertical_items=vertical_items, grow_items=grow_items,
|
|
96
|
+
)
|
|
97
|
+
elif layout == "grid":
|
|
98
|
+
self._layout_frame = GridFrame(
|
|
99
|
+
host, columns=columns, rows=rows, padding=padding, gap=gap, auto_flow=auto_flow,
|
|
100
|
+
)
|
|
101
|
+
else:
|
|
102
|
+
raise ValueError(f"page layout must be 'column', 'row', or 'grid', got {layout!r}")
|
|
103
|
+
self._layout_frame.pack(fill="both", expand=True)
|
|
104
|
+
|
|
105
|
+
def _child_master(self) -> Any:
|
|
106
|
+
return self._layout_frame
|
|
107
|
+
|
|
108
|
+
def guide_layout(self, child: Any, **layout_kw: Any) -> None:
|
|
109
|
+
if self._layout == "grid":
|
|
110
|
+
_reject_legacy_child_kwargs(layout_kw, "AppShell page")
|
|
111
|
+
_expand_margin(layout_kw)
|
|
112
|
+
options = {k: v for k, v in layout_kw.items() if k in GRID_KEYS}
|
|
113
|
+
h = layout_kw.get("horizontal") or self._horizontal_items
|
|
114
|
+
v = layout_kw.get("vertical") or self._vertical_items
|
|
115
|
+
options["sticky"] = grid_sticky(h, v)
|
|
116
|
+
child._internal.grid(in_=self._child_master(), **options)
|
|
117
|
+
return
|
|
118
|
+
place_flex_child(self._layout_frame, child, layout_kw, "AppShell page")
|
|
119
|
+
|
|
120
|
+
def __enter__(self) -> "Page":
|
|
121
|
+
push_container(self)
|
|
122
|
+
return self
|
|
123
|
+
|
|
124
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
125
|
+
pop_container(self)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PageNav:
|
|
129
|
+
"""Authorable page-list sidebar — the handle returned by `page_nav()`.
|
|
130
|
+
|
|
131
|
+
A page nav is a flat list of authored nav items, each paired with a content
|
|
132
|
+
page. Author it with `add_page()` (and `add_header()` / `add_divider()` to
|
|
133
|
+
chunk the list, `add_footer_page()` to pin an item to the bottom). Use it as a
|
|
134
|
+
context manager for grouping; the items themselves are added via the handle,
|
|
135
|
+
not parented into it.
|
|
136
|
+
"""
|
|
137
|
+
|
|
138
|
+
def __init__(self, internal_host: Any) -> None:
|
|
139
|
+
# The internal Shell (single-tier, delegates to its default workspace) or
|
|
140
|
+
# internal Workspace — both expose add_page/add_header/add_divider.
|
|
141
|
+
self._host = internal_host
|
|
142
|
+
|
|
143
|
+
def __enter__(self) -> "PageNav":
|
|
144
|
+
return self
|
|
145
|
+
|
|
146
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
147
|
+
upd = getattr(self._host, "update_idletasks", None)
|
|
148
|
+
if upd is not None:
|
|
149
|
+
try:
|
|
150
|
+
upd()
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
def add_page(
|
|
155
|
+
self,
|
|
156
|
+
key: str,
|
|
157
|
+
*,
|
|
158
|
+
text: str = "",
|
|
159
|
+
icon: str | dict | None = None,
|
|
160
|
+
scrollable: bool = False,
|
|
161
|
+
pin_to_footer: bool = False,
|
|
162
|
+
layout: LayoutKind = "column",
|
|
163
|
+
padding: Padding | None = None,
|
|
164
|
+
gap: int = 0,
|
|
165
|
+
horizontal_items: str | None = None,
|
|
166
|
+
vertical_items: str | None = None,
|
|
167
|
+
grow_items: bool = False,
|
|
168
|
+
columns: int | list[int | str] | None = None,
|
|
169
|
+
rows: int | list[int | str] | None = None,
|
|
170
|
+
auto_flow: AutoFlow = "row",
|
|
171
|
+
) -> Page:
|
|
172
|
+
"""Add a nav item and its content page; return a context-manager page.
|
|
173
|
+
|
|
174
|
+
The page IS a layout container — the `layout` / `padding` / `gap` /
|
|
175
|
+
`horizontal_items` / `vertical_items` / `grow_items` / `columns` / `rows` /
|
|
176
|
+
`auto_flow` kwargs configure it directly (same as `PageStack.add`), so
|
|
177
|
+
children inside the `with` block need no extra `Column` / `Grid` wrapper.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
key: Unique identifier for the nav item and page.
|
|
181
|
+
text: Display label in the sidebar.
|
|
182
|
+
icon: Icon name or icon configuration dict.
|
|
183
|
+
scrollable: If `True`, wrap the page in a vertical `ScrollView`.
|
|
184
|
+
pin_to_footer: If `True`, pin the nav item to the bottom of the
|
|
185
|
+
sidebar (the non-scrolling footer zone) instead of the scrolling
|
|
186
|
+
main list — handy for a Settings or Account entry.
|
|
187
|
+
layout: Page layout — `'column'` (default), `'row'`, or `'grid'`.
|
|
188
|
+
padding: Space inside the page around its content.
|
|
189
|
+
gap: Space between the page's children in pixels.
|
|
190
|
+
horizontal_items: Cross/main-axis alignment of children on the x-axis.
|
|
191
|
+
vertical_items: Cross/main-axis alignment of children on the y-axis.
|
|
192
|
+
grow_items: For `'column'`/`'row'`, every child grows to share the axis.
|
|
193
|
+
columns: Column definitions for `'grid'` layout.
|
|
194
|
+
rows: Row definitions for `'grid'` layout.
|
|
195
|
+
auto_flow: Grid auto-flow direction for `'grid'` layout.
|
|
196
|
+
"""
|
|
197
|
+
frame = self._host.add_page(key, text=text, icon=icon, footer=pin_to_footer)
|
|
198
|
+
return Page(
|
|
199
|
+
frame, scrollable=scrollable, layout=layout, padding=padding, gap=gap,
|
|
200
|
+
horizontal_items=horizontal_items, vertical_items=vertical_items,
|
|
201
|
+
grow_items=grow_items, columns=columns, rows=rows, auto_flow=auto_flow,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
def add_header(self, text: str) -> None:
|
|
205
|
+
"""Add a plain section-label header that chunks the list (grouped-static)."""
|
|
206
|
+
self._host.add_header(text)
|
|
207
|
+
|
|
208
|
+
def add_divider(self) -> None:
|
|
209
|
+
"""Add a divider to the list."""
|
|
210
|
+
self._host.add_divider()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class _SidebarHost:
|
|
214
|
+
"""Mixin for anything that owns one sidebar: `AppShell` and `Workspace`.
|
|
215
|
+
|
|
216
|
+
Exposes the four parallel provider front doors — `page_nav()` (authored
|
|
217
|
+
pages), `list_nav()` / `tree_nav()` (data-bound master-detail), and
|
|
218
|
+
`custom_nav()` (hand-built) — plus `detail()` and the `content` escape hatch.
|
|
219
|
+
A sidebar is filled by exactly one provider; the methods are mutually
|
|
220
|
+
exclusive at runtime.
|
|
221
|
+
|
|
222
|
+
Subclasses provide `_sidebar_internal()` (the internal Shell or Workspace that
|
|
223
|
+
carries the provider methods) and `_content_frame()` (the content region).
|
|
224
|
+
"""
|
|
225
|
+
|
|
226
|
+
def _sidebar_internal(self) -> Any: # pragma: no cover - overridden
|
|
227
|
+
raise NotImplementedError
|
|
228
|
+
|
|
229
|
+
def _content_frame(self) -> Any: # pragma: no cover - overridden
|
|
230
|
+
raise NotImplementedError
|
|
231
|
+
|
|
232
|
+
def page_nav(self) -> PageNav:
|
|
233
|
+
"""Declare the sidebar as an authored page list; return its handle.
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
A `PageNav` handle — author items with `add_page()` / `add_header()` /
|
|
237
|
+
`add_divider()` / `add_footer_page()`.
|
|
238
|
+
"""
|
|
239
|
+
internal = self._sidebar_internal()
|
|
240
|
+
internal.page_nav()
|
|
241
|
+
return PageNav(internal)
|
|
242
|
+
|
|
243
|
+
def list_nav(
|
|
244
|
+
self,
|
|
245
|
+
source: Any,
|
|
246
|
+
*,
|
|
247
|
+
separator: bool = False,
|
|
248
|
+
density: WidgetDensity = "default",
|
|
249
|
+
placeholder: str = "Select an item to view",
|
|
250
|
+
chevron: bool = False,
|
|
251
|
+
) -> "ListView":
|
|
252
|
+
"""Declare a data-bound list sidebar from a `DataSource` (flat master-detail).
|
|
253
|
+
|
|
254
|
+
Records render their `icon`/`title`/`text` in a recycling list. Pair it
|
|
255
|
+
with `@detail` to render the body for the selected record. Returns the
|
|
256
|
+
`ListView` driving the sidebar — use it to read `.selection` or drive
|
|
257
|
+
selection (`select_items`).
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
source: A `DataSource` (or `rows=`-compatible record list).
|
|
261
|
+
separator: Draw divider lines between rows. Default `False` (flush).
|
|
262
|
+
density: `'default'` (richer two-line rows) or `'compact'` (one-line).
|
|
263
|
+
placeholder: Empty-state message shown when the source has no records.
|
|
264
|
+
chevron: Add a per-row disclosure chevron. Default `False`.
|
|
265
|
+
"""
|
|
266
|
+
return self._sidebar_internal().list_nav(
|
|
267
|
+
source, separator=separator, density=density,
|
|
268
|
+
placeholder=placeholder, chevron=chevron,
|
|
269
|
+
).nav
|
|
270
|
+
|
|
271
|
+
def tree_nav(
|
|
272
|
+
self,
|
|
273
|
+
*,
|
|
274
|
+
nodes: list | None = None,
|
|
275
|
+
source: Any = None,
|
|
276
|
+
parent_field: str = "parent_id",
|
|
277
|
+
label_field: str = "name",
|
|
278
|
+
icon_field: str = "icon",
|
|
279
|
+
density: WidgetDensity = "default",
|
|
280
|
+
placeholder: str = "Select an item to view",
|
|
281
|
+
) -> "Tree":
|
|
282
|
+
"""Declare a data-bound tree sidebar (hierarchical master-detail).
|
|
283
|
+
|
|
284
|
+
Declare the hierarchy inline with `nodes=` or project a flat adjacency-list
|
|
285
|
+
`source=` (each row names its parent via `parent_field`). Pair it with
|
|
286
|
+
`@detail` to render the body for the selected node. Returns the `Tree`
|
|
287
|
+
driving the sidebar — use it to drive the view
|
|
288
|
+
(`expand`/`expand_all`/`collapse`/`select`/`find`).
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
nodes: Inline hierarchy as a node list, or `None` to use `source`.
|
|
292
|
+
source: A flat adjacency-list `DataSource`, or `None` to use `nodes`.
|
|
293
|
+
parent_field: Field naming each row's parent (for `source`).
|
|
294
|
+
label_field: Field providing each node's label.
|
|
295
|
+
icon_field: Field providing each node's icon name.
|
|
296
|
+
density: `'default'` or `'compact'`.
|
|
297
|
+
placeholder: Empty-state message shown until a node is picked.
|
|
298
|
+
"""
|
|
299
|
+
return self._sidebar_internal().tree_nav(
|
|
300
|
+
nodes=nodes, source=source, parent_field=parent_field,
|
|
301
|
+
label_field=label_field, icon_field=icon_field,
|
|
302
|
+
density=density, placeholder=placeholder,
|
|
303
|
+
).tree
|
|
304
|
+
|
|
305
|
+
def custom_nav(self) -> Page:
|
|
306
|
+
"""Declare a hand-built sidebar; return its container.
|
|
307
|
+
|
|
308
|
+
The escape hatch when none of the providers fit — fill the returned
|
|
309
|
+
container with your own sidebar UI (e.g. a `bs.Accordion`) and drive the
|
|
310
|
+
content region yourself via `content`.
|
|
311
|
+
"""
|
|
312
|
+
return Page(self._sidebar_internal().panel())
|
|
313
|
+
|
|
314
|
+
def detail(self, fn: Callable[[dict], Any]) -> Callable[[dict], Any]:
|
|
315
|
+
"""Register the detail-body renderer for a data-bound sidebar (decorator).
|
|
316
|
+
|
|
317
|
+
Pairs with `list_nav` / `tree_nav` to form a master-detail view: when the
|
|
318
|
+
selection changes, `fn` runs with the selected record and rebuilds the
|
|
319
|
+
content area. The first row is selected on load, so the detail is never
|
|
320
|
+
empty on open.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
fn: Builder called with the selected record `dict` (for `tree_nav`,
|
|
324
|
+
the node's record `dict`). Its return value is ignored.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
`fn` unchanged, so it works as a decorator.
|
|
328
|
+
"""
|
|
329
|
+
return self._sidebar_internal().detail(fn)
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def content(self) -> Page:
|
|
333
|
+
"""The content region as a container for hand-driven content.
|
|
334
|
+
|
|
335
|
+
Use with `with host.content:` or `parent=host.content` to place widgets in
|
|
336
|
+
the content area directly — the escape hatch for `custom_nav` mode.
|
|
337
|
+
"""
|
|
338
|
+
frame = self._content_frame()
|
|
339
|
+
host = getattr(self, "_content_host", None)
|
|
340
|
+
if host is None or host._internal is not frame:
|
|
341
|
+
host = self._content_host = Page(frame)
|
|
342
|
+
return host
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
class Workspace(_SidebarHost):
|
|
346
|
+
"""One workspace in a `Workbench` (returned by `add_workspace()`).
|
|
347
|
+
|
|
348
|
+
A sidebar host (see `_SidebarHost`) plus its own `navigate()`: declare its
|
|
349
|
+
sidebar with `page_nav()` / `list_nav()` / `tree_nav()` / `custom_nav()`,
|
|
350
|
+
exactly as you would a single-tier `AppShell`.
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
def __init__(self, internal_ws: Any, internal_shell: Any) -> None:
|
|
354
|
+
self._internal = internal_ws
|
|
355
|
+
self._shell_internal = internal_shell
|
|
356
|
+
|
|
357
|
+
@property
|
|
358
|
+
def key(self) -> str:
|
|
359
|
+
"""The workspace identifier."""
|
|
360
|
+
return self._internal.key
|
|
361
|
+
|
|
362
|
+
def __enter__(self) -> "Workspace":
|
|
363
|
+
return self
|
|
364
|
+
|
|
365
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
366
|
+
try:
|
|
367
|
+
self._shell_internal.update_idletasks()
|
|
368
|
+
except Exception:
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
def _sidebar_internal(self) -> Any:
|
|
372
|
+
return self._internal
|
|
373
|
+
|
|
374
|
+
def _content_frame(self) -> Any:
|
|
375
|
+
return self._internal.content
|
|
376
|
+
|
|
377
|
+
def navigate(self, page: str, *, data: dict | None = None) -> None:
|
|
378
|
+
"""Navigate to a page in this workspace."""
|
|
379
|
+
self._shell_internal.navigate(self.key, page, data=data)
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def current(self) -> str | None:
|
|
383
|
+
"""Key of this workspace's active page, or `None`."""
|
|
384
|
+
return self._shell_internal.model.active_page(self.key)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
class Rail:
|
|
388
|
+
"""The workspace rail (returned by `workbench.rail`).
|
|
389
|
+
|
|
390
|
+
The rail is mostly framework-driven; its public surface switches workspaces
|
|
391
|
+
and observes changes. Methods are no-ops when the rail is not rendered (a
|
|
392
|
+
single-workspace workbench).
|
|
393
|
+
"""
|
|
394
|
+
|
|
395
|
+
def __init__(self, shell: "Workbench") -> None:
|
|
396
|
+
self._shell = shell
|
|
397
|
+
|
|
398
|
+
def select(self, key: str) -> None:
|
|
399
|
+
"""Switch to the workspace `key` and show its sidebar."""
|
|
400
|
+
model = self._shell._internal.model
|
|
401
|
+
model.select_workspace(key)
|
|
402
|
+
model.show_sidebar()
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def current(self) -> str | None:
|
|
406
|
+
"""Key of the active workspace, or `None`."""
|
|
407
|
+
return self._shell._internal.current_workspace
|
|
408
|
+
|
|
409
|
+
@overload
|
|
410
|
+
def on_change(self) -> Stream: ...
|
|
411
|
+
@overload
|
|
412
|
+
def on_change(self, handler: Callable[[WorkspaceChangeEvent], Any]) -> Subscription: ...
|
|
413
|
+
def on_change(
|
|
414
|
+
self, handler: Callable[[WorkspaceChangeEvent], Any] | None = None
|
|
415
|
+
) -> Stream | Subscription:
|
|
416
|
+
"""Register a callback fired when the active workspace changes.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
handler: Called with a
|
|
420
|
+
:class:`~bootstack.events.WorkspaceChangeEvent`. Omit to get a
|
|
421
|
+
composable :class:`~bootstack.streams.Stream`.
|
|
422
|
+
"""
|
|
423
|
+
return self._shell.on_workspace_change(handler)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
class _ShellBase(AppConfigMixin, WindowControlsMixin, ChromeHostMixin, PublicWidgetBase):
|
|
427
|
+
"""Shared window/chrome/lifecycle scaffolding for `AppShell` and `Workbench`.
|
|
428
|
+
|
|
429
|
+
Private base — NOT a public type. Holds the parts both shells share: the
|
|
430
|
+
internal-`Shell` construction, configuration (`shell.theme`, etc.), toolbar
|
|
431
|
+
chrome, the status band, sidebar visibility, and the lifecycle. Navigation
|
|
432
|
+
(sidebar hosting vs the workspace rail) lives on the concrete subclasses.
|
|
433
|
+
"""
|
|
434
|
+
|
|
435
|
+
_dev_supports_inprocess = True
|
|
436
|
+
|
|
437
|
+
def _init_shell(
|
|
438
|
+
self, init_kwargs: dict[str, Any], *, icon: Any, show_statusbar: bool
|
|
439
|
+
) -> None:
|
|
440
|
+
self._statusbar: StatusBar | None = None
|
|
441
|
+
self._dev_body = None # set in __enter__ under `bootstack dev`
|
|
442
|
+
|
|
443
|
+
# Under `bootstack dev`, persist window geometry by default so the
|
|
444
|
+
# process-restart fallback re-opens in place (in-process reload preserves
|
|
445
|
+
# everything already). Dev-scoped state file; honors an explicit choice.
|
|
446
|
+
from bootstack.dev._env import DEV_MIN_SIZE, is_dev_mode
|
|
447
|
+
|
|
448
|
+
if is_dev_mode():
|
|
449
|
+
if not init_kwargs.get("remember_window_state"):
|
|
450
|
+
from bootstack._core.paths import app_config_file
|
|
451
|
+
|
|
452
|
+
init_kwargs["remember_window_state"] = True
|
|
453
|
+
if not init_kwargs.get("state_path"):
|
|
454
|
+
init_kwargs["state_path"] = str(
|
|
455
|
+
app_config_file("dev_window_state.json", init_kwargs.get("title") or "bootstack")
|
|
456
|
+
)
|
|
457
|
+
# A usable floor so a reload window starts/stays a sensible size.
|
|
458
|
+
init_kwargs.setdefault("minsize", DEV_MIN_SIZE)
|
|
459
|
+
|
|
460
|
+
self._internal = _InternalShell(**init_kwargs)
|
|
461
|
+
|
|
462
|
+
# Resolve the window icon AFTER the root exists (an AppIcon may resolve
|
|
463
|
+
# theme tokens; a deferred Image renders against the root).
|
|
464
|
+
self._app_icon_photo = None
|
|
465
|
+
if icon is not None:
|
|
466
|
+
from bootstack.widgets._core.image_binding import resolve_window_icon
|
|
467
|
+
|
|
468
|
+
icon_path, icon_image = resolve_window_icon(icon)
|
|
469
|
+
if icon_path is not None:
|
|
470
|
+
self._internal._setup_icon(icon_path)
|
|
471
|
+
elif icon_image is not None:
|
|
472
|
+
self._app_icon_photo = icon_image._materialize()
|
|
473
|
+
self._internal._setup_icon(self._app_icon_photo)
|
|
474
|
+
|
|
475
|
+
# Remember whether the band was forced on, so a dev reload can restore it.
|
|
476
|
+
self._show_statusbar_forced = bool(show_statusbar)
|
|
477
|
+
if show_statusbar:
|
|
478
|
+
# Materialize the band and force it visible now so it is present from
|
|
479
|
+
# the first frame (a content-driven band shows lazily on its first
|
|
480
|
+
# segment; `show_statusbar=True` means "always on").
|
|
481
|
+
_ = self.statusbar
|
|
482
|
+
self._internal.set_statusbar_visible(True)
|
|
483
|
+
|
|
484
|
+
@classmethod
|
|
485
|
+
def from_store(cls, store: Any, **overrides: Any):
|
|
486
|
+
"""Construct from a persisted `Store` (or plain dict).
|
|
487
|
+
|
|
488
|
+
Reads configuration from `store`, tolerantly ignoring keys that are not
|
|
489
|
+
valid configuration (so version skew does not raise). Explicit keyword
|
|
490
|
+
`overrides` win over stored values. See `App.from_store`.
|
|
491
|
+
"""
|
|
492
|
+
data = store.as_dict() if hasattr(store, "as_dict") else dict(store)
|
|
493
|
+
kwargs = {k: v for k, v in data.items() if k in APP_CONFIG_KWARGS}
|
|
494
|
+
kwargs.update(overrides)
|
|
495
|
+
return cls(**kwargs)
|
|
496
|
+
|
|
497
|
+
def _config_app(self) -> Any:
|
|
498
|
+
return self._internal
|
|
499
|
+
|
|
500
|
+
def __enter__(self):
|
|
501
|
+
from bootstack.dev._env import is_dev_mode
|
|
502
|
+
|
|
503
|
+
if is_dev_mode() and getattr(self, "_dev_body", None) is None:
|
|
504
|
+
import sys as _sys
|
|
505
|
+
from bootstack.dev._capture import capture_from_frame
|
|
506
|
+
|
|
507
|
+
try:
|
|
508
|
+
self._dev_body = capture_from_frame(_sys._getframe(1))
|
|
509
|
+
except Exception:
|
|
510
|
+
self._dev_body = None
|
|
511
|
+
return self
|
|
512
|
+
|
|
513
|
+
def __exit__(self, exc_type: Any, exc: Any, tb: Any) -> None:
|
|
514
|
+
try:
|
|
515
|
+
self._internal.update_idletasks()
|
|
516
|
+
except Exception:
|
|
517
|
+
pass
|
|
518
|
+
|
|
519
|
+
# ----- dev hot-reload hooks (used only under `bootstack dev`) -------------
|
|
520
|
+
|
|
521
|
+
def _dev_reset_region(self) -> None:
|
|
522
|
+
"""Tear down the chrome + authored navigation, keeping the shell layout."""
|
|
523
|
+
# Clear chrome toolbars (the stacked-toolbar band is pre-built and stays).
|
|
524
|
+
try:
|
|
525
|
+
for child in list(self._internal.toolbar_stack.winfo_children()):
|
|
526
|
+
child.destroy()
|
|
527
|
+
except Exception:
|
|
528
|
+
pass
|
|
529
|
+
self._chrome_toolbars = []
|
|
530
|
+
self._native_menu_renderer = None
|
|
531
|
+
self._cancel_native_menu_pending()
|
|
532
|
+
try:
|
|
533
|
+
self._internal["menu"] = ""
|
|
534
|
+
except Exception:
|
|
535
|
+
pass
|
|
536
|
+
# Clear any status-band segments so a rebuild doesn't duplicate them.
|
|
537
|
+
try:
|
|
538
|
+
for child in list(self._internal.statusbar.winfo_children()):
|
|
539
|
+
child.destroy()
|
|
540
|
+
except Exception:
|
|
541
|
+
pass
|
|
542
|
+
self._statusbar = None
|
|
543
|
+
# Reset the band to its forced state: hidden unless `show_statusbar=True`
|
|
544
|
+
# was passed (an empty band shouldn't linger if the new body adds no
|
|
545
|
+
# segment; a content-driven band re-shows lazily on the first segment).
|
|
546
|
+
try:
|
|
547
|
+
self._internal.set_statusbar_visible(
|
|
548
|
+
getattr(self, "_show_statusbar_forced", False)
|
|
549
|
+
)
|
|
550
|
+
except Exception:
|
|
551
|
+
pass
|
|
552
|
+
# Tear down + rebuild the navigation substructure.
|
|
553
|
+
self._internal._dev_reset()
|
|
554
|
+
try:
|
|
555
|
+
from bootstack.dev import _registry
|
|
556
|
+
|
|
557
|
+
_registry.reset_mounts()
|
|
558
|
+
except Exception:
|
|
559
|
+
pass
|
|
560
|
+
|
|
561
|
+
def _dev_after_rebuild(self) -> None:
|
|
562
|
+
"""Re-apply window chrome that lives outside the body, then flush + grow."""
|
|
563
|
+
self._ensure_default_titlebar()
|
|
564
|
+
try:
|
|
565
|
+
root = self._internal
|
|
566
|
+
root.update_idletasks()
|
|
567
|
+
cur_w, cur_h = root.winfo_width(), root.winfo_height()
|
|
568
|
+
new_w = max(cur_w, root.winfo_reqwidth())
|
|
569
|
+
new_h = max(cur_h, root.winfo_reqheight())
|
|
570
|
+
if new_w > cur_w or new_h > cur_h:
|
|
571
|
+
root.geometry(f"{new_w}x{new_h}+{root.winfo_x()}+{root.winfo_y()}")
|
|
572
|
+
except Exception:
|
|
573
|
+
pass
|
|
574
|
+
|
|
575
|
+
def _dev_capture_route(self) -> Any:
|
|
576
|
+
"""Capture the active workspace + per-workspace page, to restore post-reload."""
|
|
577
|
+
model = self._internal._model
|
|
578
|
+
pages = {
|
|
579
|
+
key: model.active_page(key)
|
|
580
|
+
for key in self._internal._workspaces
|
|
581
|
+
if model.active_page(key) is not None
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
"workspace": model.active_workspace,
|
|
585
|
+
"pages": pages,
|
|
586
|
+
"sidebar_mode": model.sidebar_mode,
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
def _dev_restore_route(self, route: Any) -> None:
|
|
590
|
+
"""Restore the page/workspace selected before the reload, if still present."""
|
|
591
|
+
if not isinstance(route, dict):
|
|
592
|
+
return
|
|
593
|
+
model = self._internal._model
|
|
594
|
+
for ws_key, page in route.get("pages", {}).items():
|
|
595
|
+
ws = self._internal._workspaces.get(ws_key)
|
|
596
|
+
if ws is not None and page in ws.keys():
|
|
597
|
+
model.select_page(page, workspace=ws_key)
|
|
598
|
+
active_ws = route.get("workspace")
|
|
599
|
+
if isinstance(active_ws, str) and model.has_workspace(active_ws):
|
|
600
|
+
model.select_workspace(active_ws)
|
|
601
|
+
mode = route.get("sidebar_mode")
|
|
602
|
+
if mode in ("hidden", "compact", "expanded"):
|
|
603
|
+
model.set_sidebar_mode(mode)
|
|
604
|
+
# Force the mode's *visual* (compact = icon-only + narrow) onto the
|
|
605
|
+
# freshly-rebuilt nav. set_sidebar_mode above is a no-op when the new
|
|
606
|
+
# model already holds this mode, so the view treatment (normally
|
|
607
|
+
# applied once in mainloop) must be re-applied explicitly here.
|
|
608
|
+
self._internal._apply_sidebar_mode(mode)
|
|
609
|
+
|
|
610
|
+
# ----- Chrome hooks (mount the toolbar stack into the Shell's region) -----
|
|
611
|
+
|
|
612
|
+
def _menu_root(self) -> Any:
|
|
613
|
+
# The Shell is itself a Tk root (subclasses the internal App), so the
|
|
614
|
+
# native menubar and shortcut bindings attach to it.
|
|
615
|
+
return self._internal
|
|
616
|
+
|
|
617
|
+
def _ensure_toolbar_stack(self) -> Any:
|
|
618
|
+
# The shell pre-builds its stacked-toolbar region as a first-class band
|
|
619
|
+
# (above the body); `add_toolbar()` mounts into it rather than the base
|
|
620
|
+
# mixin's content-frame stack.
|
|
621
|
+
return self._internal.toolbar_stack
|
|
622
|
+
|
|
623
|
+
# ----- Sidebar visibility / compaction -----
|
|
624
|
+
|
|
625
|
+
def toggle_sidebar(self) -> None:
|
|
626
|
+
"""Toggle the sidebar between hidden and shown (the hamburger action)."""
|
|
627
|
+
self._internal.toggle_sidebar()
|
|
628
|
+
|
|
629
|
+
def show_sidebar(self) -> None:
|
|
630
|
+
"""Show the sidebar, restoring its last non-hidden mode."""
|
|
631
|
+
self._internal.show_sidebar()
|
|
632
|
+
|
|
633
|
+
def hide_sidebar(self) -> None:
|
|
634
|
+
"""Hide the sidebar entirely."""
|
|
635
|
+
self._internal.hide_sidebar()
|
|
636
|
+
|
|
637
|
+
@property
|
|
638
|
+
def sidebar_mode(self) -> SidebarMode:
|
|
639
|
+
"""The sidebar mode (`'hidden'`/`'compact'`/`'expanded'`)."""
|
|
640
|
+
return self._internal.sidebar_mode
|
|
641
|
+
|
|
642
|
+
@sidebar_mode.setter
|
|
643
|
+
def sidebar_mode(self, mode: SidebarMode) -> None:
|
|
644
|
+
self._internal.sidebar_mode = mode
|
|
645
|
+
|
|
646
|
+
# ----- Events shared by both shells -----
|
|
647
|
+
|
|
648
|
+
@overload
|
|
649
|
+
def on_page_change(self) -> Stream: ...
|
|
650
|
+
@overload
|
|
651
|
+
def on_page_change(self, handler: Callable[[PageChangeEvent], Any]) -> Subscription: ...
|
|
652
|
+
def on_page_change(
|
|
653
|
+
self, handler: Callable[[PageChangeEvent], Any] | None = None
|
|
654
|
+
) -> Stream | Subscription:
|
|
655
|
+
"""Register a callback fired when the active page changes.
|
|
656
|
+
|
|
657
|
+
Args:
|
|
658
|
+
handler: Called with a :class:`~bootstack.events.PageChangeEvent`
|
|
659
|
+
(the new and previous page keys, plus any `navigate()` data).
|
|
660
|
+
Omit to get a composable :class:`~bootstack.streams.Stream`.
|
|
661
|
+
"""
|
|
662
|
+
return self.on("page_change", handler)
|
|
663
|
+
|
|
664
|
+
@overload
|
|
665
|
+
def on_sidebar_toggle(self) -> Stream: ...
|
|
666
|
+
@overload
|
|
667
|
+
def on_sidebar_toggle(self, handler: Callable[[PaneToggleEvent], Any]) -> Subscription: ...
|
|
668
|
+
def on_sidebar_toggle(
|
|
669
|
+
self, handler: Callable[[PaneToggleEvent], Any] | None = None
|
|
670
|
+
) -> Stream | Subscription:
|
|
671
|
+
"""Register a callback fired when the sidebar is shown or hidden.
|
|
672
|
+
|
|
673
|
+
Args:
|
|
674
|
+
handler: Called with a :class:`~bootstack.events.PaneToggleEvent`.
|
|
675
|
+
Omit to get a composable :class:`~bootstack.streams.Stream`.
|
|
676
|
+
"""
|
|
677
|
+
return self.on("sidebar_toggle", handler)
|
|
678
|
+
|
|
679
|
+
@overload
|
|
680
|
+
def on_sidebar_mode_change(self) -> Stream: ...
|
|
681
|
+
@overload
|
|
682
|
+
def on_sidebar_mode_change(
|
|
683
|
+
self, handler: Callable[[DisplayModeEvent], Any]
|
|
684
|
+
) -> Subscription: ...
|
|
685
|
+
def on_sidebar_mode_change(
|
|
686
|
+
self, handler: Callable[[DisplayModeEvent], Any] | None = None
|
|
687
|
+
) -> Stream | Subscription:
|
|
688
|
+
"""Register a callback fired when the sidebar mode changes.
|
|
689
|
+
|
|
690
|
+
Args:
|
|
691
|
+
handler: Called with a :class:`~bootstack.events.DisplayModeEvent`
|
|
692
|
+
(`compact` ↔ `expanded`). Omit to get a composable
|
|
693
|
+
:class:`~bootstack.streams.Stream`.
|
|
694
|
+
"""
|
|
695
|
+
return self.on("sidebar_mode_change", handler)
|
|
696
|
+
|
|
697
|
+
# ----- Lifecycle -----
|
|
698
|
+
|
|
699
|
+
def run(self) -> None:
|
|
700
|
+
"""Show the window and start the event loop."""
|
|
701
|
+
self._ensure_default_titlebar()
|
|
702
|
+
from bootstack.dev._env import is_dev_mode
|
|
703
|
+
|
|
704
|
+
if is_dev_mode():
|
|
705
|
+
from bootstack.dev._reloader import install_reloader
|
|
706
|
+
|
|
707
|
+
install_reloader(self)
|
|
708
|
+
self._internal.deiconify()
|
|
709
|
+
self._internal.mainloop()
|
|
710
|
+
|
|
711
|
+
# ----- Region accessors -----
|
|
712
|
+
|
|
713
|
+
@property
|
|
714
|
+
def statusbar(self) -> StatusBar:
|
|
715
|
+
"""The bottom status band (passive status). Lazily created on first access.
|
|
716
|
+
|
|
717
|
+
The band renders once a segment is added (or `show_statusbar=True`).
|
|
718
|
+
"""
|
|
719
|
+
if self._statusbar is None:
|
|
720
|
+
self._statusbar = StatusBar(
|
|
721
|
+
_toolbar=self._internal.statusbar,
|
|
722
|
+
_show=lambda: self._internal.set_statusbar_visible(True),
|
|
723
|
+
)
|
|
724
|
+
return self._statusbar
|
|
725
|
+
|
|
726
|
+
@property
|
|
727
|
+
def pages(self) -> Any:
|
|
728
|
+
"""The active static provider's content page deck (or `None`)."""
|
|
729
|
+
return self._internal.pages
|
|
730
|
+
|
|
731
|
+
@property
|
|
732
|
+
def nav(self) -> Any:
|
|
733
|
+
"""The active provider's sidebar navigation panel (or `None`)."""
|
|
734
|
+
return self._internal.nav
|
|
735
|
+
|
|
736
|
+
@property
|
|
737
|
+
def tk(self) -> Any:
|
|
738
|
+
"""Underlying `tk.Tk` root window. UNSUPPORTED — escape-hatch use only."""
|
|
739
|
+
return self._internal
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
def _build_init_kwargs(
|
|
743
|
+
*,
|
|
744
|
+
title: str,
|
|
745
|
+
undecorated: bool,
|
|
746
|
+
light_theme: str,
|
|
747
|
+
dark_theme: str,
|
|
748
|
+
follow_system_appearance: bool,
|
|
749
|
+
available_themes: Sequence[str],
|
|
750
|
+
scaling: float | None,
|
|
751
|
+
hdpi: bool,
|
|
752
|
+
locale: str | None,
|
|
753
|
+
localize_mode: LocalizeMode,
|
|
754
|
+
window_style: WindowStyle | str | None,
|
|
755
|
+
macos_quit_behavior: str,
|
|
756
|
+
remember_window_state: bool,
|
|
757
|
+
state_path: str | None,
|
|
758
|
+
sidebar_mode: SidebarMode,
|
|
759
|
+
collapsible: bool,
|
|
760
|
+
nav_accent: AccentToken | str | None,
|
|
761
|
+
remember_nav_state: bool,
|
|
762
|
+
sidebar_surface: SurfaceToken | str,
|
|
763
|
+
statusbar_surface: SurfaceToken | str,
|
|
764
|
+
size: tuple[int, int] | None,
|
|
765
|
+
theme: str | None,
|
|
766
|
+
position: tuple[int, int] | None,
|
|
767
|
+
min_size: tuple[int, int] | None,
|
|
768
|
+
max_size: tuple[int, int] | None,
|
|
769
|
+
resizable: tuple[bool, bool] | None,
|
|
770
|
+
on_close: Callable[[], bool | None] | None,
|
|
771
|
+
sidebar_width: int | None,
|
|
772
|
+
) -> dict[str, Any]:
|
|
773
|
+
"""Assemble the shared internal-`Shell` kwargs (optionals omitted when None)."""
|
|
774
|
+
kw: dict[str, Any] = {
|
|
775
|
+
"title": title,
|
|
776
|
+
"undecorated": undecorated,
|
|
777
|
+
"light_theme": light_theme,
|
|
778
|
+
"dark_theme": dark_theme,
|
|
779
|
+
"follow_system_appearance": follow_system_appearance,
|
|
780
|
+
"available_themes": available_themes,
|
|
781
|
+
"scaling": scaling,
|
|
782
|
+
"hdpi": hdpi,
|
|
783
|
+
"locale": locale,
|
|
784
|
+
"localize_mode": localize_mode,
|
|
785
|
+
"window_style": window_style,
|
|
786
|
+
"macos_quit_behavior": macos_quit_behavior,
|
|
787
|
+
"remember_window_state": remember_window_state,
|
|
788
|
+
"state_path": state_path,
|
|
789
|
+
"sidebar_mode": sidebar_mode,
|
|
790
|
+
"collapsible": collapsible,
|
|
791
|
+
"nav_accent": nav_accent,
|
|
792
|
+
"remember_nav_state": remember_nav_state,
|
|
793
|
+
"sidebar_surface": sidebar_surface,
|
|
794
|
+
"statusbar_surface": statusbar_surface,
|
|
795
|
+
}
|
|
796
|
+
if size is not None:
|
|
797
|
+
kw["size"] = size
|
|
798
|
+
if theme is not None:
|
|
799
|
+
kw["theme"] = theme
|
|
800
|
+
if position is not None:
|
|
801
|
+
kw["position"] = position
|
|
802
|
+
if min_size is not None:
|
|
803
|
+
kw["minsize"] = min_size
|
|
804
|
+
if max_size is not None:
|
|
805
|
+
kw["maxsize"] = max_size
|
|
806
|
+
if resizable is not None:
|
|
807
|
+
kw["resizable"] = resizable
|
|
808
|
+
if on_close is not None:
|
|
809
|
+
kw["on_close"] = on_close
|
|
810
|
+
if sidebar_width is not None:
|
|
811
|
+
kw["sidebar_width"] = sidebar_width
|
|
812
|
+
return kw
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
class AppShell(_SidebarHost, _ShellBase):
|
|
816
|
+
"""Single-tier application window: one navigation sidebar plus content.
|
|
817
|
+
|
|
818
|
+
The everyday desktop scaffold — an optional toolbar stack across the top, one
|
|
819
|
+
navigation sidebar on the left, a content area that swaps as you navigate, and
|
|
820
|
+
an optional status band along the bottom. For a multi-section app with a
|
|
821
|
+
workspace rail, use :class:`Workbench` instead.
|
|
822
|
+
|
|
823
|
+
Declare the sidebar with exactly one provider front door:
|
|
824
|
+
`page_nav()` (authored pages), `list_nav()` / `tree_nav()` (data-bound
|
|
825
|
+
master-detail), or `custom_nav()` (hand-built). Pages support context-manager
|
|
826
|
+
syntax so widgets inside the `with` block are parented automatically.
|
|
827
|
+
|
|
828
|
+
Like `App`, configuration is a single flat path: pass options as constructor
|
|
829
|
+
kwargs and read or change them through matching `shell.*` properties (e.g.
|
|
830
|
+
`shell.theme`, `shell.locale`, `shell.sidebar_mode`).
|
|
831
|
+
|
|
832
|
+
Args:
|
|
833
|
+
title: Window title and (in undecorated mode) chrome label.
|
|
834
|
+
size: Initial window size as `(width, height)`.
|
|
835
|
+
theme: Theme name to apply on startup (e.g. `'bootstrap-dark'`).
|
|
836
|
+
icon: Title-bar and taskbar icon — an icon file path, an `Image` handle,
|
|
837
|
+
or an `AppIcon`. Defaults to the bootstack icon.
|
|
838
|
+
light_theme: Theme used for the light end of system-appearance tracking
|
|
839
|
+
and `toggle_theme`.
|
|
840
|
+
dark_theme: Theme used for the dark end of system-appearance tracking
|
|
841
|
+
and `toggle_theme`.
|
|
842
|
+
follow_system_appearance: If True, switch between `light_theme` and
|
|
843
|
+
`dark_theme` to match the OS (currently effective on macOS).
|
|
844
|
+
available_themes: Theme names to expose to theme pickers.
|
|
845
|
+
locale: Locale identifier (e.g. `'en_US'`, `'de_DE'`).
|
|
846
|
+
localize_mode: Localization behavior.
|
|
847
|
+
window_style: Windows-only window effect, or None to disable.
|
|
848
|
+
macos_quit_behavior: macOS close / Cmd+Q behavior. No-op on Win/Linux.
|
|
849
|
+
remember_window_state: If True, window geometry is saved and restored.
|
|
850
|
+
state_path: Optional override for the persisted window-state file.
|
|
851
|
+
on_close: Handler invoked when the user clicks the window's close button.
|
|
852
|
+
Return `False` to veto the close; `None` or `True` to allow it.
|
|
853
|
+
position: Initial window position as `(x, y)`.
|
|
854
|
+
min_size: Minimum window size as `(width, height)`.
|
|
855
|
+
max_size: Maximum window size as `(width, height)`.
|
|
856
|
+
resizable: Whether the window can be resized as `(x, y)`.
|
|
857
|
+
scaling: Explicit UI scaling factor. When None, scaling is automatic.
|
|
858
|
+
hdpi: Enable high-DPI awareness for the application. Default `True`.
|
|
859
|
+
sidebar_surface: Surface token for the navigation sidebar.
|
|
860
|
+
statusbar_surface: Surface token for the bottom status band.
|
|
861
|
+
undecorated: Remove OS window decorations and draw a custom border.
|
|
862
|
+
Ignored on macOS. The shell gets a built-in draggable title bar with
|
|
863
|
+
min/max/close; add your own with `add_toolbar(show_window_controls=True)`
|
|
864
|
+
to take over the chrome.
|
|
865
|
+
show_sidebar: Render the sidebar region. Default `True`.
|
|
866
|
+
sidebar_mode: Initial sidebar mode — `'expanded'`/`'compact'`/`'hidden'`.
|
|
867
|
+
`'compact'` (icon-only) applies to an authored `page_nav` sidebar.
|
|
868
|
+
sidebar_width: Expanded sidebar width in pixels.
|
|
869
|
+
collapsible: Allow collapsing the sidebar; binds Ctrl/Cmd-B. Default `True`.
|
|
870
|
+
nav_accent: Accent for the active nav item. Defaults to `'primary'`; set
|
|
871
|
+
another accent token to retint, or `None` for a neutral wash.
|
|
872
|
+
remember_nav_state: Persist the sidebar mode and active page across
|
|
873
|
+
sessions. Default `False`.
|
|
874
|
+
show_statusbar: Force the bottom status band on (otherwise it appears once
|
|
875
|
+
a status segment is added). Default `False`.
|
|
876
|
+
"""
|
|
877
|
+
|
|
878
|
+
def __init__(
|
|
879
|
+
self,
|
|
880
|
+
*,
|
|
881
|
+
title: str = "",
|
|
882
|
+
size: tuple[int, int] | None = None,
|
|
883
|
+
theme: str | None = None,
|
|
884
|
+
icon: "str | Image | AppIcon | None" = None,
|
|
885
|
+
light_theme: str = "bootstrap-light",
|
|
886
|
+
dark_theme: str = "bootstrap-dark",
|
|
887
|
+
follow_system_appearance: bool = False,
|
|
888
|
+
available_themes: Sequence[str] = (),
|
|
889
|
+
locale: str | None = None,
|
|
890
|
+
localize_mode: LocalizeMode = "auto",
|
|
891
|
+
window_style: WindowStyle | str | None = "mica",
|
|
892
|
+
macos_quit_behavior: Literal["native", "classic"] = "native",
|
|
893
|
+
remember_window_state: bool = False,
|
|
894
|
+
state_path: str | None = None,
|
|
895
|
+
on_close: Callable[[], bool | None] | None = None,
|
|
896
|
+
position: tuple[int, int] | None = None,
|
|
897
|
+
min_size: tuple[int, int] | None = None,
|
|
898
|
+
max_size: tuple[int, int] | None = None,
|
|
899
|
+
resizable: tuple[bool, bool] | None = None,
|
|
900
|
+
scaling: float | None = None,
|
|
901
|
+
hdpi: bool = True,
|
|
902
|
+
sidebar_surface: SurfaceToken | str = "raised",
|
|
903
|
+
statusbar_surface: SurfaceToken | str = "chrome",
|
|
904
|
+
undecorated: bool = False,
|
|
905
|
+
show_sidebar: bool = True,
|
|
906
|
+
sidebar_mode: SidebarMode = "expanded",
|
|
907
|
+
sidebar_width: int | None = None,
|
|
908
|
+
collapsible: bool = True,
|
|
909
|
+
nav_accent: AccentToken | str | None = "primary",
|
|
910
|
+
remember_nav_state: bool = False,
|
|
911
|
+
show_statusbar: bool = False,
|
|
912
|
+
**kwargs: Any,
|
|
913
|
+
) -> None:
|
|
914
|
+
# `show_sidebar=False` is the hidden mode (the model is the truth).
|
|
915
|
+
if not show_sidebar:
|
|
916
|
+
sidebar_mode = "hidden"
|
|
917
|
+
|
|
918
|
+
init_kwargs = _build_init_kwargs(
|
|
919
|
+
title=title, undecorated=undecorated, light_theme=light_theme,
|
|
920
|
+
dark_theme=dark_theme, follow_system_appearance=follow_system_appearance,
|
|
921
|
+
available_themes=available_themes, scaling=scaling, hdpi=hdpi,
|
|
922
|
+
locale=locale, localize_mode=localize_mode, window_style=window_style,
|
|
923
|
+
macos_quit_behavior=macos_quit_behavior,
|
|
924
|
+
remember_window_state=remember_window_state, state_path=state_path,
|
|
925
|
+
sidebar_mode=sidebar_mode, collapsible=collapsible, nav_accent=nav_accent,
|
|
926
|
+
remember_nav_state=remember_nav_state, sidebar_surface=sidebar_surface,
|
|
927
|
+
statusbar_surface=statusbar_surface, size=size, theme=theme,
|
|
928
|
+
position=position, min_size=min_size, max_size=max_size,
|
|
929
|
+
resizable=resizable, on_close=on_close, sidebar_width=sidebar_width,
|
|
930
|
+
)
|
|
931
|
+
init_kwargs.update(kwargs)
|
|
932
|
+
self._init_shell(init_kwargs, icon=icon, show_statusbar=show_statusbar)
|
|
933
|
+
|
|
934
|
+
# ----- Sidebar hosting -----
|
|
935
|
+
|
|
936
|
+
def _sidebar_internal(self) -> Any:
|
|
937
|
+
return self._internal
|
|
938
|
+
|
|
939
|
+
def _content_frame(self) -> Any:
|
|
940
|
+
ws = self._internal.workspace
|
|
941
|
+
return ws.content if ws is not None else self._internal.content
|
|
942
|
+
|
|
943
|
+
def page_nav(self, *, variant: NavVariant = "ghost") -> PageNav:
|
|
944
|
+
"""Declare the sidebar as an authored page list; return its handle.
|
|
945
|
+
|
|
946
|
+
Args:
|
|
947
|
+
variant: How an accented selection reads — `'ghost'` (default) is a
|
|
948
|
+
subtle accent wash behind full-strength text; `'solid'` fills the
|
|
949
|
+
selected item with the accent and uses on-accent (white) text for
|
|
950
|
+
higher emphasis. Needs `nav_accent`; with `nav_accent=None` it
|
|
951
|
+
falls back to a neutral wash.
|
|
952
|
+
|
|
953
|
+
Returns:
|
|
954
|
+
A `PageNav` handle — author items with `add_page()` / `add_header()` /
|
|
955
|
+
`add_divider()` / `add_footer_page()`.
|
|
956
|
+
"""
|
|
957
|
+
self._internal.page_nav(selection=variant)
|
|
958
|
+
return PageNav(self._internal)
|
|
959
|
+
|
|
960
|
+
# ----- Navigation -----
|
|
961
|
+
|
|
962
|
+
def navigate(self, page: str, *, data: dict | None = None) -> None:
|
|
963
|
+
"""Navigate to a page; the sidebar selection follows.
|
|
964
|
+
|
|
965
|
+
Args:
|
|
966
|
+
page: The page key to activate.
|
|
967
|
+
data: Optional data dict passed to the page's change event.
|
|
968
|
+
"""
|
|
969
|
+
self._internal.navigate(page, data=data)
|
|
970
|
+
|
|
971
|
+
@property
|
|
972
|
+
def current(self) -> str | None:
|
|
973
|
+
"""Key of the active page, or `None`."""
|
|
974
|
+
return self._internal.current_page
|
|
975
|
+
|
|
976
|
+
|
|
977
|
+
class Workbench(_ShellBase):
|
|
978
|
+
"""Two-tier application window: a workspace rail plus per-workspace sidebars.
|
|
979
|
+
|
|
980
|
+
The advanced, VS Code-style scaffold — a vertical icon **rail** of workspaces
|
|
981
|
+
down the far left, each revealing its own navigation sidebar and content. Add
|
|
982
|
+
workspaces with `add_workspace()`; each is itself a sidebar host, authored with
|
|
983
|
+
the same `page_nav()` / `list_nav()` / `tree_nav()` / `custom_nav()` front
|
|
984
|
+
doors as a single-tier :class:`AppShell`.
|
|
985
|
+
|
|
986
|
+
For a single sidebar with no rail, use :class:`AppShell` instead.
|
|
987
|
+
|
|
988
|
+
Args:
|
|
989
|
+
title: Window title and (in undecorated mode) chrome label.
|
|
990
|
+
size: Initial window size as `(width, height)`.
|
|
991
|
+
theme: Theme name to apply on startup (e.g. `'bootstrap-dark'`).
|
|
992
|
+
icon: Title-bar and taskbar icon — a path, an `Image`, or an `AppIcon`.
|
|
993
|
+
light_theme: Theme used for the light end of system-appearance tracking.
|
|
994
|
+
dark_theme: Theme used for the dark end of system-appearance tracking.
|
|
995
|
+
follow_system_appearance: If True, track the OS appearance (macOS).
|
|
996
|
+
available_themes: Theme names to expose to theme pickers.
|
|
997
|
+
locale: Locale identifier (e.g. `'en_US'`, `'de_DE'`).
|
|
998
|
+
localize_mode: Localization behavior.
|
|
999
|
+
window_style: Windows-only window effect, or None to disable.
|
|
1000
|
+
macos_quit_behavior: macOS close / Cmd+Q behavior. No-op on Win/Linux.
|
|
1001
|
+
remember_window_state: If True, window geometry is saved and restored.
|
|
1002
|
+
state_path: Optional override for the persisted window-state file.
|
|
1003
|
+
on_close: Handler invoked on the window's close button. Return `False` to
|
|
1004
|
+
veto the close; `None` or `True` to allow it.
|
|
1005
|
+
position: Initial window position as `(x, y)`.
|
|
1006
|
+
min_size: Minimum window size as `(width, height)`.
|
|
1007
|
+
max_size: Maximum window size as `(width, height)`.
|
|
1008
|
+
resizable: Whether the window can be resized as `(x, y)`.
|
|
1009
|
+
scaling: Explicit UI scaling factor. When None, scaling is automatic.
|
|
1010
|
+
hdpi: Enable high-DPI awareness for the application. Default `True`.
|
|
1011
|
+
rail_surface: Surface token for the workspace rail.
|
|
1012
|
+
sidebar_surface: Surface token for the per-workspace navigation sidebar.
|
|
1013
|
+
statusbar_surface: Surface token for the bottom status band.
|
|
1014
|
+
undecorated: Remove OS window decorations and draw a custom border.
|
|
1015
|
+
show_sidebar: Render the sidebar region. Default `True`.
|
|
1016
|
+
sidebar_mode: Initial sidebar mode — `'expanded'`/`'hidden'`.
|
|
1017
|
+
sidebar_width: Expanded sidebar width in pixels.
|
|
1018
|
+
rail_width: Workspace-rail width in pixels.
|
|
1019
|
+
rail_labels: Show a caption under each rail icon (widens the rail).
|
|
1020
|
+
collapsible: Allow collapsing the sidebar; binds Ctrl/Cmd-B. Default `True`.
|
|
1021
|
+
nav_accent: Accent for the active nav item and the rail indicator. Defaults
|
|
1022
|
+
to `'primary'`; another token retints, or `None` for a neutral wash.
|
|
1023
|
+
remember_nav_state: Persist the sidebar mode and per-workspace active page
|
|
1024
|
+
across sessions. Default `False`.
|
|
1025
|
+
show_statusbar: Force the bottom status band on. Default `False`.
|
|
1026
|
+
"""
|
|
1027
|
+
|
|
1028
|
+
def __init__(
|
|
1029
|
+
self,
|
|
1030
|
+
*,
|
|
1031
|
+
title: str = "",
|
|
1032
|
+
size: tuple[int, int] | None = None,
|
|
1033
|
+
theme: str | None = None,
|
|
1034
|
+
icon: "str | Image | AppIcon | None" = None,
|
|
1035
|
+
light_theme: str = "bootstrap-light",
|
|
1036
|
+
dark_theme: str = "bootstrap-dark",
|
|
1037
|
+
follow_system_appearance: bool = False,
|
|
1038
|
+
available_themes: Sequence[str] = (),
|
|
1039
|
+
locale: str | None = None,
|
|
1040
|
+
localize_mode: LocalizeMode = "auto",
|
|
1041
|
+
window_style: WindowStyle | str | None = "mica",
|
|
1042
|
+
macos_quit_behavior: Literal["native", "classic"] = "native",
|
|
1043
|
+
remember_window_state: bool = False,
|
|
1044
|
+
state_path: str | None = None,
|
|
1045
|
+
on_close: Callable[[], bool | None] | None = None,
|
|
1046
|
+
position: tuple[int, int] | None = None,
|
|
1047
|
+
min_size: tuple[int, int] | None = None,
|
|
1048
|
+
max_size: tuple[int, int] | None = None,
|
|
1049
|
+
resizable: tuple[bool, bool] | None = None,
|
|
1050
|
+
scaling: float | None = None,
|
|
1051
|
+
hdpi: bool = True,
|
|
1052
|
+
rail_surface: SurfaceToken | str = "chrome",
|
|
1053
|
+
sidebar_surface: SurfaceToken | str = "raised",
|
|
1054
|
+
statusbar_surface: SurfaceToken | str = "chrome",
|
|
1055
|
+
undecorated: bool = False,
|
|
1056
|
+
show_sidebar: bool = True,
|
|
1057
|
+
sidebar_mode: SidebarMode = "expanded",
|
|
1058
|
+
sidebar_width: int | None = None,
|
|
1059
|
+
rail_width: int | None = None,
|
|
1060
|
+
rail_labels: bool = False,
|
|
1061
|
+
collapsible: bool = True,
|
|
1062
|
+
nav_accent: AccentToken | str | None = "primary",
|
|
1063
|
+
remember_nav_state: bool = False,
|
|
1064
|
+
show_statusbar: bool = False,
|
|
1065
|
+
**kwargs: Any,
|
|
1066
|
+
) -> None:
|
|
1067
|
+
self._rail: Rail | None = None
|
|
1068
|
+
|
|
1069
|
+
# `show_sidebar=False` is the hidden mode (the model is the truth).
|
|
1070
|
+
if not show_sidebar:
|
|
1071
|
+
sidebar_mode = "hidden"
|
|
1072
|
+
|
|
1073
|
+
init_kwargs = _build_init_kwargs(
|
|
1074
|
+
title=title, undecorated=undecorated, light_theme=light_theme,
|
|
1075
|
+
dark_theme=dark_theme, follow_system_appearance=follow_system_appearance,
|
|
1076
|
+
available_themes=available_themes, scaling=scaling, hdpi=hdpi,
|
|
1077
|
+
locale=locale, localize_mode=localize_mode, window_style=window_style,
|
|
1078
|
+
macos_quit_behavior=macos_quit_behavior,
|
|
1079
|
+
remember_window_state=remember_window_state, state_path=state_path,
|
|
1080
|
+
sidebar_mode=sidebar_mode, collapsible=collapsible, nav_accent=nav_accent,
|
|
1081
|
+
remember_nav_state=remember_nav_state, sidebar_surface=sidebar_surface,
|
|
1082
|
+
statusbar_surface=statusbar_surface, size=size, theme=theme,
|
|
1083
|
+
position=position, min_size=min_size, max_size=max_size,
|
|
1084
|
+
resizable=resizable, on_close=on_close, sidebar_width=sidebar_width,
|
|
1085
|
+
)
|
|
1086
|
+
# Rail-only chrome (two-tier).
|
|
1087
|
+
init_kwargs["rail_surface"] = rail_surface
|
|
1088
|
+
init_kwargs["rail_labels"] = rail_labels
|
|
1089
|
+
if rail_width is not None:
|
|
1090
|
+
init_kwargs["rail_width"] = rail_width
|
|
1091
|
+
init_kwargs.update(kwargs)
|
|
1092
|
+
self._init_shell(init_kwargs, icon=icon, show_statusbar=show_statusbar)
|
|
1093
|
+
|
|
1094
|
+
# ----- Workspaces -----
|
|
1095
|
+
|
|
1096
|
+
def add_workspace(
|
|
1097
|
+
self,
|
|
1098
|
+
key: str,
|
|
1099
|
+
*,
|
|
1100
|
+
text: str = "",
|
|
1101
|
+
icon: str | dict | None = None,
|
|
1102
|
+
pin_to_footer: bool = False,
|
|
1103
|
+
) -> Workspace:
|
|
1104
|
+
"""Add a workspace — a rail icon revealing its own sidebar + content.
|
|
1105
|
+
|
|
1106
|
+
The rail appears once there is more than one workspace. The returned
|
|
1107
|
+
`Workspace` is a sidebar host: declare its sidebar with `page_nav()` /
|
|
1108
|
+
`list_nav()` / `tree_nav()` / `custom_nav()`.
|
|
1109
|
+
|
|
1110
|
+
Args:
|
|
1111
|
+
key: Unique workspace identifier.
|
|
1112
|
+
text: Tooltip / rail label for the workspace icon.
|
|
1113
|
+
icon: Rail icon name or icon configuration dict.
|
|
1114
|
+
pin_to_footer: If `True`, pin the rail icon to the bottom of the rail
|
|
1115
|
+
(the conventional spot for Settings / Account) instead of the top
|
|
1116
|
+
cluster.
|
|
1117
|
+
"""
|
|
1118
|
+
add = self._internal.add_footer_workspace if pin_to_footer else self._internal.add_workspace
|
|
1119
|
+
return Workspace(add(key, text=text, icon=icon), self._internal)
|
|
1120
|
+
|
|
1121
|
+
# ----- Navigation -----
|
|
1122
|
+
|
|
1123
|
+
def navigate(self, workspace: str, page: str, *, data: dict | None = None) -> None:
|
|
1124
|
+
"""Switch to `workspace` and select `page` within it.
|
|
1125
|
+
|
|
1126
|
+
Args:
|
|
1127
|
+
workspace: The workspace key to activate.
|
|
1128
|
+
page: The page key to activate within that workspace.
|
|
1129
|
+
data: Optional data dict passed to the page's change event.
|
|
1130
|
+
"""
|
|
1131
|
+
self._internal.navigate(workspace, page, data=data)
|
|
1132
|
+
|
|
1133
|
+
@property
|
|
1134
|
+
def current(self) -> str | None:
|
|
1135
|
+
"""Key of the active workspace's active page, or `None`."""
|
|
1136
|
+
return self._internal.current_page
|
|
1137
|
+
|
|
1138
|
+
@property
|
|
1139
|
+
def current_workspace(self) -> str | None:
|
|
1140
|
+
"""Key of the active workspace, or `None`."""
|
|
1141
|
+
return self._internal.current_workspace
|
|
1142
|
+
|
|
1143
|
+
@property
|
|
1144
|
+
def rail(self) -> Rail:
|
|
1145
|
+
"""The workspace switcher. Methods no-op when the rail is not rendered."""
|
|
1146
|
+
if self._rail is None:
|
|
1147
|
+
self._rail = Rail(self)
|
|
1148
|
+
return self._rail
|
|
1149
|
+
|
|
1150
|
+
# ----- Events -----
|
|
1151
|
+
|
|
1152
|
+
@overload
|
|
1153
|
+
def on_workspace_change(self) -> Stream: ...
|
|
1154
|
+
@overload
|
|
1155
|
+
def on_workspace_change(
|
|
1156
|
+
self, handler: Callable[[WorkspaceChangeEvent], Any]
|
|
1157
|
+
) -> Subscription: ...
|
|
1158
|
+
def on_workspace_change(
|
|
1159
|
+
self, handler: Callable[[WorkspaceChangeEvent], Any] | None = None
|
|
1160
|
+
) -> Stream | Subscription:
|
|
1161
|
+
"""Register a callback fired when the rail switches workspace.
|
|
1162
|
+
|
|
1163
|
+
Args:
|
|
1164
|
+
handler: Called with a
|
|
1165
|
+
:class:`~bootstack.events.WorkspaceChangeEvent`. Omit to get a
|
|
1166
|
+
composable :class:`~bootstack.streams.Stream`.
|
|
1167
|
+
"""
|
|
1168
|
+
return self.on("workspace_change", handler)
|
|
1169
|
+
|
|
1170
|
+
|
|
1171
|
+
_SHELL_EVENTS = {
|
|
1172
|
+
"page_change": "<<PageChange>>",
|
|
1173
|
+
"sidebar_toggle": "<<SidebarToggle>>",
|
|
1174
|
+
"sidebar_mode_change": "<<SidebarModeChange>>",
|
|
1175
|
+
}
|
|
1176
|
+
_WORKBENCH_EVENTS = dict(_SHELL_EVENTS, workspace_change="<<WorkspaceChange>>")
|
|
1177
|
+
|
|
1178
|
+
register_widget_events(AppShell, _SHELL_EVENTS)
|
|
1179
|
+
register_widget_events(Workbench, _WORKBENCH_EVENTS)
|