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,764 @@
|
|
|
1
|
+
"""Canvas-based scrollable container widget with mouse wheel support."""
|
|
2
|
+
from tkinter import Canvas
|
|
3
|
+
from tkinter.ttk import Widget
|
|
4
|
+
from typing import Any, Literal
|
|
5
|
+
|
|
6
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
7
|
+
from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
|
|
8
|
+
from bootstack.widgets._impl.primitives.scrollbar import Scrollbar
|
|
9
|
+
from bootstack.widgets.types import Master
|
|
10
|
+
from bootstack.events import ScrollEvent
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ScrollView(Frame):
|
|
14
|
+
"""A canvas-based scrollable container with configurable scrollbar behavior.
|
|
15
|
+
|
|
16
|
+
The ScrollView widget provides a scrollable area for child widgets with
|
|
17
|
+
full mouse wheel support on all descendants. Scrollbars can be configured
|
|
18
|
+
to appear always, never, on hover, or when scrolling, and are only visible
|
|
19
|
+
when the content exceeds the available space.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
canvas: The underlying tkinter Canvas widget.
|
|
23
|
+
vertical_scrollbar: The vertical scrollbar widget.
|
|
24
|
+
horizontal_scrollbar: The horizontal scrollbar widget.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
master: Master = None,
|
|
30
|
+
scroll_direction: Literal['horizontal', 'vertical', 'both'] = 'both',
|
|
31
|
+
scrollbar_visibility: Literal['always', 'never', 'hover', 'scroll'] = 'always',
|
|
32
|
+
autohide_delay: int = 1000, # milliseconds for scroll mode
|
|
33
|
+
scrollbar_variant: str = 'default',
|
|
34
|
+
**kwargs: Any
|
|
35
|
+
):
|
|
36
|
+
"""Initialize a ScrollView widget.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
master: The parent widget.
|
|
40
|
+
scroll_direction: Scroll direction - 'horizontal' for horizontal only,
|
|
41
|
+
'vertical' for vertical only, or 'both' for bidirectional
|
|
42
|
+
scrolling. Horizontal scrolling uses Shift+MouseWheel.
|
|
43
|
+
scrollbar_visibility: Scrollbar visibility mode:
|
|
44
|
+
- 'always': Scrollbars always visible
|
|
45
|
+
- 'never': Scrollbars hidden (scrolling still works)
|
|
46
|
+
- 'hover': Scrollbars appear when mouse enters the widget
|
|
47
|
+
- 'scroll': Scrollbars appear when scrolling, auto-hide after delay
|
|
48
|
+
autohide_delay: Time in milliseconds before auto-hiding scrollbars
|
|
49
|
+
in 'scroll' mode. Default is 1000ms (1 second).
|
|
50
|
+
scrollbar_variant: The variant to apply to scrollbars (e.g., 'default',
|
|
51
|
+
'round'). If None, uses the default scrollbar variant.
|
|
52
|
+
**kwargs: Additional keyword arguments passed to the Frame parent class.
|
|
53
|
+
|
|
54
|
+
Note:
|
|
55
|
+
Mouse wheel scrolling is automatically enabled on all child widgets,
|
|
56
|
+
including those added dynamically. For manual refresh of bindings
|
|
57
|
+
after adding many widgets at once, call refresh_bindings().
|
|
58
|
+
"""
|
|
59
|
+
canvas_height = kwargs.pop('height', None)
|
|
60
|
+
canvas_width = kwargs.pop('width', None)
|
|
61
|
+
super().__init__(master, **kwargs)
|
|
62
|
+
|
|
63
|
+
# configuration
|
|
64
|
+
self._direction = scroll_direction
|
|
65
|
+
self._scrollbar_visibility = scrollbar_visibility
|
|
66
|
+
self._autohide_delay = autohide_delay
|
|
67
|
+
self._scrollbar_variant = scrollbar_variant
|
|
68
|
+
|
|
69
|
+
self._child_widget = None
|
|
70
|
+
self._window_id = None
|
|
71
|
+
self._hide_timer = None
|
|
72
|
+
self._scrolling_enabled = False
|
|
73
|
+
self._hovering = False
|
|
74
|
+
|
|
75
|
+
# Create unique bind tag for this scrollview
|
|
76
|
+
self._scroll_tag = f'ScrollView_{id(self)}'
|
|
77
|
+
|
|
78
|
+
# Detect windowing system
|
|
79
|
+
self.winsys = self.tk.call("tk", "windowingsystem")
|
|
80
|
+
|
|
81
|
+
# Bind scroll events to our custom tag
|
|
82
|
+
self._setup_scroll_tag_bindings()
|
|
83
|
+
|
|
84
|
+
# Create canvas
|
|
85
|
+
canvas_kw: dict[str, Any] = {"highlightthickness": 0, "borderwidth": 0}
|
|
86
|
+
if canvas_height is not None:
|
|
87
|
+
canvas_kw["height"] = canvas_height
|
|
88
|
+
if canvas_width is not None:
|
|
89
|
+
canvas_kw["width"] = canvas_width
|
|
90
|
+
self.canvas = Canvas(self, **canvas_kw)
|
|
91
|
+
self.canvas.bind("<Configure>", self._on_canvas_configure)
|
|
92
|
+
# Focus canvas on click so keyboard scrolling works.
|
|
93
|
+
self.canvas.bind("<Button-1>", lambda e: self.canvas.focus_set(), add="+")
|
|
94
|
+
self._setup_keyboard_bindings()
|
|
95
|
+
|
|
96
|
+
# Create scrollbars. Thread our own surface through so the scrollbar track
|
|
97
|
+
# matches the container (an auto-hiding bar then leaves no tinted gutter).
|
|
98
|
+
sb_surface = getattr(self, "_surface", None)
|
|
99
|
+
sb_kw: dict[str, Any] = {"surface": sb_surface} if sb_surface else {}
|
|
100
|
+
self.vertical_scrollbar = Scrollbar(
|
|
101
|
+
master=self,
|
|
102
|
+
orient='vertical',
|
|
103
|
+
command=self.canvas.yview,
|
|
104
|
+
variant=self._scrollbar_variant,
|
|
105
|
+
**sb_kw,
|
|
106
|
+
)
|
|
107
|
+
self.horizontal_scrollbar = Scrollbar(
|
|
108
|
+
master=self,
|
|
109
|
+
orient='horizontal',
|
|
110
|
+
command=self.canvas.xview,
|
|
111
|
+
variant=self._scrollbar_variant,
|
|
112
|
+
**sb_kw,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Configure canvas scrolling
|
|
116
|
+
scroll_config = {}
|
|
117
|
+
if scroll_direction in ('vertical', 'both'):
|
|
118
|
+
scroll_config['yscrollcommand'] = self._on_canvas_scroll_y
|
|
119
|
+
if scroll_direction in ('horizontal', 'both'):
|
|
120
|
+
scroll_config['xscrollcommand'] = self._on_canvas_scroll_x
|
|
121
|
+
|
|
122
|
+
self.canvas.configure(**scroll_config)
|
|
123
|
+
|
|
124
|
+
# Layout
|
|
125
|
+
self._layout_widgets()
|
|
126
|
+
|
|
127
|
+
# Bind events for autohide/hover
|
|
128
|
+
self._bind_container_events()
|
|
129
|
+
|
|
130
|
+
# Initial scrollbar visibility
|
|
131
|
+
self._update_scrollbar_visibility()
|
|
132
|
+
|
|
133
|
+
# Finalize content sizing on first show. A ScrollView is usually built
|
|
134
|
+
# inside a hidden window (the `with App()/Workbench()` block), so the
|
|
135
|
+
# canvas <Configure> that pins the content width to the viewport may fire
|
|
136
|
+
# at a transient size — or never at the final size — leaving the content
|
|
137
|
+
# at its natural width (inflated by wide children, e.g. a chart's default
|
|
138
|
+
# request). It then overflows until a user resize or theme change triggers
|
|
139
|
+
# a fresh <Configure>. Re-pin the width and recompute the scroll region
|
|
140
|
+
# once we are actually mapped.
|
|
141
|
+
self._first_map_done = False
|
|
142
|
+
self.bind('<Map>', self._on_first_map, add='+')
|
|
143
|
+
|
|
144
|
+
@configure_delegate('scroll_direction')
|
|
145
|
+
def _delegate_scroll_direction(self, value=None):
|
|
146
|
+
"""Get or set the scroll direction."""
|
|
147
|
+
if value is None:
|
|
148
|
+
return self._direction
|
|
149
|
+
else:
|
|
150
|
+
old_direction = self._direction
|
|
151
|
+
self._direction = value
|
|
152
|
+
|
|
153
|
+
# Update canvas scroll configuration
|
|
154
|
+
scroll_config = {}
|
|
155
|
+
if value in ('vertical', 'both'):
|
|
156
|
+
scroll_config['yscrollcommand'] = self._on_canvas_scroll_y
|
|
157
|
+
else:
|
|
158
|
+
scroll_config['yscrollcommand'] = None
|
|
159
|
+
|
|
160
|
+
if value in ('horizontal', 'both'):
|
|
161
|
+
scroll_config['xscrollcommand'] = self._on_canvas_scroll_x
|
|
162
|
+
else:
|
|
163
|
+
scroll_config['xscrollcommand'] = None
|
|
164
|
+
|
|
165
|
+
self.canvas.configure(**scroll_config)
|
|
166
|
+
|
|
167
|
+
# Update scrollbar visibility and layout
|
|
168
|
+
self._update_scrollbar_visibility()
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
@configure_delegate('scrollbar_visibility')
|
|
172
|
+
def _delegate_scrollbar_visibility(self, value=None):
|
|
173
|
+
"""Get or set the scrollbar visibility mode."""
|
|
174
|
+
if value is None:
|
|
175
|
+
return self._scrollbar_visibility
|
|
176
|
+
else:
|
|
177
|
+
old_value = self._scrollbar_visibility
|
|
178
|
+
self._scrollbar_visibility = value
|
|
179
|
+
|
|
180
|
+
# Unbind old events if changing from hover
|
|
181
|
+
if old_value == 'hover':
|
|
182
|
+
self.unbind('<Enter>')
|
|
183
|
+
self.unbind('<Leave>')
|
|
184
|
+
self.canvas.unbind('<Enter>')
|
|
185
|
+
self.canvas.unbind('<Leave>')
|
|
186
|
+
self.vertical_scrollbar.unbind('<Enter>')
|
|
187
|
+
self.vertical_scrollbar.unbind('<Leave>')
|
|
188
|
+
self.horizontal_scrollbar.unbind('<Enter>')
|
|
189
|
+
self.horizontal_scrollbar.unbind('<Leave>')
|
|
190
|
+
|
|
191
|
+
# Sync gutter reservation with the new mode
|
|
192
|
+
if value in ('hover', 'scroll'):
|
|
193
|
+
self._set_scrollbar_gutter(reserve=True)
|
|
194
|
+
else:
|
|
195
|
+
self._set_scrollbar_gutter(reserve=False)
|
|
196
|
+
|
|
197
|
+
# Bind new events and update scrollbar visibility
|
|
198
|
+
self._bind_container_events()
|
|
199
|
+
self._update_scrollbar_visibility()
|
|
200
|
+
|
|
201
|
+
# Update scrolling enabled state
|
|
202
|
+
if value in ('always', 'never', 'scroll'):
|
|
203
|
+
if self._child_widget:
|
|
204
|
+
self.enable_scrolling()
|
|
205
|
+
elif value == 'hover':
|
|
206
|
+
# Scrolling will be enabled on hover
|
|
207
|
+
self.disable_scrolling()
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
@configure_delegate('autohide_delay')
|
|
211
|
+
def _delegate_autohide_delay(self, value=None):
|
|
212
|
+
"""Get or set the autohide delay in milliseconds."""
|
|
213
|
+
if value is None:
|
|
214
|
+
return self._autohide_delay
|
|
215
|
+
else:
|
|
216
|
+
self._autohide_delay = value
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
@configure_delegate('scrollbar_variant')
|
|
220
|
+
def _delegate_scrollbar_variant(self, value=None):
|
|
221
|
+
"""Get or set the scrollbar variant."""
|
|
222
|
+
if value is None:
|
|
223
|
+
return self._scrollbar_variant
|
|
224
|
+
else:
|
|
225
|
+
self._scrollbar_variant = value
|
|
226
|
+
# Apply the new variant to both scrollbars
|
|
227
|
+
if value:
|
|
228
|
+
self.vertical_scrollbar.configure(variant=value)
|
|
229
|
+
self.horizontal_scrollbar.configure(variant=value)
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
def _setup_scroll_tag_bindings(self):
|
|
233
|
+
"""Setup bindings on our custom bind tag."""
|
|
234
|
+
if self.winsys.lower() == "x11":
|
|
235
|
+
self.bind_class(self._scroll_tag, "<Button-4>", self._on_mousewheel)
|
|
236
|
+
self.bind_class(self._scroll_tag, "<Button-5>", self._on_mousewheel)
|
|
237
|
+
self.bind_class(self._scroll_tag, "<Shift-Button-4>", self._on_shift_mousewheel)
|
|
238
|
+
self.bind_class(self._scroll_tag, "<Shift-Button-5>", self._on_shift_mousewheel)
|
|
239
|
+
else:
|
|
240
|
+
self.bind_class(self._scroll_tag, "<MouseWheel>", self._on_mousewheel)
|
|
241
|
+
self.bind_class(self._scroll_tag, "<Shift-MouseWheel>", self._on_shift_mousewheel)
|
|
242
|
+
|
|
243
|
+
def _setup_keyboard_bindings(self) -> None:
|
|
244
|
+
"""Bind arrow / page / home / end keys to the canvas for keyboard scroll."""
|
|
245
|
+
c = self.canvas
|
|
246
|
+
# Vertical
|
|
247
|
+
c.bind("<Up>", lambda e: self.canvas.yview_scroll(-1, "units") or "break", add="+")
|
|
248
|
+
c.bind("<Down>", lambda e: self.canvas.yview_scroll( 1, "units") or "break", add="+")
|
|
249
|
+
c.bind("<Prior>", lambda e: self.canvas.yview_scroll(-1, "pages") or "break", add="+")
|
|
250
|
+
c.bind("<Next>", lambda e: self.canvas.yview_scroll( 1, "pages") or "break", add="+")
|
|
251
|
+
c.bind("<Home>", lambda e: self.canvas.yview_moveto(0.0) or "break", add="+")
|
|
252
|
+
c.bind("<End>", lambda e: self.canvas.yview_moveto(1.0) or "break", add="+")
|
|
253
|
+
# Horizontal
|
|
254
|
+
c.bind("<Left>", lambda e: self.canvas.xview_scroll(-1, "units") or "break", add="+")
|
|
255
|
+
c.bind("<Right>", lambda e: self.canvas.xview_scroll( 1, "units") or "break", add="+")
|
|
256
|
+
|
|
257
|
+
def _layout_widgets(self):
|
|
258
|
+
"""Layout the canvas and scrollbars.
|
|
259
|
+
|
|
260
|
+
For 'hover' and 'scroll' visibility modes the grid column and row that
|
|
261
|
+
hold the scrollbars are given a fixed minimum size equal to the
|
|
262
|
+
scrollbar's natural dimensions before the scrollbars are hidden. This
|
|
263
|
+
reserves the gutter permanently so the canvas never resizes when a
|
|
264
|
+
scrollbar is toggled — the same principle as the CSS
|
|
265
|
+
`scrollbar-gutter: stable` property.
|
|
266
|
+
|
|
267
|
+
An overlay approach (lift/lower over the canvas) was considered but
|
|
268
|
+
rejected because the scrollbar then covers content, which was reported
|
|
269
|
+
as confusing by users. Reserving the gutter avoids both the
|
|
270
|
+
layout-shift problem *and* the content-coverage problem.
|
|
271
|
+
|
|
272
|
+
For 'never' mode the scrollbars are never placed in the grid at all.
|
|
273
|
+
Gridding them and immediately calling grid_remove() leaves Tkinter's
|
|
274
|
+
geometry manager holding the column's natural width until the next idle
|
|
275
|
+
cycle, which produces a visible gap beside the content area.
|
|
276
|
+
"""
|
|
277
|
+
self.canvas.grid(row=0, column=0, sticky='nsew')
|
|
278
|
+
|
|
279
|
+
# Configure grid weights
|
|
280
|
+
self.grid_rowconfigure(0, weight=1)
|
|
281
|
+
self.grid_columnconfigure(0, weight=1)
|
|
282
|
+
|
|
283
|
+
if self._scrollbar_visibility == 'never':
|
|
284
|
+
# Don't touch the grid for the scrollbar columns/rows — column 1
|
|
285
|
+
# and row 1 stay at zero width so no space is reserved.
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
if self._direction in ('vertical', 'both'):
|
|
289
|
+
self.vertical_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
290
|
+
|
|
291
|
+
if self._direction in ('horizontal', 'both'):
|
|
292
|
+
self.horizontal_scrollbar.grid(row=1, column=0, sticky='ew')
|
|
293
|
+
|
|
294
|
+
# Keep scrollbars above the canvas/content when visible
|
|
295
|
+
self.vertical_scrollbar.lift()
|
|
296
|
+
self.horizontal_scrollbar.lift()
|
|
297
|
+
|
|
298
|
+
if self._scrollbar_visibility in ('hover', 'scroll'):
|
|
299
|
+
# Hide scrollbars now; defer gutter measurement to after_idle so
|
|
300
|
+
# the style system has finished sizing the widgets before we read
|
|
301
|
+
# their dimensions. Measuring too early (before the widget is
|
|
302
|
+
# placed in its parent) can return an inflated reqwidth and leave
|
|
303
|
+
# a visible gap beside the scrollbar when it appears.
|
|
304
|
+
self.vertical_scrollbar.grid_remove()
|
|
305
|
+
self.horizontal_scrollbar.grid_remove()
|
|
306
|
+
self.after_idle(lambda: self._set_scrollbar_gutter(reserve=True))
|
|
307
|
+
|
|
308
|
+
def _set_scrollbar_gutter(self, reserve: bool):
|
|
309
|
+
"""Reserve or release the grid gutter used by each scrollbar.
|
|
310
|
+
|
|
311
|
+
When *reserve* is `True` the column (vertical scrollbar) and row
|
|
312
|
+
(horizontal scrollbar) are given a `minsize` equal to the scrollbar's
|
|
313
|
+
natural requested dimensions. The minsize persists even after the
|
|
314
|
+
scrollbar widget is removed from the grid via `grid_remove()`, so the
|
|
315
|
+
canvas occupies a constant area regardless of scrollbar visibility.
|
|
316
|
+
|
|
317
|
+
When *reserve* is `False` the minsize is cleared (set to 0), which
|
|
318
|
+
is appropriate for 'always' and 'never' modes where no gutter needs to
|
|
319
|
+
be held open.
|
|
320
|
+
"""
|
|
321
|
+
if reserve:
|
|
322
|
+
if self._direction in ('vertical', 'both'):
|
|
323
|
+
width = self.vertical_scrollbar.winfo_reqwidth()
|
|
324
|
+
if width > 1:
|
|
325
|
+
self.grid_columnconfigure(1, minsize=width)
|
|
326
|
+
if self._direction in ('horizontal', 'both'):
|
|
327
|
+
height = self.horizontal_scrollbar.winfo_reqheight()
|
|
328
|
+
if height > 1:
|
|
329
|
+
self.grid_rowconfigure(1, minsize=height)
|
|
330
|
+
else:
|
|
331
|
+
self.grid_columnconfigure(1, minsize=0)
|
|
332
|
+
self.grid_rowconfigure(1, minsize=0)
|
|
333
|
+
|
|
334
|
+
def _bind_container_events(self):
|
|
335
|
+
"""Bind events for the container (enter/leave for autohide)."""
|
|
336
|
+
if self._scrollbar_visibility == 'hover':
|
|
337
|
+
self.bind('<Enter>', self._on_container_enter)
|
|
338
|
+
self.bind('<Leave>', self._on_container_leave)
|
|
339
|
+
self.canvas.bind('<Enter>', self._on_container_enter)
|
|
340
|
+
self.canvas.bind('<Leave>', self._on_container_leave)
|
|
341
|
+
self.vertical_scrollbar.bind('<Enter>', self._on_container_enter)
|
|
342
|
+
self.vertical_scrollbar.bind('<Leave>', self._on_container_leave)
|
|
343
|
+
self.horizontal_scrollbar.bind('<Enter>', self._on_container_enter)
|
|
344
|
+
self.horizontal_scrollbar.bind('<Leave>', self._on_container_leave)
|
|
345
|
+
|
|
346
|
+
def _on_container_enter(self, event):
|
|
347
|
+
"""Handle mouse entering the container."""
|
|
348
|
+
self._hovering = True
|
|
349
|
+
self.enable_scrolling()
|
|
350
|
+
if self._scrollbar_visibility == 'hover':
|
|
351
|
+
self._show_scrollbars()
|
|
352
|
+
|
|
353
|
+
def _on_container_leave(self, event):
|
|
354
|
+
"""Handle mouse leaving the container."""
|
|
355
|
+
self._hovering = False
|
|
356
|
+
self.disable_scrolling()
|
|
357
|
+
if self._scrollbar_visibility == 'hover':
|
|
358
|
+
self._hide_scrollbars()
|
|
359
|
+
|
|
360
|
+
def _content_fits(self):
|
|
361
|
+
"""Return booleans for whether content fits in the viewport (x_fit, y_fit)."""
|
|
362
|
+
if self._window_id:
|
|
363
|
+
bbox = self.canvas.bbox(self._window_id)
|
|
364
|
+
else:
|
|
365
|
+
bbox = self.canvas.bbox('all')
|
|
366
|
+
if not bbox:
|
|
367
|
+
return True, True
|
|
368
|
+
x0, y0, x1, y1 = bbox
|
|
369
|
+
content_w = x1 - x0
|
|
370
|
+
content_h = y1 - y0
|
|
371
|
+
viewport_w = max(1, self.canvas.winfo_width())
|
|
372
|
+
viewport_h = max(1, self.canvas.winfo_height())
|
|
373
|
+
if viewport_w <= 1 or viewport_h <= 1:
|
|
374
|
+
return True, True
|
|
375
|
+
return content_w <= viewport_w, content_h <= viewport_h
|
|
376
|
+
|
|
377
|
+
def _show_scrollbars(self):
|
|
378
|
+
"""Show scrollbars only if content overflows the viewport."""
|
|
379
|
+
x_fit, y_fit = self._content_fits()
|
|
380
|
+
if self._direction in ('vertical', 'both') and not y_fit:
|
|
381
|
+
self.vertical_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
382
|
+
self.vertical_scrollbar.lift()
|
|
383
|
+
else:
|
|
384
|
+
self.vertical_scrollbar.grid_remove()
|
|
385
|
+
if self._direction in ('horizontal', 'both') and not x_fit:
|
|
386
|
+
self.horizontal_scrollbar.grid(row=1, column=0, sticky='ew')
|
|
387
|
+
self.horizontal_scrollbar.lift()
|
|
388
|
+
else:
|
|
389
|
+
self.horizontal_scrollbar.grid_remove()
|
|
390
|
+
|
|
391
|
+
def _hide_scrollbars(self):
|
|
392
|
+
"""Hide scrollbars."""
|
|
393
|
+
self.vertical_scrollbar.grid_remove()
|
|
394
|
+
self.horizontal_scrollbar.grid_remove()
|
|
395
|
+
|
|
396
|
+
def _on_canvas_configure(self, event):
|
|
397
|
+
"""Pin content width to viewport and update visibility."""
|
|
398
|
+
if self._window_id and self._direction in ('vertical', 'both'):
|
|
399
|
+
self.canvas.itemconfig(self._window_id, width=event.width)
|
|
400
|
+
self._update_scrollbar_visibility()
|
|
401
|
+
|
|
402
|
+
def _on_first_map(self, event=None):
|
|
403
|
+
"""On the first show, finalize content sizing (see __init__)."""
|
|
404
|
+
if event is not None and event.widget is not self:
|
|
405
|
+
return
|
|
406
|
+
if self._first_map_done:
|
|
407
|
+
return
|
|
408
|
+
self._first_map_done = True
|
|
409
|
+
# Defer to idle so the geometry manager has assigned the canvas its real
|
|
410
|
+
# viewport size before we read it.
|
|
411
|
+
self.after_idle(self._finalize_layout)
|
|
412
|
+
|
|
413
|
+
def _finalize_layout(self):
|
|
414
|
+
"""Pin the content width to the realized viewport and recompute scroll."""
|
|
415
|
+
try:
|
|
416
|
+
if not self.winfo_exists():
|
|
417
|
+
return
|
|
418
|
+
self.canvas.update_idletasks()
|
|
419
|
+
width = self.canvas.winfo_width()
|
|
420
|
+
if (self._window_id and width > 1
|
|
421
|
+
and self._direction in ('vertical', 'both')):
|
|
422
|
+
self.canvas.itemconfig(self._window_id, width=width)
|
|
423
|
+
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
|
|
424
|
+
self._update_scrollbar_visibility()
|
|
425
|
+
except Exception:
|
|
426
|
+
pass
|
|
427
|
+
|
|
428
|
+
def _on_canvas_scroll_y(self, first, last):
|
|
429
|
+
"""Update vertical scrollbar position."""
|
|
430
|
+
self.vertical_scrollbar.set(first, last)
|
|
431
|
+
self._update_scrollbar_visibility()
|
|
432
|
+
self._emit_scroll_event()
|
|
433
|
+
|
|
434
|
+
def _on_canvas_scroll_x(self, first, last):
|
|
435
|
+
"""Update horizontal scrollbar position."""
|
|
436
|
+
self.horizontal_scrollbar.set(first, last)
|
|
437
|
+
self._update_scrollbar_visibility()
|
|
438
|
+
self._emit_scroll_event()
|
|
439
|
+
|
|
440
|
+
def _emit_scroll_event(self) -> None:
|
|
441
|
+
y = float(self.canvas.yview()[0])
|
|
442
|
+
x = float(self.canvas.xview()[0])
|
|
443
|
+
self.event_generate("<<BsScroll>>", data=ScrollEvent(y=y, x=x))
|
|
444
|
+
|
|
445
|
+
@property
|
|
446
|
+
def scroll_position(self) -> tuple[float, float]:
|
|
447
|
+
"""Current scroll position as `(y, x)` fractions in `[0.0, 1.0]`."""
|
|
448
|
+
return float(self.canvas.yview()[0]), float(self.canvas.xview()[0])
|
|
449
|
+
|
|
450
|
+
def _update_scrollbar_visibility(self):
|
|
451
|
+
"""Update scrollbar visibility based on current mode."""
|
|
452
|
+
if self._scrollbar_visibility == 'always':
|
|
453
|
+
self._show_scrollbars()
|
|
454
|
+
elif self._scrollbar_visibility == 'never':
|
|
455
|
+
self._hide_scrollbars()
|
|
456
|
+
elif self._scrollbar_visibility == 'hover':
|
|
457
|
+
# Show only while hovering and overflowing
|
|
458
|
+
x_fit, y_fit = self._content_fits()
|
|
459
|
+
if self._hovering and self._direction in ('vertical', 'both') and not y_fit:
|
|
460
|
+
self.vertical_scrollbar.grid()
|
|
461
|
+
else:
|
|
462
|
+
self.vertical_scrollbar.grid_remove()
|
|
463
|
+
|
|
464
|
+
if self._hovering and self._direction in ('horizontal', 'both') and not x_fit:
|
|
465
|
+
self.horizontal_scrollbar.grid()
|
|
466
|
+
else:
|
|
467
|
+
self.horizontal_scrollbar.grid_remove()
|
|
468
|
+
elif self._scrollbar_visibility == 'scroll':
|
|
469
|
+
# Hide if no overflow; otherwise leave current visibility to scroll events
|
|
470
|
+
x_fit, y_fit = self._content_fits()
|
|
471
|
+
if y_fit:
|
|
472
|
+
self.vertical_scrollbar.grid_remove()
|
|
473
|
+
if x_fit:
|
|
474
|
+
self.horizontal_scrollbar.grid_remove()
|
|
475
|
+
|
|
476
|
+
def _on_frame_configure(self, event):
|
|
477
|
+
"""Update scroll region and refresh bindings on configure."""
|
|
478
|
+
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
|
|
479
|
+
self._update_scrollbar_visibility()
|
|
480
|
+
|
|
481
|
+
# Refresh bindings for any newly added widgets
|
|
482
|
+
if self._scrolling_enabled and self._child_widget:
|
|
483
|
+
self._add_scroll_binding(self._child_widget)
|
|
484
|
+
|
|
485
|
+
def _on_mousewheel(self, event):
|
|
486
|
+
"""Handle vertical mouse wheel scrolling."""
|
|
487
|
+
# Check if vertical scrolling is actually possible
|
|
488
|
+
if self._direction in ('vertical', 'both'):
|
|
489
|
+
try:
|
|
490
|
+
first, last = self.canvas.yview()
|
|
491
|
+
# If first=0.0 and last=1.0, all content is visible, no need to scroll
|
|
492
|
+
if first <= 0.0 and last >= 1.0:
|
|
493
|
+
return # Content fits, don't scroll
|
|
494
|
+
except:
|
|
495
|
+
pass # If we can't check, allow scrolling
|
|
496
|
+
|
|
497
|
+
# Show scrollbar temporarily in scroll mode
|
|
498
|
+
if self._scrollbar_visibility == 'scroll':
|
|
499
|
+
self._show_scrollbars()
|
|
500
|
+
if self._hide_timer:
|
|
501
|
+
self.after_cancel(self._hide_timer)
|
|
502
|
+
self._hide_timer = self.after(self._autohide_delay, self._hide_scrollbars)
|
|
503
|
+
|
|
504
|
+
# Calculate delta based on platform
|
|
505
|
+
delta = 0
|
|
506
|
+
if self.winsys.lower() == "win32":
|
|
507
|
+
delta = -int(event.delta / 120)
|
|
508
|
+
elif self.winsys.lower() == "aqua":
|
|
509
|
+
delta = -event.delta
|
|
510
|
+
elif event.num == 4:
|
|
511
|
+
delta = -10
|
|
512
|
+
elif event.num == 5:
|
|
513
|
+
delta = 10
|
|
514
|
+
|
|
515
|
+
# Scroll vertically
|
|
516
|
+
if self._direction in ('vertical', 'both') and delta != 0:
|
|
517
|
+
self.canvas.yview_scroll(delta, 'units')
|
|
518
|
+
|
|
519
|
+
# Don't return 'break' - allow event to propagate to other handlers if needed
|
|
520
|
+
# But we can return None to continue normal processing
|
|
521
|
+
|
|
522
|
+
def _on_shift_mousewheel(self, event):
|
|
523
|
+
"""Handle horizontal mouse wheel scrolling (Shift+MouseWheel)."""
|
|
524
|
+
# Check if horizontal scrolling is actually possible
|
|
525
|
+
if self._direction in ('horizontal', 'both'):
|
|
526
|
+
try:
|
|
527
|
+
first, last = self.canvas.xview()
|
|
528
|
+
# If first=0.0 and last=1.0, all content is visible, no need to scroll
|
|
529
|
+
if first <= 0.0 and last >= 1.0:
|
|
530
|
+
return # Content fits, don't scroll
|
|
531
|
+
except:
|
|
532
|
+
pass # If we can't check, allow scrolling
|
|
533
|
+
|
|
534
|
+
# Show scrollbar temporarily in scroll mode
|
|
535
|
+
if self._scrollbar_visibility == 'scroll':
|
|
536
|
+
self._show_scrollbars()
|
|
537
|
+
if self._hide_timer:
|
|
538
|
+
self.after_cancel(self._hide_timer)
|
|
539
|
+
self._hide_timer = self.after(self._autohide_delay, self._hide_scrollbars)
|
|
540
|
+
|
|
541
|
+
# Calculate delta based on platform
|
|
542
|
+
delta = 0
|
|
543
|
+
if self.winsys.lower() == "win32":
|
|
544
|
+
delta = -int(event.delta / 120)
|
|
545
|
+
elif self.winsys.lower() == "aqua":
|
|
546
|
+
delta = -event.delta
|
|
547
|
+
elif event.num == 4:
|
|
548
|
+
delta = -10
|
|
549
|
+
elif event.num == 5:
|
|
550
|
+
delta = 10
|
|
551
|
+
|
|
552
|
+
# Scroll horizontally
|
|
553
|
+
if self._direction in ('horizontal', 'both') and delta != 0:
|
|
554
|
+
self.canvas.xview_scroll(delta, 'units')
|
|
555
|
+
|
|
556
|
+
# Don't return 'break' - allow event to propagate if needed
|
|
557
|
+
|
|
558
|
+
def _add_scroll_binding(self, widget):
|
|
559
|
+
"""Recursively add scroll bind tag to widget and all descendants."""
|
|
560
|
+
try:
|
|
561
|
+
# Get current bindtags
|
|
562
|
+
tags = list(widget.bindtags())
|
|
563
|
+
|
|
564
|
+
# Add our scroll tag if not already present
|
|
565
|
+
if self._scroll_tag not in tags:
|
|
566
|
+
# Insert after the widget name but before the class
|
|
567
|
+
# Typical order: (widget_name, class, toplevel, 'all')
|
|
568
|
+
# We want: (widget_name, scroll_tag, class, toplevel, 'all')
|
|
569
|
+
if len(tags) >= 2:
|
|
570
|
+
tags.insert(1, self._scroll_tag)
|
|
571
|
+
else:
|
|
572
|
+
tags.append(self._scroll_tag)
|
|
573
|
+
widget.bindtags(tuple(tags))
|
|
574
|
+
except:
|
|
575
|
+
# Some widgets may not support bindtags
|
|
576
|
+
pass
|
|
577
|
+
|
|
578
|
+
# Recurse into all children
|
|
579
|
+
for child in widget.winfo_children():
|
|
580
|
+
self._add_scroll_binding(child)
|
|
581
|
+
|
|
582
|
+
def _del_scroll_binding(self, widget):
|
|
583
|
+
"""Recursively remove scroll bind tag from widget and all descendants."""
|
|
584
|
+
try:
|
|
585
|
+
tags = list(widget.bindtags())
|
|
586
|
+
if self._scroll_tag in tags:
|
|
587
|
+
tags.remove(self._scroll_tag)
|
|
588
|
+
widget.bindtags(tuple(tags))
|
|
589
|
+
except:
|
|
590
|
+
pass
|
|
591
|
+
|
|
592
|
+
# Recurse into all children
|
|
593
|
+
for child in widget.winfo_children():
|
|
594
|
+
self._del_scroll_binding(child)
|
|
595
|
+
|
|
596
|
+
def enable_scrolling(self):
|
|
597
|
+
"""Enable mouse wheel scrolling on canvas and all child widgets."""
|
|
598
|
+
if not self._scrolling_enabled:
|
|
599
|
+
# Add binding to canvas for exposed areas
|
|
600
|
+
self._add_scroll_binding(self.canvas)
|
|
601
|
+
|
|
602
|
+
# Add binding to child widget if it exists
|
|
603
|
+
if self._child_widget:
|
|
604
|
+
self._add_scroll_binding(self._child_widget)
|
|
605
|
+
|
|
606
|
+
self._scrolling_enabled = True
|
|
607
|
+
|
|
608
|
+
def disable_scrolling(self):
|
|
609
|
+
"""Disable mouse wheel scrolling on canvas and all child widgets."""
|
|
610
|
+
if self._scrolling_enabled:
|
|
611
|
+
# Remove binding from canvas
|
|
612
|
+
self._del_scroll_binding(self.canvas)
|
|
613
|
+
|
|
614
|
+
# Remove binding from child widget if it exists
|
|
615
|
+
if self._child_widget:
|
|
616
|
+
self._del_scroll_binding(self._child_widget)
|
|
617
|
+
|
|
618
|
+
self._scrolling_enabled = False
|
|
619
|
+
|
|
620
|
+
def add(self, widget: Widget = None, *, anchor: str = 'nw', **kwargs: Any) -> Widget:
|
|
621
|
+
"""Add a widget to the scrollable area, or create and return a Frame.
|
|
622
|
+
|
|
623
|
+
Args:
|
|
624
|
+
widget: The widget to add. If None, creates a Frame.
|
|
625
|
+
anchor: Anchor position for the widget in the canvas. Default is 'nw'.
|
|
626
|
+
**kwargs: When widget is None, these are passed to Frame (e.g., padding, accent).
|
|
627
|
+
|
|
628
|
+
Returns:
|
|
629
|
+
Widget: The content widget (passed or created).
|
|
630
|
+
|
|
631
|
+
Raises:
|
|
632
|
+
ValueError: If the ScrollView already contains a widget and a new one is provided.
|
|
633
|
+
"""
|
|
634
|
+
# If content exists and no widget passed, return existing (idempotent)
|
|
635
|
+
if self._child_widget is not None:
|
|
636
|
+
if widget is not None:
|
|
637
|
+
raise ValueError("ScrollView already contains a widget. Use remove() first.")
|
|
638
|
+
return self._child_widget
|
|
639
|
+
|
|
640
|
+
# Create frame with kwargs if no widget provided
|
|
641
|
+
if widget is None:
|
|
642
|
+
widget = Frame(self.canvas, **kwargs)
|
|
643
|
+
|
|
644
|
+
self._child_widget = widget
|
|
645
|
+
|
|
646
|
+
# Create window in canvas
|
|
647
|
+
self._window_id = self.canvas.create_window(0, 0, anchor=anchor, window=widget)
|
|
648
|
+
|
|
649
|
+
# Keep scrollbars above the canvas/content
|
|
650
|
+
self.vertical_scrollbar.lift()
|
|
651
|
+
self.horizontal_scrollbar.lift()
|
|
652
|
+
|
|
653
|
+
# Bind configure event to update scroll region. (bind() auto-adds here,
|
|
654
|
+
# so this must run exactly once — a second identical bind double-fires
|
|
655
|
+
# _on_frame_configure on every resize, doubling the recursive subtree walk.)
|
|
656
|
+
widget.bind('<Configure>', self._on_frame_configure)
|
|
657
|
+
self._update_scrollbar_visibility()
|
|
658
|
+
|
|
659
|
+
# Enable scrolling based on mode
|
|
660
|
+
if self._scrollbar_visibility in ('always', 'never', 'scroll'):
|
|
661
|
+
# Always enable scrolling for these modes
|
|
662
|
+
self.enable_scrolling()
|
|
663
|
+
# For 'hover' mode, scrolling is enabled on enter
|
|
664
|
+
|
|
665
|
+
# Initial scroll region update
|
|
666
|
+
self.canvas.update_idletasks()
|
|
667
|
+
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
|
|
668
|
+
|
|
669
|
+
return widget
|
|
670
|
+
|
|
671
|
+
def remove(self) -> Widget | None:
|
|
672
|
+
"""Remove the current widget from the scrollable area.
|
|
673
|
+
|
|
674
|
+
Returns:
|
|
675
|
+
The removed widget, or None if no widget was present.
|
|
676
|
+
"""
|
|
677
|
+
if self._child_widget is None:
|
|
678
|
+
return None
|
|
679
|
+
|
|
680
|
+
widget = self._child_widget
|
|
681
|
+
|
|
682
|
+
# Disable scrolling
|
|
683
|
+
self.disable_scrolling()
|
|
684
|
+
|
|
685
|
+
# Unbind events
|
|
686
|
+
widget.unbind('<Configure>')
|
|
687
|
+
|
|
688
|
+
# Delete canvas window
|
|
689
|
+
if self._window_id:
|
|
690
|
+
self.canvas.delete(self._window_id)
|
|
691
|
+
self._window_id = None
|
|
692
|
+
|
|
693
|
+
self._child_widget = None
|
|
694
|
+
self._scrolling_enabled = False
|
|
695
|
+
|
|
696
|
+
return widget
|
|
697
|
+
|
|
698
|
+
def get_child(self) -> Widget | None:
|
|
699
|
+
"""Get the current child widget.
|
|
700
|
+
|
|
701
|
+
Returns:
|
|
702
|
+
The child widget, or None if no widget is present.
|
|
703
|
+
"""
|
|
704
|
+
return self._child_widget
|
|
705
|
+
|
|
706
|
+
def refresh_bindings(self):
|
|
707
|
+
"""Refresh mouse wheel bindings for all widgets.
|
|
708
|
+
|
|
709
|
+
Call this after dynamically adding many widgets at once to ensure
|
|
710
|
+
mouse wheel scrolling works on all new widgets.
|
|
711
|
+
"""
|
|
712
|
+
if self._child_widget and self._scrolling_enabled:
|
|
713
|
+
# Re-enable to refresh bindings
|
|
714
|
+
self.disable_scrolling()
|
|
715
|
+
self.enable_scrolling()
|
|
716
|
+
|
|
717
|
+
def yview(self, *args):
|
|
718
|
+
"""Query or command vertical view position."""
|
|
719
|
+
return self.canvas.yview(*args)
|
|
720
|
+
|
|
721
|
+
def xview(self, *args):
|
|
722
|
+
"""Query or command horizontal view position."""
|
|
723
|
+
return self.canvas.xview(*args)
|
|
724
|
+
|
|
725
|
+
def yview_moveto(self, fraction: float):
|
|
726
|
+
"""Scroll to a specific vertical position.
|
|
727
|
+
|
|
728
|
+
Args:
|
|
729
|
+
fraction: Position from 0.0 (top) to 1.0 (bottom).
|
|
730
|
+
"""
|
|
731
|
+
self.canvas.yview_moveto(fraction)
|
|
732
|
+
|
|
733
|
+
def xview_moveto(self, fraction: float):
|
|
734
|
+
"""Scroll to a specific horizontal position.
|
|
735
|
+
|
|
736
|
+
Args:
|
|
737
|
+
fraction: Position from 0.0 (left) to 1.0 (right).
|
|
738
|
+
"""
|
|
739
|
+
self.canvas.xview_moveto(fraction)
|
|
740
|
+
|
|
741
|
+
def destroy(self):
|
|
742
|
+
"""Clean up resources and destroy the widget."""
|
|
743
|
+
# Cancel any pending timer
|
|
744
|
+
if self._hide_timer:
|
|
745
|
+
self.after_cancel(self._hide_timer)
|
|
746
|
+
self._hide_timer = None
|
|
747
|
+
|
|
748
|
+
# Remove scroll bindings from child widget
|
|
749
|
+
if self._child_widget:
|
|
750
|
+
self.disable_scrolling()
|
|
751
|
+
|
|
752
|
+
# Unbind class bindings for the scroll tag
|
|
753
|
+
if self.winsys.lower() == "x11":
|
|
754
|
+
self.unbind_class(self._scroll_tag, "<Button-4>")
|
|
755
|
+
self.unbind_class(self._scroll_tag, "<Button-5>")
|
|
756
|
+
self.unbind_class(self._scroll_tag, "<Shift-Button-4>")
|
|
757
|
+
self.unbind_class(self._scroll_tag, "<Shift-Button-5>")
|
|
758
|
+
else:
|
|
759
|
+
self.unbind_class(self._scroll_tag, "<MouseWheel>")
|
|
760
|
+
self.unbind_class(self._scroll_tag, "<Shift-MouseWheel>")
|
|
761
|
+
|
|
762
|
+
# Call parent destroy
|
|
763
|
+
super().destroy()
|
|
764
|
+
|