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,495 @@
|
|
|
1
|
+
"""_MultilineCore — shared internal engine for TextArea and CodeEditor.
|
|
2
|
+
|
|
3
|
+
Owns the tk.Text widget, filter chain, scrollbar wiring, and the
|
|
4
|
+
sidebar/decoration registries used by CodeEditor extensions.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import tkinter as tk
|
|
9
|
+
from typing import Callable, Literal
|
|
10
|
+
|
|
11
|
+
from bootstack.events import TextModifiedEvent
|
|
12
|
+
from bootstack.widgets._impl.primitives.scrollbar import Scrollbar
|
|
13
|
+
from bootstack.widgets._impl.composites.textarea.filter import EditFilter, FilterChain
|
|
14
|
+
from bootstack.widgets._impl.composites.textarea.change import ChangeNotifier
|
|
15
|
+
from bootstack.widgets._impl.composites.textarea.undo import UndoManager
|
|
16
|
+
from bootstack.widgets._impl.composites.textarea.style_registry import StyleRegistry
|
|
17
|
+
from bootstack.widgets._impl.composites.textarea.diff import DecorationDiff
|
|
18
|
+
from bootstack.widgets._impl.composites.textarea.decoration import (
|
|
19
|
+
LineDecoration, Position, RangeDecoration,
|
|
20
|
+
)
|
|
21
|
+
from bootstack.widgets.types import Master
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
ScrollbarMode = Literal["vertical", "both", "none", "auto"]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class _MultilineCore(tk.Frame):
|
|
28
|
+
"""Internal engine shared by TextArea and CodeEditor.
|
|
29
|
+
|
|
30
|
+
Not part of the public API. Use TextArea or CodeEditor instead.
|
|
31
|
+
|
|
32
|
+
Wraps a tk.Text widget with:
|
|
33
|
+
- A FilterChain routing all inserts/deletes through registered filters.
|
|
34
|
+
- Built-in ChangeNotifier (fires <<Change>>) and UndoManager.
|
|
35
|
+
- Scrollbar wiring with auto/always/none/both modes.
|
|
36
|
+
- value/textsignal reactive binding.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
master: Master = None,
|
|
42
|
+
value: str = "",
|
|
43
|
+
wrap: bool = True,
|
|
44
|
+
height: int = 4,
|
|
45
|
+
scrollbars: ScrollbarMode = "auto",
|
|
46
|
+
font: str = "body",
|
|
47
|
+
read_only: bool = False,
|
|
48
|
+
**text_kwargs,
|
|
49
|
+
) -> None:
|
|
50
|
+
super().__init__(master, highlightthickness=0, borderwidth=0)
|
|
51
|
+
|
|
52
|
+
self._scrollbar_mode = scrollbars
|
|
53
|
+
self._read_only = read_only
|
|
54
|
+
self._signal = None
|
|
55
|
+
self._signal_trace_id = None
|
|
56
|
+
|
|
57
|
+
# Detect windowing system for mousewheel handling
|
|
58
|
+
self.winsys: str = self.tk.call("tk", "windowingsystem")
|
|
59
|
+
|
|
60
|
+
# ── text widget ───────────────────────────────────────────────────
|
|
61
|
+
# undo=False: we manage the undo stack ourselves via UndoManager
|
|
62
|
+
# (character-class grouping for word-level undo, matching IDLE).
|
|
63
|
+
wrap_mode = tk.WORD if wrap else tk.NONE
|
|
64
|
+
self.text = tk.Text(
|
|
65
|
+
self,
|
|
66
|
+
wrap=wrap_mode,
|
|
67
|
+
height=height,
|
|
68
|
+
font=font,
|
|
69
|
+
undo=False,
|
|
70
|
+
borderwidth=0,
|
|
71
|
+
highlightthickness=0,
|
|
72
|
+
**text_kwargs,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# ── scrollbars ────────────────────────────────────────────────────
|
|
76
|
+
self._vscroll = Scrollbar(self, orient="vertical", command=self.text.yview)
|
|
77
|
+
self._hscroll = Scrollbar(self, orient="horizontal", command=self.text.xview)
|
|
78
|
+
|
|
79
|
+
self.text.configure(yscrollcommand=self._on_yscroll)
|
|
80
|
+
if not wrap:
|
|
81
|
+
self.text.configure(xscrollcommand=self._on_xscroll)
|
|
82
|
+
|
|
83
|
+
# ── layout ────────────────────────────────────────────────────────
|
|
84
|
+
# col 0: left sidebars (auto-sized, initially empty)
|
|
85
|
+
# col 1: text (fills available space)
|
|
86
|
+
# col 2: vertical scrollbar
|
|
87
|
+
self._sidebars: list = [] # list of attached Sidebar instances
|
|
88
|
+
self._text_col = 1 # text widget column (shifts right if needed)
|
|
89
|
+
self.grid_rowconfigure(0, weight=1)
|
|
90
|
+
self.grid_columnconfigure(0, weight=0) # sidebar column
|
|
91
|
+
self.grid_columnconfigure(1, weight=1) # text column
|
|
92
|
+
self.text.grid(row=0, column=1, sticky="nsew")
|
|
93
|
+
|
|
94
|
+
self._update_scrollbar_layout()
|
|
95
|
+
|
|
96
|
+
# ── filter chain ──────────────────────────────────────────────────
|
|
97
|
+
self._chain = FilterChain(self.text)
|
|
98
|
+
|
|
99
|
+
self._undo_manager = UndoManager()
|
|
100
|
+
self._change_notifier = ChangeNotifier()
|
|
101
|
+
|
|
102
|
+
self._install_filter(self._undo_manager)
|
|
103
|
+
self._install_filter(self._change_notifier)
|
|
104
|
+
|
|
105
|
+
# ── style registry + decoration diff ─────────────────────────────
|
|
106
|
+
self._style_registry = StyleRegistry(self)
|
|
107
|
+
self._decoration_diff = DecorationDiff(self, self._style_registry)
|
|
108
|
+
|
|
109
|
+
# ── mousewheel ────────────────────────────────────────────────────
|
|
110
|
+
self._scroll_tag = f"_mcore_{id(self)}"
|
|
111
|
+
self._setup_mousewheel()
|
|
112
|
+
|
|
113
|
+
# ── initial value ─────────────────────────────────────────────────
|
|
114
|
+
if value:
|
|
115
|
+
self.text.insert("1.0", value)
|
|
116
|
+
self._undo_manager.reset_undo() # clear history after seeding
|
|
117
|
+
|
|
118
|
+
if read_only:
|
|
119
|
+
self.text.configure(state=tk.DISABLED)
|
|
120
|
+
|
|
121
|
+
# Keyboard undo/redo: drive our UndoManager from Tk's platform-aware
|
|
122
|
+
# <<Undo>>/<<Redo>> virtual events rather than hardcoding keys, so the
|
|
123
|
+
# native per-OS shortcut applies (Ctrl+Z / Ctrl+Y on Windows & Linux,
|
|
124
|
+
# Cmd+Z / Cmd+Shift+Z on macOS). undo=False means Tk's own handlers for
|
|
125
|
+
# these events no-op; the widget-level binding runs first and "break"
|
|
126
|
+
# suppresses the (empty) class handler.
|
|
127
|
+
self.text.bind("<<Undo>>", lambda e: (self._undo_manager.undo(), "break")[1], add="+")
|
|
128
|
+
self.text.bind("<<Redo>>", lambda e: (self._undo_manager.redo(), "break")[1], add="+")
|
|
129
|
+
|
|
130
|
+
# <<Modified>> still fires from Tk's edit_modified flag (independent
|
|
131
|
+
# of the undo stack) — enrich with is_dirty payload.
|
|
132
|
+
self.text.bind("<<Modified>>", self._on_tk_modified, add="+")
|
|
133
|
+
|
|
134
|
+
self.bind("<Destroy>", self._on_destroy, add="+")
|
|
135
|
+
|
|
136
|
+
# ── filter management ─────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
def _install_filter(self, f: EditFilter) -> None:
|
|
139
|
+
self._chain.add(f)
|
|
140
|
+
f.attach(self)
|
|
141
|
+
|
|
142
|
+
def add_filter(self, f: EditFilter) -> None:
|
|
143
|
+
"""Install a custom filter at the top of the chain."""
|
|
144
|
+
self._install_filter(f)
|
|
145
|
+
|
|
146
|
+
def remove_filter(self, f: EditFilter) -> None:
|
|
147
|
+
"""Remove a custom filter from the chain."""
|
|
148
|
+
f.detach(self)
|
|
149
|
+
self._chain.remove(f)
|
|
150
|
+
|
|
151
|
+
# ── sidebar management ────────────────────────────────────────────────
|
|
152
|
+
|
|
153
|
+
def add_sidebar(self, sidebar, side: str = "left") -> None:
|
|
154
|
+
"""Attach a sidebar widget to this core.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
sidebar: A `Sidebar` instance.
|
|
158
|
+
side: `"left"` (default) — only left sidebars are supported
|
|
159
|
+
in the current layout.
|
|
160
|
+
"""
|
|
161
|
+
self._sidebars.append(sidebar)
|
|
162
|
+
sidebar.attach(self, side=side)
|
|
163
|
+
|
|
164
|
+
def remove_sidebar(self, sidebar) -> None:
|
|
165
|
+
"""Detach a sidebar widget from this core.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
sidebar: The `Sidebar` instance to remove.
|
|
169
|
+
"""
|
|
170
|
+
if sidebar in self._sidebars:
|
|
171
|
+
self._sidebars.remove(sidebar)
|
|
172
|
+
sidebar.detach(self)
|
|
173
|
+
|
|
174
|
+
# ── scrollbars ────────────────────────────────────────────────────────
|
|
175
|
+
|
|
176
|
+
def _on_yscroll(self, first: str, last: str) -> None:
|
|
177
|
+
self._vscroll.set(first, last)
|
|
178
|
+
for sb in self._sidebars:
|
|
179
|
+
sb.on_scroll(first, last)
|
|
180
|
+
if self._scrollbar_mode == "auto":
|
|
181
|
+
vscroll_col = self._text_col + 1
|
|
182
|
+
if float(first) <= 0.0 and float(last) >= 1.0:
|
|
183
|
+
self._vscroll.grid_remove()
|
|
184
|
+
else:
|
|
185
|
+
self._vscroll.grid(row=0, column=vscroll_col, sticky="ns")
|
|
186
|
+
|
|
187
|
+
def _on_xscroll(self, first: str, last: str) -> None:
|
|
188
|
+
self._hscroll.set(first, last)
|
|
189
|
+
if self._scrollbar_mode == "auto":
|
|
190
|
+
if float(first) <= 0.0 and float(last) >= 1.0:
|
|
191
|
+
self._hscroll.grid_remove()
|
|
192
|
+
else:
|
|
193
|
+
self._hscroll.grid(row=1, column=self._text_col, sticky="ew")
|
|
194
|
+
|
|
195
|
+
def _update_scrollbar_layout(self) -> None:
|
|
196
|
+
mode = self._scrollbar_mode
|
|
197
|
+
vscroll_col = self._text_col + 1
|
|
198
|
+
if mode in ("vertical", "both", "auto"):
|
|
199
|
+
self._vscroll.grid(row=0, column=vscroll_col, sticky="ns")
|
|
200
|
+
else:
|
|
201
|
+
self._vscroll.grid_remove()
|
|
202
|
+
|
|
203
|
+
if mode == "both":
|
|
204
|
+
self._hscroll.grid(row=1, column=self._text_col, sticky="ew")
|
|
205
|
+
else:
|
|
206
|
+
self._hscroll.grid_remove()
|
|
207
|
+
|
|
208
|
+
if mode == "auto":
|
|
209
|
+
self._vscroll.grid_remove()
|
|
210
|
+
self._hscroll.grid_remove()
|
|
211
|
+
|
|
212
|
+
# ── mousewheel ────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
def _setup_mousewheel(self) -> None:
|
|
215
|
+
if self.winsys == "x11":
|
|
216
|
+
self.bind_class(self._scroll_tag, "<Button-4>", self._on_wheel)
|
|
217
|
+
self.bind_class(self._scroll_tag, "<Button-5>", self._on_wheel)
|
|
218
|
+
else:
|
|
219
|
+
self.bind_class(self._scroll_tag, "<MouseWheel>", self._on_wheel)
|
|
220
|
+
tags = list(self.text.bindtags())
|
|
221
|
+
if self._scroll_tag not in tags:
|
|
222
|
+
tags.insert(1, self._scroll_tag)
|
|
223
|
+
self.text.bindtags(tuple(tags))
|
|
224
|
+
|
|
225
|
+
def _on_wheel(self, event: tk.Event) -> None:
|
|
226
|
+
if self.winsys == "win32":
|
|
227
|
+
delta = -int(event.delta / 120)
|
|
228
|
+
elif self.winsys == "aqua":
|
|
229
|
+
delta = -event.delta
|
|
230
|
+
elif event.num == 4:
|
|
231
|
+
delta = -1
|
|
232
|
+
else:
|
|
233
|
+
delta = 1
|
|
234
|
+
self.text.yview_scroll(delta, "units")
|
|
235
|
+
|
|
236
|
+
# ── value / signal ────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
@property
|
|
239
|
+
def value(self) -> str:
|
|
240
|
+
"""Full text content (trailing newline stripped)."""
|
|
241
|
+
return self.text.get("1.0", "end-1c")
|
|
242
|
+
|
|
243
|
+
@value.setter
|
|
244
|
+
def value(self, text: str) -> None:
|
|
245
|
+
was_disabled = self._read_only
|
|
246
|
+
if was_disabled:
|
|
247
|
+
self.text.configure(state=tk.NORMAL)
|
|
248
|
+
self.text.delete("1.0", tk.END)
|
|
249
|
+
if text:
|
|
250
|
+
self.text.insert("1.0", text)
|
|
251
|
+
if was_disabled:
|
|
252
|
+
self.text.configure(state=tk.DISABLED)
|
|
253
|
+
|
|
254
|
+
@property
|
|
255
|
+
def read_only(self) -> bool:
|
|
256
|
+
"""Whether the text is visible but not editable."""
|
|
257
|
+
return self._read_only
|
|
258
|
+
|
|
259
|
+
@read_only.setter
|
|
260
|
+
def read_only(self, value: bool) -> None:
|
|
261
|
+
# Keep the flag and the widget state in lockstep: the value/insert
|
|
262
|
+
# setters consult `_read_only` to know whether to lift the disabled
|
|
263
|
+
# state for a programmatic write.
|
|
264
|
+
self._read_only = bool(value)
|
|
265
|
+
self.text.configure(state=tk.DISABLED if self._read_only else tk.NORMAL)
|
|
266
|
+
|
|
267
|
+
def insert(self, index: str, text: str) -> None:
|
|
268
|
+
"""Insert text at `index`, applying even when read-only.
|
|
269
|
+
|
|
270
|
+
Read-only blocks *user* edits, not programmatic ones, so this lifts the
|
|
271
|
+
disabled state for the write and restores it (matching the value setter).
|
|
272
|
+
"""
|
|
273
|
+
was_disabled = self._read_only
|
|
274
|
+
if was_disabled:
|
|
275
|
+
self.text.configure(state=tk.NORMAL)
|
|
276
|
+
self.text.insert(index, text)
|
|
277
|
+
if was_disabled:
|
|
278
|
+
self.text.configure(state=tk.DISABLED)
|
|
279
|
+
|
|
280
|
+
def bind_signal(self, signal) -> None:
|
|
281
|
+
"""Bind a Signal[str] — updates the text when the signal changes."""
|
|
282
|
+
if self._signal is not None:
|
|
283
|
+
self._unbind_signal()
|
|
284
|
+
self._signal = signal
|
|
285
|
+
self._signal_trace_id = signal.subscribe(self._on_signal_change)
|
|
286
|
+
# Set initial value from signal
|
|
287
|
+
v = signal()
|
|
288
|
+
if v is not None:
|
|
289
|
+
self.value = str(v)
|
|
290
|
+
|
|
291
|
+
def _unbind_signal(self) -> None:
|
|
292
|
+
if self._signal is not None and self._signal_trace_id is not None:
|
|
293
|
+
try:
|
|
294
|
+
self._signal_trace_id.cancel()
|
|
295
|
+
except Exception:
|
|
296
|
+
pass
|
|
297
|
+
self._signal = None
|
|
298
|
+
self._signal_trace_id = None
|
|
299
|
+
|
|
300
|
+
def _on_signal_change(self, new_value) -> None:
|
|
301
|
+
if new_value is not None:
|
|
302
|
+
self.value = str(new_value)
|
|
303
|
+
|
|
304
|
+
# ── dirty tracking ────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
@property
|
|
307
|
+
def is_dirty(self) -> bool:
|
|
308
|
+
"""True if the content has changed since last save."""
|
|
309
|
+
return self._undo_manager.is_dirty
|
|
310
|
+
|
|
311
|
+
def mark_saved(self) -> None:
|
|
312
|
+
"""Mark current state as the clean baseline."""
|
|
313
|
+
self._undo_manager.mark_saved()
|
|
314
|
+
|
|
315
|
+
# ── undo / redo ───────────────────────────────────────────────────────
|
|
316
|
+
|
|
317
|
+
def undo(self) -> None:
|
|
318
|
+
"""Undo the last edit."""
|
|
319
|
+
self._undo_manager.undo()
|
|
320
|
+
|
|
321
|
+
def redo(self) -> None:
|
|
322
|
+
"""Redo the last undone edit."""
|
|
323
|
+
self._undo_manager.redo()
|
|
324
|
+
|
|
325
|
+
def undo_block_start(self) -> None:
|
|
326
|
+
"""Begin a compound undo block."""
|
|
327
|
+
self._undo_manager.undo_block_start()
|
|
328
|
+
|
|
329
|
+
def undo_block_stop(self) -> None:
|
|
330
|
+
"""End a compound undo block."""
|
|
331
|
+
self._undo_manager.undo_block_stop()
|
|
332
|
+
|
|
333
|
+
# ── decoration / style API ────────────────────────────────────────────
|
|
334
|
+
|
|
335
|
+
def register_layer(self, name: str, priority: int = 0) -> None:
|
|
336
|
+
"""Register a decoration layer.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
name: Layer identifier. Used to namespace Tk tags.
|
|
340
|
+
priority: Tag raise order — higher priority layers render on top.
|
|
341
|
+
"""
|
|
342
|
+
self._decoration_diff.register_layer(name, priority)
|
|
343
|
+
|
|
344
|
+
def define_style(self, name: str, **attrs) -> None:
|
|
345
|
+
"""Define a named text style for use in decorations.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
name: Style identifier referenced in decoration objects.
|
|
349
|
+
**attrs: Style attributes — `foreground`, `background`,
|
|
350
|
+
`font`, `underline`, `spacing1`, `spacing3`.
|
|
351
|
+
Values may be theme tokens (e.g. `'primary'`) or literal
|
|
352
|
+
Tk color/font strings.
|
|
353
|
+
"""
|
|
354
|
+
self._style_registry.define_style(name, **attrs)
|
|
355
|
+
|
|
356
|
+
def set_decorations(self, layer: str, decorations) -> None:
|
|
357
|
+
"""Apply a decoration set to *layer*, diffing against the previous set.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
layer: The decoration layer to update.
|
|
361
|
+
decorations: Sequence of `RangeDecoration` or `LineDecoration`.
|
|
362
|
+
"""
|
|
363
|
+
self._decoration_diff.set_decorations(layer, decorations)
|
|
364
|
+
|
|
365
|
+
def clear_decorations(self, layer: str) -> None:
|
|
366
|
+
"""Remove all decorations from *layer*.
|
|
367
|
+
|
|
368
|
+
Args:
|
|
369
|
+
layer: The layer to clear.
|
|
370
|
+
"""
|
|
371
|
+
self._decoration_diff.clear_layer(layer)
|
|
372
|
+
|
|
373
|
+
# ── internal Tk event enrichment ─────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
def _on_tk_modified(self, _event=None) -> None:
|
|
376
|
+
self.text.event_generate(
|
|
377
|
+
"<<TextModified>>",
|
|
378
|
+
data=TextModifiedEvent(is_dirty=self.is_dirty),
|
|
379
|
+
when="tail",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
# ── event subscription ────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
def on_change(self, callback: Callable) -> str:
|
|
385
|
+
"""Register a callback for `<<Change>>` events.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
callback: Receives the Tk event. `event.data` contains
|
|
389
|
+
`{"op": "insert"|"delete", "index": str}`.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
Bind ID — pass to `off_change()` to unsubscribe.
|
|
393
|
+
"""
|
|
394
|
+
return self.text.bind("<<Change>>", callback, add="+")
|
|
395
|
+
|
|
396
|
+
def off_change(self, bind_id: str | None = None) -> None:
|
|
397
|
+
"""Unsubscribe from `<<Change>>`.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
bind_id: ID returned by `on_change()`. If None, removes all.
|
|
401
|
+
"""
|
|
402
|
+
self.text.unbind("<<Change>>", bind_id)
|
|
403
|
+
|
|
404
|
+
def on_modified(self, callback: Callable) -> str:
|
|
405
|
+
"""Register a callback for `<<TextModified>>` events.
|
|
406
|
+
|
|
407
|
+
Fires when the dirty state changes. `event.is_dirty`
|
|
408
|
+
reflects the new state.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
callback: Receives the Tk event with `event.data = {"is_dirty": bool}`.
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
Bind ID — pass to `off_modified()` to unsubscribe.
|
|
415
|
+
"""
|
|
416
|
+
return self.text.bind("<<TextModified>>", callback, add="+")
|
|
417
|
+
|
|
418
|
+
def off_modified(self, bind_id: str | None = None) -> None:
|
|
419
|
+
"""Unsubscribe from `<<TextModified>>`.
|
|
420
|
+
|
|
421
|
+
Args:
|
|
422
|
+
bind_id: ID returned by `on_modified()`. If None, removes all.
|
|
423
|
+
"""
|
|
424
|
+
self.text.unbind("<<TextModified>>", bind_id)
|
|
425
|
+
|
|
426
|
+
def on_undo(self, callback: Callable) -> str:
|
|
427
|
+
"""Register a callback for `<<TextUndo>>` events.
|
|
428
|
+
|
|
429
|
+
Fires after an undo completes. `event.data.text` is the
|
|
430
|
+
text content after the undo.
|
|
431
|
+
|
|
432
|
+
Args:
|
|
433
|
+
callback: Receives the Tk event with `event.data = {"value": str}`.
|
|
434
|
+
|
|
435
|
+
Returns:
|
|
436
|
+
Bind ID — pass to `off_undo()` to unsubscribe.
|
|
437
|
+
"""
|
|
438
|
+
return self.text.bind("<<TextUndo>>", callback, add="+")
|
|
439
|
+
|
|
440
|
+
def off_undo(self, bind_id: str | None = None) -> None:
|
|
441
|
+
"""Unsubscribe from `<<TextUndo>>`.
|
|
442
|
+
|
|
443
|
+
Args:
|
|
444
|
+
bind_id: ID returned by `on_undo()`. If None, removes all.
|
|
445
|
+
"""
|
|
446
|
+
self.text.unbind("<<TextUndo>>", bind_id)
|
|
447
|
+
|
|
448
|
+
def on_redo(self, callback: Callable) -> str:
|
|
449
|
+
"""Register a callback for `<<TextRedo>>` events.
|
|
450
|
+
|
|
451
|
+
Fires after a redo completes. `event.data.text` is the
|
|
452
|
+
text content after the redo.
|
|
453
|
+
|
|
454
|
+
Args:
|
|
455
|
+
callback: Receives the Tk event with `event.data = {"value": str}`.
|
|
456
|
+
|
|
457
|
+
Returns:
|
|
458
|
+
Bind ID — pass to `off_redo()` to unsubscribe.
|
|
459
|
+
"""
|
|
460
|
+
return self.text.bind("<<TextRedo>>", callback, add="+")
|
|
461
|
+
|
|
462
|
+
def off_redo(self, bind_id: str | None = None) -> None:
|
|
463
|
+
"""Unsubscribe from `<<TextRedo>>`.
|
|
464
|
+
|
|
465
|
+
Args:
|
|
466
|
+
bind_id: ID returned by `on_redo()`. If None, removes all.
|
|
467
|
+
"""
|
|
468
|
+
self.text.unbind("<<TextRedo>>", bind_id)
|
|
469
|
+
|
|
470
|
+
# ── convenience: forward common Text API to self ──────────────────────
|
|
471
|
+
|
|
472
|
+
def bind(self, sequence=None, func=None, add=None):
|
|
473
|
+
return self.text.bind(sequence, func, add)
|
|
474
|
+
|
|
475
|
+
def unbind(self, sequence, funcid=None):
|
|
476
|
+
return self.text.unbind(sequence, funcid)
|
|
477
|
+
|
|
478
|
+
def focus_set(self):
|
|
479
|
+
self.text.focus_set()
|
|
480
|
+
|
|
481
|
+
# ── cleanup ───────────────────────────────────────────────────────────
|
|
482
|
+
|
|
483
|
+
def _on_destroy(self, event: tk.Event) -> None:
|
|
484
|
+
if event.widget is not self:
|
|
485
|
+
return
|
|
486
|
+
self._unbind_signal()
|
|
487
|
+
self._chain.destroy()
|
|
488
|
+
try:
|
|
489
|
+
if self.winsys == "x11":
|
|
490
|
+
self.unbind_class(self._scroll_tag, "<Button-4>")
|
|
491
|
+
self.unbind_class(self._scroll_tag, "<Button-5>")
|
|
492
|
+
else:
|
|
493
|
+
self.unbind_class(self._scroll_tag, "<MouseWheel>")
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Decoration types for the TextArea/CodeEditor decoration system."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Callable, Any
|
|
6
|
+
import tkinter as tk
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Position:
|
|
11
|
+
"""A line/column position in the text (1-indexed line, 0-indexed column)."""
|
|
12
|
+
line: int
|
|
13
|
+
col: int
|
|
14
|
+
|
|
15
|
+
def to_tk(self) -> str:
|
|
16
|
+
"""Convert to a Tk text index string (e.g. `'3.7'`)."""
|
|
17
|
+
return f"{self.line}.{self.col}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class RangeDecoration:
|
|
22
|
+
"""A styled range from *start* to *end*."""
|
|
23
|
+
start: Position
|
|
24
|
+
end: Position
|
|
25
|
+
style: str
|
|
26
|
+
meta: dict | None = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class LineDecoration:
|
|
31
|
+
"""A style applied to an entire line."""
|
|
32
|
+
line: int
|
|
33
|
+
style: str
|
|
34
|
+
meta: dict | None = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class WidgetDecoration:
|
|
39
|
+
"""A widget embedded at a position (e.g. inline diagnostic icon)."""
|
|
40
|
+
at: Position
|
|
41
|
+
widget_factory: Callable[[tk.Misc], tk.Widget]
|
|
42
|
+
meta: dict | None = None
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""Decoration-set diff engine.
|
|
2
|
+
|
|
3
|
+
Per-layer state stored as a dict keyed by (start_tk, end_tk, style).
|
|
4
|
+
Diff is a dict comparison: O(|symmetric difference|) Tk tag calls.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import tkinter as tk
|
|
9
|
+
from typing import Sequence, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
from bootstack.widgets._impl.composites.textarea.decoration import (
|
|
12
|
+
LineDecoration, RangeDecoration,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
|
|
17
|
+
from bootstack.widgets._impl.composites.textarea.style_registry import StyleRegistry
|
|
18
|
+
|
|
19
|
+
# Key: (start_tk_index, end_tk_index, style_name)
|
|
20
|
+
_LayerState = dict[tuple[str, str, str], dict | None]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DecorationDiff:
|
|
24
|
+
"""Applies decoration sets to a Text widget using a diff-based approach.
|
|
25
|
+
|
|
26
|
+
Maintains per-layer state so that only changed decorations cause Tk
|
|
27
|
+
tag_add / tag_remove calls. For a 200-decoration viewport with one
|
|
28
|
+
edited token, this is ~2 Tk calls instead of clearing and rebuilding.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, core: _MultilineCore, style_registry: StyleRegistry) -> None:
|
|
32
|
+
self._core = core
|
|
33
|
+
self._text = core.text
|
|
34
|
+
self._styles = style_registry
|
|
35
|
+
# layer -> current decoration state
|
|
36
|
+
self._layers: dict[str, _LayerState] = {}
|
|
37
|
+
# layer -> priority (lower = below)
|
|
38
|
+
self._priorities: dict[str, int] = {}
|
|
39
|
+
|
|
40
|
+
def register_layer(self, name: str, priority: int = 0) -> None:
|
|
41
|
+
"""Register a decoration layer.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
name: Layer identifier (used in style tag names).
|
|
45
|
+
priority: Tag raise order — higher priority layers appear on top.
|
|
46
|
+
"""
|
|
47
|
+
if name not in self._layers:
|
|
48
|
+
self._layers[name] = {}
|
|
49
|
+
self._priorities[name] = priority
|
|
50
|
+
|
|
51
|
+
def set_decorations(
|
|
52
|
+
self,
|
|
53
|
+
layer: str,
|
|
54
|
+
decorations: Sequence[RangeDecoration | LineDecoration],
|
|
55
|
+
) -> None:
|
|
56
|
+
"""Apply a new decoration set to *layer*, diffing against the previous set.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
layer: The layer to update.
|
|
60
|
+
decorations: New complete set of decorations for this layer.
|
|
61
|
+
"""
|
|
62
|
+
if layer not in self._layers:
|
|
63
|
+
self.register_layer(layer)
|
|
64
|
+
|
|
65
|
+
new_state = _build_state(decorations)
|
|
66
|
+
old_state = self._layers[layer]
|
|
67
|
+
|
|
68
|
+
old_keys = set(old_state.keys())
|
|
69
|
+
new_keys = set(new_state.keys())
|
|
70
|
+
|
|
71
|
+
removed = old_keys - new_keys
|
|
72
|
+
added = new_keys - old_keys
|
|
73
|
+
|
|
74
|
+
for start, end, style in removed:
|
|
75
|
+
tag = self._styles.tag_name(layer, style)
|
|
76
|
+
try:
|
|
77
|
+
self._text.tag_remove(tag, start, end)
|
|
78
|
+
except tk.TclError:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
for start, end, style in added:
|
|
82
|
+
tag = self._styles.tag_name(layer, style)
|
|
83
|
+
if not self._styles.is_configured(layer, style):
|
|
84
|
+
self._styles.configure_tag(layer, style)
|
|
85
|
+
try:
|
|
86
|
+
self._text.tag_add(tag, start, end)
|
|
87
|
+
except tk.TclError:
|
|
88
|
+
pass
|
|
89
|
+
|
|
90
|
+
self._layers[layer] = new_state
|
|
91
|
+
self._raise_layers()
|
|
92
|
+
|
|
93
|
+
def clear_layer(self, layer: str) -> None:
|
|
94
|
+
"""Remove all decorations from *layer*."""
|
|
95
|
+
self.set_decorations(layer, [])
|
|
96
|
+
|
|
97
|
+
def _raise_layers(self) -> None:
|
|
98
|
+
"""Enforce layer priority order via tag_raise."""
|
|
99
|
+
ordered = sorted(self._priorities.items(), key=lambda x: x[1])
|
|
100
|
+
for layer, _ in ordered:
|
|
101
|
+
state = self._layers.get(layer, {})
|
|
102
|
+
seen_tags: set[str] = set()
|
|
103
|
+
for _, _, style in state:
|
|
104
|
+
tag = self._styles.tag_name(layer, style)
|
|
105
|
+
if tag not in seen_tags:
|
|
106
|
+
seen_tags.add(tag)
|
|
107
|
+
try:
|
|
108
|
+
self._text.tag_raise(tag)
|
|
109
|
+
except tk.TclError:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _build_state(
|
|
114
|
+
decorations: Sequence[RangeDecoration | LineDecoration],
|
|
115
|
+
) -> _LayerState:
|
|
116
|
+
"""Convert a list of decorations to the internal dict-key format."""
|
|
117
|
+
state: _LayerState = {}
|
|
118
|
+
for dec in decorations:
|
|
119
|
+
if isinstance(dec, RangeDecoration):
|
|
120
|
+
key = (dec.start.to_tk(), dec.end.to_tk(), dec.style)
|
|
121
|
+
state[key] = dec.meta
|
|
122
|
+
elif isinstance(dec, LineDecoration):
|
|
123
|
+
start = f"{dec.line}.0"
|
|
124
|
+
end = f"{dec.line}.end"
|
|
125
|
+
key = (start, end, dec.style)
|
|
126
|
+
state[key] = dec.meta
|
|
127
|
+
return state
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Built-in TextArea/CodeEditor extensions."""
|