0

Short version: How can I subclass or otherwise modify a QLineEdit so that the context menu's "undo" option won't undo anything?

Currently I have the following, which blocks Ctrl-Z from working:

class PasswordSaveQLineEdit(QLineEdit):
    def keyPressEvent(self,event):
        if event.key()==(QtCore.Qt.Key_Control and QtCore.Qt.Key_Z):
            self.undo()
        else:
            QLineEdit.keyPressEvent(self,event)

    def undo(self):
        pass

I could get away with disabling the context menu, which would then get rid of both possible ways to undo, but I would rather not. The undo() function doesn't appear to be called directly by either Ctrl-Z or selecting the context menu option.

I also saw this thread on clearing the undo history, which would also work fine, but that unfortunately only applies to Q(Plain)TextEdit and not to QLineEdit.

Explanation of why I need this (in case someone has a better suggestion for how to do it):

Using PyQt4 with the Qt Designer, I'm trying to implement a password entry box (a QLineEdit) where the user can check a box to show or hide the password. This part is working just fine:

def toggleShowPW(self):
    doShow = self.form.showPWCheck.isChecked()
    if doShow:
        self.form.passwordBox.setEchoMode(QLineEdit.Normal)
    else:
        self.form.passwordBox.setEchoMode(QLineEdit.Password)

self.form.showPWCheck.toggled.connect(self.toggleShowPW)

However, my application also needs an option to save the password and bring it back the next time this dialog is opened. Although the password is being stored in plaintext in the database (and I plan to provide a warning to the user about that when they select the option to save the password), I would like to add at least a little bit of security so that a user who walks by can't look at the password just by ticking the box. So I added an attribute to the class that keeps track of whether the password has been loaded from the configuration file, and changed the method to this:

def toggleShowPW(self):
    doShow = self.form.showPWCheck.isChecked()
    if doShow:
        # if password was saved in the config, don't let it be shown in
        # plaintext for some basic security
        if self.passwordWasLoaded:
            r = utils.questionBox("For security reasons, you cannot view "
                    "any part of a saved password. Would you like to erase "
                    "the saved password?", "Erase Password")
            if r == QMessageBox.Yes:
                self.form.passwordBox.setText("")
                self.passwordWasLoaded = False
            else:
                self.form.showPWCheck.setChecked(False)
                return
        self.form.passwordBox.setEchoMode(QLineEdit.Normal)
    else:
        self.form.passwordBox.setEchoMode(QLineEdit.Password)

I also added and connected a method called self.unsetPasswordWasLoaded, which sets the flag to False if all of the content in the password field is erased.

This works great. Unfortunately, a user can now view the password by erasing all of the content, ticking the box, and then doing an Undo. (It's not possible to do an undo when the echoMode is set to QLineEdit.Password, but when you change it back to normal the undo history persists from before.)

Community
  • 1
  • 1
Soren Bjornstad
  • 1,292
  • 1
  • 14
  • 25
  • This is absolute madness. Why would you deliberately undermine the security of a user's password by allowing it to be shown in plain text? Please don't say "because it's more convenient". Personally, if I ever encountered an application/website that worked in the way you suggest, I would immediately abandon it and advise others to do the same. – ekhumoro Aug 11 '15 at 15:38
  • I disagree that password masking actually increases security in most cases, and [I'm not the only one](http://www.nngroup.com/articles/stop-password-masking/) (as mentioned in that link, Internet Explorer 10 now applies this feature to all website password entry boxes, as does the wireless key entry in Windows 7, my bank website, and plenty of other software I use). Most of the time, most users are alone at their computer and nobody's watching. If someone is, you don't check the box – it's that easy. – Soren Bjornstad Aug 11 '15 at 16:29
  • Also, it's been suggested (including in the link) that masking can actually *decrease* security – it discourages users from choosing longer and more complex passwords since they're harder to type out without errors when you can't see what you're doing. – Soren Bjornstad Aug 11 '15 at 16:30
  • Well sure, five minutes of web-searching will easily find you a dozen [different opinions](https://www.schneier.com/blog/archives/2009/07/the_pros_and_co.html) on pretty much any subject you like. If you really want to make entering passwords easier (and safer) for your users, encourage them to use a good password manager. To paraphrase one of your comments: "Some of the time, someone's watching" - and that's most likely because you were looking down at your keyboard whilst typing your password, and didn't remember that you'd clicked on that stupid unmask button. – ekhumoro Aug 11 '15 at 17:45
  • To me, the risk of accidentally selecting a checkbox while someone has also started looking over your shoulder without your noticing is so small as to be not worth worrying about. Anyway, I don't think this is the right place to have this discussion, except to say before doing anything involving passwords, it's probably a good idea to give some consideration to what you're doing before you do it. – Soren Bjornstad Aug 11 '15 at 17:55

1 Answers1

1

You need to override how the context menu is created so you can disable to Undo option. If you store state elsewhere you can chose whether to enable or disable the undo option depending on that state. A very basic example -

# context menu requested signal handler
def display_context_menu(point):
    global line_edit

    # grab the standard context menu
    menu = line_edit.createStandardContextMenu()

    # search through the actions i nthe menu to find undo
    for action in menu.actions():
        if action.text() == '&Undo  Ctrl+Z':
            # set undo as disabled
            action.setDisabled(True)
            break

    # show context menu where we right clicked
    menu.exec_(line_edit.mapToGlobal(point))


# create line edit, set it to use a custom context menu, connect the context menu signal to handler
line_edit = QLineEdit()
line_edit.setContextMenuPolicy(Qt.CustomContextMenu)
line_edit.customContextMenuRequested.connect(display_context_menu)

line_edit.show()
Tim Wakeham
  • 1,029
  • 1
  • 8
  • 12
  • To improve the security against any odd avenues I might not be thinking of, I've decided to just disable the option unconditionally. This works great, thanks! – Soren Bjornstad Aug 11 '15 at 16:49