Search code examples
pythonpyqt5qt5pyside

Tab key event strange behaviour


I have a small GUI app made using pyqt5. I found a strange problem while using eventFilter...

def eventFilter(self, source, event):
       if event.type() == QtCore.QEvent.KeyPress:
           # RETURN
           if event.key() == QtCore.Qt.Key_Return:
               if source is self.userLineEdit:
                   self.pswLineEdit.setFocus()
               elif source is self.pswLineEdit:
                   self.LoginButton.click()
           # TAB
           elif event.key() == QtCore.Qt.Key_Tab: 
               if source is self.userLineEdit: 
                   self.pswLineEdit.setFocus()
       return super().eventFilter(source, event)

While pressing enter key just behave normally, tab key does not. I don't know where the problem could be. I'm going to link a video to show the exact problem as I'm not able to describe how this is not working

Link to video I know it's pixelated (sorry) but the important thing is the behavior of the cursor

SMALL EXAMPLE

import sys
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit
from PyQt5 import QtCore

class App(QWidget):
    def __init__(self):
        super().__init__()
        self.title = 'Hello, world!'
        self.left = 10
        self.top = 10
        self.width = 640
        self.height = 480
        self.initUI()

    def initUI(self):
        self.setWindowTitle(self.title)
        self.setGeometry(self.left, self.top, self.width, self.height)
        self.userEdit = QLineEdit(self)
        self.pswEdit = QLineEdit(self)
        self.userEdit.setPlaceholderText("Username")
        self.pswEdit.setPlaceholderText("Password")
        self.userEdit.installEventFilter(self)
        self.pswEdit.installEventFilter(self)

        mainLayout = QVBoxLayout()
        mainLayout.addWidget(self.userEdit)
        mainLayout.addWidget(self.pswEdit)
        self.setLayout(mainLayout)
        self.show()

    def eventFilter(self, source, event):
        if event.type() == QtCore.QEvent.KeyPress:
            # RETURN
            if event.key() == QtCore.Qt.Key_Return:
                if source is self.userEdit:
                    self.pswEdit.setFocus()
            # TAB
            elif event.key() == QtCore.Qt.Key_Tab: 
                if source is self.userEdit: 
                    self.pswEdit.setFocus()
        return super().eventFilter(source, event)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    ex = App()
    sys.exit(app.exec_())


Solution

  • If you don't filter events, they are processed by the object and eventually propagated back to the parent.

    By default, QWidget will try to focus the next (or previous, for Shift+Tab) child widget in the focus chain, by calling its focusNextPrevChild(). If it can do it, it will actually set the focus on that widget, otherwise the event is ignored and propagated to the parent.

    Since most widgets (including QLineEdit) don't handle the tab keys for focus changes on their own, as they don't have children, their parent will receive it, which will call focusNextPrevChild() looking for another child widget, and so on, up to the object tree, until a widget finally can handle the key, eventually changing the focus.

    In your case, this is what's happening:

    1. you check events and find that the tab key event is received by the first line edit;
    2. you set the focus on the other line edit, the password field;
    3. you let the event be handled anyway by the widget, since you're not ignoring or filtering it out;
    4. the first line edit calls focusNextPrevChild() but is not able to do anything with it;
    5. the event is propagated to the parent, which then calls its own focusNextPrevChild();
    6. the function checks the current focused child widget, which is the password field you just focused, and finds the next, which coincidentally is the first line edit, which gets focused again;

    The simple solution is to just add return True after changing the focus, so that the event doesn't get propagated to the parent causing a further focus change:

        if event.key() == QtCore.Qt.Key_Tab: 
            if source is self.userEdit:
                self.pswEdit.setFocus()
                return True
    

    Note that overriding the focus behavior is quite complex, and you have to be very careful about how focus and events are handled, especially for specific widgets that might deal with events in particular ways (studying the Qt sources is quite useful for this), otherwise you'll get unexpected behavior or even fatal recursion.

    For instance, there's normally no need for an event filter for the return key, as QLineEdit already provides the returnPressed signal:

        self.userEdit.returnPressed.connect(self.pswEdit.setFocus)
    

    Qt already has a quite thorough focus management system, if you just want more control over the way the tab chain works use existing functions like setTabOrder() on the parent or top level window, and if you want to have more control over how (or if) they get it, use setFocusPolicy().