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,541 @@
|
|
|
1
|
+
"""FormDialog - A dialog that embeds a Form widget for data entry.
|
|
2
|
+
|
|
3
|
+
This module provides FormDialog, which combines the Dialog and Form widgets
|
|
4
|
+
to create modal or non-modal dialogs for structured data entry.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from tkinter import Widget
|
|
10
|
+
from typing import Any, Callable, Iterable, Literal, Mapping, Optional, Sequence, Tuple, Union, TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from bootstack.widgets.composites.form import FormItem
|
|
14
|
+
|
|
15
|
+
from bootstack.dialogs.dialog import Dialog, DialogButton, ButtonSpec
|
|
16
|
+
from bootstack.widgets.primitives.frame import Frame
|
|
17
|
+
from bootstack.widgets.types import Master
|
|
18
|
+
from bootstack.constants import DEFAULT_MIN_COL_WIDTH as FORM_MIN_COL_WIDTH
|
|
19
|
+
from bootstack.runtime.window_utilities import AnchorPoint
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FormDialog:
|
|
23
|
+
"""A dialog window that embeds a Form widget for structured data entry.
|
|
24
|
+
|
|
25
|
+
FormDialog combines the Dialog and Form widgets to create modal or non-modal
|
|
26
|
+
dialogs for data entry with automatic field generation or explicit layouts.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
result: The form data returned after closing (dict), or None if cancelled.
|
|
30
|
+
form: The embedded Form widget instance, accessible for advanced usage.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
master: Parent widget. If None, uses the default root window.
|
|
34
|
+
title: Dialog window title. Defaults to "Form".
|
|
35
|
+
data: Initial data backing the form. Keys become field names.
|
|
36
|
+
items: Optional explicit form definition using FieldItem/GroupItem/TabsItem.
|
|
37
|
+
If not provided, fields are inferred from data keys and types.
|
|
38
|
+
col_count: Number of columns for form layout. Defaults to 1.
|
|
39
|
+
min_col_width: Minimum width for each column in pixels. Defaults to the
|
|
40
|
+
shared form default.
|
|
41
|
+
on_data_changed: Optional callback invoked when any field value changes.
|
|
42
|
+
Receives the updated data dict as parameter.
|
|
43
|
+
width: Requested width for the form. Defaults to None (auto-size).
|
|
44
|
+
height: Requested height for the form. Defaults to None (auto-size).
|
|
45
|
+
scrollable: Deprecated; FormDialog manages scrolling internally.
|
|
46
|
+
scrollview_options: Additional options passed to ScrollView when scrollable is True.
|
|
47
|
+
buttons: Footer buttons. Can be DialogButton instances, dicts, or strings.
|
|
48
|
+
If not provided, defaults to Cancel and OK buttons.
|
|
49
|
+
First button appears rightmost (Bootstrap convention).
|
|
50
|
+
minsize: Minimum dialog window size as (width, height).
|
|
51
|
+
If None, automatically calculated based on col_count * min_col_width + padding.
|
|
52
|
+
If provided, ensures width is at least the calculated minimum to prevent
|
|
53
|
+
horizontal scrolling. Defaults to None (auto-calculate).
|
|
54
|
+
maxsize: Maximum dialog window size as (width, height). Defaults to None.
|
|
55
|
+
resizable: Allow window resizing as (width, height) bools. Defaults to (True, True).
|
|
56
|
+
alert: If True, plays system alert sound on show. Defaults to False.
|
|
57
|
+
mode: Dialog interaction mode ("modal" or "popover"). Defaults to "modal".
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(
|
|
61
|
+
self,
|
|
62
|
+
master: Master = None,
|
|
63
|
+
*,
|
|
64
|
+
title: str = "Form",
|
|
65
|
+
data: dict[str, Any] | None = None,
|
|
66
|
+
items: Sequence[FormItem | Mapping[str, Any]] | None = None,
|
|
67
|
+
col_count: int = 1,
|
|
68
|
+
min_col_width: int | None = None,
|
|
69
|
+
on_data_changed: Callable[[dict[str, Any]], Any] | None = None,
|
|
70
|
+
width: int | None = None,
|
|
71
|
+
height: int | None = None,
|
|
72
|
+
scrollable: bool = True,
|
|
73
|
+
scrollview_options: dict[str, Any] | None = None,
|
|
74
|
+
buttons: Iterable[ButtonSpec | str] | None = None,
|
|
75
|
+
minsize: tuple[int, int] | None = None,
|
|
76
|
+
maxsize: tuple[int, int] | None = None,
|
|
77
|
+
resizable: tuple[bool, bool] | bool | None = False,
|
|
78
|
+
alert: bool = False,
|
|
79
|
+
mode: Literal['modal', 'popover'] = "modal",
|
|
80
|
+
):
|
|
81
|
+
"""Initialize a FormDialog that wraps a Form inside a Dialog.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
master: Parent widget. Defaults to the default root window.
|
|
85
|
+
title: Dialog window title.
|
|
86
|
+
data: Initial data backing the form; keys become field names.
|
|
87
|
+
items: Optional explicit form layout (FieldItem/GroupItem/TabsItem or mappings).
|
|
88
|
+
col_count: Number of columns for the form layout.
|
|
89
|
+
min_col_width: Minimum width per column; defaults to the shared form default.
|
|
90
|
+
on_data_changed: Callback invoked when any field value changes.
|
|
91
|
+
width: Explicit form width; if None, size naturally.
|
|
92
|
+
height: Explicit form height; if None, size naturally.
|
|
93
|
+
scrollable: Whether the form content should be scrollable.
|
|
94
|
+
scrollview_options: Extra options passed to the ScrollView when scrollable.
|
|
95
|
+
buttons: Dialog footer buttons (DialogButton, mapping, or string).
|
|
96
|
+
minsize: Minimum dialog window size (width, height).
|
|
97
|
+
maxsize: Maximum dialog window size (width, height).
|
|
98
|
+
resizable: Bool or (width, height) tuple to allow window resizing.
|
|
99
|
+
alert: Whether to play the system alert sound when shown.
|
|
100
|
+
mode: Dialog interaction mode, "modal" or "popover".
|
|
101
|
+
"""
|
|
102
|
+
self._data = data or {}
|
|
103
|
+
self._items = items
|
|
104
|
+
self._col_count = col_count
|
|
105
|
+
# Use the shared form default when not provided to keep layouts consistent
|
|
106
|
+
self._min_col_width = min_col_width if min_col_width is not None else FORM_MIN_COL_WIDTH
|
|
107
|
+
self._on_data_changed = on_data_changed
|
|
108
|
+
self._width = width
|
|
109
|
+
self._height = height
|
|
110
|
+
self._scrollable = scrollable # deprecated; kept for compatibility with callers
|
|
111
|
+
|
|
112
|
+
# Use better default scrollview options - auto-hide when content fits
|
|
113
|
+
default_scrollview_options = {
|
|
114
|
+
# Keep the scrollbar visible to avoid width jumps when it appears/disappears
|
|
115
|
+
'scrollbar_visibility': 'always',
|
|
116
|
+
'autohide_delay': 1000,
|
|
117
|
+
}
|
|
118
|
+
if scrollview_options:
|
|
119
|
+
default_scrollview_options.update(scrollview_options)
|
|
120
|
+
self._scrollview_options = default_scrollview_options
|
|
121
|
+
|
|
122
|
+
# Normalize buttons and wrap command callbacks
|
|
123
|
+
self._buttons = self._normalize_buttons(buttons)
|
|
124
|
+
self._wrap_button_commands()
|
|
125
|
+
|
|
126
|
+
# Store minsize for later adjustment
|
|
127
|
+
self._user_minsize = minsize
|
|
128
|
+
self._user_maxsize = maxsize
|
|
129
|
+
|
|
130
|
+
# Normalize resizable flag: bool applies to both axes
|
|
131
|
+
if isinstance(resizable, bool):
|
|
132
|
+
resizable = (resizable, resizable)
|
|
133
|
+
|
|
134
|
+
# Create the dialog with form as content
|
|
135
|
+
# We'll set the proper minsize after measuring the actual form
|
|
136
|
+
self._dialog = Dialog(
|
|
137
|
+
master=master,
|
|
138
|
+
title=title,
|
|
139
|
+
content_builder=self._build_form_content,
|
|
140
|
+
buttons=self._buttons,
|
|
141
|
+
minsize=minsize, # Use user-provided or None initially
|
|
142
|
+
maxsize=maxsize,
|
|
143
|
+
resizable=resizable,
|
|
144
|
+
alert=alert,
|
|
145
|
+
mode=mode,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
self.form: Any = None # Form widget, imported lazily to avoid circular import
|
|
149
|
+
self.result: Any = None
|
|
150
|
+
self._initial_layout_done = False
|
|
151
|
+
self._scrollview = None
|
|
152
|
+
self._window_id = None
|
|
153
|
+
|
|
154
|
+
def show(
|
|
155
|
+
self,
|
|
156
|
+
position: Optional[Tuple[int, int]] = None,
|
|
157
|
+
modal: Optional[bool] = None,
|
|
158
|
+
*,
|
|
159
|
+
anchor_to: Optional[Union[Widget, Literal["screen", "cursor", "parent"]]] = None,
|
|
160
|
+
anchor_point: AnchorPoint = 'center',
|
|
161
|
+
window_point: AnchorPoint = 'center',
|
|
162
|
+
offset: Tuple[int, int] = (0, 0),
|
|
163
|
+
auto_flip: Union[bool, Literal['vertical', 'horizontal']] = False
|
|
164
|
+
):
|
|
165
|
+
"""Show the form dialog and populate `result` when closed.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
position: Optional (x, y) coordinates to position the dialog.
|
|
169
|
+
If provided, takes precedence over anchor-based positioning.
|
|
170
|
+
modal: Override the mode's default modality.
|
|
171
|
+
- If None, uses mode:
|
|
172
|
+
- "modal": grab_set + wait_window
|
|
173
|
+
- "popover": no grab, but wait_window
|
|
174
|
+
anchor_to: Positioning target. Can be:
|
|
175
|
+
- Widget: Anchor to a specific widget
|
|
176
|
+
- "screen": Anchor to screen edges/corners
|
|
177
|
+
- "cursor": Anchor to mouse cursor location
|
|
178
|
+
- "parent": Anchor to parent window (same as widget)
|
|
179
|
+
- None: Centers on parent (default)
|
|
180
|
+
anchor_point: Point on the anchor target (n, s, e, w, ne, nw, se, sw, center).
|
|
181
|
+
Default 'center'.
|
|
182
|
+
window_point: Point on the dialog window (n, s, e, w, ne, nw, se, sw, center).
|
|
183
|
+
Default 'center'.
|
|
184
|
+
offset: Additional (x, y) offset in pixels from the anchor position.
|
|
185
|
+
auto_flip: Smart positioning to keep window on screen.
|
|
186
|
+
- False: No flipping (default)
|
|
187
|
+
- True: Flip both vertically and horizontally as needed
|
|
188
|
+
- 'vertical': Only flip up/down
|
|
189
|
+
- 'horizontal': Only flip left/right
|
|
190
|
+
"""
|
|
191
|
+
# Allow initial layout priming each time the dialog is shown
|
|
192
|
+
self._initial_layout_done = False
|
|
193
|
+
|
|
194
|
+
self._dialog.show(
|
|
195
|
+
position=position,
|
|
196
|
+
modal=modal,
|
|
197
|
+
anchor_to=anchor_to,
|
|
198
|
+
anchor_point=anchor_point,
|
|
199
|
+
window_point=window_point,
|
|
200
|
+
offset=offset,
|
|
201
|
+
auto_flip=auto_flip
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Transfer the result from dialog to FormDialog
|
|
205
|
+
if self._dialog.result is not None:
|
|
206
|
+
self.result = self.form.data if self.form else None
|
|
207
|
+
else:
|
|
208
|
+
self.result = None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _build_form_content(self, parent):
|
|
212
|
+
"""Builder callback that creates the Form widget inside the dialog."""
|
|
213
|
+
# Import Form here to avoid circular import
|
|
214
|
+
from bootstack.widgets.composites.form import Form
|
|
215
|
+
from bootstack.widgets.composites.scrollview import ScrollView
|
|
216
|
+
|
|
217
|
+
# Configure parent to allow stretching
|
|
218
|
+
parent.columnconfigure(0, weight=1)
|
|
219
|
+
parent.rowconfigure(0, weight=1)
|
|
220
|
+
|
|
221
|
+
container = parent
|
|
222
|
+
if self._scrollable:
|
|
223
|
+
self._scrollview = ScrollView(parent, scroll_direction='vertical', **self._scrollview_options)
|
|
224
|
+
self._scrollview.grid(row=0, column=0, sticky="nsew")
|
|
225
|
+
self._scrollview.enable_scrolling()
|
|
226
|
+
# add a padding frame inside the scrollview so the form has margins
|
|
227
|
+
padding_frame = Frame(self._scrollview, padding=10)
|
|
228
|
+
padding_frame.columnconfigure(0, weight=1)
|
|
229
|
+
padding_frame.rowconfigure(0, weight=1)
|
|
230
|
+
self._scrollview.add(padding_frame, anchor='nw')
|
|
231
|
+
self._window_id = self._scrollview._window_id
|
|
232
|
+
self._scrollview.bind('<Configure>', self._on_scrollview_configure, add="+")
|
|
233
|
+
# Keep canvas window width in sync with viewport on every resize
|
|
234
|
+
self._scrollview.canvas.bind("<Configure>", self._on_canvas_configure, add="+")
|
|
235
|
+
container = padding_frame
|
|
236
|
+
else:
|
|
237
|
+
self._scrollview = None
|
|
238
|
+
|
|
239
|
+
# Create the form without its own scrolling; scrolling is managed by the dialog
|
|
240
|
+
self.form = Form(
|
|
241
|
+
container,
|
|
242
|
+
data=self._data,
|
|
243
|
+
items=self._items,
|
|
244
|
+
col_count=self._col_count,
|
|
245
|
+
min_col_width=self._min_col_width,
|
|
246
|
+
on_data_changed=self._on_data_changed,
|
|
247
|
+
width=None, # Let form size naturally
|
|
248
|
+
height=None if self._scrollable else self._height,
|
|
249
|
+
buttons=None,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Add the form to the scrollview container or grid directly
|
|
253
|
+
if self._scrollview:
|
|
254
|
+
self.form.grid(row=0, column=0, sticky="nsew")
|
|
255
|
+
else:
|
|
256
|
+
self.form.grid(row=0, column=0, sticky="nsew", padx=10, pady=10)
|
|
257
|
+
|
|
258
|
+
# Ensure initial layout runs right after build (for scrollable dialogs)
|
|
259
|
+
self._schedule_initial_layout()
|
|
260
|
+
|
|
261
|
+
# Measure the actual content width after rendering
|
|
262
|
+
measured_width = self._col_count * self._min_col_width
|
|
263
|
+
if self._items:
|
|
264
|
+
nested_width = self._calculate_required_width()
|
|
265
|
+
measured_width = max(measured_width, nested_width)
|
|
266
|
+
|
|
267
|
+
if self._width:
|
|
268
|
+
measured_width = self._width
|
|
269
|
+
|
|
270
|
+
# Calculate dialog size: content + padding (10x2) + dialog chrome (~40)
|
|
271
|
+
dialog_width = measured_width + 60
|
|
272
|
+
dialog_height = 500 # Default height, will adjust after geometry update
|
|
273
|
+
|
|
274
|
+
# Apply user minsize if provided
|
|
275
|
+
if self._user_minsize:
|
|
276
|
+
user_width, user_height = self._user_minsize
|
|
277
|
+
dialog_width = max(user_width, dialog_width)
|
|
278
|
+
dialog_height = max(user_height, dialog_height)
|
|
279
|
+
|
|
280
|
+
if self._height:
|
|
281
|
+
dialog_height = self._height + 100
|
|
282
|
+
|
|
283
|
+
dialog_height = min(dialog_height, 800)
|
|
284
|
+
# Store the intended content width for pre-show sizing of the scrollview
|
|
285
|
+
self._desired_canvas_width = measured_width
|
|
286
|
+
|
|
287
|
+
# Set minsize and geometry BEFORE forcing layout
|
|
288
|
+
if self._dialog.toplevel:
|
|
289
|
+
self._dialog.toplevel.minsize(dialog_width, dialog_height)
|
|
290
|
+
self._dialog.toplevel.geometry(f"{dialog_width}x{dialog_height}")
|
|
291
|
+
|
|
292
|
+
# Force complete geometry calculation while window is still withdrawn
|
|
293
|
+
self._dialog.toplevel.update_idletasks()
|
|
294
|
+
|
|
295
|
+
def _schedule_initial_layout(self):
|
|
296
|
+
"""Run layout fixups immediately before showing the window."""
|
|
297
|
+
if not self._scrollable:
|
|
298
|
+
return
|
|
299
|
+
if self._dialog and self._dialog.toplevel:
|
|
300
|
+
# Make sure all pending geometry work is processed while withdrawn.
|
|
301
|
+
# update_idletasks alone flushes layout/redraw; full update() also
|
|
302
|
+
# pumps input/IO events, which can hang on Aqua when the form
|
|
303
|
+
# contains heavy widgets (same pattern as the Dialog/BaseWindow
|
|
304
|
+
# pre-deiconify hang). Gate update() to win32 only.
|
|
305
|
+
top = self._dialog.toplevel
|
|
306
|
+
top.update_idletasks()
|
|
307
|
+
if getattr(top, 'winsys', None) == 'win32':
|
|
308
|
+
top.update()
|
|
309
|
+
# Run once synchronously so sizing is applied before deiconify
|
|
310
|
+
self._fire_initial_configure(blocking=True)
|
|
311
|
+
|
|
312
|
+
def _fire_initial_configure(self, _event=None, attempts: int = 5, blocking: bool = False):
|
|
313
|
+
"""Trigger scrollview/layout config once after geometry stabilizes."""
|
|
314
|
+
if self._initial_layout_done:
|
|
315
|
+
return
|
|
316
|
+
if not (self._scrollable and self._scrollview and self.form):
|
|
317
|
+
return
|
|
318
|
+
|
|
319
|
+
canvas = self._scrollview.canvas
|
|
320
|
+
canvas_width = 0
|
|
321
|
+
|
|
322
|
+
# When blocking, loop a few times to give Tk a chance to size the canvas
|
|
323
|
+
loops = attempts if blocking else 1
|
|
324
|
+
for _ in range(max(1, loops)):
|
|
325
|
+
if self._dialog and self._dialog.toplevel:
|
|
326
|
+
self._dialog.toplevel.update_idletasks()
|
|
327
|
+
self._scrollview.update_idletasks()
|
|
328
|
+
canvas_width = canvas.winfo_width()
|
|
329
|
+
if canvas_width > 1:
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
fallback_width = max(
|
|
333
|
+
getattr(self, "_desired_canvas_width", 0),
|
|
334
|
+
self.form.winfo_reqwidth() if self.form else 0,
|
|
335
|
+
self._col_count * self._min_col_width,
|
|
336
|
+
)
|
|
337
|
+
if canvas_width <= 1:
|
|
338
|
+
canvas_width = fallback_width
|
|
339
|
+
|
|
340
|
+
# Apply width and manually call handlers
|
|
341
|
+
if self._window_id:
|
|
342
|
+
canvas.itemconfigure(self._window_id, width=canvas_width)
|
|
343
|
+
self._on_scrollview_configure(None)
|
|
344
|
+
try:
|
|
345
|
+
self._scrollview._on_frame_configure(None)
|
|
346
|
+
except Exception:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
# Ensure scrolling bindings are active after layout adjustments
|
|
350
|
+
try:
|
|
351
|
+
if getattr(self._scrollview, "_scrolling_enabled", False):
|
|
352
|
+
self._scrollview.refresh_bindings()
|
|
353
|
+
else:
|
|
354
|
+
self._scrollview.enable_scrolling()
|
|
355
|
+
except Exception:
|
|
356
|
+
pass
|
|
357
|
+
|
|
358
|
+
# Generate configure to mirror user-driven resize
|
|
359
|
+
try:
|
|
360
|
+
canvas.event_generate("<Configure>")
|
|
361
|
+
except Exception:
|
|
362
|
+
pass
|
|
363
|
+
|
|
364
|
+
def _on_scrollview_configure(self, _event=None):
|
|
365
|
+
"""Keep scrollview content width in sync with the viewport."""
|
|
366
|
+
if self._scrollview and self._window_id:
|
|
367
|
+
canvas_width = self._scrollview.canvas.winfo_width()
|
|
368
|
+
if canvas_width > 1:
|
|
369
|
+
self._scrollview.canvas.itemconfigure(self._window_id, width=canvas_width)
|
|
370
|
+
|
|
371
|
+
def _on_canvas_configure(self, event=None):
|
|
372
|
+
"""Keep the embedded form window width aligned to the canvas viewport."""
|
|
373
|
+
if not (self._scrollview and self._window_id):
|
|
374
|
+
return
|
|
375
|
+
width = event.width if event and hasattr(event, "width") else self._scrollview.canvas.winfo_width()
|
|
376
|
+
if width <= 1:
|
|
377
|
+
return
|
|
378
|
+
self._scrollview.canvas.itemconfigure(self._window_id, width=width)
|
|
379
|
+
try:
|
|
380
|
+
self._scrollview._on_frame_configure(None)
|
|
381
|
+
except Exception:
|
|
382
|
+
pass
|
|
383
|
+
|
|
384
|
+
def _calculate_required_width(self) -> int:
|
|
385
|
+
"""Calculate the required minimum width for the dialog based on form structure."""
|
|
386
|
+
try:
|
|
387
|
+
from bootstack.widgets.composites.form import GroupItem, TabsItem
|
|
388
|
+
|
|
389
|
+
# Start by finding the maximum width requirement
|
|
390
|
+
max_content_width = self._find_max_content_width(self._items, self._col_count, self._min_col_width)
|
|
391
|
+
|
|
392
|
+
# Add all the padding layers:
|
|
393
|
+
# 1. Form widget padding (grid padx=10 on each side) = 20px
|
|
394
|
+
# 2. Dialog content frame borders = 10px
|
|
395
|
+
# 3. Window chrome and safety margin = 30px
|
|
396
|
+
total_width = max_content_width + 60
|
|
397
|
+
|
|
398
|
+
return total_width
|
|
399
|
+
except:
|
|
400
|
+
# Fallback to basic calculation
|
|
401
|
+
return (self._col_count * self._min_col_width) + 60
|
|
402
|
+
|
|
403
|
+
def _find_max_content_width(self, items, parent_col_count: int, parent_min_col_width: int) -> int:
|
|
404
|
+
"""Find the maximum content width needed, accounting for nested structures."""
|
|
405
|
+
if not items:
|
|
406
|
+
# No items, use parent layout
|
|
407
|
+
# Each column needs: min_col_width + padx (6 on each side = 12)
|
|
408
|
+
return (parent_col_count * (parent_min_col_width + 12))
|
|
409
|
+
|
|
410
|
+
max_width = 0
|
|
411
|
+
|
|
412
|
+
# Calculate width for current level
|
|
413
|
+
current_width = parent_col_count * (parent_min_col_width + 12)
|
|
414
|
+
max_width = max(max_width, current_width)
|
|
415
|
+
|
|
416
|
+
# Check all items for nested layouts
|
|
417
|
+
try:
|
|
418
|
+
from bootstack.widgets.composites.form import GroupItem, TabsItem
|
|
419
|
+
|
|
420
|
+
for item in items:
|
|
421
|
+
if isinstance(item, dict):
|
|
422
|
+
item_type = item.get('type', 'field')
|
|
423
|
+
if item_type == 'group':
|
|
424
|
+
nested_col_count = item.get('col_count', parent_col_count)
|
|
425
|
+
nested_min_col_width = item.get('min_col_width', self._min_col_width)
|
|
426
|
+
nested_items = item.get('items', [])
|
|
427
|
+
|
|
428
|
+
# GroupItem content width
|
|
429
|
+
group_content = self._find_max_content_width(nested_items, nested_col_count, nested_min_col_width)
|
|
430
|
+
# Add LabelFrame padding (8px each side) and borders (~4px) = 24px total
|
|
431
|
+
group_total = group_content + 24
|
|
432
|
+
max_width = max(max_width, group_total)
|
|
433
|
+
|
|
434
|
+
elif item_type == 'tabs':
|
|
435
|
+
tabs = item.get('tabs', [])
|
|
436
|
+
for tab in tabs:
|
|
437
|
+
if isinstance(tab, dict):
|
|
438
|
+
tab_items = tab.get('items', [])
|
|
439
|
+
tab_width = self._find_max_content_width(tab_items, parent_col_count, parent_min_col_width)
|
|
440
|
+
# Add Notebook borders = 20px
|
|
441
|
+
max_width = max(max_width, tab_width + 20)
|
|
442
|
+
|
|
443
|
+
elif isinstance(item, GroupItem):
|
|
444
|
+
nested_col_count = item.col_count if item.col_count else parent_col_count
|
|
445
|
+
nested_min_col_width = item.min_col_width if item.min_col_width else self._min_col_width
|
|
446
|
+
|
|
447
|
+
# GroupItem content width
|
|
448
|
+
group_content = self._find_max_content_width(item.items, nested_col_count, nested_min_col_width)
|
|
449
|
+
# Add LabelFrame padding
|
|
450
|
+
group_total = group_content + 24
|
|
451
|
+
max_width = max(max_width, group_total)
|
|
452
|
+
|
|
453
|
+
elif isinstance(item, TabsItem):
|
|
454
|
+
for tab in item.tabs:
|
|
455
|
+
tab_items = tab.items if hasattr(tab, 'items') else []
|
|
456
|
+
tab_width = self._find_max_content_width(tab_items, parent_col_count, parent_min_col_width)
|
|
457
|
+
# Add Notebook borders
|
|
458
|
+
max_width = max(max_width, tab_width + 20)
|
|
459
|
+
except:
|
|
460
|
+
pass
|
|
461
|
+
|
|
462
|
+
return max_width
|
|
463
|
+
|
|
464
|
+
def _wrap_button_commands(self):
|
|
465
|
+
"""Wrap button command callbacks to pass FormDialog instead of Dialog."""
|
|
466
|
+
for button in self._buttons:
|
|
467
|
+
# For non-cancel buttons, handle validation and closing manually
|
|
468
|
+
if button.role != "cancel":
|
|
469
|
+
button.closes = False
|
|
470
|
+
if button.command:
|
|
471
|
+
original_command = button.command
|
|
472
|
+
def wrapped_command(dlg, cmd=original_command, btn=button):
|
|
473
|
+
# Validate form before running custom command
|
|
474
|
+
if self.form and btn.role != "cancel":
|
|
475
|
+
if not self.form.validate():
|
|
476
|
+
return
|
|
477
|
+
result = cmd(self) # Pass FormDialog, not Dialog
|
|
478
|
+
if result is False:
|
|
479
|
+
return
|
|
480
|
+
# Set result and close manually when not cancelled
|
|
481
|
+
if self._dialog:
|
|
482
|
+
self._dialog.result = btn.result if btn.result is not None else (self.form.data if self.form else None)
|
|
483
|
+
if btn.closes is False and self._dialog.toplevel:
|
|
484
|
+
self._dialog.toplevel.destroy()
|
|
485
|
+
return result
|
|
486
|
+
button.command = wrapped_command
|
|
487
|
+
else:
|
|
488
|
+
# No custom command: inject validation and close behavior for non-cancel buttons
|
|
489
|
+
def auto_command(dlg=None, btn=button):
|
|
490
|
+
if btn.role == "cancel":
|
|
491
|
+
# Cancel button: leave result as None, dialog closes via default behavior
|
|
492
|
+
return
|
|
493
|
+
if self.form:
|
|
494
|
+
if not self.form.validate():
|
|
495
|
+
return
|
|
496
|
+
if self._dialog:
|
|
497
|
+
self._dialog.result = btn.result if btn.result is not None else (self.form.data if self.form else None)
|
|
498
|
+
if btn.closes is False and self._dialog.toplevel:
|
|
499
|
+
self._dialog.toplevel.destroy()
|
|
500
|
+
button.command = auto_command
|
|
501
|
+
|
|
502
|
+
def _normalize_buttons(self, buttons: Iterable[ButtonSpec | str] | None) -> list[DialogButton]:
|
|
503
|
+
"""Normalize button specifications, providing defaults if none given."""
|
|
504
|
+
if buttons is None:
|
|
505
|
+
# Default buttons: Cancel and OK
|
|
506
|
+
return [
|
|
507
|
+
DialogButton(text="button.cancel", role="cancel", result=None),
|
|
508
|
+
DialogButton(text="button.ok", role="primary", result="ok", default=True),
|
|
509
|
+
]
|
|
510
|
+
|
|
511
|
+
normalized: list[DialogButton] = []
|
|
512
|
+
for btn in buttons:
|
|
513
|
+
if isinstance(btn, DialogButton):
|
|
514
|
+
normalized.append(btn)
|
|
515
|
+
elif isinstance(btn, str):
|
|
516
|
+
# Simple string becomes a button
|
|
517
|
+
btn_lower = btn.lower()
|
|
518
|
+
if btn_lower == "cancel":
|
|
519
|
+
role = "cancel"
|
|
520
|
+
result = None
|
|
521
|
+
elif btn_lower in ("ok", "submit", "save"):
|
|
522
|
+
role = "primary"
|
|
523
|
+
result = btn_lower
|
|
524
|
+
else:
|
|
525
|
+
role = "primary" if not normalized else "secondary"
|
|
526
|
+
result = None
|
|
527
|
+
normalized.append(DialogButton(text=btn, role=role, result=result))
|
|
528
|
+
elif isinstance(btn, Mapping):
|
|
529
|
+
normalized.append(DialogButton(**btn))
|
|
530
|
+
else:
|
|
531
|
+
raise TypeError(f"Invalid button type: {type(btn)}")
|
|
532
|
+
|
|
533
|
+
return normalized
|
|
534
|
+
|
|
535
|
+
@property
|
|
536
|
+
def toplevel(self):
|
|
537
|
+
"""Read-only access to the underlying toplevel window."""
|
|
538
|
+
return self._dialog.toplevel if self._dialog else None
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
__all__ = ["FormDialog"]
|