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,551 @@
1
+ """Internal Gallery composite — a recycling thumbnail grid.
2
+
3
+ A scrollable, selectable grid of image tiles. Record-native: it reads from a
4
+ `DataSourceProtocol` and reuses the selection model the list/table/tree widgets
5
+ share. The public `bootstack.Gallery` wrapper drives this.
6
+
7
+ Recycle model: a FIXED pool of tiles sits in stable grid slots; scrolling slides
8
+ the data window over the pool and each tile re-renders only when the record in
9
+ its slot actually changes. Modeled as a vertical scroll of rows, each row a strip
10
+ of `columns` tiles — the 1D list recycle generalized one axis, not a 2D recycler.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import contextlib
16
+ from tkinter import Canvas, TclError
17
+ from typing import Any, Literal
18
+
19
+ from PIL import Image as PILImage, ImageDraw
20
+ from PIL.Image import Resampling
21
+ from PIL.ImageTk import PhotoImage
22
+
23
+ from bootstack.data import MemoryDataSource, DataSourceProtocol
24
+ from bootstack.style.style import get_style
25
+ from bootstack.widgets._impl.composites._image_fit import (
26
+ FitMode, fit_image, resolve_pil, round_corners,
27
+ )
28
+ from bootstack.widgets._impl.primitives.frame import Frame
29
+ from bootstack.widgets._impl.primitives.label import Label
30
+ from bootstack.widgets._impl.primitives.scrollbar import Scrollbar
31
+ from bootstack.widgets.types import Master, ScrollbarVariant
32
+
33
+ OVERSCAN_ROWS = 1
34
+ _RING_THICKNESS = 3
35
+ _RING_GAP = 3 # separation between the image and the ring
36
+ _RING_PAD = _RING_GAP + _RING_THICKNESS # margin reserved around the image
37
+ _CAPTION_H = 24 # vertical space reserved per caption row in the height floor
38
+ EMPTY = {"__empty__": True, "id": "__empty__"}
39
+
40
+
41
+ class GalleryTile(Frame):
42
+ """One thumbnail cell — a canvas (image + selection ring) and a caption."""
43
+
44
+ def __init__(self, master, *, tile_w, tile_h, fit, corner_radius, caption,
45
+ on_click=None, on_activate=None, **kwargs):
46
+ super().__init__(master, **kwargs)
47
+ self._tw = tile_w
48
+ self._th = tile_h
49
+ self._fit: FitMode = fit
50
+ self._radius = corner_radius
51
+ self._has_caption = caption
52
+ self._on_click_cb = on_click
53
+ self._on_activate_cb = on_activate
54
+ self._record: dict | None = None
55
+ self._cur_src: Any = "\x00unset" # sentinel distinct from None
56
+ self._cur_id: Any = "\x00unset"
57
+ self._photo: PhotoImage | None = None
58
+ self._ring_photo: PhotoImage | None = None
59
+
60
+ # The canvas is larger than the image by a margin on every side, so the
61
+ # selection ring can sit OUTSIDE the image (with a gap) without changing
62
+ # the layout when toggled. The image is centered; the margin blends into
63
+ # the surface when unselected.
64
+ cw, ch = tile_w + 2 * _RING_PAD, tile_h + 2 * _RING_PAD
65
+ self._canvas = Canvas(self, width=cw, height=ch,
66
+ highlightthickness=0, bd=0,
67
+ background=self._surface_color())
68
+ self._canvas.pack()
69
+ self._img_id = self._canvas.create_image(cw // 2, ch // 2, anchor="center")
70
+ self._ring_id = self._canvas.create_image(0, 0, anchor="nw", state="hidden")
71
+
72
+ if caption:
73
+ self._caption = Label(self, text="", anchor="center", font="caption")
74
+ self._caption.pack(fill="x", pady=(4, 0))
75
+ else:
76
+ self._caption = None
77
+
78
+ for seq in ("<Button-1>",):
79
+ self._canvas.bind(seq, self._on_click)
80
+ self._canvas.bind("<Double-Button-1>", self._on_double_click)
81
+
82
+ def _surface_color(self) -> str:
83
+ token = getattr(self, "_surface", None) or "background"
84
+ b = get_style().style_builder
85
+ try:
86
+ return b.color(token)
87
+ except Exception:
88
+ return b.color("background")
89
+
90
+ def set_ring(self, ring_photo: "PhotoImage | None") -> None:
91
+ """Share the gallery's prebuilt selection-ring image with this tile."""
92
+ self._ring_photo = ring_photo
93
+ if ring_photo is not None:
94
+ self._canvas.itemconfigure(self._ring_id, image=ring_photo)
95
+
96
+ def update_data(self, record, image_source, caption_text, selected: bool) -> None:
97
+ """Bind `record` to this tile, re-rendering the thumbnail only if changed."""
98
+ empty = record is None or record.get("__empty__", False)
99
+ self._record = None if empty else record
100
+ rid = None if empty else record.get("id")
101
+ if rid != self._cur_id:
102
+ self._cur_id = rid
103
+ if self._caption is not None:
104
+ self._caption.configure(
105
+ text="" if empty else self._fit_caption(str(caption_text)))
106
+ source = None if empty else image_source
107
+ if source is not self._cur_src and source != self._cur_src:
108
+ self._render_thumb(source)
109
+ self._cur_src = source
110
+ self._canvas.itemconfigure(
111
+ self._ring_id, state="normal" if (selected and not empty) else "hidden"
112
+ )
113
+
114
+ def _fit_caption(self, text: str) -> str:
115
+ """Ellipsize `text` to the tile width so a caption never widens its tile.
116
+
117
+ Keeps every tile a uniform width — so grid columns stay put instead of
118
+ shifting as longer/shorter captions scroll through them.
119
+ """
120
+ if not text:
121
+ return text
122
+ avail = self._tw + 2 * _RING_PAD - 6
123
+ cap = self._caption
124
+ try:
125
+ font = cap.cget("font")
126
+ measure = lambda s: int(cap.tk.call("font", "measure", font, s))
127
+ except Exception:
128
+ return text
129
+ if measure(text) <= avail:
130
+ return text
131
+ ell = "…"
132
+ lo, hi = 0, len(text)
133
+ while lo < hi:
134
+ mid = (lo + hi + 1) // 2
135
+ if measure(text[:mid] + ell) <= avail:
136
+ lo = mid
137
+ else:
138
+ hi = mid - 1
139
+ return text[:lo] + ell if lo > 0 else ell
140
+
141
+ def _render_thumb(self, source: Any) -> None:
142
+ pil = resolve_pil(source)
143
+ if pil is None:
144
+ self._canvas.itemconfigure(self._img_id, image="")
145
+ self._photo = None
146
+ return
147
+ composed = fit_image(pil.convert("RGBA"), self._tw, self._th, self._fit)
148
+ if self._radius > 0:
149
+ composed = round_corners(composed, self._radius)
150
+ # A plain PhotoImage (kept alive by self._photo, GC'd when replaced) — do
151
+ # NOT route through the global _ImageService cache, which never evicts and
152
+ # would accumulate one entry per distinct thumbnail as the grid scrolls.
153
+ self._photo = PhotoImage(composed)
154
+ self._canvas.itemconfigure(self._img_id, image=self._photo)
155
+
156
+ def refresh_surface(self) -> None:
157
+ """Re-resolve the canvas background after a theme change."""
158
+ try:
159
+ self._canvas.configure(background=self._surface_color())
160
+ except TclError:
161
+ pass
162
+
163
+ def _on_click(self, _event) -> None:
164
+ # Call the gallery directly: virtual events do NOT bubble from a child
165
+ # widget to an ancestor's bindings, so a container binding would never fire.
166
+ if self._record is not None and self._on_click_cb is not None:
167
+ self._on_click_cb(self._record)
168
+
169
+ def _on_double_click(self, _event) -> None:
170
+ if self._record is not None and self._on_activate_cb is not None:
171
+ self._on_activate_cb(self._record)
172
+
173
+
174
+ class Gallery(Frame):
175
+ """Recycling thumbnail grid bound to a data source."""
176
+
177
+ def __init__(
178
+ self,
179
+ master: Master = None,
180
+ *,
181
+ items: list | None = None,
182
+ datasource: DataSourceProtocol | None = None,
183
+ image_field: str = "image",
184
+ caption_field: str | None = None,
185
+ columns: "int | Literal['auto']" = "auto",
186
+ tile_size: tuple[int, int] = (160, 160),
187
+ fit: FitMode = "cover",
188
+ corner_radius: int = 0,
189
+ gap: int = 8,
190
+ selection_mode: Literal["none", "single", "multi"] = "none",
191
+ accent: str | None = None,
192
+ scrollbar_variant: ScrollbarVariant = "thin",
193
+ rows: int = 2,
194
+ **kwargs: Any,
195
+ ) -> None:
196
+ super().__init__(master, **kwargs)
197
+ self._scrollbar_variant = scrollbar_variant
198
+ self._min_rows = max(1, rows)
199
+
200
+ self._image_field = image_field
201
+ self._caption_field = caption_field
202
+ self._columns_opt = columns
203
+ self._tw, self._th = tile_size
204
+ self._fit: FitMode = fit
205
+ self._corner_radius = corner_radius
206
+ self._gap = gap
207
+ self._selection_mode = selection_mode
208
+ self._accent = accent or "primary"
209
+
210
+ page = 200
211
+ if datasource is not None:
212
+ self._datasource = datasource
213
+ elif items:
214
+ self._datasource = MemoryDataSource(page_size=page).load(items)
215
+ else:
216
+ self._datasource = MemoryDataSource(page_size=page).load([])
217
+
218
+ self._tiles: list[GalleryTile] = []
219
+ self._cols = 1
220
+ self._cfg_cols = 0 # highest grid column given a weight, for resetting
221
+ self._visible_rows = 1
222
+ self._pool_rows = 1
223
+ self._start_row = 0
224
+ self._ring_photo: PhotoImage | None = None
225
+ self._row_h = self._th + 2 * _RING_PAD + (_CAPTION_H if caption_field else 0) + gap
226
+
227
+ sb_surface = getattr(self, '_surface', None)
228
+ sb_kw = {'surface': sb_surface} if sb_surface else {}
229
+ self._scrollbar = Scrollbar(
230
+ self, orient="vertical", command=self._on_scroll,
231
+ variant=self._scrollbar_variant, **sb_kw)
232
+ self._scrollbar.pack(side="right", fill="y")
233
+ self._container = Frame(self)
234
+ self._container.pack(side="left", fill="both", expand=True)
235
+
236
+ # Requested-height FLOOR so the grid never reports ~0 height in a
237
+ # container that doesn't impose one (a scroll view, an auto-fit window).
238
+ # The public wrapper freezes this frame with `pack_propagate(False)`, so
239
+ # its own configured `height` is its requested height; `fill`/`expand`
240
+ # still grow it past this floor when the parent has room.
241
+ self.configure(height=self._min_rows * self._row_h)
242
+
243
+ # Bind resize on the CONTAINER and use the event's width/height directly —
244
+ # winfo_width() read during the parent's <Configure> lags a cycle behind
245
+ # the new size, which would wedge the column count at its initial value.
246
+ self._container.bind("<Configure>", self._on_resize, add="+")
247
+ self._bind_scroll(self)
248
+ self._bind_scroll(self._container)
249
+
250
+ self._change_sub = None
251
+ on_change = getattr(self._datasource, "on_change", None)
252
+ if callable(on_change):
253
+ try:
254
+ self._change_sub = on_change(self._on_source_change)
255
+ except Exception:
256
+ self._change_sub = None
257
+ self.bind("<Destroy>", self._on_destroy, add="+")
258
+ # Tile surfaces + the accent ring are painted imperatively, so re-resolve
259
+ # on a theme change — gated to on-screen by the Frame base hook (an
260
+ # off-screen gallery defers its thumbnail-grid repaint to <Map>).
261
+ # First render is driven by the container's <Configure> (bound above);
262
+ # no init timer needed.
263
+
264
+ # ----- theme / silence / source change ----------------------------------
265
+
266
+ def _bs_apply_theme(self, _event=None) -> None:
267
+ """Re-apply theme-dependent colors after a theme switch.
268
+
269
+ The tile surface backgrounds and the accent selection ring are resolved
270
+ from theme tokens; rebuild them so they track the new theme.
271
+ """
272
+ for tile in self._tiles:
273
+ tile.refresh_surface()
274
+ if self._tiles:
275
+ self._build_ring() # new accent color, re-shared to every tile
276
+
277
+ def _silence_source(self):
278
+ silence = getattr(self._datasource, "_silence", None)
279
+ return silence() if callable(silence) else contextlib.nullcontext()
280
+
281
+ def _on_source_change(self, _event: Any = None) -> None:
282
+ try:
283
+ self._relayout()
284
+ except Exception:
285
+ pass
286
+
287
+ def _on_destroy(self, event: Any = None) -> None:
288
+ if event is not None and getattr(event, "widget", None) is not self:
289
+ return
290
+ sub, self._change_sub = self._change_sub, None
291
+ if sub is not None:
292
+ try:
293
+ sub.cancel()
294
+ except Exception:
295
+ pass
296
+
297
+ # ----- layout / recycle --------------------------------------------------
298
+
299
+ def _resolve_columns(self, width: int) -> int:
300
+ if self._columns_opt != "auto":
301
+ return max(1, int(self._columns_opt))
302
+ cell = self._tw + 2 * _RING_PAD
303
+ # Each tile occupies cell + gap of width — _regrid gives every tile a
304
+ # symmetric padx of gap//2 on both sides. So the count is
305
+ # width // (cell + gap); the "(n-1) gaps between items" form
306
+ # ((width + gap) // (cell + gap)) permits one column too many and clips
307
+ # the last column off the frame's right edge.
308
+ return max(1, width // (cell + self._gap))
309
+
310
+ def _on_resize(self, event) -> None:
311
+ self._relayout(event.width, event.height)
312
+
313
+ def _relayout(self, width: int | None = None, height: int | None = None) -> None:
314
+ """Recompute the column count and tile pool, then refill.
315
+
316
+ `width`/`height` come straight from a `<Configure>` event when available
317
+ (authoritative); otherwise the container is measured (the initial tick).
318
+ """
319
+ if width is None or height is None:
320
+ try:
321
+ width = self._container.winfo_width()
322
+ height = self._container.winfo_height()
323
+ except TclError:
324
+ return
325
+ if width <= 1 or height <= 1:
326
+ return
327
+
328
+ cols = self._resolve_columns(width)
329
+ visible_rows = max(1, height // self._row_h + 1)
330
+ pool_rows = visible_rows + OVERSCAN_ROWS
331
+ needed = cols * pool_rows
332
+
333
+ rebuild_geometry = cols != self._cols
334
+ self._cols = cols
335
+ self._visible_rows = visible_rows
336
+ self._pool_rows = pool_rows
337
+
338
+ if self._ring_photo is None or rebuild_geometry:
339
+ self._build_ring()
340
+
341
+ self._ensure_pool(needed)
342
+ self._regrid()
343
+ self._update_tiles()
344
+
345
+ def _build_ring(self) -> None:
346
+ """Build the shared antialiased accent selection-ring overlay image.
347
+
348
+ Sized to the full padded canvas; the ring stroke runs near the canvas
349
+ edge so it sits a `_RING_GAP` outside the centered image, with corners
350
+ concentric to the image's rounded corners.
351
+ """
352
+ w, h = self._tw + 2 * _RING_PAD, self._th + 2 * _RING_PAD
353
+ color = get_style().style_builder.color(self._accent)
354
+ ss = 4
355
+ big = PILImage.new("RGBA", (w * ss, h * ss), (0, 0, 0, 0))
356
+ t = _RING_THICKNESS * ss
357
+ inset = t // 2
358
+ radius = self._corner_radius + _RING_GAP + _RING_THICKNESS // 2
359
+ ImageDraw.Draw(big).rounded_rectangle(
360
+ [inset, inset, w * ss - 1 - inset, h * ss - 1 - inset],
361
+ radius=max(0, radius) * ss, outline=color, width=t,
362
+ )
363
+ self._ring_photo = PhotoImage(image=big.resize((w, h), Resampling.LANCZOS))
364
+ for tile in self._tiles:
365
+ tile.set_ring(self._ring_photo)
366
+
367
+ def _ensure_pool(self, needed: int) -> None:
368
+ while len(self._tiles) < needed:
369
+ tile = GalleryTile(
370
+ self._container, tile_w=self._tw, tile_h=self._th, fit=self._fit,
371
+ corner_radius=self._corner_radius, caption=bool(self._caption_field),
372
+ on_click=self._handle_tile_click, on_activate=self._handle_tile_activate,
373
+ )
374
+ tile.set_ring(self._ring_photo)
375
+ self._bind_scroll(tile)
376
+ self._bind_scroll(tile._canvas)
377
+ if tile._caption is not None:
378
+ self._bind_scroll(tile._caption)
379
+ self._tiles.append(tile)
380
+ while len(self._tiles) > needed:
381
+ tile = self._tiles.pop()
382
+ try:
383
+ tile.destroy()
384
+ except TclError:
385
+ pass
386
+
387
+ def _regrid(self) -> None:
388
+ pad = self._gap // 2
389
+ # Weight the active columns equally so the fixed-width tiles spread to
390
+ # fill the row instead of left-packing with a ragged right margin (which
391
+ # reads as a wasted extra column). Tiles are gridded without sticky, so
392
+ # each stays its cell width and centers in its column. Stale columns from
393
+ # a wider previous layout are zeroed.
394
+ for c in range(self._cfg_cols):
395
+ self._container.grid_columnconfigure(c, weight=0, uniform="")
396
+ for c in range(self._cols):
397
+ self._container.grid_columnconfigure(c, weight=1, uniform="gallerycol")
398
+ self._cfg_cols = self._cols
399
+ for i, tile in enumerate(self._tiles):
400
+ tile.grid(row=i // self._cols, column=i % self._cols, padx=pad, pady=pad)
401
+
402
+ def _total_rows(self) -> int:
403
+ count = self._datasource.count
404
+ return (count + self._cols - 1) // self._cols if count else 0
405
+
406
+ def _clamp(self) -> None:
407
+ max_start = max(0, self._total_rows() - self._visible_rows)
408
+ self._start_row = max(0, min(self._start_row, max_start))
409
+
410
+ def _update_tiles(self) -> None:
411
+ self._clamp()
412
+ start_index = self._start_row * self._cols
413
+ window = self._datasource.page_slice(start_index, len(self._tiles))
414
+ for i, tile in enumerate(self._tiles):
415
+ if i < len(window):
416
+ raw = window[i]
417
+ pub = self._public(raw)
418
+ rid = pub.get("id")
419
+ image = raw.get(self._image_field)
420
+ caption = raw.get(self._caption_field, "") if self._caption_field else ""
421
+ selected = False
422
+ if rid is not None:
423
+ try:
424
+ selected = bool(self._datasource.is_selected(rid))
425
+ except Exception:
426
+ selected = False
427
+ tile.update_data(pub, image, caption, selected)
428
+ else:
429
+ tile.update_data(None, None, "", False)
430
+ self._update_scrollbar()
431
+
432
+ def _update_scrollbar(self) -> None:
433
+ total = max(1, self._total_rows())
434
+ first = self._start_row / total
435
+ last = min(1.0, (self._start_row + self._visible_rows) / total)
436
+ self._scrollbar.set(first, last)
437
+
438
+ # ----- scrolling ---------------------------------------------------------
439
+
440
+ def _on_scroll(self, *args) -> None:
441
+ if args[0] == "moveto":
442
+ fraction = float(args[1])
443
+ max_start = max(0, self._total_rows() - self._visible_rows)
444
+ self._start_row = int(round(fraction * max_start))
445
+ elif args[0] == "scroll":
446
+ amount, unit = int(args[1]), args[2]
447
+ step = max(1, self._visible_rows - 1) if unit == "pages" else 1
448
+ self._start_row += amount * step
449
+ self._clamp()
450
+ self._update_tiles()
451
+
452
+ def _bind_scroll(self, widget) -> None:
453
+ widget.bind("<MouseWheel>", self._on_wheel, add="+")
454
+ widget.bind("<Button-4>", self._on_wheel, add="+")
455
+ widget.bind("<Button-5>", self._on_wheel, add="+")
456
+
457
+ def _on_wheel(self, event) -> None:
458
+ if getattr(event, "num", None) == 4:
459
+ delta = 1
460
+ elif getattr(event, "num", None) == 5:
461
+ delta = -1
462
+ else:
463
+ delta = 1 if event.delta < 0 else -1
464
+ self._start_row += delta
465
+ self._clamp()
466
+ self._update_tiles()
467
+ return "break"
468
+
469
+ def scroll_to_top(self) -> None:
470
+ self._start_row = 0
471
+ self._update_tiles()
472
+
473
+ def scroll_to_bottom(self) -> None:
474
+ self._start_row = self._total_rows()
475
+ self._clamp()
476
+ self._update_tiles()
477
+
478
+ # ----- selection ---------------------------------------------------------
479
+
480
+ def get_selected(self) -> list[dict]:
481
+ try:
482
+ rows = self._datasource.selected()
483
+ except Exception:
484
+ rows = []
485
+ return [self._public(r) for r in rows]
486
+
487
+ def _public(self, raw: dict) -> dict:
488
+ pub = getattr(self._datasource, "_public_record", None)
489
+ return pub(raw) if callable(pub) else dict(raw)
490
+
491
+ def select_items(self, record_ids: list) -> None:
492
+ with self._silence_source():
493
+ if self._selection_mode == "single":
494
+ self._datasource.deselect_all()
495
+ if record_ids:
496
+ self._datasource.select(record_ids[-1])
497
+ else:
498
+ for rid in record_ids:
499
+ self._datasource.select(rid)
500
+ self._after_selection_change()
501
+
502
+ def deselect_items(self, record_ids: list) -> None:
503
+ with self._silence_source():
504
+ for rid in record_ids:
505
+ self._datasource.deselect(rid)
506
+ self._after_selection_change()
507
+
508
+ def select_all(self) -> None:
509
+ if self._selection_mode != "multi":
510
+ return
511
+ with self._silence_source():
512
+ self._datasource.select_all()
513
+ self._after_selection_change()
514
+
515
+ def clear_selection(self) -> None:
516
+ with self._silence_source():
517
+ self._datasource.deselect_all()
518
+ self._after_selection_change()
519
+
520
+ def _after_selection_change(self) -> None:
521
+ self._update_tiles()
522
+ self.event_generate("<<SelectionChange>>")
523
+
524
+ # ----- tile events -------------------------------------------------------
525
+
526
+ def _handle_tile_click(self, record: dict) -> None:
527
+ rid = record.get("id")
528
+ if self._selection_mode != "none" and rid is not None:
529
+ with self._silence_source():
530
+ if self._selection_mode == "single":
531
+ self._datasource.deselect_all()
532
+ self._datasource.select(rid)
533
+ else:
534
+ if self._datasource.is_selected(rid):
535
+ self._datasource.deselect(rid)
536
+ else:
537
+ self._datasource.select(rid)
538
+ self._update_tiles()
539
+ self.event_generate("<<SelectionChange>>")
540
+ # record is already the public record (the tile holds the projected copy).
541
+ self.event_generate("<<ItemClick>>", data=record)
542
+
543
+ def _handle_tile_activate(self, record: dict) -> None:
544
+ if record:
545
+ self.event_generate("<<ItemActivate>>", data=record)
546
+
547
+ def get_datasource(self) -> DataSourceProtocol:
548
+ return self._datasource
549
+
550
+ def reload(self) -> None:
551
+ self._relayout()
@@ -0,0 +1,15 @@
1
+ """List widgets for displaying collections of items."""
2
+
3
+ from bootstack.widgets._impl.composites.list.listitem import ListItem
4
+ from bootstack.widgets._impl.composites.list.listview import (
5
+ ListView,
6
+ MemoryDataSource,
7
+ DataSourceProtocol,
8
+ )
9
+
10
+ __all__ = [
11
+ 'ListItem',
12
+ 'ListView',
13
+ 'MemoryDataSource',
14
+ 'DataSourceProtocol',
15
+ ]