bootstack 0.1.0a1__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 +249 -0
- bootstack/__main__.py +5 -0
- bootstack/api/__init__.py +127 -0
- bootstack/api/app.py +30 -0
- bootstack/api/constants.py +3 -0
- bootstack/api/data.py +23 -0
- bootstack/api/dialogs.py +44 -0
- bootstack/api/i18n.py +17 -0
- bootstack/api/localization.py +16 -0
- bootstack/api/menu.py +7 -0
- bootstack/api/style.py +23 -0
- bootstack/api/utils.py +24 -0
- bootstack/api/widgets.py +137 -0
- bootstack/assets/__init__.py +24 -0
- bootstack/assets/bootstack-transparent.png +0 -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/border.png +0 -0
- bootstack/assets/elements/button-compact.png +0 -0
- bootstack/assets/elements/button-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-after-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-before-default.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-compact.png +0 -0
- bootstack/assets/elements/button-group-horizontal-center-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-after-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-before-default.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-compact.png +0 -0
- bootstack/assets/elements/button-group-vertical-center-default.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-after-compact.png +0 -0
- bootstack/assets/elements/input-after-default.png +0 -0
- bootstack/assets/elements/input-before-compact.png +0 -0
- bootstack/assets/elements/input-before-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/manifest.toml +480 -0
- bootstack/assets/elements/menu-item.png +0 -0
- bootstack/assets/elements/nav-button-compact.png +0 -0
- bootstack/assets/elements/nav-button-default.png +0 -0
- bootstack/assets/elements/nav-icon-button-compact.png +0 -0
- bootstack/assets/elements/nav-icon-button-default.png +0 -0
- bootstack/assets/elements/notebook-client-border.png +0 -0
- bootstack/assets/elements/notebook-tab-active.png +0 -0
- bootstack/assets/elements/notebook-tab-bar.png +0 -0
- bootstack/assets/elements/notebook-tab-normal.png +0 -0
- bootstack/assets/elements/notebook-tab-pill.png +0 -0
- bootstack/assets/elements/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/elements/progress-bar-solid.png +0 -0
- bootstack/assets/elements/progress-bar-thin.png +0 -0
- bootstack/assets/elements/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/elements/radio-selected.png +0 -0
- bootstack/assets/elements/radio-unselected.png +0 -0
- bootstack/assets/elements/scrollbar-horizontal.png +0 -0
- bootstack/assets/elements/scrollbar-vertical.png +0 -0
- bootstack/assets/elements/slider-handle-focus.png +0 -0
- bootstack/assets/elements/slider-handle.png +0 -0
- bootstack/assets/elements/slider-track-horizontal.png +0 -0
- bootstack/assets/elements/slider-track-vertical.png +0 -0
- bootstack/assets/elements/switch-off.png +0 -0
- bootstack/assets/elements/switch-on.png +0 -0
- bootstack/assets/elements/tabs-bar-horizontal.png +0 -0
- bootstack/assets/elements/tabs-bar-vertical.png +0 -0
- bootstack/assets/elements/tabs-pill.png +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +875 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +853 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +851 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +914 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +841 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
- bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +842 -0
- bootstack/assets/themes/__init__.py +0 -0
- bootstack/assets/themes/amber-dark.json +32 -0
- bootstack/assets/themes/amber-light.json +32 -0
- bootstack/assets/themes/aurora-dark.json +32 -0
- bootstack/assets/themes/aurora-light.json +32 -0
- bootstack/assets/themes/bootstrap-dark.json +32 -0
- bootstack/assets/themes/bootstrap-light.json +32 -0
- bootstack/assets/themes/classic-dark.json +32 -0
- bootstack/assets/themes/classic-light.json +32 -0
- bootstack/assets/themes/docs-dark.json +32 -0
- bootstack/assets/themes/docs-light.json +32 -0
- bootstack/assets/themes/forest-dark.json +32 -0
- bootstack/assets/themes/forest-light.json +32 -0
- bootstack/assets/themes/ocean-dark.json +32 -0
- bootstack/assets/themes/ocean-light.json +32 -0
- bootstack/assets/themes/rose-dark.json +32 -0
- bootstack/assets/themes/rose-light.json +32 -0
- bootstack/assets/widgets/__init__.py +0 -0
- bootstack/assets/widgets/badge-default.png +0 -0
- bootstack/assets/widgets/badge-pill.png +0 -0
- bootstack/assets/widgets/border.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-after.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-before.png +0 -0
- bootstack/assets/widgets/button-group-horizontal-center.png +0 -0
- bootstack/assets/widgets/button-group-vertical-after.png +0 -0
- bootstack/assets/widgets/button-group-vertical-before.png +0 -0
- bootstack/assets/widgets/button-group-vertical-center.png +0 -0
- bootstack/assets/widgets/button.png +0 -0
- bootstack/assets/widgets/checkbox-checked.png +0 -0
- bootstack/assets/widgets/checkbox-indeterminate.png +0 -0
- bootstack/assets/widgets/checkbox-unchecked.png +0 -0
- bootstack/assets/widgets/field.png +0 -0
- bootstack/assets/widgets/icon-button.png +0 -0
- bootstack/assets/widgets/input-inner.png +0 -0
- bootstack/assets/widgets/input-prefix.png +0 -0
- bootstack/assets/widgets/input-suffix.png +0 -0
- bootstack/assets/widgets/input.png +0 -0
- bootstack/assets/widgets/list-item-focus.png +0 -0
- bootstack/assets/widgets/list-item-separated.png +0 -0
- bootstack/assets/widgets/menu-item-separated.png +0 -0
- bootstack/assets/widgets/notebook-client-border.png +0 -0
- bootstack/assets/widgets/notebook-pill-active.png +0 -0
- bootstack/assets/widgets/notebook-pill-inactive.png +0 -0
- bootstack/assets/widgets/notebook-tab-active.png +0 -0
- bootstack/assets/widgets/notebook-tab-border.png +0 -0
- bootstack/assets/widgets/notebook-tab-normal.png +0 -0
- bootstack/assets/widgets/notebook-underline.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-default.png +0 -0
- bootstack/assets/widgets/progress-bar-horizontal-striped.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-default.png +0 -0
- bootstack/assets/widgets/progress-bar-vertical-striped.png +0 -0
- bootstack/assets/widgets/progress-trough-horizontal.png +0 -0
- bootstack/assets/widgets/progress-trough-vertical.png +0 -0
- bootstack/assets/widgets/radio-selected.png +0 -0
- bootstack/assets/widgets/radio-unselected.png +0 -0
- bootstack/assets/widgets/scrollbar-horizontal-rounded.png +0 -0
- bootstack/assets/widgets/scrollbar-vertical-rounded.png +0 -0
- bootstack/assets/widgets/separator-horizontal.png +0 -0
- bootstack/assets/widgets/separator-vertical.png +0 -0
- bootstack/assets/widgets/slider-handle-focus.png +0 -0
- bootstack/assets/widgets/slider-handle.png +0 -0
- bootstack/assets/widgets/slider-track-horizontal.png +0 -0
- bootstack/assets/widgets/slider-track-vertical.png +0 -0
- bootstack/assets/widgets/switch-off.png +0 -0
- bootstack/assets/widgets/switch-on.png +0 -0
- bootstack/assets/widgets/tabs-bar-horizontal.png +0 -0
- bootstack/assets/widgets/tabs-bar-vertical.png +0 -0
- bootstack/assets/widgets/tabs-pill.png +0 -0
- bootstack/cli/__init__.py +124 -0
- bootstack/cli/__main__.py +6 -0
- bootstack/cli/add.py +439 -0
- bootstack/cli/build.py +115 -0
- bootstack/cli/config.py +287 -0
- bootstack/cli/demo.py +1267 -0
- bootstack/cli/doctor.py +195 -0
- bootstack/cli/list_cmd.py +71 -0
- bootstack/cli/promote.py +120 -0
- bootstack/cli/pyinstaller.py +246 -0
- bootstack/cli/run.py +99 -0
- bootstack/cli/start.py +105 -0
- bootstack/cli/templates/__init__.py +861 -0
- bootstack/constants.py +325 -0
- bootstack/core/__init__.py +34 -0
- bootstack/core/capabilities/__init__.py +45 -0
- bootstack/core/capabilities/after.py +103 -0
- bootstack/core/capabilities/bind.py +154 -0
- bootstack/core/capabilities/bindtags.py +112 -0
- bootstack/core/capabilities/busy.py +61 -0
- bootstack/core/capabilities/clipboard.py +88 -0
- bootstack/core/capabilities/focus.py +118 -0
- bootstack/core/capabilities/grab.py +65 -0
- bootstack/core/capabilities/grid.py +188 -0
- bootstack/core/capabilities/localization.py +231 -0
- bootstack/core/capabilities/pack.py +119 -0
- bootstack/core/capabilities/place.py +92 -0
- bootstack/core/capabilities/selection.py +136 -0
- bootstack/core/capabilities/signals.py +242 -0
- bootstack/core/capabilities/winfo.py +315 -0
- bootstack/core/colorutils.py +234 -0
- bootstack/core/exceptions.py +95 -0
- bootstack/core/images.py +283 -0
- bootstack/core/localization/README.md +90 -0
- bootstack/core/localization/__init__.py +13 -0
- bootstack/core/localization/intl_format.py +580 -0
- bootstack/core/localization/msgcat.py +425 -0
- bootstack/core/localization/specs.py +143 -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/publisher.py +147 -0
- bootstack/core/signals/README.md +112 -0
- bootstack/core/signals/__init__.py +8 -0
- bootstack/core/signals/integration.py +100 -0
- bootstack/core/signals/signal.py +317 -0
- bootstack/core/signals/types.py +4 -0
- bootstack/core/validation/__init__.py +5 -0
- bootstack/core/validation/types.py +13 -0
- bootstack/core/validation/validation_result.py +17 -0
- bootstack/core/validation/validation_rules.py +112 -0
- bootstack/core/variables.py +62 -0
- bootstack/datasource/README.md +607 -0
- bootstack/datasource/__init__.py +51 -0
- bootstack/datasource/base.py +474 -0
- bootstack/datasource/file_source.py +541 -0
- bootstack/datasource/memory_source.py +482 -0
- bootstack/datasource/sqlite_source.py +453 -0
- bootstack/datasource/types.py +259 -0
- bootstack/dialogs/__init__.py +56 -0
- bootstack/dialogs/colorchooser.py +674 -0
- bootstack/dialogs/colordropper.py +257 -0
- bootstack/dialogs/datedialog.py +404 -0
- bootstack/dialogs/dialog.py +514 -0
- bootstack/dialogs/filterdialog.py +358 -0
- bootstack/dialogs/fontdialog.py +339 -0
- bootstack/dialogs/formdialog.py +541 -0
- bootstack/dialogs/message.py +489 -0
- bootstack/dialogs/query.py +561 -0
- bootstack/py.typed +1 -0
- bootstack/runtime/__init__.py +3 -0
- bootstack/runtime/app.py +879 -0
- bootstack/runtime/base_window.py +786 -0
- bootstack/runtime/events.py +399 -0
- bootstack/runtime/menu.py +510 -0
- bootstack/runtime/shortcuts.py +423 -0
- bootstack/runtime/tk_patch.py +31 -0
- bootstack/runtime/toplevel.py +131 -0
- bootstack/runtime/utility.py +371 -0
- bootstack/runtime/visual_focus.py +228 -0
- bootstack/runtime/window_utilities.py +1043 -0
- bootstack/style/__init__.py +5498 -0
- bootstack/style/bootstyle.py +507 -0
- bootstack/style/bootstyle_builder_base.py +752 -0
- bootstack/style/bootstyle_builder_mixed.py +93 -0
- bootstack/style/bootstyle_builder_tk.py +109 -0
- bootstack/style/bootstyle_builder_ttk.py +354 -0
- bootstack/style/builders/__init__.py +51 -0
- bootstack/style/builders/badge.py +44 -0
- bootstack/style/builders/button.py +453 -0
- bootstack/style/builders/buttongroup.py +344 -0
- bootstack/style/builders/calendar.py +271 -0
- bootstack/style/builders/checkbutton.py +95 -0
- bootstack/style/builders/combobox.py +112 -0
- bootstack/style/builders/contextmenu.py +268 -0
- bootstack/style/builders/entry.py +83 -0
- bootstack/style/builders/expander.py +171 -0
- bootstack/style/builders/field.py +312 -0
- bootstack/style/builders/frame.py +27 -0
- bootstack/style/builders/label.py +28 -0
- bootstack/style/builders/labelframe.py +41 -0
- bootstack/style/builders/listview.py +267 -0
- bootstack/style/builders/menubar.py +74 -0
- bootstack/style/builders/menubutton.py +408 -0
- bootstack/style/builders/notebook.py +316 -0
- bootstack/style/builders/panedwindow.py +25 -0
- bootstack/style/builders/progressbar.py +71 -0
- bootstack/style/builders/radiobutton.py +68 -0
- bootstack/style/builders/scale.py +66 -0
- bootstack/style/builders/scrollbar.py +360 -0
- bootstack/style/builders/separator.py +45 -0
- bootstack/style/builders/sidenav.py +313 -0
- bootstack/style/builders/sizegrip.py +15 -0
- bootstack/style/builders/spinbox.py +119 -0
- bootstack/style/builders/switch.py +67 -0
- bootstack/style/builders/tabitem.py +205 -0
- bootstack/style/builders/toolbutton.py +260 -0
- bootstack/style/builders/tooltip.py +26 -0
- bootstack/style/builders/treeview.py +269 -0
- bootstack/style/builders/utils.py +404 -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/style.py +499 -0
- bootstack/style/theme_provider.py +449 -0
- bootstack/style/tk_patch.py +5 -0
- bootstack/style/token_maps.py +42 -0
- bootstack/style/types.py +32 -0
- bootstack/style/typography.py +527 -0
- bootstack/style/utility.py +696 -0
- bootstack/themes/__init__.py +12 -0
- bootstack/themes/standard.py +415 -0
- bootstack/themes/user.py +45 -0
- bootstack/widgets/__init__.py +53 -0
- bootstack/widgets/composites/__init__.py +38 -0
- bootstack/widgets/composites/accordion.py +385 -0
- bootstack/widgets/composites/appshell.py +445 -0
- bootstack/widgets/composites/buttongroup.py +391 -0
- bootstack/widgets/composites/calendar.py +914 -0
- bootstack/widgets/composites/compositeframe.py +282 -0
- bootstack/widgets/composites/contextmenu.py +1754 -0
- bootstack/widgets/composites/dateentry.py +261 -0
- bootstack/widgets/composites/dropdownbutton.py +190 -0
- bootstack/widgets/composites/expander.py +508 -0
- bootstack/widgets/composites/field.py +448 -0
- bootstack/widgets/composites/floodgauge.py +434 -0
- bootstack/widgets/composites/form.py +983 -0
- bootstack/widgets/composites/labeledscale.py +209 -0
- bootstack/widgets/composites/list/__init__.py +15 -0
- bootstack/widgets/composites/list/listitem.py +733 -0
- bootstack/widgets/composites/list/listview.py +1507 -0
- bootstack/widgets/composites/menubar.py +303 -0
- bootstack/widgets/composites/meter.py +882 -0
- bootstack/widgets/composites/numericentry.py +183 -0
- bootstack/widgets/composites/pagestack.py +330 -0
- bootstack/widgets/composites/passwordentry.py +149 -0
- bootstack/widgets/composites/pathentry.py +223 -0
- bootstack/widgets/composites/radiogroup.py +466 -0
- bootstack/widgets/composites/scrolledtext.py +388 -0
- bootstack/widgets/composites/scrolledtext.pyi +186 -0
- bootstack/widgets/composites/scrollview.py +675 -0
- bootstack/widgets/composites/selectbox.py +544 -0
- bootstack/widgets/composites/sidenav/__init__.py +24 -0
- bootstack/widgets/composites/sidenav/group.py +485 -0
- bootstack/widgets/composites/sidenav/header.py +83 -0
- bootstack/widgets/composites/sidenav/item.py +413 -0
- bootstack/widgets/composites/sidenav/separator.py +51 -0
- bootstack/widgets/composites/sidenav/view.py +919 -0
- bootstack/widgets/composites/spinnerentry.py +232 -0
- bootstack/widgets/composites/tableview/__init__.py +5 -0
- bootstack/widgets/composites/tableview/tableview.py +2254 -0
- bootstack/widgets/composites/tableview/types.py +169 -0
- bootstack/widgets/composites/tabs/__init__.py +6 -0
- bootstack/widgets/composites/tabs/tabitem.py +372 -0
- bootstack/widgets/composites/tabs/tabs.py +478 -0
- bootstack/widgets/composites/tabs/tabview.py +352 -0
- bootstack/widgets/composites/textentry.py +90 -0
- bootstack/widgets/composites/timeentry.py +189 -0
- bootstack/widgets/composites/toast.py +364 -0
- bootstack/widgets/composites/togglegroup.py +382 -0
- bootstack/widgets/composites/toolbar.py +393 -0
- bootstack/widgets/composites/tooltip.py +404 -0
- bootstack/widgets/internal/__init__.py +0 -0
- bootstack/widgets/internal/wrapper_base.py +304 -0
- bootstack/widgets/mixins/__init__.py +25 -0
- bootstack/widgets/mixins/configure_mixin.py +186 -0
- bootstack/widgets/mixins/entry_mixin.py +70 -0
- bootstack/widgets/mixins/font_mixin.py +346 -0
- bootstack/widgets/mixins/icon_mixin.py +38 -0
- bootstack/widgets/mixins/localization_mixin.py +255 -0
- bootstack/widgets/mixins/signal_mixin.py +272 -0
- bootstack/widgets/mixins/validation_mixin.py +204 -0
- bootstack/widgets/parts/__init__.py +11 -0
- bootstack/widgets/parts/numberentry_part.py +345 -0
- bootstack/widgets/parts/spinnerentry_part.py +394 -0
- bootstack/widgets/parts/textentry_part.py +344 -0
- bootstack/widgets/primitives/__init__.py +55 -0
- bootstack/widgets/primitives/badge.py +44 -0
- bootstack/widgets/primitives/button.py +89 -0
- bootstack/widgets/primitives/card.py +66 -0
- bootstack/widgets/primitives/checkbutton.py +124 -0
- bootstack/widgets/primitives/checktoggle.py +53 -0
- bootstack/widgets/primitives/combobox.py +165 -0
- bootstack/widgets/primitives/entry.py +98 -0
- bootstack/widgets/primitives/frame.py +206 -0
- bootstack/widgets/primitives/gridframe.py +479 -0
- bootstack/widgets/primitives/label.py +95 -0
- bootstack/widgets/primitives/labelframe.py +63 -0
- bootstack/widgets/primitives/menubutton.py +118 -0
- bootstack/widgets/primitives/notebook.py +551 -0
- bootstack/widgets/primitives/optionmenu.py +248 -0
- bootstack/widgets/primitives/packframe.py +228 -0
- bootstack/widgets/primitives/panedwindow.py +58 -0
- bootstack/widgets/primitives/progressbar.py +95 -0
- bootstack/widgets/primitives/radiobutton.py +115 -0
- bootstack/widgets/primitives/radiotoggle.py +50 -0
- bootstack/widgets/primitives/scale.py +85 -0
- bootstack/widgets/primitives/scrollbar.py +56 -0
- bootstack/widgets/primitives/separator.py +56 -0
- bootstack/widgets/primitives/sizegrip.py +47 -0
- bootstack/widgets/primitives/spinbox.py +91 -0
- bootstack/widgets/primitives/switch.py +41 -0
- bootstack/widgets/primitives/treeview.py +77 -0
- bootstack/widgets/types.py +20 -0
- bootstack-0.1.0a1.dist-info/METADATA +196 -0
- bootstack-0.1.0a1.dist-info/RECORD +419 -0
- bootstack-0.1.0a1.dist-info/WHEEL +5 -0
- bootstack-0.1.0a1.dist-info/entry_points.txt +2 -0
- bootstack-0.1.0a1.dist-info/licenses/LICENSE +22 -0
- bootstack-0.1.0a1.dist-info/licenses/NOTICE +10 -0
- bootstack-0.1.0a1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
from tkinter import TclError
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from bootstack.widgets.composites.compositeframe import CompositeFrame
|
|
5
|
+
from bootstack.widgets.mixins import configure_delegate
|
|
6
|
+
from bootstack.widgets.primitives.button import Button
|
|
7
|
+
from bootstack.widgets.primitives.frame import Frame
|
|
8
|
+
from bootstack.widgets.primitives.label import Label
|
|
9
|
+
from bootstack.widgets.primitives.separator import Separator
|
|
10
|
+
from bootstack.widgets.types import Master
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ListItem(CompositeFrame):
|
|
14
|
+
"""A list item widget with support for icons, text, badges, and interactive controls.
|
|
15
|
+
|
|
16
|
+
ListItem extends CompositeFrame to provide automatic state synchronization
|
|
17
|
+
across all child widgets. It supports selection modes, focus states, dragging,
|
|
18
|
+
removal, and various visual customizations.
|
|
19
|
+
|
|
20
|
+
The widget automatically handles hover, pressed, and focus states across all
|
|
21
|
+
registered child widgets using the Composite state coordinator.
|
|
22
|
+
|
|
23
|
+
!!! note "Events"
|
|
24
|
+
- `<<ItemClick>>`: Fired when the item is clicked.
|
|
25
|
+
- `<<ItemSelecting>>`: Fired when the item is being selected/deselected.
|
|
26
|
+
- `<<ItemRemoving>>`: Fired when the remove button is clicked.
|
|
27
|
+
- `<<ItemFocus>>`: Fired when the item receives keyboard focus.
|
|
28
|
+
- `<<ItemDragStart>>`: Fired when a drag operation begins.
|
|
29
|
+
- `<<ItemDrag>>`: Fired during a drag operation.
|
|
30
|
+
- `<<ItemDragEnd>>`: Fired when a drag operation ends.
|
|
31
|
+
|
|
32
|
+
Data fields:
|
|
33
|
+
When update_data() is called, the following fields are recognized:
|
|
34
|
+
- id: Unique identifier for the item (required for selection/removal).
|
|
35
|
+
- title: Main heading text displayed at the top.
|
|
36
|
+
- text: Body text displayed below the title.
|
|
37
|
+
- caption: Small caption text displayed at the bottom.
|
|
38
|
+
- icon: Icon name or configuration to display on the left.
|
|
39
|
+
- badge: Badge text displayed on the right.
|
|
40
|
+
- selected: Boolean indicating if the item is selected.
|
|
41
|
+
- focused: Boolean indicating if the item has keyboard focus.
|
|
42
|
+
- item_index: Zero-based index of the item in the list.
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
def __init__(self, master: Master = None, **kwargs: Any):
|
|
46
|
+
"""Initialize a ListItem widget.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
master: Parent widget.
|
|
50
|
+
**kwargs: Additional keyword arguments:
|
|
51
|
+
focus_color (str): Color for the focus indicator. Defaults to None.
|
|
52
|
+
show_separator (bool): Show separator line below the item. Defaults to False.
|
|
53
|
+
show_chevron (bool): Show chevron indicator on the right. Defaults to False.
|
|
54
|
+
focusable (bool): Whether this item can receive keyboard focus. Defaults to True.
|
|
55
|
+
hoverable (bool): Whether this item shows hover state. Defaults to False.
|
|
56
|
+
draggable (bool): Whether this item can be dragged. Defaults to False.
|
|
57
|
+
removable (bool): Whether this item can be removed. Defaults to False.
|
|
58
|
+
selected_background (str): Background color when selected. Defaults to 'primary'.
|
|
59
|
+
selection_mode (str): Selection mode ('none', 'single', 'multi'). Defaults to 'none'.
|
|
60
|
+
show_selection_controls (bool): Show checkbox/radio button. Defaults to False.
|
|
61
|
+
select_on_click (bool): Whether clicking selects the item. Defaults to True
|
|
62
|
+
when selection_mode is 'single' or 'multi', False otherwise.
|
|
63
|
+
density (str): Visual density ('default' or 'compact'). Defaults to 'default'.
|
|
64
|
+
**kwargs: Additional arguments forwarded to CompositeFrame.
|
|
65
|
+
"""
|
|
66
|
+
# state tracking
|
|
67
|
+
self._data = {}
|
|
68
|
+
self._state = {}
|
|
69
|
+
self._item_index = 0
|
|
70
|
+
self._selection_icon = None
|
|
71
|
+
self._drag_state = None
|
|
72
|
+
|
|
73
|
+
# configuration properties
|
|
74
|
+
self._focus_color = kwargs.pop('focus_color', None)
|
|
75
|
+
self._show_separator = kwargs.pop('show_separator', False)
|
|
76
|
+
self._show_chevron = kwargs.pop('show_chevron', False)
|
|
77
|
+
self._focusable = kwargs.pop('focusable', True)
|
|
78
|
+
self._hoverable = kwargs.pop('hoverable', False)
|
|
79
|
+
self._draggable = kwargs.pop('draggable', False)
|
|
80
|
+
self._removable = kwargs.pop('removable', False)
|
|
81
|
+
self._selected_background = kwargs.pop('selected_background', 'primary')
|
|
82
|
+
self._selection_mode = kwargs.pop('selection_mode', 'none')
|
|
83
|
+
self._show_selection_controls = kwargs.pop('show_selection_controls', False)
|
|
84
|
+
self._density = kwargs.pop('density', 'default')
|
|
85
|
+
|
|
86
|
+
# Determine if clicking should trigger selection
|
|
87
|
+
# If selection mode is active (single/multi), enable click selection
|
|
88
|
+
# If selection mode is 'none', respect the select_on_click parameter
|
|
89
|
+
select_on_click = kwargs.pop('select_on_click', self._selection_mode != 'none')
|
|
90
|
+
self._select_on_click = select_on_click
|
|
91
|
+
|
|
92
|
+
self._get_selection_icon()
|
|
93
|
+
|
|
94
|
+
# Adjust padding based on density
|
|
95
|
+
item_padding = (6, 3) if self._density == 'compact' else (8, 4)
|
|
96
|
+
|
|
97
|
+
# Initialize CompositeFrame with selection disabled (we handle it ourselves)
|
|
98
|
+
super().__init__(
|
|
99
|
+
master=master,
|
|
100
|
+
select_on_click=False,
|
|
101
|
+
variant='separated_item' if self._show_separator else 'item',
|
|
102
|
+
takefocus=self._focusable,
|
|
103
|
+
ttk_class='ListView.TFrame',
|
|
104
|
+
padding=item_padding,
|
|
105
|
+
style_options=dict(
|
|
106
|
+
selected_background=self._selected_background,
|
|
107
|
+
focus_color=self._focus_color,
|
|
108
|
+
hoverable=self._hoverable,
|
|
109
|
+
density=self._density
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# composite container widgets
|
|
114
|
+
self._left_frame = Frame(
|
|
115
|
+
self,
|
|
116
|
+
variant='list',
|
|
117
|
+
ttk_class='ListView.TFrame',
|
|
118
|
+
takefocus=False,
|
|
119
|
+
style_options=dict(selected_background=self._selected_background,
|
|
120
|
+
hoverable=self._hoverable,
|
|
121
|
+
density=self._density)
|
|
122
|
+
)
|
|
123
|
+
self._left_frame.pack(side='left')
|
|
124
|
+
|
|
125
|
+
self._center_frame = Frame(
|
|
126
|
+
self,
|
|
127
|
+
variant='list',
|
|
128
|
+
ttk_class='ListView.TFrame',
|
|
129
|
+
takefocus=False,
|
|
130
|
+
style_options=dict(selected_background=self._selected_background,
|
|
131
|
+
hoverable=self._hoverable,
|
|
132
|
+
density=self._density)
|
|
133
|
+
)
|
|
134
|
+
self._center_frame.pack(side='left', fill='x', expand=True)
|
|
135
|
+
|
|
136
|
+
self._right_frame = Frame(
|
|
137
|
+
self,
|
|
138
|
+
variant='list',
|
|
139
|
+
ttk_class='ListView.TFrame',
|
|
140
|
+
takefocus=False,
|
|
141
|
+
style_options=dict(selected_background=self._selected_background,
|
|
142
|
+
hoverable=self._hoverable,
|
|
143
|
+
density=self._density)
|
|
144
|
+
)
|
|
145
|
+
self._right_frame.pack(side='left')
|
|
146
|
+
|
|
147
|
+
# conditional widgets
|
|
148
|
+
self._separator = Separator(self)
|
|
149
|
+
self._selection_widget = None
|
|
150
|
+
self._icon_widget = None
|
|
151
|
+
self._title_widget = None
|
|
152
|
+
self._text_widget = None
|
|
153
|
+
self._caption_widget = None
|
|
154
|
+
self._remove_widget = None
|
|
155
|
+
self._badge_widget = None
|
|
156
|
+
self._chevron_widget = None
|
|
157
|
+
self._drag_widget = None
|
|
158
|
+
|
|
159
|
+
# Register container frames with composite coordinator
|
|
160
|
+
for widget in [self._left_frame, self._center_frame, self._right_frame]:
|
|
161
|
+
self.register_composite(widget)
|
|
162
|
+
|
|
163
|
+
# Bind to composite invoke for selection and/or focus handling
|
|
164
|
+
if self._select_on_click or self._focusable:
|
|
165
|
+
self.on_invoked(self._on_click)
|
|
166
|
+
|
|
167
|
+
# Focus event handling (notify ListView of focus changes)
|
|
168
|
+
if self._focusable:
|
|
169
|
+
self.bind('<FocusIn>', self._on_focus_in, add='+')
|
|
170
|
+
self.bind('<FocusOut>', self._on_focus_out, add='+')
|
|
171
|
+
|
|
172
|
+
# row-level keyboard events
|
|
173
|
+
self.bind('<space>', self._on_space, add='+')
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def selected(self):
|
|
177
|
+
"""bool: Current selection state from data."""
|
|
178
|
+
return self._data.get('selected')
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def data(self):
|
|
182
|
+
"""dict: The data record associated with this list item."""
|
|
183
|
+
return self._data
|
|
184
|
+
|
|
185
|
+
def _get_selection_icon(self):
|
|
186
|
+
"""Determine the selection icon based on selection mode."""
|
|
187
|
+
if self._selection_mode == 'multi':
|
|
188
|
+
self._selection_icon = dict(name='square', state=[('selected', "check-square-fill")])
|
|
189
|
+
elif self._selection_mode == 'single':
|
|
190
|
+
self._selection_icon = dict(name='circle', state=[('selected', "check-circle-fill")])
|
|
191
|
+
else:
|
|
192
|
+
self._selection_icon = None
|
|
193
|
+
|
|
194
|
+
def _on_click(self, event):
|
|
195
|
+
"""Handle click on the list item."""
|
|
196
|
+
if self._focusable:
|
|
197
|
+
self.focus()
|
|
198
|
+
# notify parent list that item has been clicked
|
|
199
|
+
self.master.event_generate('<<ItemClick>>', data=self._data)
|
|
200
|
+
self.select()
|
|
201
|
+
|
|
202
|
+
def _on_space(self, event):
|
|
203
|
+
"""Handle space key press."""
|
|
204
|
+
if self._focusable:
|
|
205
|
+
self.focus()
|
|
206
|
+
# notify parent list that item has been clicked
|
|
207
|
+
self.master.event_generate('<<ItemClick>>', data=self._data)
|
|
208
|
+
self.select()
|
|
209
|
+
|
|
210
|
+
def _on_focus_in(self, event):
|
|
211
|
+
"""Handle focus in event - notify ListView."""
|
|
212
|
+
self._data['focused'] = True
|
|
213
|
+
self.master.event_generate('<<ItemFocus>>', data=self._data)
|
|
214
|
+
|
|
215
|
+
def _on_focus_out(self, event):
|
|
216
|
+
"""Handle focus out event - check if focus moved to descendant."""
|
|
217
|
+
# Keep focus styling if focus moved to a descendant of this row
|
|
218
|
+
related = getattr(event, 'related', None)
|
|
219
|
+
try:
|
|
220
|
+
if related is not None and str(related).startswith(str(self)):
|
|
221
|
+
return 'break'
|
|
222
|
+
except TclError:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
self._data['focused'] = False
|
|
226
|
+
|
|
227
|
+
def _update_selection(self, selected: bool = False):
|
|
228
|
+
"""Apply selection state atomically (style + icon) with null guards."""
|
|
229
|
+
mode = self._selection_mode
|
|
230
|
+
|
|
231
|
+
if mode == 'none':
|
|
232
|
+
if self._selection_widget is not None:
|
|
233
|
+
try:
|
|
234
|
+
self._selection_widget.pack_forget()
|
|
235
|
+
except TclError:
|
|
236
|
+
pass
|
|
237
|
+
try:
|
|
238
|
+
self._selection_widget.destroy()
|
|
239
|
+
except TclError:
|
|
240
|
+
pass
|
|
241
|
+
|
|
242
|
+
self._selection_widget = None
|
|
243
|
+
# Use CompositeFrame's set_selected to clear selection
|
|
244
|
+
self.set_selected(False)
|
|
245
|
+
|
|
246
|
+
# Notify widgets
|
|
247
|
+
for w in self._composite._composites:
|
|
248
|
+
try:
|
|
249
|
+
w.event_generate('<<CompositeDeselect>>')
|
|
250
|
+
except TclError:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
# keep a remembered icon so later comparisons are cheap
|
|
254
|
+
if hasattr(self, '_state'):
|
|
255
|
+
self._state['__sel_icon'] = None
|
|
256
|
+
self._state['selected'] = False
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
# ensure state cache exists
|
|
260
|
+
if not hasattr(self, '_state'):
|
|
261
|
+
self._state = {}
|
|
262
|
+
|
|
263
|
+
# ensure the selection control exists even if not visible
|
|
264
|
+
if self._selection_widget is None:
|
|
265
|
+
self._selection_widget = Label(
|
|
266
|
+
self._left_frame,
|
|
267
|
+
icon=self._selection_icon,
|
|
268
|
+
variant='icon',
|
|
269
|
+
ttk_class='ListView.TLabel',
|
|
270
|
+
icon_only=True,
|
|
271
|
+
style_options=dict(selected_background=self._selected_background,
|
|
272
|
+
hoverable=self._hoverable,
|
|
273
|
+
density=self._density),
|
|
274
|
+
takefocus=False,
|
|
275
|
+
)
|
|
276
|
+
if self._show_selection_controls:
|
|
277
|
+
self._selection_widget.pack(side='left', padx=5)
|
|
278
|
+
self.register_composite(self._selection_widget)
|
|
279
|
+
|
|
280
|
+
# Use CompositeFrame's set_selected to apply state
|
|
281
|
+
self.set_selected(selected)
|
|
282
|
+
|
|
283
|
+
# Notify widgets
|
|
284
|
+
for w in self._composite._composites:
|
|
285
|
+
try:
|
|
286
|
+
w.event_generate('<<CompositeSelect>>' if selected else '<<CompositeDeselect>>')
|
|
287
|
+
except TclError:
|
|
288
|
+
pass
|
|
289
|
+
|
|
290
|
+
# remember logical selected flag
|
|
291
|
+
self._state['selected'] = bool(selected)
|
|
292
|
+
|
|
293
|
+
def _update_icon(self, icon=None):
|
|
294
|
+
"""Update icon widget, or create if not existing."""
|
|
295
|
+
if icon is not None:
|
|
296
|
+
if not self._icon_widget:
|
|
297
|
+
self._icon_widget = Label(
|
|
298
|
+
self._left_frame,
|
|
299
|
+
icon=icon,
|
|
300
|
+
variant='icon',
|
|
301
|
+
ttk_class='ListView.TLabel',
|
|
302
|
+
takefocus=False,
|
|
303
|
+
icon_only=True,
|
|
304
|
+
style_options=dict(selected_background=self._selected_background,
|
|
305
|
+
hoverable=self._hoverable,
|
|
306
|
+
density=self._density),
|
|
307
|
+
)
|
|
308
|
+
self._icon_widget.pack(side='left', padx=5)
|
|
309
|
+
self.register_composite(self._icon_widget)
|
|
310
|
+
else:
|
|
311
|
+
self._icon_widget.configure(icon=icon)
|
|
312
|
+
else:
|
|
313
|
+
if self._icon_widget:
|
|
314
|
+
try:
|
|
315
|
+
self._icon_widget.pack_forget()
|
|
316
|
+
self._icon_widget.destroy()
|
|
317
|
+
except TclError:
|
|
318
|
+
pass
|
|
319
|
+
finally:
|
|
320
|
+
self._icon_widget = None
|
|
321
|
+
|
|
322
|
+
def _update_title(self, text=None):
|
|
323
|
+
"""Update title widget."""
|
|
324
|
+
# Use heading-sm font for compact, heading-lg for default
|
|
325
|
+
title_font = 'heading-sm' if self._density == 'compact' else 'heading-lg'
|
|
326
|
+
if text is not None:
|
|
327
|
+
if not self._title_widget:
|
|
328
|
+
self._title_widget = Label(
|
|
329
|
+
self._center_frame,
|
|
330
|
+
text=text,
|
|
331
|
+
font=title_font,
|
|
332
|
+
variant='list',
|
|
333
|
+
ttk_class='ListView.TLabel',
|
|
334
|
+
takefocus=False,
|
|
335
|
+
style_options=dict(selected_background=self._selected_background,
|
|
336
|
+
hoverable=self._hoverable,
|
|
337
|
+
density=self._density),
|
|
338
|
+
)
|
|
339
|
+
self._title_widget.pack(side='top', fill='x', anchor='w', padx=(0, 3))
|
|
340
|
+
self.register_composite(self._title_widget)
|
|
341
|
+
else:
|
|
342
|
+
self._title_widget.configure(text=text)
|
|
343
|
+
else:
|
|
344
|
+
if self._title_widget:
|
|
345
|
+
try:
|
|
346
|
+
self._title_widget.pack_forget()
|
|
347
|
+
self._title_widget.destroy()
|
|
348
|
+
except TclError:
|
|
349
|
+
pass
|
|
350
|
+
finally:
|
|
351
|
+
self._title_widget = None
|
|
352
|
+
|
|
353
|
+
def _update_text(self, text=None):
|
|
354
|
+
"""Update text widget."""
|
|
355
|
+
# Use caption font for compact density
|
|
356
|
+
text_font = 'caption' if self._density == 'compact' else 'body'
|
|
357
|
+
if text is not None:
|
|
358
|
+
if not self._text_widget:
|
|
359
|
+
self._text_widget = Label(
|
|
360
|
+
self._center_frame,
|
|
361
|
+
text=text,
|
|
362
|
+
font=text_font,
|
|
363
|
+
variant='list',
|
|
364
|
+
ttk_class='ListView.TLabel',
|
|
365
|
+
takefocus=False,
|
|
366
|
+
style_options=dict(selected_background=self._selected_background,
|
|
367
|
+
hoverable=self._hoverable,
|
|
368
|
+
density=self._density),
|
|
369
|
+
)
|
|
370
|
+
self._text_widget.pack(side='top', fill='x', padx=(0, 3))
|
|
371
|
+
self.register_composite(self._text_widget)
|
|
372
|
+
else:
|
|
373
|
+
self._text_widget.configure(text=text)
|
|
374
|
+
else:
|
|
375
|
+
if self._text_widget:
|
|
376
|
+
try:
|
|
377
|
+
self._text_widget.pack_forget()
|
|
378
|
+
self._text_widget.destroy()
|
|
379
|
+
except TclError:
|
|
380
|
+
pass
|
|
381
|
+
finally:
|
|
382
|
+
self._text_widget = None
|
|
383
|
+
|
|
384
|
+
def _update_caption(self, text=None):
|
|
385
|
+
"""Update caption widget. Caption is only visible in default density."""
|
|
386
|
+
# Caption is hidden in compact mode
|
|
387
|
+
if self._density == 'compact':
|
|
388
|
+
if self._caption_widget:
|
|
389
|
+
try:
|
|
390
|
+
self._caption_widget.pack_forget()
|
|
391
|
+
self._caption_widget.destroy()
|
|
392
|
+
except TclError:
|
|
393
|
+
pass
|
|
394
|
+
finally:
|
|
395
|
+
self._caption_widget = None
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
if text is not None:
|
|
399
|
+
if not self._caption_widget:
|
|
400
|
+
self._caption_widget = Label(
|
|
401
|
+
self._center_frame,
|
|
402
|
+
text=text,
|
|
403
|
+
font='caption',
|
|
404
|
+
anchor='w',
|
|
405
|
+
variant='list',
|
|
406
|
+
ttk_class='ListView.TLabel',
|
|
407
|
+
takefocus=False,
|
|
408
|
+
style_options=dict(selected_background=self._selected_background,
|
|
409
|
+
hoverable=self._hoverable,
|
|
410
|
+
density=self._density),
|
|
411
|
+
)
|
|
412
|
+
self._caption_widget.pack(side='top', fill='x', padx=(0, 3))
|
|
413
|
+
self.register_composite(self._caption_widget)
|
|
414
|
+
else:
|
|
415
|
+
self._caption_widget.configure(text=text)
|
|
416
|
+
else:
|
|
417
|
+
if self._caption_widget:
|
|
418
|
+
try:
|
|
419
|
+
self._caption_widget.pack_forget()
|
|
420
|
+
self._caption_widget.destroy()
|
|
421
|
+
except TclError:
|
|
422
|
+
pass
|
|
423
|
+
finally:
|
|
424
|
+
self._caption_widget = None
|
|
425
|
+
|
|
426
|
+
def _update_badge(self, text=None):
|
|
427
|
+
"""Update badge widget."""
|
|
428
|
+
if text is not None:
|
|
429
|
+
if not self._badge_widget:
|
|
430
|
+
self._badge_widget = Label(
|
|
431
|
+
self._right_frame,
|
|
432
|
+
text=text,
|
|
433
|
+
variant='list',
|
|
434
|
+
ttk_class='ListView.TLabel',
|
|
435
|
+
style_options=dict(selected_background=self._selected_background,
|
|
436
|
+
hoverable=self._hoverable,
|
|
437
|
+
density=self._density),
|
|
438
|
+
)
|
|
439
|
+
self._badge_widget.pack(side='right', padx=6)
|
|
440
|
+
self.register_composite(self._badge_widget)
|
|
441
|
+
else:
|
|
442
|
+
self._badge_widget.configure(text=text)
|
|
443
|
+
else:
|
|
444
|
+
if self._badge_widget:
|
|
445
|
+
try:
|
|
446
|
+
self._badge_widget.pack_forget()
|
|
447
|
+
self._badge_widget.destroy()
|
|
448
|
+
except TclError:
|
|
449
|
+
pass
|
|
450
|
+
finally:
|
|
451
|
+
self._badge_widget = None
|
|
452
|
+
|
|
453
|
+
def _update_chevron(self):
|
|
454
|
+
"""Update or create chevron widget."""
|
|
455
|
+
if self._show_chevron:
|
|
456
|
+
if not self._chevron_widget:
|
|
457
|
+
self._chevron_widget = Button(
|
|
458
|
+
self._right_frame,
|
|
459
|
+
icon='chevron-right',
|
|
460
|
+
icon_only=True,
|
|
461
|
+
variant='icon',
|
|
462
|
+
ttk_class='ListView.TButton',
|
|
463
|
+
takefocus=False,
|
|
464
|
+
style_options=dict(selected_background=self._selected_background,
|
|
465
|
+
hoverable=self._hoverable,
|
|
466
|
+
density=self._density),
|
|
467
|
+
)
|
|
468
|
+
self._chevron_widget.pack(side='right', padx=6)
|
|
469
|
+
self.register_composite(self._chevron_widget)
|
|
470
|
+
else:
|
|
471
|
+
if self._chevron_widget:
|
|
472
|
+
try:
|
|
473
|
+
self._chevron_widget.pack_forget()
|
|
474
|
+
self._chevron_widget.destroy()
|
|
475
|
+
except TclError:
|
|
476
|
+
pass
|
|
477
|
+
finally:
|
|
478
|
+
self._chevron_widget = None
|
|
479
|
+
|
|
480
|
+
def _update_remove(self):
|
|
481
|
+
"""Update or create remove button widget."""
|
|
482
|
+
if self._removable:
|
|
483
|
+
if not self._remove_widget:
|
|
484
|
+
self._remove_widget = Button(
|
|
485
|
+
self._right_frame,
|
|
486
|
+
icon='x-lg',
|
|
487
|
+
icon_only=True,
|
|
488
|
+
variant='icon',
|
|
489
|
+
ttk_class='ListView.TButton',
|
|
490
|
+
takefocus=False,
|
|
491
|
+
command=self.remove,
|
|
492
|
+
style_options=dict(selected_background=self._selected_background,
|
|
493
|
+
hoverable=self._hoverable,
|
|
494
|
+
density=self._density),
|
|
495
|
+
)
|
|
496
|
+
self._remove_widget.pack(side='right', padx=6)
|
|
497
|
+
self.register_composite(self._remove_widget)
|
|
498
|
+
else:
|
|
499
|
+
if self._remove_widget:
|
|
500
|
+
try:
|
|
501
|
+
self._remove_widget.pack_forget()
|
|
502
|
+
self._remove_widget.destroy()
|
|
503
|
+
except TclError:
|
|
504
|
+
pass
|
|
505
|
+
finally:
|
|
506
|
+
self._remove_widget = None
|
|
507
|
+
|
|
508
|
+
def _update_drag(self):
|
|
509
|
+
"""Update or create drag handle widget."""
|
|
510
|
+
if self._draggable:
|
|
511
|
+
if not self._drag_widget:
|
|
512
|
+
self._drag_widget = Button(
|
|
513
|
+
self._right_frame,
|
|
514
|
+
icon='grip-vertical',
|
|
515
|
+
icon_only=True,
|
|
516
|
+
variant='icon',
|
|
517
|
+
ttk_class='ListView.TButton',
|
|
518
|
+
cursor='fleur',
|
|
519
|
+
takefocus=False,
|
|
520
|
+
style_options=dict(selected_background=self._selected_background,
|
|
521
|
+
hoverable=self._hoverable,
|
|
522
|
+
density=self._density),
|
|
523
|
+
)
|
|
524
|
+
self._drag_widget.pack(side='right', padx=6)
|
|
525
|
+
|
|
526
|
+
# Setup drag detection
|
|
527
|
+
self._drag_state = {'dragging': False, 'start_y': None}
|
|
528
|
+
self._drag_widget.bind('<ButtonPress-1>', self._on_drag_mouse_down, add='+')
|
|
529
|
+
self._drag_widget.bind('<B1-Motion>', self._on_drag_mouse_motion, add='+')
|
|
530
|
+
self._drag_widget.bind('<ButtonRelease-1>', self._on_drag_mouse_up, add='+')
|
|
531
|
+
self.register_composite(self._drag_widget)
|
|
532
|
+
else:
|
|
533
|
+
if self._drag_widget:
|
|
534
|
+
try:
|
|
535
|
+
self._drag_widget.pack_forget()
|
|
536
|
+
self._drag_widget.destroy()
|
|
537
|
+
except TclError:
|
|
538
|
+
pass
|
|
539
|
+
finally:
|
|
540
|
+
self._drag_widget = None
|
|
541
|
+
self._drag_state = None
|
|
542
|
+
|
|
543
|
+
def set_surface(self, surface: str) -> None:
|
|
544
|
+
"""Set the surface color for the row and its container frames.
|
|
545
|
+
|
|
546
|
+
This method is used by ListView to apply alternating row colors efficiently.
|
|
547
|
+
The surface color is set once when the widget is created and remains stable
|
|
548
|
+
during scrolling.
|
|
549
|
+
|
|
550
|
+
Args:
|
|
551
|
+
surface: Surface color name or value (e.g., 'background', 'background[+1]').
|
|
552
|
+
"""
|
|
553
|
+
previous = getattr(self, "_surface", "background")
|
|
554
|
+
self.configure_style_options(surface=surface)
|
|
555
|
+
if previous != surface:
|
|
556
|
+
self.rebuild_style()
|
|
557
|
+
|
|
558
|
+
for frame in (self._left_frame, self._center_frame, self._right_frame):
|
|
559
|
+
try:
|
|
560
|
+
frame.configure_style_options(surface=surface)
|
|
561
|
+
frame.rebuild_style()
|
|
562
|
+
except Exception:
|
|
563
|
+
continue
|
|
564
|
+
|
|
565
|
+
# ---- Event Handlers (Drag-related) ----
|
|
566
|
+
|
|
567
|
+
def _on_drag_mouse_down(self, event):
|
|
568
|
+
"""Mouse pressed on drag handle - prepare for drag."""
|
|
569
|
+
if not hasattr(self, '_drag_state'):
|
|
570
|
+
self._drag_state = {}
|
|
571
|
+
self._drag_state['start_y'] = event.y_root
|
|
572
|
+
self._drag_state['dragging'] = False
|
|
573
|
+
|
|
574
|
+
def _on_drag_mouse_motion(self, event):
|
|
575
|
+
"""Mouse moving with button held - this is a drag."""
|
|
576
|
+
if not hasattr(self, '_drag_state') or self._drag_state.get('start_y') is None:
|
|
577
|
+
return
|
|
578
|
+
|
|
579
|
+
# if this is the first motion event, emit drag start
|
|
580
|
+
if not self._drag_state.get('dragging'):
|
|
581
|
+
self._drag_state['dragging'] = True
|
|
582
|
+
self.master.event_generate(
|
|
583
|
+
'<<ItemDragStart>>',
|
|
584
|
+
data={**self._data, 'source_index': self._item_index, 'y_start': self._drag_state['start_y']}
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
# emit drag motion event
|
|
588
|
+
self.master.event_generate(
|
|
589
|
+
'<<ItemDrag>>',
|
|
590
|
+
data={
|
|
591
|
+
**self._data,
|
|
592
|
+
'source_index': self._item_index,
|
|
593
|
+
'y_current': event.y_root,
|
|
594
|
+
'y_start': self._drag_state['start_y'],
|
|
595
|
+
'delta_y': event.y_root - self._drag_state['start_y']
|
|
596
|
+
}
|
|
597
|
+
)
|
|
598
|
+
|
|
599
|
+
def _on_drag_mouse_up(self, event):
|
|
600
|
+
"""Mouse release - end drag if we were dragging."""
|
|
601
|
+
if not hasattr(self, '_drag_state'):
|
|
602
|
+
return
|
|
603
|
+
|
|
604
|
+
# only emit drag end if we actually started dragging
|
|
605
|
+
if self._drag_state.get('dragging'):
|
|
606
|
+
self.master.event_generate('<<ItemDragEnd>>',
|
|
607
|
+
data={
|
|
608
|
+
**self._data,
|
|
609
|
+
'source_index': self._item_index,
|
|
610
|
+
'y_end': event.y_root,
|
|
611
|
+
'y_start': self._drag_state.get('start_y')
|
|
612
|
+
})
|
|
613
|
+
# reset drag state
|
|
614
|
+
self._drag_state = {'dragging': False, 'start_y': None}
|
|
615
|
+
|
|
616
|
+
# --- Configuration delegates ---
|
|
617
|
+
|
|
618
|
+
@configure_delegate('selection_mode')
|
|
619
|
+
def _delegate_selection_mode(self, value=None):
|
|
620
|
+
self._selection_mode = value
|
|
621
|
+
self._get_selection_icon()
|
|
622
|
+
|
|
623
|
+
@configure_delegate('show_selection_controls')
|
|
624
|
+
def _delegate_show_selection_controls(self, value=None):
|
|
625
|
+
self._show_selection_controls = value
|
|
626
|
+
self._get_selection_icon()
|
|
627
|
+
self._update_selection(False)
|
|
628
|
+
|
|
629
|
+
@configure_delegate('selected_background')
|
|
630
|
+
def _delegate_selected_background(self, value=None):
|
|
631
|
+
self._selected_background = value
|
|
632
|
+
|
|
633
|
+
@configure_delegate('show_chevron')
|
|
634
|
+
def _delegate_show_chevron(self, value=None):
|
|
635
|
+
self._show_chevron = value
|
|
636
|
+
self._update_chevron()
|
|
637
|
+
|
|
638
|
+
@configure_delegate('draggable')
|
|
639
|
+
def _delegate_draggable(self, value=None):
|
|
640
|
+
self._draggable = value
|
|
641
|
+
self._update_drag()
|
|
642
|
+
|
|
643
|
+
@configure_delegate('removable')
|
|
644
|
+
def _delegate_removable(self, value=None):
|
|
645
|
+
self._removable = value
|
|
646
|
+
self._update_remove()
|
|
647
|
+
|
|
648
|
+
# ---- Public API ----
|
|
649
|
+
|
|
650
|
+
def select(self):
|
|
651
|
+
"""Toggle or set the selection state based on selection mode.
|
|
652
|
+
|
|
653
|
+
Returns:
|
|
654
|
+
bool or None: True if selected, False if deselected, None if no action.
|
|
655
|
+
"""
|
|
656
|
+
mode = self._selection_mode
|
|
657
|
+
if mode == 'none':
|
|
658
|
+
return None
|
|
659
|
+
|
|
660
|
+
is_selected = bool(self.selected or False)
|
|
661
|
+
if is_selected:
|
|
662
|
+
if mode == 'single':
|
|
663
|
+
return None
|
|
664
|
+
self._data['selected'] = False
|
|
665
|
+
self.master.event_generate('<<ItemSelecting>>', data=self._data)
|
|
666
|
+
return False
|
|
667
|
+
|
|
668
|
+
self._data['selected'] = True
|
|
669
|
+
self.master.event_generate('<<ItemSelecting>>', data=self._data)
|
|
670
|
+
return True
|
|
671
|
+
|
|
672
|
+
def remove(self):
|
|
673
|
+
"""Notify subscribers to handle remove action."""
|
|
674
|
+
self.master.event_generate('<<ItemRemoving>>', data=self._data)
|
|
675
|
+
|
|
676
|
+
def update_data(self, record: dict | None):
|
|
677
|
+
"""Update row visuals efficiently when values have changed.
|
|
678
|
+
|
|
679
|
+
Args:
|
|
680
|
+
record: Dictionary containing the item data. If None or contains
|
|
681
|
+
'__empty__' key, the item will be hidden.
|
|
682
|
+
"""
|
|
683
|
+
if record is None or '__empty__' in record:
|
|
684
|
+
self.pack_forget()
|
|
685
|
+
return
|
|
686
|
+
|
|
687
|
+
if not self.winfo_manager():
|
|
688
|
+
self.pack(fill='x')
|
|
689
|
+
|
|
690
|
+
self._data = record
|
|
691
|
+
self._item_index = self._data.get('item_index', 0)
|
|
692
|
+
|
|
693
|
+
selected = bool(record.get('selected', False))
|
|
694
|
+
if self._state.get('selected') != selected:
|
|
695
|
+
self._update_selection(selected)
|
|
696
|
+
self._state['selected'] = selected
|
|
697
|
+
|
|
698
|
+
# handle focus - apply tkinter focus to the widget that should have logical focus
|
|
699
|
+
focused = bool(record.get('focused', False))
|
|
700
|
+
if self._state.get('focused') != focused and self._focusable:
|
|
701
|
+
if focused:
|
|
702
|
+
# this record should have focus - give tkinter focus to this widget
|
|
703
|
+
try:
|
|
704
|
+
self.focus_set()
|
|
705
|
+
self._data['focused'] = True
|
|
706
|
+
except TclError:
|
|
707
|
+
pass
|
|
708
|
+
else:
|
|
709
|
+
# This record lost focus - clear tkinter focus if we have it
|
|
710
|
+
try:
|
|
711
|
+
if self.focus_get() == self:
|
|
712
|
+
# Move focus to parent container
|
|
713
|
+
self.master.focus_set()
|
|
714
|
+
except (TclError, AttributeError):
|
|
715
|
+
pass
|
|
716
|
+
self._data['focused'] = False
|
|
717
|
+
self._state['focused'] = focused
|
|
718
|
+
|
|
719
|
+
# direct update for high-priority visuals
|
|
720
|
+
for field, updater in {
|
|
721
|
+
"title": self._update_title,
|
|
722
|
+
"text": self._update_text,
|
|
723
|
+
"caption": self._update_caption,
|
|
724
|
+
"icon": self._update_icon,
|
|
725
|
+
}.items():
|
|
726
|
+
value = record.get(field)
|
|
727
|
+
if self._state.get(field) != value:
|
|
728
|
+
updater(value)
|
|
729
|
+
self._state[field] = value
|
|
730
|
+
|
|
731
|
+
self.after_idle(self._update_chevron)
|
|
732
|
+
self.after_idle(self._update_drag)
|
|
733
|
+
self.after_idle(self._update_remove)
|