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,551 @@
|
|
|
1
|
+
"""Internal Gallery composite — a recycling thumbnail grid.
|
|
2
|
+
|
|
3
|
+
A scrollable, selectable grid of image tiles. Record-native: it reads from a
|
|
4
|
+
`DataSourceProtocol` and reuses the selection model the list/table/tree widgets
|
|
5
|
+
share. The public `bootstack.Gallery` wrapper drives this.
|
|
6
|
+
|
|
7
|
+
Recycle model: a FIXED pool of tiles sits in stable grid slots; scrolling slides
|
|
8
|
+
the data window over the pool and each tile re-renders only when the record in
|
|
9
|
+
its slot actually changes. Modeled as a vertical scroll of rows, each row a strip
|
|
10
|
+
of `columns` tiles — the 1D list recycle generalized one axis, not a 2D recycler.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import contextlib
|
|
16
|
+
from tkinter import Canvas, TclError
|
|
17
|
+
from typing import Any, Literal
|
|
18
|
+
|
|
19
|
+
from PIL import Image as PILImage, ImageDraw
|
|
20
|
+
from PIL.Image import Resampling
|
|
21
|
+
from PIL.ImageTk import PhotoImage
|
|
22
|
+
|
|
23
|
+
from bootstack.data import MemoryDataSource, DataSourceProtocol
|
|
24
|
+
from bootstack.style.style import get_style
|
|
25
|
+
from bootstack.widgets._impl.composites._image_fit import (
|
|
26
|
+
FitMode, fit_image, resolve_pil, round_corners,
|
|
27
|
+
)
|
|
28
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
29
|
+
from bootstack.widgets._impl.primitives.label import Label
|
|
30
|
+
from bootstack.widgets._impl.primitives.scrollbar import Scrollbar
|
|
31
|
+
from bootstack.widgets.types import Master, ScrollbarVariant
|
|
32
|
+
|
|
33
|
+
OVERSCAN_ROWS = 1
|
|
34
|
+
_RING_THICKNESS = 3
|
|
35
|
+
_RING_GAP = 3 # separation between the image and the ring
|
|
36
|
+
_RING_PAD = _RING_GAP + _RING_THICKNESS # margin reserved around the image
|
|
37
|
+
_CAPTION_H = 24 # vertical space reserved per caption row in the height floor
|
|
38
|
+
EMPTY = {"__empty__": True, "id": "__empty__"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class GalleryTile(Frame):
|
|
42
|
+
"""One thumbnail cell — a canvas (image + selection ring) and a caption."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, master, *, tile_w, tile_h, fit, corner_radius, caption,
|
|
45
|
+
on_click=None, on_activate=None, **kwargs):
|
|
46
|
+
super().__init__(master, **kwargs)
|
|
47
|
+
self._tw = tile_w
|
|
48
|
+
self._th = tile_h
|
|
49
|
+
self._fit: FitMode = fit
|
|
50
|
+
self._radius = corner_radius
|
|
51
|
+
self._has_caption = caption
|
|
52
|
+
self._on_click_cb = on_click
|
|
53
|
+
self._on_activate_cb = on_activate
|
|
54
|
+
self._record: dict | None = None
|
|
55
|
+
self._cur_src: Any = "\x00unset" # sentinel distinct from None
|
|
56
|
+
self._cur_id: Any = "\x00unset"
|
|
57
|
+
self._photo: PhotoImage | None = None
|
|
58
|
+
self._ring_photo: PhotoImage | None = None
|
|
59
|
+
|
|
60
|
+
# The canvas is larger than the image by a margin on every side, so the
|
|
61
|
+
# selection ring can sit OUTSIDE the image (with a gap) without changing
|
|
62
|
+
# the layout when toggled. The image is centered; the margin blends into
|
|
63
|
+
# the surface when unselected.
|
|
64
|
+
cw, ch = tile_w + 2 * _RING_PAD, tile_h + 2 * _RING_PAD
|
|
65
|
+
self._canvas = Canvas(self, width=cw, height=ch,
|
|
66
|
+
highlightthickness=0, bd=0,
|
|
67
|
+
background=self._surface_color())
|
|
68
|
+
self._canvas.pack()
|
|
69
|
+
self._img_id = self._canvas.create_image(cw // 2, ch // 2, anchor="center")
|
|
70
|
+
self._ring_id = self._canvas.create_image(0, 0, anchor="nw", state="hidden")
|
|
71
|
+
|
|
72
|
+
if caption:
|
|
73
|
+
self._caption = Label(self, text="", anchor="center", font="caption")
|
|
74
|
+
self._caption.pack(fill="x", pady=(4, 0))
|
|
75
|
+
else:
|
|
76
|
+
self._caption = None
|
|
77
|
+
|
|
78
|
+
for seq in ("<Button-1>",):
|
|
79
|
+
self._canvas.bind(seq, self._on_click)
|
|
80
|
+
self._canvas.bind("<Double-Button-1>", self._on_double_click)
|
|
81
|
+
|
|
82
|
+
def _surface_color(self) -> str:
|
|
83
|
+
token = getattr(self, "_surface", None) or "background"
|
|
84
|
+
b = get_style().style_builder
|
|
85
|
+
try:
|
|
86
|
+
return b.color(token)
|
|
87
|
+
except Exception:
|
|
88
|
+
return b.color("background")
|
|
89
|
+
|
|
90
|
+
def set_ring(self, ring_photo: "PhotoImage | None") -> None:
|
|
91
|
+
"""Share the gallery's prebuilt selection-ring image with this tile."""
|
|
92
|
+
self._ring_photo = ring_photo
|
|
93
|
+
if ring_photo is not None:
|
|
94
|
+
self._canvas.itemconfigure(self._ring_id, image=ring_photo)
|
|
95
|
+
|
|
96
|
+
def update_data(self, record, image_source, caption_text, selected: bool) -> None:
|
|
97
|
+
"""Bind `record` to this tile, re-rendering the thumbnail only if changed."""
|
|
98
|
+
empty = record is None or record.get("__empty__", False)
|
|
99
|
+
self._record = None if empty else record
|
|
100
|
+
rid = None if empty else record.get("id")
|
|
101
|
+
if rid != self._cur_id:
|
|
102
|
+
self._cur_id = rid
|
|
103
|
+
if self._caption is not None:
|
|
104
|
+
self._caption.configure(
|
|
105
|
+
text="" if empty else self._fit_caption(str(caption_text)))
|
|
106
|
+
source = None if empty else image_source
|
|
107
|
+
if source is not self._cur_src and source != self._cur_src:
|
|
108
|
+
self._render_thumb(source)
|
|
109
|
+
self._cur_src = source
|
|
110
|
+
self._canvas.itemconfigure(
|
|
111
|
+
self._ring_id, state="normal" if (selected and not empty) else "hidden"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _fit_caption(self, text: str) -> str:
|
|
115
|
+
"""Ellipsize `text` to the tile width so a caption never widens its tile.
|
|
116
|
+
|
|
117
|
+
Keeps every tile a uniform width — so grid columns stay put instead of
|
|
118
|
+
shifting as longer/shorter captions scroll through them.
|
|
119
|
+
"""
|
|
120
|
+
if not text:
|
|
121
|
+
return text
|
|
122
|
+
avail = self._tw + 2 * _RING_PAD - 6
|
|
123
|
+
cap = self._caption
|
|
124
|
+
try:
|
|
125
|
+
font = cap.cget("font")
|
|
126
|
+
measure = lambda s: int(cap.tk.call("font", "measure", font, s))
|
|
127
|
+
except Exception:
|
|
128
|
+
return text
|
|
129
|
+
if measure(text) <= avail:
|
|
130
|
+
return text
|
|
131
|
+
ell = "…"
|
|
132
|
+
lo, hi = 0, len(text)
|
|
133
|
+
while lo < hi:
|
|
134
|
+
mid = (lo + hi + 1) // 2
|
|
135
|
+
if measure(text[:mid] + ell) <= avail:
|
|
136
|
+
lo = mid
|
|
137
|
+
else:
|
|
138
|
+
hi = mid - 1
|
|
139
|
+
return text[:lo] + ell if lo > 0 else ell
|
|
140
|
+
|
|
141
|
+
def _render_thumb(self, source: Any) -> None:
|
|
142
|
+
pil = resolve_pil(source)
|
|
143
|
+
if pil is None:
|
|
144
|
+
self._canvas.itemconfigure(self._img_id, image="")
|
|
145
|
+
self._photo = None
|
|
146
|
+
return
|
|
147
|
+
composed = fit_image(pil.convert("RGBA"), self._tw, self._th, self._fit)
|
|
148
|
+
if self._radius > 0:
|
|
149
|
+
composed = round_corners(composed, self._radius)
|
|
150
|
+
# A plain PhotoImage (kept alive by self._photo, GC'd when replaced) — do
|
|
151
|
+
# NOT route through the global _ImageService cache, which never evicts and
|
|
152
|
+
# would accumulate one entry per distinct thumbnail as the grid scrolls.
|
|
153
|
+
self._photo = PhotoImage(composed)
|
|
154
|
+
self._canvas.itemconfigure(self._img_id, image=self._photo)
|
|
155
|
+
|
|
156
|
+
def refresh_surface(self) -> None:
|
|
157
|
+
"""Re-resolve the canvas background after a theme change."""
|
|
158
|
+
try:
|
|
159
|
+
self._canvas.configure(background=self._surface_color())
|
|
160
|
+
except TclError:
|
|
161
|
+
pass
|
|
162
|
+
|
|
163
|
+
def _on_click(self, _event) -> None:
|
|
164
|
+
# Call the gallery directly: virtual events do NOT bubble from a child
|
|
165
|
+
# widget to an ancestor's bindings, so a container binding would never fire.
|
|
166
|
+
if self._record is not None and self._on_click_cb is not None:
|
|
167
|
+
self._on_click_cb(self._record)
|
|
168
|
+
|
|
169
|
+
def _on_double_click(self, _event) -> None:
|
|
170
|
+
if self._record is not None and self._on_activate_cb is not None:
|
|
171
|
+
self._on_activate_cb(self._record)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class Gallery(Frame):
|
|
175
|
+
"""Recycling thumbnail grid bound to a data source."""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
master: Master = None,
|
|
180
|
+
*,
|
|
181
|
+
items: list | None = None,
|
|
182
|
+
datasource: DataSourceProtocol | None = None,
|
|
183
|
+
image_field: str = "image",
|
|
184
|
+
caption_field: str | None = None,
|
|
185
|
+
columns: "int | Literal['auto']" = "auto",
|
|
186
|
+
tile_size: tuple[int, int] = (160, 160),
|
|
187
|
+
fit: FitMode = "cover",
|
|
188
|
+
corner_radius: int = 0,
|
|
189
|
+
gap: int = 8,
|
|
190
|
+
selection_mode: Literal["none", "single", "multi"] = "none",
|
|
191
|
+
accent: str | None = None,
|
|
192
|
+
scrollbar_variant: ScrollbarVariant = "thin",
|
|
193
|
+
rows: int = 2,
|
|
194
|
+
**kwargs: Any,
|
|
195
|
+
) -> None:
|
|
196
|
+
super().__init__(master, **kwargs)
|
|
197
|
+
self._scrollbar_variant = scrollbar_variant
|
|
198
|
+
self._min_rows = max(1, rows)
|
|
199
|
+
|
|
200
|
+
self._image_field = image_field
|
|
201
|
+
self._caption_field = caption_field
|
|
202
|
+
self._columns_opt = columns
|
|
203
|
+
self._tw, self._th = tile_size
|
|
204
|
+
self._fit: FitMode = fit
|
|
205
|
+
self._corner_radius = corner_radius
|
|
206
|
+
self._gap = gap
|
|
207
|
+
self._selection_mode = selection_mode
|
|
208
|
+
self._accent = accent or "primary"
|
|
209
|
+
|
|
210
|
+
page = 200
|
|
211
|
+
if datasource is not None:
|
|
212
|
+
self._datasource = datasource
|
|
213
|
+
elif items:
|
|
214
|
+
self._datasource = MemoryDataSource(page_size=page).load(items)
|
|
215
|
+
else:
|
|
216
|
+
self._datasource = MemoryDataSource(page_size=page).load([])
|
|
217
|
+
|
|
218
|
+
self._tiles: list[GalleryTile] = []
|
|
219
|
+
self._cols = 1
|
|
220
|
+
self._cfg_cols = 0 # highest grid column given a weight, for resetting
|
|
221
|
+
self._visible_rows = 1
|
|
222
|
+
self._pool_rows = 1
|
|
223
|
+
self._start_row = 0
|
|
224
|
+
self._ring_photo: PhotoImage | None = None
|
|
225
|
+
self._row_h = self._th + 2 * _RING_PAD + (_CAPTION_H if caption_field else 0) + gap
|
|
226
|
+
|
|
227
|
+
sb_surface = getattr(self, '_surface', None)
|
|
228
|
+
sb_kw = {'surface': sb_surface} if sb_surface else {}
|
|
229
|
+
self._scrollbar = Scrollbar(
|
|
230
|
+
self, orient="vertical", command=self._on_scroll,
|
|
231
|
+
variant=self._scrollbar_variant, **sb_kw)
|
|
232
|
+
self._scrollbar.pack(side="right", fill="y")
|
|
233
|
+
self._container = Frame(self)
|
|
234
|
+
self._container.pack(side="left", fill="both", expand=True)
|
|
235
|
+
|
|
236
|
+
# Requested-height FLOOR so the grid never reports ~0 height in a
|
|
237
|
+
# container that doesn't impose one (a scroll view, an auto-fit window).
|
|
238
|
+
# The public wrapper freezes this frame with `pack_propagate(False)`, so
|
|
239
|
+
# its own configured `height` is its requested height; `fill`/`expand`
|
|
240
|
+
# still grow it past this floor when the parent has room.
|
|
241
|
+
self.configure(height=self._min_rows * self._row_h)
|
|
242
|
+
|
|
243
|
+
# Bind resize on the CONTAINER and use the event's width/height directly —
|
|
244
|
+
# winfo_width() read during the parent's <Configure> lags a cycle behind
|
|
245
|
+
# the new size, which would wedge the column count at its initial value.
|
|
246
|
+
self._container.bind("<Configure>", self._on_resize, add="+")
|
|
247
|
+
self._bind_scroll(self)
|
|
248
|
+
self._bind_scroll(self._container)
|
|
249
|
+
|
|
250
|
+
self._change_sub = None
|
|
251
|
+
on_change = getattr(self._datasource, "on_change", None)
|
|
252
|
+
if callable(on_change):
|
|
253
|
+
try:
|
|
254
|
+
self._change_sub = on_change(self._on_source_change)
|
|
255
|
+
except Exception:
|
|
256
|
+
self._change_sub = None
|
|
257
|
+
self.bind("<Destroy>", self._on_destroy, add="+")
|
|
258
|
+
# Tile surfaces + the accent ring are painted imperatively, so re-resolve
|
|
259
|
+
# on a theme change — gated to on-screen by the Frame base hook (an
|
|
260
|
+
# off-screen gallery defers its thumbnail-grid repaint to <Map>).
|
|
261
|
+
# First render is driven by the container's <Configure> (bound above);
|
|
262
|
+
# no init timer needed.
|
|
263
|
+
|
|
264
|
+
# ----- theme / silence / source change ----------------------------------
|
|
265
|
+
|
|
266
|
+
def _bs_apply_theme(self, _event=None) -> None:
|
|
267
|
+
"""Re-apply theme-dependent colors after a theme switch.
|
|
268
|
+
|
|
269
|
+
The tile surface backgrounds and the accent selection ring are resolved
|
|
270
|
+
from theme tokens; rebuild them so they track the new theme.
|
|
271
|
+
"""
|
|
272
|
+
for tile in self._tiles:
|
|
273
|
+
tile.refresh_surface()
|
|
274
|
+
if self._tiles:
|
|
275
|
+
self._build_ring() # new accent color, re-shared to every tile
|
|
276
|
+
|
|
277
|
+
def _silence_source(self):
|
|
278
|
+
silence = getattr(self._datasource, "_silence", None)
|
|
279
|
+
return silence() if callable(silence) else contextlib.nullcontext()
|
|
280
|
+
|
|
281
|
+
def _on_source_change(self, _event: Any = None) -> None:
|
|
282
|
+
try:
|
|
283
|
+
self._relayout()
|
|
284
|
+
except Exception:
|
|
285
|
+
pass
|
|
286
|
+
|
|
287
|
+
def _on_destroy(self, event: Any = None) -> None:
|
|
288
|
+
if event is not None and getattr(event, "widget", None) is not self:
|
|
289
|
+
return
|
|
290
|
+
sub, self._change_sub = self._change_sub, None
|
|
291
|
+
if sub is not None:
|
|
292
|
+
try:
|
|
293
|
+
sub.cancel()
|
|
294
|
+
except Exception:
|
|
295
|
+
pass
|
|
296
|
+
|
|
297
|
+
# ----- layout / recycle --------------------------------------------------
|
|
298
|
+
|
|
299
|
+
def _resolve_columns(self, width: int) -> int:
|
|
300
|
+
if self._columns_opt != "auto":
|
|
301
|
+
return max(1, int(self._columns_opt))
|
|
302
|
+
cell = self._tw + 2 * _RING_PAD
|
|
303
|
+
# Each tile occupies cell + gap of width — _regrid gives every tile a
|
|
304
|
+
# symmetric padx of gap//2 on both sides. So the count is
|
|
305
|
+
# width // (cell + gap); the "(n-1) gaps between items" form
|
|
306
|
+
# ((width + gap) // (cell + gap)) permits one column too many and clips
|
|
307
|
+
# the last column off the frame's right edge.
|
|
308
|
+
return max(1, width // (cell + self._gap))
|
|
309
|
+
|
|
310
|
+
def _on_resize(self, event) -> None:
|
|
311
|
+
self._relayout(event.width, event.height)
|
|
312
|
+
|
|
313
|
+
def _relayout(self, width: int | None = None, height: int | None = None) -> None:
|
|
314
|
+
"""Recompute the column count and tile pool, then refill.
|
|
315
|
+
|
|
316
|
+
`width`/`height` come straight from a `<Configure>` event when available
|
|
317
|
+
(authoritative); otherwise the container is measured (the initial tick).
|
|
318
|
+
"""
|
|
319
|
+
if width is None or height is None:
|
|
320
|
+
try:
|
|
321
|
+
width = self._container.winfo_width()
|
|
322
|
+
height = self._container.winfo_height()
|
|
323
|
+
except TclError:
|
|
324
|
+
return
|
|
325
|
+
if width <= 1 or height <= 1:
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
cols = self._resolve_columns(width)
|
|
329
|
+
visible_rows = max(1, height // self._row_h + 1)
|
|
330
|
+
pool_rows = visible_rows + OVERSCAN_ROWS
|
|
331
|
+
needed = cols * pool_rows
|
|
332
|
+
|
|
333
|
+
rebuild_geometry = cols != self._cols
|
|
334
|
+
self._cols = cols
|
|
335
|
+
self._visible_rows = visible_rows
|
|
336
|
+
self._pool_rows = pool_rows
|
|
337
|
+
|
|
338
|
+
if self._ring_photo is None or rebuild_geometry:
|
|
339
|
+
self._build_ring()
|
|
340
|
+
|
|
341
|
+
self._ensure_pool(needed)
|
|
342
|
+
self._regrid()
|
|
343
|
+
self._update_tiles()
|
|
344
|
+
|
|
345
|
+
def _build_ring(self) -> None:
|
|
346
|
+
"""Build the shared antialiased accent selection-ring overlay image.
|
|
347
|
+
|
|
348
|
+
Sized to the full padded canvas; the ring stroke runs near the canvas
|
|
349
|
+
edge so it sits a `_RING_GAP` outside the centered image, with corners
|
|
350
|
+
concentric to the image's rounded corners.
|
|
351
|
+
"""
|
|
352
|
+
w, h = self._tw + 2 * _RING_PAD, self._th + 2 * _RING_PAD
|
|
353
|
+
color = get_style().style_builder.color(self._accent)
|
|
354
|
+
ss = 4
|
|
355
|
+
big = PILImage.new("RGBA", (w * ss, h * ss), (0, 0, 0, 0))
|
|
356
|
+
t = _RING_THICKNESS * ss
|
|
357
|
+
inset = t // 2
|
|
358
|
+
radius = self._corner_radius + _RING_GAP + _RING_THICKNESS // 2
|
|
359
|
+
ImageDraw.Draw(big).rounded_rectangle(
|
|
360
|
+
[inset, inset, w * ss - 1 - inset, h * ss - 1 - inset],
|
|
361
|
+
radius=max(0, radius) * ss, outline=color, width=t,
|
|
362
|
+
)
|
|
363
|
+
self._ring_photo = PhotoImage(image=big.resize((w, h), Resampling.LANCZOS))
|
|
364
|
+
for tile in self._tiles:
|
|
365
|
+
tile.set_ring(self._ring_photo)
|
|
366
|
+
|
|
367
|
+
def _ensure_pool(self, needed: int) -> None:
|
|
368
|
+
while len(self._tiles) < needed:
|
|
369
|
+
tile = GalleryTile(
|
|
370
|
+
self._container, tile_w=self._tw, tile_h=self._th, fit=self._fit,
|
|
371
|
+
corner_radius=self._corner_radius, caption=bool(self._caption_field),
|
|
372
|
+
on_click=self._handle_tile_click, on_activate=self._handle_tile_activate,
|
|
373
|
+
)
|
|
374
|
+
tile.set_ring(self._ring_photo)
|
|
375
|
+
self._bind_scroll(tile)
|
|
376
|
+
self._bind_scroll(tile._canvas)
|
|
377
|
+
if tile._caption is not None:
|
|
378
|
+
self._bind_scroll(tile._caption)
|
|
379
|
+
self._tiles.append(tile)
|
|
380
|
+
while len(self._tiles) > needed:
|
|
381
|
+
tile = self._tiles.pop()
|
|
382
|
+
try:
|
|
383
|
+
tile.destroy()
|
|
384
|
+
except TclError:
|
|
385
|
+
pass
|
|
386
|
+
|
|
387
|
+
def _regrid(self) -> None:
|
|
388
|
+
pad = self._gap // 2
|
|
389
|
+
# Weight the active columns equally so the fixed-width tiles spread to
|
|
390
|
+
# fill the row instead of left-packing with a ragged right margin (which
|
|
391
|
+
# reads as a wasted extra column). Tiles are gridded without sticky, so
|
|
392
|
+
# each stays its cell width and centers in its column. Stale columns from
|
|
393
|
+
# a wider previous layout are zeroed.
|
|
394
|
+
for c in range(self._cfg_cols):
|
|
395
|
+
self._container.grid_columnconfigure(c, weight=0, uniform="")
|
|
396
|
+
for c in range(self._cols):
|
|
397
|
+
self._container.grid_columnconfigure(c, weight=1, uniform="gallerycol")
|
|
398
|
+
self._cfg_cols = self._cols
|
|
399
|
+
for i, tile in enumerate(self._tiles):
|
|
400
|
+
tile.grid(row=i // self._cols, column=i % self._cols, padx=pad, pady=pad)
|
|
401
|
+
|
|
402
|
+
def _total_rows(self) -> int:
|
|
403
|
+
count = self._datasource.count
|
|
404
|
+
return (count + self._cols - 1) // self._cols if count else 0
|
|
405
|
+
|
|
406
|
+
def _clamp(self) -> None:
|
|
407
|
+
max_start = max(0, self._total_rows() - self._visible_rows)
|
|
408
|
+
self._start_row = max(0, min(self._start_row, max_start))
|
|
409
|
+
|
|
410
|
+
def _update_tiles(self) -> None:
|
|
411
|
+
self._clamp()
|
|
412
|
+
start_index = self._start_row * self._cols
|
|
413
|
+
window = self._datasource.page_slice(start_index, len(self._tiles))
|
|
414
|
+
for i, tile in enumerate(self._tiles):
|
|
415
|
+
if i < len(window):
|
|
416
|
+
raw = window[i]
|
|
417
|
+
pub = self._public(raw)
|
|
418
|
+
rid = pub.get("id")
|
|
419
|
+
image = raw.get(self._image_field)
|
|
420
|
+
caption = raw.get(self._caption_field, "") if self._caption_field else ""
|
|
421
|
+
selected = False
|
|
422
|
+
if rid is not None:
|
|
423
|
+
try:
|
|
424
|
+
selected = bool(self._datasource.is_selected(rid))
|
|
425
|
+
except Exception:
|
|
426
|
+
selected = False
|
|
427
|
+
tile.update_data(pub, image, caption, selected)
|
|
428
|
+
else:
|
|
429
|
+
tile.update_data(None, None, "", False)
|
|
430
|
+
self._update_scrollbar()
|
|
431
|
+
|
|
432
|
+
def _update_scrollbar(self) -> None:
|
|
433
|
+
total = max(1, self._total_rows())
|
|
434
|
+
first = self._start_row / total
|
|
435
|
+
last = min(1.0, (self._start_row + self._visible_rows) / total)
|
|
436
|
+
self._scrollbar.set(first, last)
|
|
437
|
+
|
|
438
|
+
# ----- scrolling ---------------------------------------------------------
|
|
439
|
+
|
|
440
|
+
def _on_scroll(self, *args) -> None:
|
|
441
|
+
if args[0] == "moveto":
|
|
442
|
+
fraction = float(args[1])
|
|
443
|
+
max_start = max(0, self._total_rows() - self._visible_rows)
|
|
444
|
+
self._start_row = int(round(fraction * max_start))
|
|
445
|
+
elif args[0] == "scroll":
|
|
446
|
+
amount, unit = int(args[1]), args[2]
|
|
447
|
+
step = max(1, self._visible_rows - 1) if unit == "pages" else 1
|
|
448
|
+
self._start_row += amount * step
|
|
449
|
+
self._clamp()
|
|
450
|
+
self._update_tiles()
|
|
451
|
+
|
|
452
|
+
def _bind_scroll(self, widget) -> None:
|
|
453
|
+
widget.bind("<MouseWheel>", self._on_wheel, add="+")
|
|
454
|
+
widget.bind("<Button-4>", self._on_wheel, add="+")
|
|
455
|
+
widget.bind("<Button-5>", self._on_wheel, add="+")
|
|
456
|
+
|
|
457
|
+
def _on_wheel(self, event) -> None:
|
|
458
|
+
if getattr(event, "num", None) == 4:
|
|
459
|
+
delta = 1
|
|
460
|
+
elif getattr(event, "num", None) == 5:
|
|
461
|
+
delta = -1
|
|
462
|
+
else:
|
|
463
|
+
delta = 1 if event.delta < 0 else -1
|
|
464
|
+
self._start_row += delta
|
|
465
|
+
self._clamp()
|
|
466
|
+
self._update_tiles()
|
|
467
|
+
return "break"
|
|
468
|
+
|
|
469
|
+
def scroll_to_top(self) -> None:
|
|
470
|
+
self._start_row = 0
|
|
471
|
+
self._update_tiles()
|
|
472
|
+
|
|
473
|
+
def scroll_to_bottom(self) -> None:
|
|
474
|
+
self._start_row = self._total_rows()
|
|
475
|
+
self._clamp()
|
|
476
|
+
self._update_tiles()
|
|
477
|
+
|
|
478
|
+
# ----- selection ---------------------------------------------------------
|
|
479
|
+
|
|
480
|
+
def get_selected(self) -> list[dict]:
|
|
481
|
+
try:
|
|
482
|
+
rows = self._datasource.selected()
|
|
483
|
+
except Exception:
|
|
484
|
+
rows = []
|
|
485
|
+
return [self._public(r) for r in rows]
|
|
486
|
+
|
|
487
|
+
def _public(self, raw: dict) -> dict:
|
|
488
|
+
pub = getattr(self._datasource, "_public_record", None)
|
|
489
|
+
return pub(raw) if callable(pub) else dict(raw)
|
|
490
|
+
|
|
491
|
+
def select_items(self, record_ids: list) -> None:
|
|
492
|
+
with self._silence_source():
|
|
493
|
+
if self._selection_mode == "single":
|
|
494
|
+
self._datasource.deselect_all()
|
|
495
|
+
if record_ids:
|
|
496
|
+
self._datasource.select(record_ids[-1])
|
|
497
|
+
else:
|
|
498
|
+
for rid in record_ids:
|
|
499
|
+
self._datasource.select(rid)
|
|
500
|
+
self._after_selection_change()
|
|
501
|
+
|
|
502
|
+
def deselect_items(self, record_ids: list) -> None:
|
|
503
|
+
with self._silence_source():
|
|
504
|
+
for rid in record_ids:
|
|
505
|
+
self._datasource.deselect(rid)
|
|
506
|
+
self._after_selection_change()
|
|
507
|
+
|
|
508
|
+
def select_all(self) -> None:
|
|
509
|
+
if self._selection_mode != "multi":
|
|
510
|
+
return
|
|
511
|
+
with self._silence_source():
|
|
512
|
+
self._datasource.select_all()
|
|
513
|
+
self._after_selection_change()
|
|
514
|
+
|
|
515
|
+
def clear_selection(self) -> None:
|
|
516
|
+
with self._silence_source():
|
|
517
|
+
self._datasource.deselect_all()
|
|
518
|
+
self._after_selection_change()
|
|
519
|
+
|
|
520
|
+
def _after_selection_change(self) -> None:
|
|
521
|
+
self._update_tiles()
|
|
522
|
+
self.event_generate("<<SelectionChange>>")
|
|
523
|
+
|
|
524
|
+
# ----- tile events -------------------------------------------------------
|
|
525
|
+
|
|
526
|
+
def _handle_tile_click(self, record: dict) -> None:
|
|
527
|
+
rid = record.get("id")
|
|
528
|
+
if self._selection_mode != "none" and rid is not None:
|
|
529
|
+
with self._silence_source():
|
|
530
|
+
if self._selection_mode == "single":
|
|
531
|
+
self._datasource.deselect_all()
|
|
532
|
+
self._datasource.select(rid)
|
|
533
|
+
else:
|
|
534
|
+
if self._datasource.is_selected(rid):
|
|
535
|
+
self._datasource.deselect(rid)
|
|
536
|
+
else:
|
|
537
|
+
self._datasource.select(rid)
|
|
538
|
+
self._update_tiles()
|
|
539
|
+
self.event_generate("<<SelectionChange>>")
|
|
540
|
+
# record is already the public record (the tile holds the projected copy).
|
|
541
|
+
self.event_generate("<<ItemClick>>", data=record)
|
|
542
|
+
|
|
543
|
+
def _handle_tile_activate(self, record: dict) -> None:
|
|
544
|
+
if record:
|
|
545
|
+
self.event_generate("<<ItemActivate>>", data=record)
|
|
546
|
+
|
|
547
|
+
def get_datasource(self) -> DataSourceProtocol:
|
|
548
|
+
return self._datasource
|
|
549
|
+
|
|
550
|
+
def reload(self) -> None:
|
|
551
|
+
self._relayout()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""List widgets for displaying collections of items."""
|
|
2
|
+
|
|
3
|
+
from bootstack.widgets._impl.composites.list.listitem import ListItem
|
|
4
|
+
from bootstack.widgets._impl.composites.list.listview import (
|
|
5
|
+
ListView,
|
|
6
|
+
MemoryDataSource,
|
|
7
|
+
DataSourceProtocol,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
'ListItem',
|
|
12
|
+
'ListView',
|
|
13
|
+
'MemoryDataSource',
|
|
14
|
+
'DataSourceProtocol',
|
|
15
|
+
]
|