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,544 @@
|
|
|
1
|
+
from tkinter import Toplevel
|
|
2
|
+
|
|
3
|
+
from typing_extensions import Unpack
|
|
4
|
+
|
|
5
|
+
from bootstack.widgets.primitives.button import Button
|
|
6
|
+
from bootstack.widgets.primitives.frame import Frame
|
|
7
|
+
from bootstack.widgets.composites.scrollview import ScrollView
|
|
8
|
+
from bootstack.widgets.composites.field import Field, FieldOptions
|
|
9
|
+
from bootstack.widgets.mixins import configure_delegate
|
|
10
|
+
from bootstack.widgets.types import Master
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SelectBox(Field):
|
|
14
|
+
"""Dropdown-like field widget built on top of Field.
|
|
15
|
+
|
|
16
|
+
Renders a field with a suffix button that opens a popup list of available
|
|
17
|
+
items. Selecting an item updates the field value and emits `<<Change>>`.
|
|
18
|
+
|
|
19
|
+
!!! note "Events"
|
|
20
|
+
|
|
21
|
+
`<<Change>>`: Fired when the selection changes (user or code).
|
|
22
|
+
Provides `event.data` with keys: `value`, `prev_value`, `text`.
|
|
23
|
+
|
|
24
|
+
Attributes:
|
|
25
|
+
entry_widget (TextEntryPart): The underlying entry widget.
|
|
26
|
+
label_widget (Label): The label widget.
|
|
27
|
+
message_widget (Label): The message label widget.
|
|
28
|
+
addons (dict[str, Widget]): Dictionary of inserted addon widgets by name.
|
|
29
|
+
variable (Variable): Tkinter Variable linked to entry text.
|
|
30
|
+
signal (Signal): Signal object for reactive updates.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
master: Master = None,
|
|
36
|
+
value: str = None,
|
|
37
|
+
items: list[str] = None,
|
|
38
|
+
label: str = None,
|
|
39
|
+
message: str = None,
|
|
40
|
+
allow_custom_values: bool = False,
|
|
41
|
+
show_dropdown_button: bool = True,
|
|
42
|
+
dropdown_button_icon: str = None,
|
|
43
|
+
enable_search: bool = False,
|
|
44
|
+
**kwargs: Unpack[FieldOptions]
|
|
45
|
+
):
|
|
46
|
+
"""Create a SelectBox widget.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
master: Parent widget. If None, uses the default root window.
|
|
50
|
+
value (str): Initial selected value; should typically be present in `items`.
|
|
51
|
+
items (list): Sequence of string options to present in the popup list.
|
|
52
|
+
label (str): Optional label text shown above the field.
|
|
53
|
+
message (str): Optional helper/error message shown below the field.
|
|
54
|
+
allow_custom_values (bool): If True, the entry is editable so users can type
|
|
55
|
+
arbitrary values in addition to choosing from the list.
|
|
56
|
+
show_dropdown_button (bool): If True (default), the dropdown button is shown. This option is
|
|
57
|
+
ignored if custom values are allowed.
|
|
58
|
+
dropdown_button_icon (str): The icon to display on the dropdown button.
|
|
59
|
+
enable_search (bool): If True, allows typing in the entry to filter the popup list.
|
|
60
|
+
When combined with allow_custom_values=False, the first filtered item is selected
|
|
61
|
+
when the popup closes. With allow_custom_values=True, any typed value is kept.
|
|
62
|
+
|
|
63
|
+
Other Parameters:
|
|
64
|
+
allow_blank (bool): If True, empty input is allowed.
|
|
65
|
+
accent (str): Accent token for styling the focus ring and active border.
|
|
66
|
+
value_format (str): ICU format pattern for parsing/formatting.
|
|
67
|
+
font (str): Font for text display.
|
|
68
|
+
foreground (str): Text color.
|
|
69
|
+
initial_focus (bool): If True, widget receives focus when created.
|
|
70
|
+
justify (str): Text justification ('left', 'center', 'right').
|
|
71
|
+
show_message (bool): If True, displays message text below the field.
|
|
72
|
+
padding (str | int | tuple): Padding around the entry widget.
|
|
73
|
+
state (str): The widget state ('normal', 'disabled', 'readonly').
|
|
74
|
+
textvariable (Variable): Tkinter Variable to link with the entry text.
|
|
75
|
+
textsignal (Signal): Signal object for reactive text updates.
|
|
76
|
+
width (int): Width of the entry in characters.
|
|
77
|
+
required (bool): If True, field cannot be empty.
|
|
78
|
+
"""
|
|
79
|
+
super().__init__(master, value=value, label=label, message=message, **kwargs)
|
|
80
|
+
|
|
81
|
+
self._allow_custom_values = allow_custom_values
|
|
82
|
+
self._search_enabled = enable_search
|
|
83
|
+
self._items = items or []
|
|
84
|
+
self._last_selected_value = value
|
|
85
|
+
self._popup_open = False
|
|
86
|
+
self._dropdown_button_icon = dropdown_button_icon or 'chevron-down'
|
|
87
|
+
self._popup_frame = None
|
|
88
|
+
self._item_labels = []
|
|
89
|
+
|
|
90
|
+
# Add dropdown button if needed
|
|
91
|
+
if allow_custom_values or show_dropdown_button:
|
|
92
|
+
self.insert_addon(
|
|
93
|
+
Button,
|
|
94
|
+
position="after",
|
|
95
|
+
name="dropdown",
|
|
96
|
+
icon=self._dropdown_button_icon,
|
|
97
|
+
icon_only=True,
|
|
98
|
+
command=self._on_dropdown_click
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Configure entry state based on search and custom value settings
|
|
102
|
+
if allow_custom_values or enable_search:
|
|
103
|
+
self.entry_widget.state(['!readonly'])
|
|
104
|
+
else:
|
|
105
|
+
# Set entry to readonly but keep dropdown button enabled
|
|
106
|
+
self.entry_widget.state(['readonly'])
|
|
107
|
+
self.after_idle(self._bind_readonly_selection_on_click)
|
|
108
|
+
|
|
109
|
+
def _on_dropdown_click(self):
|
|
110
|
+
"""Handle dropdown button click by focusing entry then showing popup."""
|
|
111
|
+
self.entry_widget.focus_set()
|
|
112
|
+
self._show_selection_options()
|
|
113
|
+
|
|
114
|
+
def _bind_readonly_selection_on_click(self):
|
|
115
|
+
"""Bind entry click to show popup when in readonly mode."""
|
|
116
|
+
self.entry_widget.bind('<Button-1>', lambda _: self.after_idle(self._show_selection_options), add='+')
|
|
117
|
+
|
|
118
|
+
def _show_selection_options(self):
|
|
119
|
+
"""Create and display the popup list of selectable items."""
|
|
120
|
+
if not self._items or self._popup_open:
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
self._popup_open = True
|
|
124
|
+
self.update_idletasks()
|
|
125
|
+
|
|
126
|
+
# Create popup toplevel
|
|
127
|
+
toplevel = self._create_popup_toplevel()
|
|
128
|
+
|
|
129
|
+
# Create tracking variables for popup state
|
|
130
|
+
popup_state = {
|
|
131
|
+
'item_was_selected': False,
|
|
132
|
+
'popup_closed': False,
|
|
133
|
+
'first_filtered_item': None,
|
|
134
|
+
'entry_focus_handler': None,
|
|
135
|
+
'key_bindings': [],
|
|
136
|
+
'highlighted_index': max(0, self.selected_index),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# Setup close handler
|
|
140
|
+
def close_popup(event=None):
|
|
141
|
+
self._close_popup(toplevel, popup_state)
|
|
142
|
+
|
|
143
|
+
# Create popup content frame with items
|
|
144
|
+
self._popup_frame = self._create_popup_frame(toplevel, popup_state)
|
|
145
|
+
|
|
146
|
+
# Setup event bindings based on mode
|
|
147
|
+
self._setup_popup_bindings(toplevel, popup_state, close_popup)
|
|
148
|
+
|
|
149
|
+
# Show popup and set focus
|
|
150
|
+
toplevel.deiconify()
|
|
151
|
+
toplevel.lift()
|
|
152
|
+
|
|
153
|
+
if self._search_enabled:
|
|
154
|
+
self.entry_widget.focus_force()
|
|
155
|
+
self.entry_widget.icursor('end')
|
|
156
|
+
else:
|
|
157
|
+
toplevel.focus_force()
|
|
158
|
+
|
|
159
|
+
def _create_popup_toplevel(self):
|
|
160
|
+
"""Create and position the popup toplevel window."""
|
|
161
|
+
x = self.winfo_rootx() + (1 if self._search_enabled else 3)
|
|
162
|
+
gap_below = 8 if self._search_enabled else 5
|
|
163
|
+
gap_above = 4 if self._search_enabled else 1
|
|
164
|
+
entry_top = self.entry_widget.winfo_rooty()
|
|
165
|
+
entry_bottom = entry_top + self.entry_widget.winfo_height()
|
|
166
|
+
width = self.winfo_width() - (2 if self._search_enabled else 6)
|
|
167
|
+
max_height = 200 # Maximum popup height in pixels
|
|
168
|
+
|
|
169
|
+
# Flip above the entry when there isn't enough room below — matches
|
|
170
|
+
# Tk's combobox PlacePopdown behavior.
|
|
171
|
+
screen_h = self.winfo_screenheight()
|
|
172
|
+
if entry_bottom + gap_below + max_height > screen_h and entry_top - gap_above - max_height >= 0:
|
|
173
|
+
y = entry_top - gap_above - max_height
|
|
174
|
+
else:
|
|
175
|
+
y = entry_bottom + gap_below
|
|
176
|
+
|
|
177
|
+
# The base_window.py warning about overrideredirect on Aqua applies
|
|
178
|
+
# to full app windows that combine it with grab/transient semantics;
|
|
179
|
+
# this popup uses neither (it dismisses via a root <Button-1>
|
|
180
|
+
# binding), so overrideredirect is safe here and matches Tk's own
|
|
181
|
+
# ttk::combobox::PlacePopdown approach for Mac popdowns.
|
|
182
|
+
toplevel = Toplevel(self)
|
|
183
|
+
toplevel.withdraw()
|
|
184
|
+
toplevel.overrideredirect(True)
|
|
185
|
+
toplevel.minsize(width, 0)
|
|
186
|
+
toplevel.maxsize(width * 2, max_height)
|
|
187
|
+
toplevel.geometry(f"{width}x{max_height}+{x}+{y}")
|
|
188
|
+
|
|
189
|
+
return toplevel
|
|
190
|
+
|
|
191
|
+
def _create_popup_frame(self, toplevel, popup_state):
|
|
192
|
+
"""Create popup frame with scrollable item list."""
|
|
193
|
+
# Outer frame with border and padding — match the entry's input
|
|
194
|
+
# surface so the popup fill blends with the field background.
|
|
195
|
+
outer_frame = Frame(toplevel, padding=3, show_border=True, surface='content')
|
|
196
|
+
outer_frame.pack(fill='both', expand=True)
|
|
197
|
+
|
|
198
|
+
# Create scrollview inside the outer frame
|
|
199
|
+
scrollview = ScrollView(
|
|
200
|
+
outer_frame,
|
|
201
|
+
scroll_direction='vertical',
|
|
202
|
+
scrollbar_visibility='always',
|
|
203
|
+
)
|
|
204
|
+
scrollview.pack(fill='both', expand=True)
|
|
205
|
+
|
|
206
|
+
# Create inner frame for items
|
|
207
|
+
inner_frame = Frame(scrollview)
|
|
208
|
+
scrollview.add(inner_frame)
|
|
209
|
+
|
|
210
|
+
# Make inner frame fill the canvas width
|
|
211
|
+
def on_canvas_configure(event):
|
|
212
|
+
scrollview.canvas.itemconfig(scrollview._window_id, width=event.width)
|
|
213
|
+
scrollview.canvas.bind('<Configure>', on_canvas_configure, add='+')
|
|
214
|
+
|
|
215
|
+
# Expand scrollregion to include vertical padding so content doesn't clip borders
|
|
216
|
+
def on_inner_frame_configure(event):
|
|
217
|
+
bbox = scrollview.canvas.bbox('all')
|
|
218
|
+
if bbox:
|
|
219
|
+
x0, y0, x1, y1 = bbox
|
|
220
|
+
padding_y = 1
|
|
221
|
+
scrollview.canvas.configure(scrollregion=(x0, y0 - padding_y, x1, y1 + padding_y))
|
|
222
|
+
inner_frame.bind('<Configure>', on_inner_frame_configure, add='+')
|
|
223
|
+
|
|
224
|
+
self._item_labels = []
|
|
225
|
+
current_value = self.value
|
|
226
|
+
|
|
227
|
+
# Get accent from Field's _accent attribute, fallback to primary if None
|
|
228
|
+
accent = getattr(self, '_accent', None) or 'primary'
|
|
229
|
+
|
|
230
|
+
for i, item in enumerate(self._items):
|
|
231
|
+
btn = Button(
|
|
232
|
+
inner_frame,
|
|
233
|
+
text=item,
|
|
234
|
+
accent=accent,
|
|
235
|
+
variant='selectbox_item',
|
|
236
|
+
command=lambda v=item: self._on_item_click(v, toplevel, popup_state)
|
|
237
|
+
)
|
|
238
|
+
btn.pack(fill='x')
|
|
239
|
+
|
|
240
|
+
# Store item value on button for retrieval
|
|
241
|
+
btn._item_value = item
|
|
242
|
+
btn._item_index = i
|
|
243
|
+
|
|
244
|
+
# Apply selected state if this is the current value
|
|
245
|
+
if item == current_value:
|
|
246
|
+
btn.state(['selected'])
|
|
247
|
+
|
|
248
|
+
self._item_labels.append(btn)
|
|
249
|
+
|
|
250
|
+
return scrollview
|
|
251
|
+
|
|
252
|
+
def _on_item_click(self, value, toplevel, popup_state):
|
|
253
|
+
"""Handle click on item."""
|
|
254
|
+
popup_state['item_was_selected'] = True
|
|
255
|
+
self._set_selected_value(value, toplevel, popup_state)
|
|
256
|
+
|
|
257
|
+
def _update_highlight(self, popup_state, new_index):
|
|
258
|
+
"""Update the highlighted item in the popup."""
|
|
259
|
+
visible_buttons = [btn for btn in self._item_labels if btn.winfo_manager()]
|
|
260
|
+
if not visible_buttons:
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
# Clamp index to valid range
|
|
264
|
+
new_index = max(0, min(new_index, len(visible_buttons) - 1))
|
|
265
|
+
old_index = popup_state['highlighted_index']
|
|
266
|
+
|
|
267
|
+
# Remove highlight from old button
|
|
268
|
+
if 0 <= old_index < len(visible_buttons):
|
|
269
|
+
visible_buttons[old_index].state(['!selected'])
|
|
270
|
+
|
|
271
|
+
# Add highlight to new button
|
|
272
|
+
visible_buttons[new_index].state(['selected'])
|
|
273
|
+
popup_state['highlighted_index'] = new_index
|
|
274
|
+
|
|
275
|
+
# Scroll to make highlighted item visible
|
|
276
|
+
btn = visible_buttons[new_index]
|
|
277
|
+
if self._popup_frame and self._popup_frame.winfo_exists():
|
|
278
|
+
self._popup_frame.canvas.update_idletasks()
|
|
279
|
+
# Get button position relative to canvas
|
|
280
|
+
btn_y = btn.winfo_y()
|
|
281
|
+
btn_height = btn.winfo_height()
|
|
282
|
+
canvas_height = self._popup_frame.canvas.winfo_height()
|
|
283
|
+
|
|
284
|
+
# Scroll if needed
|
|
285
|
+
scroll_top = self._popup_frame.canvas.canvasy(0)
|
|
286
|
+
scroll_bottom = scroll_top + canvas_height
|
|
287
|
+
|
|
288
|
+
if btn_y < scroll_top:
|
|
289
|
+
self._popup_frame.canvas.yview_moveto(btn_y / self._popup_frame.canvas.bbox('all')[3])
|
|
290
|
+
elif btn_y + btn_height > scroll_bottom:
|
|
291
|
+
target = (btn_y + btn_height - canvas_height) / self._popup_frame.canvas.bbox('all')[3]
|
|
292
|
+
self._popup_frame.canvas.yview_moveto(target)
|
|
293
|
+
|
|
294
|
+
def _setup_popup_bindings(self, toplevel, popup_state, close_popup):
|
|
295
|
+
"""Setup all event bindings for the popup."""
|
|
296
|
+
# Escape always closes
|
|
297
|
+
toplevel.bind("<Escape>", close_popup)
|
|
298
|
+
|
|
299
|
+
# Arrow key navigation
|
|
300
|
+
def on_arrow_down(event):
|
|
301
|
+
self._update_highlight(popup_state, popup_state['highlighted_index'] + 1)
|
|
302
|
+
return 'break'
|
|
303
|
+
|
|
304
|
+
def on_arrow_up(event):
|
|
305
|
+
self._update_highlight(popup_state, popup_state['highlighted_index'] - 1)
|
|
306
|
+
return 'break'
|
|
307
|
+
|
|
308
|
+
def on_enter(event):
|
|
309
|
+
visible_buttons = [btn for btn in self._item_labels if btn.winfo_manager()]
|
|
310
|
+
idx = popup_state['highlighted_index']
|
|
311
|
+
if 0 <= idx < len(visible_buttons):
|
|
312
|
+
popup_state['item_was_selected'] = True
|
|
313
|
+
self._set_selected_value(visible_buttons[idx]._item_value, toplevel, popup_state)
|
|
314
|
+
return 'break'
|
|
315
|
+
|
|
316
|
+
toplevel.bind("<Down>", on_arrow_down)
|
|
317
|
+
toplevel.bind("<Up>", on_arrow_up)
|
|
318
|
+
toplevel.bind("<Return>", on_enter)
|
|
319
|
+
|
|
320
|
+
# Also bind to entry widget for search mode
|
|
321
|
+
if self._search_enabled:
|
|
322
|
+
self._setup_search_bindings(toplevel, popup_state, close_popup)
|
|
323
|
+
entry_down = self.entry_widget.bind('<Down>', on_arrow_down, add='+')
|
|
324
|
+
entry_up = self.entry_widget.bind('<Up>', on_arrow_up, add='+')
|
|
325
|
+
entry_enter = self.entry_widget.bind('<Return>', on_enter, add='+')
|
|
326
|
+
popup_state['key_bindings'].append(('<Down>', entry_down))
|
|
327
|
+
popup_state['key_bindings'].append(('<Up>', entry_up))
|
|
328
|
+
popup_state['key_bindings'].append(('<Return>', entry_enter))
|
|
329
|
+
else:
|
|
330
|
+
toplevel.bind("<FocusOut>", close_popup)
|
|
331
|
+
|
|
332
|
+
# Apply initial highlight
|
|
333
|
+
self._update_highlight(popup_state, popup_state['highlighted_index'])
|
|
334
|
+
|
|
335
|
+
def _setup_search_bindings(self, toplevel, popup_state, close_popup):
|
|
336
|
+
"""Setup search-specific event bindings."""
|
|
337
|
+
# Initialize first filtered item
|
|
338
|
+
if self._items:
|
|
339
|
+
popup_state['first_filtered_item'] = self._items[0]
|
|
340
|
+
popup_state['last_search_text'] = self.entry_widget.get().lower()
|
|
341
|
+
|
|
342
|
+
# Filter function
|
|
343
|
+
def filter_items(*args):
|
|
344
|
+
if popup_state['popup_closed'] or not self._popup_frame or not self._popup_frame.winfo_exists():
|
|
345
|
+
return
|
|
346
|
+
|
|
347
|
+
search_text = self.entry_widget.get().lower()
|
|
348
|
+
|
|
349
|
+
# Skip if search text hasn't changed (e.g., arrow key release)
|
|
350
|
+
if search_text == popup_state['last_search_text']:
|
|
351
|
+
return
|
|
352
|
+
popup_state['last_search_text'] = search_text
|
|
353
|
+
|
|
354
|
+
# Show/hide buttons based on filter
|
|
355
|
+
first_visible = None
|
|
356
|
+
for btn in self._item_labels:
|
|
357
|
+
if search_text in btn._item_value.lower():
|
|
358
|
+
btn.pack(fill='x')
|
|
359
|
+
if first_visible is None:
|
|
360
|
+
first_visible = btn
|
|
361
|
+
else:
|
|
362
|
+
btn.pack_forget()
|
|
363
|
+
|
|
364
|
+
# Track first filtered item for auto-select
|
|
365
|
+
popup_state['first_filtered_item'] = first_visible._item_value if first_visible else None
|
|
366
|
+
|
|
367
|
+
# Reset highlight to first visible item
|
|
368
|
+
self._update_highlight(popup_state, 0)
|
|
369
|
+
|
|
370
|
+
# Bind KeyRelease for filtering
|
|
371
|
+
keyrelease_binding = self.entry_widget.bind('<KeyRelease>', lambda e: filter_items(), add='+')
|
|
372
|
+
popup_state['key_bindings'].append(('<KeyRelease>', keyrelease_binding))
|
|
373
|
+
|
|
374
|
+
# Bind Tab to select highlighted item
|
|
375
|
+
def on_tab(event):
|
|
376
|
+
if popup_state['popup_closed']:
|
|
377
|
+
return
|
|
378
|
+
visible_buttons = [btn for btn in self._item_labels if btn.winfo_manager()]
|
|
379
|
+
idx = popup_state['highlighted_index']
|
|
380
|
+
if 0 <= idx < len(visible_buttons):
|
|
381
|
+
popup_state['item_was_selected'] = True
|
|
382
|
+
self._set_selected_value(visible_buttons[idx]._item_value, toplevel, popup_state)
|
|
383
|
+
return 'break'
|
|
384
|
+
|
|
385
|
+
tab_binding = self.entry_widget.bind('<Tab>', on_tab)
|
|
386
|
+
popup_state['key_bindings'].append(('<Tab>', tab_binding))
|
|
387
|
+
|
|
388
|
+
# Setup click-outside detection
|
|
389
|
+
def on_root_click(event):
|
|
390
|
+
x, y = event.x_root, event.y_root
|
|
391
|
+
|
|
392
|
+
# Check if click is inside entry widget
|
|
393
|
+
ex, ey = self.entry_widget.winfo_rootx(), self.entry_widget.winfo_rooty()
|
|
394
|
+
ew, eh = self.entry_widget.winfo_width(), self.entry_widget.winfo_height()
|
|
395
|
+
if ex <= x <= ex + ew and ey <= y <= ey + eh:
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
# Check if click is inside toplevel
|
|
399
|
+
if toplevel.winfo_exists():
|
|
400
|
+
tx, ty = toplevel.winfo_rootx(), toplevel.winfo_rooty()
|
|
401
|
+
tw, th = toplevel.winfo_width(), toplevel.winfo_height()
|
|
402
|
+
if tx <= x <= tx + tw and ty <= y <= ty + th:
|
|
403
|
+
return
|
|
404
|
+
|
|
405
|
+
close_popup()
|
|
406
|
+
|
|
407
|
+
def bind_click():
|
|
408
|
+
if popup_state['popup_closed']:
|
|
409
|
+
return
|
|
410
|
+
root = self.winfo_toplevel()
|
|
411
|
+
bind_id = root.bind('<Button-1>', on_root_click, add='+')
|
|
412
|
+
popup_state['entry_focus_handler'] = bind_id
|
|
413
|
+
|
|
414
|
+
self.after(100, bind_click)
|
|
415
|
+
|
|
416
|
+
def _close_popup(self, toplevel, popup_state):
|
|
417
|
+
"""Close the popup and cleanup bindings."""
|
|
418
|
+
if popup_state['popup_closed']:
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
popup_state['popup_closed'] = True
|
|
422
|
+
self._popup_open = False
|
|
423
|
+
|
|
424
|
+
# Unbind handlers
|
|
425
|
+
if popup_state['entry_focus_handler'] is not None and self._search_enabled:
|
|
426
|
+
root = self.winfo_toplevel()
|
|
427
|
+
root.unbind('<Button-1>', popup_state['entry_focus_handler'])
|
|
428
|
+
|
|
429
|
+
# Unbind key bindings
|
|
430
|
+
for sequence, funcid in popup_state['key_bindings']:
|
|
431
|
+
self.entry_widget.unbind(sequence, funcid)
|
|
432
|
+
|
|
433
|
+
# Destroy toplevel
|
|
434
|
+
if toplevel.winfo_exists():
|
|
435
|
+
toplevel.destroy()
|
|
436
|
+
|
|
437
|
+
# Clean up popup references
|
|
438
|
+
self._popup_frame = None
|
|
439
|
+
self._item_labels = []
|
|
440
|
+
|
|
441
|
+
# Handle value selection for search mode without custom values
|
|
442
|
+
if self._search_enabled and not self._allow_custom_values:
|
|
443
|
+
if not popup_state['item_was_selected']:
|
|
444
|
+
if popup_state['first_filtered_item'] is not None:
|
|
445
|
+
self._last_selected_value = popup_state['first_filtered_item']
|
|
446
|
+
self.value = popup_state['first_filtered_item']
|
|
447
|
+
|
|
448
|
+
def _set_selected_value(self, selected_value, toplevel, popup_state):
|
|
449
|
+
"""Set the selected value and close the popup."""
|
|
450
|
+
if selected_value is None:
|
|
451
|
+
return
|
|
452
|
+
|
|
453
|
+
self._last_selected_value = selected_value
|
|
454
|
+
self.value = selected_value
|
|
455
|
+
self._close_popup(toplevel, popup_state)
|
|
456
|
+
|
|
457
|
+
@configure_delegate('items')
|
|
458
|
+
def _delegate_items(self, value: list[str] = None):
|
|
459
|
+
"""Get or set the available items for the SelectBox."""
|
|
460
|
+
if value is None:
|
|
461
|
+
return self._items
|
|
462
|
+
else:
|
|
463
|
+
self._items = value or []
|
|
464
|
+
return None
|
|
465
|
+
|
|
466
|
+
@configure_delegate('allow_custom_values')
|
|
467
|
+
def _delegate_allow_custom_values(self, value: bool = None):
|
|
468
|
+
"""Get or set whether free-form text entry is allowed."""
|
|
469
|
+
if value is None:
|
|
470
|
+
return self._allow_custom_values
|
|
471
|
+
else:
|
|
472
|
+
self._allow_custom_values = value
|
|
473
|
+
if value or self._search_enabled:
|
|
474
|
+
self.entry_widget.state(['!readonly'])
|
|
475
|
+
else:
|
|
476
|
+
self.readonly(True)
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
@configure_delegate('enable_search')
|
|
480
|
+
def _delegate_enable_search(self, value: bool = None):
|
|
481
|
+
"""Get or set whether search filtering is enabled."""
|
|
482
|
+
if value is None:
|
|
483
|
+
return self._search_enabled
|
|
484
|
+
else:
|
|
485
|
+
self._search_enabled = value
|
|
486
|
+
if value or self._allow_custom_values:
|
|
487
|
+
self.entry_widget.state(['!readonly'])
|
|
488
|
+
else:
|
|
489
|
+
self.readonly(True)
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
@configure_delegate('value')
|
|
493
|
+
def _delegate_value(self, value=None):
|
|
494
|
+
if value is not None:
|
|
495
|
+
return self.value
|
|
496
|
+
else:
|
|
497
|
+
self.value = value
|
|
498
|
+
return None
|
|
499
|
+
|
|
500
|
+
@property
|
|
501
|
+
def selected_index(self):
|
|
502
|
+
"""Get or set the selected index.
|
|
503
|
+
|
|
504
|
+
Returns -1 if the current value is not in the items list.
|
|
505
|
+
Setting to -1 or None clears the selection.
|
|
506
|
+
"""
|
|
507
|
+
try:
|
|
508
|
+
return self._items.index(self.value)
|
|
509
|
+
except (ValueError, TypeError):
|
|
510
|
+
return -1
|
|
511
|
+
|
|
512
|
+
@selected_index.setter
|
|
513
|
+
def selected_index(self, index):
|
|
514
|
+
if index is None or index == -1:
|
|
515
|
+
self.value = ""
|
|
516
|
+
elif 0 <= index < len(self._items):
|
|
517
|
+
self.value = self._items[index]
|
|
518
|
+
else:
|
|
519
|
+
raise IndexError(f"index {index} out of range for {len(self._items)} items")
|
|
520
|
+
|
|
521
|
+
@property
|
|
522
|
+
def value(self):
|
|
523
|
+
"""Get or set the selected value."""
|
|
524
|
+
return Field.value.fget(self)
|
|
525
|
+
|
|
526
|
+
@value.setter
|
|
527
|
+
def value(self, value):
|
|
528
|
+
"""Set the selected value and emit `<<Change>>` when it differs."""
|
|
529
|
+
prev_value = Field.value.fget(self)
|
|
530
|
+
Field.value.fset(self, value)
|
|
531
|
+
new_value = Field.value.fget(self)
|
|
532
|
+
if new_value != prev_value:
|
|
533
|
+
self.entry_widget._prev_changed_value = new_value
|
|
534
|
+
if not getattr(self, "_suppress_changed_event", False):
|
|
535
|
+
self.entry_widget.event_generate(
|
|
536
|
+
'<<Change>>',
|
|
537
|
+
data={
|
|
538
|
+
'value': new_value,
|
|
539
|
+
'prev_value': prev_value,
|
|
540
|
+
'text': self.entry_widget.get()
|
|
541
|
+
},
|
|
542
|
+
when="tail"
|
|
543
|
+
)
|
|
544
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""SideNav widgets for building navigation interfaces.
|
|
2
|
+
|
|
3
|
+
This package provides a complete navigation solution:
|
|
4
|
+
|
|
5
|
+
- SideNav: Main container with pane, header, content, and footer
|
|
6
|
+
- SideNavGroup: Collapsible group of items (expander in expanded mode, popup in compact)
|
|
7
|
+
- SideNavItem: Selectable navigation item with icon and text
|
|
8
|
+
- SideNavHeader: Non-selectable section label
|
|
9
|
+
- SideNavSeparator: Visual divider between groups
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from bootstack.widgets.composites.sidenav.item import SideNavItem
|
|
13
|
+
from bootstack.widgets.composites.sidenav.group import SideNavGroup
|
|
14
|
+
from bootstack.widgets.composites.sidenav.header import SideNavHeader
|
|
15
|
+
from bootstack.widgets.composites.sidenav.separator import SideNavSeparator
|
|
16
|
+
from bootstack.widgets.composites.sidenav.view import SideNav
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
'SideNav',
|
|
20
|
+
'SideNavGroup',
|
|
21
|
+
'SideNavItem',
|
|
22
|
+
'SideNavHeader',
|
|
23
|
+
'SideNavSeparator',
|
|
24
|
+
]
|