bootstack 0.1.0a1__py3-none-any.whl

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