Error while trying to remove a widget from gui

Started by EngineerBread, 18 May 2024, 05:08:43

EngineerBread

Hi, I hope everyone is doing well.
I'm here so to expose a problem that I encountered while I was coding a pseudo context menu, the problem comes when trying to remove the afore mentioned menu, I get the following exception (I attached the screenshot)
1.png
image_2024-05-17_210506550.png
This is the code, I do not really know if I'm doing something wrong or if its a problem with TGUI library itself
   
auto resourceFileTree(gui->get<tgui::TreeView>("ProjectTree"));

    resourceFileTree->onRightClick([=, this](const tgui::String& elem){

        std::string pathyding(getFullPath(elem));

        if(pathyding.empty())
        {
            return;
        }

        if(!activeContextMenu)
        {
            auto contextMenu(tgui::Panel::create());
            auto mousePos(sf::Mouse::getPosition(mgr->getContext()->window->getHandle()));
            contextMenu->setPosition(mousePos.x, mousePos.y);
            contextMenu->setSize(100, 150);
            contextMenu->getRenderer()->setBackgroundColor(sf::Color::White);
            contextMenu->getRenderer()->setBorderColor(sf::Color::Black);

            gui->add(contextMenu);

            contextMenu->onMouseLeave([=, this] {
                gui->remove(contextMenu);
                activeContextMenu = false;
            });

            auto viewButton(tgui::Label::create("View"));
            viewButton->getRenderer()->setTextColor(sf::Color::Black);
            viewButton->setTextSize(12);

            viewButton->onClick([=, this] {
                //auto windowsPos(mgr->getContext()->window->getSize());
                //auto windowsSize(mgr->getContext()->window->getSize());

                auto viewWindow(tgui::ChildWindow::create("Preview"));

                if (isImage(pathyding))
                {
                    loadImg(viewWindow, pathyding);
                }
                else
                {
                    loadText(viewWindow, pathyding);
                }

                gui->add(viewWindow);
                gui->remove(contextMenu);
                activeContextMenu = false;

            });

            contextMenu->add(viewButton);

            auto deleteButton(tgui::Label::create("Delete"));
            deleteButton->setPosition(deleteButton->getPosition().x, viewButton->getSize().y);
            deleteButton->getRenderer()->setTextColor(sf::Color::Black);
            deleteButton->setTextSize(12);
            deleteButton->onClick([=, this]{
                resourceFileTree->removeItem(resourceFileTree->getSelectedItem());
                auto assetToBeRemoved(assetManager->queryAsset(elem.toStdString()));
                if(assetToBeRemoved)
                {
                    switch (assetToBeRemoved->getType())
                    {
                        case Bread::ResourceType::RESOURCE_GRAPHIC:
                            assetManager->removeAsset<Bread::TextureAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_TEXT:
                            assetManager->removeAsset<Bread::FontAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_SOUND:
                            assetManager->removeAsset<Bread::SoundAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_MUSIC:
                            assetManager->removeAsset<Bread::MusicAsset>(assetToBeRemoved->getId());
                            break;
                    }
                    logger->log(LEVEL::LOG, "The asset: "+elem.toStdString()+" has been deleted");
                }

                gui->remove(contextMenu);
            });
            contextMenu->add(deleteButton);


            activeContextMenu = true;
        }
    });


Here is the call stack:

Capture.PNG
Capture.PNG

texus

A call stack would be useful for this as it would show where it crashes. You should be able to get one when you press the "Retry" button in that assertion popup.

When does the error happen exactly? When you click the delete button? If you put all the code in the "deleteButton->onClick" callback in comments, does the crash still occur? Does it occur if all the code except for the "gui->remove(contextMenu);" is placed in comments?

EngineerBread

I've updated the post to add the call stack.
To answer to your questions:

1. The error happens when you click the delete button
2. If I put all the code of the deleteButton->onClick callback in comments including gui->remove(contextMenu) the error does not occur
3. If I put all the code in comments except gui->remove(contextMenu) the error still occurs

texus

Inside a callback function you shouldn't remove a widget that is handling the event. So the "deleteButton->onClick" callback shouldn't alter the existence of deleteButton or contextMenu (and it is trying to remove the contextMenu).

