Search code examples
c++qtqtreeviewqcomboboxqkeyevent

controling selected item with keyboard in a QCombobox with QTreeView


Using Qt 6.2.4, Ubuntu environment, I derived from QComboBox to set a QTreeView as its view.

It contains a tree (folders and files) with multiple parents and children, several levels of folders is possible.

All is working fine and behaves as I want.

Now, when one item is selected I'd like to use up and down arrow keys to select the previous or next item of the same parent.

I tried several things, I get the right sibling but I am not able to update the comboBox view accordingly (the comboBox always shows the item selected before, nothing is updated).

main.cpp:

#include <QApplication>

#include "tree-combobox.h"

int main(int argc, char **argv)
{
    QApplication app (argc, argv);

    TreeComboBox combo;
    combo.setGeometry(0, 0, 400, 50);
    combo.ShowFileList("/my/path/", "*");
    combo.show();

    return app.exec();
}

tree-combobox.h:

#include <QComboBox>
#include <QTreeView>
#include <QFileSystemModel>
#include <QMouseEvent>
#include <QAbstractItemView>

class TreeComboBox : public QComboBox
{
public:
    TreeComboBox(QWidget* parent = 0) : QComboBox(parent)
    {
        QTreeView* tree = new QTreeView(this); // tree view for combobox
        setView(tree); // assign it to combobox
    }

    void ShowFileList(QString path, QString filesFilter) // fill combobox with folders and files, specify path and wildcard(s)
    {
        // block signals
        this->blockSignals(true); // no signals emitted while stuffing the widget

        //// create files model
        QFileSystemModel *fileModel = new QFileSystemModel(this); // file system model to use
        // set options to file model
        fileModel->setReadOnly(true); // set it read-only
        fileModel->setFilter(QDir::AllDirs | QDir::AllEntries |QDir::NoDotAndDotDot); // all folders, all files, no file beginning with a dot
        fileModel->setOption(QFileSystemModel::DontUseCustomDirectoryIcons); // don't use icons from the files
        fileModel->setOption(QFileSystemModel::DontWatchForChanges); // the widget won't track changes on disk
        // set file filter for files model
        QStringList filter; // for files wildcard
        filter << filesFilter;
        fileModel->setNameFilters(filter);
        // set root for files model
        fileModel->setRootPath("");

        //// create view
        // tree view
        QTreeView *view = new QTreeView;
        this->setView(view); // assign tree view to combobox
        // files model
        this->setModel(fileModel); // assign files model to combobox
        // remove columns in tree view, to keep only filenames
        QModelIndex index = fileModel->index(path);
        for (int i = 1; i < fileModel->columnCount(); ++i) // all columns but first one
            view->hideColumn(i);
        // tree view options
        view->setAnimated(true); // animated
        view->setSortingEnabled(true); // sorting enabled by clicking on header
        view->sortByColumn(0, Qt::AscendingOrder); // sort values
        view->expand(index); // expand the view from the given path
        view->scrollTo(index); // set view from given path
        view->setRootIndex(index); // set root index to given path

        // allow signals again
        this->blockSignals(false);
    }

    QString GetFile() // get selected value from list
        // currentItemChanged() is emitted each time an item is clicked, even a parent item
        // this function returns an empty QString if the clicked item is not valid (i.e. a folder)
    {
        QModelIndex index = view()->currentIndex(); // current value index from QTree

        QString path = model()->data(index, QFileSystemModel::FilePathRole).toString(); // get full path value
        QFileInfo info(path); // to test this value
        if (info.isFile()) // if the value is really a file
            return path; // ... return its full path
        else // value is a folder
            return QString(); // ... so return nothing
    }

private:
    virtual void hidePopup() // control popup hiding behaviour
        // for a combobox, each time an item is clicked the popup disappears... but not for a folder this time !
    {
        if (!view()->underMouse()) { // is mouse over QTreeView ?
            QComboBox::hidePopup(); // if not collapse the comboBox
            return;
        }

        QModelIndex index = view()->currentIndex(); // get current index of selected item
        if (!model()->hasChildren(index)) // if it doesn't have children (so it is not a folder)
            QComboBox::hidePopup(); // collapse the comboBox
    }

    virtual void keyPressEvent(QKeyEvent *keyboardEvent) // keyboard event
    {
        if (this->hasFocus()) { // widget has to be active to accept keyboard keys
            if (keyboardEvent->key() == Qt::Key_Up) { // up
                //view()->setCurrentIndex(view()->currentIndex().sibling(view()->currentIndex().row() - 1, 0));
                //view()->selectionModel()->select(view()->currentIndex(), QItemSelectionModel::Select | QItemSelectionModel::Rows);

                QModelIndex index = view()->currentIndex();
                int n = index.row() - 1;
                QModelIndex sibling = index.siblingAtRow(n);
                if (sibling.isValid()) {
                    view()->setCurrentIndex(sibling);
                    view()->scrollTo(sibling);
                    //view()->selectionModel()->setCurrentIndex(sibling, QItemSelectionModel::ClearAndSelect);
                    //view()->selectionModel()->select(sibling, QItemSelectionModel::Select | QItemSelectionModel::Rows);
                    //tree->selectionModel()->select(tree->currentIndex(), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
                    //this->setCurrentIndex(sibling.row());
                }

                keyboardEvent->accept(); // accept keyboard event
            }
            else if (keyboardEvent->key() == Qt::Key_Down) { // down
                view()->setCurrentIndex(view()->currentIndex().sibling(view()->currentIndex().row() - 1, 0));

                keyboardEvent->accept();
            }
        }
    }

};

