From 7fb069997b779ad46154af69dbee516ea4df67b3 Mon Sep 17 00:00:00 2001 From: Marius PvW Date: Wed, 4 Nov 2026 12:13:25 -0500 Subject: [PATCH] TextSelect: Ctrl+A selects the selection's container, widening on each press; copy and select-all keys are configurable chords (TextSelectSetCopyShortcut(), TextSelectSetSelectAllShortcut()). --- docs/CHANGELOG.txt | 3 ++ misc/text_select/imgui_text_select.cpp | 72 ++++++++++++++++++++++++-- misc/text_select/imgui_text_select.h | 12 ++++- 3 files changed, 82 insertions(+), 5 deletions(-) diff --git a/docs/CHANGELOG.txt b/docs/CHANGELOG.txt index 1e2b4f690..0068f19cb 100644 --- a/docs/CHANGELOG.txt +++ b/docs/CHANGELOG.txt @@ -102,6 +102,9 @@ Other Changes: text selection/copy of any displayed text without changes to widget code. - Misc: added misc/text_select/ extension using the hook above: mouse selection (click/drag, word, line, block), highlight, and Ctrl+C/Cmd+C copy of any drawn text. + Ctrl+A/Cmd+A selects the selection's container, widening on each press (table -> + window -> parent window). Keys are configurable with `TextSelectSetCopyShortcut()` + and `TextSelectSetSelectAllShortcut()`. - ImVec2: added a check for cases where a custom `IM_VEC2_CLASS_EXTRA` macro adds an extra `operator=` to ImVec2. This isn't supported but should not be needed as you should have a constructor to convert from your type to ImVec2. diff --git a/misc/text_select/imgui_text_select.cpp b/misc/text_select/imgui_text_select.cpp index bc4e981ba..7fed34b0f 100644 --- a/misc/text_select/imgui_text_select.cpp +++ b/misc/text_select/imgui_text_select.cpp @@ -55,6 +55,8 @@ struct ImGuiTextSelectState bool Enabled = true; ImGuiTextSelectCopyFeedbackCallback CopyFeedback = NULL; void* CopyFeedbackUserData = NULL; + ImGuiKeyChord SelectAllChords[2] = { ImGuiMod_Ctrl | ImGuiKey_A, 0 }; + ImGuiKeyChord CopyChords[2] = { ImGuiMod_Ctrl | ImGuiKey_C, ImGuiMod_Ctrl | ImGuiKey_Insert }; double FlashStartTime = -1.0; // Built-in copy feedback: the highlight turns opaque and fades back bool Delimiters[128] = {}; @@ -855,6 +857,39 @@ static const ImGuiTextSelectRange* TextSelectExtendContainer(ImGuiTextSelectStat return (st->Dragging && has_anchor) ? &st->Anchor : NULL; } +// Selects all of the anchor's container; when a previous call already did, widens it first: a table to its window, a +// child window to its parent. A drag is clamped to its container, so only this leaves the anchor at -far_away. +static void TextSelectSelectAllInContainer(ImGuiTextSelectState* st) +{ + const float far_away = 1e8f; + ImGuiTextSelectRange c = st->Anchor; + if (c.Begin.x == -far_away && st->Focus.End.x == far_away && !c.MenuBar) + { + if (c.TableId != 0) + { + c.TableId = 0; + } + else if (c.Window != c.Window->RootWindow && c.Window->ParentWindow != NULL) + { + c.Window = c.Window->ParentWindow; + c.WindowId = c.Window->ID; + } + } + c.Begin = c.End = ImVec2(-far_away, -far_away); + st->Anchor = c; + c.Begin = c.End = ImVec2(far_away, far_away); + st->Focus = c; + st->Block = false; +} + +static ImGuiKey TextSelectChordPressed(const ImGuiKeyChord (&chords)[2]) +{ + for (ImGuiKeyChord chord : chords) + if (chord != 0 && ImGui::IsKeyChordPressed(chord)) + return (ImGuiKey)(chord & ~ImGuiMod_Mask_); + return ImGuiKey_None; +} + static void TextSelectNewFrame(ImGuiContext* ctx, ImGuiContextHook* hook) { ImGuiTextSelectState* st = (ImGuiTextSelectState*)hook->UserData; @@ -912,6 +947,19 @@ static void TextSelectNewFrame(ImGuiContext* ctx, ImGuiContextHook* hook) st->ProbeTextDistEm = FLT_MAX; st->ProbeGlyphItemId = 0; + const ImGuiKey select_all_key = TextSelectChordPressed(st->SelectAllChords); + if (select_all_key != ImGuiKey_None && st->RootWindow != NULL && !io.WantTextInput) + { + const bool owns_window = (g.HoveredWindow != NULL && g.HoveredWindow->RootWindow == st->RootWindow) || (g.NavWindow != NULL && g.NavWindow->RootWindow == st->RootWindow); + if (owns_window) + { + TextSelectSelectAllInContainer(st); + TextSelectResolve(st); + // Nobody else acts on this select-all key press + ImGui::SetKeyOwner(select_all_key, ImGuiKeyOwner_Any, ImGuiInputFlags_LockThisFrame); + } + } + if (st->CopyRequested) { st->CopyRequested = false; @@ -922,7 +970,8 @@ static void TextSelectNewFrame(ImGuiContext* ctx, ImGuiContextHook* hook) } } st->RequestedCopyText.clear(); - if (!(io.KeyCtrl || io.KeySuper) || !(ImGui::IsKeyPressed(ImGuiKey_C, false) || ImGui::IsKeyPressed(ImGuiKey_Insert, false))) + const ImGuiKey copy_key = TextSelectChordPressed(st->CopyChords); + if (copy_key == ImGuiKey_None) return; if (!st->Active) { @@ -936,8 +985,7 @@ static void TextSelectNewFrame(ImGuiContext* ctx, ImGuiContextHook* hook) { TextSelectBeginCopy(st); // Nobody else acts on this copy key press - ImGui::SetKeyOwner(ImGuiKey_C, ImGuiKeyOwner_Any, ImGuiInputFlags_LockThisFrame); - ImGui::SetKeyOwner(ImGuiKey_Insert, ImGuiKeyOwner_Any, ImGuiInputFlags_LockThisFrame); + ImGui::SetKeyOwner(copy_key, ImGuiKeyOwner_Any, ImGuiInputFlags_LockThisFrame); } } @@ -1226,6 +1274,24 @@ void ImGui::TextSelectSetCopyFeedbackCallback(ImGuiTextSelectCopyFeedbackCallbac } } +void ImGui::TextSelectSetCopyShortcut(ImGuiKeyChord key_chord, ImGuiKeyChord alt_key_chord) +{ + if (ImGuiTextSelectState* st = TextSelectGetState()) + { + st->CopyChords[0] = key_chord; + st->CopyChords[1] = alt_key_chord; + } +} + +void ImGui::TextSelectSetSelectAllShortcut(ImGuiKeyChord key_chord, ImGuiKeyChord alt_key_chord) +{ + if (ImGuiTextSelectState* st = TextSelectGetState()) + { + st->SelectAllChords[0] = key_chord; + st->SelectAllChords[1] = alt_key_chord; + } +} + void ImGui::TextSelectSetWordDelimiters(const char* delimiters) { if (ImGuiTextSelectState* st = TextSelectGetState()) diff --git a/misc/text_select/imgui_text_select.h b/misc/text_select/imgui_text_select.h index f5af87a62..587de6fbf 100644 --- a/misc/text_select/imgui_text_select.h +++ b/misc/text_select/imgui_text_select.h @@ -13,10 +13,13 @@ // the first and last selected glyphs. Child windows inside the container take part only when they overlap the // rectangle spanned by the two ends (selecting in one of two side-by-side children leaves the other out). // - A word is a run of word characters, or a run of delimiter characters (see TextSelectSetWordDelimiters()). -// - Ctrl+C (Cmd+C on macOS) or Ctrl+Insert copies the selection in reading order: lines from top to bottom, +// - Ctrl+C (Cmd+C on macOS) or Ctrl+Insert (see TextSelectSetCopyShortcut()) copies the selection in reading order: lines from top to bottom, // fragments on a line from left to right, wide gaps (e.g. table columns) as tabs, soft-wrapped lines joined by a space // (or as given to TextSelectMarkSoftWrap()). The container's rows and columns scrolled out of view are submitted for // that one frame (through ImGuiListClipper and tables' box-select unclip support) and copied; hidden columns are not. +// - Ctrl+A (Cmd+A on macOS), with a caret or selection in the hovered or focused window, selects everything in its +// container; each further press widens it: a table to its window, a child window to its parent, up to the root. +// TextSelectSetSelectAllShortcut() changes or disables the key. // - Escape, or a click on a widget other than the container's scrollbars, clears the selection. // - When io.ConfigWindowsMoveFromTitleBarOnly is off, a click on background farther than the width of an 'M' (in the // text's font) from any text on its line clears the selection and a drag there moves the window; closer, it selects. @@ -58,6 +61,11 @@ namespace ImGui void TextSelectSetCopyFeedbackCallback(ImGuiTextSelectCopyFeedbackCallback callback, void* user_data); // ASCII characters that end a word, besides blanks. Other characters, including all non-ASCII, are word characters. void TextSelectSetWordDelimiters(const char* delimiters); + // Default ImGuiMod_Ctrl | ImGuiKey_A (Cmd+A on macOS with io.ConfigMacOSXBehaviors). Either chord selects; 0 disables + // it, e.g. (ImGuiMod_Ctrl | ImGuiKey_A, ImGuiMod_Super | ImGuiKey_A) also accepts the other modifier. + // Default ImGuiMod_Ctrl | ImGuiKey_C and ImGuiMod_Ctrl | ImGuiKey_Insert. Either chord copies; 0 disables it. + void TextSelectSetCopyShortcut(ImGuiKeyChord key_chord, ImGuiKeyChord alt_key_chord = 0); + void TextSelectSetSelectAllShortcut(ImGuiKeyChord key_chord, ImGuiKeyChord alt_key_chord = 0); // Text drawn in between is ignored (as ImGuiItemFlags_NoTextSelect), and clicks on that stretch of the current // window, from the cursor at Push to the cursor at Pop, do not set the anchor. @@ -75,7 +83,7 @@ namespace ImGui // The next text drawn in the current window continues the previous line (a soft wrap done by the caller): // copies join the two with join (a static string, "" for a word cut in two) instead of a newline. void TextSelectMarkSoftWrap(const char* join = " "); - // Selects everything in the current window and its child windows (e.g. on Ctrl+A in a text view). + // Selects everything in the current window and its child windows, e.g. from a "Select all" menu item. void TextSelectSelectAll(); // Selects the text inside a rectangle of the current window and copies it on the next frame, with the usual copy // feedback, e.g. from a click handler. copy_text, when given, is what reaches the clipboard instead.