bootstack 0.1.0__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 +157 -0
- bootstack/__main__.py +5 -0
- bootstack/_core/__init__.py +21 -0
- bootstack/_core/capabilities/__init__.py +45 -0
- bootstack/_core/capabilities/after.py +103 -0
- bootstack/_core/capabilities/bind.py +192 -0
- bootstack/_core/capabilities/bindtags.py +112 -0
- bootstack/_core/capabilities/busy.py +72 -0
- bootstack/_core/capabilities/clipboard.py +89 -0
- bootstack/_core/capabilities/focus.py +118 -0
- bootstack/_core/capabilities/grab.py +65 -0
- bootstack/_core/capabilities/grid.py +211 -0
- bootstack/_core/capabilities/localization.py +231 -0
- bootstack/_core/capabilities/pack.py +140 -0
- bootstack/_core/capabilities/place.py +113 -0
- bootstack/_core/capabilities/selection.py +136 -0
- bootstack/_core/capabilities/signals.py +244 -0
- bootstack/_core/capabilities/winfo.py +315 -0
- bootstack/_core/colorutils.py +234 -0
- bootstack/_core/exceptions.py +25 -0
- bootstack/_core/images.py +463 -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/paths.py +49 -0
- bootstack/_core/publisher.py +149 -0
- bootstack/_core/variables.py +62 -0
- bootstack/_runtime/__init__.py +3 -0
- bootstack/_runtime/app.py +930 -0
- bootstack/_runtime/base_window.py +945 -0
- bootstack/_runtime/events.py +399 -0
- bootstack/_runtime/shortcuts.py +496 -0
- bootstack/_runtime/tk_patch.py +43 -0
- bootstack/_runtime/toplevel.py +276 -0
- bootstack/_runtime/utility.py +457 -0
- bootstack/_runtime/visual_focus.py +240 -0
- bootstack/_runtime/window_utilities.py +1123 -0
- bootstack/assets/__init__.py +21 -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/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
- bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
- bootstack/assets/elements/card.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-addon-compact.png +0 -0
- bootstack/assets/elements/input-addon-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/listrow-compact.png +0 -0
- bootstack/assets/elements/listrow-default.png +0 -0
- bootstack/assets/elements/manifest.toml +361 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/navitem-compact.png +0 -0
- bootstack/assets/elements/navitem-default.png +0 -0
- bootstack/assets/elements/progressbar-h-compact.png +0 -0
- bootstack/assets/elements/progressbar-h-default.png +0 -0
- bootstack/assets/elements/progressbar-v-compact.png +0 -0
- bootstack/assets/elements/progressbar-v-default.png +0 -0
- bootstack/assets/elements/radiobutton.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-h.png +0 -0
- bootstack/assets/elements/slider-track-v.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tab-h.png +0 -0
- bootstack/assets/elements/tab-v.png +0 -0
- bootstack/assets/icons/bootstrap.ttf +0 -0
- bootstack/assets/icons/glyphmap.json +2080 -0
- bootstack/assets/icons/icon_metrics.json +12470 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
- bootstack/cli/__init__.py +133 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +395 -0
- bootstack/cli/appicon.py +285 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +313 -0
- bootstack/cli/demo.py +564 -0
- bootstack/cli/dev.py +153 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/icons.py +98 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +268 -0
- bootstack/cli/run.py +95 -0
- bootstack/cli/start.py +117 -0
- bootstack/cli/templates/__init__.py +931 -0
- bootstack/clipboard.py +48 -0
- bootstack/constants.py +318 -0
- bootstack/data/README.md +615 -0
- bootstack/data/__init__.py +78 -0
- bootstack/data/_observable.py +276 -0
- bootstack/data/base.py +780 -0
- bootstack/data/file_source.py +367 -0
- bootstack/data/memory_source.py +450 -0
- bootstack/data/query.py +367 -0
- bootstack/data/readers.py +289 -0
- bootstack/data/sqlite_source.py +869 -0
- bootstack/data/types.py +354 -0
- bootstack/data/writers.py +232 -0
- bootstack/dev/__init__.py +36 -0
- bootstack/dev/_capture.py +141 -0
- bootstack/dev/_env.py +43 -0
- bootstack/dev/_registry.py +140 -0
- bootstack/dev/_reloader.py +351 -0
- bootstack/dev/_reset.py +50 -0
- bootstack/dev/_watcher.py +91 -0
- bootstack/dialogs/__init__.py +948 -0
- bootstack/dialogs/_impl/__init__.py +47 -0
- bootstack/dialogs/_impl/colorchooser.py +588 -0
- bootstack/dialogs/_impl/datedialog.py +450 -0
- bootstack/dialogs/_impl/dialog.py +594 -0
- bootstack/dialogs/_impl/filterdialog.py +358 -0
- bootstack/dialogs/_impl/fontdialog.py +364 -0
- bootstack/dialogs/_impl/formdialog.py +564 -0
- bootstack/dialogs/_impl/message.py +486 -0
- bootstack/dialogs/_impl/query.py +570 -0
- bootstack/errors.py +67 -0
- bootstack/events/__init__.py +111 -0
- bootstack/events/_event.py +135 -0
- bootstack/events/_payloads.py +539 -0
- bootstack/events/_subscription.py +38 -0
- bootstack/i18n/README.md +77 -0
- bootstack/i18n/__init__.py +23 -0
- bootstack/i18n/catalog.py +121 -0
- bootstack/i18n/intl_format.py +584 -0
- bootstack/i18n/msgcat.py +425 -0
- bootstack/i18n/specs.py +156 -0
- bootstack/images.py +563 -0
- bootstack/py.typed +1 -0
- bootstack/scheduling/__init__.py +11 -0
- bootstack/scheduling/_schedule.py +218 -0
- bootstack/shortcuts.py +21 -0
- bootstack/signals/README.md +98 -0
- bootstack/signals/__init__.py +10 -0
- bootstack/signals/integration.py +100 -0
- bootstack/signals/signal.py +353 -0
- bootstack/signals/types.py +6 -0
- bootstack/store.py +286 -0
- bootstack/streams/__init__.py +12 -0
- bootstack/streams/_stream.py +321 -0
- bootstack/style/__init__.py +38 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +46 -0
- bootstack/style/builders/button.py +339 -0
- bootstack/style/builders/buttongroup.py +311 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +110 -0
- bootstack/style/builders/combobox.py +113 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +82 -0
- bootstack/style/builders/expander.py +148 -0
- bootstack/style/builders/field.py +335 -0
- bootstack/style/builders/frame.py +50 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +34 -0
- bootstack/style/builders/listview.py +369 -0
- bootstack/style/builders/menubar.py +91 -0
- bootstack/style/builders/menubutton.py +359 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +67 -0
- bootstack/style/builders/radiobutton.py +99 -0
- bootstack/style/builders/scale.py +64 -0
- bootstack/style/builders/scrollbar.py +225 -0
- bootstack/style/builders/separator.py +49 -0
- bootstack/style/builders/sidenav.py +643 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +70 -0
- bootstack/style/builders/tabitem.py +204 -0
- bootstack/style/builders/togglegroup.py +294 -0
- bootstack/style/builders/toolbutton.py +275 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +193 -0
- bootstack/style/builders/utils.py +455 -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/fonts.py +123 -0
- bootstack/style/style.py +609 -0
- bootstack/style/style_builder_base.py +716 -0
- bootstack/style/style_builder_mixed.py +93 -0
- bootstack/style/style_builder_tk.py +109 -0
- bootstack/style/style_builder_ttk.py +353 -0
- bootstack/style/style_resolver.py +447 -0
- bootstack/style/theme.py +245 -0
- bootstack/style/theme_provider.py +471 -0
- bootstack/style/themes/__init__.py +128 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +41 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +523 -0
- bootstack/style/utility.py +746 -0
- bootstack/types.py +39 -0
- bootstack/validation/__init__.py +6 -0
- bootstack/validation/types.py +15 -0
- bootstack/validation/validation_result.py +17 -0
- bootstack/validation/validation_rules.py +205 -0
- bootstack/widgets/__init__.py +74 -0
- bootstack/widgets/_core/__init__.py +31 -0
- bootstack/widgets/_core/app_config.py +266 -0
- bootstack/widgets/_core/base.py +461 -0
- bootstack/widgets/_core/container.py +334 -0
- bootstack/widgets/_core/context.py +35 -0
- bootstack/widgets/_core/events.py +130 -0
- bootstack/widgets/_core/field_mixin.py +353 -0
- bootstack/widgets/_core/icon_image_props.py +72 -0
- bootstack/widgets/_core/image_binding.py +93 -0
- bootstack/widgets/_core/navmodel.py +345 -0
- bootstack/widgets/_core/options.py +210 -0
- bootstack/widgets/_core/selection_group.py +130 -0
- bootstack/widgets/_core/window_controls.py +96 -0
- bootstack/widgets/_core/window_menu.py +246 -0
- bootstack/widgets/_impl/__init__.py +1 -0
- bootstack/widgets/_impl/_internal/__init__.py +0 -0
- bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
- bootstack/widgets/_impl/_parts/__init__.py +11 -0
- bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
- bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
- bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
- bootstack/widgets/_impl/composites/__init__.py +33 -0
- bootstack/widgets/_impl/composites/_dateutils.py +33 -0
- bootstack/widgets/_impl/composites/_image_fit.py +105 -0
- bootstack/widgets/_impl/composites/accordion.py +381 -0
- bootstack/widgets/_impl/composites/avatar.py +191 -0
- bootstack/widgets/_impl/composites/buttongroup.py +371 -0
- bootstack/widgets/_impl/composites/calendar.py +972 -0
- bootstack/widgets/_impl/composites/carousel.py +567 -0
- bootstack/widgets/_impl/composites/chart.py +882 -0
- bootstack/widgets/_impl/composites/compositeframe.py +298 -0
- bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
- bootstack/widgets/_impl/composites/dateentry.py +404 -0
- bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
- bootstack/widgets/_impl/composites/expander.py +515 -0
- bootstack/widgets/_impl/composites/field.py +670 -0
- bootstack/widgets/_impl/composites/form.py +1066 -0
- bootstack/widgets/_impl/composites/gallery.py +551 -0
- bootstack/widgets/_impl/composites/list/__init__.py +15 -0
- bootstack/widgets/_impl/composites/list/listitem.py +802 -0
- bootstack/widgets/_impl/composites/list/listview.py +1433 -0
- bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
- bootstack/widgets/_impl/composites/menu/model.py +358 -0
- bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
- bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
- bootstack/widgets/_impl/composites/meter.py +860 -0
- bootstack/widgets/_impl/composites/numericentry.py +201 -0
- bootstack/widgets/_impl/composites/pagestack.py +395 -0
- bootstack/widgets/_impl/composites/passwordentry.py +142 -0
- bootstack/widgets/_impl/composites/pathentry.py +168 -0
- bootstack/widgets/_impl/composites/picture.py +289 -0
- bootstack/widgets/_impl/composites/radiogroup.py +511 -0
- bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
- bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/_impl/composites/scrollview.py +764 -0
- bootstack/widgets/_impl/composites/selectbox.py +1026 -0
- bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
- bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
- bootstack/widgets/_impl/composites/shell/layout.py +335 -0
- bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
- bootstack/widgets/_impl/composites/shell/providers.py +558 -0
- bootstack/widgets/_impl/composites/shell/rail.py +117 -0
- bootstack/widgets/_impl/composites/shell/shell.py +581 -0
- bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
- bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
- bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
- bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
- bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
- bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
- bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
- bootstack/widgets/_impl/composites/slider/slider.py +851 -0
- bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
- bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
- bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
- bootstack/widgets/_impl/composites/tableview/types.py +169 -0
- bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
- bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
- bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
- bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
- bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
- bootstack/widgets/_impl/composites/textarea/change.py +46 -0
- bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
- bootstack/widgets/_impl/composites/textarea/core.py +495 -0
- bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
- bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
- bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
- bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
- bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
- bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
- bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
- bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
- bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
- bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
- bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
- bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
- bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
- bootstack/widgets/_impl/composites/textentry.py +57 -0
- bootstack/widgets/_impl/composites/timeentry.py +176 -0
- bootstack/widgets/_impl/composites/toast.py +390 -0
- bootstack/widgets/_impl/composites/toast_stack.py +156 -0
- bootstack/widgets/_impl/composites/togglegroup.py +418 -0
- bootstack/widgets/_impl/composites/toolbar.py +608 -0
- bootstack/widgets/_impl/composites/tooltip.py +491 -0
- bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
- bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
- bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
- bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
- bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
- bootstack/widgets/_impl/mixins/__init__.py +24 -0
- bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
- bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
- bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
- bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
- bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
- bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
- bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
- bootstack/widgets/_impl/primitives/__init__.py +49 -0
- bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
- bootstack/widgets/_impl/primitives/badge.py +45 -0
- bootstack/widgets/_impl/primitives/button.py +76 -0
- bootstack/widgets/_impl/primitives/card.py +45 -0
- bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
- bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
- bootstack/widgets/_impl/primitives/combobox.py +156 -0
- bootstack/widgets/_impl/primitives/entry.py +87 -0
- bootstack/widgets/_impl/primitives/flexframe.py +448 -0
- bootstack/widgets/_impl/primitives/frame.py +185 -0
- bootstack/widgets/_impl/primitives/gridframe.py +546 -0
- bootstack/widgets/_impl/primitives/label.py +84 -0
- bootstack/widgets/_impl/primitives/labelframe.py +54 -0
- bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
- bootstack/widgets/_impl/primitives/packframe.py +227 -0
- bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
- bootstack/widgets/_impl/primitives/progressbar.py +83 -0
- bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
- bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
- bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
- bootstack/widgets/_impl/primitives/separator.py +43 -0
- bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
- bootstack/widgets/_impl/primitives/spinbox.py +95 -0
- bootstack/widgets/_impl/primitives/switch.py +44 -0
- bootstack/widgets/_impl/primitives/treeview.py +69 -0
- bootstack/widgets/app.py +371 -0
- bootstack/widgets/appshell.py +1179 -0
- bootstack/widgets/avatar.py +140 -0
- bootstack/widgets/boolean_controls.py +455 -0
- bootstack/widgets/button.py +224 -0
- bootstack/widgets/buttongroup.py +239 -0
- bootstack/widgets/calendar.py +195 -0
- bootstack/widgets/card.py +159 -0
- bootstack/widgets/carousel.py +241 -0
- bootstack/widgets/chart.py +302 -0
- bootstack/widgets/codeeditor.py +675 -0
- bootstack/widgets/contextmenu.py +371 -0
- bootstack/widgets/datatable.py +688 -0
- bootstack/widgets/datefield.py +395 -0
- bootstack/widgets/divider.py +60 -0
- bootstack/widgets/expander.py +579 -0
- bootstack/widgets/form.py +200 -0
- bootstack/widgets/gallery.py +250 -0
- bootstack/widgets/gauge.py +168 -0
- bootstack/widgets/grid.py +121 -0
- bootstack/widgets/groupbox.py +162 -0
- bootstack/widgets/label.py +229 -0
- bootstack/widgets/listview.py +354 -0
- bootstack/widgets/menubutton.py +419 -0
- bootstack/widgets/numberfield.py +431 -0
- bootstack/widgets/pagestack.py +345 -0
- bootstack/widgets/passwordfield.py +383 -0
- bootstack/widgets/pathfield.py +454 -0
- bootstack/widgets/picture.py +251 -0
- bootstack/widgets/progressbar.py +106 -0
- bootstack/widgets/radio_variants.py +271 -0
- bootstack/widgets/radiogroup.py +228 -0
- bootstack/widgets/scrollbar.py +64 -0
- bootstack/widgets/scrollview.py +186 -0
- bootstack/widgets/select.py +302 -0
- bootstack/widgets/selectbutton.py +182 -0
- bootstack/widgets/sidebar_toggle.py +130 -0
- bootstack/widgets/sizegrip.py +42 -0
- bootstack/widgets/slider.py +408 -0
- bootstack/widgets/spinbox.py +147 -0
- bootstack/widgets/spinnerfield.py +413 -0
- bootstack/widgets/splash.py +367 -0
- bootstack/widgets/splitview.py +482 -0
- bootstack/widgets/stacks.py +249 -0
- bootstack/widgets/statusbar.py +189 -0
- bootstack/widgets/tabs.py +425 -0
- bootstack/widgets/textarea.py +459 -0
- bootstack/widgets/textfield.py +410 -0
- bootstack/widgets/theme_toggle.py +92 -0
- bootstack/widgets/timefield.py +340 -0
- bootstack/widgets/toast.py +314 -0
- bootstack/widgets/togglegroup.py +231 -0
- bootstack/widgets/toolbar.py +347 -0
- bootstack/widgets/tooltip.py +94 -0
- bootstack/widgets/tree.py +648 -0
- bootstack/widgets/types.py +305 -0
- bootstack/widgets/window.py +307 -0
- bootstack-0.1.0.dist-info/METADATA +301 -0
- bootstack-0.1.0.dist-info/RECORD +471 -0
- bootstack-0.1.0.dist-info/WHEEL +5 -0
- bootstack-0.1.0.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
- bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""Signal integration mixins for bootstack widgets.
|
|
2
|
+
|
|
3
|
+
Provides seamless integration between tkinter Variables and reactive Signals,
|
|
4
|
+
exposing both as properties on widgets that support `textvariable` and `variable`
|
|
5
|
+
options. The mixins maintain bidirectional synchronization between Variables and
|
|
6
|
+
Signals automatically.
|
|
7
|
+
|
|
8
|
+
These mixins are thin glue layers that delegate to the core signal capability
|
|
9
|
+
module for normalization and binding logic.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import tkinter as tk
|
|
15
|
+
from typing import Any, TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from bootstack.signals import Signal
|
|
19
|
+
|
|
20
|
+
from bootstack._core.capabilities.signals import (
|
|
21
|
+
normalize_signal,
|
|
22
|
+
create_signal,
|
|
23
|
+
infer_default_value_for_widget,
|
|
24
|
+
query_binding,
|
|
25
|
+
)
|
|
26
|
+
from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TextSignalMixin:
|
|
30
|
+
"""Mixin providing `.textvariable` and `.textsignal` properties for text-based widgets.
|
|
31
|
+
|
|
32
|
+
For widgets that support the `textvariable` option (Entry, Label, Button, etc.),
|
|
33
|
+
this mixin exposes both the underlying tk.Variable and a reactive Signal as properties,
|
|
34
|
+
maintaining bidirectional synchronization between them.
|
|
35
|
+
|
|
36
|
+
This mixin delegates normalization and binding logic to the core signals capability.
|
|
37
|
+
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, *args, **kwargs):
|
|
41
|
+
"""Initialize mixin and extract textsignal/textvariable parameters before tkinter sees them."""
|
|
42
|
+
# Extract textsignal and textvariable before passing kwargs to tkinter
|
|
43
|
+
textsignal_value = kwargs.pop('textsignal', None)
|
|
44
|
+
textvariable_value = kwargs.pop('textvariable', None)
|
|
45
|
+
|
|
46
|
+
# Call parent __init__
|
|
47
|
+
super().__init__(*args, **kwargs)
|
|
48
|
+
|
|
49
|
+
# Apply textsignal or textvariable after widget construction
|
|
50
|
+
# Prefer textsignal if both are provided
|
|
51
|
+
if textsignal_value is not None:
|
|
52
|
+
self._config_delegate_set('textsignal', textsignal_value)
|
|
53
|
+
elif textvariable_value is not None:
|
|
54
|
+
self._config_delegate_set('textvariable', textvariable_value)
|
|
55
|
+
|
|
56
|
+
@configure_delegate("textvariable", "textsignal")
|
|
57
|
+
def _delegate_textsignal(self, value: Any = None):
|
|
58
|
+
"""Handle textvariable and textsignal configuration.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
value: Signal, tk.Variable, or None for query.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Current value for query path, None for set path.
|
|
65
|
+
"""
|
|
66
|
+
# Query path - return stored value
|
|
67
|
+
if value is None:
|
|
68
|
+
return query_binding(
|
|
69
|
+
getattr(self, '_textsignal', None),
|
|
70
|
+
getattr(self, '_textvariable', None),
|
|
71
|
+
self._ttk_base, # type: ignore[misc]
|
|
72
|
+
self,
|
|
73
|
+
'textvariable',
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Set path - normalize and apply
|
|
77
|
+
binding = normalize_signal(value, default_value="")
|
|
78
|
+
if binding is not None:
|
|
79
|
+
self._textsignal = binding.signal
|
|
80
|
+
self._textvariable = binding.variable
|
|
81
|
+
var_to_set = binding.tk_value
|
|
82
|
+
else:
|
|
83
|
+
# String (Tcl variable name) or other - pass through
|
|
84
|
+
var_to_set = value
|
|
85
|
+
if value:
|
|
86
|
+
self._textvariable = value
|
|
87
|
+
|
|
88
|
+
# Apply via base ttk widget to avoid recursion
|
|
89
|
+
return self._ttk_base.configure(self, textvariable=var_to_set) # type: ignore[misc]
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def textsignal(self) -> Signal[str]:
|
|
93
|
+
"""Get or lazily create the textsignal.
|
|
94
|
+
|
|
95
|
+
If no textvariable has been set, creates a new Signal with empty string.
|
|
96
|
+
If textvariable exists, wraps it in a Signal.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The reactive Signal for this widget's text.
|
|
100
|
+
"""
|
|
101
|
+
if not hasattr(self, '_textsignal'):
|
|
102
|
+
# Create fresh signal
|
|
103
|
+
binding = create_signal("")
|
|
104
|
+
self._textsignal = binding.signal
|
|
105
|
+
self._textvariable = binding.variable
|
|
106
|
+
try:
|
|
107
|
+
self._ttk_base.configure(self, textvariable=binding.tk_value) # type: ignore[misc]
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
return self._textsignal
|
|
111
|
+
|
|
112
|
+
@textsignal.setter
|
|
113
|
+
def textsignal(self, value: Signal[str]) -> None:
|
|
114
|
+
"""Set the textsignal, extracting and configuring its underlying variable.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
value: Signal to use for this widget's text.
|
|
118
|
+
"""
|
|
119
|
+
self._delegate_textsignal(value)
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def textvariable(self) -> tk.Variable:
|
|
123
|
+
"""Get the underlying tk.Variable.
|
|
124
|
+
|
|
125
|
+
If not yet created, accessing this property will trigger lazy creation
|
|
126
|
+
via the textsignal property.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
The tk.Variable (usually StringVar) for this widget's text.
|
|
130
|
+
|
|
131
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
132
|
+
"""
|
|
133
|
+
if not hasattr(self, '_textvariable'):
|
|
134
|
+
# Trigger lazy creation via textsignal
|
|
135
|
+
_ = self.textsignal
|
|
136
|
+
return self._textvariable
|
|
137
|
+
|
|
138
|
+
@textvariable.setter
|
|
139
|
+
def textvariable(self, value: tk.Variable) -> None:
|
|
140
|
+
"""Set the textvariable, creating a synced Signal automatically.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
value: tk.Variable to use for this widget's text.
|
|
144
|
+
"""
|
|
145
|
+
self._delegate_textsignal(value)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
class SignalMixin:
|
|
149
|
+
"""Mixin providing `.variable` and `.signal` properties for value-based widgets.
|
|
150
|
+
|
|
151
|
+
For widgets that support the `variable` option (Checkbutton, Radiobutton, Scale, etc.),
|
|
152
|
+
this mixin exposes both the underlying tk.Variable and a reactive Signal as properties,
|
|
153
|
+
maintaining bidirectional synchronization between them.
|
|
154
|
+
|
|
155
|
+
This mixin delegates normalization and binding logic to the core signals capability.
|
|
156
|
+
|
|
157
|
+
"""
|
|
158
|
+
|
|
159
|
+
def __init__(self, *args, **kwargs):
|
|
160
|
+
"""Initialize mixin and extract signal/variable parameters before tkinter sees them."""
|
|
161
|
+
# Extract signal and variable before passing kwargs to tkinter
|
|
162
|
+
signal_value = kwargs.pop('signal', None)
|
|
163
|
+
variable_value = kwargs.pop('variable', None)
|
|
164
|
+
|
|
165
|
+
# Call parent __init__
|
|
166
|
+
super().__init__(*args, **kwargs)
|
|
167
|
+
|
|
168
|
+
# Apply signal or variable after widget construction
|
|
169
|
+
# Prefer signal if both are provided
|
|
170
|
+
if signal_value is not None:
|
|
171
|
+
self._config_delegate_set('signal', signal_value)
|
|
172
|
+
elif variable_value is not None:
|
|
173
|
+
self._config_delegate_set('variable', variable_value)
|
|
174
|
+
|
|
175
|
+
@configure_delegate("variable", "signal")
|
|
176
|
+
def _delegate_signal(self, value: Any = None):
|
|
177
|
+
"""Handle variable and signal configuration.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
value: Signal, tk.Variable, or None for query.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
Current value for query path, None for set path.
|
|
184
|
+
"""
|
|
185
|
+
# Query path - return stored value
|
|
186
|
+
if value is None:
|
|
187
|
+
return query_binding(
|
|
188
|
+
getattr(self, '_signal', None),
|
|
189
|
+
getattr(self, '_variable', None),
|
|
190
|
+
self._ttk_base, # type: ignore[misc]
|
|
191
|
+
self,
|
|
192
|
+
'variable',
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Set path - normalize and apply
|
|
196
|
+
binding = normalize_signal(value)
|
|
197
|
+
if binding is not None:
|
|
198
|
+
self._signal = binding.signal
|
|
199
|
+
self._variable = binding.variable
|
|
200
|
+
var_to_set = binding.tk_value
|
|
201
|
+
else:
|
|
202
|
+
# String (Tcl variable name) or other - pass through
|
|
203
|
+
var_to_set = value
|
|
204
|
+
if value:
|
|
205
|
+
self._variable = value
|
|
206
|
+
|
|
207
|
+
# Apply via base ttk widget to avoid recursion
|
|
208
|
+
return self._ttk_base.configure(self, variable=var_to_set) # type: ignore[misc]
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def signal(self) -> Signal[Any]:
|
|
212
|
+
"""Get or lazily create the signal.
|
|
213
|
+
|
|
214
|
+
If no variable has been set, creates a new Signal with appropriate default
|
|
215
|
+
(False for Checkbutton, 0 for Scale, etc.). If variable exists, wraps it in a Signal.
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
The reactive Signal for this widget's value.
|
|
219
|
+
"""
|
|
220
|
+
if not hasattr(self, '_signal'):
|
|
221
|
+
# Infer default value based on widget type
|
|
222
|
+
default_value = infer_default_value_for_widget(self.winfo_class())
|
|
223
|
+
binding = create_signal(default_value)
|
|
224
|
+
self._signal = binding.signal
|
|
225
|
+
self._variable = binding.variable
|
|
226
|
+
try:
|
|
227
|
+
self._ttk_base.configure(self, variable=binding.tk_value) # type: ignore[misc]
|
|
228
|
+
except Exception:
|
|
229
|
+
pass
|
|
230
|
+
return self._signal
|
|
231
|
+
|
|
232
|
+
@signal.setter
|
|
233
|
+
def signal(self, value: Signal[Any]) -> None:
|
|
234
|
+
"""Set the signal, extracting and configuring its underlying variable.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
value: Signal to use for this widget's value.
|
|
238
|
+
"""
|
|
239
|
+
self._delegate_signal(value)
|
|
240
|
+
|
|
241
|
+
@property
|
|
242
|
+
def variable(self) -> tk.Variable:
|
|
243
|
+
"""Get the underlying tk.Variable.
|
|
244
|
+
|
|
245
|
+
If not yet created, accessing this property will trigger lazy creation
|
|
246
|
+
via the signal property.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
The tk.Variable (IntVar, BooleanVar, DoubleVar, etc.) for this widget's value.
|
|
250
|
+
|
|
251
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
252
|
+
"""
|
|
253
|
+
if not hasattr(self, '_variable'):
|
|
254
|
+
# Trigger lazy creation via signal
|
|
255
|
+
_ = self.signal
|
|
256
|
+
return self._variable
|
|
257
|
+
|
|
258
|
+
@variable.setter
|
|
259
|
+
def variable(self, value: tk.Variable) -> None:
|
|
260
|
+
"""Set the variable, creating a synced Signal automatically.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
value: tk.Variable to use for this widget's value.
|
|
264
|
+
"""
|
|
265
|
+
self._delegate_signal(value)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
__all__ = ["TextSignalMixin", "SignalMixin"]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Validation mixin for widgets with enhanced event system.
|
|
2
|
+
|
|
3
|
+
This mixin provides validation functionality using the enhanced event system
|
|
4
|
+
that allows passing data directly through virtual events.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import tkinter
|
|
10
|
+
from tkinter import TclError
|
|
11
|
+
from tkinter.ttk import Widget
|
|
12
|
+
from typing import Any, Callable
|
|
13
|
+
|
|
14
|
+
from bootstack.events import ValidationEvent
|
|
15
|
+
from bootstack.signals import Signal
|
|
16
|
+
from bootstack.validation import ValidationRule
|
|
17
|
+
from bootstack.validation.types import RuleTriggerType, RuleType, ValidationOptions
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ValidationMixin(Widget):
|
|
21
|
+
"""Pure-Tkinter validation mixin for bound widgets.
|
|
22
|
+
|
|
23
|
+
Provides debounced auto-validation on key/blur with virtual event emission.
|
|
24
|
+
Event data is accessible via `event.data` in handlers.
|
|
25
|
+
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
EVENT_VALID = '<<Valid>>'
|
|
29
|
+
EVENT_INVALID = '<<Invalid>>'
|
|
30
|
+
EVENT_VALIDATED = '<<Validate>>'
|
|
31
|
+
SEQ_KEYUP = '<KeyRelease>'
|
|
32
|
+
SEQ_BLUR = '<FocusOut>'
|
|
33
|
+
|
|
34
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
35
|
+
"""Set up validation state before the widget is fully initialized.
|
|
36
|
+
|
|
37
|
+
Initialises the rule list, debounce tracking, and optional convenience
|
|
38
|
+
callbacks. Binds `<<KeyRelease>>` and `<<FocusOut>>` to trigger
|
|
39
|
+
debounced validation automatically.
|
|
40
|
+
"""
|
|
41
|
+
# Validation rules
|
|
42
|
+
self._rules: list[ValidationRule] = []
|
|
43
|
+
|
|
44
|
+
# Debounce tracking
|
|
45
|
+
self._debounce_ids: dict[str, str] = {}
|
|
46
|
+
|
|
47
|
+
# Optional convenience callbacks
|
|
48
|
+
self._on_invalid_command: Callable | None = None
|
|
49
|
+
self._on_valid_command: Callable | None = None
|
|
50
|
+
self._on_validated_command: Callable | None = None
|
|
51
|
+
|
|
52
|
+
super().__init__(*args, **kwargs) # next in MRO must be a Tk/ttk widget
|
|
53
|
+
|
|
54
|
+
# Reactive validity state — the source of truth for the field's display
|
|
55
|
+
# and for form-level aggregation. Updated on every validation run.
|
|
56
|
+
self._valid_signal: Signal = Signal(True)
|
|
57
|
+
self._error_signal: Signal = Signal("")
|
|
58
|
+
|
|
59
|
+
self._setup_validation_binds()
|
|
60
|
+
|
|
61
|
+
def _set_validity(self, is_valid: bool, message: str) -> None:
|
|
62
|
+
"""Update the reactive validity signals (the source of truth)."""
|
|
63
|
+
self._error_signal.set("" if is_valid else message)
|
|
64
|
+
self._valid_signal.set(is_valid)
|
|
65
|
+
|
|
66
|
+
# ---------------- Public API ----------------
|
|
67
|
+
|
|
68
|
+
def value(self) -> Any:
|
|
69
|
+
"""Default value accessor; override for non-Entry widgets.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Current widget value
|
|
73
|
+
"""
|
|
74
|
+
if hasattr(self, 'get'):
|
|
75
|
+
try:
|
|
76
|
+
return self.get()
|
|
77
|
+
except AttributeError:
|
|
78
|
+
return None
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
def add_validation_rule(self, rule_type: RuleType, **kwargs: ValidationOptions) -> None:
|
|
82
|
+
"""Add a single validation rule.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
rule_type: Type of validation rule (e.g., 'required', 'min_length')
|
|
86
|
+
**kwargs: Rule-specific options (e.g., min_length=5, message='...')
|
|
87
|
+
"""
|
|
88
|
+
self._rules.append(ValidationRule(rule_type, **kwargs))
|
|
89
|
+
|
|
90
|
+
def add_validation_rules(self, rules: list[ValidationRule]) -> None:
|
|
91
|
+
"""Replace all validation rules.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
rules: List of ValidationRule objects
|
|
95
|
+
"""
|
|
96
|
+
self._rules = list(rules)
|
|
97
|
+
|
|
98
|
+
def validate(self, value: Any, trigger: RuleTriggerType = "manual") -> bool:
|
|
99
|
+
"""Run validation rules against a value.
|
|
100
|
+
|
|
101
|
+
Emits `<<Valid>>`, `<<Invalid>>`, and `<<Validate>>` events
|
|
102
|
+
with data payload containing validation results.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
value: Value to validate
|
|
106
|
+
trigger: Trigger type ('manual', 'key', 'blur', or 'always')
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if every applicable rule passed (vacuously true when no rule
|
|
110
|
+
applies for this trigger), False on the first failure.
|
|
111
|
+
"""
|
|
112
|
+
ran_rule = False
|
|
113
|
+
|
|
114
|
+
for rule in self._rules:
|
|
115
|
+
if trigger != "manual" and rule.trigger not in ("always", trigger):
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
ran_rule = True
|
|
119
|
+
result = rule.validate(value)
|
|
120
|
+
|
|
121
|
+
if not result.is_valid:
|
|
122
|
+
# Update validity state, then emit invalid + validated events.
|
|
123
|
+
self._set_validity(False, result.message)
|
|
124
|
+
payload = ValidationEvent(value=value, is_valid=False, message=result.message)
|
|
125
|
+
self.event_generate(self.EVENT_INVALID, data=payload)
|
|
126
|
+
self.event_generate(self.EVENT_VALIDATED, data=payload)
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
if ran_rule:
|
|
130
|
+
# Update validity state, then emit valid + validated events.
|
|
131
|
+
self._set_validity(True, "")
|
|
132
|
+
payload = ValidationEvent(value=value, is_valid=True, message="")
|
|
133
|
+
self.event_generate(self.EVENT_VALID, data=payload)
|
|
134
|
+
self.event_generate(self.EVENT_VALIDATED, data=payload)
|
|
135
|
+
|
|
136
|
+
# Nothing failed — pass (vacuously when no rule ran for this trigger).
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
# Optional: ergonomic callback registration
|
|
140
|
+
def on_invalid(self, func: Callable) -> None:
|
|
141
|
+
"""Register callback for `<<Invalid>>`. Callback receives a `ValidationEvent` payload (value, is_valid, message)."""
|
|
142
|
+
self._on_invalid_command = func
|
|
143
|
+
|
|
144
|
+
def off_invalid(self, bind_id: str | None = None) -> None:
|
|
145
|
+
"""Unsubscribe from `<<Invalid>>` events.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
bind_id: ID returned by `on_invalid()`. If None, removes all bindings.
|
|
149
|
+
"""
|
|
150
|
+
self.unbind('<<Invalid>>', bind_id)
|
|
151
|
+
|
|
152
|
+
def on_valid(self, func: Callable) -> None:
|
|
153
|
+
"""Register callback for `<<Valid>>`. Callback receives a `ValidationEvent` payload (value, is_valid, message)."""
|
|
154
|
+
self._on_valid_command = func
|
|
155
|
+
|
|
156
|
+
def off_valid(self, bind_id: str | None = None) -> None:
|
|
157
|
+
"""Unsubscribe from `<<Valid>>` events.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
bind_id: ID returned by `on_valid()`. If None, removes all bindings.
|
|
161
|
+
"""
|
|
162
|
+
self.unbind('<<Valid>>', bind_id)
|
|
163
|
+
|
|
164
|
+
def on_validated(self, func: Callable) -> None:
|
|
165
|
+
"""Register callback for `<<Validate>>`. Callback receives a `ValidationEvent` payload (value, is_valid, message)."""
|
|
166
|
+
self._on_validated_command = func
|
|
167
|
+
|
|
168
|
+
def off_validated(self, bind_id: str | None = None) -> None:
|
|
169
|
+
"""Unsubscribe from `<<Validate>>` events.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
bind_id: ID returned by `on_validated()`. If None, removes all bindings.
|
|
173
|
+
"""
|
|
174
|
+
self.unbind('<<Validate>>', bind_id)
|
|
175
|
+
|
|
176
|
+
# ---------------- Internals ----------------
|
|
177
|
+
|
|
178
|
+
def _get_validation_value(self) -> Any:
|
|
179
|
+
"""Get the current value for validation purposes.
|
|
180
|
+
|
|
181
|
+
For entry widgets, this should return the current text being typed,
|
|
182
|
+
not the committed value. Prefers get() method over value() method.
|
|
183
|
+
"""
|
|
184
|
+
if hasattr(self, 'get'):
|
|
185
|
+
try:
|
|
186
|
+
return self.get()
|
|
187
|
+
except (AttributeError, TclError):
|
|
188
|
+
pass
|
|
189
|
+
return self.value()
|
|
190
|
+
|
|
191
|
+
def _setup_validation_binds(self, keyup_delay_ms: int = 50, blur_delay_ms: int = 50) -> None:
|
|
192
|
+
"""Set up automatic validation bindings with debouncing."""
|
|
193
|
+
# Auto-validate (debounced)
|
|
194
|
+
self.bind(self.SEQ_KEYUP, lambda e: self._debounced("key", keyup_delay_ms), add=True)
|
|
195
|
+
self.bind(self.SEQ_BLUR, lambda e: self._debounced("blur", blur_delay_ms), add=True)
|
|
196
|
+
|
|
197
|
+
# Wire optional convenience callbacks
|
|
198
|
+
self.bind(self.EVENT_VALIDATED, self._dispatch_validated, add=True)
|
|
199
|
+
self.bind(self.EVENT_VALID, self._dispatch_valid, add=True)
|
|
200
|
+
self.bind(self.EVENT_INVALID, self._dispatch_invalid, add=True)
|
|
201
|
+
|
|
202
|
+
def _debounced(self, trigger: RuleTriggerType, ms: int) -> None:
|
|
203
|
+
"""Debounce validation to avoid excessive checks during typing."""
|
|
204
|
+
key = f"debounce:{trigger}"
|
|
205
|
+
aid: str | None = self._debounce_ids.get(key)
|
|
206
|
+
if aid:
|
|
207
|
+
try:
|
|
208
|
+
self.after_cancel(aid)
|
|
209
|
+
except TclError:
|
|
210
|
+
pass
|
|
211
|
+
# defer reading value until the timer fires
|
|
212
|
+
self._debounce_ids[key] = self.after(ms, lambda: self.validate(self._get_validation_value(), trigger))
|
|
213
|
+
|
|
214
|
+
# ----- optional dispatchers for on_* convenience -----
|
|
215
|
+
|
|
216
|
+
def _dispatch_validated(self, event: tkinter.Event) -> None:
|
|
217
|
+
if self._on_validated_command:
|
|
218
|
+
self._on_validated_command(event)
|
|
219
|
+
|
|
220
|
+
def _dispatch_valid(self, event: tkinter.Event) -> None:
|
|
221
|
+
if self._on_valid_command:
|
|
222
|
+
self._on_valid_command(event)
|
|
223
|
+
|
|
224
|
+
def _dispatch_invalid(self, event: tkinter.Event) -> None:
|
|
225
|
+
if self._on_invalid_command:
|
|
226
|
+
self._on_invalid_command(event)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Primitive bootstack widgets."""
|
|
2
|
+
|
|
3
|
+
from .badge import Badge
|
|
4
|
+
from .button import Button
|
|
5
|
+
from .card import Card
|
|
6
|
+
from .checkbutton import CheckButton
|
|
7
|
+
from .checktoggle import CheckToggle
|
|
8
|
+
from .combobox import Combobox
|
|
9
|
+
from .entry import Entry
|
|
10
|
+
from .frame import Frame
|
|
11
|
+
from .gridframe import GridFrame
|
|
12
|
+
from .label import Label
|
|
13
|
+
from .labelframe import LabelFrame
|
|
14
|
+
from .packframe import PackFrame
|
|
15
|
+
from .panedwindow import PanedWindow
|
|
16
|
+
from .progressbar import Progressbar
|
|
17
|
+
from .radiobutton import RadioButton
|
|
18
|
+
from .radiotoggle import RadioToggle
|
|
19
|
+
from .scrollbar import Scrollbar
|
|
20
|
+
from .separator import Separator
|
|
21
|
+
from .sizegrip import SizeGrip
|
|
22
|
+
from .spinbox import Spinbox
|
|
23
|
+
from .switch import Switch
|
|
24
|
+
from .treeview import TreeView
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"Badge",
|
|
28
|
+
"Button",
|
|
29
|
+
"Card",
|
|
30
|
+
"CheckButton",
|
|
31
|
+
"CheckToggle",
|
|
32
|
+
"Combobox",
|
|
33
|
+
"Entry",
|
|
34
|
+
"Frame",
|
|
35
|
+
"GridFrame",
|
|
36
|
+
"Label",
|
|
37
|
+
"LabelFrame",
|
|
38
|
+
"PackFrame",
|
|
39
|
+
"PanedWindow",
|
|
40
|
+
"Progressbar",
|
|
41
|
+
"RadioButton",
|
|
42
|
+
"RadioToggle",
|
|
43
|
+
"Scrollbar",
|
|
44
|
+
"Separator",
|
|
45
|
+
"SizeGrip",
|
|
46
|
+
"Spinbox",
|
|
47
|
+
"Switch",
|
|
48
|
+
"TreeView",
|
|
49
|
+
]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tkinter import ttk
|
|
4
|
+
from typing import Any, Literal, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Unpack
|
|
7
|
+
|
|
8
|
+
from bootstack._core.mixins.ttk_state import TtkStateMixin
|
|
9
|
+
from bootstack._core.mixins.widget import WidgetCapabilitiesMixin
|
|
10
|
+
from bootstack.widgets._impl._internal.wrapper_base import TTKWrapperBase
|
|
11
|
+
from bootstack.widgets._impl.mixins import IconMixin, LocalizationMixin, TextSignalMixin
|
|
12
|
+
from bootstack.widgets.types import Master, StyledKwargs, CompoundMode, WidgetState, WidgetDensity
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from bootstack.signals import Signal
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MenuButtonKwargs(StyledKwargs, total=False):
|
|
19
|
+
# Standard ttk.Menubutton options
|
|
20
|
+
text: Any
|
|
21
|
+
image: Any
|
|
22
|
+
icon: Any
|
|
23
|
+
icon_only: bool
|
|
24
|
+
compound: CompoundMode
|
|
25
|
+
direction: Any
|
|
26
|
+
menu: Any
|
|
27
|
+
padding: Any
|
|
28
|
+
state: WidgetState
|
|
29
|
+
textvariable: Any
|
|
30
|
+
textsignal: Signal[Any]
|
|
31
|
+
|
|
32
|
+
# bootstack-specific extensions
|
|
33
|
+
density: WidgetDensity
|
|
34
|
+
localize: bool | Literal['auto']
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class MenuButton(LocalizationMixin, TextSignalMixin, IconMixin, TTKWrapperBase, WidgetCapabilitiesMixin, TtkStateMixin, ttk.Menubutton):
|
|
38
|
+
"""bootstack wrapper for `ttk.Menubutton` with themed styling and icon support."""
|
|
39
|
+
|
|
40
|
+
_ttk_base = ttk.Menubutton
|
|
41
|
+
|
|
42
|
+
def __init__(self, master: Master = None, **kwargs: Unpack[MenuButtonKwargs]) -> None:
|
|
43
|
+
"""Create a themed bootstack Menubutton.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
master: Parent widget. If None, uses the default root window.
|
|
47
|
+
|
|
48
|
+
Other Parameters:
|
|
49
|
+
text: Text to display in the menubutton.
|
|
50
|
+
image: Image to display.
|
|
51
|
+
icon: Theme-aware icon spec handled by the style system.
|
|
52
|
+
icon_only: If True, removes the additional padding reserved for label text.
|
|
53
|
+
compound: Placement of the image relative to text.
|
|
54
|
+
direction: Direction for the menu to appear.
|
|
55
|
+
menu: Associated tk.Menu instance.
|
|
56
|
+
padding: Extra space around the content.
|
|
57
|
+
density: Widget density — 'default' or 'compact'.
|
|
58
|
+
state: Widget state — 'normal', 'active', 'disabled', or 'readonly'.
|
|
59
|
+
takefocus: Whether the widget participates in focus traversal.
|
|
60
|
+
textvariable: Tk variable linked to the text.
|
|
61
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
62
|
+
textsignal: Reactive Signal linked to the text (auto-synced with textvariable).
|
|
63
|
+
style: Explicit ttk style name (overrides accent/variant).
|
|
64
|
+
accent: Accent token for styling, e.g. 'primary', 'danger', 'success'.
|
|
65
|
+
variant: Style variant, e.g. 'solid', 'outline', 'ghost'.
|
|
66
|
+
surface: Optional surface token; otherwise inherited.
|
|
67
|
+
style_options: Optional dict forwarded to the style builder.
|
|
68
|
+
localize: Determines the widget's localization mode.
|
|
69
|
+
"""
|
|
70
|
+
kwargs.update(style_options=self._capture_style_options(['icon_only', 'icon', 'density'], kwargs))
|
|
71
|
+
super().__init__(master, **kwargs)
|
|
72
|
+
|
|
73
|
+
# When a native tk.Menu is attached, post it ourselves with the
|
|
74
|
+
# focus-ring affordance offset so it aligns with the visible button
|
|
75
|
+
# border. Subclasses (OptionMenu, DropdownButton) use a ContextMenu
|
|
76
|
+
# and override show_menu, so this only affects the native path.
|
|
77
|
+
# On Aqua, Tk's native Menubutton class binding drives an NSMenu
|
|
78
|
+
# state machine that manages mouse/focus capture; bypassing it with
|
|
79
|
+
# a manual `menu post` strands focus after item selection, so we
|
|
80
|
+
# skip the offset path on Mac and let the default class binding run.
|
|
81
|
+
if self.tk.call('tk', 'windowingsystem') != 'aqua':
|
|
82
|
+
self.bind("<Button-1>", self._on_button_press, add="+")
|
|
83
|
+
|
|
84
|
+
def _on_button_press(self, _event):
|
|
85
|
+
"""Focus self, and if a native menu is attached, post it with offset."""
|
|
86
|
+
self.focus_set()
|
|
87
|
+
try:
|
|
88
|
+
menu_path = self.cget('menu')
|
|
89
|
+
except Exception:
|
|
90
|
+
menu_path = None
|
|
91
|
+
if not menu_path:
|
|
92
|
+
return None
|
|
93
|
+
try:
|
|
94
|
+
# If the menu is already visible, unpost so a second click closes it.
|
|
95
|
+
if int(self.tk.eval(f"winfo viewable {menu_path}")):
|
|
96
|
+
self.tk.call(menu_path, 'unpost')
|
|
97
|
+
return 'break'
|
|
98
|
+
from bootstack.style.style_builder_base import StyleBuilderBase
|
|
99
|
+
offset = StyleBuilderBase.scale_from_source(10)
|
|
100
|
+
x = self.winfo_rootx() + offset
|
|
101
|
+
y = self.winfo_rooty() + self.winfo_height()
|
|
102
|
+
self.tk.call(menu_path, 'post', x, y)
|
|
103
|
+
except Exception:
|
|
104
|
+
return None
|
|
105
|
+
# Suppress Tk's default class binding so it doesn't also post the
|
|
106
|
+
# menu at its un-offset position.
|
|
107
|
+
return 'break'
|