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,387 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tkinter import StringVar
|
|
4
|
+
from typing import Any, Callable, TypedDict, TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Unpack
|
|
7
|
+
|
|
8
|
+
from bootstack.events import ChangeEvent
|
|
9
|
+
from bootstack.widgets._core.options import normalize_options, option_display, record_to_dict
|
|
10
|
+
from bootstack.widgets._impl.composites.contextmenu import ContextMenu, ContextMenuItem
|
|
11
|
+
from bootstack.widgets._impl.primitives._menubutton import MenuButton
|
|
12
|
+
from bootstack.widgets._impl.mixins import configure_delegate
|
|
13
|
+
from bootstack.widgets.types import Master, StyledKwargs, CompoundMode, WidgetState, Option
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from bootstack.signals import Signal
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OptionMenuKwargs(StyledKwargs, total=False):
|
|
20
|
+
command: Callable[[], Any] | None
|
|
21
|
+
image: Any
|
|
22
|
+
icon: Any
|
|
23
|
+
icon_only: bool
|
|
24
|
+
compound: CompoundMode
|
|
25
|
+
padding: Any
|
|
26
|
+
width: int
|
|
27
|
+
underline: int
|
|
28
|
+
state: WidgetState
|
|
29
|
+
default: Any
|
|
30
|
+
textvariable: Any
|
|
31
|
+
textsignal: Signal[str]
|
|
32
|
+
localize: Any
|
|
33
|
+
show_dropdown_button: bool
|
|
34
|
+
dropdown_button_icon: str | dict
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OptionMenu(MenuButton):
|
|
38
|
+
"""A single-selection dropdown menu backed by a ContextMenu.
|
|
39
|
+
|
|
40
|
+
Renders as a `MenuButton` with a chevron icon. Clicking the button opens
|
|
41
|
+
a popup menu containing the provided options as radiobutton items. Selecting
|
|
42
|
+
an item updates the displayed text and fires a `<<Change>>` event.
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self,
|
|
48
|
+
master: Master = None,
|
|
49
|
+
value: Any = None,
|
|
50
|
+
options: list[Option] = None,
|
|
51
|
+
**kwargs: Unpack[OptionMenuKwargs],
|
|
52
|
+
):
|
|
53
|
+
"""Create an OptionMenu backed by a ContextMenu.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
master: Parent widget. If None, uses the default root window.
|
|
57
|
+
value: Initial selected value (value-space). Must match one of the
|
|
58
|
+
options unless it is None/empty.
|
|
59
|
+
options: Options to populate the menu — each a string,
|
|
60
|
+
`(text, value)` tuple, or `{'text', 'value'}` dict. The menu
|
|
61
|
+
shows each option's text; selecting one emits its value.
|
|
62
|
+
|
|
63
|
+
Other Parameters:
|
|
64
|
+
command: Callback invoked when the value changes via menu selection.
|
|
65
|
+
image: Tk image to display.
|
|
66
|
+
icon: StyleResolver icon spec for the label content.
|
|
67
|
+
icon_only: Whether to reserve label padding when showing only an icon.
|
|
68
|
+
compound: Placement of image relative to text.
|
|
69
|
+
padding: Extra padding around the menubutton content.
|
|
70
|
+
width: Width of the menubutton.
|
|
71
|
+
underline: Index of underlined character in text.
|
|
72
|
+
state: Widget state ('normal', 'active', 'disabled', 'readonly').
|
|
73
|
+
takefocus: Participation in focus traversal.
|
|
74
|
+
style: Explicit ttk style name (overrides accent/variant).
|
|
75
|
+
textvariable: Existing Tk variable to bind; new StringVar created if omitted.
|
|
76
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
77
|
+
textsignal: Signal bound to the textvariable.
|
|
78
|
+
accent: Accent token for styling, e.g. 'primary', 'danger', 'success'.
|
|
79
|
+
variant: Style variant, e.g. 'solid', 'outline'.
|
|
80
|
+
surface: Surface token for style.
|
|
81
|
+
style_options: Dict forwarded to the style builder (e.g., icon_only, surface).
|
|
82
|
+
show_dropdown_button: Toggle visibility of the dropdown chevron.
|
|
83
|
+
dropdown_button_icon: Icon name for the chevron; defaults to 'caret-down-fill'.
|
|
84
|
+
"""
|
|
85
|
+
style_options = kwargs.pop('style_options', {})
|
|
86
|
+
style_options.update(
|
|
87
|
+
self._capture_style_options(
|
|
88
|
+
options=['icon_only', 'icon', 'show_dropdown_button', 'dropdown_button_icon'],
|
|
89
|
+
source=kwargs
|
|
90
|
+
)
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
self._bind_id = None
|
|
94
|
+
# Localization mode for the option labels + button face. None defers to
|
|
95
|
+
# the app's localize_mode; False keeps the raw labels untranslated.
|
|
96
|
+
self._localize = kwargs.pop('localize', None)
|
|
97
|
+
# Normalize options once; keep bidirectional text<->value maps so the
|
|
98
|
+
# menu can display text while the public value stays in value-space.
|
|
99
|
+
self._records = normalize_options(options)
|
|
100
|
+
self._rebuild_option_maps()
|
|
101
|
+
|
|
102
|
+
# Store the textvariable if provided, or create a new one. This variable
|
|
103
|
+
# holds the raw VALUE-KEY text (untranslated): it drives the radio
|
|
104
|
+
# highlight, the public value/selection, and <<Change>> in value-space.
|
|
105
|
+
self._textvariable = kwargs.pop('textvariable', None)
|
|
106
|
+
if self._textvariable is None:
|
|
107
|
+
self._textvariable = StringVar(value=self._resolve_initial_text(value))
|
|
108
|
+
|
|
109
|
+
# A separate variable backs the visible button face, holding the
|
|
110
|
+
# (optionally translated) DISPLAY text — decoupled from the value key so
|
|
111
|
+
# the face can localize while value-space stays raw.
|
|
112
|
+
self._display_var = StringVar(value=self._display_text(self._textvariable.get()))
|
|
113
|
+
|
|
114
|
+
# The mixin must not touch the face (we own its translation), so disable
|
|
115
|
+
# its localization; OptionMenu localizes the face via `_display_var`.
|
|
116
|
+
super().__init__(master, text=self._display_var.get(), localize=False, style_options=style_options, **kwargs)
|
|
117
|
+
|
|
118
|
+
# Bind the textsignal to the value-key variable (drives <<Change>> + the
|
|
119
|
+
# public signal), then point the visible face at the display variable.
|
|
120
|
+
self.configure(textvariable=self._textvariable)
|
|
121
|
+
self._ttk_base.configure(self, textvariable=self._display_var)
|
|
122
|
+
|
|
123
|
+
# Bind signal to change event (also keeps the face display in sync)
|
|
124
|
+
self._bind_id = self._bind_change_event()
|
|
125
|
+
self._sync_display()
|
|
126
|
+
# The locale-change event is generated on the root; bind there (not on
|
|
127
|
+
# self) so the face re-translates live, matching the mixin's pattern.
|
|
128
|
+
self.winfo_toplevel().bind('<<LocaleChanged>>', lambda _e: self._sync_display(), add="+")
|
|
129
|
+
|
|
130
|
+
# Create menu items that update the shared variable
|
|
131
|
+
self._context_menu = self._build_context_menu()
|
|
132
|
+
|
|
133
|
+
# Bind menu display to button events
|
|
134
|
+
self.bind('<Button-1>', lambda _: self.show_menu(), add="+")
|
|
135
|
+
self.bind('<Return>', lambda _: self.show_menu(), add="+")
|
|
136
|
+
self.bind('<KP_Enter>', lambda _: self.show_menu(), add="+")
|
|
137
|
+
|
|
138
|
+
def _effective_localize(self) -> Any:
|
|
139
|
+
"""Resolve the active localize mode, deferring to the app when unset."""
|
|
140
|
+
if self._localize is not None:
|
|
141
|
+
return self._localize
|
|
142
|
+
try:
|
|
143
|
+
from bootstack._runtime.app import get_app_settings
|
|
144
|
+
return get_app_settings().localize_mode
|
|
145
|
+
except Exception:
|
|
146
|
+
return 'auto'
|
|
147
|
+
|
|
148
|
+
def _display_text(self, raw: str) -> str:
|
|
149
|
+
"""Map a raw value-key label to its display text under the active mode.
|
|
150
|
+
|
|
151
|
+
Returns the label unchanged when localization is off or no translation
|
|
152
|
+
is registered; otherwise the catalog translation.
|
|
153
|
+
"""
|
|
154
|
+
if not raw:
|
|
155
|
+
return ''
|
|
156
|
+
if self._effective_localize() is False:
|
|
157
|
+
return raw
|
|
158
|
+
try:
|
|
159
|
+
from bootstack.i18n import MessageCatalog
|
|
160
|
+
return MessageCatalog.translate(raw) or raw
|
|
161
|
+
except Exception:
|
|
162
|
+
return raw
|
|
163
|
+
|
|
164
|
+
def _sync_display(self) -> None:
|
|
165
|
+
"""Refresh the visible face from the current value-key text."""
|
|
166
|
+
self._display_var.set(self._display_text(self._textvariable.get()))
|
|
167
|
+
|
|
168
|
+
def _rebuild_option_maps(self) -> None:
|
|
169
|
+
"""Rebuild the text<->value lookups from the current records.
|
|
170
|
+
|
|
171
|
+
Duplicate texts keep the first value; duplicate values keep the first
|
|
172
|
+
text. A SelectButton uses a single shared variable keyed on text, so
|
|
173
|
+
duplicate texts with distinct values cannot be told apart — keep option
|
|
174
|
+
texts unique.
|
|
175
|
+
"""
|
|
176
|
+
self._value_by_text: dict[str, Any] = {}
|
|
177
|
+
self._text_by_value: dict[Any, str] = {}
|
|
178
|
+
for rec in self._records:
|
|
179
|
+
self._value_by_text.setdefault(rec.text, rec.value)
|
|
180
|
+
try:
|
|
181
|
+
self._text_by_value.setdefault(rec.value, rec.text)
|
|
182
|
+
except TypeError:
|
|
183
|
+
pass # unhashable value — no reverse mapping; display still works
|
|
184
|
+
|
|
185
|
+
def _resolve_initial_text(self, value: Any) -> str:
|
|
186
|
+
"""Map an initial value-space value to its display text.
|
|
187
|
+
|
|
188
|
+
Returns the empty string for None/empty; raises if the value matches no
|
|
189
|
+
option.
|
|
190
|
+
"""
|
|
191
|
+
if value is None or value == "":
|
|
192
|
+
return ""
|
|
193
|
+
try:
|
|
194
|
+
if value in self._text_by_value:
|
|
195
|
+
return self._text_by_value[value]
|
|
196
|
+
except TypeError:
|
|
197
|
+
pass
|
|
198
|
+
raise ValueError(f"{value!r} is not one of the options")
|
|
199
|
+
|
|
200
|
+
def _bind_change_event(self):
|
|
201
|
+
"""(Re)bind textsignal to emit <<Change>> Tk events (in value-space).
|
|
202
|
+
|
|
203
|
+
The same subscription keeps the localized face in sync, since the value
|
|
204
|
+
key changes whenever a selection is made.
|
|
205
|
+
"""
|
|
206
|
+
if self._bind_id is not None:
|
|
207
|
+
self._bind_id.cancel()
|
|
208
|
+
|
|
209
|
+
def _on_change(text: str) -> None:
|
|
210
|
+
self._sync_display()
|
|
211
|
+
self.event_generate(
|
|
212
|
+
'<<Change>>',
|
|
213
|
+
data=ChangeEvent(value=self._value_by_text.get(text, text) if text else None),
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return self.textsignal.subscribe(_on_change)
|
|
217
|
+
|
|
218
|
+
def _build_context_menu(self):
|
|
219
|
+
# Affordance baked into the button image (focus ring + border line in
|
|
220
|
+
# source-px); aligning the menu's left edge to it matches the visible
|
|
221
|
+
# button border the same way the combobox popdown does.
|
|
222
|
+
from bootstack.style.style_builder_base import StyleBuilderBase
|
|
223
|
+
offset_x = StyleBuilderBase.scale_from_source(10)
|
|
224
|
+
|
|
225
|
+
density = self.configure_style_options('density') or 'default'
|
|
226
|
+
|
|
227
|
+
menu_items = []
|
|
228
|
+
for rec in self._records:
|
|
229
|
+
icon, disabled = option_display(rec)
|
|
230
|
+
# A per-option localize key overrides the widget-level mode.
|
|
231
|
+
item_localize = rec.extras.get("localize", self._localize)
|
|
232
|
+
menu_items.append(
|
|
233
|
+
ContextMenuItem(
|
|
234
|
+
type="radiobutton",
|
|
235
|
+
text=rec.text,
|
|
236
|
+
variable=self._textvariable,
|
|
237
|
+
value=rec.text,
|
|
238
|
+
icon=icon,
|
|
239
|
+
disabled=disabled,
|
|
240
|
+
localize=item_localize,
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
return ContextMenu(
|
|
244
|
+
self, target=self, items=menu_items,
|
|
245
|
+
anchor="nw", attach="sw",
|
|
246
|
+
offset=(offset_x, 0),
|
|
247
|
+
density=density,
|
|
248
|
+
# OptionMenu drives activation via `show_menu` (left-click,
|
|
249
|
+
# Return/KP_Enter), not ContextMenu's auto-trigger.
|
|
250
|
+
trigger=None,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
def show_menu(self):
|
|
254
|
+
"""Show the dropdown menu unless disabled or readonly."""
|
|
255
|
+
if not self.instate(("!disabled", "!readonly")):
|
|
256
|
+
return
|
|
257
|
+
# Match the menu's minimum width to the visible button width so the
|
|
258
|
+
# dropdown never renders narrower than its trigger.
|
|
259
|
+
from bootstack.style.style_builder_base import StyleBuilderBase
|
|
260
|
+
affordance = StyleBuilderBase.scale_from_source(10)
|
|
261
|
+
target_w = self.winfo_width() - 2 * affordance
|
|
262
|
+
if target_w > 0:
|
|
263
|
+
self._context_menu.configure(minwidth=max(150, target_w))
|
|
264
|
+
self._context_menu.show()
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def text(self) -> str:
|
|
268
|
+
"""The display label currently shown on the button (translated)."""
|
|
269
|
+
return self._display_var.get()
|
|
270
|
+
|
|
271
|
+
@property
|
|
272
|
+
def selection(self) -> dict | None:
|
|
273
|
+
"""The selected option as a full record dict (the data bag), or None."""
|
|
274
|
+
text = self._textvariable.get()
|
|
275
|
+
if text == "":
|
|
276
|
+
return None
|
|
277
|
+
for rec in self._records:
|
|
278
|
+
if rec.text == text:
|
|
279
|
+
return record_to_dict(rec)
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
def get(self) -> Any:
|
|
283
|
+
"""Return the current value (value-space), or `None` if nothing is selected."""
|
|
284
|
+
text = self._textvariable.get()
|
|
285
|
+
if text == "":
|
|
286
|
+
return None
|
|
287
|
+
return self._value_by_text.get(text, text)
|
|
288
|
+
|
|
289
|
+
def set(self, value: Any) -> None:
|
|
290
|
+
"""Select the option whose value is `value`, updating the displayed text.
|
|
291
|
+
|
|
292
|
+
Passing None or `''` clears the selection. A value that matches no
|
|
293
|
+
option raises `ValueError`.
|
|
294
|
+
"""
|
|
295
|
+
if value is None or value == "":
|
|
296
|
+
self._textvariable.set("")
|
|
297
|
+
return
|
|
298
|
+
try:
|
|
299
|
+
text = self._text_by_value[value]
|
|
300
|
+
except (KeyError, TypeError):
|
|
301
|
+
raise ValueError(f"{value!r} is not one of the options")
|
|
302
|
+
self._textvariable.set(text)
|
|
303
|
+
|
|
304
|
+
@property
|
|
305
|
+
def value(self) -> str:
|
|
306
|
+
"""Get or set the current value."""
|
|
307
|
+
return self.get()
|
|
308
|
+
|
|
309
|
+
@value.setter
|
|
310
|
+
def value(self, value: Any) -> None:
|
|
311
|
+
self.set(value)
|
|
312
|
+
|
|
313
|
+
def on_changed(self, callback: Callable) -> str:
|
|
314
|
+
"""Register a callback for `<<Change>>` events.
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
callback: Called when a menu item is selected. Receives a Tk event
|
|
318
|
+
object; `event.data` is a `ChangeEvent` with the attribute
|
|
319
|
+
`value` (the newly selected value, coerced to string).
|
|
320
|
+
|
|
321
|
+
Returns:
|
|
322
|
+
Bind ID — pass to `off_changed()` to unsubscribe.
|
|
323
|
+
"""
|
|
324
|
+
return self.bind('<<Change>>', callback, add="+")
|
|
325
|
+
|
|
326
|
+
def off_changed(self, bind_id: str | None = None) -> None:
|
|
327
|
+
"""Unsubscribe from `<<Change>>`.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
bind_id: ID returned by `on_changed()`. If None, removes all.
|
|
331
|
+
"""
|
|
332
|
+
self.unbind('<<Change>>', bind_id)
|
|
333
|
+
|
|
334
|
+
@configure_delegate('options')
|
|
335
|
+
def _delegate_options(self, value=None):
|
|
336
|
+
"""Get the normalized option records, or set new options."""
|
|
337
|
+
if value is None:
|
|
338
|
+
return list(self._records)
|
|
339
|
+
else:
|
|
340
|
+
self._records = normalize_options(value)
|
|
341
|
+
self._rebuild_option_maps()
|
|
342
|
+
# Reconcile the current selection: if its display text is no longer
|
|
343
|
+
# one of the options, clear it so the button never shows a value the
|
|
344
|
+
# list can't offer (and no menu item stays phantom-selected).
|
|
345
|
+
if self._textvariable.get() not in self._value_by_text:
|
|
346
|
+
self._textvariable.set("")
|
|
347
|
+
if self._context_menu:
|
|
348
|
+
self._context_menu.destroy()
|
|
349
|
+
self._context_menu = self._build_context_menu()
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
@configure_delegate('value')
|
|
353
|
+
def _delegate_value(self, value):
|
|
354
|
+
"""Get or set the current value."""
|
|
355
|
+
if value is None:
|
|
356
|
+
return self.value
|
|
357
|
+
else:
|
|
358
|
+
self.value = value
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
@configure_delegate('textsignal')
|
|
362
|
+
def _delegate_textsignal(self, value=None):
|
|
363
|
+
"""Get or set the textsignal binding."""
|
|
364
|
+
if value is None:
|
|
365
|
+
return super()._delegate_textsignal()
|
|
366
|
+
else:
|
|
367
|
+
super()._delegate_textsignal(value)
|
|
368
|
+
self._bind_change_event()
|
|
369
|
+
return None
|
|
370
|
+
|
|
371
|
+
@configure_delegate('show_dropdown_button')
|
|
372
|
+
def _delegate_show_dropdown_button(self, value=None):
|
|
373
|
+
"""Get or set visibility of the dropdown chevron."""
|
|
374
|
+
if value is None:
|
|
375
|
+
return self.configure_style_options('show_dropdown_button')
|
|
376
|
+
else:
|
|
377
|
+
self.configure_style_options(show_dropdown_button=value)
|
|
378
|
+
return self.rebuild_style()
|
|
379
|
+
|
|
380
|
+
@configure_delegate('dropdown_button_icon')
|
|
381
|
+
def _delegate_dropdown_button_icon(self, value):
|
|
382
|
+
"""Get or set the dropdown chevron icon name."""
|
|
383
|
+
if value is None:
|
|
384
|
+
return self.configure_style_options('dropdown_button_icon')
|
|
385
|
+
else:
|
|
386
|
+
self.configure_style_options(dropdown_button_icon=value)
|
|
387
|
+
return self.rebuild_style()
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import tkinter as tk
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from typing_extensions import Unpack
|
|
7
|
+
|
|
8
|
+
from bootstack.widgets._impl.primitives.frame import Frame, FrameKwargs
|
|
9
|
+
from bootstack.widgets._impl.mixins.configure_mixin import configure_delegate
|
|
10
|
+
from bootstack.widgets._core.container import normalize_fill
|
|
11
|
+
from bootstack.widgets.types import Anchor, Direction, Fill, Master, Side
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PackFrame(Frame):
|
|
15
|
+
"""A Frame with simplified pack-based layout management.
|
|
16
|
+
|
|
17
|
+
PackFrame extends the bootstack Frame with automatic pack-based
|
|
18
|
+
layout management, including support for direction, gap spacing,
|
|
19
|
+
and default fill/expand behavior.
|
|
20
|
+
|
|
21
|
+
Children packed into this frame automatically receive the frame's
|
|
22
|
+
default layout options. Simply use the standard `pack()` method
|
|
23
|
+
on child widgets — no special `add()` method is needed.
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
SIDE_MAP: dict[Direction, Side] = {
|
|
28
|
+
"vertical": "top",
|
|
29
|
+
"column": "top",
|
|
30
|
+
"column-reverse": "bottom",
|
|
31
|
+
"horizontal": "left",
|
|
32
|
+
"row": "left",
|
|
33
|
+
"row-reverse": "right",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
master: Master = None,
|
|
39
|
+
*,
|
|
40
|
+
direction: Direction = "vertical",
|
|
41
|
+
gap: int = 0,
|
|
42
|
+
fill_items: Fill | None = None,
|
|
43
|
+
expand_items: bool | None = None,
|
|
44
|
+
anchor_items: Anchor | None = None,
|
|
45
|
+
propagate: bool | None = None,
|
|
46
|
+
**kwargs: Unpack[FrameKwargs],
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Create a PackFrame with automatic pack-based layout.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
master: Parent widget. If None, uses the default root window.
|
|
52
|
+
direction: Layout direction — `'vertical'`, `'horizontal'`,
|
|
53
|
+
`'row'`, `'column'`, `'row-reverse'`, or `'column-reverse'`.
|
|
54
|
+
Defaults to `'vertical'`.
|
|
55
|
+
gap: Spacing between children in pixels. Defaults to 0.
|
|
56
|
+
fill_items: Default `fill` option for children
|
|
57
|
+
(`'none'`, `'x'`, `'y'`, or `'both'`). If None, no default fill
|
|
58
|
+
is applied.
|
|
59
|
+
expand_items: Default `expand` option for children. If None, no
|
|
60
|
+
default expand is applied.
|
|
61
|
+
anchor_items: Default `anchor` for children. If None, no default
|
|
62
|
+
anchor is applied.
|
|
63
|
+
propagate: If False, the frame will not resize to fit its contents.
|
|
64
|
+
Defaults to None (Tk default behaviour).
|
|
65
|
+
|
|
66
|
+
Other Parameters:
|
|
67
|
+
**kwargs: Additional keyword arguments forwarded to `Frame`.
|
|
68
|
+
"""
|
|
69
|
+
super().__init__(master, **kwargs)
|
|
70
|
+
|
|
71
|
+
self._direction = direction
|
|
72
|
+
self._gap = gap
|
|
73
|
+
self._default_fill = fill_items
|
|
74
|
+
self._default_expand = expand_items
|
|
75
|
+
self._default_anchor = anchor_items
|
|
76
|
+
|
|
77
|
+
# Ordered list of (widget, user_options) tuples for gap tracking
|
|
78
|
+
self._managed: list[tuple[tk.Widget, dict[str, Any]]] = []
|
|
79
|
+
|
|
80
|
+
if propagate is not None:
|
|
81
|
+
self.pack_propagate(propagate)
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def _side(self) -> Side:
|
|
85
|
+
"""Get the pack side based on direction."""
|
|
86
|
+
return self.SIDE_MAP.get(self._direction, "top")
|
|
87
|
+
|
|
88
|
+
@configure_delegate('direction')
|
|
89
|
+
def _delegate_direction(self, value=None) -> Direction:
|
|
90
|
+
"""Get or set the layout direction."""
|
|
91
|
+
if value is None:
|
|
92
|
+
return self._direction
|
|
93
|
+
self._direction = value
|
|
94
|
+
# Repack all widgets with new direction
|
|
95
|
+
self._repack_all()
|
|
96
|
+
|
|
97
|
+
@configure_delegate('gap')
|
|
98
|
+
def _delegate_gap(self, value=None) -> int:
|
|
99
|
+
"""Get or set the gap between children."""
|
|
100
|
+
if value is None:
|
|
101
|
+
return self._gap
|
|
102
|
+
self._gap = value
|
|
103
|
+
# Repack all widgets with new gap
|
|
104
|
+
self._repack_all()
|
|
105
|
+
|
|
106
|
+
def _compute_gap(self, index: int) -> dict[str, Any]:
|
|
107
|
+
"""Compute padding for gap based on position and direction."""
|
|
108
|
+
if index == 0 or self._gap == 0:
|
|
109
|
+
return {}
|
|
110
|
+
|
|
111
|
+
if self._direction in ("vertical", "column"):
|
|
112
|
+
return {"pady": (self._gap, 0)}
|
|
113
|
+
elif self._direction == "column-reverse":
|
|
114
|
+
return {"pady": (0, self._gap)}
|
|
115
|
+
elif self._direction in ("horizontal", "row"):
|
|
116
|
+
return {"padx": (self._gap, 0)}
|
|
117
|
+
elif self._direction == "row-reverse":
|
|
118
|
+
return {"padx": (0, self._gap)}
|
|
119
|
+
return {}
|
|
120
|
+
|
|
121
|
+
def _build_options(self, index: int, user_options: dict[str, Any]) -> dict[str, Any]:
|
|
122
|
+
"""Build final pack options by merging defaults with user options."""
|
|
123
|
+
options: dict[str, Any] = {"in_": self, "side": self._side}
|
|
124
|
+
|
|
125
|
+
# Apply gap based on position
|
|
126
|
+
options.update(self._compute_gap(index))
|
|
127
|
+
|
|
128
|
+
# Apply container-level defaults
|
|
129
|
+
if self._default_fill is not None:
|
|
130
|
+
options["fill"] = self._default_fill
|
|
131
|
+
if self._default_expand is not None:
|
|
132
|
+
options["expand"] = self._default_expand
|
|
133
|
+
if self._default_anchor is not None:
|
|
134
|
+
options["anchor"] = self._default_anchor
|
|
135
|
+
|
|
136
|
+
# User options override everything
|
|
137
|
+
options.update(user_options)
|
|
138
|
+
# Normalize fill aliases so "horizontal"/"vertical" reach tkinter as "x"/"y"
|
|
139
|
+
if "fill" in options:
|
|
140
|
+
options["fill"] = normalize_fill(options["fill"])
|
|
141
|
+
return options
|
|
142
|
+
|
|
143
|
+
def _repack_all(self) -> None:
|
|
144
|
+
"""Unpack and repack all widgets to maintain correct order and gaps."""
|
|
145
|
+
# Unpack all
|
|
146
|
+
for widget, _ in self._managed:
|
|
147
|
+
tk.Pack.forget(widget)
|
|
148
|
+
|
|
149
|
+
# Repack in order
|
|
150
|
+
for i, (widget, user_options) in enumerate(self._managed):
|
|
151
|
+
options = self._build_options(i, user_options)
|
|
152
|
+
tk.Pack.configure(widget, **options)
|
|
153
|
+
|
|
154
|
+
def _find_widget_index(self, widget: tk.Widget) -> int:
|
|
155
|
+
"""Find index of widget in managed list, return -1 if not found."""
|
|
156
|
+
for i, (w, _) in enumerate(self._managed):
|
|
157
|
+
if w is widget:
|
|
158
|
+
return i
|
|
159
|
+
return -1
|
|
160
|
+
|
|
161
|
+
def _find_insert_index(self, before: tk.Widget = None, after: tk.Widget = None) -> int:
|
|
162
|
+
"""Determine insertion index based on before/after options."""
|
|
163
|
+
if before is not None:
|
|
164
|
+
idx = self._find_widget_index(before)
|
|
165
|
+
if idx >= 0:
|
|
166
|
+
return idx
|
|
167
|
+
if after is not None:
|
|
168
|
+
idx = self._find_widget_index(after)
|
|
169
|
+
if idx >= 0:
|
|
170
|
+
return idx + 1
|
|
171
|
+
return len(self._managed)
|
|
172
|
+
|
|
173
|
+
# -------------------------------------------------------------------------
|
|
174
|
+
# Hook methods called by PackMixin
|
|
175
|
+
# -------------------------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
def _on_child_pack(self, widget: tk.Widget, **options: Any) -> None:
|
|
178
|
+
"""Hook called when a child widget calls pack().
|
|
179
|
+
|
|
180
|
+
Applies frame defaults, handles gap spacing, and tracks the widget.
|
|
181
|
+
"""
|
|
182
|
+
# Check if widget is already managed (reconfigure case)
|
|
183
|
+
existing_idx = self._find_widget_index(widget)
|
|
184
|
+
|
|
185
|
+
# Determine insertion position from before/after
|
|
186
|
+
before = options.pop("before", None)
|
|
187
|
+
after = options.pop("after", None)
|
|
188
|
+
|
|
189
|
+
if existing_idx >= 0:
|
|
190
|
+
# Widget already managed - update its options
|
|
191
|
+
self._managed[existing_idx] = (widget, options)
|
|
192
|
+
self._repack_all()
|
|
193
|
+
else:
|
|
194
|
+
# New widget - find insertion point
|
|
195
|
+
insert_idx = self._find_insert_index(before, after)
|
|
196
|
+
|
|
197
|
+
if insert_idx < len(self._managed):
|
|
198
|
+
# Inserting in the middle - need to repack all
|
|
199
|
+
self._managed.insert(insert_idx, (widget, options))
|
|
200
|
+
self._repack_all()
|
|
201
|
+
else:
|
|
202
|
+
# Appending at the end - just pack it
|
|
203
|
+
pack_options = self._build_options(len(self._managed), options)
|
|
204
|
+
tk.Pack.configure(widget, **pack_options)
|
|
205
|
+
self._managed.append((widget, options))
|
|
206
|
+
|
|
207
|
+
def _on_child_pack_forget(self, widget: tk.Widget) -> None:
|
|
208
|
+
"""Hook called when a child widget calls pack_forget().
|
|
209
|
+
|
|
210
|
+
Removes widget from tracking and repacks remaining widgets if needed.
|
|
211
|
+
"""
|
|
212
|
+
idx = self._find_widget_index(widget)
|
|
213
|
+
if idx < 0:
|
|
214
|
+
# Not managed by us, just forget it normally
|
|
215
|
+
tk.Pack.forget(widget)
|
|
216
|
+
return
|
|
217
|
+
|
|
218
|
+
# Remove from our tracking
|
|
219
|
+
tk.Pack.forget(widget)
|
|
220
|
+
self._managed.pop(idx)
|
|
221
|
+
|
|
222
|
+
# Only repack if we removed something that affects gaps (not the last
|
|
223
|
+
# item). With no gap, Tk preserves the order of the remaining children
|
|
224
|
+
# on its own, so a full forget/repack is pure churn (it briefly clears
|
|
225
|
+
# the whole row — a visible flash) and is skipped.
|
|
226
|
+
if self._gap and idx < len(self._managed):
|
|
227
|
+
self._repack_all()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from tkinter import ttk
|
|
4
|
+
from typing import Any
|
|
5
|
+
from typing_extensions import Unpack
|
|
6
|
+
|
|
7
|
+
from bootstack._core.mixins.ttk_state import TtkStateMixin
|
|
8
|
+
from bootstack._core.mixins.widget import WidgetCapabilitiesMixin
|
|
9
|
+
from bootstack.widgets._impl._internal.wrapper_base import TTKWrapperBase
|
|
10
|
+
from bootstack.widgets.types import Master, StyledKwargs, Orient
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PanedWindowKwargs(StyledKwargs, total=False):
|
|
14
|
+
# Standard ttk.Panedwindow options
|
|
15
|
+
orient: Orient
|
|
16
|
+
padding: Any
|
|
17
|
+
width: int
|
|
18
|
+
height: int
|
|
19
|
+
|
|
20
|
+
# bootstack-specific extensions
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PanedWindow(TTKWrapperBase, WidgetCapabilitiesMixin, TtkStateMixin, ttk.PanedWindow):
|
|
24
|
+
"""bootstack wrapper for `ttk.Panedwindow` with themed styling support."""
|
|
25
|
+
|
|
26
|
+
_ttk_base = ttk.Panedwindow
|
|
27
|
+
|
|
28
|
+
def __init__(self, master: Master = None, **kwargs: Unpack[PanedWindowKwargs]) -> None:
|
|
29
|
+
"""Create a themed bootstack Panedwindow.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
master: Parent widget. If None, uses the default root window.
|
|
33
|
+
|
|
34
|
+
Other Parameters:
|
|
35
|
+
orient: Orientation of panes ('horizontal' or 'vertical').
|
|
36
|
+
padding: Extra internal padding.
|
|
37
|
+
width: Requested width in pixels.
|
|
38
|
+
height: Requested height in pixels.
|
|
39
|
+
style: Explicit ttk style name (overrides accent/variant).
|
|
40
|
+
accent: Accent token for styling, e.g. 'primary', 'secondary'.
|
|
41
|
+
variant: Style variant (if applicable).
|
|
42
|
+
surface: Optional surface token; otherwise inherited.
|
|
43
|
+
style_options: Optional dict forwarded to the style builder.
|
|
44
|
+
"""
|
|
45
|
+
super().__init__(master, **kwargs)
|