Search code examples
pythonpyqtpyqt5tab-completionqcompleter

How to use PyQt5 QCompleter for code completion


I want to create a QLineEdit field with basic code completion capability, but so far whenever I select an attribute of an item item.attr, the item. is replaced by attr rather than inserting attr after item.. Furthermore if that attr has attr.subattr, it is impossible to predict it because item. has been replaced and attr. does not exist at the root of my model.

I have created a relatively minimal example:

import sys
from PyQt5.QtGui import QStandardItemModel, QStandardItem
from PyQt5.QtWidgets import QApplication,QWidget,QVBoxLayout,QLineEdit,QCompleter

test_model_data = [
    ('tree',[                           # tree
             ('branch', [               # tree.branch
                         ('leaf',[])]), # tree.branch.leaf
             ('roots',  [])]),          # tree.roots
    ('house',[                          # house
                ('kitchen',[]),         # house.kitchen
                ('bedroom',[])]),       # house.bedroom
    ('obj3',[]),                        # etc..
    ('obj4',[])
]
class codeCompleter(QCompleter):
    def splitPath(self, path):
        return path.split('.') #split table.member

class mainApp(QWidget):
    def __init__(self):
        super().__init__()
        self.entry = QLineEdit(self)
        self.model = QStandardItemModel(parent=self)
        self.completer = codeCompleter(self.model, self)
        self.entry.setCompleter(self.completer)
        layout = QVBoxLayout()
        layout.addWidget(self.entry)
        self.setLayout(layout)

        self.update_model() #normally called from a signal when new data is available

    def update_model(self):
        def addItems(parent, elements):
            for text, children in elements:
                item = QStandardItem(text)
                parent.appendRow(item)
                if children:
                    addItems(item, children)
        addItems(self.model, test_model_data)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    hwind = mainApp()
    hwind.show()
    sys.exit(app.exec_())

I came up with this approach from the Qt5 Docs and an example with Qt4.6, but neither combine all of what I'm trying to accomplish. Do I need a different model structure? Do I need to subclass more of QCompleter? Do I need a different Qt class?

gif of example: (sorry for quality)

enter image description here

Epilogue:

For those interested in actual code completion, I expanded on my code after integrating @eyllanesc's answer so that text before the matched sequence of identifiers was left alone (text ahead of the matched sequence does not prevent matching, nor is deleted when a new match is inserted). All it took was a little bit of regex to separate the part we want to complete from the preceeding text:

class CodeCompleter(QCompleter):
    ConcatenationRole = Qt.UserRole + 1
    def __init__(self, parent=None, data=[]):
        super().__init__(parent)
        self.create_model(data)
        self.regex = re.compile('((?:[_a-zA-Z]+\w*)(?:\.[_a-zA-Z]+\w*)*\.?)$')

    def splitPath(self, path): #breaks lineEdit.text() into list of strings to match to model
        match = self.regex.search(path)
        return match[0].split('.') if match else ['']

    def pathFromIndex(self, ix): #gets model node (QStandardItem) and returns "text" for lineEdit.setText(text)
        return self.regex.sub(ix.data(CodeCompleter.ConcatenationRole), self.completionPrefix())

Solution

  • The pathFromIndex() method returns the string that will be placed in the QLineEdit, instead it will return the concatenation of the text of the item and the texts of its predecessors. To make it more efficient and not calculate that online concatenation, a new role will be created to the model that contains that data.

    import sys
    from PyQt5.QtCore import Qt
    from PyQt5.QtGui import QStandardItemModel, QStandardItem
    from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLineEdit, QCompleter
    
    test_model_data = [
        ('tree',[                           # tree
                 ('branch', [               # tree.branch
                             ('leaf',[])]), # tree.branch.leaf
                 ('roots',  [])]),          # tree.roots
        ('house',[                          # house
                    ('kitchen',[]),         # house.kitchen
                    ('bedroom',[])]),       # house.bedroom
        ('obj3',[]),                        # etc..
        ('obj4',[])
    ]
    
    
    class CodeCompleter(QCompleter):
        ConcatenationRole = Qt.UserRole + 1
        def __init__(self, data, parent=None):
            super().__init__(parent)
            self.create_model(data)
    
        def splitPath(self, path):
            return path.split('.')
    
        def pathFromIndex(self, ix):
            return ix.data(CodeCompleter.ConcatenationRole)
    
        def create_model(self, data):
            def addItems(parent, elements, t=""):
                for text, children in elements:
                    item = QStandardItem(text)
                    data = t + "." + text if t else text
                    item.setData(data, CodeCompleter.ConcatenationRole)
                    parent.appendRow(item)
                    if children:
                        addItems(item, children, data)
            model = QStandardItemModel(self)
            addItems(model, data)
            self.setModel(model)
    
    class mainApp(QWidget):
        def __init__(self):
            super().__init__()
            self.entry = QLineEdit(self)
            self.completer = CodeCompleter(test_model_data, self)
            self.entry.setCompleter(self.completer)
            layout = QVBoxLayout()
            layout.addWidget(self.entry)
            self.setLayout(layout)
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        hwind = mainApp()
        hwind.show()
        sys.exit(app.exec_())