Project

General

Profile

PopupMenu or Combobox with submenus in WTableView

Added by Martin Melcher 8 months ago

Hi,

I have a WTableView where some items can be edited in drop-down menus.
Since WComboBox does not support sub-menus (correct?), I'm making my replacement using WPopupMenu, attached to a WPushButton.
This works fine normally, but inside the WTableView, when I press the button, the WPopupMenu is shown and immediately disappears before the user can take any action.
I'm posting just parts of the code to keep it reasonably simple. Am I doing this wrong? Any reason why the popup menu will not stay open, after the push button is clicked, until the user selects a menu item?
Or is there a more simple way to do this, using somehow a WComboBox that has sub-menus?

Here is my "mock" Combo-Box with submenus:

class NestedPopupMenu : public WPopupMenu
{
//------- Class based on WPopupMenu with some functions to conveniently add sub-menus
  Signal<unsigned> SelectionEvent; //Emitted when the user makes a selection
// ...
};

class NestedPopupSelector : public WContainerWidget
{
  NestedPopupMenu *Menu; //My WPopupMenu 
  WText *Text;
  WPushButton *Button;
  Signal<unsigned>SelectionEvent;
  bool HasSelection;
  unsigned MySelection;
public:
  NestedPopupSelector() : HasSelection{false}
  {
    Button=addWidget(make_unique<WPushButton>("V"));
    Text=addWidget(make_unique<WText>(""));
    Text->addStyleClass("SelectorText");
    Menu=new NestedPopupMenu();
    Menu->GetSelectionEvent().connect(this, &NestedPopupSelector::SelectionTrigger);
    Button->setMenu(unique_ptr<NestedPopupMenu>(Menu));
  }
  Signal<unsigned> &GetSelectionEvent() {return SelectionEvent;}
  void SelectionTrigger(unsigned ID)
  {
    SetSelection(ID);
    SelectionEvent.emit(ID);
  }
  void ClearEntries() {Menu->ClearEntries();}
  void AddEntry(unsigned ID, const string &name) {Menu->AddEntry(ID,name);}
  void SetSelection(unsigned ID) //Displays the name of the selected items in the WText box when the user made a selection
  {
    HasSelection=true;
    MySelection=ID;
    string name;
    if (Menu->FindName(ID, name))
    {
      Text->setText(name);
    }
  }
  void MakeMenu() { //Creates the submenu entries, to be called after all entries were added with AddEntry
    Menu->MakeMenu();
    if (HasSelection)
      SetSelection(MySelection);
  }
};  

My Item Delegate class (implementen of WAbstractItemDelegate) will create a custom widget in the update finction:

unique_ptr<WWidget> SimItemDelegate::update(WWidget *widget, const WModelIndex &index, WFlags<ViewItemRenderFlag> flags)
{
  SimItemWidget *MyWidget=FindWidget(widget); //Checks if widget is indeed my custom widget of type SimItemWidget and returns it
  if (MyWidget)
  {
    MyWidget->SetState(any_cast<ItemContents&>(index.data()));
    MyWidget->SetFlags(flags);
    return nullptr;
  }
  // else - will construct a new widget 
}

Finally, in my custem widget, the SetFlags function will call SimItemWidget::BuildNew() wo build my "Mock" combo box:

void SimItemWidget::BuildNew()
{
  // Check if the new editor view needs to be created, if yes:
    MySelector=addWidget(make_unique<NestedPopupSelector>());
    MySelector->GetSelectionEvent().connect(this, &SimItemWidget::ComboBoxSelection);
    UpdateSelectionMenu(); //Here the menu entries will be added and finally MySelector->MakeMenu() will be called

}


Replies (1)

RE: PopupMenu or Combobox with submenus in WTableView - Added by Martin Melcher 8 months ago

Hi,

after days of playing around, I found what the issue is - but I need to know if this expected behavior or not.

In short: Adding a WPushButton with a connected WPopupMenu in a WTableView works only when setting setEditTriggers(Wt::EditTrigers::SingleClicked). Without calling setEditTriggers or with any other argument, the WPopupMenu won't stay open when the butten is clicked, just flash for a fraction of a second.

Here is a simplified C++ code - I dumbed my widget down to remove all other possible root causes:




