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,930 @@
|
|
|
1
|
+
"""Application runtime — App window, lifecycle helpers, and settings.
|
|
2
|
+
|
|
3
|
+
Provides the main `App` class (a `Tk` subclass), `AppSettings`, and
|
|
4
|
+
process-wide helpers for accessing the active app instance, reading settings,
|
|
5
|
+
and managing the current locale and theme from anywhere in the application.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
import tkinter
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Callable, Literal, Optional, Sequence, TypedDict, Union
|
|
13
|
+
|
|
14
|
+
from babel.core import UnknownLocaleError
|
|
15
|
+
from babel.dates import get_date_format, get_time_format
|
|
16
|
+
from babel.numbers import get_decimal_symbol, get_group_symbol
|
|
17
|
+
from typing_extensions import Unpack
|
|
18
|
+
|
|
19
|
+
from bootstack.constants import *
|
|
20
|
+
from bootstack.i18n.intl_format import detect_locale
|
|
21
|
+
from bootstack.i18n.msgcat import MessageCatalog
|
|
22
|
+
from bootstack._core.publisher import Publisher
|
|
23
|
+
from bootstack._core.mixins.widget import WidgetCapabilitiesMixin
|
|
24
|
+
from bootstack._runtime.base_window import BaseWindow
|
|
25
|
+
from bootstack._runtime.utility import enable_high_dpi_awareness
|
|
26
|
+
|
|
27
|
+
_current_app: App | None = None
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def set_current_app(app: App) -> None:
|
|
31
|
+
"""Set the process-wide current App instance.
|
|
32
|
+
|
|
33
|
+
Intended to be called from App.__init__ for the first app created.
|
|
34
|
+
"""
|
|
35
|
+
global _current_app
|
|
36
|
+
_current_app = app
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_app_settings() -> AppSettings:
|
|
40
|
+
"""Return the settings for current App.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
The AppSettings instance for the current application.
|
|
44
|
+
|
|
45
|
+
Raises:
|
|
46
|
+
RuntimeError: If no active App instance is set.
|
|
47
|
+
"""
|
|
48
|
+
return get_current_app().settings
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def clear_current_app(app: App) -> None:
|
|
52
|
+
"""Clear the current app reference if it matches the given app."""
|
|
53
|
+
global _current_app
|
|
54
|
+
if _current_app is app:
|
|
55
|
+
_current_app = None
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def get_current_app() -> App:
|
|
59
|
+
"""Return the current App instance.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
The currently active App instance.
|
|
63
|
+
|
|
64
|
+
Raises:
|
|
65
|
+
RuntimeError: If no App has been registered yet.
|
|
66
|
+
"""
|
|
67
|
+
if _current_app is None:
|
|
68
|
+
raise RuntimeError(
|
|
69
|
+
"No current App instance is set. "
|
|
70
|
+
"Create an App first, e.g. `app = App()`."
|
|
71
|
+
)
|
|
72
|
+
return _current_app
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def has_current_app() -> bool:
|
|
76
|
+
"""Check if a current App instance is registered.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
True if an App instance exists, False otherwise.
|
|
80
|
+
"""
|
|
81
|
+
return _current_app is not None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_default_root(what: Optional[str] = None) -> tkinter.Tk:
|
|
85
|
+
"""Get the default Tk root window.
|
|
86
|
+
|
|
87
|
+
Returns the default root if it has been created, otherwise
|
|
88
|
+
creates and returns a new instance.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
what: Optional description of the operation requiring the root,
|
|
92
|
+
used in error messages if called too early.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The default Tk root window instance.
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
RuntimeError: If tkinter is configured to not support default root,
|
|
99
|
+
or if called too early with a 'what' description.
|
|
100
|
+
"""
|
|
101
|
+
if not tkinter._support_default_root:
|
|
102
|
+
raise RuntimeError(
|
|
103
|
+
"No master specified and tkinter is "
|
|
104
|
+
"configured to not support default root")
|
|
105
|
+
if not tkinter._default_root:
|
|
106
|
+
if what:
|
|
107
|
+
raise RuntimeError(f"Too early to {what}: no default root window")
|
|
108
|
+
root = tkinter.Tk()
|
|
109
|
+
assert tkinter._default_root is root
|
|
110
|
+
return tkinter._default_root
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def apply_class_bindings(window: tkinter.Widget | App) -> None:
|
|
114
|
+
"""Add class level event bindings in application"""
|
|
115
|
+
# Copy TCheckbutton bindings to Toolbutton class
|
|
116
|
+
# This is needed because widgets using class_='Toolbutton' have their
|
|
117
|
+
# bindtags reference 'Toolbutton' instead of 'TCheckbutton', so they
|
|
118
|
+
# need the same mouse/keyboard bindings copied over.
|
|
119
|
+
for event in ('<Button-1>', '<ButtonRelease-1>', '<B1-Leave>', '<B1-Enter>',
|
|
120
|
+
'<Enter>', '<Leave>', '<Key-space>', '<<Invoke>>'):
|
|
121
|
+
binding = window.bind_class('TCheckbutton', event)
|
|
122
|
+
if binding:
|
|
123
|
+
window.bind_class('Toolbutton', event, binding)
|
|
124
|
+
window.bind_class('ButtonGroup', event, binding)
|
|
125
|
+
|
|
126
|
+
for className in ["TEntry", "TSpinbox", "TCombobox", "Text"]:
|
|
127
|
+
window.bind_class(
|
|
128
|
+
className=className,
|
|
129
|
+
sequence="<Configure>",
|
|
130
|
+
func=on_disabled_readonly_state,
|
|
131
|
+
add="+")
|
|
132
|
+
|
|
133
|
+
for sequence in ["<Control-a>", "<Control-A>"]:
|
|
134
|
+
window.bind_class(
|
|
135
|
+
className=className,
|
|
136
|
+
sequence=sequence,
|
|
137
|
+
func=on_select_all)
|
|
138
|
+
|
|
139
|
+
window.unbind_class("TButton", "<Key-space>")
|
|
140
|
+
|
|
141
|
+
def button_default_binding(event: tkinter.Event) -> None:
|
|
142
|
+
"""The default keybind on a button when the return or enter key
|
|
143
|
+
is pressed and the button has focus or is the default button."""
|
|
144
|
+
try:
|
|
145
|
+
widget = window.nametowidget(event.widget)
|
|
146
|
+
widget.invoke()
|
|
147
|
+
except KeyError:
|
|
148
|
+
window.tk.call(event.widget, 'invoke')
|
|
149
|
+
|
|
150
|
+
window.bind_class(
|
|
151
|
+
"TButton", "<Key-Return>", button_default_binding,
|
|
152
|
+
add="+")
|
|
153
|
+
window.bind_class("TButton", "<KP_Enter>", button_default_binding, add="+")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def apply_all_bindings(window: tkinter.Widget | App) -> None:
|
|
157
|
+
"""Add bindings to all widgets in the application"""
|
|
158
|
+
window.bind_all('<Map>', on_map_child, '+')
|
|
159
|
+
window.bind_all('<Destroy>', lambda e: Publisher.unsubscribe(e.widget))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def on_disabled_readonly_state(event: tkinter.Event) -> None:
|
|
163
|
+
"""Change the cursor of entry type widgets to 'arrow' if in a
|
|
164
|
+
disabled or readonly state."""
|
|
165
|
+
try:
|
|
166
|
+
widget = event.widget
|
|
167
|
+
state = str(widget.cget('state'))
|
|
168
|
+
cursor = str(widget.cget('cursor'))
|
|
169
|
+
if state in (DISABLED, READONLY):
|
|
170
|
+
if cursor == 'arrow':
|
|
171
|
+
return
|
|
172
|
+
else:
|
|
173
|
+
widget['cursor'] = 'arrow'
|
|
174
|
+
else:
|
|
175
|
+
if cursor in ('ibeam', ''):
|
|
176
|
+
return
|
|
177
|
+
else:
|
|
178
|
+
widget['cursor'] = None
|
|
179
|
+
except:
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def on_map_child(event: tkinter.Event) -> None:
|
|
184
|
+
"""Callback for <Map> event which generates a <<MapChild>> virtual
|
|
185
|
+
event on the parent"""
|
|
186
|
+
widget: tkinter.Widget = event.widget
|
|
187
|
+
try:
|
|
188
|
+
if widget.master is None: # root widget
|
|
189
|
+
return
|
|
190
|
+
else:
|
|
191
|
+
widget.master.event_generate('<<MapChild>>')
|
|
192
|
+
except:
|
|
193
|
+
# not a tkinter widget that I'm handling (ex. Combobox.popdown)
|
|
194
|
+
return
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def on_select_all(event: tkinter.Event) -> None:
|
|
198
|
+
"""Callback to select all text in Entry or Text widget when Ctrl+A is pressed."""
|
|
199
|
+
widget = event.widget
|
|
200
|
+
|
|
201
|
+
if isinstance(widget, tkinter.Text):
|
|
202
|
+
widget.tag_add(SEL, "1.0", END)
|
|
203
|
+
widget.mark_set(INSERT, END)
|
|
204
|
+
widget.see(INSERT)
|
|
205
|
+
elif isinstance(widget, tkinter.Entry):
|
|
206
|
+
widget.selection_range(0, END)
|
|
207
|
+
widget.icursor(END)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
LocalizeMode = Union[bool, Literal['auto']]
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@dataclass
|
|
214
|
+
class AppSettings:
|
|
215
|
+
"""Internal resolved-configuration holder for an `App`.
|
|
216
|
+
|
|
217
|
+
This is an INTERNAL detail — it is not part of the public API. Users
|
|
218
|
+
configure an app through flat `App(...)` constructor kwargs and read or
|
|
219
|
+
write configuration through `app.*` properties; `App.__init__` assembles
|
|
220
|
+
those kwargs into one of these instances. It holds configuration for
|
|
221
|
+
theming, localization, and application metadata, and is automatically
|
|
222
|
+
populated with sensible locale-based defaults in `__post_init__`.
|
|
223
|
+
|
|
224
|
+
Attributes:
|
|
225
|
+
app_name: The application name displayed in the title bar.
|
|
226
|
+
theme: The current theme name ('light', 'dark', or a specific theme).
|
|
227
|
+
light_theme: The theme to use when `theme='light'`.
|
|
228
|
+
dark_theme: The theme to use when `theme='dark'`.
|
|
229
|
+
follow_system_appearance: If True, automatically switch between
|
|
230
|
+
`light_theme` and `dark_theme` to match the OS appearance and
|
|
231
|
+
track changes at runtime. Currently effective on macOS, where
|
|
232
|
+
Tk fires `<<TkSystemAppearanceChanged>>` and exposes the
|
|
233
|
+
current mode via `tk::unsupported::MacWindowStyle isDark`.
|
|
234
|
+
Defaults to False so existing apps that pin a theme keep
|
|
235
|
+
doing so.
|
|
236
|
+
available_themes: Sequence of available theme names.
|
|
237
|
+
locale: The locale identifier (e.g., 'en_US', 'de_DE'). Auto-detected
|
|
238
|
+
from system if not specified.
|
|
239
|
+
language: The base language code (e.g., 'en', 'de'). Derived from
|
|
240
|
+
locale if not specified.
|
|
241
|
+
date_format: The date format pattern. Derived from locale if not
|
|
242
|
+
specified (e.g., 'M/d/yy' for en_US).
|
|
243
|
+
time_format: The time format pattern. Derived from locale if not
|
|
244
|
+
specified (e.g., 'h:mm a' for en_US).
|
|
245
|
+
number_decimal: The decimal separator character. Derived from locale
|
|
246
|
+
if not specified (e.g., '.' for en_US).
|
|
247
|
+
number_thousands: The thousands separator character. Derived from
|
|
248
|
+
locale if not specified (e.g., ',' for en_US).
|
|
249
|
+
localize_mode: Controls localization behavior. 'auto' enables
|
|
250
|
+
localization based on locale, True always enables, False disables.
|
|
251
|
+
window_style: Windows-only pywinstyles effect for all windows.
|
|
252
|
+
Options include 'mica', 'acrylic', 'aero', 'transparent', 'win7'.
|
|
253
|
+
Defaults to 'mica'. Set to None to disable.
|
|
254
|
+
macos_quit_behavior: How the close button and Cmd+Q behave on macOS.
|
|
255
|
+
'native' (default) follows Mac convention: clicking the window
|
|
256
|
+
close button or Cmd+H hides the app (withdraws), clicking the
|
|
257
|
+
dock icon reshows it, and Cmd+Q (or Dock → Quit) actually
|
|
258
|
+
destroys it. 'classic' restores the cross-platform behavior
|
|
259
|
+
where the close button destroys the window. No-op on Win/Linux.
|
|
260
|
+
remember_window_state: If True, the App's geometry (size + position)
|
|
261
|
+
is saved on close and restored on next launch, with off-screen
|
|
262
|
+
positions clamped back into a visible monitor. Off by default
|
|
263
|
+
so existing apps that pin a size/position keep doing so.
|
|
264
|
+
state_path: Optional override for where window state is stored.
|
|
265
|
+
When None, defaults to a per-app file under the OS config
|
|
266
|
+
directory (Library/Application Support on macOS, %APPDATA% on
|
|
267
|
+
Windows, $XDG_CONFIG_HOME on Linux). The leaf filename includes
|
|
268
|
+
`app_name` so multiple bootstack apps don't collide.
|
|
269
|
+
"""
|
|
270
|
+
# information
|
|
271
|
+
app_name: str | None = None
|
|
272
|
+
|
|
273
|
+
# theme
|
|
274
|
+
theme: str = "light"
|
|
275
|
+
light_theme: str = "bootstrap-light"
|
|
276
|
+
dark_theme: str = "bootstrap-dark"
|
|
277
|
+
follow_system_appearance: bool = False
|
|
278
|
+
available_themes: Sequence[str] = ()
|
|
279
|
+
|
|
280
|
+
# internationalization
|
|
281
|
+
locale: str | None = None
|
|
282
|
+
language: str | None = None
|
|
283
|
+
date_format: str | None = None
|
|
284
|
+
time_format: str | None = None
|
|
285
|
+
number_decimal: str | None = None
|
|
286
|
+
number_thousands: str | None = None
|
|
287
|
+
|
|
288
|
+
# localization behavior
|
|
289
|
+
localize_mode: LocalizeMode = "auto"
|
|
290
|
+
|
|
291
|
+
# platform-specific
|
|
292
|
+
window_style: str | None = 'mica'
|
|
293
|
+
macos_quit_behavior: str = 'native'
|
|
294
|
+
|
|
295
|
+
# window state persistence
|
|
296
|
+
remember_window_state: bool = False
|
|
297
|
+
state_path: str | None = None
|
|
298
|
+
|
|
299
|
+
def __post_init__(self):
|
|
300
|
+
"""Populate localization defaults when not explicitly configured."""
|
|
301
|
+
_apply_localization_defaults(self)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
DEFAULT_LOCALE = "en_US"
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _apply_localization_defaults(settings: AppSettings) -> None:
|
|
308
|
+
"""Ensure locale-based fields always have meaningful defaults."""
|
|
309
|
+
locale_code = settings.locale or detect_locale(DEFAULT_LOCALE)
|
|
310
|
+
settings.locale = locale_code
|
|
311
|
+
|
|
312
|
+
if settings.language is None:
|
|
313
|
+
settings.language = _language_from_locale(locale_code)
|
|
314
|
+
|
|
315
|
+
if settings.date_format is None:
|
|
316
|
+
settings.date_format = _safe_date_format(locale_code)
|
|
317
|
+
|
|
318
|
+
if settings.time_format is None:
|
|
319
|
+
settings.time_format = _safe_time_format(locale_code)
|
|
320
|
+
|
|
321
|
+
if settings.number_decimal is None:
|
|
322
|
+
settings.number_decimal = _safe_decimal_symbol(locale_code)
|
|
323
|
+
|
|
324
|
+
if settings.number_thousands is None:
|
|
325
|
+
settings.number_thousands = _safe_group_symbol(locale_code)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _language_from_locale(locale_code: str) -> str:
|
|
329
|
+
"""Return the base language (e.g., en_US -> en)."""
|
|
330
|
+
base = locale_code.split("_", 1)[0]
|
|
331
|
+
return base.lower() if base else locale_code
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _safe_date_format(locale_code: str) -> str:
|
|
335
|
+
try:
|
|
336
|
+
return str(get_date_format("short", locale=locale_code))
|
|
337
|
+
except (UnknownLocaleError, ValueError):
|
|
338
|
+
return str(get_date_format("short", locale=DEFAULT_LOCALE))
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _safe_time_format(locale_code: str) -> str:
|
|
342
|
+
try:
|
|
343
|
+
return str(get_time_format("short", locale=locale_code))
|
|
344
|
+
except (UnknownLocaleError, ValueError):
|
|
345
|
+
return str(get_time_format("short", locale=DEFAULT_LOCALE))
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def _safe_decimal_symbol(locale_code: str) -> str:
|
|
349
|
+
try:
|
|
350
|
+
return get_decimal_symbol(locale_code)
|
|
351
|
+
except (UnknownLocaleError, ValueError):
|
|
352
|
+
return get_decimal_symbol(DEFAULT_LOCALE)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _safe_group_symbol(locale_code: str) -> str:
|
|
356
|
+
try:
|
|
357
|
+
return get_group_symbol(locale_code)
|
|
358
|
+
except (UnknownLocaleError, ValueError):
|
|
359
|
+
return get_group_symbol(DEFAULT_LOCALE)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
class TkKwargs(TypedDict, total=False):
|
|
363
|
+
"""The following attributes are available per the Tkinter API (not commonly used).
|
|
364
|
+
|
|
365
|
+
Attributes:
|
|
366
|
+
screenName: Sets the display environment variable (X11 only).
|
|
367
|
+
baseName: Name of the profile file. By default, derived from program name.
|
|
368
|
+
className: Name of the widget class
|
|
369
|
+
useTk: If True, initializes the Tk system.
|
|
370
|
+
sync: If true, executes all X server commands synchronously.
|
|
371
|
+
use: The id of the window in which to embed the application.
|
|
372
|
+
"""
|
|
373
|
+
screenName: str
|
|
374
|
+
baseName: str
|
|
375
|
+
className: str
|
|
376
|
+
useTk: bool
|
|
377
|
+
sync: bool
|
|
378
|
+
use: str
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
class App(BaseWindow, WidgetCapabilitiesMixin, tkinter.Tk):
|
|
382
|
+
"""The primary application window and entry point.
|
|
383
|
+
|
|
384
|
+
App adds theming, localization, and platform setup on top of `tkinter.Tk`.
|
|
385
|
+
|
|
386
|
+
The standard widget API (events, scheduling, clipboard, geometry managers,
|
|
387
|
+
winfo, etc.) is documented under bootstack capabilities and is available
|
|
388
|
+
on App via inheritance.
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
def __init__(
|
|
392
|
+
self,
|
|
393
|
+
title: str | None = None,
|
|
394
|
+
*,
|
|
395
|
+
theme: str | None = None,
|
|
396
|
+
icon: tkinter.PhotoImage | str | None = None,
|
|
397
|
+
|
|
398
|
+
# theme
|
|
399
|
+
light_theme: str = "bootstrap-light",
|
|
400
|
+
dark_theme: str = "bootstrap-dark",
|
|
401
|
+
follow_system_appearance: bool = False,
|
|
402
|
+
available_themes: Sequence[str] = (),
|
|
403
|
+
|
|
404
|
+
# localization
|
|
405
|
+
locale: str | None = None,
|
|
406
|
+
localize_mode: LocalizeMode = "auto",
|
|
407
|
+
|
|
408
|
+
# platform
|
|
409
|
+
window_style: str | None = "mica",
|
|
410
|
+
macos_quit_behavior: str = "native",
|
|
411
|
+
|
|
412
|
+
# window-state persistence
|
|
413
|
+
remember_window_state: bool = False,
|
|
414
|
+
state_path: str | None = None,
|
|
415
|
+
|
|
416
|
+
# window settings
|
|
417
|
+
size: tuple[int, int] | None = None,
|
|
418
|
+
position: tuple[int, int] | None = None,
|
|
419
|
+
minsize: tuple[int, int] | None = None,
|
|
420
|
+
maxsize: tuple[int, int] | None = None,
|
|
421
|
+
resizable: tuple[bool, bool] | None = None,
|
|
422
|
+
scaling: float | None = None,
|
|
423
|
+
hdpi: bool = True,
|
|
424
|
+
alpha: float = 1.0,
|
|
425
|
+
transient: object | None = None,
|
|
426
|
+
override_redirect: bool = False,
|
|
427
|
+
center_on_screen: bool = True,
|
|
428
|
+
on_close: Callable | None = None,
|
|
429
|
+
**kwargs: Unpack[TkKwargs],
|
|
430
|
+
) -> None:
|
|
431
|
+
"""Initializes the application window.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
title: The text to display in the window's title bar, and the
|
|
435
|
+
application name used for config-directory and taskbar
|
|
436
|
+
identity. Defaults to `'bootstack'`.
|
|
437
|
+
theme: The name of the theme to use on startup (e.g.
|
|
438
|
+
`'bootstrap-dark'`). Defaults to the built-in light theme.
|
|
439
|
+
icon: A PhotoImage or file path used for the window's icon.
|
|
440
|
+
If None, the default bootstack.png icon is used.
|
|
441
|
+
light_theme: Theme used when following system appearance and the
|
|
442
|
+
OS is in light mode, and as the light end of `toggle_theme`.
|
|
443
|
+
dark_theme: Theme used when following system appearance and the
|
|
444
|
+
OS is in dark mode, and as the dark end of `toggle_theme`.
|
|
445
|
+
follow_system_appearance: If True, switch between `light_theme`
|
|
446
|
+
and `dark_theme` to match the OS and track changes at runtime
|
|
447
|
+
(currently effective on macOS).
|
|
448
|
+
available_themes: Sequence of theme names to expose to theme
|
|
449
|
+
pickers. Empty means all registered themes.
|
|
450
|
+
locale: Locale identifier (e.g. `'en_US'`, `'de_DE'`).
|
|
451
|
+
Auto-detected from the system when not given.
|
|
452
|
+
localize_mode: Controls localization behavior. `'auto'` enables
|
|
453
|
+
localization based on locale, `True` always enables, `False`
|
|
454
|
+
disables.
|
|
455
|
+
window_style: Windows-only effect for all windows (`'mica'`,
|
|
456
|
+
`'acrylic'`, `'aero'`, `'transparent'`, `'win7'`). Defaults
|
|
457
|
+
to `'mica'`; set to None to disable.
|
|
458
|
+
macos_quit_behavior: How close/Cmd+Q behave on macOS, `'native'`
|
|
459
|
+
(default) or `'classic'`. No-op on Win/Linux.
|
|
460
|
+
remember_window_state: If True, the window geometry is saved on
|
|
461
|
+
close and restored on next launch.
|
|
462
|
+
state_path: Optional override for where window state is stored.
|
|
463
|
+
size: A tuple specifying the window's initial width and height.
|
|
464
|
+
position: A tuple specifying the window's initial x and y
|
|
465
|
+
coordinates on the screen.
|
|
466
|
+
minsize: A tuple specifying the window's minimum width and height.
|
|
467
|
+
maxsize: A tuple specifying the window's maximum width and height.
|
|
468
|
+
resizable: A tuple of booleans specifying whether the window can
|
|
469
|
+
be resized horizontally and vertically.
|
|
470
|
+
scaling: The DPI scaling factor for the window. If `None`,
|
|
471
|
+
automatic scaling is used.
|
|
472
|
+
hdpi: If `True`, enables high-DPI awareness for the application.
|
|
473
|
+
alpha: The window's transparency level, from 0.0 (fully
|
|
474
|
+
transparent) to 1.0 (fully opaque).
|
|
475
|
+
transient: The parent window for this window.
|
|
476
|
+
override_redirect: If `True`, creates a window without standard
|
|
477
|
+
decorations (title bar, borders, etc.).
|
|
478
|
+
center_on_screen: If True (default), center the app window on
|
|
479
|
+
the screen when `mainloop()` starts.
|
|
480
|
+
on_close: Callback invoked when the user clicks the close button.
|
|
481
|
+
Return `False` to veto the close; return `None` or `True`
|
|
482
|
+
to allow it. Equivalent to calling `add_close_handler(fn)`
|
|
483
|
+
after construction.
|
|
484
|
+
**kwargs: Additional keyword arguments to pass to the
|
|
485
|
+
underlying `tkinter.Tk` constructor.
|
|
486
|
+
"""
|
|
487
|
+
# --- Settings ---------------------------------------------------
|
|
488
|
+
# Flat constructor kwargs are the single configuration path; they are
|
|
489
|
+
# assembled into an internal AppSettings holder (which derives locale
|
|
490
|
+
# formats in __post_init__). There is no public AppSettings / settings=.
|
|
491
|
+
self.settings = AppSettings(
|
|
492
|
+
app_name=title,
|
|
493
|
+
theme=theme if theme is not None else "light",
|
|
494
|
+
light_theme=light_theme,
|
|
495
|
+
dark_theme=dark_theme,
|
|
496
|
+
follow_system_appearance=follow_system_appearance,
|
|
497
|
+
available_themes=available_themes,
|
|
498
|
+
locale=locale,
|
|
499
|
+
localize_mode=localize_mode,
|
|
500
|
+
window_style=window_style,
|
|
501
|
+
macos_quit_behavior=macos_quit_behavior,
|
|
502
|
+
remember_window_state=remember_window_state,
|
|
503
|
+
state_path=state_path,
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
# If no title/app_name was given, fall back to a sensible default.
|
|
507
|
+
if self.settings.app_name is None:
|
|
508
|
+
self.settings.app_name = "bootstack"
|
|
509
|
+
|
|
510
|
+
# --- Window options ---------------------------------------------
|
|
511
|
+
self._size = size
|
|
512
|
+
self._position = position
|
|
513
|
+
self._minsize = minsize
|
|
514
|
+
self._maxsize = maxsize
|
|
515
|
+
self._resizable = resizable
|
|
516
|
+
self._scaling = scaling
|
|
517
|
+
self._hdpi = hdpi
|
|
518
|
+
self._transient = transient
|
|
519
|
+
self._alpha = alpha
|
|
520
|
+
self._override_redirect = override_redirect
|
|
521
|
+
self._center_on_screen = center_on_screen
|
|
522
|
+
self._on_close_kwarg = on_close
|
|
523
|
+
|
|
524
|
+
# Register app
|
|
525
|
+
if not has_current_app():
|
|
526
|
+
set_current_app(self)
|
|
527
|
+
|
|
528
|
+
# Enable HDPI before creating window
|
|
529
|
+
if self._hdpi:
|
|
530
|
+
enable_high_dpi_awareness()
|
|
531
|
+
|
|
532
|
+
# Tell Windows this is a distinct app so the taskbar uses our icon
|
|
533
|
+
# instead of the generic python.exe icon.
|
|
534
|
+
if sys.platform == 'win32':
|
|
535
|
+
try:
|
|
536
|
+
import ctypes
|
|
537
|
+
app_id = f"bootstack.{(self.settings.app_name or 'app').lower().replace(' ', '.')}"
|
|
538
|
+
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(app_id)
|
|
539
|
+
except Exception:
|
|
540
|
+
pass
|
|
541
|
+
|
|
542
|
+
# Initialize Tk
|
|
543
|
+
tkinter.Tk.__init__(self, **kwargs)
|
|
544
|
+
self.withdraw() # hide immediately until ready to show.
|
|
545
|
+
|
|
546
|
+
# Setup window system info
|
|
547
|
+
self.winsys: str = self.tk.call('tk', 'windowingsystem')
|
|
548
|
+
|
|
549
|
+
# Apply theme (use resolved settings.theme). If the app opts into
|
|
550
|
+
# following system appearance, override the explicit theme with the
|
|
551
|
+
# mode-appropriate one and bind a listener to track future toggles.
|
|
552
|
+
from bootstack.style.style import set_theme
|
|
553
|
+
initial_theme = self.settings.theme
|
|
554
|
+
if self.settings.follow_system_appearance and self._is_dark_capable_platform():
|
|
555
|
+
initial_theme = (
|
|
556
|
+
self.settings.dark_theme
|
|
557
|
+
if self._system_is_dark()
|
|
558
|
+
else self.settings.light_theme
|
|
559
|
+
)
|
|
560
|
+
set_theme(initial_theme)
|
|
561
|
+
|
|
562
|
+
if self.settings.follow_system_appearance and self._is_dark_capable_platform():
|
|
563
|
+
self._bind_system_appearance_tracking()
|
|
564
|
+
|
|
565
|
+
# Install macOS-native close/quit/hide handlers when requested.
|
|
566
|
+
if self.winsys == 'aqua' and self.settings.macos_quit_behavior == 'native':
|
|
567
|
+
self._install_macos_quit_handlers()
|
|
568
|
+
|
|
569
|
+
# macOS-only polish: sync tk appname so the apple menu's first
|
|
570
|
+
# entry shows the app name (otherwise Tk uses the interpreter's
|
|
571
|
+
# name, typically "Python"), and bind Cmd+W to fire the standard
|
|
572
|
+
# WM_DELETE_WINDOW protocol so the close shortcut behaves like a
|
|
573
|
+
# close-button click.
|
|
574
|
+
if self.winsys == 'aqua':
|
|
575
|
+
if self.settings.app_name:
|
|
576
|
+
try:
|
|
577
|
+
self.tk.call('tk', 'appname', self.settings.app_name)
|
|
578
|
+
except tkinter.TclError:
|
|
579
|
+
pass
|
|
580
|
+
self.bind('<Command-w>', self._trigger_close, add='+')
|
|
581
|
+
|
|
582
|
+
# Initialize the localization bridge so MessageCatalog.translate()
|
|
583
|
+
# and <<LocaleChanged>> are available throughout the app.
|
|
584
|
+
MessageCatalog.init(
|
|
585
|
+
locales_dir=None,
|
|
586
|
+
domain="bootstack",
|
|
587
|
+
default_locale=self.settings.locale or DEFAULT_LOCALE,
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Apply HDPI scaling after window creation
|
|
591
|
+
if self._hdpi:
|
|
592
|
+
if self._scaling is None:
|
|
593
|
+
enable_high_dpi_awareness(self, 'auto')
|
|
594
|
+
else:
|
|
595
|
+
enable_high_dpi_awareness(self, self._scaling)
|
|
596
|
+
|
|
597
|
+
# Setup icon
|
|
598
|
+
self._setup_icon(icon, default_icon_enabled=True)
|
|
599
|
+
|
|
600
|
+
# If the app opted into state restoration, override the explicit
|
|
601
|
+
# size/position with whatever was saved last time. The saved geometry
|
|
602
|
+
# is a single 'WxH+X+Y' string applied after _setup_window so it
|
|
603
|
+
# supersedes both kwargs and centering logic.
|
|
604
|
+
saved_geometry = None
|
|
605
|
+
if self.settings.remember_window_state:
|
|
606
|
+
saved_geometry = self._read_saved_geometry()
|
|
607
|
+
|
|
608
|
+
# Setup window using BaseWindow
|
|
609
|
+
self._setup_window(
|
|
610
|
+
title=self.settings.app_name,
|
|
611
|
+
size=self._size,
|
|
612
|
+
position=self._position,
|
|
613
|
+
minsize=self._minsize,
|
|
614
|
+
maxsize=self._maxsize,
|
|
615
|
+
resizable=self._resizable,
|
|
616
|
+
transient=self._transient,
|
|
617
|
+
overrideredirect=self._override_redirect,
|
|
618
|
+
alpha=self._alpha,
|
|
619
|
+
window_style=self.settings.window_style,
|
|
620
|
+
)
|
|
621
|
+
|
|
622
|
+
if saved_geometry is not None:
|
|
623
|
+
self._apply_saved_geometry(saved_geometry)
|
|
624
|
+
|
|
625
|
+
# Apply bootstack-specific bindings
|
|
626
|
+
apply_class_bindings(self)
|
|
627
|
+
apply_all_bindings(self)
|
|
628
|
+
|
|
629
|
+
# Register on_close= kwarg handler after all other setup is complete
|
|
630
|
+
if self._on_close_kwarg is not None:
|
|
631
|
+
self.add_close_handler(self._on_close_kwarg)
|
|
632
|
+
|
|
633
|
+
# A registered intro splash (bs.Splash), or None. Set by the splash on
|
|
634
|
+
# construction; read at reveal time to defer the app's own appearance.
|
|
635
|
+
self._splash: Any = None
|
|
636
|
+
|
|
637
|
+
def mainloop(self, n=0) -> None:
|
|
638
|
+
"""Start the application event loop
|
|
639
|
+
|
|
640
|
+
Args:
|
|
641
|
+
n (int): A threshold that keeps the window open if at least n windows is open. This is an archaic c-level
|
|
642
|
+
detail that should not be adjusted unless you have a specific reason.
|
|
643
|
+
"""
|
|
644
|
+
if self._center_on_screen:
|
|
645
|
+
self.place_window_center()
|
|
646
|
+
splash = getattr(self, "_splash", None)
|
|
647
|
+
if splash is not None and splash.is_showing:
|
|
648
|
+
# An intro splash is up — defer the app's reveal until it dismisses,
|
|
649
|
+
# then reveal via this callback (see Splash._notify_app_ready).
|
|
650
|
+
splash._notify_app_ready(self.show)
|
|
651
|
+
else:
|
|
652
|
+
self.show()
|
|
653
|
+
super().mainloop(n=n)
|
|
654
|
+
|
|
655
|
+
def _do_close(self) -> None:
|
|
656
|
+
"""Default close action for App.
|
|
657
|
+
|
|
658
|
+
On macOS with native quit behavior, hiding the window (withdraw) is the
|
|
659
|
+
correct action when the close button is clicked — the app stays in the
|
|
660
|
+
Dock and can be re-opened. On all other platforms (and macOS without
|
|
661
|
+
native behavior), the window is destroyed.
|
|
662
|
+
"""
|
|
663
|
+
if (
|
|
664
|
+
getattr(self, 'winsys', None) == 'aqua'
|
|
665
|
+
and getattr(self.settings, 'macos_quit_behavior', None) == 'native'
|
|
666
|
+
):
|
|
667
|
+
self.withdraw()
|
|
668
|
+
else:
|
|
669
|
+
try:
|
|
670
|
+
self.destroy()
|
|
671
|
+
except tkinter.TclError:
|
|
672
|
+
pass
|
|
673
|
+
|
|
674
|
+
def close(self) -> None:
|
|
675
|
+
"""Close the application window (destroys the Tk root)"""
|
|
676
|
+
clear_current_app(self)
|
|
677
|
+
self.quit()
|
|
678
|
+
|
|
679
|
+
def destroy(self) -> None:
|
|
680
|
+
"""Destroys the window and all its children."""
|
|
681
|
+
if self.settings.remember_window_state:
|
|
682
|
+
try:
|
|
683
|
+
if self.winfo_exists():
|
|
684
|
+
self._save_window_state()
|
|
685
|
+
except tkinter.TclError:
|
|
686
|
+
pass
|
|
687
|
+
clear_current_app(self)
|
|
688
|
+
# Drop every process-wide cache that holds a Tk object bound to THIS
|
|
689
|
+
# root's interpreter — the Style singleton, the named/derived font
|
|
690
|
+
# caches, the nine-patch PhotoImage cache, and the visual-focus root
|
|
691
|
+
# ref. This must happen *before* super().destroy(): those cached
|
|
692
|
+
# PhotoImage/TkFont objects run `image delete`/`font delete` in their
|
|
693
|
+
# finalizers, and if they outlive the interpreter that owns them the
|
|
694
|
+
# call lands on a foreign/dead interpreter — a native crash in ttk
|
|
695
|
+
# element_create on the *next* root (the C layer, so Python can't catch
|
|
696
|
+
# it). Releasing + gc.collect() while this root is still alive lets the
|
|
697
|
+
# finalizers run cleanly. Only when no other App is live (single-App is
|
|
698
|
+
# the norm; this also lets the GUI test suite run module-after-module in
|
|
699
|
+
# one process, and fixes reopen-an-app-after-close in production).
|
|
700
|
+
if not has_current_app():
|
|
701
|
+
try:
|
|
702
|
+
import gc
|
|
703
|
+
from bootstack.style.style import reset_style
|
|
704
|
+
from bootstack.style.typography import Typography
|
|
705
|
+
from bootstack._core.images import _ImageService
|
|
706
|
+
from bootstack._runtime.visual_focus import reset_visual_focus_root
|
|
707
|
+
from bootstack.signals.signal import reset_realized_signals
|
|
708
|
+
reset_style()
|
|
709
|
+
Typography.reset()
|
|
710
|
+
_ImageService.clear_cache()
|
|
711
|
+
reset_visual_focus_root()
|
|
712
|
+
reset_realized_signals()
|
|
713
|
+
gc.collect()
|
|
714
|
+
except Exception:
|
|
715
|
+
pass
|
|
716
|
+
super().destroy()
|
|
717
|
+
|
|
718
|
+
# ----- Window state persistence ------------------------------------------
|
|
719
|
+
|
|
720
|
+
def _state_file_path(self):
|
|
721
|
+
"""Return the path where this App's window state is persisted."""
|
|
722
|
+
from pathlib import Path
|
|
723
|
+
from bootstack._core.paths import app_config_file
|
|
724
|
+
if self.settings.state_path:
|
|
725
|
+
return Path(self.settings.state_path)
|
|
726
|
+
# Per-platform config dir; leaf includes app_name to avoid collisions
|
|
727
|
+
# between multiple bootstack apps installed on the same machine.
|
|
728
|
+
return app_config_file('window_state.json', self.settings.app_name)
|
|
729
|
+
|
|
730
|
+
def _read_saved_geometry(self):
|
|
731
|
+
"""Return the persisted 'WxH+X+Y' string, or None if not present/valid."""
|
|
732
|
+
import json
|
|
733
|
+
path = self._state_file_path()
|
|
734
|
+
try:
|
|
735
|
+
raw = path.read_text()
|
|
736
|
+
except (FileNotFoundError, OSError):
|
|
737
|
+
return None
|
|
738
|
+
try:
|
|
739
|
+
data = json.loads(raw)
|
|
740
|
+
except json.JSONDecodeError:
|
|
741
|
+
return None
|
|
742
|
+
geo = data.get('geometry') if isinstance(data, dict) else None
|
|
743
|
+
return geo if isinstance(geo, str) and geo else None
|
|
744
|
+
|
|
745
|
+
def _apply_saved_geometry(self, geometry: str) -> None:
|
|
746
|
+
"""Restore a saved 'WxH+X+Y' string, clamping off-screen positions."""
|
|
747
|
+
try:
|
|
748
|
+
self.geometry(geometry)
|
|
749
|
+
self.update_idletasks()
|
|
750
|
+
except tkinter.TclError:
|
|
751
|
+
return
|
|
752
|
+
# If the saved position is on a now-disconnected monitor, drag it
|
|
753
|
+
# back into a visible region so the window doesn't open invisibly.
|
|
754
|
+
try:
|
|
755
|
+
from bootstack._runtime.window_utilities import WindowPositioning
|
|
756
|
+
x, y = self.winfo_x(), self.winfo_y()
|
|
757
|
+
x, y = WindowPositioning.ensure_on_screen(self, x, y)
|
|
758
|
+
self.geometry(f'+{x}+{y}')
|
|
759
|
+
except Exception:
|
|
760
|
+
pass
|
|
761
|
+
|
|
762
|
+
def _save_window_state(self) -> None:
|
|
763
|
+
"""Write the current geometry to the state file."""
|
|
764
|
+
import json
|
|
765
|
+
path = self._state_file_path()
|
|
766
|
+
try:
|
|
767
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
768
|
+
except OSError:
|
|
769
|
+
return
|
|
770
|
+
try:
|
|
771
|
+
geo = self.geometry()
|
|
772
|
+
path.write_text(json.dumps({'geometry': geo}))
|
|
773
|
+
except (OSError, tkinter.TclError):
|
|
774
|
+
pass
|
|
775
|
+
|
|
776
|
+
# ----- System appearance tracking ----------------------------------------
|
|
777
|
+
|
|
778
|
+
def _is_dark_capable_platform(self) -> bool:
|
|
779
|
+
"""Return True if the current windowing system reports an appearance.
|
|
780
|
+
|
|
781
|
+
Currently only macOS exposes a Tk-level light/dark signal
|
|
782
|
+
(`<<TkSystemAppearanceChanged>>` and `MacWindowStyle isDark`).
|
|
783
|
+
Win/Linux apps that want to track system theme need their own
|
|
784
|
+
OS-specific hook, which is out of scope for this method.
|
|
785
|
+
"""
|
|
786
|
+
return getattr(self, 'winsys', None) == 'aqua'
|
|
787
|
+
|
|
788
|
+
def _system_is_dark(self) -> bool:
|
|
789
|
+
"""Return True if the OS is currently in dark mode (macOS)."""
|
|
790
|
+
try:
|
|
791
|
+
return bool(int(self.tk.call(
|
|
792
|
+
'::tk::unsupported::MacWindowStyle', 'isDark', self,
|
|
793
|
+
)))
|
|
794
|
+
except tkinter.TclError:
|
|
795
|
+
return False
|
|
796
|
+
|
|
797
|
+
def _bind_system_appearance_tracking(self) -> None:
|
|
798
|
+
"""Switch themes when the OS toggles between light and dark mode."""
|
|
799
|
+
def on_appearance_changed(_event=None):
|
|
800
|
+
if not self.settings.follow_system_appearance:
|
|
801
|
+
return
|
|
802
|
+
from bootstack.style.style import set_theme
|
|
803
|
+
set_theme(
|
|
804
|
+
self.settings.dark_theme
|
|
805
|
+
if self._system_is_dark()
|
|
806
|
+
else self.settings.light_theme
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
try:
|
|
810
|
+
self.bind('<<TkSystemAppearanceChanged>>', on_appearance_changed, add='+')
|
|
811
|
+
except tkinter.TclError:
|
|
812
|
+
pass
|
|
813
|
+
|
|
814
|
+
# ----- macOS Quit/Close conventions --------------------------------------
|
|
815
|
+
|
|
816
|
+
def _install_macos_quit_handlers(self) -> None:
|
|
817
|
+
"""Wire macOS-native close/quit/hide gestures.
|
|
818
|
+
|
|
819
|
+
macOS convention: clicking the window close button hides the app
|
|
820
|
+
(it stays in the Dock and Cmd+Tab list) rather than destroying it.
|
|
821
|
+
Cmd+Q (and Dock → Quit) is what actually quits. Cmd+H hides the
|
|
822
|
+
app, and clicking the Dock icon brings the main window back.
|
|
823
|
+
|
|
824
|
+
This method installs the matching Tk handlers so apps that opt
|
|
825
|
+
into `macos_quit_behavior='native'` behave correctly without
|
|
826
|
+
each app duplicating the boilerplate.
|
|
827
|
+
"""
|
|
828
|
+
# Close button → _do_close() (which withdraws on macOS native).
|
|
829
|
+
# Initialise the handler chain so that any later add_close_handler()
|
|
830
|
+
# calls stack on top of the dispatch mechanism rather than overwriting
|
|
831
|
+
# the protocol registration.
|
|
832
|
+
self._init_close_handlers()
|
|
833
|
+
|
|
834
|
+
# Cmd+Q / Dock → Quit fire <<AppleQuit>>; that's the real quit signal.
|
|
835
|
+
try:
|
|
836
|
+
self.bind('<<AppleQuit>>', lambda _e: self.destroy(), add='+')
|
|
837
|
+
except tkinter.TclError:
|
|
838
|
+
pass
|
|
839
|
+
|
|
840
|
+
# Cmd+H hides the app.
|
|
841
|
+
try:
|
|
842
|
+
self.bind('<<Apple-Hide>>', lambda _e: self.withdraw(), add='+')
|
|
843
|
+
except tkinter.TclError:
|
|
844
|
+
pass
|
|
845
|
+
|
|
846
|
+
# Clicking the Dock icon when no window is visible fires
|
|
847
|
+
# <<Apple-ReopenApplication>>; bring the main window back.
|
|
848
|
+
try:
|
|
849
|
+
self.bind(
|
|
850
|
+
'<<Apple-ReopenApplication>>',
|
|
851
|
+
lambda _e: self.deiconify(),
|
|
852
|
+
add='+',
|
|
853
|
+
)
|
|
854
|
+
except tkinter.TclError:
|
|
855
|
+
pass
|
|
856
|
+
|
|
857
|
+
def _trigger_close(self, _event=None) -> str:
|
|
858
|
+
"""Invoke the registered WM_DELETE_WINDOW handler for this window.
|
|
859
|
+
|
|
860
|
+
Lets `Cmd+W` and any other "close this window" gesture flow
|
|
861
|
+
through the same code path as clicking the close button, so a
|
|
862
|
+
custom `app.on_close(handler)` is honored.
|
|
863
|
+
"""
|
|
864
|
+
try:
|
|
865
|
+
handler_script = self.tk.call(
|
|
866
|
+
'wm', 'protocol', self._w, 'WM_DELETE_WINDOW',
|
|
867
|
+
)
|
|
868
|
+
except tkinter.TclError:
|
|
869
|
+
handler_script = ''
|
|
870
|
+
if handler_script:
|
|
871
|
+
try:
|
|
872
|
+
self.tk.eval(handler_script)
|
|
873
|
+
except tkinter.TclError:
|
|
874
|
+
pass
|
|
875
|
+
else:
|
|
876
|
+
# No handler registered — fall back to the platform-correct
|
|
877
|
+
# default for this app: withdraw on native macOS, destroy
|
|
878
|
+
# otherwise.
|
|
879
|
+
if (
|
|
880
|
+
self.winsys == 'aqua'
|
|
881
|
+
and self.settings.macos_quit_behavior == 'native'
|
|
882
|
+
):
|
|
883
|
+
self.withdraw()
|
|
884
|
+
else:
|
|
885
|
+
self.destroy()
|
|
886
|
+
return 'break'
|
|
887
|
+
|
|
888
|
+
# ----- macOS apple menu hooks --------------------------------------------
|
|
889
|
+
|
|
890
|
+
def on_about(self, handler: Callable[[], Any]) -> None:
|
|
891
|
+
"""Register a handler for the macOS "About <App>" menu item.
|
|
892
|
+
|
|
893
|
+
Tk on Aqua calls `::tk::mac::standardAboutPanel` when the user
|
|
894
|
+
picks About from the application menu. This method overrides
|
|
895
|
+
that proc with the supplied Python callable. No-op on Win/Linux,
|
|
896
|
+
where there's no equivalent system menu.
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
handler: Zero-argument callable invoked when the user picks
|
|
900
|
+
About from the apple menu.
|
|
901
|
+
"""
|
|
902
|
+
if self.winsys != 'aqua':
|
|
903
|
+
return
|
|
904
|
+
try:
|
|
905
|
+
self.tk.createcommand('::tk::mac::standardAboutPanel', handler)
|
|
906
|
+
except tkinter.TclError:
|
|
907
|
+
pass
|
|
908
|
+
|
|
909
|
+
def on_preferences(self, handler: Callable[[], Any]) -> None:
|
|
910
|
+
"""Register a handler for the macOS "Preferences…" menu item.
|
|
911
|
+
|
|
912
|
+
Tk on Aqua calls `::tk::mac::ShowPreferences` when the user picks
|
|
913
|
+
Preferences (Cmd+,) from the application menu. This method
|
|
914
|
+
overrides that proc with the supplied Python callable. No-op on
|
|
915
|
+
Win/Linux.
|
|
916
|
+
|
|
917
|
+
Args:
|
|
918
|
+
handler: Zero-argument callable invoked when the user picks
|
|
919
|
+
Preferences from the apple menu.
|
|
920
|
+
"""
|
|
921
|
+
if self.winsys != 'aqua':
|
|
922
|
+
return
|
|
923
|
+
try:
|
|
924
|
+
self.tk.createcommand('::tk::mac::ShowPreferences', handler)
|
|
925
|
+
except tkinter.TclError:
|
|
926
|
+
pass
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
# Backward compatibility alias
|
|
930
|
+
Window = App
|