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,223 @@
|
|
|
1
|
+
"""Path entry field widget with file/directory chooser dialog.
|
|
2
|
+
|
|
3
|
+
Provides an entry field with a button that opens a file or directory chooser
|
|
4
|
+
dialog for selecting paths.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tkinter import filedialog
|
|
8
|
+
from typing import Any, Literal
|
|
9
|
+
|
|
10
|
+
from typing_extensions import Unpack
|
|
11
|
+
|
|
12
|
+
from bootstack.widgets.primitives.button import Button
|
|
13
|
+
from bootstack.widgets.composites.field import Field, FieldOptions
|
|
14
|
+
from bootstack.widgets.mixins import configure_delegate
|
|
15
|
+
from bootstack.widgets.types import Master
|
|
16
|
+
|
|
17
|
+
FileDialogType = Literal[
|
|
18
|
+
'openfilename', 'openfile', 'directory', 'openfilenames', 'openfiles',
|
|
19
|
+
'saveasfile', 'saveasfilename'
|
|
20
|
+
]
|
|
21
|
+
"""Type alias for file dialog types.
|
|
22
|
+
|
|
23
|
+
Available dialog types:
|
|
24
|
+
- 'openfilename': Select a single existing file (returns filename)
|
|
25
|
+
- 'openfile': Select a single existing file (returns file object)
|
|
26
|
+
- 'directory': Select a directory
|
|
27
|
+
- 'openfilenames': Select multiple existing files (returns filenames)
|
|
28
|
+
- 'openfiles': Select multiple existing files (returns file objects)
|
|
29
|
+
- 'saveasfile': Select location to save a file (returns file object)
|
|
30
|
+
- 'saveasfilename': Select location to save a file (returns filename)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PathEntry(Field):
|
|
35
|
+
"""A path entry field widget with file/directory chooser dialog button.
|
|
36
|
+
|
|
37
|
+
PathEntry extends the Field widget to provide a specialized input for file
|
|
38
|
+
and directory paths. It includes a button that opens a native file/directory
|
|
39
|
+
chooser dialog, and displays the selected path(s) in the entry field. The
|
|
40
|
+
widget supports various dialog types including single file selection, multiple
|
|
41
|
+
file selection, directory selection, and save file dialogs.
|
|
42
|
+
|
|
43
|
+
!!! note "Events"
|
|
44
|
+
|
|
45
|
+
`<<Change>>`: Fired when a path is selected from the dialog.
|
|
46
|
+
Provides `event.data` with keys: `value`, `prev_value`, `text`, `dialog_result`.
|
|
47
|
+
|
|
48
|
+
`<<Input>>`: Fired when user manually types in the entry.
|
|
49
|
+
|
|
50
|
+
`<<Valid>>`: Fired when validation passes.
|
|
51
|
+
|
|
52
|
+
`<<Invalid>>`: Fired when validation fails.
|
|
53
|
+
|
|
54
|
+
Attributes:
|
|
55
|
+
entry_widget (TextEntryPart): The underlying text entry widget.
|
|
56
|
+
label_widget (Label): The label widget above the entry (from FieldOptions).
|
|
57
|
+
message_widget (Label): The message label widget below the entry.
|
|
58
|
+
addons (dict[str, Widget]): Dictionary of inserted addon widgets by name.
|
|
59
|
+
variable (Variable): Tkinter Variable linked to entry text.
|
|
60
|
+
signal (Signal): Signal object for reactive updates.
|
|
61
|
+
dialog_result (Any): The raw result from the last file dialog operation.
|
|
62
|
+
dialog_button (Button): The button widget that opens the dialog.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
master: Master = None,
|
|
68
|
+
*,
|
|
69
|
+
value: str | None = None,
|
|
70
|
+
dialog: FileDialogType = "openfilename",
|
|
71
|
+
dialog_options: dict[str, Any] | None = None,
|
|
72
|
+
button_text: str = "Browse",
|
|
73
|
+
label: str = None,
|
|
74
|
+
message: str = None,
|
|
75
|
+
**kwargs: Unpack[FieldOptions]
|
|
76
|
+
):
|
|
77
|
+
"""Initialize a PathEntry widget.
|
|
78
|
+
|
|
79
|
+
Creates a path entry field with a button that opens a native file or
|
|
80
|
+
directory chooser dialog. The selected path(s) are automatically displayed
|
|
81
|
+
in the entry field. The widget supports various dialog types for different
|
|
82
|
+
use cases (single file, multiple files, directory, save file).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
master: Parent widget. If None, uses the default root window.
|
|
86
|
+
value: Initial path value to display in the entry field. Default is None
|
|
87
|
+
(empty field). This is updated when a path is selected from the dialog.
|
|
88
|
+
dialog: Type of file dialog to open. Default is "openfilename".
|
|
89
|
+
Options:
|
|
90
|
+
|
|
91
|
+
- 'openfilename': Single file selection (returns path string)
|
|
92
|
+
- 'openfile': Single file selection (returns file object)
|
|
93
|
+
- 'directory': Directory selection
|
|
94
|
+
- 'openfilenames': Multiple file selection (returns paths)
|
|
95
|
+
- 'openfiles': Multiple file selection (returns file objects)
|
|
96
|
+
- 'saveasfile': Save file dialog (returns file object)
|
|
97
|
+
- 'saveasfilename': Save file dialog (returns path string)
|
|
98
|
+
|
|
99
|
+
dialog_options: Dictionary of options to pass to the file dialog.
|
|
100
|
+
Common options: title, initialdir, initialfile, filetypes,
|
|
101
|
+
defaultextension, multiple.
|
|
102
|
+
button_text: Text to display on the browse button. Default is "Browse".
|
|
103
|
+
Can be changed at runtime via `configure(button_text=...)`.
|
|
104
|
+
label (str): Label text to display above the entry field (from FieldOptions).
|
|
105
|
+
message (str): Message text to display below the field.
|
|
106
|
+
|
|
107
|
+
Other Parameters:
|
|
108
|
+
required (bool): If True, field cannot be empty.
|
|
109
|
+
accent (str): Accent token for the focus ring and active border.
|
|
110
|
+
allow_blank (bool): Allow empty input.
|
|
111
|
+
cursor (str): Cursor style when hovering.
|
|
112
|
+
font (str): Font for text display.
|
|
113
|
+
foreground (str): Text color.
|
|
114
|
+
initial_focus (bool): If True, widget receives focus on creation.
|
|
115
|
+
justify (str): Text alignment.
|
|
116
|
+
show_message (bool): If True, displays message area.
|
|
117
|
+
padding (str): Padding around entry widget.
|
|
118
|
+
takefocus (bool): If True, widget accepts Tab focus.
|
|
119
|
+
textvariable (Variable): Tkinter Variable to link with text.
|
|
120
|
+
textsignal (Signal): Signal object for reactive updates.
|
|
121
|
+
width (int): Width in characters.
|
|
122
|
+
|
|
123
|
+
Note:
|
|
124
|
+
When multiple files are selected (using 'openfilenames' or 'openfiles'),
|
|
125
|
+
the paths are joined with ", " (comma-space) and displayed in the entry.
|
|
126
|
+
The raw dialog result (tuple/list) is available via the `dialog_result`
|
|
127
|
+
property.
|
|
128
|
+
"""
|
|
129
|
+
self._dialog = dialog
|
|
130
|
+
self._dialog_options = dialog_options
|
|
131
|
+
self._dialog_result = None
|
|
132
|
+
self._button_text = button_text
|
|
133
|
+
self._prev_value: str | None = value
|
|
134
|
+
|
|
135
|
+
super().__init__(master=master, label=label, message=message, value=value, **kwargs)
|
|
136
|
+
|
|
137
|
+
self.insert_addon(
|
|
138
|
+
Button,
|
|
139
|
+
position="before",
|
|
140
|
+
name="dialog-button",
|
|
141
|
+
text=self._button_text,
|
|
142
|
+
command=self._show_file_chooser
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def dialog_button(self):
|
|
147
|
+
"""Get the dialog button widget."""
|
|
148
|
+
return self.addons.get('dialog-button')
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def dialog_result(self):
|
|
152
|
+
"""Get the raw result from the last file dialog operation.
|
|
153
|
+
|
|
154
|
+
For single file selection, this returns the path string.
|
|
155
|
+
For multiple file selection, this returns a tuple/list of paths.
|
|
156
|
+
"""
|
|
157
|
+
return self._dialog_result
|
|
158
|
+
|
|
159
|
+
# ------ Configuration Delegates ------
|
|
160
|
+
|
|
161
|
+
@configure_delegate('dialog')
|
|
162
|
+
def _delegate_dialog(self, value: FileDialogType = None):
|
|
163
|
+
if value is None:
|
|
164
|
+
return self._dialog
|
|
165
|
+
else:
|
|
166
|
+
self._dialog = value
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
@configure_delegate('button_text')
|
|
170
|
+
def _delegate_button_text(self, value: str = None):
|
|
171
|
+
if value is None:
|
|
172
|
+
return self._button_text
|
|
173
|
+
else:
|
|
174
|
+
self._button_text = value
|
|
175
|
+
if self.dialog_button:
|
|
176
|
+
self.dialog_button['text'] = value
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
@configure_delegate('dialog_options')
|
|
180
|
+
def _delegate_dialog_options(self, value: dict[str, Any] = None):
|
|
181
|
+
if value is None:
|
|
182
|
+
return self._dialog_options
|
|
183
|
+
else:
|
|
184
|
+
self._dialog_options = value
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def _show_file_chooser(self):
|
|
188
|
+
"""Open the file/directory chooser dialog and update the entry."""
|
|
189
|
+
method_name = f"ask{self._dialog}"
|
|
190
|
+
dialog_func = getattr(filedialog, method_name, None)
|
|
191
|
+
|
|
192
|
+
if dialog_func is None:
|
|
193
|
+
raise ValueError(f"Invalid dialog type `{self._dialog}`")
|
|
194
|
+
|
|
195
|
+
result = dialog_func(**(self._dialog_options or {}))
|
|
196
|
+
self._dialog_result = result
|
|
197
|
+
|
|
198
|
+
# Format display text for multiple selections
|
|
199
|
+
if isinstance(result, (tuple, list)):
|
|
200
|
+
display_text = ", ".join(str(p) for p in result)
|
|
201
|
+
else:
|
|
202
|
+
display_text = result
|
|
203
|
+
|
|
204
|
+
# Only update if a selection was made (result is truthy)
|
|
205
|
+
if result:
|
|
206
|
+
prev_value = self._prev_value
|
|
207
|
+
self._prev_value = display_text
|
|
208
|
+
|
|
209
|
+
# Set the value through the field's standard mechanism
|
|
210
|
+
self.value = display_text
|
|
211
|
+
|
|
212
|
+
# Emit <<Change>> on PathEntry (the composite) with full event data
|
|
213
|
+
self.event_generate(
|
|
214
|
+
'<<Change>>',
|
|
215
|
+
data={
|
|
216
|
+
'value': display_text,
|
|
217
|
+
'prev_value': prev_value,
|
|
218
|
+
'text': display_text,
|
|
219
|
+
'dialog_result': self._dialog_result,
|
|
220
|
+
},
|
|
221
|
+
when="tail"
|
|
222
|
+
)
|
|
223
|
+
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tkinter import StringVar
|
|
4
|
+
from typing import Any, Callable, Literal, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from typing_extensions import TypedDict, Unpack
|
|
7
|
+
|
|
8
|
+
from bootstack.widgets.primitives.radiobutton import RadioButton
|
|
9
|
+
from bootstack.widgets.primitives.label import Label
|
|
10
|
+
from bootstack.widgets.mixins.configure_mixin import configure_delegate
|
|
11
|
+
from bootstack.widgets.primitives import Frame
|
|
12
|
+
from bootstack.widgets.types import Master
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from bootstack.core.signals import Signal
|
|
16
|
+
import tkinter as tk
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RadioGroupKwargs(TypedDict, total=False):
|
|
20
|
+
variable: Any
|
|
21
|
+
signal: Signal[Any]
|
|
22
|
+
value: str
|
|
23
|
+
orient: Literal['horizontal', 'vertical']
|
|
24
|
+
accent: str
|
|
25
|
+
text: str
|
|
26
|
+
labelanchor: Literal['n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw']
|
|
27
|
+
state: Literal['normal', 'disabled']
|
|
28
|
+
show_border: bool
|
|
29
|
+
surface: str
|
|
30
|
+
style_options: dict[str, Any]
|
|
31
|
+
# Frame options
|
|
32
|
+
padding: Any
|
|
33
|
+
width: int
|
|
34
|
+
height: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class RadioGroup(Frame):
|
|
38
|
+
"""A group of radio buttons with automatic state tracking and optional label.
|
|
39
|
+
|
|
40
|
+
The RadioGroup widget provides a convenient way to create groups of radio
|
|
41
|
+
buttons with automatic state management. It supports both horizontal and
|
|
42
|
+
vertical orientations, and can display an optional label in various positions.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
variable (Variable): The underlying tk.Variable for the selected value.
|
|
46
|
+
signal (Signal): Signal for reactive programming and change subscriptions.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, master: Master = None, **kwargs: Unpack[RadioGroupKwargs]):
|
|
50
|
+
"""Initialize the RadioGroup.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
master: Parent widget. If None, uses the default root window.
|
|
54
|
+
|
|
55
|
+
Other Parameters:
|
|
56
|
+
orient (str): Layout orientation - 'horizontal' (default) or 'vertical'.
|
|
57
|
+
accent (str): Accent token for styling (e.g., 'primary', 'success', 'danger').
|
|
58
|
+
Defaults to 'primary'.
|
|
59
|
+
bootstyle (str): DEPRECATED - Use `accent` instead.
|
|
60
|
+
text (str): Optional label text to display.
|
|
61
|
+
labelanchor (str): Label position - 'n' (top, default), 's' (bottom),
|
|
62
|
+
'e' (right), 'w' (left), or combinations like 'nw', 'ne', etc.
|
|
63
|
+
variable (Variable): Optional tk.StringVar for controlling the selected value.
|
|
64
|
+
signal (Signal): Optional Signal instance for reactive programming.
|
|
65
|
+
value (str): Initial selected value.
|
|
66
|
+
state (str): Initial state for all buttons - 'normal' (default) or 'disabled'.
|
|
67
|
+
show_border (bool): If True, draws a border around the group.
|
|
68
|
+
surface (str): Optional surface token; otherwise inherited.
|
|
69
|
+
style_options (dict): Additional style options passed to child buttons.
|
|
70
|
+
padding (int | tuple): Frame padding. Defaults to 1.
|
|
71
|
+
width (int): Requested width in pixels.
|
|
72
|
+
height (int): Requested height in pixels.
|
|
73
|
+
"""
|
|
74
|
+
# Extract RadioGroup-specific options before super().__init__
|
|
75
|
+
self._orientation = kwargs.pop('orient', 'horizontal')
|
|
76
|
+
# Support both 'accent' and legacy 'bootstyle'
|
|
77
|
+
self._accent = kwargs.pop('accent', None) or kwargs.pop('bootstyle', 'primary')
|
|
78
|
+
self._labeltext = kwargs.pop('text', None)
|
|
79
|
+
self._labelanchor = kwargs.pop('labelanchor', 'n')
|
|
80
|
+
self._state = kwargs.pop('state', 'normal')
|
|
81
|
+
|
|
82
|
+
# Handle signal/variable/value
|
|
83
|
+
initial_value = kwargs.pop('value', None)
|
|
84
|
+
style_options = kwargs.pop('style_options', {})
|
|
85
|
+
|
|
86
|
+
# Initialize internal state
|
|
87
|
+
self._buttons: dict[str, RadioButton] = {}
|
|
88
|
+
self._label: Label | None = None
|
|
89
|
+
self._button_container: Frame | None = None
|
|
90
|
+
|
|
91
|
+
# Extract signal/variable before Frame init
|
|
92
|
+
signal_value = kwargs.pop('signal', None)
|
|
93
|
+
variable_value = kwargs.pop('variable', None)
|
|
94
|
+
|
|
95
|
+
# Handle padding default
|
|
96
|
+
if 'padding' not in kwargs:
|
|
97
|
+
kwargs['padding'] = 1
|
|
98
|
+
|
|
99
|
+
# Call super().__init__() - just Frame now
|
|
100
|
+
super().__init__(master, style_options=style_options, **kwargs)
|
|
101
|
+
|
|
102
|
+
# Handle variable/signal setup manually
|
|
103
|
+
if signal_value is not None:
|
|
104
|
+
# Signal provided - extract its variable
|
|
105
|
+
self._signal = signal_value
|
|
106
|
+
self._variable = signal_value.var
|
|
107
|
+
elif variable_value is not None:
|
|
108
|
+
# Variable provided - wrap in Signal
|
|
109
|
+
from bootstack.core.signals import Signal
|
|
110
|
+
self._variable = variable_value
|
|
111
|
+
self._signal = Signal.from_variable(variable_value)
|
|
112
|
+
# Set initial value if provided
|
|
113
|
+
if initial_value is not None:
|
|
114
|
+
self._variable.set(initial_value)
|
|
115
|
+
else:
|
|
116
|
+
# Neither provided - create internal variable
|
|
117
|
+
from bootstack.core.signals import Signal
|
|
118
|
+
internal_var = StringVar(value=initial_value or '')
|
|
119
|
+
self._variable = internal_var
|
|
120
|
+
self._signal = Signal.from_variable(internal_var)
|
|
121
|
+
|
|
122
|
+
# Build the UI
|
|
123
|
+
self._build_ui()
|
|
124
|
+
|
|
125
|
+
def _build_ui(self):
|
|
126
|
+
"""Construct the widget layout."""
|
|
127
|
+
# Create button container
|
|
128
|
+
self._button_container = Frame(self)
|
|
129
|
+
|
|
130
|
+
# Create label if text provided
|
|
131
|
+
if self._labeltext:
|
|
132
|
+
self._label = Label(self, text=self._labeltext)
|
|
133
|
+
|
|
134
|
+
# Position based on labelanchor
|
|
135
|
+
self._update_layout()
|
|
136
|
+
|
|
137
|
+
def _update_layout(self):
|
|
138
|
+
"""Update grid layout based on labelanchor."""
|
|
139
|
+
# Clear existing layout
|
|
140
|
+
for widget in self.winfo_children():
|
|
141
|
+
widget.grid_forget()
|
|
142
|
+
|
|
143
|
+
# Determine anchor direction from labelanchor
|
|
144
|
+
# n/s = top/bottom, e/w = right/left
|
|
145
|
+
anchor = self._labelanchor.lower()
|
|
146
|
+
|
|
147
|
+
# Normalize compound anchors to primary direction
|
|
148
|
+
# Priority: n > s > w > e (vertical placement preferred over horizontal)
|
|
149
|
+
if 'n' in anchor:
|
|
150
|
+
primary = 'n'
|
|
151
|
+
elif 's' in anchor:
|
|
152
|
+
primary = 's'
|
|
153
|
+
elif 'w' in anchor:
|
|
154
|
+
primary = 'w'
|
|
155
|
+
elif 'e' in anchor:
|
|
156
|
+
primary = 'e'
|
|
157
|
+
else:
|
|
158
|
+
# Default to north
|
|
159
|
+
primary = 'n'
|
|
160
|
+
|
|
161
|
+
# Layout based on primary anchor
|
|
162
|
+
if primary == 'n':
|
|
163
|
+
# Label on top
|
|
164
|
+
if self._label:
|
|
165
|
+
self._label.grid(row=0, column=0, sticky='w', pady=(0, 4))
|
|
166
|
+
self._button_container.grid(row=1, column=0, sticky='ew')
|
|
167
|
+
self.grid_columnconfigure(0, weight=1)
|
|
168
|
+
elif primary == 's':
|
|
169
|
+
# Label on bottom
|
|
170
|
+
self._button_container.grid(row=0, column=0, sticky='ew')
|
|
171
|
+
if self._label:
|
|
172
|
+
self._label.grid(row=1, column=0, sticky='w', pady=(4, 0))
|
|
173
|
+
self.grid_columnconfigure(0, weight=1)
|
|
174
|
+
elif primary == 'w':
|
|
175
|
+
# Label on left
|
|
176
|
+
if self._label:
|
|
177
|
+
self._label.grid(row=0, column=0, sticky='w', padx=(0, 8))
|
|
178
|
+
self._button_container.grid(row=0, column=1, sticky='ew')
|
|
179
|
+
self.grid_columnconfigure(1, weight=1)
|
|
180
|
+
else: # 'e'
|
|
181
|
+
# Label on right
|
|
182
|
+
self._button_container.grid(row=0, column=0, sticky='ew')
|
|
183
|
+
if self._label:
|
|
184
|
+
self._label.grid(row=0, column=1, sticky='w', padx=(8, 0))
|
|
185
|
+
self.grid_columnconfigure(0, weight=1)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def variable(self) -> 'tk.Variable':
|
|
189
|
+
"""Get the underlying tk.Variable."""
|
|
190
|
+
return self._variable
|
|
191
|
+
|
|
192
|
+
@variable.setter
|
|
193
|
+
def variable(self, value: 'tk.Variable') -> None:
|
|
194
|
+
"""Set the variable."""
|
|
195
|
+
from bootstack.core.signals import Signal
|
|
196
|
+
self._variable = value
|
|
197
|
+
self._signal = Signal.from_variable(value)
|
|
198
|
+
# Update all buttons to use new variable
|
|
199
|
+
for button in self._buttons.values():
|
|
200
|
+
button.configure(variable=value)
|
|
201
|
+
|
|
202
|
+
@property
|
|
203
|
+
def signal(self) -> 'Signal[Any]':
|
|
204
|
+
"""Get the signal."""
|
|
205
|
+
return self._signal
|
|
206
|
+
|
|
207
|
+
@signal.setter
|
|
208
|
+
def signal(self, value: 'Signal[Any]') -> None:
|
|
209
|
+
"""Set the signal."""
|
|
210
|
+
self._signal = value
|
|
211
|
+
self._variable = value.var
|
|
212
|
+
# Update all buttons to use new variable
|
|
213
|
+
for button in self._buttons.values():
|
|
214
|
+
button.configure(variable=value.var)
|
|
215
|
+
|
|
216
|
+
def add(self, text: str = None, value: Any = None, key: str | None = None, **kwargs: Any) -> RadioButton:
|
|
217
|
+
"""Add a radio button to the group.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
text: Text to display on the button.
|
|
221
|
+
value: Value this button represents (required).
|
|
222
|
+
key: Unique identifier. Defaults to value.
|
|
223
|
+
**kwargs: Additional arguments passed to RadioButton.
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
The created RadioButton widget.
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
ValueError: If value is None or if a button with the same key already exists.
|
|
230
|
+
"""
|
|
231
|
+
if value is None:
|
|
232
|
+
raise ValueError("The 'value' argument is required.")
|
|
233
|
+
|
|
234
|
+
key = key or value
|
|
235
|
+
if key in self._buttons:
|
|
236
|
+
raise ValueError(f"A button with the key '{key}' already exists.")
|
|
237
|
+
|
|
238
|
+
btn_kwargs = kwargs.copy()
|
|
239
|
+
# Use accent for button styling
|
|
240
|
+
if 'accent' not in btn_kwargs and 'bootstyle' not in btn_kwargs:
|
|
241
|
+
btn_kwargs['accent'] = self._accent
|
|
242
|
+
# Apply current state if not explicitly provided
|
|
243
|
+
if 'state' not in btn_kwargs:
|
|
244
|
+
btn_kwargs['state'] = self._state
|
|
245
|
+
|
|
246
|
+
button = RadioButton(
|
|
247
|
+
self._button_container,
|
|
248
|
+
text=text,
|
|
249
|
+
value=value,
|
|
250
|
+
variable=self.variable,
|
|
251
|
+
**btn_kwargs
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
if self._orientation == 'horizontal':
|
|
255
|
+
button.pack(side='left', padx=2)
|
|
256
|
+
else: # vertical
|
|
257
|
+
button.pack(side='top', anchor='w', pady=2)
|
|
258
|
+
|
|
259
|
+
self._buttons[key] = button
|
|
260
|
+
return button
|
|
261
|
+
|
|
262
|
+
def get(self) -> str:
|
|
263
|
+
"""Return the currently selected value."""
|
|
264
|
+
return self.variable.get()
|
|
265
|
+
|
|
266
|
+
def set(self, value: str) -> None:
|
|
267
|
+
"""Set the selected value.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
value: The value to select (must match a button's value).
|
|
271
|
+
|
|
272
|
+
Raises:
|
|
273
|
+
TypeError: If value is not a string.
|
|
274
|
+
ValueError: If value doesn't exist in the group.
|
|
275
|
+
"""
|
|
276
|
+
if not isinstance(value, str):
|
|
277
|
+
raise TypeError(f"RadioGroup requires a string value, got {type(value).__name__}")
|
|
278
|
+
|
|
279
|
+
# Allow empty string (deselection)
|
|
280
|
+
if value and value not in self._buttons:
|
|
281
|
+
valid_values = list(self._buttons.keys())
|
|
282
|
+
raise ValueError(
|
|
283
|
+
f"Value '{value}' not found in group. "
|
|
284
|
+
f"Valid values: {valid_values}"
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
self.variable.set(value)
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def value(self) -> str:
|
|
291
|
+
"""Get or set the selected value."""
|
|
292
|
+
return self.get()
|
|
293
|
+
|
|
294
|
+
@value.setter
|
|
295
|
+
def value(self, value: str) -> None:
|
|
296
|
+
self.set(value)
|
|
297
|
+
|
|
298
|
+
def remove(self, key: str):
|
|
299
|
+
"""Remove a button by its key.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
key: The key of the button to remove.
|
|
303
|
+
|
|
304
|
+
Raises:
|
|
305
|
+
KeyError: If no button with the given key exists.
|
|
306
|
+
"""
|
|
307
|
+
if key not in self._buttons:
|
|
308
|
+
raise KeyError(f"No button with key '{key}'")
|
|
309
|
+
button = self._buttons.pop(key)
|
|
310
|
+
button.destroy()
|
|
311
|
+
|
|
312
|
+
def items(self) -> tuple[RadioButton, ...]:
|
|
313
|
+
"""Get all button widgets in the group.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
A tuple of all RadioButton instances in the group.
|
|
317
|
+
"""
|
|
318
|
+
return tuple(self._buttons.values())
|
|
319
|
+
|
|
320
|
+
def item(self, key: str) -> RadioButton:
|
|
321
|
+
"""Get a button by its key.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
key: The key of the button to retrieve.
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
The RadioButton instance.
|
|
328
|
+
|
|
329
|
+
Raises:
|
|
330
|
+
KeyError: If no button with the given key exists.
|
|
331
|
+
"""
|
|
332
|
+
if key not in self._buttons:
|
|
333
|
+
raise KeyError(f"No button with key '{key}'")
|
|
334
|
+
return self._buttons[key]
|
|
335
|
+
|
|
336
|
+
def configure_item(self, key: str, option: str = None, **kwargs: Any):
|
|
337
|
+
"""Configure a specific button by its key.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
key: The key of the button to configure.
|
|
341
|
+
option: If provided, return the value of this option.
|
|
342
|
+
**kwargs: Configuration options to apply to the button.
|
|
343
|
+
|
|
344
|
+
Returns:
|
|
345
|
+
If option is provided, returns the value of that option.
|
|
346
|
+
"""
|
|
347
|
+
button = self.item(key)
|
|
348
|
+
if option is not None:
|
|
349
|
+
return button.cget(option)
|
|
350
|
+
button.configure(**kwargs)
|
|
351
|
+
|
|
352
|
+
def on_changed(self, callback: Callable) -> Any:
|
|
353
|
+
"""Subscribe to value changes. Callback receives `new_value: str` directly."""
|
|
354
|
+
return self._signal.subscribe(callback)
|
|
355
|
+
|
|
356
|
+
def off_changed(self, bind_id: Any) -> None:
|
|
357
|
+
"""Unsubscribe from value changes."""
|
|
358
|
+
self._signal.unsubscribe(bind_id)
|
|
359
|
+
|
|
360
|
+
@configure_delegate('accent')
|
|
361
|
+
def _delegate_accent(self, value=None):
|
|
362
|
+
"""Get or set the accent. Updates all buttons when changed."""
|
|
363
|
+
if value is None:
|
|
364
|
+
return self._accent
|
|
365
|
+
|
|
366
|
+
self._accent = value
|
|
367
|
+
# Update all buttons with new accent
|
|
368
|
+
for button in self._buttons.values():
|
|
369
|
+
button.configure(accent=value)
|
|
370
|
+
|
|
371
|
+
@configure_delegate('orient')
|
|
372
|
+
def _delegate_orient(self, value=None):
|
|
373
|
+
"""Get or set orientation ('horizontal' or 'vertical'). Repacks buttons when changed."""
|
|
374
|
+
if value is None:
|
|
375
|
+
return self._orientation
|
|
376
|
+
|
|
377
|
+
if value not in ('horizontal', 'vertical'):
|
|
378
|
+
raise ValueError("orient must be 'horizontal' or 'vertical'")
|
|
379
|
+
|
|
380
|
+
self._orientation = value
|
|
381
|
+
|
|
382
|
+
# Repack all buttons in new orientation
|
|
383
|
+
for button in self._buttons.values():
|
|
384
|
+
button.pack_forget()
|
|
385
|
+
if self._orientation == 'horizontal':
|
|
386
|
+
button.pack(side='left', padx=2)
|
|
387
|
+
else:
|
|
388
|
+
button.pack(side='top', anchor='w', pady=2)
|
|
389
|
+
|
|
390
|
+
@configure_delegate('labelanchor')
|
|
391
|
+
def _delegate_labelanchor(self, value=None):
|
|
392
|
+
"""Get or set label anchor position. Updates layout when changed."""
|
|
393
|
+
if value is None:
|
|
394
|
+
return self._labelanchor
|
|
395
|
+
|
|
396
|
+
valid_anchors = ('n', 's', 'e', 'w', 'ne', 'nw', 'se', 'sw')
|
|
397
|
+
if value not in valid_anchors:
|
|
398
|
+
raise ValueError(f"labelanchor must be one of {valid_anchors}")
|
|
399
|
+
|
|
400
|
+
self._labelanchor = value
|
|
401
|
+
self._update_layout()
|
|
402
|
+
|
|
403
|
+
@configure_delegate('text')
|
|
404
|
+
def _delegate_text(self, value=None):
|
|
405
|
+
"""Get or set label text. Creates or removes label as needed."""
|
|
406
|
+
if value is None:
|
|
407
|
+
return self._labeltext if self._labeltext else ''
|
|
408
|
+
|
|
409
|
+
self._labeltext = value
|
|
410
|
+
if value and not self._label:
|
|
411
|
+
# Create label if it doesn't exist
|
|
412
|
+
self._label = Label(self, text=value)
|
|
413
|
+
self._update_layout()
|
|
414
|
+
elif self._label:
|
|
415
|
+
if value:
|
|
416
|
+
self._label.configure(text=value)
|
|
417
|
+
else:
|
|
418
|
+
# Remove label if text is empty
|
|
419
|
+
self._label.destroy()
|
|
420
|
+
self._label = None
|
|
421
|
+
self._update_layout()
|
|
422
|
+
|
|
423
|
+
@configure_delegate('value')
|
|
424
|
+
def _delegate_value(self, value=None):
|
|
425
|
+
"""Get or set the selected value."""
|
|
426
|
+
if value is None:
|
|
427
|
+
return self.get()
|
|
428
|
+
self.set(value)
|
|
429
|
+
|
|
430
|
+
@configure_delegate('state')
|
|
431
|
+
def _delegate_state(self, value=None):
|
|
432
|
+
"""Get or set state for all buttons ('normal' or 'disabled')."""
|
|
433
|
+
if value is None:
|
|
434
|
+
return self._state
|
|
435
|
+
|
|
436
|
+
if value not in ('normal', 'disabled'):
|
|
437
|
+
raise ValueError("state must be 'normal' or 'disabled'")
|
|
438
|
+
|
|
439
|
+
self._state = value
|
|
440
|
+
# Update all buttons with new state
|
|
441
|
+
for button in self._buttons.values():
|
|
442
|
+
button.configure(state=value)
|
|
443
|
+
|
|
444
|
+
def keys(self) -> tuple[str, ...]:
|
|
445
|
+
"""Get all button keys.
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
A tuple of all button keys in the group.
|
|
449
|
+
"""
|
|
450
|
+
return tuple(self._buttons.keys())
|
|
451
|
+
|
|
452
|
+
def values(self) -> tuple[str, ...]:
|
|
453
|
+
"""Get all possible values.
|
|
454
|
+
|
|
455
|
+
Returns:
|
|
456
|
+
A tuple of all values that can be selected.
|
|
457
|
+
"""
|
|
458
|
+
return tuple(self._buttons.keys())
|
|
459
|
+
|
|
460
|
+
def __len__(self) -> int:
|
|
461
|
+
"""Return the number of buttons in the group."""
|
|
462
|
+
return len(self._buttons)
|
|
463
|
+
|
|
464
|
+
def __contains__(self, key: str) -> bool:
|
|
465
|
+
"""Check if a button with the given key exists in the group."""
|
|
466
|
+
return key in self._buttons
|