class DumbItemModel : public Wt::WAbstractItemModel {
public:
    DumbItemModel() {}

    virtual int rowCount(const Wt::WModelIndex& parent = Wt::WModelIndex()) const override {
        if (!parent.isValid()) // Only the root has children
            return rowCount_;
        else
            return 0; // No children of children
    }

    virtual int columnCount(const Wt::WModelIndex& parent = Wt::WModelIndex()) const override {
        return columnCount_; // Same for all rows
    }

  virtual cpp17::any data(const Wt::WModelIndex& index, ItemDataRole role = Wt::ItemDataRole::Display) const override {
        if (role == Wt::ItemDataRole::Display)
            return std::string("test");
        return cpp17::any();
    }
  virtual cpp17::any headerData(int section, Orientation orientation=Orientation::Horizontal, ItemDataRole role=ItemDataRole::Display) const override {
        if (role == Wt::ItemDataRole::Display)
            return std::string("test");
        return cpp17::any();
    }

    void populate() { //Added to mimic the behavior of the original widget
        beginInsertRows(Wt::WModelIndex(), 0, 2);
        columnCount_ = 3;
        endInsertColumns();
        beginInsertColumns(Wt::WModelIndex(), 0, 2);
        rowCount_ = 3;
        endInsertRows();
    }
  virtual Wt::WModelIndex index(int row, int column, const Wt::WModelIndex& parent = Wt::WModelIndex()) const override {
        if (!parent.isValid() && row >= 0 && row < rowCount_ && column >= 0 && column < columnCount_)
            return createIndex(row, column, nullptr);
        return Wt::WModelIndex();
    }

    virtual Wt::WModelIndex parent(const Wt::WModelIndex& index) const override {
        return Wt::WModelIndex(); // No parent since it's a flat model
    }


private:
    int rowCount_ = 0;
    int columnCount_ = 0;
};

class DumbItemDelegate : public Wt::WAbstractItemDelegate {
public:
  DumbItemDelegate() = default;
  ~DumbItemDelegate() override = default;

  // Overloading the update function
  virtual unique_ptr<WWidget> update (WWidget *widget, const WModelIndex &index, WFlags< ViewItemRenderFlag > flags) override
  {
    if (!widget) {
      // Create a new container widget
      auto container = std::make_unique<Wt::WContainerWidget>();

      // Create a push button
      auto button = container->addWidget(std::make_unique<Wt::WPushButton>("Test"));

      // Create a popup menu
      auto menu = std::make_unique<Wt::WPopupMenu>();
      menu->addItem("Item1");
      menu->addItem("Item2");

      // Set the menu to the button
      button->setMenu(std::move(menu));

      return container;
    }

    // Return nullptr if index is valid as we are focusing on the null case
    return nullptr;
  }

  // Overloading editState (stub implementation)
  virtual cpp17::any editState (WWidget *widget, const WModelIndex &index) const override
  {
    return Wt::cpp17::any();
  }

  // Overloading setEditState (stub implementation)
  virtual void setEditState (WWidget *widget, const WModelIndex &index, const cpp17::any &value) const override {
    // Do nothing for now
  }

  // Overloading setModelData (stub implementation)
  virtual void   setModelData (const cpp17::any &editState, WAbstractItemModel *model, const WModelIndex &index) const override{}
};

class MyTestWidget : public Wt::WContainerWidget {
public:
  MyTestWidget()  {
    auto model=std::make_shared<DumbItemModel>();

    auto delegate=std::make_shared<DumbItemDelegate>();

    auto TableView = addWidget(std::make_unique<Wt::WTableView>());
    TableView->setAlternatingRowColors(true);
    TableView->setSortingEnabled(false);
    TableView->setSelectionBehavior(SelectionBehavior::Items);
    TableView->setSelectionMode(SelectionMode::Extended);

    model->populate();
  TableView->setModel(model);
    TableView->setItemDelegate(delegate);
    TableView->setHeaderItemDelegate(delegate);

//-------------------------------------------
//The push button is functional only when this line is present. WHY??---------------------
TableView->setEditTriggers(Wt::EditTrigger::SingleClicked);
//-------------------------------------------

  }
};


    (1-1/1)