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,450 @@
|
|
|
1
|
+
"""In-memory data source implementation with filtering, sorting, and pagination.
|
|
2
|
+
|
|
3
|
+
Provides a pure-Python in-memory data manager that supports:
|
|
4
|
+
- Pagination with configurable page size
|
|
5
|
+
- Filtering with `col`-based conditions
|
|
6
|
+
- Multi-column sorting
|
|
7
|
+
- Full CRUD operations (insert, get, update, delete)
|
|
8
|
+
- Record selection tracking
|
|
9
|
+
- CSV export
|
|
10
|
+
- Inferred schema from data
|
|
11
|
+
|
|
12
|
+
The MemoryDataSource stores all data in memory and provides fast access for
|
|
13
|
+
small to medium datasets. For larger datasets or persistence requirements,
|
|
14
|
+
consider using SqliteDataSource instead.
|
|
15
|
+
|
|
16
|
+
Filtering and sorting:
|
|
17
|
+
Conditions are built with `col` (see `bootstack.data.query`) and evaluated
|
|
18
|
+
in pure Python:
|
|
19
|
+
|
|
20
|
+
.. code-block:: python
|
|
21
|
+
|
|
22
|
+
from bootstack.data import col
|
|
23
|
+
|
|
24
|
+
ds.where((col("status") == "active") & (col("age") >= 18))
|
|
25
|
+
ds.where(col("name").startswith("John"))
|
|
26
|
+
ds.order("last_name", "-age")
|
|
27
|
+
|
|
28
|
+
Data Format:
|
|
29
|
+
- Records must be dictionaries or will be auto-wrapped as {"text": str(value)}
|
|
30
|
+
- Each record automatically gets an 'id' field (integer, auto-generated if missing)
|
|
31
|
+
- Each record automatically gets a 'selected' field (0 or 1)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
from collections.abc import Sequence
|
|
37
|
+
from typing import Any, Dict, List, Optional, Union, Mapping, Iterable
|
|
38
|
+
|
|
39
|
+
from bootstack.data.base import BaseDataSource
|
|
40
|
+
from bootstack.data.query import Condition, SortKey, normalize_sort_keys
|
|
41
|
+
from bootstack.data.types import Primitive
|
|
42
|
+
from bootstack.errors import DuplicateIdError
|
|
43
|
+
from bootstack.events import DataChangeEvent
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MemoryDataSource(BaseDataSource):
|
|
47
|
+
"""In-memory data manager with pagination, filtering, sorting, and CRUD operations.
|
|
48
|
+
|
|
49
|
+
Stores all records in memory as dictionaries with automatic ID generation and
|
|
50
|
+
selection tracking. Provides SQL-like filtering and sorting syntax for intuitive
|
|
51
|
+
data manipulation.
|
|
52
|
+
|
|
53
|
+
The datasource maintains an internal index for O(1) ID lookups and supports
|
|
54
|
+
dynamic schema inference from provided data.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
page_size: Number of records per page (default: 10)
|
|
58
|
+
|
|
59
|
+
Attributes:
|
|
60
|
+
page_size: Current page size setting
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
.. code-block:: python
|
|
64
|
+
|
|
65
|
+
ds = MemoryDataSource(page_size=20)
|
|
66
|
+
ds.load([
|
|
67
|
+
{"name": "Alice", "age": 30},
|
|
68
|
+
{"name": "Bob", "age": 25},
|
|
69
|
+
])
|
|
70
|
+
ds.where(col("age") >= 30)
|
|
71
|
+
first = ds.page(0)
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(self, page_size: int = 10, id_field: str = "id"):
|
|
76
|
+
"""Initialize the in-memory datasource with defaults.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
page_size: Number of records returned per page when paginating.
|
|
80
|
+
id_field: Record field used as the stable row identity. A record's own
|
|
81
|
+
value for this field is kept as its id; otherwise one is assigned.
|
|
82
|
+
"""
|
|
83
|
+
super().__init__(page_size)
|
|
84
|
+
self._table = "records"
|
|
85
|
+
self._id_field = id_field
|
|
86
|
+
self._columns: List[str] = []
|
|
87
|
+
self._data: List[Dict[str, Any]] = []
|
|
88
|
+
self._id_index: Dict[Any, int] = {}
|
|
89
|
+
self._filter: Optional[Condition] = None
|
|
90
|
+
self._sort: List[SortKey] = []
|
|
91
|
+
# Cached filter+sort view, rebuilt lazily and busted on any structural
|
|
92
|
+
# change (see _emit). Avoids re-filtering/re-sorting all rows on every
|
|
93
|
+
# count/page_slice — the hot path while a recycling widget scrolls.
|
|
94
|
+
self._view: Optional[List[Dict[str, Any]]] = None
|
|
95
|
+
|
|
96
|
+
def _emit(self, event: "DataChangeEvent") -> None:
|
|
97
|
+
"""Emit a change event, busting the cached view for structural changes.
|
|
98
|
+
|
|
99
|
+
Selection toggles a row's flag without changing which rows match or their
|
|
100
|
+
order, so they keep the cached view (and the scroll path stays fast).
|
|
101
|
+
"""
|
|
102
|
+
if event.kind != "select":
|
|
103
|
+
self._view = None
|
|
104
|
+
self._hub.emit(event)
|
|
105
|
+
|
|
106
|
+
def _rebuild_id_index(self) -> None:
|
|
107
|
+
"""Rebuild the ID-to-position index for fast lookups."""
|
|
108
|
+
self._id_index.clear()
|
|
109
|
+
for i, rec in enumerate(self._data):
|
|
110
|
+
self._id_index[rec.get(self._id_field)] = i
|
|
111
|
+
|
|
112
|
+
def _ensure_selected_column(self) -> None:
|
|
113
|
+
"""Ensure all records have a 'selected' field."""
|
|
114
|
+
if "selected" not in self._columns:
|
|
115
|
+
self._columns.append("selected")
|
|
116
|
+
for r in self._data:
|
|
117
|
+
r.setdefault("selected", 0)
|
|
118
|
+
|
|
119
|
+
def _ensure_id(self) -> None:
|
|
120
|
+
"""Ensure all records have unique integer IDs.
|
|
121
|
+
|
|
122
|
+
Records whose `id` is already an int and not a duplicate are
|
|
123
|
+
preserved as-is. Records with missing, non-integer, or duplicate
|
|
124
|
+
IDs get a freshly-allocated integer ID.
|
|
125
|
+
"""
|
|
126
|
+
used: set[int] = set()
|
|
127
|
+
needs_id: list[Dict[str, Any]] = []
|
|
128
|
+
max_id = 0
|
|
129
|
+
for r in self._data:
|
|
130
|
+
rid = r.get(self._id_field)
|
|
131
|
+
if isinstance(rid, int) and rid not in used:
|
|
132
|
+
used.add(rid)
|
|
133
|
+
if rid > max_id:
|
|
134
|
+
max_id = rid
|
|
135
|
+
else:
|
|
136
|
+
needs_id.append(r)
|
|
137
|
+
for r in needs_id:
|
|
138
|
+
max_id += 1
|
|
139
|
+
while max_id in used:
|
|
140
|
+
max_id += 1
|
|
141
|
+
r[self._id_field] = max_id
|
|
142
|
+
used.add(max_id)
|
|
143
|
+
self._rebuild_id_index()
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _sort_rows(rows: List[Dict[str, Any]], sort_keys: List[SortKey]) -> List[Dict[str, Any]]:
|
|
147
|
+
"""Stable multi-key sort in place — first key wins (applied right-to-left)."""
|
|
148
|
+
for key in reversed(sort_keys):
|
|
149
|
+
rows.sort(
|
|
150
|
+
key=lambda r, c=key.column: (r.get(c) is None, r.get(c)),
|
|
151
|
+
reverse=key.descending,
|
|
152
|
+
)
|
|
153
|
+
return rows
|
|
154
|
+
|
|
155
|
+
def _apply_filter_and_sort(self, rows: Iterable[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
156
|
+
"""Apply the current filter and sort to a row collection."""
|
|
157
|
+
if self._filter is not None:
|
|
158
|
+
rows = [r for r in rows if self._filter.matches(r)]
|
|
159
|
+
else:
|
|
160
|
+
rows = list(rows)
|
|
161
|
+
return self._sort_rows(rows, self._sort)
|
|
162
|
+
|
|
163
|
+
def _query(self, condition: Optional[Condition], sort_keys: List[SortKey]) -> List[Dict[str, Any]]:
|
|
164
|
+
"""Run a one-off filtered/sorted read without touching view state."""
|
|
165
|
+
rows = [r for r in self._data if condition is None or condition.matches(r)]
|
|
166
|
+
return [dict(r) for r in self._sort_rows(rows, sort_keys)]
|
|
167
|
+
|
|
168
|
+
def load(
|
|
169
|
+
self, records: "Iterable[Primitive] | Iterable[Dict[str, Any]]"
|
|
170
|
+
) -> "MemoryDataSource":
|
|
171
|
+
"""Load records into datasource.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
records: An iterable of dicts or primitives (auto-wrapped as
|
|
175
|
+
{"text": str(x)}) — a list, a generator, or any other iterable.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
Self for method chaining
|
|
179
|
+
"""
|
|
180
|
+
# Materialize any iterable (e.g. a streaming reader generator) — an
|
|
181
|
+
# in-memory source holds the whole set anyway, and this keeps load()
|
|
182
|
+
# accepting the same inputs as SqliteDataSource.load().
|
|
183
|
+
records = list(records)
|
|
184
|
+
if not records:
|
|
185
|
+
self._data = []
|
|
186
|
+
self._columns = []
|
|
187
|
+
self._rebuild_id_index()
|
|
188
|
+
self._emit(DataChangeEvent(kind="load"))
|
|
189
|
+
return self
|
|
190
|
+
|
|
191
|
+
if not self._is_mapping(records[0]):
|
|
192
|
+
records = [dict(text=str(x)) for x in records]
|
|
193
|
+
|
|
194
|
+
data: List[Dict[str, Any]] = []
|
|
195
|
+
seen_ids: set = set()
|
|
196
|
+
for i, rec in enumerate(records):
|
|
197
|
+
r = dict(rec)
|
|
198
|
+
r.setdefault(self._id_field, i)
|
|
199
|
+
rid = r[self._id_field]
|
|
200
|
+
if rid in seen_ids:
|
|
201
|
+
raise DuplicateIdError(
|
|
202
|
+
f"Duplicate value {rid!r} in id field {self._id_field!r} — record "
|
|
203
|
+
f"ids must be unique."
|
|
204
|
+
)
|
|
205
|
+
seen_ids.add(rid)
|
|
206
|
+
r.setdefault("selected", 0)
|
|
207
|
+
data.append(r)
|
|
208
|
+
|
|
209
|
+
self._data = data
|
|
210
|
+
self._columns = list(self._data[0].keys())
|
|
211
|
+
self._ensure_id()
|
|
212
|
+
self._ensure_selected_column()
|
|
213
|
+
self._emit(DataChangeEvent(kind="load"))
|
|
214
|
+
return self
|
|
215
|
+
|
|
216
|
+
def where(self, condition: Optional[Condition] = None) -> "MemoryDataSource":
|
|
217
|
+
"""Filter rows by a condition built with `col` (None clears the filter)."""
|
|
218
|
+
self._filter = condition
|
|
219
|
+
self._emit(DataChangeEvent(kind="filter"))
|
|
220
|
+
return self
|
|
221
|
+
|
|
222
|
+
def order(self, *keys) -> "MemoryDataSource":
|
|
223
|
+
"""Sort rows by one or more keys (no arguments clears sorting)."""
|
|
224
|
+
self._sort = normalize_sort_keys(keys)
|
|
225
|
+
self._emit(DataChangeEvent(kind="sort"))
|
|
226
|
+
return self
|
|
227
|
+
|
|
228
|
+
def _filtered_sorted_rows(self) -> List[Dict[str, Any]]:
|
|
229
|
+
"""Get all rows with current filter and sort applied (cached).
|
|
230
|
+
|
|
231
|
+
The result is memoized in `self._view` and reused until a structural
|
|
232
|
+
change (load/filter/sort/insert/update/delete/move) busts it via `_emit`.
|
|
233
|
+
"""
|
|
234
|
+
if self._view is None:
|
|
235
|
+
self._view = self._apply_filter_and_sort(self._data)
|
|
236
|
+
return self._view
|
|
237
|
+
|
|
238
|
+
def page(self, page: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
239
|
+
"""Get records for specified page."""
|
|
240
|
+
if page is not None:
|
|
241
|
+
self._page = max(0, int(page))
|
|
242
|
+
rows = self._filtered_sorted_rows()
|
|
243
|
+
start = self._page * self.page_size
|
|
244
|
+
end = start + self.page_size
|
|
245
|
+
return [dict(r) for r in rows[start:end]]
|
|
246
|
+
|
|
247
|
+
def next_page(self) -> List[Dict[str, Any]]:
|
|
248
|
+
"""Advance to next page and return its records."""
|
|
249
|
+
if self.has_next_page():
|
|
250
|
+
self._page += 1
|
|
251
|
+
return self.page()
|
|
252
|
+
|
|
253
|
+
def prev_page(self) -> List[Dict[str, Any]]:
|
|
254
|
+
"""Move to previous page and return its records."""
|
|
255
|
+
self._page = max(0, self._page - 1)
|
|
256
|
+
return self.page()
|
|
257
|
+
|
|
258
|
+
def has_next_page(self) -> bool:
|
|
259
|
+
"""Check if more pages exist after current page."""
|
|
260
|
+
return (self._page + 1) * self.page_size < self.count
|
|
261
|
+
|
|
262
|
+
@property
|
|
263
|
+
def count(self) -> int:
|
|
264
|
+
"""Total number of records matching the current filter."""
|
|
265
|
+
return len(self._filtered_sorted_rows())
|
|
266
|
+
|
|
267
|
+
def _generate_new_id(self) -> int:
|
|
268
|
+
"""Generate next available integer ID."""
|
|
269
|
+
if not self._data:
|
|
270
|
+
return 1
|
|
271
|
+
try:
|
|
272
|
+
return max(int(r.get(self._id_field, 0)) for r in self._data) + 1
|
|
273
|
+
except (TypeError, ValueError):
|
|
274
|
+
raise DuplicateIdError(
|
|
275
|
+
f"Cannot auto-assign an id: existing ids in field {self._id_field!r} are "
|
|
276
|
+
f"non-integer. Provide an explicit {self._id_field!r} on every inserted "
|
|
277
|
+
f"record when using custom ids."
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
def insert(self, record: Dict[str, Any]) -> int:
|
|
281
|
+
"""Create new record and return its ID."""
|
|
282
|
+
r = dict(record)
|
|
283
|
+
if self._id_field not in r:
|
|
284
|
+
r[self._id_field] = self._generate_new_id()
|
|
285
|
+
elif r[self._id_field] in self._id_index:
|
|
286
|
+
raise DuplicateIdError(
|
|
287
|
+
f"A record with id {r[self._id_field]!r} already exists — record ids "
|
|
288
|
+
f"must be unique."
|
|
289
|
+
)
|
|
290
|
+
if "selected" not in r:
|
|
291
|
+
r["selected"] = 0
|
|
292
|
+
self._data.append(r)
|
|
293
|
+
self._columns = list(set(self._columns) | set(r.keys()))
|
|
294
|
+
self._id_index[r[self._id_field]] = len(self._data) - 1
|
|
295
|
+
self._emit(DataChangeEvent(kind="insert", id=r[self._id_field], record=dict(r)))
|
|
296
|
+
return r[self._id_field]
|
|
297
|
+
|
|
298
|
+
def get(self, record_id: Any) -> Optional[Dict[str, Any]]:
|
|
299
|
+
"""Retrieve single record by ID."""
|
|
300
|
+
idx = self._id_index.get(record_id)
|
|
301
|
+
if idx is None:
|
|
302
|
+
return None
|
|
303
|
+
return dict(self._data[idx])
|
|
304
|
+
|
|
305
|
+
def update(self, record_id: Any, updates: Dict[str, Any]) -> bool:
|
|
306
|
+
"""Update record fields by ID."""
|
|
307
|
+
if not updates:
|
|
308
|
+
return False
|
|
309
|
+
idx = self._id_index.get(record_id)
|
|
310
|
+
if idx is None:
|
|
311
|
+
return False
|
|
312
|
+
self._data[idx].update(updates)
|
|
313
|
+
self._columns = list(set(self._columns) | set(updates.keys()))
|
|
314
|
+
self._emit(DataChangeEvent(kind="update", id=record_id, record=dict(updates)))
|
|
315
|
+
return True
|
|
316
|
+
|
|
317
|
+
def delete(self, record_id: Any) -> bool:
|
|
318
|
+
"""Delete record by ID."""
|
|
319
|
+
idx = self._id_index.get(record_id)
|
|
320
|
+
if idx is None:
|
|
321
|
+
return False
|
|
322
|
+
self._data.pop(idx)
|
|
323
|
+
self._rebuild_id_index()
|
|
324
|
+
self._emit(DataChangeEvent(kind="delete", id=record_id))
|
|
325
|
+
return True
|
|
326
|
+
|
|
327
|
+
def _internal_fields(self) -> "frozenset[str]":
|
|
328
|
+
"""Hide the per-record selection flag from users."""
|
|
329
|
+
return frozenset({"selected"})
|
|
330
|
+
|
|
331
|
+
def is_selected(self, record_id: Any) -> bool:
|
|
332
|
+
"""Check whether a record is currently selected."""
|
|
333
|
+
idx = self._id_index.get(record_id)
|
|
334
|
+
if idx is None:
|
|
335
|
+
return False
|
|
336
|
+
return bool(self._data[idx].get("selected", 0))
|
|
337
|
+
|
|
338
|
+
def select(self, record_id: Any) -> bool:
|
|
339
|
+
"""Mark record as selected."""
|
|
340
|
+
changed = self._set_selected_flag(record_id, 1)
|
|
341
|
+
if changed:
|
|
342
|
+
self._emit(DataChangeEvent(kind="select", id=record_id))
|
|
343
|
+
return changed
|
|
344
|
+
|
|
345
|
+
def deselect(self, record_id: Any) -> bool:
|
|
346
|
+
"""Mark record as unselected."""
|
|
347
|
+
changed = self._set_selected_flag(record_id, 0)
|
|
348
|
+
if changed:
|
|
349
|
+
self._emit(DataChangeEvent(kind="select", id=record_id))
|
|
350
|
+
return changed
|
|
351
|
+
|
|
352
|
+
def select_all(self, current_page_only: bool = False) -> int:
|
|
353
|
+
"""Select all records (optionally only current page)."""
|
|
354
|
+
self._ensure_selected_column()
|
|
355
|
+
if current_page_only:
|
|
356
|
+
ids = [r[self._id_field] for r in self.page()]
|
|
357
|
+
count = 0
|
|
358
|
+
idset = set(ids)
|
|
359
|
+
for r in self._data:
|
|
360
|
+
if r[self._id_field] in idset and r.get("selected") != 1:
|
|
361
|
+
r["selected"] = 1
|
|
362
|
+
count += 1
|
|
363
|
+
else:
|
|
364
|
+
count = 0
|
|
365
|
+
for r in self._data:
|
|
366
|
+
if r.get("selected") != 1:
|
|
367
|
+
r["selected"] = 1
|
|
368
|
+
count += 1
|
|
369
|
+
if count:
|
|
370
|
+
self._emit(DataChangeEvent(kind="select"))
|
|
371
|
+
return count
|
|
372
|
+
|
|
373
|
+
def deselect_all(self, current_page_only: bool = False) -> int:
|
|
374
|
+
"""Deselect all records (optionally only current page)."""
|
|
375
|
+
self._ensure_selected_column()
|
|
376
|
+
if current_page_only:
|
|
377
|
+
ids = [r[self._id_field] for r in self.page()]
|
|
378
|
+
count = 0
|
|
379
|
+
idset = set(ids)
|
|
380
|
+
for r in self._data:
|
|
381
|
+
if r[self._id_field] in idset and r.get("selected") != 0:
|
|
382
|
+
r["selected"] = 0
|
|
383
|
+
count += 1
|
|
384
|
+
else:
|
|
385
|
+
count = 0
|
|
386
|
+
for r in self._data:
|
|
387
|
+
if r.get("selected") != 0:
|
|
388
|
+
r["selected"] = 0
|
|
389
|
+
count += 1
|
|
390
|
+
if count:
|
|
391
|
+
self._emit(DataChangeEvent(kind="select"))
|
|
392
|
+
return count
|
|
393
|
+
|
|
394
|
+
def move(self, record_id: Any, target_index: int) -> bool:
|
|
395
|
+
"""Reorder a record within the in-memory list."""
|
|
396
|
+
if not self._data:
|
|
397
|
+
return False
|
|
398
|
+
|
|
399
|
+
source_index = self._id_index.get(record_id)
|
|
400
|
+
if source_index is None:
|
|
401
|
+
return False
|
|
402
|
+
|
|
403
|
+
clamped_target = max(0, min(int(target_index), len(self._data) - 1))
|
|
404
|
+
if source_index == clamped_target:
|
|
405
|
+
return False
|
|
406
|
+
|
|
407
|
+
record = self._data.pop(source_index)
|
|
408
|
+
if clamped_target > source_index:
|
|
409
|
+
clamped_target -= 1
|
|
410
|
+
self._data.insert(clamped_target, record)
|
|
411
|
+
|
|
412
|
+
start = min(source_index, clamped_target)
|
|
413
|
+
end = max(source_index, clamped_target) + 1
|
|
414
|
+
for i in range(start, end):
|
|
415
|
+
self._id_index[self._data[i][self._id_field]] = i
|
|
416
|
+
self._emit(DataChangeEvent(kind="move", id=record_id))
|
|
417
|
+
return True
|
|
418
|
+
|
|
419
|
+
def _set_selected_flag(self, record_id: Any, flag: int) -> bool:
|
|
420
|
+
"""Set selection flag for record by ID."""
|
|
421
|
+
self._ensure_selected_column()
|
|
422
|
+
idx = self._id_index.get(record_id)
|
|
423
|
+
if idx is None:
|
|
424
|
+
return False
|
|
425
|
+
self._data[idx]["selected"] = 1 if flag else 0
|
|
426
|
+
return True
|
|
427
|
+
|
|
428
|
+
def selected(self, page: Optional[int] = None) -> List[Dict[str, Any]]:
|
|
429
|
+
"""Get selected records, optionally paginated."""
|
|
430
|
+
self._ensure_selected_column()
|
|
431
|
+
rows = [r for r in self._data if r.get("selected") == 1]
|
|
432
|
+
rows = self._apply_filter_and_sort(rows)
|
|
433
|
+
if page is None:
|
|
434
|
+
return [dict(r) for r in rows]
|
|
435
|
+
start = max(0, int(page)) * self.page_size
|
|
436
|
+
end = start + self.page_size
|
|
437
|
+
return [dict(r) for r in rows[start:end]]
|
|
438
|
+
|
|
439
|
+
@property
|
|
440
|
+
def selected_count(self) -> int:
|
|
441
|
+
"""Number of selected records."""
|
|
442
|
+
self._ensure_selected_column()
|
|
443
|
+
return sum(1 for r in self._data if r.get("selected") == 1)
|
|
444
|
+
|
|
445
|
+
def page_slice(self, start_index: int, count: int) -> List[Dict[str, Any]]:
|
|
446
|
+
"""Get records by start index and count (respects filter/sort)."""
|
|
447
|
+
rows = self._filtered_sorted_rows()
|
|
448
|
+
start = max(0, int(start_index))
|
|
449
|
+
end = start + max(0, int(count))
|
|
450
|
+
return [dict(r) for r in rows[start:end]]
|