Project

General

Profile

WPopupMenu behaving oddly when deleted on click

Added by Martin Melcher over 2 years ago

Hello,

I'm using the following configuration:

A push button:
PushButton=addWidget(make_unique("V"));

A popup menu:
unique_ptr MenuPtr;
MenuPtr=make_unique();
PopupMenu=MenuPtr.get();
// ... some menu items added here

.. attached, of course to the push button:
PushButton->setMenu(move(MenuPtr));

At a later stage, I'm adding a sub-menu to the same popup menu:
unique_ptr RefMenu=make_unique();
WMenuItem MyItem;
MyItem=RefMenu->addItem(/
menu item name (string) goes here */);
MyItem->triggered().connect(this, &DataEntryActionArea::RefObjectSelected); // "this" is a class which inherited from WContainerWidget

RefMenuItem=PushButton->menu()->addMenu("Reference to..", move(RefMenu));

Clicking on the menu item sets off a series of events, in which the sub-menu is deleted and recreated with new content:
PushButton->menu()->removeItem(RefMenuItem);
/* Code for adding back a new sub-menu same as shown above */

At this moment, I get the following message on the console:

[2022-Jun-13 21:14:16.315] 3088 [/ 1ttaUJfQ4m5cFrrt] [error] "WContainerWidget: removeWidget(): widget not in container"

In the user interface, I now have a free-floating unresponsive pop-up menu. It remains present even if the parent WContainerClass widget of the push button gets deleted.

Is this expected behavior in the case the WPopupMenu is removed the moment it is clicked? Since my menu has to dynamically adapt to changing content, the WPopupMenu can be deleted at any time, regardless of whether the user is currently interacting with it.
What is the proper and clean way of implementing this?

Thanks for your help,

Martin


Replies (5)

RE: WPopupMenu behaving oddly when deleted on click - Added by Martin Melcher over 2 years ago

Hi,
I just noted that the code lines were not pasted properly so half of the text is missing, here they are with the right formatting:

Adding the push button:

    unique_ptr<WPopupMenu> MenuPtr;
    PushButton=addWidget(make_unique<WPushButton>("V"));    //PushButton is a class member of type WPushButton *
    MenuPtr=make_unique<WPopupMenu>();
    PopupMenu=MenuPtr.get(); //PopupMenu is a class member of type WPopupMenu *
    // Some items added to PopupMenu here
    PushButton->setMenu(move(MenuPtr));

At a later stage, adding a sub menu:

  unique_ptr<WPopupMenu> RefMenu=make_unique<WPopupMenu>();
// A loop with a number of items added:
    WMenuItem *MyItem;
    MyItem=RefMenu->addItem(/* some string*/);
    MyItem->triggered().connect(this, &DataEntryActionArea::RefObjectSelected); //this, of type DataEntryActionArea, is a class derived from WContainerWidget
//And then adding to the menu
  RefMenuItem=PushButton->menu()->addMenu("Reference to..", move(RefMenu)); //RefMenuItem is a class member of type WPopupMenu*

And then finally the part that generates the error:

  if (RefMenuItem) PushButton->menu()->removeItem(RefMenuItem); //RefMenuItem is initialized to nullptr in the class constructor so this is only done if an item was previously added

which generates, on the console, this message:
[2022-Jun-13 21:14:16.315] 3088 [/ 1ttaUJfQ4m5cFrrt] [error] "WContainerWidget: removeWidget(): widget not in container"

And leads to a free floating, non-functional open popup menu in the browser window.

RE: WPopupMenu behaving oddly when deleted on click - Added by Mark Travis over 2 years ago

You have to wrap it in a lambda since the popup will be out of scope of the main message loop by the time you click a button.

dialog_->finished().connect( [=] {
         if (dialog_->result() == Wt::DialogCode::Accepted)  // They clicked the "ok" button
         {
             (blah, blah, blah, your code here for processing whatever the popup needs.)
             parent()->removeChild(dialog_);  // This is the money line! This removes the popup.
         } else {
             dialog_->contents()->addWidget(std::make_unique<Wt::WText>("Some message to tell them or ask them why they didn't click OK:"));
         }


    });

    dialog_->show();

RE: WPopupMenu behaving oddly when deleted on click - Added by Martin Melcher over 2 years ago

Hi Mark,

thanks a lot for your help.
Let me make sure I understand correctly. I'm not using a dialog, and I also don't want the button or menu dot be deleted directly when it's clicked.
It's a WPopupMenu attached to the WClickButton, and in this menu there's another WPopupMenu as sub menu item.
Shouldn't WPopupMenu take care of the show and hide actions when it is clicked?
I'm storing to the pointer to the sub-menu item in a class member so that it's available in the correct scope when, at a later state, I'm replacing the sub-menu with a new one.

Best regards,

Martin

RE: WPopupMenu behaving oddly when deleted on click - Added by Mark Travis over 2 years ago

Sorry, I used dialog_ as an example since I used a pushbutton on a dialog and wanted to get rid of the dialog once a pushbutton was selected.

Your exact situation I don't know, but at a minimum, you are placing a WCompositeWidget inside of a WWebWidget. The UI paints all of this in an event loop, then waits for a signal to change the UI. The pointer that you think you have saved is not pointing to that object any longer, which is why you are getting error. So, you need to put that "removeItem(RefMenuItem)" in a lambda attached to the "triggered().connect(this, &DataEntry" more along the lines of "triggered().connect( [=] { code you want to run when triggered, such as removeWidget });

Hope that helps and hopefully someone will be along who knows it more in-depth to get a better answer.

Mark

RE: WPopupMenu behaving oddly when deleted on click - Added by Martin Melcher over 2 years ago

Hi Mark, everyone,

I understand what you are getting at, but I don't think that's the problem.

When I connect to the event signal:

   MyItem->triggered().connect(this, &DataEntryActionArea::RefObjectSelected);

I am using the version of the connect() function which takes a reference to a WObject as first argument. That way, when the menu item is clicked, and the member function (DataEntryActionArea::RefObjectSelected - sorry for the convoluted name) is called, "this" will point to the instance which indeed contains the correct reference to the WPopupMenu, since the pointer is stored as a member variable. I like this method better because it also automatically disconnects the signal when the "client" object is deleted.

I managed to find a workaround - functional, but not satisfying.
When, instead of deleting and recreating the WPopupMenu sub-menu, I delete all its entries in a loop (unfortunately, WMenu and derived classes don't seam to have a clear() method) and add new ones back, everything seems to work fine, along these lines:

  //RefMenu is a member variable pointing to the sub-menu (WPopupMenu added is item in a parent WPopupMenu connected to a WPushButton)
  if (!RefMenu) return; 
  auto OldItemList=RefMenu->items();
  for (auto &i: OldItemList)
    RefMenu->removeItem(i);

 for (/*some loop on items to be added*/)
  {
    WMenuItem *MyItem;
    MyItem=RefMenu->addItem(/*some string*/);
    MyItem->triggered().connect(this, &DataEntryActionArea::RefObjectSelected); //Again, use the version which passes the "this" pointer to the WObject-derived class storing the pointer to the WPopupMenu, along many other useful things
  }

The fact that this works seems to indicate that

  • the pointer to the WPopupMenu is indeed correctly stored and available when the triggered() event is emitted
  • WPopupMenu seems to have undefined behavior when it's deleted while open.

Are there any other suggestions, or documented restrictions as to what can and can't be done wo the WPopupMenu in the method that I connect to the triggered() signal?
What is a more elegant way to handle this?

Thanks,

Martin

    (1-5/5)