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,18 @@
|
|
|
1
|
+
"""Cross-platform menu bar.
|
|
2
|
+
|
|
3
|
+
A single platform-neutral menu *model* (`MenuModel` / `MenuGroup` / `MenuItem`)
|
|
4
|
+
is the source of truth; platform renderers consume it — a themed in-window strip
|
|
5
|
+
on Windows/Linux and the native global menu bar on macOS.
|
|
6
|
+
|
|
7
|
+
See `docs/_dev/menu-redesign.md` for the design.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from bootstack.widgets._impl.composites.menu.model import (
|
|
12
|
+
MenuGroup,
|
|
13
|
+
MenuItem,
|
|
14
|
+
MenuItemType,
|
|
15
|
+
MenuModel,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = ["MenuModel", "MenuGroup", "MenuItem", "MenuItemType"]
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
"""Platform-neutral menu model.
|
|
2
|
+
|
|
3
|
+
The menu bar is built up as data — an ordered list of `MenuGroup`s (File,
|
|
4
|
+
Edit, …), each holding a flat list of `MenuItem`s. This is the single source of
|
|
5
|
+
truth the platform renderers consume. The model is **single layer**: a group
|
|
6
|
+
holds items, and items never hold sub-items (no sub-submenus).
|
|
7
|
+
|
|
8
|
+
The model is pure data plus shortcut wiring:
|
|
9
|
+
|
|
10
|
+
- **Display** — every item resolves its `shortcut` to a platform-appropriate
|
|
11
|
+
accelerator string (`shortcut_display`) via the `Shortcuts` service.
|
|
12
|
+
- **Binding** — `bind_shortcuts()` registers *pattern* shortcuts
|
|
13
|
+
(e.g. `'Mod+S'`) with the `Shortcuts` service and binds them to a window so
|
|
14
|
+
the keypress actually fires. *Registered keys* (e.g. `'save'`) are treated as
|
|
15
|
+
display-only (their keypress is bound wherever they were registered). This is
|
|
16
|
+
renderer-independent: `tk.Menu` accelerators never bind on their own, and the
|
|
17
|
+
themed strip doesn't either.
|
|
18
|
+
"""
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import itertools
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from typing import Any, Callable, Literal
|
|
24
|
+
|
|
25
|
+
from bootstack._runtime.shortcuts import (
|
|
26
|
+
classify_shortcut,
|
|
27
|
+
format_shortcut,
|
|
28
|
+
get_shortcuts,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
MenuItemType = Literal["action", "check", "radio", "separator"]
|
|
32
|
+
"""Kind of a menu item.
|
|
33
|
+
|
|
34
|
+
- `'action'` — a command that fires `on_click` when chosen.
|
|
35
|
+
- `'check'` — a toggle with an on/off state.
|
|
36
|
+
- `'radio'` — one choice within a named group.
|
|
37
|
+
- `'separator'` — a horizontal divider (carries no other fields).
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Process-wide counter so auto-registered pattern shortcuts get unique keys.
|
|
41
|
+
_shortcut_seq = itertools.count()
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class MenuItem:
|
|
46
|
+
"""A single entry in a menu group.
|
|
47
|
+
|
|
48
|
+
Only the fields relevant to `type` are meaningful; the rest stay at their
|
|
49
|
+
defaults. `icon` is honored by the Windows/Linux renderer and dropped on
|
|
50
|
+
macOS (native menus are text-only by convention).
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
type: MenuItemType = "action"
|
|
54
|
+
"""The kind of item — see `MenuItemType`."""
|
|
55
|
+
text: str | None = None
|
|
56
|
+
"""Display label. `None` only for separators."""
|
|
57
|
+
icon: str | None = None
|
|
58
|
+
"""Icon name (Windows/Linux only; ignored on macOS)."""
|
|
59
|
+
on_click: Callable[[], Any] | None = None
|
|
60
|
+
"""Called when the item is chosen (or its shortcut fires)."""
|
|
61
|
+
shortcut: str | None = None
|
|
62
|
+
"""A registered shortcut key (display-only) or a pattern (`'Mod+S'`)."""
|
|
63
|
+
disabled: bool = False
|
|
64
|
+
"""If `True`, the item is shown dimmed and cannot be chosen."""
|
|
65
|
+
checked: bool = False
|
|
66
|
+
"""Initial on/off state for a `'check'` item."""
|
|
67
|
+
value: Any = None
|
|
68
|
+
"""The value a `'radio'` item selects within its group."""
|
|
69
|
+
group: str | None = None
|
|
70
|
+
"""Radio group name — items sharing a name are mutually exclusive."""
|
|
71
|
+
key: str | None = None
|
|
72
|
+
"""Stable identifier for lookup; auto-derived from position if omitted."""
|
|
73
|
+
|
|
74
|
+
shortcut_display: str | None = field(default=None, init=False)
|
|
75
|
+
"""Resolved accelerator text for display (set in `__post_init__`)."""
|
|
76
|
+
|
|
77
|
+
def __post_init__(self) -> None:
|
|
78
|
+
self.shortcut_display = format_shortcut(self.shortcut) or None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class MenuGroup:
|
|
82
|
+
"""A top-level menu (e.g. File) holding a flat list of items.
|
|
83
|
+
|
|
84
|
+
Usable as a context manager so the builder reads naturally::
|
|
85
|
+
|
|
86
|
+
with model.add_menu("File") as file:
|
|
87
|
+
file.add_action("Open", shortcut="Mod+O", on_click=open_file)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
def __init__(self, text: str, *, key: str | None = None) -> None:
|
|
91
|
+
self.text = text
|
|
92
|
+
self.key = key or text
|
|
93
|
+
self.items: list[MenuItem] = []
|
|
94
|
+
self.on_change: Callable[[], Any] | None = None
|
|
95
|
+
"""Optional callback fired after any item is added (set by the host
|
|
96
|
+
facade to trigger a coalesced re-render)."""
|
|
97
|
+
|
|
98
|
+
# ----- builders -----
|
|
99
|
+
|
|
100
|
+
def add_action(
|
|
101
|
+
self,
|
|
102
|
+
text: str,
|
|
103
|
+
*,
|
|
104
|
+
icon: str | None = None,
|
|
105
|
+
on_click: Callable[[], Any] | None = None,
|
|
106
|
+
shortcut: str | None = None,
|
|
107
|
+
disabled: bool = False,
|
|
108
|
+
key: str | None = None,
|
|
109
|
+
) -> MenuItem:
|
|
110
|
+
"""Add a command item that fires `on_click` when chosen."""
|
|
111
|
+
return self._append(
|
|
112
|
+
MenuItem(
|
|
113
|
+
type="action",
|
|
114
|
+
text=text,
|
|
115
|
+
icon=icon,
|
|
116
|
+
on_click=on_click,
|
|
117
|
+
shortcut=shortcut,
|
|
118
|
+
disabled=disabled,
|
|
119
|
+
key=key,
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def add_check(
|
|
124
|
+
self,
|
|
125
|
+
text: str,
|
|
126
|
+
*,
|
|
127
|
+
checked: bool = False,
|
|
128
|
+
on_click: Callable[[], Any] | None = None,
|
|
129
|
+
shortcut: str | None = None,
|
|
130
|
+
disabled: bool = False,
|
|
131
|
+
key: str | None = None,
|
|
132
|
+
) -> MenuItem:
|
|
133
|
+
"""Add a toggle item with an on/off state."""
|
|
134
|
+
return self._append(
|
|
135
|
+
MenuItem(
|
|
136
|
+
type="check",
|
|
137
|
+
text=text,
|
|
138
|
+
checked=checked,
|
|
139
|
+
on_click=on_click,
|
|
140
|
+
shortcut=shortcut,
|
|
141
|
+
disabled=disabled,
|
|
142
|
+
key=key,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def add_radio(
|
|
147
|
+
self,
|
|
148
|
+
text: str,
|
|
149
|
+
*,
|
|
150
|
+
value: Any = None,
|
|
151
|
+
group: str | None = None,
|
|
152
|
+
on_click: Callable[[], Any] | None = None,
|
|
153
|
+
disabled: bool = False,
|
|
154
|
+
key: str | None = None,
|
|
155
|
+
) -> MenuItem:
|
|
156
|
+
"""Add a radio item — one choice within `group`."""
|
|
157
|
+
return self._append(
|
|
158
|
+
MenuItem(
|
|
159
|
+
type="radio",
|
|
160
|
+
text=text,
|
|
161
|
+
value=value if value is not None else text,
|
|
162
|
+
group=group or self.key,
|
|
163
|
+
on_click=on_click,
|
|
164
|
+
disabled=disabled,
|
|
165
|
+
key=key,
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
def add_divider(self, *, key: str | None = None) -> MenuItem:
|
|
170
|
+
"""Add a horizontal divider."""
|
|
171
|
+
return self._append(MenuItem(type="separator", key=key))
|
|
172
|
+
|
|
173
|
+
# ----- internals -----
|
|
174
|
+
|
|
175
|
+
def _append(self, item: MenuItem) -> MenuItem:
|
|
176
|
+
if item.key is None:
|
|
177
|
+
item.key = f"{self.key}.{len(self.items)}"
|
|
178
|
+
self.items.append(item)
|
|
179
|
+
if self.on_change is not None:
|
|
180
|
+
self.on_change()
|
|
181
|
+
return item
|
|
182
|
+
|
|
183
|
+
def __enter__(self) -> "MenuGroup":
|
|
184
|
+
return self
|
|
185
|
+
|
|
186
|
+
def __exit__(self, *exc: Any) -> bool:
|
|
187
|
+
return False
|
|
188
|
+
|
|
189
|
+
def __len__(self) -> int:
|
|
190
|
+
return len(self.items)
|
|
191
|
+
|
|
192
|
+
def __repr__(self) -> str:
|
|
193
|
+
return f"MenuGroup(text={self.text!r}, items={len(self.items)})"
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Item dict keys recognized by the declarative `load()` form.
|
|
197
|
+
_ITEM_KEYS = frozenset(
|
|
198
|
+
{"type", "text", "icon", "on_click", "shortcut", "disabled", "checked",
|
|
199
|
+
"value", "group", "key"}
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
class MenuModel:
|
|
204
|
+
"""The whole menu bar — an ordered list of `MenuGroup`s.
|
|
205
|
+
|
|
206
|
+
Build imperatively with `add_menu()`, or declaratively with `load()`; both
|
|
207
|
+
produce the same structure.
|
|
208
|
+
"""
|
|
209
|
+
|
|
210
|
+
def __init__(self) -> None:
|
|
211
|
+
self.groups: list[MenuGroup] = []
|
|
212
|
+
self._bound_window: Any = None
|
|
213
|
+
self._registered_keys: list[str] = []
|
|
214
|
+
|
|
215
|
+
# ----- building -----
|
|
216
|
+
|
|
217
|
+
def add_menu(self, text: str, *, key: str | None = None) -> MenuGroup:
|
|
218
|
+
"""Append a top-level menu and return it (context-manager capable)."""
|
|
219
|
+
group = MenuGroup(text, key=key)
|
|
220
|
+
self.groups.append(group)
|
|
221
|
+
return group
|
|
222
|
+
|
|
223
|
+
def load(self, spec: list[dict]) -> None:
|
|
224
|
+
"""Build the model from a declarative spec, replacing any contents.
|
|
225
|
+
|
|
226
|
+
Each top-level dict is a menu group: `{"text": ..., "items": [...]}`.
|
|
227
|
+
Each item dict carries `type` (default `'action'`) plus the fields for
|
|
228
|
+
that type. Items must NOT contain a nested `items` key — the model is
|
|
229
|
+
single layer.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
spec: List of group dicts.
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
ValueError: If an item nests its own `items` (no sub-submenus), or
|
|
236
|
+
an unknown item field is supplied.
|
|
237
|
+
"""
|
|
238
|
+
self.clear()
|
|
239
|
+
for raw_group in spec:
|
|
240
|
+
group = self.add_menu(raw_group["text"], key=raw_group.get("key"))
|
|
241
|
+
for raw_item in raw_group.get("items", []):
|
|
242
|
+
self._load_item(group, raw_item)
|
|
243
|
+
|
|
244
|
+
@staticmethod
|
|
245
|
+
def _load_item(group: MenuGroup, raw: dict) -> None:
|
|
246
|
+
if "items" in raw:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
"Menu items cannot contain nested 'items' — the menu bar is "
|
|
249
|
+
"single layer (no sub-submenus)."
|
|
250
|
+
)
|
|
251
|
+
unknown = set(raw) - _ITEM_KEYS
|
|
252
|
+
if unknown:
|
|
253
|
+
raise ValueError(
|
|
254
|
+
f"Unknown menu item field(s): {', '.join(sorted(unknown))}. "
|
|
255
|
+
f"Allowed: {', '.join(sorted(_ITEM_KEYS))}."
|
|
256
|
+
)
|
|
257
|
+
item_type = raw.get("type", "action")
|
|
258
|
+
if item_type in ("divider", "separator"):
|
|
259
|
+
group.add_divider(key=raw.get("key"))
|
|
260
|
+
elif item_type == "check":
|
|
261
|
+
group.add_check(
|
|
262
|
+
raw.get("text"),
|
|
263
|
+
checked=raw.get("checked", False),
|
|
264
|
+
on_click=raw.get("on_click"),
|
|
265
|
+
shortcut=raw.get("shortcut"),
|
|
266
|
+
disabled=raw.get("disabled", False),
|
|
267
|
+
key=raw.get("key"),
|
|
268
|
+
)
|
|
269
|
+
elif item_type == "radio":
|
|
270
|
+
group.add_radio(
|
|
271
|
+
raw.get("text"),
|
|
272
|
+
value=raw.get("value"),
|
|
273
|
+
group=raw.get("group"),
|
|
274
|
+
on_click=raw.get("on_click"),
|
|
275
|
+
disabled=raw.get("disabled", False),
|
|
276
|
+
key=raw.get("key"),
|
|
277
|
+
)
|
|
278
|
+
elif item_type == "action":
|
|
279
|
+
group.add_action(
|
|
280
|
+
raw.get("text"),
|
|
281
|
+
icon=raw.get("icon"),
|
|
282
|
+
on_click=raw.get("on_click"),
|
|
283
|
+
shortcut=raw.get("shortcut"),
|
|
284
|
+
disabled=raw.get("disabled", False),
|
|
285
|
+
key=raw.get("key"),
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
raise ValueError(
|
|
289
|
+
f"Unknown menu item type {item_type!r}. Use 'action', 'check', "
|
|
290
|
+
f"'radio', or 'divider'."
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
def clear(self) -> None:
|
|
294
|
+
"""Remove all groups and release any registered shortcuts."""
|
|
295
|
+
self.unbind_shortcuts()
|
|
296
|
+
self.groups = []
|
|
297
|
+
|
|
298
|
+
# ----- iteration -----
|
|
299
|
+
|
|
300
|
+
def iter_items(self):
|
|
301
|
+
"""Yield every `MenuItem` across all groups, in order."""
|
|
302
|
+
for group in self.groups:
|
|
303
|
+
yield from group.items
|
|
304
|
+
|
|
305
|
+
def __len__(self) -> int:
|
|
306
|
+
return len(self.groups)
|
|
307
|
+
|
|
308
|
+
def __iter__(self):
|
|
309
|
+
return iter(self.groups)
|
|
310
|
+
|
|
311
|
+
# ----- shortcuts -----
|
|
312
|
+
|
|
313
|
+
def bind_shortcuts(self, window: Any = None) -> None:
|
|
314
|
+
"""Register pattern shortcuts and bind them so keypresses fire.
|
|
315
|
+
|
|
316
|
+
Idempotent: previously registered pattern shortcuts are released
|
|
317
|
+
first, then re-registered from the current model. Items whose
|
|
318
|
+
`shortcut` is a *registered key* or a *literal* are left alone (their
|
|
319
|
+
keypress is bound elsewhere, or the text is display-only). Items
|
|
320
|
+
without an `on_click` are skipped — there would be nothing to fire.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
window: Window (App/Toplevel) to bind the shortcuts to. If omitted,
|
|
324
|
+
the last window passed to `bind_shortcuts` is reused, if any.
|
|
325
|
+
Shortcuts still register without a window; they bind once one
|
|
326
|
+
is known.
|
|
327
|
+
"""
|
|
328
|
+
self.unbind_shortcuts()
|
|
329
|
+
if window is not None:
|
|
330
|
+
self._bound_window = window
|
|
331
|
+
|
|
332
|
+
shortcuts = get_shortcuts()
|
|
333
|
+
for item in self.iter_items():
|
|
334
|
+
if not item.shortcut or item.on_click is None:
|
|
335
|
+
continue
|
|
336
|
+
if classify_shortcut(item.shortcut) != "pattern":
|
|
337
|
+
continue # registered key or literal — bind handled elsewhere
|
|
338
|
+
reg_key = f"__menu_{next(_shortcut_seq)}"
|
|
339
|
+
shortcuts.register(reg_key, item.shortcut, item.on_click)
|
|
340
|
+
self._registered_keys.append(reg_key)
|
|
341
|
+
|
|
342
|
+
if self._bound_window is not None:
|
|
343
|
+
shortcuts.bind_to(self._bound_window)
|
|
344
|
+
|
|
345
|
+
def unbind_shortcuts(self) -> None:
|
|
346
|
+
"""Release every pattern shortcut this model registered."""
|
|
347
|
+
if not self._registered_keys:
|
|
348
|
+
return
|
|
349
|
+
shortcuts = get_shortcuts()
|
|
350
|
+
for reg_key in self._registered_keys:
|
|
351
|
+
try:
|
|
352
|
+
shortcuts.unregister(reg_key)
|
|
353
|
+
except KeyError:
|
|
354
|
+
pass
|
|
355
|
+
self._registered_keys = []
|
|
356
|
+
|
|
357
|
+
def __repr__(self) -> str:
|
|
358
|
+
return f"MenuModel(groups={[g.text for g in self.groups]})"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""macOS renderer — the native global menu bar.
|
|
2
|
+
|
|
3
|
+
Builds a flat `tk.Menu` cascade structure from a `MenuModel` and assigns it as
|
|
4
|
+
the window's menubar (`root['menu']`). On macOS this materializes as the native
|
|
5
|
+
global menu bar at the top of the screen.
|
|
6
|
+
|
|
7
|
+
Deliberately thin (this is why `MenuManager` is retired): single layer, **no
|
|
8
|
+
icons** (native Mac menus are text-only by convention), label translation for
|
|
9
|
+
semantic keys, and accelerator text for display. Actual key binding is owned by
|
|
10
|
+
`MenuModel.bind_shortcuts`, not the menu — `tk.Menu` accelerators never fire on
|
|
11
|
+
their own.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import tkinter as tk
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from bootstack._runtime.shortcuts import tk_aqua_accelerator
|
|
19
|
+
from bootstack.widgets._impl.composites.menu.model import MenuItem, MenuModel
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _translate(text: str | None) -> str:
|
|
23
|
+
"""Resolve a semantic message key; pass plain text through unchanged."""
|
|
24
|
+
if not text:
|
|
25
|
+
return ""
|
|
26
|
+
try:
|
|
27
|
+
from bootstack.i18n import MessageCatalog
|
|
28
|
+
|
|
29
|
+
return MessageCatalog.translate(text) or text
|
|
30
|
+
except Exception:
|
|
31
|
+
return text
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NativeMenuBar:
|
|
35
|
+
"""Native menubar (macOS) built from a `MenuModel` and attached to a window.
|
|
36
|
+
|
|
37
|
+
Call `rebuild()` to re-render after the model changes; `destroy()` detaches
|
|
38
|
+
the menubar from the window.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
def __init__(self, root: Any, model: MenuModel) -> None:
|
|
42
|
+
self._root = root
|
|
43
|
+
self._model = model
|
|
44
|
+
self._menubar: tk.Menu | None = None
|
|
45
|
+
# Strong refs so Tk variables backing check/radio items aren't GC'd
|
|
46
|
+
# while the menu holds them.
|
|
47
|
+
self._var_refs: list[Any] = []
|
|
48
|
+
self.rebuild()
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def model(self) -> MenuModel:
|
|
52
|
+
"""The menu model this menubar renders."""
|
|
53
|
+
return self._model
|
|
54
|
+
|
|
55
|
+
def set_model(self, model: MenuModel) -> None:
|
|
56
|
+
"""Swap in a new model and re-render."""
|
|
57
|
+
self._model = model
|
|
58
|
+
self.rebuild()
|
|
59
|
+
|
|
60
|
+
def rebuild(self) -> None:
|
|
61
|
+
"""Rebuild the cascade structure and (re)attach it to the window."""
|
|
62
|
+
menubar = tk.Menu(self._root, tearoff=0)
|
|
63
|
+
self._var_refs = []
|
|
64
|
+
radio_vars: dict[str, tk.StringVar] = {}
|
|
65
|
+
|
|
66
|
+
for group in self._model:
|
|
67
|
+
submenu = tk.Menu(menubar, tearoff=0)
|
|
68
|
+
for item in group.items:
|
|
69
|
+
self._add_item(submenu, item, radio_vars)
|
|
70
|
+
menubar.add_cascade(label=_translate(group.text), menu=submenu)
|
|
71
|
+
|
|
72
|
+
self._root["menu"] = menubar
|
|
73
|
+
self._menubar = menubar
|
|
74
|
+
|
|
75
|
+
def _add_item(
|
|
76
|
+
self,
|
|
77
|
+
submenu: tk.Menu,
|
|
78
|
+
item: MenuItem,
|
|
79
|
+
radio_vars: dict[str, tk.StringVar],
|
|
80
|
+
) -> None:
|
|
81
|
+
if item.type == "separator":
|
|
82
|
+
submenu.add_separator()
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
if item.type == "check":
|
|
86
|
+
var = tk.BooleanVar(value=item.checked)
|
|
87
|
+
self._var_refs.append(var)
|
|
88
|
+
submenu.add_checkbutton(
|
|
89
|
+
label=_translate(item.text),
|
|
90
|
+
variable=var,
|
|
91
|
+
command=item.on_click,
|
|
92
|
+
state="disabled" if item.disabled else "normal",
|
|
93
|
+
)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
if item.type == "radio":
|
|
97
|
+
var = radio_vars.setdefault(item.group or "", tk.StringVar())
|
|
98
|
+
if var not in self._var_refs:
|
|
99
|
+
self._var_refs.append(var)
|
|
100
|
+
submenu.add_radiobutton(
|
|
101
|
+
label=_translate(item.text),
|
|
102
|
+
variable=var,
|
|
103
|
+
value=item.value,
|
|
104
|
+
command=item.on_click,
|
|
105
|
+
state="disabled" if item.disabled else "normal",
|
|
106
|
+
)
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
# action
|
|
110
|
+
opts: dict[str, Any] = {
|
|
111
|
+
"label": _translate(item.text),
|
|
112
|
+
"command": item.on_click,
|
|
113
|
+
"state": "disabled" if item.disabled else "normal",
|
|
114
|
+
}
|
|
115
|
+
# Native menus need the word-form accelerator (Tk renders the glyphs);
|
|
116
|
+
# the pre-symbolized display would show the modifier but drop the key.
|
|
117
|
+
acc = tk_aqua_accelerator(item.shortcut)
|
|
118
|
+
if acc:
|
|
119
|
+
opts["accelerator"] = acc
|
|
120
|
+
submenu.add_command(**opts)
|
|
121
|
+
|
|
122
|
+
def destroy(self) -> None:
|
|
123
|
+
"""Detach the menubar from the window and drop it."""
|
|
124
|
+
try:
|
|
125
|
+
self._root["menu"] = ""
|
|
126
|
+
except Exception:
|
|
127
|
+
pass
|
|
128
|
+
if self._menubar is not None:
|
|
129
|
+
try:
|
|
130
|
+
self._menubar.destroy()
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
self._menubar = None
|
|
134
|
+
self._var_refs = []
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Windows/Linux renderer — a themed in-window menu-bar strip.
|
|
2
|
+
|
|
3
|
+
Builds a horizontal strip of trigger buttons (one per `MenuGroup`) from a
|
|
4
|
+
`MenuModel`. Each trigger is a `DropdownButton` whose popdown is the themed
|
|
5
|
+
`_ToplevelContextMenu` backend, so theme tokens, density, and icons all apply.
|
|
6
|
+
macOS uses the native renderer instead (`render_native`).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from tkinter import StringVar
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from bootstack.widgets._impl.composites.contextmenu import ContextMenuItem
|
|
14
|
+
from bootstack.widgets._impl.composites.dropdownbutton import DropdownButton
|
|
15
|
+
from bootstack.widgets._impl.primitives.packframe import PackFrame
|
|
16
|
+
from bootstack.widgets._impl.composites.menu.model import MenuItem, MenuModel
|
|
17
|
+
from bootstack.widgets.types import Master, WidgetDensity
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def menu_item_to_context_item(
|
|
21
|
+
item: MenuItem, radio_vars: dict[str, StringVar]
|
|
22
|
+
) -> ContextMenuItem:
|
|
23
|
+
"""Translate a model `MenuItem` into a `ContextMenuItem`.
|
|
24
|
+
|
|
25
|
+
Maps the public type names to the ContextMenu backend's Tk-derived names
|
|
26
|
+
(`action`->`command`, `check`->`checkbutton`, `radio`->`radiobutton`). The
|
|
27
|
+
raw `shortcut` is forwarded for display only — actual key binding is owned by
|
|
28
|
+
`MenuModel.bind_shortcuts`. `radio_vars` holds one shared `StringVar` per
|
|
29
|
+
radio-group name so items in a group are mutually exclusive; it is mutated in
|
|
30
|
+
place as new groups are seen.
|
|
31
|
+
"""
|
|
32
|
+
if item.type == "separator":
|
|
33
|
+
return ContextMenuItem("separator", key=item.key)
|
|
34
|
+
|
|
35
|
+
if item.type == "check":
|
|
36
|
+
return ContextMenuItem(
|
|
37
|
+
"checkbutton",
|
|
38
|
+
text=item.text,
|
|
39
|
+
value=item.checked,
|
|
40
|
+
command=item.on_click,
|
|
41
|
+
key=item.key,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if item.type == "radio":
|
|
45
|
+
var = radio_vars.setdefault(item.group or "", StringVar())
|
|
46
|
+
return ContextMenuItem(
|
|
47
|
+
"radiobutton",
|
|
48
|
+
text=item.text,
|
|
49
|
+
value=item.value,
|
|
50
|
+
variable=var,
|
|
51
|
+
command=item.on_click,
|
|
52
|
+
disabled=item.disabled,
|
|
53
|
+
key=item.key,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# action
|
|
57
|
+
kwargs: dict[str, Any] = {
|
|
58
|
+
"text": item.text,
|
|
59
|
+
"command": item.on_click,
|
|
60
|
+
"disabled": item.disabled,
|
|
61
|
+
"key": item.key,
|
|
62
|
+
}
|
|
63
|
+
if item.icon:
|
|
64
|
+
kwargs["icon"] = item.icon
|
|
65
|
+
if item.shortcut:
|
|
66
|
+
kwargs["shortcut"] = item.shortcut
|
|
67
|
+
return ContextMenuItem("command", **kwargs)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class ThemedMenuBar(PackFrame):
|
|
71
|
+
"""Themed menu-bar strip (Windows/Linux) rendered from a `MenuModel`.
|
|
72
|
+
|
|
73
|
+
The strip is a horizontal row of dropdown triggers. Call `rebuild()` to
|
|
74
|
+
re-render after the model changes.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
master: Master = None,
|
|
80
|
+
model: MenuModel | None = None,
|
|
81
|
+
*,
|
|
82
|
+
density: WidgetDensity = "compact",
|
|
83
|
+
surface: str = "chrome",
|
|
84
|
+
**kwargs: Any,
|
|
85
|
+
) -> None:
|
|
86
|
+
kwargs.setdefault("surface", surface)
|
|
87
|
+
super().__init__(master, direction="horizontal", fill_items="y", **kwargs)
|
|
88
|
+
self._model = model if model is not None else MenuModel()
|
|
89
|
+
self._density = density
|
|
90
|
+
# Threaded into the menubar-item triggers so they paint this surface
|
|
91
|
+
# (the builders paint the normal bg with the surface color, not parent).
|
|
92
|
+
self._surface = surface
|
|
93
|
+
self._triggers: list[DropdownButton] = []
|
|
94
|
+
# One shared Tk variable per radio group name so radio items in the
|
|
95
|
+
# same group are mutually exclusive.
|
|
96
|
+
self._radio_vars: dict[str, StringVar] = {}
|
|
97
|
+
self.rebuild()
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def model(self) -> MenuModel:
|
|
101
|
+
"""The menu model this strip renders."""
|
|
102
|
+
return self._model
|
|
103
|
+
|
|
104
|
+
def set_model(self, model: MenuModel) -> None:
|
|
105
|
+
"""Swap in a new model and re-render."""
|
|
106
|
+
self._model = model
|
|
107
|
+
self.rebuild()
|
|
108
|
+
|
|
109
|
+
def rebuild(self) -> None:
|
|
110
|
+
"""Tear down and re-render every trigger from the current model."""
|
|
111
|
+
for trigger in self._triggers:
|
|
112
|
+
try:
|
|
113
|
+
trigger.destroy()
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
self._triggers = []
|
|
117
|
+
self._radio_vars = {}
|
|
118
|
+
|
|
119
|
+
for group in self._model:
|
|
120
|
+
items = [
|
|
121
|
+
menu_item_to_context_item(item, self._radio_vars)
|
|
122
|
+
for item in group.items
|
|
123
|
+
]
|
|
124
|
+
trigger = DropdownButton(
|
|
125
|
+
self,
|
|
126
|
+
text=group.text,
|
|
127
|
+
items=items,
|
|
128
|
+
variant="menubar-item",
|
|
129
|
+
show_dropdown_button=False,
|
|
130
|
+
density=self._density,
|
|
131
|
+
surface=self._surface,
|
|
132
|
+
)
|
|
133
|
+
trigger.pack(side="left")
|
|
134
|
+
self._triggers.append(trigger)
|