Search code examples
c++qt

Infinite focus loop in QT


enter image description here

I'm writing a simple tool in Qt 6.8 C++23 to learn about Qt. Ubuntu 22.04 VS Code.

I have 2 QLineEdit for source directory and target directory. When the user focuses on the line edit the tool should open QFileDialog so that they don't have to type in the full path. The line edit itself should be read only.

The source for the containing dialog can be found on GitHub.

The problem is that when the file dialog closes it immediately reopens because the focus is still on the line edit. How do I lose the focus after the file dialog closes?

#ifndef DIRECTORYLINEEDIT_H_
#define DIRECTORYLINEEDIT_H_

#include <QLineEdit>
#include <QFileDialog>
class DirectoryLineEdit : public QLineEdit {
    Q_OBJECT

public:
    explicit DirectoryLineEdit(const char* dleName, const char* title, int leWidth, QWidget *parent = nullptr)
    : QLineEdit{parent}, fileDialogTitle{title}
    {
        setObjectName(QString::fromUtf8(dleName));
        setStyleSheet("width: " + QString::number(leWidth) + "px;");
        setReadOnly(true);
    }

    void focusInEvent(QFocusEvent *event)
    {
        QString textToChange = text();
    
        textToChange = QFileDialog::getExistingDirectory(nullptr, fileDialogTitle,
            textToChange, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
    
        setText(textToChange);    
    }
    

private:
    QString fileDialogTitle;
};

#endif // DIRECTORYLINEEDIT_H_

Solution

  • It's important to be very careful when creating/destroying widgets on focus events, especially if they are windows, as it would likely create issues with some level of recursion, which is exactly your case: even though this is not an "instant recursion", it conceptually is, because the focus is automatically reacquired as soon as the dialog is closed.

    The QFocusEvent provides a reason() function that allows us to understand what caused the focus event.

    Checking the reason is of utmost importance for a case like this (creating a new dialog), because there are many reasons for which the override should not proceed: even assuming you didn't have the partial recursion issue explained above, the dialog would be shown if the user opens the context menu of the line edit (specifically, right after the menu is closed), or if the user switched to another program and then switched back to yours.

    For this situation, you should probably show the dialog only if the reason is one of the following:

    • MouseFocusReason
    • TabFocusReason
    • BacktabFocusReason
    • ShortcutFocusReason (if you used a QLabel as "buddy")

    The function would be something like this:

    void focusInEvent(QFocusEvent *event)
    {
        if (event->reason() == Qt::MouseFocusReason ||
            event->reason() == Qt::TabFocusReason ||
            event->reason() == Qt::BacktabFocusReason ||
            event->reason() == Qt::ShortcutFocusReason)
        {
            QString textToChange = text();
    
            textToChange = QFileDialog::getExistingDirectory(nullptr, fileDialogTitle,
                textToChange, QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks);
    
            setText(textToChange);
        }
    }
    

    You may also want to check if the line edit has no contents before opening the dialog when the reason is tab navigation, otherwise it would be quite annoying to cycle through widgets: you may still override the line edit's keyPressEvent and open the dialog only with common "edit" keys (such as F2, Space, Enter or Return).

    Also remember that the user could cancel the dialog (which returns an empty string), so you should probably call setText() only as long as textToChange is not empty.

    Then, if you want to allow the context menu (and you normally should), you should also query the QGuiApplication::mouseButtons() in case of MouseFocusReason and only show the dialog if the line edit has no contents or the button is not RightButton.

    Finally, consider that the above obviously prevents showing the dialog as long as the line edit already got focus, therefore if the user selected the wrong directory by mistake, they would need to click on another widget and click back on the line edit in order to change it, which is quite unintuitive. This could be solved either by overriding mousePressEvent() too or by adding an action to the line edit (see QLineEdit::addAction()), which is probably much more intuitive.