controling selected item with keyboard in a QCombobox with QTreeView

244 Views Asked by At

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).

1

There are 1 best solutions below

0
On BEST ANSWER

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: