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,381 @@
|
|
|
1
|
+
"""Accordion widget - a container of mutually exclusive expanders."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing_extensions import Unpack
|
|
5
|
+
from typing import Any, Callable, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from bootstack.events import AccordionChangeEvent
|
|
8
|
+
from bootstack.widgets._impl.primitives.frame import Frame, FrameKwargs
|
|
9
|
+
from bootstack.widgets._impl.primitives.separator import Separator
|
|
10
|
+
from bootstack.widgets._impl.composites.expander import Expander
|
|
11
|
+
from bootstack.widgets._impl.mixins import configure_delegate
|
|
12
|
+
from bootstack.widgets.types import Master
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
pass
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Accordion(Frame):
|
|
19
|
+
"""A container of Expander widgets with optional mutual exclusion.
|
|
20
|
+
|
|
21
|
+
The Accordion manages a group of Expanders, optionally enforcing that only
|
|
22
|
+
one can be expanded at a time. When `allow_multiple=False`, expanding one
|
|
23
|
+
section automatically collapses the others. When `allow_collapse_all=False`,
|
|
24
|
+
at least one section must remain open.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
master: Master = None,
|
|
30
|
+
*,
|
|
31
|
+
allow_multiple: bool = False,
|
|
32
|
+
allow_collapse_all: bool = True,
|
|
33
|
+
show_separators: bool = False,
|
|
34
|
+
accent: str = None,
|
|
35
|
+
variant: str = None,
|
|
36
|
+
**kwargs: Unpack[FrameKwargs]
|
|
37
|
+
):
|
|
38
|
+
"""Create an Accordion widget.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
master: Parent widget. If None, uses the default root window.
|
|
42
|
+
allow_multiple: If True, multiple sections can be open at once.
|
|
43
|
+
If False (default), only one section can be open at a time.
|
|
44
|
+
allow_collapse_all: If True (default), all sections can be collapsed.
|
|
45
|
+
If False, at least one section must remain open.
|
|
46
|
+
show_separators: If True, show separators between expanders.
|
|
47
|
+
accent: Accent token for the expanders (e.g., 'success', 'primary').
|
|
48
|
+
variant: Variant for the expanders (e.g., 'solid', 'default').
|
|
49
|
+
**kwargs: Additional arguments passed to Frame.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
if 'show_border' in kwargs:
|
|
53
|
+
kwargs.setdefault('padding', 1) # required to avoid clipping corners
|
|
54
|
+
|
|
55
|
+
super().__init__(master, **kwargs)
|
|
56
|
+
|
|
57
|
+
self._allow_multiple = allow_multiple
|
|
58
|
+
self._allow_collapse_all = allow_collapse_all
|
|
59
|
+
self._show_separators = show_separators
|
|
60
|
+
self._accent = accent
|
|
61
|
+
self._variant = variant
|
|
62
|
+
self._expanders: dict[str, Expander] = {}
|
|
63
|
+
self._expander_order: list[str] = []
|
|
64
|
+
self._separator_widgets: list[Separator] = []
|
|
65
|
+
self._updating = False # Prevent recursive updates
|
|
66
|
+
self._counter = 0 # For auto-generating keys
|
|
67
|
+
|
|
68
|
+
def add(
|
|
69
|
+
self,
|
|
70
|
+
expander: Expander = None,
|
|
71
|
+
*,
|
|
72
|
+
key: str = None,
|
|
73
|
+
title: str = "",
|
|
74
|
+
icon: str | dict = None,
|
|
75
|
+
expanded: bool = None,
|
|
76
|
+
**kwargs
|
|
77
|
+
) -> Expander:
|
|
78
|
+
"""Add an expander to the accordion.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
expander: Existing Expander to add. If None, creates one.
|
|
82
|
+
key: Unique identifier for the expander. Auto-generated if not provided.
|
|
83
|
+
title: Title for the expander header (when creating new).
|
|
84
|
+
icon: Icon for the expander header (when creating new).
|
|
85
|
+
expanded: Initial expansion state. If None, first expander
|
|
86
|
+
is expanded when allow_collapse_all=False, otherwise collapsed.
|
|
87
|
+
**kwargs: When expander is None, passed to Expander constructor.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
Expander: The added or created Expander widget.
|
|
91
|
+
|
|
92
|
+
Raises:
|
|
93
|
+
ValueError: If an expander with the same key already exists.
|
|
94
|
+
"""
|
|
95
|
+
# Auto-generate key if not provided
|
|
96
|
+
if key is None:
|
|
97
|
+
key = f"expander_{self._counter}"
|
|
98
|
+
self._counter += 1
|
|
99
|
+
|
|
100
|
+
if key in self._expanders:
|
|
101
|
+
raise ValueError(f"An expander with the key '{key}' already exists.")
|
|
102
|
+
|
|
103
|
+
# Add separator before this expander (if not the first)
|
|
104
|
+
if self._show_separators and len(self._expanders) > 0:
|
|
105
|
+
sep = Separator(self, orient='horizontal')
|
|
106
|
+
sep.pack(fill='x')
|
|
107
|
+
self._separator_widgets.append(sep)
|
|
108
|
+
|
|
109
|
+
# Create expander if not provided
|
|
110
|
+
if expander is None:
|
|
111
|
+
# Determine initial expanded state
|
|
112
|
+
if expanded is None:
|
|
113
|
+
# If not collapsible and this is the first, expand it
|
|
114
|
+
if not self._allow_collapse_all and len(self._expanders) == 0:
|
|
115
|
+
expanded = True
|
|
116
|
+
else:
|
|
117
|
+
expanded = False
|
|
118
|
+
|
|
119
|
+
# Get accent/variant from kwargs or use accordion defaults
|
|
120
|
+
accent = self._accent or kwargs.pop('accent', None)
|
|
121
|
+
variant = self._variant or kwargs.pop('variant', None)
|
|
122
|
+
|
|
123
|
+
expander = Expander(
|
|
124
|
+
self,
|
|
125
|
+
title=title,
|
|
126
|
+
icon=icon,
|
|
127
|
+
expanded=expanded,
|
|
128
|
+
highlight=True,
|
|
129
|
+
accent=accent,
|
|
130
|
+
variant=variant,
|
|
131
|
+
**kwargs
|
|
132
|
+
)
|
|
133
|
+
else:
|
|
134
|
+
# Configure existing expander
|
|
135
|
+
expander.configure(highlight=False)
|
|
136
|
+
if expanded is not None:
|
|
137
|
+
if expanded:
|
|
138
|
+
expander.expand()
|
|
139
|
+
else:
|
|
140
|
+
expander.collapse()
|
|
141
|
+
|
|
142
|
+
expander.pack(fill='x')
|
|
143
|
+
self._expanders[key] = expander
|
|
144
|
+
self._expander_order.append(key)
|
|
145
|
+
|
|
146
|
+
# Bind to toggle events
|
|
147
|
+
expander.on_toggled(lambda e, exp=expander: self._on_expander_toggle(exp, e))
|
|
148
|
+
|
|
149
|
+
return expander
|
|
150
|
+
|
|
151
|
+
def remove(self, key: str) -> None:
|
|
152
|
+
"""Remove an expander from the accordion.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
key: The key of the expander to remove.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
KeyError: If no expander with the given key exists.
|
|
159
|
+
|
|
160
|
+
Note:
|
|
161
|
+
The expander widget is destroyed. If allow_collapse_all=False and
|
|
162
|
+
removing would leave no expanders, or would remove the only
|
|
163
|
+
open expander, another expander will be expanded automatically.
|
|
164
|
+
"""
|
|
165
|
+
if key not in self._expanders:
|
|
166
|
+
raise KeyError(f"No expander with key '{key}'")
|
|
167
|
+
|
|
168
|
+
expander = self._expanders[key]
|
|
169
|
+
index = self._expander_order.index(key)
|
|
170
|
+
was_expanded = expander['expanded']
|
|
171
|
+
|
|
172
|
+
# Remove associated separator
|
|
173
|
+
if self._show_separators and self._separator_widgets:
|
|
174
|
+
if index > 0:
|
|
175
|
+
# Remove the separator before this expander
|
|
176
|
+
sep_index = index - 1
|
|
177
|
+
elif len(self._expanders) > 1:
|
|
178
|
+
# Removing first expander - remove the separator after it
|
|
179
|
+
sep_index = 0
|
|
180
|
+
else:
|
|
181
|
+
sep_index = None
|
|
182
|
+
|
|
183
|
+
if sep_index is not None and sep_index < len(self._separator_widgets):
|
|
184
|
+
sep = self._separator_widgets.pop(sep_index)
|
|
185
|
+
sep.destroy()
|
|
186
|
+
|
|
187
|
+
# Remove expander from dict/list and destroy
|
|
188
|
+
del self._expanders[key]
|
|
189
|
+
self._expander_order.remove(key)
|
|
190
|
+
expander.destroy()
|
|
191
|
+
|
|
192
|
+
# Handle allow_collapse_all=False constraint
|
|
193
|
+
if not self._allow_collapse_all and was_expanded and self._expanders:
|
|
194
|
+
# The removed expander was open - need to open another
|
|
195
|
+
expander_list = [self._expanders[k] for k in self._expander_order]
|
|
196
|
+
any_open = any(exp['expanded'] for exp in expander_list)
|
|
197
|
+
if not any_open:
|
|
198
|
+
expander_list[0].expand()
|
|
199
|
+
|
|
200
|
+
# Fire change event
|
|
201
|
+
if self._expanders:
|
|
202
|
+
expander_list = [self._expanders[k] for k in self._expander_order]
|
|
203
|
+
self.event_generate('<<AccordionChange>>', data=AccordionChangeEvent(
|
|
204
|
+
expanded=tuple(exp for exp in expander_list if exp['expanded'])
|
|
205
|
+
))
|
|
206
|
+
|
|
207
|
+
def _on_expander_toggle(self, expander: Expander, event):
|
|
208
|
+
"""Handle expander toggle events."""
|
|
209
|
+
if self._updating:
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
self._updating = True
|
|
213
|
+
try:
|
|
214
|
+
is_expanded = event.data.expanded
|
|
215
|
+
expander_list = [self._expanders[k] for k in self._expander_order]
|
|
216
|
+
|
|
217
|
+
if is_expanded:
|
|
218
|
+
# Expander was just opened
|
|
219
|
+
if not self._allow_multiple:
|
|
220
|
+
# Collapse all others
|
|
221
|
+
for exp in expander_list:
|
|
222
|
+
if exp is not expander and exp['expanded']:
|
|
223
|
+
exp.collapse()
|
|
224
|
+
else:
|
|
225
|
+
# Expander was just closed
|
|
226
|
+
if not self._allow_collapse_all:
|
|
227
|
+
# Check if any are still open
|
|
228
|
+
any_open = any(exp['expanded'] for exp in expander_list)
|
|
229
|
+
if not any_open:
|
|
230
|
+
# Re-open this one - can't collapse all
|
|
231
|
+
expander.expand()
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
# Fire change event
|
|
235
|
+
self.event_generate('<<AccordionChange>>', data=AccordionChangeEvent(
|
|
236
|
+
expanded=tuple(exp for exp in expander_list if exp['expanded'])
|
|
237
|
+
))
|
|
238
|
+
finally:
|
|
239
|
+
self._updating = False
|
|
240
|
+
|
|
241
|
+
def item(self, key: str) -> Expander:
|
|
242
|
+
"""Get an expander by its key.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
key: The key of the expander to retrieve.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
The Expander instance.
|
|
249
|
+
|
|
250
|
+
Raises:
|
|
251
|
+
KeyError: If no expander with the given key exists.
|
|
252
|
+
"""
|
|
253
|
+
if key not in self._expanders:
|
|
254
|
+
raise KeyError(f"No expander with key '{key}'")
|
|
255
|
+
return self._expanders[key]
|
|
256
|
+
|
|
257
|
+
def items(self, expanded: bool | None = None) -> tuple[Expander, ...]:
|
|
258
|
+
"""Get expander widgets in insertion order, optionally filtered by state.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
expanded: If `True`, return only expanded expanders. If `False`,
|
|
262
|
+
return only collapsed expanders. If `None` (default), return all.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
A tuple of matching Expander instances.
|
|
266
|
+
"""
|
|
267
|
+
expanders = (self._expanders[key] for key in self._expander_order)
|
|
268
|
+
if expanded is None:
|
|
269
|
+
return tuple(expanders)
|
|
270
|
+
return tuple(e for e in expanders if bool(e['expanded']) == expanded)
|
|
271
|
+
|
|
272
|
+
def keys(self) -> tuple[str, ...]:
|
|
273
|
+
"""Get all expander keys in order.
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
A tuple of all expander keys in the order they were added.
|
|
277
|
+
"""
|
|
278
|
+
return tuple(self._expander_order)
|
|
279
|
+
|
|
280
|
+
def configure_item(self, key: str, option: str = None, **kwargs: Any):
|
|
281
|
+
"""Configure a specific expander by its key.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
key: The key of the expander to configure.
|
|
285
|
+
option: If provided, return the value of this option.
|
|
286
|
+
**kwargs: Configuration options to apply to the expander.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
If option is provided, returns the value of that option.
|
|
290
|
+
"""
|
|
291
|
+
expander = self.item(key)
|
|
292
|
+
if option is not None:
|
|
293
|
+
return expander.cget(option)
|
|
294
|
+
expander.configure(**kwargs)
|
|
295
|
+
|
|
296
|
+
def expand(self, key: str):
|
|
297
|
+
"""Expand the expander with the given key.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
key: Key of the expander to expand.
|
|
301
|
+
"""
|
|
302
|
+
if key in self._expanders:
|
|
303
|
+
self._expanders[key].expand()
|
|
304
|
+
|
|
305
|
+
def collapse(self, key: str):
|
|
306
|
+
"""Collapse the expander with the given key.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
key: Key of the expander to collapse.
|
|
310
|
+
|
|
311
|
+
Note:
|
|
312
|
+
If allow_collapse_all=False and this is the only open expander,
|
|
313
|
+
this call will be ignored.
|
|
314
|
+
"""
|
|
315
|
+
if key in self._expanders:
|
|
316
|
+
exp = self._expanders[key]
|
|
317
|
+
if not self._allow_collapse_all:
|
|
318
|
+
# Check if this is the only open one
|
|
319
|
+
expander_list = [self._expanders[k] for k in self._expander_order]
|
|
320
|
+
open_count = sum(1 for e in expander_list if e['expanded'])
|
|
321
|
+
if open_count <= 1 and exp['expanded']:
|
|
322
|
+
return # Can't collapse the last one
|
|
323
|
+
exp.collapse()
|
|
324
|
+
|
|
325
|
+
def expand_all(self):
|
|
326
|
+
"""Expand all expanders (only effective when allow_multiple=True)."""
|
|
327
|
+
if not self._allow_multiple:
|
|
328
|
+
return
|
|
329
|
+
for exp in self._expanders.values():
|
|
330
|
+
exp.expand()
|
|
331
|
+
|
|
332
|
+
def collapse_all(self):
|
|
333
|
+
"""Collapse all expanders (only effective when allow_collapse_all=True)."""
|
|
334
|
+
if not self._allow_collapse_all:
|
|
335
|
+
return
|
|
336
|
+
for exp in self._expanders.values():
|
|
337
|
+
exp.collapse()
|
|
338
|
+
|
|
339
|
+
@configure_delegate('allow_multiple')
|
|
340
|
+
def _delegate_allow_multiple(self, value=None):
|
|
341
|
+
"""Get or set whether multiple sections can be open at once."""
|
|
342
|
+
if value is None:
|
|
343
|
+
return self._allow_multiple
|
|
344
|
+
self._allow_multiple = value
|
|
345
|
+
return None
|
|
346
|
+
|
|
347
|
+
@configure_delegate('allow_collapse_all')
|
|
348
|
+
def _delegate_allow_collapse_all(self, value=None):
|
|
349
|
+
"""Get or set whether all sections can be collapsed."""
|
|
350
|
+
if value is None:
|
|
351
|
+
return self._allow_collapse_all
|
|
352
|
+
self._allow_collapse_all = value
|
|
353
|
+
return None
|
|
354
|
+
|
|
355
|
+
@configure_delegate('show_separators')
|
|
356
|
+
def _delegate_show_separators(self, value=None):
|
|
357
|
+
"""Get or set whether separators are shown between sections."""
|
|
358
|
+
if value is None:
|
|
359
|
+
return self._show_separators
|
|
360
|
+
self._show_separators = value
|
|
361
|
+
return None
|
|
362
|
+
|
|
363
|
+
def on_accordion_changed(self, callback: Callable[[AccordionChangeEvent], None]) -> str:
|
|
364
|
+
"""Register a callback for `<<AccordionChange>>` events.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
callback: Receives an `AccordionChangeEvent` payload with attribute
|
|
368
|
+
`expanded` (list of currently open Expanders).
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Bind ID — pass to `off_accordion_changed()` to unsubscribe.
|
|
372
|
+
"""
|
|
373
|
+
return self.bind('<<AccordionChange>>', callback, add='+')
|
|
374
|
+
|
|
375
|
+
def off_accordion_changed(self, bind_id: str | None = None) -> None:
|
|
376
|
+
"""Unsubscribe from `<<AccordionChange>>`.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
bind_id: ID returned by `on_accordion_changed()`. If None, removes all.
|
|
380
|
+
"""
|
|
381
|
+
self.unbind('<<AccordionChange>>', bind_id)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Internal Avatar composite — a small identity badge.
|
|
2
|
+
|
|
3
|
+
Shows either an image (cover-fit, clipped to the avatar shape) or initials on a
|
|
4
|
+
filled tile. Canvas-backed: the image path reuses the shared `_image_fit`
|
|
5
|
+
helpers, and initials are drawn as Tk canvas text (so they use the framework's
|
|
6
|
+
fonts cross-platform — PIL has no bundled text font). The public
|
|
7
|
+
`bootstack.Avatar` wrapper drives this.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from tkinter import Canvas, TclError
|
|
13
|
+
from typing import Any, Literal
|
|
14
|
+
|
|
15
|
+
from PIL import Image as PILImage, ImageDraw
|
|
16
|
+
from PIL.Image import Resampling
|
|
17
|
+
from PIL.ImageTk import PhotoImage
|
|
18
|
+
|
|
19
|
+
from bootstack.style.style import get_style
|
|
20
|
+
from bootstack.widgets._impl.composites._image_fit import fit_image, resolve_pil, round_corners
|
|
21
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
22
|
+
from bootstack.widgets.types import Master
|
|
23
|
+
|
|
24
|
+
Shape = Literal["circle", "rounded", "square"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _initials_from(name: str | None, initials: str | None) -> str:
|
|
28
|
+
"""Resolve the initials to show: explicit override, else derived from `name`."""
|
|
29
|
+
if initials:
|
|
30
|
+
return initials.strip()[:2].upper()
|
|
31
|
+
if name:
|
|
32
|
+
parts = [p for p in name.split() if p]
|
|
33
|
+
if not parts:
|
|
34
|
+
return ""
|
|
35
|
+
if len(parts) == 1:
|
|
36
|
+
return parts[0][:2].upper()
|
|
37
|
+
return (parts[0][0] + parts[-1][0]).upper()
|
|
38
|
+
return ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Avatar(Frame):
|
|
42
|
+
"""Canvas-backed avatar — an image or initials on a filled tile."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
master: Master = None,
|
|
47
|
+
*,
|
|
48
|
+
source: Any = None,
|
|
49
|
+
name: str | None = None,
|
|
50
|
+
initials: str | None = None,
|
|
51
|
+
size: int = 40,
|
|
52
|
+
shape: Shape = "circle",
|
|
53
|
+
background: str = "primary",
|
|
54
|
+
foreground: str = "white",
|
|
55
|
+
**kwargs: Any,
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__(master, **kwargs)
|
|
58
|
+
|
|
59
|
+
self._size = max(8, int(size))
|
|
60
|
+
self._shape: Shape = shape
|
|
61
|
+
self._background = background
|
|
62
|
+
self._foreground = foreground
|
|
63
|
+
self._source = source
|
|
64
|
+
self._name = name
|
|
65
|
+
self._initials = initials
|
|
66
|
+
self._photo: PhotoImage | None = None # strong ref (Tk GC)
|
|
67
|
+
|
|
68
|
+
self._canvas = Canvas(self, width=self._size, height=self._size,
|
|
69
|
+
highlightthickness=0, bd=0,
|
|
70
|
+
background=self._surface_color())
|
|
71
|
+
self._canvas.pack()
|
|
72
|
+
self._canvas.bind("<Button-1>", self._forward_click)
|
|
73
|
+
|
|
74
|
+
self._render()
|
|
75
|
+
|
|
76
|
+
# ----- helpers -----------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
def _surface_color(self) -> str:
|
|
79
|
+
token = getattr(self, "_surface", None) or "background"
|
|
80
|
+
b = get_style().style_builder
|
|
81
|
+
try:
|
|
82
|
+
return b.color(token)
|
|
83
|
+
except Exception:
|
|
84
|
+
return b.color("background")
|
|
85
|
+
|
|
86
|
+
def _color(self, token: str) -> str:
|
|
87
|
+
try:
|
|
88
|
+
return get_style().style_builder.color(token)
|
|
89
|
+
except Exception:
|
|
90
|
+
return token
|
|
91
|
+
|
|
92
|
+
def _radius(self) -> int:
|
|
93
|
+
if self._shape == "circle":
|
|
94
|
+
return self._size // 2
|
|
95
|
+
if self._shape == "rounded":
|
|
96
|
+
return max(1, int(self._size * 0.25))
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
# ----- rendering ---------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def _render(self) -> None:
|
|
102
|
+
self._canvas.delete("all")
|
|
103
|
+
pil = resolve_pil(self._source) if self._source is not None else None
|
|
104
|
+
if pil is not None:
|
|
105
|
+
self._render_image(pil)
|
|
106
|
+
else:
|
|
107
|
+
self._render_initials()
|
|
108
|
+
|
|
109
|
+
def _render_image(self, pil: PILImage.Image) -> None:
|
|
110
|
+
composed = fit_image(pil.convert("RGBA"), self._size, self._size, "cover")
|
|
111
|
+
radius = self._radius()
|
|
112
|
+
if radius > 0:
|
|
113
|
+
composed = round_corners(composed, radius)
|
|
114
|
+
self._photo = PhotoImage(composed)
|
|
115
|
+
self._canvas.create_image(self._size // 2, self._size // 2,
|
|
116
|
+
image=self._photo, anchor="center")
|
|
117
|
+
|
|
118
|
+
def _render_initials(self) -> None:
|
|
119
|
+
# Filled tile (antialiased) in the background color, clipped to the shape.
|
|
120
|
+
ss = 4
|
|
121
|
+
s = self._size
|
|
122
|
+
big = PILImage.new("RGBA", (s * ss, s * ss), (0, 0, 0, 0))
|
|
123
|
+
fill = _hex_or_name_to_rgba(self._color(self._background))
|
|
124
|
+
ImageDraw.Draw(big).rounded_rectangle(
|
|
125
|
+
[0, 0, s * ss - 1, s * ss - 1], radius=self._radius() * ss, fill=fill
|
|
126
|
+
)
|
|
127
|
+
self._photo = PhotoImage(big.resize((s, s), Resampling.LANCZOS))
|
|
128
|
+
self._canvas.create_image(s // 2, s // 2, image=self._photo, anchor="center")
|
|
129
|
+
|
|
130
|
+
text = _initials_from(self._name, self._initials)
|
|
131
|
+
if text:
|
|
132
|
+
font_size = max(8, int(s * 0.4))
|
|
133
|
+
self._canvas.create_text(
|
|
134
|
+
s // 2, s // 2, text=text, fill=self._text_color(),
|
|
135
|
+
font=(self._ui_family(), font_size, "bold"), anchor="center",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
def _text_color(self) -> str:
|
|
139
|
+
"""The resolved foreground color, validated as a Tk color (else white)."""
|
|
140
|
+
color = self._color(self._foreground)
|
|
141
|
+
try:
|
|
142
|
+
self._canvas.winfo_rgb(color) # authoritative Tk color parse
|
|
143
|
+
return color
|
|
144
|
+
except TclError:
|
|
145
|
+
return "#ffffff"
|
|
146
|
+
|
|
147
|
+
@staticmethod
|
|
148
|
+
def _ui_family() -> str:
|
|
149
|
+
"""The framework's configured UI font family.
|
|
150
|
+
|
|
151
|
+
bootstack overrides `TkDefaultFont` to its body font, so reading its
|
|
152
|
+
actual family gives the app's UI font for the initials text.
|
|
153
|
+
"""
|
|
154
|
+
from tkinter import font as tkfont
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
return tkfont.nametofont("TkDefaultFont").actual("family")
|
|
158
|
+
except Exception:
|
|
159
|
+
return ""
|
|
160
|
+
|
|
161
|
+
def _bs_apply_theme(self, _event=None) -> None:
|
|
162
|
+
try:
|
|
163
|
+
self._canvas.configure(background=self._surface_color())
|
|
164
|
+
except TclError:
|
|
165
|
+
return
|
|
166
|
+
self._render()
|
|
167
|
+
|
|
168
|
+
def _forward_click(self, event) -> None:
|
|
169
|
+
self.event_generate("<<AvatarClick>>", x=event.x, y=event.y)
|
|
170
|
+
|
|
171
|
+
# ----- live updates ------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
def set_source(self, source: Any) -> None:
|
|
174
|
+
self._source = source
|
|
175
|
+
self._render()
|
|
176
|
+
|
|
177
|
+
def set_text(self, *, name: str | None = None, initials: str | None = None) -> None:
|
|
178
|
+
self._name = name
|
|
179
|
+
self._initials = initials
|
|
180
|
+
self._render()
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _hex_or_name_to_rgba(color: str) -> tuple[int, int, int, int]:
|
|
184
|
+
"""Parse a resolved color (hex or name) to an opaque RGBA tuple for PIL fill."""
|
|
185
|
+
from PIL import ImageColor
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
r, g, b = ImageColor.getrgb(color)[:3]
|
|
189
|
+
return (r, g, b, 255)
|
|
190
|
+
except (ValueError, TypeError):
|
|
191
|
+
return (108, 117, 125, 255) # neutral gray fallback
|