2

I have read through the QCompleter docs (https://doc.qt.io/qt-5/qcompleter.html) and I've tried to implement QCompleter for a QPlainTextEdit.

Now I've got it to work like this:

enter image description here

But the problem with that is, if you start writing a word that is in the list created by keyword.kwlist, then it focuses on the popup that pops up under the cursor and it doesn't let me keep typing.

But when converting the code from c++ to python on the QCompleter docs page, i could still type even if it offered me a selection of words below.

I've tried setting focus to self.editor but that didn't work. I need help with this and the position of the popup. Right now it's kind of blocking the view of the word.

The way it should function is like this:

enter image description here

but that only works with QLineEdit.

from PyQt5.QtWidgets import QCompleter, QPlainTextEdit, QApplication, QWidget, QHBoxLayout
import sys
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt5.QtGui import QTextCursor, QFont, QTextOption
import keyword

class Completer(QCompleter):

    insertText = pyqtSignal(str)

    def __init__(self, myKeywords=None, parent=None):

        myKeywords = keyword.kwlist

        QCompleter.__init__(self, myKeywords, parent)
        self.activated.connect(self.changeCompletion)

    def changeCompletion(self, completion):
        if completion.find("(") != -1:
            completion = completion[:completion.find("(")]
            print(completion)
        print("completion is " + str(completion))
        self.insertText.emit(completion + " ")
        self.popup().hide()


class MyTextEdit(QWidget):

    def __init__(self, *args):
        super().__init__(*args)
        font = QFont()

        font.setPointSize(12)
        self.editor = QPlainTextEdit()
        self.setFont(font)
        self.completer = None
        self.hbox = QHBoxLayout(self)
        self.editor.textChanged.connect(self.complete)
        self.hbox.addWidget(self.editor)

    def setCompleter(self, completer):
        if self.completer:
            print("completer is: " + str(completer))
            self.disconnect()

        if not completer:
            print("completer is: " + str(completer))
            return

        completer.setWidget(self)
        completer.setCompletionMode(QCompleter.PopupCompletion)
        completer.setCaseSensitivity(Qt.CaseInsensitive)
        self.completer = completer

        self.completer.insertText.connect(self.insertCompletion)

    def insertCompletion(self, completion):
        tc = self.editor.textCursor()
        extra = (len(completion) - len(self.completer.completionPrefix()))
        tc.movePosition(QTextCursor.Left)
        tc.movePosition(QTextCursor.EndOfWord)
        tc.insertText(completion[-extra:])
        self.editor.setTextCursor(tc)

    def textUnderCursor(self):
        tc = self.editor.textCursor()
        tc.select(QTextCursor.WordUnderCursor)
        return tc.selectedText()

    def complete(self):
        completionPrefix = self.textUnderCursor()
        print("completion prefix is: " + str(completionPrefix))

        self.completer.setCompletionPrefix(completionPrefix)
        popup = self.completer.popup()
        popup.setCurrentIndex(
            self.completer.completionModel().index(0, 0))
        cr = self.editor.cursorRect()
        cr.setWidth(
            self.completer.popup().sizeHintForColumn(0) + self.completer.popup().verticalScrollBar().sizeHint().width())

        self.completer.complete(cr)


if __name__ == "__main__":

    app = QApplication(sys.argv)
    completer = Completer()
    te = MyTextEdit()
    te.setCompleter(completer)
    te.show()
    sys.exit(app.exec_())
  • Could you explain the following: *But when converting the code from c++ to python on the QCompleter docs page, i could still type even if it offered me a selection of words below*? – eyllanesc Aug 18 '18 at 21:21
  • I added a gif that explains it. Anyway I'll try to explain it better. When writing into the QLineEdit, the second it suggest a word then you can still keep typing but with my implementation it is stuck and you need to select a word to keep typing. –  Aug 18 '18 at 22:23
  • No, my question was on the other hand, you point out that you are converting a code from C++ to python, what is that code? – eyllanesc Aug 18 '18 at 22:24
  • The code under Basic usage: https://doc.qt.io/qt-5/qcompleter.html#details –  Aug 18 '18 at 22:27

1 Answers1

1

I realize the answer might be a tad late, especially since the user is not with us anymore, but for future reference and as a pyqt6 implementation:

There is a full fledged C example provided by Qt, which i have adapted to python. To keep being able to type once the completion popup has popped up, one has to monitor the keypressed events and filter accordingly.

See def keyPressEvent[...] in MyTextEdit.

enter image description here

import keyword
import sys

from PyQt6 import QtGui
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QTextCursor
from PyQt6.QtWidgets import QApplication, QCompleter, QMainWindow, QPlainTextEdit


class TextEdit(QPlainTextEdit):
    """Custom texteditor."""

    def __init__(self):
        super().__init__()
        completer = QCompleter(keyword.kwlist)
        completer.activated.connect(self.insert_completion)
        completer.setWidget(self)
        completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
        completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
        self.completer = completer
        self.textChanged.connect(self.complete)

    def insert_completion(self, completion):
        tc = self.textCursor()
        extra = len(completion) - len(self.completer.completionPrefix())
        tc.movePosition(QTextCursor.MoveOperation.Left)
        tc.movePosition(QTextCursor.MoveOperation.EndOfWord)
        tc.insertText(completion[-extra:] + " ")
        self.setTextCursor(tc)

    @property
    def text_under_cursor(self):
        tc = self.textCursor()
        tc.select(QTextCursor.SelectionType.WordUnderCursor)
        return tc.selectedText()

    def complete(self):
        prefix = self.text_under_cursor
        self.completer.setCompletionPrefix(prefix)
        popup = self.completer.popup()
        cr = self.cursorRect()
        popup.setCurrentIndex(self.completer.completionModel().index(0, 0))
        cr.setWidth(
            self.completer.popup().sizeHintForColumn(0)
            + self.completer.popup().verticalScrollBar().sizeHint().width()
        )
        self.completer.complete(cr)

    def keyPressEvent(self, event: QtGui.QKeyEvent) -> None:
        if self.completer.popup().isVisible() and event.key() in [
            Qt.Key.Key_Enter,
            Qt.Key.Key_Return,
            Qt.Key.Key_Up,
            Qt.Key.Key_Down,
            Qt.Key.Key_Tab,
            Qt.Key.Key_Backtab,
        ]:
            event.ignore()
            return
        super().keyPressEvent(event)


class TextCompleter(QMainWindow):
    def __init__(self):
        super().__init__()
        self.editor = TextEdit()
        self.setCentralWidget(self.editor)


if __name__ == "__main__":
    app = QApplication([])
    te = TextCompleter()
    te.show()
    sys.exit(app.exec())
Christian Karcher
  • 2,533
  • 1
  • 12
  • 17