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,648 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Iterator, TYPE_CHECKING, overload
|
|
4
|
+
|
|
5
|
+
from bootstack.widgets._impl.composites.tree.treeview import TreeView as _InternalTreeView
|
|
6
|
+
from bootstack.widgets._impl.composites.tree.treenode import TreeNode
|
|
7
|
+
from bootstack.widgets._core.base import PublicWidgetBase
|
|
8
|
+
from bootstack.widgets._core.events import register_widget_events
|
|
9
|
+
from bootstack.events import Subscription, TreeSelectionEvent
|
|
10
|
+
from bootstack.streams import Stream
|
|
11
|
+
from bootstack.widgets.types import AccentToken, WidgetDensity, SelectionMode, ScrollbarVariant
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from collections.abc import Sequence
|
|
15
|
+
from bootstack.data.types import DataSourceProtocol
|
|
16
|
+
from bootstack.data.query import Column, Condition, SortKey
|
|
17
|
+
|
|
18
|
+
__all__ = ["Tree", "TreeNode"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _node_key(value: Any) -> Any:
|
|
22
|
+
"""Normalize an id for cross-type comparison (mirrors the source binding)."""
|
|
23
|
+
return None if value is None else str(value)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Tree(PublicWidgetBase):
|
|
27
|
+
"""A hierarchical tree for navigation and selection.
|
|
28
|
+
|
|
29
|
+
Tree displays nested data as expandable rows. It is a *hierarchy-first*
|
|
30
|
+
widget — reach for it when the structure is the point (file trees, outlines,
|
|
31
|
+
settings navigation, grouped pickers). For flat records with columns,
|
|
32
|
+
sorting, filtering, and paging, use `DataTable` instead.
|
|
33
|
+
|
|
34
|
+
Nodes are object handles: `add()` returns a `TreeNode` that you hold and
|
|
35
|
+
pass back to `expand()`, `select()`, `remove()`, and so on — there are no
|
|
36
|
+
string ids. Each node carries a `label` and an optional `icon` (with
|
|
37
|
+
optional open/closed variants), plus an open-ended `data` bag for your own
|
|
38
|
+
attributes.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
nodes: Declarative initial tree — a list of node specs, where each spec
|
|
42
|
+
is a label string or a dict like
|
|
43
|
+
`{"label": "src", "icon": "folder", "children": [...]}`. Anything
|
|
44
|
+
not a recognized display key becomes that node's `data`. Mutually
|
|
45
|
+
exclusive with `data_source`.
|
|
46
|
+
data_source: A flat data source to project as a hierarchy. Each record
|
|
47
|
+
carries a `parent_field` pointing at its parent's id (an adjacency
|
|
48
|
+
list); the tree loads one branch at a time, querying a node's
|
|
49
|
+
children only when it is expanded — so a huge hierarchy shows
|
|
50
|
+
instantly. Any data-source-protocol source works. Mutually exclusive
|
|
51
|
+
with `nodes`.
|
|
52
|
+
parent_field: With `data_source`, the record field that holds each row's
|
|
53
|
+
parent id. Defaults to `'parent_id'`.
|
|
54
|
+
root_value: With `data_source`, the `parent_field` value that marks a
|
|
55
|
+
root node. Defaults to `None` (a NULL/absent parent).
|
|
56
|
+
label_field: With `data_source`, the record field used as the node label.
|
|
57
|
+
Defaults to `'name'`. Ignored when `node_builder` is given.
|
|
58
|
+
icon_field: With `data_source`, an optional record field whose value is
|
|
59
|
+
used as the node icon.
|
|
60
|
+
node_builder: With `data_source`, an optional callable taking a record
|
|
61
|
+
and returning a node spec dict (`{'label': ..., 'icon': ...}`) for
|
|
62
|
+
full control over how a record renders. Overrides
|
|
63
|
+
`label_field`/`icon_field`.
|
|
64
|
+
order: With `data_source`, the ordering applied to each sibling group —
|
|
65
|
+
a sort key or sequence of keys (`'name'`, `'-created'`, a `col`,
|
|
66
|
+
or a `SortKey`). Defaults to the source's natural order.
|
|
67
|
+
selection_mode: Node selection behavior — `'single'` keeps one
|
|
68
|
+
highlighted node, `'multi'` is click-to-toggle a set, `'none'` does
|
|
69
|
+
no highlight selection (navigation/display only). Default `'single'`.
|
|
70
|
+
show_selection_controls: If `True`, show a per-node selection control
|
|
71
|
+
(a checkbox in `multi` mode, a radio in `single` mode) as the
|
|
72
|
+
visible affordance for selection — mirroring ListView and DataTable.
|
|
73
|
+
A selected row shows both the control and the highlight wash.
|
|
74
|
+
select_on_click: If `True` (default), clicking a row selects it. Set
|
|
75
|
+
`False` with `show_selection_controls=True` so that only the
|
|
76
|
+
control selects and a row click just focuses (e.g. to drive
|
|
77
|
+
`on_activate` for opening, VS Code style).
|
|
78
|
+
indent: Horizontal indent per depth level, in pixels. Defaults to `16`.
|
|
79
|
+
striped: If `True`, alternate the row background color.
|
|
80
|
+
show_scrollbar: If `True` (default), show the vertical scrollbar.
|
|
81
|
+
Mousewheel scrolling works regardless.
|
|
82
|
+
scrollbar_variant: Scrollbar style — `'thin'` (default) for a slim bar,
|
|
83
|
+
or `'default'` for the standard rounded bar.
|
|
84
|
+
height: Fixed height in pixels. When set, the tree maintains this
|
|
85
|
+
height regardless of its content (so it can scroll without the
|
|
86
|
+
parent layout providing a vertical constraint).
|
|
87
|
+
density: Row height. Default `'default'`.
|
|
88
|
+
accent: Color intent token for the selection highlight. Defaults to the
|
|
89
|
+
theme's default color.
|
|
90
|
+
parent: Override the context-stack parent.
|
|
91
|
+
**kwargs: Layout placement options applied by the parent container —
|
|
92
|
+
`fill`, `expand`, `anchor`, `margin`, `row`, `column`, `sticky`.
|
|
93
|
+
See :doc:`/tasks/layout`.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def __init__(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
nodes: list[str | dict] | None = None,
|
|
100
|
+
data_source: DataSourceProtocol | None = None,
|
|
101
|
+
parent_field: str = "parent_id",
|
|
102
|
+
root_value: Any = None,
|
|
103
|
+
label_field: str = "name",
|
|
104
|
+
icon_field: str | None = None,
|
|
105
|
+
node_builder: Callable[[dict], dict] | None = None,
|
|
106
|
+
order: str | Column | SortKey | Sequence[str | Column | SortKey] | None = None,
|
|
107
|
+
selection_mode: SelectionMode = "single",
|
|
108
|
+
show_selection_controls: bool = False,
|
|
109
|
+
select_on_click: bool = True,
|
|
110
|
+
indent: int = 16,
|
|
111
|
+
striped: bool = False,
|
|
112
|
+
show_scrollbar: bool = True,
|
|
113
|
+
scrollbar_variant: ScrollbarVariant = "thin",
|
|
114
|
+
height: int | None = None,
|
|
115
|
+
density: WidgetDensity = "default",
|
|
116
|
+
accent: AccentToken | str | None = None,
|
|
117
|
+
accent_selection: bool = True,
|
|
118
|
+
parent: Any = None,
|
|
119
|
+
**kwargs: Any,
|
|
120
|
+
) -> None:
|
|
121
|
+
self._parent = self._resolve_parent(parent)
|
|
122
|
+
self._selection_mode = selection_mode
|
|
123
|
+
layout_kw = self._split_layout_kwargs(kwargs)
|
|
124
|
+
tk_master = self._parent._child_master() if self._parent else None
|
|
125
|
+
|
|
126
|
+
internal_kwargs: dict[str, Any] = {
|
|
127
|
+
"selection_mode": selection_mode,
|
|
128
|
+
"show_selection_controls": show_selection_controls,
|
|
129
|
+
"select_on_click": select_on_click,
|
|
130
|
+
"indent": indent,
|
|
131
|
+
"striped": striped,
|
|
132
|
+
"scrollbar_visibility": "always" if show_scrollbar else "never",
|
|
133
|
+
"scrollbar_variant": scrollbar_variant,
|
|
134
|
+
"density": density,
|
|
135
|
+
"accent_selection": accent_selection,
|
|
136
|
+
}
|
|
137
|
+
if accent is not None:
|
|
138
|
+
internal_kwargs["accent"] = accent
|
|
139
|
+
|
|
140
|
+
if height is not None and "horizontal" not in layout_kw:
|
|
141
|
+
layout_kw["horizontal"] = "stretch"
|
|
142
|
+
|
|
143
|
+
self._internal = _InternalTreeView(tk_master, **internal_kwargs)
|
|
144
|
+
# Stop the row pool from feeding back into the layout (see ListView).
|
|
145
|
+
self._internal.pack_propagate(False)
|
|
146
|
+
self._attach_to_parent(layout_kw)
|
|
147
|
+
|
|
148
|
+
if height is not None:
|
|
149
|
+
self._internal.configure(height=height)
|
|
150
|
+
|
|
151
|
+
# Data-source backing: project a flat adjacency-list source as a lazy
|
|
152
|
+
# hierarchy. Mutually exclusive with declarative `nodes=`.
|
|
153
|
+
self._binding: Any = None
|
|
154
|
+
if data_source is not None:
|
|
155
|
+
if nodes:
|
|
156
|
+
raise ValueError(
|
|
157
|
+
"Tree: pass either nodes= or data_source=, not both."
|
|
158
|
+
)
|
|
159
|
+
from bootstack.widgets._impl.composites.tree.source_binding import (
|
|
160
|
+
SourceBinding,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
self._binding = SourceBinding(
|
|
164
|
+
data_source,
|
|
165
|
+
parent_field=parent_field,
|
|
166
|
+
root_value=root_value,
|
|
167
|
+
label_field=label_field,
|
|
168
|
+
icon_field=icon_field,
|
|
169
|
+
node_builder=node_builder,
|
|
170
|
+
order=order,
|
|
171
|
+
)
|
|
172
|
+
self._internal.load(self._binding.roots())
|
|
173
|
+
elif nodes:
|
|
174
|
+
self._internal.load(nodes)
|
|
175
|
+
|
|
176
|
+
# Per-node context menu (set via set_context_menu).
|
|
177
|
+
self._ctx_builder: Callable[[TreeNode, Any], Any] | None = None
|
|
178
|
+
self._ctx_menu: Any = None
|
|
179
|
+
self._ctx_sub: Subscription | None = None
|
|
180
|
+
|
|
181
|
+
# ----- building -----
|
|
182
|
+
|
|
183
|
+
def add(
|
|
184
|
+
self,
|
|
185
|
+
label: str = "",
|
|
186
|
+
*,
|
|
187
|
+
parent: TreeNode | None = None,
|
|
188
|
+
icon: str | None = None,
|
|
189
|
+
open_icon: str | None = None,
|
|
190
|
+
closed_icon: str | None = None,
|
|
191
|
+
expanded: bool = False,
|
|
192
|
+
children: list | None = None,
|
|
193
|
+
loader: Callable[[TreeNode], Any] | None = None,
|
|
194
|
+
data: dict | None = None,
|
|
195
|
+
**extra: Any,
|
|
196
|
+
) -> TreeNode:
|
|
197
|
+
"""Add a node and return its `TreeNode` handle.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
label: The node's display label.
|
|
201
|
+
parent: Parent node, or `None` for a root node.
|
|
202
|
+
icon: Bootstrap icon name shown before the label.
|
|
203
|
+
open_icon: Icon used when the node is expanded (overrides `icon`).
|
|
204
|
+
closed_icon: Icon used when the node is collapsed (overrides `icon`).
|
|
205
|
+
expanded: Whether the node starts expanded.
|
|
206
|
+
children: Optional child specs (same format as `nodes=`).
|
|
207
|
+
loader: Callable invoked on first expand to fetch children lazily.
|
|
208
|
+
Receives the node; returns an iterable of child specs.
|
|
209
|
+
data: Initial data bag for the node.
|
|
210
|
+
**extra: Extra keywords folded into the node's `data` bag.
|
|
211
|
+
"""
|
|
212
|
+
return self._internal.add(
|
|
213
|
+
label, parent=parent, icon=icon, open_icon=open_icon,
|
|
214
|
+
closed_icon=closed_icon, expanded=expanded, children=children,
|
|
215
|
+
loader=loader, data=data, **extra,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def insert(self, index: int, label: str = "", *, parent: TreeNode | None = None,
|
|
219
|
+
**kwargs: Any) -> TreeNode:
|
|
220
|
+
"""Add a node at a specific position among its siblings.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
index: Zero-based position among siblings.
|
|
224
|
+
label: The node's display label.
|
|
225
|
+
parent: Parent node, or `None` for a root node.
|
|
226
|
+
**kwargs: Same as `add()`.
|
|
227
|
+
"""
|
|
228
|
+
return self._internal.add(label, parent=parent, index=index, **kwargs)
|
|
229
|
+
|
|
230
|
+
def remove(self, node: TreeNode) -> None:
|
|
231
|
+
"""Remove a node (and its descendants).
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
node: The node to remove.
|
|
235
|
+
"""
|
|
236
|
+
self._internal.remove(node)
|
|
237
|
+
|
|
238
|
+
def move(self, node: TreeNode, parent: TreeNode | None = None,
|
|
239
|
+
index: int | str = "end") -> None:
|
|
240
|
+
"""Move a node to a new parent and/or position.
|
|
241
|
+
|
|
242
|
+
Args:
|
|
243
|
+
node: The node to move.
|
|
244
|
+
parent: New parent, or `None` for the root level.
|
|
245
|
+
index: Position among the new siblings — `'end'` or an integer.
|
|
246
|
+
"""
|
|
247
|
+
self._internal.move(node, parent, index)
|
|
248
|
+
|
|
249
|
+
def clear(self) -> None:
|
|
250
|
+
"""Remove all nodes."""
|
|
251
|
+
self._internal.clear()
|
|
252
|
+
|
|
253
|
+
@property
|
|
254
|
+
def data_source(self) -> DataSourceProtocol | None:
|
|
255
|
+
"""The backing data source, or `None` for a declarative tree."""
|
|
256
|
+
return self._binding.source if self._binding is not None else None
|
|
257
|
+
|
|
258
|
+
def refresh(self) -> None:
|
|
259
|
+
"""Reload a data-source-backed tree from its source.
|
|
260
|
+
|
|
261
|
+
Re-queries the roots and discards loaded branches, so the tree reflects
|
|
262
|
+
records that were inserted, updated, or deleted in the source. Selection
|
|
263
|
+
and expansion state are reset. A no-op for a declarative tree.
|
|
264
|
+
"""
|
|
265
|
+
if self._binding is None:
|
|
266
|
+
return
|
|
267
|
+
self._internal.load(self._binding.roots())
|
|
268
|
+
|
|
269
|
+
# ----- navigation / lookup -----
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def roots(self) -> list[TreeNode]:
|
|
273
|
+
"""The top-level nodes, in order.
|
|
274
|
+
|
|
275
|
+
Useful for reaching handles after declarative construction
|
|
276
|
+
(`Tree(nodes=...)`), which otherwise returns no `TreeNode` handles.
|
|
277
|
+
"""
|
|
278
|
+
return list(self._internal.roots)
|
|
279
|
+
|
|
280
|
+
def walk(self) -> Iterator[TreeNode]:
|
|
281
|
+
"""Yield every node depth-first, including collapsed ones."""
|
|
282
|
+
return self._internal._iter_all_nodes()
|
|
283
|
+
|
|
284
|
+
def find(self, matcher: "Callable[[TreeNode], bool] | Condition") -> TreeNode | None:
|
|
285
|
+
"""Return the first node matching `matcher`, or `None`.
|
|
286
|
+
|
|
287
|
+
`matcher` is either a predicate `(TreeNode) -> bool` or a `col(...)`
|
|
288
|
+
condition (see :doc:`/reference/data-sources`):
|
|
289
|
+
|
|
290
|
+
- A **predicate** is evaluated over the materialized nodes. For a
|
|
291
|
+
data-source-backed tree that means the currently loaded branches only
|
|
292
|
+
— a Python callable cannot be pushed down to the source.
|
|
293
|
+
- A **condition** is matched against each node's `data`. On a
|
|
294
|
+
declarative tree this scans every node; on a data-source-backed tree
|
|
295
|
+
it is pushed down to the source, so it reaches nodes in unexpanded
|
|
296
|
+
branches, and the path to the match is loaded (without expanding it)
|
|
297
|
+
so a real `TreeNode` handle is returned. Call `reveal()` on the result
|
|
298
|
+
to scroll it into view.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
matcher: A `(TreeNode) -> bool` predicate, or a `col(...)` condition.
|
|
302
|
+
"""
|
|
303
|
+
results = self._find(matcher, first=True)
|
|
304
|
+
return results[0] if results else None
|
|
305
|
+
|
|
306
|
+
def find_all(self, matcher: "Callable[[TreeNode], bool] | Condition") -> list[TreeNode]:
|
|
307
|
+
"""Return every node matching `matcher`.
|
|
308
|
+
|
|
309
|
+
Accepts the same predicate-or-condition `matcher` as `find()`, with the
|
|
310
|
+
same reach (a predicate sees only materialized nodes; a condition is
|
|
311
|
+
pushed down on a data-source-backed tree). Predicate and declarative
|
|
312
|
+
matches come back in tree order; pushed-down condition matches come back
|
|
313
|
+
in the source's order.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
matcher: A `(TreeNode) -> bool` predicate, or a `col(...)` condition.
|
|
317
|
+
"""
|
|
318
|
+
return self._find(matcher, first=False)
|
|
319
|
+
|
|
320
|
+
def _find(self, matcher: Any, *, first: bool) -> list[TreeNode]:
|
|
321
|
+
from bootstack.data.query import Condition
|
|
322
|
+
|
|
323
|
+
# Source-backed tree + condition: push down to reach unloaded branches.
|
|
324
|
+
if isinstance(matcher, Condition) and self._binding is not None:
|
|
325
|
+
records = self._binding.source._query(matcher, self._binding.sort_keys)
|
|
326
|
+
out: list[TreeNode] = []
|
|
327
|
+
for record in records:
|
|
328
|
+
node = self._materialize_path(record)
|
|
329
|
+
if node is not None:
|
|
330
|
+
out.append(node)
|
|
331
|
+
if first:
|
|
332
|
+
break
|
|
333
|
+
return out
|
|
334
|
+
|
|
335
|
+
# Otherwise evaluate over the materialized nodes.
|
|
336
|
+
if isinstance(matcher, Condition):
|
|
337
|
+
predicate: Callable[[TreeNode], bool] = lambda n: matcher.matches(n.data)
|
|
338
|
+
else:
|
|
339
|
+
predicate = matcher
|
|
340
|
+
out = []
|
|
341
|
+
for node in self._internal._iter_all_nodes():
|
|
342
|
+
if predicate(node):
|
|
343
|
+
out.append(node)
|
|
344
|
+
if first:
|
|
345
|
+
break
|
|
346
|
+
return out
|
|
347
|
+
|
|
348
|
+
def _materialize_path(self, record: dict) -> TreeNode | None:
|
|
349
|
+
"""Load (without expanding) the path from a root to `record`'s node.
|
|
350
|
+
|
|
351
|
+
Walks `parent_field` up the source to build the root→record id chain,
|
|
352
|
+
then descends the view, loading each level's lazy children as needed,
|
|
353
|
+
and returns the matching `TreeNode` (or `None` if the chain can't be
|
|
354
|
+
resolved — e.g. a broken adjacency link or a custom `node_builder` whose
|
|
355
|
+
`data` omits the id).
|
|
356
|
+
"""
|
|
357
|
+
b = self._binding
|
|
358
|
+
src = b.source
|
|
359
|
+
|
|
360
|
+
def ident(rec: Any) -> Any:
|
|
361
|
+
# Normalize identity across raw records (where `_record_id` reads the
|
|
362
|
+
# source's identity column, e.g. SQLite's internal row id) and public
|
|
363
|
+
# node `data` (where the identity is surfaced as `'id'`).
|
|
364
|
+
rid = src._record_id(rec)
|
|
365
|
+
return _node_key(rid if rid is not None else rec.get("id"))
|
|
366
|
+
|
|
367
|
+
# Build the root→record chain by walking parent ids up the source. A
|
|
368
|
+
# row's `parent_field` holds the parent's identity, so fetch each parent
|
|
369
|
+
# by id via `get()` (handles sources whose identity isn't a plain column).
|
|
370
|
+
chain = [record]
|
|
371
|
+
cur = record
|
|
372
|
+
seen: set = set()
|
|
373
|
+
while _node_key(cur.get(b.parent_field)) != _node_key(b.root_value):
|
|
374
|
+
pid = cur.get(b.parent_field)
|
|
375
|
+
key = _node_key(pid)
|
|
376
|
+
if key in seen: # guard a cyclic/broken adjacency list
|
|
377
|
+
break
|
|
378
|
+
seen.add(key)
|
|
379
|
+
parent = src.get(pid)
|
|
380
|
+
if parent is None:
|
|
381
|
+
break
|
|
382
|
+
cur = parent
|
|
383
|
+
chain.append(cur)
|
|
384
|
+
chain.reverse()
|
|
385
|
+
|
|
386
|
+
# Descend the view, loading each level's children as needed.
|
|
387
|
+
level = list(self._internal.roots)
|
|
388
|
+
node: TreeNode | None = None
|
|
389
|
+
for i, rec in enumerate(chain):
|
|
390
|
+
rid = ident(rec)
|
|
391
|
+
node = next((n for n in level if ident(n.data) == rid), None)
|
|
392
|
+
if node is None:
|
|
393
|
+
return None
|
|
394
|
+
if i < len(chain) - 1: # not the target yet — ensure children exist
|
|
395
|
+
if node.loader is not None and not node._loaded:
|
|
396
|
+
self._internal._load_children(node)
|
|
397
|
+
level = node.children
|
|
398
|
+
return node
|
|
399
|
+
|
|
400
|
+
# ----- expansion -----
|
|
401
|
+
|
|
402
|
+
def expand(self, node: TreeNode) -> None:
|
|
403
|
+
"""Expand a node to reveal its children."""
|
|
404
|
+
self._internal.expand(node)
|
|
405
|
+
|
|
406
|
+
def collapse(self, node: TreeNode) -> None:
|
|
407
|
+
"""Collapse a node to hide its children."""
|
|
408
|
+
self._internal.collapse(node)
|
|
409
|
+
|
|
410
|
+
def toggle(self, node: TreeNode) -> None:
|
|
411
|
+
"""Expand the node if collapsed, or collapse it if expanded."""
|
|
412
|
+
self._internal.toggle(node)
|
|
413
|
+
|
|
414
|
+
def expand_all(self) -> None:
|
|
415
|
+
"""Expand every node (loading lazy children as needed)."""
|
|
416
|
+
self._internal.expand_all()
|
|
417
|
+
|
|
418
|
+
def collapse_all(self) -> None:
|
|
419
|
+
"""Collapse every node."""
|
|
420
|
+
self._internal.collapse_all()
|
|
421
|
+
|
|
422
|
+
def reveal(self, node: TreeNode) -> None:
|
|
423
|
+
"""Expand ancestors and scroll so `node` is visible."""
|
|
424
|
+
self._internal.reveal(node)
|
|
425
|
+
|
|
426
|
+
def reload_children(self, node: TreeNode) -> None:
|
|
427
|
+
"""Refresh a lazy node's children — drop them and re-fetch via its
|
|
428
|
+
`loader`. If `node` is expanded the children reload immediately;
|
|
429
|
+
otherwise they reload on the next expand. A no-op for non-lazy nodes.
|
|
430
|
+
|
|
431
|
+
Args:
|
|
432
|
+
node: The lazy node (created with `loader=`) to refresh.
|
|
433
|
+
"""
|
|
434
|
+
self._internal.reload_children(node)
|
|
435
|
+
|
|
436
|
+
# ----- selection -----
|
|
437
|
+
|
|
438
|
+
@property
|
|
439
|
+
def selection(self) -> TreeNode | list[TreeNode] | None:
|
|
440
|
+
"""The selected node(s) — node handles, in tree order.
|
|
441
|
+
|
|
442
|
+
In `'single'` mode, the selected :class:`TreeNode` (or `None` when
|
|
443
|
+
nothing is selected). In `'multi'` mode, a `list` of nodes (empty when
|
|
444
|
+
nothing is selected). Each node's data bag is at `node.data`. Read-only.
|
|
445
|
+
"""
|
|
446
|
+
nodes = self._internal.get_selected()
|
|
447
|
+
if self._selection_mode == "multi":
|
|
448
|
+
return nodes
|
|
449
|
+
return nodes[0] if nodes else None
|
|
450
|
+
|
|
451
|
+
def select(self, node: TreeNode) -> None:
|
|
452
|
+
"""Select a node (replaces in single mode, adds in multi)."""
|
|
453
|
+
self._internal.select(node)
|
|
454
|
+
|
|
455
|
+
def deselect(self, node: TreeNode) -> None:
|
|
456
|
+
"""Remove a node from the selection."""
|
|
457
|
+
self._internal.deselect(node)
|
|
458
|
+
|
|
459
|
+
def select_all(self) -> None:
|
|
460
|
+
"""Select every node. Only effective when `selection_mode='multi'`."""
|
|
461
|
+
self._internal.select_all()
|
|
462
|
+
|
|
463
|
+
def clear_selection(self) -> None:
|
|
464
|
+
"""Clear the selection."""
|
|
465
|
+
self._internal.clear_selection()
|
|
466
|
+
|
|
467
|
+
# ----- events -----
|
|
468
|
+
|
|
469
|
+
@overload
|
|
470
|
+
def on_select(self) -> Stream: ...
|
|
471
|
+
@overload
|
|
472
|
+
def on_select(self, handler: Callable[[TreeSelectionEvent], Any]) -> Subscription: ...
|
|
473
|
+
def on_select(self, handler: Callable[[TreeSelectionEvent], Any] | None = None) -> Stream | Subscription:
|
|
474
|
+
"""Fired when the set of selected nodes changes.
|
|
475
|
+
|
|
476
|
+
Args:
|
|
477
|
+
handler: Called with a :class:`~bootstack.events.TreeSelectionEvent`
|
|
478
|
+
carrying `nodes` (the full selection, in tree order). Omit to get
|
|
479
|
+
a composable :class:`~bootstack.streams.Stream` instead.
|
|
480
|
+
|
|
481
|
+
Returns:
|
|
482
|
+
A cancellable :class:`~bootstack.events.Subscription` when a handler
|
|
483
|
+
is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
484
|
+
"""
|
|
485
|
+
return self.on("select", handler)
|
|
486
|
+
|
|
487
|
+
@overload
|
|
488
|
+
def on_activate(self) -> Stream: ...
|
|
489
|
+
@overload
|
|
490
|
+
def on_activate(self, handler: Callable[[TreeNode], Any]) -> Subscription: ...
|
|
491
|
+
def on_activate(self, handler: Callable[[TreeNode], Any] | None = None) -> Stream | Subscription:
|
|
492
|
+
"""Fired when a node is activated (double-click or Enter).
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
handler: Called with the activated `TreeNode`. Omit to get a
|
|
496
|
+
composable :class:`~bootstack.streams.Stream` instead.
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
A cancellable :class:`~bootstack.events.Subscription` when a handler
|
|
500
|
+
is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
501
|
+
"""
|
|
502
|
+
return self.on("activate", handler)
|
|
503
|
+
|
|
504
|
+
@overload
|
|
505
|
+
def on_expand(self) -> Stream: ...
|
|
506
|
+
@overload
|
|
507
|
+
def on_expand(self, handler: Callable[[TreeNode], Any]) -> Subscription: ...
|
|
508
|
+
def on_expand(self, handler: Callable[[TreeNode], Any] | None = None) -> Stream | Subscription:
|
|
509
|
+
"""Fired when a node is expanded.
|
|
510
|
+
|
|
511
|
+
Args:
|
|
512
|
+
handler: Called with the expanded `TreeNode`. Omit to get a
|
|
513
|
+
composable :class:`~bootstack.streams.Stream` instead.
|
|
514
|
+
|
|
515
|
+
Returns:
|
|
516
|
+
A cancellable :class:`~bootstack.events.Subscription` when a handler
|
|
517
|
+
is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
518
|
+
"""
|
|
519
|
+
return self.on("expand", handler)
|
|
520
|
+
|
|
521
|
+
@overload
|
|
522
|
+
def on_collapse(self) -> Stream: ...
|
|
523
|
+
@overload
|
|
524
|
+
def on_collapse(self, handler: Callable[[TreeNode], Any]) -> Subscription: ...
|
|
525
|
+
def on_collapse(self, handler: Callable[[TreeNode], Any] | None = None) -> Stream | Subscription:
|
|
526
|
+
"""Fired when a node is collapsed.
|
|
527
|
+
|
|
528
|
+
Args:
|
|
529
|
+
handler: Called with the collapsed `TreeNode`. Omit to get a
|
|
530
|
+
composable :class:`~bootstack.streams.Stream` instead.
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
A cancellable :class:`~bootstack.events.Subscription` when a handler
|
|
534
|
+
is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
535
|
+
"""
|
|
536
|
+
return self.on("collapse", handler)
|
|
537
|
+
|
|
538
|
+
@overload
|
|
539
|
+
def on_right_click(self) -> Stream: ...
|
|
540
|
+
@overload
|
|
541
|
+
def on_right_click(self, handler: Callable[[dict[str, Any]], Any]) -> Subscription: ...
|
|
542
|
+
def on_right_click(self, handler: Callable[[dict[str, Any]], Any] | None = None) -> Stream | Subscription:
|
|
543
|
+
"""Fired on a right-click.
|
|
544
|
+
|
|
545
|
+
Args:
|
|
546
|
+
handler: Called with a `dict` carrying `node`, `x_root`, and
|
|
547
|
+
`y_root` — enough to position a context menu. Omit to get a
|
|
548
|
+
composable :class:`~bootstack.streams.Stream` instead.
|
|
549
|
+
|
|
550
|
+
Returns:
|
|
551
|
+
A cancellable :class:`~bootstack.events.Subscription` when a handler
|
|
552
|
+
is given, otherwise a :class:`~bootstack.streams.Stream`.
|
|
553
|
+
"""
|
|
554
|
+
return self.on("right_click", handler)
|
|
555
|
+
|
|
556
|
+
# ----- context menu -----
|
|
557
|
+
|
|
558
|
+
def set_context_menu(
|
|
559
|
+
self,
|
|
560
|
+
builder: Callable[[TreeNode, Any], Any] | None,
|
|
561
|
+
*,
|
|
562
|
+
min_width: int = 150,
|
|
563
|
+
density: WidgetDensity = "default",
|
|
564
|
+
) -> None:
|
|
565
|
+
"""Attach a per-node context menu.
|
|
566
|
+
|
|
567
|
+
On right-click, `builder(node, menu)` is called with the right-clicked
|
|
568
|
+
`TreeNode` and a fresh (emptied) `ContextMenu`; populate it with
|
|
569
|
+
`menu.add_item(...)` / `add_check_item(...)` / `add_separator()` etc.
|
|
570
|
+
(capture `node` in your callbacks). The menu is then shown at the cursor.
|
|
571
|
+
Because it is rebuilt on every right-click, the items can depend on the
|
|
572
|
+
node. If the builder adds no items, nothing is shown. Pass `None` to
|
|
573
|
+
remove the menu.
|
|
574
|
+
|
|
575
|
+
Example::
|
|
576
|
+
|
|
577
|
+
def build(node, menu):
|
|
578
|
+
menu.add_item("Rename", on_click=lambda: rename(node))
|
|
579
|
+
menu.add_item("Delete", icon="trash", on_click=lambda: tree.remove(node))
|
|
580
|
+
tree.set_context_menu(build)
|
|
581
|
+
|
|
582
|
+
Args:
|
|
583
|
+
builder: Callable taking `(node, menu)` and populating `menu`, or
|
|
584
|
+
`None` to detach.
|
|
585
|
+
min_width: Minimum menu width in pixels.
|
|
586
|
+
density: Menu item density — `'default'` or `'compact'`.
|
|
587
|
+
"""
|
|
588
|
+
self._ctx_builder = builder
|
|
589
|
+
if builder is None:
|
|
590
|
+
if self._ctx_sub is not None:
|
|
591
|
+
self._ctx_sub.cancel()
|
|
592
|
+
self._ctx_sub = None
|
|
593
|
+
return
|
|
594
|
+
# Menu-level options changed? Drop the cached menu so it's rebuilt.
|
|
595
|
+
opts = (min_width, density)
|
|
596
|
+
if getattr(self, "_ctx_opts", None) != opts:
|
|
597
|
+
self._ctx_opts = opts
|
|
598
|
+
if self._ctx_menu is not None:
|
|
599
|
+
self._ctx_menu.destroy()
|
|
600
|
+
self._ctx_menu = None
|
|
601
|
+
if self._ctx_sub is None:
|
|
602
|
+
self._ctx_sub = self.on_right_click(self._show_context_menu)
|
|
603
|
+
|
|
604
|
+
def _show_context_menu(self, data: dict[str, Any]) -> None:
|
|
605
|
+
node = data.get("node")
|
|
606
|
+
if node is None or self._ctx_builder is None:
|
|
607
|
+
return
|
|
608
|
+
from bootstack.widgets.contextmenu import ContextMenu
|
|
609
|
+
# Reuse one menu, cleared each time, so repeated right-clicks don't
|
|
610
|
+
# accumulate popups.
|
|
611
|
+
if self._ctx_menu is None:
|
|
612
|
+
min_width, density = getattr(self, "_ctx_opts", (150, "default"))
|
|
613
|
+
self._ctx_menu = ContextMenu(
|
|
614
|
+
parent=self, trigger=None, min_width=min_width, density=density
|
|
615
|
+
)
|
|
616
|
+
menu = self._ctx_menu
|
|
617
|
+
for key in list(menu.keys):
|
|
618
|
+
menu.remove_item(key)
|
|
619
|
+
self._ctx_builder(node, menu)
|
|
620
|
+
if menu.keys: # only show if the builder added something
|
|
621
|
+
menu.show((data.get("x_root", 0), data.get("y_root", 0)))
|
|
622
|
+
|
|
623
|
+
# ----- lifecycle -----
|
|
624
|
+
|
|
625
|
+
def destroy(self) -> None:
|
|
626
|
+
"""Detach the context menu and destroy the widget (cancels pending
|
|
627
|
+
relayout/type-ahead callbacks on the internal view)."""
|
|
628
|
+
if self._ctx_sub is not None:
|
|
629
|
+
self._ctx_sub.cancel()
|
|
630
|
+
self._ctx_sub = None
|
|
631
|
+
if self._ctx_menu is not None:
|
|
632
|
+
try:
|
|
633
|
+
self._ctx_menu.destroy()
|
|
634
|
+
except Exception:
|
|
635
|
+
pass
|
|
636
|
+
self._ctx_menu = None
|
|
637
|
+
self._internal.destroy()
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
_TREE_EVENTS: dict[str, str] = {
|
|
641
|
+
"select": "<<TreeSelectionChange>>",
|
|
642
|
+
"activate": "<<TreeActivate>>",
|
|
643
|
+
"expand": "<<TreeExpand>>",
|
|
644
|
+
"collapse": "<<TreeCollapse>>",
|
|
645
|
+
"right_click": "<<TreeRightClick>>",
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
register_widget_events(Tree, _TREE_EVENTS)
|