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,802 @@
|
|
|
1
|
+
from tkinter import TclError
|
|
2
|
+
from typing import Literal
|
|
3
|
+
|
|
4
|
+
from typing_extensions import Unpack
|
|
5
|
+
|
|
6
|
+
from bootstack.widgets._impl.composites.compositeframe import CompositeFrame
|
|
7
|
+
from bootstack.widgets._impl.mixins import configure_delegate
|
|
8
|
+
from bootstack.widgets._impl.primitives.button import Button
|
|
9
|
+
from bootstack.widgets._impl.primitives.frame import Frame
|
|
10
|
+
from bootstack.widgets._impl.primitives.label import Label
|
|
11
|
+
from bootstack.widgets._impl.primitives.separator import Separator
|
|
12
|
+
from bootstack.widgets.types import Master, StyledKwargs, WidgetDensity
|
|
13
|
+
from bootstack.style.typography import get_font
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ListItemKwargs(StyledKwargs, total=False):
|
|
17
|
+
"""Kwargs for ListItem."""
|
|
18
|
+
show_separator: bool
|
|
19
|
+
show_chevron: bool
|
|
20
|
+
focusable: bool
|
|
21
|
+
hoverable: bool
|
|
22
|
+
draggable: bool
|
|
23
|
+
removable: bool
|
|
24
|
+
selection_mode: Literal['none', 'single', 'multi']
|
|
25
|
+
show_selection_controls: bool
|
|
26
|
+
select_on_click: bool
|
|
27
|
+
density: WidgetDensity
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ListItem(CompositeFrame):
|
|
31
|
+
"""A list item widget with support for icons, text, badges, and interactive controls.
|
|
32
|
+
|
|
33
|
+
ListItem extends CompositeFrame to provide automatic state synchronization
|
|
34
|
+
across all child widgets. It supports selection modes, focus states, dragging,
|
|
35
|
+
removal, and various visual customizations.
|
|
36
|
+
|
|
37
|
+
The widget automatically handles hover, pressed, and focus states across all
|
|
38
|
+
registered child widgets using the Composite state coordinator.
|
|
39
|
+
|
|
40
|
+
Data fields:
|
|
41
|
+
When update_data() is called, the following fields are recognized:
|
|
42
|
+
- id: Unique identifier for the item (required for selection/removal).
|
|
43
|
+
- title: Primary text displayed in bold body font.
|
|
44
|
+
- text: Secondary text displayed below the title.
|
|
45
|
+
- icon: Icon name or configuration to display on the left.
|
|
46
|
+
- badge: Short label displayed on the right.
|
|
47
|
+
- selected: Boolean indicating if the item is selected.
|
|
48
|
+
- focused: Boolean indicating if the item has keyboard focus.
|
|
49
|
+
- item_index: Zero-based index of the item in the list.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, master: Master = None, **kwargs: Unpack[ListItemKwargs]) -> None:
|
|
53
|
+
"""Initialize a ListItem widget.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
master: Parent widget.
|
|
57
|
+
show_separator: Show separator line below the item. Default False.
|
|
58
|
+
show_chevron: Show chevron indicator on the right. Default False.
|
|
59
|
+
focusable: Whether this item can receive keyboard focus. Default True.
|
|
60
|
+
hoverable: Whether this item shows hover state. Default False.
|
|
61
|
+
draggable: Whether this item can be dragged. Default False.
|
|
62
|
+
removable: Whether this item can be removed. Default False.
|
|
63
|
+
selection_mode: Selection mode ('none', 'single', 'multi'). Default 'none'.
|
|
64
|
+
show_selection_controls: Show checkbox/radio button. Default False.
|
|
65
|
+
select_on_click: Whether clicking selects the item. Defaults to True when
|
|
66
|
+
selection_mode is 'single' or 'multi', False otherwise.
|
|
67
|
+
density: Visual density ('default' or 'compact'). Default 'default'.
|
|
68
|
+
**kwargs: Additional arguments forwarded to CompositeFrame.
|
|
69
|
+
"""
|
|
70
|
+
# state tracking
|
|
71
|
+
self._data = {}
|
|
72
|
+
self._state = {}
|
|
73
|
+
self._item_index = 0
|
|
74
|
+
self._selection_icon = None
|
|
75
|
+
self._drag_state = None
|
|
76
|
+
# Full (untruncated) title/text — the labels render an ellipsized copy
|
|
77
|
+
# sized to the row's available width (see _retruncate).
|
|
78
|
+
self._full_title = None
|
|
79
|
+
self._full_text = None
|
|
80
|
+
self._last_avail = -1
|
|
81
|
+
|
|
82
|
+
# configuration properties
|
|
83
|
+
self._show_separator = kwargs.pop('show_separator', False)
|
|
84
|
+
self._show_chevron = kwargs.pop('show_chevron', False)
|
|
85
|
+
self._focusable = kwargs.pop('focusable', True)
|
|
86
|
+
self._hoverable = kwargs.pop('hoverable', False)
|
|
87
|
+
self._draggable = kwargs.pop('draggable', False)
|
|
88
|
+
self._removable = kwargs.pop('removable', False)
|
|
89
|
+
# Capture accent (pop) so super() doesn't get it twice, and we can pass it
|
|
90
|
+
# explicitly to super() below.
|
|
91
|
+
self._accent = kwargs.pop('accent', None) or 'primary'
|
|
92
|
+
self._selection_mode = kwargs.pop('selection_mode', 'none')
|
|
93
|
+
self._show_selection_controls = kwargs.pop('show_selection_controls', False)
|
|
94
|
+
self._density = kwargs.pop('density', 'default')
|
|
95
|
+
# Divider color for a separated row (set to the stripe color when striped
|
|
96
|
+
# so the divider matches the band instead of a darker line).
|
|
97
|
+
self._separator_color = kwargs.pop('separator_color', None)
|
|
98
|
+
|
|
99
|
+
# Determine if clicking should trigger selection
|
|
100
|
+
# If selection mode is active (single/multi), enable click selection
|
|
101
|
+
# If selection mode is 'none', respect the select_on_click parameter
|
|
102
|
+
select_on_click = kwargs.pop('select_on_click', self._selection_mode != 'none')
|
|
103
|
+
self._select_on_click = select_on_click
|
|
104
|
+
|
|
105
|
+
self._get_selection_icon()
|
|
106
|
+
|
|
107
|
+
# Selected rows always show the wash, even alongside a selection control:
|
|
108
|
+
# wash + control, consistent with DataTable.
|
|
109
|
+
self._wash = True
|
|
110
|
+
# Neutral selection wash by default; opt in to an accent-tinted wash.
|
|
111
|
+
self._accent_selection = kwargs.pop('accent_selection', False)
|
|
112
|
+
|
|
113
|
+
# Adjust padding based on density
|
|
114
|
+
item_padding = (6, 3) if self._density == 'compact' else (8, 4)
|
|
115
|
+
|
|
116
|
+
# Initialize CompositeFrame with selection disabled (we handle it ourselves)
|
|
117
|
+
super().__init__(
|
|
118
|
+
master=master,
|
|
119
|
+
select_on_click=False,
|
|
120
|
+
variant='separated_item' if self._show_separator else 'item',
|
|
121
|
+
takefocus=self._focusable,
|
|
122
|
+
ttk_class='ListView.TFrame',
|
|
123
|
+
padding=item_padding,
|
|
124
|
+
accent=self._accent,
|
|
125
|
+
style_options=self._li_style_opts(),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# composite container widgets
|
|
129
|
+
self._left_frame = Frame(
|
|
130
|
+
self,
|
|
131
|
+
variant='list',
|
|
132
|
+
ttk_class='ListView.TFrame',
|
|
133
|
+
takefocus=False,
|
|
134
|
+
accent=self._accent,
|
|
135
|
+
style_options=self._li_style_opts()
|
|
136
|
+
)
|
|
137
|
+
self._left_frame.pack(side='left')
|
|
138
|
+
|
|
139
|
+
self._center_frame = Frame(
|
|
140
|
+
self,
|
|
141
|
+
variant='list',
|
|
142
|
+
ttk_class='ListView.TFrame',
|
|
143
|
+
takefocus=False,
|
|
144
|
+
accent=self._accent,
|
|
145
|
+
style_options=self._li_style_opts()
|
|
146
|
+
)
|
|
147
|
+
self._center_frame.pack(side='left', fill='x', expand=True)
|
|
148
|
+
|
|
149
|
+
self._right_frame = Frame(
|
|
150
|
+
self,
|
|
151
|
+
variant='list',
|
|
152
|
+
ttk_class='ListView.TFrame',
|
|
153
|
+
takefocus=False,
|
|
154
|
+
accent=self._accent,
|
|
155
|
+
style_options=self._li_style_opts()
|
|
156
|
+
)
|
|
157
|
+
self._right_frame.pack(side='left')
|
|
158
|
+
|
|
159
|
+
# conditional widgets
|
|
160
|
+
self._separator = Separator(self)
|
|
161
|
+
self._selection_widget = None
|
|
162
|
+
self._icon_widget = None
|
|
163
|
+
self._title_widget = None
|
|
164
|
+
self._text_widget = None
|
|
165
|
+
self._caption_widget = None
|
|
166
|
+
self._remove_widget = None
|
|
167
|
+
self._badge_widget = None
|
|
168
|
+
self._chevron_widget = None
|
|
169
|
+
self._drag_widget = None
|
|
170
|
+
|
|
171
|
+
# Register container frames with composite coordinator
|
|
172
|
+
for widget in [self._left_frame, self._center_frame, self._right_frame]:
|
|
173
|
+
self.register_composite(widget)
|
|
174
|
+
|
|
175
|
+
# Bind to composite invoke for selection and/or focus handling
|
|
176
|
+
if self._select_on_click or self._focusable:
|
|
177
|
+
self.on_invoked(self._on_click)
|
|
178
|
+
|
|
179
|
+
# Focus event handling (notify ListView of focus changes)
|
|
180
|
+
if self._focusable:
|
|
181
|
+
self.bind('<FocusIn>', self._on_focus_in, add='+')
|
|
182
|
+
self.bind('<FocusOut>', self._on_focus_out, add='+')
|
|
183
|
+
|
|
184
|
+
# row-level keyboard events
|
|
185
|
+
self.bind('<space>', self._on_space, add='+')
|
|
186
|
+
|
|
187
|
+
# Re-ellipsize the title/text whenever the center frame's allotment
|
|
188
|
+
# changes (sidebar resize, or the icon/chevron settling) so a long value
|
|
189
|
+
# can never push the icon/chevron out of the row.
|
|
190
|
+
self._center_frame.bind('<Configure>', self._on_configure_truncate, add='+')
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def selected(self):
|
|
194
|
+
"""bool: Current selection state from data."""
|
|
195
|
+
return self._data.get('selected')
|
|
196
|
+
|
|
197
|
+
@property
|
|
198
|
+
def data(self):
|
|
199
|
+
"""dict: The data record associated with this list item."""
|
|
200
|
+
return self._data
|
|
201
|
+
|
|
202
|
+
def _li_style_opts(self) -> dict:
|
|
203
|
+
"""Common style options for child widgets (carries the wash flag)."""
|
|
204
|
+
opts = dict(hoverable=self._hoverable, density=self._density, wash=self._wash,
|
|
205
|
+
accent_selection=self._accent_selection)
|
|
206
|
+
if self._separator_color:
|
|
207
|
+
opts['separator'] = self._separator_color
|
|
208
|
+
return opts
|
|
209
|
+
|
|
210
|
+
def _get_selection_icon(self):
|
|
211
|
+
"""Determine the selection icon based on selection mode."""
|
|
212
|
+
if self._selection_mode == 'multi':
|
|
213
|
+
self._selection_icon = dict(name='square', state=[('selected', "check-square-fill")])
|
|
214
|
+
elif self._selection_mode == 'single':
|
|
215
|
+
self._selection_icon = dict(name='circle', state=[('selected', "check-circle-fill")])
|
|
216
|
+
else:
|
|
217
|
+
self._selection_icon = None
|
|
218
|
+
|
|
219
|
+
def _on_click(self, event):
|
|
220
|
+
"""Handle click on the list item."""
|
|
221
|
+
if self._focusable:
|
|
222
|
+
self.focus()
|
|
223
|
+
# notify parent list that item has been clicked
|
|
224
|
+
self.master.event_generate('<<ItemClick>>', data=self._data)
|
|
225
|
+
self.select()
|
|
226
|
+
|
|
227
|
+
def _on_space(self, event):
|
|
228
|
+
"""Handle space key press."""
|
|
229
|
+
if self._focusable:
|
|
230
|
+
self.focus()
|
|
231
|
+
# notify parent list that item has been clicked
|
|
232
|
+
self.master.event_generate('<<ItemClick>>', data=self._data)
|
|
233
|
+
self.select()
|
|
234
|
+
|
|
235
|
+
def _on_focus_in(self, event):
|
|
236
|
+
"""Handle focus in event - notify ListView."""
|
|
237
|
+
self._data['focused'] = True
|
|
238
|
+
self.master.event_generate('<<ItemFocus>>', data=self._data)
|
|
239
|
+
|
|
240
|
+
def _on_focus_out(self, event):
|
|
241
|
+
"""Handle focus out event - check if focus moved to descendant."""
|
|
242
|
+
# Keep focus styling if focus moved to a descendant of this row
|
|
243
|
+
related = getattr(event, 'related', None)
|
|
244
|
+
try:
|
|
245
|
+
if related is not None and str(related).startswith(str(self)):
|
|
246
|
+
return 'break'
|
|
247
|
+
except TclError:
|
|
248
|
+
pass
|
|
249
|
+
|
|
250
|
+
self._data['focused'] = False
|
|
251
|
+
|
|
252
|
+
def _update_selection(self, selected: bool = False):
|
|
253
|
+
"""Apply selection state atomically (style + icon) with null guards."""
|
|
254
|
+
mode = self._selection_mode
|
|
255
|
+
|
|
256
|
+
if mode == 'none':
|
|
257
|
+
if self._selection_widget is not None:
|
|
258
|
+
try:
|
|
259
|
+
self._selection_widget.pack_forget()
|
|
260
|
+
except TclError:
|
|
261
|
+
pass
|
|
262
|
+
try:
|
|
263
|
+
self._selection_widget.destroy()
|
|
264
|
+
except TclError:
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
self._selection_widget = None
|
|
268
|
+
# Use CompositeFrame's set_selected to clear selection
|
|
269
|
+
self.set_selected(False)
|
|
270
|
+
|
|
271
|
+
# Notify widgets
|
|
272
|
+
for w in self._composite._composites:
|
|
273
|
+
try:
|
|
274
|
+
w.event_generate('<<CompositeDeselect>>')
|
|
275
|
+
except TclError:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
# keep a remembered icon so later comparisons are cheap
|
|
279
|
+
if hasattr(self, '_state'):
|
|
280
|
+
self._state['__sel_icon'] = None
|
|
281
|
+
self._state['selected'] = False
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
# ensure state cache exists
|
|
285
|
+
if not hasattr(self, '_state'):
|
|
286
|
+
self._state = {}
|
|
287
|
+
|
|
288
|
+
# ensure the selection control exists even if not visible
|
|
289
|
+
if self._selection_widget is None:
|
|
290
|
+
self._selection_widget = Label(
|
|
291
|
+
self._left_frame,
|
|
292
|
+
icon=self._selection_icon,
|
|
293
|
+
variant='selection',
|
|
294
|
+
ttk_class='ListView.TLabel',
|
|
295
|
+
icon_only=True,
|
|
296
|
+
accent=self._accent,
|
|
297
|
+
style_options=self._li_style_opts(),
|
|
298
|
+
takefocus=False,
|
|
299
|
+
)
|
|
300
|
+
if self._show_selection_controls:
|
|
301
|
+
self._selection_widget.pack(side='left', padx=5)
|
|
302
|
+
self.register_composite(self._selection_widget)
|
|
303
|
+
|
|
304
|
+
# Use CompositeFrame's set_selected to apply state
|
|
305
|
+
self.set_selected(selected)
|
|
306
|
+
|
|
307
|
+
# Notify widgets
|
|
308
|
+
for w in self._composite._composites:
|
|
309
|
+
try:
|
|
310
|
+
w.event_generate('<<CompositeSelect>>' if selected else '<<CompositeDeselect>>')
|
|
311
|
+
except TclError:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
# remember logical selected flag
|
|
315
|
+
self._state['selected'] = bool(selected)
|
|
316
|
+
|
|
317
|
+
def _update_icon(self, icon=None):
|
|
318
|
+
"""Update icon widget, or create if not existing."""
|
|
319
|
+
if icon is not None:
|
|
320
|
+
if not self._icon_widget:
|
|
321
|
+
self._icon_widget = Label(
|
|
322
|
+
self._left_frame,
|
|
323
|
+
icon=icon,
|
|
324
|
+
variant='icon',
|
|
325
|
+
ttk_class='ListView.TLabel',
|
|
326
|
+
takefocus=False,
|
|
327
|
+
icon_only=True,
|
|
328
|
+
accent=self._accent,
|
|
329
|
+
style_options=self._li_style_opts(),
|
|
330
|
+
)
|
|
331
|
+
self._icon_widget.pack(side='left', padx=5)
|
|
332
|
+
self.register_composite(self._icon_widget)
|
|
333
|
+
else:
|
|
334
|
+
self._icon_widget.configure(icon=icon)
|
|
335
|
+
else:
|
|
336
|
+
if self._icon_widget:
|
|
337
|
+
try:
|
|
338
|
+
self._icon_widget.pack_forget()
|
|
339
|
+
self._icon_widget.destroy()
|
|
340
|
+
except TclError:
|
|
341
|
+
pass
|
|
342
|
+
finally:
|
|
343
|
+
self._icon_widget = None
|
|
344
|
+
|
|
345
|
+
def _update_title(self, text=None):
|
|
346
|
+
"""Update title widget."""
|
|
347
|
+
title_font = 'caption[bold]' if self._density == 'compact' else 'body[bold]'
|
|
348
|
+
self._full_title = text
|
|
349
|
+
if text is not None:
|
|
350
|
+
shown = self._elide(text, get_font(title_font), self._avail_text_px())
|
|
351
|
+
if not self._title_widget:
|
|
352
|
+
self._title_widget = Label(
|
|
353
|
+
self._center_frame,
|
|
354
|
+
text=shown,
|
|
355
|
+
font=title_font,
|
|
356
|
+
variant='list',
|
|
357
|
+
ttk_class='ListView.TLabel',
|
|
358
|
+
takefocus=False,
|
|
359
|
+
accent=self._accent,
|
|
360
|
+
# width=1 so the label never drives the center frame wider
|
|
361
|
+
# than its allotment — fill='x' stretches it back out, and
|
|
362
|
+
# the elided text is sized to that allotment (see _retruncate).
|
|
363
|
+
width=1,
|
|
364
|
+
style_options=self._li_style_opts(),
|
|
365
|
+
)
|
|
366
|
+
self._title_widget.pack(side='top', fill='x', anchor='w', padx=(0, 3))
|
|
367
|
+
self.register_composite(self._title_widget)
|
|
368
|
+
else:
|
|
369
|
+
self._title_widget.configure(text=shown)
|
|
370
|
+
else:
|
|
371
|
+
if self._title_widget:
|
|
372
|
+
try:
|
|
373
|
+
self._title_widget.pack_forget()
|
|
374
|
+
self._title_widget.destroy()
|
|
375
|
+
except TclError:
|
|
376
|
+
pass
|
|
377
|
+
finally:
|
|
378
|
+
self._title_widget = None
|
|
379
|
+
|
|
380
|
+
def _update_text(self, text=None):
|
|
381
|
+
"""Update text widget."""
|
|
382
|
+
# Use caption font for compact density
|
|
383
|
+
text_font = 'caption' if self._density == 'compact' else 'body'
|
|
384
|
+
self._full_text = text
|
|
385
|
+
if text is not None:
|
|
386
|
+
shown = self._elide(text, get_font(text_font), self._avail_text_px())
|
|
387
|
+
if not self._text_widget:
|
|
388
|
+
self._text_widget = Label(
|
|
389
|
+
self._center_frame,
|
|
390
|
+
text=shown,
|
|
391
|
+
font=text_font,
|
|
392
|
+
variant='list',
|
|
393
|
+
ttk_class='ListView.TLabel',
|
|
394
|
+
takefocus=False,
|
|
395
|
+
accent=self._accent,
|
|
396
|
+
width=1,
|
|
397
|
+
style_options=self._li_style_opts(),
|
|
398
|
+
)
|
|
399
|
+
self._text_widget.pack(side='top', fill='x', padx=(0, 3))
|
|
400
|
+
self.register_composite(self._text_widget)
|
|
401
|
+
else:
|
|
402
|
+
self._text_widget.configure(text=shown)
|
|
403
|
+
else:
|
|
404
|
+
if self._text_widget:
|
|
405
|
+
try:
|
|
406
|
+
self._text_widget.pack_forget()
|
|
407
|
+
self._text_widget.destroy()
|
|
408
|
+
except TclError:
|
|
409
|
+
pass
|
|
410
|
+
finally:
|
|
411
|
+
self._text_widget = None
|
|
412
|
+
|
|
413
|
+
# ----- Title/text ellipsis -----
|
|
414
|
+
|
|
415
|
+
def _avail_text_px(self) -> int:
|
|
416
|
+
"""Pixels available to the center title/text before they must elide.
|
|
417
|
+
|
|
418
|
+
Read straight off the center frame's own allotted width. Because the
|
|
419
|
+
labels carry `width=1`, the center frame never inflates past its
|
|
420
|
+
allotment, so this is the true available width and it re-fires its
|
|
421
|
+
`<Configure>` whenever the icon/chevron settle or the sidebar resizes.
|
|
422
|
+
"""
|
|
423
|
+
try:
|
|
424
|
+
w = self._center_frame.winfo_width()
|
|
425
|
+
if w <= 1:
|
|
426
|
+
return 0
|
|
427
|
+
# 3px = the label's own right padx.
|
|
428
|
+
return max(0, w - 3)
|
|
429
|
+
except TclError:
|
|
430
|
+
return 0
|
|
431
|
+
|
|
432
|
+
@staticmethod
|
|
433
|
+
def _elide(text, font, avail: int):
|
|
434
|
+
"""Return `text` shortened with a trailing ellipsis to fit `avail` px."""
|
|
435
|
+
if not text or avail <= 0:
|
|
436
|
+
return text
|
|
437
|
+
try:
|
|
438
|
+
if font.measure(text) <= avail:
|
|
439
|
+
return text
|
|
440
|
+
ell = '…'
|
|
441
|
+
if font.measure(ell) > avail:
|
|
442
|
+
return ''
|
|
443
|
+
lo, hi = 0, len(text)
|
|
444
|
+
while lo < hi:
|
|
445
|
+
mid = (lo + hi + 1) // 2
|
|
446
|
+
if font.measure(text[:mid].rstrip() + ell) <= avail:
|
|
447
|
+
lo = mid
|
|
448
|
+
else:
|
|
449
|
+
hi = mid - 1
|
|
450
|
+
return (text[:lo].rstrip() + ell) if lo > 0 else ell
|
|
451
|
+
except TclError:
|
|
452
|
+
return text
|
|
453
|
+
|
|
454
|
+
def _on_configure_truncate(self, event=None):
|
|
455
|
+
"""Re-ellipsize when the row's available width changes."""
|
|
456
|
+
avail = self._avail_text_px()
|
|
457
|
+
if avail <= 0 or avail == self._last_avail:
|
|
458
|
+
return
|
|
459
|
+
self._last_avail = avail
|
|
460
|
+
self._retruncate(avail)
|
|
461
|
+
|
|
462
|
+
def _retruncate(self, avail: int | None = None):
|
|
463
|
+
"""Re-apply ellipsis to the title/text against the current width."""
|
|
464
|
+
if avail is None:
|
|
465
|
+
avail = self._avail_text_px()
|
|
466
|
+
if avail <= 0:
|
|
467
|
+
return
|
|
468
|
+
if self._title_widget and self._full_title is not None:
|
|
469
|
+
font = get_font('caption[bold]' if self._density == 'compact' else 'body[bold]')
|
|
470
|
+
self._title_widget.configure(text=self._elide(self._full_title, font, avail))
|
|
471
|
+
if self._text_widget and self._full_text is not None:
|
|
472
|
+
font = get_font('caption' if self._density == 'compact' else 'body')
|
|
473
|
+
self._text_widget.configure(text=self._elide(self._full_text, font, avail))
|
|
474
|
+
|
|
475
|
+
def _update_badge(self, text=None):
|
|
476
|
+
"""Update badge widget."""
|
|
477
|
+
if text is not None:
|
|
478
|
+
if not self._badge_widget:
|
|
479
|
+
self._badge_widget = Label(
|
|
480
|
+
self._right_frame,
|
|
481
|
+
text=text,
|
|
482
|
+
variant='list',
|
|
483
|
+
ttk_class='ListView.TLabel',
|
|
484
|
+
accent=self._accent,
|
|
485
|
+
style_options=self._li_style_opts(),
|
|
486
|
+
)
|
|
487
|
+
self._badge_widget.pack(side='right', padx=6)
|
|
488
|
+
self.register_composite(self._badge_widget)
|
|
489
|
+
else:
|
|
490
|
+
self._badge_widget.configure(text=text)
|
|
491
|
+
else:
|
|
492
|
+
if self._badge_widget:
|
|
493
|
+
try:
|
|
494
|
+
self._badge_widget.pack_forget()
|
|
495
|
+
self._badge_widget.destroy()
|
|
496
|
+
except TclError:
|
|
497
|
+
pass
|
|
498
|
+
finally:
|
|
499
|
+
self._badge_widget = None
|
|
500
|
+
|
|
501
|
+
def _update_chevron(self):
|
|
502
|
+
"""Update or create chevron widget."""
|
|
503
|
+
if self._show_chevron:
|
|
504
|
+
if not self._chevron_widget:
|
|
505
|
+
self._chevron_widget = Button(
|
|
506
|
+
self._right_frame,
|
|
507
|
+
icon='chevron-right',
|
|
508
|
+
icon_only=True,
|
|
509
|
+
variant='icon',
|
|
510
|
+
ttk_class='ListView.TButton',
|
|
511
|
+
takefocus=False,
|
|
512
|
+
accent=self._accent,
|
|
513
|
+
style_options=self._li_style_opts(),
|
|
514
|
+
)
|
|
515
|
+
self._chevron_widget.pack(side='right', padx=6)
|
|
516
|
+
self.register_composite(self._chevron_widget)
|
|
517
|
+
# The chevron is created lazily (after_idle), after the row's
|
|
518
|
+
# selection state was already applied to the other composites —
|
|
519
|
+
# so it misses the sync. Its selected color comes from the ttk
|
|
520
|
+
# 'selected' STATE (see CompositeFrame._update_states), not the
|
|
521
|
+
# <<CompositeSelect>> event, so apply that state directly now;
|
|
522
|
+
# otherwise a row selected on first load shows an unselected
|
|
523
|
+
# chevron color.
|
|
524
|
+
if self._data.get('selected'):
|
|
525
|
+
try:
|
|
526
|
+
self._chevron_widget.state(['selected'])
|
|
527
|
+
except TclError:
|
|
528
|
+
pass
|
|
529
|
+
else:
|
|
530
|
+
if self._chevron_widget:
|
|
531
|
+
try:
|
|
532
|
+
self._chevron_widget.pack_forget()
|
|
533
|
+
self._chevron_widget.destroy()
|
|
534
|
+
except TclError:
|
|
535
|
+
pass
|
|
536
|
+
finally:
|
|
537
|
+
self._chevron_widget = None
|
|
538
|
+
|
|
539
|
+
def _update_remove(self):
|
|
540
|
+
"""Update or create remove button widget."""
|
|
541
|
+
if self._removable:
|
|
542
|
+
if not self._remove_widget:
|
|
543
|
+
self._remove_widget = Button(
|
|
544
|
+
self._right_frame,
|
|
545
|
+
icon='x-lg',
|
|
546
|
+
icon_only=True,
|
|
547
|
+
variant='icon',
|
|
548
|
+
ttk_class='ListView.TButton',
|
|
549
|
+
takefocus=False,
|
|
550
|
+
command=self.remove,
|
|
551
|
+
accent=self._accent,
|
|
552
|
+
style_options=self._li_style_opts(),
|
|
553
|
+
)
|
|
554
|
+
self._remove_widget.pack(side='right', padx=6)
|
|
555
|
+
self.register_composite(self._remove_widget)
|
|
556
|
+
else:
|
|
557
|
+
if self._remove_widget:
|
|
558
|
+
try:
|
|
559
|
+
self._remove_widget.pack_forget()
|
|
560
|
+
self._remove_widget.destroy()
|
|
561
|
+
except TclError:
|
|
562
|
+
pass
|
|
563
|
+
finally:
|
|
564
|
+
self._remove_widget = None
|
|
565
|
+
|
|
566
|
+
def _update_drag(self):
|
|
567
|
+
"""Update or create drag handle widget."""
|
|
568
|
+
if self._draggable:
|
|
569
|
+
if not self._drag_widget:
|
|
570
|
+
self._drag_widget = Button(
|
|
571
|
+
self._right_frame,
|
|
572
|
+
icon='grip-vertical',
|
|
573
|
+
icon_only=True,
|
|
574
|
+
variant='icon',
|
|
575
|
+
ttk_class='ListView.TButton',
|
|
576
|
+
cursor='fleur',
|
|
577
|
+
takefocus=False,
|
|
578
|
+
accent=self._accent,
|
|
579
|
+
style_options=self._li_style_opts(),
|
|
580
|
+
)
|
|
581
|
+
self._drag_widget.pack(side='right', padx=6)
|
|
582
|
+
|
|
583
|
+
# Setup drag detection
|
|
584
|
+
self._drag_state = {'dragging': False, 'start_y': None}
|
|
585
|
+
self._drag_widget.bind('<ButtonPress-1>', self._on_drag_mouse_down, add='+')
|
|
586
|
+
self._drag_widget.bind('<B1-Motion>', self._on_drag_mouse_motion, add='+')
|
|
587
|
+
self._drag_widget.bind('<ButtonRelease-1>', self._on_drag_mouse_up, add='+')
|
|
588
|
+
self.register_composite(self._drag_widget)
|
|
589
|
+
else:
|
|
590
|
+
if self._drag_widget:
|
|
591
|
+
try:
|
|
592
|
+
self._drag_widget.pack_forget()
|
|
593
|
+
self._drag_widget.destroy()
|
|
594
|
+
except TclError:
|
|
595
|
+
pass
|
|
596
|
+
finally:
|
|
597
|
+
self._drag_widget = None
|
|
598
|
+
self._drag_state = None
|
|
599
|
+
|
|
600
|
+
def set_surface(self, surface: str) -> None:
|
|
601
|
+
"""Set the surface color for the row and its container frames.
|
|
602
|
+
|
|
603
|
+
This method is used by ListView to apply alternating row colors efficiently.
|
|
604
|
+
The surface color is set once when the widget is created and remains stable
|
|
605
|
+
during scrolling.
|
|
606
|
+
|
|
607
|
+
Args:
|
|
608
|
+
surface: Surface color name or value (e.g., 'background', 'background[+1]').
|
|
609
|
+
"""
|
|
610
|
+
previous = getattr(self, "_surface", "background")
|
|
611
|
+
self.configure_style_options(surface=surface)
|
|
612
|
+
if previous != surface:
|
|
613
|
+
self.rebuild_style()
|
|
614
|
+
|
|
615
|
+
for frame in (self._left_frame, self._center_frame, self._right_frame):
|
|
616
|
+
try:
|
|
617
|
+
frame.configure_style_options(surface=surface)
|
|
618
|
+
frame.rebuild_style()
|
|
619
|
+
except Exception:
|
|
620
|
+
continue
|
|
621
|
+
|
|
622
|
+
# ---- Event Handlers (Drag-related) ----
|
|
623
|
+
|
|
624
|
+
def _on_drag_mouse_down(self, event):
|
|
625
|
+
"""Mouse pressed on drag handle - prepare for drag."""
|
|
626
|
+
if not hasattr(self, '_drag_state'):
|
|
627
|
+
self._drag_state = {}
|
|
628
|
+
self._drag_state['start_y'] = event.y_root
|
|
629
|
+
self._drag_state['dragging'] = False
|
|
630
|
+
|
|
631
|
+
def _on_drag_mouse_motion(self, event):
|
|
632
|
+
"""Mouse moving with button held - this is a drag."""
|
|
633
|
+
if not hasattr(self, '_drag_state') or self._drag_state.get('start_y') is None:
|
|
634
|
+
return
|
|
635
|
+
|
|
636
|
+
# if this is the first motion event, emit drag start
|
|
637
|
+
if not self._drag_state.get('dragging'):
|
|
638
|
+
self._drag_state['dragging'] = True
|
|
639
|
+
self.master.event_generate(
|
|
640
|
+
'<<ItemDragStart>>',
|
|
641
|
+
data={**self._data, 'source_index': self._item_index, 'y_start': self._drag_state['start_y']}
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# emit drag motion event
|
|
645
|
+
self.master.event_generate(
|
|
646
|
+
'<<ItemDrag>>',
|
|
647
|
+
data={
|
|
648
|
+
**self._data,
|
|
649
|
+
'source_index': self._item_index,
|
|
650
|
+
'y_current': event.y_root,
|
|
651
|
+
'y_start': self._drag_state['start_y'],
|
|
652
|
+
'delta_y': event.y_root - self._drag_state['start_y']
|
|
653
|
+
}
|
|
654
|
+
)
|
|
655
|
+
|
|
656
|
+
def _on_drag_mouse_up(self, event):
|
|
657
|
+
"""Mouse release - end drag if we were dragging."""
|
|
658
|
+
if not hasattr(self, '_drag_state'):
|
|
659
|
+
return
|
|
660
|
+
|
|
661
|
+
# only emit drag end if we actually started dragging
|
|
662
|
+
if self._drag_state.get('dragging'):
|
|
663
|
+
self.master.event_generate('<<ItemDragEnd>>',
|
|
664
|
+
data={
|
|
665
|
+
**self._data,
|
|
666
|
+
'source_index': self._item_index,
|
|
667
|
+
'y_end': event.y_root,
|
|
668
|
+
'y_start': self._drag_state.get('start_y')
|
|
669
|
+
})
|
|
670
|
+
# reset drag state
|
|
671
|
+
self._drag_state = {'dragging': False, 'start_y': None}
|
|
672
|
+
|
|
673
|
+
# --- Configuration delegates ---
|
|
674
|
+
|
|
675
|
+
@configure_delegate('selection_mode')
|
|
676
|
+
def _delegate_selection_mode(self, value=None):
|
|
677
|
+
self._selection_mode = value
|
|
678
|
+
self._get_selection_icon()
|
|
679
|
+
|
|
680
|
+
@configure_delegate('show_selection_controls')
|
|
681
|
+
def _delegate_show_selection_controls(self, value=None):
|
|
682
|
+
self._show_selection_controls = value
|
|
683
|
+
self._get_selection_icon()
|
|
684
|
+
self._update_selection(False)
|
|
685
|
+
|
|
686
|
+
@configure_delegate('show_chevron')
|
|
687
|
+
def _delegate_show_chevron(self, value=None):
|
|
688
|
+
self._show_chevron = value
|
|
689
|
+
self._update_chevron()
|
|
690
|
+
|
|
691
|
+
@configure_delegate('draggable')
|
|
692
|
+
def _delegate_draggable(self, value=None):
|
|
693
|
+
self._draggable = value
|
|
694
|
+
self._update_drag()
|
|
695
|
+
|
|
696
|
+
@configure_delegate('removable')
|
|
697
|
+
def _delegate_removable(self, value=None):
|
|
698
|
+
self._removable = value
|
|
699
|
+
self._update_remove()
|
|
700
|
+
|
|
701
|
+
# ---- Public API ----
|
|
702
|
+
|
|
703
|
+
def select(self):
|
|
704
|
+
"""Toggle or set the selection state based on selection mode.
|
|
705
|
+
|
|
706
|
+
Returns:
|
|
707
|
+
bool or None: True if selected, False if deselected, None if no action.
|
|
708
|
+
"""
|
|
709
|
+
mode = self._selection_mode
|
|
710
|
+
if mode == 'none':
|
|
711
|
+
return None
|
|
712
|
+
|
|
713
|
+
is_selected = bool(self.selected or False)
|
|
714
|
+
if is_selected:
|
|
715
|
+
if mode == 'single':
|
|
716
|
+
return None
|
|
717
|
+
self._data['selected'] = False
|
|
718
|
+
self.master.event_generate('<<ItemSelecting>>', data=self._data)
|
|
719
|
+
return False
|
|
720
|
+
|
|
721
|
+
self._data['selected'] = True
|
|
722
|
+
self.master.event_generate('<<ItemSelecting>>', data=self._data)
|
|
723
|
+
return True
|
|
724
|
+
|
|
725
|
+
def remove(self):
|
|
726
|
+
"""Notify subscribers to handle remove action."""
|
|
727
|
+
self.master.event_generate('<<ItemRemoving>>', data=self._data)
|
|
728
|
+
|
|
729
|
+
def update_data(self, record: dict | None):
|
|
730
|
+
"""Update row visuals efficiently when values have changed.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
record: Dictionary containing the item data. If None or contains
|
|
734
|
+
'__empty__' key, the item will be hidden.
|
|
735
|
+
"""
|
|
736
|
+
if record is None or '__empty__' in record:
|
|
737
|
+
self.pack_forget()
|
|
738
|
+
return
|
|
739
|
+
|
|
740
|
+
if not self.winfo_manager():
|
|
741
|
+
self.pack(fill='x')
|
|
742
|
+
|
|
743
|
+
self._data = record
|
|
744
|
+
self._item_index = self._data.get('item_index', 0)
|
|
745
|
+
|
|
746
|
+
selected = bool(record.get('selected', False))
|
|
747
|
+
|
|
748
|
+
# handle focus - apply tkinter focus to the widget that should have logical focus
|
|
749
|
+
focused = bool(record.get('focused', False))
|
|
750
|
+
if self._state.get('focused') != focused and self._focusable:
|
|
751
|
+
if focused:
|
|
752
|
+
# this record should have focus - give tkinter focus to this widget
|
|
753
|
+
try:
|
|
754
|
+
self.focus_set()
|
|
755
|
+
self._data['focused'] = True
|
|
756
|
+
except TclError:
|
|
757
|
+
pass
|
|
758
|
+
else:
|
|
759
|
+
# This record lost focus - clear tkinter focus if we have it,
|
|
760
|
+
# and drop the keyboard-focus ring state so a recycled/relocated
|
|
761
|
+
# row doesn't keep a stale ring.
|
|
762
|
+
try:
|
|
763
|
+
if self.focus_get() == self:
|
|
764
|
+
# Move focus to parent container
|
|
765
|
+
self.master.focus_set()
|
|
766
|
+
except (TclError, AttributeError):
|
|
767
|
+
pass
|
|
768
|
+
try:
|
|
769
|
+
self.state(['!background'])
|
|
770
|
+
except TclError:
|
|
771
|
+
pass
|
|
772
|
+
self._data['focused'] = False
|
|
773
|
+
self._state['focused'] = focused
|
|
774
|
+
|
|
775
|
+
# Create/update field widgets first so they exist before selection state is applied
|
|
776
|
+
for field, updater in {
|
|
777
|
+
"title": self._update_title,
|
|
778
|
+
"text": self._update_text,
|
|
779
|
+
"icon": self._update_icon,
|
|
780
|
+
}.items():
|
|
781
|
+
value = record.get(field)
|
|
782
|
+
if self._state.get(field) != value:
|
|
783
|
+
updater(value)
|
|
784
|
+
self._state[field] = value
|
|
785
|
+
|
|
786
|
+
# Apply selection after widgets exist so _update_states reaches all composites
|
|
787
|
+
if self._state.get('selected') != selected:
|
|
788
|
+
self._update_selection(selected)
|
|
789
|
+
self._state['selected'] = selected
|
|
790
|
+
|
|
791
|
+
def _safe(fn):
|
|
792
|
+
def _call():
|
|
793
|
+
try:
|
|
794
|
+
if self.winfo_exists():
|
|
795
|
+
fn()
|
|
796
|
+
except TclError:
|
|
797
|
+
pass
|
|
798
|
+
return _call
|
|
799
|
+
|
|
800
|
+
self.after_idle(_safe(self._update_chevron))
|
|
801
|
+
self.after_idle(_safe(self._update_drag))
|
|
802
|
+
self.after_idle(_safe(self._update_remove))
|