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,670 @@
|
|
|
1
|
+
"""Field widget module.
|
|
2
|
+
|
|
3
|
+
Provides a flexible generic entry field composite widget used as the foundation
|
|
4
|
+
for creating specialized entry widgets like TextEntry, PasswordEntry, NumberEntry, etc.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from tkinter import TclError, Variable
|
|
8
|
+
from typing import Any, Callable, Literal, Type, TypedDict, cast
|
|
9
|
+
|
|
10
|
+
from bootstack.signals import Signal
|
|
11
|
+
from bootstack.validation.types import RuleType
|
|
12
|
+
from bootstack.widgets._impl.primitives.button import Button
|
|
13
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
14
|
+
from bootstack.widgets._impl.primitives.label import Label
|
|
15
|
+
from bootstack.widgets._impl.primitives.checkbutton import CheckButton
|
|
16
|
+
from bootstack.widgets._impl.primitives.checktoggle import CheckToggle
|
|
17
|
+
from bootstack.widgets._impl.mixins import configure_delegate
|
|
18
|
+
from bootstack.widgets._impl.mixins.entry_mixin import EntryMixin
|
|
19
|
+
from bootstack._runtime.utility import scale_padding_floor
|
|
20
|
+
from bootstack.widgets._impl._parts.numberentry_part import NumberEntryPart
|
|
21
|
+
from bootstack.widgets._impl._parts.textentry_part import TextEntryPart
|
|
22
|
+
from bootstack.widgets._impl._parts.spinnerentry_part import SpinnerEntryPart
|
|
23
|
+
from bootstack.widgets.types import Master, AccentToken, Justify, WidgetDensity, WidgetState
|
|
24
|
+
|
|
25
|
+
FieldKind = Literal['text', 'numeric', 'spinbox']
|
|
26
|
+
|
|
27
|
+
EntryWidget = TextEntryPart | NumberEntryPart | SpinnerEntryPart
|
|
28
|
+
"""The internal entry widget used by a Field — one of the three entry part types."""
|
|
29
|
+
|
|
30
|
+
FieldAddonWidget = Button | Label | CheckToggle
|
|
31
|
+
"""Widget types supported by `Field.insert_addon`."""
|
|
32
|
+
"""Type alias for field kind specification.
|
|
33
|
+
|
|
34
|
+
Determines which entry part widget to use:
|
|
35
|
+
- 'text': Uses TextEntryPart for text input with formatting support
|
|
36
|
+
- 'numeric': Uses NumberEntryPart for numeric input with bounds and stepping
|
|
37
|
+
- 'spinbox': Uses SpinnerEntryPart for spinner input (supports text or numeric values)
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class FieldOptions(TypedDict, total=False):
|
|
42
|
+
"""Type hints for Field widget configuration options.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
allow_blank: If True, empty input is allowed. If False, empty input preserves previous value.
|
|
46
|
+
accent: Accent token for the focus ring and active border of the input.
|
|
47
|
+
density: Widget density. 'default' for normal size, 'compact' for smaller size.
|
|
48
|
+
variant: Style variant (if applicable).
|
|
49
|
+
cursor: Cursor to display when hovering over the widget.
|
|
50
|
+
value_format: ICU format pattern for parsing/formatting (e.g., '$#,##0.00' for currency).
|
|
51
|
+
exportselection: If True, selected text is exported to X selection.
|
|
52
|
+
font: Font to use for text display.
|
|
53
|
+
foreground: Text color.
|
|
54
|
+
initial_focus: If True, widget receives focus when created.
|
|
55
|
+
justify: Text justification ('left', 'center', 'right').
|
|
56
|
+
show_message: If True, displays message text below the field.
|
|
57
|
+
padding: Padding around the entry widget.
|
|
58
|
+
show: Character to display instead of typed characters (for password fields).
|
|
59
|
+
state: The widget state. One of 'normal', 'disabled', or 'readonly'.
|
|
60
|
+
takefocus: If True, widget can receive focus via Tab key.
|
|
61
|
+
textvariable: Tkinter Variable to link with the entry text.
|
|
62
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
63
|
+
textsignal: Signal object for reactive text updates.
|
|
64
|
+
width: Width of the entry in characters.
|
|
65
|
+
required: If True, field cannot be empty (adds validation rule).
|
|
66
|
+
xscrollcommand: Callback for horizontal scrolling.
|
|
67
|
+
localize: Determines the field label localization mode. 'auto', True, False.
|
|
68
|
+
"""
|
|
69
|
+
allow_blank: bool
|
|
70
|
+
accent: AccentToken | str
|
|
71
|
+
density: WidgetDensity
|
|
72
|
+
variant: str
|
|
73
|
+
cursor: str
|
|
74
|
+
value_format: str
|
|
75
|
+
exportselection: bool
|
|
76
|
+
font: str
|
|
77
|
+
foreground: str
|
|
78
|
+
initial_focus: bool
|
|
79
|
+
justify: Justify
|
|
80
|
+
show_message: bool
|
|
81
|
+
padding: str
|
|
82
|
+
show: str
|
|
83
|
+
state: WidgetState
|
|
84
|
+
takefocus: bool
|
|
85
|
+
textvariable: Variable
|
|
86
|
+
textsignal: Signal
|
|
87
|
+
width: int
|
|
88
|
+
required: bool
|
|
89
|
+
placeholder: str
|
|
90
|
+
xscrollcommand: Callable[[int, int], None]
|
|
91
|
+
localize: bool | Literal['auto']
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class Field(EntryMixin, Frame):
|
|
95
|
+
"""A flexible generic composite entry field widget.
|
|
96
|
+
|
|
97
|
+
Field is a base composite widget that combines a label, entry input, and
|
|
98
|
+
message area into a complete input field component. It serves as the foundation
|
|
99
|
+
for creating specialized entry widgets like TextEntry, PasswordEntry, NumberEntry,
|
|
100
|
+
and other custom entry types.
|
|
101
|
+
|
|
102
|
+
The widget automatically handles layout, focus states, validation feedback, and
|
|
103
|
+
provides a consistent API for all entry-based components. It supports both text
|
|
104
|
+
and numeric input types through the `kind` parameter.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
master: Master = None,
|
|
110
|
+
*,
|
|
111
|
+
value: str | int | float = None,
|
|
112
|
+
label: str = None,
|
|
113
|
+
message: str = None,
|
|
114
|
+
show_message: bool = False,
|
|
115
|
+
required: bool = False,
|
|
116
|
+
kind: FieldKind = "text",
|
|
117
|
+
**kwargs: Any
|
|
118
|
+
):
|
|
119
|
+
"""Args:
|
|
120
|
+
master: Parent widget. If None, uses the default root window.
|
|
121
|
+
value: Initial value to display. Can be str, int, or float depending
|
|
122
|
+
on the field kind. For 'text' kind, should be a string. For
|
|
123
|
+
'numeric' kind, can be int or float. Default is None.
|
|
124
|
+
label: Optional label text to display above the entry field.
|
|
125
|
+
If required=True, an asterisk (*) is automatically appended to
|
|
126
|
+
indicate the field is mandatory.
|
|
127
|
+
message: Optional message text to display below the entry field.
|
|
128
|
+
Used for hints, instructions, or help text. This text is replaced
|
|
129
|
+
by validation error messages when validation fails, and restored
|
|
130
|
+
when validation passes. Default is None (no message).
|
|
131
|
+
show_message: If True, displays the message area below the field.
|
|
132
|
+
If False, hides the message area entirely (validation errors
|
|
133
|
+
won't be shown). Defaults to False, but auto-enables when
|
|
134
|
+
`message=` or `required=True` is set.
|
|
135
|
+
required: If True, marks the field as required and automatically adds
|
|
136
|
+
a 'required' validation rule. An asterisk (*) is appended to the
|
|
137
|
+
label. The field cannot be left empty. Default is False.
|
|
138
|
+
kind: Type of entry field to create. Either 'text' for text input
|
|
139
|
+
(uses TextEntryPart) or 'numeric' for numeric input (uses
|
|
140
|
+
NumberEntryPart). Default is 'text'.
|
|
141
|
+
|
|
142
|
+
Other Parameters:
|
|
143
|
+
accent: Accent token for the focus ring and active border.
|
|
144
|
+
density: Widget density — 'default' or 'compact'.
|
|
145
|
+
state: Initial widget state — 'normal', 'disabled', or 'readonly'.
|
|
146
|
+
value_format: ICU format pattern for parsing/formatting.
|
|
147
|
+
allow_blank: Allow empty input.
|
|
148
|
+
initial_focus: Focus on creation.
|
|
149
|
+
show: Character to mask input (e.g., '*' for passwords).
|
|
150
|
+
width: Width in characters.
|
|
151
|
+
font: Font specification.
|
|
152
|
+
justify: Text alignment ('left', 'center', 'right').
|
|
153
|
+
textvariable: Tkinter Variable linked to entry text.
|
|
154
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
155
|
+
textsignal: Signal for reactive text updates.
|
|
156
|
+
minvalue: Minimum allowed value (numeric kind only).
|
|
157
|
+
maxvalue: Maximum allowed value (numeric kind only).
|
|
158
|
+
increment: Step size for up/down arrows (numeric kind only).
|
|
159
|
+
wrap: Wrap around at boundaries (numeric kind only).
|
|
160
|
+
"""
|
|
161
|
+
# Accept legacy parameter name and prevent it from reaching the Tk widget.
|
|
162
|
+
if 'show_messages' in kwargs:
|
|
163
|
+
show_message = kwargs.pop('show_messages')
|
|
164
|
+
# Track if user explicitly provided show_message
|
|
165
|
+
show_message_explicit = 'show_message' in kwargs
|
|
166
|
+
show_message = kwargs.pop('show_message', show_message)
|
|
167
|
+
|
|
168
|
+
# Auto-enable show_message if there is any chance a message will be shown
|
|
169
|
+
if (message or required) and not show_message_explicit:
|
|
170
|
+
show_message = True
|
|
171
|
+
|
|
172
|
+
accent = kwargs.pop('accent', None)
|
|
173
|
+
self._density = kwargs.pop('density', 'default')
|
|
174
|
+
self._localize = cast(bool | Literal['auto'], kwargs.pop('localize', 'auto'))
|
|
175
|
+
placeholder = kwargs.pop('placeholder', None)
|
|
176
|
+
|
|
177
|
+
# Field itself (outer Frame) doesn't need styling - only pass master
|
|
178
|
+
super().__init__(master)
|
|
179
|
+
|
|
180
|
+
# Set accent AFTER super().__init__ to avoid being overwritten by wrapper
|
|
181
|
+
self._accent = accent
|
|
182
|
+
|
|
183
|
+
# configuration
|
|
184
|
+
self._message_text = message
|
|
185
|
+
self._show_messages = show_message
|
|
186
|
+
self._addons: dict[str, Button | Label | CheckToggle] = {}
|
|
187
|
+
self._required = required
|
|
188
|
+
self._kind = kind
|
|
189
|
+
self._label_text = label
|
|
190
|
+
self._value = value
|
|
191
|
+
|
|
192
|
+
self._entry: EntryWidget
|
|
193
|
+
self._addons: dict[str, Button | Label | CheckToggle] = {}
|
|
194
|
+
# Addon keys that stay interactive while the field is read-only (opt-in
|
|
195
|
+
# via insert_addon(active_when_readonly=True)) — for read-only-safe
|
|
196
|
+
# actions like copy or reveal. They still dim when the field is disabled.
|
|
197
|
+
self._readonly_active: set[str] = set()
|
|
198
|
+
|
|
199
|
+
# layout
|
|
200
|
+
label_text = self._label_text or ''
|
|
201
|
+
self._label_lbl = Label(
|
|
202
|
+
self,
|
|
203
|
+
localize=self._localize,
|
|
204
|
+
text=f"{label_text}*" if required else label_text,
|
|
205
|
+
font="label[normal]"
|
|
206
|
+
)
|
|
207
|
+
self._message_lbl = Label(self, localize=self._localize, text=message or '', font="caption", accent="secondary")
|
|
208
|
+
|
|
209
|
+
# The TField frame draws the visible border via a nine-patch whose border
|
|
210
|
+
# slice scales with DPI; this gap must grow with it, or at high DPI the
|
|
211
|
+
# entry overpaints the border slice and the resting border vanishes (#90).
|
|
212
|
+
field_padding = scale_padding_floor(5)
|
|
213
|
+
self._field = Frame(self, accent=self._accent, padding=field_padding, ttk_class="TField", style_options={'density': self._density})
|
|
214
|
+
|
|
215
|
+
if kind == "numeric":
|
|
216
|
+
self._entry = NumberEntryPart(self._field, value=value, density=self._density, **kwargs)
|
|
217
|
+
elif kind == "spinbox":
|
|
218
|
+
self._entry = SpinnerEntryPart(self._field, value=value, density=self._density, **kwargs)
|
|
219
|
+
else:
|
|
220
|
+
self._entry = TextEntryPart(self._field, value=value, density=self._density, placeholder=placeholder, **kwargs)
|
|
221
|
+
|
|
222
|
+
# attach widgets
|
|
223
|
+
if label:
|
|
224
|
+
self._label_lbl.pack(side='top', fill='x', padx=(4, 0))
|
|
225
|
+
|
|
226
|
+
self._field.pack(side='top', fill='x', expand=True)
|
|
227
|
+
self._entry.pack(side='left', fill='x', expand=True, padx=(0, 6) if kind == "spinbox" else 0, pady=0)
|
|
228
|
+
|
|
229
|
+
self._entry.bind('<<StateChanged>>', self._sync_addon_state, add=True)
|
|
230
|
+
self._sync_addon_state()
|
|
231
|
+
|
|
232
|
+
if self._show_messages:
|
|
233
|
+
self._message_lbl.pack(side='top', fill='x', padx=4)
|
|
234
|
+
|
|
235
|
+
# The message label follows the entry's reactive error signal: a
|
|
236
|
+
# non-empty error shows in danger, an empty one restores the static
|
|
237
|
+
# helper message.
|
|
238
|
+
self._entry._error_signal.subscribe(self._on_validation_message)
|
|
239
|
+
|
|
240
|
+
# bind focus styling to the field frame
|
|
241
|
+
self._entry.bind('<FocusIn>', lambda _: self._field.state(['focus']), add=True)
|
|
242
|
+
self._entry.bind('<FocusOut>', lambda _: self._field.state(['!focus']), add=True)
|
|
243
|
+
|
|
244
|
+
# add required validation
|
|
245
|
+
if required:
|
|
246
|
+
self._entry.add_validation_rule("required")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
self.validation = self._entry.validate
|
|
250
|
+
|
|
251
|
+
# Copy Field's delegate handlers to entry for configuration forwarding
|
|
252
|
+
for key, method_name in self._configure_delegate_map.items():
|
|
253
|
+
if hasattr(self, method_name):
|
|
254
|
+
# Attach the Field's handler method to the entry instance
|
|
255
|
+
setattr(self._entry, method_name, getattr(self, method_name))
|
|
256
|
+
# Add to entry's delegate map
|
|
257
|
+
self._entry._configure_delegate_map[key] = method_name
|
|
258
|
+
|
|
259
|
+
# Forward configuration methods to entry widget
|
|
260
|
+
self.configure = self._entry.configure
|
|
261
|
+
self.config = self._entry.config
|
|
262
|
+
self.cget = self._entry.cget
|
|
263
|
+
self.__getitem__ = self._entry.__getitem__
|
|
264
|
+
self.__setitem__ = self._entry.__setitem__
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def value(self) -> Any:
|
|
268
|
+
"""Get or set the parsed value via the underlying entry widget."""
|
|
269
|
+
return self._entry.value()
|
|
270
|
+
|
|
271
|
+
@value.setter
|
|
272
|
+
def value(self, value: Any) -> None:
|
|
273
|
+
self._entry.value(value)
|
|
274
|
+
|
|
275
|
+
def get(self) -> str:
|
|
276
|
+
"""Return the raw text from the underlying entry widget."""
|
|
277
|
+
return self._entry.get()
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def entry_widget(self) -> EntryWidget:
|
|
281
|
+
"""Get the underlying entry widget."""
|
|
282
|
+
return self._entry
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def label_widget(self) -> Label:
|
|
286
|
+
"""Get the label widget."""
|
|
287
|
+
return self._label_lbl
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def message_widget(self) -> Label:
|
|
291
|
+
"""Get the message widget."""
|
|
292
|
+
return self._message_lbl
|
|
293
|
+
|
|
294
|
+
@property
|
|
295
|
+
def addons(self) -> dict[str, FieldAddonWidget]:
|
|
296
|
+
"""Get the dictionary of inserted addon widgets."""
|
|
297
|
+
return self._addons
|
|
298
|
+
|
|
299
|
+
@property
|
|
300
|
+
def variable(self) -> Variable:
|
|
301
|
+
"""The `StringVar` linked to the entry text.
|
|
302
|
+
|
|
303
|
+
Use when bridging to a plain tkinter widget that requires `textvariable=`,
|
|
304
|
+
or for direct read/write access to the raw string value. For reactive
|
|
305
|
+
bindings between bootstack widgets, prefer `signal` instead.
|
|
306
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
307
|
+
"""
|
|
308
|
+
return self._entry.textvariable
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def signal(self) -> Signal:
|
|
312
|
+
"""The `Signal[str]` linked to the entry text.
|
|
313
|
+
|
|
314
|
+
Pass as `textsignal=` to any other bootstack widget to keep its text
|
|
315
|
+
in sync with this field's value, or call `signal.subscribe(callback)`
|
|
316
|
+
to observe changes programmatically. See the
|
|
317
|
+
[Reactivity guide](../../guides/reactivity.md) for patterns.
|
|
318
|
+
"""
|
|
319
|
+
return self._entry.textsignal
|
|
320
|
+
|
|
321
|
+
@configure_delegate
|
|
322
|
+
def _config_accent(self, value=None):
|
|
323
|
+
if value is None:
|
|
324
|
+
return self._accent
|
|
325
|
+
else:
|
|
326
|
+
self._accent = value
|
|
327
|
+
self._field['accent'] = value
|
|
328
|
+
return None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
# ------ Event registration ------
|
|
332
|
+
|
|
333
|
+
def on_input(self, callback: Callable) -> str:
|
|
334
|
+
"""Register a callback for `<<Input>>` events (fires on each keystroke).
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
callback: Receives a Tkinter `Event` object whose `event.data` is an
|
|
338
|
+
`InputEvent` payload with `text` (current raw text).
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Bind ID — pass to `off_input()` to unsubscribe.
|
|
342
|
+
"""
|
|
343
|
+
return self._entry.on_input(callback)
|
|
344
|
+
|
|
345
|
+
def off_input(self, bind_id: str | None = None) -> None:
|
|
346
|
+
"""Unsubscribe from `<<Input>>`.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
bind_id: ID returned by `on_input()`.
|
|
350
|
+
"""
|
|
351
|
+
self._entry.off_input(bind_id)
|
|
352
|
+
|
|
353
|
+
def on_changed(self, callback: Callable) -> str:
|
|
354
|
+
"""Register a callback for `<<Change>>` events (fires on commit).
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
callback: Receives a Tkinter `Event` object whose `event.data` is a
|
|
358
|
+
`ChangeEvent` payload with `value` (committed value),
|
|
359
|
+
`prev_value` (previous value), and `text` (raw display string).
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Bind ID — pass to `off_changed()` to unsubscribe.
|
|
363
|
+
"""
|
|
364
|
+
return self._entry.on_changed(callback)
|
|
365
|
+
|
|
366
|
+
def off_changed(self, bind_id: str | None = None) -> None:
|
|
367
|
+
"""Unsubscribe from `<<Change>>`.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
bind_id: ID returned by `on_changed()`.
|
|
371
|
+
"""
|
|
372
|
+
self._entry.off_changed(bind_id)
|
|
373
|
+
|
|
374
|
+
def on_enter(self, callback: Callable) -> str:
|
|
375
|
+
"""Register a callback for `<Return>` key events.
|
|
376
|
+
|
|
377
|
+
Args:
|
|
378
|
+
callback: Receives a Tkinter `Event` object whose `event.data` is an
|
|
379
|
+
the curated `Event` for the Return key press.
|
|
380
|
+
|
|
381
|
+
Returns:
|
|
382
|
+
Bind ID — pass to `off_enter()` to unsubscribe.
|
|
383
|
+
"""
|
|
384
|
+
return self._entry.on_enter(callback)
|
|
385
|
+
|
|
386
|
+
def off_enter(self, bind_id: str | None = None) -> None:
|
|
387
|
+
"""Unsubscribe from `<Return>`.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
bind_id: ID returned by `on_enter()`.
|
|
391
|
+
"""
|
|
392
|
+
self._entry.off_enter(bind_id)
|
|
393
|
+
|
|
394
|
+
def on_valid(self, callback: Callable) -> None:
|
|
395
|
+
"""Register a callback for `<<Valid>>` events (fires when validation passes).
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
callback: Receives the event; `event.data` is a `ValidationEvent`
|
|
399
|
+
payload with `value`, `is_valid` (`True`), and `message`.
|
|
400
|
+
"""
|
|
401
|
+
self._entry.on_valid(callback)
|
|
402
|
+
|
|
403
|
+
def off_valid(self, bind_id: str | None = None) -> None:
|
|
404
|
+
"""Unsubscribe from `<<Valid>>`.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
bind_id: Bind ID from a direct `widget.bind()` call, or `None`
|
|
408
|
+
to remove all `<<Valid>>` bindings.
|
|
409
|
+
"""
|
|
410
|
+
self._entry.off_valid(bind_id)
|
|
411
|
+
|
|
412
|
+
def on_invalid(self, callback: Callable) -> None:
|
|
413
|
+
"""Register a callback for `<<Invalid>>` events (fires when validation fails).
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
callback: Receives the event; `event.data` is a `ValidationEvent`
|
|
417
|
+
payload with `value`, `is_valid` (`False`), and `message`.
|
|
418
|
+
"""
|
|
419
|
+
self._entry.on_invalid(callback)
|
|
420
|
+
|
|
421
|
+
def off_invalid(self, bind_id: str | None = None) -> None:
|
|
422
|
+
"""Unsubscribe from `<<Invalid>>`.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
bind_id: Bind ID from a direct `widget.bind()` call, or `None`
|
|
426
|
+
to remove all `<<Invalid>>` bindings.
|
|
427
|
+
"""
|
|
428
|
+
self._entry.off_invalid(bind_id)
|
|
429
|
+
|
|
430
|
+
def on_validated(self, callback: Callable) -> None:
|
|
431
|
+
"""Register a callback for `<<Validate>>` events (fires after any validation).
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
callback: Receives the event; `event.data` is a `ValidationEvent`
|
|
435
|
+
payload with `value`, `is_valid` (bool), and `message`.
|
|
436
|
+
"""
|
|
437
|
+
self._entry.on_validated(callback)
|
|
438
|
+
|
|
439
|
+
def off_validated(self, bind_id: str | None = None) -> None:
|
|
440
|
+
"""Unsubscribe from `<<Validate>>`.
|
|
441
|
+
|
|
442
|
+
Args:
|
|
443
|
+
bind_id: Bind ID from a direct `widget.bind()` call, or `None`
|
|
444
|
+
to remove all `<<Validate>>` bindings.
|
|
445
|
+
"""
|
|
446
|
+
self._entry.off_validated(bind_id)
|
|
447
|
+
|
|
448
|
+
# ------ Validation ------
|
|
449
|
+
|
|
450
|
+
def add_validation_rule(
|
|
451
|
+
self,
|
|
452
|
+
rule_type: RuleType,
|
|
453
|
+
**kwargs,
|
|
454
|
+
) -> None:
|
|
455
|
+
"""Add a validation rule to the field.
|
|
456
|
+
|
|
457
|
+
Rules are evaluated on blur and on Enter. When a rule fails the field
|
|
458
|
+
emits `<<Invalid>>`; when all rules pass it emits `<<Valid>>`. Both
|
|
459
|
+
carry a `ValidationEvent` payload.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
rule_type: Rule type. One of:
|
|
463
|
+
|
|
464
|
+
- `"required"` — field must not be empty.
|
|
465
|
+
- `"email"` — value must be a valid email address.
|
|
466
|
+
- `"pattern"` — value must match a regex. Pass `pattern=`.
|
|
467
|
+
- `"stringLength"` — length bounds. Pass `min=` and/or `max=`.
|
|
468
|
+
- `"compare"` — must match another field's value. Pass `other_field=`.
|
|
469
|
+
- `"custom"` — arbitrary logic. Pass `func=`, a callable that
|
|
470
|
+
receives the value and returns `bool` or `(bool, message)`.
|
|
471
|
+
|
|
472
|
+
**kwargs: Rule-specific options. `message=` is accepted by all rule
|
|
473
|
+
types to override the default failure message.
|
|
474
|
+
"""
|
|
475
|
+
self._reserve_message_space()
|
|
476
|
+
self._entry.add_validation_rule(rule_type, **kwargs)
|
|
477
|
+
|
|
478
|
+
def disable(self):
|
|
479
|
+
"""Disable the field, preventing user input."""
|
|
480
|
+
self._entry.state(['disabled !readonly'])
|
|
481
|
+
self._field.state(['disabled'])
|
|
482
|
+
self._set_addons_state(True)
|
|
483
|
+
|
|
484
|
+
def enable(self):
|
|
485
|
+
"""Enable the field, allowing user input."""
|
|
486
|
+
self._entry.state(['!disabled !readonly'])
|
|
487
|
+
self._field.state(['!disabled'])
|
|
488
|
+
self._set_addons_state(False)
|
|
489
|
+
|
|
490
|
+
def readonly(self, value: bool = None):
|
|
491
|
+
"""Set or toggle the readonly state of the field."""
|
|
492
|
+
if value == False:
|
|
493
|
+
self._field.state(['disabled'])
|
|
494
|
+
self._entry.state(['readonly'])
|
|
495
|
+
elif value:
|
|
496
|
+
self._field.state(['!disabled'])
|
|
497
|
+
self._entry.state(['readonly'])
|
|
498
|
+
else:
|
|
499
|
+
self._entry.state(['readonly !disabled'])
|
|
500
|
+
self._field.state(['disabled'])
|
|
501
|
+
self._sync_addon_state()
|
|
502
|
+
|
|
503
|
+
@configure_delegate('state')
|
|
504
|
+
def _delegate_state(self, value=None):
|
|
505
|
+
if value is None:
|
|
506
|
+
states = self._entry.state()
|
|
507
|
+
if 'disabled' in states:
|
|
508
|
+
return 'disabled'
|
|
509
|
+
if 'readonly' in states:
|
|
510
|
+
return 'readonly'
|
|
511
|
+
return 'normal'
|
|
512
|
+
if value == 'disabled':
|
|
513
|
+
self._entry.state(['disabled', '!readonly'])
|
|
514
|
+
self._field.state(['disabled'])
|
|
515
|
+
elif value == 'readonly':
|
|
516
|
+
self._entry.state(['readonly', '!disabled'])
|
|
517
|
+
self._field.state(['!disabled'])
|
|
518
|
+
else:
|
|
519
|
+
self._entry.state(['!disabled', '!readonly'])
|
|
520
|
+
self._field.state(['!disabled'])
|
|
521
|
+
self._sync_addon_state()
|
|
522
|
+
self._entry.event_generate('<<StateChanged>>')
|
|
523
|
+
return None
|
|
524
|
+
|
|
525
|
+
def insert_addon(
|
|
526
|
+
self,
|
|
527
|
+
widget: Type[FieldAddonWidget],
|
|
528
|
+
position: Literal['before', 'after'],
|
|
529
|
+
name: str | None = None,
|
|
530
|
+
accent: str | None = None,
|
|
531
|
+
pack_options: dict[str, Any] = None,
|
|
532
|
+
active_when_readonly: bool = False,
|
|
533
|
+
**kwargs: Any
|
|
534
|
+
) -> FieldAddonWidget:
|
|
535
|
+
"""Insert a widget addon before or after the entry input.
|
|
536
|
+
|
|
537
|
+
Addons are Button, Label, or CheckToggle widgets positioned inside the field container,
|
|
538
|
+
either before (left of) or after (right of) the entry input. Common use
|
|
539
|
+
cases include search buttons, icons, clear buttons, or status indicators.
|
|
540
|
+
|
|
541
|
+
The addon widget automatically:
|
|
542
|
+
- Inherits the field's disabled state
|
|
543
|
+
- Participates in focus state styling (highlights field on addon focus)
|
|
544
|
+
- Is stored in the addons dictionary for later reference
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
widget: Widget class to instantiate. Must be Button, Label, or CheckToggle.
|
|
548
|
+
position: Position relative to the entry input:
|
|
549
|
+
|
|
550
|
+
- 'before': Insert to the left of the entry (prefix)
|
|
551
|
+
- 'after': Insert to the right of the entry (suffix)
|
|
552
|
+
name: Optional name for the addon. If provided, the addon can be
|
|
553
|
+
retrieved from the addons dictionary using this name. If None,
|
|
554
|
+
the widget's string representation is used as the key.
|
|
555
|
+
accent: Optional accent color for the addon. Default to surface color. Prefer to use an accent if using
|
|
556
|
+
a text-only button.
|
|
557
|
+
pack_options: Optional dictionary of additional pack() options to apply when placing the addon widget.
|
|
558
|
+
Common options include padx, pady, etc. The side and after/before options are set automatically based
|
|
559
|
+
on position.
|
|
560
|
+
active_when_readonly: If True, the addon stays interactive while the
|
|
561
|
+
field is read-only (for read-only-safe actions like copy or
|
|
562
|
+
reveal). It still dims when the field is fully disabled. Default
|
|
563
|
+
False — the addon follows the field's read-only state.
|
|
564
|
+
**kwargs: Additional keyword arguments passed to the widget constructor.
|
|
565
|
+
For Button: text, command, icon, accent, variant, etc.
|
|
566
|
+
For Label: text, icon, image, accent, etc.
|
|
567
|
+
For CheckToggle: text, icon, signal, command, accent, etc.
|
|
568
|
+
Note: variant and takefocus are set automatically but can be
|
|
569
|
+
overridden.
|
|
570
|
+
"""
|
|
571
|
+
variant = "suffix" if position == "after" else "prefix"
|
|
572
|
+
kwargs.setdefault('ttk_class', 'TField')
|
|
573
|
+
kwargs.setdefault('variant', variant)
|
|
574
|
+
kwargs.setdefault('takefocus', False)
|
|
575
|
+
kwargs.setdefault('density', self._density)
|
|
576
|
+
kwargs.setdefault('anchor', 'center')
|
|
577
|
+
if 'icon' in kwargs and 'text' not in kwargs:
|
|
578
|
+
kwargs.setdefault('compound', 'image')
|
|
579
|
+
kwargs.setdefault('icon_only', True)
|
|
580
|
+
|
|
581
|
+
if issubclass(widget, (Button, CheckToggle)):
|
|
582
|
+
icon_only = kwargs.get('icon_only', False)
|
|
583
|
+
if 'style_options' in kwargs:
|
|
584
|
+
kwargs['style_options'].update(use_active_states=True, density=self._density, icon_only=icon_only)
|
|
585
|
+
else:
|
|
586
|
+
kwargs['style_options'] = dict(use_active_states=True, density=self._density, icon_only=icon_only)
|
|
587
|
+
instance = widget(master=self._field, accent=accent, **kwargs)
|
|
588
|
+
key = name or str(instance)
|
|
589
|
+
self._addons[key] = instance
|
|
590
|
+
if active_when_readonly:
|
|
591
|
+
self._readonly_active.add(key)
|
|
592
|
+
|
|
593
|
+
# configure layout
|
|
594
|
+
options = pack_options or {}
|
|
595
|
+
if position == "after":
|
|
596
|
+
options.update(side="right", after=self._entry)
|
|
597
|
+
else:
|
|
598
|
+
options.update(side="left", before=self._entry)
|
|
599
|
+
instance.pack(**options)
|
|
600
|
+
|
|
601
|
+
# match parent disabled state
|
|
602
|
+
self._sync_addon_state()
|
|
603
|
+
|
|
604
|
+
# bind focus events to field frame
|
|
605
|
+
instance.bind('<FocusIn>', lambda _: self._field.state(['focus']), add=True)
|
|
606
|
+
instance.bind('<FocusOut>', lambda _: self._field.state(['!focus']), add=True)
|
|
607
|
+
|
|
608
|
+
return instance
|
|
609
|
+
|
|
610
|
+
def remove_addon(self, key: str) -> None:
|
|
611
|
+
"""Remove a previously-inserted addon by its key.
|
|
612
|
+
|
|
613
|
+
Args:
|
|
614
|
+
key: The addon's name (as passed to or returned by `insert_addon`).
|
|
615
|
+
|
|
616
|
+
Raises:
|
|
617
|
+
KeyError: If no addon with that key exists.
|
|
618
|
+
"""
|
|
619
|
+
instance = self._addons.pop(key)
|
|
620
|
+
self._readonly_active.discard(key)
|
|
621
|
+
instance.destroy()
|
|
622
|
+
|
|
623
|
+
def _reserve_message_space(self) -> None:
|
|
624
|
+
if not self._show_messages:
|
|
625
|
+
self._show_messages = True
|
|
626
|
+
self._message_lbl.pack(side='top', fill='x', padx=4)
|
|
627
|
+
|
|
628
|
+
def _on_validation_message(self, message: str) -> None:
|
|
629
|
+
"""React to the entry's error signal — show the error or restore helper text.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
message: The current validation error, or an empty string when the
|
|
633
|
+
field is valid.
|
|
634
|
+
"""
|
|
635
|
+
if message:
|
|
636
|
+
self._message_lbl['text'] = message
|
|
637
|
+
self._message_lbl['accent'] = "danger"
|
|
638
|
+
self._message_lbl.pack(side='top', after=self._field, padx=4)
|
|
639
|
+
else:
|
|
640
|
+
self._message_lbl['text'] = self._message_text or ''
|
|
641
|
+
self._message_lbl['accent'] = "secondary"
|
|
642
|
+
|
|
643
|
+
def _set_addons_state(self, disabled: bool) -> None:
|
|
644
|
+
"""Configure addon widgets based on whether the entry is interactive."""
|
|
645
|
+
state_value = 'disabled' if disabled else '!disabled'
|
|
646
|
+
for item in self._addons.values():
|
|
647
|
+
try:
|
|
648
|
+
item.configure(state=state_value)
|
|
649
|
+
except TclError:
|
|
650
|
+
pass
|
|
651
|
+
|
|
652
|
+
def _sync_addon_state(self, event: Any = None) -> None:
|
|
653
|
+
"""Match each addon to the field's interactivity state.
|
|
654
|
+
|
|
655
|
+
A `disabled` field dims every addon. A `readonly` field dims its addons
|
|
656
|
+
too — most act on the value (clear, the spin buttons), so leaving them
|
|
657
|
+
live would let the user mutate a field that is supposed to be
|
|
658
|
+
uneditable. Addons inserted with `active_when_readonly=True` (read-only
|
|
659
|
+
safe actions, e.g. copy or reveal) are the exception: they stay live
|
|
660
|
+
while read-only, but still dim when the field is fully disabled.
|
|
661
|
+
"""
|
|
662
|
+
states = self._entry.state()
|
|
663
|
+
fully_disabled = 'disabled' in states
|
|
664
|
+
readonly = 'readonly' in states
|
|
665
|
+
for key, item in self._addons.items():
|
|
666
|
+
off = fully_disabled or (readonly and key not in self._readonly_active)
|
|
667
|
+
try:
|
|
668
|
+
item.configure(state='disabled' if off else '!disabled')
|
|
669
|
+
except TclError:
|
|
670
|
+
pass
|