How to achieve autocomplete on a substring of QLineEdit in PyQt6? - python

I'm very new to QT so please assume I know nothing. I want an auto-complete where it only matches against the text after the last comma.
e.g. if my word bank is ["alpha", "beta", "vector space"], and the user currently has typed "epsilon,dog,space" then it should match against "vector space" since "space" is a substring of "vector space".
I'm using PyQt6 and my current code looks something like this (adapted from a YouTube tutorial):
line_of_text = QLineEdit("")
word_bank = [name for name,value in names_dict.items()]
completer = QCompleter(word_bank)
completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
completer.setFilterMode(Qt.MatchContains)
line_of_text.setCompleter(completer)
So currently, if word_bank is ["alpha", "beta", "vector space"], then if line_of_text had the string "epsilon,dog,space" then it wouldn't match against anything because "epsilon,dog,space" isn't a substring of "alpha" nor "beta" nor "vector space".
How can I alter my code to achieve what I would like to achieve? -- I'm experienced with programming, just not with Qt.
PS: I have tried doing
line_of_text.textChanged[str].connect(my_function)
where my_function takes only the substring of line_of_text after the last comma and feeds it to completer.setCompletionPrefix and then calls completer.complete().
This simply does not work. I assume the reason is that completer.complete() updates the completion prefix from line_of_text, causing the call to completer.setCompletionPrefix to be overwritten immediately after.

One solution is to use the textChanged signal to set the completion prefix, and bypass the built-in behaviour of the line-edit to allow greater control of when and how the completions happen.
Below is a basic implementation that shows how to do that. By default, it only shows completions when the text after the last comma has more than single character - but that can be easily adjusted:
from PyQt6 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.edit = QtWidgets.QLineEdit()
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.edit)
word_bank = ['alpha', 'beta', 'vector space']
self.completer = QtWidgets.QCompleter(word_bank)
self.completer.setCaseSensitivity(
QtCore.Qt.CaseSensitivity.CaseInsensitive)
self.completer.setFilterMode(QtCore.Qt.MatchFlag.MatchContains)
self.completer.setWidget(self.edit)
self.completer.activated.connect(self.handleCompletion)
self.edit.textChanged.connect(self.handleTextChanged)
self._completing = False
def handleTextChanged(self, text):
if not self._completing:
found = False
prefix = text.rpartition(',')[-1]
if len(prefix) > 1:
self.completer.setCompletionPrefix(prefix)
if self.completer.currentRow() >= 0:
found = True
if found:
self.completer.complete()
else:
self.completer.popup().hide()
def handleCompletion(self, text):
if not self._completing:
self._completing = True
prefix = self.completer.completionPrefix()
self.edit.setText(self.edit.text()[:-len(prefix)] + text)
self._completing = False
if __name__ == '__main__':
app = QtWidgets.QApplication(['Test'])
window = Window()
window.setGeometry(600, 100, 300, 50)
window.show()
app.exec()

(Assuming str is the string from the text input) Have you tried separating str using the , character, then iterating over each of the words separated by a comma.
for word in str.split(","):
for check in word_bank:
etc...
the connected function will be called in your case whenever the string entered changes.

Related

Tkinter Text widget not recognizing "<" as part of a word

I'm trying to make a simple HTML Editor with Tkinter, with Python 3.10. Right now I'm trying to implement auto-completion using the Tkinter Text widget. The widget checks the word currently typed to see if it matches with any of the HTML tags in self._tags_list. And if it does, it auto-completes it.
Code:
import random
import tkinter as tk
class IDE(tk.Text):
def __init__(self, *args, **kwargs):
tk.Text.__init__(self, *args, **kwargs)
self._tags_list = ['<html></html>', '<head></head>', '<title></title>', '<body></body>', '<h1></h1>',
'<h2></h2>', '<h3></h3>', '<h4></h4>', '<h5></h5>', '<h6></h6>', '<p></p>', '<b></b>']
self.bind("<Any-KeyRelease>", self._autocomplete)
self.bind("<Tab>", self._completion, add=True)
self.bind("<Return>", self._completion, add=True)
def callback(self, word):
# Returns possible matches of `word`
matches = [x for x in self._tags_list if x.startswith(word)]
return matches
def _completion(self, _event):
tag_ranges = self.tag_ranges("autocomplete")
if tag_ranges:
self.mark_set("insert", tag_ranges[1])
self.tag_remove("sel", "1.0", "end")
self.tag_remove("autocomplete", "1.0", "end")
return "break"
def _autocomplete(self, event):
if event.keysym not in ["Return"] and not self.tag_ranges("sel"):
self.tag_remove("sel", "1.0", "end")
self.tag_remove("autocomplete", "1.0", "end")
word = self.get("insert-1c wordstart", "insert-1c wordend")
print(f"word: {word}")
matches = self.callback(word)
print(f"matches: {matches}")
if matches:
remainder = random.choice(matches)[len(word):]
print(remainder)
insert = self.index("insert")
self.insert(insert, remainder, ("sel", "autocomplete"))
self.mark_set("insert", insert)
return
if __name__ == "__main__":
window = tk.Tk()
window.title("Autocomplete")
window.geometry("500x500")
text = IDE(window)
text.pack(fill=tk.BOTH, expand=True)
window.mainloop()
Used from #TRCK.
When I type in the "<" to start an HTML tag, it detects the first possible auto-completion (a random tag from self._tags_list minus the "<" at the beginning). However, when I type in a second character, it detects that character as a separate word and does not auto-complete. I've tested it with words not starting with "<" and it worked fine.
For example, terminal output when I type "<h" into the widget:
When I hit "<":
word: <
matches: ['<html></html>', '<head></head>', '<title></title>', '<body></body>', '<h1></h1>', '<h2></h2>', '<h3></h3>', '<h4></h4>', '<h5></h5>', '<h6></h6>', '<p></p>', '<b></b>']
remainder: h3></h3>
When I add the "h":
word: h
matches: []
Image of Tkinter window:
Can someone please tell me why this is happening and how to fix it? I've checked all around and found nothing yet. Sorry if it is obvious, but I am now learning Tkinter. Also if there are any other problems in the code, please tell me.
A word in wordstart is defined as
"a word is either a string of consecutive letter, digit, or underbar (_) characters, or a single character that is none of these types."
when you type more characters after <, the wordstart starts from the character next to the <. You can make this work by adding -1c at the end of wordstart
word = self.get("insert -1c wordstart -1c", "insert -1c wordend")
By adding
if "<" in word:
word = word[word.index("<"):]
after word you can have a slight improvement.
Besides that you also need to exclude BackSpace, Ctrl+a etc from your keysym

QLineEdit unexpected behavior using QRegExpValidator

I'm using a QLineEdit widget to enter email addresses and I set up a QRegExpValidor to validate the entry.
The validator is working as far as preventing the input of characters not allowed in the QRegExp, but funny enough, it allows to enter intermediate input, either by pushing the enter key or by firing the "editingfinished" signal.
I verified the return of the validator which is correct (Intermediate or Acceptable).
Checking the PyQt5 documentation it seams that an intermediate state of the validator does not prevent the focus to change to another widget. Furthermore, it stops the firing of the editingFinished and returnedPressed signals so the user may enter wrong address as far as it marches partially the RegExp.
Ref: https://doc.qt.io/qt-5/qlineedit.html#acceptableInput-prop
I was able to solve my needs by removing the validator from the QLineEdit widget, and placing a method "checkValidator" linked to the cursorPositionChanged of the QLineEdit widget discarting the last character entered when nonacceptable, and setting the focus back to the QLineEdit when it validats intermmediate. It works just fine but when the the focus was reset, the others signals on other widgets were fired one at a time. Momentarily, I handle this by checking if the sender has focus at the start of the methods (see lookForFile)
Even though I could handle the issue, I appreciate very much anybody explaining me the proper way to use the RegExpValidator and or why resetting the focus fires the other signals out of the blue.
def setUI(self):
self.setWindowTitle("EMail Settings")
self.setModal(True)
rx = QRegExp(r"[a-z0-9_%]+#[a-z0-9%_]+\.[a-z0-9%_]{3,3}")
lblAddress = QLabel("EMail Address")
self.lineAddress = QLineEdit(self)
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
#self.lineAddress.setValidator(self.mailValidator)
self.lineAddress.cursorPositionChanged.connect(self.checkValidator)
self.lineAddress.returnPressed.connect(self.checkValidator)
lblPassword = QLabel("Password")
self.linePwd = QLineEdit()
self.linePwd.setEchoMode(QLineEdit.PasswordEchoOnEdit)
lblOauth2 = QLabel("Oauth2 Token")
self.lineOauth = QLineEdit()
pushOauth = QPushButton("...")
pushOauth.setObjectName("token")
pushOauth.clicked.connect(self.lookForFile)
pushOauth.setFixedWidth(30)
#pyqtSlot()
def checkValidator(self):
self.lineAddress.blockSignals(True)
v = self.mailValidator.validate(self.lineAddress.text(), len(self.lineAddress.text()))
if v[0] == 0:
self.lineAddress.setText(self.lineAddress.text()[:-1])
elif v[0] == 1:
self.lineAddress.setFocus()
elif v[0] == 2:
pass
print("validates", v)
self.lineAddress.blockSignals(False)
#pyqtSlot()
def lookForFile(self):
try:
if not self.sender().hasFocus():
return
baseDir = "C"
obj = self.sender()
if obj.objectName() == "Draft":
capt = "Email Draft"
baseDir = os.getcwd() + "\\draft"
fileType = "Polo Management Email (*.pad)"
dialog = QFileDialog(self, directory=os.getcwd())
dialog.setFileMode(QFileDialog.Directory)
res = dialog.getExistingDirectory()
elif obj.objectName() == "token":
capt = "Gmail Outh2 token File"
fileType = "Gmail token Files (*.json)"
baseDir = self.lineOauth.text()
res = QFileDialog.getOpenFileName(self, caption=capt, directory=baseDir, filter=fileType)[0]
fileName = res
if obj.objectName() == "Draft":
self.lineDraft.setText(fileName)
elif obj.objectName() == "tokenFile":
self.lineOauth.setText(fileName)
except Exception as err:
print("settings: lookForFile", err.args)
Hope to answer #eyllanesc and Qmusicmante request with this minimal reproducible example. I change the regex for a simple one allowing for a string of lower case a-z follow by a dot and three more lowercase characters.
What I intend is the validator not allowing the user to enter a wrong input. The example allows for "xxxzb.ods" but also allows "xxxzb" or "xxxzb.o" for instance.
In short, not allowing the user to enter a wrong input.
This is my minimal reproducible example:
class CheckValidator(QDialog):
def __init__(self, parent=None):
super().__init__()
self.parent = parent
self.setUI()
def setUI(self):
self.setWindowTitle("EMail Settings")
self.setModal(True)
rx = QRegExp(r"[a-z]+\.[a-z]{3}")
lblAddress = QLabel("Check Line")
self.lineAddress = QLineEdit()
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
self.lineAddress.setValidator(self.mailValidator)
self.lineAddress.cursorPositionChanged[int, int].connect(lambda
oldPos, newPos: self.printValidator(newPos))
lblCheck = QLabel("Check")
lineCheck = QLineEdit()
formLayout = QFormLayout()
formLayout.addRow(lblAddress, self.lineAddress)
formLayout.addRow(lblCheck, lineCheck)
self.setLayout(formLayout)
#pyqtSlot(int)
def printValidator(self, pos):
print(self.mailValidator.validate(self.lineAddress.text(), pos))
if __name__ == '__main__':
app = QApplication(sys.argv)
tst = CheckValidator()
tst.show()
app.exec()
I found a solution and I post it here for such a case it may help somebody else.
First I remove the QRegExpValidator from the QLineEdit widget. The reason being is that QLineEdit only fires editingFinished (and we'll need it ) only when QRegExpValidator returns QValidator.Acceptable while the validator is present.
Then we set a method fired by the 'cursorPositionchanged' signal of QlineEdit widget. On this method, using the QRegExpValidator we determine if the last character entered is a valid one. If not we remove it.
Finally, I set a method fired by the 'editingFinished' signal using the RegEx exactMatch function to determine if the entry is valid. If it is not, we give the user the option to clear the entry or to return to the widget to continue entering data. The regex used is for testing purposes only, for further information about email validation check #Musicamante comments.
This is the code involved:
def setUI(self):
................
................
rx = QRegExp(r"[a-z0-9_%]+#[a-z0-9%_]+\.[a-z0-9%_]{3,3}")
lblAddress = QLabel("EMail Address")
self.lineAddress = QLineEdit(self)
self.mailValidator = QRegExpValidator(rx, self.lineAddress)
self.lineAddress.cursorPositionChanged[int, int].connect(lambda oldPos,
newPos: self.checkValidator(newPos))
self.lineAddress.editingFinished.connect(lambda : self.checkRegExp(rx))
#pyqtSlot(int)
def checkValidator(self, pos):
v = self.mailValidator.validate(self.lineAddress.text(), pos ))
if v[0] == 0:
self.lineAddress.setText(self.lineAddress.text()[:-1])
#pyqtSlot(QRegExp)
def checkRegExp(self, rx):
if not rx.exactMatch(self.lineAddress.text()) and self.lineAddress.text():
if QMessageBox.question(self, "Leave the Address field",
"The string entered is not a valid email address! \n"
"Do you want to clear the field?", QMessageBox.Yes|QMessageBox.No) ==
QMessageBox.Yes:
self.lineAddress.clear()
else:
self.lineAddress.setFocus()

