TextSelect: Ctrl+A selects the selection's container, widening on each press; copy and select-all keys are configurable chords (TextSelectSetCopyShortcut(), TextSelectSetSelectAllShortcut()).

This commit is contained in:
Marius PvW 2026-11-04 12:13:25 -05:00
parent f70c1bc285
commit 7fb069997b
3 changed files with 82 additions and 5 deletions

View File

@ -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.

View File

@ -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())

View File

@ -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.