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,46 @@
1
+ """ChangeNotifier filter — fires <<Change>> after every edit."""
2
+ from __future__ import annotations
3
+
4
+ from typing import TYPE_CHECKING
5
+ from bootstack.widgets._impl.composites.textarea.filter import EditFilter
6
+
7
+ if TYPE_CHECKING:
8
+ from bootstack.widgets._impl.composites.textarea.core import _MultilineCore
9
+
10
+
11
+ class ChangeNotifier(EditFilter):
12
+ """Fires `<<Change>>` on the core's Text widget after every edit.
13
+
14
+ The event payload (`event.data`) is a dict with keys:
15
+ - `op`: `"insert"` or `"delete"`
16
+ - `index`: The position where the edit occurred (Tk index string).
17
+ """
18
+
19
+ def __init__(self) -> None:
20
+ super().__init__()
21
+ self._core: _MultilineCore | None = None
22
+
23
+ def attach(self, core: _MultilineCore) -> None:
24
+ self._core = core
25
+
26
+ def detach(self, core: _MultilineCore) -> None:
27
+ self._core = None
28
+
29
+ def insert(self, index: str, chars: str, tags=None) -> None:
30
+ self.delegate.insert(index, chars, tags)
31
+ self._notify("insert", index)
32
+
33
+ def delete(self, index1: str, index2: str | None = None) -> None:
34
+ self.delegate.delete(index1, index2)
35
+ self._notify("delete", index1)
36
+
37
+ def _notify(self, op: str, index: str) -> None:
38
+ if self._core is not None:
39
+ try:
40
+ self._core.text.event_generate(
41
+ "<<Change>>",
42
+ data={"op": op, "index": index},
43
+ when="tail",
44
+ )
45
+ except Exception:
46
+ pass
@@ -0,0 +1,526 @@
1
+ """CodeEditor — a full-featured code editing widget."""
2
+ from __future__ import annotations
3
+
4
+ import tkinter as tk
5
+ from typing import Callable, Literal
6
+
7
+ from bootstack.widgets._impl.primitives.frame import Frame
8
+ from bootstack.widgets._impl.composites.textarea.core import _MultilineCore, ScrollbarMode
9
+ from bootstack.widgets._impl.composites.textarea.filter import EditFilter
10
+ from bootstack.widgets._impl.composites.textarea.extensions.line_numbers import LineNumbers
11
+ from bootstack.widgets._impl.composites.textarea.extensions.bracket_matcher import BracketMatcher
12
+ from bootstack.widgets._impl.composites.textarea.extensions.smart_indent import SmartIndent
13
+ from bootstack.widgets._impl.composites.textarea.extensions.indent_guides import IndentGuides
14
+ from bootstack.widgets._impl.composites.textarea.search_overlay import SearchOverlay
15
+ from bootstack.widgets.types import Master
16
+
17
+ from bootstack.events import ValidationEvent
18
+ from bootstack.signals import Signal
19
+
20
+ from typing import TYPE_CHECKING
21
+
22
+
23
+ class CodeEditor(Frame):
24
+ """A code editor widget with line numbers, bracket matching, and smart indent.
25
+
26
+ CodeEditor wraps `_MultilineCore` with editor-grade defaults and
27
+ pre-installed extensions. It is not Field-wrapped — use `TextArea`
28
+ for labeled form inputs.
29
+
30
+ Pre-installed extensions:
31
+ - `LineNumbers` sidebar (when `show_line_numbers=True`)
32
+ - `BracketMatcher` — highlights matching bracket pairs
33
+ - `SmartIndent` — auto-indent on Return, Tab-to-spaces
34
+
35
+ Add your own extensions with `editor.install(filter)` and remove
36
+ them with `editor.uninstall(filter)`.
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ master: Master = None,
42
+ *,
43
+ value: str = "",
44
+ textsignal: Signal | None = None,
45
+ language: str | None = None,
46
+ pygments_style: str = "auto",
47
+ light_theme: str = "default",
48
+ dark_theme: str = "monokai",
49
+ read_only: bool = False,
50
+ wrap: bool = False,
51
+ tab_width: int = 4,
52
+ insert_spaces: bool = True,
53
+ auto_indent: bool = True,
54
+ show_line_numbers: bool = True,
55
+ show_indent_guides: bool = False,
56
+ height: int = 20,
57
+ scrollbars: ScrollbarMode = "both",
58
+ font: str = "TkFixedFont",
59
+ show_border: bool = True,
60
+ accent: str = "primary",
61
+ extensions: list[EditFilter] | None = None,
62
+ on_change: Callable | None = None,
63
+ on_cursor_move: Callable | None = None,
64
+ ) -> None:
65
+ """Create a CodeEditor widget.
66
+
67
+ Args:
68
+ master: Parent widget. If None, uses the default root window.
69
+ value: Initial text content.
70
+ textsignal: Reactive `Signal[str]` bound to the editor content.
71
+ language: Pygments lexer name for syntax highlighting (e.g.
72
+ `'python'`, `'sql'`). Pass `None` to disable highlighting.
73
+ pygments_style: Pygments style name for syntax colors (e.g.
74
+ `'default'`, `'monokai'`, `'dracula'`). Defaults to
75
+ `'default'`.
76
+ read_only: If True, the editor is not editable.
77
+ wrap: If True, long lines wrap. Defaults to False (horizontal
78
+ scroll) which is the typical code editor behavior.
79
+ tab_width: Number of spaces per tab stop. Default is 4.
80
+ insert_spaces: If True, Tab inserts spaces. Default is True.
81
+ auto_indent: If True, Return replicates the current line's
82
+ indentation. Default is True.
83
+ show_line_numbers: If True (default), shows a line number
84
+ gutter to the left of the text.
85
+ show_indent_guides: If True, draws subtle vertical guide marks
86
+ at each indent stop. Defaults to False.
87
+ scrollbars: Scrollbar visibility mode. Defaults to `"both"`
88
+ (always show both axes).
89
+ font: Font for the editor. Defaults to the system fixed-width
90
+ font (`"TkFixedFont"`).
91
+ extensions: Additional `EditFilter` instances to install
92
+ on top of the built-in set.
93
+ show_border: If True (default), styles the editor frame as a
94
+ themed border that gains a focus ring on interaction.
95
+ accent: Accent token for the focus border. Defaults to `"primary"`.
96
+ on_change: Callback for `<<Change>>` events.
97
+ on_cursor_move: Callback for cursor movement events.
98
+ """
99
+ frame_kw = (
100
+ {"ttk_class": "TField", "accent": accent, "padding": 5}
101
+ if show_border else {}
102
+ )
103
+ super().__init__(master, **frame_kw)
104
+
105
+ self._language = language
106
+ self._pygments_style = pygments_style
107
+ self._light_theme = light_theme
108
+ self._dark_theme = dark_theme
109
+ self._show_line_numbers = show_line_numbers
110
+ self._tab_width = tab_width
111
+ self._insert_spaces = insert_spaces
112
+ self._highlighter = None
113
+ self._rules: list = []
114
+ self._valid_signal: Signal = Signal(True)
115
+ self._error_signal: Signal = Signal("")
116
+
117
+ # ── core ──────────────────────────────────────────────────────────
118
+ self._core = _MultilineCore(
119
+ self,
120
+ value=value,
121
+ wrap=wrap,
122
+ height=height,
123
+ scrollbars=scrollbars,
124
+ font=font,
125
+ read_only=read_only,
126
+ )
127
+
128
+ if show_border:
129
+ self._core.text.bind(
130
+ "<FocusIn>", lambda _: self.state(["focus"]), add="+",
131
+ )
132
+ self._core.text.bind(
133
+ "<FocusOut>", lambda _: self.state(["!focus"]), add="+",
134
+ )
135
+
136
+ # ── search overlay ────────────────────────────────────────────────
137
+ self._search = SearchOverlay(self, self._core)
138
+
139
+ # _core fills the frame; _search is permanently placed at the bottom
140
+ # and rendered at all times — hidden by keeping _core on top (lift).
141
+ # show_search() just lifts _search; hide_search() lifts _core back.
142
+ # This means there is never a first-render delay on open.
143
+ self._core.pack(fill="both", expand=True)
144
+ self._search.place(
145
+ x=0, rely=1.0, relwidth=1.0,
146
+ height=self._search._height, anchor="sw",
147
+ )
148
+ self._core.lift()
149
+
150
+ # ── signal binding ────────────────────────────────────────────────
151
+ if textsignal is not None:
152
+ self._core.bind_signal(textsignal)
153
+
154
+ # ── pre-installed extensions ──────────────────────────────────────
155
+ if show_line_numbers:
156
+ self._line_numbers = LineNumbers()
157
+ self._core.add_sidebar(self._line_numbers)
158
+ else:
159
+ self._line_numbers = None
160
+
161
+ self._bracket_matcher = BracketMatcher()
162
+ self._core.add_filter(self._bracket_matcher)
163
+
164
+ if auto_indent:
165
+ self._smart_indent = SmartIndent(
166
+ tab_width=tab_width,
167
+ insert_spaces=insert_spaces,
168
+ )
169
+ self._core.add_filter(self._smart_indent)
170
+ else:
171
+ self._smart_indent = None
172
+
173
+ # ── indent guides ─────────────────────────────────────────────────
174
+ if show_indent_guides:
175
+ self._indent_guides = IndentGuides(tab_width=tab_width)
176
+ self._core.add_filter(self._indent_guides)
177
+ else:
178
+ self._indent_guides = None
179
+
180
+ # ── user-supplied extensions ──────────────────────────────────────
181
+ if extensions:
182
+ for ext in extensions:
183
+ self._core.add_filter(ext)
184
+
185
+ # ── syntax highlighting ───────────────────────────────────────────
186
+ if language is not None:
187
+ self._try_install_highlighter(language)
188
+
189
+ # ── callbacks ─────────────────────────────────────────────────────
190
+ if on_change is not None:
191
+ self._core.on_change(on_change)
192
+
193
+ if on_cursor_move is not None:
194
+ self._core.text.bind("<<CursorMove>>", on_cursor_move, add="+")
195
+ self._core.text.bind("<KeyRelease>", on_cursor_move, add="+")
196
+ self._core.text.bind("<ButtonRelease-1>", on_cursor_move, add="+")
197
+
198
+ # ── validation: auto-run on blur ──────────────────────────────────
199
+ self._core.text.bind("<FocusOut>", self._on_focus_out_validate, add="+")
200
+
201
+ # ── public API ────────────────────────────────────────────────────────
202
+
203
+ @property
204
+ def value(self) -> str:
205
+ """Full text content (trailing newline stripped)."""
206
+ return self._core.value
207
+
208
+ @value.setter
209
+ def value(self, text: str) -> None:
210
+ self._core.value = text
211
+
212
+ @property
213
+ def is_dirty(self) -> bool:
214
+ """True if the content has changed since last save."""
215
+ return self._core.is_dirty
216
+
217
+ def mark_saved(self) -> None:
218
+ """Mark the current state as the saved baseline."""
219
+ self._core.mark_saved()
220
+
221
+ def undo(self) -> None:
222
+ """Undo the last edit."""
223
+ self._core.undo()
224
+
225
+ def redo(self) -> None:
226
+ """Redo the last undone edit."""
227
+ self._core.redo()
228
+
229
+ def undo_block_start(self) -> None:
230
+ """Begin a compound undo block."""
231
+ self._core.undo_block_start()
232
+
233
+ def undo_block_stop(self) -> None:
234
+ """End a compound undo block."""
235
+ self._core.undo_block_stop()
236
+
237
+ def goto(self, line: int, col: int = 1) -> None:
238
+ """Move the cursor to a 1-indexed *(line, col)* and scroll it into view.
239
+
240
+ Args:
241
+ line: 1-indexed line number.
242
+ col: 1-indexed column. Defaults to 1 (the start of the line).
243
+ """
244
+ idx = f"{line}.{col - 1}"
245
+ try:
246
+ self._core.text.mark_set("insert", idx)
247
+ self._core.text.see(idx)
248
+ except tk.TclError:
249
+ pass
250
+
251
+ def set_language(self, language: str | None) -> None:
252
+ """Set or change the syntax highlighting language.
253
+
254
+ Args:
255
+ language: Pygments lexer name (e.g. `'python'`), or `None`
256
+ to disable syntax highlighting.
257
+ """
258
+ self._language = language
259
+ if self._highlighter is not None:
260
+ self._core.remove_filter(self._highlighter)
261
+ self._highlighter = None
262
+ if language is not None:
263
+ self._try_install_highlighter(language)
264
+
265
+ def install(self, f: EditFilter) -> None:
266
+ """Install a custom filter extension.
267
+
268
+ Args:
269
+ f: The `EditFilter` to add.
270
+ """
271
+ self._core.add_filter(f)
272
+
273
+ def uninstall(self, f: EditFilter) -> None:
274
+ """Remove a previously installed filter extension.
275
+
276
+ Args:
277
+ f: The `EditFilter` to remove.
278
+ """
279
+ self._core.remove_filter(f)
280
+
281
+ def focus_set(self) -> None:
282
+ """Set focus to the editor."""
283
+ self._core.focus_set()
284
+
285
+ def on_change(self, callback: Callable) -> str:
286
+ """Register a callback for `<<Change>>` events.
287
+
288
+ Args:
289
+ callback: Receives the Tk event.
290
+
291
+ Returns:
292
+ Bind ID — pass to `off_change()` to unsubscribe.
293
+ """
294
+ return self._core.on_change(callback)
295
+
296
+ def off_change(self, bind_id: str | None = None) -> None:
297
+ """Unsubscribe from `<<Change>>`.
298
+
299
+ Args:
300
+ bind_id: ID returned by `on_change()`. If None, removes all.
301
+ """
302
+ self._core.off_change(bind_id)
303
+
304
+ def on_modified(self, callback: Callable) -> str:
305
+ """Register a callback for `<<TextModified>>` events.
306
+
307
+ Args:
308
+ callback: Receives event with `event.data = {"is_dirty": bool}`.
309
+
310
+ Returns:
311
+ Bind ID — pass to `off_modified()` to unsubscribe.
312
+ """
313
+ return self._core.on_modified(callback)
314
+
315
+ def off_modified(self, bind_id: str | None = None) -> None:
316
+ """Unsubscribe from `<<TextModified>>`.
317
+
318
+ Args:
319
+ bind_id: ID returned by `on_modified()`. If None, removes all.
320
+ """
321
+ self._core.off_modified(bind_id)
322
+
323
+ def on_undo(self, callback: Callable) -> str:
324
+ """Register a callback for `<<TextUndo>>` events.
325
+
326
+ Args:
327
+ callback: Receives event with `event.data = {"value": str}`.
328
+
329
+ Returns:
330
+ Bind ID — pass to `off_undo()` to unsubscribe.
331
+ """
332
+ return self._core.on_undo(callback)
333
+
334
+ def off_undo(self, bind_id: str | None = None) -> None:
335
+ """Unsubscribe from `<<TextUndo>>`.
336
+
337
+ Args:
338
+ bind_id: ID returned by `on_undo()`. If None, removes all.
339
+ """
340
+ self._core.off_undo(bind_id)
341
+
342
+ def on_redo(self, callback: Callable) -> str:
343
+ """Register a callback for `<<TextRedo>>` events.
344
+
345
+ Args:
346
+ callback: Receives event with `event.data = {"value": str}`.
347
+
348
+ Returns:
349
+ Bind ID — pass to `off_redo()` to unsubscribe.
350
+ """
351
+ return self._core.on_redo(callback)
352
+
353
+ def off_redo(self, bind_id: str | None = None) -> None:
354
+ """Unsubscribe from `<<TextRedo>>`.
355
+
356
+ Args:
357
+ bind_id: ID returned by `on_redo()`. If None, removes all.
358
+ """
359
+ self._core.off_redo(bind_id)
360
+
361
+ @property
362
+ def core(self) -> _MultilineCore:
363
+ """The internal `_MultilineCore` — for advanced extension use."""
364
+ return self._core
365
+
366
+ @property
367
+ def language(self) -> str | None:
368
+ """Active syntax highlighting language, or `None` if disabled."""
369
+ return self._language
370
+
371
+ @property
372
+ def theme(self) -> str:
373
+ """Active Pygments color scheme, or `'auto'` if tracking the bootstack theme."""
374
+ return self._pygments_style
375
+
376
+ def set_theme(self, v: str) -> None:
377
+ """Change the active Pygments color scheme.
378
+
379
+ Args:
380
+ v: Pygments style name, or `'auto'` to track the bootstack theme.
381
+ """
382
+ self._pygments_style = v
383
+ if self._highlighter is not None:
384
+ self._highlighter._auto = (v == "auto")
385
+ if v != "auto":
386
+ self._highlighter._load_style(v)
387
+ for sname, color in self._highlighter._style_colors.items():
388
+ self._core.define_style(sname, foreground=color)
389
+ self._highlighter._apply_widget_colors(self._core)
390
+ self._highlighter._schedule()
391
+
392
+ def show_search(self) -> None:
393
+ """Show the find bar and focus the search input."""
394
+ self._search.show(replace=False)
395
+
396
+ def show_replace(self) -> None:
397
+ """Show the find/replace bar with the replace row visible."""
398
+ self._search.show(replace=True)
399
+
400
+ def hide_search(self) -> None:
401
+ """Hide the find/replace bar and clear search highlights."""
402
+ self._search.hide()
403
+
404
+ # ── indent/dedent ─────────────────────────────────────────────────────
405
+
406
+ def indent(self) -> None:
407
+ """Indent selected lines by one tab stop, or insert at cursor."""
408
+ if self._smart_indent is not None:
409
+ self._smart_indent.indent()
410
+ else:
411
+ self._indent_or_dedent(indent=True)
412
+
413
+ def dedent(self) -> None:
414
+ """Dedent selected lines (or current line) by one tab stop."""
415
+ if self._smart_indent is not None:
416
+ self._smart_indent.dedent()
417
+ else:
418
+ self._indent_or_dedent(indent=False)
419
+
420
+ def _indent_or_dedent(self, indent: bool) -> None:
421
+ from bootstack.widgets._impl.composites.textarea.extensions.smart_indent import _sel_line_range
422
+ tw = self._core.text
423
+ ranges = tw.tag_ranges("sel")
424
+ if indent:
425
+ prefix = " " * self._tab_width if self._insert_spaces else "\t"
426
+ if not ranges:
427
+ col = int(tw.index("insert").split(".")[1])
428
+ spaces = self._tab_width - (col % self._tab_width)
429
+ tw.insert("insert", " " * spaces if self._insert_spaces else "\t")
430
+ return
431
+ start_line, end_line = _sel_line_range(ranges)
432
+ self._core.undo_block_start()
433
+ try:
434
+ for ln in range(start_line, end_line + 1):
435
+ if tw.get(f"{ln}.0", f"{ln}.end"):
436
+ tw.insert(f"{ln}.0", prefix)
437
+ finally:
438
+ self._core.undo_block_stop()
439
+ tw.tag_add("sel", f"{start_line}.0", f"{end_line}.end")
440
+ else:
441
+ if ranges:
442
+ start_line, end_line = _sel_line_range(ranges)
443
+ else:
444
+ start_line = end_line = int(tw.index("insert").split(".")[0])
445
+ self._core.undo_block_start()
446
+ try:
447
+ for ln in range(start_line, end_line + 1):
448
+ line_text = tw.get(f"{ln}.0", f"{ln}.end")
449
+ if self._insert_spaces:
450
+ n = min(self._tab_width, len(line_text) - len(line_text.lstrip(" ")))
451
+ if n:
452
+ tw.delete(f"{ln}.0", f"{ln}.{n}")
453
+ else:
454
+ if line_text.startswith("\t"):
455
+ tw.delete(f"{ln}.0", f"{ln}.1")
456
+ finally:
457
+ self._core.undo_block_stop()
458
+ if ranges:
459
+ tw.tag_add("sel", f"{start_line}.0", f"{end_line}.end")
460
+
461
+ # ── validation ────────────────────────────────────────────────────────
462
+
463
+ def add_validation_rule(
464
+ self,
465
+ rule_type: str,
466
+ message: str | None = None,
467
+ **kwargs,
468
+ ) -> None:
469
+ """Add a validation rule to this editor.
470
+
471
+ Args:
472
+ rule_type: Rule name — `"required"`, `"pattern"`, `"stringLength"`, etc.
473
+ message: Custom error message. If None, the rule's default is used.
474
+ **kwargs: Rule-specific parameters (e.g. `minLength=`, `func=`).
475
+ """
476
+ from bootstack.validation.validation_rules import ValidationRule
477
+ self._rules.append(ValidationRule(rule_type, message=message, **kwargs))
478
+
479
+ def validate(self) -> bool:
480
+ """Run all validation rules immediately and return True if valid.
481
+
482
+ Triggers `<<Valid>>` / `<<Invalid>>` / `<<Validate>>` events and
483
+ updates the `_valid_signal` and `_error_signal`.
484
+ """
485
+ return self._run_validation(trigger="manual")
486
+
487
+ def _set_validity(self, is_valid: bool, message: str) -> None:
488
+ self._error_signal.set("" if is_valid else message)
489
+ self._valid_signal.set(is_valid)
490
+
491
+ def _run_validation(self, trigger: str = "blur") -> bool:
492
+ value = self.value
493
+ for rule in self._rules:
494
+ if trigger != "manual" and rule.trigger not in ("always", trigger):
495
+ continue
496
+ result = rule.validate(value)
497
+ if not result.is_valid:
498
+ self._set_validity(False, result.message)
499
+ data = ValidationEvent(value=value, is_valid=False, message=result.message)
500
+ self.event_generate("<<Invalid>>", data=data, when="tail")
501
+ self.event_generate("<<Validate>>", data=data, when="tail")
502
+ return False
503
+ self._set_validity(True, "")
504
+ data = ValidationEvent(value=value, is_valid=True, message="")
505
+ self.event_generate("<<Valid>>", data=data, when="tail")
506
+ self.event_generate("<<Validate>>", data=data, when="tail")
507
+ return True
508
+
509
+ def _on_focus_out_validate(self, _event: tk.Event) -> None:
510
+ if self._rules:
511
+ self._run_validation(trigger="blur")
512
+
513
+ # ── internal ─────────────────────────────────────────────────────────
514
+
515
+ def _try_install_highlighter(self, language: str) -> None:
516
+ from bootstack.widgets._impl.composites.textarea.extensions.pygments_highlighter import (
517
+ PygmentsHighlighter,
518
+ )
519
+ h = PygmentsHighlighter(
520
+ language,
521
+ pygments_style=self._pygments_style,
522
+ light_style=self._light_theme,
523
+ dark_style=self._dark_theme,
524
+ )
525
+ self._core.add_filter(h)
526
+ self._highlighter = h