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,353 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable, Literal, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from bootstack.errors import BootstackError
|
|
6
|
+
from bootstack.validation import RuleType
|
|
7
|
+
from bootstack.validation.validation_rules import rule_applies_to_kind
|
|
8
|
+
from bootstack.widgets.types import AccentToken
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from bootstack.signals import Signal
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FieldAddonMixin:
|
|
15
|
+
"""Mixin that exposes `insert_addon` and `addons` on public field wrappers.
|
|
16
|
+
|
|
17
|
+
Requires the inheriting class to have `self._internal` pointing to a
|
|
18
|
+
`Field`-based composite that implements `insert_addon` / `addons`.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
#: The kind of value this field holds, used to reject inapplicable
|
|
22
|
+
#: validation rules at attach time. Text fields keep the default; typed
|
|
23
|
+
#: field wrappers override it (`'number'`, `'date'`, `'time'`).
|
|
24
|
+
_VALIDATION_KIND: str = "text"
|
|
25
|
+
|
|
26
|
+
_ADDON_TYPES = {
|
|
27
|
+
"button": "bootstack.widgets._impl.primitives.button::Button",
|
|
28
|
+
"label": "bootstack.widgets._impl.primitives.label::Label",
|
|
29
|
+
"toggle": "bootstack.widgets._impl.primitives.checktoggle::CheckToggle",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def insert_addon(
|
|
33
|
+
self,
|
|
34
|
+
widget: Literal["button", "label", "toggle"],
|
|
35
|
+
position: Literal["before", "after"],
|
|
36
|
+
*,
|
|
37
|
+
name: str | None = None,
|
|
38
|
+
text: str | None = None,
|
|
39
|
+
icon: str | None = None,
|
|
40
|
+
accent: AccentToken | str | None = None,
|
|
41
|
+
on_click: Callable[[], Any] | None = None,
|
|
42
|
+
signal: "Signal | None" = None,
|
|
43
|
+
active_when_readonly: bool = False,
|
|
44
|
+
) -> Any:
|
|
45
|
+
"""Insert a small widget inside the field border, before or after the input.
|
|
46
|
+
|
|
47
|
+
Use this for affordances such as a clear button, a search icon, a unit
|
|
48
|
+
suffix label, or an on/off toggle.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
widget: Addon type — `'button'` (clickable), `'label'` (static
|
|
52
|
+
text or icon), or `'toggle'` (on/off control).
|
|
53
|
+
position: `'before'` (left of the input) or `'after'` (right).
|
|
54
|
+
name: Key to retrieve the addon later via `addons`. Auto-generated
|
|
55
|
+
if omitted.
|
|
56
|
+
text: Text shown on the addon. Applies to any addon type.
|
|
57
|
+
icon: Bootstrap Icons name shown on the addon (e.g. `'search'`,
|
|
58
|
+
`'x-lg'`). An icon-only addon is rendered when `icon` is given
|
|
59
|
+
without `text`.
|
|
60
|
+
accent: Accent token for the addon. Prefer an accent for a
|
|
61
|
+
text-only button.
|
|
62
|
+
on_click: Called with no arguments when a `'button'` or `'toggle'`
|
|
63
|
+
addon is activated.
|
|
64
|
+
signal: Reactive `Signal[bool]` bound to a `'toggle'` addon's
|
|
65
|
+
on/off state.
|
|
66
|
+
active_when_readonly: Keep the addon interactive while the field is
|
|
67
|
+
read-only. Off by default, so addons follow the field's
|
|
68
|
+
read-only state — appropriate since most act on the value (a
|
|
69
|
+
clear button, the spin buttons). Set `True` only for a
|
|
70
|
+
read-only-safe action such as a copy or reveal button; it still
|
|
71
|
+
dims when the field is fully disabled.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The created addon widget instance.
|
|
75
|
+
"""
|
|
76
|
+
if widget not in self._ADDON_TYPES:
|
|
77
|
+
raise ValueError(f"widget must be 'button', 'label', or 'toggle'; got {widget!r}")
|
|
78
|
+
if on_click is not None and widget == "label":
|
|
79
|
+
raise ValueError("on_click is not valid for a 'label' addon")
|
|
80
|
+
if signal is not None and widget != "toggle":
|
|
81
|
+
raise ValueError("signal is only valid for a 'toggle' addon")
|
|
82
|
+
|
|
83
|
+
module_path, cls_name = self._ADDON_TYPES[widget].split("::")
|
|
84
|
+
import importlib
|
|
85
|
+
cls = getattr(importlib.import_module(module_path), cls_name)
|
|
86
|
+
|
|
87
|
+
if name is None:
|
|
88
|
+
seq = getattr(self, "_addon_seq", 0)
|
|
89
|
+
self._addon_seq = seq + 1
|
|
90
|
+
name = f"addon_{seq}"
|
|
91
|
+
|
|
92
|
+
addon_kwargs: dict[str, Any] = {}
|
|
93
|
+
if text is not None:
|
|
94
|
+
addon_kwargs["text"] = text
|
|
95
|
+
if icon is not None:
|
|
96
|
+
addon_kwargs["icon"] = icon
|
|
97
|
+
if on_click is not None:
|
|
98
|
+
addon_kwargs["command"] = on_click
|
|
99
|
+
if signal is not None:
|
|
100
|
+
addon_kwargs["signal"] = signal
|
|
101
|
+
|
|
102
|
+
return self._internal.insert_addon(
|
|
103
|
+
cls, position, name=name, accent=accent,
|
|
104
|
+
active_when_readonly=active_when_readonly, **addon_kwargs,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def update_addon(
|
|
108
|
+
self,
|
|
109
|
+
name: str,
|
|
110
|
+
*,
|
|
111
|
+
text: str | None = None,
|
|
112
|
+
icon: str | None = None,
|
|
113
|
+
accent: AccentToken | str | None = None,
|
|
114
|
+
on_click: Callable[[], Any] | None = None,
|
|
115
|
+
) -> None:
|
|
116
|
+
"""Reconfigure an existing addon in place.
|
|
117
|
+
|
|
118
|
+
Only the options you pass are changed. Use this, for example, to flip a
|
|
119
|
+
toggle addon's label between two units, or swap a button's icon.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
name: The addon's name (as passed to `insert_addon`).
|
|
123
|
+
text: New text for the addon.
|
|
124
|
+
icon: New Bootstrap Icons name for the addon.
|
|
125
|
+
accent: New accent token for the addon.
|
|
126
|
+
on_click: New click handler for a `'button'` or `'toggle'` addon.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
KeyError: If no addon with that name exists.
|
|
130
|
+
"""
|
|
131
|
+
try:
|
|
132
|
+
addon = self._internal.addons[name]
|
|
133
|
+
except KeyError:
|
|
134
|
+
raise KeyError(f"no addon named {name!r}") from None
|
|
135
|
+
config: dict[str, Any] = {}
|
|
136
|
+
if text is not None:
|
|
137
|
+
config["text"] = text
|
|
138
|
+
if icon is not None:
|
|
139
|
+
config["icon"] = icon
|
|
140
|
+
if accent is not None:
|
|
141
|
+
config["accent"] = accent
|
|
142
|
+
if on_click is not None:
|
|
143
|
+
config["command"] = on_click
|
|
144
|
+
if config:
|
|
145
|
+
addon.configure(**config)
|
|
146
|
+
|
|
147
|
+
def remove_addon(self, name: str) -> None:
|
|
148
|
+
"""Remove an addon inserted with `insert_addon()`.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
name: The addon's name (as passed to `insert_addon`).
|
|
152
|
+
|
|
153
|
+
Raises:
|
|
154
|
+
KeyError: If no addon with that name exists.
|
|
155
|
+
"""
|
|
156
|
+
self._internal.remove_addon(name)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def addons(self) -> dict[str, Any]:
|
|
160
|
+
"""Named addon widgets inserted via `insert_addon()`."""
|
|
161
|
+
return self._internal.addons
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def text(self) -> str:
|
|
165
|
+
"""The current display text shown in the field.
|
|
166
|
+
|
|
167
|
+
This is the formatted string the user sees, as opposed to `value`, which
|
|
168
|
+
is the raw parsed datum (e.g. for a `DateField`, `text` is `'Jan 2, 2024'`
|
|
169
|
+
while `value` is a `date`). Read-only — assign to `value` to change it.
|
|
170
|
+
"""
|
|
171
|
+
return self._internal.get()
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
def valid(self) -> "Signal":
|
|
175
|
+
"""Reactive `Signal[bool]` — whether the field currently passes validation.
|
|
176
|
+
|
|
177
|
+
Call it to read the current state (`field.valid()`), `subscribe` to
|
|
178
|
+
react, or bind it. Updated on every validation run — a blur, a keystroke
|
|
179
|
+
(for key-triggered rules), or a manual `validate()`.
|
|
180
|
+
"""
|
|
181
|
+
return self._internal._entry._valid_signal
|
|
182
|
+
|
|
183
|
+
@property
|
|
184
|
+
def error(self) -> "Signal":
|
|
185
|
+
"""Reactive `Signal[str]` — the current validation error, `''` when valid.
|
|
186
|
+
|
|
187
|
+
Bind it to surface the message anywhere, for example
|
|
188
|
+
`bs.Label(textsignal=field.error)`.
|
|
189
|
+
"""
|
|
190
|
+
return self._internal._entry._error_signal
|
|
191
|
+
|
|
192
|
+
def add_validation_rule(self, rule_type: RuleType, **kwargs: Any) -> None:
|
|
193
|
+
"""Add a validation rule to the field.
|
|
194
|
+
|
|
195
|
+
Rules run automatically on blur or key events depending on the rule
|
|
196
|
+
type, or manually via `validate()`. Multiple rules can be added;
|
|
197
|
+
they are evaluated in order and stop at the first failure.
|
|
198
|
+
|
|
199
|
+
Rules validate the field's **typed value** — a `custom` rule on a
|
|
200
|
+
numeric field receives a number, on a date field a `date`. Text rules
|
|
201
|
+
(`stringLength`, `pattern`, `email`) apply only to text fields; `range`
|
|
202
|
+
applies only to numeric/date/time fields. Attaching a rule to a field
|
|
203
|
+
whose value kind it cannot validate raises `BootstackError`.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
rule_type: The kind of validation rule to apply.
|
|
207
|
+
**kwargs: Rule-specific options:
|
|
208
|
+
|
|
209
|
+
- `message` *(all rules)* — override the default error message.
|
|
210
|
+
- `trigger` *(all rules)* — when to run: `'always'` (key and
|
|
211
|
+
blur), `'key'`, `'blur'`, or `'manual'`. Each rule type
|
|
212
|
+
has its own sensible default (see the Validation reference).
|
|
213
|
+
- `min`, `max` *(stringLength)* — minimum/maximum character count.
|
|
214
|
+
- `min`, `max` *(range)* — lower/upper bound for the typed value
|
|
215
|
+
(numbers, or `date`/`time` objects).
|
|
216
|
+
- `pattern` *(pattern)* — regex string the value must match.
|
|
217
|
+
- `other_field` *(compare)* — field whose value must match.
|
|
218
|
+
- `func` *(custom)* — callable `(value) -> bool` receiving the
|
|
219
|
+
field's typed value.
|
|
220
|
+
|
|
221
|
+
Example::
|
|
222
|
+
|
|
223
|
+
field.add_validation_rule("required")
|
|
224
|
+
field.add_validation_rule("stringLength", min=3, max=50)
|
|
225
|
+
field.add_validation_rule("email", message="Enter a valid email.")
|
|
226
|
+
field.add_validation_rule("custom", func=lambda v: v.isdigit())
|
|
227
|
+
|
|
228
|
+
age.add_validation_rule("range", min=18, message="Must be 18+.")
|
|
229
|
+
"""
|
|
230
|
+
if not isinstance(rule_type, str):
|
|
231
|
+
raise TypeError(
|
|
232
|
+
f"add_validation_rule() takes a rule-type string (e.g. "
|
|
233
|
+
f"'stringLength'), not a {type(rule_type).__name__} object. Pass "
|
|
234
|
+
f"the rule's options as keyword arguments: "
|
|
235
|
+
f"field.add_validation_rule('stringLength', min=3)."
|
|
236
|
+
)
|
|
237
|
+
kind = self._VALIDATION_KIND
|
|
238
|
+
if not rule_applies_to_kind(rule_type, kind):
|
|
239
|
+
if kind == "text":
|
|
240
|
+
hint = "'stringLength' bounds text length."
|
|
241
|
+
else:
|
|
242
|
+
hint = f"'range' or 'custom' validates {kind} values."
|
|
243
|
+
raise BootstackError(
|
|
244
|
+
f"the {rule_type!r} rule does not apply to a {kind} field "
|
|
245
|
+
f"({type(self).__name__}). {hint}"
|
|
246
|
+
)
|
|
247
|
+
self._internal.add_validation_rule(rule_type, **kwargs)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class ValueSignalMixin:
|
|
251
|
+
"""Two-way binds a field's typed `.value` to a public `signal`.
|
|
252
|
+
|
|
253
|
+
The signal carries the field's **value** (a number, date, time, …) — not its
|
|
254
|
+
text. The inheriting wrapper must expose a `value` property (get and set) and
|
|
255
|
+
an `on_change` event; this mixin keeps the two in sync, seeding the field from
|
|
256
|
+
the signal and updating the signal when the field commits a new value.
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
value: Any
|
|
260
|
+
on_change: Callable
|
|
261
|
+
|
|
262
|
+
def _bind_value_signal(self, signal: "Signal") -> None:
|
|
263
|
+
"""Establish the two-way binding between `self.value` and `signal`."""
|
|
264
|
+
self._value_signal = signal
|
|
265
|
+
self._value_syncing = False
|
|
266
|
+
|
|
267
|
+
current = signal()
|
|
268
|
+
if current is not None:
|
|
269
|
+
self.value = current
|
|
270
|
+
else:
|
|
271
|
+
self._push_to_signal(self.value)
|
|
272
|
+
|
|
273
|
+
def _from_signal(v: Any) -> None:
|
|
274
|
+
if self._value_syncing or v is None:
|
|
275
|
+
return
|
|
276
|
+
self._value_syncing = True
|
|
277
|
+
try:
|
|
278
|
+
self.value = v
|
|
279
|
+
finally:
|
|
280
|
+
self._value_syncing = False
|
|
281
|
+
|
|
282
|
+
def _to_signal(_event: Any) -> None:
|
|
283
|
+
if self._value_syncing:
|
|
284
|
+
return
|
|
285
|
+
v = self.value
|
|
286
|
+
if v is None:
|
|
287
|
+
return
|
|
288
|
+
self._value_syncing = True
|
|
289
|
+
try:
|
|
290
|
+
self._push_to_signal(v)
|
|
291
|
+
finally:
|
|
292
|
+
self._value_syncing = False
|
|
293
|
+
|
|
294
|
+
# Hold the subscription id so it can be released on destroy. A Signal
|
|
295
|
+
# usually outlives the widgets bound to it, so a dangling subscription
|
|
296
|
+
# would pin a destroyed field (and all it references) in memory.
|
|
297
|
+
self._value_sub = signal.subscribe(_from_signal)
|
|
298
|
+
self.on_change(_to_signal)
|
|
299
|
+
self.on_destroy(lambda _e: self._release_value_signal())
|
|
300
|
+
|
|
301
|
+
def _sync_value_set(self, value: Any) -> None:
|
|
302
|
+
"""Push a programmatic `field.value = …` to the bound signal.
|
|
303
|
+
|
|
304
|
+
The `on_change`-driven sync only fires on a user commit, so a
|
|
305
|
+
programmatic value set would otherwise leave the signal stale. Call this
|
|
306
|
+
from the wrapper's `value` setter. A no-op when no signal is bound, while
|
|
307
|
+
a signal↔field sync is in flight, or for an empty value.
|
|
308
|
+
"""
|
|
309
|
+
if getattr(self, "_value_signal", None) is None:
|
|
310
|
+
return
|
|
311
|
+
if getattr(self, "_value_syncing", False) or value is None:
|
|
312
|
+
return
|
|
313
|
+
self._value_syncing = True
|
|
314
|
+
try:
|
|
315
|
+
self._push_to_signal(self.value)
|
|
316
|
+
finally:
|
|
317
|
+
self._value_syncing = False
|
|
318
|
+
|
|
319
|
+
def _push_to_signal(self, value: Any) -> None:
|
|
320
|
+
"""Write `value` to the bound signal, reconciling numeric types.
|
|
321
|
+
|
|
322
|
+
`Signal` is strict about value type (it only widens `int` into a
|
|
323
|
+
`float`-typed signal). A numeric field can produce either an `int` or a
|
|
324
|
+
`float`, so reconcile the two here. If the signal's type is genuinely
|
|
325
|
+
incompatible (for example an `int`-typed signal and a fractional value),
|
|
326
|
+
leave the signal unchanged rather than raising into the Tk event loop.
|
|
327
|
+
"""
|
|
328
|
+
signal = self._value_signal
|
|
329
|
+
target = getattr(signal, "_type", None)
|
|
330
|
+
if target is not None and type(value) is not target:
|
|
331
|
+
if target is float and type(value) is int:
|
|
332
|
+
value = float(value)
|
|
333
|
+
elif target is int and type(value) is float and value.is_integer():
|
|
334
|
+
value = int(value)
|
|
335
|
+
try:
|
|
336
|
+
signal.set(value)
|
|
337
|
+
except TypeError:
|
|
338
|
+
pass
|
|
339
|
+
|
|
340
|
+
def _release_value_signal(self) -> None:
|
|
341
|
+
"""Drop the value-signal subscription (called on widget destroy)."""
|
|
342
|
+
sub = getattr(self, "_value_sub", None)
|
|
343
|
+
if sub is not None:
|
|
344
|
+
try:
|
|
345
|
+
sub.cancel()
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
self._value_sub = None
|
|
349
|
+
|
|
350
|
+
@property
|
|
351
|
+
def signal(self) -> "Signal | None":
|
|
352
|
+
"""The reactive `Signal` bound to this field's value, or `None`."""
|
|
353
|
+
return getattr(self, "_value_signal", None)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Reusable `icon` / `image` property mixins for widgets that accept them.
|
|
2
|
+
|
|
3
|
+
Widgets in the button family take a widget-level `icon=` (a Bootstrap icon name)
|
|
4
|
+
and/or `image=` (an `Image` handle) at construction. These mixins expose those as
|
|
5
|
+
live, settable properties so they can be changed after construction — `image`
|
|
6
|
+
updates in place (no rebuild, no flicker).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, TYPE_CHECKING
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from bootstack.widgets.types import IconSpec
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class IconProperty:
|
|
18
|
+
"""Adds a live `icon` property to a widget whose internal accepts `icon=`."""
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def icon(self) -> "str | IconSpec | None":
|
|
22
|
+
"""The icon shown on the widget, or `None` if none is set.
|
|
23
|
+
|
|
24
|
+
A Bootstrap Icons name, or an `IconSpec` mapping (`{'name', 'size',
|
|
25
|
+
'color'}`) for control over size and color.
|
|
26
|
+
"""
|
|
27
|
+
return self._internal.configure_style_options("icon")
|
|
28
|
+
|
|
29
|
+
@icon.setter
|
|
30
|
+
def icon(self, value: "str | IconSpec | None") -> None:
|
|
31
|
+
self._internal.configure(icon=value)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ImageProperty:
|
|
35
|
+
"""Adds a live `image` property to a widget whose internal accepts `image=`."""
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def image(self) -> Any:
|
|
39
|
+
"""The displayed image handle, or `None`.
|
|
40
|
+
|
|
41
|
+
Assigning an `Image` (from `bootstack.images`) updates the picture in
|
|
42
|
+
place — no rebuild — which avoids the flicker of recreating the widget.
|
|
43
|
+
"""
|
|
44
|
+
return getattr(self, "_image_handle", None)
|
|
45
|
+
|
|
46
|
+
@image.setter
|
|
47
|
+
def image(self, value: Any) -> None:
|
|
48
|
+
from bootstack.widgets._core.image_binding import (
|
|
49
|
+
_release_theme_binding,
|
|
50
|
+
bind_image,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if value is None:
|
|
54
|
+
# Drop any theme-following binding from a previous Image, otherwise it
|
|
55
|
+
# keeps re-applying a now-removed image on theme changes (and pins it).
|
|
56
|
+
_release_theme_binding(self)
|
|
57
|
+
self._internal.configure(image="")
|
|
58
|
+
self._image_handle = None
|
|
59
|
+
self._image_photo = None
|
|
60
|
+
return
|
|
61
|
+
from bootstack.images import Image as _ImageHandle
|
|
62
|
+
|
|
63
|
+
if isinstance(value, _ImageHandle):
|
|
64
|
+
bind_image(self, self._internal, value)
|
|
65
|
+
else:
|
|
66
|
+
_release_theme_binding(self)
|
|
67
|
+
self._internal.configure(image=value)
|
|
68
|
+
self._image_handle = None
|
|
69
|
+
self._image_photo = value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
__all__ = ["IconProperty", "ImageProperty"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Bind a public image handle onto an internal widget.
|
|
2
|
+
|
|
3
|
+
The `image=` slot on text widgets accepts a public `bootstack.images.Image`
|
|
4
|
+
handle. Because a handle holds no display resource until it is used, this helper
|
|
5
|
+
renders it against the target widget and, when the handle follows the theme (a
|
|
6
|
+
token-colored icon), re-renders it whenever the theme changes — releasing the
|
|
7
|
+
binding when the widget is destroyed.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import tkinter as tk
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from bootstack.images import Image
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def bind_image(public_widget, internal_widget, image: "Image") -> None:
|
|
20
|
+
"""Render `image` onto `internal_widget`, following the theme if needed.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
public_widget: The public wrapper; strong references to the handle and
|
|
24
|
+
its rendered image are stored on it so they are not collected.
|
|
25
|
+
internal_widget: The underlying widget to display the image on.
|
|
26
|
+
image: The image handle to render and bind.
|
|
27
|
+
"""
|
|
28
|
+
# Calling this again on the same widget (a live `image=` update) must not
|
|
29
|
+
# stack theme bindings — release any prior one first.
|
|
30
|
+
_release_theme_binding(public_widget)
|
|
31
|
+
|
|
32
|
+
photo = image._materialize(internal_widget)
|
|
33
|
+
internal_widget.configure(image=photo)
|
|
34
|
+
public_widget._image_handle = image
|
|
35
|
+
public_widget._image_photo = photo
|
|
36
|
+
|
|
37
|
+
if not image._is_theme_following:
|
|
38
|
+
return
|
|
39
|
+
|
|
40
|
+
root = internal_widget.winfo_toplevel()
|
|
41
|
+
|
|
42
|
+
def _reapply(_event=None) -> None:
|
|
43
|
+
try:
|
|
44
|
+
image._invalidate()
|
|
45
|
+
new_photo = image._materialize(internal_widget)
|
|
46
|
+
internal_widget.configure(image=new_photo)
|
|
47
|
+
public_widget._image_photo = new_photo
|
|
48
|
+
except tk.TclError:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
bind_id = root.bind("<<BsThemeChanged>>", _reapply, add="+")
|
|
52
|
+
public_widget._image_theme_binding = (root, bind_id)
|
|
53
|
+
|
|
54
|
+
def _release(event) -> None:
|
|
55
|
+
if event.widget is not internal_widget:
|
|
56
|
+
return
|
|
57
|
+
_release_theme_binding(public_widget)
|
|
58
|
+
|
|
59
|
+
internal_widget.bind("<Destroy>", _release, add="+")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _release_theme_binding(public_widget) -> None:
|
|
63
|
+
"""Unbind a previously installed `<<BsThemeChanged>>` handler, if any."""
|
|
64
|
+
prev = getattr(public_widget, "_image_theme_binding", None)
|
|
65
|
+
if prev is None:
|
|
66
|
+
return
|
|
67
|
+
root, bind_id = prev
|
|
68
|
+
try:
|
|
69
|
+
root.unbind("<<BsThemeChanged>>", bind_id)
|
|
70
|
+
except tk.TclError:
|
|
71
|
+
pass
|
|
72
|
+
public_widget._image_theme_binding = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def resolve_window_icon(icon):
|
|
76
|
+
"""Normalize a window `icon=` argument to `(path, deferred_image)`.
|
|
77
|
+
|
|
78
|
+
Exactly one of the returned values is non-`None` (or both are, for `None`
|
|
79
|
+
input). A `path` can be handed to the window at construction; a deferred
|
|
80
|
+
image must be rendered and applied after the window's root exists.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
icon: An icon file path, an `Image` handle, or an `AppIcon`.
|
|
84
|
+
"""
|
|
85
|
+
if icon is None:
|
|
86
|
+
return None, None
|
|
87
|
+
from bootstack.images import AppIcon, Image
|
|
88
|
+
|
|
89
|
+
if isinstance(icon, AppIcon):
|
|
90
|
+
return icon._icon_path(), None
|
|
91
|
+
if isinstance(icon, Image):
|
|
92
|
+
return None, icon
|
|
93
|
+
return str(icon), None
|