ocornut / imgui

Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies
MIT License
61.1k stars 10.29k forks source link

How to change text of selectable on second click? #2718

Open jsandham opened 5 years ago

jsandham commented 5 years ago

Version: 1.72 Branch: master

Back-ends: imgui_impl_win32.cpp + imgui_impl_opengl3.cpp Operating System: Windows 10

I am trying to create selectables similar to what unity has in their Hierarchy window (and many other progams such as file browsers) where you can select a game object once and it will highlight and then if you click it again it will allow you to change the text (name of the game object in the case of unity). I didn't see anything like this in the demo. I suspect something like this could be made using InputText and Selectables from ImGui but I am not sure how. Does such a thing exist in ImGui or does someone know how to achieve the desired affect by combining different components?

Thanks

ocornut commented 4 years ago

Hello,

This is not trivial or available out of the box but it should be possible to build a custom combined widgets. Depending on which exact flags and features of Selectable and InputText you need it may be easier or harder.

I have now pushed a change on master (898e91f20d5c35a9148c31596211123958464622) which makes it easier to use this custom widget: (here I used double-click to edit, which is simpler to implement as there's less state tracking):

namespace ImGui
{
    bool SelectableInput(const char* str_id, bool selected, ImGuiSelectableFlags flags, char* buf, size_t buf_size)
    {
        ImGuiContext& g = *GImGui;
        ImGuiWindow* window = g.CurrentWindow;
        ImVec2 pos_before = window->DC.CursorPos;

        PushID(str_id);
        PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(g.Style.ItemSpacing.x, g.Style.FramePadding.y * 2.0f));
        bool ret = Selectable("##Selectable", selected, flags | ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_AllowItemOverlap);
        PopStyleVar();

        ImGuiID id = window->GetID("##Input");
        bool temp_input_is_active = TempInputIsActive(id);
        bool temp_input_start = ret ? IsMouseDoubleClicked(0) : false;

        if (temp_input_start)
            SetActiveID(id, window);

        if (temp_input_is_active || temp_input_start)
        {
            ImVec2 pos_after = window->DC.CursorPos;
            window->DC.CursorPos = pos_before;
            ret = TempInputText(window->DC.LastItemRect, id, "##Input", buf, (int)buf_size, ImGuiInputTextFlags_None);
            window->DC.CursorPos = pos_after;
        }
        else
        {
            window->DrawList->AddText(pos_before, GetColorU32(ImGuiCol_Text), buf);
        }

        PopID();
        return ret;
    }
}

Usage demo

static char buf1[256] = "Hello1";
static char buf2[256] = "Hello2";
ImGui::SelectableInput("obj1", false, ImGuiSelectableFlags_None, buf1, IM_ARRAYSIZE(buf1));
ImGui::SelectableInput("obj2", false, ImGuiSelectableFlags_None, buf2, IM_ARRAYSIZE(buf2));

image

What's not really covered here honestly are how/why you intend to have an actual Selectable(). Used as-is, the code doesn't need a Selectable and could be implemented differently. Exactly how you intend to rely on Selectable() specific feature would alter how this code may work.

twaritwaikar commented 4 years ago

I have worked around this likewise:

// Originally m_IsNameBeingEdited = false
if (m_IsNameBeingEdited)
{
    if (ImGui::InputText("Entity Name", &m_OpenedEntityName, ImGuiInputTextFlags_EnterReturnsTrue | ImGuiInputTextFlags_AutoSelectAll))
    {
        m_OpenedEntity->setName(m_OpenedEntityName);
        m_IsNameBeingEdited = false;
    }
    if (!ImGui::IsItemHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left))
    {
        m_IsNameBeingEdited = false;
    }
}
else
{
    ImGui::TreeNodeEx(m_OpenedEntity->getFullName().c_str(), ImGuiTreeNodeFlags_CollapsingHeader | ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_Selected);
    if (ImGui::IsItemClicked(ImGuiMouseButton_Left))
    {
        m_IsNameBeingEdited = true;
    }
}

Edit: Comment by Omar on this:

The problem with your implementation as posted is that it won't correctly fit in with all dear imgui features, such as keyboard and gamepad controls, but if you are not using them it may be ok! My job is to try to come up with more example to make it more natural to make conforming widgets.

RT2Code commented 3 years ago

Hello,

I'm trying to build a scene explorer using tables and the SelectableInput widget posted by @ocornut here. It's working fine except for a small visual problem. When adjacent SelectableInput widgets are selected, their selection boxes overlaps.

1

If I comment out the style change made to the Selectable widget, I get the expected result.

   //PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(g.Style.ItemSpacing.x, g.Style.FramePadding.y * 2.0f));
   bool ret = Selectable("##Selectable", selected, flags | ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_AllowItemOverlap);
   //PopStyleVar();

2

But there's a remaining issue. I'm using a clipper to remove non visible rows. When a row is double clicked and goes to input text mode, it's height slightly increase. When this happens on the first row, it throws of the clipper calculations and a gap appears between the bottom of the window and the last widget drawn.

3

I tried many things like tweaking the styles or widget size, but my understanding of ImGUI internals is not good enough for me to be able to fix this alone.

Here's the minimal code to reproduce the issue.

struct Item
{
    std::string name;
    bool is_selected;
};

std::vector<Item> items;
for (int i{}; i < 100; ++i)
{
    std::string name{"entity_"};
    name += std::to_string(i);
    items.emplace_back(name, true);
}

