Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BeginPopupContextItem forcing InputText to lose focus #4275

Open
martinpetkovski opened this issue Jun 26, 2021 · 5 comments
Open

BeginPopupContextItem forcing InputText to lose focus #4275

martinpetkovski opened this issue Jun 26, 2021 · 5 comments

Comments

@martinpetkovski
Copy link

Consider the following simple example:

ImGui::InputText(...)
if(ImGui::BeginPopupContextItem())
{
   // do something with the *selected text* from the above input text
}

Whenever the BeginPopupContextItem() is called the focus is immediately switched from the InputText to the newly spawned window. This causes the text selection to disappear along with the data for the SelectionStart / SelectionEnd.

One possible workaround is to save the SelectionStart / SelectionEnd whenever the popup is not opened but still the text highlight on the selection would disappear. Is there any way to keep the focus whenever a context menu is spawned above an InputText or maybe even get the focus back as soon as the popup is opened?

@ocornut
Copy link
Owner

ocornut commented Jun 28, 2021

Hello,

There are many aspects to your question:

1. Selection is not lost until another InputText is activated. Using imgui_internal.h you can access it:

ImGui::InputText("input text", str0, IM_ARRAYSIZE(str0));
ImGuiID id = ImGui::GetItemID();
if (ImGui::BeginPopupContextItem())
{
    ImGui::Text("HELLO");
    if (ImGuiInputTextState* state = ImGui::GetInputTextState(id))
    {
        ImGui::Text("Selection: %d-%d", state->GetSelectionStart(), state->GetSelectionEnd());
    }
    ImGui::EndPopup();
}

