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,312 @@
|
|
|
1
|
+
"""PygmentsHighlighter — syntax highlighting via Pygments.
|
|
2
|
+
|
|
3
|
+
Implements a debounced full-document retokenize strategy: on each
|
|
4
|
+
insert/delete, schedules a retokenize after 150 ms. Tokens are mapped
|
|
5
|
+
to `RangeDecoration` objects and applied via the decoration layer system.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from pygments import lex
|
|
12
|
+
from pygments.lexers import get_lexer_by_name, TextLexer
|
|
13
|
+
from pygments.styles import get_style_by_name
|
|
14
|
+
from pygments.token import Token
|
|
15
|
+
from pygments.util import ClassNotFound
|
|
16
|
+
|
|
17
|
+
from bootstack.widgets._impl.composites.textarea.filter import EditFilter
|
|
18
|
+
from bootstack.widgets._impl.composites.textarea.decoration import Position, RangeDecoration
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_LAYER = "syntax"
|
|
25
|
+
_DEBOUNCE_MS = 150
|
|
26
|
+
|
|
27
|
+
# Token family → bootstack style name. More-specific entries must come
|
|
28
|
+
# before their parents so that the parent-walk hits the right name first.
|
|
29
|
+
_TOKEN_TO_STYLE: dict = {
|
|
30
|
+
Token.String.Doc: "string_doc",
|
|
31
|
+
Token.String: "string",
|
|
32
|
+
Token.Keyword: "keyword",
|
|
33
|
+
Token.Comment: "comment",
|
|
34
|
+
Token.Number: "number",
|
|
35
|
+
Token.Operator: "operator",
|
|
36
|
+
Token.Name.Builtin: "name_builtin",
|
|
37
|
+
Token.Name.Function: "name_function",
|
|
38
|
+
Token.Name.Class: "name_class",
|
|
39
|
+
Token.Name.Decorator: "name_decorator",
|
|
40
|
+
Token.Name.Exception: "name_exception",
|
|
41
|
+
Token.Name.Namespace: "name_namespace",
|
|
42
|
+
Token.Name.Tag: "name_tag", # JSON keys, HTML/XML tags
|
|
43
|
+
Token.Name.Attribute: "name_attribute", # HTML/XML attributes
|
|
44
|
+
Token.Punctuation: "punctuation",
|
|
45
|
+
Token.Error: "error",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Reverse map: style name → canonical token (used for color extraction)
|
|
49
|
+
_STYLE_TO_TOKEN: dict = {v: k for k, v in _TOKEN_TO_STYLE.items()}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _find_style(ttype) -> str | None:
|
|
53
|
+
"""Walk up the Pygments token hierarchy to find the nearest style name."""
|
|
54
|
+
t = ttype
|
|
55
|
+
while t is not None:
|
|
56
|
+
if t in _TOKEN_TO_STYLE:
|
|
57
|
+
return _TOKEN_TO_STYLE[t]
|
|
58
|
+
t = t.parent
|
|
59
|
+
return None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _extract_colors(style_cls) -> dict[str, str]:
|
|
63
|
+
"""Return `{style_name: '#RRGGBB'}` for all style names that have a color."""
|
|
64
|
+
colors: dict[str, str] = {}
|
|
65
|
+
for name, token in _STYLE_TO_TOKEN.items():
|
|
66
|
+
info = style_cls.style_for_token(token)
|
|
67
|
+
color = info.get("color")
|
|
68
|
+
if color:
|
|
69
|
+
colors[name] = f"#{color}"
|
|
70
|
+
return colors
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _contrast_color(hex_color: str) -> str:
|
|
74
|
+
"""Return black or white for maximum contrast against `hex_color`."""
|
|
75
|
+
h = hex_color.lstrip("#")
|
|
76
|
+
try:
|
|
77
|
+
r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
78
|
+
luminance = 0.299 * r + 0.587 * g + 0.114 * b
|
|
79
|
+
return "#000000" if luminance > 128 else "#ffffff"
|
|
80
|
+
except Exception:
|
|
81
|
+
return "#000000"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class PygmentsHighlighter(EditFilter):
|
|
85
|
+
"""Syntax highlighting via Pygments.
|
|
86
|
+
|
|
87
|
+
Install via the `language=` parameter on `CodeEditor`, or manually:
|
|
88
|
+
|
|
89
|
+
core.add_filter(PygmentsHighlighter("python"))
|
|
90
|
+
|
|
91
|
+
Uses a debounced full-document retokenize: 150 ms after the last edit the
|
|
92
|
+
full text is re-lexed, tokens are converted to `RangeDecoration` objects,
|
|
93
|
+
and applied via `core.set_decorations()`.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
language: Pygments lexer name (e.g. `'python'`, `'sql'`,
|
|
97
|
+
`'javascript'`). Unknown names fall back to plain text.
|
|
98
|
+
pygments_style: Pygments style name, or `'auto'` to track the
|
|
99
|
+
bootstack theme. When `'auto'`, `light_style` and `dark_style`
|
|
100
|
+
determine the actual Pygments style used.
|
|
101
|
+
light_style: Pygments style used in auto mode when the active
|
|
102
|
+
bootstack theme is light. Default `'default'`.
|
|
103
|
+
dark_style: Pygments style used in auto mode when the active
|
|
104
|
+
bootstack theme is dark. Default `'monokai'`.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(
|
|
108
|
+
self,
|
|
109
|
+
language: str,
|
|
110
|
+
pygments_style: str = "default",
|
|
111
|
+
light_style: str = "default",
|
|
112
|
+
dark_style: str = "monokai",
|
|
113
|
+
) -> None:
|
|
114
|
+
super().__init__()
|
|
115
|
+
self._core: _MultilineCore | None = None
|
|
116
|
+
self._after_id: str | None = None
|
|
117
|
+
self._theme_bind_id: str | None = None
|
|
118
|
+
self._theme_root = None
|
|
119
|
+
|
|
120
|
+
self._auto = pygments_style == "auto"
|
|
121
|
+
self._light_style = light_style
|
|
122
|
+
self._dark_style = dark_style
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
self._lexer = get_lexer_by_name(language, stripall=False, stripnl=False)
|
|
126
|
+
except ClassNotFound:
|
|
127
|
+
self._lexer = TextLexer()
|
|
128
|
+
|
|
129
|
+
# Load initial style — auto resolves after attach when the theme is known.
|
|
130
|
+
initial = self._resolve_auto_name() if self._auto else pygments_style
|
|
131
|
+
self._load_style(initial)
|
|
132
|
+
|
|
133
|
+
# Saved originals — restored on detach.
|
|
134
|
+
self._orig_bg: str | None = None
|
|
135
|
+
self._orig_fg: str | None = None
|
|
136
|
+
self._orig_ibg: str | None = None
|
|
137
|
+
|
|
138
|
+
# ── EditFilter protocol ───────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
def attach(self, core: _MultilineCore) -> None:
|
|
141
|
+
self._core = core
|
|
142
|
+
|
|
143
|
+
# Resolve auto style now that we can query the theme provider.
|
|
144
|
+
if self._auto:
|
|
145
|
+
self._load_style(self._resolve_auto_name())
|
|
146
|
+
|
|
147
|
+
core.register_layer(_LAYER, priority=1)
|
|
148
|
+
for sname, color in self._style_colors.items():
|
|
149
|
+
core.define_style(sname, foreground=color)
|
|
150
|
+
|
|
151
|
+
# Save current text-widget colors before overriding them.
|
|
152
|
+
self._orig_bg = core.text.cget("background")
|
|
153
|
+
self._orig_fg = core.text.cget("foreground")
|
|
154
|
+
self._orig_ibg = core.text.cget("insertbackground")
|
|
155
|
+
self._apply_widget_colors(core)
|
|
156
|
+
|
|
157
|
+
# Bind to <<BsThemeChanged>> on the root — fired by bootstack after full
|
|
158
|
+
# rebuild so colors and mode are already accurate when the handler runs.
|
|
159
|
+
self._theme_root = core.winfo_toplevel()
|
|
160
|
+
self._theme_bind_id = self._theme_root.bind(
|
|
161
|
+
"<<BsThemeChanged>>", self._on_framework_theme_changed, add="+"
|
|
162
|
+
)
|
|
163
|
+
self._schedule()
|
|
164
|
+
|
|
165
|
+
def detach(self, core: _MultilineCore) -> None:
|
|
166
|
+
self._cancel()
|
|
167
|
+
if self._theme_bind_id is not None and self._theme_root is not None:
|
|
168
|
+
try:
|
|
169
|
+
self._theme_root.unbind("<<BsThemeChanged>>", self._theme_bind_id)
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|
|
172
|
+
self._theme_bind_id = None
|
|
173
|
+
self._theme_root = None
|
|
174
|
+
if self._core is not None:
|
|
175
|
+
core.clear_decorations(_LAYER)
|
|
176
|
+
self._restore_widget_colors(core)
|
|
177
|
+
self._core = None
|
|
178
|
+
|
|
179
|
+
def insert(self, index: str, chars: str, tags=None) -> None:
|
|
180
|
+
self.delegate.insert(index, chars, tags)
|
|
181
|
+
self._schedule()
|
|
182
|
+
|
|
183
|
+
def delete(self, index1: str, index2: str | None = None) -> None:
|
|
184
|
+
self.delegate.delete(index1, index2)
|
|
185
|
+
self._schedule()
|
|
186
|
+
|
|
187
|
+
# ── internal ──────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
def _apply_widget_colors(self, core: _MultilineCore, notify: bool = True) -> None:
|
|
190
|
+
kw: dict = {"background": self._style_bg}
|
|
191
|
+
if self._style_fg:
|
|
192
|
+
kw["foreground"] = self._style_fg
|
|
193
|
+
kw["insertbackground"] = self._style_fg or _contrast_color(self._style_bg)
|
|
194
|
+
try:
|
|
195
|
+
core.text.configure(**kw)
|
|
196
|
+
if notify:
|
|
197
|
+
# Notify extensions that the editor background changed so they
|
|
198
|
+
# can re-calibrate colors. Only fire on deliberate style
|
|
199
|
+
# application (attach / set_language), not on every theme
|
|
200
|
+
# rebuild — extensions already react to <<BsThemeChanged>>
|
|
201
|
+
# directly and cascading it causes unnecessary repaints.
|
|
202
|
+
core.text.event_generate("<<EditorBgChanged>>", when="tail")
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
def _restore_widget_colors(self, core: _MultilineCore) -> None:
|
|
207
|
+
kw: dict = {}
|
|
208
|
+
if self._orig_bg is not None:
|
|
209
|
+
kw["background"] = self._orig_bg
|
|
210
|
+
if self._orig_fg is not None:
|
|
211
|
+
kw["foreground"] = self._orig_fg
|
|
212
|
+
if self._orig_ibg is not None:
|
|
213
|
+
kw["insertbackground"] = self._orig_ibg
|
|
214
|
+
if kw:
|
|
215
|
+
try:
|
|
216
|
+
core.text.configure(**kw)
|
|
217
|
+
except Exception:
|
|
218
|
+
pass
|
|
219
|
+
|
|
220
|
+
def _resolve_auto_name(self) -> str:
|
|
221
|
+
"""Return the appropriate Pygments style name for the current theme mode."""
|
|
222
|
+
try:
|
|
223
|
+
from bootstack.style.style import get_theme_provider
|
|
224
|
+
mode = get_theme_provider().mode
|
|
225
|
+
except Exception:
|
|
226
|
+
mode = "light"
|
|
227
|
+
return self._dark_style if mode == "dark" else self._light_style
|
|
228
|
+
|
|
229
|
+
def _load_style(self, style_name: str) -> None:
|
|
230
|
+
"""Load a Pygments style, updating all color state."""
|
|
231
|
+
try:
|
|
232
|
+
style_cls = get_style_by_name(style_name)
|
|
233
|
+
except ClassNotFound:
|
|
234
|
+
style_cls = get_style_by_name("default")
|
|
235
|
+
self._style_colors = _extract_colors(style_cls)
|
|
236
|
+
self._active_styles: frozenset[str] = frozenset(self._style_colors)
|
|
237
|
+
self._style_bg: str = style_cls.background_color
|
|
238
|
+
raw_fg = (
|
|
239
|
+
style_cls.style_for_token(Token.Text).get("color")
|
|
240
|
+
or style_cls.style_for_token(Token).get("color")
|
|
241
|
+
)
|
|
242
|
+
self._style_fg: str | None = f"#{raw_fg}" if raw_fg else None
|
|
243
|
+
|
|
244
|
+
def _on_framework_theme_changed(self, event=None) -> None:
|
|
245
|
+
"""Called via <<BsThemeChanged>> after _rebuild_all_styles() completes."""
|
|
246
|
+
if self._core is None:
|
|
247
|
+
return
|
|
248
|
+
if self._auto:
|
|
249
|
+
mode = (event.data or {}).get("mode") if event else None
|
|
250
|
+
resolved = self._dark_style if mode == "dark" else self._resolve_auto_name()
|
|
251
|
+
self._load_style(resolved)
|
|
252
|
+
for sname, color in self._style_colors.items():
|
|
253
|
+
self._core.define_style(sname, foreground=color)
|
|
254
|
+
self._apply_widget_colors(self._core, notify=True)
|
|
255
|
+
self._schedule()
|
|
256
|
+
|
|
257
|
+
def _schedule(self) -> None:
|
|
258
|
+
self._cancel()
|
|
259
|
+
if self._core is not None:
|
|
260
|
+
self._after_id = self._core.text.after(_DEBOUNCE_MS, self._retokenize)
|
|
261
|
+
|
|
262
|
+
def _cancel(self) -> None:
|
|
263
|
+
if self._after_id is not None and self._core is not None:
|
|
264
|
+
try:
|
|
265
|
+
self._core.text.after_cancel(self._after_id)
|
|
266
|
+
except Exception:
|
|
267
|
+
pass
|
|
268
|
+
self._after_id = None
|
|
269
|
+
|
|
270
|
+
def _retokenize(self) -> None:
|
|
271
|
+
self._after_id = None
|
|
272
|
+
if self._core is None:
|
|
273
|
+
return
|
|
274
|
+
try:
|
|
275
|
+
text = self._core.value
|
|
276
|
+
except Exception:
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
if not text:
|
|
280
|
+
self._core.clear_decorations(_LAYER)
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
decorations: list[RangeDecoration] = []
|
|
284
|
+
line, col = 1, 0
|
|
285
|
+
active = self._active_styles
|
|
286
|
+
|
|
287
|
+
for ttype, value in lex(text, self._lexer):
|
|
288
|
+
if value:
|
|
289
|
+
style = _find_style(ttype)
|
|
290
|
+
if style is not None and style in active:
|
|
291
|
+
if "\n" in value:
|
|
292
|
+
vlines = value.split("\n")
|
|
293
|
+
end_line = line + len(vlines) - 1
|
|
294
|
+
end_col = len(vlines[-1])
|
|
295
|
+
else:
|
|
296
|
+
end_line = line
|
|
297
|
+
end_col = col + len(value)
|
|
298
|
+
decorations.append(RangeDecoration(
|
|
299
|
+
Position(line, col),
|
|
300
|
+
Position(end_line, end_col),
|
|
301
|
+
style,
|
|
302
|
+
))
|
|
303
|
+
|
|
304
|
+
# Advance position regardless of whether we added a decoration.
|
|
305
|
+
if "\n" in value:
|
|
306
|
+
vlines = value.split("\n")
|
|
307
|
+
line += len(vlines) - 1
|
|
308
|
+
col = len(vlines[-1])
|
|
309
|
+
else:
|
|
310
|
+
col += len(value)
|
|
311
|
+
|
|
312
|
+
self._core.set_decorations(_LAYER, decorations)
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""SmartIndent — auto-indent on Enter and Tab-to-spaces conversion.
|
|
2
|
+
|
|
3
|
+
Filter-style extension: intercepts Return and Tab key events to provide
|
|
4
|
+
editor-grade indentation. Does not intercept insert/delete in the chain
|
|
5
|
+
(key bindings are used instead to avoid interfering with clipboard ops).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import tkinter as tk
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
from bootstack.widgets._impl.composites.textarea.filter import EditFilter
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SmartIndent(EditFilter):
|
|
19
|
+
"""Provides auto-indent on Return, Tab-to-spaces, and block indent/dedent.
|
|
20
|
+
|
|
21
|
+
On `<Return>`:
|
|
22
|
+
Inserts a newline followed by the same leading whitespace as the
|
|
23
|
+
current line, so the cursor lands at the same indent level.
|
|
24
|
+
|
|
25
|
+
On `<Tab>` with a selection:
|
|
26
|
+
Indents every selected line by one tab stop.
|
|
27
|
+
|
|
28
|
+
On `<Tab>` with no selection (when `insert_spaces=True`):
|
|
29
|
+
Inserts spaces to the next tab stop instead of a literal tab character.
|
|
30
|
+
|
|
31
|
+
On `<Shift-Tab>`:
|
|
32
|
+
Dedents every selected line (or the current line) by one tab stop.
|
|
33
|
+
|
|
34
|
+
Install via `core.add_filter(SmartIndent())`.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, tab_width: int = 4, insert_spaces: bool = True) -> None:
|
|
38
|
+
super().__init__()
|
|
39
|
+
self._tab_width = tab_width
|
|
40
|
+
self._insert_spaces = insert_spaces
|
|
41
|
+
self._core: _MultilineCore | None = None
|
|
42
|
+
|
|
43
|
+
def attach(self, core: _MultilineCore) -> None:
|
|
44
|
+
self._core = core
|
|
45
|
+
core.text.bind("<Return>", self._on_return, add="+")
|
|
46
|
+
core.text.bind("<Tab>", self._on_tab, add="+")
|
|
47
|
+
core.text.bind("<Shift-Tab>", self._on_shift_tab, add="+")
|
|
48
|
+
core.text.bind("<ISO_Left_Tab>", self._on_shift_tab, add="+") # Linux/X11
|
|
49
|
+
|
|
50
|
+
def detach(self, core: _MultilineCore) -> None:
|
|
51
|
+
self._core = None
|
|
52
|
+
|
|
53
|
+
# ── filter pass-through ───────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
def insert(self, index: str, chars: str, tags=None) -> None:
|
|
56
|
+
self.delegate.insert(index, chars, tags)
|
|
57
|
+
|
|
58
|
+
def delete(self, index1: str, index2: str | None = None) -> None:
|
|
59
|
+
self.delegate.delete(index1, index2)
|
|
60
|
+
|
|
61
|
+
# ── public block operations ───────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
def indent(self) -> None:
|
|
64
|
+
"""Indent selected lines by one tab stop, or insert at cursor."""
|
|
65
|
+
if self._core is None:
|
|
66
|
+
return
|
|
67
|
+
tw = self._core.text
|
|
68
|
+
ranges = tw.tag_ranges("sel")
|
|
69
|
+
if ranges:
|
|
70
|
+
self._indent_block(tw, ranges)
|
|
71
|
+
else:
|
|
72
|
+
col = int(tw.index("insert").split(".")[1])
|
|
73
|
+
spaces = self._tab_width - (col % self._tab_width)
|
|
74
|
+
tw.insert("insert", " " * spaces if self._insert_spaces else "\t")
|
|
75
|
+
|
|
76
|
+
def dedent(self) -> None:
|
|
77
|
+
"""Dedent selected lines (or current line) by one tab stop."""
|
|
78
|
+
if self._core is None:
|
|
79
|
+
return
|
|
80
|
+
self._dedent_block(self._core.text, self._core.text.tag_ranges("sel"))
|
|
81
|
+
|
|
82
|
+
# ── key handlers ─────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
def _on_return(self, _event: tk.Event) -> str:
|
|
85
|
+
"""Insert a newline with matching indentation."""
|
|
86
|
+
if self._core is None:
|
|
87
|
+
return ""
|
|
88
|
+
text = self._core.text
|
|
89
|
+
cursor = text.index("insert")
|
|
90
|
+
line_start = text.index(f"{cursor} linestart")
|
|
91
|
+
line_text = text.get(line_start, cursor)
|
|
92
|
+
indent = _leading_whitespace(line_text)
|
|
93
|
+
# Use the undo block API so auto-indent is one undo step with the newline
|
|
94
|
+
self._core.undo_block_start()
|
|
95
|
+
text.insert("insert", f"\n{indent}")
|
|
96
|
+
self._core.undo_block_stop()
|
|
97
|
+
return "break" # prevent default Return handling
|
|
98
|
+
|
|
99
|
+
def _on_tab(self, _event: tk.Event) -> str | None:
|
|
100
|
+
"""Indent selection, or insert spaces / fall through for a literal tab."""
|
|
101
|
+
if self._core is None:
|
|
102
|
+
return None
|
|
103
|
+
tw = self._core.text
|
|
104
|
+
ranges = tw.tag_ranges("sel")
|
|
105
|
+
if ranges:
|
|
106
|
+
self._indent_block(tw, ranges)
|
|
107
|
+
return "break"
|
|
108
|
+
if self._insert_spaces:
|
|
109
|
+
col = int(tw.index("insert").split(".")[1])
|
|
110
|
+
spaces = self._tab_width - (col % self._tab_width)
|
|
111
|
+
tw.insert("insert", " " * spaces)
|
|
112
|
+
return "break"
|
|
113
|
+
# insert_spaces=False, no selection: let native Tab insert \t
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def _on_shift_tab(self, _event: tk.Event) -> str:
|
|
117
|
+
"""Dedent selection or current line."""
|
|
118
|
+
if self._core is None:
|
|
119
|
+
return "break"
|
|
120
|
+
self._dedent_block(self._core.text, self._core.text.tag_ranges("sel"))
|
|
121
|
+
return "break"
|
|
122
|
+
|
|
123
|
+
# ── helpers ───────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
def _indent_block(self, tw: tk.Text, ranges: tuple) -> None:
|
|
126
|
+
start_line, end_line = _sel_line_range(ranges)
|
|
127
|
+
indent = " " * self._tab_width if self._insert_spaces else "\t"
|
|
128
|
+
self._core.undo_block_start()
|
|
129
|
+
try:
|
|
130
|
+
for ln in range(start_line, end_line + 1):
|
|
131
|
+
if tw.get(f"{ln}.0", f"{ln}.end"): # skip blank lines
|
|
132
|
+
tw.insert(f"{ln}.0", indent)
|
|
133
|
+
finally:
|
|
134
|
+
self._core.undo_block_stop()
|
|
135
|
+
tw.tag_add("sel", f"{start_line}.0", f"{end_line}.end")
|
|
136
|
+
|
|
137
|
+
def _dedent_block(self, tw: tk.Text, ranges: tuple) -> None:
|
|
138
|
+
if ranges:
|
|
139
|
+
start_line, end_line = _sel_line_range(ranges)
|
|
140
|
+
else:
|
|
141
|
+
start_line = end_line = int(tw.index("insert").split(".")[0])
|
|
142
|
+
self._core.undo_block_start()
|
|
143
|
+
try:
|
|
144
|
+
for ln in range(start_line, end_line + 1):
|
|
145
|
+
line_text = tw.get(f"{ln}.0", f"{ln}.end")
|
|
146
|
+
if self._insert_spaces:
|
|
147
|
+
n = min(self._tab_width, len(line_text) - len(line_text.lstrip(" ")))
|
|
148
|
+
if n:
|
|
149
|
+
tw.delete(f"{ln}.0", f"{ln}.{n}")
|
|
150
|
+
else:
|
|
151
|
+
if line_text.startswith("\t"):
|
|
152
|
+
tw.delete(f"{ln}.0", f"{ln}.1")
|
|
153
|
+
finally:
|
|
154
|
+
self._core.undo_block_stop()
|
|
155
|
+
if ranges:
|
|
156
|
+
tw.tag_add("sel", f"{start_line}.0", f"{end_line}.end")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _leading_whitespace(line: str) -> str:
|
|
160
|
+
"""Return the leading whitespace characters of *line*."""
|
|
161
|
+
return line[: len(line) - len(line.lstrip())]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _sel_line_range(ranges: tuple) -> tuple[int, int]:
|
|
165
|
+
"""Return (start_line, end_line) from a tag_ranges("sel") result.
|
|
166
|
+
|
|
167
|
+
If the selection ends exactly at column 0 of the last line (i.e. the user
|
|
168
|
+
selected to the very start of a line without including any content), that
|
|
169
|
+
trailing line is excluded so we don't indent/dedent a line the user didn't
|
|
170
|
+
intend to touch.
|
|
171
|
+
"""
|
|
172
|
+
start_str, end_str = str(ranges[0]), str(ranges[1])
|
|
173
|
+
start_line = int(start_str.split(".")[0])
|
|
174
|
+
end_line = int(end_str.split(".")[0])
|
|
175
|
+
if end_str.split(".")[1] == "0" and end_line > start_line:
|
|
176
|
+
end_line -= 1
|
|
177
|
+
return start_line, end_line
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""EditFilter base class and FilterChain.
|
|
2
|
+
|
|
3
|
+
Adaptation of IDLE's Delegator/Percolator pattern
|
|
4
|
+
(idlelib/delegator.py + idlelib/percolator.py).
|
|
5
|
+
|
|
6
|
+
WidgetRedirector intercepts every insert/delete at the Tk command level —
|
|
7
|
+
including keyboard input from Tk's default text bindings — and routes them
|
|
8
|
+
through the Python filter chain. The bottom of the chain calls the original
|
|
9
|
+
Tk command via OriginalCommand objects returned by redirector.register(),
|
|
10
|
+
which are clean Python callables, not raw Tcl strings.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import tkinter as tk
|
|
15
|
+
from idlelib.redirector import WidgetRedirector
|
|
16
|
+
from typing import TYPE_CHECKING
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# NOTE(editfilter-public-api): EditFilter was demoted from the top-level public
|
|
23
|
+
# namespace during the API Reference restructure (Stage 4). It is a Tk-coupled
|
|
24
|
+
# extension hook — subclasses work in raw Tk text indices, tags, and the Tk Text
|
|
25
|
+
# API — which contradicts the framework's "Tkinter is invisible" premise, and it
|
|
26
|
+
# ships no narrative docs. CodeEditor still accepts user filters via
|
|
27
|
+
# `extensions=`/`install()`, so it stays importable for power users, but it is no
|
|
28
|
+
# longer presented as compose surface. TODO: decide whether CodeEditor should have
|
|
29
|
+
# a real, documented, de-Tkinter-ed extension/plugin API (one that hides Tk text
|
|
30
|
+
# indices behind framework-level edit events) and, if so, design that as the public
|
|
31
|
+
# face — re-promoting only once it no longer leaks Tk. See memory
|
|
32
|
+
# project_editfilter_public_api.
|
|
33
|
+
class EditFilter:
|
|
34
|
+
"""Sits in the mutation chain between user code and the Tk Text widget.
|
|
35
|
+
|
|
36
|
+
Subclass and override `insert` and/or `delete` to intercept edits.
|
|
37
|
+
Always forward to `self.delegate.insert` / `self.delegate.delete`
|
|
38
|
+
unless you intend to suppress the edit.
|
|
39
|
+
|
|
40
|
+
Attribute reads (`self.get(...)`, `self.tag_add(...)`, etc.) forward
|
|
41
|
+
automatically to the next delegate via `__getattr__`, so a filter
|
|
42
|
+
behaves like the Text widget itself when reading state.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self) -> None:
|
|
46
|
+
self.delegate: EditFilter | _BottomFilter | None = None
|
|
47
|
+
self._cache: set[str] = set()
|
|
48
|
+
|
|
49
|
+
def __getattr__(self, name: str):
|
|
50
|
+
if name.startswith('_') or self.delegate is None:
|
|
51
|
+
raise AttributeError(name)
|
|
52
|
+
attr = getattr(self.delegate, name)
|
|
53
|
+
setattr(self, name, attr)
|
|
54
|
+
self._cache.add(name)
|
|
55
|
+
return attr
|
|
56
|
+
|
|
57
|
+
def _reset_cache(self) -> None:
|
|
58
|
+
for k in self._cache:
|
|
59
|
+
try:
|
|
60
|
+
delattr(self, k)
|
|
61
|
+
except AttributeError:
|
|
62
|
+
pass
|
|
63
|
+
self._cache.clear()
|
|
64
|
+
|
|
65
|
+
def _set_delegate(self, d: EditFilter | _BottomFilter | None) -> None:
|
|
66
|
+
self._reset_cache()
|
|
67
|
+
self.delegate = d
|
|
68
|
+
|
|
69
|
+
def insert(self, index: str, chars: str, tags=None) -> None:
|
|
70
|
+
"""Forward an insert to the next delegate."""
|
|
71
|
+
self.delegate.insert(index, chars, tags)
|
|
72
|
+
|
|
73
|
+
def delete(self, index1: str, index2: str | None = None) -> None:
|
|
74
|
+
"""Forward a delete to the next delegate."""
|
|
75
|
+
self.delegate.delete(index1, index2)
|
|
76
|
+
|
|
77
|
+
def attach(self, core: _MultilineCore) -> None:
|
|
78
|
+
"""Called when this filter is added to a core's chain."""
|
|
79
|
+
|
|
80
|
+
def detach(self, core: _MultilineCore) -> None:
|
|
81
|
+
"""Called when this filter is removed from a core's chain."""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _BottomFilter:
|
|
85
|
+
"""Terminal filter — calls the original Tk Text command via OriginalCommand.
|
|
86
|
+
|
|
87
|
+
WidgetRedirector.register() returns an OriginalCommand callable for each
|
|
88
|
+
intercepted operation. Calling it routes directly to the renamed Tcl proc
|
|
89
|
+
(e.g. `.!text_orig insert ...`) without going back through the chain.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(self, orig_insert, orig_delete, text: tk.Text) -> None:
|
|
93
|
+
self._orig_insert = orig_insert # OriginalCommand returned by register()
|
|
94
|
+
self._orig_delete = orig_delete # OriginalCommand returned by register()
|
|
95
|
+
self._text = text
|
|
96
|
+
|
|
97
|
+
def __getattr__(self, name: str):
|
|
98
|
+
return getattr(self._text, name)
|
|
99
|
+
|
|
100
|
+
def insert(self, index: str, chars: str, tags=None) -> None:
|
|
101
|
+
if tags:
|
|
102
|
+
self._orig_insert(index, chars, tags)
|
|
103
|
+
else:
|
|
104
|
+
self._orig_insert(index, chars)
|
|
105
|
+
|
|
106
|
+
def delete(self, index1: str, index2: str | None = None) -> None:
|
|
107
|
+
if index2 is not None:
|
|
108
|
+
self._orig_delete(index1, index2)
|
|
109
|
+
else:
|
|
110
|
+
self._orig_delete(index1)
|
|
111
|
+
|
|
112
|
+
def _set_delegate(self, d) -> None:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class FilterChain:
|
|
117
|
+
"""Owns the ordered chain of EditFilters.
|
|
118
|
+
|
|
119
|
+
Uses WidgetRedirector so that every insert/delete on the Text widget —
|
|
120
|
+
including those triggered by Tk's default key bindings — flows through
|
|
121
|
+
the chain before reaching the underlying Tk Text widget.
|
|
122
|
+
"""
|
|
123
|
+
|
|
124
|
+
def __init__(self, text: tk.Text) -> None:
|
|
125
|
+
self._text = text
|
|
126
|
+
self._redirector = WidgetRedirector(text)
|
|
127
|
+
self._filters: list[EditFilter] = []
|
|
128
|
+
|
|
129
|
+
# Register our handlers; register() returns OriginalCommand callables
|
|
130
|
+
orig_insert = self._redirector.register('insert', self._chain_insert)
|
|
131
|
+
orig_delete = self._redirector.register('delete', self._chain_delete)
|
|
132
|
+
|
|
133
|
+
self._bottom = _BottomFilter(orig_insert, orig_delete, text)
|
|
134
|
+
self._top: EditFilter | _BottomFilter = self._bottom
|
|
135
|
+
|
|
136
|
+
def _chain_insert(self, index: str, chars: str, tags=None) -> None:
|
|
137
|
+
self._top.insert(index, chars, tags)
|
|
138
|
+
|
|
139
|
+
def _chain_delete(self, index1: str, index2: str | None = None) -> None:
|
|
140
|
+
self._top.delete(index1, index2)
|
|
141
|
+
|
|
142
|
+
def add(self, f: EditFilter) -> None:
|
|
143
|
+
"""Prepend a filter to the top of the chain."""
|
|
144
|
+
f._set_delegate(self._top)
|
|
145
|
+
self._top = f
|
|
146
|
+
self._filters.insert(0, f)
|
|
147
|
+
|
|
148
|
+
def remove(self, f: EditFilter) -> None:
|
|
149
|
+
"""Remove a filter from the chain."""
|
|
150
|
+
if f not in self._filters:
|
|
151
|
+
return
|
|
152
|
+
idx = self._filters.index(f)
|
|
153
|
+
above = self._filters[idx - 1] if idx > 0 else None
|
|
154
|
+
below = self._filters[idx + 1] if idx < len(self._filters) - 1 else self._bottom
|
|
155
|
+
|
|
156
|
+
if above is None:
|
|
157
|
+
self._top = below
|
|
158
|
+
else:
|
|
159
|
+
above._set_delegate(below)
|
|
160
|
+
|
|
161
|
+
f._set_delegate(None)
|
|
162
|
+
self._filters.pop(idx)
|
|
163
|
+
|
|
164
|
+
def destroy(self) -> None:
|
|
165
|
+
"""Uninstall the redirector."""
|
|
166
|
+
try:
|
|
167
|
+
self._redirector.unregister('insert')
|
|
168
|
+
self._redirector.unregister('delete')
|
|
169
|
+
self._redirector.close()
|
|
170
|
+
except Exception:
|
|
171
|
+
pass
|