Added ImGuiItemFlags_LiveEdit flag, and much-awaited support for disabling it. (#9476, #701)

cc #3936, #3946, #5904, #6284, #8149, #8065, #8665, #9117, #9299, #700, #1351, #1875, #2060, #2215, #2380, #2550, #3083, #3338, #3556, #4373, #4714, #4885, #5184,#5777, #6707, #6766, #8004, #8303, #8915, #9308
This commit is contained in:
ocornut
2026-04-02 15:57:36 +02:00
parent 2392a52660
commit 18d63b12be
4 changed files with 64 additions and 28 deletions

View File

@@ -4833,6 +4833,7 @@ void ImGui::MarkItemEdited(ImGuiID id)
// ActiveId might have been released by the time we call this (as in the typical press/release button behavior) but still need to fill the data.
ImGuiContext& g = *GImGui;
//IM_ASSERT(g.LastItemData.ID == id); // Failing cases include: "widgets_inputtext_scrolling", "widgets_inputtext_multiline_status", "widgets_selectable_input" = case of e.g TempInputText() overlayed manually with different ID (#2718)
g.LastItemData.StatusFlags |= ImGuiItemStatusFlags_EditedInternal;
if (g.LastItemData.ItemFlags & ImGuiItemFlags_NoMarkEdited)
return;
@@ -4843,14 +4844,14 @@ void ImGui::MarkItemEdited(ImGuiID id)
// FIXME: Can't we fully rely on LastItemData yet?
g.ActiveIdHasBeenEditedThisFrame = true;
g.ActiveIdHasBeenEditedBefore = true;
if (g.DeactivatedItemData.ID == id)
g.DeactivatedItemData.HasBeenEditedBefore = true;
}
if (g.DeactivatedItemData.ID == id)
g.DeactivatedItemData.HasBeenEditedBefore = true;
// We accept a MarkItemEdited() on drag and drop targets (see https://github.com/ocornut/imgui/issues/1875#issuecomment-978243343)
// We accept 'ActiveIdPreviousFrame == id' for InputText() returning an edit after it has been taken ActiveId away (#4714)
// FIXME: This assert is getting a bit meaningless over time. It helped detect some unusual use cases but eventually it is becoming an unnecessary restriction.
IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.ActiveIdPreviousFrame == id || g.NavJustMovedToId || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive));
IM_ASSERT(g.DragDropActive || g.ActiveId == id || g.ActiveId == 0 || g.DeactivatedItemData.ID == id || g.ActiveIdPreviousFrame == id || g.NavJustMovedToId || (g.CurrentMultiSelect != NULL && g.BoxSelectState.IsActive));
}
bool ImGui::IsWindowContentHoverable(ImGuiWindow* window, ImGuiHoveredFlags flags)

View File

@@ -30,7 +30,7 @@
// Library Version
// (Integer encoded as XYYZZ for use in #if preprocessor conditionals, e.g. '#if IMGUI_VERSION_NUM >= 12345')
#define IMGUI_VERSION "1.92.9 WIP"
#define IMGUI_VERSION_NUM 19285
#define IMGUI_VERSION_NUM 19286
#define IMGUI_HAS_TABLE // Added BeginTable() - from IMGUI_VERSION_NUM >= 18000
#define IMGUI_HAS_TEXTURES // Added ImGuiBackendFlags_RendererHasTextures - from IMGUI_VERSION_NUM >= 19198
@@ -1246,6 +1246,8 @@ enum ImGuiItemFlags_
ImGuiItemFlags_AutoClosePopups = 1 << 4, // true // MenuItem()/Selectable() automatically close their parent popup window.
ImGuiItemFlags_AllowDuplicateId = 1 << 5, // false // Allow submitting an item with the same identifier as an item already submitted this frame without triggering a warning tooltip if io.ConfigDebugHighlightIdConflicts is set.
ImGuiItemFlags_Disabled = 1 << 6, // false // [Internal] Disable interactions. DOES NOT affect visuals. This is used by BeginDisabled()/EndDisabled() and only provided here so you can read back via GetItemFlags().
ImGuiItemFlags_LiveEdit = 1 << 7, // true // WIP
};
// Flags for ImGui::InputText()
@@ -1275,6 +1277,7 @@ enum ImGuiInputTextFlags_
ImGuiInputTextFlags_DisplayEmptyRefVal = 1 << 14, // InputFloat(), InputInt(), InputScalar() etc. only: when value is zero, do not display it. Generally used with ImGuiInputTextFlags_ParseEmptyRefVal.
ImGuiInputTextFlags_NoHorizontalScroll = 1 << 15, // Disable following the cursor horizontally
ImGuiInputTextFlags_NoUndoRedo = 1 << 16, // Disable undo/redo. Note that input text owns the text data while active, if you want to provide your own undo/redo stack you need e.g. to call ClearActiveID().
//ImGuiInputTextFlags_NoLiveEdit = 1 << 25,
// Elide display / Alignment
ImGuiInputTextFlags_ElideLeft = 1 << 17, // When text doesn't fit, elide left side to ensure right side stays visible. Useful for path/filenames. Single-line only!

