0

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()
Erick
  • 301
  • 3
  • 12
  • If you attempt is to provide a minimal reproducible example, please ensure that it actually is both minimal *and* reproducible. Your code contains unnecessary parts that are pointless to your question, it's also incomplete and has indentation problems. Despite that, I'm not sure I understand what you're asking, but consider that the fact that an input is not valid doesn't automatically prevent changing focus (nor it should!) – musicamante Apr 08 '21 at 16:13
  • Also note that the mail regex is quite incomplete and prone to errors: it's missing important characters like plus and period (but, actually, all the following should also be technically allowed: `!#$%&'*+-/=?^_\`{|}~`), and it doesn't allow multi level domains (@something.co.uk). See [How to validate an email address using a regular expression?](https://stackoverflow.com/q/201323). – musicamante Apr 08 '21 at 16:31
  • 1
    please provide a [mre], Your goal is that the user cannot change focus if a valid email is not placed? – eyllanesc Apr 08 '21 at 16:33
  • From what I understand for you X="xxxzb" and Y="xxxzb.o" are invalid, on the other hand Z="xxxzb.ods" is valid but according to the logic to write Z you must first write X and then Y so preventing the user from writing X or Y would make it impossible for him to write Z. Z is the valid value but X and Y are intermediate values since they allow us to write Z – eyllanesc Apr 08 '21 at 19:07
  • Your comment @Elyllanesc is correct. What I was hopping is for a way of keeping the QRegExpValidator behavior while enforcing an exact match as the fire of editingFinished. I found a solution myself taking out the RegExpValidator from the QLineEdit,(because only fires editingFinished when return QValidator.Acceptable) and setting two methods: one on the cursorPositionChanged using the validator results to eliminate the last entry when incorrect; and other on the editingFinished signal using the exactMatch of the RecExp to determine if it is a valid entry. I'll post the details as an answer. – Erick Apr 08 '21 at 20:16

1 Answers1

0

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()
Erick
  • 301
  • 3
  • 12