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,491 @@
|
|
|
1
|
+
import tkinter as tk
|
|
2
|
+
from tkinter import Misc
|
|
3
|
+
from types import SimpleNamespace
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from bootstack.constants import *
|
|
7
|
+
from bootstack._runtime.toplevel import Toplevel
|
|
8
|
+
from bootstack._runtime.utility import scale_size
|
|
9
|
+
from bootstack._runtime.window_utilities import WindowPositioning, AnchorPoint
|
|
10
|
+
from bootstack.widgets._impl.primitives import Button, Frame, Label
|
|
11
|
+
|
|
12
|
+
ttk = SimpleNamespace(
|
|
13
|
+
Button=Button,
|
|
14
|
+
Frame=Frame,
|
|
15
|
+
Label=Label,
|
|
16
|
+
window=SimpleNamespace(Toplevel=Toplevel),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ToolTip:
|
|
21
|
+
# Position offset from mouse pointer
|
|
22
|
+
_MOUSE_OFFSET_X = 25
|
|
23
|
+
_MOUSE_OFFSET_Y = 10
|
|
24
|
+
|
|
25
|
+
# Spacing between tooltip and widget when anchored
|
|
26
|
+
_WIDGET_SPACING = 1
|
|
27
|
+
|
|
28
|
+
# Fallback dimensions for tooltip sizing
|
|
29
|
+
_FALLBACK_WIDTH = 200
|
|
30
|
+
_FALLBACK_HEIGHT = 50
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
widget: Misc,
|
|
35
|
+
text: str = "widget info",
|
|
36
|
+
padding: int = 10,
|
|
37
|
+
justify: Literal["left", "center", "right"] = "left",
|
|
38
|
+
accent: str | None = None,
|
|
39
|
+
wraplength: int | None = None,
|
|
40
|
+
delay: int = 250, # milliseconds
|
|
41
|
+
image: Any = None,
|
|
42
|
+
anchor_point: AnchorPoint | None = None,
|
|
43
|
+
window_point: AnchorPoint | None = None,
|
|
44
|
+
auto_flip: bool | Literal['vertical', 'horizontal'] = True,
|
|
45
|
+
**kwargs: Any,
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Initialize a ToolTip instance for the specified widget.
|
|
48
|
+
|
|
49
|
+
Creates a tooltip that appears after a configurable delay when the mouse
|
|
50
|
+
enters the widget and disappears when the mouse leaves or on button press.
|
|
51
|
+
The tooltip can either follow the mouse pointer or be anchored to a specific
|
|
52
|
+
position relative to the widget.
|
|
53
|
+
|
|
54
|
+
The tooltip window is created with semi-transparency (alpha=0.95 by default)
|
|
55
|
+
and uses the Bootstrap styling system for consistent theming. Text automatically
|
|
56
|
+
wraps based on the specified wraplength, and optional images can be displayed
|
|
57
|
+
alongside the text.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
widget: The tkinter widget to attach this tooltip to. The tooltip will
|
|
61
|
+
appear when hovering over this widget.
|
|
62
|
+
text: The text content to display in the tooltip. Supports multi-line
|
|
63
|
+
text that will wrap according to wraplength. Defaults to "widget info".
|
|
64
|
+
padding: The internal padding in pixels between the tooltip text and the
|
|
65
|
+
tooltip border. Defaults to 10.
|
|
66
|
+
justify: Text alignment within the tooltip. Valid options are "left",
|
|
67
|
+
"center", or "right". Defaults to "left".
|
|
68
|
+
accent: Accent token for the tooltip frame (e.g., "danger", "info").
|
|
69
|
+
If None, uses default elevated background styling.
|
|
70
|
+
to the tooltip frame.
|
|
71
|
+
wraplength: Maximum width in screen units before text wraps to a new line.
|
|
72
|
+
If None, defaults to a scaled value of 300 based on the widget's display.
|
|
73
|
+
delay: Time in milliseconds to wait before showing the tooltip after mouse
|
|
74
|
+
enters the widget. Defaults to 250ms.
|
|
75
|
+
image: Optional image to display in the tooltip below the text. Should be
|
|
76
|
+
a PhotoImage or compatible tkinter image object.
|
|
77
|
+
anchor_point: Point on the widget to anchor to (n, s, e, w, ne, nw, se, sw, center).
|
|
78
|
+
If None, tooltip follows the mouse pointer.
|
|
79
|
+
window_point: Point on the tooltip window to align with anchor_point
|
|
80
|
+
(n, s, e, w, ne, nw, se, sw, center). If None, auto-defaults to the
|
|
81
|
+
opposite of anchor_point for natural positioning.
|
|
82
|
+
auto_flip: Smart positioning to keep tooltip on screen. Defaults to True.
|
|
83
|
+
- False: No flipping
|
|
84
|
+
- True: Flip both vertically and horizontally as needed
|
|
85
|
+
- 'vertical': Only flip up/down
|
|
86
|
+
- 'horizontal': Only flip left/right
|
|
87
|
+
**kwargs: Additional keyword arguments passed to the Toplevel window
|
|
88
|
+
constructor. Common options include alpha, topmost, etc. The arguments
|
|
89
|
+
overrideredirect, master, and windowtype are set automatically.
|
|
90
|
+
"""
|
|
91
|
+
# Configuration
|
|
92
|
+
self._widget = widget
|
|
93
|
+
self._text = text
|
|
94
|
+
self._padding = padding
|
|
95
|
+
self._justify = justify
|
|
96
|
+
self._accent = accent
|
|
97
|
+
self._wraplength = wraplength if wraplength is not None else scale_size(self._widget, 300)
|
|
98
|
+
self._delay = delay
|
|
99
|
+
self._image = image
|
|
100
|
+
self._anchor_point = anchor_point
|
|
101
|
+
self._window_point = window_point
|
|
102
|
+
self._auto_flip = auto_flip
|
|
103
|
+
|
|
104
|
+
self._toplevel = None
|
|
105
|
+
self._label = None
|
|
106
|
+
self._id = None
|
|
107
|
+
|
|
108
|
+
# Set keyword arguments (create copy to avoid mutating caller's dict)
|
|
109
|
+
self.toplevel_kwargs = kwargs.copy()
|
|
110
|
+
self.toplevel_kwargs["overrideredirect"] = True
|
|
111
|
+
self.toplevel_kwargs["master"] = self._widget
|
|
112
|
+
self.toplevel_kwargs["windowtype"] = "tooltip"
|
|
113
|
+
if "alpha" not in self.toplevel_kwargs:
|
|
114
|
+
self.toplevel_kwargs["alpha"] = 0.95
|
|
115
|
+
|
|
116
|
+
# Event binding. add="+" so we never replace a handler the target
|
|
117
|
+
# already has; keep the bind ids so destroy() removes only our own.
|
|
118
|
+
self._bind_ids = {
|
|
119
|
+
"<Enter>": self._widget.bind("<Enter>", self._on_enter, add="+"),
|
|
120
|
+
"<Leave>": self._widget.bind("<Leave>", self._on_leave, add="+"),
|
|
121
|
+
"<Motion>": self._widget.bind("<Motion>", self._move_tip, add="+"),
|
|
122
|
+
"<ButtonPress>": self._widget.bind("<ButtonPress>", self._on_button_press, add="+"),
|
|
123
|
+
}
|
|
124
|
+
# Self-release when the target is destroyed, so a pending timer or a
|
|
125
|
+
# visible popup can't outlive it (and a later destroy() can't crash on a
|
|
126
|
+
# dead widget). <Destroy> propagates up from descendants that carry the
|
|
127
|
+
# target's bindtag, so the handler guards on the target itself.
|
|
128
|
+
self._destroy_id = self._widget.bind("<Destroy>", self._on_target_destroy, add="+")
|
|
129
|
+
|
|
130
|
+
# Tk events don't bubble, so hovering a child of a container target
|
|
131
|
+
# would otherwise never reach these bindings. Extend them across the
|
|
132
|
+
# subtree so the tip shows anywhere inside the container.
|
|
133
|
+
self.refresh_bindings()
|
|
134
|
+
|
|
135
|
+
def refresh_bindings(self) -> None:
|
|
136
|
+
"""Re-cover the target's subtree after children were added post-attach.
|
|
137
|
+
|
|
138
|
+
`propagate_target_bindings` only tags the descendants present when it
|
|
139
|
+
runs, so children added to a container target after the tooltip was
|
|
140
|
+
created are not covered until this is called again. The call is
|
|
141
|
+
idempotent — already-tagged descendants are left untouched.
|
|
142
|
+
"""
|
|
143
|
+
from bootstack._runtime.utility import propagate_target_bindings
|
|
144
|
+
try:
|
|
145
|
+
if self._widget.winfo_exists():
|
|
146
|
+
propagate_target_bindings(self._widget)
|
|
147
|
+
except tk.TclError:
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
def destroy(self) -> None:
|
|
151
|
+
"""Cleanup tooltip resources and unbind all event handlers.
|
|
152
|
+
|
|
153
|
+
This method should be called when the tooltip is no longer needed to prevent
|
|
154
|
+
memory leaks. It cancels any pending tooltip display, hides any visible tooltip,
|
|
155
|
+
and removes all event bindings from the widget.
|
|
156
|
+
"""
|
|
157
|
+
self._unschedule()
|
|
158
|
+
self._hide_tip()
|
|
159
|
+
# The target may already be gone (e.g. destroyed before destroy() is
|
|
160
|
+
# called); unbinding a dead widget path raises a TclError, so guard it.
|
|
161
|
+
try:
|
|
162
|
+
if self._widget.winfo_exists():
|
|
163
|
+
for sequence, bind_id in self._bind_ids.items():
|
|
164
|
+
self._widget.unbind(sequence, bind_id)
|
|
165
|
+
if self._destroy_id:
|
|
166
|
+
self._widget.unbind("<Destroy>", self._destroy_id)
|
|
167
|
+
except tk.TclError:
|
|
168
|
+
pass
|
|
169
|
+
self._bind_ids = {}
|
|
170
|
+
self._destroy_id = None
|
|
171
|
+
|
|
172
|
+
@property
|
|
173
|
+
def text(self) -> str:
|
|
174
|
+
"""The tooltip text. Assigning updates a visible popup live."""
|
|
175
|
+
return self._text
|
|
176
|
+
|
|
177
|
+
@text.setter
|
|
178
|
+
def text(self, value: str) -> None:
|
|
179
|
+
self._text = value
|
|
180
|
+
if self._toplevel is not None and self._label is not None:
|
|
181
|
+
try:
|
|
182
|
+
self._label.configure(text=value)
|
|
183
|
+
except tk.TclError:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
def _on_target_destroy(self, event: Any) -> None:
|
|
187
|
+
"""Release the timer and popup when the target widget is destroyed."""
|
|
188
|
+
if event.widget is not self._widget:
|
|
189
|
+
return # a descendant was destroyed, not the target itself
|
|
190
|
+
self._unschedule()
|
|
191
|
+
self._hide_tip()
|
|
192
|
+
|
|
193
|
+
def _on_enter(self, _) -> None:
|
|
194
|
+
"""Handle mouse enter event by scheduling tooltip display."""
|
|
195
|
+
self._schedule()
|
|
196
|
+
|
|
197
|
+
def _on_leave(self, _) -> None:
|
|
198
|
+
"""Handle mouse leave event by canceling and hiding tooltip.
|
|
199
|
+
|
|
200
|
+
When the target is a container, crossing from the container onto one of
|
|
201
|
+
its children fires a `<Leave>` on the container even though the pointer
|
|
202
|
+
is still inside it. Keep the tip in that case so it doesn't flicker as
|
|
203
|
+
the pointer moves over child widgets.
|
|
204
|
+
"""
|
|
205
|
+
if self._pointer_within_target():
|
|
206
|
+
return
|
|
207
|
+
self._unschedule()
|
|
208
|
+
self._hide_tip()
|
|
209
|
+
|
|
210
|
+
def _on_button_press(self, _) -> None:
|
|
211
|
+
"""Hide the tooltip on any button press inside the target subtree."""
|
|
212
|
+
self._unschedule()
|
|
213
|
+
self._hide_tip()
|
|
214
|
+
|
|
215
|
+
def _pointer_within_target(self) -> bool:
|
|
216
|
+
"""Return True if the pointer is over the target or one of its children."""
|
|
217
|
+
try:
|
|
218
|
+
x = self._widget.winfo_pointerx()
|
|
219
|
+
y = self._widget.winfo_pointery()
|
|
220
|
+
under = self._widget.winfo_containing(x, y)
|
|
221
|
+
except tk.TclError:
|
|
222
|
+
return False
|
|
223
|
+
if under is None:
|
|
224
|
+
return False
|
|
225
|
+
target_path = str(self._widget)
|
|
226
|
+
under_path = str(under)
|
|
227
|
+
return under_path == target_path or under_path.startswith(target_path + ".")
|
|
228
|
+
|
|
229
|
+
def _schedule(self) -> None:
|
|
230
|
+
"""Schedule the tooltip to appear after the configured delay."""
|
|
231
|
+
self._unschedule()
|
|
232
|
+
self._id = self._widget.after(self._delay, self._show_tip)
|
|
233
|
+
|
|
234
|
+
def _unschedule(self) -> None:
|
|
235
|
+
"""Cancel any pending scheduled tooltip display."""
|
|
236
|
+
_id = self._id
|
|
237
|
+
self._id = None
|
|
238
|
+
if _id:
|
|
239
|
+
self._widget.after_cancel(_id)
|
|
240
|
+
|
|
241
|
+
def _show_tip(self, *_: Any) -> None:
|
|
242
|
+
"""Create and display the tooltip window at the appropriate position."""
|
|
243
|
+
if self._toplevel:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
# Check if widget still exists before showing tooltip
|
|
247
|
+
try:
|
|
248
|
+
if not self._widget.winfo_exists():
|
|
249
|
+
return
|
|
250
|
+
except tk.TclError:
|
|
251
|
+
return
|
|
252
|
+
|
|
253
|
+
# Create the tooltip window (position will be set after content is built)
|
|
254
|
+
self._toplevel = Toplevel(**self.toplevel_kwargs)
|
|
255
|
+
# Use accent with tooltip variant
|
|
256
|
+
accent = 'background[+1]' if self._accent is None else self._accent
|
|
257
|
+
frame = ttk.Frame(
|
|
258
|
+
self._toplevel,
|
|
259
|
+
accent=accent,
|
|
260
|
+
variant='tooltip',
|
|
261
|
+
padding=self._padding
|
|
262
|
+
)
|
|
263
|
+
frame.pack(fill=BOTH, expand=YES)
|
|
264
|
+
|
|
265
|
+
lbl = ttk.Label(
|
|
266
|
+
master=frame,
|
|
267
|
+
text=self._text,
|
|
268
|
+
image=self._image,
|
|
269
|
+
compound='bottom',
|
|
270
|
+
justify=self._justify,
|
|
271
|
+
font="caption",
|
|
272
|
+
wraplength=self._wraplength,
|
|
273
|
+
)
|
|
274
|
+
lbl.pack(fill=BOTH, expand=YES)
|
|
275
|
+
self._label = lbl
|
|
276
|
+
|
|
277
|
+
# Wait until size is known, then position
|
|
278
|
+
self._toplevel.update_idletasks()
|
|
279
|
+
|
|
280
|
+
if self._anchor_point:
|
|
281
|
+
# Use WindowPositioning for anchored tooltips (sets geometry directly)
|
|
282
|
+
self._position_anchored()
|
|
283
|
+
else:
|
|
284
|
+
# Mouse-following tooltip
|
|
285
|
+
x, y = self._get_mouse_position()
|
|
286
|
+
self._toplevel.geometry(f"+{x}+{y}")
|
|
287
|
+
|
|
288
|
+
# Ensure the tooltip is visible
|
|
289
|
+
self._toplevel.deiconify()
|
|
290
|
+
|
|
291
|
+
def _move_tip(self, *_: Any) -> None:
|
|
292
|
+
"""Update the tooltip position based on mouse or anchor position."""
|
|
293
|
+
if self._toplevel:
|
|
294
|
+
if self._anchor_point:
|
|
295
|
+
# Anchored tooltips don't move
|
|
296
|
+
pass
|
|
297
|
+
else:
|
|
298
|
+
# Update mouse-following tooltip
|
|
299
|
+
x, y = self._get_mouse_position()
|
|
300
|
+
self._toplevel.geometry(f"+{x}+{y}")
|
|
301
|
+
|
|
302
|
+
def _hide_tip(self, *_: Any) -> None:
|
|
303
|
+
"""Hide and destroy the tooltip window."""
|
|
304
|
+
if self._toplevel:
|
|
305
|
+
self._toplevel.destroy()
|
|
306
|
+
self._toplevel = None
|
|
307
|
+
self._label = None
|
|
308
|
+
|
|
309
|
+
def _get_mouse_position(self) -> tuple[int, int]:
|
|
310
|
+
"""Get tooltip position offset from the current mouse pointer.
|
|
311
|
+
|
|
312
|
+
Uses WindowPositioning to ensure the tooltip stays on screen.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Tuple of (x, y) screen coordinates for the tooltip.
|
|
316
|
+
"""
|
|
317
|
+
x = self._widget.winfo_pointerx() + self._MOUSE_OFFSET_X
|
|
318
|
+
y = self._widget.winfo_pointery() + self._MOUSE_OFFSET_Y
|
|
319
|
+
|
|
320
|
+
# Ensure tooltip stays on screen (no titlebar since overrideredirect=True)
|
|
321
|
+
if self._toplevel:
|
|
322
|
+
x, y = WindowPositioning.ensure_on_screen(
|
|
323
|
+
self._toplevel, x, y, padding=5, titlebar_height=0
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
return x, y
|
|
327
|
+
|
|
328
|
+
def _position_anchored(self) -> None:
|
|
329
|
+
"""Position tooltip using WindowPositioning for intelligent anchor-based positioning.
|
|
330
|
+
|
|
331
|
+
Uses anchor_point and window_point attributes and applies auto-flip
|
|
332
|
+
to keep tooltip on screen.
|
|
333
|
+
"""
|
|
334
|
+
if not self._toplevel or not self._anchor_point:
|
|
335
|
+
return
|
|
336
|
+
|
|
337
|
+
# Ensure the widget is fully laid out before calculating position
|
|
338
|
+
self._widget.update_idletasks()
|
|
339
|
+
|
|
340
|
+
# Default window_point based on anchor_point if not specified
|
|
341
|
+
window_point = self._window_point
|
|
342
|
+
if window_point is None:
|
|
343
|
+
# Auto-determine opposite anchor point for natural positioning
|
|
344
|
+
opposite = {
|
|
345
|
+
'n': 's', 's': 'n', 'e': 'w', 'w': 'e',
|
|
346
|
+
'ne': 'sw', 'nw': 'se', 'se': 'nw', 'sw': 'ne',
|
|
347
|
+
'center': 'center'
|
|
348
|
+
}
|
|
349
|
+
window_point = opposite.get(self._anchor_point, 's')
|
|
350
|
+
|
|
351
|
+
# Get widget dimensions using max of requested and actual size
|
|
352
|
+
widget_x = self._widget.winfo_rootx()
|
|
353
|
+
widget_y = self._widget.winfo_rooty()
|
|
354
|
+
widget_w = max(self._widget.winfo_reqwidth(), self._widget.winfo_width())
|
|
355
|
+
widget_h = max(self._widget.winfo_reqheight(), self._widget.winfo_height())
|
|
356
|
+
|
|
357
|
+
# Calculate anchor coordinates based on anchor_point
|
|
358
|
+
if self._anchor_point == 'nw':
|
|
359
|
+
anchor_x, anchor_y = widget_x, widget_y
|
|
360
|
+
elif self._anchor_point == 'n':
|
|
361
|
+
anchor_x, anchor_y = widget_x + widget_w // 2, widget_y
|
|
362
|
+
elif self._anchor_point == 'ne':
|
|
363
|
+
anchor_x, anchor_y = widget_x + widget_w, widget_y
|
|
364
|
+
elif self._anchor_point == 'w':
|
|
365
|
+
anchor_x, anchor_y = widget_x, widget_y + widget_h // 2
|
|
366
|
+
elif self._anchor_point == 'center':
|
|
367
|
+
anchor_x, anchor_y = widget_x + widget_w // 2, widget_y + widget_h // 2
|
|
368
|
+
elif self._anchor_point == 'e':
|
|
369
|
+
anchor_x, anchor_y = widget_x + widget_w, widget_y + widget_h // 2
|
|
370
|
+
elif self._anchor_point == 'sw':
|
|
371
|
+
anchor_x, anchor_y = widget_x, widget_y + widget_h
|
|
372
|
+
elif self._anchor_point == 's':
|
|
373
|
+
anchor_x, anchor_y = widget_x + widget_w // 2, widget_y + widget_h
|
|
374
|
+
elif self._anchor_point == 'se':
|
|
375
|
+
anchor_x, anchor_y = widget_x + widget_w, widget_y + widget_h
|
|
376
|
+
else:
|
|
377
|
+
anchor_x, anchor_y = widget_x, widget_y
|
|
378
|
+
|
|
379
|
+
# Use req dimensions — the Toplevel is freshly created and withdrawn;
|
|
380
|
+
# winfo_width/height return the default 200×200 placeholder, not content size.
|
|
381
|
+
w_width = self._toplevel.winfo_reqwidth()
|
|
382
|
+
w_height = self._toplevel.winfo_reqheight()
|
|
383
|
+
|
|
384
|
+
x_offset, y_offset = 0, 0
|
|
385
|
+
if window_point == 'nw':
|
|
386
|
+
x_offset, y_offset = 0, 0
|
|
387
|
+
elif window_point == 'n':
|
|
388
|
+
x_offset, y_offset = -w_width // 2, 0
|
|
389
|
+
elif window_point == 'ne':
|
|
390
|
+
x_offset, y_offset = -w_width, 0
|
|
391
|
+
elif window_point == 'w':
|
|
392
|
+
x_offset, y_offset = 0, -w_height // 2
|
|
393
|
+
elif window_point == 'center':
|
|
394
|
+
x_offset, y_offset = -w_width // 2, -w_height // 2
|
|
395
|
+
elif window_point == 'e':
|
|
396
|
+
x_offset, y_offset = -w_width, -w_height // 2
|
|
397
|
+
elif window_point == 'sw':
|
|
398
|
+
x_offset, y_offset = 0, -w_height
|
|
399
|
+
elif window_point == 's':
|
|
400
|
+
x_offset, y_offset = -w_width // 2, -w_height
|
|
401
|
+
elif window_point == 'se':
|
|
402
|
+
x_offset, y_offset = -w_width, -w_height
|
|
403
|
+
|
|
404
|
+
# Calculate position with offset
|
|
405
|
+
x = int(anchor_x + x_offset + self._WIDGET_SPACING)
|
|
406
|
+
y = int(anchor_y + y_offset + self._WIDGET_SPACING)
|
|
407
|
+
|
|
408
|
+
# Auto-flip logic
|
|
409
|
+
if self._auto_flip:
|
|
410
|
+
vertical_offscreen, horizontal_offscreen = WindowPositioning._check_offscreen(
|
|
411
|
+
self._toplevel, x, y, padding=5
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
should_flip_vertical = False
|
|
415
|
+
should_flip_horizontal = False
|
|
416
|
+
|
|
417
|
+
if self._auto_flip is True or self._auto_flip == 'vertical':
|
|
418
|
+
should_flip_vertical = vertical_offscreen
|
|
419
|
+
|
|
420
|
+
if self._auto_flip is True or self._auto_flip == 'horizontal':
|
|
421
|
+
should_flip_horizontal = horizontal_offscreen
|
|
422
|
+
|
|
423
|
+
# Flip if needed
|
|
424
|
+
if should_flip_vertical or should_flip_horizontal:
|
|
425
|
+
flipped_anchor_point = self._anchor_point
|
|
426
|
+
flipped_window_point = window_point
|
|
427
|
+
|
|
428
|
+
if should_flip_vertical:
|
|
429
|
+
flipped_anchor_point = WindowPositioning._flip_anchor_vertical(flipped_anchor_point)
|
|
430
|
+
flipped_window_point = WindowPositioning._flip_anchor_vertical(flipped_window_point)
|
|
431
|
+
|
|
432
|
+
if should_flip_horizontal:
|
|
433
|
+
flipped_anchor_point = WindowPositioning._flip_anchor_horizontal(flipped_anchor_point)
|
|
434
|
+
flipped_window_point = WindowPositioning._flip_anchor_horizontal(flipped_window_point)
|
|
435
|
+
|
|
436
|
+
# Recalculate anchor coordinates with flipped anchor point
|
|
437
|
+
if flipped_anchor_point == 'nw':
|
|
438
|
+
anchor_x, anchor_y = widget_x, widget_y
|
|
439
|
+
elif flipped_anchor_point == 'n':
|
|
440
|
+
anchor_x, anchor_y = widget_x + widget_w // 2, widget_y
|
|
441
|
+
elif flipped_anchor_point == 'ne':
|
|
442
|
+
anchor_x, anchor_y = widget_x + widget_w, widget_y
|
|
443
|
+
elif flipped_anchor_point == 'w':
|
|
444
|
+
anchor_x, anchor_y = widget_x, widget_y + widget_h // 2
|
|
445
|
+
elif flipped_anchor_point == 'center':
|
|
446
|
+
anchor_x, anchor_y = widget_x + widget_w // 2, widget_y + widget_h // 2
|
|
447
|
+
elif flipped_anchor_point == 'e':
|
|
448
|
+
anchor_x, anchor_y = widget_x + widget_w, widget_y + widget_h // 2
|
|
449
|
+
elif flipped_anchor_point == 'sw':
|
|
450
|
+
anchor_x, anchor_y = widget_x, widget_y + widget_h
|
|
451
|
+
elif flipped_anchor_point == 's':
|
|
452
|
+
anchor_x, anchor_y = widget_x + widget_w // 2, widget_y + widget_h
|
|
453
|
+
elif flipped_anchor_point == 'se':
|
|
454
|
+
anchor_x, anchor_y = widget_x + widget_w, widget_y + widget_h
|
|
455
|
+
else:
|
|
456
|
+
anchor_x, anchor_y = widget_x, widget_y
|
|
457
|
+
|
|
458
|
+
# Recalculate window offset with flipped window point
|
|
459
|
+
if flipped_window_point == 'nw':
|
|
460
|
+
x_offset, y_offset = 0, 0
|
|
461
|
+
elif flipped_window_point == 'n':
|
|
462
|
+
x_offset, y_offset = -w_width // 2, 0
|
|
463
|
+
elif flipped_window_point == 'ne':
|
|
464
|
+
x_offset, y_offset = -w_width, 0
|
|
465
|
+
elif flipped_window_point == 'w':
|
|
466
|
+
x_offset, y_offset = 0, -w_height // 2
|
|
467
|
+
elif flipped_window_point == 'center':
|
|
468
|
+
x_offset, y_offset = -w_width // 2, -w_height // 2
|
|
469
|
+
elif flipped_window_point == 'e':
|
|
470
|
+
x_offset, y_offset = -w_width, -w_height // 2
|
|
471
|
+
elif flipped_window_point == 'sw':
|
|
472
|
+
x_offset, y_offset = 0, -w_height
|
|
473
|
+
elif flipped_window_point == 's':
|
|
474
|
+
x_offset, y_offset = -w_width // 2, -w_height
|
|
475
|
+
elif flipped_window_point == 'se':
|
|
476
|
+
x_offset, y_offset = -w_width, -w_height
|
|
477
|
+
|
|
478
|
+
# Recalculate position with flipped anchors
|
|
479
|
+
x = int(anchor_x + x_offset + self._WIDGET_SPACING)
|
|
480
|
+
y = int(anchor_y + y_offset + self._WIDGET_SPACING)
|
|
481
|
+
|
|
482
|
+
# Ensure on screen (final safety check)
|
|
483
|
+
try:
|
|
484
|
+
x, y = WindowPositioning.ensure_on_screen(
|
|
485
|
+
self._toplevel, x, y, padding=5, titlebar_height=0
|
|
486
|
+
)
|
|
487
|
+
except Exception:
|
|
488
|
+
pass
|
|
489
|
+
|
|
490
|
+
# Set geometry
|
|
491
|
+
self._toplevel.geometry(f"+{x}+{y}")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""Internal recycle-view Tree composite (engine behind the public Tree)."""
|
|
2
|
+
|
|
3
|
+
from bootstack.widgets._impl.composites.tree.treenode import TreeNode
|
|
4
|
+
from bootstack.widgets._impl.composites.tree.treeitem import TreeItem
|
|
5
|
+
from bootstack.widgets._impl.composites.tree.treeview import TreeView
|
|
6
|
+
|
|
7
|
+
__all__ = ["TreeNode", "TreeItem", "TreeView"]
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Project a flat adjacency-list data source as a lazy Tree hierarchy.
|
|
2
|
+
|
|
3
|
+
A data-source-backed tree is "auto-generate the loader from a source": the
|
|
4
|
+
source stores flat records that each carry a `parent_field` pointing at their
|
|
5
|
+
parent's id (an adjacency list), and the binding projects that flat shape as a
|
|
6
|
+
hierarchy by issuing one indexed query per expanded node. Nothing about the flat
|
|
7
|
+
`DataSourceProtocol` changes — the hierarchy lives entirely in this binding, so
|
|
8
|
+
any source (SQLite, memory, a custom adapter) can back a tree.
|
|
9
|
+
|
|
10
|
+
Scale comes for free: only the children of *expanded* nodes are ever queried
|
|
11
|
+
(one `WHERE parent_field = ?` each), and a single batched existence query per
|
|
12
|
+
expand decides which of those children get a chevron.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from typing import Any, Callable, List, Sequence
|
|
18
|
+
|
|
19
|
+
from bootstack.data.query import Condition, SortKey, col, normalize_sort_keys
|
|
20
|
+
|
|
21
|
+
__all__ = ["SourceBinding"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _key(value: Any) -> Any:
|
|
25
|
+
"""Normalize an identifier for cross-column comparison.
|
|
26
|
+
|
|
27
|
+
A record's id and another record's `parent_field` may be stored with
|
|
28
|
+
different types depending on the source — for instance a column whose sampled
|
|
29
|
+
values are all NULL gets TEXT affinity, so its ids can come back as `'1'`
|
|
30
|
+
while a sibling id column returns `1`. They are the same logical key, so
|
|
31
|
+
compare them as strings (NULL stays distinct).
|
|
32
|
+
"""
|
|
33
|
+
return None if value is None else str(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _child_condition(parent_field: str, value: Any) -> Condition:
|
|
37
|
+
"""Match the rows whose `parent_field` equals `value`.
|
|
38
|
+
|
|
39
|
+
A `value` of `None` means "the roots" and matches a NULL/absent parent.
|
|
40
|
+
"""
|
|
41
|
+
column = col(parent_field)
|
|
42
|
+
if value is None:
|
|
43
|
+
return column.is_null()
|
|
44
|
+
return column == value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SourceBinding:
|
|
48
|
+
"""Binds a flat adjacency-list data source to a `Tree`.
|
|
49
|
+
|
|
50
|
+
Holds the projection config and turns records into node specs, wiring each
|
|
51
|
+
node that has children with a `loader` closure that re-queries on expand.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
source: Any,
|
|
57
|
+
*,
|
|
58
|
+
parent_field: str,
|
|
59
|
+
root_value: Any,
|
|
60
|
+
label_field: str,
|
|
61
|
+
icon_field: str | None,
|
|
62
|
+
node_builder: Callable[[dict], dict] | None,
|
|
63
|
+
order: Any = None,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.source = source
|
|
66
|
+
self.parent_field = parent_field
|
|
67
|
+
self.root_value = root_value
|
|
68
|
+
self.label_field = label_field
|
|
69
|
+
self.icon_field = icon_field
|
|
70
|
+
self.node_builder = node_builder
|
|
71
|
+
self.sort_keys: List[SortKey] = self._normalize_order(order)
|
|
72
|
+
|
|
73
|
+
@staticmethod
|
|
74
|
+
def _normalize_order(order: Any) -> List[SortKey]:
|
|
75
|
+
if order is None:
|
|
76
|
+
return []
|
|
77
|
+
keys: Sequence[Any]
|
|
78
|
+
if isinstance(order, (list, tuple)):
|
|
79
|
+
keys = order
|
|
80
|
+
else:
|
|
81
|
+
keys = (order,)
|
|
82
|
+
return normalize_sort_keys(keys)
|
|
83
|
+
|
|
84
|
+
# ----- spec building -----
|
|
85
|
+
|
|
86
|
+
def _spec(self, record: Any, has_children: bool) -> dict:
|
|
87
|
+
"""Turn one raw record into a node spec dict."""
|
|
88
|
+
public = self.source._public_record(record)
|
|
89
|
+
if self.node_builder is not None:
|
|
90
|
+
spec = dict(self.node_builder(public))
|
|
91
|
+
else:
|
|
92
|
+
if self.label_field not in public:
|
|
93
|
+
raise KeyError(
|
|
94
|
+
f"Tree(data_source=...): no field {self.label_field!r} in the "
|
|
95
|
+
f"record {dict(public)!r}. Set label_field= to the column that "
|
|
96
|
+
f"holds the node label, or pass node_builder=."
|
|
97
|
+
)
|
|
98
|
+
spec = {"label": str(public.get(self.label_field, ""))}
|
|
99
|
+
if self.icon_field and public.get(self.icon_field):
|
|
100
|
+
spec["icon"] = public[self.icon_field]
|
|
101
|
+
spec.setdefault("data", dict(public))
|
|
102
|
+
if has_children:
|
|
103
|
+
record_id = self.source._record_id(record)
|
|
104
|
+
spec["loader"] = lambda _node, _pid=record_id: self._fetch(_pid)
|
|
105
|
+
return spec
|
|
106
|
+
|
|
107
|
+
def _fetch(self, parent_value: Any) -> List[dict]:
|
|
108
|
+
"""Fetch and build the child specs for one parent value."""
|
|
109
|
+
records = self.source._query(
|
|
110
|
+
_child_condition(self.parent_field, parent_value), self.sort_keys
|
|
111
|
+
)
|
|
112
|
+
if not records:
|
|
113
|
+
return []
|
|
114
|
+
ids = [self.source._record_id(r) for r in records]
|
|
115
|
+
with_children = self._records_with_children(ids)
|
|
116
|
+
return [
|
|
117
|
+
self._spec(r, _key(self.source._record_id(r)) in with_children)
|
|
118
|
+
for r in records
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
def _records_with_children(self, ids: Sequence[Any]) -> set:
|
|
122
|
+
"""Return the subset of `ids` that are some row's `parent_field` value.
|
|
123
|
+
|
|
124
|
+
One batched existence query decides chevrons for a whole sibling group,
|
|
125
|
+
so the cost is O(1 query) per expand regardless of how many children the
|
|
126
|
+
expanded node has.
|
|
127
|
+
"""
|
|
128
|
+
ids = [i for i in ids if i is not None]
|
|
129
|
+
if not ids:
|
|
130
|
+
return set()
|
|
131
|
+
rows = self.source._query(col(self.parent_field).is_in(ids), [])
|
|
132
|
+
return {_key(r.get(self.parent_field)) for r in rows}
|
|
133
|
+
|
|
134
|
+
def roots(self) -> List[dict]:
|
|
135
|
+
"""Build the specs for the top-level (root) nodes."""
|
|
136
|
+
return self._fetch(self.root_value)
|