SaltyCrane Blog — Notes on Javascript and web development

Python PyQt Tab Completion example

Here is an example Python GUI that implements tab completion. It uses the open source Qt 4.3 toolkit and PyQt 4.3 Python bindings.

A list of words is presented in a list box. As the user types, the list is shortened to show possible matches. If the user presses TAB, the input text is "completed" to the longest possible string match. This may be a whole word or a common substring of multiple words.

This example consists of two basic elements:

  • MyLineEdit is a subclass of the QLineEdit class. It is used as an input box to enter text. I needed to subclass QLineEdit because I needed to capture the TAB key press event for tab completion. (See this previous post.)
  • QListView and MyListModel implement a list with a simple model/view architechture. MyListModel is a subclass of QAbstractListModel. I implemented the required rowCount and data methods as well as a method called setAllData which replaces the entire existing data with a new list of data.

This example makes use of two SIGNALs:

  • The textChanged signal is emitted each time the user types a letter inside the QLineEdit box. It is connected to the text_changed method which updates the list of words in the QListView. MyListModel's setAllData method is used to update the data.
  • The tabPressed signal is a custom signal I added to my QLineEdit subclass. It is emitted each time the user presses the TAB key. This signal is connected the tab_pressed method which completes the input to the longest matching substring of the available words.

import sys
from PyQt4.QtCore import * 
from PyQt4.QtGui import * 

LIST_DATA = ['a', 'aardvark', 'aardvarks', 'aardwolf', 'aardwolves',
             'abacus', 'babel', 'bach', 'cache', 
             'daggle', 'facet', 'kabob', 'kansas']

#################################################################### 
def main(): 
    app = QApplication(sys.argv) 
    w = MyWindow() 
    w.show() 
    sys.exit(app.exec_()) 

#################################################################### 
class MyWindow(QWidget): 
    def __init__(self, *args): 
        QWidget.__init__(self, *args) 

        # create objects
        self.la = QLabel("Start typing to match items in list:")
        self.le = MyLineEdit()
        self.lm = MyListModel(LIST_DATA, self)
        self.lv = QListView()
        self.lv.setModel(self.lm)

        # layout
        layout = QVBoxLayout()
        layout.addWidget(self.la)
        layout.addWidget(self.le)
        layout.addWidget(self.lv) 
        self.setLayout(layout)

        # connections
        self.connect(self.le, SIGNAL("textChanged(QString)"),
                     self.text_changed)
        self.connect(self.le, SIGNAL("tabPressed"),
                     self.tab_pressed)

    def text_changed(self):
        """ updates the list of possible completions each time a key is 
            pressed """
        pattern = str(self.le.text())
        self.new_list = [item for item in LIST_DATA if item.find(pattern) == 0]
        self.lm.setAllData(self.new_list)

    def tab_pressed(self):
        """ completes the word to the longest matching string 
            when the tab key is pressed """

        # only one item in the completion list
        if len(self.new_list) == 1:
            newtext = self.new_list[0] + " "
            self.le.setText(newtext)

        # more than one remaining matches
        elif len(self.new_list) > 1:
            match = self.new_list.pop(0)
            for word in self.new_list:
                match = string_intersect(word, match)
            self.le.setText(match)

####################################################################
class MyLineEdit(QLineEdit):
    def __init__(self, *args):
        QLineEdit.__init__(self, *args)
        
    def event(self, event):
        if (event.type()==QEvent.KeyPress) and (event.key()==Qt.Key_Tab):
            self.emit(SIGNAL("tabPressed"))
            return True
        return QLineEdit.event(self, event)

#################################################################### 
class MyListModel(QAbstractListModel): 
    def __init__(self, datain, parent=None, *args): 
        """ datain: a list where each item is a row
        """
        QAbstractTableModel.__init__(self, parent, *args) 
        self.listdata = datain
 
    def rowCount(self, parent=QModelIndex()): 
        return len(self.listdata) 
 
    def data(self, index, role): 
        if index.isValid() and role == Qt.DisplayRole:
            return QVariant(self.listdata[index.row()])
        else: 
            return QVariant()

    def setAllData(self, newdata):
        """ replace all data with new data """
        self.listdata = newdata
        self.reset()

####################################################################
def string_intersect(str1, str2):
    newlist = []
    for i,j in zip(str1, str2):
        if i == j:
            newlist.append(i)
        else:
            break
    return ''.join(newlist)

####################################################################
if __name__ == "__main__": 
    main()

Comments


#1 jm commented on :

just add

    self.new_list = []

after line 26 to avoid "AttributeError: 'MyWindow' object has no attribute 'new_list'" if tab is used before any data entering