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,558 @@
|
|
|
1
|
+
"""Navigation providers — what fills a workspace's sidebar panel.
|
|
2
|
+
|
|
3
|
+
A provider owns one workspace's sidebar UI *and* its content-rendering strategy,
|
|
4
|
+
behind a small contract: populate the sidebar, report selections via an
|
|
5
|
+
`on_select` callback, and render content for the active key. The shell hosts one
|
|
6
|
+
provider per workspace, routes selections through `NavModel`, and asks the active
|
|
7
|
+
provider to show content. This is the seam that lets static (`add_page`) and the
|
|
8
|
+
data-bound providers (`list_nav`/`tree_nav`, later) be interchangeable.
|
|
9
|
+
|
|
10
|
+
`StaticProvider` is the first concrete provider: authored items, each with its
|
|
11
|
+
own authored page (N items -> N independent bodies), rendered through a
|
|
12
|
+
`PageStack`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Callable, Protocol, runtime_checkable
|
|
18
|
+
|
|
19
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
20
|
+
from bootstack.widgets._impl.composites.pagestack import PageStack
|
|
21
|
+
from bootstack.widgets._impl.composites.shell.content_host import ContentHost
|
|
22
|
+
from bootstack.widgets._impl.composites.shell.nav_panel import NavPanel
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@runtime_checkable
|
|
26
|
+
class NavProvider(Protocol):
|
|
27
|
+
"""The contract a workspace's sidebar provider fulfills.
|
|
28
|
+
|
|
29
|
+
A provider populates the sidebar and renders content; selection truth lives
|
|
30
|
+
in the shell's `NavModel`, so the provider only *reports* clicks (via the
|
|
31
|
+
`on_select` callback given to `mount`) and *reflects* the active key (via
|
|
32
|
+
`select_visual` / `show`).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
supports_compact: bool
|
|
36
|
+
"""Whether the sidebar may render icon-only (compact).
|
|
37
|
+
|
|
38
|
+
True only for the static provider, whose items carry authored icons. A
|
|
39
|
+
data-bound list (`list_nav`) or hierarchy (`tree_nav`) cannot meaningfully
|
|
40
|
+
compact, so theirs is False — they are hidden or shown, never icon-only.
|
|
41
|
+
Compaction is also gated by the host to the standalone case (no rail).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
def mount(
|
|
45
|
+
self,
|
|
46
|
+
sidebar: Frame,
|
|
47
|
+
content: Frame,
|
|
48
|
+
*,
|
|
49
|
+
on_select: Callable[[str], None],
|
|
50
|
+
on_refresh: Callable[[], None] | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Build the sidebar UI in `sidebar` and prepare the `content` region.
|
|
53
|
+
|
|
54
|
+
`on_refresh` (optional) is invoked after the provider rebuilds its items
|
|
55
|
+
from a changed source, so the host can reconcile the active selection.
|
|
56
|
+
"""
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
def set_compact(self, compact: bool) -> None:
|
|
60
|
+
"""Render the sidebar icon-only (`compact`) or with labels.
|
|
61
|
+
|
|
62
|
+
A no-op for providers where `supports_compact` is False.
|
|
63
|
+
"""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def show(self, key: str, data: dict | None = None) -> None:
|
|
67
|
+
"""Render content for the selected `key`."""
|
|
68
|
+
...
|
|
69
|
+
|
|
70
|
+
def select_visual(self, key: str | None) -> None:
|
|
71
|
+
"""Reflect the active `key` in the sidebar's selection visual."""
|
|
72
|
+
...
|
|
73
|
+
|
|
74
|
+
def keys(self) -> tuple[str, ...]:
|
|
75
|
+
"""All selectable keys this provider exposes."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class StaticProvider:
|
|
80
|
+
"""Static provider — authored items, each with its own authored page.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
accent: Accent token for the active nav item.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
supports_compact = True
|
|
87
|
+
|
|
88
|
+
def __init__(self, *, accent: str | None = None, variant: str = "nav-quiet",
|
|
89
|
+
surface: str = "card", selection: str = "ghost") -> None:
|
|
90
|
+
self._accent = accent
|
|
91
|
+
self._variant = variant
|
|
92
|
+
self._surface = surface
|
|
93
|
+
self._selection = selection
|
|
94
|
+
self._nav: NavPanel | None = None
|
|
95
|
+
self._pages: PageStack | None = None
|
|
96
|
+
self._keys: list[str] = []
|
|
97
|
+
|
|
98
|
+
def mount(
|
|
99
|
+
self,
|
|
100
|
+
sidebar: Frame,
|
|
101
|
+
content: Frame,
|
|
102
|
+
*,
|
|
103
|
+
on_select: Callable[[str], None],
|
|
104
|
+
on_refresh: Callable[[], None] | None = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
# Static authoring never refreshes from a source; on_refresh is ignored.
|
|
107
|
+
self._nav = NavPanel(
|
|
108
|
+
sidebar, on_select=on_select, accent=self._accent,
|
|
109
|
+
variant=self._variant, surface=self._surface, selection=self._selection,
|
|
110
|
+
)
|
|
111
|
+
self._nav.pack(fill="both", expand=True)
|
|
112
|
+
self._pages = PageStack(content)
|
|
113
|
+
self._pages.pack(fill="both", expand=True)
|
|
114
|
+
|
|
115
|
+
# ----- Authoring -----
|
|
116
|
+
|
|
117
|
+
def add_page(self, key: str, *, text: str = "", icon=None, footer: bool = False) -> Any:
|
|
118
|
+
"""Add a nav item and its page; return the page frame."""
|
|
119
|
+
page = self._pages.add(key)
|
|
120
|
+
if footer:
|
|
121
|
+
self._nav.add_footer_item(key, text=text, icon=icon)
|
|
122
|
+
else:
|
|
123
|
+
self._nav.add_item(key, text=text, icon=icon)
|
|
124
|
+
self._keys.append(key)
|
|
125
|
+
return page
|
|
126
|
+
|
|
127
|
+
def add_header(self, text: str) -> Any:
|
|
128
|
+
"""Add a plain section-label header."""
|
|
129
|
+
return self._nav.add_header(text)
|
|
130
|
+
|
|
131
|
+
def add_divider(self) -> Any:
|
|
132
|
+
"""Add a divider."""
|
|
133
|
+
return self._nav.add_divider()
|
|
134
|
+
|
|
135
|
+
# ----- Provider contract -----
|
|
136
|
+
|
|
137
|
+
def set_compact(self, compact: bool) -> None:
|
|
138
|
+
self._nav.set_compact(compact)
|
|
139
|
+
|
|
140
|
+
def show(self, key: str, data: dict | None = None) -> None:
|
|
141
|
+
self._pages.navigate(key, data=data)
|
|
142
|
+
|
|
143
|
+
def select_visual(self, key: str | None) -> None:
|
|
144
|
+
self._nav.select(key)
|
|
145
|
+
|
|
146
|
+
def keys(self) -> tuple[str, ...]:
|
|
147
|
+
return tuple(self._keys)
|
|
148
|
+
|
|
149
|
+
# ----- Accessors -----
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def nav(self) -> NavPanel:
|
|
153
|
+
"""The sidebar navigation panel."""
|
|
154
|
+
return self._nav
|
|
155
|
+
|
|
156
|
+
@property
|
|
157
|
+
def pages(self) -> PageStack:
|
|
158
|
+
"""The content page deck."""
|
|
159
|
+
return self._pages
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ListNavProvider:
|
|
163
|
+
"""Flat, data-bound provider — a `DataSource` drives a sidebar `ListView`.
|
|
164
|
+
|
|
165
|
+
Each record becomes a single-select row (keyed by record id); selecting one
|
|
166
|
+
re-renders a single `@detail` body parameterized by that record — the public
|
|
167
|
+
record dict (the universal `.selection` shape). Rows render the record's
|
|
168
|
+
`text`/`icon`; the recycling `ListView` follows the source live and inherits
|
|
169
|
+
the sidebar surface (no surface plumbing needed).
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
source: A `DataSourceProtocol` supplying records (with `text`/`icon`).
|
|
173
|
+
accent: Accent token (the selection control follows it; the row
|
|
174
|
+
selection itself is a neutral wash by default).
|
|
175
|
+
separator: Draw separator lines between rows. Default False (flush).
|
|
176
|
+
chevron: Show a right-pointing chevron on each row. Off by default — a
|
|
177
|
+
disclosure affordance worth adding only when the row visibly drills
|
|
178
|
+
into something.
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
# A label-less data list is a poor nav, so list_nav is hidden-or-shown,
|
|
182
|
+
# never icon-only (spec Revision 2 / R1).
|
|
183
|
+
supports_compact = False
|
|
184
|
+
|
|
185
|
+
def __init__(
|
|
186
|
+
self,
|
|
187
|
+
source: Any,
|
|
188
|
+
*,
|
|
189
|
+
accent: str | None = None,
|
|
190
|
+
separator: bool = False,
|
|
191
|
+
density: str = "default",
|
|
192
|
+
placeholder: str = "Select an item to view",
|
|
193
|
+
chevron: bool = False,
|
|
194
|
+
) -> None:
|
|
195
|
+
self._source = source
|
|
196
|
+
self._accent = accent
|
|
197
|
+
self._separator = separator
|
|
198
|
+
self._density = density
|
|
199
|
+
self._placeholder = placeholder
|
|
200
|
+
self._chevron = chevron
|
|
201
|
+
self._listview: Any = None
|
|
202
|
+
self._host: ContentHost | None = None
|
|
203
|
+
self._sidebar_host: ContentHost | None = None
|
|
204
|
+
self._detail: Callable[[dict], Any] | None = None
|
|
205
|
+
self._selected: str | None = None
|
|
206
|
+
self._cache: dict[str, dict] = {}
|
|
207
|
+
self._on_select: Callable[[str], None] | None = None
|
|
208
|
+
self._on_refresh: Callable[[], None] | None = None
|
|
209
|
+
self._sub: Any = None
|
|
210
|
+
self._src_sub: Any = None
|
|
211
|
+
|
|
212
|
+
def mount(
|
|
213
|
+
self,
|
|
214
|
+
sidebar: Frame,
|
|
215
|
+
content: Frame,
|
|
216
|
+
*,
|
|
217
|
+
on_select: Callable[[str], None],
|
|
218
|
+
on_refresh: Callable[[], None] | None = None,
|
|
219
|
+
) -> None:
|
|
220
|
+
from bootstack.widgets.listview import ListView
|
|
221
|
+
|
|
222
|
+
self._on_select = on_select
|
|
223
|
+
self._on_refresh = on_refresh
|
|
224
|
+
self._host = ContentHost(content)
|
|
225
|
+
self._sidebar_host = ContentHost(sidebar)
|
|
226
|
+
self._listview = ListView(
|
|
227
|
+
parent=self._sidebar_host,
|
|
228
|
+
data_source=self._source,
|
|
229
|
+
selection_mode="single",
|
|
230
|
+
show_separators=self._separator,
|
|
231
|
+
show_scrollbar=False, # mousewheel scrolls; no always-on bar in a nav
|
|
232
|
+
show_chevron=self._chevron, # opt-in disclosure affordance
|
|
233
|
+
density=self._density,
|
|
234
|
+
accent=self._accent,
|
|
235
|
+
# When a nav accent is set, the row selection tints to it (else neutral).
|
|
236
|
+
accent_selection=bool(self._accent),
|
|
237
|
+
fill="both",
|
|
238
|
+
expand=True,
|
|
239
|
+
)
|
|
240
|
+
self._cache = self._read()
|
|
241
|
+
self._sub = self._listview.on_select(self._on_listview_select)
|
|
242
|
+
# The ListView refreshes its own rows from the source; we observe too, to
|
|
243
|
+
# re-render the open detail when the selected record changes in place and
|
|
244
|
+
# to let the host reconcile selection on add/remove.
|
|
245
|
+
self._src_sub = self._source.on_change(self._on_source_change)
|
|
246
|
+
# An empty source has no first record to auto-select, so the content area
|
|
247
|
+
# would be blank — show a quiet placeholder until rows arrive.
|
|
248
|
+
if not self._cache:
|
|
249
|
+
self._render_placeholder()
|
|
250
|
+
|
|
251
|
+
def _render_placeholder(self) -> None:
|
|
252
|
+
"""Render a centered, muted empty-state in the content region."""
|
|
253
|
+
from bootstack.widgets._impl.primitives.label import Label
|
|
254
|
+
|
|
255
|
+
self._host.clear()
|
|
256
|
+
Label(self._host._internal, text=self._placeholder, accent="muted").pack(expand=True)
|
|
257
|
+
|
|
258
|
+
def set_detail(self, fn: Callable[[dict], Any]) -> None:
|
|
259
|
+
"""Register the parameterized detail body builder."""
|
|
260
|
+
self._detail = fn
|
|
261
|
+
|
|
262
|
+
def _read(self) -> dict[str, dict]:
|
|
263
|
+
"""Read all current public records keyed by id, in source order."""
|
|
264
|
+
src = self._source
|
|
265
|
+
out: dict[str, dict] = {}
|
|
266
|
+
for raw in src.page_slice(0, src.count):
|
|
267
|
+
rec = src._public_record(raw)
|
|
268
|
+
out[str(rec.get("id"))] = rec
|
|
269
|
+
return out
|
|
270
|
+
|
|
271
|
+
def _on_listview_select(self, _event: Any = None) -> None:
|
|
272
|
+
sel = self._listview.selection
|
|
273
|
+
if not sel:
|
|
274
|
+
return
|
|
275
|
+
self._selected = str(sel.get("id"))
|
|
276
|
+
self._on_select(self._selected)
|
|
277
|
+
|
|
278
|
+
def _on_source_change(self, _event: Any = None) -> None:
|
|
279
|
+
"""React to a source change (the ListView refreshes its own rows).
|
|
280
|
+
|
|
281
|
+
Re-render the open detail if the selected record changed in place, and
|
|
282
|
+
let the host reconcile the active selection (e.g. the selected record was
|
|
283
|
+
removed).
|
|
284
|
+
"""
|
|
285
|
+
new = self._read()
|
|
286
|
+
key = self._selected
|
|
287
|
+
changed = (
|
|
288
|
+
key is not None and key in new and key in self._cache
|
|
289
|
+
and new[key] != self._cache[key]
|
|
290
|
+
)
|
|
291
|
+
emptied = bool(self._cache) and not new
|
|
292
|
+
self._cache = new
|
|
293
|
+
if changed:
|
|
294
|
+
self.show(key)
|
|
295
|
+
elif emptied:
|
|
296
|
+
# The list drained to nothing — replace the stale detail with the
|
|
297
|
+
# placeholder (the host won't reconcile to a new selection).
|
|
298
|
+
self._selected = None
|
|
299
|
+
self._render_placeholder()
|
|
300
|
+
if self._on_refresh is not None:
|
|
301
|
+
self._on_refresh()
|
|
302
|
+
|
|
303
|
+
def close(self) -> None:
|
|
304
|
+
"""Cancel the selection + source subscriptions."""
|
|
305
|
+
for sub in (self._sub, self._src_sub):
|
|
306
|
+
if sub is not None:
|
|
307
|
+
try:
|
|
308
|
+
sub.cancel()
|
|
309
|
+
except Exception:
|
|
310
|
+
pass
|
|
311
|
+
self._sub = self._src_sub = None
|
|
312
|
+
|
|
313
|
+
# ----- Provider contract -----
|
|
314
|
+
|
|
315
|
+
def set_compact(self, compact: bool) -> None:
|
|
316
|
+
# list_nav never compacts (supports_compact is False); see R1.
|
|
317
|
+
pass
|
|
318
|
+
|
|
319
|
+
def show(self, key: str, data: dict | None = None) -> None:
|
|
320
|
+
record = self._cache.get(key)
|
|
321
|
+
if self._detail is not None and record is not None:
|
|
322
|
+
self._host.clear()
|
|
323
|
+
with self._host:
|
|
324
|
+
self._detail(record)
|
|
325
|
+
else:
|
|
326
|
+
self._render_placeholder()
|
|
327
|
+
|
|
328
|
+
def select_visual(self, key: str | None) -> None:
|
|
329
|
+
self._selected = key
|
|
330
|
+
if key is None:
|
|
331
|
+
self._listview.clear_selection()
|
|
332
|
+
return
|
|
333
|
+
record = self._cache.get(key)
|
|
334
|
+
if record is not None:
|
|
335
|
+
self._listview.select_items([record.get("id")])
|
|
336
|
+
|
|
337
|
+
def keys(self) -> tuple[str, ...]:
|
|
338
|
+
return tuple(self._read().keys())
|
|
339
|
+
|
|
340
|
+
# ----- Accessors -----
|
|
341
|
+
|
|
342
|
+
@property
|
|
343
|
+
def nav(self) -> Any:
|
|
344
|
+
"""The sidebar list widget (the `ListView`)."""
|
|
345
|
+
return self._listview
|
|
346
|
+
|
|
347
|
+
@property
|
|
348
|
+
def listview(self) -> Any:
|
|
349
|
+
"""The sidebar `ListView`."""
|
|
350
|
+
return self._listview
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
class TreeNavProvider:
|
|
354
|
+
"""Hierarchical, data-bound provider — a `Tree` drives the sidebar.
|
|
355
|
+
|
|
356
|
+
Nodes may be declared inline (`nodes=`) or projected from a flat
|
|
357
|
+
adjacency-list `source` (`source=`/`parent_field=`). Selecting a node
|
|
358
|
+
re-renders one `@detail` body with the node normalized to a record dict
|
|
359
|
+
(`text` from the node label, `icon`, the node id, plus the node's data bag) —
|
|
360
|
+
the same universal `.selection` shape `list_nav` uses (spec decision #10).
|
|
361
|
+
|
|
362
|
+
Hierarchy is incompatible with the icon-rail (a branch cannot render as one
|
|
363
|
+
glyph), so this provider does not support compaction.
|
|
364
|
+
"""
|
|
365
|
+
|
|
366
|
+
supports_compact = False
|
|
367
|
+
|
|
368
|
+
def __init__(
|
|
369
|
+
self,
|
|
370
|
+
*,
|
|
371
|
+
nodes: list | None = None,
|
|
372
|
+
source: Any = None,
|
|
373
|
+
parent_field: str = "parent_id",
|
|
374
|
+
label_field: str = "name",
|
|
375
|
+
icon_field: str = "icon",
|
|
376
|
+
accent: str | None = None,
|
|
377
|
+
density: str = "default",
|
|
378
|
+
placeholder: str = "Select an item to view",
|
|
379
|
+
) -> None:
|
|
380
|
+
self._nodes = nodes
|
|
381
|
+
self._source = source
|
|
382
|
+
self._parent_field = parent_field
|
|
383
|
+
self._label_field = label_field
|
|
384
|
+
self._icon_field = icon_field
|
|
385
|
+
self._accent = accent
|
|
386
|
+
self._density = density
|
|
387
|
+
self._placeholder = placeholder
|
|
388
|
+
self._tree: Any = None
|
|
389
|
+
self._host: ContentHost | None = None
|
|
390
|
+
self._sidebar_host: ContentHost | None = None
|
|
391
|
+
self._detail: Callable[[dict], Any] | None = None
|
|
392
|
+
self._on_select: Callable[[str], None] | None = None
|
|
393
|
+
self._nodes_by_key: dict[str, Any] = {}
|
|
394
|
+
self._sub: Any = None
|
|
395
|
+
|
|
396
|
+
def mount(
|
|
397
|
+
self,
|
|
398
|
+
sidebar: Frame,
|
|
399
|
+
content: Frame,
|
|
400
|
+
*,
|
|
401
|
+
on_select: Callable[[str], None],
|
|
402
|
+
on_refresh: Callable[[], None] | None = None,
|
|
403
|
+
) -> None:
|
|
404
|
+
from bootstack.widgets.tree import Tree
|
|
405
|
+
|
|
406
|
+
self._on_select = on_select
|
|
407
|
+
self._host = ContentHost(content)
|
|
408
|
+
self._sidebar_host = ContentHost(sidebar)
|
|
409
|
+
|
|
410
|
+
tree_kwargs: dict[str, Any] = {
|
|
411
|
+
"parent": self._sidebar_host,
|
|
412
|
+
"selection_mode": "single",
|
|
413
|
+
"show_scrollbar": False,
|
|
414
|
+
"density": self._density,
|
|
415
|
+
"accent": self._accent,
|
|
416
|
+
# When a nav accent is set, the row selection tints to it (else neutral).
|
|
417
|
+
"accent_selection": bool(self._accent),
|
|
418
|
+
"fill": "both",
|
|
419
|
+
"expand": True,
|
|
420
|
+
}
|
|
421
|
+
if self._source is not None:
|
|
422
|
+
tree_kwargs.update(
|
|
423
|
+
data_source=self._source,
|
|
424
|
+
parent_field=self._parent_field,
|
|
425
|
+
label_field=self._label_field,
|
|
426
|
+
icon_field=self._icon_field,
|
|
427
|
+
)
|
|
428
|
+
elif self._nodes is not None:
|
|
429
|
+
tree_kwargs["nodes"] = self._nodes
|
|
430
|
+
self._tree = Tree(**tree_kwargs)
|
|
431
|
+
self._sub = self._tree.on_select(self._on_tree_select)
|
|
432
|
+
# A tree opens with nothing selected, so the content area would be blank.
|
|
433
|
+
# Show a quiet placeholder until the first node is picked.
|
|
434
|
+
self._render_placeholder()
|
|
435
|
+
|
|
436
|
+
def _render_placeholder(self) -> None:
|
|
437
|
+
"""Render a centered, muted empty-state in the content region."""
|
|
438
|
+
from bootstack.widgets._impl.primitives.label import Label
|
|
439
|
+
|
|
440
|
+
self._host.clear()
|
|
441
|
+
Label(self._host._internal, text=self._placeholder, accent="muted").pack(expand=True)
|
|
442
|
+
|
|
443
|
+
def set_detail(self, fn: Callable[[dict], Any]) -> None:
|
|
444
|
+
"""Register the parameterized detail body builder."""
|
|
445
|
+
self._detail = fn
|
|
446
|
+
|
|
447
|
+
# ----- Selection plumbing (node identity -> string key) -----
|
|
448
|
+
|
|
449
|
+
def _key_for(self, node: Any) -> str:
|
|
450
|
+
key = str(id(node))
|
|
451
|
+
self._nodes_by_key[key] = node
|
|
452
|
+
return key
|
|
453
|
+
|
|
454
|
+
def _record(self, node: Any, key: str) -> dict:
|
|
455
|
+
rec = dict(getattr(node, "data", {}) or {})
|
|
456
|
+
rec["text"] = node.label
|
|
457
|
+
rec["icon"] = node.icon
|
|
458
|
+
rec["id"] = key
|
|
459
|
+
return rec
|
|
460
|
+
|
|
461
|
+
def _on_tree_select(self, _event: Any) -> None:
|
|
462
|
+
node = self._tree.selection
|
|
463
|
+
if node is None:
|
|
464
|
+
return
|
|
465
|
+
self._on_select(self._key_for(node))
|
|
466
|
+
|
|
467
|
+
# ----- Provider contract -----
|
|
468
|
+
|
|
469
|
+
def set_compact(self, compact: bool) -> None:
|
|
470
|
+
# A hierarchy cannot render as icons; tree_nav never compacts (see R1).
|
|
471
|
+
pass
|
|
472
|
+
|
|
473
|
+
def show(self, key: str, data: dict | None = None) -> None:
|
|
474
|
+
node = self._nodes_by_key.get(key)
|
|
475
|
+
if self._detail is not None and node is not None:
|
|
476
|
+
self._host.clear()
|
|
477
|
+
with self._host:
|
|
478
|
+
self._detail(self._record(node, key))
|
|
479
|
+
else:
|
|
480
|
+
self._render_placeholder()
|
|
481
|
+
|
|
482
|
+
def select_visual(self, key: str | None) -> None:
|
|
483
|
+
node = self._nodes_by_key.get(key) if key is not None else None
|
|
484
|
+
if node is not None:
|
|
485
|
+
self._tree.select(node)
|
|
486
|
+
|
|
487
|
+
def keys(self) -> tuple[str, ...]:
|
|
488
|
+
# Trees register keys lazily as nodes are selected (nodes may be created
|
|
489
|
+
# on expand). Empty until the first selection — so the shell never
|
|
490
|
+
# auto-selects a tree (a tree opens with nothing selected).
|
|
491
|
+
return tuple(self._nodes_by_key)
|
|
492
|
+
|
|
493
|
+
def close(self) -> None:
|
|
494
|
+
"""Cancel the tree selection subscription."""
|
|
495
|
+
if self._sub is not None:
|
|
496
|
+
try:
|
|
497
|
+
self._sub.cancel()
|
|
498
|
+
except Exception:
|
|
499
|
+
pass
|
|
500
|
+
self._sub = None
|
|
501
|
+
|
|
502
|
+
# ----- Accessors -----
|
|
503
|
+
|
|
504
|
+
@property
|
|
505
|
+
def tree(self) -> Any:
|
|
506
|
+
"""The sidebar `Tree` widget."""
|
|
507
|
+
return self._tree
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
class CustomProvider:
|
|
511
|
+
"""Escape-hatch provider — a blank sidebar container the user fills.
|
|
512
|
+
|
|
513
|
+
There is no cascade and no per-page memory: the workspace hands back the
|
|
514
|
+
sidebar container (via `panel`) and its content region (via `Workspace.content`)
|
|
515
|
+
and the user drives content-switching themselves. A custom panel cannot
|
|
516
|
+
collapse to icons (its content is arbitrary), so `supports_compact` is False —
|
|
517
|
+
it only hides.
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
supports_compact = False
|
|
521
|
+
|
|
522
|
+
def __init__(self) -> None:
|
|
523
|
+
self._container: Frame | None = None
|
|
524
|
+
self._content: Frame | None = None
|
|
525
|
+
|
|
526
|
+
def mount(
|
|
527
|
+
self,
|
|
528
|
+
sidebar: Frame,
|
|
529
|
+
content: Frame,
|
|
530
|
+
*,
|
|
531
|
+
on_select: Callable[[str], None],
|
|
532
|
+
on_refresh: Callable[[], None] | None = None,
|
|
533
|
+
) -> None:
|
|
534
|
+
# No selection model: on_select/on_refresh are unused.
|
|
535
|
+
self._container = Frame(sidebar)
|
|
536
|
+
self._container.pack(fill="both", expand=True)
|
|
537
|
+
self._content = content
|
|
538
|
+
|
|
539
|
+
# ----- Provider contract (mostly inert) -----
|
|
540
|
+
|
|
541
|
+
def set_compact(self, compact: bool) -> None:
|
|
542
|
+
pass
|
|
543
|
+
|
|
544
|
+
def show(self, key: str, data: dict | None = None) -> None:
|
|
545
|
+
pass
|
|
546
|
+
|
|
547
|
+
def select_visual(self, key: str | None) -> None:
|
|
548
|
+
pass
|
|
549
|
+
|
|
550
|
+
def keys(self) -> tuple[str, ...]:
|
|
551
|
+
return ()
|
|
552
|
+
|
|
553
|
+
# ----- Accessors -----
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def container(self) -> Frame:
|
|
557
|
+
"""The blank sidebar container to fill."""
|
|
558
|
+
return self._container
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"""The workspace rail — the tier-1 icon switcher.
|
|
2
|
+
|
|
3
|
+
A vertical strip of icon-only items, one per workspace, with a pinned footer
|
|
4
|
+
(for global workspaces like Settings). The rail is a single-select **radio
|
|
5
|
+
group**: every item is a `RadioToggle` sharing one `Signal`, so single-select is
|
|
6
|
+
the radio machinery itself — no hand-rolled selection state.
|
|
7
|
+
|
|
8
|
+
The items use the `rail` Toolbutton style variant (the VS Code activity-bar
|
|
9
|
+
treatment: muted glyph -> full-strength glyph + accent indicator bar on the
|
|
10
|
+
chrome surface). Selection truth still lives in the shell's `NavModel`; the rail
|
|
11
|
+
reports clicks and reflects the active workspace by setting its shared signal.
|
|
12
|
+
|
|
13
|
+
The VS Code gesture (click the active icon -> hide the sidebar) needs *every*
|
|
14
|
+
click — including a re-click on the already-selected item, which a radio does not
|
|
15
|
+
report as a change — so each item also has a direct click binding that routes to
|
|
16
|
+
the shell.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
from typing import Callable
|
|
22
|
+
|
|
23
|
+
from bootstack.signals import Signal
|
|
24
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
25
|
+
from bootstack.widgets._impl.primitives.radiotoggle import RadioToggle
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Rail(Frame):
|
|
29
|
+
"""A single-select icon rail of workspaces (a radio-toggle group).
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
master: Parent widget (the rail region).
|
|
33
|
+
on_select: Called with a workspace key when its icon is clicked.
|
|
34
|
+
accent: Accent token for the active item.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
# Rail glyphs read as standalone, categorical marks — distinctly larger than
|
|
38
|
+
# the inline sidebar icons. Decision #13's rail_icon_size.
|
|
39
|
+
DEFAULT_ICON_SIZE = 28
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
master,
|
|
44
|
+
*,
|
|
45
|
+
on_select: Callable[[str], None],
|
|
46
|
+
accent: str | None = None,
|
|
47
|
+
icon_size: int | None = None,
|
|
48
|
+
labels: bool = False,
|
|
49
|
+
surface: str = "chrome",
|
|
50
|
+
) -> None:
|
|
51
|
+
super().__init__(master, surface=surface)
|
|
52
|
+
self._on_select = on_select
|
|
53
|
+
self._accent = accent
|
|
54
|
+
self._surface = surface
|
|
55
|
+
self._labels = labels
|
|
56
|
+
self._icon_size = icon_size if icon_size is not None else self.DEFAULT_ICON_SIZE
|
|
57
|
+
# Shared selection signal: the value is the active workspace key, so the
|
|
58
|
+
# matching RadioToggle renders selected (radio single-select). A Signal is
|
|
59
|
+
# typed by its initial value, so use an empty-string sentinel for "none"
|
|
60
|
+
# (no workspace key is empty) rather than None.
|
|
61
|
+
self._signal: Signal = Signal("")
|
|
62
|
+
self._items: dict[str, RadioToggle] = {}
|
|
63
|
+
|
|
64
|
+
self._footer = Frame(self, surface=surface)
|
|
65
|
+
self._footer.pack(side="bottom", fill="x")
|
|
66
|
+
self._main = Frame(self, surface=surface)
|
|
67
|
+
self._main.pack(side="top", fill="both", expand=True)
|
|
68
|
+
|
|
69
|
+
def add_workspace(self, key: str, *, icon=None, text: str = "", footer: bool = False) -> RadioToggle:
|
|
70
|
+
"""Add a workspace icon to the rail (or its footer)."""
|
|
71
|
+
if key in self._items:
|
|
72
|
+
raise ValueError(f"duplicate rail key: {key!r}")
|
|
73
|
+
parent = self._footer if footer else self._main
|
|
74
|
+
# Labeled rail: a small caption under the icon (compound='top'); otherwise
|
|
75
|
+
# icon-only (relies on tooltips for discoverability — a future add).
|
|
76
|
+
toggle_kwargs: dict = dict(
|
|
77
|
+
value=key,
|
|
78
|
+
signal=self._signal,
|
|
79
|
+
icon=self._icon_spec(icon),
|
|
80
|
+
accent=self._accent,
|
|
81
|
+
variant="rail",
|
|
82
|
+
surface=self._surface,
|
|
83
|
+
)
|
|
84
|
+
if self._labels:
|
|
85
|
+
toggle_kwargs.update(text=text, compound="top", style_options={"labeled": True})
|
|
86
|
+
else:
|
|
87
|
+
toggle_kwargs.update(icon_only=True)
|
|
88
|
+
item = RadioToggle(parent, **toggle_kwargs)
|
|
89
|
+
item.pack(fill="x")
|
|
90
|
+
# Catch ALL clicks (including a re-click on the active item) so the shell
|
|
91
|
+
# can run the VS Code gesture; the radio itself only reports changes.
|
|
92
|
+
item.bind("<Button-1>", lambda _e, k=key: self._on_select(k), add="+")
|
|
93
|
+
self._items[key] = item
|
|
94
|
+
return item
|
|
95
|
+
|
|
96
|
+
def _icon_spec(self, icon):
|
|
97
|
+
"""Normalize an icon name/spec to carry the rail icon size."""
|
|
98
|
+
if icon is None:
|
|
99
|
+
return None
|
|
100
|
+
if isinstance(icon, dict):
|
|
101
|
+
spec = dict(icon)
|
|
102
|
+
spec.setdefault("size", self._icon_size)
|
|
103
|
+
return spec
|
|
104
|
+
return {"name": icon, "size": self._icon_size}
|
|
105
|
+
|
|
106
|
+
def select(self, key: str | None) -> None:
|
|
107
|
+
"""Set the visual selection to `key` (or clear with `None`)."""
|
|
108
|
+
self._signal.set(key if key is not None else "")
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def selected(self) -> str | None:
|
|
112
|
+
"""The selected workspace key, or `None`."""
|
|
113
|
+
return self._signal() or None
|
|
114
|
+
|
|
115
|
+
def keys(self) -> tuple[str, ...]:
|
|
116
|
+
"""All workspace keys in insertion order."""
|
|
117
|
+
return tuple(self._items)
|