What's working so far in keyPressEvent(): the sibling index (QModelIndex) is the right one.

What I tried: the lines commented out with //.

Desired result: the comboBox selects and shows the previous or next item in the list, for the same parent (no need to go up or down to another parent).


Solution

  • Both the tree view and combobox need to be updated on key press, in order for the items to be visibly updated on the combobox.

    And since it appears a rootModelIndex needs to be set, and the initial root getting lost on subfolders, I added a QModelIndex as a member to store it in.

    Modified class with explanatory comments:

    class TreeComboBox : public QComboBox
    {
    public:
        //add a new memeber to save the root index 
        QModelIndex rootIndex;
        TreeComboBox(QWidget* parent = 0) : QComboBox(parent)
        {
            QTreeView* tree = new QTreeView(this);
            setView(tree);
        }
    
        void ShowFileList(QString path, QString filesFilter) 
        {
            this->blockSignals(true); 
    
            QFileSystemModel *fileModel = new QFileSystemModel(this); 
            fileModel->setReadOnly(true);
            fileModel->setFilter(QDir::AllDirs | QDir::AllEntries |QDir::NoDotAndDotDot);
            fileModel->setOption(QFileSystemModel::DontUseCustomDirectoryIcons); 
            fileModel->setOption(QFileSystemModel::DontWatchForChanges); 
            
            QStringList filter; 
            filter << filesFilter;
            fileModel->setNameFilters(filter);
            fileModel->setRootPath("");
    
            QTreeView *view = new QTreeView;
            this->setView(view); 
    
            this->setModel(fileModel);
    
            QModelIndex index = fileModel->index(path);
            for (int i = 1; i < fileModel->columnCount(); ++i) 
                view->hideColumn(i);
            
            view->setAnimated(true); 
            view->setSortingEnabled(true);
            view->sortByColumn(0, Qt::AscendingOrder); 
            view->expand(index); 
            view->scrollTo(index); 
            view->setRootIndex(index); 
    
            //save the root index
            rootIndex=index;
            //then set to your comboBox
            setRootModelIndex(index);
    
            this->blockSignals(false);
        }
    
        QString GetFile()
        {
            QModelIndex index = view()->currentIndex();
    
            QString path = model()->data(index, QFileSystemModel::FilePathRole).toString(); 
            QFileInfo info(path); 
            if (info.isFile()) 
                return path; 
            else 
                return QString(); 
        }
    
    private:
        virtual void hidePopup() 
        {
            if (!view()->underMouse()) { 
                QComboBox::hidePopup(); 
                return;
            }
    
            QModelIndex index = view()->currentIndex();
            if (!model()->hasChildren(index))
                QComboBox::hidePopup();
        }
    
        virtual void keyPressEvent(QKeyEvent *keyboardEvent)
        {
            if (this->hasFocus())
            {
                //I just used this to avoid having to select an item by clicking on it
                //you can remove it if it's of no use to you
                if(!view()->currentIndex().isValid())
                {
                    view()->setCurrentIndex(view()->indexAt(QPoint(0,0)));
                }
                if (keyboardEvent->key() == Qt::Key_Up)
                {
                    QModelIndex index = view()->currentIndex();
                    int n = index.row() - 1;
                    QModelIndex sibling = index.siblingAtRow(n);
    
                    if (sibling.isValid())
                    {
                        //update view's current index
                        view()->setCurrentIndex(sibling);
                        //update combobox
                        setRootModelIndex(sibling.parent());
                        setCurrentIndex(sibling.row());
                        //this is where you save the root index from being lost
                        if(rootModelIndex()!=rootIndex)
                            setRootModelIndex(rootIndex);
                    }
    
                    keyboardEvent->accept();
                }
                else
                    if (keyboardEvent->key() == Qt::Key_Down)
                    {
                        QModelIndex index = view()->currentIndex();
                        int n = index.row() + 1;
                        QModelIndex sibling = index.siblingAtRow(n);
    
                        if(sibling.isValid())
                        {
                            //update view's current index
                            view()->setCurrentIndex(sibling);
                            //update combobox
                            setRootModelIndex(sibling.parent());
                            setCurrentIndex(sibling.row());
                            
                            if(rootModelIndex()!=rootIndex)
                                setRootModelIndex(rootIndex);
                        }
                        
                        keyboardEvent->accept();
                    }
            }
        }
    };
    

    Result:

    navigating combobox's view using keyboard

    For more: