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