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,447 @@
|
|
|
1
|
+
"""Style resolution and widget integration for bootstack.
|
|
2
|
+
|
|
3
|
+
Resolves accent/variant tokens into TTK style names and wires those styles
|
|
4
|
+
onto widgets by overriding their constructors.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from bootstack._runtime.app import get_app_settings, has_current_app
|
|
12
|
+
from bootstack.style.token_maps import (COLOR_TOKENS, CONTAINER_CLASSES, ORIENT_CLASSES, WIDGET_CLASS_MAP)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def to_pascal_case(s: str) -> str:
|
|
17
|
+
"""Convert dash-separated string to PascalCase.
|
|
18
|
+
|
|
19
|
+
Examples:
|
|
20
|
+
'context-check' -> 'ContextCheck'
|
|
21
|
+
'outline' -> 'Outline'
|
|
22
|
+
'solid' -> 'Solid'
|
|
23
|
+
"""
|
|
24
|
+
return ''.join(part.capitalize() for part in s.split('-'))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def from_pascal_case(s: str) -> str:
|
|
28
|
+
"""Convert PascalCase string to dash-separated lowercase.
|
|
29
|
+
|
|
30
|
+
Examples:
|
|
31
|
+
'ContextCheck' -> 'context-check'
|
|
32
|
+
'Outline' -> 'outline'
|
|
33
|
+
'Solid' -> 'solid'
|
|
34
|
+
"""
|
|
35
|
+
import re
|
|
36
|
+
# Insert dash before uppercase letters (except at start), then lowercase
|
|
37
|
+
return re.sub(r'(?<!^)(?=[A-Z])', '-', s).lower()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def generate_ttk_style_name(
|
|
41
|
+
accent: Optional[str],
|
|
42
|
+
variant: Optional[str],
|
|
43
|
+
widget_class: str,
|
|
44
|
+
custom_prefix: Optional[str] = None,
|
|
45
|
+
orient: Optional[str] = None,
|
|
46
|
+
) -> str:
|
|
47
|
+
"""Generate TTK style name from parsed components.
|
|
48
|
+
|
|
49
|
+
Returns style name in format: [custom_prefix].[accent].[Variant].[Orient].[Widget]
|
|
50
|
+
"""
|
|
51
|
+
parts = []
|
|
52
|
+
|
|
53
|
+
if custom_prefix:
|
|
54
|
+
parts.append(custom_prefix)
|
|
55
|
+
if accent:
|
|
56
|
+
parts.append(accent)
|
|
57
|
+
if variant:
|
|
58
|
+
parts.append(to_pascal_case(variant))
|
|
59
|
+
if orient:
|
|
60
|
+
parts.append(normalize_orientation(orient))
|
|
61
|
+
parts.append(widget_class)
|
|
62
|
+
return '.'.join(parts)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def normalize_orientation(orient: str):
|
|
66
|
+
"""Normalize TTK style orientation."""
|
|
67
|
+
if orient.lower().startswith('v'):
|
|
68
|
+
return 'Vertical'
|
|
69
|
+
else:
|
|
70
|
+
return 'Horizontal'
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def extract_orient_from_style(ttk_style: str):
|
|
75
|
+
"""Extract orientation from TTK style name."""
|
|
76
|
+
if 'horizontal' in ttk_style.lower():
|
|
77
|
+
return 'Horizontal'
|
|
78
|
+
elif 'vertical' in ttk_style.lower():
|
|
79
|
+
return 'Vertical'
|
|
80
|
+
else:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def extract_accent_from_style(ttk_style: str, default: str = 'primary') -> str:
|
|
85
|
+
"""Extract accent token from TTK style name, including modifiers like [subtle]."""
|
|
86
|
+
parts = ttk_style.split('.')
|
|
87
|
+
|
|
88
|
+
# Skip custom prefix if present (e.g., bs[hash] or custom_xyz)
|
|
89
|
+
if parts and (parts[0].startswith('bs[') or parts[0].startswith('custom_')):
|
|
90
|
+
parts = parts[1:]
|
|
91
|
+
|
|
92
|
+
for part in parts:
|
|
93
|
+
part_lower = part.lower()
|
|
94
|
+
# Check exact match first
|
|
95
|
+
if part_lower in COLOR_TOKENS:
|
|
96
|
+
return part_lower
|
|
97
|
+
# Check if base accent (before modifier) is an accent token
|
|
98
|
+
# e.g., 'primary[subtle]' -> 'primary'
|
|
99
|
+
if '[' in part_lower:
|
|
100
|
+
base_accent = part_lower.split('[')[0]
|
|
101
|
+
if base_accent in COLOR_TOKENS:
|
|
102
|
+
return part_lower # Return full token with modifier
|
|
103
|
+
|
|
104
|
+
return default
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def extract_variant_from_style(ttk_style: str, widget_class: str = None) -> Optional[str]:
|
|
108
|
+
"""Extract variant name from TTK style name.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
ttk_style: The TTK style name to parse.
|
|
112
|
+
widget_class: Optional widget class from winfo_class() to exclude from parsing.
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Variant name in dash-separated lowercase format (e.g., 'context-check')
|
|
116
|
+
"""
|
|
117
|
+
parts = ttk_style.split('.')
|
|
118
|
+
|
|
119
|
+
# Skip custom prefix if present (e.g., bs[hash] or custom_xyz)
|
|
120
|
+
if parts and (parts[0].startswith('bs[') or parts[0].startswith('custom_')):
|
|
121
|
+
parts = parts[1:]
|
|
122
|
+
|
|
123
|
+
# Build set of class-related parts to skip (e.g., 'Expander', 'TLabel')
|
|
124
|
+
class_parts = set()
|
|
125
|
+
if widget_class:
|
|
126
|
+
class_parts.update(widget_class.split('.'))
|
|
127
|
+
|
|
128
|
+
for part in parts:
|
|
129
|
+
part_lower = part.lower()
|
|
130
|
+
# Skip color tokens (including those with modifiers like 'primary[subtle]')
|
|
131
|
+
if part_lower in COLOR_TOKENS:
|
|
132
|
+
continue
|
|
133
|
+
if '[' in part_lower:
|
|
134
|
+
base_color = part_lower.split('[')[0]
|
|
135
|
+
if base_color in COLOR_TOKENS:
|
|
136
|
+
continue
|
|
137
|
+
# Skip known class parts from widget_class
|
|
138
|
+
if part in class_parts:
|
|
139
|
+
continue
|
|
140
|
+
# Skip standard ttk class names (TLabel, TButton, etc.)
|
|
141
|
+
if part.startswith('T'):
|
|
142
|
+
continue
|
|
143
|
+
# Skip orientation parts
|
|
144
|
+
if part in ('Horizontal', 'Vertical'):
|
|
145
|
+
continue
|
|
146
|
+
# Found a variant - convert from PascalCase to dash-separated
|
|
147
|
+
return from_pascal_case(part)
|
|
148
|
+
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def extract_widget_class_from_style(ttk_style: str) -> Optional[str]:
|
|
153
|
+
"""Extract widget class from TTK style name."""
|
|
154
|
+
parts = ttk_style.split('.')
|
|
155
|
+
|
|
156
|
+
for part in parts:
|
|
157
|
+
if part.startswith('T'):
|
|
158
|
+
return part
|
|
159
|
+
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class StyleResolver:
|
|
164
|
+
"""Widget integration layer for the style system.
|
|
165
|
+
|
|
166
|
+
Wires bootstack into tkinter/ttk by overriding widget constructors to
|
|
167
|
+
resolve `accent`/`variant` tokens into TTK style names.
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
@staticmethod
|
|
171
|
+
def create_ttk_style(
|
|
172
|
+
widget_class: str,
|
|
173
|
+
style_options: Optional[dict] = None,
|
|
174
|
+
*,
|
|
175
|
+
accent: Optional[str] = None,
|
|
176
|
+
variant: Optional[str] = None,
|
|
177
|
+
) -> str:
|
|
178
|
+
"""Create or get TTK style name for a widget.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
widget_class: TTK widget class (e.g., "TButton")
|
|
182
|
+
style_options: Custom style options dict
|
|
183
|
+
accent: Accent token (e.g., "success", "primary[subtle]")
|
|
184
|
+
variant: Variant name (e.g., "outline", "solid")
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
Generated TTK style name
|
|
188
|
+
"""
|
|
189
|
+
from bootstack.style.style_builder_ttk import StyleBuilderTtk
|
|
190
|
+
|
|
191
|
+
# If no accent and no variant, return base widget class
|
|
192
|
+
if not accent and not variant:
|
|
193
|
+
return widget_class
|
|
194
|
+
|
|
195
|
+
# Initialize style_options to empty dict if None
|
|
196
|
+
if style_options is None:
|
|
197
|
+
style_options = {}
|
|
198
|
+
|
|
199
|
+
surface = style_options.get("surface")
|
|
200
|
+
|
|
201
|
+
builder_variant = variant if variant is not None else \
|
|
202
|
+
StyleBuilderTtk.get_default_variant(widget_class)
|
|
203
|
+
|
|
204
|
+
custom_prefix = None
|
|
205
|
+
|
|
206
|
+
if style_options.keys() or surface != 'background':
|
|
207
|
+
import hashlib
|
|
208
|
+
import json
|
|
209
|
+
options_str = json.dumps(style_options, sort_keys=True)
|
|
210
|
+
options_hash = hashlib.md5(options_str.encode()).hexdigest()[:8]
|
|
211
|
+
custom_prefix = f"bs[{options_hash}]"
|
|
212
|
+
|
|
213
|
+
ttk_style = generate_ttk_style_name(
|
|
214
|
+
accent=accent,
|
|
215
|
+
variant=variant,
|
|
216
|
+
widget_class=widget_class,
|
|
217
|
+
custom_prefix=custom_prefix,
|
|
218
|
+
orient=style_options.get('orient'),
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
from bootstack.style.style import get_style
|
|
222
|
+
style = get_style()
|
|
223
|
+
|
|
224
|
+
style.create_style(
|
|
225
|
+
widget_class=widget_class,
|
|
226
|
+
variant=builder_variant,
|
|
227
|
+
ttk_style=ttk_style,
|
|
228
|
+
accent=accent,
|
|
229
|
+
options=style_options
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
return ttk_style
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def override_ttk_widget_constructor(func):
|
|
236
|
+
"""Override ttk widget __init__ to accept accent and variant parameters."""
|
|
237
|
+
|
|
238
|
+
def __init__wrapper(self, *args, **kwargs):
|
|
239
|
+
|
|
240
|
+
# Extract accent/variant parameters
|
|
241
|
+
accent = kwargs.pop("accent", None)
|
|
242
|
+
if accent == 'default':
|
|
243
|
+
accent = None
|
|
244
|
+
variant = kwargs.pop("variant", None)
|
|
245
|
+
|
|
246
|
+
had_style_kwarg = 'style' in kwargs
|
|
247
|
+
|
|
248
|
+
style_options = kwargs.pop("style_options", {})
|
|
249
|
+
inherit_surface = kwargs.pop('inherit_surface', None)
|
|
250
|
+
surface_token = kwargs.pop('surface', None)
|
|
251
|
+
input_bg_token = kwargs.pop('input_background', None)
|
|
252
|
+
|
|
253
|
+
# Extract ttk_class for style lookup (doesn't affect widget's actual class_)
|
|
254
|
+
# This allows custom style builders without affecting bindtags
|
|
255
|
+
ttk_class = kwargs.pop('ttk_class', None)
|
|
256
|
+
|
|
257
|
+
func(self, *args, **kwargs) # the actual widget constructor
|
|
258
|
+
|
|
259
|
+
# Use ttk_class for style lookup if provided, otherwise use widget's actual class
|
|
260
|
+
widget_class = self.winfo_class()
|
|
261
|
+
style_class = ttk_class or widget_class
|
|
262
|
+
|
|
263
|
+
# ===== Surface color inheritance =====
|
|
264
|
+
|
|
265
|
+
# Surface inheritance is always on in this framework (the former
|
|
266
|
+
# `inherit_surface_color` app toggle was removed); an explicit
|
|
267
|
+
# per-call `inherit_surface` kwarg can still override locally.
|
|
268
|
+
if inherit_surface is None:
|
|
269
|
+
inherit_surface = True
|
|
270
|
+
|
|
271
|
+
if hasattr(self, 'master') and self.master is not None:
|
|
272
|
+
parent_surface_token = getattr(self.master, '_surface', 'content')
|
|
273
|
+
else:
|
|
274
|
+
parent_surface_token = 'content'
|
|
275
|
+
|
|
276
|
+
if surface_token:
|
|
277
|
+
effective_surface_token = surface_token
|
|
278
|
+
elif inherit_surface:
|
|
279
|
+
effective_surface_token = parent_surface_token
|
|
280
|
+
else:
|
|
281
|
+
effective_surface_token = 'content'
|
|
282
|
+
|
|
283
|
+
# container widgets can take their surface color from the accent param
|
|
284
|
+
# Use style_class so custom ttk_class like 'Field' can opt out of this behavior
|
|
285
|
+
if accent and style_class in CONTAINER_CLASSES and surface_token is None:
|
|
286
|
+
effective_surface_token = accent
|
|
287
|
+
|
|
288
|
+
# cache the surface color for child components
|
|
289
|
+
setattr(self, '_surface', effective_surface_token)
|
|
290
|
+
if effective_surface_token != 'content' and effective_surface_token is not None:
|
|
291
|
+
style_options.setdefault('surface', effective_surface_token)
|
|
292
|
+
|
|
293
|
+
# ===== Input background inheritance =====
|
|
294
|
+
|
|
295
|
+
if hasattr(self, 'master') and self.master is not None:
|
|
296
|
+
parent_input_bg = getattr(self.master, '_input_background', None)
|
|
297
|
+
else:
|
|
298
|
+
parent_input_bg = None
|
|
299
|
+
|
|
300
|
+
if input_bg_token:
|
|
301
|
+
effective_input_bg = input_bg_token
|
|
302
|
+
elif inherit_surface and parent_input_bg:
|
|
303
|
+
effective_input_bg = parent_input_bg
|
|
304
|
+
else:
|
|
305
|
+
effective_input_bg = None
|
|
306
|
+
|
|
307
|
+
setattr(self, '_input_background', effective_input_bg)
|
|
308
|
+
if effective_input_bg:
|
|
309
|
+
style_options.setdefault('input_background', effective_input_bg)
|
|
310
|
+
|
|
311
|
+
# ==== Orientation =====
|
|
312
|
+
|
|
313
|
+
# handle widgets with orientation
|
|
314
|
+
if widget_class in ORIENT_CLASSES:
|
|
315
|
+
orient = str(self.cget('orient'))
|
|
316
|
+
style_options.setdefault('orient', orient)
|
|
317
|
+
|
|
318
|
+
# ==== Create actual ttk style & assign to widget =====
|
|
319
|
+
|
|
320
|
+
if (accent or variant) and style_class:
|
|
321
|
+
|
|
322
|
+
ttk_style = StyleResolver.create_ttk_style(
|
|
323
|
+
widget_class=style_class,
|
|
324
|
+
style_options=style_options,
|
|
325
|
+
accent=accent,
|
|
326
|
+
variant=variant,
|
|
327
|
+
)
|
|
328
|
+
self.configure(style=ttk_style)
|
|
329
|
+
|
|
330
|
+
elif style_class and not had_style_kwarg:
|
|
331
|
+
from bootstack.style.style_builder_ttk import StyleBuilderTtk
|
|
332
|
+
from bootstack.style.style import get_style
|
|
333
|
+
|
|
334
|
+
default_variant = StyleBuilderTtk.get_default_variant(style_class)
|
|
335
|
+
|
|
336
|
+
if StyleBuilderTtk.has_builder(style_class, default_variant):
|
|
337
|
+
|
|
338
|
+
# Build options first so we can decide if a custom bs[...] prefix is needed
|
|
339
|
+
custom_prefix = None
|
|
340
|
+
if style_options.keys():
|
|
341
|
+
import hashlib
|
|
342
|
+
import json
|
|
343
|
+
options_str = json.dumps(style_options, sort_keys=True)
|
|
344
|
+
options_hash = hashlib.md5(options_str.encode()).hexdigest()[:8]
|
|
345
|
+
custom_prefix = f"bs[{options_hash}]"
|
|
346
|
+
|
|
347
|
+
ttk_style = generate_ttk_style_name(
|
|
348
|
+
accent=None,
|
|
349
|
+
variant=default_variant,
|
|
350
|
+
widget_class=style_class,
|
|
351
|
+
custom_prefix=custom_prefix,
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
style_instance = get_style()
|
|
355
|
+
if style_instance is not None:
|
|
356
|
+
style_instance.create_style(
|
|
357
|
+
widget_class=style_class,
|
|
358
|
+
variant=default_variant,
|
|
359
|
+
ttk_style=ttk_style,
|
|
360
|
+
options=style_options,
|
|
361
|
+
)
|
|
362
|
+
self.configure(style=ttk_style)
|
|
363
|
+
else:
|
|
364
|
+
self.configure(style=style_class)
|
|
365
|
+
|
|
366
|
+
# Store accent, variant, ttk_class, and style_options for later retrieval
|
|
367
|
+
setattr(self, '_accent', accent)
|
|
368
|
+
setattr(self, '_variant', variant)
|
|
369
|
+
setattr(self, '_ttk_class', ttk_class)
|
|
370
|
+
setattr(self, '_style_options', style_options)
|
|
371
|
+
|
|
372
|
+
return __init__wrapper
|
|
373
|
+
|
|
374
|
+
@staticmethod
|
|
375
|
+
def override_tk_widget_constructor(func):
|
|
376
|
+
"""Override Tk widget __init__ to apply theme background when autostyle=True."""
|
|
377
|
+
|
|
378
|
+
def __init__wrapper(self, *args, **kwargs):
|
|
379
|
+
|
|
380
|
+
# capture bootstrap arguments
|
|
381
|
+
auto_style = kwargs.pop("autostyle", True)
|
|
382
|
+
|
|
383
|
+
# Class-level opt-out: widget manages its own styling
|
|
384
|
+
if auto_style and not getattr(type(self), '_bs_autostyle', True):
|
|
385
|
+
auto_style = False
|
|
386
|
+
|
|
387
|
+
# Parent-level opt-out: parent manages styling for its children
|
|
388
|
+
if auto_style:
|
|
389
|
+
master = args[0] if args else kwargs.get('master')
|
|
390
|
+
if master is not None and not getattr(master, '_bs_autostyle', True):
|
|
391
|
+
auto_style = False
|
|
392
|
+
|
|
393
|
+
inherit_surface = kwargs.pop('inherit_surface', None)
|
|
394
|
+
surface_token = kwargs.pop('surface', None)
|
|
395
|
+
|
|
396
|
+
# Outside an active App there is no theme/settings context — build
|
|
397
|
+
# the widget as plain Tk (no autostyle) instead of raising.
|
|
398
|
+
if not has_current_app():
|
|
399
|
+
func(self, *args, **kwargs)
|
|
400
|
+
setattr(self, '_surface', surface_token or 'content')
|
|
401
|
+
return
|
|
402
|
+
|
|
403
|
+
if inherit_surface is None:
|
|
404
|
+
inherit_surface = True
|
|
405
|
+
|
|
406
|
+
func(self, *args, **kwargs) # the actual constructor
|
|
407
|
+
|
|
408
|
+
# ===== Surface color inheritance =====
|
|
409
|
+
|
|
410
|
+
if hasattr(self, 'master') and self.master is not None:
|
|
411
|
+
parent_surface_token = getattr(self.master, '_surface', 'content')
|
|
412
|
+
else:
|
|
413
|
+
parent_surface_token = 'content'
|
|
414
|
+
|
|
415
|
+
# An explicit `surface=` always wins; otherwise inherit from the
|
|
416
|
+
# parent (the default) or fall back to 'content'. (This mirrors the
|
|
417
|
+
# ttk autostyle path — the Tk path previously let inheritance clobber
|
|
418
|
+
# an explicit surface, freezing e.g. a meter canvas to a resolved hex.)
|
|
419
|
+
if surface_token is None:
|
|
420
|
+
surface_token = parent_surface_token if inherit_surface else 'content'
|
|
421
|
+
|
|
422
|
+
setattr(self, '_surface', surface_token)
|
|
423
|
+
|
|
424
|
+
if not auto_style:
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
# ==== Apply the initial theme background =====
|
|
428
|
+
# `_surface` (the token, set above) is what the unified theme walk
|
|
429
|
+
# later re-resolves on a theme change; `_bs_theme_version` lets the
|
|
430
|
+
# walk tell whether this widget already matches the current theme.
|
|
431
|
+
|
|
432
|
+
from bootstack.style.style import get_style
|
|
433
|
+
style = get_style()
|
|
434
|
+
surface = getattr(self, '_surface', 'content')
|
|
435
|
+
style._get_tk_builder().call_builder(self, surface=surface)
|
|
436
|
+
self._bs_theme_version = style._theme_version
|
|
437
|
+
|
|
438
|
+
return __init__wrapper
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
__all__ = [
|
|
442
|
+
'generate_ttk_style_name',
|
|
443
|
+
'extract_accent_from_style',
|
|
444
|
+
'extract_variant_from_style',
|
|
445
|
+
'extract_widget_class_from_style',
|
|
446
|
+
'StyleResolver',
|
|
447
|
+
]
|
bootstack/style/theme.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Public `Theme` object for declaring and installing color themes.
|
|
2
|
+
|
|
3
|
+
A theme is a *family*: you declare the semantic accent colors once (as the
|
|
4
|
+
midpoint `[500]` of each ramp) plus a light and/or dark background block, and the
|
|
5
|
+
family generates both mode variants. Each accent expands into a 50–950 tint/shade
|
|
6
|
+
spectrum; the framework selects the right step per mode (a darker solid on light,
|
|
7
|
+
a brighter one on dark) so you never hand-write per-mode shades.
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
from bootstack.style import Theme
|
|
11
|
+
|
|
12
|
+
Theme(
|
|
13
|
+
name="bootstrap",
|
|
14
|
+
primary="#0d6efd", success="#198754", danger="#dc3545",
|
|
15
|
+
info="#0dcaf0", warning="#ffc107", # the [500] anchors
|
|
16
|
+
light=dict(background="#ffffff", foreground="#212529"),
|
|
17
|
+
dark=dict(background="#212529", foreground="#f8f9fa"),
|
|
18
|
+
).install() # registers bootstrap-light + -dark
|
|
19
|
+
|
|
20
|
+
bs.set_theme("bootstrap-dark") # activate a generated variant
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Installing registers the generated variants (`<name>-light` / `<name>-dark`);
|
|
24
|
+
activation is a separate `set_theme` call (or `install(activate=True)`). Themes
|
|
25
|
+
can be declared and installed at module level before an app exists — color
|
|
26
|
+
resolution is deferred until activation.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from dataclasses import dataclass, field, fields, replace
|
|
31
|
+
from typing import Literal
|
|
32
|
+
|
|
33
|
+
from bootstack._core.exceptions import ThemeError
|
|
34
|
+
from bootstack.style.theme_provider import register_theme
|
|
35
|
+
|
|
36
|
+
ThemeMode = Literal["light", "dark"]
|
|
37
|
+
|
|
38
|
+
# Registry of declared theme FAMILIES by name (e.g. "bootstrap" -> Theme), so a
|
|
39
|
+
# theme can be derived from an existing one by name. Built-ins register
|
|
40
|
+
# themselves via install() at startup; see _resolve_base().
|
|
41
|
+
_FAMILIES: dict[str, "Theme"] = {}
|
|
42
|
+
|
|
43
|
+
# Accent roles that take a `[500]` anchor and generate their own ramp.
|
|
44
|
+
_ACCENT_ROLES = ("primary", "success", "info", "warning", "danger")
|
|
45
|
+
|
|
46
|
+
# Which ramp step a SOLID (button fill, etc.) uses per mode. On light a darker
|
|
47
|
+
# step reads against white; on dark a brighter step reads against the dark
|
|
48
|
+
# background. `warning`/`info` stay at the bright `[500]` on light — they can't
|
|
49
|
+
# carry white text at any shade (they use dark on-color), so darkening them only
|
|
50
|
+
# mutes them.
|
|
51
|
+
_SOLID_STOP = {
|
|
52
|
+
"light": {"primary": 600, "success": 600, "danger": 600, "info": 500, "warning": 500},
|
|
53
|
+
"dark": {"primary": 400, "success": 400, "danger": 400, "info": 400, "warning": 400},
|
|
54
|
+
}
|
|
55
|
+
# Neutral (gray) step used for the `secondary` role per mode.
|
|
56
|
+
_SECONDARY_STOP = {"light": 700, "dark": 400}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _default_display_name(name: str) -> str:
|
|
60
|
+
return name.replace("-", " ").replace("_", " ").title()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class Theme:
|
|
65
|
+
"""A color theme family declared in code.
|
|
66
|
+
|
|
67
|
+
Declare the semantic accent colors as `[500]` anchors plus a `light` and/or
|
|
68
|
+
`dark` block (`background`/`foreground`). Each accent generates a 50–950
|
|
69
|
+
ramp; the framework picks the per-mode step for solids, washes, borders, and
|
|
70
|
+
emphasis text. `install()` registers the generated `<name>-light` and
|
|
71
|
+
`<name>-dark` variants.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
name: Family name; generated variants are `<name>-light` / `<name>-dark`.
|
|
75
|
+
primary: Primary accent color (the `[500]` ramp midpoint), as hex.
|
|
76
|
+
success: Success accent color (hex).
|
|
77
|
+
info: Info accent color (hex).
|
|
78
|
+
warning: Warning accent color (hex).
|
|
79
|
+
danger: Danger accent color (hex).
|
|
80
|
+
secondary: Optional colored secondary accent (hex). When omitted,
|
|
81
|
+
`secondary` derives from the neutral ramp.
|
|
82
|
+
neutral: Gray base for the neutral ramp (borders, muted text, secondary).
|
|
83
|
+
light: `{'background': ..., 'foreground': ...}` for the light variant, or
|
|
84
|
+
None to skip it.
|
|
85
|
+
dark: `{'background': ..., 'foreground': ...}` for the dark variant.
|
|
86
|
+
surfaces: Optional surface overrides — either a flat
|
|
87
|
+
`{'chrome': '#...'}` (both modes) or per-mode
|
|
88
|
+
`{'light': {...}, 'dark': {...}}`. Pins surfaces the auto-derivation
|
|
89
|
+
can't produce well (e.g. chrome on a very-dark background).
|
|
90
|
+
display_name: Human label base; the mode is appended (` Light`/` Dark`).
|
|
91
|
+
white: Reference white. Defaults to `'#ffffff'`.
|
|
92
|
+
black: Reference black. Defaults to `'#000000'`.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
name: str
|
|
96
|
+
|
|
97
|
+
primary: str | None = None
|
|
98
|
+
success: str | None = None
|
|
99
|
+
info: str | None = None
|
|
100
|
+
warning: str | None = None
|
|
101
|
+
danger: str | None = None
|
|
102
|
+
secondary: str | None = None
|
|
103
|
+
|
|
104
|
+
neutral: str = "#adb5bd"
|
|
105
|
+
|
|
106
|
+
light: dict[str, str] | None = None
|
|
107
|
+
dark: dict[str, str] | None = None
|
|
108
|
+
|
|
109
|
+
surfaces: dict | None = None
|
|
110
|
+
display_name: str | None = None
|
|
111
|
+
white: str = "#ffffff"
|
|
112
|
+
black: str = "#000000"
|
|
113
|
+
|
|
114
|
+
def _schema(self, mode: ThemeMode) -> dict | None:
|
|
115
|
+
block = self.light if mode == "light" else self.dark
|
|
116
|
+
if block is None:
|
|
117
|
+
return None
|
|
118
|
+
if "background" not in block or "foreground" not in block:
|
|
119
|
+
raise ThemeError(
|
|
120
|
+
f"Theme '{self.name}' {mode} block must define both "
|
|
121
|
+
f"'background' and 'foreground'."
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
solid = _SOLID_STOP[mode]
|
|
125
|
+
shades: dict[str, str] = {"gray": self.neutral}
|
|
126
|
+
semantic: dict[str, str] = {}
|
|
127
|
+
|
|
128
|
+
for role in _ACCENT_ROLES:
|
|
129
|
+
anchor = getattr(self, role)
|
|
130
|
+
if anchor:
|
|
131
|
+
shades[role] = anchor
|
|
132
|
+
semantic[role] = f"{role}[{solid[role]}]"
|
|
133
|
+
|
|
134
|
+
if self.secondary:
|
|
135
|
+
shades["secondary"] = self.secondary
|
|
136
|
+
semantic["secondary"] = f"secondary[{solid['primary']}]"
|
|
137
|
+
else:
|
|
138
|
+
semantic["secondary"] = f"gray[{_SECONDARY_STOP[mode]}]"
|
|
139
|
+
|
|
140
|
+
base_label = self.display_name or _default_display_name(self.name)
|
|
141
|
+
schema: dict = {
|
|
142
|
+
"name": f"{self.name}-{mode}",
|
|
143
|
+
"display_name": f"{base_label} {mode.title()}",
|
|
144
|
+
"mode": mode,
|
|
145
|
+
"foreground": block["foreground"],
|
|
146
|
+
"background": block["background"],
|
|
147
|
+
"white": self.white,
|
|
148
|
+
"black": self.black,
|
|
149
|
+
"shades": shades,
|
|
150
|
+
"semantic": semantic,
|
|
151
|
+
}
|
|
152
|
+
if self.surfaces:
|
|
153
|
+
override = self.surfaces.get(mode, None)
|
|
154
|
+
if override is None and not ({"light", "dark"} & set(self.surfaces)):
|
|
155
|
+
override = self.surfaces # flat dict → applies to both modes
|
|
156
|
+
if override:
|
|
157
|
+
schema["surfaces"] = dict(override)
|
|
158
|
+
return schema
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def from_existing(cls, base: "str | Theme", *, name: str, **overrides) -> "Theme":
|
|
162
|
+
"""Derive a new theme from an existing one, overriding only some tokens.
|
|
163
|
+
|
|
164
|
+
Every token you don't override is inherited from `base`, so you can brand
|
|
165
|
+
a built-in family by changing just its `primary` (and any others you want):
|
|
166
|
+
|
|
167
|
+
Theme.from_existing("bootstrap", name="acme", primary="#ff5722").install()
|
|
168
|
+
bs.set_theme("acme-light")
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
base: The theme to build on — a built-in or already-installed family
|
|
172
|
+
name (e.g. `'bootstrap'`), or a `Theme` instance. Required.
|
|
173
|
+
name: Family name for the derived theme; its `install()` registers
|
|
174
|
+
`<name>-light` / `<name>-dark`. Required.
|
|
175
|
+
**overrides: Any `Theme` token to replace (`primary`, `neutral`,
|
|
176
|
+
`light`, `dark`, `surfaces`, `display_name`, ...).
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
A new `Theme`; call `install()` on it to register its variants.
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
ThemeError: If `base` names a theme family that is not registered, or
|
|
183
|
+
an override names a token that is not a `Theme` field.
|
|
184
|
+
"""
|
|
185
|
+
source = base if isinstance(base, Theme) else _resolve_base(base)
|
|
186
|
+
unknown = set(overrides) - {f.name for f in fields(cls)}
|
|
187
|
+
if unknown:
|
|
188
|
+
raise ThemeError(
|
|
189
|
+
f"Unknown theme token(s): {', '.join(sorted(unknown))}."
|
|
190
|
+
)
|
|
191
|
+
return replace(source, name=name, **overrides)
|
|
192
|
+
|
|
193
|
+
def variants(self) -> list[dict]:
|
|
194
|
+
"""Return the generated per-mode schema dicts (light first, then dark)."""
|
|
195
|
+
return [s for s in (self._schema("light"), self._schema("dark")) if s]
|
|
196
|
+
|
|
197
|
+
def install(self, *, activate: bool | str = False) -> Theme:
|
|
198
|
+
"""Register the generated variants so they can be activated by name.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
activate: True activates the light variant; pass a variant name
|
|
202
|
+
(e.g. `'bootstrap-dark'`) to activate that one.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
This theme, to allow `theme = Theme(...).install()`.
|
|
206
|
+
"""
|
|
207
|
+
schemas = self.variants()
|
|
208
|
+
if not schemas:
|
|
209
|
+
raise ThemeError(
|
|
210
|
+
f"Theme '{self.name}' defines neither a 'light' nor a 'dark' block."
|
|
211
|
+
)
|
|
212
|
+
for schema in schemas:
|
|
213
|
+
register_theme(schema["name"], schema)
|
|
214
|
+
_FAMILIES[self.name] = self
|
|
215
|
+
if activate:
|
|
216
|
+
from bootstack.style.style import set_theme
|
|
217
|
+
target = activate if isinstance(activate, str) else schemas[0]["name"]
|
|
218
|
+
set_theme(target)
|
|
219
|
+
return self
|
|
220
|
+
|
|
221
|
+
def __repr__(self) -> str:
|
|
222
|
+
modes = "+".join(m for m, b in (("light", self.light), ("dark", self.dark)) if b)
|
|
223
|
+
return f"<Theme {self.name} ({modes or 'empty'})>"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _resolve_base(name: str) -> Theme:
|
|
227
|
+
"""Resolve a theme-family name to its `Theme`, seeding built-ins lazily."""
|
|
228
|
+
if name not in _FAMILIES:
|
|
229
|
+
# Seed the built-in families on demand (lazy import avoids a cycle).
|
|
230
|
+
try:
|
|
231
|
+
from bootstack.style.themes import BUILTIN_THEMES
|
|
232
|
+
for theme in BUILTIN_THEMES:
|
|
233
|
+
_FAMILIES.setdefault(theme.name, theme)
|
|
234
|
+
except Exception:
|
|
235
|
+
pass
|
|
236
|
+
source = _FAMILIES.get(name)
|
|
237
|
+
if source is None and (name.endswith("-light") or name.endswith("-dark")):
|
|
238
|
+
source = _FAMILIES.get(name.rsplit("-", 1)[0])
|
|
239
|
+
if source is None:
|
|
240
|
+
known = ", ".join(sorted(_FAMILIES)) or "none"
|
|
241
|
+
raise ThemeError(
|
|
242
|
+
f"Unknown base theme '{name}'. Pass a registered family name "
|
|
243
|
+
f"(known: {known}) or a Theme instance."
|
|
244
|
+
)
|
|
245
|
+
return source
|