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,919 @@
|
|
|
1
|
+
"""SideNav widget - a sidebar navigation container."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from tkinter import Variable
|
|
6
|
+
from typing import Any, Literal, TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from typing_extensions import TypedDict, Unpack
|
|
9
|
+
|
|
10
|
+
from bootstack.widgets.primitives.frame import Frame
|
|
11
|
+
from bootstack.widgets.primitives.gridframe import GridFrame
|
|
12
|
+
from bootstack.widgets.primitives.button import Button
|
|
13
|
+
from bootstack.widgets.primitives.label import Label
|
|
14
|
+
from bootstack.widgets.primitives.separator import Separator
|
|
15
|
+
from bootstack.widgets.composites.scrollview import ScrollView
|
|
16
|
+
from bootstack.widgets.composites.toolbar import Toolbar
|
|
17
|
+
from bootstack.widgets.composites.sidenav.item import SideNavItem
|
|
18
|
+
from bootstack.widgets.composites.sidenav.group import SideNavGroup
|
|
19
|
+
from bootstack.widgets.composites.sidenav.header import SideNavHeader
|
|
20
|
+
from bootstack.widgets.composites.sidenav.separator import SideNavSeparator
|
|
21
|
+
from bootstack.widgets.mixins import configure_delegate
|
|
22
|
+
from bootstack.widgets.types import Master
|
|
23
|
+
from bootstack.core.signals import Signal
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
DisplayMode = Literal['expanded', 'compact', 'minimal']
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SideNavKwargs(TypedDict, total=False):
|
|
30
|
+
title: str
|
|
31
|
+
show_header: bool
|
|
32
|
+
show_back_button: bool
|
|
33
|
+
collapsible: bool
|
|
34
|
+
display_mode: DisplayMode
|
|
35
|
+
is_pane_open: bool
|
|
36
|
+
pane_width: int
|
|
37
|
+
signal: Any
|
|
38
|
+
variable: Variable
|
|
39
|
+
accent: str
|
|
40
|
+
# Frame options
|
|
41
|
+
padding: Any
|
|
42
|
+
width: int
|
|
43
|
+
height: int
|
|
44
|
+
surface: str
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class SideNav(Frame):
|
|
48
|
+
"""A sidebar navigation container with header, scrollable items, and footer.
|
|
49
|
+
|
|
50
|
+
SideNav provides a complete navigation solution with:
|
|
51
|
+
- Pane header with optional title and menu button
|
|
52
|
+
- Scrollable navigation items area
|
|
53
|
+
- Groups for organizing related items (expand/collapse in expanded mode,
|
|
54
|
+
popup flyout in compact mode)
|
|
55
|
+
- Footer section for settings and other fixed items
|
|
56
|
+
- Display modes: expanded (full), compact (icons only), minimal (hidden)
|
|
57
|
+
|
|
58
|
+
The pane manages a shared selection variable/signal that all items use
|
|
59
|
+
for radio-group behavior.
|
|
60
|
+
|
|
61
|
+
!!! note "Events"
|
|
62
|
+
- `<<PaneToggled>>`: Fired when pane is opened/closed.
|
|
63
|
+
`event.data = {'is_open': bool}`
|
|
64
|
+
- `<<DisplayModeChanged>>`: Fired when display mode changes.
|
|
65
|
+
`event.data = {'mode': str}`
|
|
66
|
+
- `<<SelectionChanged>>`: Fired when selected item changes.
|
|
67
|
+
`event.data = {'key': str}`
|
|
68
|
+
- `<<BackRequested>>`: Fired when back button is clicked.
|
|
69
|
+
|
|
70
|
+
Example:
|
|
71
|
+
```python
|
|
72
|
+
nav = SideNav(root, title='My App')
|
|
73
|
+
|
|
74
|
+
# Add root-level items
|
|
75
|
+
nav.add_item('home', text='Home', icon='house')
|
|
76
|
+
nav.add_item('docs', text='Documents', icon='file-earmark-text')
|
|
77
|
+
|
|
78
|
+
# Add a group with items
|
|
79
|
+
nav.add_group('files', text='Files', icon='folder')
|
|
80
|
+
nav.add_item('local', text='Local', icon='hdd', group='files')
|
|
81
|
+
nav.add_item('cloud', text='Cloud', icon='cloud', group='files')
|
|
82
|
+
|
|
83
|
+
# Add section header
|
|
84
|
+
nav.add_header('Favorites')
|
|
85
|
+
nav.add_item('photos', text='Photos', icon='image')
|
|
86
|
+
|
|
87
|
+
# Add footer item
|
|
88
|
+
nav.add_footer_item('settings', text='Settings', icon='gear')
|
|
89
|
+
|
|
90
|
+
# Bind to selection
|
|
91
|
+
nav.on_selection_changed(lambda e: print(f"Selected: {e.data['key']}"))
|
|
92
|
+
```
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
# Default pane widths
|
|
96
|
+
PANE_WIDTH_EXPANDED = 280
|
|
97
|
+
PANE_WIDTH_COMPACT = 52
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self,
|
|
101
|
+
master: Master = None,
|
|
102
|
+
title: str = '',
|
|
103
|
+
show_header: bool = True,
|
|
104
|
+
show_back_button: bool = False,
|
|
105
|
+
collapsible: bool = True,
|
|
106
|
+
display_mode: DisplayMode = 'expanded',
|
|
107
|
+
is_pane_open: bool = True,
|
|
108
|
+
pane_width: int = None,
|
|
109
|
+
signal: 'Signal[str]' = None,
|
|
110
|
+
variable: Variable = None,
|
|
111
|
+
accent: str = 'primary',
|
|
112
|
+
**kwargs: Unpack[SideNavKwargs]
|
|
113
|
+
):
|
|
114
|
+
"""Initialize a SideNav.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
master (Master | None): Parent widget.
|
|
118
|
+
title (str): Title displayed in the pane header.
|
|
119
|
+
show_header (bool): Show internal header with toolbar. Default True.
|
|
120
|
+
Set to False when using an external toolbar.
|
|
121
|
+
show_back_button (bool): Show back button in header. Default False.
|
|
122
|
+
collapsible (bool): Allow pane to collapse. Shows hamburger menu. Default True.
|
|
123
|
+
display_mode (DisplayMode): Initial display mode. Default 'expanded'.
|
|
124
|
+
is_pane_open (bool): Initial pane state. Default True.
|
|
125
|
+
pane_width (int | None): Custom pane width. Uses default based on mode.
|
|
126
|
+
signal (Signal | None): Reactive signal for selection state.
|
|
127
|
+
variable (Variable | None): Tk variable for selection state.
|
|
128
|
+
accent (str): Accent color for selection indicators. Default 'primary'.
|
|
129
|
+
**kwargs: Additional arguments passed to Frame.
|
|
130
|
+
"""
|
|
131
|
+
super().__init__(master, **kwargs)
|
|
132
|
+
|
|
133
|
+
self._accent = accent
|
|
134
|
+
|
|
135
|
+
self._title = title
|
|
136
|
+
self._show_header = show_header
|
|
137
|
+
self._show_back_button = show_back_button
|
|
138
|
+
self._collapsible = collapsible
|
|
139
|
+
self._display_mode = display_mode
|
|
140
|
+
self._is_pane_open = is_pane_open
|
|
141
|
+
self._pane_width = pane_width
|
|
142
|
+
|
|
143
|
+
# Selection state - create signal if neither provided
|
|
144
|
+
if signal is not None:
|
|
145
|
+
self._signal = signal
|
|
146
|
+
elif variable is not None:
|
|
147
|
+
self._signal = None
|
|
148
|
+
self._variable = variable
|
|
149
|
+
else:
|
|
150
|
+
# Create internal signal for selection
|
|
151
|
+
self._signal = Signal('')
|
|
152
|
+
|
|
153
|
+
# Get the variable for items (from signal or direct)
|
|
154
|
+
self._selection_var = self._signal._var if self._signal else variable
|
|
155
|
+
|
|
156
|
+
# Track variable changes for efficient selection updates
|
|
157
|
+
self._prev_selection: str | None = None # Track previous selection for efficient updates
|
|
158
|
+
if self._selection_var:
|
|
159
|
+
self._selection_var.trace_add('write', self._on_selection_changed)
|
|
160
|
+
|
|
161
|
+
# Item and group tracking
|
|
162
|
+
self._items: dict[str, SideNavItem] = {} # All items by key
|
|
163
|
+
self._item_to_group: dict[str, str] = {} # item_key -> group_key lookup
|
|
164
|
+
self._groups: dict[str, SideNavGroup] = {} # Groups by key
|
|
165
|
+
self._footer_items: dict[str, SideNavItem] = {}
|
|
166
|
+
self._headers: list[SideNavHeader] = []
|
|
167
|
+
self._separators: list[SideNavSeparator] = []
|
|
168
|
+
|
|
169
|
+
# Track all content widgets in order for proper re-packing
|
|
170
|
+
self._content_widgets: list = []
|
|
171
|
+
self._footer_order: list[str] = []
|
|
172
|
+
|
|
173
|
+
# Widget references
|
|
174
|
+
self._pane_frame: Frame | None = None
|
|
175
|
+
self._header_frame: Frame | None = None
|
|
176
|
+
self._content_scroll: ScrollView | None = None
|
|
177
|
+
self._content_frame: Frame | None = None
|
|
178
|
+
self._footer_frame: Frame | None = None
|
|
179
|
+
self._menu_button: Button | None = None
|
|
180
|
+
self._menu_separator: Separator | None = None
|
|
181
|
+
self._back_button: Button | None = None
|
|
182
|
+
self._title_label: Label | None = None
|
|
183
|
+
|
|
184
|
+
# Build the widget
|
|
185
|
+
self._build_widget()
|
|
186
|
+
|
|
187
|
+
# Initialize previous selection tracker from current value
|
|
188
|
+
if self._selection_var:
|
|
189
|
+
self._prev_selection = self._selection_var.get() or None
|
|
190
|
+
|
|
191
|
+
def _build_widget(self):
|
|
192
|
+
"""Build the internal widget structure."""
|
|
193
|
+
# Pane container - uses 'chrome' surface for UI chrome
|
|
194
|
+
pane_width = self._pane_width or self.PANE_WIDTH_EXPANDED
|
|
195
|
+
# padding=(left, top, right, bottom): right=0 so the scrollbar lane in
|
|
196
|
+
# the ScrollView runs flush to the pane edge with no trailing gap.
|
|
197
|
+
self._pane_frame = Frame(self, width=pane_width, padding=(4, 4, 0, 4), surface='chrome')
|
|
198
|
+
self._pane_frame.pack(side='left', fill='y')
|
|
199
|
+
self._pane_frame.pack_propagate(False) # Fixed width
|
|
200
|
+
|
|
201
|
+
# Hamburger menu button at top (if collapsible)
|
|
202
|
+
if self._collapsible:
|
|
203
|
+
self._menu_button = Button(
|
|
204
|
+
self._pane_frame,
|
|
205
|
+
icon='list',
|
|
206
|
+
icon_only=True,
|
|
207
|
+
variant='ghost',
|
|
208
|
+
command=self.toggle_pane,
|
|
209
|
+
padding=(8, 6),
|
|
210
|
+
)
|
|
211
|
+
# Pack based on initial mode - compact stretches, expanded doesn't
|
|
212
|
+
if self._display_mode == 'compact':
|
|
213
|
+
self._menu_button.pack(fill='x')
|
|
214
|
+
else:
|
|
215
|
+
self._menu_button.pack(anchor='w')
|
|
216
|
+
|
|
217
|
+
# Separator after menu button
|
|
218
|
+
self._menu_separator = Separator(self._pane_frame, orient='horizontal')
|
|
219
|
+
self._menu_separator.pack(fill='x', pady=4)
|
|
220
|
+
|
|
221
|
+
# Header section (optional - can be disabled when using external toolbar)
|
|
222
|
+
self._toolbar = None
|
|
223
|
+
if self._show_header:
|
|
224
|
+
self._build_header()
|
|
225
|
+
|
|
226
|
+
# Scrollable content area (vertical only, scrollbar on hover)
|
|
227
|
+
self._content_scroll = ScrollView(
|
|
228
|
+
self._pane_frame,
|
|
229
|
+
scroll_direction='vertical',
|
|
230
|
+
scrollbar_visibility='hover',
|
|
231
|
+
)
|
|
232
|
+
self._content_scroll.pack(fill='both', expand=True)
|
|
233
|
+
self._content_frame = GridFrame(
|
|
234
|
+
self._content_scroll.canvas,
|
|
235
|
+
columns=1,
|
|
236
|
+
gap=(0, 2),
|
|
237
|
+
sticky_items='ew',
|
|
238
|
+
padding=(4, 0),
|
|
239
|
+
)
|
|
240
|
+
self._content_scroll.add(self._content_frame)
|
|
241
|
+
|
|
242
|
+
# Stretch content to fill width when canvas resizes (debounced)
|
|
243
|
+
self._resize_after_id = None
|
|
244
|
+
|
|
245
|
+
def on_canvas_resize(event):
|
|
246
|
+
# Debounce resize events to avoid excessive layout recalculations
|
|
247
|
+
if self._resize_after_id:
|
|
248
|
+
self.after_cancel(self._resize_after_id)
|
|
249
|
+
self._resize_after_id = self.after(16, lambda: self._apply_canvas_width(event.width))
|
|
250
|
+
|
|
251
|
+
self._content_scroll.canvas.bind('<Configure>', on_canvas_resize, add='+')
|
|
252
|
+
|
|
253
|
+
# Footer section
|
|
254
|
+
self._footer_frame = Frame(self._pane_frame, padding=(4, 0))
|
|
255
|
+
self._footer_frame.pack(side='bottom', fill='x')
|
|
256
|
+
|
|
257
|
+
# Separator before footer (shown when footer has items)
|
|
258
|
+
self._footer_separator = Separator(self._pane_frame, orient='horizontal')
|
|
259
|
+
|
|
260
|
+
# Apply initial display mode
|
|
261
|
+
self._apply_display_mode()
|
|
262
|
+
|
|
263
|
+
def _apply_canvas_width(self, width: int):
|
|
264
|
+
"""Apply the canvas width after debounce delay."""
|
|
265
|
+
self._resize_after_id = None
|
|
266
|
+
if self._content_scroll and self._content_scroll.winfo_exists():
|
|
267
|
+
self._content_scroll.canvas.itemconfigure(
|
|
268
|
+
self._content_scroll._window_id,
|
|
269
|
+
width=width
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def _build_header(self):
|
|
273
|
+
"""Build the pane header using Toolbar."""
|
|
274
|
+
# Create toolbar for header
|
|
275
|
+
self._toolbar = Toolbar(self._pane_frame, padding=(4, 4))
|
|
276
|
+
self._toolbar.pack(fill='x')
|
|
277
|
+
|
|
278
|
+
# Back button (optional)
|
|
279
|
+
if self._show_back_button:
|
|
280
|
+
self._back_button = self._toolbar.add_button(
|
|
281
|
+
icon={'name': 'arrow-left', 'size': 16},
|
|
282
|
+
command=self._on_back_clicked,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Title
|
|
286
|
+
if self._title:
|
|
287
|
+
self._title_label = self._toolbar.add_label(
|
|
288
|
+
text=self._title,
|
|
289
|
+
font='heading-md',
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def _apply_display_mode(self):
|
|
293
|
+
"""Apply the current display mode to the pane."""
|
|
294
|
+
is_compact = self._display_mode == 'compact'
|
|
295
|
+
|
|
296
|
+
# Update pane width and visibility
|
|
297
|
+
if self._display_mode == 'expanded':
|
|
298
|
+
width = self._pane_width or self.PANE_WIDTH_EXPANDED
|
|
299
|
+
self._pane_frame.configure(width=width, padding=(4, 4, 0, 4))
|
|
300
|
+
if self._is_pane_open:
|
|
301
|
+
self._pane_frame.pack(side='left', fill='y')
|
|
302
|
+
else:
|
|
303
|
+
self._pane_frame.pack_forget()
|
|
304
|
+
elif self._display_mode == 'compact':
|
|
305
|
+
self._pane_frame.configure(width=self.PANE_WIDTH_COMPACT, padding=(2, 2, 0, 2))
|
|
306
|
+
self._pane_frame.pack(side='left', fill='y')
|
|
307
|
+
elif self._display_mode == 'minimal':
|
|
308
|
+
if self._is_pane_open:
|
|
309
|
+
width = self._pane_width or self.PANE_WIDTH_EXPANDED
|
|
310
|
+
self._pane_frame.configure(width=width)
|
|
311
|
+
self._pane_frame.pack(side='left', fill='y')
|
|
312
|
+
else:
|
|
313
|
+
self._pane_frame.pack_forget()
|
|
314
|
+
|
|
315
|
+
# Update menu button pack options based on mode
|
|
316
|
+
if self._collapsible and self._menu_button:
|
|
317
|
+
if is_compact:
|
|
318
|
+
self._menu_button.pack_configure(fill='x', anchor='center')
|
|
319
|
+
else:
|
|
320
|
+
self._menu_button.pack_configure(fill='none', anchor='w')
|
|
321
|
+
|
|
322
|
+
# Hide section headers in compact mode
|
|
323
|
+
for header in self._headers:
|
|
324
|
+
if is_compact:
|
|
325
|
+
header.grid_remove()
|
|
326
|
+
else:
|
|
327
|
+
header.grid()
|
|
328
|
+
|
|
329
|
+
# Hide only the title in compact mode (keep back and menu buttons visible)
|
|
330
|
+
if self._title_label:
|
|
331
|
+
if is_compact:
|
|
332
|
+
self._title_label.pack_forget()
|
|
333
|
+
else:
|
|
334
|
+
self._title_label.pack(side='left', fill='x', expand=True)
|
|
335
|
+
|
|
336
|
+
# In compact mode the pane is icon-only (52 px) and content never
|
|
337
|
+
# overflows vertically, so reserving a scrollbar gutter wastes ~25 % of
|
|
338
|
+
# the pane width. Switch the ScrollView to 'never' visibility to
|
|
339
|
+
# release the gutter; restore 'hover' (with stable gutter) when the
|
|
340
|
+
# pane is expanded again.
|
|
341
|
+
if self._content_scroll:
|
|
342
|
+
if is_compact:
|
|
343
|
+
self._content_scroll.configure(scrollbar_visibility='never')
|
|
344
|
+
else:
|
|
345
|
+
self._content_scroll.configure(scrollbar_visibility='hover')
|
|
346
|
+
|
|
347
|
+
# Set compact mode on all items and groups
|
|
348
|
+
for item in self._items.values():
|
|
349
|
+
# Only set compact on root items (not in groups)
|
|
350
|
+
if not self._get_item_group(item.key):
|
|
351
|
+
item.set_compact(is_compact)
|
|
352
|
+
|
|
353
|
+
for group in self._groups.values():
|
|
354
|
+
group.set_compact(is_compact)
|
|
355
|
+
|
|
356
|
+
for item in self._footer_items.values():
|
|
357
|
+
item.set_compact(is_compact)
|
|
358
|
+
|
|
359
|
+
def _get_item_group(self, key: str) -> str | None:
|
|
360
|
+
"""Get the group key for an item, or None if at root.
|
|
361
|
+
|
|
362
|
+
Uses O(1) lookup via _item_to_group cache.
|
|
363
|
+
"""
|
|
364
|
+
return self._item_to_group.get(key)
|
|
365
|
+
|
|
366
|
+
def _on_selection_changed(self, *args):
|
|
367
|
+
"""Handle selection variable changes.
|
|
368
|
+
|
|
369
|
+
This centralizes selection state updates for better performance.
|
|
370
|
+
Instead of each item tracing the variable (O(n) callbacks), we only
|
|
371
|
+
update the previously-selected and newly-selected items (O(1)).
|
|
372
|
+
"""
|
|
373
|
+
if not self._selection_var:
|
|
374
|
+
return
|
|
375
|
+
|
|
376
|
+
new_key = self._selection_var.get()
|
|
377
|
+
old_key = self._prev_selection
|
|
378
|
+
|
|
379
|
+
# Skip if selection hasn't actually changed
|
|
380
|
+
if new_key == old_key:
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
# Update the previously selected item (deselect)
|
|
384
|
+
if old_key:
|
|
385
|
+
old_group = self._get_item_group(old_key)
|
|
386
|
+
if old_key in self._items:
|
|
387
|
+
self._items[old_key].set_selected(False)
|
|
388
|
+
elif old_key in self._footer_items:
|
|
389
|
+
self._footer_items[old_key].set_selected(False)
|
|
390
|
+
|
|
391
|
+
# Update old group's selection state
|
|
392
|
+
if old_group and old_group in self._groups:
|
|
393
|
+
self._groups[old_group].set_child_selected(False)
|
|
394
|
+
|
|
395
|
+
# Update the newly selected item (select)
|
|
396
|
+
new_group = None
|
|
397
|
+
if new_key:
|
|
398
|
+
new_group = self._get_item_group(new_key)
|
|
399
|
+
if new_key in self._items:
|
|
400
|
+
self._items[new_key].set_selected(True)
|
|
401
|
+
elif new_key in self._footer_items:
|
|
402
|
+
self._footer_items[new_key].set_selected(True)
|
|
403
|
+
|
|
404
|
+
# Update new group's selection state
|
|
405
|
+
if new_group and new_group in self._groups:
|
|
406
|
+
self._groups[new_group].set_child_selected(True)
|
|
407
|
+
|
|
408
|
+
# Track current selection for next change
|
|
409
|
+
self._prev_selection = new_key
|
|
410
|
+
|
|
411
|
+
# Fire selection changed event
|
|
412
|
+
self.event_generate('<<SelectionChanged>>', data={'key': new_key})
|
|
413
|
+
|
|
414
|
+
def _on_back_clicked(self):
|
|
415
|
+
"""Handle back button click."""
|
|
416
|
+
self.event_generate('<<BackRequested>>')
|
|
417
|
+
|
|
418
|
+
# --- Public API: Groups ---
|
|
419
|
+
|
|
420
|
+
def add_group(
|
|
421
|
+
self,
|
|
422
|
+
key: str,
|
|
423
|
+
text: str = '',
|
|
424
|
+
icon: str | dict = None,
|
|
425
|
+
is_expanded: bool = False,
|
|
426
|
+
**kwargs
|
|
427
|
+
) -> SideNavGroup:
|
|
428
|
+
"""Add a navigation group to the pane.
|
|
429
|
+
|
|
430
|
+
Groups contain related items and can be expanded/collapsed in expanded
|
|
431
|
+
mode. In compact mode, clicking a group shows a popup with its items.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
key (str): Unique identifier for the group.
|
|
435
|
+
text (str): Display text.
|
|
436
|
+
icon (str | dict | None): Icon name or configuration.
|
|
437
|
+
is_expanded (bool): Initial expansion state. Default False.
|
|
438
|
+
**kwargs: Additional arguments passed to SideNavGroup.
|
|
439
|
+
|
|
440
|
+
Returns:
|
|
441
|
+
SideNavGroup: The created group.
|
|
442
|
+
|
|
443
|
+
Raises:
|
|
444
|
+
ValueError: If a group or item with the given key already exists.
|
|
445
|
+
"""
|
|
446
|
+
if key in self._groups or key in self._items or key in self._footer_items:
|
|
447
|
+
raise ValueError(f"Key '{key}' already exists")
|
|
448
|
+
|
|
449
|
+
# Use view's accent as default, allow override via kwargs
|
|
450
|
+
group_kwargs = {'accent': self._accent, **kwargs}
|
|
451
|
+
|
|
452
|
+
group = SideNavGroup(
|
|
453
|
+
self._content_frame,
|
|
454
|
+
key=key,
|
|
455
|
+
text=text,
|
|
456
|
+
icon=icon,
|
|
457
|
+
variable=self._selection_var,
|
|
458
|
+
is_expanded=is_expanded,
|
|
459
|
+
**group_kwargs
|
|
460
|
+
)
|
|
461
|
+
group.grid()
|
|
462
|
+
self._groups[key] = group
|
|
463
|
+
self._content_widgets.append(group)
|
|
464
|
+
|
|
465
|
+
# Apply current display mode
|
|
466
|
+
if self._display_mode == 'compact':
|
|
467
|
+
group.set_compact(True)
|
|
468
|
+
|
|
469
|
+
return group
|
|
470
|
+
|
|
471
|
+
# --- Public API: Items ---
|
|
472
|
+
|
|
473
|
+
def add_item(
|
|
474
|
+
self,
|
|
475
|
+
key: str,
|
|
476
|
+
text: str = '',
|
|
477
|
+
icon: str | dict = None,
|
|
478
|
+
group: str = None,
|
|
479
|
+
**kwargs
|
|
480
|
+
) -> SideNavItem:
|
|
481
|
+
"""Add a navigation item to the pane.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
key (str): Unique identifier for the item.
|
|
485
|
+
text (str): Display text.
|
|
486
|
+
icon (str | dict | None): Icon name or configuration.
|
|
487
|
+
group (str | None): Key of the group to add this item to.
|
|
488
|
+
If None, item is added at root level.
|
|
489
|
+
**kwargs: Additional arguments passed to SideNavItem.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
SideNavItem: The created item.
|
|
493
|
+
|
|
494
|
+
Raises:
|
|
495
|
+
ValueError: If an item with the given key already exists.
|
|
496
|
+
ValueError: If the specified group does not exist.
|
|
497
|
+
"""
|
|
498
|
+
if key in self._items or key in self._groups or key in self._footer_items:
|
|
499
|
+
raise ValueError(f"Key '{key}' already exists")
|
|
500
|
+
|
|
501
|
+
# Use view's accent as default, allow override via kwargs
|
|
502
|
+
item_kwargs = {'accent': self._accent, **kwargs}
|
|
503
|
+
|
|
504
|
+
if group is not None:
|
|
505
|
+
# Add to a group
|
|
506
|
+
if group not in self._groups:
|
|
507
|
+
raise ValueError(f"Group '{group}' does not exist")
|
|
508
|
+
|
|
509
|
+
target_group = self._groups[group]
|
|
510
|
+
item = SideNavItem(
|
|
511
|
+
target_group.content_frame,
|
|
512
|
+
key=key,
|
|
513
|
+
text=text,
|
|
514
|
+
icon=icon,
|
|
515
|
+
variable=self._selection_var,
|
|
516
|
+
indent_level=0,
|
|
517
|
+
**item_kwargs
|
|
518
|
+
)
|
|
519
|
+
item.grid()
|
|
520
|
+
|
|
521
|
+
# Register with group and update lookup cache
|
|
522
|
+
target_group._add_item(item)
|
|
523
|
+
self._item_to_group[key] = group
|
|
524
|
+
else:
|
|
525
|
+
# Add at root level
|
|
526
|
+
item = SideNavItem(
|
|
527
|
+
self._content_frame,
|
|
528
|
+
key=key,
|
|
529
|
+
text=text,
|
|
530
|
+
icon=icon,
|
|
531
|
+
variable=self._selection_var,
|
|
532
|
+
**item_kwargs
|
|
533
|
+
)
|
|
534
|
+
item.grid()
|
|
535
|
+
self._content_widgets.append(item)
|
|
536
|
+
|
|
537
|
+
# Apply current display mode
|
|
538
|
+
if self._display_mode == 'compact':
|
|
539
|
+
item.set_compact(True)
|
|
540
|
+
|
|
541
|
+
self._items[key] = item
|
|
542
|
+
return item
|
|
543
|
+
|
|
544
|
+
def add_header(self, text: str, **kwargs) -> SideNavHeader:
|
|
545
|
+
"""Add a section header to the pane.
|
|
546
|
+
|
|
547
|
+
Headers are hidden in compact display mode.
|
|
548
|
+
|
|
549
|
+
Args:
|
|
550
|
+
text (str): Header text.
|
|
551
|
+
**kwargs: Additional arguments passed to SideNavHeader.
|
|
552
|
+
|
|
553
|
+
Returns:
|
|
554
|
+
SideNavHeader: The created header.
|
|
555
|
+
"""
|
|
556
|
+
header = SideNavHeader(self._content_frame, text=text, **kwargs)
|
|
557
|
+
self._headers.append(header)
|
|
558
|
+
header.grid()
|
|
559
|
+
self._content_widgets.append(header)
|
|
560
|
+
|
|
561
|
+
# Hide if in compact mode
|
|
562
|
+
if self._display_mode == 'compact':
|
|
563
|
+
header.grid_remove()
|
|
564
|
+
|
|
565
|
+
return header
|
|
566
|
+
|
|
567
|
+
def add_separator(self, **kwargs) -> SideNavSeparator:
|
|
568
|
+
"""Add a separator to the pane.
|
|
569
|
+
|
|
570
|
+
Args:
|
|
571
|
+
**kwargs: Additional arguments passed to SideNavSeparator.
|
|
572
|
+
|
|
573
|
+
Returns:
|
|
574
|
+
SideNavSeparator: The created separator.
|
|
575
|
+
"""
|
|
576
|
+
sep = SideNavSeparator(self._content_frame, **kwargs)
|
|
577
|
+
sep.grid()
|
|
578
|
+
self._content_widgets.append(sep)
|
|
579
|
+
self._separators.append(sep)
|
|
580
|
+
return sep
|
|
581
|
+
|
|
582
|
+
def add_footer_item(
|
|
583
|
+
self,
|
|
584
|
+
key: str,
|
|
585
|
+
text: str = '',
|
|
586
|
+
icon: str | dict = None,
|
|
587
|
+
**kwargs
|
|
588
|
+
) -> SideNavItem:
|
|
589
|
+
"""Add a navigation item to the footer section.
|
|
590
|
+
|
|
591
|
+
Args:
|
|
592
|
+
key (str): Unique identifier for the item.
|
|
593
|
+
text (str): Display text.
|
|
594
|
+
icon (str | dict | None): Icon name or configuration.
|
|
595
|
+
**kwargs: Additional arguments passed to SideNavItem.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
SideNavItem: The created item.
|
|
599
|
+
|
|
600
|
+
Raises:
|
|
601
|
+
ValueError: If an item with the given key already exists.
|
|
602
|
+
"""
|
|
603
|
+
if key in self._items or key in self._groups or key in self._footer_items:
|
|
604
|
+
raise ValueError(f"Key '{key}' already exists")
|
|
605
|
+
|
|
606
|
+
# Show footer separator if this is the first footer item
|
|
607
|
+
if not self._footer_items:
|
|
608
|
+
self._footer_separator.pack(fill='x', pady=4)
|
|
609
|
+
|
|
610
|
+
# Use view's accent as default, allow override via kwargs
|
|
611
|
+
item_kwargs = {'accent': self._accent, **kwargs}
|
|
612
|
+
|
|
613
|
+
item = SideNavItem(
|
|
614
|
+
self._footer_frame,
|
|
615
|
+
key=key,
|
|
616
|
+
text=text,
|
|
617
|
+
icon=icon,
|
|
618
|
+
variable=self._selection_var,
|
|
619
|
+
**item_kwargs
|
|
620
|
+
)
|
|
621
|
+
item.pack(fill='x')
|
|
622
|
+
|
|
623
|
+
self._footer_items[key] = item
|
|
624
|
+
self._footer_order.append(key)
|
|
625
|
+
|
|
626
|
+
# Apply current display mode
|
|
627
|
+
if self._display_mode == 'compact':
|
|
628
|
+
item.set_compact(True)
|
|
629
|
+
|
|
630
|
+
return item
|
|
631
|
+
|
|
632
|
+
def node(self, key: str) -> SideNavItem:
|
|
633
|
+
"""Get an item by key.
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
key (str): The item key.
|
|
637
|
+
|
|
638
|
+
Returns:
|
|
639
|
+
SideNavItem: The item.
|
|
640
|
+
|
|
641
|
+
Raises:
|
|
642
|
+
KeyError: If no item with the given key exists.
|
|
643
|
+
"""
|
|
644
|
+
if key in self._items:
|
|
645
|
+
return self._items[key]
|
|
646
|
+
if key in self._footer_items:
|
|
647
|
+
return self._footer_items[key]
|
|
648
|
+
raise KeyError(f"No item with key '{key}'")
|
|
649
|
+
|
|
650
|
+
def nodes(self) -> tuple[SideNavItem, ...]:
|
|
651
|
+
"""Get all items (excluding footer items).
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
A tuple of all SideNavItem instances.
|
|
655
|
+
"""
|
|
656
|
+
return tuple(self._items.values())
|
|
657
|
+
|
|
658
|
+
def node_keys(self) -> tuple[str, ...]:
|
|
659
|
+
"""Get all item keys (excluding footer items).
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
A tuple of all item keys.
|
|
663
|
+
"""
|
|
664
|
+
return tuple(self._items.keys())
|
|
665
|
+
|
|
666
|
+
def group(self, key: str) -> SideNavGroup:
|
|
667
|
+
"""Get a group by key.
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
key (str): The group key.
|
|
671
|
+
|
|
672
|
+
Returns:
|
|
673
|
+
SideNavGroup: The group.
|
|
674
|
+
|
|
675
|
+
Raises:
|
|
676
|
+
KeyError: If no group with the given key exists.
|
|
677
|
+
"""
|
|
678
|
+
if key not in self._groups:
|
|
679
|
+
raise KeyError(f"No group with key '{key}'")
|
|
680
|
+
return self._groups[key]
|
|
681
|
+
|
|
682
|
+
def groups(self) -> tuple[SideNavGroup, ...]:
|
|
683
|
+
"""Get all groups.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
A tuple of all SideNavGroup instances.
|
|
687
|
+
"""
|
|
688
|
+
return tuple(self._groups.values())
|
|
689
|
+
|
|
690
|
+
def configure_node(self, key: str, option: str = None, **kwargs: Any):
|
|
691
|
+
"""Configure a specific item by its key.
|
|
692
|
+
|
|
693
|
+
Args:
|
|
694
|
+
key: The key of the item to configure.
|
|
695
|
+
option: If provided, return the value of this option.
|
|
696
|
+
**kwargs: Configuration options to apply to the item.
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
If option is provided, returns the value of that option.
|
|
700
|
+
"""
|
|
701
|
+
item = self.node(key)
|
|
702
|
+
if option is not None:
|
|
703
|
+
return item.cget(option)
|
|
704
|
+
item.configure(**kwargs)
|
|
705
|
+
|
|
706
|
+
def remove_item(self, key: str) -> None:
|
|
707
|
+
"""Remove an item by key.
|
|
708
|
+
|
|
709
|
+
Args:
|
|
710
|
+
key (str): The item key to remove.
|
|
711
|
+
"""
|
|
712
|
+
if key in self._items:
|
|
713
|
+
item = self._items.pop(key)
|
|
714
|
+
|
|
715
|
+
# Remove from group if in one (check before removing from lookup)
|
|
716
|
+
group_key = self._item_to_group.pop(key, None)
|
|
717
|
+
if group_key:
|
|
718
|
+
self._groups[group_key]._remove_item(key)
|
|
719
|
+
|
|
720
|
+
# Remove from content widgets if at root
|
|
721
|
+
if item in self._content_widgets:
|
|
722
|
+
self._content_widgets.remove(item)
|
|
723
|
+
|
|
724
|
+
item.destroy()
|
|
725
|
+
|
|
726
|
+
elif key in self._footer_items:
|
|
727
|
+
item = self._footer_items.pop(key)
|
|
728
|
+
self._footer_order.remove(key)
|
|
729
|
+
item.destroy()
|
|
730
|
+
# Hide footer separator if no more footer items
|
|
731
|
+
if not self._footer_items:
|
|
732
|
+
self._footer_separator.pack_forget()
|
|
733
|
+
|
|
734
|
+
def remove_group(self, key: str) -> None:
|
|
735
|
+
"""Remove a group and all its items.
|
|
736
|
+
|
|
737
|
+
Args:
|
|
738
|
+
key (str): The group key to remove.
|
|
739
|
+
"""
|
|
740
|
+
if key not in self._groups:
|
|
741
|
+
return
|
|
742
|
+
|
|
743
|
+
group = self._groups.pop(key)
|
|
744
|
+
|
|
745
|
+
# Remove all items in the group and clean up lookups
|
|
746
|
+
for item_key in list(group._items.keys()):
|
|
747
|
+
if item_key in self._items:
|
|
748
|
+
del self._items[item_key]
|
|
749
|
+
self._item_to_group.pop(item_key, None)
|
|
750
|
+
|
|
751
|
+
# Remove from content widgets
|
|
752
|
+
if group in self._content_widgets:
|
|
753
|
+
self._content_widgets.remove(group)
|
|
754
|
+
|
|
755
|
+
group.destroy()
|
|
756
|
+
|
|
757
|
+
def select(self, key: str) -> None:
|
|
758
|
+
"""Select an item by key.
|
|
759
|
+
|
|
760
|
+
Args:
|
|
761
|
+
key (str): The item key to select.
|
|
762
|
+
"""
|
|
763
|
+
if self._selection_var:
|
|
764
|
+
self._selection_var.set(key)
|
|
765
|
+
|
|
766
|
+
# --- Public API: Pane Control ---
|
|
767
|
+
|
|
768
|
+
def toggle_pane(self) -> None:
|
|
769
|
+
"""Toggle the pane between expanded and compact modes.
|
|
770
|
+
|
|
771
|
+
In minimal mode, this toggles visibility instead.
|
|
772
|
+
"""
|
|
773
|
+
if self._display_mode == 'minimal':
|
|
774
|
+
# In minimal mode, toggle visibility
|
|
775
|
+
self._is_pane_open = not self._is_pane_open
|
|
776
|
+
self._apply_display_mode()
|
|
777
|
+
self.event_generate('<<PaneToggled>>', data={'is_open': self._is_pane_open})
|
|
778
|
+
else:
|
|
779
|
+
# Toggle between expanded and compact
|
|
780
|
+
new_mode = 'compact' if self._display_mode == 'expanded' else 'expanded'
|
|
781
|
+
self.set_display_mode(new_mode)
|
|
782
|
+
|
|
783
|
+
def open_pane(self) -> None:
|
|
784
|
+
"""Open the pane."""
|
|
785
|
+
if not self._is_pane_open:
|
|
786
|
+
self._is_pane_open = True
|
|
787
|
+
self._apply_display_mode()
|
|
788
|
+
self.event_generate('<<PaneToggled>>', data={'is_open': True})
|
|
789
|
+
|
|
790
|
+
def close_pane(self) -> None:
|
|
791
|
+
"""Close the pane."""
|
|
792
|
+
if self._is_pane_open:
|
|
793
|
+
self._is_pane_open = False
|
|
794
|
+
self._apply_display_mode()
|
|
795
|
+
self.event_generate('<<PaneToggled>>', data={'is_open': False})
|
|
796
|
+
|
|
797
|
+
def set_display_mode(self, mode: DisplayMode) -> None:
|
|
798
|
+
"""Set the display mode.
|
|
799
|
+
|
|
800
|
+
Args:
|
|
801
|
+
mode (DisplayMode): 'expanded', 'compact', or 'minimal'.
|
|
802
|
+
"""
|
|
803
|
+
if mode != self._display_mode:
|
|
804
|
+
self._display_mode = mode
|
|
805
|
+
self._apply_display_mode()
|
|
806
|
+
self.event_generate('<<DisplayModeChanged>>', data={'mode': mode})
|
|
807
|
+
|
|
808
|
+
# --- Properties ---
|
|
809
|
+
|
|
810
|
+
@property
|
|
811
|
+
def is_pane_open(self) -> bool:
|
|
812
|
+
"""Check if the pane is open."""
|
|
813
|
+
return self._is_pane_open
|
|
814
|
+
|
|
815
|
+
@property
|
|
816
|
+
def display_mode(self) -> DisplayMode:
|
|
817
|
+
"""Get the current display mode."""
|
|
818
|
+
return self._display_mode
|
|
819
|
+
|
|
820
|
+
@property
|
|
821
|
+
def selected_key(self) -> str | None:
|
|
822
|
+
"""Get the currently selected item key."""
|
|
823
|
+
if self._selection_var:
|
|
824
|
+
return self._selection_var.get() or None
|
|
825
|
+
return None
|
|
826
|
+
|
|
827
|
+
def footer_nodes(self) -> tuple[SideNavItem, ...]:
|
|
828
|
+
"""Get all footer items in order.
|
|
829
|
+
|
|
830
|
+
Returns:
|
|
831
|
+
A tuple of all footer SideNavItem instances.
|
|
832
|
+
"""
|
|
833
|
+
return tuple(self._footer_items[key] for key in self._footer_order)
|
|
834
|
+
|
|
835
|
+
def footer_node_keys(self) -> tuple[str, ...]:
|
|
836
|
+
"""Get all footer item keys in order.
|
|
837
|
+
|
|
838
|
+
Returns:
|
|
839
|
+
A tuple of all footer item keys.
|
|
840
|
+
"""
|
|
841
|
+
return tuple(self._footer_order)
|
|
842
|
+
|
|
843
|
+
@property
|
|
844
|
+
def signal(self) -> 'Signal[str] | None':
|
|
845
|
+
"""Get the selection signal."""
|
|
846
|
+
return self._signal
|
|
847
|
+
|
|
848
|
+
@property
|
|
849
|
+
def variable(self) -> Variable | None:
|
|
850
|
+
"""Get the selection variable."""
|
|
851
|
+
return self._selection_var
|
|
852
|
+
|
|
853
|
+
# --- Configuration Delegates ---
|
|
854
|
+
|
|
855
|
+
@configure_delegate('title')
|
|
856
|
+
def _delegate_title(self, value: str = None):
|
|
857
|
+
"""Configure the pane title."""
|
|
858
|
+
if value is None:
|
|
859
|
+
return self._title
|
|
860
|
+
self._title = value
|
|
861
|
+
if self._title_label:
|
|
862
|
+
self._title_label.configure(text=value)
|
|
863
|
+
return None
|
|
864
|
+
|
|
865
|
+
@configure_delegate('accent')
|
|
866
|
+
def _delegate_accent(self, value: str = None):
|
|
867
|
+
"""Configure the accent color (read-only after creation)."""
|
|
868
|
+
if value is None:
|
|
869
|
+
return self._accent
|
|
870
|
+
# Accent is read-only after creation since changing it would
|
|
871
|
+
# require rebuilding all item styles
|
|
872
|
+
return None
|
|
873
|
+
|
|
874
|
+
# --- Event Binding Helpers ---
|
|
875
|
+
|
|
876
|
+
def on_selection_changed(self, callback) -> str:
|
|
877
|
+
"""Bind to `<<SelectionChanged>>`.
|
|
878
|
+
|
|
879
|
+
Args:
|
|
880
|
+
callback: Function to call when selection changes.
|
|
881
|
+
|
|
882
|
+
Returns:
|
|
883
|
+
str: Binding identifier.
|
|
884
|
+
"""
|
|
885
|
+
return self.bind('<<SelectionChanged>>', callback, add='+')
|
|
886
|
+
|
|
887
|
+
def off_selection_changed(self, bind_id: str = None) -> None:
|
|
888
|
+
"""Unbind from `<<SelectionChanged>>`."""
|
|
889
|
+
self.unbind('<<SelectionChanged>>', bind_id)
|
|
890
|
+
|
|
891
|
+
def on_back_requested(self, callback) -> str:
|
|
892
|
+
"""Bind to `<<BackRequested>>`.
|
|
893
|
+
|
|
894
|
+
Args:
|
|
895
|
+
callback: Function to call when back button is clicked.
|
|
896
|
+
|
|
897
|
+
Returns:
|
|
898
|
+
str: Binding identifier.
|
|
899
|
+
"""
|
|
900
|
+
return self.bind('<<BackRequested>>', callback, add='+')
|
|
901
|
+
|
|
902
|
+
def off_back_requested(self, bind_id: str = None) -> None:
|
|
903
|
+
"""Unbind from `<<BackRequested>>`."""
|
|
904
|
+
self.unbind('<<BackRequested>>', bind_id)
|
|
905
|
+
|
|
906
|
+
def on_pane_toggled(self, callback) -> str:
|
|
907
|
+
"""Bind to `<<PaneToggled>>`.
|
|
908
|
+
|
|
909
|
+
Args:
|
|
910
|
+
callback: Function to call when pane is toggled.
|
|
911
|
+
|
|
912
|
+
Returns:
|
|
913
|
+
str: Binding identifier.
|
|
914
|
+
"""
|
|
915
|
+
return self.bind('<<PaneToggled>>', callback, add='+')
|
|
916
|
+
|
|
917
|
+
def off_pane_toggled(self, bind_id: str = None) -> None:
|
|
918
|
+
"""Unbind from `<<PaneToggled>>`."""
|
|
919
|
+
self.unbind('<<PaneToggled>>', bind_id)
|