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,312 @@
1
+ """PygmentsHighlighter — syntax highlighting via Pygments.
2
+
3
+ Implements a debounced full-document retokenize strategy: on each
4
+ insert/delete, schedules a retokenize after 150 ms. Tokens are mapped
5
+ to `RangeDecoration` objects and applied via the decoration layer system.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from pygments import lex
12
+ from pygments.lexers import get_lexer_by_name, TextLexer
13
+ from pygments.styles import get_style_by_name
14
+ from pygments.token import Token
15
+ from pygments.util import ClassNotFound
16
+
17
+ from bootstack.widgets._impl.composites.textarea.filter import EditFilter
18
+ from bootstack.widgets._impl.composites.textarea.decoration import Position, RangeDecoration
19
+
20
+ if TYPE_CHECKING:
21
+ from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
22
+
23
+
24
+ _LAYER = "syntax"
25
+ _DEBOUNCE_MS = 150
26
+
27
+ # Token family → bootstack style name. More-specific entries must come
28
+ # before their parents so that the parent-walk hits the right name first.
29
+ _TOKEN_TO_STYLE: dict = {
30
+ Token.String.Doc: "string_doc",
31
+ Token.String: "string",
32
+ Token.Keyword: "keyword",
33
+ Token.Comment: "comment",
34
+ Token.Number: "number",
35
+ Token.Operator: "operator",
36
+ Token.Name.Builtin: "name_builtin",
37
+ Token.Name.Function: "name_function",
38
+ Token.Name.Class: "name_class",
39
+ Token.Name.Decorator: "name_decorator",
40
+ Token.Name.Exception: "name_exception",
41
+ Token.Name.Namespace: "name_namespace",
42
+ Token.Name.Tag: "name_tag", # JSON keys, HTML/XML tags
43
+ Token.Name.Attribute: "name_attribute", # HTML/XML attributes
44
+ Token.Punctuation: "punctuation",
45
+ Token.Error: "error",
46
+ }
47
+
48
+ # Reverse map: style name → canonical token (used for color extraction)
49
+ _STYLE_TO_TOKEN: dict = {v: k for k, v in _TOKEN_TO_STYLE.items()}
50
+
51
+
52
+ def _find_style(ttype) -> str | None:
53
+ """Walk up the Pygments token hierarchy to find the nearest style name."""
54
+ t = ttype
55
+ while t is not None:
56
+ if t in _TOKEN_TO_STYLE:
57
+ return _TOKEN_TO_STYLE[t]
58
+ t = t.parent
59
+ return None
60
+
61
+
62
+ def _extract_colors(style_cls) -> dict[str, str]:
63
+ """Return `{style_name: '#RRGGBB'}` for all style names that have a color."""
64
+ colors: dict[str, str] = {}
65
+ for name, token in _STYLE_TO_TOKEN.items():
66
+ info = style_cls.style_for_token(token)
67
+ color = info.get("color")
68
+ if color:
69
+ colors[name] = f"#{color}"
70
+ return colors
71
+
72
+
73
+ def _contrast_color(hex_color: str) -> str:
74
+ """Return black or white for maximum contrast against `hex_color`."""
75
+ h = hex_color.lstrip("#")
76
+ try:
77
+ r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
78
+ luminance = 0.299 * r + 0.587 * g + 0.114 * b
79
+ return "#000000" if luminance > 128 else "#ffffff"
80
+ except Exception:
81
+ return "#000000"
82
+
83
+
84
+ class PygmentsHighlighter(EditFilter):
85
+ """Syntax highlighting via Pygments.
86
+
87
+ Install via the `language=` parameter on `CodeEditor`, or manually:
88
+
89
+ core.add_filter(PygmentsHighlighter("python"))
90
+
91
+ Uses a debounced full-document retokenize: 150 ms after the last edit the
92
+ full text is re-lexed, tokens are converted to `RangeDecoration` objects,
93
+ and applied via `core.set_decorations()`.
94
+
95
+ Args:
96
+ language: Pygments lexer name (e.g. `'python'`, `'sql'`,
97
+ `'javascript'`). Unknown names fall back to plain text.
98
+ pygments_style: Pygments style name, or `'auto'` to track the
99
+ bootstack theme. When `'auto'`, `light_style` and `dark_style`
100
+ determine the actual Pygments style used.
101
+ light_style: Pygments style used in auto mode when the active
102
+ bootstack theme is light. Default `'default'`.
103
+ dark_style: Pygments style used in auto mode when the active
104
+ bootstack theme is dark. Default `'monokai'`.
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ language: str,
110
+ pygments_style: str = "default",
111
+ light_style: str = "default",
112
+ dark_style: str = "monokai",
113
+ ) -> None:
114
+ super().__init__()
115
+ self._core: _MultilineCore | None = None
116
+ self._after_id: str | None = None
117
+ self._theme_bind_id: str | None = None
118
+ self._theme_root = None
119
+
120
+ self._auto = pygments_style == "auto"
121
+ self._light_style = light_style
122
+ self._dark_style = dark_style
123
+
124
+ try:
125
+ self._lexer = get_lexer_by_name(language, stripall=False, stripnl=False)
126
+ except ClassNotFound:
127
+ self._lexer = TextLexer()
128
+
129
+ # Load initial style — auto resolves after attach when the theme is known.
130
+ initial = self._resolve_auto_name() if self._auto else pygments_style
131
+ self._load_style(initial)
132
+
133
+ # Saved originals — restored on detach.
134
+ self._orig_bg: str | None = None
135
+ self._orig_fg: str | None = None
136
+ self._orig_ibg: str | None = None
137
+
138
+ # ── EditFilter protocol ───────────────────────────────────────────────
139
+
140
+ def attach(self, core: _MultilineCore) -> None:
141
+ self._core = core
142
+
143
+ # Resolve auto style now that we can query the theme provider.
144
+ if self._auto:
145
+ self._load_style(self._resolve_auto_name())
146
+
147
+ core.register_layer(_LAYER, priority=1)
148
+ for sname, color in self._style_colors.items():
149
+ core.define_style(sname, foreground=color)
150
+
151
+ # Save current text-widget colors before overriding them.
152
+ self._orig_bg = core.text.cget("background")
153
+ self._orig_fg = core.text.cget("foreground")
154
+ self._orig_ibg = core.text.cget("insertbackground")
155
+ self._apply_widget_colors(core)
156
+
157
+ # Bind to <<BsThemeChanged>> on the root — fired by bootstack after full
158
+ # rebuild so colors and mode are already accurate when the handler runs.
159
+ self._theme_root = core.winfo_toplevel()
160
+ self._theme_bind_id = self._theme_root.bind(
161
+ "<<BsThemeChanged>>", self._on_framework_theme_changed, add="+"
162
+ )
163
+ self._schedule()
164
+
165
+ def detach(self, core: _MultilineCore) -> None:
166
+ self._cancel()
167
+ if self._theme_bind_id is not None and self._theme_root is not None:
168
+ try:
169
+ self._theme_root.unbind("<<BsThemeChanged>>", self._theme_bind_id)
170
+ except Exception:
171
+ pass
172
+ self._theme_bind_id = None
173
+ self._theme_root = None
174
+ if self._core is not None:
175
+ core.clear_decorations(_LAYER)
176
+ self._restore_widget_colors(core)
177
+ self._core = None
178
+
179
+ def insert(self, index: str, chars: str, tags=None) -> None:
180
+ self.delegate.insert(index, chars, tags)
181
+ self._schedule()
182
+
183
+ def delete(self, index1: str, index2: str | None = None) -> None:
184
+ self.delegate.delete(index1, index2)
185
+ self._schedule()
186
+
187
+ # ── internal ──────────────────────────────────────────────────────────
188
+
189
+ def _apply_widget_colors(self, core: _MultilineCore, notify: bool = True) -> None:
190
+ kw: dict = {"background": self._style_bg}
191
+ if self._style_fg:
192
+ kw["foreground"] = self._style_fg
193
+ kw["insertbackground"] = self._style_fg or _contrast_color(self._style_bg)
194
+ try:
195
+ core.text.configure(**kw)
196
+ if notify:
197
+ # Notify extensions that the editor background changed so they
198
+ # can re-calibrate colors. Only fire on deliberate style
199
+ # application (attach / set_language), not on every theme
200
+ # rebuild — extensions already react to <<BsThemeChanged>>
201
+ # directly and cascading it causes unnecessary repaints.
202
+ core.text.event_generate("<<EditorBgChanged>>", when="tail")
203
+ except Exception:
204
+ pass
205
+
206
+ def _restore_widget_colors(self, core: _MultilineCore) -> None:
207
+ kw: dict = {}
208
+ if self._orig_bg is not None:
209
+ kw["background"] = self._orig_bg
210
+ if self._orig_fg is not None:
211
+ kw["foreground"] = self._orig_fg
212
+ if self._orig_ibg is not None:
213
+ kw["insertbackground"] = self._orig_ibg
214
+ if kw:
215
+ try:
216
+ core.text.configure(**kw)
217
+ except Exception:
218
+ pass
219
+
220
+ def _resolve_auto_name(self) -> str:
221
+ """Return the appropriate Pygments style name for the current theme mode."""
222
+ try:
223
+ from bootstack.style.style import get_theme_provider
224
+ mode = get_theme_provider().mode
225
+ except Exception:
226
+ mode = "light"
227
+ return self._dark_style if mode == "dark" else self._light_style
228
+
229
+ def _load_style(self, style_name: str) -> None:
230
+ """Load a Pygments style, updating all color state."""
231
+ try:
232
+ style_cls = get_style_by_name(style_name)
233
+ except ClassNotFound:
234
+ style_cls = get_style_by_name("default")
235
+ self._style_colors = _extract_colors(style_cls)
236
+ self._active_styles: frozenset[str] = frozenset(self._style_colors)
237
+ self._style_bg: str = style_cls.background_color
238
+ raw_fg = (
239
+ style_cls.style_for_token(Token.Text).get("color")
240
+ or style_cls.style_for_token(Token).get("color")
241
+ )
242
+ self._style_fg: str | None = f"#{raw_fg}" if raw_fg else None
243
+
244
+ def _on_framework_theme_changed(self, event=None) -> None:
245
+ """Called via <<BsThemeChanged>> after _rebuild_all_styles() completes."""
246
+ if self._core is None:
247
+ return
248
+ if self._auto:
249
+ mode = (event.data or {}).get("mode") if event else None
250
+ resolved = self._dark_style if mode == "dark" else self._resolve_auto_name()
251
+ self._load_style(resolved)
252
+ for sname, color in self._style_colors.items():
253
+ self._core.define_style(sname, foreground=color)
254
+ self._apply_widget_colors(self._core, notify=True)
255
+ self._schedule()
256
+
257
+ def _schedule(self) -> None:
258
+ self._cancel()
259
+ if self._core is not None:
260
+ self._after_id = self._core.text.after(_DEBOUNCE_MS, self._retokenize)
261
+
262
+ def _cancel(self) -> None:
263
+ if self._after_id is not None and self._core is not None:
264
+ try:
265
+ self._core.text.after_cancel(self._after_id)
266
+ except Exception:
267
+ pass
268
+ self._after_id = None
269
+
270
+ def _retokenize(self) -> None:
271
+ self._after_id = None
272
+ if self._core is None:
273
+ return
274
+ try:
275
+ text = self._core.value
276
+ except Exception:
277
+ return
278
+
279
+ if not text:
280
+ self._core.clear_decorations(_LAYER)
281
+ return
282
+
283
+ decorations: list[RangeDecoration] = []
284
+ line, col = 1, 0
285
+ active = self._active_styles
286
+
287
+ for ttype, value in lex(text, self._lexer):
288
+ if value:
289
+ style = _find_style(ttype)
290
+ if style is not None and style in active:
291
+ if "\n" in value:
292
+ vlines = value.split("\n")
293
+ end_line = line + len(vlines) - 1
294
+ end_col = len(vlines[-1])
295
+ else:
296
+ end_line = line
297
+ end_col = col + len(value)
298
+ decorations.append(RangeDecoration(
299
+ Position(line, col),
300
+ Position(end_line, end_col),
301
+ style,
302
+ ))
303
+
304
+ # Advance position regardless of whether we added a decoration.
305
+ if "\n" in value:
306
+ vlines = value.split("\n")
307
+ line += len(vlines) - 1
308
+ col = len(vlines[-1])
309
+ else:
310
+ col += len(value)
311
+
312
+ self._core.set_decorations(_LAYER, decorations)
@@ -0,0 +1,177 @@
1
+ """SmartIndent — auto-indent on Enter and Tab-to-spaces conversion.
2
+
3
+ Filter-style extension: intercepts Return and Tab key events to provide
4
+ editor-grade indentation. Does not intercept insert/delete in the chain
5
+ (key bindings are used instead to avoid interfering with clipboard ops).
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import tkinter as tk
10
+ from typing import TYPE_CHECKING
11
+
12
+ from bootstack.widgets._impl.composites.textarea.filter import EditFilter
13
+
14
+ if TYPE_CHECKING:
15
+ from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
16
+
17
+
18
+ class SmartIndent(EditFilter):
19
+ """Provides auto-indent on Return, Tab-to-spaces, and block indent/dedent.
20
+
21
+ On `<Return>`:
22
+ Inserts a newline followed by the same leading whitespace as the
23
+ current line, so the cursor lands at the same indent level.
24
+
25
+ On `<Tab>` with a selection:
26
+ Indents every selected line by one tab stop.
27
+
28
+ On `<Tab>` with no selection (when `insert_spaces=True`):
29
+ Inserts spaces to the next tab stop instead of a literal tab character.
30
+
31
+ On `<Shift-Tab>`:
32
+ Dedents every selected line (or the current line) by one tab stop.
33
+
34
+ Install via `core.add_filter(SmartIndent())`.
35
+ """
36
+
37
+ def __init__(self, tab_width: int = 4, insert_spaces: bool = True) -> None:
38
+ super().__init__()
39
+ self._tab_width = tab_width
40
+ self._insert_spaces = insert_spaces
41
+ self._core: _MultilineCore | None = None
42
+
43
+ def attach(self, core: _MultilineCore) -> None:
44
+ self._core = core
45
+ core.text.bind("<Return>", self._on_return, add="+")
46
+ core.text.bind("<Tab>", self._on_tab, add="+")
47
+ core.text.bind("<Shift-Tab>", self._on_shift_tab, add="+")
48
+ core.text.bind("<ISO_Left_Tab>", self._on_shift_tab, add="+") # Linux/X11
49
+
50
+ def detach(self, core: _MultilineCore) -> None:
51
+ self._core = None
52
+
53
+ # ── filter pass-through ───────────────────────────────────────────────
54
+
55
+ def insert(self, index: str, chars: str, tags=None) -> None:
56
+ self.delegate.insert(index, chars, tags)
57
+
58
+ def delete(self, index1: str, index2: str | None = None) -> None:
59
+ self.delegate.delete(index1, index2)
60
+
61
+ # ── public block operations ───────────────────────────────────────────
62
+
63
+ def indent(self) -> None:
64
+ """Indent selected lines by one tab stop, or insert at cursor."""
65
+ if self._core is None:
66
+ return
67
+ tw = self._core.text
68
+ ranges = tw.tag_ranges("sel")
69
+ if ranges:
70
+ self._indent_block(tw, ranges)
71
+ else:
72
+ col = int(tw.index("insert").split(".")[1])
73
+ spaces = self._tab_width - (col % self._tab_width)
74
+ tw.insert("insert", " " * spaces if self._insert_spaces else "\t")
75
+
76
+ def dedent(self) -> None:
77
+ """Dedent selected lines (or current line) by one tab stop."""
78
+ if self._core is None:
79
+ return
80
+ self._dedent_block(self._core.text, self._core.text.tag_ranges("sel"))
81
+
82
+ # ── key handlers ─────────────────────────────────────────────────────
83
+
84
+ def _on_return(self, _event: tk.Event) -> str:
85
+ """Insert a newline with matching indentation."""
86
+ if self._core is None:
87
+ return ""
88
+ text = self._core.text
89
+ cursor = text.index("insert")
90
+ line_start = text.index(f"{cursor} linestart")
91
+ line_text = text.get(line_start, cursor)
92
+ indent = _leading_whitespace(line_text)
93
+ # Use the undo block API so auto-indent is one undo step with the newline
94
+ self._core.undo_block_start()
95
+ text.insert("insert", f"\n{indent}")
96
+ self._core.undo_block_stop()
97
+ return "break" # prevent default Return handling
98
+
99
+ def _on_tab(self, _event: tk.Event) -> str | None:
100
+ """Indent selection, or insert spaces / fall through for a literal tab."""
101
+ if self._core is None:
102
+ return None
103
+ tw = self._core.text
104
+ ranges = tw.tag_ranges("sel")
105
+ if ranges:
106
+ self._indent_block(tw, ranges)
107
+ return "break"
108
+ if self._insert_spaces:
109
+ col = int(tw.index("insert").split(".")[1])
110
+ spaces = self._tab_width - (col % self._tab_width)
111
+ tw.insert("insert", " " * spaces)
112
+ return "break"
113
+ # insert_spaces=False, no selection: let native Tab insert \t
114
+ return None
115
+
116
+ def _on_shift_tab(self, _event: tk.Event) -> str:
117
+ """Dedent selection or current line."""
118
+ if self._core is None:
119
+ return "break"
120
+ self._dedent_block(self._core.text, self._core.text.tag_ranges("sel"))
121
+ return "break"
122
+
123
+ # ── helpers ───────────────────────────────────────────────────────────
124
+
125
+ def _indent_block(self, tw: tk.Text, ranges: tuple) -> None:
126
+ start_line, end_line = _sel_line_range(ranges)
127
+ indent = " " * self._tab_width if self._insert_spaces else "\t"
128
+ self._core.undo_block_start()
129
+ try:
130
+ for ln in range(start_line, end_line + 1):
131
+ if tw.get(f"{ln}.0", f"{ln}.end"): # skip blank lines
132
+ tw.insert(f"{ln}.0", indent)
133
+ finally:
134
+ self._core.undo_block_stop()
135
+ tw.tag_add("sel", f"{start_line}.0", f"{end_line}.end")
136
+
137
+ def _dedent_block(self, tw: tk.Text, ranges: tuple) -> None:
138
+ if ranges:
139
+ start_line, end_line = _sel_line_range(ranges)
140
+ else:
141
+ start_line = end_line = int(tw.index("insert").split(".")[0])
142
+ self._core.undo_block_start()
143
+ try:
144
+ for ln in range(start_line, end_line + 1):
145
+ line_text = tw.get(f"{ln}.0", f"{ln}.end")
146
+ if self._insert_spaces:
147
+ n = min(self._tab_width, len(line_text) - len(line_text.lstrip(" ")))
148
+ if n:
149
+ tw.delete(f"{ln}.0", f"{ln}.{n}")
150
+ else:
151
+ if line_text.startswith("\t"):
152
+ tw.delete(f"{ln}.0", f"{ln}.1")
153
+ finally:
154
+ self._core.undo_block_stop()
155
+ if ranges:
156
+ tw.tag_add("sel", f"{start_line}.0", f"{end_line}.end")
157
+
158
+
159
+ def _leading_whitespace(line: str) -> str:
160
+ """Return the leading whitespace characters of *line*."""
161
+ return line[: len(line) - len(line.lstrip())]
162
+
163
+
164
+ def _sel_line_range(ranges: tuple) -> tuple[int, int]:
165
+ """Return (start_line, end_line) from a tag_ranges("sel") result.
166
+
167
+ If the selection ends exactly at column 0 of the last line (i.e. the user
168
+ selected to the very start of a line without including any content), that
169
+ trailing line is excluded so we don't indent/dedent a line the user didn't
170
+ intend to touch.
171
+ """
172
+ start_str, end_str = str(ranges[0]), str(ranges[1])
173
+ start_line = int(start_str.split(".")[0])
174
+ end_line = int(end_str.split(".")[0])
175
+ if end_str.split(".")[1] == "0" and end_line > start_line:
176
+ end_line -= 1
177
+ return start_line, end_line
@@ -0,0 +1,171 @@
1
+ """EditFilter base class and FilterChain.
2
+
3
+ Adaptation of IDLE's Delegator/Percolator pattern
4
+ (idlelib/delegator.py + idlelib/percolator.py).
5
+
6
+ WidgetRedirector intercepts every insert/delete at the Tk command level —
7
+ including keyboard input from Tk's default text bindings — and routes them
8
+ through the Python filter chain. The bottom of the chain calls the original
9
+ Tk command via OriginalCommand objects returned by redirector.register(),
10
+ which are clean Python callables, not raw Tcl strings.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import tkinter as tk
15
+ from idlelib.redirector import WidgetRedirector
16
+ from typing import TYPE_CHECKING
17
+
18
+ if TYPE_CHECKING:
19
+ from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
20
+
21
+
22
+ # NOTE(editfilter-public-api): EditFilter was demoted from the top-level public
23
+ # namespace during the API Reference restructure (Stage 4). It is a Tk-coupled
24
+ # extension hook — subclasses work in raw Tk text indices, tags, and the Tk Text
25
+ # API — which contradicts the framework's "Tkinter is invisible" premise, and it
26
+ # ships no narrative docs. CodeEditor still accepts user filters via
27
+ # `extensions=`/`install()`, so it stays importable for power users, but it is no
28
+ # longer presented as compose surface. TODO: decide whether CodeEditor should have
29
+ # a real, documented, de-Tkinter-ed extension/plugin API (one that hides Tk text
30
+ # indices behind framework-level edit events) and, if so, design that as the public
31
+ # face — re-promoting only once it no longer leaks Tk. See memory
32
+ # project_editfilter_public_api.
33
+ class EditFilter:
34
+ """Sits in the mutation chain between user code and the Tk Text widget.
35
+
36
+ Subclass and override `insert` and/or `delete` to intercept edits.
37
+ Always forward to `self.delegate.insert` / `self.delegate.delete`
38
+ unless you intend to suppress the edit.
39
+
40
+ Attribute reads (`self.get(...)`, `self.tag_add(...)`, etc.) forward
41
+ automatically to the next delegate via `__getattr__`, so a filter
42
+ behaves like the Text widget itself when reading state.
43
+ """
44
+
45
+ def __init__(self) -> None:
46
+ self.delegate: EditFilter | _BottomFilter | None = None
47
+ self._cache: set[str] = set()
48
+
49
+ def __getattr__(self, name: str):
50
+ if name.startswith('_') or self.delegate is None:
51
+ raise AttributeError(name)
52
+ attr = getattr(self.delegate, name)
53
+ setattr(self, name, attr)
54
+ self._cache.add(name)
55
+ return attr
56
+
57
+ def _reset_cache(self) -> None:
58
+ for k in self._cache:
59
+ try:
60
+ delattr(self, k)
61
+ except AttributeError:
62
+ pass
63
+ self._cache.clear()
64
+
65
+ def _set_delegate(self, d: EditFilter | _BottomFilter | None) -> None:
66
+ self._reset_cache()
67
+ self.delegate = d
68
+
69
+ def insert(self, index: str, chars: str, tags=None) -> None:
70
+ """Forward an insert to the next delegate."""
71
+ self.delegate.insert(index, chars, tags)
72
+
73
+ def delete(self, index1: str, index2: str | None = None) -> None:
74
+ """Forward a delete to the next delegate."""
75
+ self.delegate.delete(index1, index2)
76
+
77
+ def attach(self, core: _MultilineCore) -> None:
78
+ """Called when this filter is added to a core's chain."""
79
+
80
+ def detach(self, core: _MultilineCore) -> None:
81
+ """Called when this filter is removed from a core's chain."""
82
+
83
+
84
+ class _BottomFilter:
85
+ """Terminal filter — calls the original Tk Text command via OriginalCommand.
86
+
87
+ WidgetRedirector.register() returns an OriginalCommand callable for each
88
+ intercepted operation. Calling it routes directly to the renamed Tcl proc
89
+ (e.g. `.!text_orig insert ...`) without going back through the chain.
90
+ """
91
+
92
+ def __init__(self, orig_insert, orig_delete, text: tk.Text) -> None:
93
+ self._orig_insert = orig_insert # OriginalCommand returned by register()
94
+ self._orig_delete = orig_delete # OriginalCommand returned by register()
95
+ self._text = text
96
+
97
+ def __getattr__(self, name: str):
98
+ return getattr(self._text, name)
99
+
100
+ def insert(self, index: str, chars: str, tags=None) -> None:
101
+ if tags:
102
+ self._orig_insert(index, chars, tags)
103
+ else:
104
+ self._orig_insert(index, chars)
105
+
106
+ def delete(self, index1: str, index2: str | None = None) -> None:
107
+ if index2 is not None:
108
+ self._orig_delete(index1, index2)
109
+ else:
110
+ self._orig_delete(index1)
111
+
112
+ def _set_delegate(self, d) -> None:
113
+ pass
114
+
115
+
116
+ class FilterChain:
117
+ """Owns the ordered chain of EditFilters.
118
+
119
+ Uses WidgetRedirector so that every insert/delete on the Text widget —
120
+ including those triggered by Tk's default key bindings — flows through
121
+ the chain before reaching the underlying Tk Text widget.
122
+ """
123
+
124
+ def __init__(self, text: tk.Text) -> None:
125
+ self._text = text
126
+ self._redirector = WidgetRedirector(text)
127
+ self._filters: list[EditFilter] = []
128
+
129
+ # Register our handlers; register() returns OriginalCommand callables
130
+ orig_insert = self._redirector.register('insert', self._chain_insert)
131
+ orig_delete = self._redirector.register('delete', self._chain_delete)
132
+
133
+ self._bottom = _BottomFilter(orig_insert, orig_delete, text)
134
+ self._top: EditFilter | _BottomFilter = self._bottom
135
+
136
+ def _chain_insert(self, index: str, chars: str, tags=None) -> None:
137
+ self._top.insert(index, chars, tags)
138
+
139
+ def _chain_delete(self, index1: str, index2: str | None = None) -> None:
140
+ self._top.delete(index1, index2)
141
+
142
+ def add(self, f: EditFilter) -> None:
143
+ """Prepend a filter to the top of the chain."""
144
+ f._set_delegate(self._top)
145
+ self._top = f
146
+ self._filters.insert(0, f)
147
+
148
+ def remove(self, f: EditFilter) -> None:
149
+ """Remove a filter from the chain."""
150
+ if f not in self._filters:
151
+ return
152
+ idx = self._filters.index(f)
153
+ above = self._filters[idx - 1] if idx > 0 else None
154
+ below = self._filters[idx + 1] if idx < len(self._filters) - 1 else self._bottom
155
+
156
+ if above is None:
157
+ self._top = below
158
+ else:
159
+ above._set_delegate(below)
160
+
161
+ f._set_delegate(None)
162
+ self._filters.pop(idx)
163
+
164
+ def destroy(self) -> None:
165
+ """Uninstall the redirector."""
166
+ try:
167
+ self._redirector.unregister('insert')
168
+ self._redirector.unregister('delete')
169
+ self._redirector.close()
170
+ except Exception:
171
+ pass