Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

Always select the first item in a QListView during a search



  • Hi,

    I need your help. I have created a subclass of QSortFilterProxyModel with a custom filter which works very well. This proxymodel is set as model to my QListView. The code of the proxymodel looks like this:

    fungussortfilterproxymodel.h

    #ifndef FUNGUSSORTFILTERPROXYMODEL_H
    #define FUNGUSSORTFILTERPROXYMODEL_H
    
    #include <QSortFilterProxyModel>
    
    class FungusSortFilterProxyModel : public QSortFilterProxyModel
    {
        Q_OBJECT
    public:
        explicit FungusSortFilterProxyModel(QObject *parent = nullptr);
        void setIdFilter(const int &id);
        void setNameFilter(const QString &name);
        bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
    
    private:
        const int mIdColumn = 0;
        const int mNameColumn = 1;
        int mIdFilterValue = -1;
        QRegularExpression mRegularExpression;
    };
    
    #endif // FUNGUSSORTFILTERPROXYMODEL_H
    

    fungussortfilterproxymodel.cpp

    #include "fungussortfilterproxymodel.h"
    
    FungusSortFilterProxyModel::FungusSortFilterProxyModel(QObject *parent): QSortFilterProxyModel{parent} {}
    
    void FungusSortFilterProxyModel::setIdFilter(const int &id)
    {
        if(mIdFilterValue != id)
        {
            mIdFilterValue = id;
            emit invalidateFilter();
        }
    }
    
    void FungusSortFilterProxyModel::setNameFilter(const QString &name)
    {
        mRegularExpression.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
        if(name.isEmpty()){
            mRegularExpression.setPattern(QString());
        } else if (name.contains(QRegularExpression("\\s"))) {
            QStringList word = name.split(QRegularExpression("\\s"));
            mRegularExpression.setPattern("^" + QRegularExpression::escape(word[0]) + "\\w*\\s" + QRegularExpression::escape(word[1]) + "\\w*");
        } else {
            mRegularExpression.setPattern("^" + QRegularExpression::escape(name) + "\\w*");
        }
    
        emit invalidateRowsFilter();
    }
    
    bool FungusSortFilterProxyModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
    {
        if(!mRegularExpression.pattern().isEmpty()){
            const QModelIndex index = sourceModel()->index(source_row, mNameColumn, source_parent);
            const QString name = sourceModel()->data(index).toString();
            return (name.contains(mRegularExpression));
        }
        if(mIdFilterValue != -1){
            const int id = sourceModel()->index(source_row, mIdColumn).data().toInt();
            return (id == mIdFilterValue);
        } else {
            return true;
        }
    }
    

    In the next step, I want to highlight the first Item of QListView at programm start and during search via a QLineEdit. So I make a slot and connect the signal textChanged to my custom slot in the constructor of mainwindow.cpp.

    connect(ui->searchbar, &QLineEdit::textChanged, this, &MainWindow::handleItemSelection);
    

    My custom slot, called handleItemSelection looks like this:

    void MainWindow::handleItemSelection(const QString &name)
    {
        Q_UNUSED(name);
    
        QModelIndex index;
    
        if(ui->modelSelection->currentIndex() == Model::Fullnames){
            index = mFungusList->model()->index(0,1);
            mFungusList->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
        }
    }
    

    This works, too. So I connect the signal QItemSelectionModel::selectionChanged to my custom slot.

    connect(mFungusList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::fungusSelectionChanged);
    

    fungusSelectionChanged slot:

    void MainWindow::fungusSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
    {
        QModelIndex index = selected.indexes().first();
    
        const int id = mFungusProxyModel->data(index.siblingAtColumn(0)).toInt();
        const QString redlist = mFungusProxyModel->data(index.siblingAtColumn(7)).toString();
    
        if(redlist.isEmpty())
            ui->redlist->setCurrentIndex(-1);
    
        mSynonymFilterModel->setIdFilter(id);
        index = mFungusProxyModel->mapToSource(index);
        mMapper->setCurrentModelIndex(index);
    }
    

    And this works too if I click the item in QListView with my mouse. But when I use the QLineEdit (search function for QListView) my programm crash at FungusSortFilterProxyModel at the position "emit invalidateRowsFilter()" and says:

    error.png

    In my qDebug() output qt creator says:
    ASSERT: "!isEmpty()" in file /usr/include/qt6/QtCore/qlist.h, line 591

    I have also figured out that it must have something to do with this line. If I remove this line, the search works and my program does not crash, but I do not see the error.

    QModelIndex index = selected.indexes().first();
    

    However, if I omit this line then my QDataWidgetMapper (mMapper) no longer works....

    Can someone please help me and tell me what I'm doing wrong?


  • Lifetime Qt Champion

    Hi,

    From the looks of it, you do not take into account the empty selection case.



  • @SGaist

    Can you explain in more detail where I need to put this? Or do you mean in the FungusSortFilterProxyModel::setNameFilter(const QString &name)?



  • @Gabber said in Always select the first item in a QListView during a search:

    selected.indexes().first();

    if(selected.indexes().isEmpy())
    {
    // no selection at all
    }
    else
    {
    QModelIndex index = selected.indexes().first();
    }
    


  • @mpergand
    I am so ....! This was the solution! Thanks
    Now I do:

        QModelIndex index;
        if(selected.indexes().isEmpty()){
            return;
        } else {
           index = selected.indexes().first();
        }
    


  • Something is still strange, maybe someone can give me a hint. In my MainWindow I have a QStackWidget with 3 QListViews. The three QListViews are selected via a QComboBox. I connect it in my mainwindow.cpp via:

        connect(ui->modelSelection, QOverload<int>::of(&QComboBox::currentIndexChanged), ui->stackedListViews, &QStackedWidget::setCurrentIndex);
        connect(ui->searchbar, &QLineEdit::textChanged, this, &MainWindow::handleItemSelection);
        connect(ui->modelSelection, &QComboBox::currentTextChanged, this, &MainWindow::handleItemSelection);
        connect(mFungusList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::fungusSelectionChanged);
        connect(mSynonymList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::synonymSelectionChanged);
    

    Yesterday I wrote that it works fine with my first QListView (index 0 of the QComboBox). But as soon as it selects the second QListView via the QComboBox, the search works, but it doesn't map me the data via the QDataWidgetMapper (mMapper) anymore. I even created a separate slot.

    void MainWindow::synonymSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
    {
        QModelIndex selectedIndex;
        QModelIndex proxyIndex;
        QModelIndex sourceIndex;
    
        if(selected.indexes().isEmpty()){
            return;
        } else {
           selectedIndex = selected.indexes().first();
        }
    
        const int id = mSynonymProxyModel->data(selectedIndex.siblingAtColumn(2)).toInt();
        qDebug() << id;
        mFungusProxyModel->setIdFilter(id);
        mSynonymFilterModel->setIdFilter(id);
    
        proxyIndex = mFungusProxyModel->index(0,0);
        sourceIndex = mFungusProxyModel->mapToSource(proxyIndex);
    
        qDebug() << sourceIndex;
    
        const QString redlist = mFungusProxyModel->data(proxyIndex.siblingAtColumn(7)).toString();
    
        qDebug() << redlist;
    
        if(redlist.isEmpty())
            ui->redlist->setCurrentIndex(-1);
    
        mMapper->setCurrentModelIndex(sourceIndex);
        mFungusProxyModel->setIdFilter(-1);
    
    }
    

    My complete handleItemSelection method looks like this:

    void MainWindow::handleItemSelection(const QString &name)
    {
        Q_UNUSED(name);
    
        QModelIndex index;
    
        if(ui->modelSelection->currentIndex() == Model::Fullnames){
            index = mFungusList->model()->index(0,1);
            mFungusList->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
        }
        if(ui->modelSelection->currentIndex() == Model::Synonyms){
            index = mSynonymList->model()->index(0,1);
            mSynonymList->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
        }
        if(ui->modelSelection->currentIndex() == Model::GermanNames){
            index = mGermanNameList->model()->index(0,0);
            mGermanNameList->selectionModel()->select(index, QItemSelectionModel::ClearAndSelect);
        }
    }
    

    When I switch to synonyms in my QComboBox and select an entry via mouse click from the QListView it works as expected, when I try to select an entry via the search it does not recognize the data for me. My proxyIndex is then always QModelIndex(-1,-1,0x0,QObject(0x0)).

    I have no idea what could be wrong. Does anyone have a tip for me again?



  • @Gabber said in Always select the first item in a QListView during a search:

    when I try to select an entry via the search it does not recognize the data for me. My proxyIndex is then always QModelIndex(-1,-1,0x0,QObject(0x0)).

    I'm not sure to understand what " to select an entry via the search" really means.

    QModelIndex(-1,-1,0x0,QObject(0x0)).
    Obviously, there's no selection, so you have to select one item by code I guess.



  • @mpergand said in Always select the first item in a QListView during a search:

    I'm not sure to understand what " to select an entry via the search" really means.

    I have a QLineEdit that searches my data in my subclass QSortFilterProxyModel (in my case FungusSortFilterProxyModel) via the setNameFilter(QString) function using a Regular Expression. And after each search I want to select the first element in my second QListView (mSynonymList) and map the data via QDataWidgetmapper to the corresponding QLineEdits.

    If I click it with the mouse everything works as expected, but if I tell it to select it (see handleItemSelection) unfortunately it doesn't work. But it is not clear to me why not....

    I hope I could explain it a little bit...



  • @Gabber
    Put a breakpoint at the beginning of handleItemSelection(),
    run the debugger and step by step look what happens here.



  • I think I have found my error.

        //3 QListViews with 3 subclass QSortFilterProxyModel
        mFungusList->setModel(mFungusProxyModel);
        mSynonymList->setModel(mSynonymProxyModel);
        mGermanNameList->setModel(mGermanNameProxyModel);
    

    I already said that I have my 3 QListViews in a QStack widget. If I now search the first ProxyModel (in my case mFungusProxyModel) and have no hit then select the second page of my QStackWidget and search the stored model (in my case mSynonynProxyModel). Since I have the following signal

    connect(mSynonymList->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::synonymSelectionChanged);
    

    it accesses the slot SynonymSelectionChanged and exactly here it uses mFungusProxyModel which is "empty" since no hit was found and therefore I always get an empty QModelIndex.

    I simply created a second model in my Constructor of MainWindow.cpp and added the source model twice.

        mFungusProxyModel = new FungusSortFilterProxyModel;
        mFungusFilterModel = new FungusSortFilterProxyModel;
    
        // add source Model
        mFungusProxyModel->setSourceModel(mFungusDataModel);
        mFungusFilterModel->setSourceModel(mFungusDataModel);
    

    @SGaist @mpergand
    Now I just have a question for the experts, can you do something like that or can it cause complications?

    It is currently working.



  • @Gabber said in Always select the first item in a QListView during a search:

    // add source Model
    mFungusProxyModel->setSourceModel(mFungusDataModel);
    mFungusFilterModel->setSourceModel(mFungusDataModel);

    Seems strange to me, but maybe i'm wrong - Model-View pattern gives me headache, too complicated for my little brain :)

    In one of my app, i'm using a sorting model and i do like this:

    historyModel=new HistoryModel();
    historySortFilter= new HistorySortFilter();
    
    historySortFilter->setSourceModel(historyModel);
    tableView->setModel(historySortFilter);
    

    As you can see, i first set the proxy model to the sorting model,
    then set the sorting model to the view.

    Hope this helps.


Log in to reply