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,983 @@
|
|
|
1
|
+
"""Dynamic form widget for building data entry layouts quickly."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import date, datetime
|
|
7
|
+
from tkinter import BooleanVar, DoubleVar, IntVar, StringVar, Text, Variable
|
|
8
|
+
from typing import Any, Callable, Iterable, Literal, Mapping, Sequence, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from bootstack.constants import DEFAULT_MIN_COL_WIDTH
|
|
11
|
+
from bootstack.widgets.primitives.button import Button
|
|
12
|
+
from bootstack.widgets.primitives.checkbutton import CheckButton
|
|
13
|
+
from bootstack.widgets.primitives.switch import Switch
|
|
14
|
+
from bootstack.widgets.composites.dateentry import DateEntry
|
|
15
|
+
from bootstack.widgets.composites.field import Field
|
|
16
|
+
from bootstack.widgets.primitives.frame import Frame
|
|
17
|
+
from bootstack.widgets.primitives.label import Label
|
|
18
|
+
from bootstack.widgets.primitives.labelframe import LabelFrame
|
|
19
|
+
from bootstack.widgets.mixins import configure_delegate
|
|
20
|
+
from bootstack.widgets.primitives.notebook import Notebook
|
|
21
|
+
from bootstack.widgets.composites.numericentry import NumericEntry
|
|
22
|
+
from bootstack.widgets.composites.passwordentry import PasswordEntry
|
|
23
|
+
from bootstack.widgets.primitives.scale import Scale
|
|
24
|
+
from bootstack.widgets.composites.selectbox import SelectBox
|
|
25
|
+
from bootstack.widgets.primitives.spinbox import Spinbox
|
|
26
|
+
from bootstack.widgets.composites.textentry import TextEntry
|
|
27
|
+
from bootstack.widgets.mixins.validation_mixin import ValidationMixin
|
|
28
|
+
from bootstack.widgets.types import Master
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from bootstack.dialogs.dialog import DialogButton
|
|
32
|
+
ButtonInput = str | Mapping[str, Any] | DialogButton
|
|
33
|
+
|
|
34
|
+
DType = Literal['int', 'float', 'bool', 'date', 'datetime', 'password', 'str'] | type | None
|
|
35
|
+
|
|
36
|
+
EditorType = Literal[
|
|
37
|
+
'selectbox',
|
|
38
|
+
'combobox',
|
|
39
|
+
'spinbox',
|
|
40
|
+
'text',
|
|
41
|
+
'textentry',
|
|
42
|
+
'numericentry',
|
|
43
|
+
'dateentry',
|
|
44
|
+
'passwordentry',
|
|
45
|
+
'toggle',
|
|
46
|
+
'switch',
|
|
47
|
+
'checkbutton',
|
|
48
|
+
'scale',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class FieldItem:
|
|
54
|
+
"""Field definition used by Form."""
|
|
55
|
+
key: str
|
|
56
|
+
label: str | None = None
|
|
57
|
+
dtype: DType = None
|
|
58
|
+
readonly: bool = False
|
|
59
|
+
visible: bool = True
|
|
60
|
+
column: int | None = None
|
|
61
|
+
row: int | None = None
|
|
62
|
+
columnspan: int = 1
|
|
63
|
+
rowspan: int = 1
|
|
64
|
+
editor: EditorType | None = None
|
|
65
|
+
editor_options: dict[str, Any] = field(default_factory=dict)
|
|
66
|
+
type: Literal['field'] = "field"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class GroupItem:
|
|
71
|
+
"""Grouping of field items laid out in a grid with optional label/padding."""
|
|
72
|
+
items: list[FieldItem | Mapping[str, Any] | GroupItem | TabsItem] = field(default_factory=list)
|
|
73
|
+
label: str | None = None
|
|
74
|
+
col_count: int = 1
|
|
75
|
+
min_col_width: int = DEFAULT_MIN_COL_WIDTH
|
|
76
|
+
width: int | None = None
|
|
77
|
+
height: int | None = None
|
|
78
|
+
column: int | None = None
|
|
79
|
+
row: int | None = None
|
|
80
|
+
columnspan: int = 1
|
|
81
|
+
rowspan: int = 1
|
|
82
|
+
padding: int | str | tuple[int, int] | tuple[int, int, int, int] | None = 8
|
|
83
|
+
type: Literal['group'] = "group"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class TabItem:
|
|
88
|
+
"""Single tab within a TabsItem."""
|
|
89
|
+
label: str
|
|
90
|
+
items: list[FieldItem | Mapping[str, Any] | GroupItem | TabsItem] = field(default_factory=list)
|
|
91
|
+
padding: int | str | tuple[int, int] | tuple[int, int, int, int] | None = 8
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class TabsItem:
|
|
96
|
+
"""Notebook container with one or more TabItem entries."""
|
|
97
|
+
tabs: list[TabItem | Mapping[str, Any]] = field(default_factory=list)
|
|
98
|
+
label: str | None = None
|
|
99
|
+
width: int | None = None
|
|
100
|
+
height: int | None = None
|
|
101
|
+
column: int | None = None
|
|
102
|
+
row: int | None = None
|
|
103
|
+
columnspan: int = 1
|
|
104
|
+
rowspan: int = 1
|
|
105
|
+
type: Literal['tabs'] = "tabs"
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
FormItem = FieldItem | GroupItem | TabsItem
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class Form(Frame):
|
|
112
|
+
"""A configurable form that can be generated from data or explicit items.
|
|
113
|
+
|
|
114
|
+
Form is a *field manager* that provides a domain-specific API for accessing
|
|
115
|
+
and manipulating form fields and their values.
|
|
116
|
+
|
|
117
|
+
Field Access:
|
|
118
|
+
- `field(key)` — returns the Field widget for a key
|
|
119
|
+
- `fields()` — returns all Field widgets in order
|
|
120
|
+
- `keys()` — returns all field keys in order
|
|
121
|
+
|
|
122
|
+
Value Operations:
|
|
123
|
+
- `get_field_value(key)` — get a single field's value
|
|
124
|
+
- `set_field_value(key, value)` — set a single field's value
|
|
125
|
+
- `get()` / `set(values)` — get/set all values as a dict
|
|
126
|
+
- `value` property — get/set all values as a dict
|
|
127
|
+
|
|
128
|
+
Variable & Signal Access:
|
|
129
|
+
- `field_variable(key)` — get Tk Variable for a field
|
|
130
|
+
- `field_signal(key)` — get Signal for a field's value
|
|
131
|
+
- `field_textsignal(key)` — get Signal for a field's text
|
|
132
|
+
|
|
133
|
+
Attributes:
|
|
134
|
+
data (dict): Current form data (read-only property).
|
|
135
|
+
value (dict): Alias for form data (get/set property).
|
|
136
|
+
result (Any): Result value set by button commands.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
master: Parent widget.
|
|
140
|
+
data: Initial data backing the form. If items are not provided,
|
|
141
|
+
field items are inferred from the keys and value types.
|
|
142
|
+
items: Optional explicit form definition. Accepts dictionaries that
|
|
143
|
+
match the FieldItem/GroupItem/TabsItem shapes or the dataclass
|
|
144
|
+
instances directly.
|
|
145
|
+
col_count: Number of columns at the top level.
|
|
146
|
+
min_col_width: Minimum width for each column in pixels.
|
|
147
|
+
on_data_changed: Optional callback invoked with the updated data dict
|
|
148
|
+
whenever a field value changes.
|
|
149
|
+
width: Requested width for the form container.
|
|
150
|
+
height: Requested height for the form container.
|
|
151
|
+
accent: Accent token for the form container (e.g., 'primary', 'secondary').
|
|
152
|
+
buttons: Optional footer buttons. Accepts plain strings, DialogButton
|
|
153
|
+
instances, or dictionaries that map to DialogButton kwargs.
|
|
154
|
+
**kwargs: Additional Frame configuration options.
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def __init__(
|
|
158
|
+
self,
|
|
159
|
+
master: Master = None,
|
|
160
|
+
*,
|
|
161
|
+
data: dict[str, Any] | None = None,
|
|
162
|
+
items: Sequence[FormItem | Mapping[str, Any]] | None = None,
|
|
163
|
+
col_count: int = 1,
|
|
164
|
+
min_col_width: int = DEFAULT_MIN_COL_WIDTH,
|
|
165
|
+
on_data_changed: Callable[[dict[str, Any]], Any] | None = None,
|
|
166
|
+
width: int | None = None,
|
|
167
|
+
height: int | None = None,
|
|
168
|
+
accent: str | None = None,
|
|
169
|
+
buttons: Sequence[ButtonInput] | None = None,
|
|
170
|
+
**kwargs: Any,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""Build a configurable form from data or explicit items.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
master: Parent widget.
|
|
176
|
+
data: Initial data backing the form; keys become field names.
|
|
177
|
+
items: Explicit form layout (FieldItem/GroupItem/TabsItem or mappings).
|
|
178
|
+
col_count: Number of columns at the top level.
|
|
179
|
+
min_col_width: Minimum width per column in pixels.
|
|
180
|
+
on_data_changed: Callback invoked with updated data when a field changes.
|
|
181
|
+
width: Requested form width; if None, size naturally.
|
|
182
|
+
height: Requested form height; if None, size naturally.
|
|
183
|
+
accent: Accent token for the form container.
|
|
184
|
+
buttons: Optional footer buttons (DialogButton, mapping, or string).
|
|
185
|
+
**kwargs: Additional Frame configuration options.
|
|
186
|
+
"""
|
|
187
|
+
# Support legacy bootstyle parameter
|
|
188
|
+
if 'bootstyle' in kwargs:
|
|
189
|
+
accent = accent or kwargs.pop('bootstyle')
|
|
190
|
+
super().__init__(master=master, width=width, height=height, accent=accent, **kwargs)
|
|
191
|
+
|
|
192
|
+
self._data: dict[str, Any] = dict(data) if data else {}
|
|
193
|
+
self.result: Any = None
|
|
194
|
+
self._on_data_changed = on_data_changed
|
|
195
|
+
self._col_count = col_count
|
|
196
|
+
self._min_col_width = min_col_width
|
|
197
|
+
self._widgets: dict[str, Any] = {}
|
|
198
|
+
self._variables: dict[str, Variable] = {}
|
|
199
|
+
self._signals: dict[str, Any] = {}
|
|
200
|
+
self._textsignals: dict[str, Any] = {}
|
|
201
|
+
self._items_by_key: dict[str, FieldItem] = {}
|
|
202
|
+
self._suspend_sync = False
|
|
203
|
+
|
|
204
|
+
normalized_items = self._normalize_items(items or self._infer_items_from_data(self._data))
|
|
205
|
+
|
|
206
|
+
self.columnconfigure(0, weight=1)
|
|
207
|
+
self.rowconfigure(0, weight=1)
|
|
208
|
+
|
|
209
|
+
container = Frame(self)
|
|
210
|
+
container.grid(row=0, column=0, sticky='nsew')
|
|
211
|
+
self._content_frame = Frame(container)
|
|
212
|
+
self._content_frame.pack(fill='both', expand=True)
|
|
213
|
+
|
|
214
|
+
# Respect explicit width/height by preventing geometry propagation.
|
|
215
|
+
if width or height:
|
|
216
|
+
if width:
|
|
217
|
+
self.configure(width=width)
|
|
218
|
+
container.configure(width=width)
|
|
219
|
+
if height:
|
|
220
|
+
self.configure(height=height)
|
|
221
|
+
container.configure(height=height)
|
|
222
|
+
self.grid_propagate(False)
|
|
223
|
+
self.pack_propagate(False)
|
|
224
|
+
container.grid_propagate(False)
|
|
225
|
+
|
|
226
|
+
self._build_items(
|
|
227
|
+
self._content_frame, normalized_items, col_count=self._col_count, min_col_width=self._min_col_width)
|
|
228
|
+
|
|
229
|
+
if buttons:
|
|
230
|
+
footer = Frame(self)
|
|
231
|
+
footer.grid(row=1, column=0, sticky='ew', pady=(8, 0))
|
|
232
|
+
footer.columnconfigure(0, weight=1)
|
|
233
|
+
self._build_buttons(footer, buttons)
|
|
234
|
+
|
|
235
|
+
@property
|
|
236
|
+
def data(self) -> dict[str, Any]:
|
|
237
|
+
"""Current data backing the form."""
|
|
238
|
+
return dict(self._collect_data())
|
|
239
|
+
|
|
240
|
+
def validate(self) -> bool:
|
|
241
|
+
"""Run validation rules on all field widgets; returns True if all pass."""
|
|
242
|
+
all_valid = True
|
|
243
|
+
first_invalid_widget = None
|
|
244
|
+
|
|
245
|
+
def _validate_field(widget: Field) -> bool:
|
|
246
|
+
entry = getattr(widget, "_entry", widget)
|
|
247
|
+
rules = getattr(entry, "_rules", [])
|
|
248
|
+
if not rules:
|
|
249
|
+
return True
|
|
250
|
+
value = widget.value
|
|
251
|
+
payload: dict[str, Any] = {"value": value, "is_valid": True, "message": ""}
|
|
252
|
+
is_valid = True
|
|
253
|
+
for rule in rules:
|
|
254
|
+
if rule.trigger not in ("always", "manual"):
|
|
255
|
+
continue
|
|
256
|
+
result = rule.validate(value)
|
|
257
|
+
payload.update(is_valid=result.is_valid, message=result.message)
|
|
258
|
+
if not result.is_valid:
|
|
259
|
+
is_valid = False
|
|
260
|
+
try:
|
|
261
|
+
entry.event_generate(ValidationMixin.EVENT_INVALID, data=payload)
|
|
262
|
+
entry.event_generate(ValidationMixin.EVENT_VALIDATED, data=payload)
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
break
|
|
266
|
+
if is_valid:
|
|
267
|
+
try:
|
|
268
|
+
entry.event_generate(ValidationMixin.EVENT_VALID, data=payload)
|
|
269
|
+
entry.event_generate(ValidationMixin.EVENT_VALIDATED, data=payload)
|
|
270
|
+
except Exception:
|
|
271
|
+
pass
|
|
272
|
+
return is_valid
|
|
273
|
+
|
|
274
|
+
for widget in self._widgets.values():
|
|
275
|
+
if isinstance(widget, Field):
|
|
276
|
+
ok = _validate_field(widget)
|
|
277
|
+
if not ok and first_invalid_widget is None:
|
|
278
|
+
first_invalid_widget = widget
|
|
279
|
+
all_valid = all_valid and ok
|
|
280
|
+
if first_invalid_widget:
|
|
281
|
+
try:
|
|
282
|
+
first_invalid_widget.focus_set()
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
return all_valid
|
|
286
|
+
|
|
287
|
+
# --- Field access API (v2) -------------------------------------------
|
|
288
|
+
|
|
289
|
+
def field(self, key: str) -> Field:
|
|
290
|
+
"""Return the Field widget for the given key.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
key: The field key.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
The Field widget instance.
|
|
297
|
+
|
|
298
|
+
Raises:
|
|
299
|
+
KeyError: If no field with the given key exists.
|
|
300
|
+
"""
|
|
301
|
+
if key not in self._widgets:
|
|
302
|
+
raise KeyError(f"No field with key '{key}'")
|
|
303
|
+
return self._widgets[key]
|
|
304
|
+
|
|
305
|
+
def fields(self) -> tuple[Field, ...]:
|
|
306
|
+
"""Return all field widgets in insertion order.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Tuple of Field widget instances.
|
|
310
|
+
"""
|
|
311
|
+
return tuple(self._widgets[k] for k in self._items_by_key.keys() if k in self._widgets)
|
|
312
|
+
|
|
313
|
+
def keys(self) -> tuple[str, ...]:
|
|
314
|
+
"""Return all field keys in insertion order.
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
Tuple of field key strings.
|
|
318
|
+
"""
|
|
319
|
+
return tuple(self._items_by_key.keys())
|
|
320
|
+
|
|
321
|
+
# --- Value helpers (explicit, no overloading) -------------------------
|
|
322
|
+
|
|
323
|
+
def get_field_value(self, key: str) -> Any:
|
|
324
|
+
"""Return the current value of the field.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
key: The field key.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
The current field value.
|
|
331
|
+
|
|
332
|
+
Raises:
|
|
333
|
+
KeyError: If no field with the given key exists.
|
|
334
|
+
"""
|
|
335
|
+
if key not in self._widgets:
|
|
336
|
+
raise KeyError(f"No field with key '{key}'")
|
|
337
|
+
return self._read_value_from_widget(key)
|
|
338
|
+
|
|
339
|
+
def set_field_value(self, key: str, value: Any) -> None:
|
|
340
|
+
"""Set the value of the field.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
key: The field key.
|
|
344
|
+
value: The new value to set.
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
KeyError: If no field with the given key exists.
|
|
348
|
+
"""
|
|
349
|
+
if key not in self._items_by_key:
|
|
350
|
+
raise KeyError(f"No field with key '{key}'")
|
|
351
|
+
item = self._items_by_key[key]
|
|
352
|
+
self._apply_value_to_widget(key, item, value)
|
|
353
|
+
self._data[key] = value
|
|
354
|
+
|
|
355
|
+
# --- Variable & signal accessors (v2 short names) ---------------------
|
|
356
|
+
|
|
357
|
+
def field_variable(self, key: str) -> Variable | None:
|
|
358
|
+
"""Return the Tk Variable for the field.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
key: The field key.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
The Tk Variable, or None if not available.
|
|
365
|
+
"""
|
|
366
|
+
return self._variables.get(key)
|
|
367
|
+
|
|
368
|
+
def field_signal(self, key: str):
|
|
369
|
+
"""Return the Signal for the field value.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
key: The field key.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
The Signal, or None if not available.
|
|
376
|
+
"""
|
|
377
|
+
return self._signals.get(key)
|
|
378
|
+
|
|
379
|
+
def field_textsignal(self, key: str):
|
|
380
|
+
"""Return the Signal for the field text.
|
|
381
|
+
|
|
382
|
+
Args:
|
|
383
|
+
key: The field key.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
The text Signal, or None if not available.
|
|
387
|
+
"""
|
|
388
|
+
return self._textsignals.get(key)
|
|
389
|
+
|
|
390
|
+
# --- Legacy variable/signal accessors (backward compatibility) --------
|
|
391
|
+
|
|
392
|
+
def get_field_variable(self, key: str) -> Variable | None:
|
|
393
|
+
"""Return the Tk variable associated with a field key, if any.
|
|
394
|
+
|
|
395
|
+
Deprecated:
|
|
396
|
+
Use `field_variable(key)` instead.
|
|
397
|
+
"""
|
|
398
|
+
return self.field_variable(key)
|
|
399
|
+
|
|
400
|
+
def get_field_signal(self, key: str):
|
|
401
|
+
"""Return the Signal associated with a field key, if any.
|
|
402
|
+
|
|
403
|
+
Deprecated:
|
|
404
|
+
Use `field_signal(key)` instead.
|
|
405
|
+
"""
|
|
406
|
+
return self.field_signal(key)
|
|
407
|
+
|
|
408
|
+
def get_field_textsignal(self, key: str):
|
|
409
|
+
"""Return the TextSignal associated with a field key, if any.
|
|
410
|
+
|
|
411
|
+
Deprecated:
|
|
412
|
+
Use `field_textsignal(key)` instead.
|
|
413
|
+
"""
|
|
414
|
+
return self.field_textsignal(key)
|
|
415
|
+
|
|
416
|
+
# --- Form-level value API (v2 standardization) ------------------------
|
|
417
|
+
|
|
418
|
+
def get(self) -> dict[str, Any]:
|
|
419
|
+
"""Return all field values as a dictionary.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
Dictionary mapping field keys to their current values.
|
|
423
|
+
"""
|
|
424
|
+
return self.data
|
|
425
|
+
|
|
426
|
+
def set(self, values: Mapping[str, Any]) -> None:
|
|
427
|
+
"""Set multiple field values from a dictionary.
|
|
428
|
+
|
|
429
|
+
Args:
|
|
430
|
+
values: Dictionary mapping field keys to values.
|
|
431
|
+
"""
|
|
432
|
+
self._data = dict(values)
|
|
433
|
+
self._suspend_sync = True
|
|
434
|
+
try:
|
|
435
|
+
for key, item in self._items_by_key.items():
|
|
436
|
+
value = self._data.get(key)
|
|
437
|
+
self._apply_value_to_widget(key, item, value)
|
|
438
|
+
finally:
|
|
439
|
+
self._suspend_sync = False
|
|
440
|
+
|
|
441
|
+
@property
|
|
442
|
+
def value(self) -> dict[str, Any]:
|
|
443
|
+
"""Get or set all form field values as a dictionary."""
|
|
444
|
+
return self.get()
|
|
445
|
+
|
|
446
|
+
@value.setter
|
|
447
|
+
def value(self, values: Mapping[str, Any]) -> None:
|
|
448
|
+
self.set(values)
|
|
449
|
+
|
|
450
|
+
@configure_delegate('data')
|
|
451
|
+
def _delegate_data(self, value: Mapping[str, Any] = None):
|
|
452
|
+
if value is None:
|
|
453
|
+
return dict(self._collect_data())
|
|
454
|
+
else:
|
|
455
|
+
self._data = dict(value)
|
|
456
|
+
self._suspend_sync = True
|
|
457
|
+
try:
|
|
458
|
+
for key, item in self._items_by_key.items():
|
|
459
|
+
value = self._data.get(key)
|
|
460
|
+
self._apply_value_to_widget(key, item, value)
|
|
461
|
+
finally:
|
|
462
|
+
self._suspend_sync = False
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
def _build_items(self, parent: Frame, items: Sequence[FormItem], *, col_count: int, min_col_width: int) -> None:
|
|
466
|
+
for col in range(col_count):
|
|
467
|
+
parent.columnconfigure(col, weight=1, minsize=min_col_width)
|
|
468
|
+
|
|
469
|
+
auto_row = 0
|
|
470
|
+
auto_col = 0
|
|
471
|
+
|
|
472
|
+
for item in items:
|
|
473
|
+
widget = None
|
|
474
|
+
columnspan = 1
|
|
475
|
+
rowspan = 1
|
|
476
|
+
|
|
477
|
+
if isinstance(item, FieldItem):
|
|
478
|
+
widget = self._build_field(parent, item)
|
|
479
|
+
columnspan = item.columnspan
|
|
480
|
+
rowspan = item.rowspan
|
|
481
|
+
elif isinstance(item, GroupItem):
|
|
482
|
+
container = LabelFrame(parent, text=item.label, padding=item.padding) if item.label else Frame(
|
|
483
|
+
parent, padding=item.padding)
|
|
484
|
+
if item.width:
|
|
485
|
+
container.configure(width=item.width)
|
|
486
|
+
if item.height:
|
|
487
|
+
container.configure(height=item.height)
|
|
488
|
+
nested_items = self._normalize_items(item.items)
|
|
489
|
+
self._build_items(
|
|
490
|
+
container,
|
|
491
|
+
nested_items,
|
|
492
|
+
col_count=item.col_count or col_count,
|
|
493
|
+
min_col_width=item.min_col_width or min_col_width,
|
|
494
|
+
)
|
|
495
|
+
widget = container
|
|
496
|
+
columnspan = item.columnspan
|
|
497
|
+
rowspan = item.rowspan
|
|
498
|
+
elif isinstance(item, TabsItem):
|
|
499
|
+
notebook = Notebook(parent, width=item.width, height=item.height)
|
|
500
|
+
for tab in self._normalize_tabs(item.tabs):
|
|
501
|
+
tab_frame = Frame(notebook, padding=tab.padding)
|
|
502
|
+
self._build_items(
|
|
503
|
+
tab_frame, self._normalize_items(tab.items), col_count=col_count, min_col_width=min_col_width)
|
|
504
|
+
notebook.add(tab_frame, text=tab.label)
|
|
505
|
+
widget = notebook
|
|
506
|
+
columnspan = item.columnspan
|
|
507
|
+
rowspan = item.rowspan
|
|
508
|
+
|
|
509
|
+
if widget is None:
|
|
510
|
+
continue
|
|
511
|
+
|
|
512
|
+
row = item.row if isinstance(item, (FieldItem, GroupItem, TabsItem)) and item.row is not None else auto_row
|
|
513
|
+
column = item.column if isinstance(
|
|
514
|
+
item, (FieldItem, GroupItem, TabsItem)) and item.column is not None else auto_col
|
|
515
|
+
|
|
516
|
+
widget.grid(row=row, column=column, columnspan=columnspan, rowspan=rowspan, sticky='nsew', padx=6, pady=4)
|
|
517
|
+
|
|
518
|
+
if isinstance(item, (FieldItem, GroupItem, TabsItem)) and item.row is None and item.column is None:
|
|
519
|
+
auto_col += columnspan
|
|
520
|
+
if auto_col >= col_count:
|
|
521
|
+
auto_col = 0
|
|
522
|
+
auto_row += 1
|
|
523
|
+
|
|
524
|
+
def _build_field(self, parent: Frame, item: FieldItem):
|
|
525
|
+
if not item.visible:
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
editor = item.editor or self._default_editor_for_dtype(item.dtype, self._data.get(item.key))
|
|
529
|
+
options = dict(item.editor_options or {})
|
|
530
|
+
initial_value = self._data.get(item.key)
|
|
531
|
+
variable = self._variable_for_item(item, initial_value, editor)
|
|
532
|
+
label_text = item.label if item.label is not None else item.key.replace("_", " ").title()
|
|
533
|
+
|
|
534
|
+
container = Frame(parent)
|
|
535
|
+
container.columnconfigure(0, weight=1) # Allow field widgets to expand horizontally
|
|
536
|
+
|
|
537
|
+
# Validation options that should only be passed to widgets supporting ValidationMixin
|
|
538
|
+
validation_options = {'show_message', 'required', 'validator'}
|
|
539
|
+
|
|
540
|
+
field_widget: Any
|
|
541
|
+
if editor == 'textentry':
|
|
542
|
+
field_widget = TextEntry(
|
|
543
|
+
container, value=initial_value or "", label=label_text, textvariable=variable, **options)
|
|
544
|
+
elif editor == 'numericentry':
|
|
545
|
+
numeric_value = initial_value if initial_value is not None else 0
|
|
546
|
+
field_widget = NumericEntry(
|
|
547
|
+
container, value=numeric_value, label=label_text, **options)
|
|
548
|
+
self._bind_numeric_variable(item.key, field_widget, variable)
|
|
549
|
+
elif editor == 'passwordentry':
|
|
550
|
+
field_widget = PasswordEntry(
|
|
551
|
+
container, value=initial_value or "", label=label_text, textvariable=variable, **options)
|
|
552
|
+
elif editor == 'dateentry':
|
|
553
|
+
field_widget = DateEntry(container, value=initial_value, label=label_text, textvariable=variable, **options)
|
|
554
|
+
else:
|
|
555
|
+
# Filter out validation options for widgets that don't support ValidationMixin
|
|
556
|
+
filtered_options = {k: v for k, v in options.items() if k not in validation_options}
|
|
557
|
+
|
|
558
|
+
# Use inline label for checkbutton/toggle/switch, otherwise show a Label widget.
|
|
559
|
+
if editor in ("checkbutton", "toggle", "switch"):
|
|
560
|
+
if not filtered_options.get("text"):
|
|
561
|
+
filtered_options["text"] = label_text
|
|
562
|
+
elif label_text != "" and editor not in ('selectbox', 'combobox'):
|
|
563
|
+
Label(container, text=label_text).pack(anchor='w', pady=(0, 2))
|
|
564
|
+
|
|
565
|
+
if editor in ('selectbox', 'combobox'):
|
|
566
|
+
items = options.pop('items', options.pop('values', None)) or []
|
|
567
|
+
items = [str(i) for i in items]
|
|
568
|
+
field_widget = SelectBox(
|
|
569
|
+
container,
|
|
570
|
+
label=label_text,
|
|
571
|
+
value=initial_value or "",
|
|
572
|
+
items=items,
|
|
573
|
+
textvariable=variable,
|
|
574
|
+
**options
|
|
575
|
+
)
|
|
576
|
+
if initial_value is not None and variable is not None:
|
|
577
|
+
variable.set(initial_value)
|
|
578
|
+
elif editor == 'spinbox':
|
|
579
|
+
field_widget = Spinbox(container, textvariable=variable, **filtered_options)
|
|
580
|
+
if initial_value is not None:
|
|
581
|
+
variable.set(initial_value)
|
|
582
|
+
elif editor == 'text':
|
|
583
|
+
field_widget = Text(container, **filtered_options)
|
|
584
|
+
if initial_value:
|
|
585
|
+
field_widget.insert('1.0', str(initial_value))
|
|
586
|
+
elif editor in ('toggle', 'switch'):
|
|
587
|
+
field_widget = Switch(container, variable=variable, **filtered_options)
|
|
588
|
+
elif editor == 'checkbutton':
|
|
589
|
+
field_widget = CheckButton(container, variable=variable, **filtered_options)
|
|
590
|
+
elif editor == 'scale':
|
|
591
|
+
field_widget = Scale(container, variable=variable, **filtered_options)
|
|
592
|
+
else:
|
|
593
|
+
field_widget = TextEntry(
|
|
594
|
+
container, value=initial_value or "", label=label_text, textvariable=variable, **options)
|
|
595
|
+
|
|
596
|
+
if editor != 'text':
|
|
597
|
+
field_widget.pack(fill='x', expand=True)
|
|
598
|
+
else:
|
|
599
|
+
field_widget.pack(fill='both', expand=True)
|
|
600
|
+
|
|
601
|
+
if isinstance(field_widget, Field):
|
|
602
|
+
field_widget.bind("<<Change>>", lambda _e, k=item.key: self._sync_value_from_widget(k))
|
|
603
|
+
field_widget.pack(fill='both', expand=True)
|
|
604
|
+
if not isinstance(field_widget, NumericEntry):
|
|
605
|
+
traced_var = getattr(field_widget, "variable", None)
|
|
606
|
+
if traced_var is not None:
|
|
607
|
+
self._register_variable(item.key, traced_var)
|
|
608
|
+
elif isinstance(field_widget, Text):
|
|
609
|
+
text_var = variable or StringVar(value=str(initial_value) if initial_value is not None else "")
|
|
610
|
+
self._register_variable(item.key, text_var)
|
|
611
|
+
self._bind_text_change(field_widget, item.key, text_var)
|
|
612
|
+
elif variable is not None:
|
|
613
|
+
self._register_variable(item.key, variable)
|
|
614
|
+
|
|
615
|
+
# record signals if the widget exposes them
|
|
616
|
+
signal_obj = getattr(field_widget, "_signal", None)
|
|
617
|
+
if signal_obj is not None:
|
|
618
|
+
self._signals[item.key] = signal_obj
|
|
619
|
+
text_signal = getattr(field_widget, "_textsignal", None)
|
|
620
|
+
if text_signal is not None:
|
|
621
|
+
self._textsignals[item.key] = text_signal
|
|
622
|
+
|
|
623
|
+
if item.readonly:
|
|
624
|
+
self._set_readonly(field_widget)
|
|
625
|
+
|
|
626
|
+
self._widgets[item.key] = field_widget
|
|
627
|
+
return container
|
|
628
|
+
|
|
629
|
+
def _build_buttons(self, parent: Frame, buttons: Sequence[ButtonInput]) -> None:
|
|
630
|
+
parsed = self._normalize_buttons(buttons)
|
|
631
|
+
for spec in reversed(parsed):
|
|
632
|
+
# Support both color and legacy bootstyle from DialogButton
|
|
633
|
+
btn_color = getattr(spec, 'color', None) or spec.bootstyle
|
|
634
|
+
btn_variant = getattr(spec, 'variant', None)
|
|
635
|
+
|
|
636
|
+
if not btn_color:
|
|
637
|
+
# Get color and variant from role
|
|
638
|
+
btn_color, btn_variant = self._style_for_role(spec.role)
|
|
639
|
+
|
|
640
|
+
btn = Button(parent, text=spec.text, accent=btn_color, variant=btn_variant)
|
|
641
|
+
btn.configure(command=self._make_button_command(spec))
|
|
642
|
+
btn.pack(side='right', padx=(4, 0))
|
|
643
|
+
|
|
644
|
+
# --- normalization --------------------------------------------------
|
|
645
|
+
def _normalize_items(self, items: Iterable[FormItem | Mapping[str, Any]]) -> list[FormItem]:
|
|
646
|
+
normalized: list[FormItem] = []
|
|
647
|
+
for raw in items:
|
|
648
|
+
item: FormItem | None = None
|
|
649
|
+
if isinstance(raw, (FieldItem, GroupItem, TabsItem)):
|
|
650
|
+
item = raw
|
|
651
|
+
elif isinstance(raw, Mapping):
|
|
652
|
+
type_hint = raw.get('type', 'field')
|
|
653
|
+
if type_hint == 'group':
|
|
654
|
+
item = GroupItem(
|
|
655
|
+
items=list(raw.get('items', [])),
|
|
656
|
+
label=raw.get('label'),
|
|
657
|
+
col_count=raw.get('col_count', 1),
|
|
658
|
+
min_col_width=raw.get('min_col_width', DEFAULT_MIN_COL_WIDTH),
|
|
659
|
+
width=raw.get('width'),
|
|
660
|
+
height=raw.get('height'),
|
|
661
|
+
column=raw.get('column'),
|
|
662
|
+
row=raw.get('row'),
|
|
663
|
+
columnspan=raw.get('columnspan', 1),
|
|
664
|
+
rowspan=raw.get('rowspan', 1),
|
|
665
|
+
)
|
|
666
|
+
elif type_hint == 'tabs':
|
|
667
|
+
item = TabsItem(
|
|
668
|
+
tabs=list(raw.get('tabs', [])),
|
|
669
|
+
label=raw.get('label'),
|
|
670
|
+
width=raw.get('width'),
|
|
671
|
+
height=raw.get('height'),
|
|
672
|
+
column=raw.get('column'),
|
|
673
|
+
row=raw.get('row'),
|
|
674
|
+
columnspan=raw.get('columnspan', 1),
|
|
675
|
+
rowspan=raw.get('rowspan', 1),
|
|
676
|
+
)
|
|
677
|
+
else:
|
|
678
|
+
key_value = raw.get('key')
|
|
679
|
+
if key_value is None:
|
|
680
|
+
continue
|
|
681
|
+
item = FieldItem(
|
|
682
|
+
key=str(key_value),
|
|
683
|
+
label=raw.get('label'),
|
|
684
|
+
dtype=raw.get('dtype'),
|
|
685
|
+
readonly=raw.get('readonly', False),
|
|
686
|
+
visible=raw.get('visible', True),
|
|
687
|
+
column=raw.get('column'),
|
|
688
|
+
row=raw.get('row'),
|
|
689
|
+
columnspan=raw.get('columnspan', 1),
|
|
690
|
+
rowspan=raw.get('rowspan', 1),
|
|
691
|
+
editor=raw.get('editor'),
|
|
692
|
+
editor_options=dict(raw.get('editor_options', {}) or {}),
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
if isinstance(item, GroupItem):
|
|
696
|
+
item.items = self._normalize_items(item.items)
|
|
697
|
+
if isinstance(item, TabsItem):
|
|
698
|
+
item.tabs = self._normalize_tabs(item.tabs)
|
|
699
|
+
if isinstance(item, FieldItem):
|
|
700
|
+
self._items_by_key[item.key] = item
|
|
701
|
+
|
|
702
|
+
if item:
|
|
703
|
+
normalized.append(item)
|
|
704
|
+
return normalized
|
|
705
|
+
|
|
706
|
+
def _normalize_tabs(self, tabs: Iterable[TabItem | Mapping[str, Any]]) -> list[TabItem]:
|
|
707
|
+
normalized: list[TabItem] = []
|
|
708
|
+
for raw in tabs:
|
|
709
|
+
if isinstance(raw, TabItem):
|
|
710
|
+
normalized.append(raw)
|
|
711
|
+
elif isinstance(raw, Mapping):
|
|
712
|
+
normalized.append(TabItem(label=str(raw.get('label', 'Tab')), items=list(raw.get('items', []))))
|
|
713
|
+
return normalized
|
|
714
|
+
|
|
715
|
+
def _normalize_buttons(self, buttons: Sequence[ButtonInput]) -> list["DialogButton"]:
|
|
716
|
+
from bootstack.dialogs.dialog import DialogButton # local import to avoid circular init
|
|
717
|
+
|
|
718
|
+
normalized: list[DialogButton] = []
|
|
719
|
+
for raw in buttons:
|
|
720
|
+
if isinstance(raw, DialogButton):
|
|
721
|
+
normalized.append(raw)
|
|
722
|
+
elif isinstance(raw, Mapping):
|
|
723
|
+
normalized.append(DialogButton(**raw)) # type: ignore[arg-type]
|
|
724
|
+
elif isinstance(raw, str):
|
|
725
|
+
normalized.append(DialogButton(text=raw, role="primary" if not normalized else "secondary"))
|
|
726
|
+
return normalized
|
|
727
|
+
|
|
728
|
+
# --- data helpers ---------------------------------------------------
|
|
729
|
+
def _collect_data(self) -> dict[str, Any]:
|
|
730
|
+
current: dict[str, Any] = dict(self._data)
|
|
731
|
+
for key in self._widgets.keys():
|
|
732
|
+
current[key] = self._read_value_from_widget(key)
|
|
733
|
+
return current
|
|
734
|
+
|
|
735
|
+
def _read_value_from_widget(self, key: str) -> Any:
|
|
736
|
+
widget = self._widgets.get(key)
|
|
737
|
+
if widget is None:
|
|
738
|
+
return self._data.get(key)
|
|
739
|
+
|
|
740
|
+
if hasattr(widget, "value"):
|
|
741
|
+
val_attr = getattr(widget, "value")
|
|
742
|
+
value = val_attr() if callable(val_attr) else val_attr
|
|
743
|
+
elif key in self._variables:
|
|
744
|
+
value = self._variables[key].get()
|
|
745
|
+
elif isinstance(widget, Text):
|
|
746
|
+
value = widget.get("1.0", "end-1c")
|
|
747
|
+
else:
|
|
748
|
+
try:
|
|
749
|
+
value = widget.get()
|
|
750
|
+
except Exception:
|
|
751
|
+
value = self._data.get(key)
|
|
752
|
+
|
|
753
|
+
item = self._items_by_key.get(key)
|
|
754
|
+
if item:
|
|
755
|
+
value = self._coerce_value(item.dtype, value)
|
|
756
|
+
return value
|
|
757
|
+
|
|
758
|
+
def _apply_value_to_widget(self, key: str, item: FieldItem, value: Any) -> None:
|
|
759
|
+
widget = self._widgets.get(key)
|
|
760
|
+
if widget is None:
|
|
761
|
+
return
|
|
762
|
+
|
|
763
|
+
if isinstance(widget, Field):
|
|
764
|
+
if hasattr(widget, "_suppress_changed_event"):
|
|
765
|
+
widget._suppress_changed_event = True # type: ignore[attr-defined]
|
|
766
|
+
try:
|
|
767
|
+
widget.value = value
|
|
768
|
+
finally:
|
|
769
|
+
widget._suppress_changed_event = False # type: ignore[attr-defined]
|
|
770
|
+
else:
|
|
771
|
+
widget.value = value
|
|
772
|
+
return
|
|
773
|
+
|
|
774
|
+
if isinstance(widget, Text):
|
|
775
|
+
widget.delete("1.0", "end")
|
|
776
|
+
if value is not None:
|
|
777
|
+
widget.insert("1.0", str(value))
|
|
778
|
+
return
|
|
779
|
+
|
|
780
|
+
if key in self._variables:
|
|
781
|
+
self._variables[key].set("" if value is None else value)
|
|
782
|
+
return
|
|
783
|
+
|
|
784
|
+
try:
|
|
785
|
+
widget.configure(value=value)
|
|
786
|
+
except Exception:
|
|
787
|
+
pass
|
|
788
|
+
|
|
789
|
+
def _sync_value_from_widget(self, key: str) -> None:
|
|
790
|
+
if self._suspend_sync:
|
|
791
|
+
return
|
|
792
|
+
if key not in self._items_by_key:
|
|
793
|
+
return
|
|
794
|
+
new_value = self._read_value_from_widget(key)
|
|
795
|
+
self._data[key] = new_value
|
|
796
|
+
if self._on_data_changed:
|
|
797
|
+
self._on_data_changed(dict(self._data))
|
|
798
|
+
|
|
799
|
+
def _variable_for_item(self, item: FieldItem, initial: Any, editor: EditorType | None) -> Variable | None:
|
|
800
|
+
dtype = item.dtype
|
|
801
|
+
if editor in ('checkbutton', 'toggle'):
|
|
802
|
+
return BooleanVar(value=bool(initial) if initial is not None else False)
|
|
803
|
+
if editor in ('numericentry', 'spinbox', 'scale') or dtype in ('int', int, 'float', float):
|
|
804
|
+
if dtype in ('float', float):
|
|
805
|
+
return DoubleVar(value=float(initial) if initial is not None else 0.0)
|
|
806
|
+
return IntVar(value=int(initial) if initial is not None else 0)
|
|
807
|
+
return StringVar(value="" if initial is None else str(initial))
|
|
808
|
+
|
|
809
|
+
def _default_editor_for_dtype(self, dtype: Any, value: Any) -> EditorType:
|
|
810
|
+
if dtype in ('int', int, 'float', float):
|
|
811
|
+
return 'numericentry'
|
|
812
|
+
if dtype in ('bool', bool):
|
|
813
|
+
return 'checkbutton'
|
|
814
|
+
if dtype in ('date', 'datetime', date, datetime):
|
|
815
|
+
return 'dateentry'
|
|
816
|
+
if dtype in ('password',):
|
|
817
|
+
return 'passwordentry'
|
|
818
|
+
if value is not None:
|
|
819
|
+
if isinstance(value, (int, float)):
|
|
820
|
+
return 'numericentry'
|
|
821
|
+
if isinstance(value, (bool,)):
|
|
822
|
+
return 'checkbutton'
|
|
823
|
+
if isinstance(value, (date, datetime)):
|
|
824
|
+
return 'dateentry'
|
|
825
|
+
return 'textentry'
|
|
826
|
+
|
|
827
|
+
def _coerce_value(self, dtype: Any, value: Any) -> Any:
|
|
828
|
+
if dtype in ('int', int):
|
|
829
|
+
try:
|
|
830
|
+
return int(value)
|
|
831
|
+
except Exception:
|
|
832
|
+
return value
|
|
833
|
+
if dtype in ('float', float):
|
|
834
|
+
try:
|
|
835
|
+
return float(value)
|
|
836
|
+
except Exception:
|
|
837
|
+
return value
|
|
838
|
+
if dtype in ('bool', bool):
|
|
839
|
+
return bool(value)
|
|
840
|
+
if dtype in ('date', 'datetime', date, datetime):
|
|
841
|
+
return value
|
|
842
|
+
return value
|
|
843
|
+
|
|
844
|
+
def _register_variable(self, key: str, var: Variable) -> None:
|
|
845
|
+
self._variables[key] = var
|
|
846
|
+
var.trace_add("write", lambda *_a, k=key: self._sync_value_from_widget(k))
|
|
847
|
+
|
|
848
|
+
def _bind_text_change(self, widget: Text, key: str, var: StringVar | None = None) -> None:
|
|
849
|
+
_updating = {"text": False}
|
|
850
|
+
|
|
851
|
+
def _on_change(_event=None):
|
|
852
|
+
if _updating["text"]:
|
|
853
|
+
return
|
|
854
|
+
_updating["text"] = True
|
|
855
|
+
try:
|
|
856
|
+
widget.edit_modified(False)
|
|
857
|
+
text = widget.get("1.0", "end-1c")
|
|
858
|
+
if var is not None:
|
|
859
|
+
var.set(text)
|
|
860
|
+
self._sync_value_from_widget(key)
|
|
861
|
+
finally:
|
|
862
|
+
_updating["text"] = False
|
|
863
|
+
|
|
864
|
+
def _on_var_change(*_args):
|
|
865
|
+
if _updating["text"] or var is None:
|
|
866
|
+
return
|
|
867
|
+
_updating["text"] = True
|
|
868
|
+
try:
|
|
869
|
+
widget.delete("1.0", "end")
|
|
870
|
+
widget.insert("1.0", var.get())
|
|
871
|
+
finally:
|
|
872
|
+
_updating["text"] = False
|
|
873
|
+
|
|
874
|
+
widget.bind("<<Modified>>", _on_change)
|
|
875
|
+
widget.edit_modified(False)
|
|
876
|
+
|
|
877
|
+
if var is not None:
|
|
878
|
+
var.trace_add("write", _on_var_change)
|
|
879
|
+
|
|
880
|
+
def _bind_numeric_variable(self, key: str, widget: NumericEntry, var: Variable | None) -> None:
|
|
881
|
+
if var is None:
|
|
882
|
+
return
|
|
883
|
+
|
|
884
|
+
self._register_variable(key, var)
|
|
885
|
+
|
|
886
|
+
def _sync_numeric_var(*_args):
|
|
887
|
+
new_value = widget.value
|
|
888
|
+
if new_value is None:
|
|
889
|
+
return
|
|
890
|
+
try:
|
|
891
|
+
current_value = var.get()
|
|
892
|
+
except Exception:
|
|
893
|
+
current_value = None
|
|
894
|
+
if current_value == new_value:
|
|
895
|
+
return
|
|
896
|
+
previous_suspend = self._suspend_sync
|
|
897
|
+
self._suspend_sync = True
|
|
898
|
+
try:
|
|
899
|
+
var.set(new_value)
|
|
900
|
+
finally:
|
|
901
|
+
self._suspend_sync = previous_suspend
|
|
902
|
+
|
|
903
|
+
text_signal = getattr(widget, "signal", None)
|
|
904
|
+
if text_signal is not None:
|
|
905
|
+
text_signal.subscribe(lambda *_: _sync_numeric_var())
|
|
906
|
+
_sync_numeric_var()
|
|
907
|
+
|
|
908
|
+
def _set_readonly(self, widget: Any) -> None:
|
|
909
|
+
if isinstance(widget, Field):
|
|
910
|
+
widget.readonly(True)
|
|
911
|
+
else:
|
|
912
|
+
try:
|
|
913
|
+
widget.state(['disabled'])
|
|
914
|
+
except Exception:
|
|
915
|
+
try:
|
|
916
|
+
widget.configure(state='disabled')
|
|
917
|
+
except Exception:
|
|
918
|
+
pass
|
|
919
|
+
|
|
920
|
+
# --- button helpers -------------------------------------------------
|
|
921
|
+
def _make_button_command(self, spec: DialogButton):
|
|
922
|
+
def command():
|
|
923
|
+
if spec.command:
|
|
924
|
+
spec.command(self) # type: ignore[arg-type]
|
|
925
|
+
self.result = spec.result if spec.result is not None else self.data
|
|
926
|
+
|
|
927
|
+
return command
|
|
928
|
+
|
|
929
|
+
def _style_for_role(self, role: str) -> tuple[str, str | None]:
|
|
930
|
+
"""Get color and variant for a button role.
|
|
931
|
+
|
|
932
|
+
Returns:
|
|
933
|
+
Tuple of (color, variant) for the role.
|
|
934
|
+
"""
|
|
935
|
+
if role == "primary":
|
|
936
|
+
return ("primary", None)
|
|
937
|
+
if role == "secondary":
|
|
938
|
+
return ("secondary", None)
|
|
939
|
+
if role == "danger":
|
|
940
|
+
return ("danger", None)
|
|
941
|
+
if role == "cancel":
|
|
942
|
+
return ("secondary", "outline")
|
|
943
|
+
if role == "help":
|
|
944
|
+
return ("info", "link")
|
|
945
|
+
return ("secondary", None)
|
|
946
|
+
|
|
947
|
+
# --- inference ------------------------------------------------------
|
|
948
|
+
def _infer_items_from_data(self, data: Mapping[str, Any]) -> list[FieldItem]:
|
|
949
|
+
inferred: list[FieldItem] = []
|
|
950
|
+
for key, value in data.items():
|
|
951
|
+
inferred.append(
|
|
952
|
+
FieldItem(
|
|
953
|
+
key=str(key),
|
|
954
|
+
label=str(key).replace('_', ' ').title(),
|
|
955
|
+
dtype=self._infer_dtype_from_value(value),
|
|
956
|
+
editor=self._default_editor_for_dtype(self._infer_dtype_from_value(value), value),
|
|
957
|
+
editor_options={"show_message": True},
|
|
958
|
+
)
|
|
959
|
+
)
|
|
960
|
+
return inferred
|
|
961
|
+
|
|
962
|
+
@staticmethod
|
|
963
|
+
def _infer_dtype_from_value(value: Any) -> DType:
|
|
964
|
+
if isinstance(value, bool):
|
|
965
|
+
return 'bool'
|
|
966
|
+
if isinstance(value, int) and not isinstance(value, bool):
|
|
967
|
+
return 'int'
|
|
968
|
+
if isinstance(value, float):
|
|
969
|
+
return 'float'
|
|
970
|
+
if isinstance(value, (date, datetime)):
|
|
971
|
+
return 'date'
|
|
972
|
+
return 'str'
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
__all__ = [
|
|
976
|
+
"Form",
|
|
977
|
+
"FormItem",
|
|
978
|
+
"FieldItem",
|
|
979
|
+
"GroupItem",
|
|
980
|
+
"TabsItem",
|
|
981
|
+
"TabItem",
|
|
982
|
+
"EditorType",
|
|
983
|
+
]
|