pythonpyqt5qlineeditqregexp

QLineEdit unexpected behavior using QRegExpValidator


I'm using a QLineEdit widget to enter email addresses and I set up a QRegExpValidor to validate the entry.

The validator is working as far as preventing the input of characters not allowed in the QRegExp, but funny enough, it allows to enter intermediate input, either by pushing the enter key or by firing the "editingfinished" signal.

I verified the return of the validator which is correct (Intermediate or Acceptable).

Checking the PyQt5 documentation it seams that an intermediate state of the validator does not prevent the focus to change to another widget. Furthermore, it stops the firing of the editingFinished and returnedPressed signals so the user may enter wrong address as far as it marches partially the RegExp. Ref: https://doc.qt.io/qt-5/qlineedit.html#acceptableInput-prop

I was able to solve my needs by removing the validator from the QLineEdit widget, and placing a method "checkValidator" linked to the cursorPositionChanged of the QLineEdit widget discarting the last character entered when nonacceptable, and setting the focus back to the QLineEdit when it validats intermmediate. It works just fine but when the the focus was reset, the others signals on other widgets were fired one at a time. Momentarily, I handle this by checking if the sender has focus at the start of the methods (see lookForFile)

Even though I could handle the issue, I appreciate very much anybody explaining me the proper way to use the RegExpValidator and or why resetting the focus fires the other signals out of the blue.

 def setUI(self):
    self.setWindowTitle("EMail Settings")
    self.setModal(True)

    rx = QRegExp(r"[a-z0-9_%]+@[a-z0-9%_]+\.[a-z0-9%_]{3,3}")
    lblAddress = QLabel("EMail Address")
    self.lineAddress = QLineEdit(self)
    self.mailValidator = QRegExpValidator(rx, self.lineAddress)
    #self.lineAddress.setValidator(self.mailValidator)
    self.lineAddress.cursorPositionChanged.connect(self.checkValidator)
    self.lineAddress.returnPressed.connect(self.checkValidator)

    lblPassword = QLabel("Password")
    self.linePwd = QLineEdit()
    self.linePwd.setEchoMode(QLineEdit.PasswordEchoOnEdit)

    lblOauth2 = QLabel("Oauth2 Token")
    self.lineOauth = QLineEdit()
    pushOauth = QPushButton("...")
    pushOauth.setObjectName("token")
    pushOauth.clicked.connect(self.lookForFile)
    pushOauth.setFixedWidth(30)



@pyqtSlot()
def checkValidator(self):
    self.lineAddress.blockSignals(True)
    v = self.mailValidator.validate(self.lineAddress.text(), len(self.lineAddress.text()))
    if v[0] == 0:
        self.lineAddress.setText(self.lineAddress.text()[:-1])
    elif v[0] == 1:
        self.lineAddress.setFocus()
    elif v[0] == 2:
        pass
    print("validates", v)
    self.lineAddress.blockSignals(False)

 @pyqtSlot()
def lookForFile(self):
    try:
        if not self.sender().hasFocus():
            return
        baseDir = "C"
        obj = self.sender()
        if obj.objectName() == "Draft":
            capt = "Email Draft"
            baseDir = os.getcwd() + "\\draft"
            fileType = "Polo Management Email (*.pad)"
            dialog = QFileDialog(self, directory=os.getcwd())
            dialog.setFileMode(QFileDialog.Directory)
            res = dialog.getExistingDirectory()

        elif obj.objectName() == "token":
            capt = "Gmail Outh2 token File"
            fileType = "Gmail token Files (*.json)"
            baseDir = self.lineOauth.text()
            res = QFileDialog.getOpenFileName(self, caption=capt, directory=baseDir, filter=fileType)[0]
        fileName = res
        if obj.objectName() == "Draft":
            self.lineDraft.setText(fileName)
        elif obj.objectName() == "tokenFile":
            self.lineOauth.setText(fileName)
    except Exception as err:
        print("settings: lookForFile", err.args)

Hope to answer @eyllanesc and Qmusicmante request with this minimal reproducible example. I change the regex for a simple one allowing for a string of lower case a-z follow by a dot and three more lowercase characters.

What I intend is the validator not allowing the user to enter a wrong input. The example allows for "xxxzb.ods" but also allows "xxxzb" or "xxxzb.o" for instance. In short, not allowing the user to enter a wrong input.

This is my minimal reproducible example:

class CheckValidator(QDialog):
    def __init__(self, parent=None):
         super().__init__()
         self.parent = parent
         self.setUI()

    def setUI(self):
        self.setWindowTitle("EMail Settings")
        self.setModal(True)

        rx = QRegExp(r"[a-z]+\.[a-z]{3}")
        lblAddress = QLabel("Check Line")
        self.lineAddress = QLineEdit()
        self.mailValidator = QRegExpValidator(rx, self.lineAddress)
        self.lineAddress.setValidator(self.mailValidator)
        self.lineAddress.cursorPositionChanged[int, int].connect(lambda 
             oldPos, newPos: self.printValidator(newPos))

        lblCheck = QLabel("Check")
        lineCheck = QLineEdit()

        formLayout = QFormLayout()
        formLayout.addRow(lblAddress, self.lineAddress)
        formLayout.addRow(lblCheck, lineCheck)
        self.setLayout(formLayout)

    @pyqtSlot(int)
    def printValidator(self, pos):
        print(self.mailValidator.validate(self.lineAddress.text(), pos))

if __name__ == '__main__':
app = QApplication(sys.argv)
tst = CheckValidator()
tst.show()
app.exec()

Solution

  • I found a solution and I post it here for such a case it may help somebody else. First I remove the QRegExpValidator from the QLineEdit widget. The reason being is that QLineEdit only fires editingFinished (and we'll need it ) only when QRegExpValidator returns QValidator.Acceptable while the validator is present.

    Then we set a method fired by the 'cursorPositionchanged' signal of QlineEdit widget. On this method, using the QRegExpValidator we determine if the last character entered is a valid one. If not we remove it.

    Finally, I set a method fired by the 'editingFinished' signal using the RegEx exactMatch function to determine if the entry is valid. If it is not, we give the user the option to clear the entry or to return to the widget to continue entering data. The regex used is for testing purposes only, for further information about email validation check @Musicamante comments.

    This is the code involved:

        def setUI(self):
            ................
            ................
            rx = QRegExp(r"[a-z0-9_%]+@[a-z0-9%_]+\.[a-z0-9%_]{3,3}")
        
            lblAddress = QLabel("EMail Address")
            self.lineAddress = QLineEdit(self)
            self.mailValidator = QRegExpValidator(rx, self.lineAddress)
            self.lineAddress.cursorPositionChanged[int, int].connect(lambda oldPos, 
                  newPos: self.checkValidator(newPos))
            self.lineAddress.editingFinished.connect(lambda : self.checkRegExp(rx))
    
        @pyqtSlot(int)
        def checkValidator(self, pos):
            v = self.mailValidator.validate(self.lineAddress.text(), pos ))
            if v[0] == 0:
                self.lineAddress.setText(self.lineAddress.text()[:-1])
    
        @pyqtSlot(QRegExp)
        def checkRegExp(self, rx):
            if not rx.exactMatch(self.lineAddress.text()) and self.lineAddress.text():
                if QMessageBox.question(self, "Leave the Address field",
                   "The string entered is not a valid email address! \n" 
                   "Do you want to clear the field?", QMessageBox.Yes|QMessageBox.No) == 
                    QMessageBox.Yes:
                    self.lineAddress.clear()
                else:
                    self.lineAddress.setFocus()