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,495 @@
1
+ """_MultilineCore — shared internal engine for TextArea and CodeEditor.
2
+
3
+ Owns the tk.Text widget, filter chain, scrollbar wiring, and the
4
+ sidebar/decoration registries used by CodeEditor extensions.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import tkinter as tk
9
+ from typing import Callable, Literal
10
+
11
+ from bootstack.events import TextModifiedEvent
12
+ from bootstack.widgets._impl.primitives.scrollbar import Scrollbar
13
+ from bootstack.widgets._impl.composites.textarea.filter import EditFilter, FilterChain
14
+ from bootstack.widgets._impl.composites.textarea.change import ChangeNotifier
15
+ from bootstack.widgets._impl.composites.textarea.undo import UndoManager
16
+ from bootstack.widgets._impl.composites.textarea.style_registry import StyleRegistry
17
+ from bootstack.widgets._impl.composites.textarea.diff import DecorationDiff
18
+ from bootstack.widgets._impl.composites.textarea.decoration import (
19
+ LineDecoration, Position, RangeDecoration,
20
+ )
21
+ from bootstack.widgets.types import Master
22
+
23
+
24
+ ScrollbarMode = Literal["vertical", "both", "none", "auto"]
25
+
26
+
27
+ class _MultilineCore(tk.Frame):
28
+ """Internal engine shared by TextArea and CodeEditor.
29
+
30
+ Not part of the public API. Use TextArea or CodeEditor instead.
31
+
32
+ Wraps a tk.Text widget with:
33
+ - A FilterChain routing all inserts/deletes through registered filters.
34
+ - Built-in ChangeNotifier (fires <<Change>>) and UndoManager.
35
+ - Scrollbar wiring with auto/always/none/both modes.
36
+ - value/textsignal reactive binding.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ master: Master = None,
42
+ value: str = "",
43
+ wrap: bool = True,
44
+ height: int = 4,
45
+ scrollbars: ScrollbarMode = "auto",
46
+ font: str = "body",
47
+ read_only: bool = False,
48
+ **text_kwargs,
49
+ ) -> None:
50
+ super().__init__(master, highlightthickness=0, borderwidth=0)
51
+
52
+ self._scrollbar_mode = scrollbars
53
+ self._read_only = read_only
54
+ self._signal = None
55
+ self._signal_trace_id = None
56
+
57
+ # Detect windowing system for mousewheel handling
58
+ self.winsys: str = self.tk.call("tk", "windowingsystem")
59
+
60
+ # ── text widget ───────────────────────────────────────────────────
61
+ # undo=False: we manage the undo stack ourselves via UndoManager
62
+ # (character-class grouping for word-level undo, matching IDLE).
63
+ wrap_mode = tk.WORD if wrap else tk.NONE
64
+ self.text = tk.Text(
65
+ self,
66
+ wrap=wrap_mode,
67
+ height=height,
68
+ font=font,
69
+ undo=False,
70
+ borderwidth=0,
71
+ highlightthickness=0,
72
+ **text_kwargs,
73
+ )
74
+
75
+ # ── scrollbars ────────────────────────────────────────────────────
76
+ self._vscroll = Scrollbar(self, orient="vertical", command=self.text.yview)
77
+ self._hscroll = Scrollbar(self, orient="horizontal", command=self.text.xview)
78
+
79
+ self.text.configure(yscrollcommand=self._on_yscroll)
80
+ if not wrap:
81
+ self.text.configure(xscrollcommand=self._on_xscroll)
82
+
83
+ # ── layout ────────────────────────────────────────────────────────
84
+ # col 0: left sidebars (auto-sized, initially empty)
85
+ # col 1: text (fills available space)
86
+ # col 2: vertical scrollbar
87
+ self._sidebars: list = [] # list of attached Sidebar instances
88
+ self._text_col = 1 # text widget column (shifts right if needed)
89
+ self.grid_rowconfigure(0, weight=1)
90
+ self.grid_columnconfigure(0, weight=0) # sidebar column
91
+ self.grid_columnconfigure(1, weight=1) # text column
92
+ self.text.grid(row=0, column=1, sticky="nsew")
93
+
94
+ self._update_scrollbar_layout()
95
+
96
+ # ── filter chain ──────────────────────────────────────────────────
97
+ self._chain = FilterChain(self.text)
98
+
99
+ self._undo_manager = UndoManager()
100
+ self._change_notifier = ChangeNotifier()
101
+
102
+ self._install_filter(self._undo_manager)
103
+ self._install_filter(self._change_notifier)
104
+
105
+ # ── style registry + decoration diff ─────────────────────────────
106
+ self._style_registry = StyleRegistry(self)
107
+ self._decoration_diff = DecorationDiff(self, self._style_registry)
108
+
109
+ # ── mousewheel ────────────────────────────────────────────────────
110
+ self._scroll_tag = f"_mcore_{id(self)}"
111
+ self._setup_mousewheel()
112
+
113
+ # ── initial value ─────────────────────────────────────────────────
114
+ if value:
115
+ self.text.insert("1.0", value)
116
+ self._undo_manager.reset_undo() # clear history after seeding
117
+
118
+ if read_only:
119
+ self.text.configure(state=tk.DISABLED)
120
+
121
+ # Keyboard undo/redo: drive our UndoManager from Tk's platform-aware
122
+ # <<Undo>>/<<Redo>> virtual events rather than hardcoding keys, so the
123
+ # native per-OS shortcut applies (Ctrl+Z / Ctrl+Y on Windows & Linux,
124
+ # Cmd+Z / Cmd+Shift+Z on macOS). undo=False means Tk's own handlers for
125
+ # these events no-op; the widget-level binding runs first and "break"
126
+ # suppresses the (empty) class handler.
127
+ self.text.bind("<<Undo>>", lambda e: (self._undo_manager.undo(), "break")[1], add="+")
128
+ self.text.bind("<<Redo>>", lambda e: (self._undo_manager.redo(), "break")[1], add="+")
129
+
130
+ # <<Modified>> still fires from Tk's edit_modified flag (independent
131
+ # of the undo stack) — enrich with is_dirty payload.
132
+ self.text.bind("<<Modified>>", self._on_tk_modified, add="+")
133
+
134
+ self.bind("<Destroy>", self._on_destroy, add="+")
135
+
136
+ # ── filter management ─────────────────────────────────────────────────
137
+
138
+ def _install_filter(self, f: EditFilter) -> None:
139
+ self._chain.add(f)
140
+ f.attach(self)
141
+
142
+ def add_filter(self, f: EditFilter) -> None:
143
+ """Install a custom filter at the top of the chain."""
144
+ self._install_filter(f)
145
+
146
+ def remove_filter(self, f: EditFilter) -> None:
147
+ """Remove a custom filter from the chain."""
148
+ f.detach(self)
149
+ self._chain.remove(f)
150
+
151
+ # ── sidebar management ────────────────────────────────────────────────
152
+
153
+ def add_sidebar(self, sidebar, side: str = "left") -> None:
154
+ """Attach a sidebar widget to this core.
155
+
156
+ Args:
157
+ sidebar: A `Sidebar` instance.
158
+ side: `"left"` (default) — only left sidebars are supported
159
+ in the current layout.
160
+ """
161
+ self._sidebars.append(sidebar)
162
+ sidebar.attach(self, side=side)
163
+
164
+ def remove_sidebar(self, sidebar) -> None:
165
+ """Detach a sidebar widget from this core.
166
+
167
+ Args:
168
+ sidebar: The `Sidebar` instance to remove.
169
+ """
170
+ if sidebar in self._sidebars:
171
+ self._sidebars.remove(sidebar)
172
+ sidebar.detach(self)
173
+
174
+ # ── scrollbars ────────────────────────────────────────────────────────
175
+
176
+ def _on_yscroll(self, first: str, last: str) -> None:
177
+ self._vscroll.set(first, last)
178
+ for sb in self._sidebars:
179
+ sb.on_scroll(first, last)
180
+ if self._scrollbar_mode == "auto":
181
+ vscroll_col = self._text_col + 1
182
+ if float(first) <= 0.0 and float(last) >= 1.0:
183
+ self._vscroll.grid_remove()
184
+ else:
185
+ self._vscroll.grid(row=0, column=vscroll_col, sticky="ns")
186
+
187
+ def _on_xscroll(self, first: str, last: str) -> None:
188
+ self._hscroll.set(first, last)
189
+ if self._scrollbar_mode == "auto":
190
+ if float(first) <= 0.0 and float(last) >= 1.0:
191
+ self._hscroll.grid_remove()
192
+ else:
193
+ self._hscroll.grid(row=1, column=self._text_col, sticky="ew")
194
+
195
+ def _update_scrollbar_layout(self) -> None:
196
+ mode = self._scrollbar_mode
197
+ vscroll_col = self._text_col + 1
198
+ if mode in ("vertical", "both", "auto"):
199
+ self._vscroll.grid(row=0, column=vscroll_col, sticky="ns")
200
+ else:
201
+ self._vscroll.grid_remove()
202
+
203
+ if mode == "both":
204
+ self._hscroll.grid(row=1, column=self._text_col, sticky="ew")
205
+ else:
206
+ self._hscroll.grid_remove()
207
+
208
+ if mode == "auto":
209
+ self._vscroll.grid_remove()
210
+ self._hscroll.grid_remove()
211
+
212
+ # ── mousewheel ────────────────────────────────────────────────────────
213
+
214
+ def _setup_mousewheel(self) -> None:
215
+ if self.winsys == "x11":
216
+ self.bind_class(self._scroll_tag, "<Button-4>", self._on_wheel)
217
+ self.bind_class(self._scroll_tag, "<Button-5>", self._on_wheel)
218
+ else:
219
+ self.bind_class(self._scroll_tag, "<MouseWheel>", self._on_wheel)
220
+ tags = list(self.text.bindtags())
221
+ if self._scroll_tag not in tags:
222
+ tags.insert(1, self._scroll_tag)
223
+ self.text.bindtags(tuple(tags))
224
+
225
+ def _on_wheel(self, event: tk.Event) -> None:
226
+ if self.winsys == "win32":
227
+ delta = -int(event.delta / 120)
228
+ elif self.winsys == "aqua":
229
+ delta = -event.delta
230
+ elif event.num == 4:
231
+ delta = -1
232
+ else:
233
+ delta = 1
234
+ self.text.yview_scroll(delta, "units")
235
+
236
+ # ── value / signal ────────────────────────────────────────────────────
237
+
238
+ @property
239
+ def value(self) -> str:
240
+ """Full text content (trailing newline stripped)."""
241
+ return self.text.get("1.0", "end-1c")
242
+
243
+ @value.setter
244
+ def value(self, text: str) -> None:
245
+ was_disabled = self._read_only
246
+ if was_disabled:
247
+ self.text.configure(state=tk.NORMAL)
248
+ self.text.delete("1.0", tk.END)
249
+ if text:
250
+ self.text.insert("1.0", text)
251
+ if was_disabled:
252
+ self.text.configure(state=tk.DISABLED)
253
+
254
+ @property
255
+ def read_only(self) -> bool:
256
+ """Whether the text is visible but not editable."""
257
+ return self._read_only
258
+
259
+ @read_only.setter
260
+ def read_only(self, value: bool) -> None:
261
+ # Keep the flag and the widget state in lockstep: the value/insert
262
+ # setters consult `_read_only` to know whether to lift the disabled
263
+ # state for a programmatic write.
264
+ self._read_only = bool(value)
265
+ self.text.configure(state=tk.DISABLED if self._read_only else tk.NORMAL)
266
+
267
+ def insert(self, index: str, text: str) -> None:
268
+ """Insert text at `index`, applying even when read-only.
269
+
270
+ Read-only blocks *user* edits, not programmatic ones, so this lifts the
271
+ disabled state for the write and restores it (matching the value setter).
272
+ """
273
+ was_disabled = self._read_only
274
+ if was_disabled:
275
+ self.text.configure(state=tk.NORMAL)
276
+ self.text.insert(index, text)
277
+ if was_disabled:
278
+ self.text.configure(state=tk.DISABLED)
279
+
280
+ def bind_signal(self, signal) -> None:
281
+ """Bind a Signal[str] — updates the text when the signal changes."""
282
+ if self._signal is not None:
283
+ self._unbind_signal()
284
+ self._signal = signal
285
+ self._signal_trace_id = signal.subscribe(self._on_signal_change)
286
+ # Set initial value from signal
287
+ v = signal()
288
+ if v is not None:
289
+ self.value = str(v)
290
+
291
+ def _unbind_signal(self) -> None:
292
+ if self._signal is not None and self._signal_trace_id is not None:
293
+ try:
294
+ self._signal_trace_id.cancel()
295
+ except Exception:
296
+ pass
297
+ self._signal = None
298
+ self._signal_trace_id = None
299
+
300
+ def _on_signal_change(self, new_value) -> None:
301
+ if new_value is not None:
302
+ self.value = str(new_value)
303
+
304
+ # ── dirty tracking ────────────────────────────────────────────────────
305
+
306
+ @property
307
+ def is_dirty(self) -> bool:
308
+ """True if the content has changed since last save."""
309
+ return self._undo_manager.is_dirty
310
+
311
+ def mark_saved(self) -> None:
312
+ """Mark current state as the clean baseline."""
313
+ self._undo_manager.mark_saved()
314
+
315
+ # ── undo / redo ───────────────────────────────────────────────────────
316
+
317
+ def undo(self) -> None:
318
+ """Undo the last edit."""
319
+ self._undo_manager.undo()
320
+
321
+ def redo(self) -> None:
322
+ """Redo the last undone edit."""
323
+ self._undo_manager.redo()
324
+
325
+ def undo_block_start(self) -> None:
326
+ """Begin a compound undo block."""
327
+ self._undo_manager.undo_block_start()
328
+
329
+ def undo_block_stop(self) -> None:
330
+ """End a compound undo block."""
331
+ self._undo_manager.undo_block_stop()
332
+
333
+ # ── decoration / style API ────────────────────────────────────────────
334
+
335
+ def register_layer(self, name: str, priority: int = 0) -> None:
336
+ """Register a decoration layer.
337
+
338
+ Args:
339
+ name: Layer identifier. Used to namespace Tk tags.
340
+ priority: Tag raise order — higher priority layers render on top.
341
+ """
342
+ self._decoration_diff.register_layer(name, priority)
343
+
344
+ def define_style(self, name: str, **attrs) -> None:
345
+ """Define a named text style for use in decorations.
346
+
347
+ Args:
348
+ name: Style identifier referenced in decoration objects.
349
+ **attrs: Style attributes — `foreground`, `background`,
350
+ `font`, `underline`, `spacing1`, `spacing3`.
351
+ Values may be theme tokens (e.g. `'primary'`) or literal
352
+ Tk color/font strings.
353
+ """
354
+ self._style_registry.define_style(name, **attrs)
355
+
356
+ def set_decorations(self, layer: str, decorations) -> None:
357
+ """Apply a decoration set to *layer*, diffing against the previous set.
358
+
359
+ Args:
360
+ layer: The decoration layer to update.
361
+ decorations: Sequence of `RangeDecoration` or `LineDecoration`.
362
+ """
363
+ self._decoration_diff.set_decorations(layer, decorations)
364
+
365
+ def clear_decorations(self, layer: str) -> None:
366
+ """Remove all decorations from *layer*.
367
+
368
+ Args:
369
+ layer: The layer to clear.
370
+ """
371
+ self._decoration_diff.clear_layer(layer)
372
+
373
+ # ── internal Tk event enrichment ─────────────────────────────────────
374
+
375
+ def _on_tk_modified(self, _event=None) -> None:
376
+ self.text.event_generate(
377
+ "<<TextModified>>",
378
+ data=TextModifiedEvent(is_dirty=self.is_dirty),
379
+ when="tail",
380
+ )
381
+
382
+ # ── event subscription ────────────────────────────────────────────────
383
+
384
+ def on_change(self, callback: Callable) -> str:
385
+ """Register a callback for `<<Change>>` events.
386
+
387
+ Args:
388
+ callback: Receives the Tk event. `event.data` contains
389
+ `{"op": "insert"|"delete", "index": str}`.
390
+
391
+ Returns:
392
+ Bind ID — pass to `off_change()` to unsubscribe.
393
+ """
394
+ return self.text.bind("<<Change>>", callback, add="+")
395
+
396
+ def off_change(self, bind_id: str | None = None) -> None:
397
+ """Unsubscribe from `<<Change>>`.
398
+
399
+ Args:
400
+ bind_id: ID returned by `on_change()`. If None, removes all.
401
+ """
402
+ self.text.unbind("<<Change>>", bind_id)
403
+
404
+ def on_modified(self, callback: Callable) -> str:
405
+ """Register a callback for `<<TextModified>>` events.
406
+
407
+ Fires when the dirty state changes. `event.is_dirty`
408
+ reflects the new state.
409
+
410
+ Args:
411
+ callback: Receives the Tk event with `event.data = {"is_dirty": bool}`.
412
+
413
+ Returns:
414
+ Bind ID — pass to `off_modified()` to unsubscribe.
415
+ """
416
+ return self.text.bind("<<TextModified>>", callback, add="+")
417
+
418
+ def off_modified(self, bind_id: str | None = None) -> None:
419
+ """Unsubscribe from `<<TextModified>>`.
420
+
421
+ Args:
422
+ bind_id: ID returned by `on_modified()`. If None, removes all.
423
+ """
424
+ self.text.unbind("<<TextModified>>", bind_id)
425
+
426
+ def on_undo(self, callback: Callable) -> str:
427
+ """Register a callback for `<<TextUndo>>` events.
428
+
429
+ Fires after an undo completes. `event.data.text` is the
430
+ text content after the undo.
431
+
432
+ Args:
433
+ callback: Receives the Tk event with `event.data = {"value": str}`.
434
+
435
+ Returns:
436
+ Bind ID — pass to `off_undo()` to unsubscribe.
437
+ """
438
+ return self.text.bind("<<TextUndo>>", callback, add="+")
439
+
440
+ def off_undo(self, bind_id: str | None = None) -> None:
441
+ """Unsubscribe from `<<TextUndo>>`.
442
+
443
+ Args:
444
+ bind_id: ID returned by `on_undo()`. If None, removes all.
445
+ """
446
+ self.text.unbind("<<TextUndo>>", bind_id)
447
+
448
+ def on_redo(self, callback: Callable) -> str:
449
+ """Register a callback for `<<TextRedo>>` events.
450
+
451
+ Fires after a redo completes. `event.data.text` is the
452
+ text content after the redo.
453
+
454
+ Args:
455
+ callback: Receives the Tk event with `event.data = {"value": str}`.
456
+
457
+ Returns:
458
+ Bind ID — pass to `off_redo()` to unsubscribe.
459
+ """
460
+ return self.text.bind("<<TextRedo>>", callback, add="+")
461
+
462
+ def off_redo(self, bind_id: str | None = None) -> None:
463
+ """Unsubscribe from `<<TextRedo>>`.
464
+
465
+ Args:
466
+ bind_id: ID returned by `on_redo()`. If None, removes all.
467
+ """
468
+ self.text.unbind("<<TextRedo>>", bind_id)
469
+
470
+ # ── convenience: forward common Text API to self ──────────────────────
471
+
472
+ def bind(self, sequence=None, func=None, add=None):
473
+ return self.text.bind(sequence, func, add)
474
+
475
+ def unbind(self, sequence, funcid=None):
476
+ return self.text.unbind(sequence, funcid)
477
+
478
+ def focus_set(self):
479
+ self.text.focus_set()
480
+
481
+ # ── cleanup ───────────────────────────────────────────────────────────
482
+
483
+ def _on_destroy(self, event: tk.Event) -> None:
484
+ if event.widget is not self:
485
+ return
486
+ self._unbind_signal()
487
+ self._chain.destroy()
488
+ try:
489
+ if self.winsys == "x11":
490
+ self.unbind_class(self._scroll_tag, "<Button-4>")
491
+ self.unbind_class(self._scroll_tag, "<Button-5>")
492
+ else:
493
+ self.unbind_class(self._scroll_tag, "<MouseWheel>")
494
+ except Exception:
495
+ pass
@@ -0,0 +1,42 @@
1
+ """Decoration types for the TextArea/CodeEditor decoration system."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from typing import Callable, Any
6
+ import tkinter as tk
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Position:
11
+ """A line/column position in the text (1-indexed line, 0-indexed column)."""
12
+ line: int
13
+ col: int
14
+
15
+ def to_tk(self) -> str:
16
+ """Convert to a Tk text index string (e.g. `'3.7'`)."""
17
+ return f"{self.line}.{self.col}"
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class RangeDecoration:
22
+ """A styled range from *start* to *end*."""
23
+ start: Position
24
+ end: Position
25
+ style: str
26
+ meta: dict | None = None
27
+
28
+
29
+ @dataclass(frozen=True)
30
+ class LineDecoration:
31
+ """A style applied to an entire line."""
32
+ line: int
33
+ style: str
34
+ meta: dict | None = None
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class WidgetDecoration:
39
+ """A widget embedded at a position (e.g. inline diagnostic icon)."""
40
+ at: Position
41
+ widget_factory: Callable[[tk.Misc], tk.Widget]
42
+ meta: dict | None = None
@@ -0,0 +1,127 @@
1
+ """Decoration-set diff engine.
2
+
3
+ Per-layer state stored as a dict keyed by (start_tk, end_tk, style).
4
+ Diff is a dict comparison: O(|symmetric difference|) Tk tag calls.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import tkinter as tk
9
+ from typing import Sequence, TYPE_CHECKING
10
+
11
+ from bootstack.widgets._impl.composites.textarea.decoration import (
12
+ LineDecoration, RangeDecoration,
13
+ )
14
+
15
+ if TYPE_CHECKING:
16
+ from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
17
+ from bootstack.widgets._impl.composites.textarea.style_registry import StyleRegistry
18
+
19
+ # Key: (start_tk_index, end_tk_index, style_name)
20
+ _LayerState = dict[tuple[str, str, str], dict | None]
21
+
22
+
23
+ class DecorationDiff:
24
+ """Applies decoration sets to a Text widget using a diff-based approach.
25
+
26
+ Maintains per-layer state so that only changed decorations cause Tk
27
+ tag_add / tag_remove calls. For a 200-decoration viewport with one
28
+ edited token, this is ~2 Tk calls instead of clearing and rebuilding.
29
+ """
30
+
31
+ def __init__(self, core: _MultilineCore, style_registry: StyleRegistry) -> None:
32
+ self._core = core
33
+ self._text = core.text
34
+ self._styles = style_registry
35
+ # layer -> current decoration state
36
+ self._layers: dict[str, _LayerState] = {}
37
+ # layer -> priority (lower = below)
38
+ self._priorities: dict[str, int] = {}
39
+
40
+ def register_layer(self, name: str, priority: int = 0) -> None:
41
+ """Register a decoration layer.
42
+
43
+ Args:
44
+ name: Layer identifier (used in style tag names).
45
+ priority: Tag raise order — higher priority layers appear on top.
46
+ """
47
+ if name not in self._layers:
48
+ self._layers[name] = {}
49
+ self._priorities[name] = priority
50
+
51
+ def set_decorations(
52
+ self,
53
+ layer: str,
54
+ decorations: Sequence[RangeDecoration | LineDecoration],
55
+ ) -> None:
56
+ """Apply a new decoration set to *layer*, diffing against the previous set.
57
+
58
+ Args:
59
+ layer: The layer to update.
60
+ decorations: New complete set of decorations for this layer.
61
+ """
62
+ if layer not in self._layers:
63
+ self.register_layer(layer)
64
+
65
+ new_state = _build_state(decorations)
66
+ old_state = self._layers[layer]
67
+
68
+ old_keys = set(old_state.keys())
69
+ new_keys = set(new_state.keys())
70
+
71
+ removed = old_keys - new_keys
72
+ added = new_keys - old_keys
73
+
74
+ for start, end, style in removed:
75
+ tag = self._styles.tag_name(layer, style)
76
+ try:
77
+ self._text.tag_remove(tag, start, end)
78
+ except tk.TclError:
79
+ pass
80
+
81
+ for start, end, style in added:
82
+ tag = self._styles.tag_name(layer, style)
83
+ if not self._styles.is_configured(layer, style):
84
+ self._styles.configure_tag(layer, style)
85
+ try:
86
+ self._text.tag_add(tag, start, end)
87
+ except tk.TclError:
88
+ pass
89
+
90
+ self._layers[layer] = new_state
91
+ self._raise_layers()
92
+
93
+ def clear_layer(self, layer: str) -> None:
94
+ """Remove all decorations from *layer*."""
95
+ self.set_decorations(layer, [])
96
+
97
+ def _raise_layers(self) -> None:
98
+ """Enforce layer priority order via tag_raise."""
99
+ ordered = sorted(self._priorities.items(), key=lambda x: x[1])
100
+ for layer, _ in ordered:
101
+ state = self._layers.get(layer, {})
102
+ seen_tags: set[str] = set()
103
+ for _, _, style in state:
104
+ tag = self._styles.tag_name(layer, style)
105
+ if tag not in seen_tags:
106
+ seen_tags.add(tag)
107
+ try:
108
+ self._text.tag_raise(tag)
109
+ except tk.TclError:
110
+ pass
111
+
112
+
113
+ def _build_state(
114
+ decorations: Sequence[RangeDecoration | LineDecoration],
115
+ ) -> _LayerState:
116
+ """Convert a list of decorations to the internal dict-key format."""
117
+ state: _LayerState = {}
118
+ for dec in decorations:
119
+ if isinstance(dec, RangeDecoration):
120
+ key = (dec.start.to_tk(), dec.end.to_tk(), dec.style)
121
+ state[key] = dec.meta
122
+ elif isinstance(dec, LineDecoration):
123
+ start = f"{dec.line}.0"
124
+ end = f"{dec.line}.end"
125
+ key = (start, end, dec.style)
126
+ state[key] = dec.meta
127
+ return state
@@ -0,0 +1 @@
1
+ """Built-in TextArea/CodeEditor extensions."""