2. Selection is not rendered when inactive
See const bool RENDER_SELECTION_WHEN_INACTIVE = false; near the top of InputText().
This is disabled because it doesn't make sense until we have more explicit control of the lifetime of that data. Because selection is tied to local undo-stack and the later is trickier to preserve long term we should probably separate those and then we can contemplate allowing to preserve selection of multiple text fields separately from the undo stack.
(PS.1: Perhaps selection can live next to insert state if the later somehow can't be global, #2863)
(PS.2. Better control of selection is crucial to many improvements of InputText and often initially ignored in favor of an arbitrary decision #2890, which incidentally I just noticed had an answer for this problem now so will be reevaluated)

3. Popups is stealing focus
This is something we ought to control better. Also see #718
Normally we could aim to replace BeginPopupContextItem() with:

ImGui::OpenPopupOnItemClick("popup");
if (ImGui::BeginPopup("popup", ImGuiWindowFlags_NoFocusOnAppearing))

So that the popup doesn't take focus. But it doesn't work because of how we currently handle closing popups (the fact that the underlying window keeps focus will currently make the popup automatically close. Whereas this is a bug or not I don't know, would need to investigate this more thoroughly. #718 is generally devoted to workaround for this: either use a normal window e.g. Begi() using ImGuiWindowFlags_NoFocusOnAppearing. Either it seems like there is an ill-defined kludge where using ImGuiWindowFlags_ChildWindow on a popup can work but then it doesn't handle auto-closure the normal way.
To follow on (3) please move to #718.

ocornut added a commit that referenced this issue Jun 28, 2021
…ocks so badly. Add helper in internal's ImGuiInputTextState. (#4275)
@martinpetkovski
Copy link
Author

martinpetkovski commented Jun 28, 2021

I really appreciate your input, I managed to find a solution for my specific case based on that.

Now, while trying to find a solution myself, I noticed that there are a pair of Push\PopFocusScope functions - I thought that they would define a scope in which the focus could not be gained/lost, but that was not the case. Which lead me to think such functions may come handy in many scenarios (and I don't know how/if this is achievable by any means, I'm just thinking out loud). In most of the scenarios I can think of the first element is the one that needs to keep its focus, so if that element has the focus, the guard would not allow any subsequent focus changes inside the guard scope until that element loses focus outside the guard. The same principle would be valid for all elements inside the guard, but I think this would be a bit harder to achieve, if even possible. Here are two possible use cases:

mStr = some_external_data_str;
ImGui::PushFocusGuard();
ImGui::InputText("##someInput", &mStr);
// because InputText doesn't lose focus inside the focus guard, clicking the button receives the modified mStr data
if(ImGui::CustomButton("Do Action")) // custom content rendering which changes focus
{
   // do something with mStr
}
ImGui::PopFocusGuard();
ImGui::PushFocusGuard();
ImGui::InputText(...)
// context popup is inside the focus guard, input text still has focus
if(ImGui::BeginPopupContextItem())
{
   // do something with the *selected text* from the above input text
   // maybe will have to manually close the popup?
}
ImGui::PopFocusGuard();

@black-square
Copy link

Would it be easier if RENDER_SELECTION_WHEN_INACTIVE becomes configurable (or just true)?
This is going to be the first local lib change that I cannot resist making :(

@MartinClementson
Copy link

MartinClementson commented Mar 13, 2024

Hello, I'm resuming this conversation in hopes that there has been a solution in later versions.

I am implementing a cut/copy/paste menu that performs actions based on the selection. My code looks like the example at the top. I can work around storing the selection so that the operations will work, but I can't render the selection while the context menu is open.

The TextInput callback is not called while the ContextMenu is open due to the loss of focus. This means I can't do operations like ImGuiInputTextCallbackData::DeleteChars and InsertChars. Functions that are ideal when manipulating the input string as they keep the history for undo/redo operations, which is something I lose when I work around this by manipulating my input string variable outside of ImGui::InputText instead,
Perhaps there is a way of manipulating the string buffer outside of the callback that allows for history to be stored?

UPDATE:
I was able to find a solution for not being able to use DeleteChars and InsertChars. I set the focus back to the input when an edit action has been clicked in the context menu. This triggers the callback, where I can perform the operations.

In hopes that this helps someone in a similar situation, here is a basic example that should work. (I use openframeworks, replace the clipboard function with one that suits you).

What's missing now is that the selection won't render when the menu is open and that the cursor resets to the beginning. You could change RENDER_SELECTION_WHEN_INACTIVE to true. But since this affects all inputs in the app it's not a viable solution for me. I might just draw a custom selection when the menu is open.

UPDATE 2
I came up with a solution for my situation, I had to set RENDER_SELECTION_WHEN_INACTIVE to true but I did not want that behavior on all other inputs in the app or when the context menu was not taking focus. It turns out that if I call ImGui::IsItemDeactivated it won't return true when the PopupContext is opened. I use this to my advantage by clearing the selection when the Input is deactivated.

ImGui::InputText("ContextMenuInput", &inputText, ImGuiInputTextFlags_CallbackAlways, callback);
inputid = ImGui::GetItemID();
if (ImGui::IsItemDeactivated()) {
	if (auto* state = ImGui::GetInputTextState(inputid)) {
		state->ClearSelection();
	}
}

Since IsItemDeactivated won't return true when the contextmenu is open, the selection will stay only in this scenario when I need it to. I also had to add a "setActiveID" when refocusing the input to fully restore the state. (I've updated the example below)

ImGui 1.90.1

//Saved selection state
 string inputText = "";
 int inputStart = 0;
 int inputEnd = 0;
 int cursorPos = 0;
 bool inputHasSelection = false;
 ImGuiID inputid;
 bool contextOpen = false;
/////

//Action data 
 bool deleteSelection = false;
 bool insertString = false;
 std::string pasteString = "";
 bool focus_InputText = false;
/////

void drawInputWContext(){
	static auto callback = [](ImGuiInputTextCallbackData* data) {	
		auto cut = [&]() {
			if (inputHasSelection) {
				int start = inputStart < inputEnd ? inputStart : inputEnd;
				int end = inputStart > start ? inputStart : inputEnd;
				data->DeleteChars(start, end - start);
			}
		};
		if (deleteSelection || insertString) {
			//Restore the selection state to what it was before the context was opened
			data->CursorPos       = cursorPos;
			data->SelectionStart = inputStart;
			data->SelectionEnd   = inputEnd;

			if (deleteSelection) {
				deleteSelection = false;
				cut();
			}
			else if (insertString ) {
				insertString = false; 
				if (!pasteString.empty()) {
					cut();
					data->InsertChars(data->CursorPos, pasteString.c_str());
					pasteString = "";
				}
			}
		}

		inputStart               = data->SelectionStart;
		inputEnd                = data->SelectionEnd;
		inputHasSelection  = data->HasSelection();
		cursorPos                = data->CursorPos;
		return 1;
	};

	if (focus_InputText){
		//Set Focus on the input to trigger the callback
		ImGui::SetWindowFocus(); 
		ImGui::SetKeyboardFocusHere();
		ImGui::SetActiveID(inputid, ImGui::GetCurrentWindow());
		focus_InputText = false;
	}
	ImGui::InputText("ContextMenuInput", &inputText, ImGuiInputTextFlags_CallbackAlways, callback);
	inputid = ImGui::GetItemID();
        bool inputActive = ImGui::IsItemActive();
	if (ImGui::IsItemDeactivated()) {
		if (auto* state = ImGui::GetInputTextState(inputid)) {
			state->ClearSelection();
		}
	}
//Only allow opening of the context menu if the input is active.
//Otherwise we'll have a buffer bug if inputText is changed outside of inputText (long story)
	if (inputActive || contextOpen) { 
		bool contextWasOpen = contextOpen;
		if (contextOpen = ImGui::BeginPopupContextItem("##popupContext", ImGuiPopupFlags_MouseButtonRight)) {
			auto copyToClipboard = [&]() {
				int start = inputStart < inputEnd ? inputStart : inputEnd;
				int end = inputStart > start ? inputStart : inputEnd;
				if (start == end) return;
				std::string cutStr = inputText.substr(start, end - start);
				ofSetClipboardString(cutStr);
			};
			ImGui::BeginDisabled(!inputHasSelection);
			if (ImGui::MenuItem("Cut")) { 
				copyToClipboard();
				deleteSelection = true;
			}
			if (ImGui::MenuItem("Copy")) {
				copyToClipboard();
			}
			ImGui::EndDisabled();

			std::string clip = ofGetClipboardString();
			ImGui::BeginDisabled(clip.empty());
			if (ImGui::MenuItem("Paste")) {
				pasteString  = clip;
				insertString = !clip.empty();
			}
			ImGui::EndDisabled();

			ImGui::EndPopup();
		}
		else if(contextWasOpen){
			focus_InputText = true;
		}
	}

}

inputContextMenu

@SheridanR
Copy link

I came up with a solution for a context menu that simply modifies InputTextEx() and used it for my game engine, here is the code.

    const bool RENDER_SELECTION_WHEN_INACTIVE = true;

    ...

    const bool user_clicked = hovered && (io.MouseClicked[0] || io.MouseClicked[1]); // must check for right clicks as well

    ...

    // context menu
    // insert this code after the block for: Process other shortcuts/key-presses
    if (state && BeginPopupContextItem())
    {
#ifdef __APPLE__
        static const char* shortcuts[] = {
            "Cmd+Z",
            "Shift+Cmd+Z",
            "Cmd+X",
            "Cmd+C",
            "Cmd+V",
            "Cmd+D",
            "Del",
            "Cmd+A",
        };
#else
        static const char* shortcuts[] = {
            "Ctrl+Z",
            "Ctrl+Y",
            "Ctrl+X",
            "Ctrl+C",
            "Ctrl+V",
            "Ctrl+D",
            "Del",
            "Ctrl+A",
        };
#endif

        if (MenuItem("Undo", shortcuts[0], false, !(flags & (ImGuiInputTextFlags_NoUndoRedo | ImGuiInputTextFlags_ReadOnly)) && state->GetUndoAvailCount() > 0))
        {
            state->OnKeyPressed(STB_TEXTEDIT_K_UNDO);
            state->ClearSelection();
        }
        if (MenuItem("Redo", shortcuts[1], false, !(flags & (ImGuiInputTextFlags_NoUndoRedo | ImGuiInputTextFlags_ReadOnly)) && state->GetRedoAvailCount() > 0))
        {
            state->OnKeyPressed(STB_TEXTEDIT_K_REDO);
            state->ClearSelection();
        }
        Separator();
        if (MenuItem("Cut", shortcuts[2], false, !(flags & ImGuiInputTextFlags_ReadOnly) && state->HasSelection()))
        {
            if (io.SetClipboardTextFn)
            {
                const int ib = state->HasSelection() ? ImMin(state->Stb.select_start, state->Stb.select_end) : 0;
                const int ie = state->HasSelection() ? ImMax(state->Stb.select_start, state->Stb.select_end) : state->CurLenW;
                const int clipboard_data_len = ImTextCountUtf8BytesFromStr(state->TextW.Data + ib, state->TextW.Data + ie) + 1;
                char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char));
                ImTextStrToUtf8(clipboard_data, clipboard_data_len, state->TextW.Data + ib, state->TextW.Data + ie);
                SetClipboardText(clipboard_data);
                MemFree(clipboard_data);
            }
            if (!state->HasSelection())
                state->SelectAll();
            state->CursorFollow = true;
            stb_textedit_cut(state, &state->Stb);
        }
        if (MenuItem("Copy", shortcuts[3], false, state->HasSelection()))
        {
            if (io.SetClipboardTextFn)
            {
                const int ib = state->HasSelection() ? ImMin(state->Stb.select_start, state->Stb.select_end) : 0;
                const int ie = state->HasSelection() ? ImMax(state->Stb.select_start, state->Stb.select_end) : state->CurLenW;
                const int clipboard_data_len = ImTextCountUtf8BytesFromStr(state->TextW.Data + ib, state->TextW.Data + ie) + 1;
                char* clipboard_data = (char*)IM_ALLOC(clipboard_data_len * sizeof(char));
                ImTextStrToUtf8(clipboard_data, clipboard_data_len, state->TextW.Data + ib, state->TextW.Data + ie);
                SetClipboardText(clipboard_data);
                MemFree(clipboard_data);
            }
        }
        if (MenuItem("Paste", shortcuts[4], false, !(flags & ImGuiInputTextFlags_ReadOnly)))
        {
            if (const char* clipboard = GetClipboardText())
            {
                // Filter pasted buffer
                const int clipboard_len = (int)strlen(clipboard);
                ImWchar* clipboard_filtered = (ImWchar*)IM_ALLOC((clipboard_len + 1) * sizeof(ImWchar));
                int clipboard_filtered_len = 0;
                for (const char* s = clipboard; *s != 0; )
                {
                    unsigned int c;
                    s += ImTextCharFromUtf8(&c, s, NULL);
                    if (!InputTextFilterCharacter(&g, &c, flags, callback, callback_user_data, ImGuiInputSource_Clipboard))
                        continue;
                    clipboard_filtered[clipboard_filtered_len++] = (ImWchar)c;
                }
                clipboard_filtered[clipboard_filtered_len] = 0;
                if (clipboard_filtered_len > 0) // If everything was filtered, ignore the pasting operation
                {
                    stb_textedit_paste(state, &state->Stb, clipboard_filtered, clipboard_filtered_len);
                    state->CursorFollow = true;
                }
                MemFree(clipboard_filtered);
            }
        }
        if (MenuItem("Duplicate", shortcuts[5], false, !(flags & ImGuiInputTextFlags_ReadOnly) && state->HasSelection()))
        {
            const int ib = state->HasSelection() ? ImMin(state->Stb.select_start, state->Stb.select_end) : 0;
            const int ie = state->HasSelection() ? ImMax(state->Stb.select_start, state->Stb.select_end) : state->CurLenW;
            const int clipboard_data_len = ie - ib;
            if (clipboard_data_len > 0)
            {
                ImWchar* clipboard_data = (ImWchar*)IM_ALLOC(clipboard_data_len * sizeof(ImWchar));
                memcpy(clipboard_data, state->TextW.Data + ib, clipboard_data_len * sizeof(ImWchar));
                state->Stb.select_start = state->Stb.select_end;
                stb_textedit_paste(state, &state->Stb, clipboard_data, clipboard_data_len);
                state->CursorFollow = true;
                MemFree(clipboard_data);
            }
        }
        if (MenuItem("Delete", shortcuts[6], false, !(flags & ImGuiInputTextFlags_ReadOnly) && state->HasSelection()))
        {
            state->OnKeyPressed(STB_TEXTEDIT_K_DELETE);
        }
        Separator();
        if (MenuItem("Select All", shortcuts[7], false, true))
        {
            state->SelectAll();
        }
        EndPopup();

        // revert id
        g.ActiveId = id;
    }
2024-08-27_11-17-02.mov

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants