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,567 @@
|
|
|
1
|
+
"""Internal Carousel composite — a one-at-a-time image stepper.
|
|
2
|
+
|
|
3
|
+
A canvas-backed stage that shows a single slide from an ordered collection, with
|
|
4
|
+
overlaid navigation chevrons, a slide indicator, an optional caption, autoplay,
|
|
5
|
+
and slide/fade transitions. The public `bootstack.Carousel` wrapper drives this.
|
|
6
|
+
|
|
7
|
+
The stage is its OWN canvas (built like Picture's internal, on the shared
|
|
8
|
+
`_image_fit` helpers) rather than an embedded Picture widget — that is what makes
|
|
9
|
+
the transitions possible: a slide animates two image items via `canvas.move`, and
|
|
10
|
+
a fade blends two composed images frame by frame. Chevrons and dots are drawn from
|
|
11
|
+
the Bootstrap icon font through the icon renderer.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from tkinter import Canvas, TclError
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
from PIL import Image as PILImage, ImageOps
|
|
20
|
+
from PIL.ImageTk import PhotoImage
|
|
21
|
+
|
|
22
|
+
from bootstack._core.images import _ImageService
|
|
23
|
+
from bootstack.data import MemoryDataSource, DataSourceProtocol
|
|
24
|
+
from bootstack.scheduling import Schedule
|
|
25
|
+
from bootstack.style.style import get_style
|
|
26
|
+
from bootstack.widgets._impl.composites._image_fit import (
|
|
27
|
+
FitMode, fit_image, resolve_pil, rounded_mask,
|
|
28
|
+
)
|
|
29
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
30
|
+
from bootstack.widgets.types import Master
|
|
31
|
+
|
|
32
|
+
Transition = Literal["slide", "fade", "none"]
|
|
33
|
+
Indicator = Literal["dots", "count", "none"]
|
|
34
|
+
|
|
35
|
+
_NAV_SIZE = 34 # chevron glyph size
|
|
36
|
+
_NAV_MARGIN = 14 # inset of a chevron from the stage edge
|
|
37
|
+
_DOT_SIZE = 13
|
|
38
|
+
_DOT_GAP = 6
|
|
39
|
+
_DOTS_MAX = 8 # above this, 'dots' auto-switches to a 'count' label
|
|
40
|
+
_CAPTION_INSET = 14 # caption baseline inset from the bottom
|
|
41
|
+
_FRAME_MS = 16
|
|
42
|
+
# Nominal stage width (px) behind the aspect-ratio height floor: with no explicit
|
|
43
|
+
# `height`, the minimum stage height is `_FLOOR_STAGE_WIDTH / aspect_ratio`
|
|
44
|
+
# (e.g. 400 / 1.5 ≈ 267). A floor, not a cap — `fill`/`expand` grow it past this,
|
|
45
|
+
# and a larger `aspect_ratio` (wider slide) yields a shorter floor.
|
|
46
|
+
_FLOOR_STAGE_WIDTH = 400
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _hex_rgb(color: str) -> tuple[int, int, int]:
|
|
50
|
+
"""Parse a `#rgb`/`#rrggbb` color to an (r, g, b) tuple."""
|
|
51
|
+
c = color.lstrip("#")
|
|
52
|
+
if len(c) == 3:
|
|
53
|
+
c = "".join(ch * 2 for ch in c)
|
|
54
|
+
try:
|
|
55
|
+
return int(c[0:2], 16), int(c[2:4], 16), int(c[4:6], 16)
|
|
56
|
+
except (ValueError, IndexError):
|
|
57
|
+
return (255, 255, 255)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class Carousel(Frame):
|
|
61
|
+
"""Canvas-backed single-slide stepper with transitions and nav chrome."""
|
|
62
|
+
|
|
63
|
+
def __init__(
|
|
64
|
+
self,
|
|
65
|
+
master: Master = None,
|
|
66
|
+
*,
|
|
67
|
+
items: list | None = None,
|
|
68
|
+
datasource: DataSourceProtocol | None = None,
|
|
69
|
+
image_field: str = "image",
|
|
70
|
+
caption_field: str | None = None,
|
|
71
|
+
index: int = 0,
|
|
72
|
+
fit: FitMode = "contain",
|
|
73
|
+
transition: Transition = "slide",
|
|
74
|
+
show_arrows: bool = True,
|
|
75
|
+
indicator: Indicator = "dots",
|
|
76
|
+
autoplay: bool = False,
|
|
77
|
+
interval: int = 4000,
|
|
78
|
+
loop: bool = True,
|
|
79
|
+
corner_radius: int = 0,
|
|
80
|
+
aspect_ratio: float = 1.5,
|
|
81
|
+
height: int | None = None,
|
|
82
|
+
accent: str | None = None,
|
|
83
|
+
**kwargs: Any,
|
|
84
|
+
) -> None:
|
|
85
|
+
super().__init__(master, **kwargs)
|
|
86
|
+
|
|
87
|
+
self._image_field = image_field
|
|
88
|
+
self._caption_field = caption_field
|
|
89
|
+
self._fit: FitMode = fit
|
|
90
|
+
self._transition: Transition = transition
|
|
91
|
+
self._show_arrows = show_arrows
|
|
92
|
+
self._indicator: Indicator = indicator
|
|
93
|
+
self._loop = loop
|
|
94
|
+
self._radius = corner_radius
|
|
95
|
+
# None -> neutral white dots (the carousel default); a token brands the
|
|
96
|
+
# active dot with that accent color.
|
|
97
|
+
self._accent = accent
|
|
98
|
+
self._interval = max(250, int(interval))
|
|
99
|
+
|
|
100
|
+
if datasource is not None:
|
|
101
|
+
self._datasource = datasource
|
|
102
|
+
elif items:
|
|
103
|
+
self._datasource = MemoryDataSource(page_size=10_000).load(items)
|
|
104
|
+
else:
|
|
105
|
+
self._datasource = MemoryDataSource(page_size=10_000).load([])
|
|
106
|
+
|
|
107
|
+
n = self._count()
|
|
108
|
+
self._index = max(0, min(index, n - 1)) if n else 0
|
|
109
|
+
self._box = (0, 0)
|
|
110
|
+
self._cur_pil: PILImage.Image | None = None
|
|
111
|
+
self._photo: PhotoImage | None = None
|
|
112
|
+
self._img_id: int | None = None
|
|
113
|
+
self._frame_id: int | None = None # rounded corner-mask overlay
|
|
114
|
+
self._frame_photo: PhotoImage | None = None
|
|
115
|
+
self._frame_key: tuple | None = None
|
|
116
|
+
self._transitioning = False
|
|
117
|
+
self._schedule = Schedule(self)
|
|
118
|
+
|
|
119
|
+
self._autoplay = bool(autoplay)
|
|
120
|
+
self._autoplay_job = None
|
|
121
|
+
self._first_render = True
|
|
122
|
+
|
|
123
|
+
# Strong refs to overlay PhotoImages (Tk GC) — keyed so they survive redraws.
|
|
124
|
+
self._nav_photos: dict[str, PhotoImage] = {}
|
|
125
|
+
self._dot_photos: dict[str, PhotoImage] = {}
|
|
126
|
+
self._fade_photo: PhotoImage | None = None
|
|
127
|
+
self._next_photo: PhotoImage | None = None
|
|
128
|
+
|
|
129
|
+
surface = self._surface_color()
|
|
130
|
+
# Requested-height FLOOR so the carousel never collapses in a container
|
|
131
|
+
# that doesn't impose a height (a scroll view, an auto-fit window). The
|
|
132
|
+
# public wrapper freezes this frame with `pack_propagate(False)`, so its
|
|
133
|
+
# own configured `height` is its requested height; `fill`/`expand` still
|
|
134
|
+
# grow it past this floor when the parent has room.
|
|
135
|
+
floor_h = height if height else round(_FLOOR_STAGE_WIDTH / max(0.1, aspect_ratio))
|
|
136
|
+
self.configure(height=floor_h)
|
|
137
|
+
self._stage = Canvas(self, highlightthickness=0, bd=0, background=surface)
|
|
138
|
+
self._stage.pack(fill="both", expand=True)
|
|
139
|
+
self._stage.bind("<Configure>", self._on_configure, add="+")
|
|
140
|
+
self._stage.bind("<Button-1>", self._on_stage_click, add="+")
|
|
141
|
+
self._stage.bind("<Left>", lambda e: self.previous())
|
|
142
|
+
self._stage.bind("<Right>", lambda e: self.next())
|
|
143
|
+
|
|
144
|
+
# Auto-refresh when the data source changes externally (parity with the
|
|
145
|
+
# other record-native widgets); the subscription is released on destroy.
|
|
146
|
+
self._change_sub = None
|
|
147
|
+
on_change = getattr(self._datasource, "on_change", None)
|
|
148
|
+
if callable(on_change):
|
|
149
|
+
try:
|
|
150
|
+
self._change_sub = on_change(self._on_source_change)
|
|
151
|
+
except Exception:
|
|
152
|
+
self._change_sub = None
|
|
153
|
+
self.bind("<Destroy>", self._on_destroy, add="+")
|
|
154
|
+
|
|
155
|
+
# ----- theme / data ------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
def _on_source_change(self, _event: Any = None) -> None:
|
|
158
|
+
try:
|
|
159
|
+
self.reload()
|
|
160
|
+
except Exception:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
def _on_destroy(self, event: Any = None) -> None:
|
|
164
|
+
if event is not None and getattr(event, "widget", None) is not self:
|
|
165
|
+
return
|
|
166
|
+
sub, self._change_sub = self._change_sub, None
|
|
167
|
+
if sub is not None:
|
|
168
|
+
try:
|
|
169
|
+
sub.cancel()
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
def reload(self) -> None:
|
|
174
|
+
"""Reload from the data source and refresh the current slide."""
|
|
175
|
+
n = self._count()
|
|
176
|
+
self._index = max(0, min(self._index, n - 1)) if n else 0
|
|
177
|
+
if self._box[0] > 1 and self._box[1] > 1:
|
|
178
|
+
self._set_current(self._compose(self._pil_at(self._index), *self._box))
|
|
179
|
+
self._ensure_frame(*self._box)
|
|
180
|
+
self._draw_overlays(*self._box)
|
|
181
|
+
|
|
182
|
+
def _surface_color(self) -> str:
|
|
183
|
+
token = getattr(self, "_surface", None) or "background"
|
|
184
|
+
b = get_style().style_builder
|
|
185
|
+
try:
|
|
186
|
+
return b.color(token)
|
|
187
|
+
except Exception:
|
|
188
|
+
return b.color("background")
|
|
189
|
+
|
|
190
|
+
def _bs_apply_theme(self, _event=None) -> None:
|
|
191
|
+
try:
|
|
192
|
+
self._stage.configure(background=self._surface_color())
|
|
193
|
+
except TclError:
|
|
194
|
+
return
|
|
195
|
+
self._nav_photos.clear()
|
|
196
|
+
self._dot_photos.clear()
|
|
197
|
+
self._frame_key = None # surface color changed — rebuild the corner mask
|
|
198
|
+
if self._box[0] > 1:
|
|
199
|
+
self._ensure_frame(*self._box)
|
|
200
|
+
self._draw_overlays(*self._box)
|
|
201
|
+
|
|
202
|
+
def _count(self) -> int:
|
|
203
|
+
try:
|
|
204
|
+
return int(self._datasource.count)
|
|
205
|
+
except Exception:
|
|
206
|
+
return 0
|
|
207
|
+
|
|
208
|
+
def _raw_at(self, i: int) -> dict | None:
|
|
209
|
+
try:
|
|
210
|
+
rows = self._datasource.page_slice(i, 1)
|
|
211
|
+
except Exception:
|
|
212
|
+
return None
|
|
213
|
+
return rows[0] if rows else None
|
|
214
|
+
|
|
215
|
+
def _public_at(self, i: int) -> dict | None:
|
|
216
|
+
raw = self._raw_at(i)
|
|
217
|
+
if raw is None:
|
|
218
|
+
return None
|
|
219
|
+
pub = getattr(self._datasource, "_public_record", None)
|
|
220
|
+
return pub(raw) if callable(pub) else dict(raw)
|
|
221
|
+
|
|
222
|
+
def _pil_at(self, i: int) -> "PILImage.Image | None":
|
|
223
|
+
raw = self._raw_at(i)
|
|
224
|
+
return resolve_pil(raw.get(self._image_field)) if raw else None
|
|
225
|
+
|
|
226
|
+
def _caption_at(self, i: int) -> str:
|
|
227
|
+
raw = self._raw_at(i)
|
|
228
|
+
if raw is None or not self._caption_field:
|
|
229
|
+
return ""
|
|
230
|
+
return str(raw.get(self._caption_field, ""))
|
|
231
|
+
|
|
232
|
+
# ----- compositing -------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def _compose(self, pil: "PILImage.Image | None", w: int, h: int) -> PILImage.Image:
|
|
235
|
+
"""Fit `pil` into a full `w`x`h` RGBA frame (centered; transparent margin).
|
|
236
|
+
|
|
237
|
+
Corners are NOT rounded here — `corner_radius` rounds the whole stage via
|
|
238
|
+
a fixed corner-mask overlay, so transitions slide under a steady frame.
|
|
239
|
+
"""
|
|
240
|
+
base = PILImage.new("RGBA", (w, h), (0, 0, 0, 0))
|
|
241
|
+
if pil is None:
|
|
242
|
+
return base
|
|
243
|
+
fitted = fit_image(pil.convert("RGBA"), w, h, self._fit)
|
|
244
|
+
base.alpha_composite(fitted, ((w - fitted.width) // 2, (h - fitted.height) // 2))
|
|
245
|
+
return base
|
|
246
|
+
|
|
247
|
+
def _set_current(self, pil_box: PILImage.Image) -> None:
|
|
248
|
+
self._cur_pil = pil_box
|
|
249
|
+
self._photo = PhotoImage(pil_box)
|
|
250
|
+
if self._img_id is None:
|
|
251
|
+
self._img_id = self._stage.create_image(0, 0, anchor="nw", image=self._photo)
|
|
252
|
+
else:
|
|
253
|
+
self._stage.itemconfigure(self._img_id, image=self._photo)
|
|
254
|
+
self._stage.coords(self._img_id, 0, 0)
|
|
255
|
+
self._stage.tag_lower(self._img_id)
|
|
256
|
+
|
|
257
|
+
def _ensure_frame(self, w: int, h: int) -> None:
|
|
258
|
+
"""Create/update the rounded corner-mask that rounds the whole stage.
|
|
259
|
+
|
|
260
|
+
The mask paints the four corner nooks in the surface color (antialiased),
|
|
261
|
+
so the viewport reads as a rounded card while slides move underneath it.
|
|
262
|
+
"""
|
|
263
|
+
if self._radius <= 0:
|
|
264
|
+
if self._frame_id is not None:
|
|
265
|
+
self._stage.delete(self._frame_id)
|
|
266
|
+
self._frame_id = None
|
|
267
|
+
return
|
|
268
|
+
key = (w, h, self._radius, self._surface_color())
|
|
269
|
+
if self._frame_key != key or self._frame_photo is None:
|
|
270
|
+
rgb = _hex_rgb(self._surface_color())
|
|
271
|
+
cover = PILImage.new("RGBA", (w, h), rgb + (255,))
|
|
272
|
+
inside = rounded_mask(w, h, self._radius) # 255 inside (AA edge)
|
|
273
|
+
cover.putalpha(ImageOps.invert(inside)) # opaque only in the corners
|
|
274
|
+
self._frame_photo = PhotoImage(cover)
|
|
275
|
+
self._frame_key = key
|
|
276
|
+
if self._frame_id is None:
|
|
277
|
+
self._frame_id = self._stage.create_image(
|
|
278
|
+
0, 0, anchor="nw", image=self._frame_photo, tags="frame"
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
self._stage.itemconfigure(self._frame_id, image=self._frame_photo)
|
|
282
|
+
self._stage.coords(self._frame_id, 0, 0)
|
|
283
|
+
self._stage.tag_raise(self._frame_id)
|
|
284
|
+
|
|
285
|
+
# ----- layout / render ---------------------------------------------------
|
|
286
|
+
|
|
287
|
+
def _on_configure(self, event) -> None:
|
|
288
|
+
box = (int(event.width), int(event.height))
|
|
289
|
+
if box == self._box:
|
|
290
|
+
return
|
|
291
|
+
self._box = box
|
|
292
|
+
if box[0] <= 1 or box[1] <= 1:
|
|
293
|
+
return
|
|
294
|
+
# Don't fight an in-flight transition; it recomposes at the settled box.
|
|
295
|
+
if self._transitioning:
|
|
296
|
+
return
|
|
297
|
+
self._set_current(self._compose(self._pil_at(self._index), *box))
|
|
298
|
+
self._ensure_frame(*box)
|
|
299
|
+
self._draw_overlays(*box)
|
|
300
|
+
if self._first_render:
|
|
301
|
+
self._first_render = False
|
|
302
|
+
if self._autoplay:
|
|
303
|
+
self._arm_autoplay()
|
|
304
|
+
|
|
305
|
+
# ----- navigation --------------------------------------------------------
|
|
306
|
+
|
|
307
|
+
def show(self, index: int, *, direction: int = 0) -> None:
|
|
308
|
+
n = self._count()
|
|
309
|
+
if n == 0 or self._transitioning:
|
|
310
|
+
return
|
|
311
|
+
index = index % n if self._loop else max(0, min(index, n - 1))
|
|
312
|
+
if index == self._index and self._img_id is not None:
|
|
313
|
+
return
|
|
314
|
+
w, h = self._box
|
|
315
|
+
if w <= 1 or h <= 1:
|
|
316
|
+
# Not laid out yet — record the index and announce the change; the
|
|
317
|
+
# first <Configure> will render it.
|
|
318
|
+
self._index = index
|
|
319
|
+
record = self._public_at(index)
|
|
320
|
+
if record is not None:
|
|
321
|
+
self.event_generate("<<Change>>", data=record)
|
|
322
|
+
return
|
|
323
|
+
next_pil = self._compose(self._pil_at(index), w, h)
|
|
324
|
+
self._index = index
|
|
325
|
+
|
|
326
|
+
if self._transition == "none" or self._img_id is None or direction == 0:
|
|
327
|
+
self._set_current(next_pil)
|
|
328
|
+
self._after_change()
|
|
329
|
+
elif self._transition == "fade":
|
|
330
|
+
self._fade_to(next_pil)
|
|
331
|
+
else:
|
|
332
|
+
self._slide_to(next_pil, direction)
|
|
333
|
+
|
|
334
|
+
def next(self) -> None:
|
|
335
|
+
self.show(self._index + 1, direction=1)
|
|
336
|
+
|
|
337
|
+
def previous(self) -> None:
|
|
338
|
+
self.show(self._index - 1, direction=-1)
|
|
339
|
+
|
|
340
|
+
def go_to(self, index: int) -> None:
|
|
341
|
+
direction = (index > self._index) - (index < self._index)
|
|
342
|
+
self.show(index, direction=direction)
|
|
343
|
+
|
|
344
|
+
def _slide_to(self, next_pil: PILImage.Image, d: int) -> None:
|
|
345
|
+
w, h = self._box
|
|
346
|
+
self._transitioning = True
|
|
347
|
+
self._next_photo = PhotoImage(next_pil)
|
|
348
|
+
next_id = self._stage.create_image(d * w, 0, anchor="nw", image=self._next_photo)
|
|
349
|
+
cur_id = self._img_id
|
|
350
|
+
self._stage.tag_lower(next_id)
|
|
351
|
+
if cur_id is not None:
|
|
352
|
+
self._stage.tag_lower(cur_id)
|
|
353
|
+
steps = 8
|
|
354
|
+
|
|
355
|
+
def _ease(t: float) -> float:
|
|
356
|
+
return 2 * t * t if t < 0.5 else 1 - (-2 * t + 2) ** 2 / 2
|
|
357
|
+
|
|
358
|
+
def step(i: int) -> None:
|
|
359
|
+
if i >= steps:
|
|
360
|
+
if cur_id is not None:
|
|
361
|
+
self._stage.delete(cur_id)
|
|
362
|
+
self._stage.coords(next_id, 0, 0)
|
|
363
|
+
self._img_id = next_id
|
|
364
|
+
self._photo = self._next_photo
|
|
365
|
+
self._cur_pil = next_pil
|
|
366
|
+
self._stage.tag_lower(self._img_id)
|
|
367
|
+
self._transitioning = False
|
|
368
|
+
self._after_change()
|
|
369
|
+
return
|
|
370
|
+
t = _ease(i / steps)
|
|
371
|
+
self._stage.coords(next_id, round(d * w * (1 - t)), 0)
|
|
372
|
+
if cur_id is not None:
|
|
373
|
+
self._stage.coords(cur_id, round(-d * w * t), 0)
|
|
374
|
+
self._schedule.delay(_FRAME_MS, lambda: step(i + 1))
|
|
375
|
+
|
|
376
|
+
step(1)
|
|
377
|
+
|
|
378
|
+
def _fade_to(self, next_pil: PILImage.Image) -> None:
|
|
379
|
+
cur = self._cur_pil
|
|
380
|
+
if cur is None:
|
|
381
|
+
self._set_current(next_pil)
|
|
382
|
+
self._after_change()
|
|
383
|
+
return
|
|
384
|
+
self._transitioning = True
|
|
385
|
+
steps = 10
|
|
386
|
+
# Pre-compute all frames up front so PhotoImage allocation doesn't happen
|
|
387
|
+
# inside the after() loop where it blocks the canvas refresh.
|
|
388
|
+
frames = [PhotoImage(PILImage.blend(cur, next_pil, i / steps)) for i in range(1, steps + 1)]
|
|
389
|
+
|
|
390
|
+
def step(i: int) -> None:
|
|
391
|
+
if i >= len(frames):
|
|
392
|
+
self._set_current(next_pil)
|
|
393
|
+
self._transitioning = False
|
|
394
|
+
self._after_change()
|
|
395
|
+
return
|
|
396
|
+
self._fade_photo = frames[i]
|
|
397
|
+
self._stage.itemconfigure(self._img_id, image=self._fade_photo)
|
|
398
|
+
self._schedule.delay(_FRAME_MS + 6, lambda: step(i + 1))
|
|
399
|
+
|
|
400
|
+
step(0)
|
|
401
|
+
|
|
402
|
+
def _after_change(self) -> None:
|
|
403
|
+
bw, bh = self._box
|
|
404
|
+
if bw > 1 and bh > 1:
|
|
405
|
+
# A resize deferred during the transition leaves the settled image at
|
|
406
|
+
# the old size — recompose if it no longer matches the live box.
|
|
407
|
+
if self._cur_pil is None or self._cur_pil.size != (bw, bh):
|
|
408
|
+
self._set_current(self._compose(self._pil_at(self._index), bw, bh))
|
|
409
|
+
self._ensure_frame(bw, bh)
|
|
410
|
+
self._draw_overlays(bw, bh)
|
|
411
|
+
record = self._public_at(self._index)
|
|
412
|
+
if record is not None:
|
|
413
|
+
self.event_generate("<<Change>>", data=record)
|
|
414
|
+
if self._autoplay:
|
|
415
|
+
self._arm_autoplay()
|
|
416
|
+
|
|
417
|
+
# ----- overlays (chevrons, indicator, caption) ---------------------------
|
|
418
|
+
|
|
419
|
+
def _draw_overlays(self, w: int, h: int) -> None:
|
|
420
|
+
self._stage.delete("ov")
|
|
421
|
+
n = self._count()
|
|
422
|
+
self._draw_caption(w, h)
|
|
423
|
+
self._draw_indicator(w, h, n)
|
|
424
|
+
if self._show_arrows and n > 1:
|
|
425
|
+
self._draw_chevrons(w, h)
|
|
426
|
+
|
|
427
|
+
def _nav_photo(self, glyph: str) -> PhotoImage:
|
|
428
|
+
if glyph not in self._nav_photos:
|
|
429
|
+
pad = 3
|
|
430
|
+
canvas = _NAV_SIZE + pad * 2
|
|
431
|
+
base = PILImage.new("RGBA", (canvas, canvas), (0, 0, 0, 0))
|
|
432
|
+
shadow = _ImageService._render_icon(glyph, _NAV_SIZE, "#000000")
|
|
433
|
+
white = _ImageService._render_icon(glyph, _NAV_SIZE, "#ffffff")
|
|
434
|
+
base.alpha_composite(shadow, (pad, pad + 1)) # soft 1px drop shadow
|
|
435
|
+
base.alpha_composite(white, (pad, pad))
|
|
436
|
+
self._nav_photos[glyph] = PhotoImage(base)
|
|
437
|
+
return self._nav_photos[glyph]
|
|
438
|
+
|
|
439
|
+
def _draw_chevrons(self, w: int, h: int) -> None:
|
|
440
|
+
left = self._nav_photo("chevron-left")
|
|
441
|
+
right = self._nav_photo("chevron-right")
|
|
442
|
+
cy = h // 2
|
|
443
|
+
# Tag the chevrons "nav" so the stage's Button-1 handler knows the click
|
|
444
|
+
# was navigation and skips emitting ItemClick (a "break" from an item-tag
|
|
445
|
+
# binding does NOT stop the canvas-wide binding — separate mechanisms).
|
|
446
|
+
lid = self._stage.create_image(_NAV_MARGIN, cy, anchor="w", image=left, tags=("ov", "nav"))
|
|
447
|
+
rid = self._stage.create_image(w - _NAV_MARGIN, cy, anchor="e", image=right, tags=("ov", "nav"))
|
|
448
|
+
self._stage.tag_bind(lid, "<Button-1>", lambda e: self.previous())
|
|
449
|
+
self._stage.tag_bind(rid, "<Button-1>", lambda e: self.next())
|
|
450
|
+
|
|
451
|
+
def _dot_photo(self, key: str, color: str, glyph: str) -> PhotoImage:
|
|
452
|
+
if key not in self._dot_photos:
|
|
453
|
+
self._dot_photos[key] = _ImageService.get_icon(glyph, _DOT_SIZE, color)
|
|
454
|
+
return self._dot_photos[key]
|
|
455
|
+
|
|
456
|
+
def _effective_indicator(self, n: int) -> Indicator:
|
|
457
|
+
if self._indicator == "dots" and n > _DOTS_MAX:
|
|
458
|
+
return "count"
|
|
459
|
+
return self._indicator
|
|
460
|
+
|
|
461
|
+
def _draw_indicator(self, w: int, h: int, n: int) -> None:
|
|
462
|
+
mode = self._effective_indicator(n)
|
|
463
|
+
if mode == "none" or n <= 1:
|
|
464
|
+
return
|
|
465
|
+
y = h - _CAPTION_INSET
|
|
466
|
+
if mode == "count":
|
|
467
|
+
label = f"{self._index + 1} / {n}"
|
|
468
|
+
self._stage.create_text(w // 2 + 1, y + 1, text=label, anchor="s",
|
|
469
|
+
fill="#000000", font=("", 10, "bold"), tags="ov")
|
|
470
|
+
self._stage.create_text(w // 2, y, text=label, anchor="s",
|
|
471
|
+
fill="#ffffff", font=("", 10, "bold"), tags="ov")
|
|
472
|
+
return
|
|
473
|
+
# Neutral by default — solid white active, translucent white inactive;
|
|
474
|
+
# an explicit accent brands the active dot.
|
|
475
|
+
active_color = get_style().style_builder.color(self._accent) if self._accent else "#ffffff"
|
|
476
|
+
active = self._dot_photo(f"active:{active_color}", active_color, "circle-fill")
|
|
477
|
+
inactive = self._dot_photo("inactive", "#ffffff66", "circle-fill")
|
|
478
|
+
total = n * _DOT_SIZE + (n - 1) * _DOT_GAP
|
|
479
|
+
x = w // 2 - total // 2 + _DOT_SIZE // 2
|
|
480
|
+
for i in range(n):
|
|
481
|
+
photo = active if i == self._index else inactive
|
|
482
|
+
did = self._stage.create_image(x, y - _DOT_SIZE // 2, image=photo, tags=("ov", "nav"))
|
|
483
|
+
self._stage.tag_bind(did, "<Button-1>", lambda e, idx=i: self.go_to(idx))
|
|
484
|
+
x += _DOT_SIZE + _DOT_GAP
|
|
485
|
+
|
|
486
|
+
def _draw_caption(self, w: int, h: int) -> None:
|
|
487
|
+
text = self._caption_at(self._index)
|
|
488
|
+
if not text:
|
|
489
|
+
return
|
|
490
|
+
n = self._count()
|
|
491
|
+
bump = _DOT_SIZE + 8 if self._effective_indicator(n) != "none" and n > 1 else 0
|
|
492
|
+
y = h - _CAPTION_INSET - bump
|
|
493
|
+
self._stage.create_text(w // 2 + 1, y + 1, text=text, anchor="s",
|
|
494
|
+
fill="#000000", font=("", 11, "bold"), tags="ov")
|
|
495
|
+
self._stage.create_text(w // 2, y, text=text, anchor="s",
|
|
496
|
+
fill="#ffffff", font=("", 11, "bold"), tags="ov")
|
|
497
|
+
|
|
498
|
+
# ----- autoplay ----------------------------------------------------------
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def is_playing(self) -> bool:
|
|
502
|
+
return self._autoplay
|
|
503
|
+
|
|
504
|
+
def play(self) -> None:
|
|
505
|
+
if self._count() <= 1:
|
|
506
|
+
return
|
|
507
|
+
self._autoplay = True
|
|
508
|
+
self._arm_autoplay()
|
|
509
|
+
|
|
510
|
+
def pause(self) -> None:
|
|
511
|
+
self._autoplay = False
|
|
512
|
+
self._cancel_autoplay()
|
|
513
|
+
|
|
514
|
+
def stop(self) -> None:
|
|
515
|
+
"""Stop autoplay and return to the first slide."""
|
|
516
|
+
self.pause()
|
|
517
|
+
self.go_to(0)
|
|
518
|
+
|
|
519
|
+
def _arm_autoplay(self) -> None:
|
|
520
|
+
self._cancel_autoplay()
|
|
521
|
+
if self._autoplay:
|
|
522
|
+
self._autoplay_job = self._schedule.delay(self._interval, self._autoplay_tick)
|
|
523
|
+
|
|
524
|
+
def _cancel_autoplay(self) -> None:
|
|
525
|
+
if self._autoplay_job is not None:
|
|
526
|
+
self._autoplay_job.cancel()
|
|
527
|
+
self._autoplay_job = None
|
|
528
|
+
|
|
529
|
+
def _autoplay_tick(self) -> None:
|
|
530
|
+
self._autoplay_job = None
|
|
531
|
+
if self._transitioning:
|
|
532
|
+
# A tick landed mid-transition; next() would no-op and autoplay would
|
|
533
|
+
# die (no _after_change to re-arm). Retry shortly instead.
|
|
534
|
+
if self._autoplay:
|
|
535
|
+
self._autoplay_job = self._schedule.delay(_FRAME_MS * 8, self._autoplay_tick)
|
|
536
|
+
return
|
|
537
|
+
self.next() # _after_change re-arms
|
|
538
|
+
|
|
539
|
+
# ----- events / accessors ------------------------------------------------
|
|
540
|
+
|
|
541
|
+
def _on_stage_click(self, event) -> None:
|
|
542
|
+
self._stage.focus_set()
|
|
543
|
+
# Skip when the click landed on a chevron or dot — that was navigation,
|
|
544
|
+
# not a click on the slide. The canvas-wide binding fires in addition to
|
|
545
|
+
# the item bindings (a "break" from an item binding won't stop it), so
|
|
546
|
+
# check the topmost item at the click point for the "nav" tag.
|
|
547
|
+
hit = self._stage.find_overlapping(event.x, event.y, event.x, event.y)
|
|
548
|
+
if hit and "nav" in self._stage.gettags(hit[-1]):
|
|
549
|
+
return
|
|
550
|
+
record = self._public_at(self._index)
|
|
551
|
+
if record is not None:
|
|
552
|
+
self.event_generate("<<ItemClick>>", data=record)
|
|
553
|
+
|
|
554
|
+
@property
|
|
555
|
+
def index(self) -> int:
|
|
556
|
+
return self._index
|
|
557
|
+
|
|
558
|
+
@property
|
|
559
|
+
def current(self) -> dict | None:
|
|
560
|
+
return self._public_at(self._index)
|
|
561
|
+
|
|
562
|
+
@property
|
|
563
|
+
def count(self) -> int:
|
|
564
|
+
return self._count()
|
|
565
|
+
|
|
566
|
+
def get_datasource(self) -> DataSourceProtocol:
|
|
567
|
+
return self._datasource
|