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,581 @@
|
|
|
1
|
+
"""The shell composite — wires `NavModel` to the region layout.
|
|
2
|
+
|
|
3
|
+
`Shell` ties Layer 1 (`ShellLayout` regions) to Layer 2 (`NavModel`) via Layer 3
|
|
4
|
+
workspaces. Every workspace owns one provider mounted into its own sidebar-panel
|
|
5
|
+
frame and content frame; the sidebar and content regions are each a *deck* of
|
|
6
|
+
per-workspace frames, and switching the active workspace swaps which frames show
|
|
7
|
+
(the nested page-stack cascade — rail chooses the panel, the panel chooses the
|
|
8
|
+
content). The `NavModel` is the single source of truth; the view reacts to it.
|
|
9
|
+
|
|
10
|
+
Single-tier is the degenerate case (spec decision #1): shell-level `add_page` /
|
|
11
|
+
`list_nav` / `tree_nav` target an implicit default workspace, so the rail never
|
|
12
|
+
renders. `add_workspace()` creates additional workspaces (revealing the rail in
|
|
13
|
+
step 6b). The two are mutually exclusive — author one style or the other.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any, Callable
|
|
19
|
+
|
|
20
|
+
from bootstack.events import (
|
|
21
|
+
DisplayModeEvent,
|
|
22
|
+
PageChangeEvent,
|
|
23
|
+
PaneToggleEvent,
|
|
24
|
+
WorkspaceChangeEvent,
|
|
25
|
+
)
|
|
26
|
+
from bootstack.widgets._core.navmodel import NavChange, NavModel, SidebarMode
|
|
27
|
+
from bootstack.widgets._impl.composites.pagestack import PageStack
|
|
28
|
+
from bootstack.widgets._impl.composites.shell.layout import ShellLayout
|
|
29
|
+
from bootstack.widgets._impl.composites.shell.rail import Rail
|
|
30
|
+
from bootstack.widgets._impl.composites.shell.workspace import Workspace
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Key of the implicit single workspace backing the shell-level page methods.
|
|
34
|
+
_DEFAULT_WORKSPACE = "__shell__"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Shell(ShellLayout):
|
|
38
|
+
"""Application shell with workspace-based navigation.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
title: Window title.
|
|
42
|
+
theme: Theme name, or None for the default.
|
|
43
|
+
size: Initial window size as `(width, height)`.
|
|
44
|
+
undecorated: Remove OS chrome and draw a custom border.
|
|
45
|
+
sidebar_mode: Initial sidebar mode (`'expanded'`/`'compact'`/`'hidden'`).
|
|
46
|
+
`'compact'` (icon-only) applies only to a standalone static sidebar.
|
|
47
|
+
collapsible: Allow collapsing the sidebar; binds Ctrl/Cmd-B to toggle it.
|
|
48
|
+
remember_nav_state: Persist the sidebar mode + per-workspace active page
|
|
49
|
+
across sessions, restoring them on the next launch.
|
|
50
|
+
nav_accent: Accent token for the active nav item.
|
|
51
|
+
nav_selection: Accented-selection treatment for the standalone primary
|
|
52
|
+
nav (the no-rail `nav-pill` default workspace) — `'ghost'` (subtle
|
|
53
|
+
wash) or `'solid'` (filled accent + on-accent text). Under-rail
|
|
54
|
+
workspace navs always use the wash.
|
|
55
|
+
**kwargs: Forwarded to `ShellLayout` / `App` (widths, position, etc.).
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(
|
|
59
|
+
self,
|
|
60
|
+
*,
|
|
61
|
+
title: str = "",
|
|
62
|
+
theme: str | None = None,
|
|
63
|
+
size: tuple[int, int] | None = None,
|
|
64
|
+
undecorated: bool = False,
|
|
65
|
+
sidebar_mode: SidebarMode = "expanded",
|
|
66
|
+
collapsible: bool = True,
|
|
67
|
+
remember_nav_state: bool = False,
|
|
68
|
+
nav_accent: str | None = "primary",
|
|
69
|
+
nav_selection: str = "ghost",
|
|
70
|
+
rail_labels: bool = False,
|
|
71
|
+
**kwargs: Any,
|
|
72
|
+
) -> None:
|
|
73
|
+
# A labeled rail (caption under each icon) needs a wider strip than the
|
|
74
|
+
# icon-only default; widen unless the caller pinned rail_width explicitly.
|
|
75
|
+
if rail_labels and "rail_width" not in kwargs:
|
|
76
|
+
kwargs["rail_width"] = 72
|
|
77
|
+
super().__init__(
|
|
78
|
+
title=title, theme=theme, size=size, undecorated=undecorated, **kwargs
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
self._model = NavModel(sidebar_mode=sidebar_mode)
|
|
82
|
+
self._nav_accent = nav_accent
|
|
83
|
+
self._nav_selection = nav_selection
|
|
84
|
+
self._rail_labels = rail_labels
|
|
85
|
+
self._collapsible = collapsible
|
|
86
|
+
self._remember_nav_state = remember_nav_state
|
|
87
|
+
self._initial_applied = False
|
|
88
|
+
self._workspaces: dict[str, Workspace] = {}
|
|
89
|
+
self._pending_data: dict | None = None
|
|
90
|
+
self._prev_page: str | None = None
|
|
91
|
+
self._prev_workspace: str | None = None
|
|
92
|
+
# Track sidebar visibility separately so compact<->expanded transitions
|
|
93
|
+
# fire <<SidebarModeChange>> but NOT <<SidebarToggle>> (a pure-visibility
|
|
94
|
+
# event); only a hidden<->shown flip toggles.
|
|
95
|
+
self._prev_sidebar_visible: bool = sidebar_mode != "hidden"
|
|
96
|
+
|
|
97
|
+
# Build the per-workspace decks, rail switcher, and model wiring. Factored
|
|
98
|
+
# out so dev hot reload can tear the authored navigation down and rebuild
|
|
99
|
+
# it against the same (stable) region frames (see _dev_reset).
|
|
100
|
+
self._build_nav_region()
|
|
101
|
+
|
|
102
|
+
# Ctrl/Cmd-B toggles sidebar visibility (the hamburger action).
|
|
103
|
+
if self._collapsible:
|
|
104
|
+
self.bind("<Control-b>", self._on_toggle_shortcut, add="+")
|
|
105
|
+
self.bind("<Command-b>", self._on_toggle_shortcut, add="+")
|
|
106
|
+
|
|
107
|
+
def _build_nav_region(self) -> None:
|
|
108
|
+
"""Build the sidebar/content decks and rail switcher, and wire the model.
|
|
109
|
+
|
|
110
|
+
Parents into the stable region frames (`self.sidebar`/`content`/`rail`
|
|
111
|
+
from `ShellLayout`), so it is safe to call again after `_dev_reset` has
|
|
112
|
+
destroyed the previous decks.
|
|
113
|
+
"""
|
|
114
|
+
# Sidebar + content are decks of per-workspace frames.
|
|
115
|
+
self._panel_stack = PageStack(self.sidebar)
|
|
116
|
+
self._panel_stack.pack(fill="both", expand=True)
|
|
117
|
+
self._content_stack = PageStack(self.content)
|
|
118
|
+
self._content_stack.pack(fill="both", expand=True)
|
|
119
|
+
|
|
120
|
+
# The tier-1 workspace switcher (hidden until > 1 workspace).
|
|
121
|
+
# NB: must NOT be named `self._rail` — that is ShellLayout's rail *region*
|
|
122
|
+
# frame; shadowing it orphans the region so it never packs into the body.
|
|
123
|
+
self._railnav = Rail(
|
|
124
|
+
self.rail, on_select=self._rail_select, accent=self._nav_accent,
|
|
125
|
+
labels=self._rail_labels, surface=self.rail_surface,
|
|
126
|
+
)
|
|
127
|
+
self._railnav.pack(fill="both", expand=True)
|
|
128
|
+
|
|
129
|
+
self._model.subscribe(self._on_model_change)
|
|
130
|
+
|
|
131
|
+
def _dev_reset(self) -> None:
|
|
132
|
+
"""Tear down all authored navigation so the with-body can rebuild it.
|
|
133
|
+
|
|
134
|
+
Dev hot reload only — closes provider subscriptions, destroys the decks
|
|
135
|
+
and rail, and starts a fresh model + nav region against the (stable)
|
|
136
|
+
region frames. Mirrors the navigation half of `__init__`.
|
|
137
|
+
"""
|
|
138
|
+
for ws in list(self._workspaces.values()):
|
|
139
|
+
try:
|
|
140
|
+
ws.close()
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
self._workspaces = {}
|
|
144
|
+
for attr in ("_panel_stack", "_content_stack", "_railnav"):
|
|
145
|
+
widget = getattr(self, attr, None)
|
|
146
|
+
if widget is not None:
|
|
147
|
+
try:
|
|
148
|
+
widget.destroy()
|
|
149
|
+
except Exception:
|
|
150
|
+
pass
|
|
151
|
+
mode = self._model.sidebar_mode
|
|
152
|
+
self._model = NavModel(sidebar_mode=mode)
|
|
153
|
+
self._initial_applied = False
|
|
154
|
+
self._prev_workspace = None
|
|
155
|
+
self._prev_page = None
|
|
156
|
+
self._prev_sidebar_visible = mode != "hidden"
|
|
157
|
+
self._pending_data = None
|
|
158
|
+
self._build_nav_region()
|
|
159
|
+
|
|
160
|
+
# ----- Workspace creation -----
|
|
161
|
+
|
|
162
|
+
def _create_workspace(self, key: str, *, text: str = "", icon=None, is_footer: bool = False) -> Workspace:
|
|
163
|
+
panel = self._panel_stack.add(key)
|
|
164
|
+
content = self._content_stack.add(key)
|
|
165
|
+
# Tier-relative static treatment: the implicit default workspace is the
|
|
166
|
+
# standalone primary nav (no rail) -> prominent pills; a named workspace
|
|
167
|
+
# sits under the rail -> quiet rows. (list_nav/tree keep their own row
|
|
168
|
+
# language regardless; only the static provider reads this.)
|
|
169
|
+
nav_variant = "nav-pill" if key == _DEFAULT_WORKSPACE else "nav-quiet"
|
|
170
|
+
# The `'solid'` (filled-accent) selection is the standalone primary nav's
|
|
171
|
+
# higher-emphasis look ONLY (the no-rail `nav-pill` default workspace). A
|
|
172
|
+
# named workspace's nav sits under the rail as quiet rows — a solid fill
|
|
173
|
+
# there would compete with the rail, so it always uses the subtle wash.
|
|
174
|
+
nav_selection = self._nav_selection if key == _DEFAULT_WORKSPACE else "ghost"
|
|
175
|
+
ws = Workspace(
|
|
176
|
+
key, panel, content,
|
|
177
|
+
nav_accent=self._nav_accent,
|
|
178
|
+
on_select=self._workspace_select,
|
|
179
|
+
on_refresh=self._workspace_refresh,
|
|
180
|
+
on_first_page=self._on_first_page,
|
|
181
|
+
nav_variant=nav_variant,
|
|
182
|
+
nav_surface=self.sidebar_surface,
|
|
183
|
+
nav_selection=nav_selection,
|
|
184
|
+
)
|
|
185
|
+
self._workspaces[key] = ws
|
|
186
|
+
# The implicit default workspace never gets a rail icon (single-tier).
|
|
187
|
+
if key != _DEFAULT_WORKSPACE:
|
|
188
|
+
self._railnav.add_workspace(key, icon=icon, text=text, footer=is_footer)
|
|
189
|
+
self._model.add_workspace(key, text=text, icon=icon, is_footer=is_footer)
|
|
190
|
+
if self._model.active_workspace == key:
|
|
191
|
+
self._panel_stack.navigate(key)
|
|
192
|
+
self._content_stack.navigate(key)
|
|
193
|
+
if key != _DEFAULT_WORKSPACE:
|
|
194
|
+
self._railnav.select(key)
|
|
195
|
+
return ws
|
|
196
|
+
|
|
197
|
+
def _default_workspace(self) -> Workspace:
|
|
198
|
+
if _DEFAULT_WORKSPACE not in self._workspaces:
|
|
199
|
+
if self._workspaces:
|
|
200
|
+
raise RuntimeError(
|
|
201
|
+
"cannot use shell-level pages together with add_workspace(); "
|
|
202
|
+
"author one style or the other"
|
|
203
|
+
)
|
|
204
|
+
self._create_workspace(_DEFAULT_WORKSPACE)
|
|
205
|
+
return self._workspaces[_DEFAULT_WORKSPACE]
|
|
206
|
+
|
|
207
|
+
def add_workspace(self, key: str, *, text: str = "", icon=None) -> Workspace:
|
|
208
|
+
"""Add a workspace (a rail icon → its own sidebar panel + content).
|
|
209
|
+
|
|
210
|
+
Adding a second workspace reveals the rail. Mutually exclusive with the
|
|
211
|
+
shell-level page methods.
|
|
212
|
+
|
|
213
|
+
Raises:
|
|
214
|
+
RuntimeError: If shell-level pages already claimed the implicit
|
|
215
|
+
workspace, or the key is already used.
|
|
216
|
+
"""
|
|
217
|
+
if _DEFAULT_WORKSPACE in self._workspaces:
|
|
218
|
+
raise RuntimeError("cannot mix add_workspace() with shell-level pages")
|
|
219
|
+
if key in self._workspaces:
|
|
220
|
+
raise RuntimeError(f"duplicate workspace key: {key!r}")
|
|
221
|
+
return self._create_workspace(key, text=text, icon=icon)
|
|
222
|
+
|
|
223
|
+
def add_footer_workspace(self, key: str, *, text: str = "", icon=None) -> Workspace:
|
|
224
|
+
"""Add a workspace pinned to the bottom of the rail."""
|
|
225
|
+
if _DEFAULT_WORKSPACE in self._workspaces:
|
|
226
|
+
raise RuntimeError("cannot mix add_workspace() with shell-level pages")
|
|
227
|
+
if key in self._workspaces:
|
|
228
|
+
raise RuntimeError(f"duplicate workspace key: {key!r}")
|
|
229
|
+
return self._create_workspace(key, text=text, icon=icon, is_footer=True)
|
|
230
|
+
|
|
231
|
+
# ----- Shell-level content API (delegates to the default workspace) -----
|
|
232
|
+
|
|
233
|
+
def add_page(self, key: str, *, text: str = "", icon=None, footer: bool = False) -> Any:
|
|
234
|
+
"""Add a nav item and its page in the implicit workspace."""
|
|
235
|
+
return self._default_workspace().add_page(key, text=text, icon=icon, footer=footer)
|
|
236
|
+
|
|
237
|
+
def add_footer_page(self, key: str, *, text: str = "", icon=None) -> Any:
|
|
238
|
+
"""Add a footer-pinned nav item and its page in the implicit workspace."""
|
|
239
|
+
return self._default_workspace().add_footer_page(key, text=text, icon=icon)
|
|
240
|
+
|
|
241
|
+
def add_header(self, text: str) -> Any:
|
|
242
|
+
"""Add a plain section-label header in the implicit workspace."""
|
|
243
|
+
return self._default_workspace().add_header(text)
|
|
244
|
+
|
|
245
|
+
def add_divider(self) -> Any:
|
|
246
|
+
"""Add a divider to the implicit workspace."""
|
|
247
|
+
return self._default_workspace().add_divider()
|
|
248
|
+
|
|
249
|
+
def page_nav(self, *, selection: str | None = None) -> Any:
|
|
250
|
+
"""Establish the implicit workspace's static page-list provider."""
|
|
251
|
+
return self._default_workspace().page_nav(selection=selection)
|
|
252
|
+
|
|
253
|
+
def panel(self) -> Any:
|
|
254
|
+
"""Claim the implicit workspace as a custom panel; return its container."""
|
|
255
|
+
return self._default_workspace().panel()
|
|
256
|
+
|
|
257
|
+
def list_nav(
|
|
258
|
+
self,
|
|
259
|
+
source: Any,
|
|
260
|
+
*,
|
|
261
|
+
separator: bool = False,
|
|
262
|
+
density: str = "default",
|
|
263
|
+
placeholder: str = "Select an item to view",
|
|
264
|
+
chevron: bool = False,
|
|
265
|
+
) -> Any:
|
|
266
|
+
"""Fill the implicit workspace from a `DataSource` (flat master-detail)."""
|
|
267
|
+
return self._default_workspace().list_nav(
|
|
268
|
+
source, separator=separator, density=density,
|
|
269
|
+
placeholder=placeholder, chevron=chevron,
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def tree_nav(self, **kwargs: Any) -> Any:
|
|
273
|
+
"""Fill the implicit workspace from a hierarchy (tree master-detail)."""
|
|
274
|
+
return self._default_workspace().tree_nav(**kwargs)
|
|
275
|
+
|
|
276
|
+
def detail(self, fn: Callable[[dict], Any]) -> Callable[[dict], Any]:
|
|
277
|
+
"""Register the implicit workspace's data-bound detail body (decorator)."""
|
|
278
|
+
return self._default_workspace().detail(fn)
|
|
279
|
+
|
|
280
|
+
# ----- Navigation -----
|
|
281
|
+
|
|
282
|
+
def navigate(self, *args: str, data: dict | None = None) -> None:
|
|
283
|
+
"""Navigate to a page (single-tier) or a workspace + page (two-tier).
|
|
284
|
+
|
|
285
|
+
`navigate(page)` selects a page in the active workspace;
|
|
286
|
+
`navigate(workspace, page)` switches workspace and selects a page in it.
|
|
287
|
+
|
|
288
|
+
Raises:
|
|
289
|
+
KeyError: If the workspace or page does not exist.
|
|
290
|
+
TypeError: If called with other than one or two positional keys.
|
|
291
|
+
"""
|
|
292
|
+
if len(args) == 2:
|
|
293
|
+
ws_key, page = args
|
|
294
|
+
if ws_key not in self._workspaces:
|
|
295
|
+
raise KeyError(ws_key)
|
|
296
|
+
self._model.select_workspace(ws_key)
|
|
297
|
+
self._model.show_sidebar()
|
|
298
|
+
self._select_page(ws_key, page, data=data)
|
|
299
|
+
elif len(args) == 1:
|
|
300
|
+
ws_key = self._model.active_workspace
|
|
301
|
+
if ws_key is None:
|
|
302
|
+
raise RuntimeError("no active workspace to navigate")
|
|
303
|
+
self._select_page(ws_key, args[0], data=data)
|
|
304
|
+
else:
|
|
305
|
+
raise TypeError("navigate() takes 1 (page) or 2 (workspace, page) keys")
|
|
306
|
+
|
|
307
|
+
def _select_page(self, ws_key: str, page_key: str, *, data: dict | None = None) -> None:
|
|
308
|
+
if page_key not in self._workspaces[ws_key].keys():
|
|
309
|
+
raise KeyError(page_key)
|
|
310
|
+
self._pending_data = data
|
|
311
|
+
self._model.select_page(page_key, workspace=ws_key)
|
|
312
|
+
|
|
313
|
+
@property
|
|
314
|
+
def current_page(self) -> str | None:
|
|
315
|
+
"""Key of the active workspace's active page, or `None`."""
|
|
316
|
+
return self._model.active_page()
|
|
317
|
+
|
|
318
|
+
@property
|
|
319
|
+
def current_workspace(self) -> str | None:
|
|
320
|
+
"""Key of the active workspace, or `None`."""
|
|
321
|
+
return self._model.active_workspace
|
|
322
|
+
|
|
323
|
+
# ----- Sidebar visibility / compaction -----
|
|
324
|
+
|
|
325
|
+
def toggle_sidebar(self) -> None:
|
|
326
|
+
"""Toggle the sidebar between hidden and shown (the hamburger action)."""
|
|
327
|
+
self._model.toggle_sidebar()
|
|
328
|
+
|
|
329
|
+
def show_sidebar(self) -> None:
|
|
330
|
+
"""Show the sidebar, restoring its last non-hidden mode."""
|
|
331
|
+
self._model.show_sidebar()
|
|
332
|
+
|
|
333
|
+
def hide_sidebar(self) -> None:
|
|
334
|
+
"""Hide the sidebar entirely."""
|
|
335
|
+
self._model.hide_sidebar()
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def sidebar_mode(self) -> SidebarMode:
|
|
339
|
+
"""The sidebar mode (`'hidden'`/`'compact'`/`'expanded'`).
|
|
340
|
+
|
|
341
|
+
Setting `'compact'` only takes visual effect on a standalone static
|
|
342
|
+
sidebar (no rail); otherwise it renders as expanded.
|
|
343
|
+
"""
|
|
344
|
+
return self._model.sidebar_mode
|
|
345
|
+
|
|
346
|
+
@sidebar_mode.setter
|
|
347
|
+
def sidebar_mode(self, mode: SidebarMode) -> None:
|
|
348
|
+
self._model.set_sidebar_mode(mode)
|
|
349
|
+
|
|
350
|
+
def _can_compact_active(self) -> bool:
|
|
351
|
+
"""Whether the active sidebar can compact (standalone static, no rail)."""
|
|
352
|
+
ws = self.workspace
|
|
353
|
+
return not self._model.rail_visible and ws is not None and ws.supports_compact
|
|
354
|
+
|
|
355
|
+
def _on_toggle_shortcut(self, _event: Any = None) -> str:
|
|
356
|
+
# Collapse the sidebar as far as is useful: to icons when the active
|
|
357
|
+
# sidebar can compact (standalone static — never strands the only nav),
|
|
358
|
+
# otherwise hide it (a data list can't compact; under a rail the rail
|
|
359
|
+
# stays as nav, so hiding is safe). The explicit toggle_sidebar()/
|
|
360
|
+
# show_sidebar()/hide_sidebar() verbs remain pure visibility.
|
|
361
|
+
if self._can_compact_active():
|
|
362
|
+
self.sidebar_mode = (
|
|
363
|
+
"expanded" if self._model.sidebar_mode == "compact" else "compact"
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
self.toggle_sidebar()
|
|
367
|
+
return "break"
|
|
368
|
+
|
|
369
|
+
# ----- Model -> view -----
|
|
370
|
+
|
|
371
|
+
def _rail_select(self, ws_key: str) -> None:
|
|
372
|
+
# The VS Code gesture lives in the model: active icon -> hide sidebar,
|
|
373
|
+
# different icon -> switch workspace + show.
|
|
374
|
+
self._model.activate_rail(ws_key)
|
|
375
|
+
|
|
376
|
+
def _workspace_select(self, ws_key: str, page_key: str) -> None:
|
|
377
|
+
self._model.select_page(page_key, workspace=ws_key)
|
|
378
|
+
|
|
379
|
+
def _on_first_page(self, ws_key: str, page_key: str) -> None:
|
|
380
|
+
if self._model.active_page(ws_key) is None:
|
|
381
|
+
self._model.select_page(page_key, workspace=ws_key)
|
|
382
|
+
|
|
383
|
+
def _workspace_refresh(self, ws_key: str) -> None:
|
|
384
|
+
if ws_key != self._model.active_workspace:
|
|
385
|
+
return
|
|
386
|
+
ws = self._workspaces[ws_key]
|
|
387
|
+
keys = ws.keys()
|
|
388
|
+
if keys and self._model.active_page(ws_key) not in keys:
|
|
389
|
+
self._select_page(ws_key, keys[0])
|
|
390
|
+
|
|
391
|
+
def _on_model_change(self, change: NavChange) -> None:
|
|
392
|
+
if change.facet == "page" and change.page is not None:
|
|
393
|
+
if change.workspace == self._model.active_workspace:
|
|
394
|
+
self._render_page(change.workspace, change.page)
|
|
395
|
+
elif change.facet == "workspace" and change.workspace is not None:
|
|
396
|
+
self._switch_workspace(change.workspace)
|
|
397
|
+
payload = WorkspaceChangeEvent(
|
|
398
|
+
workspace=change.workspace, prev_workspace=self._prev_workspace
|
|
399
|
+
)
|
|
400
|
+
self._prev_workspace = change.workspace
|
|
401
|
+
self.event_generate("<<WorkspaceChange>>", data=payload, when="tail")
|
|
402
|
+
elif change.facet == "sidebar":
|
|
403
|
+
self._apply_sidebar_mode(change.sidebar_mode)
|
|
404
|
+
mode = change.sidebar_mode or self._model.sidebar_mode
|
|
405
|
+
self.event_generate(
|
|
406
|
+
"<<SidebarModeChange>>", data=DisplayModeEvent(mode=mode), when="tail"
|
|
407
|
+
)
|
|
408
|
+
visible = self._model.sidebar_visible
|
|
409
|
+
if visible != self._prev_sidebar_visible:
|
|
410
|
+
self._prev_sidebar_visible = visible
|
|
411
|
+
self.event_generate(
|
|
412
|
+
"<<SidebarToggle>>", data=PaneToggleEvent(is_open=visible), when="tail"
|
|
413
|
+
)
|
|
414
|
+
elif change.facet == "structure":
|
|
415
|
+
self._sync_rail_visibility()
|
|
416
|
+
|
|
417
|
+
def _apply_sidebar_mode(self, mode: SidebarMode | None) -> None:
|
|
418
|
+
"""Reflect a sidebar mode in the view.
|
|
419
|
+
|
|
420
|
+
`compact` (icon-only + narrower width) is gated to the standalone static
|
|
421
|
+
case: it applies only when no rail is showing and the active provider
|
|
422
|
+
supports it; otherwise the sidebar renders expanded.
|
|
423
|
+
"""
|
|
424
|
+
if mode is None:
|
|
425
|
+
mode = self._model.sidebar_mode
|
|
426
|
+
if mode == "hidden":
|
|
427
|
+
self.set_sidebar_visible(False)
|
|
428
|
+
return
|
|
429
|
+
self.set_sidebar_visible(True)
|
|
430
|
+
ws = self.workspace
|
|
431
|
+
compact = (
|
|
432
|
+
mode == "compact"
|
|
433
|
+
and not self._model.rail_visible
|
|
434
|
+
and ws is not None
|
|
435
|
+
and ws.supports_compact
|
|
436
|
+
)
|
|
437
|
+
# Configure the slot frame directly so the stored expanded-width token is
|
|
438
|
+
# not clobbered (set_sidebar_width mutates the token).
|
|
439
|
+
self.sidebar.configure(width=self._rail_width if compact else self._sidebar_width)
|
|
440
|
+
if ws is not None:
|
|
441
|
+
ws.set_compact(compact)
|
|
442
|
+
|
|
443
|
+
def _render_page(self, ws_key: str, page_key: str) -> None:
|
|
444
|
+
ws = self._workspaces[ws_key]
|
|
445
|
+
data = self._pending_data or {}
|
|
446
|
+
self._pending_data = None
|
|
447
|
+
ws.select_visual(page_key)
|
|
448
|
+
ws.show(page_key, data=data)
|
|
449
|
+
payload = PageChangeEvent(page=page_key, prev_page=self._prev_page, data=dict(data))
|
|
450
|
+
self._prev_page = page_key
|
|
451
|
+
self.event_generate("<<PageChange>>", data=payload, when="tail")
|
|
452
|
+
|
|
453
|
+
def _switch_workspace(self, ws_key: str) -> None:
|
|
454
|
+
self._panel_stack.navigate(ws_key)
|
|
455
|
+
self._content_stack.navigate(ws_key)
|
|
456
|
+
if ws_key in self._railnav.keys():
|
|
457
|
+
self._railnav.select(ws_key)
|
|
458
|
+
page = self._model.active_page(ws_key)
|
|
459
|
+
if page is not None:
|
|
460
|
+
self._render_page(ws_key, page)
|
|
461
|
+
|
|
462
|
+
def _sync_rail_visibility(self) -> None:
|
|
463
|
+
# The rail widget is wired in step 6b; for now reflect model state only.
|
|
464
|
+
self.set_rail_visible(self._model.rail_visible)
|
|
465
|
+
|
|
466
|
+
# ----- Lifecycle -----
|
|
467
|
+
|
|
468
|
+
def mainloop(self, n: int = 0) -> None:
|
|
469
|
+
"""Apply the initial (and any persisted) nav state, then run the loop."""
|
|
470
|
+
self._apply_initial_state()
|
|
471
|
+
super().mainloop(n=n)
|
|
472
|
+
|
|
473
|
+
def _apply_initial_state(self) -> None:
|
|
474
|
+
"""Restore persisted state (if enabled) and reflect the sidebar mode.
|
|
475
|
+
|
|
476
|
+
Runs once, just before the event loop starts — after all pages have been
|
|
477
|
+
authored — so a restored page/workspace overrides the authored default
|
|
478
|
+
(mirroring window-geometry restore).
|
|
479
|
+
"""
|
|
480
|
+
if self._initial_applied:
|
|
481
|
+
return
|
|
482
|
+
self._initial_applied = True
|
|
483
|
+
if self._remember_nav_state:
|
|
484
|
+
self._restore_nav_state()
|
|
485
|
+
self._apply_sidebar_mode(self._model.sidebar_mode)
|
|
486
|
+
|
|
487
|
+
# ----- Nav-state persistence -----
|
|
488
|
+
|
|
489
|
+
def _nav_state_path(self):
|
|
490
|
+
from bootstack._core.paths import app_config_file
|
|
491
|
+
return app_config_file("nav_state.json", self.settings.app_name)
|
|
492
|
+
|
|
493
|
+
def _restore_nav_state(self) -> None:
|
|
494
|
+
import json
|
|
495
|
+
try:
|
|
496
|
+
raw = self._nav_state_path().read_text()
|
|
497
|
+
data = json.loads(raw)
|
|
498
|
+
except (FileNotFoundError, OSError, json.JSONDecodeError):
|
|
499
|
+
return
|
|
500
|
+
if not isinstance(data, dict):
|
|
501
|
+
return
|
|
502
|
+
# Per-workspace remembered pages (only those still present).
|
|
503
|
+
pages = data.get("active_page")
|
|
504
|
+
if isinstance(pages, dict):
|
|
505
|
+
for ws_key, page in pages.items():
|
|
506
|
+
ws = self._workspaces.get(ws_key)
|
|
507
|
+
if ws is not None and page in ws.keys():
|
|
508
|
+
self._model.select_page(page, workspace=ws_key)
|
|
509
|
+
# Active workspace.
|
|
510
|
+
active_ws = data.get("active_workspace")
|
|
511
|
+
if isinstance(active_ws, str) and self._model.has_workspace(active_ws):
|
|
512
|
+
self._model.select_workspace(active_ws)
|
|
513
|
+
# Sidebar mode.
|
|
514
|
+
mode = data.get("sidebar_mode")
|
|
515
|
+
if mode in ("hidden", "compact", "expanded"):
|
|
516
|
+
self._model.set_sidebar_mode(mode)
|
|
517
|
+
|
|
518
|
+
def _save_nav_state(self) -> None:
|
|
519
|
+
import json
|
|
520
|
+
path = self._nav_state_path()
|
|
521
|
+
try:
|
|
522
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
523
|
+
except OSError:
|
|
524
|
+
return
|
|
525
|
+
pages = {
|
|
526
|
+
k: self._model.active_page(k)
|
|
527
|
+
for k in self._workspaces
|
|
528
|
+
if self._model.active_page(k) is not None
|
|
529
|
+
}
|
|
530
|
+
data = {
|
|
531
|
+
"sidebar_mode": self._model.sidebar_mode,
|
|
532
|
+
"active_workspace": self._model.active_workspace,
|
|
533
|
+
"active_page": pages,
|
|
534
|
+
}
|
|
535
|
+
try:
|
|
536
|
+
path.write_text(json.dumps(data))
|
|
537
|
+
except OSError:
|
|
538
|
+
pass
|
|
539
|
+
|
|
540
|
+
def destroy(self) -> None:
|
|
541
|
+
"""Persist nav state, close provider subscriptions, then tear down."""
|
|
542
|
+
if getattr(self, "_remember_nav_state", False):
|
|
543
|
+
try:
|
|
544
|
+
self._save_nav_state()
|
|
545
|
+
except Exception:
|
|
546
|
+
pass
|
|
547
|
+
for ws in getattr(self, "_workspaces", {}).values():
|
|
548
|
+
ws.close()
|
|
549
|
+
super().destroy()
|
|
550
|
+
|
|
551
|
+
# ----- Accessors -----
|
|
552
|
+
|
|
553
|
+
@property
|
|
554
|
+
def model(self) -> NavModel:
|
|
555
|
+
"""The navigation model (single source of truth)."""
|
|
556
|
+
return self._model
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def workspace(self) -> Workspace | None:
|
|
560
|
+
"""The active workspace (or `None`)."""
|
|
561
|
+
return self._workspaces.get(self._model.active_workspace)
|
|
562
|
+
|
|
563
|
+
def workspaces(self) -> tuple[Workspace, ...]:
|
|
564
|
+
"""All workspaces in insertion order."""
|
|
565
|
+
return tuple(self._workspaces.values())
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def provider(self) -> Any:
|
|
569
|
+
"""The active workspace's navigation provider (or `None`)."""
|
|
570
|
+
ws = self.workspace
|
|
571
|
+
return ws.provider if ws is not None else None
|
|
572
|
+
|
|
573
|
+
@property
|
|
574
|
+
def pages(self) -> Any:
|
|
575
|
+
"""The active static provider's content page deck (or `None`)."""
|
|
576
|
+
return getattr(self.provider, "pages", None)
|
|
577
|
+
|
|
578
|
+
@property
|
|
579
|
+
def nav(self) -> Any:
|
|
580
|
+
"""The active provider's sidebar navigation panel (or `None`)."""
|
|
581
|
+
return getattr(self.provider, "nav", None)
|