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,406 @@
|
|
|
1
|
+
from tkinter import Event, TclError
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
|
|
4
|
+
from bootstack.events import ChangeEvent, InputEvent
|
|
5
|
+
from bootstack.i18n import MessageCatalog, IntlFormatter
|
|
6
|
+
from bootstack.widgets._impl.primitives.entry import Entry
|
|
7
|
+
from bootstack.widgets._impl.mixins import ValidationMixin
|
|
8
|
+
from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TextEntryPart(ValidationMixin, Entry):
|
|
12
|
+
"""Internationalization-aware entry widget with deferred parsing and formatting.
|
|
13
|
+
|
|
14
|
+
This widget separates user input (display text) from the committed/parsed value,
|
|
15
|
+
providing a clean pattern for handling formatted data entry. Parsing and formatting
|
|
16
|
+
only occur when the user commits the value via `<FocusOut>` or `<Return>`.
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
master=None,
|
|
23
|
+
*,
|
|
24
|
+
value='',
|
|
25
|
+
value_format=None,
|
|
26
|
+
initial_focus: bool = False,
|
|
27
|
+
allow_blank: bool = True,
|
|
28
|
+
placeholder: str | None = None,
|
|
29
|
+
**kwargs
|
|
30
|
+
):
|
|
31
|
+
"""Initialize a TextEntryPart widget with internationalization support.
|
|
32
|
+
|
|
33
|
+
Creates an entry widget that separates display text from the parsed value.
|
|
34
|
+
The widget defers parsing and formatting until the user commits the value
|
|
35
|
+
by pressing Enter or moving focus away from the widget.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
master: Parent widget. If None, uses the default root window.
|
|
39
|
+
value: Initial value to display and parse. Can be a string or any value
|
|
40
|
+
that can be formatted using value_format. Default is empty string.
|
|
41
|
+
value_format: ICU format pattern for parsing and formatting the value.
|
|
42
|
+
Common patterns: `'#,##0.00'` (decimal), `'¤#,##0.00'` (currency),
|
|
43
|
+
`'yyyy-MM-dd'` (date), `'#,##0.00%'` (percent).
|
|
44
|
+
If None, value is treated as plain text (no parsing/formatting).
|
|
45
|
+
initial_focus: If True, widget receives focus when created.
|
|
46
|
+
allow_blank: If True, empty input is parsed as None. If False, empty
|
|
47
|
+
input preserves the previous value.
|
|
48
|
+
placeholder: Text shown when the field is empty and unfocused.
|
|
49
|
+
**kwargs: Additional keyword arguments passed to the Entry base class.
|
|
50
|
+
|
|
51
|
+
Note:
|
|
52
|
+
The widget automatically subscribes to text changes and sets up
|
|
53
|
+
event handlers for `<FocusIn>`, `<FocusOut>`, and `<Return>`.
|
|
54
|
+
"""
|
|
55
|
+
self._placeholder_text = placeholder
|
|
56
|
+
self._showing_placeholder = False
|
|
57
|
+
kwargs.setdefault('ttk_class', 'TField')
|
|
58
|
+
kwargs.setdefault('variant', 'input')
|
|
59
|
+
super().__init__(master, **kwargs)
|
|
60
|
+
|
|
61
|
+
# configuration
|
|
62
|
+
self._value_format = value_format
|
|
63
|
+
self._allow_blank = allow_blank
|
|
64
|
+
self._on_input_fid = None
|
|
65
|
+
self._fmt = IntlFormatter(locale=MessageCatalog.locale())
|
|
66
|
+
|
|
67
|
+
# set the initial display value
|
|
68
|
+
# Convert to string if it's a number
|
|
69
|
+
if isinstance(value, (int, float)):
|
|
70
|
+
initial_display = str(value)
|
|
71
|
+
else:
|
|
72
|
+
initial_display = value or self.textsignal() or ''
|
|
73
|
+
|
|
74
|
+
# Parse initial value if format is specified
|
|
75
|
+
if value_format is not None:
|
|
76
|
+
initial_value = self._parse_or_none(initial_display)
|
|
77
|
+
else:
|
|
78
|
+
initial_value = initial_display or ''
|
|
79
|
+
|
|
80
|
+
self._value = initial_value
|
|
81
|
+
self._prev_changed_value = initial_value
|
|
82
|
+
|
|
83
|
+
# normalize initial display if we already have a parsed value
|
|
84
|
+
if self._value is not None:
|
|
85
|
+
formatted_text = self._format_value(self._value)
|
|
86
|
+
self.textsignal.set(formatted_text)
|
|
87
|
+
else:
|
|
88
|
+
self.textsignal.set('')
|
|
89
|
+
|
|
90
|
+
# track last text emitted for CHANGE
|
|
91
|
+
self._prev_change_text = self.textsignal()
|
|
92
|
+
|
|
93
|
+
# subscribe to text changes
|
|
94
|
+
self._on_input_fid = self.textsignal.subscribe(self._handle_change)
|
|
95
|
+
|
|
96
|
+
# Commit on focus out / enter;
|
|
97
|
+
self.bind('<FocusIn>', self._store_prev_value, add=True)
|
|
98
|
+
self.bind('<FocusOut>', self._handle_focus_out, add=True)
|
|
99
|
+
self.bind('<Return>', self._handle_return, add=True)
|
|
100
|
+
self.winfo_toplevel().bind('<<LocaleChanged>>', self._on_locale_changed, add='+')
|
|
101
|
+
|
|
102
|
+
# set initial focus
|
|
103
|
+
if initial_focus:
|
|
104
|
+
self.focus()
|
|
105
|
+
|
|
106
|
+
# placeholder — shown when empty and unfocused; uses textvariable detach
|
|
107
|
+
# so the Signal is never set to the placeholder text
|
|
108
|
+
if placeholder:
|
|
109
|
+
self._default_fg = self.cget("foreground")
|
|
110
|
+
# remember any masking char so the placeholder can render unmasked
|
|
111
|
+
# (a password '•'/'*' mask must not turn the placeholder into bullets)
|
|
112
|
+
self._show_char = self.cget("show")
|
|
113
|
+
if not self.textsignal():
|
|
114
|
+
self._show_placeholder()
|
|
115
|
+
self.bind('<FocusIn>', self._on_focus_in_placeholder, add=True)
|
|
116
|
+
self.bind('<FocusOut>', self._on_focus_out_placeholder, add=True)
|
|
117
|
+
|
|
118
|
+
def _show_placeholder(self) -> None:
|
|
119
|
+
self._showing_placeholder = True
|
|
120
|
+
self.configure(textvariable='')
|
|
121
|
+
# render the placeholder unmasked even when an input mask is active
|
|
122
|
+
if getattr(self, '_show_char', ''):
|
|
123
|
+
self.configure(show='')
|
|
124
|
+
self.insert(0, self._placeholder_text)
|
|
125
|
+
self.configure(foreground='grey')
|
|
126
|
+
|
|
127
|
+
def _hide_placeholder(self) -> None:
|
|
128
|
+
self._showing_placeholder = False
|
|
129
|
+
self.delete(0, 'end')
|
|
130
|
+
self.configure(textvariable=self.textsignal.var)
|
|
131
|
+
# restore the input mask now that real input is shown
|
|
132
|
+
if getattr(self, '_show_char', ''):
|
|
133
|
+
self.configure(show=self._show_char)
|
|
134
|
+
self.configure(foreground=self._default_fg)
|
|
135
|
+
|
|
136
|
+
def _on_focus_in_placeholder(self, _event) -> None:
|
|
137
|
+
if self._showing_placeholder:
|
|
138
|
+
self._hide_placeholder()
|
|
139
|
+
|
|
140
|
+
def _on_focus_out_placeholder(self, _event) -> None:
|
|
141
|
+
if self._placeholder_text and not self.textsignal():
|
|
142
|
+
self._show_placeholder()
|
|
143
|
+
|
|
144
|
+
def _store_prev_value(self, _: Any):
|
|
145
|
+
"""Store current value on focus-in to detect changes later."""
|
|
146
|
+
self._prev_changed_value = self._value
|
|
147
|
+
|
|
148
|
+
def _handle_focus_out(self, _):
|
|
149
|
+
"""Commit value and check for changes when focus leaves the widget."""
|
|
150
|
+
if 'readonly' in self.state():
|
|
151
|
+
return
|
|
152
|
+
self.commit()
|
|
153
|
+
self._check_if_changed()
|
|
154
|
+
|
|
155
|
+
def _handle_return(self, _):
|
|
156
|
+
"""Commit value and check for changes when Return key is pressed."""
|
|
157
|
+
self.commit()
|
|
158
|
+
self._check_if_changed()
|
|
159
|
+
|
|
160
|
+
def _handle_change(self, event):
|
|
161
|
+
"""Emit <<Input>> event on every text change without parsing."""
|
|
162
|
+
text = self.textsignal()
|
|
163
|
+
if text == self._prev_change_text:
|
|
164
|
+
return
|
|
165
|
+
if self._showing_placeholder:
|
|
166
|
+
if text:
|
|
167
|
+
self._hide_placeholder()
|
|
168
|
+
else:
|
|
169
|
+
return
|
|
170
|
+
self._prev_change_text = text
|
|
171
|
+
self.event_generate('<<Input>>', data=InputEvent(text=text))
|
|
172
|
+
|
|
173
|
+
def _check_if_changed(self):
|
|
174
|
+
"""Emit <<Change>> event if parsed value changed since focus-in."""
|
|
175
|
+
if self._value != self._prev_changed_value:
|
|
176
|
+
data = ChangeEvent(
|
|
177
|
+
value=self._value,
|
|
178
|
+
prev_value=self._prev_changed_value,
|
|
179
|
+
text=self.textsignal(),
|
|
180
|
+
)
|
|
181
|
+
self.event_generate('<<Change>>', data=data)
|
|
182
|
+
self._prev_changed_value = self._value
|
|
183
|
+
|
|
184
|
+
def _parse_or_none(self, s: str):
|
|
185
|
+
"""Parse string using value_format, returning None on empty/invalid input."""
|
|
186
|
+
# If a non-string is passed (e.g., datetime/date), assume it's already parsed.
|
|
187
|
+
if not isinstance(s, str):
|
|
188
|
+
return s
|
|
189
|
+
s2 = (s or '').strip()
|
|
190
|
+
if not s2:
|
|
191
|
+
return None
|
|
192
|
+
try:
|
|
193
|
+
if self._value_format is None:
|
|
194
|
+
return s2
|
|
195
|
+
return self._fmt.parse(s2, self._value_format)
|
|
196
|
+
except ValueError:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
def _format_value(self, value: Any) -> str:
|
|
200
|
+
"""Format a value for display using value_format.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
value: The value to format (can be string, int, float, etc.)
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Formatted string representation of the value
|
|
207
|
+
"""
|
|
208
|
+
if value is None:
|
|
209
|
+
return ''
|
|
210
|
+
|
|
211
|
+
# If no format specified, just convert to string
|
|
212
|
+
if self._value_format is None:
|
|
213
|
+
return str(value)
|
|
214
|
+
|
|
215
|
+
# IntlFormatter.format() expects numeric values, not strings
|
|
216
|
+
# So pass the value directly (it will be a number from parse())
|
|
217
|
+
try:
|
|
218
|
+
return self._fmt.format(value, self._value_format)
|
|
219
|
+
except (ValueError, TypeError):
|
|
220
|
+
# If formatting fails, return string representation
|
|
221
|
+
return str(value)
|
|
222
|
+
|
|
223
|
+
def on_input(self, callback: Callable) -> str:
|
|
224
|
+
"""Bind to `<<Input>>`. Callback receives `event.data = {'text': str}`."""
|
|
225
|
+
return self.bind('<<Input>>', callback, add=True)
|
|
226
|
+
|
|
227
|
+
def off_input(self, bind_id: str | None = None) -> None:
|
|
228
|
+
"""Unbind from `<<Input>>`."""
|
|
229
|
+
self.unbind('<<Input>>', bind_id)
|
|
230
|
+
|
|
231
|
+
def on_enter(self, callback: Callable) -> str:
|
|
232
|
+
"""Bind to `<Return>`. Callback receives `event.data = {'value': Any, 'text': str}`."""
|
|
233
|
+
|
|
234
|
+
def enrich_callback(event: Event) -> None:
|
|
235
|
+
data = {"value": self._value, "text": self.textsignal()}
|
|
236
|
+
event.data = data
|
|
237
|
+
return callback(event)
|
|
238
|
+
|
|
239
|
+
return self.bind('<Return>', enrich_callback, add=True)
|
|
240
|
+
|
|
241
|
+
def off_enter(self, bind_id: str | None = None) -> None:
|
|
242
|
+
"""Unbind from `<Return>`."""
|
|
243
|
+
self.unbind('<Return>', bind_id)
|
|
244
|
+
|
|
245
|
+
def on_changed(self, callback: Callable) -> str:
|
|
246
|
+
"""Bind to `<<Change>>`. Callback receives `event.data = {'value': Any, 'prev_value': Any, 'text': str}`."""
|
|
247
|
+
return self.bind("<<Change>>", callback, add=True)
|
|
248
|
+
|
|
249
|
+
def off_changed(self, bind_id: str | None = None) -> None:
|
|
250
|
+
"""Unbind from `<<Change>>`."""
|
|
251
|
+
self.unbind('<<Change>>', bind_id)
|
|
252
|
+
|
|
253
|
+
def value(self, value=None):
|
|
254
|
+
"""Get or set the parsed/committed value.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
value: If provided, sets the display text and internal value with formatting
|
|
258
|
+
|
|
259
|
+
Returns:
|
|
260
|
+
Current parsed value if no argument provided, None otherwise
|
|
261
|
+
"""
|
|
262
|
+
if value is None:
|
|
263
|
+
return self._value
|
|
264
|
+
else:
|
|
265
|
+
# Store the value and format it for display
|
|
266
|
+
if isinstance(value, (int, float)):
|
|
267
|
+
value_str = str(value)
|
|
268
|
+
else:
|
|
269
|
+
value_str = str(value) if value is not None else ''
|
|
270
|
+
|
|
271
|
+
# Parse the value if format is specified
|
|
272
|
+
if self._value_format is not None and value_str:
|
|
273
|
+
self._value = self._parse_or_none(value_str)
|
|
274
|
+
else:
|
|
275
|
+
self._value = value_str if value_str else None
|
|
276
|
+
|
|
277
|
+
# Format and display
|
|
278
|
+
formatted_text = self._format_value(self._value)
|
|
279
|
+
self.textsignal.set(formatted_text)
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def _get_validation_value(self):
|
|
283
|
+
"""Return the current input parsed to its typed value, for validation.
|
|
284
|
+
|
|
285
|
+
Validation runs against the field's typed value — the string itself for
|
|
286
|
+
a text field, a `date`/`time` for a date or time field — parsed from
|
|
287
|
+
what is currently shown, not the raw display text and not the
|
|
288
|
+
last-committed value. This keeps a manual `validate()`, the automatic
|
|
289
|
+
blur/key validation, and a form-level validate in agreement on the same
|
|
290
|
+
live, typed input.
|
|
291
|
+
"""
|
|
292
|
+
try:
|
|
293
|
+
return self._parse_or_none(self.get())
|
|
294
|
+
except Exception:
|
|
295
|
+
return self.get()
|
|
296
|
+
|
|
297
|
+
def text(self, value=None):
|
|
298
|
+
"""Get or set the raw display text without committing.
|
|
299
|
+
|
|
300
|
+
Args:
|
|
301
|
+
value: If provided, sets the display text without parsing
|
|
302
|
+
|
|
303
|
+
Returns:
|
|
304
|
+
Current display text if no argument provided, None otherwise
|
|
305
|
+
"""
|
|
306
|
+
if value is None:
|
|
307
|
+
return self.textsignal()
|
|
308
|
+
else:
|
|
309
|
+
self.textsignal.set(value)
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def commit(self):
|
|
313
|
+
"""Parse display text, update value, and normalize display (called on FocusOut/Return)."""
|
|
314
|
+
if self._showing_placeholder:
|
|
315
|
+
return
|
|
316
|
+
s = self.get().strip()
|
|
317
|
+
|
|
318
|
+
# parse once
|
|
319
|
+
if s == '':
|
|
320
|
+
self._value = None if self._allow_blank else self._value
|
|
321
|
+
else:
|
|
322
|
+
try:
|
|
323
|
+
self._value = s if self._value_format is None else self._fmt.parse(s, self._value_format)
|
|
324
|
+
except ValueError:
|
|
325
|
+
# keep prior value on parse failure
|
|
326
|
+
return
|
|
327
|
+
|
|
328
|
+
# Format the value for display
|
|
329
|
+
new_text = self._format_value(self._value)
|
|
330
|
+
|
|
331
|
+
if new_text != self.textsignal():
|
|
332
|
+
# temporarily silence CHANGE while normalizing text
|
|
333
|
+
fid = getattr(self, '_on_input_fid', None)
|
|
334
|
+
if fid:
|
|
335
|
+
try:
|
|
336
|
+
fid.cancel()
|
|
337
|
+
except TclError:
|
|
338
|
+
pass
|
|
339
|
+
self.textsignal.set(new_text)
|
|
340
|
+
if fid:
|
|
341
|
+
self._on_input_fid = self.textsignal.subscribe(self._handle_change)
|
|
342
|
+
|
|
343
|
+
def _on_locale_changed(self, event=None):
|
|
344
|
+
"""Respond to global changes in locale by updating formatter and text"""
|
|
345
|
+
self._fmt = IntlFormatter(locale=MessageCatalog.locale())
|
|
346
|
+
|
|
347
|
+
# reformat current value with new locale + same value_format
|
|
348
|
+
if self._value is not None:
|
|
349
|
+
formatted_text = self._format_value(self._value)
|
|
350
|
+
self.textsignal.set(formatted_text)
|
|
351
|
+
|
|
352
|
+
@configure_delegate('text')
|
|
353
|
+
def _delegate_text(self, value=None):
|
|
354
|
+
if value is None:
|
|
355
|
+
return self.text()
|
|
356
|
+
else:
|
|
357
|
+
return self.text(value)
|
|
358
|
+
|
|
359
|
+
@configure_delegate('value')
|
|
360
|
+
def _delegate_value(self, value=None):
|
|
361
|
+
if value is None:
|
|
362
|
+
return self.value()
|
|
363
|
+
else:
|
|
364
|
+
return self.value(value)
|
|
365
|
+
|
|
366
|
+
@configure_delegate('value_format')
|
|
367
|
+
def _delegate_value_format(self, value: str):
|
|
368
|
+
"""Get or set the value format pattern and reformat display.
|
|
369
|
+
|
|
370
|
+
Can be accessed via:
|
|
371
|
+
widget.configure(value_format='#,##0.00')
|
|
372
|
+
widget['value_format'] = '#,##0.00'
|
|
373
|
+
widget.cget('value_format')
|
|
374
|
+
"""
|
|
375
|
+
if value is None:
|
|
376
|
+
return self._value_format
|
|
377
|
+
|
|
378
|
+
self._value_format = value
|
|
379
|
+
# Reformat current value with new format
|
|
380
|
+
if self._value is not None:
|
|
381
|
+
formatted_text = self._format_value(self._value)
|
|
382
|
+
self.textsignal.set(formatted_text)
|
|
383
|
+
|
|
384
|
+
@configure_delegate('allow_blank')
|
|
385
|
+
def _delegate_allow_blank(self, value: bool):
|
|
386
|
+
"""Get or set whether blank input is allowed.
|
|
387
|
+
|
|
388
|
+
Can be accessed via:
|
|
389
|
+
widget.configure(allow_blank=True)
|
|
390
|
+
widget['allow_blank'] = True
|
|
391
|
+
widget.cget('allow_blank')
|
|
392
|
+
"""
|
|
393
|
+
if value is None:
|
|
394
|
+
return self._allow_blank
|
|
395
|
+
|
|
396
|
+
self._allow_blank = bool(value)
|
|
397
|
+
|
|
398
|
+
def destroy(self):
|
|
399
|
+
"""Clean up signal subscriptions and destroy the widget."""
|
|
400
|
+
if self._on_input_fid:
|
|
401
|
+
try:
|
|
402
|
+
self._on_input_fid.cancel()
|
|
403
|
+
except Exception:
|
|
404
|
+
pass # Ignore all errors during cleanup
|
|
405
|
+
self._on_input_fid = None
|
|
406
|
+
super().destroy()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Composite bootstack widgets."""
|
|
2
|
+
|
|
3
|
+
from bootstack.widgets._impl.composites.accordion import Accordion
|
|
4
|
+
from bootstack.widgets._impl.composites.compositeframe import Composite, CompositeFrame
|
|
5
|
+
from bootstack.widgets._impl.composites.expander import Expander
|
|
6
|
+
from bootstack.widgets._impl.composites.list import ListItem, ListView, MemoryDataSource, DataSourceProtocol
|
|
7
|
+
from bootstack.widgets._impl.composites.sidenav import (
|
|
8
|
+
SideNavHeader,
|
|
9
|
+
SideNavSeparator,
|
|
10
|
+
)
|
|
11
|
+
from bootstack.widgets._impl.composites.tabs import Tabs, TabView
|
|
12
|
+
from bootstack.widgets._impl.composites.selectbox import SelectBox
|
|
13
|
+
from bootstack.widgets._impl.composites.slider import Slider, RangeSlider
|
|
14
|
+
from bootstack.widgets._impl.composites.toolbar import Toolbar
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
'Accordion',
|
|
18
|
+
'Composite',
|
|
19
|
+
'CompositeFrame',
|
|
20
|
+
'Expander',
|
|
21
|
+
'ListItem',
|
|
22
|
+
'ListView',
|
|
23
|
+
'MemoryDataSource',
|
|
24
|
+
'DataSourceProtocol',
|
|
25
|
+
'RangeSlider',
|
|
26
|
+
'SideNavHeader',
|
|
27
|
+
'SideNavSeparator',
|
|
28
|
+
'Slider',
|
|
29
|
+
'Tabs',
|
|
30
|
+
'TabView',
|
|
31
|
+
'SelectBox',
|
|
32
|
+
'Toolbar',
|
|
33
|
+
]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Shared date helpers for the date-picker composites."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from datetime import date, datetime
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def coerce_date(value: date | datetime | str | None) -> date | None:
|
|
8
|
+
"""Coerce a date, datetime, or date string to a plain `date`, or `None`.
|
|
9
|
+
|
|
10
|
+
Accepts `date`/`datetime` instances (a `datetime` is narrowed to its
|
|
11
|
+
`date`), `'YYYY-MM-DD'` or `'MM/DD/YYYY'` strings, and any ISO-8601 string.
|
|
12
|
+
Returns `None` for `None` or anything unparseable.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
value: The value to coerce.
|
|
16
|
+
"""
|
|
17
|
+
if value is None:
|
|
18
|
+
return None
|
|
19
|
+
if isinstance(value, datetime):
|
|
20
|
+
return value.date()
|
|
21
|
+
if isinstance(value, date):
|
|
22
|
+
return value
|
|
23
|
+
if isinstance(value, str):
|
|
24
|
+
for fmt in ("%Y-%m-%d", "%m/%d/%Y"):
|
|
25
|
+
try:
|
|
26
|
+
return datetime.strptime(value, fmt).date()
|
|
27
|
+
except Exception:
|
|
28
|
+
continue
|
|
29
|
+
try:
|
|
30
|
+
return datetime.fromisoformat(value).date()
|
|
31
|
+
except Exception:
|
|
32
|
+
return None
|
|
33
|
+
return None
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Shared image fit/crop/round helpers for the media widgets.
|
|
2
|
+
|
|
3
|
+
Pure functions used by both `Picture` and the `Gallery` tile so the
|
|
4
|
+
object-fit scaling and antialiased rounded-corner masking live in one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any, Literal
|
|
11
|
+
|
|
12
|
+
from PIL import Image as PILImage, ImageChops, ImageDraw
|
|
13
|
+
from PIL.Image import Resampling
|
|
14
|
+
|
|
15
|
+
FitMode = Literal["contain", "cover", "fill", "none", "scale-down"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def resolve_pil(source: Any) -> "PILImage.Image | None":
|
|
19
|
+
"""Resolve an image source (Image handle, path, or PIL image) to a PIL image.
|
|
20
|
+
|
|
21
|
+
Returns the first frame for an animated source, or None when `source` is
|
|
22
|
+
None or cannot be decoded.
|
|
23
|
+
"""
|
|
24
|
+
if source is None:
|
|
25
|
+
return None
|
|
26
|
+
from bootstack.images import Image as _ImageHandle
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
if isinstance(source, _ImageHandle):
|
|
30
|
+
return source._load_pil()
|
|
31
|
+
if isinstance(source, (str, Path)):
|
|
32
|
+
return PILImage.open(Path(source).expanduser())
|
|
33
|
+
if isinstance(source, PILImage.Image):
|
|
34
|
+
return source
|
|
35
|
+
except Exception:
|
|
36
|
+
return None
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def fit_image(frame: PILImage.Image, bw: int, bh: int, fit: FitMode) -> PILImage.Image:
|
|
41
|
+
"""Scale (and for `cover`, crop) `frame` to a `bw`x`bh` box per `fit`.
|
|
42
|
+
|
|
43
|
+
For `contain`/`scale-down`/`none` the result may be smaller than the box —
|
|
44
|
+
the caller positions it. For `cover`/`fill` the result is exactly the box.
|
|
45
|
+
"""
|
|
46
|
+
iw, ih = frame.size
|
|
47
|
+
if iw <= 0 or ih <= 0:
|
|
48
|
+
return frame
|
|
49
|
+
|
|
50
|
+
if fit == "fill":
|
|
51
|
+
return frame.resize((bw, bh), Resampling.LANCZOS)
|
|
52
|
+
|
|
53
|
+
if fit == "cover":
|
|
54
|
+
scale = max(bw / iw, bh / ih)
|
|
55
|
+
scaled = frame.resize(
|
|
56
|
+
(max(1, round(iw * scale)), max(1, round(ih * scale))), Resampling.LANCZOS
|
|
57
|
+
)
|
|
58
|
+
left = (scaled.width - bw) // 2
|
|
59
|
+
top = (scaled.height - bh) // 2
|
|
60
|
+
return scaled.crop((left, top, left + bw, top + bh))
|
|
61
|
+
|
|
62
|
+
# contain / scale-down / none — never crop.
|
|
63
|
+
if fit == "none":
|
|
64
|
+
scale = 1.0
|
|
65
|
+
else:
|
|
66
|
+
scale = min(bw / iw, bh / ih)
|
|
67
|
+
if fit == "scale-down":
|
|
68
|
+
scale = min(scale, 1.0)
|
|
69
|
+
tw, th = max(1, round(iw * scale)), max(1, round(ih * scale))
|
|
70
|
+
if (tw, th) == (iw, ih):
|
|
71
|
+
return frame.copy()
|
|
72
|
+
return frame.resize((tw, th), Resampling.LANCZOS)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def rounded_mask(w: int, h: int, radius: int) -> PILImage.Image:
|
|
76
|
+
"""An antialiased rounded-rectangle alpha mask for a `w`x`h` box.
|
|
77
|
+
|
|
78
|
+
Drawn supersampled and downscaled with LANCZOS — `ImageDraw.rounded_rectangle`
|
|
79
|
+
is aliased at 1x, so the curve would otherwise be jagged. Callers should cache
|
|
80
|
+
the result; it depends only on `(w, h, radius)`.
|
|
81
|
+
"""
|
|
82
|
+
radius = min(radius, w // 2, h // 2)
|
|
83
|
+
ss = 4 if max(w, h) <= 600 else 2
|
|
84
|
+
big = PILImage.new("L", (w * ss, h * ss), 0)
|
|
85
|
+
ImageDraw.Draw(big).rounded_rectangle(
|
|
86
|
+
[0, 0, w * ss - 1, h * ss - 1], radius=radius * ss, fill=255
|
|
87
|
+
)
|
|
88
|
+
return big.resize((w, h), Resampling.LANCZOS)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def round_corners(
|
|
92
|
+
img: PILImage.Image, radius: int, mask: PILImage.Image | None = None
|
|
93
|
+
) -> PILImage.Image:
|
|
94
|
+
"""Apply a rounded-rectangle alpha mask to `img` (intersecting existing alpha).
|
|
95
|
+
|
|
96
|
+
Pass a precomputed `mask` (from `rounded_mask`) to avoid rebuilding it.
|
|
97
|
+
"""
|
|
98
|
+
w, h = img.size
|
|
99
|
+
if radius <= 0:
|
|
100
|
+
return img
|
|
101
|
+
if mask is None:
|
|
102
|
+
mask = rounded_mask(w, h, radius)
|
|
103
|
+
out = img.convert("RGBA")
|
|
104
|
+
out.putalpha(ImageChops.multiply(mask, out.getchannel("A")))
|
|
105
|
+
return out
|