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)
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())
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_())