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,142 @@
|
|
|
1
|
+
"""Password entry field widget with visibility toggle.
|
|
2
|
+
|
|
3
|
+
Provides a specialized text entry field for password input with masked characters
|
|
4
|
+
and an optional visibility toggle button.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from bootstack.widgets._impl.primitives.button import Button
|
|
8
|
+
from bootstack.widgets._impl.composites.field import Field, FieldOptions
|
|
9
|
+
from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
|
|
10
|
+
from bootstack.widgets.types import Master
|
|
11
|
+
from typing_extensions import Unpack
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PasswordEntry(Field):
|
|
15
|
+
"""A password entry field widget with masked input and visibility toggle.
|
|
16
|
+
|
|
17
|
+
PasswordEntry extends the Field widget to provide password-specific functionality,
|
|
18
|
+
including character masking and a toggle button to temporarily reveal the password.
|
|
19
|
+
The widget automatically inserts a visibility toggle button (eye icon) that shows
|
|
20
|
+
the password while pressed and hides it when released.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
master: Master = None,
|
|
26
|
+
value: str = None,
|
|
27
|
+
label: str = None,
|
|
28
|
+
message: str = None,
|
|
29
|
+
show_visibility_toggle: bool = True,
|
|
30
|
+
**kwargs: Unpack[FieldOptions]):
|
|
31
|
+
"""Args:
|
|
32
|
+
master: Parent widget. If None, uses the default root window.
|
|
33
|
+
value: Initial password value to display (masked). Default is None.
|
|
34
|
+
label: Optional label text to display above the entry field.
|
|
35
|
+
If required=True, an asterisk (*) is automatically appended.
|
|
36
|
+
message: Optional message text to display below the entry field.
|
|
37
|
+
Used for hints or help text. Replaced by validation errors when
|
|
38
|
+
validation fails.
|
|
39
|
+
show_visibility_toggle: If True, displays the visibility toggle button
|
|
40
|
+
(eye icon) that reveals the password while pressed. Default is True.
|
|
41
|
+
Can be changed at runtime via `configure(show_visibility_toggle=...)`.
|
|
42
|
+
|
|
43
|
+
Other Parameters:
|
|
44
|
+
show: Character to mask password input. Default is '•'.
|
|
45
|
+
required: If True, field cannot be empty.
|
|
46
|
+
accent: Accent token for the focus ring and active border.
|
|
47
|
+
allow_blank: Allow empty input.
|
|
48
|
+
cursor: Cursor style when hovering.
|
|
49
|
+
font: Font for text display.
|
|
50
|
+
foreground: Text color.
|
|
51
|
+
initial_focus: If True, widget receives focus on creation.
|
|
52
|
+
justify: Text alignment.
|
|
53
|
+
show_message: If True, displays message area.
|
|
54
|
+
padding: Padding around entry widget.
|
|
55
|
+
takefocus: If True, widget accepts Tab focus.
|
|
56
|
+
textvariable: Tkinter Variable to link with text.
|
|
57
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
58
|
+
textsignal: Signal object for reactive updates.
|
|
59
|
+
width: Width in characters.
|
|
60
|
+
"""
|
|
61
|
+
# set default mask if not provided
|
|
62
|
+
self._show_indicator = kwargs.get('show', '•')
|
|
63
|
+
kwargs.setdefault('show', self._show_indicator)
|
|
64
|
+
super().__init__(master, value=value, label=label, message=message, **kwargs)
|
|
65
|
+
|
|
66
|
+
# configuration
|
|
67
|
+
self._show_visibility_toggle = show_visibility_toggle
|
|
68
|
+
self._show_visibility_pack = {}
|
|
69
|
+
|
|
70
|
+
self.insert_addon(
|
|
71
|
+
Button,
|
|
72
|
+
position="after",
|
|
73
|
+
name="visibility",
|
|
74
|
+
icon={"name": "eye", "state": [("pressed", "eye-slash")]},
|
|
75
|
+
compound="image",
|
|
76
|
+
icon_only=True,
|
|
77
|
+
# Revealing only flips the mask char — it never mutates the value —
|
|
78
|
+
# so the toggle stays usable on a read-only field (e.g. a generated
|
|
79
|
+
# secret shown for the user to peek at). A fully disabled field still
|
|
80
|
+
# dims it. (active_when_readonly postdates this widget, hence the gap.)
|
|
81
|
+
active_when_readonly=True,
|
|
82
|
+
)
|
|
83
|
+
addon = self.addons['visibility']
|
|
84
|
+
addon.bind('<ButtonPress>', self._show_password, add=True)
|
|
85
|
+
addon.bind('<ButtonRelease>', self._hide_password, add=True)
|
|
86
|
+
|
|
87
|
+
# Apply initial visibility setting
|
|
88
|
+
if not show_visibility_toggle:
|
|
89
|
+
self._apply_visibility_toggle(False)
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def _visibility_toggle(self):
|
|
93
|
+
"""Get the visibility toggle button widget."""
|
|
94
|
+
return self.addons['visibility']
|
|
95
|
+
|
|
96
|
+
def _show_password(self, _):
|
|
97
|
+
"""Reveal the password by removing character masking."""
|
|
98
|
+
self.entry_widget['show'] = ''
|
|
99
|
+
|
|
100
|
+
def _hide_password(self, _):
|
|
101
|
+
"""Hide the password by restoring character masking."""
|
|
102
|
+
# leave an unmasked placeholder alone — it is not real input to mask
|
|
103
|
+
if getattr(self.entry_widget, '_showing_placeholder', False):
|
|
104
|
+
return
|
|
105
|
+
self.entry_widget['show'] = self._show_indicator
|
|
106
|
+
|
|
107
|
+
# ------ Configuration Delegates ------
|
|
108
|
+
|
|
109
|
+
@configure_delegate('show_visibility_toggle')
|
|
110
|
+
def _delegate_show_visibility_toggle(self, value=None):
|
|
111
|
+
"""Get or set the visibility of the eye-icon toggle button."""
|
|
112
|
+
if value is None:
|
|
113
|
+
return self._show_visibility_toggle
|
|
114
|
+
else:
|
|
115
|
+
self._apply_visibility_toggle(value)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
def _apply_visibility_toggle(self, value: bool):
|
|
119
|
+
"""Show or hide the visibility toggle button.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
value: If True, show the toggle button. If False, hide it.
|
|
123
|
+
"""
|
|
124
|
+
self._show_visibility_toggle = value
|
|
125
|
+
is_managed = bool(self._visibility_toggle.winfo_manager())
|
|
126
|
+
if value and not is_managed:
|
|
127
|
+
self._visibility_toggle.pack(**self._show_visibility_pack)
|
|
128
|
+
elif not value and is_managed:
|
|
129
|
+
self._show_visibility_pack = self._visibility_toggle.pack_info()
|
|
130
|
+
self._visibility_toggle.pack_forget()
|
|
131
|
+
|
|
132
|
+
def reveal(self) -> None:
|
|
133
|
+
"""Remove character masking to show the password in plain text."""
|
|
134
|
+
self.entry_widget['show'] = ''
|
|
135
|
+
|
|
136
|
+
def hide(self) -> None:
|
|
137
|
+
"""Restore character masking."""
|
|
138
|
+
# leave an unmasked placeholder alone — it is not real input to mask
|
|
139
|
+
if getattr(self.entry_widget, '_showing_placeholder', False):
|
|
140
|
+
return
|
|
141
|
+
self.entry_widget['show'] = self._show_indicator
|
|
142
|
+
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""Path entry field widget with file/directory chooser dialog."""
|
|
2
|
+
|
|
3
|
+
from tkinter import filedialog
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Unpack
|
|
7
|
+
|
|
8
|
+
from bootstack.events import ChangeEvent
|
|
9
|
+
from bootstack.widgets._impl.primitives.button import Button
|
|
10
|
+
from bootstack.widgets._impl.composites.field import Field, FieldOptions
|
|
11
|
+
from bootstack.widgets._impl.mixins import configure_delegate
|
|
12
|
+
from bootstack.widgets.types import Master
|
|
13
|
+
|
|
14
|
+
PathMode = Literal['open', 'open_multiple', 'save', 'directory']
|
|
15
|
+
|
|
16
|
+
_MODE_TO_DIALOG = {
|
|
17
|
+
'open': filedialog.askopenfilename,
|
|
18
|
+
'open_multiple': filedialog.askopenfilenames,
|
|
19
|
+
'save': filedialog.asksaveasfilename,
|
|
20
|
+
'directory': filedialog.askdirectory,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class PathEntry(Field):
|
|
25
|
+
"""A path entry field with a browse button that opens a native dialog."""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
master: Master = None,
|
|
30
|
+
*,
|
|
31
|
+
value: str | None = None,
|
|
32
|
+
mode: PathMode = 'open',
|
|
33
|
+
dialog_title: str | None = None,
|
|
34
|
+
start_dir: str | None = None,
|
|
35
|
+
file_filters: list[tuple[str, str]] | None = None,
|
|
36
|
+
default_extension: str | None = None,
|
|
37
|
+
default_filename: str | None = None,
|
|
38
|
+
label: str = None,
|
|
39
|
+
message: str = None,
|
|
40
|
+
**kwargs: Unpack[FieldOptions]
|
|
41
|
+
):
|
|
42
|
+
"""Args:
|
|
43
|
+
master: Parent widget. If None, uses the default root window.
|
|
44
|
+
value: Initial path value. Updated when the user picks a path.
|
|
45
|
+
mode: Dialog type — `'open'` (default), `'open_multiple'`,
|
|
46
|
+
`'save'`, or `'directory'`.
|
|
47
|
+
dialog_title: Title shown in the native picker window.
|
|
48
|
+
start_dir: Directory the dialog opens in.
|
|
49
|
+
file_filters: File type filters, e.g. `[('Images', '*.png *.jpg')]`.
|
|
50
|
+
Not used for `'directory'` mode.
|
|
51
|
+
default_extension: Extension appended automatically if the user
|
|
52
|
+
omits it. `'save'` mode only.
|
|
53
|
+
default_filename: Suggested filename pre-filled in the dialog.
|
|
54
|
+
`'save'` mode only.
|
|
55
|
+
label: Label displayed above the field.
|
|
56
|
+
message: Hint text displayed below the field.
|
|
57
|
+
"""
|
|
58
|
+
self._mode = mode
|
|
59
|
+
self._dialog_title = dialog_title
|
|
60
|
+
self._start_dir = start_dir
|
|
61
|
+
self._file_filters = file_filters
|
|
62
|
+
self._default_extension = default_extension
|
|
63
|
+
self._default_filename = default_filename
|
|
64
|
+
self._dialog_result = None
|
|
65
|
+
self._prev_value: str | None = value
|
|
66
|
+
|
|
67
|
+
super().__init__(master=master, label=label, message=message, value=value, **kwargs)
|
|
68
|
+
|
|
69
|
+
self.insert_addon(
|
|
70
|
+
Button,
|
|
71
|
+
position="after",
|
|
72
|
+
name="dialog-button",
|
|
73
|
+
icon="folder",
|
|
74
|
+
icon_only=True,
|
|
75
|
+
command=self._show_file_chooser
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def dialog_button(self) -> Button:
|
|
80
|
+
"""The browse button widget."""
|
|
81
|
+
return self.addons.get('dialog-button')
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def dialog_result(self) -> str | tuple[str, ...] | None:
|
|
85
|
+
"""Raw result from the most recent dialog (string or tuple of strings)."""
|
|
86
|
+
return self._dialog_result
|
|
87
|
+
|
|
88
|
+
def _build_dialog_kwargs(self) -> dict[str, Any]:
|
|
89
|
+
"""Assemble the kwargs dict for the native dialog call."""
|
|
90
|
+
opts: dict[str, Any] = {}
|
|
91
|
+
if self._dialog_title is not None:
|
|
92
|
+
opts['title'] = self._dialog_title
|
|
93
|
+
if self._start_dir is not None:
|
|
94
|
+
opts['initialdir'] = self._start_dir
|
|
95
|
+
if self._file_filters is not None:
|
|
96
|
+
opts['filetypes'] = self._file_filters
|
|
97
|
+
if self._default_extension is not None:
|
|
98
|
+
opts['defaultextension'] = self._default_extension
|
|
99
|
+
if self._default_filename is not None:
|
|
100
|
+
opts['initialfile'] = self._default_filename
|
|
101
|
+
return opts
|
|
102
|
+
|
|
103
|
+
# ------ Configuration delegates ------
|
|
104
|
+
|
|
105
|
+
@configure_delegate('mode')
|
|
106
|
+
def _delegate_mode(self, value: PathMode = None):
|
|
107
|
+
"""Get or set the dialog mode."""
|
|
108
|
+
if value is None:
|
|
109
|
+
return self._mode
|
|
110
|
+
self._mode = value
|
|
111
|
+
|
|
112
|
+
@configure_delegate('dialog_title')
|
|
113
|
+
def _delegate_dialog_title(self, value: str = None):
|
|
114
|
+
if value is None:
|
|
115
|
+
return self._dialog_title
|
|
116
|
+
self._dialog_title = value
|
|
117
|
+
|
|
118
|
+
@configure_delegate('start_dir')
|
|
119
|
+
def _delegate_start_dir(self, value: str = None):
|
|
120
|
+
if value is None:
|
|
121
|
+
return self._start_dir
|
|
122
|
+
self._start_dir = value
|
|
123
|
+
|
|
124
|
+
@configure_delegate('file_filters')
|
|
125
|
+
def _delegate_file_filters(self, value: list = None):
|
|
126
|
+
if value is None:
|
|
127
|
+
return self._file_filters
|
|
128
|
+
self._file_filters = value
|
|
129
|
+
|
|
130
|
+
@configure_delegate('default_extension')
|
|
131
|
+
def _delegate_default_extension(self, value: str = None):
|
|
132
|
+
if value is None:
|
|
133
|
+
return self._default_extension
|
|
134
|
+
self._default_extension = value
|
|
135
|
+
|
|
136
|
+
@configure_delegate('default_filename')
|
|
137
|
+
def _delegate_default_filename(self, value: str = None):
|
|
138
|
+
if value is None:
|
|
139
|
+
return self._default_filename
|
|
140
|
+
self._default_filename = value
|
|
141
|
+
|
|
142
|
+
def _show_file_chooser(self):
|
|
143
|
+
"""Open the native dialog and update the entry with the selected path."""
|
|
144
|
+
dialog_func = _MODE_TO_DIALOG.get(self._mode)
|
|
145
|
+
if dialog_func is None:
|
|
146
|
+
raise ValueError(f"Invalid mode {self._mode!r}")
|
|
147
|
+
|
|
148
|
+
result = dialog_func(**self._build_dialog_kwargs())
|
|
149
|
+
self._dialog_result = result
|
|
150
|
+
|
|
151
|
+
if isinstance(result, (tuple, list)):
|
|
152
|
+
display_text = ", ".join(str(p) for p in result)
|
|
153
|
+
else:
|
|
154
|
+
display_text = result
|
|
155
|
+
|
|
156
|
+
if result:
|
|
157
|
+
prev_value = self._prev_value
|
|
158
|
+
self._prev_value = display_text
|
|
159
|
+
self.value = display_text
|
|
160
|
+
self.event_generate(
|
|
161
|
+
'<<Change>>',
|
|
162
|
+
data=ChangeEvent(
|
|
163
|
+
value=display_text,
|
|
164
|
+
prev_value=prev_value,
|
|
165
|
+
text=display_text,
|
|
166
|
+
),
|
|
167
|
+
when="tail"
|
|
168
|
+
)
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Internal Picture composite — a canvas-backed image display.
|
|
2
|
+
|
|
3
|
+
Renders a single image (or an animated sequence) into a themed canvas, fitting
|
|
4
|
+
it to the canvas's allocated size with a CSS `object-fit`-style policy. The
|
|
5
|
+
public `bootstack.Picture` wrapper drives this; nothing here is public.
|
|
6
|
+
|
|
7
|
+
The canvas (not a Label) is the backing widget on purpose: it does not
|
|
8
|
+
shrink-wrap to the image, it reports its allocated pixel size through
|
|
9
|
+
`<Configure>`, and it letterboxes cleanly behind a fitted image.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from tkinter import Canvas, TclError
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from PIL import Image as PILImage, ImageSequence
|
|
18
|
+
from PIL.ImageTk import PhotoImage
|
|
19
|
+
|
|
20
|
+
from bootstack.events import ImageErrorEvent, ImageLoadEvent
|
|
21
|
+
from bootstack.style.style import get_style
|
|
22
|
+
from bootstack.widgets._impl.composites._image_fit import (
|
|
23
|
+
FitMode, fit_image, round_corners, rounded_mask,
|
|
24
|
+
)
|
|
25
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
26
|
+
from bootstack.widgets.types import Master
|
|
27
|
+
|
|
28
|
+
# Map an anchor token to a (relative-x, relative-y) position in the box and the
|
|
29
|
+
# matching Tk canvas image anchor. Used to place an image that does not fill the
|
|
30
|
+
# box (contain / scale-down / none).
|
|
31
|
+
_ANCHOR_POS: dict[str, tuple[float, float]] = {
|
|
32
|
+
"center": (0.5, 0.5),
|
|
33
|
+
"n": (0.5, 0.0), "s": (0.5, 1.0), "e": (1.0, 0.5), "w": (0.0, 0.5),
|
|
34
|
+
"nw": (0.0, 0.0), "ne": (1.0, 0.0), "sw": (0.0, 1.0), "se": (1.0, 1.0),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Picture(Frame):
|
|
39
|
+
"""Canvas-backed image display with fit modes, resize, and GIF playback."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
master: Master = None,
|
|
44
|
+
*,
|
|
45
|
+
source: Any = None,
|
|
46
|
+
fit: FitMode = "contain",
|
|
47
|
+
anchor: str = "center",
|
|
48
|
+
corner_radius: int = 0,
|
|
49
|
+
autoplay: bool = True,
|
|
50
|
+
loop: bool = True,
|
|
51
|
+
img_width: int | None = None,
|
|
52
|
+
img_height: int | None = None,
|
|
53
|
+
**kwargs: Any,
|
|
54
|
+
) -> None:
|
|
55
|
+
super().__init__(master, **kwargs)
|
|
56
|
+
|
|
57
|
+
self._fit: FitMode = fit
|
|
58
|
+
self._anchor = anchor if anchor in _ANCHOR_POS else "center"
|
|
59
|
+
self._corner_radius = max(0, int(corner_radius))
|
|
60
|
+
self._autoplay = autoplay
|
|
61
|
+
self._loop = loop
|
|
62
|
+
self._fixed_w = img_width
|
|
63
|
+
self._fixed_h = img_height
|
|
64
|
+
|
|
65
|
+
from bootstack.scheduling import Schedule
|
|
66
|
+
|
|
67
|
+
self._source: Any = None # public Image handle, or None
|
|
68
|
+
self._frames: list[tuple[PILImage.Image, int]] = [] # (RGBA frame, ms)
|
|
69
|
+
self._frame_index = 0
|
|
70
|
+
self._schedule = Schedule(self) # cancels itself on destroy
|
|
71
|
+
self._anim_job = None
|
|
72
|
+
self._photo: PhotoImage | None = None # strong ref — Tk GC guard
|
|
73
|
+
self._img_id: int | None = None
|
|
74
|
+
self._box: tuple[int, int] = (0, 0)
|
|
75
|
+
self._mask_key: tuple[int, int, int] | None = None # single-entry cache
|
|
76
|
+
self._mask: PILImage.Image | None = None
|
|
77
|
+
|
|
78
|
+
self._surface_color = self._resolve_surface()
|
|
79
|
+
self._canvas = Canvas(
|
|
80
|
+
self,
|
|
81
|
+
highlightthickness=0,
|
|
82
|
+
bd=0,
|
|
83
|
+
background=self._surface_color,
|
|
84
|
+
width=self._fixed_w or 1,
|
|
85
|
+
height=self._fixed_h or 1,
|
|
86
|
+
)
|
|
87
|
+
self._canvas.pack(fill="both", expand=True)
|
|
88
|
+
self._canvas.bind("<Configure>", self._on_configure)
|
|
89
|
+
self._canvas.bind("<Button-1>", self._forward_click)
|
|
90
|
+
|
|
91
|
+
# Defer the initial load to idle so the constructor returns first: the
|
|
92
|
+
# caller can bind on_load/on_error before the <<PictureLoad>> fires.
|
|
93
|
+
if source is not None:
|
|
94
|
+
self._schedule.idle(lambda: self.set_source(source))
|
|
95
|
+
|
|
96
|
+
# ----- theme / surface ---------------------------------------------------
|
|
97
|
+
|
|
98
|
+
def _resolve_surface(self) -> str:
|
|
99
|
+
"""Resolve the canvas letterbox color from the frame's surface token."""
|
|
100
|
+
token = getattr(self, "_surface", None) or "background"
|
|
101
|
+
builder = get_style().style_builder
|
|
102
|
+
try:
|
|
103
|
+
return builder.color(token)
|
|
104
|
+
except Exception:
|
|
105
|
+
return builder.color("background")
|
|
106
|
+
|
|
107
|
+
def _bs_apply_theme(self, _event=None) -> None:
|
|
108
|
+
self._surface_color = self._resolve_surface()
|
|
109
|
+
try:
|
|
110
|
+
self._canvas.configure(background=self._surface_color)
|
|
111
|
+
except TclError:
|
|
112
|
+
return
|
|
113
|
+
# A theme-following icon source must re-render in the new theme color.
|
|
114
|
+
src = self._source
|
|
115
|
+
if src is not None and getattr(src, "_is_theme_following", False):
|
|
116
|
+
self.set_source(src)
|
|
117
|
+
|
|
118
|
+
# ----- source ------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
def set_source(self, source: Any) -> None:
|
|
121
|
+
"""Replace the displayed image and (re)start playback if animated.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
source: A public `bootstack.images.Image` handle, or None to clear.
|
|
125
|
+
"""
|
|
126
|
+
self._stop_animation()
|
|
127
|
+
self._source = source
|
|
128
|
+
if source is None:
|
|
129
|
+
self._frames = []
|
|
130
|
+
self._clear_canvas()
|
|
131
|
+
return
|
|
132
|
+
try:
|
|
133
|
+
self._frames = self._load_frames(source)
|
|
134
|
+
except Exception as exc: # decode/seek/format failures
|
|
135
|
+
self._frames = []
|
|
136
|
+
self._clear_canvas()
|
|
137
|
+
self.event_generate(
|
|
138
|
+
"<<PictureError>>", data=ImageErrorEvent(message=str(exc))
|
|
139
|
+
)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
self._frame_index = 0
|
|
143
|
+
self._render_current()
|
|
144
|
+
|
|
145
|
+
first = self._frames[0][0]
|
|
146
|
+
self.event_generate(
|
|
147
|
+
"<<PictureLoad>>",
|
|
148
|
+
data=ImageLoadEvent(
|
|
149
|
+
width=first.width, height=first.height, frames=len(self._frames)
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
if self._autoplay and len(self._frames) > 1:
|
|
153
|
+
self.play()
|
|
154
|
+
|
|
155
|
+
def _load_frames(self, image: Any) -> list[tuple[PILImage.Image, int]]:
|
|
156
|
+
"""Decode `image` into a list of (RGBA frame, duration-ms) tuples."""
|
|
157
|
+
# A deferred icon handle has no raster source to open; render its glyph.
|
|
158
|
+
icon = getattr(image, "_icon", None)
|
|
159
|
+
if icon is not None:
|
|
160
|
+
from bootstack._core.images import _ImageService
|
|
161
|
+
|
|
162
|
+
color = image._resolve_icon_color()
|
|
163
|
+
rgba = _ImageService._render_icon(icon.name, icon.size, color)
|
|
164
|
+
return [(rgba.convert("RGBA"), 0)]
|
|
165
|
+
|
|
166
|
+
pil = image._load_pil()
|
|
167
|
+
frames: list[tuple[PILImage.Image, int]] = []
|
|
168
|
+
for frame in ImageSequence.Iterator(pil):
|
|
169
|
+
rgba = frame.convert("RGBA").copy()
|
|
170
|
+
duration = int(frame.info.get("duration", 0) or 0)
|
|
171
|
+
frames.append((rgba, duration))
|
|
172
|
+
if not frames:
|
|
173
|
+
raise ValueError("image has no frames")
|
|
174
|
+
return frames
|
|
175
|
+
|
|
176
|
+
# ----- fit / render ------------------------------------------------------
|
|
177
|
+
|
|
178
|
+
def set_fit(self, fit: FitMode) -> None:
|
|
179
|
+
"""Change the fit policy and re-render."""
|
|
180
|
+
self._fit = fit
|
|
181
|
+
self._render_current()
|
|
182
|
+
|
|
183
|
+
def _on_configure(self, event) -> None:
|
|
184
|
+
box = (int(event.width), int(event.height))
|
|
185
|
+
if box == self._box:
|
|
186
|
+
return
|
|
187
|
+
self._box = box
|
|
188
|
+
self._render_current()
|
|
189
|
+
|
|
190
|
+
def _render_current(self) -> None:
|
|
191
|
+
"""Compose the current frame to the live box and draw it."""
|
|
192
|
+
if not self._frames:
|
|
193
|
+
return
|
|
194
|
+
bw, bh = self._box
|
|
195
|
+
if self._fixed_w:
|
|
196
|
+
bw = self._fixed_w
|
|
197
|
+
if self._fixed_h:
|
|
198
|
+
bh = self._fixed_h
|
|
199
|
+
if bw <= 1 or bh <= 1:
|
|
200
|
+
return # not laid out yet; the <Configure> pass will render
|
|
201
|
+
|
|
202
|
+
frame = self._frames[self._frame_index][0]
|
|
203
|
+
composed = self._compose(frame, bw, bh)
|
|
204
|
+
self._photo = PhotoImage(image=composed)
|
|
205
|
+
|
|
206
|
+
rx, ry = _ANCHOR_POS[self._anchor]
|
|
207
|
+
x = int(bw * rx)
|
|
208
|
+
y = int(bh * ry)
|
|
209
|
+
if self._img_id is None:
|
|
210
|
+
self._img_id = self._canvas.create_image(
|
|
211
|
+
x, y, image=self._photo, anchor=self._anchor
|
|
212
|
+
)
|
|
213
|
+
else:
|
|
214
|
+
self._canvas.coords(self._img_id, x, y)
|
|
215
|
+
self._canvas.itemconfigure(self._img_id, image=self._photo, anchor=self._anchor)
|
|
216
|
+
|
|
217
|
+
def _compose(self, frame: PILImage.Image, bw: int, bh: int) -> PILImage.Image:
|
|
218
|
+
"""Scale/crop `frame` to the box per the fit policy; round the corners."""
|
|
219
|
+
result = fit_image(frame, bw, bh, self._fit)
|
|
220
|
+
if self._corner_radius > 0:
|
|
221
|
+
result = round_corners(result, self._corner_radius, self._corner_mask(*result.size))
|
|
222
|
+
return result
|
|
223
|
+
|
|
224
|
+
def _corner_mask(self, w: int, h: int) -> PILImage.Image:
|
|
225
|
+
"""A cached rounded-rect mask for a `w`x`h` result (reused across frames)."""
|
|
226
|
+
key = (w, h, self._corner_radius)
|
|
227
|
+
if self._mask_key != key or self._mask is None:
|
|
228
|
+
self._mask = rounded_mask(w, h, self._corner_radius)
|
|
229
|
+
self._mask_key = key
|
|
230
|
+
return self._mask
|
|
231
|
+
|
|
232
|
+
def _clear_canvas(self) -> None:
|
|
233
|
+
if self._img_id is not None:
|
|
234
|
+
try:
|
|
235
|
+
self._canvas.delete(self._img_id)
|
|
236
|
+
except TclError:
|
|
237
|
+
pass
|
|
238
|
+
self._img_id = None
|
|
239
|
+
self._photo = None
|
|
240
|
+
|
|
241
|
+
# ----- animation ---------------------------------------------------------
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def is_playing(self) -> bool:
|
|
245
|
+
return self._anim_job is not None
|
|
246
|
+
|
|
247
|
+
def play(self) -> None:
|
|
248
|
+
"""Start (or resume) playback of an animated source."""
|
|
249
|
+
if len(self._frames) <= 1 or self._anim_job is not None:
|
|
250
|
+
return
|
|
251
|
+
self._schedule_next()
|
|
252
|
+
|
|
253
|
+
def pause(self) -> None:
|
|
254
|
+
"""Pause playback on the current frame."""
|
|
255
|
+
self._stop_animation()
|
|
256
|
+
|
|
257
|
+
def stop(self) -> None:
|
|
258
|
+
"""Stop playback and reset to the first frame."""
|
|
259
|
+
self._stop_animation()
|
|
260
|
+
if self._frames:
|
|
261
|
+
self._frame_index = 0
|
|
262
|
+
self._render_current()
|
|
263
|
+
|
|
264
|
+
def _schedule_next(self) -> None:
|
|
265
|
+
duration = self._frames[self._frame_index][1] or 100
|
|
266
|
+
self._anim_job = self._schedule.delay(duration, self._advance_frame)
|
|
267
|
+
|
|
268
|
+
def _advance_frame(self) -> None:
|
|
269
|
+
self._anim_job = None
|
|
270
|
+
nxt = self._frame_index + 1
|
|
271
|
+
if nxt >= len(self._frames):
|
|
272
|
+
if not self._loop:
|
|
273
|
+
return
|
|
274
|
+
nxt = 0
|
|
275
|
+
self._frame_index = nxt
|
|
276
|
+
self._render_current()
|
|
277
|
+
self._schedule_next()
|
|
278
|
+
|
|
279
|
+
def _stop_animation(self) -> None:
|
|
280
|
+
if self._anim_job is not None:
|
|
281
|
+
self._anim_job.cancel()
|
|
282
|
+
self._anim_job = None
|
|
283
|
+
|
|
284
|
+
# ----- events ------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
def _forward_click(self, event) -> None:
|
|
287
|
+
# The canvas covers the frame, so clicks land on it, not the frame the
|
|
288
|
+
# public wrapper binds. Re-emit on the frame so on_click works.
|
|
289
|
+
self.event_generate("<<PictureClick>>", x=event.x, y=event.y)
|