View File

@@ -1006,7 +1006,7 @@ enum ImGuiItemFlagsPrivate_
ImGuiItemFlags_HasSelectionUserData = 1 << 21, // false // Set by SetNextItemSelectionUserData()
ImGuiItemFlags_IsMultiSelect = 1 << 22, // false // Set by SetNextItemSelectionUserData()
ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups, // Please don't change, use PushItemFlag() instead.
ImGuiItemFlags_Default_ = ImGuiItemFlags_AutoClosePopups | ImGuiItemFlags_LiveEdit, // Please don't change, use PushItemFlag() instead.
// Obsolete
//ImGuiItemFlags_SelectableDontClosePopup = !ImGuiItemFlags_AutoClosePopups, // Can't have a redirect as we inverted the behavior
@@ -3799,7 +3799,7 @@ namespace ImGui
IMGUI_API void InputTextDeactivateHook(ImGuiID id);
IMGUI_API bool TempInputText(const ImRect& bb, ImGuiID id, const char* label, char* buf, size_t buf_size, ImGuiInputTextFlags flags = 0, ImGuiInputTextCallback callback = NULL, void* user_data = NULL);
IMGUI_API bool TempInputScalar(const ImRect& bb, ImGuiID id, const char* label, ImGuiDataType data_type, void* p_data, const char* format, const void* p_clamp_min = NULL, const void* p_clamp_max = NULL);
inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return g.ActiveId == id && g.TempInputId == id; }
inline bool TempInputIsActive(ImGuiID id) { ImGuiContext& g = *GImGui; return (g.TempInputId == id && g.ActiveId == id) || (g.InputTextDeactivatedState.ID == id); }
inline ImGuiInputTextState* GetInputTextState(ImGuiID id) { ImGuiContext& g = *GImGui; return (id != 0 && g.InputTextState.ID == id) ? &g.InputTextState : NULL; } // Get input text state if active
IMGUI_API void SetNextItemRefVal(ImGuiDataType data_type, void* p_data);
inline bool IsItemActiveAsInputText() { ImGuiContext& g = *GImGui; return g.ActiveId != 0 && g.ActiveId == g.LastItemData.ID && g.InputTextState.ID == g.LastItemData.ID; } // This may be useful to apply workaround that a based on distinguish whenever an item is active as a text input field.

View File

