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,982 @@
|
|
|
1
|
+
"""bs.RangeSlider — a themed two-handle range slider widget."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import tkinter as tk
|
|
5
|
+
import tkinter.font as tkfont
|
|
6
|
+
from typing import Any, Callable
|
|
7
|
+
|
|
8
|
+
from bootstack.widgets._impl.mixins.configure_mixin import ConfigureDelegationMixin, configure_delegate
|
|
9
|
+
from bootstack.widgets.types import Master, Orient, AccentToken, SurfaceToken, WidgetState
|
|
10
|
+
from bootstack.signals import Signal
|
|
11
|
+
from bootstack.events import RangeSliderEvent, RangeSliderCommitEvent
|
|
12
|
+
from ._shared import (
|
|
13
|
+
HL, HANDLE_SIZE, TRACK_H, HALO_PAD,
|
|
14
|
+
BADGE_H, BADGE_PAD_X, BADGE_GAP, BADGE_FONT_SIZE,
|
|
15
|
+
TICK_GAP, MAJOR_TICK_H, MINOR_TICK_H, LABEL_GAP, LABEL_FONT_SIZE,
|
|
16
|
+
STEP, STEP_LARGE,
|
|
17
|
+
_make_badge, _make_handle, _make_track, _make_focus_ring,
|
|
18
|
+
resolve_colors, get_widget_bg,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RangeSlider(ConfigureDelegationMixin, tk.Frame):
|
|
23
|
+
_bs_autostyle = False
|
|
24
|
+
|
|
25
|
+
"""A themed two-handle range slider widget.
|
|
26
|
+
|
|
27
|
+
Provides a low-value and high-value handle that define a selected range
|
|
28
|
+
within `[minvalue, maxvalue]`. Keyboard model: the slider is a single tab
|
|
29
|
+
stop (`Tab` moves focus in, then straight out — never trapped). The
|
|
30
|
+
primary-axis arrows move the active handle; the cross-axis arrows switch
|
|
31
|
+
which handle is active, and the active handle shows a focus ring.
|
|
32
|
+
|
|
33
|
+
Current range is accessible via `lovalue` / `hivalue` properties,
|
|
34
|
+
`get_lo()` / `get_hi()` / `set_lo()` / `set_hi()` methods, or
|
|
35
|
+
bound `lo_variable` / `hi_variable` / `lo_signal` / `hi_signal`.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
master: Master = None,
|
|
41
|
+
*,
|
|
42
|
+
minvalue: float = 0,
|
|
43
|
+
maxvalue: float = 100,
|
|
44
|
+
lovalue: float = 0,
|
|
45
|
+
hivalue: float = 100,
|
|
46
|
+
orient: Orient = "horizontal",
|
|
47
|
+
accent: AccentToken = "primary",
|
|
48
|
+
surface: SurfaceToken = "content",
|
|
49
|
+
lo_variable: tk.DoubleVar | None = None,
|
|
50
|
+
hi_variable: tk.DoubleVar | None = None,
|
|
51
|
+
lo_signal: Signal[float] | None = None,
|
|
52
|
+
hi_signal: Signal[float] | None = None,
|
|
53
|
+
state: WidgetState = "normal",
|
|
54
|
+
show_value: bool = False,
|
|
55
|
+
tick_interval: float | None = None,
|
|
56
|
+
minor_ticks: int = 0,
|
|
57
|
+
tick_labels: bool = True,
|
|
58
|
+
tick_format: str = "{:.0f}",
|
|
59
|
+
step: float | None = None,
|
|
60
|
+
**kw: Any,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Create a RangeSlider widget.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
master: Parent widget.
|
|
66
|
+
minvalue: Minimum bound of the slider range.
|
|
67
|
+
maxvalue: Maximum bound of the slider range.
|
|
68
|
+
lovalue: Initial low-handle value. Ignored when `lo_variable`
|
|
69
|
+
or `lo_signal` is provided.
|
|
70
|
+
hivalue: Initial high-handle value. Ignored when `hi_variable`
|
|
71
|
+
or `hi_signal` is provided.
|
|
72
|
+
orient: Orientation — `'horizontal'` or `'vertical'`.
|
|
73
|
+
accent: Accent token controlling fill, handles, and focus ring color.
|
|
74
|
+
surface: Surface token for trough and background color context.
|
|
75
|
+
lo_variable: Tkinter DoubleVar bound to the low-handle value.
|
|
76
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
77
|
+
hi_variable: Tkinter DoubleVar bound to the high-handle value.
|
|
78
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
79
|
+
lo_signal: Reactive Signal linked to the low-handle value.
|
|
80
|
+
hi_signal: Reactive Signal linked to the high-handle value.
|
|
81
|
+
state: Widget state — `'normal'` or `'disabled'`.
|
|
82
|
+
show_value: Show floating badges above/beside each handle displaying
|
|
83
|
+
their current values.
|
|
84
|
+
tick_interval: Spacing between major tick marks. `None` disables ticks.
|
|
85
|
+
minor_ticks: Number of minor tick marks between each major tick.
|
|
86
|
+
tick_labels: Show numeric labels at major tick positions.
|
|
87
|
+
tick_format: Format string for tick and badge labels (e.g. `"{:.1f}°C"`).
|
|
88
|
+
step: Snap increment. When set, handle values snap to multiples of
|
|
89
|
+
`step` (measured from `minvalue`) and arrow keys move by `step`.
|
|
90
|
+
`None` leaves the values continuous. Independent of `tick_interval`.
|
|
91
|
+
"""
|
|
92
|
+
self._minvalue = float(minvalue)
|
|
93
|
+
self._maxvalue = float(maxvalue)
|
|
94
|
+
self._step_size = float(step) if step else None
|
|
95
|
+
self._kbd_step = self._step_size if self._step_size else float(STEP)
|
|
96
|
+
self._kbd_step_large = self._step_size * 10 if self._step_size else float(STEP_LARGE)
|
|
97
|
+
self._orient = orient
|
|
98
|
+
self._accent = accent
|
|
99
|
+
self._surface = surface
|
|
100
|
+
self._state = state
|
|
101
|
+
self._show_value = show_value
|
|
102
|
+
self._tick_interval = tick_interval
|
|
103
|
+
self._minor_ticks = minor_ticks
|
|
104
|
+
self._tick_labels = tick_labels
|
|
105
|
+
self._tick_format = tick_format
|
|
106
|
+
|
|
107
|
+
self._handle_size = HANDLE_SIZE
|
|
108
|
+
self._track_h = TRACK_H
|
|
109
|
+
|
|
110
|
+
if show_value:
|
|
111
|
+
if orient == "vertical":
|
|
112
|
+
_bfont = tkfont.Font(family="TkDefaultFont", size=BADGE_FONT_SIZE, weight="bold")
|
|
113
|
+
_bw = max(_bfont.measure(tick_format.format(float(maxvalue))) + BADGE_PAD_X * 2, BADGE_H)
|
|
114
|
+
self._badge_zone = _bw + BADGE_GAP
|
|
115
|
+
else:
|
|
116
|
+
self._badge_zone = BADGE_H + BADGE_GAP
|
|
117
|
+
else:
|
|
118
|
+
self._badge_zone = 0
|
|
119
|
+
self._tick_zone = self._calc_tick_zone()
|
|
120
|
+
# Reserve HALO_PAD on each side of the handle band so the keyboard focus
|
|
121
|
+
# ring (a halo just outside the active handle) is never clipped.
|
|
122
|
+
cross_size = self._badge_zone + HALO_PAD + self._handle_size + HALO_PAD + self._tick_zone
|
|
123
|
+
|
|
124
|
+
# Variable / signal binding for lo and hi — always create a Signal so
|
|
125
|
+
# callers get a clean reactive API; derive the tk.DoubleVar from it.
|
|
126
|
+
if lo_signal is not None:
|
|
127
|
+
self._lo_signal: Signal[float] = lo_signal
|
|
128
|
+
elif lo_variable is not None:
|
|
129
|
+
self._lo_signal = Signal(lo_variable.get())
|
|
130
|
+
self._lo_signal.var.trace_add(
|
|
131
|
+
"write", lambda *_: lo_variable.set(self._lo_signal.var.get()))
|
|
132
|
+
lo_variable.trace_add(
|
|
133
|
+
"write", lambda *_: self._lo_signal.var.set(lo_variable.get()))
|
|
134
|
+
else:
|
|
135
|
+
self._lo_signal = Signal(self._snap(float(lovalue)))
|
|
136
|
+
self._lo_var = self._lo_signal.var
|
|
137
|
+
|
|
138
|
+
if hi_signal is not None:
|
|
139
|
+
self._hi_signal: Signal[float] = hi_signal
|
|
140
|
+
elif hi_variable is not None:
|
|
141
|
+
self._hi_signal = Signal(hi_variable.get())
|
|
142
|
+
self._hi_signal.var.trace_add(
|
|
143
|
+
"write", lambda *_: hi_variable.set(self._hi_signal.var.get()))
|
|
144
|
+
hi_variable.trace_add(
|
|
145
|
+
"write", lambda *_: self._hi_signal.var.set(hi_variable.get()))
|
|
146
|
+
else:
|
|
147
|
+
self._hi_signal = Signal(self._snap(float(hivalue)))
|
|
148
|
+
self._hi_var = self._hi_signal.var
|
|
149
|
+
|
|
150
|
+
self._prev_lovalue: float = self._lo_var.get()
|
|
151
|
+
self._prev_hivalue: float = self._hi_var.get()
|
|
152
|
+
|
|
153
|
+
# Image refs (prevent GC)
|
|
154
|
+
self._handle_lo_photo: Any = None
|
|
155
|
+
self._handle_hi_photo: Any = None
|
|
156
|
+
self._track_photo: Any = None
|
|
157
|
+
|
|
158
|
+
# Badge geometry (set when show_value=True)
|
|
159
|
+
self._badge_w: int = 0
|
|
160
|
+
self._badge_lo_photo: Any = None
|
|
161
|
+
self._badge_hi_photo: Any = None
|
|
162
|
+
|
|
163
|
+
# Stable canvas width captured in _on_configure — used for badge clamping.
|
|
164
|
+
self._cw: int = 0
|
|
165
|
+
|
|
166
|
+
self._tick_items: list[int] = []
|
|
167
|
+
|
|
168
|
+
# Drag / keyboard focus state
|
|
169
|
+
self._dragging: str | None = None # "lo" | "hi" | None
|
|
170
|
+
self._focus_handle = "lo"
|
|
171
|
+
self._kbd_focused = False # True while the widget holds keyboard focus
|
|
172
|
+
# (gates the active-handle focus ring; set
|
|
173
|
+
# before the first _render_handles below)
|
|
174
|
+
|
|
175
|
+
kw.setdefault('bd', 0)
|
|
176
|
+
kw.setdefault('relief', 'flat')
|
|
177
|
+
|
|
178
|
+
self._colors = resolve_colors(self._accent, self._surface, get_widget_bg(master))
|
|
179
|
+
|
|
180
|
+
if self._orient == "horizontal":
|
|
181
|
+
super().__init__(
|
|
182
|
+
master,
|
|
183
|
+
width=1,
|
|
184
|
+
height=cross_size + HL * 2,
|
|
185
|
+
takefocus=True,
|
|
186
|
+
highlightthickness=HL,
|
|
187
|
+
highlightcolor=self._colors['bg'],
|
|
188
|
+
highlightbackground=self._colors['bg'],
|
|
189
|
+
**kw,
|
|
190
|
+
)
|
|
191
|
+
self.pack_propagate(False)
|
|
192
|
+
self._canvas = tk.Canvas(
|
|
193
|
+
self, width=1, height=cross_size,
|
|
194
|
+
bd=0, highlightthickness=0, bg=self._colors['bg'],
|
|
195
|
+
)
|
|
196
|
+
self._canvas.place(x=0, y=0, relwidth=1, height=cross_size)
|
|
197
|
+
else:
|
|
198
|
+
super().__init__(
|
|
199
|
+
master,
|
|
200
|
+
width=cross_size + HL * 2,
|
|
201
|
+
height=1,
|
|
202
|
+
takefocus=True,
|
|
203
|
+
highlightthickness=HL,
|
|
204
|
+
highlightcolor=self._colors['bg'],
|
|
205
|
+
highlightbackground=self._colors['bg'],
|
|
206
|
+
**kw,
|
|
207
|
+
)
|
|
208
|
+
self.pack_propagate(False)
|
|
209
|
+
self._canvas = tk.Canvas(
|
|
210
|
+
self, width=cross_size, height=1,
|
|
211
|
+
bd=0, highlightthickness=0, bg=self._colors['bg'],
|
|
212
|
+
)
|
|
213
|
+
self._canvas.place(x=0, y=0, relheight=1, width=cross_size)
|
|
214
|
+
|
|
215
|
+
self._track_center = self._badge_zone + HALO_PAD + self._handle_size // 2
|
|
216
|
+
|
|
217
|
+
if self._orient == "horizontal":
|
|
218
|
+
self._track_item = self._canvas.create_image(0, self._track_center, anchor="w")
|
|
219
|
+
self._handle_lo_item = self._canvas.create_image(0, self._track_center, anchor="center")
|
|
220
|
+
self._handle_hi_item = self._canvas.create_image(0, self._track_center, anchor="center")
|
|
221
|
+
else:
|
|
222
|
+
self._track_item = self._canvas.create_image(self._track_center, 0, anchor="n")
|
|
223
|
+
self._handle_lo_item = self._canvas.create_image(self._track_center, 0, anchor="center")
|
|
224
|
+
self._handle_hi_item = self._canvas.create_image(self._track_center, 0, anchor="center")
|
|
225
|
+
|
|
226
|
+
# Keyboard focus ring — drawn ON the active handle (a ring just inside its
|
|
227
|
+
# edge, transparent center), above both handles. Within the handle
|
|
228
|
+
# footprint (no overhang) so the track runs edge-to-edge. Hidden until the
|
|
229
|
+
# widget takes keyboard focus.
|
|
230
|
+
self._focus_ring_diam = self._handle_size - 2
|
|
231
|
+
self._focus_ring_photo = _make_focus_ring(self._focus_ring_diam, self._colors['focus'])
|
|
232
|
+
self._focus_ring_item = self._canvas.create_image(
|
|
233
|
+
0, 0, anchor="center", image=self._focus_ring_photo, state="hidden")
|
|
234
|
+
|
|
235
|
+
# Badge items — PIL image background + text items on top
|
|
236
|
+
self._badge_lo_item: int | None = None
|
|
237
|
+
self._badge_hi_item: int | None = None
|
|
238
|
+
self._badge_lo_text: int | None = None
|
|
239
|
+
self._badge_hi_text: int | None = None
|
|
240
|
+
if self._show_value:
|
|
241
|
+
badge_font = tkfont.Font(family="TkDefaultFont", size=BADGE_FONT_SIZE, weight="bold")
|
|
242
|
+
max_text = self._tick_format.format(self._maxvalue)
|
|
243
|
+
self._badge_w = max(badge_font.measure(max_text) + BADGE_PAD_X * 2, BADGE_H)
|
|
244
|
+
_bg = self._colors['badge_bg']
|
|
245
|
+
self._badge_lo_photo = _make_badge(self._badge_w, BADGE_H, _bg)
|
|
246
|
+
self._badge_hi_photo = _make_badge(self._badge_w, BADGE_H, _bg)
|
|
247
|
+
self._badge_lo_item = self._canvas.create_image(0, 0, anchor="nw", image=self._badge_lo_photo)
|
|
248
|
+
self._badge_hi_item = self._canvas.create_image(0, 0, anchor="nw", image=self._badge_hi_photo)
|
|
249
|
+
self._badge_lo_text = self._canvas.create_text(
|
|
250
|
+
0, 0, text="", fill=self._colors['badge_text'],
|
|
251
|
+
font=badge_font, anchor="center",
|
|
252
|
+
)
|
|
253
|
+
self._badge_hi_text = self._canvas.create_text(
|
|
254
|
+
0, 0, text="", fill=self._colors['badge_text'],
|
|
255
|
+
font=badge_font, anchor="center",
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
self._render_handles(self._colors)
|
|
259
|
+
|
|
260
|
+
# Keyboard-vs-mouse focus tracking (ring only on Tab navigation)
|
|
261
|
+
self._mouse_pressed = False
|
|
262
|
+
|
|
263
|
+
# Canvas-level click: pick nearest handle
|
|
264
|
+
self._canvas.bind("<Button-1>", self._on_canvas_click)
|
|
265
|
+
self._canvas.bind("<B1-Motion>", self._on_canvas_drag)
|
|
266
|
+
self._canvas.bind("<Button-1>", self._mouse_focus_set, add=True)
|
|
267
|
+
self._canvas.bind("<ButtonRelease-1>", self._on_release)
|
|
268
|
+
|
|
269
|
+
# Per-handle drag bindings
|
|
270
|
+
self._canvas.tag_bind(self._handle_lo_item, "<Button-1>",
|
|
271
|
+
lambda e: self._handle_press(e, "lo"))
|
|
272
|
+
self._canvas.tag_bind(self._handle_lo_item, "<B1-Motion>", self._on_handle_drag)
|
|
273
|
+
self._canvas.tag_bind(self._handle_lo_item, "<ButtonRelease-1>", self._on_release)
|
|
274
|
+
self._canvas.tag_bind(self._handle_hi_item, "<Button-1>",
|
|
275
|
+
lambda e: self._handle_press(e, "hi"))
|
|
276
|
+
self._canvas.tag_bind(self._handle_hi_item, "<B1-Motion>", self._on_handle_drag)
|
|
277
|
+
self._canvas.tag_bind(self._handle_hi_item, "<ButtonRelease-1>", self._on_release)
|
|
278
|
+
|
|
279
|
+
self.bind("<FocusIn>", self._on_focus_in)
|
|
280
|
+
self.bind("<FocusOut>", self._on_focus_out)
|
|
281
|
+
# Keyboard model: the slider is a SINGLE tab stop. We bind NO <Tab>, so
|
|
282
|
+
# Tk's normal focus traversal moves focus in and straight back out — focus
|
|
283
|
+
# is never trapped. The primary-axis arrows move the active handle; the
|
|
284
|
+
# cross-axis arrows switch which handle is active (a two-handle slider needs
|
|
285
|
+
# a switch gesture, and Tab is reserved for leaving the widget).
|
|
286
|
+
self.bind("<Home>", lambda e: self._jump(self._minvalue))
|
|
287
|
+
self.bind("<End>", lambda e: self._jump(self._maxvalue))
|
|
288
|
+
if self._orient == "horizontal":
|
|
289
|
+
self.bind("<Left>", lambda e: self._step(-self._kbd_step))
|
|
290
|
+
self.bind("<Right>", lambda e: self._step(+self._kbd_step))
|
|
291
|
+
self.bind("<Shift-Left>", lambda e: self._step(-self._kbd_step_large))
|
|
292
|
+
self.bind("<Shift-Right>", lambda e: self._step(+self._kbd_step_large))
|
|
293
|
+
self.bind("<Down>", lambda e: self._select_handle("lo"))
|
|
294
|
+
self.bind("<Up>", lambda e: self._select_handle("hi"))
|
|
295
|
+
else:
|
|
296
|
+
self.bind("<Down>", lambda e: self._step(-self._kbd_step))
|
|
297
|
+
self.bind("<Up>", lambda e: self._step(+self._kbd_step))
|
|
298
|
+
self.bind("<Shift-Down>", lambda e: self._step(-self._kbd_step_large))
|
|
299
|
+
self.bind("<Shift-Up>", lambda e: self._step(+self._kbd_step_large))
|
|
300
|
+
self.bind("<Left>", lambda e: self._select_handle("lo"))
|
|
301
|
+
self.bind("<Right>", lambda e: self._select_handle("hi"))
|
|
302
|
+
self._canvas.bind("<Configure>", self._on_configure)
|
|
303
|
+
|
|
304
|
+
self._lo_var.trace_add("write", self._on_lo_write)
|
|
305
|
+
self._hi_var.trace_add("write", self._on_hi_write)
|
|
306
|
+
# The unified theme walk re-resolves track/handle colors via
|
|
307
|
+
# `_bs_apply_theme` on a theme change. The <Map> flush also covers a
|
|
308
|
+
# redraw deferred while the slider was off-screen (value/size changes).
|
|
309
|
+
self.bind("<Map>", lambda e: self._on_map(), add="+")
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
# Layout helpers
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
def _calc_tick_zone(self) -> int:
|
|
316
|
+
if self._tick_interval is None:
|
|
317
|
+
return 0
|
|
318
|
+
base = TICK_GAP + MAJOR_TICK_H
|
|
319
|
+
if self._tick_labels:
|
|
320
|
+
if self._orient == "vertical":
|
|
321
|
+
base += LABEL_GAP + self._measure_max_label_width()
|
|
322
|
+
else:
|
|
323
|
+
base += LABEL_GAP + LABEL_FONT_SIZE + 4
|
|
324
|
+
return base
|
|
325
|
+
|
|
326
|
+
def _measure_max_label_width(self) -> int:
|
|
327
|
+
font = tkfont.Font(family="TkDefaultFont", size=LABEL_FONT_SIZE)
|
|
328
|
+
candidates = [self._tick_format.format(self._minvalue),
|
|
329
|
+
self._tick_format.format(self._maxvalue)]
|
|
330
|
+
return max(font.measure(t) for t in candidates) + 4
|
|
331
|
+
|
|
332
|
+
def _track_range(self) -> tuple[int, int]:
|
|
333
|
+
# Inset by the handle half-width only — the handle edge sits flush with the
|
|
334
|
+
# canvas edge at the extremes, and the in-handle focus ring needs no overhang.
|
|
335
|
+
edge = self._handle_size // 2
|
|
336
|
+
if self._orient == "horizontal":
|
|
337
|
+
w = self._cw if self._cw > 0 else max(1, self._canvas.winfo_width())
|
|
338
|
+
return edge, max(edge + 1, w - edge - 1)
|
|
339
|
+
else:
|
|
340
|
+
h = self.winfo_height() - 2 * HL
|
|
341
|
+
return edge, max(edge + 1, h - edge - 1)
|
|
342
|
+
|
|
343
|
+
def _value_to_pos(self, val: float) -> int:
|
|
344
|
+
start, end = self._track_range()
|
|
345
|
+
span = self._maxvalue - self._minvalue
|
|
346
|
+
if span == 0:
|
|
347
|
+
return start
|
|
348
|
+
ratio = max(0.0, min(1.0, (val - self._minvalue) / span))
|
|
349
|
+
if self._orient == "vertical":
|
|
350
|
+
ratio = 1.0 - ratio
|
|
351
|
+
return start + int(ratio * (end - start))
|
|
352
|
+
|
|
353
|
+
def _track_extent(self) -> int:
|
|
354
|
+
"""Length of the full-width track (handle's min edge to max edge).
|
|
355
|
+
|
|
356
|
+
The track spans `[0, extent]` so it meets the container edges; each handle
|
|
357
|
+
still travels `[r, extent - r]` (its center), so ticks placed at the handle
|
|
358
|
+
centers sit `r` inside each end and the handle covers that lead-in/out.
|
|
359
|
+
"""
|
|
360
|
+
_, end = self._track_range()
|
|
361
|
+
return end + self._handle_size // 2
|
|
362
|
+
|
|
363
|
+
def _pos_to_value(self, pos: int) -> float:
|
|
364
|
+
start, end = self._track_range()
|
|
365
|
+
if end == start:
|
|
366
|
+
return self._minvalue
|
|
367
|
+
ratio = max(0.0, min(1.0, (pos - start) / (end - start)))
|
|
368
|
+
if self._orient == "vertical":
|
|
369
|
+
ratio = 1.0 - ratio
|
|
370
|
+
return self._minvalue + ratio * (self._maxvalue - self._minvalue)
|
|
371
|
+
|
|
372
|
+
def _event_pos(self, event: tk.Event) -> int:
|
|
373
|
+
# event.x/y are already in canvas coordinates.
|
|
374
|
+
if self._orient == "horizontal":
|
|
375
|
+
return event.x
|
|
376
|
+
return event.y
|
|
377
|
+
|
|
378
|
+
def _snap(self, v: float) -> float:
|
|
379
|
+
"""Clamp `v` to [min, max] and, when `step` is set, snap it to the
|
|
380
|
+
nearest grid point (measured from `minvalue`). `maxvalue` stays reachable
|
|
381
|
+
even when the range is not an exact multiple of `step`."""
|
|
382
|
+
v = max(self._minvalue, min(self._maxvalue, float(v)))
|
|
383
|
+
if not self._step_size:
|
|
384
|
+
return v
|
|
385
|
+
grid = self._minvalue + round((v - self._minvalue) / self._step_size) * self._step_size
|
|
386
|
+
grid = max(self._minvalue, min(self._maxvalue, grid))
|
|
387
|
+
if abs(v - self._maxvalue) < abs(v - grid):
|
|
388
|
+
return self._maxvalue
|
|
389
|
+
return grid
|
|
390
|
+
|
|
391
|
+
# ------------------------------------------------------------------
|
|
392
|
+
# Rendering helpers
|
|
393
|
+
# ------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
def _render_handles(self, colors: dict[str, str]) -> None:
|
|
396
|
+
fill = colors['handle_disabled'] if self._state == "disabled" else colors['handle']
|
|
397
|
+
self._handle_lo_photo = _make_handle(fill, colors['ring'], colors['handle_border'])
|
|
398
|
+
self._handle_hi_photo = _make_handle(fill, colors['ring'], colors['handle_border'])
|
|
399
|
+
self._canvas.itemconfig(self._handle_lo_item, image=self._handle_lo_photo)
|
|
400
|
+
self._canvas.itemconfig(self._handle_hi_item, image=self._handle_hi_photo)
|
|
401
|
+
|
|
402
|
+
def _update_focus_ring(self) -> None:
|
|
403
|
+
"""Position the focus halo over the active handle when the widget holds
|
|
404
|
+
keyboard focus; hide it otherwise."""
|
|
405
|
+
item = getattr(self, "_focus_ring_item", None)
|
|
406
|
+
if item is None:
|
|
407
|
+
return
|
|
408
|
+
if not self._kbd_focused or self._state == "disabled":
|
|
409
|
+
self._canvas.itemconfigure(item, state="hidden")
|
|
410
|
+
return
|
|
411
|
+
val = self._lo_var.get() if self._focus_handle == "lo" else self._hi_var.get()
|
|
412
|
+
pos = self._value_to_pos(val)
|
|
413
|
+
if self._orient == "horizontal":
|
|
414
|
+
self._canvas.coords(item, pos, self._track_center)
|
|
415
|
+
else:
|
|
416
|
+
self._canvas.coords(item, self._track_center, pos)
|
|
417
|
+
self._canvas.itemconfigure(item, state="normal")
|
|
418
|
+
self._canvas.tag_raise(item) # keep the halo above the handles
|
|
419
|
+
|
|
420
|
+
def _update_badge_colors(self, colors: dict[str, str]) -> None:
|
|
421
|
+
bg = colors['badge_bg']
|
|
422
|
+
if self._badge_lo_item is not None:
|
|
423
|
+
self._badge_lo_photo = _make_badge(self._badge_w, BADGE_H, bg)
|
|
424
|
+
self._canvas.itemconfig(self._badge_lo_item, image=self._badge_lo_photo)
|
|
425
|
+
if self._badge_hi_item is not None:
|
|
426
|
+
self._badge_hi_photo = _make_badge(self._badge_w, BADGE_H, bg)
|
|
427
|
+
self._canvas.itemconfig(self._badge_hi_item, image=self._badge_hi_photo)
|
|
428
|
+
for item in (self._badge_lo_text, self._badge_hi_text):
|
|
429
|
+
if item is not None:
|
|
430
|
+
self._canvas.itemconfig(item, fill=colors['badge_text'])
|
|
431
|
+
|
|
432
|
+
# ------------------------------------------------------------------
|
|
433
|
+
# Sync: values → visual
|
|
434
|
+
# ------------------------------------------------------------------
|
|
435
|
+
|
|
436
|
+
def _sync(self) -> None:
|
|
437
|
+
lo_val = self._lo_var.get()
|
|
438
|
+
hi_val = self._hi_var.get()
|
|
439
|
+
lo_pos = self._value_to_pos(lo_val)
|
|
440
|
+
hi_pos = self._value_to_pos(hi_val)
|
|
441
|
+
# Full-width track (0 .. extent); the active fill spans between the handles.
|
|
442
|
+
extent = self._track_extent()
|
|
443
|
+
|
|
444
|
+
fill_c = self._colors['fill_disabled'] if self._state == "disabled" else self._colors['fill']
|
|
445
|
+
|
|
446
|
+
if self._orient == "horizontal":
|
|
447
|
+
photo = _make_track(extent, self._track_h, lo_pos, hi_pos,
|
|
448
|
+
self._colors['trough'], fill_c)
|
|
449
|
+
if photo:
|
|
450
|
+
self._track_photo = photo
|
|
451
|
+
self._canvas.coords(self._track_item, 0, self._track_center)
|
|
452
|
+
self._canvas.itemconfig(self._track_item, image=self._track_photo)
|
|
453
|
+
self._canvas.coords(self._handle_lo_item, lo_pos, self._track_center)
|
|
454
|
+
self._canvas.coords(self._handle_hi_item, hi_pos, self._track_center)
|
|
455
|
+
if self._show_value:
|
|
456
|
+
self._update_badges_h(lo_pos, hi_pos, lo_val, hi_val)
|
|
457
|
+
else:
|
|
458
|
+
# Vertical: top = max (small canvas y), bottom = min (large canvas y).
|
|
459
|
+
# lo_pos >= hi_pos. PIL rotate(90) is CCW (LEFT → BOTTOM), so to fill
|
|
460
|
+
# from canvas y=hi_pos (top) to y=lo_pos (bottom) on the full-height
|
|
461
|
+
# track: fill_start = extent - lo_pos, fill_end = extent - hi_pos.
|
|
462
|
+
fill_start = max(0, extent - lo_pos)
|
|
463
|
+
fill_end = min(extent, extent - hi_pos)
|
|
464
|
+
photo = _make_track(extent, self._track_h, fill_start, fill_end,
|
|
465
|
+
self._colors['trough'], fill_c,
|
|
466
|
+
vertical=True)
|
|
467
|
+
if photo:
|
|
468
|
+
self._track_photo = photo
|
|
469
|
+
self._canvas.coords(self._track_item, self._track_center, 0)
|
|
470
|
+
self._canvas.itemconfig(self._track_item, image=self._track_photo)
|
|
471
|
+
self._canvas.coords(self._handle_lo_item, self._track_center, lo_pos)
|
|
472
|
+
self._canvas.coords(self._handle_hi_item, self._track_center, hi_pos)
|
|
473
|
+
if self._show_value:
|
|
474
|
+
self._update_badges_v(lo_pos, hi_pos, lo_val, hi_val)
|
|
475
|
+
|
|
476
|
+
self._update_focus_ring() # keep the halo on the active handle
|
|
477
|
+
|
|
478
|
+
def _update_badges_h(self, lo_x: int, hi_x: int, lo_val: float, hi_val: float) -> None:
|
|
479
|
+
if self._badge_lo_item is None:
|
|
480
|
+
return
|
|
481
|
+
bw = self._badge_w
|
|
482
|
+
cw = self._cw if self._cw > 0 else max(1, self.winfo_width() - 2 * HL)
|
|
483
|
+
badge_y = self._badge_zone - BADGE_H - 2
|
|
484
|
+
# Clamp so image occupies [bx, bx+bw-1] ⊆ [0, cw-1].
|
|
485
|
+
lo_bx = max(0, min(lo_x - bw // 2, cw - bw))
|
|
486
|
+
hi_bx = max(0, min(hi_x - bw // 2, cw - bw))
|
|
487
|
+
for img_item, text_item, bx, val in (
|
|
488
|
+
(self._badge_lo_item, self._badge_lo_text, lo_bx, lo_val),
|
|
489
|
+
(self._badge_hi_item, self._badge_hi_text, hi_bx, hi_val),
|
|
490
|
+
):
|
|
491
|
+
self._canvas.coords(img_item, bx, badge_y)
|
|
492
|
+
if text_item is not None:
|
|
493
|
+
self._canvas.coords(text_item, bx + bw // 2, badge_y + BADGE_H // 2)
|
|
494
|
+
self._canvas.itemconfig(text_item, text=self._tick_format.format(val))
|
|
495
|
+
|
|
496
|
+
def _update_badges_v(self, lo_y: int, hi_y: int, lo_val: float, hi_val: float) -> None:
|
|
497
|
+
if self._badge_lo_item is None:
|
|
498
|
+
return
|
|
499
|
+
bw = self._badge_w
|
|
500
|
+
ch = self.winfo_height() - 2 * HL
|
|
501
|
+
badge_x = self._badge_zone - bw - 2
|
|
502
|
+
for img_item, text_item, by_center, val in (
|
|
503
|
+
(self._badge_lo_item, self._badge_lo_text, lo_y, lo_val),
|
|
504
|
+
(self._badge_hi_item, self._badge_hi_text, hi_y, hi_val),
|
|
505
|
+
):
|
|
506
|
+
badge_y = max(0, min(by_center - BADGE_H // 2, ch - BADGE_H))
|
|
507
|
+
self._canvas.coords(img_item, badge_x, badge_y)
|
|
508
|
+
if text_item is not None:
|
|
509
|
+
self._canvas.coords(text_item, badge_x + bw // 2, badge_y + BADGE_H // 2)
|
|
510
|
+
self._canvas.itemconfig(text_item, text=self._tick_format.format(val))
|
|
511
|
+
|
|
512
|
+
# ------------------------------------------------------------------
|
|
513
|
+
# Tick marks
|
|
514
|
+
# ------------------------------------------------------------------
|
|
515
|
+
|
|
516
|
+
def _setup_ticks(self) -> None:
|
|
517
|
+
if self._tick_interval is None:
|
|
518
|
+
return
|
|
519
|
+
for item in self._tick_items:
|
|
520
|
+
self._canvas.delete(item)
|
|
521
|
+
self._tick_items.clear()
|
|
522
|
+
|
|
523
|
+
tick_color = self._colors['tick']
|
|
524
|
+
font = tkfont.Font(family="TkDefaultFont", size=LABEL_FONT_SIZE)
|
|
525
|
+
start, end = self._track_range()
|
|
526
|
+
span = self._maxvalue - self._minvalue
|
|
527
|
+
if span <= 0:
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
if self._orient == "horizontal":
|
|
531
|
+
major_y0 = self._track_center + self._handle_size // 2 + TICK_GAP
|
|
532
|
+
major_y1 = major_y0 + MAJOR_TICK_H
|
|
533
|
+
minor_y1 = major_y0 + MINOR_TICK_H
|
|
534
|
+
label_y = major_y1 + LABEL_GAP
|
|
535
|
+
|
|
536
|
+
val = self._minvalue
|
|
537
|
+
while val <= self._maxvalue + 1e-9:
|
|
538
|
+
x = self._value_to_pos(val)
|
|
539
|
+
self._tick_items.append(
|
|
540
|
+
self._canvas.create_line(x, major_y0, x, major_y1, fill=tick_color, width=1))
|
|
541
|
+
if self._tick_labels:
|
|
542
|
+
self._tick_items.append(
|
|
543
|
+
self._canvas.create_text(x, label_y, text=self._tick_format.format(val),
|
|
544
|
+
fill=tick_color, font=font, anchor="n"))
|
|
545
|
+
if self._minor_ticks > 0:
|
|
546
|
+
minor_step = self._tick_interval / (self._minor_ticks + 1)
|
|
547
|
+
for i in range(1, self._minor_ticks + 1):
|
|
548
|
+
mv = val + i * minor_step
|
|
549
|
+
if mv >= self._maxvalue - 1e-9:
|
|
550
|
+
break
|
|
551
|
+
mx = self._value_to_pos(mv)
|
|
552
|
+
self._tick_items.append(
|
|
553
|
+
self._canvas.create_line(mx, major_y0, mx, minor_y1,
|
|
554
|
+
fill=tick_color, width=1))
|
|
555
|
+
val += self._tick_interval
|
|
556
|
+
else:
|
|
557
|
+
major_x0 = self._track_center + self._handle_size // 2 + TICK_GAP
|
|
558
|
+
major_x1 = major_x0 + MAJOR_TICK_H
|
|
559
|
+
minor_x1 = major_x0 + MINOR_TICK_H
|
|
560
|
+
label_x = major_x1 + LABEL_GAP
|
|
561
|
+
|
|
562
|
+
val = self._minvalue
|
|
563
|
+
while val <= self._maxvalue + 1e-9:
|
|
564
|
+
y = self._value_to_pos(val)
|
|
565
|
+
self._tick_items.append(
|
|
566
|
+
self._canvas.create_line(major_x0, y, major_x1, y, fill=tick_color, width=1))
|
|
567
|
+
if self._tick_labels:
|
|
568
|
+
self._tick_items.append(
|
|
569
|
+
self._canvas.create_text(label_x, y, text=self._tick_format.format(val),
|
|
570
|
+
fill=tick_color, font=font, anchor="w"))
|
|
571
|
+
if self._minor_ticks > 0:
|
|
572
|
+
minor_step = self._tick_interval / (self._minor_ticks + 1)
|
|
573
|
+
for i in range(1, self._minor_ticks + 1):
|
|
574
|
+
mv = val + i * minor_step
|
|
575
|
+
if mv >= self._maxvalue - 1e-9:
|
|
576
|
+
break
|
|
577
|
+
my = self._value_to_pos(mv)
|
|
578
|
+
self._tick_items.append(
|
|
579
|
+
self._canvas.create_line(major_x0, my, minor_x1, my,
|
|
580
|
+
fill=tick_color, width=1))
|
|
581
|
+
val += self._tick_interval
|
|
582
|
+
|
|
583
|
+
# ------------------------------------------------------------------
|
|
584
|
+
# Event handlers
|
|
585
|
+
# ------------------------------------------------------------------
|
|
586
|
+
|
|
587
|
+
def _on_configure(self, event: tk.Event) -> None:
|
|
588
|
+
if self._orient == "horizontal" and event.width > self._handle_size:
|
|
589
|
+
self._cw = event.width
|
|
590
|
+
self._canvas.configure(scrollregion=(0, 0, event.width, event.height))
|
|
591
|
+
self._setup_ticks()
|
|
592
|
+
self._sync()
|
|
593
|
+
elif self._orient == "vertical" and event.height > self._handle_size:
|
|
594
|
+
self._canvas.configure(scrollregion=(0, 0, event.width, event.height))
|
|
595
|
+
self._setup_ticks()
|
|
596
|
+
self._sync()
|
|
597
|
+
|
|
598
|
+
def _mouse_focus_set(self, _event: tk.Event) -> None:
|
|
599
|
+
self._mouse_pressed = True
|
|
600
|
+
self.focus_set()
|
|
601
|
+
|
|
602
|
+
def _on_focus_in(self, _event: tk.Event) -> None:
|
|
603
|
+
if self._mouse_pressed:
|
|
604
|
+
self._mouse_pressed = False
|
|
605
|
+
return
|
|
606
|
+
self._kbd_focused = True
|
|
607
|
+
# The per-handle halo is the focus indicator — no whole-widget frame ring
|
|
608
|
+
# (that would be redundant with the halo).
|
|
609
|
+
self._update_focus_ring()
|
|
610
|
+
|
|
611
|
+
def _on_focus_out(self, _event: tk.Event) -> None:
|
|
612
|
+
self._kbd_focused = False
|
|
613
|
+
self._update_focus_ring()
|
|
614
|
+
|
|
615
|
+
def _on_canvas_click(self, event: tk.Event) -> None:
|
|
616
|
+
if self._state == "disabled":
|
|
617
|
+
return
|
|
618
|
+
pos = self._event_pos(event)
|
|
619
|
+
lo_pos = self._value_to_pos(self._lo_var.get())
|
|
620
|
+
hi_pos = self._value_to_pos(self._hi_var.get())
|
|
621
|
+
self._dragging = "lo" if abs(pos - lo_pos) <= abs(pos - hi_pos) else "hi"
|
|
622
|
+
self._focus_handle = self._dragging
|
|
623
|
+
self._move(pos)
|
|
624
|
+
|
|
625
|
+
def _on_canvas_drag(self, event: tk.Event) -> None:
|
|
626
|
+
if self._state == "disabled":
|
|
627
|
+
return
|
|
628
|
+
self._move(self._event_pos(event))
|
|
629
|
+
|
|
630
|
+
def _handle_press(self, event: tk.Event, which: str) -> None:
|
|
631
|
+
if self._state == "disabled":
|
|
632
|
+
return
|
|
633
|
+
self._dragging = which
|
|
634
|
+
self._focus_handle = which
|
|
635
|
+
self.focus_set()
|
|
636
|
+
|
|
637
|
+
def _on_handle_drag(self, event: tk.Event) -> None:
|
|
638
|
+
if self._state == "disabled" or self._dragging is None:
|
|
639
|
+
return
|
|
640
|
+
self._move(self._event_pos(event))
|
|
641
|
+
|
|
642
|
+
def _on_release(self, event: tk.Event) -> None:
|
|
643
|
+
if self._state == "disabled":
|
|
644
|
+
return
|
|
645
|
+
self._dragging = None
|
|
646
|
+
self.event_generate("<<Commit>>", data=RangeSliderCommitEvent(
|
|
647
|
+
low_value=self._lo_var.get(),
|
|
648
|
+
high_value=self._hi_var.get(),
|
|
649
|
+
))
|
|
650
|
+
|
|
651
|
+
def _move(self, pos: int) -> None:
|
|
652
|
+
value = self._snap(self._pos_to_value(pos))
|
|
653
|
+
if self._dragging == "lo":
|
|
654
|
+
self._lo_var.set(max(self._minvalue, min(value, self._hi_var.get())))
|
|
655
|
+
else:
|
|
656
|
+
self._hi_var.set(min(self._maxvalue, max(value, self._lo_var.get())))
|
|
657
|
+
|
|
658
|
+
def _step(self, amount: float) -> None:
|
|
659
|
+
if self._state == "disabled":
|
|
660
|
+
return
|
|
661
|
+
self._set_focused(
|
|
662
|
+
(self._lo_var.get() if self._focus_handle == "lo" else self._hi_var.get()) + amount
|
|
663
|
+
)
|
|
664
|
+
self.event_generate("<<Commit>>", data=RangeSliderCommitEvent(
|
|
665
|
+
low_value=self._lo_var.get(),
|
|
666
|
+
high_value=self._hi_var.get(),
|
|
667
|
+
))
|
|
668
|
+
|
|
669
|
+
def _set_focused(self, value: float) -> None:
|
|
670
|
+
value = self._snap(value)
|
|
671
|
+
if self._focus_handle == "lo":
|
|
672
|
+
self._lo_var.set(min(value, self._hi_var.get()))
|
|
673
|
+
else:
|
|
674
|
+
self._hi_var.set(max(value, self._lo_var.get()))
|
|
675
|
+
|
|
676
|
+
def _jump(self, value: float) -> None:
|
|
677
|
+
"""Keyboard jump (Home/End) — move the focused handle then commit."""
|
|
678
|
+
if self._state == "disabled":
|
|
679
|
+
return
|
|
680
|
+
self._set_focused(value)
|
|
681
|
+
self.event_generate("<<Commit>>", data=RangeSliderCommitEvent(
|
|
682
|
+
low_value=self._lo_var.get(),
|
|
683
|
+
high_value=self._hi_var.get(),
|
|
684
|
+
))
|
|
685
|
+
|
|
686
|
+
def _reclamp(self) -> None:
|
|
687
|
+
"""Pull both handles back into [min, max] (and lo <= hi) after a range
|
|
688
|
+
change, emitting <<Change>> only for handles that actually move."""
|
|
689
|
+
lo, hi = self._lo_var.get(), self._hi_var.get()
|
|
690
|
+
nlo = self._snap(lo)
|
|
691
|
+
nhi = self._snap(hi)
|
|
692
|
+
nlo = min(nlo, nhi)
|
|
693
|
+
changed = False
|
|
694
|
+
if nlo != lo:
|
|
695
|
+
self._lo_var.set(nlo) # trace fires _sync + <<Change>>
|
|
696
|
+
changed = True
|
|
697
|
+
if nhi != hi:
|
|
698
|
+
self._hi_var.set(nhi)
|
|
699
|
+
changed = True
|
|
700
|
+
if not changed:
|
|
701
|
+
self._sync()
|
|
702
|
+
|
|
703
|
+
def _select_handle(self, which: str) -> str:
|
|
704
|
+
"""Make `which` ('lo'|'hi') the active handle (cross-axis arrow keys).
|
|
705
|
+
|
|
706
|
+
Re-renders so the focus ring moves to the newly active handle. Returns
|
|
707
|
+
`'break'` so the arrow press isn't reinterpreted by any default binding.
|
|
708
|
+
"""
|
|
709
|
+
if self._state == "disabled":
|
|
710
|
+
return "break"
|
|
711
|
+
self._focus_handle = which
|
|
712
|
+
self._update_focus_ring()
|
|
713
|
+
return "break"
|
|
714
|
+
|
|
715
|
+
def _on_lo_write(self, *_: Any) -> None:
|
|
716
|
+
self._sync()
|
|
717
|
+
lo = self._lo_var.get()
|
|
718
|
+
hi = self._hi_var.get()
|
|
719
|
+
self.event_generate("<<Change>>", data=RangeSliderEvent(
|
|
720
|
+
low_value=lo,
|
|
721
|
+
high_value=hi,
|
|
722
|
+
prev_low_value=self._prev_lovalue,
|
|
723
|
+
prev_high_value=self._prev_hivalue,
|
|
724
|
+
))
|
|
725
|
+
self._prev_lovalue = lo
|
|
726
|
+
self._prev_hivalue = hi
|
|
727
|
+
|
|
728
|
+
def _on_hi_write(self, *_: Any) -> None:
|
|
729
|
+
self._sync()
|
|
730
|
+
lo = self._lo_var.get()
|
|
731
|
+
hi = self._hi_var.get()
|
|
732
|
+
self.event_generate("<<Change>>", data=RangeSliderEvent(
|
|
733
|
+
low_value=lo,
|
|
734
|
+
high_value=hi,
|
|
735
|
+
prev_low_value=self._prev_lovalue,
|
|
736
|
+
prev_high_value=self._prev_hivalue,
|
|
737
|
+
))
|
|
738
|
+
self._prev_lovalue = lo
|
|
739
|
+
self._prev_hivalue = hi
|
|
740
|
+
|
|
741
|
+
def _on_map(self) -> None:
|
|
742
|
+
if getattr(self, '_theme_update_pending', False):
|
|
743
|
+
self._on_theme_changed()
|
|
744
|
+
|
|
745
|
+
def _bs_apply_theme(self) -> None:
|
|
746
|
+
"""Theme walk hook — re-resolve colors and redraw track, handles, badge,
|
|
747
|
+
focus halo, and tick marks + tick labels. The walk owns visibility
|
|
748
|
+
gating, so this always repaints (no early-out).
|
|
749
|
+
"""
|
|
750
|
+
self._theme_update_pending = False
|
|
751
|
+
self._colors = resolve_colors(self._accent, self._surface, get_widget_bg(self.master))
|
|
752
|
+
colors = self._colors
|
|
753
|
+
self.configure(
|
|
754
|
+
highlightbackground=colors['bg'],
|
|
755
|
+
highlightcolor=colors['bg'], # reset to hidden; re-shown on next keyboard focus
|
|
756
|
+
)
|
|
757
|
+
self._canvas.configure(bg=colors['bg'])
|
|
758
|
+
self._render_handles(colors)
|
|
759
|
+
self._update_badge_colors(colors)
|
|
760
|
+
# Recolor the focus halo to the new theme's focus color.
|
|
761
|
+
self._focus_ring_photo = _make_focus_ring(self._focus_ring_diam, colors['focus'])
|
|
762
|
+
self._canvas.itemconfig(self._focus_ring_item, image=self._focus_ring_photo)
|
|
763
|
+
self._setup_ticks() # redraws ticks + tick labels with colors['tick']
|
|
764
|
+
self._sync() # _sync repositions/shows the halo as needed
|
|
765
|
+
|
|
766
|
+
def _on_theme_changed(self) -> None:
|
|
767
|
+
# Value/size redraws keep their own off-screen deferral (flushed on
|
|
768
|
+
# <Map>); a genuine theme change is driven through _bs_apply_theme by the
|
|
769
|
+
# unified theme walk.
|
|
770
|
+
if not self.winfo_viewable():
|
|
771
|
+
self._theme_update_pending = True
|
|
772
|
+
return
|
|
773
|
+
self._bs_apply_theme()
|
|
774
|
+
|
|
775
|
+
# ------------------------------------------------------------------
|
|
776
|
+
# Public API
|
|
777
|
+
# ------------------------------------------------------------------
|
|
778
|
+
|
|
779
|
+
@property
|
|
780
|
+
def lo_signal(self) -> Signal[float]:
|
|
781
|
+
"""Reactive Signal linked to the low-handle value."""
|
|
782
|
+
return self._lo_signal
|
|
783
|
+
|
|
784
|
+
@property
|
|
785
|
+
def hi_signal(self) -> Signal[float]:
|
|
786
|
+
"""Reactive Signal linked to the high-handle value."""
|
|
787
|
+
return self._hi_signal
|
|
788
|
+
|
|
789
|
+
@property
|
|
790
|
+
def lo_variable(self) -> tk.DoubleVar:
|
|
791
|
+
"""Underlying Tkinter DoubleVar for the low handle.
|
|
792
|
+
|
|
793
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
794
|
+
"""
|
|
795
|
+
return self._lo_var
|
|
796
|
+
|
|
797
|
+
@property
|
|
798
|
+
def hi_variable(self) -> tk.DoubleVar:
|
|
799
|
+
"""Underlying Tkinter DoubleVar for the high handle.
|
|
800
|
+
|
|
801
|
+
See [tkinter Variables](https://docs.python.org/3/library/tkinter.html#tkinter-variables).
|
|
802
|
+
"""
|
|
803
|
+
return self._hi_var
|
|
804
|
+
|
|
805
|
+
@property
|
|
806
|
+
def lovalue(self) -> float:
|
|
807
|
+
"""Current low-handle value."""
|
|
808
|
+
return self._lo_var.get()
|
|
809
|
+
|
|
810
|
+
@property
|
|
811
|
+
def hivalue(self) -> float:
|
|
812
|
+
"""Current high-handle value."""
|
|
813
|
+
return self._hi_var.get()
|
|
814
|
+
|
|
815
|
+
def get_lo(self) -> float:
|
|
816
|
+
"""Return the current low-handle value."""
|
|
817
|
+
return self._lo_var.get()
|
|
818
|
+
|
|
819
|
+
def get_hi(self) -> float:
|
|
820
|
+
"""Return the current high-handle value."""
|
|
821
|
+
return self._hi_var.get()
|
|
822
|
+
|
|
823
|
+
def set_lo(self, value: float) -> None:
|
|
824
|
+
"""Set the low handle to a new value, clamped to [minvalue, hivalue] and
|
|
825
|
+
snapped to the step grid when `step` is set.
|
|
826
|
+
|
|
827
|
+
Args:
|
|
828
|
+
value: New low-handle value.
|
|
829
|
+
"""
|
|
830
|
+
self._lo_var.set(min(self._snap(value), self._hi_var.get()))
|
|
831
|
+
|
|
832
|
+
def set_hi(self, value: float) -> None:
|
|
833
|
+
"""Set the high handle to a new value, clamped to [lovalue, maxvalue] and
|
|
834
|
+
snapped to the step grid when `step` is set.
|
|
835
|
+
|
|
836
|
+
Args:
|
|
837
|
+
value: New high-handle value.
|
|
838
|
+
"""
|
|
839
|
+
self._hi_var.set(max(self._snap(value), self._lo_var.get()))
|
|
840
|
+
|
|
841
|
+
def on_changed(self, callback: Callable[[RangeSliderEvent], None]) -> str:
|
|
842
|
+
"""Register a callback for `<<Change>>` events.
|
|
843
|
+
|
|
844
|
+
Args:
|
|
845
|
+
callback: Called with a Tkinter event; `event.low_value`
|
|
846
|
+
and `event.high_value` are the current handle values;
|
|
847
|
+
`event.prev_low_value` and `event.prev_high_value`
|
|
848
|
+
are the values before the change.
|
|
849
|
+
|
|
850
|
+
Returns:
|
|
851
|
+
Bind ID — pass to `off_changed()` to unsubscribe.
|
|
852
|
+
"""
|
|
853
|
+
return self.bind("<<Change>>", callback, add="+")
|
|
854
|
+
|
|
855
|
+
def off_changed(self, bind_id: str | None = None) -> None:
|
|
856
|
+
"""Unsubscribe from `<<Change>>`.
|
|
857
|
+
|
|
858
|
+
Args:
|
|
859
|
+
bind_id: ID returned by `on_changed()`. If None, removes all.
|
|
860
|
+
"""
|
|
861
|
+
self.unbind("<<Change>>", bind_id)
|
|
862
|
+
|
|
863
|
+
def on_commit(self, callback: Callable[[RangeSliderCommitEvent], None]) -> str:
|
|
864
|
+
"""Register a callback for `<<Commit>>` events.
|
|
865
|
+
|
|
866
|
+
Fires on mouse release and keyboard commit. Use for expensive
|
|
867
|
+
downstream operations.
|
|
868
|
+
|
|
869
|
+
Args:
|
|
870
|
+
callback: Called with a Tkinter event; `event.low_value`
|
|
871
|
+
and `event.high_value` are the committed values.
|
|
872
|
+
|
|
873
|
+
Returns:
|
|
874
|
+
Bind ID — pass to `off_commit()` to unsubscribe.
|
|
875
|
+
"""
|
|
876
|
+
return self.bind("<<Commit>>", callback, add="+")
|
|
877
|
+
|
|
878
|
+
def off_commit(self, bind_id: str | None = None) -> None:
|
|
879
|
+
"""Unsubscribe from `<<Commit>>`.
|
|
880
|
+
|
|
881
|
+
Args:
|
|
882
|
+
bind_id: ID returned by `on_commit()`. If None, removes all.
|
|
883
|
+
"""
|
|
884
|
+
self.unbind("<<Commit>>", bind_id)
|
|
885
|
+
|
|
886
|
+
# ------------------------------------------------------------------
|
|
887
|
+
# Configure delegates
|
|
888
|
+
# ------------------------------------------------------------------
|
|
889
|
+
|
|
890
|
+
@configure_delegate('lovalue')
|
|
891
|
+
def _delegate_lovalue(self, value=None):
|
|
892
|
+
if value is None:
|
|
893
|
+
return self._lo_var.get()
|
|
894
|
+
self.set_lo(value)
|
|
895
|
+
|
|
896
|
+
@configure_delegate('hivalue')
|
|
897
|
+
def _delegate_hivalue(self, value=None):
|
|
898
|
+
if value is None:
|
|
899
|
+
return self._hi_var.get()
|
|
900
|
+
self.set_hi(value)
|
|
901
|
+
|
|
902
|
+
@configure_delegate('minvalue')
|
|
903
|
+
def _delegate_minvalue(self, value=None):
|
|
904
|
+
if value is None:
|
|
905
|
+
return self._minvalue
|
|
906
|
+
self._minvalue = float(value)
|
|
907
|
+
self._setup_ticks()
|
|
908
|
+
self._reclamp()
|
|
909
|
+
|
|
910
|
+
@configure_delegate('maxvalue')
|
|
911
|
+
def _delegate_maxvalue(self, value=None):
|
|
912
|
+
if value is None:
|
|
913
|
+
return self._maxvalue
|
|
914
|
+
self._maxvalue = float(value)
|
|
915
|
+
self._setup_ticks()
|
|
916
|
+
self._reclamp()
|
|
917
|
+
|
|
918
|
+
@configure_delegate('step')
|
|
919
|
+
def _delegate_step(self, value=None):
|
|
920
|
+
if value is None:
|
|
921
|
+
return self._step_size
|
|
922
|
+
self._step_size = float(value) if value else None
|
|
923
|
+
self._kbd_step = self._step_size if self._step_size else float(STEP)
|
|
924
|
+
self._kbd_step_large = self._step_size * 10 if self._step_size else float(STEP_LARGE)
|
|
925
|
+
self._reclamp() # re-snap both handles onto the new grid
|
|
926
|
+
|
|
927
|
+
@configure_delegate('accent')
|
|
928
|
+
def _delegate_accent(self, value=None):
|
|
929
|
+
if value is None:
|
|
930
|
+
return self._accent
|
|
931
|
+
self._accent = value
|
|
932
|
+
self._on_theme_changed()
|
|
933
|
+
|
|
934
|
+
@configure_delegate('state')
|
|
935
|
+
def _delegate_state(self, value=None):
|
|
936
|
+
if value is None:
|
|
937
|
+
return self._state
|
|
938
|
+
self._state = value
|
|
939
|
+
self._render_handles(self._colors)
|
|
940
|
+
self._sync()
|
|
941
|
+
|
|
942
|
+
@configure_delegate('tick_format')
|
|
943
|
+
def _delegate_tick_format(self, value=None):
|
|
944
|
+
if value is None:
|
|
945
|
+
return self._tick_format
|
|
946
|
+
self._tick_format = value
|
|
947
|
+
self._setup_ticks()
|
|
948
|
+
self._sync()
|
|
949
|
+
|
|
950
|
+
@configure_delegate('tick_interval')
|
|
951
|
+
def _delegate_tick_interval(self, value=None):
|
|
952
|
+
if value is None:
|
|
953
|
+
return self._tick_interval
|
|
954
|
+
self._tick_interval = value
|
|
955
|
+
self._setup_ticks()
|
|
956
|
+
|
|
957
|
+
@configure_delegate('show_value')
|
|
958
|
+
def _delegate_show_value(self, value=None):
|
|
959
|
+
if value is None:
|
|
960
|
+
return self._show_value
|
|
961
|
+
# show_value creates/destroys canvas items; must be set at construction time.
|
|
962
|
+
|
|
963
|
+
@configure_delegate('minor_ticks')
|
|
964
|
+
def _delegate_minor_ticks(self, value=None):
|
|
965
|
+
if value is None:
|
|
966
|
+
return self._minor_ticks
|
|
967
|
+
self._minor_ticks = int(value)
|
|
968
|
+
self._setup_ticks()
|
|
969
|
+
|
|
970
|
+
@configure_delegate('tick_labels')
|
|
971
|
+
def _delegate_tick_labels(self, value=None):
|
|
972
|
+
if value is None:
|
|
973
|
+
return self._tick_labels
|
|
974
|
+
self._tick_labels = bool(value)
|
|
975
|
+
self._setup_ticks()
|
|
976
|
+
|
|
977
|
+
@configure_delegate('surface')
|
|
978
|
+
def _delegate_surface(self, value=None):
|
|
979
|
+
if value is None:
|
|
980
|
+
return self._surface
|
|
981
|
+
self._surface = value
|
|
982
|
+
self._on_theme_changed()
|