bootstack 0.1.0a1__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 +249 -0
- bootstack/__main__.py +5 -0
- bootstack/api/__init__.py +127 -0
- bootstack/api/app.py +30 -0
- bootstack/api/constants.py +3 -0
- bootstack/api/data.py +23 -0
- bootstack/api/dialogs.py +44 -0
- bootstack/api/i18n.py +17 -0
- bootstack/api/localization.py +16 -0
- bootstack/api/menu.py +7 -0
- bootstack/api/style.py +23 -0
- bootstack/api/utils.py +24 -0
- bootstack/api/widgets.py +137 -0
- bootstack/assets/__init__.py +24 -0
- bootstack/assets/bootstack-transparent.png +0 -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/border.png +0 -0
- bootstack/assets/elements/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-default.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-after-compact.png +0 -0
- bootstack/assets/elements/input-after-default.png +0 -0
- bootstack/assets/elements/input-before-compact.png +0 -0
- bootstack/assets/elements/input-before-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/manifest.toml +480 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/nav-button-compact.png +0 -0
- bootstack/assets/elements/nav-button-default.png +0 -0
- bootstack/assets/elements/nav-icon-button-compact.png +0 -0
- bootstack/assets/elements/nav-icon-button-default.png +0 -0
- bootstack/assets/elements/notebook-client-border.png +0 -0
- bootstack/assets/elements/notebook-tab-active.png +0 -0
- bootstack/assets/elements/notebook-tab-bar.png +0 -0
- bootstack/assets/elements/notebook-tab-normal.png +0 -0
- bootstack/assets/elements/notebook-tab-pill.png +0 -0
- bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/elements/progress-bar-solid.png +0 -0
- bootstack/assets/elements/progress-bar-thin.png +0 -0
- bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/elements/radio-selected.png +0 -0
- bootstack/assets/elements/radio-unselected.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle-focus.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-horizontal.png +0 -0
- bootstack/assets/elements/slider-track-vertical.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
- bootstack/assets/elements/tabs-bar-vertical.png +0 -0
- bootstack/assets/elements/tabs-pill.png +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/themes/__init__.py +0 -0
- bootstack/assets/themes/amber-dark.json +32 -0
- bootstack/assets/themes/amber-light.json +32 -0
- bootstack/assets/themes/aurora-dark.json +32 -0
- bootstack/assets/themes/aurora-light.json +32 -0
- bootstack/assets/themes/bootstrap-dark.json +32 -0
- bootstack/assets/themes/bootstrap-light.json +32 -0
- bootstack/assets/themes/classic-dark.json +32 -0
- bootstack/assets/themes/classic-light.json +32 -0
- bootstack/assets/themes/docs-dark.json +32 -0
- bootstack/assets/themes/docs-light.json +32 -0
- bootstack/assets/themes/forest-dark.json +32 -0
- bootstack/assets/themes/forest-light.json +32 -0
- bootstack/assets/themes/ocean-dark.json +32 -0
- bootstack/assets/themes/ocean-light.json +32 -0
- bootstack/assets/themes/rose-dark.json +32 -0
- bootstack/assets/themes/rose-light.json +32 -0
- bootstack/assets/widgets/__init__.py +0 -0
- bootstack/assets/widgets/badge-default.png +0 -0
- bootstack/assets/widgets/badge-pill.png +0 -0
- bootstack/assets/widgets/border.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
- bootstack/assets/widgets/button-group-vertical-after.png +0 -0
- bootstack/assets/widgets/button-group-vertical-before.png +0 -0
- bootstack/assets/widgets/button-group-vertical-center.png +0 -0
- bootstack/assets/widgets/button.png +0 -0
- bootstack/assets/widgets/checkbox-checked.png +0 -0
- bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
- bootstack/assets/widgets/checkbox-unchecked.png +0 -0
- bootstack/assets/widgets/field.png +0 -0
- bootstack/assets/widgets/icon-button.png +0 -0
- bootstack/assets/widgets/input-inner.png +0 -0
- bootstack/assets/widgets/input-prefix.png +0 -0
- bootstack/assets/widgets/input-suffix.png +0 -0
- bootstack/assets/widgets/input.png +0 -0
- bootstack/assets/widgets/list-item-focus.png +0 -0
- bootstack/assets/widgets/list-item-separated.png +0 -0
- bootstack/assets/widgets/menu-item-separated.png +0 -0
- bootstack/assets/widgets/notebook-client-border.png +0 -0
- bootstack/assets/widgets/notebook-pill-active.png +0 -0
- bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
- bootstack/assets/widgets/notebook-tab-active.png +0 -0
- bootstack/assets/widgets/notebook-tab-border.png +0 -0
- bootstack/assets/widgets/notebook-tab-normal.png +0 -0
- bootstack/assets/widgets/notebook-underline.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
- bootstack/assets/widgets/progress-trough-vertical.png +0 -0
- bootstack/assets/widgets/radio-selected.png +0 -0
- bootstack/assets/widgets/radio-unselected.png +0 -0
- bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
- bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
- bootstack/assets/widgets/separator-horizontal.png +0 -0
- bootstack/assets/widgets/separator-vertical.png +0 -0
- bootstack/assets/widgets/slider-handle-focus.png +0 -0
- bootstack/assets/widgets/slider-handle.png +0 -0
- bootstack/assets/widgets/slider-track-horizontal.png +0 -0
- bootstack/assets/widgets/slider-track-vertical.png +0 -0
- bootstack/assets/widgets/switch-off.png +0 -0
- bootstack/assets/widgets/switch-on.png +0 -0
- bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
- bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
- bootstack/assets/widgets/tabs-pill.png +0 -0
- bootstack/cli/__init__.py +124 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +439 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +287 -0
- bootstack/cli/demo.py +1267 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/list_cmd.py +71 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +246 -0
- bootstack/cli/run.py +99 -0
- bootstack/cli/start.py +105 -0
- bootstack/cli/templates/__init__.py +861 -0
- bootstack/constants.py +325 -0
- bootstack/core/__init__.py +34 -0
- bootstack/core/capabilities/__init__.py +45 -0
- bootstack/core/capabilities/after.py +103 -0
- bootstack/core/capabilities/bind.py +154 -0
- bootstack/core/capabilities/bindtags.py +112 -0
- bootstack/core/capabilities/busy.py +61 -0
- bootstack/core/capabilities/clipboard.py +88 -0
- bootstack/core/capabilities/focus.py +118 -0
- bootstack/core/capabilities/grab.py +65 -0
- bootstack/core/capabilities/grid.py +188 -0
- bootstack/core/capabilities/localization.py +231 -0
- bootstack/core/capabilities/pack.py +119 -0
- bootstack/core/capabilities/place.py +92 -0
- bootstack/core/capabilities/selection.py +136 -0
- bootstack/core/capabilities/signals.py +242 -0
- bootstack/core/capabilities/winfo.py +315 -0
- bootstack/core/colorutils.py +234 -0
- bootstack/core/exceptions.py +95 -0
- bootstack/core/images.py +283 -0
- bootstack/core/localization/README.md +90 -0
- bootstack/core/localization/__init__.py +13 -0
- bootstack/core/localization/intl_format.py +580 -0
- bootstack/core/localization/msgcat.py +425 -0
- bootstack/core/localization/specs.py +143 -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/publisher.py +147 -0
- bootstack/core/signals/README.md +112 -0
- bootstack/core/signals/__init__.py +8 -0
- bootstack/core/signals/integration.py +100 -0
- bootstack/core/signals/signal.py +317 -0
- bootstack/core/signals/types.py +4 -0
- bootstack/core/validation/__init__.py +5 -0
- bootstack/core/validation/types.py +13 -0
- bootstack/core/validation/validation_result.py +17 -0
- bootstack/core/validation/validation_rules.py +112 -0
- bootstack/core/variables.py +62 -0
- bootstack/datasource/README.md +607 -0
- bootstack/datasource/__init__.py +51 -0
- bootstack/datasource/base.py +474 -0
- bootstack/datasource/file_source.py +541 -0
- bootstack/datasource/memory_source.py +482 -0
- bootstack/datasource/sqlite_source.py +453 -0
- bootstack/datasource/types.py +259 -0
- bootstack/dialogs/__init__.py +56 -0
- bootstack/dialogs/colorchooser.py +674 -0
- bootstack/dialogs/colordropper.py +257 -0
- bootstack/dialogs/datedialog.py +404 -0
- bootstack/dialogs/dialog.py +514 -0
- bootstack/dialogs/filterdialog.py +358 -0
- bootstack/dialogs/fontdialog.py +339 -0
- bootstack/dialogs/formdialog.py +541 -0
- bootstack/dialogs/message.py +489 -0
- bootstack/dialogs/query.py +561 -0
- bootstack/py.typed +1 -0
- bootstack/runtime/__init__.py +3 -0
- bootstack/runtime/app.py +879 -0
- bootstack/runtime/base_window.py +786 -0
- bootstack/runtime/events.py +399 -0
- bootstack/runtime/menu.py +510 -0
- bootstack/runtime/shortcuts.py +423 -0
- bootstack/runtime/tk_patch.py +31 -0
- bootstack/runtime/toplevel.py +131 -0
- bootstack/runtime/utility.py +371 -0
- bootstack/runtime/visual_focus.py +228 -0
- bootstack/runtime/window_utilities.py +1043 -0
- bootstack/style/__init__.py +5498 -0
- bootstack/style/bootstyle.py +507 -0
- bootstack/style/bootstyle_builder_base.py +752 -0
- bootstack/style/bootstyle_builder_mixed.py +93 -0
- bootstack/style/bootstyle_builder_tk.py +109 -0
- bootstack/style/bootstyle_builder_ttk.py +354 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +44 -0
- bootstack/style/builders/button.py +453 -0
- bootstack/style/builders/buttongroup.py +344 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +95 -0
- bootstack/style/builders/combobox.py +112 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +83 -0
- bootstack/style/builders/expander.py +171 -0
- bootstack/style/builders/field.py +312 -0
- bootstack/style/builders/frame.py +27 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +41 -0
- bootstack/style/builders/listview.py +267 -0
- bootstack/style/builders/menubar.py +74 -0
- bootstack/style/builders/menubutton.py +408 -0
- bootstack/style/builders/notebook.py +316 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +71 -0
- bootstack/style/builders/radiobutton.py +68 -0
- bootstack/style/builders/scale.py +66 -0
- bootstack/style/builders/scrollbar.py +360 -0
- bootstack/style/builders/separator.py +45 -0
- bootstack/style/builders/sidenav.py +313 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +67 -0
- bootstack/style/builders/tabitem.py +205 -0
- bootstack/style/builders/toolbutton.py +260 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +269 -0
- bootstack/style/builders/utils.py +404 -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/style.py +499 -0
- bootstack/style/theme_provider.py +449 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +42 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +527 -0
- bootstack/style/utility.py +696 -0
- bootstack/themes/__init__.py +12 -0
- bootstack/themes/standard.py +415 -0
- bootstack/themes/user.py +45 -0
- bootstack/widgets/__init__.py +53 -0
- bootstack/widgets/composites/__init__.py +38 -0
- bootstack/widgets/composites/accordion.py +385 -0
- bootstack/widgets/composites/appshell.py +445 -0
- bootstack/widgets/composites/buttongroup.py +391 -0
- bootstack/widgets/composites/calendar.py +914 -0
- bootstack/widgets/composites/compositeframe.py +282 -0
- bootstack/widgets/composites/contextmenu.py +1754 -0
- bootstack/widgets/composites/dateentry.py +261 -0
- bootstack/widgets/composites/dropdownbutton.py +190 -0
- bootstack/widgets/composites/expander.py +508 -0
- bootstack/widgets/composites/field.py +448 -0
- bootstack/widgets/composites/floodgauge.py +434 -0
- bootstack/widgets/composites/form.py +983 -0
- bootstack/widgets/composites/labeledscale.py +209 -0
- bootstack/widgets/composites/list/__init__.py +15 -0
- bootstack/widgets/composites/list/listitem.py +733 -0
- bootstack/widgets/composites/list/listview.py +1507 -0
- bootstack/widgets/composites/menubar.py +303 -0
- bootstack/widgets/composites/meter.py +882 -0
- bootstack/widgets/composites/numericentry.py +183 -0
- bootstack/widgets/composites/pagestack.py +330 -0
- bootstack/widgets/composites/passwordentry.py +149 -0
- bootstack/widgets/composites/pathentry.py +223 -0
- bootstack/widgets/composites/radiogroup.py +466 -0
- bootstack/widgets/composites/scrolledtext.py +388 -0
- bootstack/widgets/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/composites/scrollview.py +675 -0
- bootstack/widgets/composites/selectbox.py +544 -0
- bootstack/widgets/composites/sidenav/__init__.py +24 -0
- bootstack/widgets/composites/sidenav/group.py +485 -0
- bootstack/widgets/composites/sidenav/header.py +83 -0
- bootstack/widgets/composites/sidenav/item.py +413 -0
- bootstack/widgets/composites/sidenav/separator.py +51 -0
- bootstack/widgets/composites/sidenav/view.py +919 -0
- bootstack/widgets/composites/spinnerentry.py +232 -0
- bootstack/widgets/composites/tableview/__init__.py +5 -0
- bootstack/widgets/composites/tableview/tableview.py +2254 -0
- bootstack/widgets/composites/tableview/types.py +169 -0
- bootstack/widgets/composites/tabs/__init__.py +6 -0
- bootstack/widgets/composites/tabs/tabitem.py +372 -0
- bootstack/widgets/composites/tabs/tabs.py +478 -0
- bootstack/widgets/composites/tabs/tabview.py +352 -0
- bootstack/widgets/composites/textentry.py +90 -0
- bootstack/widgets/composites/timeentry.py +189 -0
- bootstack/widgets/composites/toast.py +364 -0
- bootstack/widgets/composites/togglegroup.py +382 -0
- bootstack/widgets/composites/toolbar.py +393 -0
- bootstack/widgets/composites/tooltip.py +404 -0
- bootstack/widgets/internal/__init__.py +0 -0
- bootstack/widgets/internal/wrapper_base.py +304 -0
- bootstack/widgets/mixins/__init__.py +25 -0
- bootstack/widgets/mixins/configure_mixin.py +186 -0
- bootstack/widgets/mixins/entry_mixin.py +70 -0
- bootstack/widgets/mixins/font_mixin.py +346 -0
- bootstack/widgets/mixins/icon_mixin.py +38 -0
- bootstack/widgets/mixins/localization_mixin.py +255 -0
- bootstack/widgets/mixins/signal_mixin.py +272 -0
- bootstack/widgets/mixins/validation_mixin.py +204 -0
- bootstack/widgets/parts/__init__.py +11 -0
- bootstack/widgets/parts/numberentry_part.py +345 -0
- bootstack/widgets/parts/spinnerentry_part.py +394 -0
- bootstack/widgets/parts/textentry_part.py +344 -0
- bootstack/widgets/primitives/__init__.py +55 -0
- bootstack/widgets/primitives/badge.py +44 -0
- bootstack/widgets/primitives/button.py +89 -0
- bootstack/widgets/primitives/card.py +66 -0
- bootstack/widgets/primitives/checkbutton.py +124 -0
- bootstack/widgets/primitives/checktoggle.py +53 -0
- bootstack/widgets/primitives/combobox.py +165 -0
- bootstack/widgets/primitives/entry.py +98 -0
- bootstack/widgets/primitives/frame.py +206 -0
- bootstack/widgets/primitives/gridframe.py +479 -0
- bootstack/widgets/primitives/label.py +95 -0
- bootstack/widgets/primitives/labelframe.py +63 -0
- bootstack/widgets/primitives/menubutton.py +118 -0
- bootstack/widgets/primitives/notebook.py +551 -0
- bootstack/widgets/primitives/optionmenu.py +248 -0
- bootstack/widgets/primitives/packframe.py +228 -0
- bootstack/widgets/primitives/panedwindow.py +58 -0
- bootstack/widgets/primitives/progressbar.py +95 -0
- bootstack/widgets/primitives/radiobutton.py +115 -0
- bootstack/widgets/primitives/radiotoggle.py +50 -0
- bootstack/widgets/primitives/scale.py +85 -0
- bootstack/widgets/primitives/scrollbar.py +56 -0
- bootstack/widgets/primitives/separator.py +56 -0
- bootstack/widgets/primitives/sizegrip.py +47 -0
- bootstack/widgets/primitives/spinbox.py +91 -0
- bootstack/widgets/primitives/switch.py +41 -0
- bootstack/widgets/primitives/treeview.py +77 -0
- bootstack/widgets/types.py +20 -0
- bootstack-0.1.0a1.dist-info/METADATA +196 -0
- bootstack-0.1.0a1.dist-info/RECORD +419 -0
- bootstack-0.1.0a1.dist-info/WHEEL +5 -0
- bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
- bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,675 @@
|
|
|
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, Optional
|
|
5
|
+
|
|
6
|
+
from bootstack.widgets.primitives.frame import Frame
|
|
7
|
+
from bootstack.widgets.mixins.configure_mixin import configure_delegate
|
|
8
|
+
from bootstack.widgets.primitives.scrollbar import Scrollbar
|
|
9
|
+
from bootstack.widgets.types import Master
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ScrollView(Frame):
|
|
13
|
+
"""A canvas-based scrollable container with configurable scrollbar behavior.
|
|
14
|
+
|
|
15
|
+
The ScrollView widget provides a scrollable area for child widgets with
|
|
16
|
+
full mouse wheel support on all descendants. Scrollbars can be configured
|
|
17
|
+
to appear always, never, on hover, or when scrolling, and are only visible
|
|
18
|
+
when the content exceeds the available space.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
canvas (Canvas): The underlying tkinter Canvas widget.
|
|
22
|
+
vertical_scrollbar (Scrollbar): The vertical scrollbar widget.
|
|
23
|
+
horizontal_scrollbar (Scrollbar): The horizontal scrollbar widget.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
master: Master = None,
|
|
29
|
+
scroll_direction: Literal['horizontal', 'vertical', 'both'] = 'both',
|
|
30
|
+
scrollbar_visibility: Literal['always', 'never', 'hover', 'scroll'] = 'always',
|
|
31
|
+
autohide_delay: int = 1000, # milliseconds for scroll mode
|
|
32
|
+
scrollbar_variant: str = 'default',
|
|
33
|
+
**kwargs: Any
|
|
34
|
+
):
|
|
35
|
+
"""Initialize a ScrollView widget.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
master: The parent widget.
|
|
39
|
+
scroll_direction: Scroll direction - 'horizontal' for horizontal only,
|
|
40
|
+
'vertical' for vertical only, or 'both' for bidirectional
|
|
41
|
+
scrolling. Horizontal scrolling uses Shift+MouseWheel.
|
|
42
|
+
scrollbar_visibility: Scrollbar visibility mode:
|
|
43
|
+
- 'always': Scrollbars always visible
|
|
44
|
+
- 'never': Scrollbars hidden (scrolling still works)
|
|
45
|
+
- 'hover': Scrollbars appear when mouse enters the widget
|
|
46
|
+
- 'scroll': Scrollbars appear when scrolling, auto-hide after delay
|
|
47
|
+
autohide_delay: Time in milliseconds before auto-hiding scrollbars
|
|
48
|
+
in 'scroll' mode. Default is 1000ms (1 second).
|
|
49
|
+
scrollbar_variant: The variant to apply to scrollbars (e.g., 'default',
|
|
50
|
+
'round'). If None, uses the default scrollbar variant.
|
|
51
|
+
**kwargs: Additional keyword arguments passed to the Frame parent class.
|
|
52
|
+
|
|
53
|
+
Note:
|
|
54
|
+
Mouse wheel scrolling is automatically enabled on all child widgets,
|
|
55
|
+
including those added dynamically. For manual refresh of bindings
|
|
56
|
+
after adding many widgets at once, call refresh_bindings().
|
|
57
|
+
"""
|
|
58
|
+
super().__init__(master, **kwargs)
|
|
59
|
+
|
|
60
|
+
# configuration
|
|
61
|
+
self._direction = scroll_direction
|
|
62
|
+
self._scrollbar_visibility = scrollbar_visibility
|
|
63
|
+
self._autohide_delay = autohide_delay
|
|
64
|
+
self._scrollbar_variant = scrollbar_variant
|
|
65
|
+
|
|
66
|
+
self._child_widget = None
|
|
67
|
+
self._window_id = None
|
|
68
|
+
self._hide_timer = None
|
|
69
|
+
self._scrolling_enabled = False
|
|
70
|
+
self._hovering = False
|
|
71
|
+
|
|
72
|
+
# Create unique bind tag for this scrollview
|
|
73
|
+
self._scroll_tag = f'ScrollView_{id(self)}'
|
|
74
|
+
|
|
75
|
+
# Detect windowing system
|
|
76
|
+
self.winsys = self.tk.call("tk", "windowingsystem")
|
|
77
|
+
|
|
78
|
+
# Bind scroll events to our custom tag
|
|
79
|
+
self._setup_scroll_tag_bindings()
|
|
80
|
+
|
|
81
|
+
# Create canvas
|
|
82
|
+
self.canvas = Canvas(
|
|
83
|
+
self,
|
|
84
|
+
highlightthickness=0,
|
|
85
|
+
borderwidth=0
|
|
86
|
+
)
|
|
87
|
+
self.canvas.bind("<Configure>", self._on_canvas_configure)
|
|
88
|
+
|
|
89
|
+
# Create scrollbars
|
|
90
|
+
self.vertical_scrollbar = Scrollbar(
|
|
91
|
+
master=self,
|
|
92
|
+
orient='vertical',
|
|
93
|
+
command=self.canvas.yview,
|
|
94
|
+
variant=self._scrollbar_variant
|
|
95
|
+
)
|
|
96
|
+
self.horizontal_scrollbar = Scrollbar(
|
|
97
|
+
master=self,
|
|
98
|
+
orient='horizontal',
|
|
99
|
+
command=self.canvas.xview,
|
|
100
|
+
variant=self._scrollbar_variant
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Configure canvas scrolling
|
|
104
|
+
scroll_config = {}
|
|
105
|
+
if scroll_direction in ('vertical', 'both'):
|
|
106
|
+
scroll_config['yscrollcommand'] = self._on_canvas_scroll_y
|
|
107
|
+
if scroll_direction in ('horizontal', 'both'):
|
|
108
|
+
scroll_config['xscrollcommand'] = self._on_canvas_scroll_x
|
|
109
|
+
|
|
110
|
+
self.canvas.configure(**scroll_config)
|
|
111
|
+
|
|
112
|
+
# Layout
|
|
113
|
+
self._layout_widgets()
|
|
114
|
+
|
|
115
|
+
# Bind events for autohide/hover
|
|
116
|
+
self._bind_container_events()
|
|
117
|
+
|
|
118
|
+
# Initial scrollbar visibility
|
|
119
|
+
self._update_scrollbar_visibility()
|
|
120
|
+
|
|
121
|
+
@configure_delegate('scroll_direction')
|
|
122
|
+
def _delegate_scroll_direction(self, value=None):
|
|
123
|
+
if value is None:
|
|
124
|
+
return self._direction
|
|
125
|
+
else:
|
|
126
|
+
old_direction = self._direction
|
|
127
|
+
self._direction = value
|
|
128
|
+
|
|
129
|
+
# Update canvas scroll configuration
|
|
130
|
+
scroll_config = {}
|
|
131
|
+
if value in ('vertical', 'both'):
|
|
132
|
+
scroll_config['yscrollcommand'] = self._on_canvas_scroll_y
|
|
133
|
+
else:
|
|
134
|
+
scroll_config['yscrollcommand'] = None
|
|
135
|
+
|
|
136
|
+
if value in ('horizontal', 'both'):
|
|
137
|
+
scroll_config['xscrollcommand'] = self._on_canvas_scroll_x
|
|
138
|
+
else:
|
|
139
|
+
scroll_config['xscrollcommand'] = None
|
|
140
|
+
|
|
141
|
+
self.canvas.configure(**scroll_config)
|
|
142
|
+
|
|
143
|
+
# Update scrollbar visibility and layout
|
|
144
|
+
self._update_scrollbar_visibility()
|
|
145
|
+
return None
|
|
146
|
+
|
|
147
|
+
@configure_delegate('scrollbar_visibility')
|
|
148
|
+
def _delegate_scrollbar_visibility(self, value=None):
|
|
149
|
+
if value is None:
|
|
150
|
+
return self._scrollbar_visibility
|
|
151
|
+
else:
|
|
152
|
+
old_value = self._scrollbar_visibility
|
|
153
|
+
self._scrollbar_visibility = value
|
|
154
|
+
|
|
155
|
+
# Unbind old events if changing from hover
|
|
156
|
+
if old_value == 'hover':
|
|
157
|
+
self.unbind('<Enter>')
|
|
158
|
+
self.unbind('<Leave>')
|
|
159
|
+
self.canvas.unbind('<Enter>')
|
|
160
|
+
self.canvas.unbind('<Leave>')
|
|
161
|
+
self.vertical_scrollbar.unbind('<Enter>')
|
|
162
|
+
self.vertical_scrollbar.unbind('<Leave>')
|
|
163
|
+
self.horizontal_scrollbar.unbind('<Enter>')
|
|
164
|
+
self.horizontal_scrollbar.unbind('<Leave>')
|
|
165
|
+
|
|
166
|
+
# Sync gutter reservation with the new mode
|
|
167
|
+
if value in ('hover', 'scroll'):
|
|
168
|
+
self._set_scrollbar_gutter(reserve=True)
|
|
169
|
+
else:
|
|
170
|
+
self._set_scrollbar_gutter(reserve=False)
|
|
171
|
+
|
|
172
|
+
# Bind new events and update scrollbar visibility
|
|
173
|
+
self._bind_container_events()
|
|
174
|
+
self._update_scrollbar_visibility()
|
|
175
|
+
|
|
176
|
+
# Update scrolling enabled state
|
|
177
|
+
if value in ('always', 'never', 'scroll'):
|
|
178
|
+
if self._child_widget:
|
|
179
|
+
self.enable_scrolling()
|
|
180
|
+
elif value == 'hover':
|
|
181
|
+
# Scrolling will be enabled on hover
|
|
182
|
+
self.disable_scrolling()
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
@configure_delegate('autohide_delay')
|
|
186
|
+
def _delegate_autohide_delay(self, value=None):
|
|
187
|
+
if value is None:
|
|
188
|
+
return self._autohide_delay
|
|
189
|
+
else:
|
|
190
|
+
self._autohide_delay = value
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
@configure_delegate('scrollbar_variant')
|
|
194
|
+
def _delegate_scrollbar_variant(self, value=None):
|
|
195
|
+
if value is None:
|
|
196
|
+
return self._scrollbar_variant
|
|
197
|
+
else:
|
|
198
|
+
self._scrollbar_variant = value
|
|
199
|
+
# Apply the new variant to both scrollbars
|
|
200
|
+
if value:
|
|
201
|
+
self.vertical_scrollbar.configure(variant=value)
|
|
202
|
+
self.horizontal_scrollbar.configure(variant=value)
|
|
203
|
+
return None
|
|
204
|
+
|
|
205
|
+
def _setup_scroll_tag_bindings(self):
|
|
206
|
+
"""Setup bindings on our custom bind tag."""
|
|
207
|
+
if self.winsys.lower() == "x11":
|
|
208
|
+
self.bind_class(self._scroll_tag, "<Button-4>", self._on_mousewheel)
|
|
209
|
+
self.bind_class(self._scroll_tag, "<Button-5>", self._on_mousewheel)
|
|
210
|
+
self.bind_class(self._scroll_tag, "<Shift-Button-4>", self._on_shift_mousewheel)
|
|
211
|
+
self.bind_class(self._scroll_tag, "<Shift-Button-5>", self._on_shift_mousewheel)
|
|
212
|
+
else:
|
|
213
|
+
self.bind_class(self._scroll_tag, "<MouseWheel>", self._on_mousewheel)
|
|
214
|
+
self.bind_class(self._scroll_tag, "<Shift-MouseWheel>", self._on_shift_mousewheel)
|
|
215
|
+
|
|
216
|
+
def _layout_widgets(self):
|
|
217
|
+
"""Layout the canvas and scrollbars.
|
|
218
|
+
|
|
219
|
+
For 'hover' and 'scroll' visibility modes the grid column and row that
|
|
220
|
+
hold the scrollbars are given a fixed minimum size equal to the
|
|
221
|
+
scrollbar's natural dimensions before the scrollbars are hidden. This
|
|
222
|
+
reserves the gutter permanently so the canvas never resizes when a
|
|
223
|
+
scrollbar is toggled — the same principle as the CSS
|
|
224
|
+
`scrollbar-gutter: stable` property.
|
|
225
|
+
|
|
226
|
+
An overlay approach (lift/lower over the canvas) was considered but
|
|
227
|
+
rejected because the scrollbar then covers content, which was reported
|
|
228
|
+
as confusing by users. Reserving the gutter avoids both the
|
|
229
|
+
layout-shift problem *and* the content-coverage problem.
|
|
230
|
+
"""
|
|
231
|
+
self.canvas.grid(row=0, column=0, sticky='nsew')
|
|
232
|
+
|
|
233
|
+
if self._direction in ('vertical', 'both'):
|
|
234
|
+
self.vertical_scrollbar.grid(row=0, column=1, sticky='ns')
|
|
235
|
+
|
|
236
|
+
if self._direction in ('horizontal', 'both'):
|
|
237
|
+
self.horizontal_scrollbar.grid(row=1, column=0, sticky='ew')
|
|
238
|
+
|
|
239
|
+
# Configure grid weights
|
|
240
|
+
self.grid_rowconfigure(0, weight=1)
|
|
241
|
+
self.grid_columnconfigure(0, weight=1)
|
|
242
|
+
# Keep scrollbars above the canvas/content when visible
|
|
243
|
+
self.vertical_scrollbar.lift()
|
|
244
|
+
self.horizontal_scrollbar.lift()
|
|
245
|
+
|
|
246
|
+
# Initially hide scrollbars based on scrollbar_visibility setting
|
|
247
|
+
if self._scrollbar_visibility == 'never':
|
|
248
|
+
self.vertical_scrollbar.grid_remove()
|
|
249
|
+
self.horizontal_scrollbar.grid_remove()
|
|
250
|
+
elif self._scrollbar_visibility in ('hover', 'scroll'):
|
|
251
|
+
# Hide scrollbars now; defer gutter measurement to after_idle so
|
|
252
|
+
# the style system has finished sizing the widgets before we read
|
|
253
|
+
# their dimensions. Measuring too early (before the widget is
|
|
254
|
+
# placed in its parent) can return an inflated reqwidth and leave
|
|
255
|
+
# a visible gap beside the scrollbar when it appears.
|
|
256
|
+
self.vertical_scrollbar.grid_remove()
|
|
257
|
+
self.horizontal_scrollbar.grid_remove()
|
|
258
|
+
self.after_idle(lambda: self._set_scrollbar_gutter(reserve=True))
|
|
259
|
+
|
|
260
|
+
def _set_scrollbar_gutter(self, reserve: bool):
|
|
261
|
+
"""Reserve or release the grid gutter used by each scrollbar.
|
|
262
|
+
|
|
263
|
+
When *reserve* is `True` the column (vertical scrollbar) and row
|
|
264
|
+
(horizontal scrollbar) are given a `minsize` equal to the scrollbar's
|
|
265
|
+
natural requested dimensions. The minsize persists even after the
|
|
266
|
+
scrollbar widget is removed from the grid via `grid_remove()`, so the
|
|
267
|
+
canvas occupies a constant area regardless of scrollbar visibility.
|
|
268
|
+
|
|
269
|
+
When *reserve* is `False` the minsize is cleared (set to 0), which
|
|
270
|
+
is appropriate for 'always' and 'never' modes where no gutter needs to
|
|
271
|
+
be held open.
|
|
272
|
+
"""
|
|
273
|
+
if reserve:
|
|
274
|
+
if self._direction in ('vertical', 'both'):
|
|
275
|
+
width = self.vertical_scrollbar.winfo_reqwidth()
|
|
276
|
+
if width > 1:
|
|
277
|
+
self.grid_columnconfigure(1, minsize=width)
|
|
278
|
+
if self._direction in ('horizontal', 'both'):
|
|
279
|
+
height = self.horizontal_scrollbar.winfo_reqheight()
|
|
280
|
+
if height > 1:
|
|
281
|
+
self.grid_rowconfigure(1, minsize=height)
|
|
282
|
+
else:
|
|
283
|
+
self.grid_columnconfigure(1, minsize=0)
|
|
284
|
+
self.grid_rowconfigure(1, minsize=0)
|
|
285
|
+
|
|
286
|
+
def _bind_container_events(self):
|
|
287
|
+
"""Bind events for the container (enter/leave for autohide)."""
|
|
288
|
+
if self._scrollbar_visibility == 'hover':
|
|
289
|
+
self.bind('<Enter>', self._on_container_enter)
|
|
290
|
+
self.bind('<Leave>', self._on_container_leave)
|
|
291
|
+
self.canvas.bind('<Enter>', self._on_container_enter)
|
|
292
|
+
self.canvas.bind('<Leave>', self._on_container_leave)
|
|
293
|
+
self.vertical_scrollbar.bind('<Enter>', self._on_container_enter)
|
|
294
|
+
self.vertical_scrollbar.bind('<Leave>', self._on_container_leave)
|
|
295
|
+
self.horizontal_scrollbar.bind('<Enter>', self._on_container_enter)
|
|
296
|
+
self.horizontal_scrollbar.bind('<Leave>', self._on_container_leave)
|
|
297
|
+
|
|
298
|
+
def _on_container_enter(self, event):
|
|
299
|
+
"""Handle mouse entering the container."""
|
|
300
|
+
self._hovering = True
|
|
301
|
+
self.enable_scrolling()
|
|
302
|
+
if self._scrollbar_visibility == 'hover':
|
|
303
|
+
self._show_scrollbars()
|
|
304
|
+
|
|
305
|
+
def _on_container_leave(self, event):
|
|
306
|
+
"""Handle mouse leaving the container."""
|
|
307
|
+
self._hovering = False
|
|
308
|
+
self.disable_scrolling()
|
|
309
|
+
if self._scrollbar_visibility == 'hover':
|
|
310
|
+
self._hide_scrollbars()
|
|
311
|
+
|
|
312
|
+
def _content_fits(self):
|
|
313
|
+
"""Return booleans for whether content fits in the viewport (x_fit, y_fit)."""
|
|
314
|
+
if self._window_id:
|
|
315
|
+
bbox = self.canvas.bbox(self._window_id)
|
|
316
|
+
else:
|
|
317
|
+
bbox = self.canvas.bbox('all')
|
|
318
|
+
if not bbox:
|
|
319
|
+
return True, True
|
|
320
|
+
x0, y0, x1, y1 = bbox
|
|
321
|
+
content_w = x1 - x0
|
|
322
|
+
content_h = y1 - y0
|
|
323
|
+
viewport_w = max(1, self.canvas.winfo_width())
|
|
324
|
+
viewport_h = max(1, self.canvas.winfo_height())
|
|
325
|
+
if viewport_w <= 1 or viewport_h <= 1:
|
|
326
|
+
return True, True
|
|
327
|
+
return content_w <= viewport_w, content_h <= viewport_h
|
|
328
|
+
|
|
329
|
+
def _show_scrollbars(self):
|
|
330
|
+
"""Show scrollbars only if content overflows the viewport."""
|
|
331
|
+
x_fit, y_fit = self._content_fits()
|
|
332
|
+
if self._direction in ('vertical', 'both') and not y_fit:
|
|
333
|
+
self.vertical_scrollbar.grid()
|
|
334
|
+
else:
|
|
335
|
+
self.vertical_scrollbar.grid_remove()
|
|
336
|
+
if self._direction in ('horizontal', 'both') and not x_fit:
|
|
337
|
+
self.horizontal_scrollbar.grid()
|
|
338
|
+
else:
|
|
339
|
+
self.horizontal_scrollbar.grid_remove()
|
|
340
|
+
|
|
341
|
+
def _hide_scrollbars(self):
|
|
342
|
+
"""Hide scrollbars."""
|
|
343
|
+
self.vertical_scrollbar.grid_remove()
|
|
344
|
+
self.horizontal_scrollbar.grid_remove()
|
|
345
|
+
|
|
346
|
+
def _on_canvas_configure(self, event):
|
|
347
|
+
"""Update visibility when the viewport size changes."""
|
|
348
|
+
self._update_scrollbar_visibility()
|
|
349
|
+
|
|
350
|
+
def _on_canvas_scroll_y(self, first, last):
|
|
351
|
+
"""Update vertical scrollbar position."""
|
|
352
|
+
self.vertical_scrollbar.set(first, last)
|
|
353
|
+
self._update_scrollbar_visibility()
|
|
354
|
+
|
|
355
|
+
def _on_canvas_scroll_x(self, first, last):
|
|
356
|
+
"""Update horizontal scrollbar position."""
|
|
357
|
+
self.horizontal_scrollbar.set(first, last)
|
|
358
|
+
self._update_scrollbar_visibility()
|
|
359
|
+
|
|
360
|
+
def _update_scrollbar_visibility(self):
|
|
361
|
+
"""Update scrollbar visibility based on current mode."""
|
|
362
|
+
if self._scrollbar_visibility == 'always':
|
|
363
|
+
self._show_scrollbars()
|
|
364
|
+
elif self._scrollbar_visibility == 'never':
|
|
365
|
+
self._hide_scrollbars()
|
|
366
|
+
elif self._scrollbar_visibility == 'hover':
|
|
367
|
+
# Show only while hovering and overflowing
|
|
368
|
+
x_fit, y_fit = self._content_fits()
|
|
369
|
+
if self._hovering and self._direction in ('vertical', 'both') and not y_fit:
|
|
370
|
+
self.vertical_scrollbar.grid()
|
|
371
|
+
else:
|
|
372
|
+
self.vertical_scrollbar.grid_remove()
|
|
373
|
+
|
|
374
|
+
if self._hovering and self._direction in ('horizontal', 'both') and not x_fit:
|
|
375
|
+
self.horizontal_scrollbar.grid()
|
|
376
|
+
else:
|
|
377
|
+
self.horizontal_scrollbar.grid_remove()
|
|
378
|
+
elif self._scrollbar_visibility == 'scroll':
|
|
379
|
+
# Hide if no overflow; otherwise leave current visibility to scroll events
|
|
380
|
+
x_fit, y_fit = self._content_fits()
|
|
381
|
+
if y_fit:
|
|
382
|
+
self.vertical_scrollbar.grid_remove()
|
|
383
|
+
if x_fit:
|
|
384
|
+
self.horizontal_scrollbar.grid_remove()
|
|
385
|
+
|
|
386
|
+
def _on_frame_configure(self, event):
|
|
387
|
+
"""Update scroll region and refresh bindings on configure."""
|
|
388
|
+
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
|
|
389
|
+
self._update_scrollbar_visibility()
|
|
390
|
+
|
|
391
|
+
# Refresh bindings for any newly added widgets
|
|
392
|
+
if self._scrolling_enabled and self._child_widget:
|
|
393
|
+
self._add_scroll_binding(self._child_widget)
|
|
394
|
+
|
|
395
|
+
def _on_mousewheel(self, event):
|
|
396
|
+
"""Handle vertical mouse wheel scrolling."""
|
|
397
|
+
# Check if vertical scrolling is actually possible
|
|
398
|
+
if self._direction in ('vertical', 'both'):
|
|
399
|
+
try:
|
|
400
|
+
first, last = self.canvas.yview()
|
|
401
|
+
# If first=0.0 and last=1.0, all content is visible, no need to scroll
|
|
402
|
+
if first <= 0.0 and last >= 1.0:
|
|
403
|
+
return # Content fits, don't scroll
|
|
404
|
+
except:
|
|
405
|
+
pass # If we can't check, allow scrolling
|
|
406
|
+
|
|
407
|
+
# Show scrollbar temporarily in scroll mode
|
|
408
|
+
if self._scrollbar_visibility == 'scroll':
|
|
409
|
+
self._show_scrollbars()
|
|
410
|
+
if self._hide_timer:
|
|
411
|
+
self.after_cancel(self._hide_timer)
|
|
412
|
+
self._hide_timer = self.after(self._autohide_delay, self._hide_scrollbars)
|
|
413
|
+
|
|
414
|
+
# Calculate delta based on platform
|
|
415
|
+
delta = 0
|
|
416
|
+
if self.winsys.lower() == "win32":
|
|
417
|
+
delta = -int(event.delta / 120)
|
|
418
|
+
elif self.winsys.lower() == "aqua":
|
|
419
|
+
delta = -event.delta
|
|
420
|
+
elif event.num == 4:
|
|
421
|
+
delta = -10
|
|
422
|
+
elif event.num == 5:
|
|
423
|
+
delta = 10
|
|
424
|
+
|
|
425
|
+
# Scroll vertically
|
|
426
|
+
if self._direction in ('vertical', 'both') and delta != 0:
|
|
427
|
+
self.canvas.yview_scroll(delta, 'units')
|
|
428
|
+
|
|
429
|
+
# Don't return 'break' - allow event to propagate to other handlers if needed
|
|
430
|
+
# But we can return None to continue normal processing
|
|
431
|
+
|
|
432
|
+
def _on_shift_mousewheel(self, event):
|
|
433
|
+
"""Handle horizontal mouse wheel scrolling (Shift+MouseWheel)."""
|
|
434
|
+
# Check if horizontal scrolling is actually possible
|
|
435
|
+
if self._direction in ('horizontal', 'both'):
|
|
436
|
+
try:
|
|
437
|
+
first, last = self.canvas.xview()
|
|
438
|
+
# If first=0.0 and last=1.0, all content is visible, no need to scroll
|
|
439
|
+
if first <= 0.0 and last >= 1.0:
|
|
440
|
+
return # Content fits, don't scroll
|
|
441
|
+
except:
|
|
442
|
+
pass # If we can't check, allow scrolling
|
|
443
|
+
|
|
444
|
+
# Show scrollbar temporarily in scroll mode
|
|
445
|
+
if self._scrollbar_visibility == 'scroll':
|
|
446
|
+
self._show_scrollbars()
|
|
447
|
+
if self._hide_timer:
|
|
448
|
+
self.after_cancel(self._hide_timer)
|
|
449
|
+
self._hide_timer = self.after(self._autohide_delay, self._hide_scrollbars)
|
|
450
|
+
|
|
451
|
+
# Calculate delta based on platform
|
|
452
|
+
delta = 0
|
|
453
|
+
if self.winsys.lower() == "win32":
|
|
454
|
+
delta = -int(event.delta / 120)
|
|
455
|
+
elif self.winsys.lower() == "aqua":
|
|
456
|
+
delta = -event.delta
|
|
457
|
+
elif event.num == 4:
|
|
458
|
+
delta = -10
|
|
459
|
+
elif event.num == 5:
|
|
460
|
+
delta = 10
|
|
461
|
+
|
|
462
|
+
# Scroll horizontally
|
|
463
|
+
if self._direction in ('horizontal', 'both') and delta != 0:
|
|
464
|
+
self.canvas.xview_scroll(delta, 'units')
|
|
465
|
+
|
|
466
|
+
# Don't return 'break' - allow event to propagate if needed
|
|
467
|
+
|
|
468
|
+
def _add_scroll_binding(self, widget):
|
|
469
|
+
"""Recursively add scroll bind tag to widget and all descendants."""
|
|
470
|
+
try:
|
|
471
|
+
# Get current bindtags
|
|
472
|
+
tags = list(widget.bindtags())
|
|
473
|
+
|
|
474
|
+
# Add our scroll tag if not already present
|
|
475
|
+
if self._scroll_tag not in tags:
|
|
476
|
+
# Insert after the widget name but before the class
|
|
477
|
+
# Typical order: (widget_name, class, toplevel, 'all')
|
|
478
|
+
# We want: (widget_name, scroll_tag, class, toplevel, 'all')
|
|
479
|
+
if len(tags) >= 2:
|
|
480
|
+
tags.insert(1, self._scroll_tag)
|
|
481
|
+
else:
|
|
482
|
+
tags.append(self._scroll_tag)
|
|
483
|
+
widget.bindtags(tuple(tags))
|
|
484
|
+
except:
|
|
485
|
+
# Some widgets may not support bindtags
|
|
486
|
+
pass
|
|
487
|
+
|
|
488
|
+
# Recurse into all children
|
|
489
|
+
for child in widget.winfo_children():
|
|
490
|
+
self._add_scroll_binding(child)
|
|
491
|
+
|
|
492
|
+
def _del_scroll_binding(self, widget):
|
|
493
|
+
"""Recursively remove scroll bind tag from widget and all descendants."""
|
|
494
|
+
try:
|
|
495
|
+
tags = list(widget.bindtags())
|
|
496
|
+
if self._scroll_tag in tags:
|
|
497
|
+
tags.remove(self._scroll_tag)
|
|
498
|
+
widget.bindtags(tuple(tags))
|
|
499
|
+
except:
|
|
500
|
+
pass
|
|
501
|
+
|
|
502
|
+
# Recurse into all children
|
|
503
|
+
for child in widget.winfo_children():
|
|
504
|
+
self._del_scroll_binding(child)
|
|
505
|
+
|
|
506
|
+
def enable_scrolling(self):
|
|
507
|
+
"""Enable mouse wheel scrolling on canvas and all child widgets."""
|
|
508
|
+
if not self._scrolling_enabled:
|
|
509
|
+
# Add binding to canvas for exposed areas
|
|
510
|
+
self._add_scroll_binding(self.canvas)
|
|
511
|
+
|
|
512
|
+
# Add binding to child widget if it exists
|
|
513
|
+
if self._child_widget:
|
|
514
|
+
self._add_scroll_binding(self._child_widget)
|
|
515
|
+
|
|
516
|
+
self._scrolling_enabled = True
|
|
517
|
+
|
|
518
|
+
def disable_scrolling(self):
|
|
519
|
+
"""Disable mouse wheel scrolling on canvas and all child widgets."""
|
|
520
|
+
if self._scrolling_enabled:
|
|
521
|
+
# Remove binding from canvas
|
|
522
|
+
self._del_scroll_binding(self.canvas)
|
|
523
|
+
|
|
524
|
+
# Remove binding from child widget if it exists
|
|
525
|
+
if self._child_widget:
|
|
526
|
+
self._del_scroll_binding(self._child_widget)
|
|
527
|
+
|
|
528
|
+
self._scrolling_enabled = False
|
|
529
|
+
|
|
530
|
+
def add(self, widget: Widget = None, *, anchor: str = 'nw', **kwargs: Any) -> Widget:
|
|
531
|
+
"""Add a widget to the scrollable area, or create and return a Frame.
|
|
532
|
+
|
|
533
|
+
Args:
|
|
534
|
+
widget (Widget | None): The widget to add. If None, creates a Frame.
|
|
535
|
+
anchor (str): Anchor position for the widget in the canvas. Default is 'nw'.
|
|
536
|
+
**kwargs: When widget is None, these are passed to Frame (e.g., padding, bootstyle).
|
|
537
|
+
|
|
538
|
+
Returns:
|
|
539
|
+
Widget: The content widget (passed or created).
|
|
540
|
+
|
|
541
|
+
Raises:
|
|
542
|
+
ValueError: If the ScrollView already contains a widget and a new one is provided.
|
|
543
|
+
"""
|
|
544
|
+
# If content exists and no widget passed, return existing (idempotent)
|
|
545
|
+
if self._child_widget is not None:
|
|
546
|
+
if widget is not None:
|
|
547
|
+
raise ValueError("ScrollView already contains a widget. Use remove() first.")
|
|
548
|
+
return self._child_widget
|
|
549
|
+
|
|
550
|
+
# Create frame with kwargs if no widget provided
|
|
551
|
+
if widget is None:
|
|
552
|
+
widget = Frame(self.canvas, **kwargs)
|
|
553
|
+
|
|
554
|
+
self._child_widget = widget
|
|
555
|
+
|
|
556
|
+
# Create window in canvas
|
|
557
|
+
self._window_id = self.canvas.create_window(0, 0, anchor=anchor, window=widget)
|
|
558
|
+
|
|
559
|
+
# Keep scrollbars above the canvas/content
|
|
560
|
+
self.vertical_scrollbar.lift()
|
|
561
|
+
self.horizontal_scrollbar.lift()
|
|
562
|
+
|
|
563
|
+
# Bind configure event to update scroll region
|
|
564
|
+
widget.bind('<Configure>', self._on_frame_configure)
|
|
565
|
+
self._update_scrollbar_visibility()
|
|
566
|
+
|
|
567
|
+
# Bind configure event to update scroll region
|
|
568
|
+
widget.bind('<Configure>', self._on_frame_configure)
|
|
569
|
+
|
|
570
|
+
# Enable scrolling based on mode
|
|
571
|
+
if self._scrollbar_visibility in ('always', 'never', 'scroll'):
|
|
572
|
+
# Always enable scrolling for these modes
|
|
573
|
+
self.enable_scrolling()
|
|
574
|
+
# For 'hover' mode, scrolling is enabled on enter
|
|
575
|
+
|
|
576
|
+
# Initial scroll region update
|
|
577
|
+
self.canvas.update_idletasks()
|
|
578
|
+
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
|
|
579
|
+
|
|
580
|
+
return widget
|
|
581
|
+
|
|
582
|
+
def remove(self) -> Optional[Widget]:
|
|
583
|
+
"""Remove the current widget from the scrollable area.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
The removed widget, or None if no widget was present.
|
|
587
|
+
"""
|
|
588
|
+
if self._child_widget is None:
|
|
589
|
+
return None
|
|
590
|
+
|
|
591
|
+
widget = self._child_widget
|
|
592
|
+
|
|
593
|
+
# Disable scrolling
|
|
594
|
+
self.disable_scrolling()
|
|
595
|
+
|
|
596
|
+
# Unbind events
|
|
597
|
+
widget.unbind('<Configure>')
|
|
598
|
+
|
|
599
|
+
# Delete canvas window
|
|
600
|
+
if self._window_id:
|
|
601
|
+
self.canvas.delete(self._window_id)
|
|
602
|
+
self._window_id = None
|
|
603
|
+
|
|
604
|
+
self._child_widget = None
|
|
605
|
+
self._scrolling_enabled = False
|
|
606
|
+
|
|
607
|
+
return widget
|
|
608
|
+
|
|
609
|
+
def get_child(self) -> Optional[Widget]:
|
|
610
|
+
"""Get the current child widget.
|
|
611
|
+
|
|
612
|
+
Returns:
|
|
613
|
+
The child widget, or None if no widget is present.
|
|
614
|
+
"""
|
|
615
|
+
return self._child_widget
|
|
616
|
+
|
|
617
|
+
def refresh_bindings(self):
|
|
618
|
+
"""Refresh mouse wheel bindings for all widgets.
|
|
619
|
+
|
|
620
|
+
Call this after dynamically adding many widgets at once to ensure
|
|
621
|
+
mouse wheel scrolling works on all new widgets.
|
|
622
|
+
"""
|
|
623
|
+
if self._child_widget and self._scrolling_enabled:
|
|
624
|
+
# Re-enable to refresh bindings
|
|
625
|
+
self.disable_scrolling()
|
|
626
|
+
self.enable_scrolling()
|
|
627
|
+
|
|
628
|
+
def yview(self, *args):
|
|
629
|
+
"""Query or command vertical view position."""
|
|
630
|
+
return self.canvas.yview(*args)
|
|
631
|
+
|
|
632
|
+
def xview(self, *args):
|
|
633
|
+
"""Query or command horizontal view position."""
|
|
634
|
+
return self.canvas.xview(*args)
|
|
635
|
+
|
|
636
|
+
def yview_moveto(self, fraction: float):
|
|
637
|
+
"""Scroll to a specific vertical position.
|
|
638
|
+
|
|
639
|
+
Args:
|
|
640
|
+
fraction: Position from 0.0 (top) to 1.0 (bottom).
|
|
641
|
+
"""
|
|
642
|
+
self.canvas.yview_moveto(fraction)
|
|
643
|
+
|
|
644
|
+
def xview_moveto(self, fraction: float):
|
|
645
|
+
"""Scroll to a specific horizontal position.
|
|
646
|
+
|
|
647
|
+
Args:
|
|
648
|
+
fraction: Position from 0.0 (left) to 1.0 (right).
|
|
649
|
+
"""
|
|
650
|
+
self.canvas.xview_moveto(fraction)
|
|
651
|
+
|
|
652
|
+
def destroy(self):
|
|
653
|
+
"""Clean up resources and destroy the widget."""
|
|
654
|
+
# Cancel any pending timer
|
|
655
|
+
if self._hide_timer:
|
|
656
|
+
self.after_cancel(self._hide_timer)
|
|
657
|
+
self._hide_timer = None
|
|
658
|
+
|
|
659
|
+
# Remove scroll bindings from child widget
|
|
660
|
+
if self._child_widget:
|
|
661
|
+
self.disable_scrolling()
|
|
662
|
+
|
|
663
|
+
# Unbind class bindings for the scroll tag
|
|
664
|
+
if self.winsys.lower() == "x11":
|
|
665
|
+
self.unbind_class(self._scroll_tag, "<Button-4>")
|
|
666
|
+
self.unbind_class(self._scroll_tag, "<Button-5>")
|
|
667
|
+
self.unbind_class(self._scroll_tag, "<Shift-Button-4>")
|
|
668
|
+
self.unbind_class(self._scroll_tag, "<Shift-Button-5>")
|
|
669
|
+
else:
|
|
670
|
+
self.unbind_class(self._scroll_tag, "<MouseWheel>")
|
|
671
|
+
self.unbind_class(self._scroll_tag, "<Shift-MouseWheel>")
|
|
672
|
+
|
|
673
|
+
# Call parent destroy
|
|
674
|
+
super().destroy()
|
|
675
|
+
|