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,393 @@
|
|
|
1
|
+
"""TreeItem — a single recycled row in the Tree's virtual view.
|
|
2
|
+
|
|
3
|
+
Mirrors `ListItem` (a `CompositeFrame` composing on-demand child widgets with
|
|
4
|
+
synchronized hover/focus/selected state) and adds the two things a tree row
|
|
5
|
+
needs that a flat list row does not: a depth indent and an expander chevron.
|
|
6
|
+
|
|
7
|
+
A row is recycled: `update_data(record)` re-points it at whichever visible node
|
|
8
|
+
currently occupies its slot. The record is built by the internal `TreeView`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from tkinter import TclError
|
|
14
|
+
from typing import Any, Optional
|
|
15
|
+
|
|
16
|
+
from bootstack.widgets._impl.composites.compositeframe import CompositeFrame
|
|
17
|
+
from bootstack.widgets._impl.primitives.button import Button
|
|
18
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
19
|
+
from bootstack.widgets._impl.primitives.label import Label
|
|
20
|
+
from bootstack.widgets._impl.primitives.separator import Separator
|
|
21
|
+
from bootstack.widgets.types import Master
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Empty-slot sentinel (a record with this key hides the row).
|
|
25
|
+
EMPTY = {"__empty__": True}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TreeItem(CompositeFrame):
|
|
29
|
+
"""A recycled tree row: indent, chevron, selection control, icon, label."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
master: Master = None,
|
|
34
|
+
*,
|
|
35
|
+
indent: int = 16,
|
|
36
|
+
chevron_width: int = 26,
|
|
37
|
+
focusable: bool = True,
|
|
38
|
+
hoverable: bool = True,
|
|
39
|
+
selection_mode: str = "single",
|
|
40
|
+
show_selection_controls: bool = False,
|
|
41
|
+
select_on_click: bool = True,
|
|
42
|
+
show_separator: bool = False,
|
|
43
|
+
density: str = "default",
|
|
44
|
+
accent: Optional[str] = None,
|
|
45
|
+
accent_selection: bool = False,
|
|
46
|
+
) -> None:
|
|
47
|
+
self._data: dict = {}
|
|
48
|
+
self._state: dict = {}
|
|
49
|
+
self._node = None
|
|
50
|
+
self._item_index = 0
|
|
51
|
+
|
|
52
|
+
self._indent = indent
|
|
53
|
+
self._chevron_width = chevron_width
|
|
54
|
+
self._focusable = focusable
|
|
55
|
+
self._hoverable = hoverable
|
|
56
|
+
self._selection_mode = selection_mode
|
|
57
|
+
self._show_selection_controls = show_selection_controls
|
|
58
|
+
self._select_on_click = select_on_click
|
|
59
|
+
self._show_separator = show_separator
|
|
60
|
+
self._density = density
|
|
61
|
+
self._accent = accent or "primary"
|
|
62
|
+
# Selected rows always show the wash, even alongside a selection control
|
|
63
|
+
# (checkbox/radio): wash + control, consistent with DataTable.
|
|
64
|
+
self._wash = True
|
|
65
|
+
# Neutral selection wash by default; opt in to an accent-tinted wash.
|
|
66
|
+
self._accent_selection = accent_selection
|
|
67
|
+
|
|
68
|
+
item_padding = (6, 3) if density == "compact" else (8, 4)
|
|
69
|
+
|
|
70
|
+
super().__init__(
|
|
71
|
+
master=master,
|
|
72
|
+
select_on_click=False,
|
|
73
|
+
variant="separated_item" if show_separator else "item",
|
|
74
|
+
takefocus=focusable,
|
|
75
|
+
ttk_class="ListView.TFrame",
|
|
76
|
+
padding=item_padding,
|
|
77
|
+
accent=self._accent,
|
|
78
|
+
style_options=dict(hoverable=hoverable, density=density, wash=self._wash,
|
|
79
|
+
accent_selection=accent_selection),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
style_opts = dict(hoverable=hoverable, density=density, wash=self._wash,
|
|
83
|
+
accent_selection=accent_selection)
|
|
84
|
+
|
|
85
|
+
# Left region: [<indent padx> chevron-slot][checkbox?][icon]
|
|
86
|
+
self._left_frame = Frame(
|
|
87
|
+
self, variant="list", ttk_class="ListView.TFrame",
|
|
88
|
+
takefocus=False, accent=self._accent, style_options=style_opts,
|
|
89
|
+
)
|
|
90
|
+
self._left_frame.pack(side="left", fill="y")
|
|
91
|
+
|
|
92
|
+
self._center_frame = Frame(
|
|
93
|
+
self, variant="list", ttk_class="ListView.TFrame",
|
|
94
|
+
takefocus=False, accent=self._accent, style_options=style_opts,
|
|
95
|
+
)
|
|
96
|
+
self._center_frame.pack(side="left", fill="x", expand=True)
|
|
97
|
+
|
|
98
|
+
# Fixed-width chevron slot. The chevron button is PLACED inside it (place
|
|
99
|
+
# children don't influence the slot's requested width), so the gutter is
|
|
100
|
+
# exactly `chevron_width` whether or not a chevron is shown — leaf and
|
|
101
|
+
# parent rows at the same depth line up perfectly. The depth indent is
|
|
102
|
+
# applied as LEFT PADDING on this slot via pack_configure: a ttk frame
|
|
103
|
+
# won't shrink its -width back down once set, so a width-resized spacer
|
|
104
|
+
# goes stale on recycle; padx is reliable and the gap shows the left
|
|
105
|
+
# frame's background (which already follows the row's wash).
|
|
106
|
+
self._chevron_slot = Frame(
|
|
107
|
+
self._left_frame, variant="list", ttk_class="ListView.TFrame",
|
|
108
|
+
takefocus=False, accent=self._accent, style_options=style_opts,
|
|
109
|
+
width=chevron_width,
|
|
110
|
+
)
|
|
111
|
+
self._chevron_slot.pack(side="left", fill="y")
|
|
112
|
+
self._chevron_btn: Optional[Button] = None
|
|
113
|
+
|
|
114
|
+
self.register_composite(self._chevron_slot)
|
|
115
|
+
|
|
116
|
+
self._select_ctrl: Optional[Label] = None
|
|
117
|
+
self._icon_widget: Optional[Label] = None
|
|
118
|
+
self._label_widget: Optional[Label] = None
|
|
119
|
+
|
|
120
|
+
self._separator = Separator(self)
|
|
121
|
+
|
|
122
|
+
for w in (self._left_frame, self._center_frame):
|
|
123
|
+
self.register_composite(w)
|
|
124
|
+
|
|
125
|
+
# Selection control (checkbox for multi, radio for single) — the visible
|
|
126
|
+
# affordance for selection, the same 'selection' variant ListView uses.
|
|
127
|
+
# Its fill is driven by the row's 'selected' state via the coordinator;
|
|
128
|
+
# the 'alternate' state shows a partially-selected (mixed) parent.
|
|
129
|
+
if self._show_selection_controls and self._selection_mode != "none":
|
|
130
|
+
if self._selection_mode == "multi":
|
|
131
|
+
glyph = dict(name="square", state=[
|
|
132
|
+
("selected", "check-square-fill"),
|
|
133
|
+
("alternate", "dash-square-fill"),
|
|
134
|
+
])
|
|
135
|
+
else:
|
|
136
|
+
glyph = dict(name="circle", state=[("selected", "check-circle-fill")])
|
|
137
|
+
self._select_ctrl = Label(
|
|
138
|
+
self._left_frame, icon=glyph, variant="selection",
|
|
139
|
+
ttk_class="ListView.TLabel", icon_only=True, accent=self._accent,
|
|
140
|
+
takefocus=False, style_options=self._style_opts(),
|
|
141
|
+
)
|
|
142
|
+
self._select_ctrl.pack(side="left", after=self._chevron_slot, padx=(0, 4))
|
|
143
|
+
self.register_composite(self._select_ctrl)
|
|
144
|
+
# When the row itself doesn't select on click, the control still must
|
|
145
|
+
# — give it its own click path (the row's invoke is gated below).
|
|
146
|
+
if not self._select_on_click:
|
|
147
|
+
self._select_ctrl.bind("<Button-1>", self._on_ctrl_click, add="+")
|
|
148
|
+
|
|
149
|
+
# Row interactions. Double-/right-click must be bound on the leaf widgets
|
|
150
|
+
# too — a click lands on the label/icon, not the row frame, and Tk does
|
|
151
|
+
# not propagate it to the parent's bindings.
|
|
152
|
+
if self._focusable:
|
|
153
|
+
self.on_invoked(self._on_invoke)
|
|
154
|
+
self.bind("<FocusIn>", self._on_focus_in, add="+")
|
|
155
|
+
for w in (self, self._left_frame, self._center_frame,
|
|
156
|
+
self._chevron_slot):
|
|
157
|
+
self._bind_row_mouse(w)
|
|
158
|
+
|
|
159
|
+
# ----- helpers -----
|
|
160
|
+
|
|
161
|
+
def _style_opts(self) -> dict:
|
|
162
|
+
return dict(hoverable=self._hoverable, density=self._density, wash=self._wash,
|
|
163
|
+
accent_selection=self._accent_selection)
|
|
164
|
+
|
|
165
|
+
def _bind_row_mouse(self, widget) -> None:
|
|
166
|
+
"""Bind double-click (activate) and right-click on a row-level widget."""
|
|
167
|
+
widget.bind("<Double-Button-1>", self._on_activate, add="+")
|
|
168
|
+
widget.bind("<Button-3>", self._on_right_click, add="+")
|
|
169
|
+
widget.bind("<Button-2>", self._on_right_click, add="+") # mac
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def node(self):
|
|
173
|
+
"""The TreeNode currently rendered in this row (or None)."""
|
|
174
|
+
return self._node
|
|
175
|
+
|
|
176
|
+
# ----- row -> container events -----
|
|
177
|
+
|
|
178
|
+
def _emit(self, sequence: str, data: Any) -> None:
|
|
179
|
+
try:
|
|
180
|
+
self.master.event_generate(sequence, data=data)
|
|
181
|
+
except TclError:
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
def _on_invoke(self, event) -> None:
|
|
185
|
+
if self._node is None:
|
|
186
|
+
return
|
|
187
|
+
if self._focusable:
|
|
188
|
+
self.focus()
|
|
189
|
+
# When select_on_click is off, a row click only focuses (the selection
|
|
190
|
+
# control is the sole way to select). The control routes through
|
|
191
|
+
# _on_ctrl_click in that case.
|
|
192
|
+
if self._select_on_click:
|
|
193
|
+
self._emit("<<TreeItemSelect>>", self._node)
|
|
194
|
+
|
|
195
|
+
def _on_ctrl_click(self, event=None) -> str:
|
|
196
|
+
if self._node is not None:
|
|
197
|
+
if self._focusable:
|
|
198
|
+
self.focus()
|
|
199
|
+
self._emit("<<TreeItemSelect>>", self._node)
|
|
200
|
+
return "break"
|
|
201
|
+
|
|
202
|
+
def _on_focus_in(self, event) -> None:
|
|
203
|
+
if self._node is not None:
|
|
204
|
+
self._emit("<<TreeItemFocus>>", self._node)
|
|
205
|
+
|
|
206
|
+
def _on_activate(self, event=None) -> str:
|
|
207
|
+
if self._node is not None:
|
|
208
|
+
self._emit("<<TreeItemActivate>>", self._node)
|
|
209
|
+
return "break"
|
|
210
|
+
|
|
211
|
+
def _on_right_click(self, event) -> str:
|
|
212
|
+
if self._node is not None:
|
|
213
|
+
self._emit(
|
|
214
|
+
"<<TreeItemRightClick>>",
|
|
215
|
+
{"node": self._node, "x_root": event.x_root, "y_root": event.y_root},
|
|
216
|
+
)
|
|
217
|
+
return "break"
|
|
218
|
+
|
|
219
|
+
def _on_chevron(self) -> None:
|
|
220
|
+
if self._node is not None:
|
|
221
|
+
self._emit("<<TreeItemToggle>>", self._node)
|
|
222
|
+
|
|
223
|
+
# ----- widget updates -----
|
|
224
|
+
|
|
225
|
+
def _update_chevron(self, expandable: bool, expanded: bool) -> None:
|
|
226
|
+
if expandable:
|
|
227
|
+
icon = "chevron-down" if expanded else "chevron-right"
|
|
228
|
+
if self._chevron_btn is None:
|
|
229
|
+
self._chevron_btn = Button(
|
|
230
|
+
self._chevron_slot, icon=icon, icon_only=True, variant="icon",
|
|
231
|
+
ttk_class="ListView.TButton", takefocus=False,
|
|
232
|
+
accent=self._accent, command=self._on_chevron,
|
|
233
|
+
style_options=self._style_opts(),
|
|
234
|
+
)
|
|
235
|
+
self.register_composite(self._chevron_btn)
|
|
236
|
+
else:
|
|
237
|
+
self._chevron_btn.configure(icon=icon)
|
|
238
|
+
if not self._chevron_btn.winfo_manager():
|
|
239
|
+
self._chevron_btn.place(relx=0.5, rely=0.5, anchor="center")
|
|
240
|
+
else:
|
|
241
|
+
if self._chevron_btn is not None:
|
|
242
|
+
self._chevron_btn.place_forget()
|
|
243
|
+
|
|
244
|
+
def set_mixed(self, mixed: bool) -> None:
|
|
245
|
+
"""Mark the selection control as a partially-selected (mixed) parent.
|
|
246
|
+
|
|
247
|
+
Driven explicitly (not by the coordinator) so it survives selection
|
|
248
|
+
state updates. Used by tri-state cascade.
|
|
249
|
+
"""
|
|
250
|
+
if self._select_ctrl is not None:
|
|
251
|
+
self._select_ctrl.state(("alternate",) if mixed else ("!alternate",))
|
|
252
|
+
|
|
253
|
+
def _update_icon(self, icon: Optional[str]) -> None:
|
|
254
|
+
if icon:
|
|
255
|
+
if self._icon_widget is None:
|
|
256
|
+
self._icon_widget = Label(
|
|
257
|
+
self._left_frame, icon=icon, variant="icon",
|
|
258
|
+
ttk_class="ListView.TLabel", takefocus=False, icon_only=True,
|
|
259
|
+
accent=self._accent, style_options=self._style_opts(),
|
|
260
|
+
)
|
|
261
|
+
self._icon_widget.pack(side="left", padx=(0, 5))
|
|
262
|
+
self.register_composite(self._icon_widget)
|
|
263
|
+
self._bind_row_mouse(self._icon_widget)
|
|
264
|
+
else:
|
|
265
|
+
self._icon_widget.configure(icon=icon)
|
|
266
|
+
elif self._icon_widget is not None:
|
|
267
|
+
self._destroy_widget("_icon_widget")
|
|
268
|
+
|
|
269
|
+
def _update_label(self, text: Optional[str]) -> None:
|
|
270
|
+
font = "caption" if self._density == "compact" else "body"
|
|
271
|
+
if text is not None:
|
|
272
|
+
if self._label_widget is None:
|
|
273
|
+
self._label_widget = Label(
|
|
274
|
+
self._center_frame, text=text, font=font, variant="list",
|
|
275
|
+
ttk_class="ListView.TLabel", takefocus=False,
|
|
276
|
+
accent=self._accent, style_options=self._style_opts(),
|
|
277
|
+
)
|
|
278
|
+
self._label_widget.pack(side="left", anchor="w")
|
|
279
|
+
self.register_composite(self._label_widget)
|
|
280
|
+
self._bind_row_mouse(self._label_widget)
|
|
281
|
+
else:
|
|
282
|
+
self._label_widget.configure(text=text)
|
|
283
|
+
elif self._label_widget is not None:
|
|
284
|
+
self._destroy_widget("_label_widget")
|
|
285
|
+
|
|
286
|
+
def _destroy_widget(self, attr: str) -> None:
|
|
287
|
+
w = getattr(self, attr, None)
|
|
288
|
+
if w is not None:
|
|
289
|
+
# Unregister BEFORE destroy so a later hover/state update doesn't
|
|
290
|
+
# call .state() on a dead widget.
|
|
291
|
+
try:
|
|
292
|
+
self.unregister_composite(w)
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
try:
|
|
296
|
+
w.pack_forget()
|
|
297
|
+
w.destroy()
|
|
298
|
+
except TclError:
|
|
299
|
+
pass
|
|
300
|
+
setattr(self, attr, None)
|
|
301
|
+
|
|
302
|
+
# ----- surface (striping) -----
|
|
303
|
+
|
|
304
|
+
def set_surface(self, surface: str) -> None:
|
|
305
|
+
"""Set the row + container surface color (used for striped rows).
|
|
306
|
+
|
|
307
|
+
Idempotent: a no-op when the surface is unchanged, so it is safe (and
|
|
308
|
+
cheap) to call on every recycle — that is what keeps striping correct as
|
|
309
|
+
rows are repointed at different data indices.
|
|
310
|
+
"""
|
|
311
|
+
if getattr(self, "_applied_surface", None) == surface:
|
|
312
|
+
return
|
|
313
|
+
self._applied_surface = surface
|
|
314
|
+
self.configure_style_options(surface=surface)
|
|
315
|
+
self.rebuild_style()
|
|
316
|
+
targets = [self._left_frame, self._center_frame, self._chevron_slot]
|
|
317
|
+
if self._select_ctrl is not None:
|
|
318
|
+
targets.append(self._select_ctrl)
|
|
319
|
+
for frame in targets:
|
|
320
|
+
try:
|
|
321
|
+
frame.configure_style_options(surface=surface)
|
|
322
|
+
frame.rebuild_style()
|
|
323
|
+
except Exception:
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
# ----- recycle entry point -----
|
|
327
|
+
|
|
328
|
+
def update_data(self, record: Optional[dict]) -> None:
|
|
329
|
+
"""Re-point this row at the node in `record`, or hide it if empty."""
|
|
330
|
+
if record is None or "__empty__" in record:
|
|
331
|
+
self._node = None
|
|
332
|
+
self.pack_forget()
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
if not self.winfo_manager():
|
|
336
|
+
self.pack(fill="x")
|
|
337
|
+
|
|
338
|
+
self._data = record
|
|
339
|
+
self._node = record.get("node")
|
|
340
|
+
self._item_index = record.get("item_index", 0)
|
|
341
|
+
|
|
342
|
+
# Indent reflects depth, applied as left padding on the chevron slot.
|
|
343
|
+
depth = record.get("depth", 0)
|
|
344
|
+
if self._state.get("depth") != depth:
|
|
345
|
+
self._chevron_slot.pack_configure(padx=(max(0, depth * self._indent), 0))
|
|
346
|
+
self._state["depth"] = depth
|
|
347
|
+
|
|
348
|
+
# Focus. (The keyboard-focus ring rides the 'background' state, set by
|
|
349
|
+
# the keyboard nav via focus_set(visual_focus=True). When a row stops
|
|
350
|
+
# being the focused node, drop that state so a recycled/relocated row
|
|
351
|
+
# doesn't keep a stale ring.)
|
|
352
|
+
focused = bool(record.get("focused", False))
|
|
353
|
+
if self._state.get("focused") != focused:
|
|
354
|
+
if focused and self._focusable:
|
|
355
|
+
try:
|
|
356
|
+
self.focus_set()
|
|
357
|
+
except TclError:
|
|
358
|
+
pass
|
|
359
|
+
elif not focused:
|
|
360
|
+
try:
|
|
361
|
+
self.state(["!background"])
|
|
362
|
+
except TclError:
|
|
363
|
+
pass
|
|
364
|
+
self._state["focused"] = focused
|
|
365
|
+
|
|
366
|
+
# Chevron (re-pack only when the expandable/expanded pair changes).
|
|
367
|
+
chev = (bool(record.get("expandable", False)), bool(record.get("expanded", False)))
|
|
368
|
+
if self._state.get("chevron") != chev:
|
|
369
|
+
self._update_chevron(*chev)
|
|
370
|
+
self._state["chevron"] = chev
|
|
371
|
+
|
|
372
|
+
# Content widgets (diffed against last render).
|
|
373
|
+
for field, updater in (
|
|
374
|
+
("icon", self._update_icon),
|
|
375
|
+
("label", self._update_label),
|
|
376
|
+
):
|
|
377
|
+
value = record.get(field)
|
|
378
|
+
if self._state.get(field) != value:
|
|
379
|
+
updater(value)
|
|
380
|
+
self._state[field] = value
|
|
381
|
+
|
|
382
|
+
# Selection highlight — drives the row wash AND the selection control's
|
|
383
|
+
# fill (the control follows the coordinator's 'selected' state).
|
|
384
|
+
selected = bool(record.get("selected", False))
|
|
385
|
+
if self._state.get("selected") != selected:
|
|
386
|
+
self.set_selected(selected)
|
|
387
|
+
self._state["selected"] = selected
|
|
388
|
+
|
|
389
|
+
# Mixed (partially-selected parent) marker on the selection control.
|
|
390
|
+
mixed = bool(record.get("mixed", False))
|
|
391
|
+
if self._state.get("mixed") != mixed:
|
|
392
|
+
self.set_mixed(mixed)
|
|
393
|
+
self._state["mixed"] = mixed
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""TreeNode — the object handle for nodes in the Tree widget.
|
|
2
|
+
|
|
3
|
+
A `TreeNode` is a plain Python object (not a Tk widget) that the user holds and
|
|
4
|
+
passes back to the tree. It carries display configuration, an opaque data bag,
|
|
5
|
+
and its place in the hierarchy. Structural changes route back to the owning tree
|
|
6
|
+
so the view re-renders.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Callable, Iterator, Optional
|
|
12
|
+
|
|
13
|
+
# Sentinel separating recognized display parameters from the user data bag.
|
|
14
|
+
_DISPLAY_PARAMS = frozenset(
|
|
15
|
+
{"label", "icon", "open_icon", "closed_icon", "expanded", "children", "loader"}
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TreeNode:
|
|
20
|
+
"""A single node in a `Tree`.
|
|
21
|
+
|
|
22
|
+
Nodes are identity-based handles: hold the object returned by `Tree.add()`
|
|
23
|
+
and pass it back to `expand()`, `select()`, `remove()`, and friends. Two
|
|
24
|
+
nodes are never equal unless they are the same object.
|
|
25
|
+
|
|
26
|
+
Display attributes (`label`, `icon`, `open_icon`, `closed_icon`, ...) are a
|
|
27
|
+
non-destructive view over the node. Any keyword not recognized as a display
|
|
28
|
+
parameter is folded into `data`, so a handler always gets the user's domain
|
|
29
|
+
data back.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__slots__ = (
|
|
33
|
+
"label", "icon", "open_icon", "closed_icon",
|
|
34
|
+
"expanded", "children", "parent", "data", "loader",
|
|
35
|
+
"_tree", "_loaded",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
label: str
|
|
39
|
+
"""The node's display label."""
|
|
40
|
+
|
|
41
|
+
icon: str | None
|
|
42
|
+
"""Bootstrap icon name shown before the label, or `None` for no icon."""
|
|
43
|
+
|
|
44
|
+
open_icon: str | None
|
|
45
|
+
"""Icon shown while the node is expanded, overriding `icon` when set."""
|
|
46
|
+
|
|
47
|
+
closed_icon: str | None
|
|
48
|
+
"""Icon shown while the node is collapsed, overriding `icon` when set."""
|
|
49
|
+
|
|
50
|
+
expanded: bool
|
|
51
|
+
"""Whether the node is currently expanded — kept in sync as it expands and
|
|
52
|
+
collapses."""
|
|
53
|
+
|
|
54
|
+
children: list["TreeNode"]
|
|
55
|
+
"""The child nodes, in order. Empty for a leaf, and empty for a lazy node
|
|
56
|
+
until its `loader` has run on first expand."""
|
|
57
|
+
|
|
58
|
+
parent: "TreeNode | None"
|
|
59
|
+
"""The parent node, or `None` for a root node."""
|
|
60
|
+
|
|
61
|
+
data: dict[str, Any]
|
|
62
|
+
"""Open-ended data bag for your own attributes. Holds any keyword passed to
|
|
63
|
+
`add()` that is not a recognized display parameter, and — for a
|
|
64
|
+
data-source-backed tree — the node's source record."""
|
|
65
|
+
|
|
66
|
+
loader: Callable[["TreeNode"], Any] | None
|
|
67
|
+
"""Callable invoked on first expand to fetch children lazily, or `None` for
|
|
68
|
+
an eagerly-populated node. Receives the node; returns an iterable of child
|
|
69
|
+
specs."""
|
|
70
|
+
|
|
71
|
+
def __init__(
|
|
72
|
+
self,
|
|
73
|
+
label: str = "",
|
|
74
|
+
*,
|
|
75
|
+
icon: Optional[str] = None,
|
|
76
|
+
open_icon: Optional[str] = None,
|
|
77
|
+
closed_icon: Optional[str] = None,
|
|
78
|
+
expanded: bool = False,
|
|
79
|
+
loader: Optional[Callable[["TreeNode"], Any]] = None,
|
|
80
|
+
data: Optional[dict] = None,
|
|
81
|
+
**extra: Any,
|
|
82
|
+
) -> None:
|
|
83
|
+
self.label = label
|
|
84
|
+
self.icon = icon
|
|
85
|
+
self.open_icon = open_icon
|
|
86
|
+
self.closed_icon = closed_icon
|
|
87
|
+
self.expanded = expanded
|
|
88
|
+
self.loader = loader
|
|
89
|
+
|
|
90
|
+
self.children: list[TreeNode] = []
|
|
91
|
+
self.parent: Optional[TreeNode] = None
|
|
92
|
+
|
|
93
|
+
# The data bag: explicit data= merged with any overflow kwargs. Nothing
|
|
94
|
+
# consumed by a display parameter ever lands here; nothing else is lost.
|
|
95
|
+
self.data: dict = dict(data or {})
|
|
96
|
+
self.data.update(extra)
|
|
97
|
+
|
|
98
|
+
# Owning internal TreeView (set when attached); used to request renders.
|
|
99
|
+
self._tree = None
|
|
100
|
+
# Lazy-loading state: a node with a loader is "expandable" before its
|
|
101
|
+
# children exist; _loaded flips True after the loader runs once.
|
|
102
|
+
self._loaded = loader is None
|
|
103
|
+
|
|
104
|
+
# ----- structure -----
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def is_leaf(self) -> bool:
|
|
108
|
+
"""Whether this node has no children and cannot lazily load any."""
|
|
109
|
+
return not self.children and self.loader is None
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def expandable(self) -> bool:
|
|
113
|
+
"""Whether this node can be expanded (has children or a lazy loader)."""
|
|
114
|
+
return bool(self.children) or (self.loader is not None and not self._loaded)
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def depth(self) -> int:
|
|
118
|
+
"""Distance from the root (root nodes are depth 0)."""
|
|
119
|
+
d = 0
|
|
120
|
+
p = self.parent
|
|
121
|
+
while p is not None:
|
|
122
|
+
d += 1
|
|
123
|
+
p = p.parent
|
|
124
|
+
return d
|
|
125
|
+
|
|
126
|
+
def ancestors(self) -> Iterator["TreeNode"]:
|
|
127
|
+
"""Yield this node's parent, grandparent, ... up to the root."""
|
|
128
|
+
p = self.parent
|
|
129
|
+
while p is not None:
|
|
130
|
+
yield p
|
|
131
|
+
p = p.parent
|
|
132
|
+
|
|
133
|
+
def descendants(self) -> Iterator["TreeNode"]:
|
|
134
|
+
"""Yield every node beneath this one, depth-first."""
|
|
135
|
+
for child in self.children:
|
|
136
|
+
yield child
|
|
137
|
+
yield from child.descendants()
|
|
138
|
+
|
|
139
|
+
# ----- convenience mutation (routes to the owning tree) -----
|
|
140
|
+
|
|
141
|
+
def add(self, label: str = "", **kwargs: Any) -> "TreeNode":
|
|
142
|
+
"""Add a child node under this node and return it.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
label: The child's display label.
|
|
146
|
+
**kwargs: Forwarded to `Tree.add` (icon, open_icon, closed_icon,
|
|
147
|
+
expanded, children, loader, data, and overflow data kwargs).
|
|
148
|
+
"""
|
|
149
|
+
if self._tree is None:
|
|
150
|
+
raise RuntimeError("node is not attached to a tree")
|
|
151
|
+
return self._tree.add(label, parent=self, **kwargs)
|
|
152
|
+
|
|
153
|
+
def remove(self) -> None:
|
|
154
|
+
"""Remove this node (and its descendants) from the tree."""
|
|
155
|
+
if self._tree is not None:
|
|
156
|
+
self._tree.remove(self)
|
|
157
|
+
|
|
158
|
+
def expand(self) -> None:
|
|
159
|
+
"""Expand this node to reveal its children."""
|
|
160
|
+
if self._tree is not None:
|
|
161
|
+
self._tree.expand(self)
|
|
162
|
+
|
|
163
|
+
def collapse(self) -> None:
|
|
164
|
+
"""Collapse this node to hide its children."""
|
|
165
|
+
if self._tree is not None:
|
|
166
|
+
self._tree.collapse(self)
|
|
167
|
+
|
|
168
|
+
def reload_children(self) -> None:
|
|
169
|
+
"""Refresh a lazy node's children (drop and re-fetch via its loader)."""
|
|
170
|
+
if self._tree is not None:
|
|
171
|
+
self._tree.reload_children(self)
|
|
172
|
+
|
|
173
|
+
def __repr__(self) -> str: # pragma: no cover - debug aid
|
|
174
|
+
return f"TreeNode(label={self.label!r}, children={len(self.children)})"
|