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,608 @@
|
|
|
1
|
+
"""Toolbar widget with customizable content and optional window controls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from tkinter import Widget
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Callable, Literal
|
|
8
|
+
|
|
9
|
+
from typing_extensions import TypedDict, Unpack
|
|
10
|
+
|
|
11
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
12
|
+
from bootstack.widgets._impl.primitives.button import Button
|
|
13
|
+
from bootstack.widgets._impl.primitives.label import Label
|
|
14
|
+
from bootstack.widgets._impl.primitives.separator import Separator
|
|
15
|
+
from bootstack.widgets._impl.mixins import configure_delegate
|
|
16
|
+
from bootstack.widgets.types import Master, SurfaceToken, WidgetDensity
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from tkinter import StringVar
|
|
20
|
+
|
|
21
|
+
from bootstack.widgets._impl.composites.dropdownbutton import DropdownButton
|
|
22
|
+
from bootstack.widgets._impl.composites.menu.model import MenuGroup, MenuModel
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ToolbarKwargs(TypedDict, total=False):
|
|
26
|
+
show_window_controls: bool
|
|
27
|
+
draggable: bool
|
|
28
|
+
button_variant: str
|
|
29
|
+
density: WidgetDensity
|
|
30
|
+
padding: Any
|
|
31
|
+
# Frame options
|
|
32
|
+
width: int
|
|
33
|
+
height: int
|
|
34
|
+
surface: SurfaceToken | str
|
|
35
|
+
show_border: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Toolbar(Frame):
|
|
39
|
+
"""A horizontal toolbar with customizable content and optional window controls.
|
|
40
|
+
|
|
41
|
+
Toolbar provides a container for icon buttons, labels, and other widgets
|
|
42
|
+
arranged horizontally. It optionally supports window control buttons
|
|
43
|
+
(minimize, maximize, close) and window dragging for custom titlebars.
|
|
44
|
+
|
|
45
|
+
Items are added from left to right. Use `add_spacer()` to push
|
|
46
|
+
subsequent items to the right side.
|
|
47
|
+
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
master: Master = None,
|
|
53
|
+
show_window_controls: bool = False,
|
|
54
|
+
draggable: bool = False,
|
|
55
|
+
button_variant: str = 'ghost',
|
|
56
|
+
density: Literal['default', 'compact'] = 'default',
|
|
57
|
+
padding: int | tuple = None,
|
|
58
|
+
**kwargs: Unpack[ToolbarKwargs]
|
|
59
|
+
):
|
|
60
|
+
"""Initialize a Toolbar.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
master: Parent widget.
|
|
64
|
+
show_window_controls: Show minimize/maximize/close buttons. Default False.
|
|
65
|
+
draggable: Enable window dragging by clicking and dragging the toolbar.
|
|
66
|
+
Default False.
|
|
67
|
+
button_variant: Default variant for toolbar buttons. Default 'ghost'.
|
|
68
|
+
density: Button density for toolbar items. 'compact' for smaller buttons,
|
|
69
|
+
'default' for standard size. Default 'default'.
|
|
70
|
+
padding: Toolbar padding. If None, uses a uniform density-based
|
|
71
|
+
default (2 for compact, 3 for default) so items have even
|
|
72
|
+
breathing room on all sides.
|
|
73
|
+
**kwargs: Additional arguments passed to Frame.
|
|
74
|
+
"""
|
|
75
|
+
if padding is None:
|
|
76
|
+
padding = 2 if density == 'compact' else 3
|
|
77
|
+
super().__init__(master, padding=padding, **kwargs)
|
|
78
|
+
|
|
79
|
+
self._show_window_controls = show_window_controls
|
|
80
|
+
self._draggable = draggable or show_window_controls # Auto-enable drag with window controls
|
|
81
|
+
self._button_variant = button_variant
|
|
82
|
+
self._density = density
|
|
83
|
+
|
|
84
|
+
# Content container (left side)
|
|
85
|
+
self._content_frame = Frame(self)
|
|
86
|
+
self._content_frame.pack(side='left', fill='both', expand=True)
|
|
87
|
+
|
|
88
|
+
# Window controls container (right side)
|
|
89
|
+
self._controls_frame: Frame | None = None
|
|
90
|
+
if show_window_controls:
|
|
91
|
+
self._build_window_controls()
|
|
92
|
+
|
|
93
|
+
# Drag state
|
|
94
|
+
self._drag_start_x = 0
|
|
95
|
+
self._drag_start_y = 0
|
|
96
|
+
|
|
97
|
+
if self._draggable:
|
|
98
|
+
self._setup_drag()
|
|
99
|
+
|
|
100
|
+
# Menu state — populated lazily by add_menu(). The model is the single
|
|
101
|
+
# source of truth for this toolbar's menus (so a host can aggregate them
|
|
102
|
+
# for the macOS native bar); the triggers are the in-window dropdowns.
|
|
103
|
+
self._menu_model: MenuModel | None = None
|
|
104
|
+
self._menu_triggers: dict[str, DropdownButton] = {}
|
|
105
|
+
self._menu_radio_vars: dict[str, StringVar] = {}
|
|
106
|
+
self._menu_rebind_pending: Any = None
|
|
107
|
+
# Optional host hook fired when this toolbar's menus change, so a window
|
|
108
|
+
# can rebuild the aggregated macOS native menu bar.
|
|
109
|
+
self._on_menu_change: Callable[[], Any] | None = None
|
|
110
|
+
|
|
111
|
+
# Cancel a pending menu-shortcut rebind if the bar is destroyed first.
|
|
112
|
+
self.bind("<Destroy>", self._on_destroy, add="+")
|
|
113
|
+
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _control_glyph(name: str) -> dict:
|
|
116
|
+
"""A title-bar control glyph spec — a small (~14px) glyph regardless of
|
|
117
|
+
the toolbar density, for a tight native-style control rather than a
|
|
118
|
+
chunky compact-icon button (the 17px compact-icon default)."""
|
|
119
|
+
return {'name': name, 'size': 14}
|
|
120
|
+
|
|
121
|
+
def _build_window_controls(self):
|
|
122
|
+
"""Build window control buttons (minimize, maximize, close)."""
|
|
123
|
+
self._controls_frame = Frame(self)
|
|
124
|
+
self._controls_frame.pack(side='right', fill='y', padx=(8, 0))
|
|
125
|
+
|
|
126
|
+
# Minimize button
|
|
127
|
+
self._minimize_btn = Button(
|
|
128
|
+
self._controls_frame,
|
|
129
|
+
icon=self._control_glyph('dash-lg'),
|
|
130
|
+
icon_only=True,
|
|
131
|
+
variant='ghost',
|
|
132
|
+
density='compact',
|
|
133
|
+
surface=self._surface,
|
|
134
|
+
command=self._on_minimize,
|
|
135
|
+
)
|
|
136
|
+
self._minimize_btn.pack(side='left', padx=2)
|
|
137
|
+
|
|
138
|
+
# Maximize/Restore button
|
|
139
|
+
self._maximize_btn = Button(
|
|
140
|
+
self._controls_frame,
|
|
141
|
+
icon=self._control_glyph('fullscreen'),
|
|
142
|
+
icon_only=True,
|
|
143
|
+
variant='ghost',
|
|
144
|
+
density='compact',
|
|
145
|
+
surface=self._surface,
|
|
146
|
+
command=self._on_maximize,
|
|
147
|
+
)
|
|
148
|
+
self._maximize_btn.pack(side='left', padx=2)
|
|
149
|
+
|
|
150
|
+
# Close button — danger accent so it hovers red (the native convention).
|
|
151
|
+
self._close_btn = Button(
|
|
152
|
+
self._controls_frame,
|
|
153
|
+
icon=self._control_glyph('x-lg'),
|
|
154
|
+
icon_only=True,
|
|
155
|
+
variant='ghost',
|
|
156
|
+
accent='danger',
|
|
157
|
+
density='compact',
|
|
158
|
+
surface=self._surface,
|
|
159
|
+
command=self._on_close,
|
|
160
|
+
)
|
|
161
|
+
self._close_btn.pack(side='left', padx=2)
|
|
162
|
+
|
|
163
|
+
# Disable maximize if window is not resizable (checked after idle so
|
|
164
|
+
# the window geometry is finalised).
|
|
165
|
+
self.after_idle(self._sync_maximize_state)
|
|
166
|
+
|
|
167
|
+
def _sync_maximize_state(self) -> None:
|
|
168
|
+
"""Hide the maximize button when the window is not resizable."""
|
|
169
|
+
try:
|
|
170
|
+
w, h = self.winfo_toplevel().resizable()
|
|
171
|
+
if not w and not h:
|
|
172
|
+
self._maximize_btn.pack_forget()
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
def _on_minimize(self):
|
|
177
|
+
"""Minimize the window, keeping it recoverable from the taskbar."""
|
|
178
|
+
window = self.winfo_toplevel()
|
|
179
|
+
if not window.overrideredirect():
|
|
180
|
+
window.iconify()
|
|
181
|
+
return
|
|
182
|
+
# A borderless (override_redirect) window has no taskbar button by
|
|
183
|
+
# default, so a plain iconify() would leave it with no way back. On
|
|
184
|
+
# Windows, give it the APPWINDOW taskbar style and minimize via the Win32
|
|
185
|
+
# API — it stays borderless and the taskbar button restores it. Elsewhere,
|
|
186
|
+
# fall back to briefly restoring native decorations (which grant a taskbar
|
|
187
|
+
# button), iconify, then re-hide them once the user restores it.
|
|
188
|
+
if sys.platform == 'win32' and self._win32_minimize(window):
|
|
189
|
+
return
|
|
190
|
+
window.update_idletasks()
|
|
191
|
+
window.overrideredirect(False)
|
|
192
|
+
window.update_idletasks()
|
|
193
|
+
window.iconify()
|
|
194
|
+
window.bind('<Map>', self._on_restore_override_redirect, add='+')
|
|
195
|
+
|
|
196
|
+
def _win32_minimize(self, window) -> bool:
|
|
197
|
+
"""Minimize a borderless window via Win32, with a taskbar button.
|
|
198
|
+
|
|
199
|
+
Adds `WS_EX_APPWINDOW` (and drops `WS_EX_TOOLWINDOW`) so the borderless
|
|
200
|
+
window gets a taskbar button, then minimizes it. Returns True on success.
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
from ctypes import windll
|
|
204
|
+
|
|
205
|
+
GWL_EXSTYLE = -20
|
|
206
|
+
WS_EX_APPWINDOW = 0x00040000
|
|
207
|
+
WS_EX_TOOLWINDOW = 0x00000080
|
|
208
|
+
SW_HIDE, SW_SHOW, SW_MINIMIZE = 0, 5, 6
|
|
209
|
+
|
|
210
|
+
hwnd = windll.user32.GetParent(window.winfo_id()) or window.winfo_id()
|
|
211
|
+
style = windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
|
|
212
|
+
new_style = (style & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW
|
|
213
|
+
if new_style != style:
|
|
214
|
+
windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, new_style)
|
|
215
|
+
# The taskbar only re-reads the style on a re-show, so cycle the
|
|
216
|
+
# window once (a brief flash) the first time the style changes.
|
|
217
|
+
windll.user32.ShowWindow(hwnd, SW_HIDE)
|
|
218
|
+
windll.user32.ShowWindow(hwnd, SW_SHOW)
|
|
219
|
+
windll.user32.ShowWindow(hwnd, SW_MINIMIZE)
|
|
220
|
+
return True
|
|
221
|
+
except Exception:
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
def _on_restore_override_redirect(self, event=None) -> None:
|
|
225
|
+
"""Restore override_redirect after the window is deiconified."""
|
|
226
|
+
window = self.winfo_toplevel()
|
|
227
|
+
if event is not None and getattr(event, 'widget', window) is not window:
|
|
228
|
+
return
|
|
229
|
+
window.unbind('<Map>')
|
|
230
|
+
window.overrideredirect(True)
|
|
231
|
+
|
|
232
|
+
def _on_maximize(self):
|
|
233
|
+
"""Handle maximize/restore button click."""
|
|
234
|
+
window = self.winfo_toplevel()
|
|
235
|
+
w, h = window.resizable()
|
|
236
|
+
if not w and not h:
|
|
237
|
+
return
|
|
238
|
+
if window.state() == 'zoomed':
|
|
239
|
+
window.state('normal')
|
|
240
|
+
self._maximize_btn.configure(icon=self._control_glyph('fullscreen'))
|
|
241
|
+
else:
|
|
242
|
+
window.state('zoomed')
|
|
243
|
+
self._maximize_btn.configure(icon=self._control_glyph('fullscreen-exit'))
|
|
244
|
+
|
|
245
|
+
def _on_close(self):
|
|
246
|
+
"""Handle close button click."""
|
|
247
|
+
window = self.winfo_toplevel()
|
|
248
|
+
window.destroy()
|
|
249
|
+
|
|
250
|
+
def _setup_drag(self):
|
|
251
|
+
"""Set up window dragging (and double-click-to-maximize) bindings."""
|
|
252
|
+
for widget in (self, self._content_frame):
|
|
253
|
+
widget.bind('<Button-1>', self._on_drag_start, add='+')
|
|
254
|
+
widget.bind('<B1-Motion>', self._on_drag_motion, add='+')
|
|
255
|
+
widget.bind('<Double-Button-1>', self._on_drag_double, add='+')
|
|
256
|
+
|
|
257
|
+
def _on_drag_start(self, event):
|
|
258
|
+
"""Record drag start position."""
|
|
259
|
+
self._drag_start_x = event.x_root
|
|
260
|
+
self._drag_start_y = event.y_root
|
|
261
|
+
|
|
262
|
+
def _on_drag_motion(self, event):
|
|
263
|
+
"""Handle drag motion to move window."""
|
|
264
|
+
window = self.winfo_toplevel()
|
|
265
|
+
|
|
266
|
+
# A maximized window snaps back to its normal size before it can move.
|
|
267
|
+
# The grab offset was captured against the *maximized* width, so simply
|
|
268
|
+
# restoring would leave the window offset from the cursor (#165). Keep the
|
|
269
|
+
# pointer on the title bar: capture where the cursor sits over the
|
|
270
|
+
# maximized window (its horizontal fraction + vertical offset), restore,
|
|
271
|
+
# then reposition the smaller window so the cursor lands at the same spot.
|
|
272
|
+
if window.state() == 'zoomed':
|
|
273
|
+
max_w = max(window.winfo_width(), 1)
|
|
274
|
+
frac_x = (event.x_root - window.winfo_rootx()) / max_w
|
|
275
|
+
offset_y = event.y_root - window.winfo_rooty()
|
|
276
|
+
try:
|
|
277
|
+
window.state('normal')
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
window.update_idletasks()
|
|
281
|
+
new_x = int(event.x_root - frac_x * window.winfo_width())
|
|
282
|
+
new_y = int(event.y_root - offset_y)
|
|
283
|
+
window.geometry(f'+{new_x}+{new_y}')
|
|
284
|
+
self._sync_maximize_icon('fullscreen')
|
|
285
|
+
self._drag_start_x = event.x_root
|
|
286
|
+
self._drag_start_y = event.y_root
|
|
287
|
+
return
|
|
288
|
+
|
|
289
|
+
# Calculate delta
|
|
290
|
+
dx = event.x_root - self._drag_start_x
|
|
291
|
+
dy = event.y_root - self._drag_start_y
|
|
292
|
+
|
|
293
|
+
# Get current position
|
|
294
|
+
x = window.winfo_x() + dx
|
|
295
|
+
y = window.winfo_y() + dy
|
|
296
|
+
|
|
297
|
+
# Move window
|
|
298
|
+
window.geometry(f'+{x}+{y}')
|
|
299
|
+
|
|
300
|
+
# Update drag start for next motion
|
|
301
|
+
self._drag_start_x = event.x_root
|
|
302
|
+
self._drag_start_y = event.y_root
|
|
303
|
+
|
|
304
|
+
def _on_drag_double(self, _event):
|
|
305
|
+
"""Double-clicking the bar toggles the window's maximized state."""
|
|
306
|
+
self._on_maximize()
|
|
307
|
+
return 'break'
|
|
308
|
+
|
|
309
|
+
def _sync_maximize_icon(self, name: str) -> None:
|
|
310
|
+
"""Keep the maximize button's icon in sync with the window state."""
|
|
311
|
+
btn = getattr(self, '_maximize_btn', None)
|
|
312
|
+
if btn is not None:
|
|
313
|
+
try:
|
|
314
|
+
btn.configure(icon=self._control_glyph(name))
|
|
315
|
+
except Exception:
|
|
316
|
+
pass
|
|
317
|
+
|
|
318
|
+
# --- Public API: Adding Items ---
|
|
319
|
+
|
|
320
|
+
def add_button(
|
|
321
|
+
self,
|
|
322
|
+
icon: str | dict = None,
|
|
323
|
+
text: str = None,
|
|
324
|
+
command: Callable = None,
|
|
325
|
+
accent: str = None,
|
|
326
|
+
variant: str = None,
|
|
327
|
+
**kwargs
|
|
328
|
+
) -> Button:
|
|
329
|
+
"""Add a button to the toolbar.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
icon: Icon name or configuration.
|
|
333
|
+
text: Button text. If None and icon provided, creates icon-only button.
|
|
334
|
+
command: Button click callback.
|
|
335
|
+
accent: Button accent token.
|
|
336
|
+
variant: Button variant. Uses toolbar default if None.
|
|
337
|
+
**kwargs: Additional arguments passed to Button.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Button: The created button.
|
|
341
|
+
"""
|
|
342
|
+
btn = Button(
|
|
343
|
+
self._content_frame,
|
|
344
|
+
icon=icon,
|
|
345
|
+
text=text,
|
|
346
|
+
icon_only=(icon is not None and text is None),
|
|
347
|
+
command=command,
|
|
348
|
+
accent=accent,
|
|
349
|
+
variant=variant or self._button_variant,
|
|
350
|
+
density=kwargs.pop('density', self._density),
|
|
351
|
+
surface=kwargs.pop('surface', self._surface),
|
|
352
|
+
**kwargs
|
|
353
|
+
)
|
|
354
|
+
btn.pack(side='left')
|
|
355
|
+
return btn
|
|
356
|
+
|
|
357
|
+
def add_menu(self, text: str, *, key: str | None = None) -> "MenuGroup":
|
|
358
|
+
"""Add a dropdown menu (File / Edit / …) as a toolbar item.
|
|
359
|
+
|
|
360
|
+
Returns the menu's `MenuGroup` builder — add items with
|
|
361
|
+
`add_action` / `add_check` / `add_radio` / `add_divider`, ideally in a
|
|
362
|
+
`with` block. On Windows/Linux the menu renders as an in-window dropdown
|
|
363
|
+
trigger in the toolbar; the menu also feeds this toolbar's menu model so a
|
|
364
|
+
host can surface it in the macOS native menu bar.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
text: The menu's label (e.g. `'File'`).
|
|
368
|
+
key: Optional stable identifier; defaults to `text`.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
The `MenuGroup` for this menu.
|
|
372
|
+
"""
|
|
373
|
+
from bootstack.widgets._impl.composites.dropdownbutton import DropdownButton
|
|
374
|
+
from bootstack.widgets._impl.composites.menu.model import MenuModel
|
|
375
|
+
|
|
376
|
+
if self._menu_model is None:
|
|
377
|
+
self._menu_model = MenuModel()
|
|
378
|
+
group = self._menu_model.add_menu(text, key=key)
|
|
379
|
+
|
|
380
|
+
# A flat menu-bar-style trigger whose popdown is the themed context menu.
|
|
381
|
+
trigger = DropdownButton(
|
|
382
|
+
self._content_frame,
|
|
383
|
+
text=text,
|
|
384
|
+
items=[],
|
|
385
|
+
variant='menubar-item',
|
|
386
|
+
show_dropdown_button=False,
|
|
387
|
+
density=self._density,
|
|
388
|
+
surface=self._surface,
|
|
389
|
+
)
|
|
390
|
+
trigger.pack(side='left')
|
|
391
|
+
self._menu_triggers[group.key] = trigger
|
|
392
|
+
|
|
393
|
+
# Items are appended to the trigger as they are added to the group (the
|
|
394
|
+
# build-up case), so the `with tb.add_menu(...)` block renders live; each
|
|
395
|
+
# change also (coalesced) rebinds the model's keyboard shortcuts.
|
|
396
|
+
group.on_change = lambda g=group, t=trigger: self._on_menu_item_added(g, t)
|
|
397
|
+
return group
|
|
398
|
+
|
|
399
|
+
def _on_menu_item_added(self, group: "MenuGroup", trigger: "DropdownButton") -> None:
|
|
400
|
+
from bootstack.widgets._impl.composites.menu.render_themed import (
|
|
401
|
+
menu_item_to_context_item,
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
item = group.items[-1]
|
|
405
|
+
trigger.add_items([menu_item_to_context_item(item, self._menu_radio_vars)])
|
|
406
|
+
self._schedule_menu_rebind()
|
|
407
|
+
if self._on_menu_change is not None:
|
|
408
|
+
self._on_menu_change()
|
|
409
|
+
|
|
410
|
+
def _schedule_menu_rebind(self) -> None:
|
|
411
|
+
"""Coalesce shortcut (re)binding to idle so a built menu binds once."""
|
|
412
|
+
if self._menu_rebind_pending is not None:
|
|
413
|
+
try:
|
|
414
|
+
self.after_cancel(self._menu_rebind_pending)
|
|
415
|
+
except Exception:
|
|
416
|
+
pass
|
|
417
|
+
try:
|
|
418
|
+
self._menu_rebind_pending = self.after_idle(self._rebind_menu_shortcuts)
|
|
419
|
+
except Exception:
|
|
420
|
+
self._rebind_menu_shortcuts()
|
|
421
|
+
|
|
422
|
+
def _rebind_menu_shortcuts(self) -> None:
|
|
423
|
+
self._menu_rebind_pending = None
|
|
424
|
+
if self._menu_model is not None:
|
|
425
|
+
try:
|
|
426
|
+
self._menu_model.bind_shortcuts(self.winfo_toplevel())
|
|
427
|
+
except Exception:
|
|
428
|
+
pass
|
|
429
|
+
|
|
430
|
+
def _on_destroy(self, event: Any) -> None:
|
|
431
|
+
"""Cancel a pending menu-shortcut rebind when the toolbar is destroyed.
|
|
432
|
+
|
|
433
|
+
`<Destroy>` propagates up from descendants, so act only for the bar itself.
|
|
434
|
+
"""
|
|
435
|
+
if event.widget is not self:
|
|
436
|
+
return
|
|
437
|
+
if self._menu_rebind_pending is not None:
|
|
438
|
+
try:
|
|
439
|
+
self.after_cancel(self._menu_rebind_pending)
|
|
440
|
+
except Exception:
|
|
441
|
+
pass
|
|
442
|
+
self._menu_rebind_pending = None
|
|
443
|
+
|
|
444
|
+
@property
|
|
445
|
+
def menu_model(self) -> "MenuModel | None":
|
|
446
|
+
"""This toolbar's menu model (or `None` if no menus were added)."""
|
|
447
|
+
return self._menu_model
|
|
448
|
+
|
|
449
|
+
def add_label(
|
|
450
|
+
self,
|
|
451
|
+
text: str = '',
|
|
452
|
+
icon: str | dict = None,
|
|
453
|
+
font: str = None,
|
|
454
|
+
**kwargs
|
|
455
|
+
) -> Label:
|
|
456
|
+
"""Add a label to the toolbar.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
text: Label text.
|
|
460
|
+
icon: Optional icon name or configuration.
|
|
461
|
+
font: Font specification.
|
|
462
|
+
**kwargs: Additional arguments passed to Label.
|
|
463
|
+
|
|
464
|
+
Returns:
|
|
465
|
+
Label: The created label.
|
|
466
|
+
"""
|
|
467
|
+
# Show icon AND text when both are given. The internal Label primitive
|
|
468
|
+
# defaults to icon-only (no compound) — the public Label wrapper's
|
|
469
|
+
# compound logic doesn't run on this path, so set it here.
|
|
470
|
+
has_text = bool(text) or kwargs.get('textsignal') is not None
|
|
471
|
+
if icon is not None and has_text and 'compound' not in kwargs:
|
|
472
|
+
kwargs['compound'] = 'left'
|
|
473
|
+
lbl = Label(
|
|
474
|
+
self._content_frame,
|
|
475
|
+
text=text,
|
|
476
|
+
icon=icon,
|
|
477
|
+
font=font,
|
|
478
|
+
surface=kwargs.pop('surface', self._surface),
|
|
479
|
+
**kwargs
|
|
480
|
+
)
|
|
481
|
+
lbl.pack(side='left', padx=4)
|
|
482
|
+
self._attach_drag(lbl)
|
|
483
|
+
return lbl
|
|
484
|
+
|
|
485
|
+
def _attach_drag(self, widget) -> None:
|
|
486
|
+
"""Wire window-drag bindings onto a child widget so the bar can be
|
|
487
|
+
grabbed there (used for a draggable titlebar). No-op when the bar is not
|
|
488
|
+
draggable. Buttons are intentionally excluded — they are click targets."""
|
|
489
|
+
if not self._draggable:
|
|
490
|
+
return
|
|
491
|
+
widget.bind('<Button-1>', self._on_drag_start, add='+')
|
|
492
|
+
widget.bind('<B1-Motion>', self._on_drag_motion, add='+')
|
|
493
|
+
widget.bind('<Double-Button-1>', self._on_drag_double, add='+')
|
|
494
|
+
|
|
495
|
+
def add_separator(self, length: int = 16, **kwargs) -> Separator:
|
|
496
|
+
"""Add a vertical separator to the toolbar.
|
|
497
|
+
|
|
498
|
+
Args:
|
|
499
|
+
length: Fixed length in pixels. If None, stretches to fill the
|
|
500
|
+
toolbar height.
|
|
501
|
+
**kwargs: Additional arguments passed to Separator.
|
|
502
|
+
|
|
503
|
+
Returns:
|
|
504
|
+
Separator: The created separator.
|
|
505
|
+
"""
|
|
506
|
+
sep = Separator(
|
|
507
|
+
self._content_frame,
|
|
508
|
+
orient='vertical',
|
|
509
|
+
length=length,
|
|
510
|
+
**kwargs
|
|
511
|
+
)
|
|
512
|
+
# Only fill='y' if no fixed length specified
|
|
513
|
+
if length:
|
|
514
|
+
sep.pack(side='left', padx=4)
|
|
515
|
+
else:
|
|
516
|
+
sep.pack(side='left', fill='y', padx=4)
|
|
517
|
+
return sep
|
|
518
|
+
|
|
519
|
+
def add_spacer(self) -> Frame:
|
|
520
|
+
"""Add a flexible spacer that pushes subsequent items to the right.
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
Frame: The spacer frame.
|
|
524
|
+
"""
|
|
525
|
+
spacer = Frame(self._content_frame)
|
|
526
|
+
spacer.pack(side='left', fill='both', expand=True)
|
|
527
|
+
self._attach_drag(spacer)
|
|
528
|
+
return spacer
|
|
529
|
+
|
|
530
|
+
def add_widget(self, widget: Widget, **pack_kwargs) -> Widget:
|
|
531
|
+
"""Add a custom widget to the toolbar.
|
|
532
|
+
|
|
533
|
+
The widget must already be created with the toolbar's content frame
|
|
534
|
+
as its parent. Use `toolbar.content` to get the parent frame.
|
|
535
|
+
|
|
536
|
+
Args:
|
|
537
|
+
widget: The widget to add.
|
|
538
|
+
**pack_kwargs: Arguments passed to pack(). Defaults to side='left'.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
Widget: The added widget.
|
|
542
|
+
"""
|
|
543
|
+
pack_kwargs.setdefault('side', 'left')
|
|
544
|
+
pack_kwargs.setdefault('padx', 2)
|
|
545
|
+
widget.pack(**pack_kwargs)
|
|
546
|
+
return widget
|
|
547
|
+
|
|
548
|
+
# --- Properties ---
|
|
549
|
+
|
|
550
|
+
@property
|
|
551
|
+
def content(self) -> Frame:
|
|
552
|
+
"""Get the content frame for adding custom widgets."""
|
|
553
|
+
return self._content_frame
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def show_window_controls(self) -> bool:
|
|
557
|
+
"""Check if window controls are shown."""
|
|
558
|
+
return self._show_window_controls
|
|
559
|
+
|
|
560
|
+
@property
|
|
561
|
+
def draggable(self) -> bool:
|
|
562
|
+
"""Check if toolbar is draggable."""
|
|
563
|
+
return self._draggable
|
|
564
|
+
|
|
565
|
+
@property
|
|
566
|
+
def density(self) -> str:
|
|
567
|
+
"""Get the toolbar's button density."""
|
|
568
|
+
return self._density
|
|
569
|
+
|
|
570
|
+
@property
|
|
571
|
+
def surface(self) -> str:
|
|
572
|
+
"""The surface token applied to this toolbar."""
|
|
573
|
+
return getattr(self, "_surface", "chrome")
|
|
574
|
+
|
|
575
|
+
# --- Window Control Access ---
|
|
576
|
+
|
|
577
|
+
@property
|
|
578
|
+
def minimize_button(self) -> Button | None:
|
|
579
|
+
"""Get the minimize button (if window controls are shown)."""
|
|
580
|
+
return getattr(self, '_minimize_btn', None)
|
|
581
|
+
|
|
582
|
+
@property
|
|
583
|
+
def maximize_button(self) -> Button | None:
|
|
584
|
+
"""Get the maximize button (if window controls are shown)."""
|
|
585
|
+
return getattr(self, '_maximize_btn', None)
|
|
586
|
+
|
|
587
|
+
@property
|
|
588
|
+
def close_button(self) -> Button | None:
|
|
589
|
+
"""Get the close button (if window controls are shown)."""
|
|
590
|
+
return getattr(self, '_close_btn', None)
|
|
591
|
+
|
|
592
|
+
# --- Configuration Delegates ---
|
|
593
|
+
|
|
594
|
+
@configure_delegate('density')
|
|
595
|
+
def _delegate_density(self, value: str = None):
|
|
596
|
+
"""Configure the toolbar's default button density."""
|
|
597
|
+
if value is None:
|
|
598
|
+
return self._density
|
|
599
|
+
self._density = value
|
|
600
|
+
return None
|
|
601
|
+
|
|
602
|
+
@configure_delegate('button_variant')
|
|
603
|
+
def _delegate_button_variant(self, value: str = None):
|
|
604
|
+
"""Configure the toolbar's default button variant."""
|
|
605
|
+
if value is None:
|
|
606
|
+
return self._button_variant
|
|
607
|
+
self._button_variant = value
|
|
608
|
+
return None
|