QCompleter and Comma-Separated Tags

Here is a python script demonstrating how to use QCompleter to complete multiple tags in a QLineEdit. A few features of this script are: removing tags from the drop down that already appear in the QLineEdit, caching the tags, and inserting a , after completion to ease adding more tags. There are a few parts of this script that I’m going to go into detail about.

#!/usr/bin/env python

'''
Copyright (c) 2009 John Schember

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
'''

import sys

from PyQt4.Qt import Qt, QObject, QApplication, QLineEdit, QCompleter, 
    QStringListModel, SIGNAL

TAGS = ['Nature', 'buildings', 'home', 'City', 'country', 'Berlin']

class CompleterLineEdit(QLineEdit):

    def __init__(self, *args):
        QLineEdit.__init__(self, *args)

        QObject.connect(self, SIGNAL('textChanged(QString)'), self.text_changed)

    def text_changed(self, text):
        all_text = unicode(text)
        text = all_text[:self.cursorPosition()]
        prefix = text.split(',')[-1].strip()

        text_tags = []
        for t in all_text.split(','):
            t1 = unicode(t).strip()
            if t1 != '':
                text_tags.append(t)
        text_tags = list(set(text_tags))

        self.emit(SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
            text_tags, prefix)

    def complete_text(self, text):
        cursor_pos = self.cursorPosition()
        before_text = unicode(self.text())[:cursor_pos]
        after_text = unicode(self.text())[cursor_pos:]
        prefix_len = len(before_text.split(',')[-1].strip())
        self.setText('%s%s, %s' % (before_text[:cursor_pos - prefix_len], text,
            after_text))
        self.setCursorPosition(cursor_pos - prefix_len + len(text) + 2)


class TagsCompleter(QCompleter):

    def __init__(self, parent, all_tags):
        QCompleter.__init__(self, all_tags, parent)
        self.all_tags = set(all_tags)

    def update(self, text_tags, completion_prefix):
        tags = list(self.all_tags.difference(text_tags))
        model = QStringListModel(tags, self)
        self.setModel(model)

        self.setCompletionPrefix(completion_prefix)
        if completion_prefix.strip() != '':
            self.complete()


def main():
    app = QApplication(sys.argv)

    editor = CompleterLineEdit()

    completer = TagsCompleter(editor, TAGS)
    completer.setCaseSensitivity(Qt.CaseInsensitive)

    QObject.connect(editor,
        SIGNAL('text_changed(PyQt_PyObject, PyQt_PyObject)'),
        completer.update)
    QObject.connect(completer, SIGNAL('activated(QString)'),
        editor.complete_text)

    completer.setWidget(editor)

    editor.show()

    return app.exec_()

if __name__ == '__main__':
    main()

Looking at the main() function the editor widget’s text_changed signal is connected to the completer’s update slot. This serves two purposes. It provides the completer with all tags that are in the editor. Also, it provides the completer with the prefix of the current text that is being entered. The prefix is used for listing matching tags that are stored in the completer’s cache.

editor’s complete_text function takes the text in the editor before the cursor and subtracts the length of the prefix from that position because the prefix will be included in the completed text. The text before, the completed text, a comma, a space, and the text after the cursor are combined. This becomes the text in the QLineEdit. The cursor is advanced the position it was at minus the length of the prefix and plus 2 characters (, ) so that typing can immediately continue.

TAGS can be replaced with a function that gets all relevant tags if caching is not wanted.

Also, note that completer.setWidget(editor) was used not QLineEdit’s setCompleter() function. If setCompleter is used completion will only take place at the beginning of the QLineEdit. Meaning it will match everything before as one string and ignore the , delimiter separating the tags.

One thought on “QCompleter and Comma-Separated Tags

  1. THANK YOU!

    This example was exactly what I needed. You saved me half a day of heartache. Thank you for working out the solution and taking the time to share it.

    Keep kicking ass…

    Like

Comments are closed.