ImGui::SetNextWindowSize({200, 600});
if (ImGui::Begin("Scene", nullptr, ImGuiWindowFlags_None))
{
    if (ImGui::BeginTable("Scene", 1, ImGuiTableFlags_RowBg))
    {
        ImGui::TableNextRow();
        ImGui::TableNextColumn();
        ImGuiListClipper clipper;
        clipper.Begin((int)items.size());
        while (clipper.Step())
        {
            for (int row{clipper.DisplayStart}; row < clipper.DisplayEnd; ++row)
            {
                ImGui::TableNextRow();
                ImGui::TableNextColumn();
            ImGui::PushID(row);
                auto& entity = items[row];
                ImGui::SelectableInput("row", &entity.is_selected, entity.name, ImGuiSelectableFlags_None);
                ImGui::PopID();
            }
        }
        ImGui::EndTable();
    }
}
ImGui::End();

Thanks. :)

Halarious commented 2 years ago

I was playing around with Omars SelectableInput because it looked like it was almost what I wanted, it worked well except I kept getting crashes when trying to extend it to a multiline input text.

I tried updating to see if this would resolve the crash but it seems the snippet doesn't work for 1.88 at all? It starts out the same as with the older versions, tempInputIsActive returns true in the next frame, but in the frames afterwards it falls back to false, so the input text is never really active. I haven't looked too deeply on what is causing this.

Maybe there are better ways to do this now, I see some things have shifted around since, so I apologize if this is not relevant anymore. Thanks!

Update: The issue started when ImGuiInputTextFlags_MergedItem was introduced into TempInputText(), it seems. I resolved this by manually calling KeepAliveID() in the widget, as referenced here. Not sure if that is the correct way to handle this, but after some reading through the code it seems like it. https://github.com/ocornut/imgui/blob/master/imgui.cpp#L4447

mehlian commented 5 months ago

Any update on this? I tried to recreate a calculator with the ability to edit history, similarly to the Windows 7 calculator, but from what I see, it can't be done? image

ocornut commented 5 months ago

Here's the same logic as posted in https://github.com/ocornut/imgui/issues/2718#issuecomment-591057202 but fixed/updated for recent versions:

#include "imgui_internal.h"

bool SelectableInput(const char* str_id, bool selected, ImGuiSelectableFlags flags, char* buf, size_t buf_size)
{
    using namespace ImGui;
    ImGuiContext& g = *GImGui;
    ImGuiWindow* window = g.CurrentWindow;
    ImVec2 pos = window->DC.CursorPos;

    PushID(str_id);
    PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(g.Style.ItemSpacing.x, g.Style.FramePadding.y * 2.0f));
    bool ret = Selectable("##Selectable", selected, flags | ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_AllowOverlap);
    PopStyleVar();

    ImGuiID id = window->GetID("##Input");
    bool temp_input_is_active = TempInputIsActive(id);
    bool temp_input_start = ret ? IsMouseDoubleClicked(0) : false;
    if (temp_input_is_active || temp_input_start)
    {
        ret = TempInputText(g.LastItemData.Rect, id, "##Input", buf, (int)buf_size, ImGuiInputTextFlags_None);
        KeepAliveID(id);
    }
    else
    {
        window->DrawList->AddText(pos, GetColorU32(ImGuiCol_Text), buf);
    }

    PopID();
    return ret;
}
victornvq30 commented 4 months ago

Hi Omar, your new version is working great ! We hoverwer would like not only edit text when double-clicked but also when focus the selectable then press Enter. Please advise test

victornvq30 commented 4 months ago

Hi Omar, temporary i'm solved by using popup with input text. Thanks

the-goodies commented 3 months ago

Hi Omar, your new version is working great ! We hoverwer would like not only edit text when double-clicked but also when focus the selectable then press Enter

I would also like to know how to activate InputText by pressing enter.

I tried doing this (adding temp_input_start_by_enter_pressed):

#include "imgui_internal.h"

bool SelectableInput(const char* str_id, bool selected, ImGuiSelectableFlags flags, char* buf, size_t buf_size)
{
    using namespace ImGui;
    ImGuiContext& g = *GImGui;
    ImGuiWindow* window = g.CurrentWindow;
    ImVec2 pos = window->DC.CursorPos;

    PushID(str_id);
    PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(g.Style.ItemSpacing.x, g.Style.FramePadding.y * 2.0f));
    bool ret = Selectable("##Selectable", selected, flags | ImGuiSelectableFlags_AllowDoubleClick | ImGuiSelectableFlags_AllowOverlap);
    PopStyleVar();

    ImGuiID id = window->GetID("##Input");
    bool temp_input_is_active = TempInputIsActive(id);
    bool temp_input_start = ret ? IsMouseDoubleClicked(0) : false;
    bool temp_input_start_by_enter_pressed = IsItemFocused() && (IsKeyPressed(ImGuiKey_Enter) || IsKeyPressed(ImGuiKey_KeypadEnter));
    if (temp_input_is_active || temp_input_start || temp_input_start_by_enter_pressed)
    {
        ret = TempInputText(g.LastItemData.Rect, id, "##Input", buf, (int)buf_size, ImGuiInputTextFlags_None);
        KeepAliveID(id);
    }
    else
    {
        window->DrawList->AddText(pos, GetColorU32(ImGuiCol_Text), buf);
    }

    PopID();
    return ret;
}

But when I click enter I get assert from within TempInputText:

    if (init)
    {
        // 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 I comment out this assert, then InputText flickers (probably gets active only for a single frame) and gets back to being a Selectable.