python-2.7validationpyqt4qt4qlineedit

PyQt 4. I can't delete content of QLineEdit()-object


I wrote an executable example - you can test it. When you start this program you will get three QPushButton()-objects and one QLineEdit()-object. There you can install or deinstall/uninstall the event filter or close the application. Please install the event filter and type a text. You will see what I want. I want the example program to protect the space key. In this current version the user can't press the space key more than 2 times. This program does work.

But I have a little problem. When I write a text in the QLineEdit()-object and then I highlight the text and I press the delete or return key, nothing happens. I am not able to delete the text. I am also not able to copy the marked text.

Whats wrong with the code below?

#!/usr/bin/env python
import sys

from PyQt4.QtCore import QEvent, Qt
from PyQt4.QtGui import QMainWindow, QWidget, QApplication, QVBoxLayout, QLineEdit, QPushButton

class Window(QMainWindow):

    def __init__(self, parent=None):

        QMainWindow.__init__(self, parent)

        self.count_space_pressed = 0
        self.current_pos = None

        self.init_ui()
        self.init_signal_slot_push_button()

    def init_ui(self):
        centralwidget = QWidget(self)
        self.input_line_edit = QLineEdit(self)


        self.close_push = QPushButton(self)
        self.close_push.setEnabled(False)
        self.close_push.setText("Close")

        self.push_install = QPushButton(self)
        self.push_install.setText("Install eventFilter")

        self.push_deinstall = QPushButton(self)
        self.push_deinstall.setText("Deinstall eventFilter")

        layout = QVBoxLayout(centralwidget)        
        layout.addWidget(self.input_line_edit)
        layout.addWidget(self.push_install)
        layout.addWidget(self.push_deinstall)
        layout.addWidget(self.close_push)

        self.setCentralWidget(centralwidget)
        return

    def install_filter_event(self, widget_object):
        widget_object.installEventFilter(self)
        return

    def deinstall_filter_event(self, widget_object):
        widget_object.removeEventFilter(self)
        return

    def init_signal_slot_push_button(self):

        self.close_push.clicked.connect(self.close)
        self.push_install.clicked.connect(lambda: self.install_filter_event(self.input_line_edit))
        self.push_deinstall.clicked.connect(lambda: self.deinstall_filter_event(self.input_line_edit))
        return

    def strip_string(self, content, site=None):
        if site == "right":
            return content.rstrip()
        elif site == "right_left":
            return content.strip()
        elif site == "left":
            return content.lstrip()

    def eventFilter(self, received_object, event):

        content_line_edit = unicode(received_object.text())

        if event.type() == QEvent.KeyPress:

            if event.key() == Qt.Key_Space:
                '''
                    Yes, the user did press the Space-Key. We
                    count how often he pressed the space key.
                '''
                self.count_space_pressed = self.count_space_pressed + 1

                if int(self.count_space_pressed) > 1:
                    '''
                        The user did press the space key more than 1 time.
                    '''

                    self.close_push.setEnabled(False)

                    '''
                        Now we know the user did press the
                        space key more than 1 time. We take a look,
                        if variablenamed (sel.current_pos) is None.
                        That means, no current position is saved.
                    '''
                    if self.current_pos is None:
                        '''
                            Well no current position is saved,
                            that why we save the new position anf
                            then we set the position of the cursor.
                        '''

                        self.current_pos = received_object.cursorPosition()

                        received_object.setCursorPosition(int(self.current_pos))

                        received_object.clear()
                        received_object.setText(self.strip_string(content_line_edit, site="right"))

                    else:
                        '''
                            Well the user press the space key again, for
                            example 3, 4, 5, 6 times we want to keep the
                            old position of the cursor until he press
                            no space key.
                        '''                        
                        received_object.setCursorPosition(int(self.current_pos))

                        '''
                            We have to remove all spaces in a string
                            on the right side and set the content on QLineEdit-widget.
                        '''
                        received_object.clear()
                        received_object.setText(self.strip_string(content_line_edit, site="right"))

                else: pass                

            else:
                '''
                    No the user didn't press the space key.
                    So we set all setting on default.
                '''
                self.close_push.setEnabled(True)
                self.current_pos = None
                self.count_space_pressed = 0

                received_object.clear()
                received_object.setText(self.strip_string(content_line_edit, site="left"))

        # Call Base Class Method to Continue Normal Event Processing
        return QMainWindow.eventFilter(self, received_object, event)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = Window()
    window.show()
    app.exec_()

