Wiki

Customizing for Completion

by Mark Summerfield

QTextEdit can be used to provide a simple text editor within an application. QTextEdit provides a lot of functionality out of the box, but one feature it doesn't provide is automatic completion. In this article, we present a QTextEdit subclass that provides completion and that demonstrates how to extend and enhance QTextEdit's functionality.

Subclassing QTextEdit for Completion

Completion is a means by which an editor automatically completes words that the user is typing. For example, in a code editor, a programmer might type "sur", then Tab, and the editor will complete the word the programmer was typing so that "sur" is replaced by "surnameLineEdit". This is very useful for text that contains long words or variable names. The completion mechanism usually works by looking at the existing text to see if any words begin with what the user has typed, and in most editors completion is invoked by a special key sequence.

Completion

We need to create a QTextEdit subclass that can detect a special key sequence to invoke the completion mechanism, and that can handle three different situations:

  • There are no possible completions.
  • There is a single possible completion.
  • There are two or more possible completions.
We will handle the first case by simply doing nothing in response to the completion key sequence. For the second case, we will insert the word immediately. For the third case, we will pop up a list of possible completions and let the user choose one. We've chosen Ctrl+P as our completion key sequence.

Let's start with the header file:

    class QListBox;
    class QListBoxItem;
 
    class CompTextEdit : public QTextEdit
    {
        Q_OBJECT
    public:
        CompTextEdit(QWidget *parent, const char *name = 0);
 
    private slots:
        void complete();
        void itemChosen(QListBoxItem *item);
 
    private:
        void createListBox();
        void adjustListBoxSize(int maxHeight = 32767, int maxWidth = 32767);
        QPoint textCursorPoint() const;
 
        QString wordPrefix;
        QListBox *listBox;
    };
 

We'll look at each function and slot in turn, and discuss the two private variables as we encounter them.

    CompTextEdit::CompTextEdit(QWidget *parent, const char *name)
        : QTextEdit(parent, name)
    {
        QAccel *accel = new QAccel(this);
        accel->connectItem(accel->insertItem(CTRL+Key_P), this, SLOT(complete()));
        listBox = 0;
    }
 

In the constructor, we create an accelerator for Ctrl+P and connect it to the complete() slot, which we'll discuss next. The list box will be used to present a list of possible completions to the user. We will only create it if we need it.

    void CompTextEdit::complete()
    {
        int cursorPara;
        int cursorPos;
        getCursorPosition(&cursorPara, &cursorPos);
 
        QString para = text(cursorPara);
        int wordStart = cursorPos;
        while (wordStart > 0 && para[wordStart - 1].isLetterOrNumber())
            --wordStart;
        wordPrefix = para.mid(wordStart, cursorPos - wordStart);
        if (wordPrefix.isEmpty())
            return;
 
        QStringList list = QStringList::split(QRegExp("\\W+"), text());
        QMap<QString, QString> map;
        QStringList::Iterator it = list.begin();
        while (it != list.end()) {
            if ((*it).startsWith(wordPrefix) && (*it).length() > wordPrefix.length())
                map[(*it).lower()] = *it;
            ++it;
        }
 
        if (map.count() == 1) {
            insert((*map.begin()).mid(wordPrefix.length()));
        } else if (map.count() > 1) {
            if (!listBox)
                createListBox();
            listBox->clear();
            listBox->insertStringList(map.values());
 
            QPoint point = textCursorPoint();
            adjustListBoxSize(qApp->desktop()->height() - point.y(), width() / 2);
            listBox->move(point);
            listBox->show();
            listBox->raise();
            listBox->setActiveWindow();
        }
    }
 

The complete() slot is the heart of the CompTextEdit class. First we put the characters that precede the cursor into the wordPrefix string. We have opted to only consider letters or digits as forming part of a word, so we will stop gathering characters if we encounter punctuation or whitespace.

Next, we take the QTextEdit's entire text and split it on "non-word" characters using a QRegExp; this produces a list of words. We iterate over the words storing each one that begins with (but isn't equal to) the text the user typed, in a map. We use the map for two reasons. First, it only stores one value per unique key, so duplicates are effectively discarded. Second, it orders its values by their keys, and since we use the lower-case value as the key for each word, when we come to use the values they will be ordered case-insensitively. As a consequence, if we have "Iterator" and "iterator" in the text, only the one that is read last will go into the list of possible completions.