There are some exceptional situations where it actually works fine, and when looking at the code I expected this to be one of them. Since it crashes when you add the remove call, it however looks like the remove won't work in this context.

One solution is to delay the execution of the line by placing it in a timer. Simply replace the "gui->remove(contextMenu);" with "tgui::Timer::scheduleCallback([=]{ gui->remove(contextMenu); });". This will cause the contextMenu to be removed at the next gui.draw() call instead of immediately while still handling the mouse release event.

EngineerBread

I've already tried what you have told me, and the issue is still there, the stack trace is the same.
Here is the code:
     
            auto deleteButton(tgui::Label::create("Delete"));
            deleteButton->setPosition(deleteButton->getPosition().x, viewButton->getSize().y);
            deleteButton->getRenderer()->setTextColor(sf::Color::Black);
            deleteButton->setTextSize(12);
            deleteButton->onClick([=, this]{

                resourceFileTree->removeItem(resourceFileTree->getSelectedItem());
                auto assetToBeRemoved(assetManager->queryAsset(elem.toStdString()));
                if(assetToBeRemoved)
                {
                    switch (assetToBeRemoved->getType())
                    {
                        case Bread::ResourceType::RESOURCE_GRAPHIC:
                            assetManager->removeAsset<Bread::TextureAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_TEXT:
                            assetManager->removeAsset<Bread::FontAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_SOUND:
                            assetManager->removeAsset<Bread::SoundAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_MUSIC:
                            assetManager->removeAsset<Bread::MusicAsset>(assetToBeRemoved->getId());
                            break;
                    }
                    logger->log(LEVEL::LOG, "The asset: "+elem.toStdString()+" has been deleted");
                }

                tgui::Timer::scheduleCallback([=, this]{ gui->remove(contextMenu); });
            });
            contextMenu->add(deleteButton);

texus

What exactly is on line 181 in editorscene.cpp (or whatever the line is now when you get a new stacktrace)?

EngineerBread

This is the whole function, I added the line: tgui::Timer::scheduleCallback([=]{ gui->remove(contextMenu); }); like you told me, unless I've done something wrong I don't know what might be happening

    auto resourceFileTree(gui->get<tgui::TreeView>("ProjectTree"));

    resourceFileTree->onRightClick([=, this](const tgui::String& elem){

        std::string pathyding(getFullPath(elem));

        if(pathyding.empty())
        {
            return;
        }

        if(!activeContextMenu)
        {
            auto contextMenu(tgui::Panel::create());
            auto mousePos(sf::Mouse::getPosition(mgr->getContext()->window->getHandle()));
            contextMenu->setPosition(mousePos.x, mousePos.y);
            contextMenu->setSize(100, 150);
            contextMenu->getRenderer()->setBackgroundColor(sf::Color::White);
            contextMenu->getRenderer()->setBorderColor(sf::Color::Black);

            gui->add(contextMenu);

            contextMenu->onMouseLeave([=, this] {
                gui->remove(contextMenu);
                activeContextMenu = false;
            });

            auto viewButton(tgui::Label::create("View"));
            viewButton->getRenderer()->setTextColor(sf::Color::Black);
            viewButton->setTextSize(12);

            viewButton->onClick([=, this] {
                //auto windowsPos(mgr->getContext()->window->getSize());
                //auto windowsSize(mgr->getContext()->window->getSize());

                auto viewWindow(tgui::ChildWindow::create("Preview"));

                if (isImage(pathyding))
                {
                    loadImg(viewWindow, pathyding);
                }
                else
                {
                    loadText(viewWindow, pathyding);
                }

                gui->add(viewWindow);
                tgui::Timer::scheduleCallback([=, this]{ gui->remove(contextMenu); });
                activeContextMenu = false;

            });

            contextMenu->add(viewButton);

            auto deleteButton(tgui::Label::create("Delete"));
            deleteButton->setPosition(deleteButton->getPosition().x, viewButton->getSize().y);
            deleteButton->getRenderer()->setTextColor(sf::Color::Black);
            deleteButton->setTextSize(12);
            contextMenu->add(deleteButton);
            deleteButton->onClick([=, this]{

                resourceFileTree->removeItem(resourceFileTree->getSelectedItem());
                auto assetToBeRemoved(assetManager->queryAsset(elem.toStdString()));
                if(assetToBeRemoved)
                {
                    switch (assetToBeRemoved->getType())
                    {
                        case Bread::ResourceType::RESOURCE_GRAPHIC:
                            assetManager->removeAsset<Bread::TextureAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_TEXT:
                            assetManager->removeAsset<Bread::FontAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_SOUND:
                            assetManager->removeAsset<Bread::SoundAsset>(assetToBeRemoved->getId());
                            break;
                        case Bread::ResourceType::RESOURCE_MUSIC:
                            assetManager->removeAsset<Bread::MusicAsset>(assetToBeRemoved->getId());
                            break;
                    }
                    logger->log(LEVEL::LOG, "The asset: "+elem.toStdString()+" has been deleted");
                }

                tgui::Timer::scheduleCallback([=]{ gui->remove(contextMenu); });
                activeContextMenu = false;

            });

            activeContextMenu = true;
        }
    });

