bootstack 0.1.0a1__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 +249 -0
- bootstack/__main__.py +5 -0
- bootstack/api/__init__.py +127 -0
- bootstack/api/app.py +30 -0
- bootstack/api/constants.py +3 -0
- bootstack/api/data.py +23 -0
- bootstack/api/dialogs.py +44 -0
- bootstack/api/i18n.py +17 -0
- bootstack/api/localization.py +16 -0
- bootstack/api/menu.py +7 -0
- bootstack/api/style.py +23 -0
- bootstack/api/utils.py +24 -0
- bootstack/api/widgets.py +137 -0
- bootstack/assets/__init__.py +24 -0
- bootstack/assets/bootstack-transparent.png +0 -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/border.png +0 -0
- bootstack/assets/elements/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-default.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-after-compact.png +0 -0
- bootstack/assets/elements/input-after-default.png +0 -0
- bootstack/assets/elements/input-before-compact.png +0 -0
- bootstack/assets/elements/input-before-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/manifest.toml +480 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/nav-button-compact.png +0 -0
- bootstack/assets/elements/nav-button-default.png +0 -0
- bootstack/assets/elements/nav-icon-button-compact.png +0 -0
- bootstack/assets/elements/nav-icon-button-default.png +0 -0
- bootstack/assets/elements/notebook-client-border.png +0 -0
- bootstack/assets/elements/notebook-tab-active.png +0 -0
- bootstack/assets/elements/notebook-tab-bar.png +0 -0
- bootstack/assets/elements/notebook-tab-normal.png +0 -0
- bootstack/assets/elements/notebook-tab-pill.png +0 -0
- bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/elements/progress-bar-solid.png +0 -0
- bootstack/assets/elements/progress-bar-thin.png +0 -0
- bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/elements/radio-selected.png +0 -0
- bootstack/assets/elements/radio-unselected.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle-focus.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-horizontal.png +0 -0
- bootstack/assets/elements/slider-track-vertical.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
- bootstack/assets/elements/tabs-bar-vertical.png +0 -0
- bootstack/assets/elements/tabs-pill.png +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/themes/__init__.py +0 -0
- bootstack/assets/themes/amber-dark.json +32 -0
- bootstack/assets/themes/amber-light.json +32 -0
- bootstack/assets/themes/aurora-dark.json +32 -0
- bootstack/assets/themes/aurora-light.json +32 -0
- bootstack/assets/themes/bootstrap-dark.json +32 -0
- bootstack/assets/themes/bootstrap-light.json +32 -0
- bootstack/assets/themes/classic-dark.json +32 -0
- bootstack/assets/themes/classic-light.json +32 -0
- bootstack/assets/themes/docs-dark.json +32 -0
- bootstack/assets/themes/docs-light.json +32 -0
- bootstack/assets/themes/forest-dark.json +32 -0
- bootstack/assets/themes/forest-light.json +32 -0
- bootstack/assets/themes/ocean-dark.json +32 -0
- bootstack/assets/themes/ocean-light.json +32 -0
- bootstack/assets/themes/rose-dark.json +32 -0
- bootstack/assets/themes/rose-light.json +32 -0
- bootstack/assets/widgets/__init__.py +0 -0
- bootstack/assets/widgets/badge-default.png +0 -0
- bootstack/assets/widgets/badge-pill.png +0 -0
- bootstack/assets/widgets/border.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
- bootstack/assets/widgets/button-group-vertical-after.png +0 -0
- bootstack/assets/widgets/button-group-vertical-before.png +0 -0
- bootstack/assets/widgets/button-group-vertical-center.png +0 -0
- bootstack/assets/widgets/button.png +0 -0
- bootstack/assets/widgets/checkbox-checked.png +0 -0
- bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
- bootstack/assets/widgets/checkbox-unchecked.png +0 -0
- bootstack/assets/widgets/field.png +0 -0
- bootstack/assets/widgets/icon-button.png +0 -0
- bootstack/assets/widgets/input-inner.png +0 -0
- bootstack/assets/widgets/input-prefix.png +0 -0
- bootstack/assets/widgets/input-suffix.png +0 -0
- bootstack/assets/widgets/input.png +0 -0
- bootstack/assets/widgets/list-item-focus.png +0 -0
- bootstack/assets/widgets/list-item-separated.png +0 -0
- bootstack/assets/widgets/menu-item-separated.png +0 -0
- bootstack/assets/widgets/notebook-client-border.png +0 -0
- bootstack/assets/widgets/notebook-pill-active.png +0 -0
- bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
- bootstack/assets/widgets/notebook-tab-active.png +0 -0
- bootstack/assets/widgets/notebook-tab-border.png +0 -0
- bootstack/assets/widgets/notebook-tab-normal.png +0 -0
- bootstack/assets/widgets/notebook-underline.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
- bootstack/assets/widgets/progress-trough-vertical.png +0 -0
- bootstack/assets/widgets/radio-selected.png +0 -0
- bootstack/assets/widgets/radio-unselected.png +0 -0
- bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
- bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
- bootstack/assets/widgets/separator-horizontal.png +0 -0
- bootstack/assets/widgets/separator-vertical.png +0 -0
- bootstack/assets/widgets/slider-handle-focus.png +0 -0
- bootstack/assets/widgets/slider-handle.png +0 -0
- bootstack/assets/widgets/slider-track-horizontal.png +0 -0
- bootstack/assets/widgets/slider-track-vertical.png +0 -0
- bootstack/assets/widgets/switch-off.png +0 -0
- bootstack/assets/widgets/switch-on.png +0 -0
- bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
- bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
- bootstack/assets/widgets/tabs-pill.png +0 -0
- bootstack/cli/__init__.py +124 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +439 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +287 -0
- bootstack/cli/demo.py +1267 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/list_cmd.py +71 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +246 -0
- bootstack/cli/run.py +99 -0
- bootstack/cli/start.py +105 -0
- bootstack/cli/templates/__init__.py +861 -0
- bootstack/constants.py +325 -0
- bootstack/core/__init__.py +34 -0
- bootstack/core/capabilities/__init__.py +45 -0
- bootstack/core/capabilities/after.py +103 -0
- bootstack/core/capabilities/bind.py +154 -0
- bootstack/core/capabilities/bindtags.py +112 -0
- bootstack/core/capabilities/busy.py +61 -0
- bootstack/core/capabilities/clipboard.py +88 -0
- bootstack/core/capabilities/focus.py +118 -0
- bootstack/core/capabilities/grab.py +65 -0
- bootstack/core/capabilities/grid.py +188 -0
- bootstack/core/capabilities/localization.py +231 -0
- bootstack/core/capabilities/pack.py +119 -0
- bootstack/core/capabilities/place.py +92 -0
- bootstack/core/capabilities/selection.py +136 -0
- bootstack/core/capabilities/signals.py +242 -0
- bootstack/core/capabilities/winfo.py +315 -0
- bootstack/core/colorutils.py +234 -0
- bootstack/core/exceptions.py +95 -0
- bootstack/core/images.py +283 -0
- bootstack/core/localization/README.md +90 -0
- bootstack/core/localization/__init__.py +13 -0
- bootstack/core/localization/intl_format.py +580 -0
- bootstack/core/localization/msgcat.py +425 -0
- bootstack/core/localization/specs.py +143 -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/publisher.py +147 -0
- bootstack/core/signals/README.md +112 -0
- bootstack/core/signals/__init__.py +8 -0
- bootstack/core/signals/integration.py +100 -0
- bootstack/core/signals/signal.py +317 -0
- bootstack/core/signals/types.py +4 -0
- bootstack/core/validation/__init__.py +5 -0
- bootstack/core/validation/types.py +13 -0
- bootstack/core/validation/validation_result.py +17 -0
- bootstack/core/validation/validation_rules.py +112 -0
- bootstack/core/variables.py +62 -0
- bootstack/datasource/README.md +607 -0
- bootstack/datasource/__init__.py +51 -0
- bootstack/datasource/base.py +474 -0
- bootstack/datasource/file_source.py +541 -0
- bootstack/datasource/memory_source.py +482 -0
- bootstack/datasource/sqlite_source.py +453 -0
- bootstack/datasource/types.py +259 -0
- bootstack/dialogs/__init__.py +56 -0
- bootstack/dialogs/colorchooser.py +674 -0
- bootstack/dialogs/colordropper.py +257 -0
- bootstack/dialogs/datedialog.py +404 -0
- bootstack/dialogs/dialog.py +514 -0
- bootstack/dialogs/filterdialog.py +358 -0
- bootstack/dialogs/fontdialog.py +339 -0
- bootstack/dialogs/formdialog.py +541 -0
- bootstack/dialogs/message.py +489 -0
- bootstack/dialogs/query.py +561 -0
- bootstack/py.typed +1 -0
- bootstack/runtime/__init__.py +3 -0
- bootstack/runtime/app.py +879 -0
- bootstack/runtime/base_window.py +786 -0
- bootstack/runtime/events.py +399 -0
- bootstack/runtime/menu.py +510 -0
- bootstack/runtime/shortcuts.py +423 -0
- bootstack/runtime/tk_patch.py +31 -0
- bootstack/runtime/toplevel.py +131 -0
- bootstack/runtime/utility.py +371 -0
- bootstack/runtime/visual_focus.py +228 -0
- bootstack/runtime/window_utilities.py +1043 -0
- bootstack/style/__init__.py +5498 -0
- bootstack/style/bootstyle.py +507 -0
- bootstack/style/bootstyle_builder_base.py +752 -0
- bootstack/style/bootstyle_builder_mixed.py +93 -0
- bootstack/style/bootstyle_builder_tk.py +109 -0
- bootstack/style/bootstyle_builder_ttk.py +354 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +44 -0
- bootstack/style/builders/button.py +453 -0
- bootstack/style/builders/buttongroup.py +344 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +95 -0
- bootstack/style/builders/combobox.py +112 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +83 -0
- bootstack/style/builders/expander.py +171 -0
- bootstack/style/builders/field.py +312 -0
- bootstack/style/builders/frame.py +27 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +41 -0
- bootstack/style/builders/listview.py +267 -0
- bootstack/style/builders/menubar.py +74 -0
- bootstack/style/builders/menubutton.py +408 -0
- bootstack/style/builders/notebook.py +316 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +71 -0
- bootstack/style/builders/radiobutton.py +68 -0
- bootstack/style/builders/scale.py +66 -0
- bootstack/style/builders/scrollbar.py +360 -0
- bootstack/style/builders/separator.py +45 -0
- bootstack/style/builders/sidenav.py +313 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +67 -0
- bootstack/style/builders/tabitem.py +205 -0
- bootstack/style/builders/toolbutton.py +260 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +269 -0
- bootstack/style/builders/utils.py +404 -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/style.py +499 -0
- bootstack/style/theme_provider.py +449 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +42 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +527 -0
- bootstack/style/utility.py +696 -0
- bootstack/themes/__init__.py +12 -0
- bootstack/themes/standard.py +415 -0
- bootstack/themes/user.py +45 -0
- bootstack/widgets/__init__.py +53 -0
- bootstack/widgets/composites/__init__.py +38 -0
- bootstack/widgets/composites/accordion.py +385 -0
- bootstack/widgets/composites/appshell.py +445 -0
- bootstack/widgets/composites/buttongroup.py +391 -0
- bootstack/widgets/composites/calendar.py +914 -0
- bootstack/widgets/composites/compositeframe.py +282 -0
- bootstack/widgets/composites/contextmenu.py +1754 -0
- bootstack/widgets/composites/dateentry.py +261 -0
- bootstack/widgets/composites/dropdownbutton.py +190 -0
- bootstack/widgets/composites/expander.py +508 -0
- bootstack/widgets/composites/field.py +448 -0
- bootstack/widgets/composites/floodgauge.py +434 -0
- bootstack/widgets/composites/form.py +983 -0
- bootstack/widgets/composites/labeledscale.py +209 -0
- bootstack/widgets/composites/list/__init__.py +15 -0
- bootstack/widgets/composites/list/listitem.py +733 -0
- bootstack/widgets/composites/list/listview.py +1507 -0
- bootstack/widgets/composites/menubar.py +303 -0
- bootstack/widgets/composites/meter.py +882 -0
- bootstack/widgets/composites/numericentry.py +183 -0
- bootstack/widgets/composites/pagestack.py +330 -0
- bootstack/widgets/composites/passwordentry.py +149 -0
- bootstack/widgets/composites/pathentry.py +223 -0
- bootstack/widgets/composites/radiogroup.py +466 -0
- bootstack/widgets/composites/scrolledtext.py +388 -0
- bootstack/widgets/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/composites/scrollview.py +675 -0
- bootstack/widgets/composites/selectbox.py +544 -0
- bootstack/widgets/composites/sidenav/__init__.py +24 -0
- bootstack/widgets/composites/sidenav/group.py +485 -0
- bootstack/widgets/composites/sidenav/header.py +83 -0
- bootstack/widgets/composites/sidenav/item.py +413 -0
- bootstack/widgets/composites/sidenav/separator.py +51 -0
- bootstack/widgets/composites/sidenav/view.py +919 -0
- bootstack/widgets/composites/spinnerentry.py +232 -0
- bootstack/widgets/composites/tableview/__init__.py +5 -0
- bootstack/widgets/composites/tableview/tableview.py +2254 -0
- bootstack/widgets/composites/tableview/types.py +169 -0
- bootstack/widgets/composites/tabs/__init__.py +6 -0
- bootstack/widgets/composites/tabs/tabitem.py +372 -0
- bootstack/widgets/composites/tabs/tabs.py +478 -0
- bootstack/widgets/composites/tabs/tabview.py +352 -0
- bootstack/widgets/composites/textentry.py +90 -0
- bootstack/widgets/composites/timeentry.py +189 -0
- bootstack/widgets/composites/toast.py +364 -0
- bootstack/widgets/composites/togglegroup.py +382 -0
- bootstack/widgets/composites/toolbar.py +393 -0
- bootstack/widgets/composites/tooltip.py +404 -0
- bootstack/widgets/internal/__init__.py +0 -0
- bootstack/widgets/internal/wrapper_base.py +304 -0
- bootstack/widgets/mixins/__init__.py +25 -0
- bootstack/widgets/mixins/configure_mixin.py +186 -0
- bootstack/widgets/mixins/entry_mixin.py +70 -0
- bootstack/widgets/mixins/font_mixin.py +346 -0
- bootstack/widgets/mixins/icon_mixin.py +38 -0
- bootstack/widgets/mixins/localization_mixin.py +255 -0
- bootstack/widgets/mixins/signal_mixin.py +272 -0
- bootstack/widgets/mixins/validation_mixin.py +204 -0
- bootstack/widgets/parts/__init__.py +11 -0
- bootstack/widgets/parts/numberentry_part.py +345 -0
- bootstack/widgets/parts/spinnerentry_part.py +394 -0
- bootstack/widgets/parts/textentry_part.py +344 -0
- bootstack/widgets/primitives/__init__.py +55 -0
- bootstack/widgets/primitives/badge.py +44 -0
- bootstack/widgets/primitives/button.py +89 -0
- bootstack/widgets/primitives/card.py +66 -0
- bootstack/widgets/primitives/checkbutton.py +124 -0
- bootstack/widgets/primitives/checktoggle.py +53 -0
- bootstack/widgets/primitives/combobox.py +165 -0
- bootstack/widgets/primitives/entry.py +98 -0
- bootstack/widgets/primitives/frame.py +206 -0
- bootstack/widgets/primitives/gridframe.py +479 -0
- bootstack/widgets/primitives/label.py +95 -0
- bootstack/widgets/primitives/labelframe.py +63 -0
- bootstack/widgets/primitives/menubutton.py +118 -0
- bootstack/widgets/primitives/notebook.py +551 -0
- bootstack/widgets/primitives/optionmenu.py +248 -0
- bootstack/widgets/primitives/packframe.py +228 -0
- bootstack/widgets/primitives/panedwindow.py +58 -0
- bootstack/widgets/primitives/progressbar.py +95 -0
- bootstack/widgets/primitives/radiobutton.py +115 -0
- bootstack/widgets/primitives/radiotoggle.py +50 -0
- bootstack/widgets/primitives/scale.py +85 -0
- bootstack/widgets/primitives/scrollbar.py +56 -0
- bootstack/widgets/primitives/separator.py +56 -0
- bootstack/widgets/primitives/sizegrip.py +47 -0
- bootstack/widgets/primitives/spinbox.py +91 -0
- bootstack/widgets/primitives/switch.py +41 -0
- bootstack/widgets/primitives/treeview.py +77 -0
- bootstack/widgets/types.py +20 -0
- bootstack-0.1.0a1.dist-info/METADATA +196 -0
- bootstack-0.1.0a1.dist-info/RECORD +419 -0
- bootstack-0.1.0a1.dist-info/WHEEL +5 -0
- bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
- bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
bootstack/runtime/app.py
ADDED
|
@@ -0,0 +1,879 @@
|
|
|
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.core.localization.intl_format import detect_locale
|
|
21
|
+
from bootstack.core.localization.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
|
+
# Sentinel for "use settings default"
|
|
30
|
+
_USE_SETTINGS = object()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def set_current_app(app: App) -> None:
|
|
34
|
+
"""Set the process-wide current App instance.
|
|
35
|
+
|
|
36
|
+
Intended to be called from App.__init__ for the first app created.
|
|
37
|
+
"""
|
|
38
|
+
global _current_app
|
|
39
|
+
_current_app = app
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get_app_settings() -> AppSettings:
|
|
43
|
+
"""Return the settings for current App.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
The AppSettings instance for the current application.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
RuntimeError: If no active App instance is set.
|
|
50
|
+
"""
|
|
51
|
+
return get_current_app().settings
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def clear_current_app(app: App) -> None:
|
|
55
|
+
"""Clear the current app reference if it matches the given app."""
|
|
56
|
+
global _current_app
|
|
57
|
+
if _current_app is app:
|
|
58
|
+
_current_app = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def get_current_app() -> App:
|
|
62
|
+
"""Return the current App instance.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
The currently active App instance.
|
|
66
|
+
|
|
67
|
+
Raises:
|
|
68
|
+
RuntimeError: If no App has been registered yet.
|
|
69
|
+
"""
|
|
70
|
+
if _current_app is None:
|
|
71
|
+
raise RuntimeError(
|
|
72
|
+
"No current App instance is set. "
|
|
73
|
+
"Create an App first, e.g. `app = App()`."
|
|
74
|
+
)
|
|
75
|
+
return _current_app
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def has_current_app() -> bool:
|
|
79
|
+
"""Check if a current App instance is registered.
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
True if an App instance exists, False otherwise.
|
|
83
|
+
"""
|
|
84
|
+
return _current_app is not None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def get_default_root(what: Optional[str] = None) -> tkinter.Tk:
|
|
88
|
+
"""Get the default Tk root window.
|
|
89
|
+
|
|
90
|
+
Returns the default root if it has been created, otherwise
|
|
91
|
+
creates and returns a new instance.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
what: Optional description of the operation requiring the root,
|
|
95
|
+
used in error messages if called too early.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The default Tk root window instance.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
RuntimeError: If tkinter is configured to not support default root,
|
|
102
|
+
or if called too early with a 'what' description.
|
|
103
|
+
"""
|
|
104
|
+
if not tkinter._support_default_root:
|
|
105
|
+
raise RuntimeError(
|
|
106
|
+
"No master specified and tkinter is "
|
|
107
|
+
"configured to not support default root")
|
|
108
|
+
if not tkinter._default_root:
|
|
109
|
+
if what:
|
|
110
|
+
raise RuntimeError(f"Too early to {what}: no default root window")
|
|
111
|
+
root = tkinter.Tk()
|
|
112
|
+
assert tkinter._default_root is root
|
|
113
|
+
return tkinter._default_root
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def apply_class_bindings(window: tkinter.Widget | App) -> None:
|
|
117
|
+
"""Add class level event bindings in application"""
|
|
118
|
+
# Copy TCheckbutton bindings to Toolbutton class
|
|
119
|
+
# This is needed because widgets using class_='Toolbutton' have their
|
|
120
|
+
# bindtags reference 'Toolbutton' instead of 'TCheckbutton', so they
|
|
121
|
+
# need the same mouse/keyboard bindings copied over.
|
|
122
|
+
for event in ('<Button-1>', '<ButtonRelease-1>', '<B1-Leave>', '<B1-Enter>',
|
|
123
|
+
'<Enter>', '<Leave>', '<Key-space>', '<<Invoke>>'):
|
|
124
|
+
binding = window.bind_class('TCheckbutton', event)
|
|
125
|
+
if binding:
|
|
126
|
+
window.bind_class('Toolbutton', event, binding)
|
|
127
|
+
window.bind_class('ButtonGroup', event, binding)
|
|
128
|
+
|
|
129
|
+
for className in ["TEntry", "TSpinbox", "TCombobox", "Text"]:
|
|
130
|
+
window.bind_class(
|
|
131
|
+
className=className,
|
|
132
|
+
sequence="<Configure>",
|
|
133
|
+
func=on_disabled_readonly_state,
|
|
134
|
+
add="+")
|
|
135
|
+
|
|
136
|
+
for sequence in ["<Control-a>", "<Control-A>"]:
|
|
137
|
+
window.bind_class(
|
|
138
|
+
className=className,
|
|
139
|
+
sequence=sequence,
|
|
140
|
+
func=on_select_all)
|
|
141
|
+
|
|
142
|
+
window.unbind_class("TButton", "<Key-space>")
|
|
143
|
+
|
|
144
|
+
def button_default_binding(event: tkinter.Event) -> None:
|
|
145
|
+
"""The default keybind on a button when the return or enter key
|
|
146
|
+
is pressed and the button has focus or is the default button."""
|
|
147
|
+
try:
|
|
148
|
+
widget = window.nametowidget(event.widget)
|
|
149
|
+
widget.invoke()
|
|
150
|
+
except KeyError:
|
|
151
|
+
window.tk.call(event.widget, 'invoke')
|
|
152
|
+
|
|
153
|
+
window.bind_class(
|
|
154
|
+
"TButton", "<Key-Return>", button_default_binding,
|
|
155
|
+
add="+")
|
|
156
|
+
window.bind_class("TButton", "<KP_Enter>", button_default_binding, add="+")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def apply_all_bindings(window: tkinter.Widget | App) -> None:
|
|
160
|
+
"""Add bindings to all widgets in the application"""
|
|
161
|
+
window.bind_all('<Map>', on_map_child, '+')
|
|
162
|
+
window.bind_all('<Destroy>', lambda e: Publisher.unsubscribe(e.widget))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def on_disabled_readonly_state(event: tkinter.Event) -> None:
|
|
166
|
+
"""Change the cursor of entry type widgets to 'arrow' if in a
|
|
167
|
+
disabled or readonly state."""
|
|
168
|
+
try:
|
|
169
|
+
widget = event.widget
|
|
170
|
+
state = str(widget.cget('state'))
|
|
171
|
+
cursor = str(widget.cget('cursor'))
|
|
172
|
+
if state in (DISABLED, READONLY):
|
|
173
|
+
if cursor == 'arrow':
|
|
174
|
+
return
|
|
175
|
+
else:
|
|
176
|
+
widget['cursor'] = 'arrow'
|
|
177
|
+
else:
|
|
178
|
+
if cursor in ('ibeam', ''):
|
|
179
|
+
return
|
|
180
|
+
else:
|
|
181
|
+
widget['cursor'] = None
|
|
182
|
+
except:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def on_map_child(event: tkinter.Event) -> None:
|
|
187
|
+
"""Callback for <Map> event which generates a <<MapChild>> virtual
|
|
188
|
+
event on the parent"""
|
|
189
|
+
widget: tkinter.Widget = event.widget
|
|
190
|
+
try:
|
|
191
|
+
if widget.master is None: # root widget
|
|
192
|
+
return
|
|
193
|
+
else:
|
|
194
|
+
widget.master.event_generate('<<MapChild>>')
|
|
195
|
+
except:
|
|
196
|
+
# not a tkinter widget that I'm handling (ex. Combobox.popdown)
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def on_select_all(event: tkinter.Event) -> None:
|
|
201
|
+
"""Callback to select all text in Entry or Text widget when Ctrl+A is pressed."""
|
|
202
|
+
widget = event.widget
|
|
203
|
+
|
|
204
|
+
if isinstance(widget, tkinter.Text):
|
|
205
|
+
widget.tag_add(SEL, "1.0", END)
|
|
206
|
+
widget.mark_set(INSERT, END)
|
|
207
|
+
widget.see(INSERT)
|
|
208
|
+
elif isinstance(widget, tkinter.Entry):
|
|
209
|
+
widget.selection_range(0, END)
|
|
210
|
+
widget.icursor(END)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
LocalizeMode = Union[bool, Literal['auto']]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@dataclass
|
|
217
|
+
class AppSettings:
|
|
218
|
+
"""Application-wide settings for bootstack applications.
|
|
219
|
+
|
|
220
|
+
This dataclass holds configuration for theming, localization, and
|
|
221
|
+
application metadata. It is automatically populated with sensible
|
|
222
|
+
defaults based on the system locale.
|
|
223
|
+
|
|
224
|
+
Attributes:
|
|
225
|
+
app_name: The application name displayed in the title bar.
|
|
226
|
+
app_author: The application author (used for config paths).
|
|
227
|
+
app_version: The application version string.
|
|
228
|
+
theme: The current theme name ('light', 'dark', or a specific theme).
|
|
229
|
+
light_theme: The theme to use when `theme='light'`.
|
|
230
|
+
dark_theme: The theme to use when `theme='dark'`.
|
|
231
|
+
follow_system_appearance: If True, automatically switch between
|
|
232
|
+
`light_theme` and `dark_theme` to match the OS appearance and
|
|
233
|
+
track changes at runtime. Currently effective on macOS, where
|
|
234
|
+
Tk fires `<<TkSystemAppearanceChanged>>` and exposes the
|
|
235
|
+
current mode via `tk::unsupported::MacWindowStyle isDark`.
|
|
236
|
+
Defaults to False so existing apps that pin a theme keep
|
|
237
|
+
doing so.
|
|
238
|
+
available_themes: Sequence of available theme names.
|
|
239
|
+
inherit_surface_color: If True, child widgets inherit the parent's
|
|
240
|
+
surface color for consistent backgrounds.
|
|
241
|
+
locale: The locale identifier (e.g., 'en_US', 'de_DE'). Auto-detected
|
|
242
|
+
from system if not specified.
|
|
243
|
+
language: The base language code (e.g., 'en', 'de'). Derived from
|
|
244
|
+
locale if not specified.
|
|
245
|
+
date_format: The date format pattern. Derived from locale if not
|
|
246
|
+
specified (e.g., 'M/d/yy' for en_US).
|
|
247
|
+
time_format: The time format pattern. Derived from locale if not
|
|
248
|
+
specified (e.g., 'h:mm a' for en_US).
|
|
249
|
+
number_decimal: The decimal separator character. Derived from locale
|
|
250
|
+
if not specified (e.g., '.' for en_US).
|
|
251
|
+
number_thousands: The thousands separator character. Derived from
|
|
252
|
+
locale if not specified (e.g., ',' for en_US).
|
|
253
|
+
localize_mode: Controls localization behavior. 'auto' enables
|
|
254
|
+
localization based on locale, True always enables, False disables.
|
|
255
|
+
window_style: Windows-only pywinstyles effect for all windows.
|
|
256
|
+
Options include 'mica', 'acrylic', 'aero', 'transparent', 'win7'.
|
|
257
|
+
Defaults to 'mica'. Set to None to disable.
|
|
258
|
+
macos_quit_behavior: How the close button and Cmd+Q behave on macOS.
|
|
259
|
+
'native' (default) follows Mac convention: clicking the window
|
|
260
|
+
close button or Cmd+H hides the app (withdraws), clicking the
|
|
261
|
+
dock icon reshows it, and Cmd+Q (or Dock → Quit) actually
|
|
262
|
+
destroys it. 'classic' restores the cross-platform behavior
|
|
263
|
+
where the close button destroys the window. No-op on Win/Linux.
|
|
264
|
+
remember_window_state: If True, the App's geometry (size + position)
|
|
265
|
+
is saved on close and restored on next launch, with off-screen
|
|
266
|
+
positions clamped back into a visible monitor. Off by default
|
|
267
|
+
so existing apps that pin a size/position keep doing so.
|
|
268
|
+
state_path: Optional override for where window state is stored.
|
|
269
|
+
When None, defaults to a per-app file under the OS config
|
|
270
|
+
directory (Library/Application Support on macOS, %APPDATA% on
|
|
271
|
+
Windows, $XDG_CONFIG_HOME on Linux). The leaf filename includes
|
|
272
|
+
`app_name` so multiple bootstack apps don't collide.
|
|
273
|
+
|
|
274
|
+
Examples:
|
|
275
|
+
```python
|
|
276
|
+
# Create app with default settings
|
|
277
|
+
app = App()
|
|
278
|
+
|
|
279
|
+
# Create app with custom settings
|
|
280
|
+
settings = AppSettings(
|
|
281
|
+
app_name="My App",
|
|
282
|
+
theme="dark",
|
|
283
|
+
locale="de_DE"
|
|
284
|
+
)
|
|
285
|
+
app = App(settings=settings)
|
|
286
|
+
|
|
287
|
+
# Access settings
|
|
288
|
+
print(app.settings.locale) # 'de_DE'
|
|
289
|
+
print(app.settings.date_format) # 'd.M.yy'
|
|
290
|
+
```
|
|
291
|
+
"""
|
|
292
|
+
# information
|
|
293
|
+
app_name: str | None = None
|
|
294
|
+
app_author: str | None = None
|
|
295
|
+
app_version: str | None = None
|
|
296
|
+
|
|
297
|
+
# theme
|
|
298
|
+
theme: str = "light"
|
|
299
|
+
light_theme: str = "docs-light"
|
|
300
|
+
dark_theme: str = "docs-dark"
|
|
301
|
+
follow_system_appearance: bool = False
|
|
302
|
+
available_themes: Sequence[str] = ()
|
|
303
|
+
inherit_surface_color: bool = True
|
|
304
|
+
|
|
305
|
+
# internationalization
|
|
306
|
+
locale: str | None = None
|
|
307
|
+
language: str | None = None
|
|
308
|
+
date_format: str | None = None
|
|
309
|
+
time_format: str | None = None
|
|
310
|
+
number_decimal: str | None = None
|
|
311
|
+
number_thousands: str | None = None
|
|
312
|
+
|
|
313
|
+
# localization behavior
|
|
314
|
+
localize_mode: LocalizeMode = "auto"
|
|
315
|
+
|
|
316
|
+
# platform-specific
|
|
317
|
+
window_style: str | None = 'mica'
|
|
318
|
+
macos_quit_behavior: str = 'native'
|
|
319
|
+
|
|
320
|
+
# window state persistence
|
|
321
|
+
remember_window_state: bool = False
|
|
322
|
+
state_path: str | None = None
|
|
323
|
+
|
|
324
|
+
def __post_init__(self):
|
|
325
|
+
"""Populate localization defaults when not explicitly configured."""
|
|
326
|
+
_apply_localization_defaults(self)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
class AppSettingsKwargs(TypedDict, total=False):
|
|
330
|
+
app_name: str
|
|
331
|
+
app_author: str
|
|
332
|
+
app_version: str
|
|
333
|
+
|
|
334
|
+
# theme
|
|
335
|
+
theme: str
|
|
336
|
+
light_theme: str
|
|
337
|
+
dark_theme: str
|
|
338
|
+
follow_system_appearance: bool
|
|
339
|
+
available_themes: Sequence[str]
|
|
340
|
+
inherit_surface_color: bool
|
|
341
|
+
|
|
342
|
+
# localization
|
|
343
|
+
locale: str
|
|
344
|
+
language: str
|
|
345
|
+
date_format: str
|
|
346
|
+
time_format: str
|
|
347
|
+
number_decimal: str
|
|
348
|
+
number_thousands: str
|
|
349
|
+
|
|
350
|
+
# platform-specific
|
|
351
|
+
window_style: str | None
|
|
352
|
+
macos_quit_behavior: str
|
|
353
|
+
|
|
354
|
+
# window state persistence
|
|
355
|
+
remember_window_state: bool
|
|
356
|
+
state_path: str | None
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
DEFAULT_LOCALE = "en_US"
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _apply_localization_defaults(settings: AppSettings) -> None:
|
|
363
|
+
"""Ensure locale-based fields always have meaningful defaults."""
|
|
364
|
+
locale_code = settings.locale or detect_locale(DEFAULT_LOCALE)
|
|
365
|
+
settings.locale = locale_code
|
|
366
|
+
|
|
367
|
+
if settings.language is None:
|
|
368
|
+
settings.language = _language_from_locale(locale_code)
|
|
369
|
+
|
|
370
|
+
if settings.date_format is None:
|
|
371
|
+
settings.date_format = _safe_date_format(locale_code)
|
|
372
|
+
|
|
373
|
+
if settings.time_format is None:
|
|
374
|
+
settings.time_format = _safe_time_format(locale_code)
|
|
375
|
+
|
|
376
|
+
if settings.number_decimal is None:
|
|
377
|
+
settings.number_decimal = _safe_decimal_symbol(locale_code)
|
|
378
|
+
|
|
379
|
+
if settings.number_thousands is None:
|
|
380
|
+
settings.number_thousands = _safe_group_symbol(locale_code)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def _language_from_locale(locale_code: str) -> str:
|
|
384
|
+
"""Return the base language (e.g., en_US -> en)."""
|
|
385
|
+
base = locale_code.split("_", 1)[0]
|
|
386
|
+
return base.lower() if base else locale_code
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _safe_date_format(locale_code: str) -> str:
|
|
390
|
+
try:
|
|
391
|
+
return str(get_date_format("short", locale=locale_code))
|
|
392
|
+
except (UnknownLocaleError, ValueError):
|
|
393
|
+
return str(get_date_format("short", locale=DEFAULT_LOCALE))
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def _safe_time_format(locale_code: str) -> str:
|
|
397
|
+
try:
|
|
398
|
+
return str(get_time_format("short", locale=locale_code))
|
|
399
|
+
except (UnknownLocaleError, ValueError):
|
|
400
|
+
return str(get_time_format("short", locale=DEFAULT_LOCALE))
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _safe_decimal_symbol(locale_code: str) -> str:
|
|
404
|
+
try:
|
|
405
|
+
return get_decimal_symbol(locale_code)
|
|
406
|
+
except (UnknownLocaleError, ValueError):
|
|
407
|
+
return get_decimal_symbol(DEFAULT_LOCALE)
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def _safe_group_symbol(locale_code: str) -> str:
|
|
411
|
+
try:
|
|
412
|
+
return get_group_symbol(locale_code)
|
|
413
|
+
except (UnknownLocaleError, ValueError):
|
|
414
|
+
return get_group_symbol(DEFAULT_LOCALE)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
class TkKwargs(TypedDict, total=False):
|
|
418
|
+
"""The following attributes are available per the Tkinter API (not commonly used).
|
|
419
|
+
|
|
420
|
+
Attributes:
|
|
421
|
+
screenName: Sets the display environment variable (X11 only).
|
|
422
|
+
baseName: Name of the profile file. By default, derived from program name.
|
|
423
|
+
className: Name of the widget class
|
|
424
|
+
useTk: If True, initializes the Tk system.
|
|
425
|
+
sync: If true, executes all X server commands synchronously.
|
|
426
|
+
use: The id of the window in which to embed the application.
|
|
427
|
+
"""
|
|
428
|
+
screenName: str
|
|
429
|
+
baseName: str
|
|
430
|
+
className: str
|
|
431
|
+
useTk: bool
|
|
432
|
+
sync: bool
|
|
433
|
+
use: str
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
class App(BaseWindow, WidgetCapabilitiesMixin, tkinter.Tk):
|
|
437
|
+
"""The primary application window and entry point.
|
|
438
|
+
|
|
439
|
+
App adds theming, localization, and platform setup on top of `tkinter.Tk`.
|
|
440
|
+
|
|
441
|
+
The standard widget API (events, scheduling, clipboard, geometry managers,
|
|
442
|
+
winfo, etc.) is documented under bootstack capabilities and is available
|
|
443
|
+
on App via inheritance.
|
|
444
|
+
"""
|
|
445
|
+
|
|
446
|
+
def __init__(
|
|
447
|
+
self,
|
|
448
|
+
title: str | None = None,
|
|
449
|
+
theme: str | None = None,
|
|
450
|
+
icon: tkinter.PhotoImage | None = None,
|
|
451
|
+
|
|
452
|
+
settings: AppSettings | AppSettingsKwargs | None = None,
|
|
453
|
+
localize: LocalizeMode | None = None,
|
|
454
|
+
|
|
455
|
+
# window settings
|
|
456
|
+
size: tuple[int, int] | None = None,
|
|
457
|
+
position: tuple[int, int] | None = None,
|
|
458
|
+
minsize: tuple[int, int] | None = None,
|
|
459
|
+
maxsize: tuple[int, int] | None = None,
|
|
460
|
+
resizable: tuple[bool, bool] | None = None,
|
|
461
|
+
scaling: float | None = None,
|
|
462
|
+
hdpi: bool = True,
|
|
463
|
+
alpha: float = 1.0,
|
|
464
|
+
transient: object | None = None,
|
|
465
|
+
override_redirect: bool = False,
|
|
466
|
+
window_style: str | None | object = _USE_SETTINGS,
|
|
467
|
+
**kwargs: Unpack[TkKwargs],
|
|
468
|
+
) -> None:
|
|
469
|
+
"""Initializes the application window.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
title: The text to display in the window's title bar. This
|
|
473
|
+
overrides the `app_name` in `settings` if provided.
|
|
474
|
+
theme: The name of the theme to use. This overrides the `theme`
|
|
475
|
+
in `settings` if provided.
|
|
476
|
+
icon: A PhotoImage or file path used for the window's icon.
|
|
477
|
+
If None, the default bootstack.png icon is used.
|
|
478
|
+
settings: A dictionary or `AppSettings` object containing
|
|
479
|
+
application-wide settings. If not provided, default settings
|
|
480
|
+
are used.
|
|
481
|
+
localize: The localization mode for the application. Can be
|
|
482
|
+
'auto', `True`, or `False`. This overrides the `localize_mode`
|
|
483
|
+
in `settings`.
|
|
484
|
+
size: A tuple specifying the window's initial width and height.
|
|
485
|
+
position: A tuple specifying the window's initial x and y
|
|
486
|
+
coordinates on the screen.
|
|
487
|
+
minsize: A tuple specifying the window's minimum width and height.
|
|
488
|
+
maxsize: A tuple specifying the window's maximum width and height.
|
|
489
|
+
resizable: A tuple of booleans specifying whether the window can
|
|
490
|
+
be resized horizontally and vertically.
|
|
491
|
+
scaling: The DPI scaling factor for the window. If `None`,
|
|
492
|
+
automatic scaling is used.
|
|
493
|
+
hdpi: If `True`, enables high-DPI awareness for the application.
|
|
494
|
+
alpha: The window's transparency level, from 0.0 (fully
|
|
495
|
+
transparent) to 1.0 (fully opaque).
|
|
496
|
+
transient: The parent window for this window.
|
|
497
|
+
override_redirect: If `True`, creates a window without standard
|
|
498
|
+
decorations (title bar, borders, etc.).
|
|
499
|
+
**kwargs: Additional keyword arguments to pass to the
|
|
500
|
+
underlying `tkinter.Tk` constructor.
|
|
501
|
+
"""
|
|
502
|
+
# --- Settings ---------------------------------------------------
|
|
503
|
+
if settings is None:
|
|
504
|
+
self.settings = AppSettings()
|
|
505
|
+
elif isinstance(settings, AppSettings):
|
|
506
|
+
self.settings = settings
|
|
507
|
+
else:
|
|
508
|
+
self.settings = AppSettings(**settings)
|
|
509
|
+
|
|
510
|
+
# App-level overrides from ctor
|
|
511
|
+
if theme is not None:
|
|
512
|
+
self.settings.theme = theme
|
|
513
|
+
if title is not None:
|
|
514
|
+
self.settings.app_name = title
|
|
515
|
+
|
|
516
|
+
# If app_name is still None, give it a sensible default
|
|
517
|
+
if self.settings.app_name is None:
|
|
518
|
+
self.settings.app_name = "bootstack"
|
|
519
|
+
|
|
520
|
+
if localize is not None:
|
|
521
|
+
self.settings.localize_mode = localize
|
|
522
|
+
|
|
523
|
+
# --- Window options ---------------------------------------------
|
|
524
|
+
self._size = size
|
|
525
|
+
self._position = position
|
|
526
|
+
self._minsize = minsize
|
|
527
|
+
self._maxsize = maxsize
|
|
528
|
+
self._resizable = resizable
|
|
529
|
+
self._scaling = scaling
|
|
530
|
+
self._hdpi = hdpi
|
|
531
|
+
self._transient = transient
|
|
532
|
+
self._alpha = alpha
|
|
533
|
+
self._override_redirect = override_redirect
|
|
534
|
+
|
|
535
|
+
# Register app
|
|
536
|
+
if not has_current_app():
|
|
537
|
+
set_current_app(self)
|
|
538
|
+
|
|
539
|
+
# Enable HDPI before creating window
|
|
540
|
+
if self._hdpi:
|
|
541
|
+
enable_high_dpi_awareness()
|
|
542
|
+
|
|
543
|
+
# Initialize Tk
|
|
544
|
+
tkinter.Tk.__init__(self, **kwargs)
|
|
545
|
+
self.withdraw() # hide immediately until ready to show.
|
|
546
|
+
|
|
547
|
+
# Setup window system info
|
|
548
|
+
self.winsys: str = self.tk.call('tk', 'windowingsystem')
|
|
549
|
+
|
|
550
|
+
# Apply theme (use resolved settings.theme). If the app opts into
|
|
551
|
+
# following system appearance, override the explicit theme with the
|
|
552
|
+
# mode-appropriate one and bind a listener to track future toggles.
|
|
553
|
+
from bootstack.style.style import set_theme
|
|
554
|
+
initial_theme = self.settings.theme
|
|
555
|
+
if self.settings.follow_system_appearance and self._is_dark_capable_platform():
|
|
556
|
+
initial_theme = (
|
|
557
|
+
self.settings.dark_theme
|
|
558
|
+
if self._system_is_dark()
|
|
559
|
+
else self.settings.light_theme
|
|
560
|
+
)
|
|
561
|
+
set_theme(initial_theme)
|
|
562
|
+
|
|
563
|
+
if self.settings.follow_system_appearance and self._is_dark_capable_platform():
|
|
564
|
+
self._bind_system_appearance_tracking()
|
|
565
|
+
|
|
566
|
+
# Install macOS-native close/quit/hide handlers when requested.
|
|
567
|
+
if self.winsys == 'aqua' and self.settings.macos_quit_behavior == 'native':
|
|
568
|
+
self._install_macos_quit_handlers()
|
|
569
|
+
|
|
570
|
+
# macOS-only polish: sync tk appname so the apple menu's first
|
|
571
|
+
# entry shows the app name (otherwise Tk uses the interpreter's
|
|
572
|
+
# name, typically "Python"), and bind Cmd+W to fire the standard
|
|
573
|
+
# WM_DELETE_WINDOW protocol so the close shortcut behaves like a
|
|
574
|
+
# close-button click.
|
|
575
|
+
if self.winsys == 'aqua':
|
|
576
|
+
if self.settings.app_name:
|
|
577
|
+
try:
|
|
578
|
+
self.tk.call('tk', 'appname', self.settings.app_name)
|
|
579
|
+
except tkinter.TclError:
|
|
580
|
+
pass
|
|
581
|
+
self.bind('<Command-w>', self._trigger_close, add='+')
|
|
582
|
+
|
|
583
|
+
# Initialize the localization bridge so MessageCatalog.translate()
|
|
584
|
+
# and <<LocaleChanged>> are available throughout the app.
|
|
585
|
+
MessageCatalog.init(
|
|
586
|
+
locales_dir=None,
|
|
587
|
+
domain="bootstack",
|
|
588
|
+
default_locale=self.settings.locale or DEFAULT_LOCALE,
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
# Apply HDPI scaling after window creation
|
|
592
|
+
if self._hdpi:
|
|
593
|
+
if self._scaling is None:
|
|
594
|
+
enable_high_dpi_awareness(self, 'auto')
|
|
595
|
+
else:
|
|
596
|
+
enable_high_dpi_awareness(self, self._scaling)
|
|
597
|
+
|
|
598
|
+
# Setup icon
|
|
599
|
+
self._setup_icon(icon, default_icon_enabled=True)
|
|
600
|
+
|
|
601
|
+
# If the app opted into state restoration, override the explicit
|
|
602
|
+
# size/position with whatever was saved last time. The saved geometry
|
|
603
|
+
# is a single 'WxH+X+Y' string applied after _setup_window so it
|
|
604
|
+
# supersedes both kwargs and centering logic.
|
|
605
|
+
saved_geometry = None
|
|
606
|
+
if self.settings.remember_window_state:
|
|
607
|
+
saved_geometry = self._read_saved_geometry()
|
|
608
|
+
|
|
609
|
+
# Setup window using BaseWindow
|
|
610
|
+
# Use window_style from parameter if explicitly provided, otherwise use settings
|
|
611
|
+
_window_style = self.settings.window_style if window_style is _USE_SETTINGS else window_style
|
|
612
|
+
self._setup_window(
|
|
613
|
+
title=self.settings.app_name,
|
|
614
|
+
size=self._size,
|
|
615
|
+
position=self._position,
|
|
616
|
+
minsize=self._minsize,
|
|
617
|
+
maxsize=self._maxsize,
|
|
618
|
+
resizable=self._resizable,
|
|
619
|
+
transient=self._transient,
|
|
620
|
+
overrideredirect=self._override_redirect,
|
|
621
|
+
alpha=self._alpha,
|
|
622
|
+
window_style=_window_style,
|
|
623
|
+
)
|
|
624
|
+
|
|
625
|
+
if saved_geometry is not None:
|
|
626
|
+
self._apply_saved_geometry(saved_geometry)
|
|
627
|
+
|
|
628
|
+
# Apply bootstack-specific bindings
|
|
629
|
+
apply_class_bindings(self)
|
|
630
|
+
apply_all_bindings(self)
|
|
631
|
+
|
|
632
|
+
def mainloop(self, n=0) -> None:
|
|
633
|
+
"""Start the application event loop
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
n (int): A threshold that keeps the window open if at least n windows is open. This is an archaic c-level
|
|
637
|
+
detail that should not be adjusted unless you have a specific reason.
|
|
638
|
+
"""
|
|
639
|
+
self.place_window_center()
|
|
640
|
+
self.show()
|
|
641
|
+
super().mainloop(n=n)
|
|
642
|
+
|
|
643
|
+
def close(self) -> None:
|
|
644
|
+
"""Close the application window (destroys the Tk root)"""
|
|
645
|
+
clear_current_app(self)
|
|
646
|
+
self.quit()
|
|
647
|
+
|
|
648
|
+
def destroy(self) -> None:
|
|
649
|
+
"""Destroys the window and all its children."""
|
|
650
|
+
if self.settings.remember_window_state:
|
|
651
|
+
try:
|
|
652
|
+
if self.winfo_exists():
|
|
653
|
+
self._save_window_state()
|
|
654
|
+
except tkinter.TclError:
|
|
655
|
+
pass
|
|
656
|
+
clear_current_app(self)
|
|
657
|
+
super().destroy()
|
|
658
|
+
|
|
659
|
+
# ----- Window state persistence ------------------------------------------
|
|
660
|
+
|
|
661
|
+
def _state_file_path(self):
|
|
662
|
+
"""Return the path where this App's window state is persisted."""
|
|
663
|
+
from pathlib import Path
|
|
664
|
+
import os
|
|
665
|
+
if self.settings.state_path:
|
|
666
|
+
return Path(self.settings.state_path)
|
|
667
|
+
# Per-platform config dir; leaf includes app_name to avoid collisions
|
|
668
|
+
# between multiple bootstack apps installed on the same machine.
|
|
669
|
+
app_name = self.settings.app_name or 'bootstack'
|
|
670
|
+
if sys.platform == 'darwin':
|
|
671
|
+
base = Path.home() / 'Library' / 'Application Support'
|
|
672
|
+
elif sys.platform == 'win32':
|
|
673
|
+
base = Path(os.environ.get('APPDATA') or (Path.home() / 'AppData' / 'Roaming'))
|
|
674
|
+
else:
|
|
675
|
+
base = Path(os.environ.get('XDG_CONFIG_HOME') or (Path.home() / '.config'))
|
|
676
|
+
return base / app_name / 'window_state.json'
|
|
677
|
+
|
|
678
|
+
def _read_saved_geometry(self):
|
|
679
|
+
"""Return the persisted 'WxH+X+Y' string, or None if not present/valid."""
|
|
680
|
+
import json
|
|
681
|
+
path = self._state_file_path()
|
|
682
|
+
try:
|
|
683
|
+
raw = path.read_text()
|
|
684
|
+
except (FileNotFoundError, OSError):
|
|
685
|
+
return None
|
|
686
|
+
try:
|
|
687
|
+
data = json.loads(raw)
|
|
688
|
+
except json.JSONDecodeError:
|
|
689
|
+
return None
|
|
690
|
+
geo = data.get('geometry') if isinstance(data, dict) else None
|
|
691
|
+
return geo if isinstance(geo, str) and geo else None
|
|
692
|
+
|
|
693
|
+
def _apply_saved_geometry(self, geometry: str) -> None:
|
|
694
|
+
"""Restore a saved 'WxH+X+Y' string, clamping off-screen positions."""
|
|
695
|
+
try:
|
|
696
|
+
self.geometry(geometry)
|
|
697
|
+
self.update_idletasks()
|
|
698
|
+
except tkinter.TclError:
|
|
699
|
+
return
|
|
700
|
+
# If the saved position is on a now-disconnected monitor, drag it
|
|
701
|
+
# back into a visible region so the window doesn't open invisibly.
|
|
702
|
+
try:
|
|
703
|
+
from bootstack.runtime.window_utilities import WindowPositioning
|
|
704
|
+
x, y = self.winfo_x(), self.winfo_y()
|
|
705
|
+
x, y = WindowPositioning.ensure_on_screen(self, x, y)
|
|
706
|
+
self.geometry(f'+{x}+{y}')
|
|
707
|
+
except Exception:
|
|
708
|
+
pass
|
|
709
|
+
|
|
710
|
+
def _save_window_state(self) -> None:
|
|
711
|
+
"""Write the current geometry to the state file."""
|
|
712
|
+
import json
|
|
713
|
+
path = self._state_file_path()
|
|
714
|
+
try:
|
|
715
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
716
|
+
except OSError:
|
|
717
|
+
return
|
|
718
|
+
try:
|
|
719
|
+
geo = self.geometry()
|
|
720
|
+
path.write_text(json.dumps({'geometry': geo}))
|
|
721
|
+
except (OSError, tkinter.TclError):
|
|
722
|
+
pass
|
|
723
|
+
|
|
724
|
+
# ----- System appearance tracking ----------------------------------------
|
|
725
|
+
|
|
726
|
+
def _is_dark_capable_platform(self) -> bool:
|
|
727
|
+
"""Return True if the current windowing system reports an appearance.
|
|
728
|
+
|
|
729
|
+
Currently only macOS exposes a Tk-level light/dark signal
|
|
730
|
+
(`<<TkSystemAppearanceChanged>>` and `MacWindowStyle isDark`).
|
|
731
|
+
Win/Linux apps that want to track system theme need their own
|
|
732
|
+
OS-specific hook, which is out of scope for this method.
|
|
733
|
+
"""
|
|
734
|
+
return getattr(self, 'winsys', None) == 'aqua'
|
|
735
|
+
|
|
736
|
+
def _system_is_dark(self) -> bool:
|
|
737
|
+
"""Return True if the OS is currently in dark mode (macOS)."""
|
|
738
|
+
try:
|
|
739
|
+
return bool(int(self.tk.call(
|
|
740
|
+
'::tk::unsupported::MacWindowStyle', 'isDark', self,
|
|
741
|
+
)))
|
|
742
|
+
except tkinter.TclError:
|
|
743
|
+
return False
|
|
744
|
+
|
|
745
|
+
def _bind_system_appearance_tracking(self) -> None:
|
|
746
|
+
"""Switch themes when the OS toggles between light and dark mode."""
|
|
747
|
+
def on_appearance_changed(_event=None):
|
|
748
|
+
if not self.settings.follow_system_appearance:
|
|
749
|
+
return
|
|
750
|
+
from bootstack.style.style import set_theme
|
|
751
|
+
set_theme(
|
|
752
|
+
self.settings.dark_theme
|
|
753
|
+
if self._system_is_dark()
|
|
754
|
+
else self.settings.light_theme
|
|
755
|
+
)
|
|
756
|
+
|
|
757
|
+
try:
|
|
758
|
+
self.bind('<<TkSystemAppearanceChanged>>', on_appearance_changed, add='+')
|
|
759
|
+
except tkinter.TclError:
|
|
760
|
+
pass
|
|
761
|
+
|
|
762
|
+
# ----- macOS Quit/Close conventions --------------------------------------
|
|
763
|
+
|
|
764
|
+
def _install_macos_quit_handlers(self) -> None:
|
|
765
|
+
"""Wire macOS-native close/quit/hide gestures.
|
|
766
|
+
|
|
767
|
+
macOS convention: clicking the window close button hides the app
|
|
768
|
+
(it stays in the Dock and Cmd+Tab list) rather than destroying it.
|
|
769
|
+
Cmd+Q (and Dock → Quit) is what actually quits. Cmd+H hides the
|
|
770
|
+
app, and clicking the Dock icon brings the main window back.
|
|
771
|
+
|
|
772
|
+
This method installs the matching Tk handlers so apps that opt
|
|
773
|
+
into `macos_quit_behavior='native'` behave correctly without
|
|
774
|
+
each app duplicating the boilerplate.
|
|
775
|
+
"""
|
|
776
|
+
# Close button → withdraw. We replace the protocol unconditionally
|
|
777
|
+
# because the default Tk behavior on close is to destroy, which is
|
|
778
|
+
# wrong on Mac. Apps that want to hook close should call
|
|
779
|
+
# `app.on_close(my_handler)` after construction; that overrides
|
|
780
|
+
# this default.
|
|
781
|
+
self.protocol('WM_DELETE_WINDOW', self.withdraw)
|
|
782
|
+
|
|
783
|
+
# Cmd+Q / Dock → Quit fire <<AppleQuit>>; that's the real quit signal.
|
|
784
|
+
try:
|
|
785
|
+
self.bind('<<AppleQuit>>', lambda _e: self.destroy(), add='+')
|
|
786
|
+
except tkinter.TclError:
|
|
787
|
+
pass
|
|
788
|
+
|
|
789
|
+
# Cmd+H hides the app.
|
|
790
|
+
try:
|
|
791
|
+
self.bind('<<Apple-Hide>>', lambda _e: self.withdraw(), add='+')
|
|
792
|
+
except tkinter.TclError:
|
|
793
|
+
pass
|
|
794
|
+
|
|
795
|
+
# Clicking the Dock icon when no window is visible fires
|
|
796
|
+
# <<Apple-ReopenApplication>>; bring the main window back.
|
|
797
|
+
try:
|
|
798
|
+
self.bind(
|
|
799
|
+
'<<Apple-ReopenApplication>>',
|
|
800
|
+
lambda _e: self.deiconify(),
|
|
801
|
+
add='+',
|
|
802
|
+
)
|
|
803
|
+
except tkinter.TclError:
|
|
804
|
+
pass
|
|
805
|
+
|
|
806
|
+
def _trigger_close(self, _event=None) -> str:
|
|
807
|
+
"""Invoke the registered WM_DELETE_WINDOW handler for this window.
|
|
808
|
+
|
|
809
|
+
Lets `Cmd+W` and any other "close this window" gesture flow
|
|
810
|
+
through the same code path as clicking the close button, so a
|
|
811
|
+
custom `app.on_close(handler)` is honored.
|
|
812
|
+
"""
|
|
813
|
+
try:
|
|
814
|
+
handler_script = self.tk.call(
|
|
815
|
+
'wm', 'protocol', self._w, 'WM_DELETE_WINDOW',
|
|
816
|
+
)
|
|
817
|
+
except tkinter.TclError:
|
|
818
|
+
handler_script = ''
|
|
819
|
+
if handler_script:
|
|
820
|
+
try:
|
|
821
|
+
self.tk.eval(handler_script)
|
|
822
|
+
except tkinter.TclError:
|
|
823
|
+
pass
|
|
824
|
+
else:
|
|
825
|
+
# No handler registered — fall back to the platform-correct
|
|
826
|
+
# default for this app: withdraw on native macOS, destroy
|
|
827
|
+
# otherwise.
|
|
828
|
+
if (
|
|
829
|
+
self.winsys == 'aqua'
|
|
830
|
+
and self.settings.macos_quit_behavior == 'native'
|
|
831
|
+
):
|
|
832
|
+
self.withdraw()
|
|
833
|
+
else:
|
|
834
|
+
self.destroy()
|
|
835
|
+
return 'break'
|
|
836
|
+
|
|
837
|
+
# ----- macOS apple menu hooks --------------------------------------------
|
|
838
|
+
|
|
839
|
+
def on_about(self, handler: Callable[[], Any]) -> None:
|
|
840
|
+
"""Register a handler for the macOS "About <App>" menu item.
|
|
841
|
+
|
|
842
|
+
Tk on Aqua calls `::tk::mac::standardAboutPanel` when the user
|
|
843
|
+
picks About from the application menu. This method overrides
|
|
844
|
+
that proc with the supplied Python callable. No-op on Win/Linux,
|
|
845
|
+
where there's no equivalent system menu.
|
|
846
|
+
|
|
847
|
+
Args:
|
|
848
|
+
handler: Zero-argument callable invoked when the user picks
|
|
849
|
+
About from the apple menu.
|
|
850
|
+
"""
|
|
851
|
+
if self.winsys != 'aqua':
|
|
852
|
+
return
|
|
853
|
+
try:
|
|
854
|
+
self.tk.createcommand('::tk::mac::standardAboutPanel', handler)
|
|
855
|
+
except tkinter.TclError:
|
|
856
|
+
pass
|
|
857
|
+
|
|
858
|
+
def on_preferences(self, handler: Callable[[], Any]) -> None:
|
|
859
|
+
"""Register a handler for the macOS "Preferences…" menu item.
|
|
860
|
+
|
|
861
|
+
Tk on Aqua calls `::tk::mac::ShowPreferences` when the user picks
|
|
862
|
+
Preferences (Cmd+,) from the application menu. This method
|
|
863
|
+
overrides that proc with the supplied Python callable. No-op on
|
|
864
|
+
Win/Linux.
|
|
865
|
+
|
|
866
|
+
Args:
|
|
867
|
+
handler: Zero-argument callable invoked when the user picks
|
|
868
|
+
Preferences from the apple menu.
|
|
869
|
+
"""
|
|
870
|
+
if self.winsys != 'aqua':
|
|
871
|
+
return
|
|
872
|
+
try:
|
|
873
|
+
self.tk.createcommand('::tk::mac::ShowPreferences', handler)
|
|
874
|
+
except tkinter.TclError:
|
|
875
|
+
pass
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
# Backward compatibility alias
|
|
879
|
+
Window = App
|