If the map has only one word, we immediately insert it --- or, rather, we insert the characters from the word that follow those already typed in by the user.

If there are two or more candidates, we create the completion list box if we haven't already done so. We clear the list box's contents, insert the values from the map, and move and size the list box ready for the user. We need the desktop's height because if the QTextEdit is at the bottom of the screen and the user presses Ctrl+P on the last line we don't want the list to be cut off by the bottom of the screen.

    QPoint CompTextEdit::textCursorPoint() const
    {
        int cursorPara;
        int cursorPos;
        getCursorPosition(&cursorPara, &cursorPos);
        QRect rect = paragraphRect(cursorPara);
        QPoint point(rect.left(), rect.bottom());
        while (charAt(point, 0) < cursorPos)
            point.rx() += 10;
        return mapToGlobal(contentsToViewport(point));
    }
 

The positioning code is slightly convoluted because we want the list box to appear just below and to the right of the word the user is typing and QTextEdit's API does not directly provide the coordinates of the text cursor. We had to guess the position by using paragraphRect() and charAt(). (This may not work perfectly if line-breaking is enabled.)

We must call raise() and setActiveWindow() because if the list box has already been used for an earlier completion and has therefore been closed, in addition to being shown it must be raised to the top of the stack of windows and made the active window whenever it is subsequently used. An alternative would have been to use the WDestructiveClose flag and recreate the list box every time it was needed.

    void CompTextEdit::itemChosen(QListBoxItem *item)
    {
        if (item)
            insert(item->text().mid(wordPrefix.length()));
        listBox->close();
    }
 

If the user chose an item (either by clicking or by pressing the arrow keys and Enter), we insert the rest of that word and close the completion box. By default, a close() call merely hides a top-level widget, without deleting it.

Smart List Box Sizing

    void CompTextEdit::createListBox()
    {
        listBox = new QListBox(this, "listBox", WType_Popup);
        QAccel *accel = new QAccel(listBox);
        accel->connectItem(accel->insertItem(Key_Escape), listBox, SLOT(close()));
 
        connect(listBox, SIGNAL(clicked(QListBoxItem *)),
                this, SLOT(itemChosen(QListBoxItem *)));
        connect(listBox,
                SIGNAL(returnPressed(QListBoxItem *)),
                this, SLOT(itemChosen(QListBoxItem *)));
    }
 

Creating the list box is straightforward. We make sure it is closed when users press Esc (in case none of the completions is what they want), and we make provision for both mouse and keyboard users to choose a completion word.

    void CompTextEdit::adjustListBoxSize(int maxHeight, int maxWidth)
    {
        if (!listBox->count())
            return;
        int totalHeight = listBox->itemHeight(0) * listBox->count();
        if (listBox->variableHeight()) {
            totalHeight = 0;
            for (int i = 0; i < (int)listBox->count(); ++i)
                totalHeight += listBox->itemHeight(i);
        }
        listBox->setFixedHeight(QMIN(totalHeight, maxHeight));
        listBox->setFixedWidth(QMIN(listBox->maxItemWidth(), maxWidth));
    }
 

QListBox adjusts its size depending on its content and the layout it is in. But since we are using a QListBox as a top-level window we will size it ourselves. We want the list box to be as high and as wide as it needs to display all the candidate words, providing it doesn't exceed the given limits. In the complete() function, we chose a limit based on the size of the QTextEdit.

Conclusion

Customizing Qt widgets by subclassing is very easy. It is a technique that can be used for anything from small modifications, like adding an extra function or overriding an existing function, to substantial additions involving new functions, signals and slots. And if your subclass is likely to be useful in many different places or projects, you can make it easy for people to design with it in Qt Designer by making it into a plugin.

A straightforward modification of the CompTextEdit presented in this article would be to use a glossary instead of gathering the list of words from the QTextEdit itself. This would only require a modification to the complete() function.


Copyright © 2003 Trolltech Trademarks