demo_text_edit.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681
  1. # Part of ImGui Bundle - MIT License - Copyright (c) 2022-2026 Pascal Thomet - https://github.com/pthom/imgui_bundle
  2. import inspect
  3. import textwrap
  4. from typing import Callable, Any
  5. from imgui_bundle import imgui, imgui_color_text_edit as ed, imgui_md, ImVec2
  6. from imgui_bundle.immapp import static
  7. TextEditor = ed.TextEditor
  8. TextDiff = ed.TextDiff
  9. # ============================================================================
  10. # Source display helper: shows the code of a demo function in a collapsible section
  11. # ============================================================================
  12. _source_show_flags: dict[str, bool] = {}
  13. _source_editors: dict[str, TextEditor] = {}
  14. # Map function names to their actual function objects (filled by each demo function)
  15. _source_functions: dict[str, Callable[..., Any]] = {}
  16. def _show_source_toggle(func: Callable[..., Any]) -> None:
  17. """Show a 'Show source' checkbox. When checked, displays the function's source
  18. (obtained via inspect.getsource) in a read-only editor with light theme."""
  19. func_name = func.__name__
  20. if func_name not in _source_show_flags:
  21. _source_show_flags[func_name] = False
  22. _, _source_show_flags[func_name] = imgui.checkbox("Show source", _source_show_flags[func_name])
  23. if _source_show_flags[func_name]:
  24. imgui.separator_text("Source")
  25. if func_name not in _source_editors:
  26. source = textwrap.dedent(inspect.getsource(func))
  27. editor = TextEditor()
  28. editor.set_text(source)
  29. editor.set_language(TextEditor.Language.python())
  30. editor.set_palette(TextEditor.get_light_palette())
  31. editor.set_read_only_enabled(True)
  32. editor.set_carets_visible(False)
  33. _source_editors[func_name] = editor
  34. code_font = imgui_md.get_code_font()
  35. imgui.push_font(code_font.font, code_font.size)
  36. _source_editors[func_name].render(f"##src_{func_name}", ImVec2(-1, imgui.get_text_line_height() * 15), False)
  37. imgui.pop_font()
  38. imgui.separator_text("Demo")
  39. # ============================================================================
  40. # Tab 1: Basic Editor
  41. # Demonstrates: text loading, language selection, palette switching
  42. # ============================================================================
  43. @static(initialized=False, editor=None, lang_idx=3) # default: Python
  44. def demo_basic_editor():
  45. _show_source_toggle(demo_basic_editor)
  46. statics = demo_basic_editor
  47. if not statics.initialized:
  48. statics.editor = TextEditor()
  49. with open(__file__, encoding="utf8") as f:
  50. code = f.read()
  51. statics.editor.set_text(code)
  52. statics.editor.set_language(TextEditor.Language.python())
  53. statics.initialized = True
  54. editor = statics.editor
  55. # Palette buttons
  56. if imgui.small_button("Dark"):
  57. editor.set_palette(TextEditor.get_dark_palette())
  58. imgui.same_line()
  59. if imgui.small_button("Light"):
  60. editor.set_palette(TextEditor.get_light_palette())
  61. # Language selection
  62. imgui.same_line()
  63. imgui.set_next_item_width(imgui.calc_text_size("AngelScript__").x)
  64. lang_names = ["None", "C++", "C", "Python", "GLSL", "HLSL", "Lua", "SQL", "AngelScript", "C#", "JSON", "Markdown"]
  65. changed, statics.lang_idx = imgui.combo("Language", statics.lang_idx, lang_names)
  66. if changed:
  67. langs = [
  68. None,
  69. TextEditor.Language.cpp(), TextEditor.Language.c(),
  70. TextEditor.Language.python(), TextEditor.Language.glsl(),
  71. TextEditor.Language.hlsl(), TextEditor.Language.lua(),
  72. TextEditor.Language.sql(), TextEditor.Language.angel_script(),
  73. TextEditor.Language.cs(), TextEditor.Language.json(),
  74. TextEditor.Language.markdown(),
  75. ]
  76. editor.set_language(langs[statics.lang_idx])
  77. # Cursor position display (doc coords: row != line once word-wrap is on)
  78. imgui.same_line()
  79. pos = editor.get_main_cursor_position()
  80. imgui.text(f"Line: {pos.line + 1} Col: {pos.index + 1} (doc)")
  81. # Second row: editor view/behavior toggles
  82. wrap = editor.is_word_wrap_enabled()
  83. changed, wrap = imgui.checkbox("Word Wrap", wrap)
  84. if changed:
  85. editor.set_word_wrap_enabled(wrap)
  86. imgui.same_line()
  87. read_only = editor.is_read_only_enabled()
  88. changed, read_only = imgui.checkbox("Read Only", read_only)
  89. if changed:
  90. editor.set_read_only_enabled(read_only)
  91. imgui.same_line()
  92. folding = editor.is_line_folding_enabled()
  93. changed, folding = imgui.checkbox("Line Folding", folding)
  94. if changed:
  95. editor.set_line_folding_enabled(folding)
  96. imgui.same_line()
  97. imgui.begin_disabled(not folding)
  98. if imgui.small_button("Unfold All"):
  99. editor.unfold_all()
  100. imgui.end_disabled()
  101. imgui.same_line()
  102. mini_map = editor.is_show_mini_map_enabled()
  103. changed, mini_map = imgui.checkbox("Show Mini Map", mini_map)
  104. if changed:
  105. editor.set_show_mini_map_enabled(mini_map)
  106. imgui.text_disabled("(folding uses brackets for C/C++, indentation for Python)")
  107. # Render editor: we shall use a monospace font
  108. code_font = imgui_md.get_code_font()
  109. imgui.push_font(code_font.font, code_font.size)
  110. editor.render("##basic")
  111. imgui.pop_font()
  112. # ============================================================================
  113. # Tab 2: Change Callback
  114. # Demonstrates: detecting edits via set_change_callback
  115. # ============================================================================
  116. @static(initialized=False, editor=None, change_count=0)
  117. def demo_change_callback():
  118. _show_source_toggle(demo_change_callback)
  119. statics = demo_change_callback
  120. if not statics.initialized:
  121. statics.editor = TextEditor()
  122. statics.editor.set_text("Edit this text to see the change callback in action.\n\nTry typing, deleting, or pasting.\n")
  123. statics.editor.set_language(TextEditor.Language.python())
  124. statics.editor.set_change_callback(lambda: setattr(statics, 'change_count', statics.change_count + 1), 200)
  125. statics.initialized = True
  126. editor = statics.editor
  127. imgui.text(f"Change count: {statics.change_count}")
  128. code_font = imgui_md.get_code_font()
  129. imgui.push_font(code_font.font, code_font.size)
  130. editor.render("##changes")
  131. imgui.pop_font()
  132. # ============================================================================
  133. # Tab 3: Filters
  134. # Demonstrates: filter_selections to transform selected text
  135. # ============================================================================
  136. @static(initialized=False, editor=None)
  137. def demo_filters():
  138. _show_source_toggle(demo_filters)
  139. statics = demo_filters
  140. if not statics.initialized:
  141. statics.editor = TextEditor()
  142. statics.editor.set_text(
  143. "Select some text below, then click a filter button.\n"
  144. "\n"
  145. "Hello World\n"
  146. "the quick brown fox jumps over the lazy dog\n"
  147. "SOME UPPERCASE TEXT\n"
  148. "mixed Case Text Here\n"
  149. )
  150. statics.initialized = True
  151. editor = statics.editor
  152. imgui.text("Select text, then apply a filter:")
  153. imgui.same_line()
  154. if imgui.small_button("UPPER"):
  155. editor.filter_selections(lambda text: text.upper())
  156. imgui.same_line()
  157. if imgui.small_button("lower"):
  158. editor.filter_selections(lambda text: text.lower())
  159. imgui.same_line()
  160. if imgui.small_button("Strip trailing spaces"):
  161. editor.strip_trailing_whitespaces()
  162. code_font = imgui_md.get_code_font()
  163. imgui.push_font(code_font.font, code_font.size)
  164. editor.render("##filters")
  165. imgui.pop_font()
  166. # ============================================================================
  167. # Tab 4: Annotations & Interactivity
  168. # Demonstrates four ways to annotate or react inside an editor:
  169. # - Line decorator (custom-drawn gutter): red circle on breakpoint lines
  170. # - Line markers (colored gutter + tooltips): error & warning markers
  171. # - Context menus on line numbers and on text (right-click)
  172. # - Hover callback in text: live popup showing the word under the cursor
  173. # ============================================================================
  174. @static(initialized=False, editor=None, breakpoints=None, last_action="")
  175. def demo_decorators_and_context_menus():
  176. _show_source_toggle(demo_decorators_and_context_menus)
  177. statics = demo_decorators_and_context_menus
  178. if not statics.initialized:
  179. statics.breakpoints = set()
  180. statics.editor = TextEditor()
  181. statics.editor.set_text(
  182. "import math\n"
  183. "from typing import List\n"
  184. "\n"
  185. "# Compute summary statistics for a list of numbers.\n"
  186. "def stats(values):\n"
  187. " if not values:\n"
  188. " return None\n"
  189. " total = sum(values)\n"
  190. " count = len(values)\n"
  191. " mean = total / count\n"
  192. " variance = sum((v - mean) ** 2 for v in values) / count\n"
  193. " unused = 'leftover'\n"
  194. " return mean, math.sqrt(variance)\n"
  195. "\n"
  196. "def greet(name):\n"
  197. ' print(f"Hello {name}")\n'
  198. " return name\n"
  199. "\n"
  200. "def divide(a, b):\n"
  201. " return a // b\n"
  202. "\n"
  203. "def main():\n"
  204. " data = [1, 2, 3, 4, 5]\n"
  205. " m, s = stats(data)\n"
  206. ' print(f"mean={m} std={s}")\n'
  207. ' greet("world")\n'
  208. " print(divide(10, 0))\n"
  209. "\n"
  210. 'if __name__ == "__main__":\n'
  211. " main()\n"
  212. )
  213. statics.editor.set_language(TextEditor.Language.python())
  214. # Markers: persistent colored bands in the gutter / text background,
  215. # with built-in tooltips on hover (no callback needed for the tooltip).
  216. # Spread across the file so the scrollbar mini map has visible ticks.
  217. # Line numbers are zero-based.
  218. warning_color = imgui.IM_COL32(180, 140, 0, 255)
  219. warning_bg = imgui.IM_COL32(180, 140, 0, 48)
  220. error_color = imgui.IM_COL32(220, 50, 50, 255)
  221. error_bg = imgui.IM_COL32(220, 50, 50, 56)
  222. statics.editor.add_marker(
  223. 1, warning_color, warning_bg,
  224. "warning", "Imported but never used: 'List'",
  225. )
  226. statics.editor.add_marker(
  227. 11, warning_color, warning_bg,
  228. "warning", "Unused variable: 'unused'",
  229. )
  230. statics.editor.add_marker(
  231. 19, error_color, error_bg,
  232. "error", "Unchecked division: b may be zero",
  233. )
  234. statics.editor.add_marker(
  235. 26, error_color, error_bg,
  236. "error", "ZeroDivisionError at runtime: divide(10, 0)",
  237. )
  238. # Decorator: draw a red circle on lines that have a breakpoint
  239. def decorator_callback(decorator: TextEditor.Decorator):
  240. if decorator.line in statics.breakpoints:
  241. cursor_pos = imgui.get_cursor_screen_pos()
  242. center = ImVec2(
  243. cursor_pos.x + decorator.glyph_size.x * 0.5,
  244. cursor_pos.y + decorator.glyph_size.y * 0.5,
  245. )
  246. radius = decorator.glyph_size.y * 0.35
  247. imgui.get_window_draw_list().add_circle_filled(center, radius, imgui.IM_COL32(255, 60, 60, 255))
  248. statics.editor.set_line_decorator(-2.0, decorator_callback)
  249. # Right-click on line numbers: toggle breakpoint
  250. def line_number_context_menu(data: TextEditor.PopupData):
  251. line = data.pos.line
  252. has = line in statics.breakpoints
  253. label = ("Remove breakpoint" if has else "Set breakpoint") + f" (line {line + 1})"
  254. if imgui.menu_item_simple(label):
  255. if has:
  256. statics.breakpoints.discard(line)
  257. else:
  258. statics.breakpoints.add(line)
  259. statics.editor.set_line_number_context_menu_callback(line_number_context_menu)
  260. # Right-click in the text: different context menu
  261. def text_context_menu(data: TextEditor.PopupData):
  262. line = data.pos.line
  263. column = data.pos.index
  264. if imgui.menu_item_simple("Go to definition"):
  265. statics.last_action = f"Go to definition at {line + 1}:{column + 1}"
  266. if imgui.menu_item_simple("Find references"):
  267. statics.last_action = f"Find references at {line + 1}:{column + 1}"
  268. statics.editor.set_text_context_menu_callback(text_context_menu)
  269. # Hover in text: live popup with the word under the mouse
  270. # (think IDE quick-info / type-on-hover).
  271. def text_hover(data: TextEditor.PopupData):
  272. word = statics.editor.get_word_at_mouse_pos(imgui.get_mouse_pos())
  273. imgui.text(f"Hovered: line {data.pos.line + 1}, col {data.pos.index + 1}")
  274. if word:
  275. imgui.text(f"Word: {word}")
  276. statics.text_hover = text_hover # kept so the toggle can re-install it
  277. statics.editor.set_text_hover_callback(text_hover)
  278. statics.initialized = True
  279. editor = statics.editor
  280. imgui.text(
  281. "Right-click line numbers (or F9) to toggle breakpoints. "
  282. "Right-click in text for a menu. Hover text for live info. "
  283. "Hover the colored gutter bands for marker tooltips."
  284. )
  285. if statics.last_action:
  286. imgui.text_colored(imgui.ImVec4(0.5, 0.8, 1.0, 1.0), statics.last_action)
  287. imgui.begin_disabled(not editor.has_markers())
  288. if imgui.small_button("Clear markers"):
  289. editor.clear_markers()
  290. imgui.end_disabled()
  291. imgui.same_line()
  292. mini_map = editor.is_show_mini_map_enabled()
  293. changed, mini_map = imgui.checkbox("Show Mini Map", mini_map)
  294. if changed:
  295. editor.set_show_mini_map_enabled(mini_map)
  296. imgui.same_line()
  297. hover_on = editor.has_text_hover_callback()
  298. changed, hover_on = imgui.checkbox("Hover hints", hover_on)
  299. if changed:
  300. if hover_on:
  301. editor.set_text_hover_callback(statics.text_hover)
  302. else:
  303. editor.clear_text_hover_callback()
  304. imgui.same_line()
  305. scrollbar_mini = editor.is_show_scrollbar_mini_map_enabled()
  306. changed, scrollbar_mini = imgui.checkbox("Show Scrollbar Mini Map", scrollbar_mini)
  307. if changed:
  308. editor.set_show_scrollbar_mini_map_enabled(scrollbar_mini)
  309. imgui.same_line()
  310. imgui.text_disabled("(ticks in the scrollbar at marker / selection lines)")
  311. imgui.new_line()
  312. imgui.new_line()
  313. code_font = imgui_md.get_code_font()
  314. imgui.push_font(code_font.font, code_font.size)
  315. editor.render("##decorators_ctx", ImVec2(-1, imgui.get_text_line_height() * 20))
  316. imgui.pop_font()
  317. # F9: toggle breakpoint on current line
  318. if imgui.shortcut(imgui.Key.f9):
  319. line = editor.get_main_cursor_position().line
  320. if line in statics.breakpoints:
  321. statics.breakpoints.discard(line)
  322. else:
  323. statics.breakpoints.add(line)
  324. # ============================================================================
  325. # Tab 5: Text Diff
  326. # Demonstrates: side-by-side text comparison with TextDiff
  327. # ============================================================================
  328. @static(initialized=False, diff=None, side_by_side=False)
  329. def demo_text_diff():
  330. _show_source_toggle(demo_text_diff)
  331. statics = demo_text_diff
  332. if not statics.initialized:
  333. # The long docstring line on the second row exists so that toggling
  334. # the Word Wrap checkbox produces a visible effect on the diff.
  335. left = (
  336. "import math\n"
  337. '"""Module that says hello. A tiny example used in the ImGui Bundle TextDiff demo to show how line-by-line comparison works."""\n'
  338. "\n"
  339. "def greet():\n"
  340. ' print("Hello")\n'
  341. " return 0\n"
  342. )
  343. right = (
  344. "import math\n"
  345. '"""Module that says hello to anyone by name. A tiny example used in the ImGui Bundle TextDiff demo to show how line-by-line comparison and word-wrap work together."""\n'
  346. "import os\n"
  347. "\n"
  348. "def greet(name):\n"
  349. ' print(f"Hello, {name}")\n'
  350. " return 0\n"
  351. )
  352. statics.diff = TextDiff()
  353. statics.diff.set_text(left, right)
  354. statics.diff.set_language(TextEditor.Language.python())
  355. statics.initialized = True
  356. changed, statics.side_by_side = imgui.checkbox("Side by side", statics.side_by_side)
  357. if changed:
  358. statics.diff.set_side_by_side_mode(statics.side_by_side)
  359. imgui.same_line()
  360. wrap = statics.diff.is_word_wrap_enabled()
  361. changed, wrap = imgui.checkbox("Word Wrap", wrap)
  362. if changed:
  363. statics.diff.set_word_wrap_enabled(wrap)
  364. code_font = imgui_md.get_code_font()
  365. imgui.push_font(code_font.font, code_font.size)
  366. statics.diff.render("##diff")
  367. imgui.pop_font()
  368. # ============================================================================
  369. # Tab 6: Editor with Menus
  370. # Demonstrates how menus can be added to supplement the editor
  371. # ============================================================================
  372. @static(initialized=False, editor=None, find_text="", replace_text="", case_sensitive=True, whole_word=False)
  373. def demo_editor_with_menus():
  374. _show_source_toggle(demo_editor_with_menus)
  375. statics = demo_editor_with_menus
  376. if not statics.initialized:
  377. statics.editor = TextEditor()
  378. statics.editor.set_text(
  379. "import math\n"
  380. "\n"
  381. "def greet(name):\n"
  382. ' print(f"Hello {name}")\n'
  383. " x = 42\n"
  384. " pi = math.pi\n"
  385. " return x\n"
  386. )
  387. statics.editor.set_language(TextEditor.Language.python())
  388. statics.initialized = True
  389. editor = statics.editor
  390. imgui.begin_child("editor_with_menus", ImVec2(0, 0), 0, imgui.WindowFlags_.menu_bar.value)
  391. import platform
  392. _shortcut = "Cmd-" if platform.system() == "Darwin" else "Ctrl-"
  393. if imgui.begin_menu_bar():
  394. if imgui.begin_menu("Edit"):
  395. if imgui.menu_item_simple("Undo", f"{_shortcut}Z", enabled=editor.can_undo()):
  396. editor.undo()
  397. if imgui.menu_item_simple("Redo", f"{_shortcut}Y", enabled=editor.can_redo()):
  398. editor.redo()
  399. imgui.separator()
  400. if imgui.menu_item_simple("Cut", f"{_shortcut}X", enabled=editor.any_cursor_has_selection()):
  401. editor.cut()
  402. if imgui.menu_item_simple("Copy", f"{_shortcut}C", enabled=editor.any_cursor_has_selection()):
  403. editor.copy()
  404. if imgui.menu_item_simple("Paste", f"{_shortcut}V"):
  405. editor.paste()
  406. imgui.separator()
  407. _, flag = imgui.menu_item("Insert Spaces on Tabs", "", editor.is_insert_spaces_on_tabs())
  408. if _:
  409. editor.set_insert_spaces_on_tabs(flag)
  410. if imgui.menu_item_simple("Tabs To Spaces"):
  411. editor.tabs_to_spaces()
  412. if imgui.menu_item_simple("Spaces To Tabs", enabled=not editor.is_insert_spaces_on_tabs()):
  413. editor.spaces_to_tabs()
  414. if imgui.menu_item_simple("Strip Trailing Whitespaces"):
  415. editor.strip_trailing_whitespaces()
  416. imgui.end_menu()
  417. if imgui.begin_menu("Selection"):
  418. if imgui.menu_item_simple("Select All", f"{_shortcut}A", enabled=not editor.is_empty()):
  419. editor.select_all()
  420. imgui.separator()
  421. if imgui.menu_item_simple("Indent Line(s)", f"{_shortcut}]", enabled=not editor.is_empty()):
  422. editor.indent_lines()
  423. if imgui.menu_item_simple("Deindent Line(s)", f"{_shortcut}[", enabled=not editor.is_empty()):
  424. editor.deindent_lines()
  425. if imgui.menu_item_simple("Move Line(s) Up", "Alt-Up", enabled=not editor.is_empty()):
  426. editor.move_up_lines()
  427. if imgui.menu_item_simple("Move Line(s) Down", "Alt-Down ", enabled=not editor.is_empty()):
  428. editor.move_down_lines()
  429. if imgui.menu_item_simple("Toggle Comments", f"{_shortcut}/", enabled=editor.has_language()):
  430. editor.toggle_comments()
  431. imgui.separator()
  432. if imgui.menu_item_simple("To Uppercase", enabled=editor.any_cursor_has_selection()):
  433. editor.selection_to_upper_case()
  434. if imgui.menu_item_simple("To Lowercase", enabled=editor.any_cursor_has_selection()):
  435. editor.selection_to_lower_case()
  436. imgui.separator()
  437. if imgui.menu_item_simple("Add Next Occurrence", f"{_shortcut}D", enabled=editor.current_cursor_has_selection()):
  438. editor.add_next_occurrence()
  439. if imgui.menu_item_simple("Select All Occurrences", f"^{_shortcut}D", enabled=editor.current_cursor_has_selection()):
  440. editor.select_all_occurrences()
  441. imgui.end_menu()
  442. if imgui.begin_menu("View"):
  443. _, flag = imgui.menu_item("Show Whitespaces", "", editor.is_show_whitespaces_enabled())
  444. if _:
  445. editor.set_show_whitespaces_enabled(flag)
  446. _, flag = imgui.menu_item("Show Spaces", "", editor.is_show_spaces_enabled())
  447. if _:
  448. editor.set_show_spaces_enabled(flag)
  449. _, flag = imgui.menu_item("Show Tabs", "", editor.is_show_tabs_enabled())
  450. if _:
  451. editor.set_show_tabs_enabled(flag)
  452. _, flag = imgui.menu_item("Show Line Numbers", "", editor.is_show_line_numbers_enabled())
  453. if _:
  454. editor.set_show_line_numbers_enabled(flag)
  455. _, flag = imgui.menu_item("Show Matching Brackets", "", editor.is_showing_matching_brackets())
  456. if _:
  457. editor.set_show_matching_brackets(flag)
  458. _, flag = imgui.menu_item("Complete Matching Glyphs", "", editor.is_completing_paired_glyphs())
  459. if _:
  460. editor.set_complete_paired_glyphs(flag)
  461. _, flag = imgui.menu_item("Show Pan/Scroll Indicator", "", editor.is_show_pan_scroll_indicator_enabled())
  462. if _:
  463. editor.set_show_pan_scroll_indicator_enabled(flag)
  464. _, flag = imgui.menu_item("Middle Mouse Pan Mode", "", editor.is_middle_mouse_pan_mode())
  465. if _:
  466. if flag:
  467. editor.set_middle_mouse_pan_mode()
  468. else:
  469. editor.set_middle_mouse_scroll_mode()
  470. imgui.separator()
  471. _, flag = imgui.menu_item("Word Wrap", "", editor.is_word_wrap_enabled())
  472. if _:
  473. editor.set_word_wrap_enabled(flag)
  474. _, flag = imgui.menu_item("Line Folding", "", editor.is_line_folding_enabled())
  475. if _:
  476. editor.set_line_folding_enabled(flag)
  477. _, flag = imgui.menu_item("Show Mini Map", "", editor.is_show_mini_map_enabled())
  478. if _:
  479. editor.set_show_mini_map_enabled(flag)
  480. _, flag = imgui.menu_item("Show Scrollbar Mini Map", "", editor.is_show_scrollbar_mini_map_enabled())
  481. if _:
  482. editor.set_show_scrollbar_mini_map_enabled(flag)
  483. imgui.end_menu()
  484. if imgui.begin_menu("Find"):
  485. if imgui.menu_item_simple("Find", f"{_shortcut}F"):
  486. editor.open_find_replace_window()
  487. if imgui.menu_item_simple("Find Next", f"{_shortcut}G", enabled=editor.has_find_string()):
  488. editor.find_next()
  489. # if imgui.menu_item_simple(f"Find All", f"Shift {_shortcut}G", enabled=editor.has_find_string()):
  490. # editor.find_all()
  491. imgui.end_menu()
  492. imgui.end_menu_bar()
  493. code_font = imgui_md.get_code_font()
  494. imgui.push_font(code_font.font, code_font.size)
  495. editor.render("##editor_menus")
  496. imgui.pop_font()
  497. imgui.end_child()
  498. # ============================================================================
  499. # Tab 7: Multi-Cursor
  500. # Demonstrates: multiple simultaneous cursors and selections.
  501. # get_number_of_cursors / get_cursor_selection / get_cursor_text.
  502. # ============================================================================
  503. @static(initialized=False, editor=None)
  504. def demo_multi_cursor():
  505. def _init_statics():
  506. statics = demo_multi_cursor
  507. if not statics.initialized:
  508. statics.editor = TextEditor()
  509. statics.editor.set_text(
  510. "# Multi-cursor playground.\n"
  511. "# - Alt-click (Mac) / Ctrl-click (Windows, Linux) to add a cursor\n"
  512. "# at the click location.\n"
  513. "# - Hold the same modifier and drag to add a cursor and extend its selection\n"
  514. "# - Double-click a word to select it, then Ctrl-D (or Command-D on Mac)\n"
  515. "# to add a cursor at the next occurrence of the same word.\n"
  516. "\n"
  517. "value = 1\n"
  518. "value = value + 1\n"
  519. "value = value * value\n"
  520. "value = value - 1\n"
  521. "print(value, value, value)\n"
  522. )
  523. statics.editor.set_language(TextEditor.Language.python())
  524. statics.initialized = True
  525. def _gui_select_occurrences():
  526. editor = demo_multi_cursor.editor
  527. # Quick-action buttons. add_next_occurrence / select_all_occurrences
  528. # require the current cursor to have a selection (a word to match).
  529. imgui.begin_disabled(not editor.current_cursor_has_selection())
  530. if imgui.small_button("Add Next Occurrence"):
  531. editor.add_next_occurrence()
  532. imgui.same_line()
  533. if imgui.small_button("Select All Occurrences"):
  534. editor.select_all_occurrences()
  535. imgui.end_disabled()
  536. imgui.same_line()
  537. imgui.begin_disabled(editor.get_number_of_cursors() <= 1)
  538. if imgui.small_button("Clear Extra Cursors"):
  539. editor.clear_cursors() # leaves a single cursor at the main location
  540. imgui.end_disabled()
  541. def _gui_editor():
  542. editor = demo_multi_cursor.editor
  543. code_font = imgui_md.get_code_font()
  544. imgui.push_font(code_font.font, code_font.size)
  545. editor_height = imgui.get_text_line_height() * 20
  546. editor.render("##multi_cursor", size=ImVec2(0, editor_height))
  547. imgui.pop_font()
  548. # Live cursor status panel
  549. def _gui_cursors_info():
  550. editor = demo_multi_cursor.editor
  551. n_cursors = editor.get_number_of_cursors()
  552. imgui.text(f"Cursors: {n_cursors}")
  553. for i in range(n_cursors):
  554. sel = editor.get_cursor_selection(i)
  555. text = editor.get_cursor_text(i)
  556. if not text:
  557. imgui.bullet_text(
  558. f"[{i}] L:{sel.start.line + 1} C:{sel.start.index + 1}"
  559. )
  560. else:
  561. imgui.bullet_text(
  562. f"[{i}] L:{sel.start.line + 1} C:{sel.start.index + 1} "
  563. f"-> L:{sel.end.line + 1} C:{sel.end.index + 1} "
  564. f"selected: {text!r}"
  565. )
  566. # Main gui for demo_multi_cursors
  567. _init_statics()
  568. _show_source_toggle(demo_multi_cursor)
  569. _gui_select_occurrences()
  570. _gui_editor()
  571. _gui_cursors_info()
  572. # ============================================================================
  573. # Main demo function
  574. # ============================================================================
  575. def demo_gui():
  576. imgui_md.render(
  577. """
  578. # ImGuiColorTextEdit
  579. [ImGuiColorTextEdit](https://github.com/goossens/ImGuiColorTextEdit) is a syntax highlighting text editor for Dear ImGui (originally by BalazsJako, rewritten by Johan A. Goossens)
  580. """
  581. )
  582. if imgui.begin_tab_bar("##TextEditorDemos"):
  583. if imgui.begin_tab_item("Basic Editor")[0]:
  584. demo_basic_editor()
  585. imgui.end_tab_item()
  586. if imgui.begin_tab_item("Change Callback")[0]:
  587. demo_change_callback()
  588. imgui.end_tab_item()
  589. if imgui.begin_tab_item("Annotations & Interactivity")[0]:
  590. demo_decorators_and_context_menus()
  591. imgui.end_tab_item()
  592. if imgui.begin_tab_item("Filters")[0]:
  593. demo_filters()
  594. imgui.end_tab_item()
  595. if imgui.begin_tab_item("Text Diff")[0]:
  596. demo_text_diff()
  597. imgui.end_tab_item()
  598. if imgui.begin_tab_item("Multi-Cursor")[0]:
  599. demo_multi_cursor()
  600. imgui.end_tab_item()
  601. if imgui.begin_tab_item("Editor with Menus")[0]:
  602. demo_editor_with_menus()
  603. imgui.end_tab_item()
  604. imgui.end_tab_bar()
  605. def main():
  606. from imgui_bundle import immapp
  607. immapp.run(demo_gui, with_markdown=True, window_size=(1000, 800))
  608. if __name__ == "__main__":
  609. main()