Editing behavior of “backspace” key of QDoubleSpinBox

I’ve got a problem with QDoubleSpinBox. Editing behavior of “backspace” key somehow depends on the size of the suffix.
If I set “m” as suffix, then set the cursor at the end of the spinbox and press “backspace”, the cursor jumps over the “m” suffix to the value which then can be edited with further “backspaces”.
If I set the suffix to “mm” or any double-lettered word, the cursor remains at the end of the spinbox no matter how many “backspaces” I press.
I tried to debug what comes into “validate” method, and got a peculiar result:
When the “backspace” is pressed while cursor is at the end of "0,00m", validate receives "0,00m".
When the “backspace” is pressed while cursor is at the end of "0,00_m" validate receives "0,00__m"
When the “backspace” is pressed while cursor is at the end of "0,00_mm" validate receives "0,00_m_mm"
What is the cause of such behavior and how can I overcome it?
# coding=utf-8
from PyQt5 import QtWidgets
class SpinBox(QtWidgets.QDoubleSpinBox):
def __init__(self):
super().__init__()
def validate(self, text, index):
res = super().validate(text, index)
print(text, res, self.text())
return res
if __name__ == "__main__":
q_app = QtWidgets.QApplication([])
sb = SpinBox()
sb.setSuffix(" m")
sb.show()
q_app.exec_()
The source code for QDoubleSpinBox/QAbstractSpinBox is extremely convoluted when it comes to key-event handling - I couldn't work out what default behaviour is supposed to be, or even where it might be implemented. There might be a bug somewhere, but I wouldn't want to bet on it.
It looks like the only option is to reimplement keyPressEvent:
class SpinBox(QtWidgets.QDoubleSpinBox):
def keyPressEvent(self, event):
if event.key() == QtCore.Qt.Key_Backspace:
suffix = self.suffix()
if suffix:
edit = self.lineEdit()
text = edit.text()
if (text.endswith(suffix) and
text != self.specialValueText()):
pos = edit.cursorPosition()
end = len(text) - len(suffix)
if pos > end:
edit.setCursorPosition(end)
return
super().keyPressEvent(event)

How to print more than one items in QListWidget if more than one items selected

I have QListWidget and there are strings there, when I select a string, I wanted to display the index number and text of that. But the problem is, if I select more than 1 items, it doesn't display all of the indexes. It displays only one.
from PyQt5.QtWidgets import *
import sys
class Pencere(QWidget):
def __init__(self):
super().__init__()
self.layout = QVBoxLayout(self)
self.listwidget = QListWidget(self)
self.listwidget.addItems(["Python","Ruby","Go","Perl"])
self.listwidget.setSelectionMode(QAbstractItemView.MultiSelection)
self.buton = QPushButton(self)
self.buton.setText("Ok")
self.buton.clicked.connect(self.but)
self.layout.addWidget(self.listwidget)
self.layout.addWidget(self.buton)
def but(self):
print (self.listwidget.currentRow()+1)
uygulama = QApplication(sys.argv)
pencere = Pencere()
pencere.show()
uygulama.exec_()
How can I display all of the items names and indexes if I select more than 1 items?
I solved it with this
def but(self):
x = self.listwidget.selectedItems()
for y in x:
print (y.text())
You need to use QListWidget's selectedItems() function, which returns a list. currentRow() only returns a single integer, and is intended to be used only in single-selection instances.
Once you've got the list of QListWidgetItems, you can use the text() function on each item to retreive the text.
Getting the index is slightly more complicated, you'll have to get a QModelIndex object from your original QListWidgetItem using the QListWidget.indexFromItem() and then use the QModelIndex.row() function.
Source: http://pyqt.sourceforge.net/Docs/PyQt4/qlistwidget.html#selectedItems
Note: You specified PyQt5 in your tags, but the API of QListWidget remains the same in this case; see the C++ API docs if you want to make sure.

