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,463 @@
|
|
|
1
|
+
"""Image loading and caching utilities for bootstack.
|
|
2
|
+
|
|
3
|
+
This module provides a unified image service for loading, caching, and managing
|
|
4
|
+
Tk-compatible PhotoImage objects. It uses Pillow as the backend to support a wide
|
|
5
|
+
variety of image formats beyond what Tk natively supports.
|
|
6
|
+
|
|
7
|
+
The primary interface is the `_ImageService` class, which provides class methods
|
|
8
|
+
for loading images from various sources while automatically caching them to avoid
|
|
9
|
+
repeated decoding and to prevent garbage collection issues with Tk images.
|
|
10
|
+
|
|
11
|
+
This is the internal render/cache backend. The public, Tk-free image handle that
|
|
12
|
+
delegates to it lives in `bootstack.images` (`Image`/`get_icon`/`AppIcon`).
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
Basic usage with file paths:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from bootstack._core.images import _ImageService
|
|
19
|
+
|
|
20
|
+
# Load an image from disk (cached automatically)
|
|
21
|
+
photo = _ImageService.open("icons/save.png")
|
|
22
|
+
button = Button(app, image=photo)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Loading from bytes (e.g., embedded resources):
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
photo = _ImageService.from_bytes(icon_data)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Creating transparent spacer images:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
spacer = _ImageService.transparent(16, 16)
|
|
35
|
+
```
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
import hashlib
|
|
41
|
+
import io
|
|
42
|
+
import json
|
|
43
|
+
from dataclasses import dataclass
|
|
44
|
+
from pathlib import Path
|
|
45
|
+
from typing import Any, Hashable
|
|
46
|
+
|
|
47
|
+
from PIL import Image as PILImage, ImageDraw, ImageFilter, ImageFont
|
|
48
|
+
from PIL.ImageTk import PhotoImage as PILPhotoImage
|
|
49
|
+
|
|
50
|
+
_ICONS_DIR = Path(__file__).parent.parent / "assets" / "icons"
|
|
51
|
+
_ICON_Y_BIAS = 0.02
|
|
52
|
+
_ICON_PAD_FACTOR = 0.10
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True)
|
|
56
|
+
class ImageCacheInfo:
|
|
57
|
+
"""Information about the current state of an image cache.
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
items: The number of images currently stored in the cache.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
items: int
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _ImageService:
|
|
67
|
+
"""Platform image service for loading and caching PhotoImage objects.
|
|
68
|
+
|
|
69
|
+
This class provides a centralized service for working with images in
|
|
70
|
+
bootstack applications. It handles:
|
|
71
|
+
|
|
72
|
+
- Loading images from files, bytes, or PIL Image objects
|
|
73
|
+
- Automatic caching to avoid repeated decoding
|
|
74
|
+
- Keeping strong references to prevent Tk garbage collection issues
|
|
75
|
+
- Support for many image formats via Pillow (PNG, JPEG, GIF, BMP, etc.)
|
|
76
|
+
|
|
77
|
+
All methods are class methods, so no instantiation is required.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
```python
|
|
81
|
+
# From a file path
|
|
82
|
+
icon = _ImageService.open("path/to/icon.png")
|
|
83
|
+
|
|
84
|
+
# From raw bytes
|
|
85
|
+
icon = _ImageService.from_bytes(embedded_data)
|
|
86
|
+
|
|
87
|
+
# From a PIL Image object
|
|
88
|
+
pil_img = PILImage.open("photo.jpg").resize((100, 100))
|
|
89
|
+
icon = _ImageService.from_pil(pil_img)
|
|
90
|
+
|
|
91
|
+
# Create a transparent spacer
|
|
92
|
+
spacer = _ImageService.transparent(32, 32)
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Note:
|
|
96
|
+
Images are cached by default using automatically generated keys based
|
|
97
|
+
on the source (file path, content hash, or object id). You can provide
|
|
98
|
+
a custom `key` parameter to control caching behavior.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
_cache: dict[Hashable, PILPhotoImage] = {}
|
|
102
|
+
_icon_font_bytes: bytes | None = None
|
|
103
|
+
_icon_glyphmap: dict[str, str] | None = None
|
|
104
|
+
_icon_metrics: dict[str, list[float]] | None = None
|
|
105
|
+
_icon_font_cache: dict[int, Any] = {}
|
|
106
|
+
|
|
107
|
+
# =========================================================================
|
|
108
|
+
# Cache Management
|
|
109
|
+
# =========================================================================
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def get_cached(cls, key: Hashable) -> PILPhotoImage | None:
|
|
113
|
+
"""Retrieve a cached PhotoImage by its key.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
key: The cache key to look up.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
The cached PhotoImage if found, or None if not in cache.
|
|
120
|
+
"""
|
|
121
|
+
return cls._cache.get(key)
|
|
122
|
+
|
|
123
|
+
@classmethod
|
|
124
|
+
def set_cached(cls, key: Hashable, img: PILPhotoImage) -> PILPhotoImage:
|
|
125
|
+
"""Store a PhotoImage in the cache.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
key: The cache key to store the image under.
|
|
129
|
+
img: The PhotoImage to cache.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
The same PhotoImage that was passed in (for chaining).
|
|
133
|
+
"""
|
|
134
|
+
cls._cache[key] = img
|
|
135
|
+
return img
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def clear_cache(cls) -> None:
|
|
139
|
+
"""Clear all cached images.
|
|
140
|
+
|
|
141
|
+
This removes all images from the cache. Use with caution, as any
|
|
142
|
+
widgets still referencing these images may display incorrectly.
|
|
143
|
+
"""
|
|
144
|
+
cls._cache.clear()
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def cache_info(cls) -> ImageCacheInfo:
|
|
148
|
+
"""Get information about the current cache state.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
An ImageCacheInfo object containing cache statistics.
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
>>> info = _ImageService.cache_info()
|
|
155
|
+
>>> print(f"Cached images: {info.items}")
|
|
156
|
+
"""
|
|
157
|
+
return ImageCacheInfo(items=len(cls._cache))
|
|
158
|
+
|
|
159
|
+
# =========================================================================
|
|
160
|
+
# Image Constructors
|
|
161
|
+
# =========================================================================
|
|
162
|
+
|
|
163
|
+
@classmethod
|
|
164
|
+
def open(cls, path: str | Path, *, key: Hashable | None = None) -> PILPhotoImage:
|
|
165
|
+
"""Load an image from a file path.
|
|
166
|
+
|
|
167
|
+
Opens an image file using Pillow and converts it to a Tk-compatible
|
|
168
|
+
PhotoImage. The result is cached to avoid repeated disk reads and
|
|
169
|
+
decoding for the same file.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
path: Path to the image file. Supports ~ expansion and relative paths.
|
|
173
|
+
key: Optional custom cache key. If not provided, the absolute file
|
|
174
|
+
path is used as the cache key.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
A Tk-compatible PhotoImage that can be used with widgets.
|
|
178
|
+
|
|
179
|
+
Examples:
|
|
180
|
+
>>> photo = _ImageService.open("assets/logo.png")
|
|
181
|
+
>>> label = Label(app, image=photo)
|
|
182
|
+
|
|
183
|
+
>>> # With custom cache key for versioning
|
|
184
|
+
>>> photo = _ImageService.open("icon.png", key=("icon", "v2"))
|
|
185
|
+
"""
|
|
186
|
+
p = Path(path).expanduser().resolve()
|
|
187
|
+
cache_key = key if key is not None else ("file", str(p))
|
|
188
|
+
cached = cls.get_cached(cache_key)
|
|
189
|
+
if cached is not None:
|
|
190
|
+
return cached
|
|
191
|
+
|
|
192
|
+
pil = PILImage.open(p)
|
|
193
|
+
photo = PILPhotoImage(image=pil)
|
|
194
|
+
return cls.set_cached(cache_key, photo)
|
|
195
|
+
|
|
196
|
+
@classmethod
|
|
197
|
+
def from_pil(cls, image: PILImage.Image, *, key: Hashable | None = None) -> PILPhotoImage:
|
|
198
|
+
"""Convert a PIL Image to a Tk PhotoImage.
|
|
199
|
+
|
|
200
|
+
Wraps an existing PIL Image object in a Tk-compatible PhotoImage.
|
|
201
|
+
This is useful when you need to perform image manipulation (resizing,
|
|
202
|
+
filtering, etc.) before displaying the image.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
image: A PIL Image object to convert.
|
|
206
|
+
key: Optional custom cache key. If not provided, a key derived from
|
|
207
|
+
the image's content is used, so equal images share a photo and a
|
|
208
|
+
new image can never collide with a freed one's cached photo.
|
|
209
|
+
|
|
210
|
+
Returns:
|
|
211
|
+
A Tk-compatible PhotoImage that can be used with widgets.
|
|
212
|
+
|
|
213
|
+
Examples:
|
|
214
|
+
>>> from PIL import Image as PILImage
|
|
215
|
+
>>> pil_img = PILImage.open("photo.jpg")
|
|
216
|
+
>>> pil_img = pil_img.resize((100, 100))
|
|
217
|
+
>>> photo = _ImageService.from_pil(pil_img)
|
|
218
|
+
"""
|
|
219
|
+
if key is not None:
|
|
220
|
+
cache_key: Hashable = key
|
|
221
|
+
else:
|
|
222
|
+
# Content-based key. id(image) is NOT safe: PIL images are short-lived
|
|
223
|
+
# (e.g. a freshly rendered icon), and once garbage-collected their id
|
|
224
|
+
# can be reused by a different image — which would then be served the
|
|
225
|
+
# previous image's cached photo (a stale/duplicated picture).
|
|
226
|
+
try:
|
|
227
|
+
cache_key = (
|
|
228
|
+
"pil", image.mode, image.size,
|
|
229
|
+
hashlib.md5(image.tobytes()).hexdigest(),
|
|
230
|
+
)
|
|
231
|
+
except Exception:
|
|
232
|
+
# Content not byte-serializable — render fresh, uncached.
|
|
233
|
+
return PILPhotoImage(image=image)
|
|
234
|
+
cached = cls.get_cached(cache_key)
|
|
235
|
+
if cached is not None:
|
|
236
|
+
return cached
|
|
237
|
+
|
|
238
|
+
photo = PILPhotoImage(image=image)
|
|
239
|
+
return cls.set_cached(cache_key, photo)
|
|
240
|
+
|
|
241
|
+
@classmethod
|
|
242
|
+
def from_bytes(cls, data: bytes, *, key: Hashable | None = None) -> PILPhotoImage:
|
|
243
|
+
"""Create a PhotoImage from raw image bytes.
|
|
244
|
+
|
|
245
|
+
Decodes image data from bytes using Pillow and converts it to a
|
|
246
|
+
Tk-compatible PhotoImage. This is useful for embedded resources,
|
|
247
|
+
downloaded images, or any other source of raw image data.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
data: Raw image bytes in any format supported by Pillow
|
|
251
|
+
(PNG, JPEG, GIF, BMP, etc.).
|
|
252
|
+
key: Optional custom cache key. If not provided, an MD5 hash
|
|
253
|
+
of the bytes is used as the cache key.
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
A Tk-compatible PhotoImage that can be used with widgets.
|
|
257
|
+
|
|
258
|
+
Examples:
|
|
259
|
+
>>> # Load from embedded resource
|
|
260
|
+
>>> with open("icon.png", "rb") as f:
|
|
261
|
+
... icon_data = f.read()
|
|
262
|
+
>>> photo = _ImageService.from_bytes(icon_data)
|
|
263
|
+
|
|
264
|
+
>>> # With custom cache key
|
|
265
|
+
>>> photo = _ImageService.from_bytes(data, key="my-icon")
|
|
266
|
+
"""
|
|
267
|
+
digest = hashlib.md5(data).hexdigest()
|
|
268
|
+
cache_key = key if key is not None else ("bytes", digest)
|
|
269
|
+
cached = cls.get_cached(cache_key)
|
|
270
|
+
if cached is not None:
|
|
271
|
+
return cached
|
|
272
|
+
|
|
273
|
+
pil = PILImage.open(io.BytesIO(data))
|
|
274
|
+
photo = PILPhotoImage(image=pil)
|
|
275
|
+
return cls.set_cached(cache_key, photo)
|
|
276
|
+
|
|
277
|
+
@classmethod
|
|
278
|
+
def transparent(cls, width: int, height: int, *, key: Hashable | None = None) -> PILPhotoImage:
|
|
279
|
+
"""Create a transparent spacer image.
|
|
280
|
+
|
|
281
|
+
Creates a fully transparent RGBA image of the specified dimensions.
|
|
282
|
+
This is useful for creating spacing in layouts or as a placeholder
|
|
283
|
+
image.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
width: Width of the image in pixels.
|
|
287
|
+
height: Height of the image in pixels.
|
|
288
|
+
key: Optional custom cache key. If not provided, a tuple of
|
|
289
|
+
("transparent", width, height) is used.
|
|
290
|
+
|
|
291
|
+
Returns:
|
|
292
|
+
A transparent Tk-compatible PhotoImage.
|
|
293
|
+
|
|
294
|
+
Examples:
|
|
295
|
+
>>> # Create a 16x16 transparent spacer
|
|
296
|
+
>>> spacer = _ImageService.transparent(16, 16)
|
|
297
|
+
>>> label = Label(app, image=spacer)
|
|
298
|
+
|
|
299
|
+
>>> # Use as compound image padding
|
|
300
|
+
>>> button = Button(app, image=spacer, compound="left")
|
|
301
|
+
"""
|
|
302
|
+
cache_key = key if key is not None else ("transparent", width, height)
|
|
303
|
+
cached = cls.get_cached(cache_key)
|
|
304
|
+
if cached is not None:
|
|
305
|
+
return cached
|
|
306
|
+
|
|
307
|
+
pil = PILImage.new("RGBA", (width, height), (255, 255, 255, 0))
|
|
308
|
+
photo = PILPhotoImage(image=pil)
|
|
309
|
+
return cls.set_cached(cache_key, photo)
|
|
310
|
+
|
|
311
|
+
# =========================================================================
|
|
312
|
+
# Icon Rendering
|
|
313
|
+
# =========================================================================
|
|
314
|
+
|
|
315
|
+
@classmethod
|
|
316
|
+
def _load_icon_assets(cls) -> tuple[bytes, dict[str, str]]:
|
|
317
|
+
if cls._icon_font_bytes is not None:
|
|
318
|
+
return cls._icon_font_bytes, cls._icon_glyphmap # type: ignore[return-value]
|
|
319
|
+
cls._icon_font_bytes = (_ICONS_DIR / "bootstrap.ttf").read_bytes()
|
|
320
|
+
raw: dict[str, int] = json.loads((_ICONS_DIR / "glyphmap.json").read_text(encoding="utf-8"))
|
|
321
|
+
cls._icon_glyphmap = {name: chr(cp) for name, cp in raw.items()}
|
|
322
|
+
return cls._icon_font_bytes, cls._icon_glyphmap
|
|
323
|
+
|
|
324
|
+
@classmethod
|
|
325
|
+
def _get_icon_metrics(cls) -> dict[str, list[float]]:
|
|
326
|
+
"""Lazy-load precomputed normalized ink bounds per glyph.
|
|
327
|
+
|
|
328
|
+
Maps icon name to `[left, top, width, height]` as fractions of the font
|
|
329
|
+
size (from the text draw origin), generated by
|
|
330
|
+
`tools/generate_icon_metrics.py`. Missing file degrades gracefully to an
|
|
331
|
+
empty map (the renderer then falls back to measuring at render time).
|
|
332
|
+
"""
|
|
333
|
+
if cls._icon_metrics is None:
|
|
334
|
+
try:
|
|
335
|
+
cls._icon_metrics = json.loads((_ICONS_DIR / "icon_metrics.json").read_text(encoding="utf-8"))
|
|
336
|
+
except (OSError, ValueError):
|
|
337
|
+
cls._icon_metrics = {}
|
|
338
|
+
return cls._icon_metrics
|
|
339
|
+
|
|
340
|
+
@classmethod
|
|
341
|
+
def _get_icon_font(cls, size: int) -> ImageFont.FreeTypeFont:
|
|
342
|
+
font = cls._icon_font_cache.get(size)
|
|
343
|
+
if font is None:
|
|
344
|
+
font_bytes, _ = cls._load_icon_assets()
|
|
345
|
+
font = ImageFont.truetype(io.BytesIO(font_bytes), size)
|
|
346
|
+
cls._icon_font_cache[size] = font
|
|
347
|
+
return font
|
|
348
|
+
|
|
349
|
+
@classmethod
|
|
350
|
+
def get_icon(cls, name: str, size: int, color: str) -> PILPhotoImage:
|
|
351
|
+
"""Render a Bootstrap icon and return a cached Tk PhotoImage.
|
|
352
|
+
|
|
353
|
+
Even-pixel snapping is applied before rendering to eliminate half-pixel
|
|
354
|
+
LANCZOS blur at non-integer DPI scale factors (e.g., 125%, 150%).
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
name: Bootstrap icon name (e.g., `"house"`, `"arrow-right-fill"`).
|
|
358
|
+
size: Requested pixel size. Snapped to the nearest even integer.
|
|
359
|
+
color: Foreground color as a hex string (e.g., `"#ffffff"`).
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
A cached Tk-compatible PhotoImage for the icon.
|
|
363
|
+
"""
|
|
364
|
+
size = size if size % 2 == 0 else size + 1
|
|
365
|
+
key = ("icon", name, size, color)
|
|
366
|
+
cached = cls.get_cached(key)
|
|
367
|
+
if cached is not None:
|
|
368
|
+
return cached
|
|
369
|
+
pil = cls._render_icon(name, size, color)
|
|
370
|
+
photo = PILPhotoImage(image=pil)
|
|
371
|
+
return cls.set_cached(key, photo)
|
|
372
|
+
|
|
373
|
+
@classmethod
|
|
374
|
+
def _render_icon(
|
|
375
|
+
cls, name: str, size: int, color: str, *, align: bool = False
|
|
376
|
+
) -> PILImage.Image:
|
|
377
|
+
"""Render a named glyph to an RGBA image of `size` x `size`.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
name: Bootstrap Icons glyph name.
|
|
381
|
+
size: Output edge length in pixels.
|
|
382
|
+
color: Fill color (hex).
|
|
383
|
+
align: Snap the glyph's draw origin to the final pixel grid. Crisper
|
|
384
|
+
for standalone marks (e.g. app icons) where the glyph fills the
|
|
385
|
+
frame; off by default so glyphs sitting beside text keep their
|
|
386
|
+
exact optical centering.
|
|
387
|
+
"""
|
|
388
|
+
_, glyphmap = cls._load_icon_assets()
|
|
389
|
+
glyph = glyphmap.get(name)
|
|
390
|
+
if glyph is None:
|
|
391
|
+
return PILImage.new("RGBA", (size, size), (0, 0, 0, 0))
|
|
392
|
+
|
|
393
|
+
if size < 32:
|
|
394
|
+
oversample = 3
|
|
395
|
+
elif size < 64:
|
|
396
|
+
oversample = 2
|
|
397
|
+
else:
|
|
398
|
+
oversample = 1
|
|
399
|
+
|
|
400
|
+
def _snap(dx: float, dy: float) -> tuple[float, float]:
|
|
401
|
+
# Round the draw origin to a multiple of `oversample` so that after
|
|
402
|
+
# the downscale the glyph lands on whole pixels — its edges stay crisp
|
|
403
|
+
# instead of being antialiased across a sub-pixel offset.
|
|
404
|
+
if not align:
|
|
405
|
+
return dx, dy
|
|
406
|
+
return round(dx / oversample) * oversample, round(dy / oversample) * oversample
|
|
407
|
+
|
|
408
|
+
canvas_size = size * oversample
|
|
409
|
+
pad = int(canvas_size * _ICON_PAD_FACTOR)
|
|
410
|
+
inner = canvas_size - 2 * pad
|
|
411
|
+
|
|
412
|
+
img = PILImage.new("RGBA", (canvas_size, canvas_size), (0, 0, 0, 0))
|
|
413
|
+
draw = ImageDraw.Draw(img)
|
|
414
|
+
|
|
415
|
+
metrics = cls._get_icon_metrics().get(name)
|
|
416
|
+
if metrics:
|
|
417
|
+
# Precomputed normalized ink bounds (em fractions, from the text
|
|
418
|
+
# draw origin): [left, top, width, height]. Fit the ink to the inner
|
|
419
|
+
# box (never enlarging past natural size) and center on the true ink
|
|
420
|
+
# — accurate for full-bleed glyphs and free of getbbox skew.
|
|
421
|
+
nl, nt, nw, nh = metrics
|
|
422
|
+
font_size = max(1, int(min(canvas_size, inner / max(nw, nh, 1e-6))))
|
|
423
|
+
font = cls._get_icon_font(font_size)
|
|
424
|
+
ink_w, ink_h = nw * font_size, nh * font_size
|
|
425
|
+
dx = (canvas_size - ink_w) / 2 - nl * font_size
|
|
426
|
+
dy = (canvas_size - ink_h) / 2 - nt * font_size
|
|
427
|
+
if _ICON_Y_BIAS:
|
|
428
|
+
dy += canvas_size * _ICON_Y_BIAS
|
|
429
|
+
dx, dy = _snap(dx, dy)
|
|
430
|
+
draw.text((dx, dy), glyph, font=font, fill=color)
|
|
431
|
+
else:
|
|
432
|
+
# Fallback for glyphs absent from icon_metrics.json: measure with
|
|
433
|
+
# getbbox at render time (under-reports full-bleed ink, but always
|
|
434
|
+
# works). Regenerate the metrics with tools/generate_icon_metrics.py.
|
|
435
|
+
eff_size = max(1, canvas_size)
|
|
436
|
+
font = cls._get_icon_font(eff_size)
|
|
437
|
+
ascent, descent = font.getmetrics()
|
|
438
|
+
bbox = font.getbbox(glyph)
|
|
439
|
+
glyph_w = bbox[2] - bbox[0]
|
|
440
|
+
glyph_h = bbox[3] - bbox[1]
|
|
441
|
+
|
|
442
|
+
if glyph_w > inner or glyph_h > inner:
|
|
443
|
+
scale_factor = min(inner / max(glyph_w, 1), inner / max(glyph_h, 1)) * 0.95
|
|
444
|
+
scaled_size = max(1, int(eff_size * scale_factor))
|
|
445
|
+
font = cls._get_icon_font(scaled_size)
|
|
446
|
+
ascent, descent = font.getmetrics()
|
|
447
|
+
bbox = font.getbbox(glyph)
|
|
448
|
+
glyph_w = bbox[2] - bbox[0]
|
|
449
|
+
glyph_h = bbox[3] - bbox[1]
|
|
450
|
+
|
|
451
|
+
full_height = ascent + descent
|
|
452
|
+
dx = pad + (inner - glyph_w) // 2 - bbox[0]
|
|
453
|
+
dy = pad + (inner - full_height) // 2 + (ascent - bbox[3])
|
|
454
|
+
if _ICON_Y_BIAS:
|
|
455
|
+
dy += int(canvas_size * _ICON_Y_BIAS)
|
|
456
|
+
dx, dy = _snap(dx, dy)
|
|
457
|
+
draw.text((dx, dy), glyph, font=font, fill=color)
|
|
458
|
+
|
|
459
|
+
if oversample > 1:
|
|
460
|
+
img = img.resize((size, size), PILImage.Resampling.LANCZOS)
|
|
461
|
+
img = img.filter(ImageFilter.UnsharpMask(radius=0.6, percent=60, threshold=0))
|
|
462
|
+
|
|
463
|
+
return img
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Widget capability and TTK state mixins for bootstack widgets."""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Iterable, Any
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TtkStateMixin:
|
|
7
|
+
"""ttk-only widget helpers.
|
|
8
|
+
|
|
9
|
+
This mixin contains methods that exist on ttk widgets but not on classic Tk widgets
|
|
10
|
+
(e.g., `tk.Text`, `tk.Canvas`).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def state(self, statespec: str | Iterable[str] | None = None) -> Any:
|
|
14
|
+
"""Get or modify the ttk state of the widget.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
statespec: State specification (e.g. ("disabled",) or ("!disabled",)).
|
|
18
|
+
If None, returns the current state.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
The current state (getter) or an implementation-dependent result.
|
|
22
|
+
"""
|
|
23
|
+
return super().state(statespec) # type: ignore[misc]
|
|
24
|
+
|
|
25
|
+
def instate(self, statespec: str | Iterable[str], callback: Any = None) -> bool:
|
|
26
|
+
"""Test the ttk state of the widget.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
statespec: State specification to test.
|
|
30
|
+
callback: Optional callable invoked if the test succeeds.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
True if the widget matches the state spec; otherwise False.
|
|
34
|
+
"""
|
|
35
|
+
return super().instate(statespec, callback) # type: ignore[misc]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Mapping, Any
|
|
4
|
+
|
|
5
|
+
from bootstack._core.capabilities.after import AfterMixin
|
|
6
|
+
from bootstack._core.capabilities.bind import BindingsMixin
|
|
7
|
+
from bootstack._core.capabilities.bindtags import BindtagsMixin
|
|
8
|
+
from bootstack._core.capabilities.clipboard import ClipboardMixin
|
|
9
|
+
from bootstack._core.capabilities.focus import FocusMixin
|
|
10
|
+
from bootstack._core.capabilities.grab import GrabMixin
|
|
11
|
+
from bootstack._core.capabilities.grid import GridMixin
|
|
12
|
+
from bootstack._core.capabilities.pack import PackMixin
|
|
13
|
+
from bootstack._core.capabilities.place import PlaceMixin
|
|
14
|
+
from bootstack._core.capabilities.selection import SelectionMixin
|
|
15
|
+
from bootstack._core.capabilities.winfo import WinfoMixin
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class WidgetCapabilitiesMixin(
|
|
19
|
+
AfterMixin,
|
|
20
|
+
BindingsMixin,
|
|
21
|
+
BindtagsMixin,
|
|
22
|
+
ClipboardMixin,
|
|
23
|
+
FocusMixin,
|
|
24
|
+
GrabMixin,
|
|
25
|
+
GridMixin,
|
|
26
|
+
PackMixin,
|
|
27
|
+
PlaceMixin,
|
|
28
|
+
SelectionMixin,
|
|
29
|
+
WinfoMixin,
|
|
30
|
+
):
|
|
31
|
+
"""Common widget API surface (tk + ttk).
|
|
32
|
+
|
|
33
|
+
This mixin aggregates documented capability mixins into a single interface
|
|
34
|
+
suitable for both Tk and ttk widgets, plus a small set of commonly used
|
|
35
|
+
widget operations (configure/cget/destroy, stacking order, etc.).
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# -------------------------------------------------------------------------
|
|
39
|
+
# Lifecycle
|
|
40
|
+
# -------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
def destroy(self) -> None:
|
|
43
|
+
"""Destroy this widget and release its Tk resources."""
|
|
44
|
+
return super().destroy() # type: ignore[misc]
|
|
45
|
+
|
|
46
|
+
# -------------------------------------------------------------------------
|
|
47
|
+
# Configuration
|
|
48
|
+
# -------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
def configure(self, cnf: Mapping[str, Any] | None = None, **kw: Any) -> Any:
|
|
51
|
+
"""Configure widget options.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
cnf: Optional mapping of option values.
|
|
55
|
+
**kw: Option values as keyword arguments.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Tk returns configuration details when called with no args; otherwise
|
|
59
|
+
the return value is implementation-dependent.
|
|
60
|
+
"""
|
|
61
|
+
if cnf is None:
|
|
62
|
+
return super().configure(**kw) # type: ignore[misc]
|
|
63
|
+
return super().configure(cnf, **kw) # type: ignore[misc]
|
|
64
|
+
|
|
65
|
+
config = configure
|
|
66
|
+
|
|
67
|
+
def cget(self, key: str) -> Any:
|
|
68
|
+
"""Return the current value for an option.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
key: Option name (with or without a leading dash).
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The option value.
|
|
75
|
+
"""
|
|
76
|
+
return super().cget(key) # type: ignore[misc]
|
|
77
|
+
|
|
78
|
+
# -------------------------------------------------------------------------
|
|
79
|
+
# Stacking order
|
|
80
|
+
# -------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
def lift(self, aboveThis: Any | None = None) -> None:
|
|
83
|
+
"""Raise this widget above its siblings.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
aboveThis: Optional sibling widget to raise above.
|
|
87
|
+
"""
|
|
88
|
+
return super().lift(aboveThis) # type: ignore[misc]
|
|
89
|
+
|
|
90
|
+
tkraise = lift
|
|
91
|
+
|
|
92
|
+
def lower(self, belowThis: Any | None = None) -> None:
|
|
93
|
+
"""Lower this widget below its siblings.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
belowThis: Optional sibling widget to lower below.
|
|
97
|
+
"""
|
|
98
|
+
return super().lower(belowThis) # type: ignore[misc]
|
|
99
|
+
|
|
100
|
+
# -------------------------------------------------------------------------
|
|
101
|
+
# Layout convenience
|
|
102
|
+
# -------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def forget(self) -> None:
|
|
105
|
+
"""Remove the widget from layout (best-effort).
|
|
106
|
+
|
|
107
|
+
This calls the appropriate forget method depending on the active geometry manager:
|
|
108
|
+
|
|
109
|
+
- grid -> grid_forget()
|
|
110
|
+
- pack -> pack_forget()
|
|
111
|
+
- place -> place_forget()
|
|
112
|
+
"""
|
|
113
|
+
try:
|
|
114
|
+
mgr = self.winfo_manager() # type: ignore[attr-defined]
|
|
115
|
+
except Exception:
|
|
116
|
+
mgr = ""
|
|
117
|
+
|
|
118
|
+
if mgr == "grid":
|
|
119
|
+
try:
|
|
120
|
+
self.grid_forget() # type: ignore[attr-defined]
|
|
121
|
+
except Exception:
|
|
122
|
+
pass
|
|
123
|
+
elif mgr == "pack":
|
|
124
|
+
try:
|
|
125
|
+
self.pack_forget() # type: ignore[attr-defined]
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
elif mgr == "place":
|
|
129
|
+
try:
|
|
130
|
+
self.place_forget() # type: ignore[attr-defined]
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
bootstack/_core/paths.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Platform configuration paths for bootstack apps.
|
|
2
|
+
|
|
3
|
+
A single home for the per-platform config directory convention so window-state
|
|
4
|
+
persistence, `Store`, and anything else that writes user state all agree on
|
|
5
|
+
where files live.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
__all__ = ["user_config_dir", "app_config_dir", "app_config_file"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def user_config_dir() -> Path:
|
|
18
|
+
"""Return the OS user-configuration base directory.
|
|
19
|
+
|
|
20
|
+
`~/Library/Application Support` on macOS, `%APPDATA%` on Windows, and
|
|
21
|
+
`$XDG_CONFIG_HOME` (falling back to `~/.config`) elsewhere.
|
|
22
|
+
"""
|
|
23
|
+
if sys.platform == "darwin":
|
|
24
|
+
return Path.home() / "Library" / "Application Support"
|
|
25
|
+
if sys.platform == "win32":
|
|
26
|
+
return Path(os.environ.get("APPDATA") or (Path.home() / "AppData" / "Roaming"))
|
|
27
|
+
return Path(os.environ.get("XDG_CONFIG_HOME") or (Path.home() / ".config"))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def app_config_dir(app_name: str | None = None) -> Path:
|
|
31
|
+
"""Return the per-app config directory under `user_config_dir()`.
|
|
32
|
+
|
|
33
|
+
The leaf is `app_name` (or `'bootstack'` when not given) so multiple
|
|
34
|
+
bootstack apps on one machine do not collide.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
app_name: The application name, used as the directory leaf.
|
|
38
|
+
"""
|
|
39
|
+
return user_config_dir() / (app_name or "bootstack")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def app_config_file(filename: str, app_name: str | None = None) -> Path:
|
|
43
|
+
"""Return the path to a named file inside the per-app config directory.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
filename: The leaf file name (for example `'settings.json'`).
|
|
47
|
+
app_name: The application name, used as the directory leaf.
|
|
48
|
+
"""
|
|
49
|
+
return app_config_dir(app_name) / filename
|