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,974 @@
|
|
|
1
|
+
"""Tabs widget - a tab bar container for TabItem widgets."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
__all__ = ['Tabs']
|
|
5
|
+
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
from tkinter import Variable
|
|
8
|
+
from typing_extensions import Unpack
|
|
9
|
+
from typing import Any, Callable, Literal, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from bootstack.widgets._impl.primitives.packframe import PackFrame
|
|
12
|
+
from bootstack.widgets._impl.primitives.frame import Frame, FrameKwargs
|
|
13
|
+
from bootstack.widgets._impl.primitives.separator import Separator
|
|
14
|
+
from bootstack.widgets._impl.primitives.button import Button
|
|
15
|
+
from bootstack.widgets._impl.composites.scrollview import ScrollView
|
|
16
|
+
from bootstack.widgets._impl.composites.tabs.tabitem import TabItem
|
|
17
|
+
from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
|
|
18
|
+
from bootstack.widgets.types import Master
|
|
19
|
+
from bootstack._core import NavigationError
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from bootstack.signals import Signal
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class Tabs(Frame):
|
|
26
|
+
"""A container widget for grouping TabItem widgets.
|
|
27
|
+
|
|
28
|
+
Tabs provides a tab bar with optional divider. It manages the layout,
|
|
29
|
+
orientation, and styling of child TabItems.
|
|
30
|
+
|
|
31
|
+
Attributes:
|
|
32
|
+
orient: The orientation of the tab bar ('horizontal' or 'vertical').
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
master: Master = None,
|
|
38
|
+
orient: Literal['horizontal', 'vertical'] = 'horizontal',
|
|
39
|
+
variant: Literal['bar'] = 'bar',
|
|
40
|
+
show_divider: bool = None,
|
|
41
|
+
compound: Literal['left', 'right', 'top', 'bottom', 'center', 'none'] = 'left',
|
|
42
|
+
tab_width: None | int | Literal['stretch'] = None,
|
|
43
|
+
tab_min_width: int = 80,
|
|
44
|
+
tab_padding: tuple = (12, 8),
|
|
45
|
+
tab_anchor: str = None,
|
|
46
|
+
enable_closing: bool | Literal['hover'] = False,
|
|
47
|
+
enable_adding: bool = False,
|
|
48
|
+
max_tabs: int | None = None,
|
|
49
|
+
variable: Variable = None,
|
|
50
|
+
signal: 'Signal[Any]' = None,
|
|
51
|
+
accent: str = None,
|
|
52
|
+
**kwargs: Unpack[FrameKwargs]
|
|
53
|
+
):
|
|
54
|
+
"""Create a Tabs widget.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
master: Parent widget. If None, uses the default root window.
|
|
58
|
+
orient: Orientation of the tab bar. 'horizontal' places tabs in a row,
|
|
59
|
+
'vertical' places tabs in a column. Default is 'horizontal'.
|
|
60
|
+
variant: Visual style variant. Only 'bar' is supported.
|
|
61
|
+
show_divider: Whether to show a divider line. If None (default),
|
|
62
|
+
automatically set to True for 'bar' variant, False for others.
|
|
63
|
+
compound: How to position icon relative to text in tabs.
|
|
64
|
+
Passed to all TabItems. Default is 'left'.
|
|
65
|
+
tab_width: Width of tabs. None for auto-sizing, an integer for
|
|
66
|
+
fixed character width, or 'stretch' to expand tabs to fill
|
|
67
|
+
available space (horizontal only). Default is None.
|
|
68
|
+
tab_min_width: Minimum tab width in pixels. Tabs will not shrink
|
|
69
|
+
below this value regardless of label content. Default is 80.
|
|
70
|
+
tab_padding: Padding for all tabs as (horizontal, vertical).
|
|
71
|
+
Default is (12, 8).
|
|
72
|
+
tab_anchor: Anchor for tab text/icon alignment. If None, defaults
|
|
73
|
+
to 'w' for vertical orientation, 'center' for horizontal.
|
|
74
|
+
enable_closing: Default close button visibility for all tabs.
|
|
75
|
+
True=always visible, False=hidden, 'hover'=visible on hover.
|
|
76
|
+
Can be overridden per-tab via `closable` in add_tab().
|
|
77
|
+
enable_adding: If True, shows an "add" button. In horizontal
|
|
78
|
+
orientation, shows a plus icon on the right. In vertical
|
|
79
|
+
orientation, shows "New Tab" at the bottom. Fires `<<TabAdd>>`
|
|
80
|
+
event when clicked.
|
|
81
|
+
max_tabs: Maximum number of tabs allowed. When the tab count
|
|
82
|
+
reaches this value the add button is disabled (and re-enabled
|
|
83
|
+
when a tab is removed). None (default) means no limit.
|
|
84
|
+
variable: Tk variable for tracking selected tab value.
|
|
85
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
86
|
+
signal: Reactive Signal for tracking selected tab value.
|
|
87
|
+
accent: Accent token for styling tabs.
|
|
88
|
+
**kwargs: Additional arguments passed to Frame.
|
|
89
|
+
"""
|
|
90
|
+
super().__init__(master=master, **kwargs)
|
|
91
|
+
|
|
92
|
+
self._orient = orient
|
|
93
|
+
self._variant = variant
|
|
94
|
+
self._compound = compound
|
|
95
|
+
self._tab_width = tab_width
|
|
96
|
+
self._tab_min_width = tab_min_width
|
|
97
|
+
self._tab_padding = tab_padding
|
|
98
|
+
self._enable_closing = enable_closing
|
|
99
|
+
self._enable_adding = enable_adding
|
|
100
|
+
self._max_tabs = max_tabs
|
|
101
|
+
self._accent = accent
|
|
102
|
+
|
|
103
|
+
# Handle variable/signal setup
|
|
104
|
+
if signal is not None:
|
|
105
|
+
# Signal provided - extract its variable
|
|
106
|
+
self._signal = signal
|
|
107
|
+
self._variable = signal.var
|
|
108
|
+
elif variable is not None:
|
|
109
|
+
# Variable provided - wrap in Signal
|
|
110
|
+
from bootstack.signals import Signal
|
|
111
|
+
self._variable = variable
|
|
112
|
+
self._signal = Signal.from_variable(variable)
|
|
113
|
+
else:
|
|
114
|
+
# Neither provided - create internal variable and signal
|
|
115
|
+
from bootstack.signals import Signal
|
|
116
|
+
self._variable = tk.StringVar()
|
|
117
|
+
self._signal = Signal.from_variable(self._variable)
|
|
118
|
+
|
|
119
|
+
# Default anchor based on orientation
|
|
120
|
+
if tab_anchor is None:
|
|
121
|
+
self._tab_anchor = 'w' if orient == 'vertical' else 'center'
|
|
122
|
+
else:
|
|
123
|
+
self._tab_anchor = tab_anchor
|
|
124
|
+
|
|
125
|
+
# Determine show_divider default based on variant
|
|
126
|
+
if show_divider is None:
|
|
127
|
+
self._show_divider = variant == 'bar'
|
|
128
|
+
else:
|
|
129
|
+
self._show_divider = show_divider
|
|
130
|
+
|
|
131
|
+
# Set direction based on orient
|
|
132
|
+
direction = 'horizontal' if orient == 'horizontal' else 'vertical'
|
|
133
|
+
|
|
134
|
+
# Gap between tabs
|
|
135
|
+
gap = 0
|
|
136
|
+
|
|
137
|
+
# Internal divider widget
|
|
138
|
+
self._divider: Separator | None = None
|
|
139
|
+
|
|
140
|
+
# Internal add button
|
|
141
|
+
self._add_button: Button | None = None
|
|
142
|
+
|
|
143
|
+
# Tab tracking by key
|
|
144
|
+
self._tabs: dict[str, TabItem] = {}
|
|
145
|
+
self._tab_order: list[str] = []
|
|
146
|
+
self._tab_values: dict[str, Any] = {}
|
|
147
|
+
self._tab_icons: dict[str, Any] = {}
|
|
148
|
+
self._hidden: set[str] = set()
|
|
149
|
+
self._counter = 0 # For auto-generating keys
|
|
150
|
+
|
|
151
|
+
# Tabs scroll when they exceed the strip. 'stretch' always fits, so it
|
|
152
|
+
# uses the plain non-scrolling strip instead.
|
|
153
|
+
self._scrollable = tab_width != 'stretch'
|
|
154
|
+
self._axis = 'x' if orient == 'horizontal' else 'y'
|
|
155
|
+
self._scroll: ScrollView | None = None
|
|
156
|
+
self._strip_row: Frame | None = None
|
|
157
|
+
self._overflow_button: Button | None = None
|
|
158
|
+
self._overflow_menu = None
|
|
159
|
+
self._overflow_visible = False
|
|
160
|
+
self._overflow_after: str | None = None
|
|
161
|
+
self._canvas_cross = 0
|
|
162
|
+
self._wheel_tag = f'TabStripWheel_{id(self)}'
|
|
163
|
+
|
|
164
|
+
# Build the tab strip (scrollable or static) + divider
|
|
165
|
+
if self._scrollable:
|
|
166
|
+
self._build_scrollable_strip(orient, direction, gap)
|
|
167
|
+
else:
|
|
168
|
+
self._build_static_strip(orient, direction, gap)
|
|
169
|
+
|
|
170
|
+
# Create add button if enabled (must be after the strip is built)
|
|
171
|
+
if enable_adding:
|
|
172
|
+
self._create_add_button()
|
|
173
|
+
|
|
174
|
+
# Wire scroll behavior once everything exists
|
|
175
|
+
if self._scrollable:
|
|
176
|
+
self._setup_wheel_bindings()
|
|
177
|
+
self._signal.subscribe(self._on_selection_scroll)
|
|
178
|
+
self.bind('<Destroy>', self._on_overflow_destroy, add='+')
|
|
179
|
+
|
|
180
|
+
def _build_static_strip(self, orient: str, direction: str, gap: int) -> None:
|
|
181
|
+
"""Build the plain non-scrolling strip (used when `tab_width='stretch'`)."""
|
|
182
|
+
self._tab_bar = PackFrame(self, direction=direction, gap=gap)
|
|
183
|
+
if orient == 'horizontal':
|
|
184
|
+
self._tab_bar.pack(side='top', fill='x')
|
|
185
|
+
if self._show_divider:
|
|
186
|
+
self._divider = Separator(self, orient='horizontal')
|
|
187
|
+
self._divider.pack(side='top', fill='x')
|
|
188
|
+
else:
|
|
189
|
+
self._tab_bar.pack(side='left', fill='y')
|
|
190
|
+
if self._show_divider:
|
|
191
|
+
self._divider = Separator(self, orient='vertical')
|
|
192
|
+
self._divider.pack(side='left', fill='y')
|
|
193
|
+
|
|
194
|
+
def _build_scrollable_strip(self, orient: str, direction: str, gap: int) -> None:
|
|
195
|
+
"""Build a scrollbar-less scrolling strip with a trailing overflow menu.
|
|
196
|
+
|
|
197
|
+
The tab `PackFrame` lives inside a `ScrollView`; the add and overflow
|
|
198
|
+
buttons are pinned outside the scroll region (right edge for horizontal,
|
|
199
|
+
bottom for vertical) so they stay reachable while the tabs scroll.
|
|
200
|
+
"""
|
|
201
|
+
if orient == 'horizontal':
|
|
202
|
+
self._strip_row = Frame(self)
|
|
203
|
+
self._strip_row.pack(side='top', fill='x')
|
|
204
|
+
self._scroll = ScrollView(
|
|
205
|
+
self._strip_row,
|
|
206
|
+
scroll_direction='horizontal',
|
|
207
|
+
scrollbar_visibility='never',
|
|
208
|
+
)
|
|
209
|
+
# Small gap between the scrolling tabs and the pinned add/overflow buttons.
|
|
210
|
+
self._scroll.pack(side='left', fill='x', expand=True, padx=(0, 6))
|
|
211
|
+
else:
|
|
212
|
+
self._strip_row = Frame(self)
|
|
213
|
+
self._strip_row.pack(side='left', fill='y')
|
|
214
|
+
self._scroll = ScrollView(
|
|
215
|
+
self._strip_row,
|
|
216
|
+
scroll_direction='vertical',
|
|
217
|
+
scrollbar_visibility='never',
|
|
218
|
+
)
|
|
219
|
+
# Small gap between the scrolling tabs and the pinned add/overflow buttons.
|
|
220
|
+
self._scroll.pack(side='top', fill='y', expand=True, pady=(0, 6))
|
|
221
|
+
|
|
222
|
+
self._tab_bar = PackFrame(self._scroll.canvas, direction=direction, gap=gap)
|
|
223
|
+
self._scroll.add(self._tab_bar)
|
|
224
|
+
|
|
225
|
+
# Match the canvas cross-size to the strip and re-evaluate overflow on
|
|
226
|
+
# any size change (ScrollView's own <Configure> bindings are kept via +).
|
|
227
|
+
self._tab_bar.bind('<Configure>', self._on_tabbar_configure, add='+')
|
|
228
|
+
self._scroll.canvas.bind('<Configure>', self._on_viewport_configure, add='+')
|
|
229
|
+
|
|
230
|
+
if self._show_divider:
|
|
231
|
+
if orient == 'horizontal':
|
|
232
|
+
self._divider = Separator(self, orient='horizontal')
|
|
233
|
+
self._divider.pack(side='top', fill='x')
|
|
234
|
+
else:
|
|
235
|
+
self._divider = Separator(self, orient='vertical')
|
|
236
|
+
self._divider.pack(side='left', fill='y')
|
|
237
|
+
|
|
238
|
+
def _create_divider(self):
|
|
239
|
+
"""Create the divider separator widget."""
|
|
240
|
+
if self._divider is not None:
|
|
241
|
+
return
|
|
242
|
+
if self._orient == 'horizontal':
|
|
243
|
+
self._divider = Separator(self, orient='horizontal')
|
|
244
|
+
self._divider.pack(side='top', fill='x')
|
|
245
|
+
else:
|
|
246
|
+
self._divider = Separator(self, orient='vertical')
|
|
247
|
+
self._divider.pack(side='left', fill='y')
|
|
248
|
+
|
|
249
|
+
def _destroy_divider(self):
|
|
250
|
+
"""Remove the divider widget."""
|
|
251
|
+
if self._divider is not None:
|
|
252
|
+
self._divider.pack_forget()
|
|
253
|
+
self._divider.destroy()
|
|
254
|
+
self._divider = None
|
|
255
|
+
|
|
256
|
+
def _create_add_button(self):
|
|
257
|
+
"""Create the add button widget."""
|
|
258
|
+
if self._add_button is not None:
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
# When scrolling, the add button is pinned outside the scroll region
|
|
262
|
+
# (right edge for horizontal, bottom for vertical) so it stays put.
|
|
263
|
+
parent = self._strip_row if self._scrollable else self._tab_bar
|
|
264
|
+
|
|
265
|
+
if self._orient == 'horizontal':
|
|
266
|
+
self._add_button = Button(
|
|
267
|
+
parent,
|
|
268
|
+
icon={'name': 'plus-lg', 'size': 16},
|
|
269
|
+
icon_only=True,
|
|
270
|
+
variant='ghost',
|
|
271
|
+
command=self._on_add_click,
|
|
272
|
+
)
|
|
273
|
+
if self._scrollable:
|
|
274
|
+
self._add_button.pack(side='right')
|
|
275
|
+
else:
|
|
276
|
+
self._add_button.pack()
|
|
277
|
+
else:
|
|
278
|
+
# "New Tab" with plus icon
|
|
279
|
+
self._add_button = Button(
|
|
280
|
+
parent,
|
|
281
|
+
text='New',
|
|
282
|
+
icon='plus-lg',
|
|
283
|
+
variant='ghost',
|
|
284
|
+
padding=2,
|
|
285
|
+
command=self._on_add_click,
|
|
286
|
+
anchor='w',
|
|
287
|
+
)
|
|
288
|
+
if self._scrollable:
|
|
289
|
+
self._add_button.pack(side='bottom', fill='x')
|
|
290
|
+
else:
|
|
291
|
+
self._add_button.pack(fill='x')
|
|
292
|
+
|
|
293
|
+
self._update_add_button_state()
|
|
294
|
+
|
|
295
|
+
def _destroy_add_button(self):
|
|
296
|
+
"""Remove the add button widget."""
|
|
297
|
+
if self._add_button is not None:
|
|
298
|
+
self._add_button.pack_forget()
|
|
299
|
+
self._add_button.destroy()
|
|
300
|
+
self._add_button = None
|
|
301
|
+
|
|
302
|
+
def _update_add_button_state(self):
|
|
303
|
+
"""Disable the add button when the tab count reaches `max_tabs`."""
|
|
304
|
+
if self._add_button is None:
|
|
305
|
+
return
|
|
306
|
+
at_limit = self._max_tabs is not None and len(self._tabs) >= self._max_tabs
|
|
307
|
+
if at_limit:
|
|
308
|
+
self._add_button.state(['disabled'])
|
|
309
|
+
else:
|
|
310
|
+
self._add_button.state(['!disabled'])
|
|
311
|
+
|
|
312
|
+
def _on_add_click(self):
|
|
313
|
+
"""Handle add button click."""
|
|
314
|
+
# Guard against a queued click that lands after the limit is reached.
|
|
315
|
+
if self._max_tabs is not None and len(self._tabs) >= self._max_tabs:
|
|
316
|
+
return
|
|
317
|
+
self.event_generate('<<TabAdd>>')
|
|
318
|
+
|
|
319
|
+
def on_tab_added(self, callback: Callable) -> str:
|
|
320
|
+
"""Register a callback for `<<TabAdd>>` events (fires when the add button is clicked).
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
callback: Called when the add button is clicked. `event.data` is None.
|
|
324
|
+
|
|
325
|
+
Returns:
|
|
326
|
+
Bind ID — pass to `off_tab_added()` to unsubscribe.
|
|
327
|
+
"""
|
|
328
|
+
return self.bind('<<TabAdd>>', callback, add='+')
|
|
329
|
+
|
|
330
|
+
def off_tab_added(self, bind_id: str | None = None) -> None:
|
|
331
|
+
"""Unsubscribe from `<<TabAdd>>`.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
bind_id: ID returned by `on_tab_added()`. If None, removes all.
|
|
335
|
+
"""
|
|
336
|
+
self.unbind('<<TabAdd>>', bind_id)
|
|
337
|
+
|
|
338
|
+
def on_tab_closed(self, callback: Callable) -> str:
|
|
339
|
+
"""Register a callback for `<<TabClose>>` events (fires when a tab's close button is clicked).
|
|
340
|
+
|
|
341
|
+
Args:
|
|
342
|
+
callback: Receives `event.data = {'value': Any}` — the closed tab's value.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
Bind ID — pass to `off_tab_closed()` to unsubscribe.
|
|
346
|
+
"""
|
|
347
|
+
return self.bind('<<TabClose>>', callback, add='+')
|
|
348
|
+
|
|
349
|
+
def off_tab_closed(self, bind_id: str | None = None) -> None:
|
|
350
|
+
"""Unsubscribe from `<<TabClose>>`.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
bind_id: ID returned by `on_tab_closed()`. If None, removes all.
|
|
354
|
+
"""
|
|
355
|
+
self.unbind('<<TabClose>>', bind_id)
|
|
356
|
+
|
|
357
|
+
def add(
|
|
358
|
+
self,
|
|
359
|
+
text: str = "",
|
|
360
|
+
*,
|
|
361
|
+
key: str = None,
|
|
362
|
+
icon: str | dict = None,
|
|
363
|
+
value: Any = None,
|
|
364
|
+
closable: bool | Literal['hover'] | None = None,
|
|
365
|
+
close_command: Callable = None,
|
|
366
|
+
command: Callable = None,
|
|
367
|
+
**kwargs
|
|
368
|
+
) -> TabItem:
|
|
369
|
+
"""Add a new tab to the tab bar.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
text: Text to display on the tab.
|
|
373
|
+
key: Unique identifier for the tab. Auto-generated if not provided.
|
|
374
|
+
icon: Icon to display on the tab.
|
|
375
|
+
value: Value associated with this tab for selection tracking.
|
|
376
|
+
If None, defaults to the key.
|
|
377
|
+
closable: Close button visibility (True, False, or 'hover').
|
|
378
|
+
If None, uses the widget's `enable_closing` setting.
|
|
379
|
+
close_command: Callback when close button is clicked.
|
|
380
|
+
command: Callback when tab is selected.
|
|
381
|
+
**kwargs: Additional arguments passed to TabItem.
|
|
382
|
+
|
|
383
|
+
Returns:
|
|
384
|
+
The created TabItem widget.
|
|
385
|
+
|
|
386
|
+
Raises:
|
|
387
|
+
ValueError: If a tab with the same key already exists.
|
|
388
|
+
"""
|
|
389
|
+
# Auto-generate key if not provided
|
|
390
|
+
if key is None:
|
|
391
|
+
key = f"tab_{self._counter}"
|
|
392
|
+
self._counter += 1
|
|
393
|
+
|
|
394
|
+
if key in self._tabs:
|
|
395
|
+
raise NavigationError(f"A tab with the key '{key}' already exists.")
|
|
396
|
+
|
|
397
|
+
# Default value to key if not specified
|
|
398
|
+
if value is None:
|
|
399
|
+
value = key
|
|
400
|
+
|
|
401
|
+
# Use widget-level default if not specified
|
|
402
|
+
if closable is None:
|
|
403
|
+
closable = self._enable_closing
|
|
404
|
+
|
|
405
|
+
# Apply container defaults
|
|
406
|
+
tab_kwargs = {
|
|
407
|
+
'compound': self._compound,
|
|
408
|
+
'orient': self._orient,
|
|
409
|
+
'padding': self._tab_padding,
|
|
410
|
+
'anchor': self._tab_anchor,
|
|
411
|
+
'min_width': self._tab_min_width,
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
# Apply tab_width if specified (not 'stretch')
|
|
415
|
+
if self._tab_width is not None and self._tab_width != 'stretch':
|
|
416
|
+
tab_kwargs['width'] = self._tab_width
|
|
417
|
+
|
|
418
|
+
# Apply accent if specified
|
|
419
|
+
if self._accent is not None:
|
|
420
|
+
tab_kwargs['accent'] = self._accent
|
|
421
|
+
|
|
422
|
+
# Pass signal to TabItem for selection sync
|
|
423
|
+
tab_kwargs['signal'] = self._signal
|
|
424
|
+
|
|
425
|
+
# User kwargs override defaults
|
|
426
|
+
tab_kwargs.update(kwargs)
|
|
427
|
+
|
|
428
|
+
tab = TabItem(
|
|
429
|
+
self._tab_bar,
|
|
430
|
+
text=text,
|
|
431
|
+
icon=icon,
|
|
432
|
+
value=value,
|
|
433
|
+
closable=closable,
|
|
434
|
+
close_command=close_command,
|
|
435
|
+
command=command,
|
|
436
|
+
**tab_kwargs
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
# Determine pack options
|
|
440
|
+
pack_opts = {'fill': 'x'}
|
|
441
|
+
if self._tab_width == 'stretch' and self._orient == 'horizontal':
|
|
442
|
+
pack_opts['expand'] = True
|
|
443
|
+
|
|
444
|
+
if self._scrollable:
|
|
445
|
+
# Tabs live inside the scroll region; the add/overflow buttons are
|
|
446
|
+
# pinned outside it, so just append in order.
|
|
447
|
+
tab.pack(**pack_opts)
|
|
448
|
+
self._add_wheel_binding(tab)
|
|
449
|
+
self._schedule_overflow_update()
|
|
450
|
+
elif self._add_button is not None:
|
|
451
|
+
tab.pack(before=self._add_button, **pack_opts)
|
|
452
|
+
else:
|
|
453
|
+
tab.pack(**pack_opts)
|
|
454
|
+
|
|
455
|
+
# Track tab by key
|
|
456
|
+
self._tabs[key] = tab
|
|
457
|
+
self._tab_order.append(key)
|
|
458
|
+
self._tab_values[key] = value
|
|
459
|
+
self._tab_icons[key] = icon
|
|
460
|
+
|
|
461
|
+
# Auto-select first tab
|
|
462
|
+
if len(self._tabs) == 1:
|
|
463
|
+
self._variable.set(value)
|
|
464
|
+
|
|
465
|
+
self._update_add_button_state()
|
|
466
|
+
return tab
|
|
467
|
+
|
|
468
|
+
def remove(self, key: str) -> None:
|
|
469
|
+
"""Remove a tab by its key.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
key: The key of the tab to remove.
|
|
473
|
+
|
|
474
|
+
Raises:
|
|
475
|
+
KeyError: If no tab with the given key exists.
|
|
476
|
+
"""
|
|
477
|
+
if key not in self._tabs:
|
|
478
|
+
raise NavigationError(f"No tab with key '{key}'")
|
|
479
|
+
|
|
480
|
+
tab = self._tabs.pop(key)
|
|
481
|
+
self._tab_order.remove(key)
|
|
482
|
+
self._tab_values.pop(key, None)
|
|
483
|
+
self._tab_icons.pop(key, None)
|
|
484
|
+
tab.pack_forget()
|
|
485
|
+
tab.destroy()
|
|
486
|
+
self._update_add_button_state()
|
|
487
|
+
self._schedule_overflow_update()
|
|
488
|
+
|
|
489
|
+
def hide(self, key: str) -> None:
|
|
490
|
+
"""Hide a tab without removing it from the registry.
|
|
491
|
+
|
|
492
|
+
The tab disappears from the bar but can be restored with `show()`.
|
|
493
|
+
|
|
494
|
+
Args:
|
|
495
|
+
key: Key of the tab to hide.
|
|
496
|
+
|
|
497
|
+
Raises:
|
|
498
|
+
KeyError: If no tab with the given key exists.
|
|
499
|
+
"""
|
|
500
|
+
if key not in self._tabs:
|
|
501
|
+
raise NavigationError(f"No tab with key '{key}'")
|
|
502
|
+
if key not in self._hidden:
|
|
503
|
+
self._hidden.add(key)
|
|
504
|
+
self._tabs[key].pack_forget()
|
|
505
|
+
self._schedule_overflow_update()
|
|
506
|
+
|
|
507
|
+
def show(self, key: str) -> None:
|
|
508
|
+
"""Restore a previously hidden tab to the bar.
|
|
509
|
+
|
|
510
|
+
Args:
|
|
511
|
+
key: Key of the tab to show.
|
|
512
|
+
|
|
513
|
+
Raises:
|
|
514
|
+
KeyError: If no tab with the given key exists.
|
|
515
|
+
"""
|
|
516
|
+
if key not in self._tabs:
|
|
517
|
+
raise NavigationError(f"No tab with key '{key}'")
|
|
518
|
+
if key in self._hidden:
|
|
519
|
+
self._hidden.discard(key)
|
|
520
|
+
tab = self._tabs[key]
|
|
521
|
+
pack_opts: dict = {'fill': 'x'}
|
|
522
|
+
if self._tab_width == 'stretch' and self._orient == 'horizontal':
|
|
523
|
+
pack_opts['expand'] = True
|
|
524
|
+
# Re-insert at the correct position relative to visible tabs
|
|
525
|
+
next_visible = self._next_visible_tab_after(key)
|
|
526
|
+
if next_visible is not None:
|
|
527
|
+
tab.pack(before=self._tabs[next_visible], **pack_opts)
|
|
528
|
+
elif self._add_button is not None and not self._scrollable:
|
|
529
|
+
tab.pack(before=self._add_button, **pack_opts)
|
|
530
|
+
else:
|
|
531
|
+
tab.pack(**pack_opts)
|
|
532
|
+
if self._scrollable:
|
|
533
|
+
self._add_wheel_binding(tab)
|
|
534
|
+
self._schedule_overflow_update()
|
|
535
|
+
|
|
536
|
+
def _next_visible_tab_after(self, key: str) -> str | None:
|
|
537
|
+
"""Return the key of the next visible tab after *key* in tab order."""
|
|
538
|
+
found = False
|
|
539
|
+
for k in self._tab_order:
|
|
540
|
+
if found and k not in self._hidden:
|
|
541
|
+
return k
|
|
542
|
+
if k == key:
|
|
543
|
+
found = True
|
|
544
|
+
return None
|
|
545
|
+
|
|
546
|
+
# -------------------------------------------------------------------------
|
|
547
|
+
# Overflow scrolling (scrollbar-less strip + trailing overflow menu)
|
|
548
|
+
# -------------------------------------------------------------------------
|
|
549
|
+
|
|
550
|
+
def _on_tabbar_configure(self, _event=None) -> None:
|
|
551
|
+
"""Keep the canvas cross-size matched to the strip; recheck overflow."""
|
|
552
|
+
if not self._scrollable:
|
|
553
|
+
return
|
|
554
|
+
if self._axis == 'x':
|
|
555
|
+
cross = self._tab_bar.winfo_reqheight()
|
|
556
|
+
else:
|
|
557
|
+
cross = self._tab_bar.winfo_reqwidth()
|
|
558
|
+
if cross > 1 and cross != self._canvas_cross:
|
|
559
|
+
self._canvas_cross = cross
|
|
560
|
+
if self._axis == 'x':
|
|
561
|
+
self._scroll.canvas.configure(height=cross)
|
|
562
|
+
else:
|
|
563
|
+
self._scroll.canvas.configure(width=cross)
|
|
564
|
+
self._schedule_overflow_update()
|
|
565
|
+
|
|
566
|
+
def _on_viewport_configure(self, _event=None) -> None:
|
|
567
|
+
"""Re-evaluate overflow when the visible viewport is resized."""
|
|
568
|
+
self._schedule_overflow_update()
|
|
569
|
+
|
|
570
|
+
def _content_size(self) -> int:
|
|
571
|
+
"""Total length of the tab strip along the scroll axis (pixels)."""
|
|
572
|
+
if self._axis == 'x':
|
|
573
|
+
return self._tab_bar.winfo_reqwidth()
|
|
574
|
+
return self._tab_bar.winfo_reqheight()
|
|
575
|
+
|
|
576
|
+
def _viewport_size(self) -> int:
|
|
577
|
+
"""Visible length of the scroll viewport along the scroll axis (pixels)."""
|
|
578
|
+
canvas = self._scroll.canvas
|
|
579
|
+
return canvas.winfo_width() if self._axis == 'x' else canvas.winfo_height()
|
|
580
|
+
|
|
581
|
+
def _content_overflows(self) -> bool:
|
|
582
|
+
"""Whether the strip is longer than the viewport (tabs are off-screen)."""
|
|
583
|
+
if not self._scrollable:
|
|
584
|
+
return False
|
|
585
|
+
view = self._viewport_size()
|
|
586
|
+
return view > 1 and self._content_size() > view + 1
|
|
587
|
+
|
|
588
|
+
def _view_first(self) -> float:
|
|
589
|
+
"""First-visible fraction (0..1) of the scroll axis."""
|
|
590
|
+
view = self._scroll.xview() if self._axis == 'x' else self._scroll.yview()
|
|
591
|
+
return view[0]
|
|
592
|
+
|
|
593
|
+
def _view_moveto(self, fraction: float) -> None:
|
|
594
|
+
"""Scroll so *fraction* (0..1) of the content is at the leading edge."""
|
|
595
|
+
if self._axis == 'x':
|
|
596
|
+
self._scroll.xview_moveto(fraction)
|
|
597
|
+
else:
|
|
598
|
+
self._scroll.yview_moveto(fraction)
|
|
599
|
+
|
|
600
|
+
def _tab_start(self, tab: TabItem) -> int:
|
|
601
|
+
"""Leading-edge offset of *tab* within the strip along the scroll axis."""
|
|
602
|
+
return tab.winfo_x() if self._axis == 'x' else tab.winfo_y()
|
|
603
|
+
|
|
604
|
+
def _tab_extent(self, tab: TabItem) -> int:
|
|
605
|
+
"""Length of *tab* along the scroll axis (pixels)."""
|
|
606
|
+
return tab.winfo_width() if self._axis == 'x' else tab.winfo_height()
|
|
607
|
+
|
|
608
|
+
def _schedule_overflow_update(self) -> None:
|
|
609
|
+
"""Coalesce overflow re-evaluation to the next idle moment."""
|
|
610
|
+
if not self._scrollable or self._overflow_after is not None:
|
|
611
|
+
return
|
|
612
|
+
try:
|
|
613
|
+
self._overflow_after = self.after_idle(self._update_overflow)
|
|
614
|
+
except Exception:
|
|
615
|
+
self._overflow_after = None
|
|
616
|
+
|
|
617
|
+
def _update_overflow(self) -> None:
|
|
618
|
+
"""Show or hide the trailing overflow button to match overflow state."""
|
|
619
|
+
self._overflow_after = None
|
|
620
|
+
if not self._scrollable or not self.winfo_exists():
|
|
621
|
+
return
|
|
622
|
+
overflow = self._content_overflows()
|
|
623
|
+
if overflow and not self._overflow_visible:
|
|
624
|
+
self._show_overflow_button()
|
|
625
|
+
elif not overflow and self._overflow_visible:
|
|
626
|
+
self._hide_overflow_button()
|
|
627
|
+
|
|
628
|
+
def _show_overflow_button(self) -> None:
|
|
629
|
+
"""Create (once) and pin the trailing overflow button.
|
|
630
|
+
|
|
631
|
+
Horizontal uses a compact icon-only chevron; vertical uses a full-width,
|
|
632
|
+
left-aligned `⌄ More` to match the tab rows and the `+ New` button.
|
|
633
|
+
"""
|
|
634
|
+
if self._overflow_button is None:
|
|
635
|
+
if self._axis == 'x':
|
|
636
|
+
self._overflow_button = Button(
|
|
637
|
+
self._strip_row,
|
|
638
|
+
icon={'name': 'chevron-down', 'size': 16},
|
|
639
|
+
icon_only=True,
|
|
640
|
+
variant='ghost',
|
|
641
|
+
command=self._on_overflow_click,
|
|
642
|
+
)
|
|
643
|
+
else:
|
|
644
|
+
self._overflow_button = Button(
|
|
645
|
+
self._strip_row,
|
|
646
|
+
text='More',
|
|
647
|
+
icon='chevron-down',
|
|
648
|
+
variant='ghost',
|
|
649
|
+
anchor='w',
|
|
650
|
+
command=self._on_overflow_click,
|
|
651
|
+
)
|
|
652
|
+
if self._axis == 'x':
|
|
653
|
+
self._overflow_button.pack(side='right')
|
|
654
|
+
else:
|
|
655
|
+
self._overflow_button.pack(side='bottom', fill='x')
|
|
656
|
+
self._overflow_visible = True
|
|
657
|
+
|
|
658
|
+
def _hide_overflow_button(self) -> None:
|
|
659
|
+
"""Unpin the trailing overflow button (kept for reuse)."""
|
|
660
|
+
if self._overflow_button is not None:
|
|
661
|
+
self._overflow_button.pack_forget()
|
|
662
|
+
self._overflow_visible = False
|
|
663
|
+
|
|
664
|
+
def _offscreen_tab_keys(self) -> list[str]:
|
|
665
|
+
"""Keys of visible tabs that are not fully within the viewport."""
|
|
666
|
+
if not self._scrollable:
|
|
667
|
+
return []
|
|
668
|
+
content = self._content_size()
|
|
669
|
+
view = self._viewport_size()
|
|
670
|
+
lead = self._view_first() * content
|
|
671
|
+
trail = lead + view
|
|
672
|
+
keys: list[str] = []
|
|
673
|
+
for key in self._tab_order:
|
|
674
|
+
if key in self._hidden:
|
|
675
|
+
continue
|
|
676
|
+
tab = self._tabs[key]
|
|
677
|
+
start = self._tab_start(tab)
|
|
678
|
+
end = start + self._tab_extent(tab)
|
|
679
|
+
if start < lead - 1 or end > trail + 1:
|
|
680
|
+
keys.append(key)
|
|
681
|
+
return keys
|
|
682
|
+
|
|
683
|
+
def _scroll_into_view(self, key: str, _retries: int = 3) -> None:
|
|
684
|
+
"""Scroll the strip so the tab for *key* is fully visible."""
|
|
685
|
+
if not self._scrollable or key not in self._tabs or key in self._hidden:
|
|
686
|
+
return
|
|
687
|
+
if not self.winfo_exists():
|
|
688
|
+
return
|
|
689
|
+
tab = self._tabs[key]
|
|
690
|
+
# A just-added tab may not be laid out yet (winfo_x/y still 0). Re-defer
|
|
691
|
+
# rather than force a synchronous flush, which would paint a half-settled
|
|
692
|
+
# layout and make the page visibly jump. Only the first tab legitimately
|
|
693
|
+
# sits at offset 0.
|
|
694
|
+
if _retries > 0 and key != self._tab_order[0] and self._tab_start(tab) == 0:
|
|
695
|
+
self.after_idle(lambda: self._scroll_into_view(key, _retries - 1))
|
|
696
|
+
return
|
|
697
|
+
content = self._content_size()
|
|
698
|
+
view = self._viewport_size()
|
|
699
|
+
if view <= 1 or content <= view:
|
|
700
|
+
return
|
|
701
|
+
start = self._tab_start(tab)
|
|
702
|
+
end = start + self._tab_extent(tab)
|
|
703
|
+
lead = self._view_first() * content
|
|
704
|
+
trail = lead + view
|
|
705
|
+
if start < lead:
|
|
706
|
+
new_lead = start
|
|
707
|
+
elif end > trail:
|
|
708
|
+
new_lead = end - view
|
|
709
|
+
else:
|
|
710
|
+
return
|
|
711
|
+
new_lead = max(0, min(new_lead, content - view))
|
|
712
|
+
self._view_moveto(new_lead / content)
|
|
713
|
+
|
|
714
|
+
def _on_selection_scroll(self, value: Any) -> None:
|
|
715
|
+
"""Auto-scroll the newly selected tab into view (signal subscriber)."""
|
|
716
|
+
if not self._scrollable:
|
|
717
|
+
return
|
|
718
|
+
key = self._key_for_value(value)
|
|
719
|
+
if key is not None:
|
|
720
|
+
self.after_idle(lambda: self._scroll_into_view(key))
|
|
721
|
+
|
|
722
|
+
def _key_for_value(self, value: Any) -> str | None:
|
|
723
|
+
"""Reverse-map a selection value back to a tab key."""
|
|
724
|
+
for key, val in self._tab_values.items():
|
|
725
|
+
if val == value:
|
|
726
|
+
return key
|
|
727
|
+
return None
|
|
728
|
+
|
|
729
|
+
def _ensure_overflow_menu(self):
|
|
730
|
+
"""Create (once) the dropdown used by the overflow button."""
|
|
731
|
+
if self._overflow_menu is None:
|
|
732
|
+
from bootstack.widgets._impl.composites.contextmenu import ContextMenu
|
|
733
|
+
if self._axis == 'x':
|
|
734
|
+
anchor, attach = 'ne', 'se'
|
|
735
|
+
else:
|
|
736
|
+
anchor, attach = 'sw', 'nw'
|
|
737
|
+
self._overflow_menu = ContextMenu(
|
|
738
|
+
self._overflow_button,
|
|
739
|
+
target=self._overflow_button,
|
|
740
|
+
trigger='manual',
|
|
741
|
+
anchor=anchor,
|
|
742
|
+
attach=attach,
|
|
743
|
+
offset=(0, 2),
|
|
744
|
+
)
|
|
745
|
+
return self._overflow_menu
|
|
746
|
+
|
|
747
|
+
def _on_overflow_click(self) -> None:
|
|
748
|
+
"""Populate and show the overflow dropdown with the off-screen tabs."""
|
|
749
|
+
keys = self._offscreen_tab_keys()
|
|
750
|
+
menu = self._ensure_overflow_menu()
|
|
751
|
+
menu.items([]) # clear previous contents
|
|
752
|
+
for key in keys:
|
|
753
|
+
tab = self._tabs[key]
|
|
754
|
+
try:
|
|
755
|
+
label = tab.cget('text') or key
|
|
756
|
+
except Exception:
|
|
757
|
+
label = key
|
|
758
|
+
icon = self._tab_icons.get(key)
|
|
759
|
+
kw = {'text': label, 'command': lambda k=key: self._on_overflow_select(k)}
|
|
760
|
+
if icon:
|
|
761
|
+
kw['icon'] = icon
|
|
762
|
+
menu.add_command(**kw)
|
|
763
|
+
menu.show()
|
|
764
|
+
|
|
765
|
+
def _on_overflow_select(self, key: str) -> None:
|
|
766
|
+
"""Select a tab chosen from the overflow menu and reveal it."""
|
|
767
|
+
if key not in self._tab_values:
|
|
768
|
+
return
|
|
769
|
+
self._variable.set(self._tab_values[key])
|
|
770
|
+
# Selection scroll also fires via the signal, but request it explicitly
|
|
771
|
+
# too in case the value did not actually change.
|
|
772
|
+
self.after_idle(lambda: self._scroll_into_view(key))
|
|
773
|
+
|
|
774
|
+
def _setup_wheel_bindings(self) -> None:
|
|
775
|
+
"""Bind wheel scrolling along the strip axis via a private bindtag."""
|
|
776
|
+
winsys = self.tk.call('tk', 'windowingsystem')
|
|
777
|
+
if winsys == 'x11':
|
|
778
|
+
self.bind_class(self._wheel_tag, '<Button-4>', self._on_strip_wheel, add='+')
|
|
779
|
+
self.bind_class(self._wheel_tag, '<Button-5>', self._on_strip_wheel, add='+')
|
|
780
|
+
else:
|
|
781
|
+
self.bind_class(self._wheel_tag, '<MouseWheel>', self._on_strip_wheel, add='+')
|
|
782
|
+
self._add_wheel_binding(self._tab_bar)
|
|
783
|
+
|
|
784
|
+
def _add_wheel_binding(self, widget) -> None:
|
|
785
|
+
"""Add the wheel bindtag to *widget* and its descendants (idempotent)."""
|
|
786
|
+
try:
|
|
787
|
+
tags = list(widget.bindtags())
|
|
788
|
+
if self._wheel_tag not in tags:
|
|
789
|
+
tags.insert(1 if len(tags) > 1 else len(tags), self._wheel_tag)
|
|
790
|
+
widget.bindtags(tuple(tags))
|
|
791
|
+
except Exception:
|
|
792
|
+
pass
|
|
793
|
+
try:
|
|
794
|
+
children = widget.winfo_children()
|
|
795
|
+
except Exception:
|
|
796
|
+
return
|
|
797
|
+
for child in children:
|
|
798
|
+
self._add_wheel_binding(child)
|
|
799
|
+
|
|
800
|
+
def _on_strip_wheel(self, event) -> str | None:
|
|
801
|
+
"""Scroll the strip along its axis in response to the mouse wheel."""
|
|
802
|
+
if not self._scrollable or not self._content_overflows():
|
|
803
|
+
return None
|
|
804
|
+
winsys = self.tk.call('tk', 'windowingsystem')
|
|
805
|
+
if winsys == 'win32':
|
|
806
|
+
notches = -event.delta / 120
|
|
807
|
+
elif winsys == 'aqua':
|
|
808
|
+
notches = -event.delta
|
|
809
|
+
elif getattr(event, 'num', None) == 4:
|
|
810
|
+
notches = -1
|
|
811
|
+
elif getattr(event, 'num', None) == 5:
|
|
812
|
+
notches = 1
|
|
813
|
+
else:
|
|
814
|
+
notches = 0
|
|
815
|
+
if not notches:
|
|
816
|
+
return 'break'
|
|
817
|
+
content = self._content_size()
|
|
818
|
+
view = self._viewport_size()
|
|
819
|
+
if content <= view:
|
|
820
|
+
return 'break'
|
|
821
|
+
step = 60 * notches
|
|
822
|
+
lead = self._view_first() * content
|
|
823
|
+
lead = max(0, min(lead + step, content - view))
|
|
824
|
+
self._view_moveto(lead / content)
|
|
825
|
+
return 'break'
|
|
826
|
+
|
|
827
|
+
def _on_overflow_destroy(self, event) -> None:
|
|
828
|
+
"""Release overflow resources when the tab strip is destroyed."""
|
|
829
|
+
if event.widget is not self:
|
|
830
|
+
return
|
|
831
|
+
if self._overflow_after is not None:
|
|
832
|
+
try:
|
|
833
|
+
self.after_cancel(self._overflow_after)
|
|
834
|
+
except Exception:
|
|
835
|
+
pass
|
|
836
|
+
self._overflow_after = None
|
|
837
|
+
if self._overflow_menu is not None:
|
|
838
|
+
try:
|
|
839
|
+
self._overflow_menu.destroy()
|
|
840
|
+
except Exception:
|
|
841
|
+
pass
|
|
842
|
+
self._overflow_menu = None
|
|
843
|
+
|
|
844
|
+
def item(self, key: str) -> TabItem:
|
|
845
|
+
"""Get a tab by its key.
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
key: The key of the tab to retrieve.
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
The TabItem instance.
|
|
852
|
+
|
|
853
|
+
Raises:
|
|
854
|
+
KeyError: If no tab with the given key exists.
|
|
855
|
+
"""
|
|
856
|
+
if key not in self._tabs:
|
|
857
|
+
raise NavigationError(f"No tab with key '{key}'")
|
|
858
|
+
return self._tabs[key]
|
|
859
|
+
|
|
860
|
+
def items(self) -> tuple[TabItem, ...]:
|
|
861
|
+
"""Get all tab widgets in order.
|
|
862
|
+
|
|
863
|
+
Returns:
|
|
864
|
+
A tuple of all TabItem instances in the order they were added.
|
|
865
|
+
"""
|
|
866
|
+
return tuple(self._tabs[key] for key in self._tab_order)
|
|
867
|
+
|
|
868
|
+
def keys(self) -> tuple[str, ...]:
|
|
869
|
+
"""Get all tab keys in order.
|
|
870
|
+
|
|
871
|
+
Returns:
|
|
872
|
+
A tuple of all tab keys in the order they were added.
|
|
873
|
+
"""
|
|
874
|
+
return tuple(self._tab_order)
|
|
875
|
+
|
|
876
|
+
def configure_item(self, key: str, option: str = None, **kwargs: Any):
|
|
877
|
+
"""Configure a specific tab by its key.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
key: The key of the tab to configure.
|
|
881
|
+
option: If provided, return the value of this option.
|
|
882
|
+
**kwargs: Configuration options to apply to the tab.
|
|
883
|
+
|
|
884
|
+
Returns:
|
|
885
|
+
If option is provided, returns the value of that option.
|
|
886
|
+
"""
|
|
887
|
+
tab = self.item(key)
|
|
888
|
+
if option is not None:
|
|
889
|
+
return tab.cget(option)
|
|
890
|
+
tab.configure(**kwargs)
|
|
891
|
+
|
|
892
|
+
@configure_delegate('orient')
|
|
893
|
+
def _delegate_orient(self, value=None):
|
|
894
|
+
"""Get orientation (read-only after creation)."""
|
|
895
|
+
if value is None:
|
|
896
|
+
return self._orient
|
|
897
|
+
raise ValueError("orient cannot be changed after creation")
|
|
898
|
+
|
|
899
|
+
@configure_delegate('variant')
|
|
900
|
+
def _delegate_variant(self, value=None):
|
|
901
|
+
"""Get variant (read-only after creation)."""
|
|
902
|
+
if value is None:
|
|
903
|
+
return self._variant
|
|
904
|
+
raise ValueError("variant cannot be changed after creation")
|
|
905
|
+
|
|
906
|
+
@property
|
|
907
|
+
def variable(self) -> tk.Variable:
|
|
908
|
+
"""Get the underlying tk.Variable for tab selection.
|
|
909
|
+
|
|
910
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
911
|
+
"""
|
|
912
|
+
return self._variable
|
|
913
|
+
|
|
914
|
+
@property
|
|
915
|
+
def signal(self) -> 'Signal[Any]':
|
|
916
|
+
"""Get the Signal for tab selection."""
|
|
917
|
+
return self._signal
|
|
918
|
+
|
|
919
|
+
@configure_delegate('show_divider')
|
|
920
|
+
def _delegate_show_divider(self, value=None):
|
|
921
|
+
"""Get or set whether the divider is shown."""
|
|
922
|
+
if value is None:
|
|
923
|
+
return self._show_divider
|
|
924
|
+
if value != self._show_divider:
|
|
925
|
+
self._show_divider = value
|
|
926
|
+
if value:
|
|
927
|
+
self._create_divider()
|
|
928
|
+
else:
|
|
929
|
+
self._destroy_divider()
|
|
930
|
+
|
|
931
|
+
def get(self) -> str:
|
|
932
|
+
"""Return the currently selected tab value."""
|
|
933
|
+
return self._variable.get()
|
|
934
|
+
|
|
935
|
+
def set(self, value: str) -> None:
|
|
936
|
+
"""Set the selected tab value."""
|
|
937
|
+
self._variable.set(value)
|
|
938
|
+
|
|
939
|
+
@property
|
|
940
|
+
def value(self) -> str:
|
|
941
|
+
"""Get or set the selected tab value."""
|
|
942
|
+
return self.get()
|
|
943
|
+
|
|
944
|
+
@value.setter
|
|
945
|
+
def value(self, value: str) -> None:
|
|
946
|
+
self.set(value)
|
|
947
|
+
|
|
948
|
+
@configure_delegate('value')
|
|
949
|
+
def _delegate_value(self, value=None):
|
|
950
|
+
"""Get or set the value via configure."""
|
|
951
|
+
if value is None:
|
|
952
|
+
return self.get()
|
|
953
|
+
self.set(value)
|
|
954
|
+
|
|
955
|
+
def on_tab_changed(self, callback: Callable) -> Any:
|
|
956
|
+
"""Register a callback for tab selection changes.
|
|
957
|
+
|
|
958
|
+
Args:
|
|
959
|
+
callback: Called with the new selected tab value (str) when the
|
|
960
|
+
selection changes.
|
|
961
|
+
|
|
962
|
+
Returns:
|
|
963
|
+
Subscription ID — pass to `off_tab_changed()` to unsubscribe.
|
|
964
|
+
"""
|
|
965
|
+
return self._signal.subscribe(callback)
|
|
966
|
+
|
|
967
|
+
def off_tab_changed(self, bind_id: Any) -> None:
|
|
968
|
+
"""Cancel a subscription created by `on_tab_changed()`.
|
|
969
|
+
|
|
970
|
+
Args:
|
|
971
|
+
bind_id: The handle returned by `on_tab_changed()`.
|
|
972
|
+
"""
|
|
973
|
+
if bind_id is not None:
|
|
974
|
+
bind_id.cancel()
|