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.
Files changed (471) hide show
  1. bootstack/__init__.py +157 -0
  2. bootstack/__main__.py +5 -0
  3. bootstack/_core/__init__.py +21 -0
  4. bootstack/_core/capabilities/__init__.py +45 -0
  5. bootstack/_core/capabilities/after.py +103 -0
  6. bootstack/_core/capabilities/bind.py +192 -0
  7. bootstack/_core/capabilities/bindtags.py +112 -0
  8. bootstack/_core/capabilities/busy.py +72 -0
  9. bootstack/_core/capabilities/clipboard.py +89 -0
  10. bootstack/_core/capabilities/focus.py +118 -0
  11. bootstack/_core/capabilities/grab.py +65 -0
  12. bootstack/_core/capabilities/grid.py +211 -0
  13. bootstack/_core/capabilities/localization.py +231 -0
  14. bootstack/_core/capabilities/pack.py +140 -0
  15. bootstack/_core/capabilities/place.py +113 -0
  16. bootstack/_core/capabilities/selection.py +136 -0
  17. bootstack/_core/capabilities/signals.py +244 -0
  18. bootstack/_core/capabilities/winfo.py +315 -0
  19. bootstack/_core/colorutils.py +234 -0
  20. bootstack/_core/exceptions.py +25 -0
  21. bootstack/_core/images.py +463 -0
  22. bootstack/_core/mixins/__init__.py +1 -0
  23. bootstack/_core/mixins/ttk_state.py +35 -0
  24. bootstack/_core/mixins/widget.py +132 -0
  25. bootstack/_core/paths.py +49 -0
  26. bootstack/_core/publisher.py +149 -0
  27. bootstack/_core/variables.py +62 -0
  28. bootstack/_runtime/__init__.py +3 -0
  29. bootstack/_runtime/app.py +930 -0
  30. bootstack/_runtime/base_window.py +945 -0
  31. bootstack/_runtime/events.py +399 -0
  32. bootstack/_runtime/shortcuts.py +496 -0
  33. bootstack/_runtime/tk_patch.py +43 -0
  34. bootstack/_runtime/toplevel.py +276 -0
  35. bootstack/_runtime/utility.py +457 -0
  36. bootstack/_runtime/visual_focus.py +240 -0
  37. bootstack/_runtime/window_utilities.py +1123 -0
  38. bootstack/assets/__init__.py +21 -0
  39. bootstack/assets/bootstack.ico +0 -0
  40. bootstack/assets/bootstack.png +0 -0
  41. bootstack/assets/elements/__init__.py +0 -0
  42. bootstack/assets/elements/badge-pill.png +0 -0
  43. bootstack/assets/elements/badge-square.png +0 -0
  44. bootstack/assets/elements/button-compact.png +0 -0
  45. bootstack/assets/elements/button-default.png +0 -0
  46. bootstack/assets/elements/buttongroup-after-h-compact.png +0 -0
  47. bootstack/assets/elements/buttongroup-after-h-default.png +0 -0
  48. bootstack/assets/elements/buttongroup-after-v-compact.png +0 -0
  49. bootstack/assets/elements/buttongroup-after-v-default.png +0 -0
  50. bootstack/assets/elements/buttongroup-before-h-compact.png +0 -0
  51. bootstack/assets/elements/buttongroup-before-h-default.png +0 -0
  52. bootstack/assets/elements/buttongroup-before-v-compact.png +0 -0
  53. bootstack/assets/elements/buttongroup-before-v-default.png +0 -0
  54. bootstack/assets/elements/buttongroup-center-h-compact.png +0 -0
  55. bootstack/assets/elements/buttongroup-center-h-default.png +0 -0
  56. bootstack/assets/elements/buttongroup-center-v-compact.png +0 -0
  57. bootstack/assets/elements/buttongroup-center-v-default.png +0 -0
  58. bootstack/assets/elements/card.png +0 -0
  59. bootstack/assets/elements/checkbox-checked.png +0 -0
  60. bootstack/assets/elements/checkbox-indeterminate.png +0 -0
  61. bootstack/assets/elements/checkbox-unchecked.png +0 -0
  62. bootstack/assets/elements/field.png +0 -0
  63. bootstack/assets/elements/input-addon-compact.png +0 -0
  64. bootstack/assets/elements/input-addon-default.png +0 -0
  65. bootstack/assets/elements/input-compact.png +0 -0
  66. bootstack/assets/elements/input-default.png +0 -0
  67. bootstack/assets/elements/list-item-separated.png +0 -0
  68. bootstack/assets/elements/list-item.png +0 -0
  69. bootstack/assets/elements/listrow-compact.png +0 -0
  70. bootstack/assets/elements/listrow-default.png +0 -0
  71. bootstack/assets/elements/manifest.toml +361 -0
  72. bootstack/assets/elements/menu-item.png +0 -0
  73. bootstack/assets/elements/navitem-compact.png +0 -0
  74. bootstack/assets/elements/navitem-default.png +0 -0
  75. bootstack/assets/elements/progressbar-h-compact.png +0 -0
  76. bootstack/assets/elements/progressbar-h-default.png +0 -0
  77. bootstack/assets/elements/progressbar-v-compact.png +0 -0
  78. bootstack/assets/elements/progressbar-v-default.png +0 -0
  79. bootstack/assets/elements/radiobutton.png +0 -0
  80. bootstack/assets/elements/scrollbar-horizontal.png +0 -0
  81. bootstack/assets/elements/scrollbar-vertical.png +0 -0
  82. bootstack/assets/elements/slider-handle.png +0 -0
  83. bootstack/assets/elements/slider-track-h.png +0 -0
  84. bootstack/assets/elements/slider-track-v.png +0 -0
  85. bootstack/assets/elements/switch-off.png +0 -0
  86. bootstack/assets/elements/switch-on.png +0 -0
  87. bootstack/assets/elements/tab-h.png +0 -0
  88. bootstack/assets/elements/tab-v.png +0 -0
  89. bootstack/assets/icons/bootstrap.ttf +0 -0
  90. bootstack/assets/icons/glyphmap.json +2080 -0
  91. bootstack/assets/icons/icon_metrics.json +12470 -0
  92. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.mo +0 -0
  93. bootstack/assets/locales/ar/LC_MESSAGES/bootstack.po +856 -0
  94. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.mo +0 -0
  95. bootstack/assets/locales/bg/LC_MESSAGES/bootstack.po +878 -0
  96. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.mo +0 -0
  97. bootstack/assets/locales/cs/LC_MESSAGES/bootstack.po +856 -0
  98. bootstack/assets/locales/da/LC_MESSAGES/bootstack.mo +0 -0
  99. bootstack/assets/locales/da/LC_MESSAGES/bootstack.po +856 -0
  100. bootstack/assets/locales/de/LC_MESSAGES/bootstack.mo +0 -0
  101. bootstack/assets/locales/de/LC_MESSAGES/bootstack.po +856 -0
  102. bootstack/assets/locales/en/LC_MESSAGES/bootstack.mo +0 -0
  103. bootstack/assets/locales/en/LC_MESSAGES/bootstack.po +878 -0
  104. bootstack/assets/locales/es/LC_MESSAGES/bootstack.mo +0 -0
  105. bootstack/assets/locales/es/LC_MESSAGES/bootstack.po +856 -0
  106. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.mo +0 -0
  107. bootstack/assets/locales/fr/LC_MESSAGES/bootstack.po +856 -0
  108. bootstack/assets/locales/he/LC_MESSAGES/bootstack.mo +0 -0
  109. bootstack/assets/locales/he/LC_MESSAGES/bootstack.po +854 -0
  110. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.mo +0 -0
  111. bootstack/assets/locales/hi/LC_MESSAGES/bootstack.po +845 -0
  112. bootstack/assets/locales/it/LC_MESSAGES/bootstack.mo +0 -0
  113. bootstack/assets/locales/it/LC_MESSAGES/bootstack.po +844 -0
  114. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.mo +0 -0
  115. bootstack/assets/locales/ja/LC_MESSAGES/bootstack.po +917 -0
  116. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.mo +0 -0
  117. bootstack/assets/locales/ko/LC_MESSAGES/bootstack.po +845 -0
  118. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.mo +0 -0
  119. bootstack/assets/locales/nb/LC_MESSAGES/bootstack.po +844 -0
  120. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.mo +0 -0
  121. bootstack/assets/locales/nl/LC_MESSAGES/bootstack.po +844 -0
  122. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.mo +0 -0
  123. bootstack/assets/locales/pl/LC_MESSAGES/bootstack.po +845 -0
  124. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.mo +0 -0
  125. bootstack/assets/locales/pt/LC_MESSAGES/bootstack.po +845 -0
  126. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.mo +0 -0
  127. bootstack/assets/locales/pt_BR/LC_MESSAGES/bootstack.po +845 -0
  128. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.mo +0 -0
  129. bootstack/assets/locales/sl/LC_MESSAGES/bootstack.po +845 -0
  130. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.mo +0 -0
  131. bootstack/assets/locales/sv/LC_MESSAGES/bootstack.po +845 -0
  132. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.mo +0 -0
  133. bootstack/assets/locales/tr/LC_MESSAGES/bootstack.po +845 -0
  134. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.mo +0 -0
  135. bootstack/assets/locales/zh_CN/LC_MESSAGES/bootstack.po +845 -0
  136. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.mo +0 -0
  137. bootstack/assets/locales/zh_TW/LC_MESSAGES/bootstack.po +845 -0
  138. bootstack/cli/__init__.py +133 -0
  139. bootstack/cli/__main__.py +6 -0
  140. bootstack/cli/add.py +395 -0
  141. bootstack/cli/appicon.py +285 -0
  142. bootstack/cli/build.py +115 -0
  143. bootstack/cli/config.py +313 -0
  144. bootstack/cli/demo.py +564 -0
  145. bootstack/cli/dev.py +153 -0
  146. bootstack/cli/doctor.py +195 -0
  147. bootstack/cli/icons.py +98 -0
  148. bootstack/cli/promote.py +120 -0
  149. bootstack/cli/pyinstaller.py +268 -0
  150. bootstack/cli/run.py +95 -0
  151. bootstack/cli/start.py +117 -0
  152. bootstack/cli/templates/__init__.py +931 -0
  153. bootstack/clipboard.py +48 -0
  154. bootstack/constants.py +318 -0
  155. bootstack/data/README.md +615 -0
  156. bootstack/data/__init__.py +78 -0
  157. bootstack/data/_observable.py +276 -0
  158. bootstack/data/base.py +780 -0
  159. bootstack/data/file_source.py +367 -0
  160. bootstack/data/memory_source.py +450 -0
  161. bootstack/data/query.py +367 -0
  162. bootstack/data/readers.py +289 -0
  163. bootstack/data/sqlite_source.py +869 -0
  164. bootstack/data/types.py +354 -0
  165. bootstack/data/writers.py +232 -0
  166. bootstack/dev/__init__.py +36 -0
  167. bootstack/dev/_capture.py +141 -0
  168. bootstack/dev/_env.py +43 -0
  169. bootstack/dev/_registry.py +140 -0
  170. bootstack/dev/_reloader.py +351 -0
  171. bootstack/dev/_reset.py +50 -0
  172. bootstack/dev/_watcher.py +91 -0
  173. bootstack/dialogs/__init__.py +948 -0
  174. bootstack/dialogs/_impl/__init__.py +47 -0
  175. bootstack/dialogs/_impl/colorchooser.py +588 -0
  176. bootstack/dialogs/_impl/datedialog.py +450 -0
  177. bootstack/dialogs/_impl/dialog.py +594 -0
  178. bootstack/dialogs/_impl/filterdialog.py +358 -0
  179. bootstack/dialogs/_impl/fontdialog.py +364 -0
  180. bootstack/dialogs/_impl/formdialog.py +564 -0
  181. bootstack/dialogs/_impl/message.py +486 -0
  182. bootstack/dialogs/_impl/query.py +570 -0
  183. bootstack/errors.py +67 -0
  184. bootstack/events/__init__.py +111 -0
  185. bootstack/events/_event.py +135 -0
  186. bootstack/events/_payloads.py +539 -0
  187. bootstack/events/_subscription.py +38 -0
  188. bootstack/i18n/README.md +77 -0
  189. bootstack/i18n/__init__.py +23 -0
  190. bootstack/i18n/catalog.py +121 -0
  191. bootstack/i18n/intl_format.py +584 -0
  192. bootstack/i18n/msgcat.py +425 -0
  193. bootstack/i18n/specs.py +156 -0
  194. bootstack/images.py +563 -0
  195. bootstack/py.typed +1 -0
  196. bootstack/scheduling/__init__.py +11 -0
  197. bootstack/scheduling/_schedule.py +218 -0
  198. bootstack/shortcuts.py +21 -0
  199. bootstack/signals/README.md +98 -0
  200. bootstack/signals/__init__.py +10 -0
  201. bootstack/signals/integration.py +100 -0
  202. bootstack/signals/signal.py +353 -0
  203. bootstack/signals/types.py +6 -0
  204. bootstack/store.py +286 -0
  205. bootstack/streams/__init__.py +12 -0
  206. bootstack/streams/_stream.py +321 -0
  207. bootstack/style/__init__.py +38 -0
  208. bootstack/style/builders/__init__.py +51 -0
  209. bootstack/style/builders/badge.py +46 -0
  210. bootstack/style/builders/button.py +339 -0
  211. bootstack/style/builders/buttongroup.py +311 -0
  212. bootstack/style/builders/calendar.py +271 -0
  213. bootstack/style/builders/checkbutton.py +110 -0
  214. bootstack/style/builders/combobox.py +113 -0
  215. bootstack/style/builders/contextmenu.py +268 -0
  216. bootstack/style/builders/entry.py +82 -0
  217. bootstack/style/builders/expander.py +148 -0
  218. bootstack/style/builders/field.py +335 -0
  219. bootstack/style/builders/frame.py +50 -0
  220. bootstack/style/builders/label.py +28 -0
  221. bootstack/style/builders/labelframe.py +34 -0
  222. bootstack/style/builders/listview.py +369 -0
  223. bootstack/style/builders/menubar.py +91 -0
  224. bootstack/style/builders/menubutton.py +359 -0
  225. bootstack/style/builders/panedwindow.py +25 -0
  226. bootstack/style/builders/progressbar.py +67 -0
  227. bootstack/style/builders/radiobutton.py +99 -0
  228. bootstack/style/builders/scale.py +64 -0
  229. bootstack/style/builders/scrollbar.py +225 -0
  230. bootstack/style/builders/separator.py +49 -0
  231. bootstack/style/builders/sidenav.py +643 -0
  232. bootstack/style/builders/sizegrip.py +15 -0
  233. bootstack/style/builders/spinbox.py +119 -0
  234. bootstack/style/builders/switch.py +70 -0
  235. bootstack/style/builders/tabitem.py +204 -0
  236. bootstack/style/builders/togglegroup.py +294 -0
  237. bootstack/style/builders/toolbutton.py +275 -0
  238. bootstack/style/builders/tooltip.py +26 -0
  239. bootstack/style/builders/treeview.py +193 -0
  240. bootstack/style/builders/utils.py +455 -0
  241. bootstack/style/builders_tk/__init__.py +16 -0
  242. bootstack/style/builders_tk/defaults.py +229 -0
  243. bootstack/style/element.py +173 -0
  244. bootstack/style/fonts.py +123 -0
  245. bootstack/style/style.py +609 -0
  246. bootstack/style/style_builder_base.py +716 -0
  247. bootstack/style/style_builder_mixed.py +93 -0
  248. bootstack/style/style_builder_tk.py +109 -0
  249. bootstack/style/style_builder_ttk.py +353 -0
  250. bootstack/style/style_resolver.py +447 -0
  251. bootstack/style/theme.py +245 -0
  252. bootstack/style/theme_provider.py +471 -0
  253. bootstack/style/themes/__init__.py +128 -0
  254. bootstack/style/tk_patch.py +5 -0
  255. bootstack/style/token_maps.py +41 -0
  256. bootstack/style/types.py +32 -0
  257. bootstack/style/typography.py +523 -0
  258. bootstack/style/utility.py +746 -0
  259. bootstack/types.py +39 -0
  260. bootstack/validation/__init__.py +6 -0
  261. bootstack/validation/types.py +15 -0
  262. bootstack/validation/validation_result.py +17 -0
  263. bootstack/validation/validation_rules.py +205 -0
  264. bootstack/widgets/__init__.py +74 -0
  265. bootstack/widgets/_core/__init__.py +31 -0
  266. bootstack/widgets/_core/app_config.py +266 -0
  267. bootstack/widgets/_core/base.py +461 -0
  268. bootstack/widgets/_core/container.py +334 -0
  269. bootstack/widgets/_core/context.py +35 -0
  270. bootstack/widgets/_core/events.py +130 -0
  271. bootstack/widgets/_core/field_mixin.py +353 -0
  272. bootstack/widgets/_core/icon_image_props.py +72 -0
  273. bootstack/widgets/_core/image_binding.py +93 -0
  274. bootstack/widgets/_core/navmodel.py +345 -0
  275. bootstack/widgets/_core/options.py +210 -0
  276. bootstack/widgets/_core/selection_group.py +130 -0
  277. bootstack/widgets/_core/window_controls.py +96 -0
  278. bootstack/widgets/_core/window_menu.py +246 -0
  279. bootstack/widgets/_impl/__init__.py +1 -0
  280. bootstack/widgets/_impl/_internal/__init__.py +0 -0
  281. bootstack/widgets/_impl/_internal/wrapper_base.py +307 -0
  282. bootstack/widgets/_impl/_parts/__init__.py +11 -0
  283. bootstack/widgets/_impl/_parts/numberentry_part.py +385 -0
  284. bootstack/widgets/_impl/_parts/spinnerentry_part.py +434 -0
  285. bootstack/widgets/_impl/_parts/textentry_part.py +406 -0
  286. bootstack/widgets/_impl/composites/__init__.py +33 -0
  287. bootstack/widgets/_impl/composites/_dateutils.py +33 -0
  288. bootstack/widgets/_impl/composites/_image_fit.py +105 -0
  289. bootstack/widgets/_impl/composites/accordion.py +381 -0
  290. bootstack/widgets/_impl/composites/avatar.py +191 -0
  291. bootstack/widgets/_impl/composites/buttongroup.py +371 -0
  292. bootstack/widgets/_impl/composites/calendar.py +972 -0
  293. bootstack/widgets/_impl/composites/carousel.py +567 -0
  294. bootstack/widgets/_impl/composites/chart.py +882 -0
  295. bootstack/widgets/_impl/composites/compositeframe.py +298 -0
  296. bootstack/widgets/_impl/composites/contextmenu.py +1951 -0
  297. bootstack/widgets/_impl/composites/dateentry.py +404 -0
  298. bootstack/widgets/_impl/composites/dropdownbutton.py +325 -0
  299. bootstack/widgets/_impl/composites/expander.py +515 -0
  300. bootstack/widgets/_impl/composites/field.py +670 -0
  301. bootstack/widgets/_impl/composites/form.py +1066 -0
  302. bootstack/widgets/_impl/composites/gallery.py +551 -0
  303. bootstack/widgets/_impl/composites/list/__init__.py +15 -0
  304. bootstack/widgets/_impl/composites/list/listitem.py +802 -0
  305. bootstack/widgets/_impl/composites/list/listview.py +1433 -0
  306. bootstack/widgets/_impl/composites/menu/__init__.py +18 -0
  307. bootstack/widgets/_impl/composites/menu/model.py +358 -0
  308. bootstack/widgets/_impl/composites/menu/render_native.py +134 -0
  309. bootstack/widgets/_impl/composites/menu/render_themed.py +134 -0
  310. bootstack/widgets/_impl/composites/meter.py +860 -0
  311. bootstack/widgets/_impl/composites/numericentry.py +201 -0
  312. bootstack/widgets/_impl/composites/pagestack.py +395 -0
  313. bootstack/widgets/_impl/composites/passwordentry.py +142 -0
  314. bootstack/widgets/_impl/composites/pathentry.py +168 -0
  315. bootstack/widgets/_impl/composites/picture.py +289 -0
  316. bootstack/widgets/_impl/composites/radiogroup.py +511 -0
  317. bootstack/widgets/_impl/composites/scrolledtext.py +375 -0
  318. bootstack/widgets/_impl/composites/scrolledtext.pyi +186 -0
  319. bootstack/widgets/_impl/composites/scrollview.py +764 -0
  320. bootstack/widgets/_impl/composites/selectbox.py +1026 -0
  321. bootstack/widgets/_impl/composites/shell/__init__.py +40 -0
  322. bootstack/widgets/_impl/composites/shell/content_host.py +52 -0
  323. bootstack/widgets/_impl/composites/shell/layout.py +335 -0
  324. bootstack/widgets/_impl/composites/shell/nav_panel.py +345 -0
  325. bootstack/widgets/_impl/composites/shell/providers.py +558 -0
  326. bootstack/widgets/_impl/composites/shell/rail.py +117 -0
  327. bootstack/widgets/_impl/composites/shell/shell.py +581 -0
  328. bootstack/widgets/_impl/composites/shell/workspace.py +273 -0
  329. bootstack/widgets/_impl/composites/sidenav/__init__.py +16 -0
  330. bootstack/widgets/_impl/composites/sidenav/header.py +81 -0
  331. bootstack/widgets/_impl/composites/sidenav/separator.py +44 -0
  332. bootstack/widgets/_impl/composites/slider/__init__.py +7 -0
  333. bootstack/widgets/_impl/composites/slider/_shared.py +195 -0
  334. bootstack/widgets/_impl/composites/slider/rangeslider.py +982 -0
  335. bootstack/widgets/_impl/composites/slider/slider.py +851 -0
  336. bootstack/widgets/_impl/composites/spinnerentry.py +185 -0
  337. bootstack/widgets/_impl/composites/tableview/__init__.py +5 -0
  338. bootstack/widgets/_impl/composites/tableview/tableview.py +3388 -0
  339. bootstack/widgets/_impl/composites/tableview/types.py +169 -0
  340. bootstack/widgets/_impl/composites/tabs/__init__.py +23 -0
  341. bootstack/widgets/_impl/composites/tabs/tabitem.py +389 -0
  342. bootstack/widgets/_impl/composites/tabs/tabs.py +974 -0
  343. bootstack/widgets/_impl/composites/tabs/tabview.py +650 -0
  344. bootstack/widgets/_impl/composites/textarea/__init__.py +26 -0
  345. bootstack/widgets/_impl/composites/textarea/change.py +46 -0
  346. bootstack/widgets/_impl/composites/textarea/codeeditor.py +526 -0
  347. bootstack/widgets/_impl/composites/textarea/core.py +495 -0
  348. bootstack/widgets/_impl/composites/textarea/decoration.py +42 -0
  349. bootstack/widgets/_impl/composites/textarea/diff.py +127 -0
  350. bootstack/widgets/_impl/composites/textarea/extensions/__init__.py +1 -0
  351. bootstack/widgets/_impl/composites/textarea/extensions/bracket_matcher.py +138 -0
  352. bootstack/widgets/_impl/composites/textarea/extensions/indent_guides.py +158 -0
  353. bootstack/widgets/_impl/composites/textarea/extensions/line_numbers.py +138 -0
  354. bootstack/widgets/_impl/composites/textarea/extensions/pygments_highlighter.py +312 -0
  355. bootstack/widgets/_impl/composites/textarea/extensions/smart_indent.py +177 -0
  356. bootstack/widgets/_impl/composites/textarea/filter.py +171 -0
  357. bootstack/widgets/_impl/composites/textarea/search_overlay.py +459 -0
  358. bootstack/widgets/_impl/composites/textarea/sidebar.py +88 -0
  359. bootstack/widgets/_impl/composites/textarea/style_registry.py +178 -0
  360. bootstack/widgets/_impl/composites/textarea/textarea.py +606 -0
  361. bootstack/widgets/_impl/composites/textarea/undo.py +217 -0
  362. bootstack/widgets/_impl/composites/textentry.py +57 -0
  363. bootstack/widgets/_impl/composites/timeentry.py +176 -0
  364. bootstack/widgets/_impl/composites/toast.py +390 -0
  365. bootstack/widgets/_impl/composites/toast_stack.py +156 -0
  366. bootstack/widgets/_impl/composites/togglegroup.py +418 -0
  367. bootstack/widgets/_impl/composites/toolbar.py +608 -0
  368. bootstack/widgets/_impl/composites/tooltip.py +491 -0
  369. bootstack/widgets/_impl/composites/tree/__init__.py +7 -0
  370. bootstack/widgets/_impl/composites/tree/source_binding.py +136 -0
  371. bootstack/widgets/_impl/composites/tree/treeitem.py +393 -0
  372. bootstack/widgets/_impl/composites/tree/treenode.py +174 -0
  373. bootstack/widgets/_impl/composites/tree/treeview.py +841 -0
  374. bootstack/widgets/_impl/mixins/__init__.py +24 -0
  375. bootstack/widgets/_impl/mixins/configure_mixin.py +216 -0
  376. bootstack/widgets/_impl/mixins/entry_mixin.py +134 -0
  377. bootstack/widgets/_impl/mixins/font_mixin.py +368 -0
  378. bootstack/widgets/_impl/mixins/icon_mixin.py +61 -0
  379. bootstack/widgets/_impl/mixins/localization_mixin.py +253 -0
  380. bootstack/widgets/_impl/mixins/signal_mixin.py +268 -0
  381. bootstack/widgets/_impl/mixins/validation_mixin.py +226 -0
  382. bootstack/widgets/_impl/primitives/__init__.py +49 -0
  383. bootstack/widgets/_impl/primitives/_menubutton.py +107 -0
  384. bootstack/widgets/_impl/primitives/badge.py +45 -0
  385. bootstack/widgets/_impl/primitives/button.py +76 -0
  386. bootstack/widgets/_impl/primitives/card.py +45 -0
  387. bootstack/widgets/_impl/primitives/checkbutton.py +124 -0
  388. bootstack/widgets/_impl/primitives/checktoggle.py +62 -0
  389. bootstack/widgets/_impl/primitives/combobox.py +156 -0
  390. bootstack/widgets/_impl/primitives/entry.py +87 -0
  391. bootstack/widgets/_impl/primitives/flexframe.py +448 -0
  392. bootstack/widgets/_impl/primitives/frame.py +185 -0
  393. bootstack/widgets/_impl/primitives/gridframe.py +546 -0
  394. bootstack/widgets/_impl/primitives/label.py +84 -0
  395. bootstack/widgets/_impl/primitives/labelframe.py +54 -0
  396. bootstack/widgets/_impl/primitives/optionmenu.py +387 -0
  397. bootstack/widgets/_impl/primitives/packframe.py +227 -0
  398. bootstack/widgets/_impl/primitives/panedwindow.py +45 -0
  399. bootstack/widgets/_impl/primitives/progressbar.py +83 -0
  400. bootstack/widgets/_impl/primitives/radiobutton.py +115 -0
  401. bootstack/widgets/_impl/primitives/radiotoggle.py +54 -0
  402. bootstack/widgets/_impl/primitives/scrollbar.py +42 -0
  403. bootstack/widgets/_impl/primitives/separator.py +43 -0
  404. bootstack/widgets/_impl/primitives/sizegrip.py +33 -0
  405. bootstack/widgets/_impl/primitives/spinbox.py +95 -0
  406. bootstack/widgets/_impl/primitives/switch.py +44 -0
  407. bootstack/widgets/_impl/primitives/treeview.py +69 -0
  408. bootstack/widgets/app.py +371 -0
  409. bootstack/widgets/appshell.py +1179 -0
  410. bootstack/widgets/avatar.py +140 -0
  411. bootstack/widgets/boolean_controls.py +455 -0
  412. bootstack/widgets/button.py +224 -0
  413. bootstack/widgets/buttongroup.py +239 -0
  414. bootstack/widgets/calendar.py +195 -0
  415. bootstack/widgets/card.py +159 -0
  416. bootstack/widgets/carousel.py +241 -0
  417. bootstack/widgets/chart.py +302 -0
  418. bootstack/widgets/codeeditor.py +675 -0
  419. bootstack/widgets/contextmenu.py +371 -0
  420. bootstack/widgets/datatable.py +688 -0
  421. bootstack/widgets/datefield.py +395 -0
  422. bootstack/widgets/divider.py +60 -0
  423. bootstack/widgets/expander.py +579 -0
  424. bootstack/widgets/form.py +200 -0
  425. bootstack/widgets/gallery.py +250 -0
  426. bootstack/widgets/gauge.py +168 -0
  427. bootstack/widgets/grid.py +121 -0
  428. bootstack/widgets/groupbox.py +162 -0
  429. bootstack/widgets/label.py +229 -0
  430. bootstack/widgets/listview.py +354 -0
  431. bootstack/widgets/menubutton.py +419 -0
  432. bootstack/widgets/numberfield.py +431 -0
  433. bootstack/widgets/pagestack.py +345 -0
  434. bootstack/widgets/passwordfield.py +383 -0
  435. bootstack/widgets/pathfield.py +454 -0
  436. bootstack/widgets/picture.py +251 -0
  437. bootstack/widgets/progressbar.py +106 -0
  438. bootstack/widgets/radio_variants.py +271 -0
  439. bootstack/widgets/radiogroup.py +228 -0
  440. bootstack/widgets/scrollbar.py +64 -0
  441. bootstack/widgets/scrollview.py +186 -0
  442. bootstack/widgets/select.py +302 -0
  443. bootstack/widgets/selectbutton.py +182 -0
  444. bootstack/widgets/sidebar_toggle.py +130 -0
  445. bootstack/widgets/sizegrip.py +42 -0
  446. bootstack/widgets/slider.py +408 -0
  447. bootstack/widgets/spinbox.py +147 -0
  448. bootstack/widgets/spinnerfield.py +413 -0
  449. bootstack/widgets/splash.py +367 -0
  450. bootstack/widgets/splitview.py +482 -0
  451. bootstack/widgets/stacks.py +249 -0
  452. bootstack/widgets/statusbar.py +189 -0
  453. bootstack/widgets/tabs.py +425 -0
  454. bootstack/widgets/textarea.py +459 -0
  455. bootstack/widgets/textfield.py +410 -0
  456. bootstack/widgets/theme_toggle.py +92 -0
  457. bootstack/widgets/timefield.py +340 -0
  458. bootstack/widgets/toast.py +314 -0
  459. bootstack/widgets/togglegroup.py +231 -0
  460. bootstack/widgets/toolbar.py +347 -0
  461. bootstack/widgets/tooltip.py +94 -0
  462. bootstack/widgets/tree.py +648 -0
  463. bootstack/widgets/types.py +305 -0
  464. bootstack/widgets/window.py +307 -0
  465. bootstack-0.1.0.dist-info/METADATA +301 -0
  466. bootstack-0.1.0.dist-info/RECORD +471 -0
  467. bootstack-0.1.0.dist-info/WHEEL +5 -0
  468. bootstack-0.1.0.dist-info/entry_points.txt +2 -0
  469. bootstack-0.1.0.dist-info/licenses/LICENSE +21 -0
  470. bootstack-0.1.0.dist-info/licenses/NOTICE +10 -0
  471. 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]]