QValidator fixup issue

Following is my code to validate an account code, and add dashes at certain intervals. An example account code is 140-100-1000-6610-543. The code takes the regex: \d{3}-\d{3}-\d{4}-\d{4}-\d{3] and allows the user to type in numbers, and where there is a dash in the regex, it places the dash for the user. Or rather, it should. On our production server (('Qt version:', '4.8.1'), ('SIP version:', '4.13.2'), ('PyQt version:', '4.9.1')) it does work. Our dev server is upgraded to ('Qt version:', '4.8.6'), ('SIP version:', '4.15.5'), ('PyQt version:', '4.10.4') and it doesn't work there.
On each server, I type in 140. On the production (older version), the line edit value changes to 140-. On the newer version development server, it does not add the dash.
Please let me know if you see my issue, and let me know if this is a PyQt issue, or a Qt issue.
import sys
from PyQt4 import QtGui, QtCore
DEBUG = True
class acValidator(QtGui.QRegExpValidator):
def __init__(self, regexp, widget):
QtGui.QRegExpValidator.__init__(self, regexp, widget)
self.widget = widget
def validate(self, text, pos):
'''function to decide if this account code is valid'''
valid, _npos = QtGui.QRegExpValidator.validate(self, text, pos)
if valid != QtGui.QValidator.Acceptable:
self.fixup(text)
print 'acWidget.validate result of fixup', text
# move position to the end, if fixup just added a dash
# to the end
newstr = self.widget.text()
newpos = len(str(newstr))
if pos + 1 == newpos and newstr[pos:newpos] == '-':
pos = newpos
# return the valid variable, and the current position
return (valid, pos)
def fixup(self, text):
'''place dashes, if we can'''
# pylint: disable=no-member
if DEBUG:
print 'acWidget.py fixup'
reParts = self.regExp().pattern().split('-')
if DEBUG:
print list(reParts)
newreg = ''
for i in range(len(reParts)):
newreg += reParts[i]
if DEBUG:
print i, reParts[i]
nr = QtCore.QRegExp(newreg)
# pylint: disable=no-member
if nr.exactMatch(text) and not self.regExp().exactMatch(text):
if DEBUG:
print 'adding dash'
text += '-'
return
newreg += '-'
def isValid(self):
'''return a true or false for the validity based on whether the
widget is a lineEdit or a comboBox (acCB). true only if the
validator returns QtGui.QValidator.Acceptable
'''
valid, _npos = QtGui.QRegExpValidator.validate(self,
self.widget.text(),
self.widget.cursorPosition())
if valid == QtGui.QValidator.Acceptable:
return True
return False
class acWidget(QtGui.QLineEdit):
def __init__(self, parent=None):
QtGui.QLineEdit.__init__(self, parent)
self.regex = r'\d{3}-\d{3}-\d{4}-\d{4}-\d{3}'
self.setMinimumWidth(200)
self.setValidator(acValidator(QtCore.QRegExp(self.regex),
self))
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
form = QtGui.QDialog()
layout = QtGui.QVBoxLayout(form)
a = acWidget(form)
layout.addWidget(a)
form.setLayout(layout)
form.setMinimumWidth(400)
form.setMinimumHeight(200)
form.show()
app.exec_()
The problem is caused by the C++ signatures of fixup and validate requiring that the text argument is modifiable. If you're using Python 2, this distinctly unpythonic way of doing things is honoured by PyQt; whereas with Python 3, the signatures have been changed so that the methods simply return the modified values.
The specific issue in your code can be found here:
def fixup(self, text):
...
text += '-'
It seems that, in earlier versions of PyQt, augmented assignment on a QString did an implicit in-place mutation. But in more recent versions, it works more like normal python augmented assignment and simply re-binds the local variable like this:
text = text + '-'
To work around this, you can do an explict in-place mutation:
text.append('-')

Categories

Resources