texus

I can't tell you how to fix it if I don't even know what is causing the crash. I can see a line number in the call stack, but I don't have your source files so I don't know what is on that line.

At which line in the code is it crashing exactly? I assumed it would be the "gui->remove" line, but if you added tgui::Timer::scheduleCallback and got the same call stack, then it feels like the code must be crashing at some other line.

If your project isn't too big, could you perhaps upload a zip file of it, so that I can maybe try building it here tomorrow?
Otherwise it would be better to try and reproduce this in a simple program with minimal code, that makes it a lot easier to determine where it goes wrong as well.

EngineerBread

It fails at the gui->draw() function, here is the code to reproduce the issue:

#include <TGUI/TGUI.hpp>
#include <TGUI/Backend/SFML-Graphics.hpp>

void createMenu(std::shared_ptr<tgui::Gui> gui)
{
    auto contextMenu(tgui::Panel::create());
    contextMenu->setPosition(200,200);
    contextMenu->setSize(100, 150);
    contextMenu->getRenderer()->setBackgroundColor(sf::Color::White);
    contextMenu->getRenderer()->setBorderColor(sf::Color::Black);

    gui->add(contextMenu);

    contextMenu->onMouseLeave([=] {
        gui->remove(contextMenu);
    });

    auto viewButton(tgui::Label::create("View"));
    viewButton->getRenderer()->setTextColor(sf::Color::Black);
    viewButton->setTextSize(12);

    viewButton->onClick([=] {

        tgui::Timer::scheduleCallback([=]{ gui->remove(contextMenu); });
    });

    contextMenu->add(viewButton);

    auto deleteButton(tgui::Label::create("Delete"));
    deleteButton->setPosition(deleteButton->getPosition().x, viewButton->getSize().y);
    deleteButton->getRenderer()->setTextColor(sf::Color::Black);
    deleteButton->setTextSize(12);
    contextMenu->add(deleteButton);
    deleteButton->onClick([=]{

        tgui::Timer::scheduleCallback([=]{ gui->remove(contextMenu); });

    });
}

int main()
{
    sf::RenderWindow window(sf::VideoMode(800,600),"ISSUE");
    std::shared_ptr<tgui::Gui> gui(std::make_shared<tgui::Gui>(window));

    while(window.isOpen())
    {
        sf::Event event;
        while(window.pollEvent(event))
        {
            if(event.type == sf::Event::Closed)
            {
                window.close();
            }
            if(event.type == sf::Event::MouseButtonPressed)
            {
                if(event.mouseButton.button == sf::Mouse::Right)
                {
                    createMenu(gui);
                }
            }
            gui->handleEvent(event);
        }

        window.clear(sf::Color::Black);
        gui->draw();
        window.display();
    }
}


texus

#9
The problem lies with the remove call in onMouseLeave.

The remove call in deleteButton->onClick wasn't an issue by itself, but it causes contextMenu->onMouseLeave to be called during the remove. Calling remove again there (while the previous call hadn't finished yet) causes the crash.

If you put "tgui::Timer::scheduleCallback([=]{ gui->remove(contextMenu); });" in contextMenu->onMouseLeave then it won't crash. It will call "gui->remove(contextMenu);" one time too much (once for the onClick and once for the onMouseLeave), but TGUI will just ignore the second remove call.

EngineerBread

That did the trick, its working now, thank you so much ^^