@@ -3722,8 +3722,9 @@ bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char*
ImGuiContext& g = *GImGui;
ImGuiWindow* window = g.CurrentWindow;
const bool init = (g.TempInputId != id);
if (init)
const bool is_deactivated = (g.InputTextDeactivatedState.ID == id);
const bool is_active = (g.TempInputId == id);
if (!is_active && !is_deactivated)
ClearActiveID();
ImVec2 backup_pos = window->DC.CursorPos;
@@ -3731,13 +3732,13 @@ bool ImGui::TempInputText(const ImRect& bb, ImGuiID id, const char* label, char*
g.LastItemData.ItemFlags |= ImGuiItemFlags_AllowDuplicateId; // Using ImGuiInputTextFlags_MergedItem above will skip ItemAdd() so we poke here.
bool value_changed = InputTextEx(label, NULL, buf, (int)buf_size, bb.GetSize(), flags | ImGuiInputTextFlags_TempInput | ImGuiInputTextFlags_AutoSelectAll, callback, user_data);
KeepAliveID(id); // Not done because of ImGuiInputTextFlags_TempInput
if (init)
if (!is_active && !is_deactivated)
{
// First frame we started displaying the InputText widget, we expect it to take the active id.
IM_ASSERT(g.ActiveId == id);
g.TempInputId = g.ActiveId;
}
if (g.ActiveId != id)
if (is_active && g.ActiveId != id)
g.TempInputId = 0;
window->DC.CursorPos = backup_pos;
return value_changed;
@@ -3841,8 +3842,23 @@ bool ImGui::InputScalar(const char* label, ImGuiDataType data_type, void* p_data
}
// Apply
bool input_edited = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_EditedInternal) != 0; // We would be using 'ret' if ImGuiInputTextFlags_EnterReturnsTrue was not involved.
bool value_changed = input_edited ? DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL) : false;
bool value_changed = false;
if (g.LastItemData.ItemFlags & ImGuiItemFlags_LiveEdit)
{
bool input_edited = (g.LastItemData.StatusFlags & ImGuiItemStatusFlags_EditedInternal) != 0; // We would be using 'ret' if ImGuiInputTextFlags_EnterReturnsTrue was not involved.
if (input_edited)
value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
}
else
{
//g.LastItemData.StatusFlags &= ~ImGuiItemStatusFlags_Edited;
if (g.DeactivatedItemData.ID == g.LastItemData.ID)
{
//g.DeactivatedItemData.HasBeenEditedBefore = false; // Will be set below by MarkItemEdited()
//if (IsItemDeactivated()) // Should be unnecessary
value_changed = DataTypeApplyFromText(buf, data_type, p_data, format, (flags & ImGuiInputTextFlags_ParseEmptyRefVal) ? p_data_default : NULL);
}
}
// Step buttons
if (has_step_buttons)
@@ -4584,7 +4600,7 @@ void ImGui::InputTextDeactivateHook(ImGuiID id)
{
ImGuiContext& g = *GImGui;
ImGuiInputTextState* state = &g.InputTextState;
if (id == 0 || state->ID != id)
if (id == 0 || state->ID != id || g.ActiveId != id)
return;
if (!state->EditedBefore)
return;
@@ -4820,7 +4836,7 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
const bool user_clicked = hovered && io.MouseClicked[0];
const bool input_requested_by_nav = (g.ActiveId != id) && (g.NavActivateId == id);
const bool input_requested_by_reactivate = (g.InputTextReactivateId == id); // for io.ConfigInputTextEnterKeepActive
const bool input_requested_by_user = (user_clicked) || (g.ActiveId == 0 && (flags & ImGuiInputTextFlags_TempInput));
const bool input_requested_by_user = (user_clicked) || (g.ActiveId == 0 && (flags & ImGuiInputTextFlags_TempInput) && g.InputTextDeactivatedState.ID != id);
const ImGuiID scrollbar_id = (is_multiline && state != NULL) ? GetWindowScrollbarID(draw_window, ImGuiAxis_Y) : 0;
const bool user_scroll_finish = is_multiline && state != NULL && g.ActiveId == 0 && g.ActiveIdPreviousFrame == scrollbar_id;
const bool user_scroll_active = is_multiline && state != NULL && g.ActiveId == scrollbar_id;
@@ -4852,7 +4868,8 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
state->CursorAnimReset();
// Backup state of deactivating item so they'll have a chance to do a write to output buffer on the same frame they report IsItemDeactivatedAfterEdit (#4714)
InputTextDeactivateHook(state->ID);
if (state->ID != id && state->ID == g.ActiveId && (init_make_active && g.ActiveId != id))
InputTextDeactivateHook(state->ID); // <-- this is essentially an earlier call to what SetActiveID() would do below.
// Take a copy of the initial buffer value.
// From the moment we focused we are normally ignoring the content of 'buf' (unless we are in read-only mode)
@@ -5389,13 +5406,27 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
}
}
// Will copy result string if modified.
// Write back result string if modified.
// FIXME-OPT: Could mark dirty state from the stb_textedit callbacks
if (!is_readonly && strcmp(state->TextSrc, buf) != 0)
if (!is_readonly)
{
apply_new_text = state->TextSrc;
apply_new_text_length = state->TextLen;
value_changed = true;
if (g.LastItemData.ItemFlags & ImGuiItemFlags_LiveEdit)
{
// Apply when modified
if (strcmp(state->TextSrc, buf) != 0)
{
apply_new_text = state->TextSrc;
apply_new_text_length = state->TextLen;
value_changed = true;
}
}
else
{
// Apply on validation/deactivation, otherwise cancel out previous apply attempts (e.g. revert)
value_changed = ((validated || clear_active_id || revert_edit) && strcmp(state->TextSrc, buf) != 0);
apply_new_text = value_changed ? state->TextSrc : NULL;
apply_new_text_length = value_changed ? state->TextLen : 0;
}
}
}
@@ -5403,14 +5434,15 @@ bool ImGui::InputTextEx(const char* label, const char* hint, char* buf, int buf_
// This is used when e.g. losing focus or tabbing out into another InputText() which may already be using the temp buffer.
if (g.InputTextDeactivatedState.ID == id)
{
// The state only exists after an Edit. More-over we cannot use IsItemDeactivatedAfterEdit().
if (g.ActiveId != id && IsItemDeactivated() && !is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
{
apply_new_text = g.InputTextDeactivatedState.TextA.Data;
apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
value_changed = true;
//IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
}
// The state only exists after an Edit. IsItemDeactivatedAfterEdit() is not valid in every code path (see "widgets_inputtext_status_noliveedit" test).
if ((g.ActiveId != id && IsItemDeactivated()) || (g.ActiveId == id && (flags & ImGuiInputTextFlags_TempInput)))
if (!is_readonly && strcmp(g.InputTextDeactivatedState.TextA.Data, buf) != 0)
{
apply_new_text = g.InputTextDeactivatedState.TextA.Data;
apply_new_text_length = g.InputTextDeactivatedState.TextA.Size - 1;
value_changed = true;
//IMGUI_DEBUG_LOG("InputText(): apply Deactivated data for 0x%08X: \"%.*s\".\n", id, apply_new_text_length, apply_new_text);
}
g.InputTextDeactivatedState.ID = 0;
}