EDIT:

import sys, re
from PyQt4 import QtCore, QtGui

class Window(QtGui.QWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.edit = QtGui.QLineEdit(self)

        self.edit.textChanged.connect(self.handleTextChanged)
        layout = QtGui.QVBoxLayout(self)
        layout.addWidget(self.edit)

        #   First we save the  the regular expression pattern
        #   in a variable named regex.
        #@  This means: one whitespace character, followed by
        #@  one or more whitespaces chatacters

        regex = r"\s\s+"

        #   Now we comple the pattern.
        #   After then we save the compiled patter
        #   as result in a variable named compiled_re.
        self.compiled_re = re.compile(regex)

    def handleTextChanged(self, text):
        #   When the text of a widget-object is changed,
        #   we do something.

        #   Here I am really  not sure.
        #   Do you want to look if the given text isn't empty?
        #@  No, we want to search the string to see if it
        #@  contains any runs of multiple spaces

        if self.compiled_re.search(text):

            #   We know that given text is a QString-object.
            #   So we have to convert the given text
            #   into a python-string, because we want to work
            #   with them in python.
            text = unicode(text)

            # NOTICE: Do replacements before and after cursor pos

            #   We save the current and correct cursor position
            #   of a QLineEdit()-object in the variable named pos.
            pos = self.edit.cursorPosition()

            #   Search and Replace: Here the sub()-method
            #   replaces all occurrences of the RE pattern
            #   in string with text.
            #   And then it returns modified string and saves
            #   it in the variables prefix and suffix.

            #   BUT I am not sure If I understand this: [:pos]
            #   and [pos:]. I will try to understnand.
            #   I think we are talking about slicing, right?
            #   And I think the slicing works like string[start:end]:

            #   So text[:pos] means, search and replace all whitesapce
            #   at the end of the text-string. And the same again, but
            #   text[pos:] means, search and replace all whitesapce
            #   at the start of the string-text.
            #@  Right, but the wrong way round. text[:pos] means from
            #@  the start of the string up to pos (the prefix); and
            #@  text[pos:] means from pos up to the end of the string
            #@  (the suffix)

            prefix = self.compiled_re.sub(' ', text[:pos])
            suffix = self.compiled_re.sub(' ', text[pos:])    

            # NOTICE: Cursor might be between spaces
            #   Now we take a look if the variable prefix ends
            #   with a whitespace and we check if suffix starts
            #   with a whitespace.

            #   BUT, why we do that?
            #@  Imagine that the string is "A |B C" (with the cursor
            #@  shown as "|"). If "B" is deleted, we will get "A | C"
            #@  with the cursor left between multiple spaces. But
            #@  when the string is split into prefix and suffix,
            #@  each part will contain only *one* space, so the
            #@  regexp won't replace them.

            if prefix.endswith(' ') and suffix.startswith(' '):

                #   Yes its True, so we overwrite the variable named
                #   suffix and slice it. suffix[1:] means, we starts
                #   at 1 until open end.
                #@  This removes the extra space at the start of the
                #@  suffix that was missed by the regexp (see above)

                suffix = suffix[1:]

            #   Now we have to set the text of the QLineEdit()-object,
            #   so we put the both varialbes named prefix and suffix
            #   together.
            self.edit.setText(prefix + suffix)

            #   After then, we have to set the cursor position.
            #   I know that the len()-method returns the length of the
            #   variable named prefix.

            #   BUT why we have to do that?
            #@  When the text is set, it will clear the cursor. The
            #@  prefix and suffix gives the text before and after the
            #@  old cursor position. Removing spaces may have shifted
            #@  the old position, so the new postion is calculated
            #@  from the length of the current prefix

            self.edit.setCursorPosition(len(prefix))

if __name__ == '__main__':

    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.setGeometry(500, 150, 300, 100)
    window.show()
    sys.exit(app.exec_())

EDIT 2:

Two question:

First Question: in the if.condition, where we take a look if prefix ends and suffix starts with sapces, there we are about to remove the extra space at the start of the suffix. But why don't we also remove the extra space at start of the prefix? Imagine: The user types " Prefix and Suffix " - with extra whitespaces at start and end. Don't we have to remove the extra space at start of the prefix - like: prefix= prefix[:1]?

Second Question: At the end of the handleTextChanged()-method, we have to calculate the new position of the cursor. In the current case we use prefix to get the length of the string. Why not the len from the new modified text, that is a part from prefix and suffix? Example: The old string is " Prefix and Suffix ", the user removes the word 'and". Now our string looks like " Prefix | Suffix ". After all whitespaces are removed we get the new modified text: "Prefix Suffix". Why don't we calculate the new position from the modified text? Or did I miss something?

EDIT 3:

I am sorry, I still don't understand the situation.

First situation: When the user types the following string: "A B C |" (| it is shown as cursor). Now the user presses the space key more than 2 times, we get a prefix that contains "A B C |" - and no suffix. And currently the length of the prexis is 6 - suffix has no lenght, because its empty. And the whole word is length 6. The current position of the cursor is 7.

Second situation: The user types "A B D E F |". And now he is realizing that a letter is missing: C. He moves his cursor back between B and D and types C and then he is about to press the space key 2 times. Now we have prefix that contains "A B C " and suffix which content "D E F". The length of prefix is 6 and of suffix is 5. The length of the whole word is 11. And in this moment the current position of the cursor is 7. In this situation you take the length of prefix and set the cursor position, right?


Solution

  • Filtering key-presses is not enough if you really want to prevent multiple spaces.

    For instance, the user can simply drag and drop multiple spaces; or paste them either with the mouse, the built-in context menu, or with the standard keyboard shortcuts.

    It's also very easy to break your space-key counting method: for example, just type A B C then move back two places and delete B!

    A much more robust way to do this is to connect to the textChanged signal and use a regexp to check if there's any multiple spaces. If there are, use the same regexp to replace them, and then restore the cursor to it's original position.

    Here's a demo:

    import sys, re
    from PyQt4 import QtCore, QtGui
    
    class Window(QtGui.QWidget):
        def __init__(self):
            super(Window, self).__init__()
            self.edit = QtGui.QLineEdit(self)
            self.edit.textChanged.connect(self.handleTextChanged)
            layout = QtGui.QVBoxLayout(self)
            layout.addWidget(self.edit)
            self.regexp = re.compile(r'\s\s+')
    
        def handleTextChanged(self, text):
            if self.regexp.search(text):
                text = unicode(text)
                # do replacements before and after cursor pos
                pos = self.edit.cursorPosition()
                prefix = self.regexp.sub(' ', text[:pos])
                suffix = self.regexp.sub(' ', text[pos:])
                # cursor might be between spaces
                if prefix.endswith(' ') and suffix.startswith(' '):
                    suffix = suffix[1:]
                self.edit.setText(prefix + suffix)
                self.edit.setCursorPosition(len(prefix))
    
    if __name__ == '__main__':
    
        app = QtGui.QApplication(sys.argv)
        window = Window()
        window.setGeometry(500, 150, 300, 100)
        window.show()
        sys.exit(app.exec_())