Posts Tagged ‘qt’

* Qt Remove Directory and Its Contents

Posted on June 8th, 2010 by John. Filed under programming.


When dealing with directories, Qt has a large number of functions to make manipulating them easy. However, it does not include a way to delete a non-empty directory. This little omission is easily solved.

Following is a recursive function that will delete a directory along with all of it’s contents. This will delete depth first. Meaning it will recurse into sub-directories and only start deleting once the directory has no sub-directories. Changing QDir::DirsFirst to QDir::DirsLast will change this into a breadth first search.

fileutils.h

#ifndef FILEUTILS_H
#define FILEUTILS_H
 
#include <QString>
 
class FileUtils
{
public:
    static bool removeDir(const QString &dirName);
};
 
#endif // FILEUTILS_H

fileutils.cpp

#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFileInfoList>
 
#include "fileutils.h"
 
/*!
   Delete a directory along with all of its contents.
 
   \param dirName Path of directory to remove.
   \return true on success; false on error.
*/
bool FileUtils::removeDir(const QString &dirName)
{
    bool result = true;
    QDir dir(dirName);
 
    if (dir.exists(dirName)) {
        Q_FOREACH(QFileInfo info, dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System | QDir::Hidden  | QDir::AllDirs | QDir::Files, QDir::DirsFirst)) {
            if (info.isDir()) {
                result = removeDir(info.absoluteFilePath());
            }
            else {
                result = QFile::remove(info.absoluteFilePath());
            }
 
            if (!result) {
                return result;
            }
        }
        result = dir.rmdir(dirName);
    }
 
    return result;
}

Tags: , .



* lebookread

Posted on May 16th, 2010 by John. Filed under lebookread, programming.


I have been taking a short break from blogging again. The pressure at work has only increased and is eating into a lot of my time. I haven’t been motivated to work on personal projects because well they are work. However, this has recently changed a bit.

I’ve started a Qt based library for reading ebooks in a generic manner. It is called lebookread! It is it’s early stages. So far I have it supporting epub, palmdoc pdb, ztxt pdb, tcr, and rb files. I plan to support ereader pdb, mobi, and plucker files in the near future.

The main goal of this project is to make reading ebooks easy for Qt based projects. I’ve chose to write the library in C++. This is also my first attempt at writing a library and it shows. I hope that it will be used by Sigil.

The real motivation of writing lebook read is I really want a good light weight ebook reader. The current offering have issues. I want something that is a bit more advanced in it’s rendering than FBReader. I also didn’t want anything with as large a dependency list as calibre. So, I plan on using lebookread to write my own ebook viewer.

Tags: , , , , , , , .



* KDocker 4.2 released

Posted on September 27th, 2009 by John. Filed under KDocker.


This release includes bug fixes as usual. Some new features: bash completion, iconify on focus lost option and Italian translation thanks to Alessio Cassibba. There is also a small change to the behavior when activating the tray icon. If the window is not visible it will become active and if it is active it will iconify. You can get it at the launchpad page.

Somehow I forget to mention the 4.1 release last week… I don’t think I’m going to be getting into the habit of making a release each week but there is still more I have planned. Mostly refactoring, and not to much in the way of new features.

Tags: , , , .



* KDocker 4.0

Posted on September 16th, 2009 by John. Filed under KDocker.


Recently I’ve become the maintainer of the KDocker project. KDocker is a Qt application what allows you dock any application into the system tray. It currently supports any X Windows system. What I’ve done for the 4.0 release is, move the project to launchpad (Girish, the creator of the project is locked out of the Source Forge page) and port/re-write the entire app to use Qt 4.

The port/re-write to Qt 4 is complete and I’ve released it. The version has jumped from 1.3 to 4.0 to better illustrate the the severity of this change. Also, it takes it to a similar version scheme to Qt and KDE.

There are a few things to note about this release and KDocker in general. For this release session management and auto starting have been removed. This is mainly because modern desktop environments support both of these very well. Also, I believe that environments that do not support this are better of using a dedicated application instead of having the functionality rolled into a docker. Another point to note is KDocker 4.0 as well as the older version are pure Qt and Xlib applications. They do not depend on KDE.

The new project location is at https://launchpad.net/kdocker and you can download the 4.0 release at https://launchpad.net/kdocker/+download.

Tags: , , .



* Niw Markdown Editor

Posted on August 30th, 2009 by John. Filed under niwmarkdowneditor.


For the past three weeks I’ve been working on an editor for working with plain text files and making it easy to add markdown syntax to them. My main goal is to make it easier to format the large number of ebooks I have. Almost all of them are plain text files.

It’s a python project using PyQt4 and I’m hosting it on Launchpad. here is the project page and you can find some screen shots here.

The features of this application and what makes it more useful that a generic text editor are the tool box and the tools. The toolbox allows for a number of markdown syntax changes to be made with one click. The tools menu supports a number of options that make formatting text a bit easier.

The current tools are:

  • Heading list which shows a listing of all headings in the document
  • Link list which shows a listing of all links in the document
  • Image list which shows a listing of all images in the document
  • ASCIIize which will turn all unicode characters into an ASCII equivalent
  • Remove leading spaces
  • Remove trailing spaces
  • Replace tabs with spaces
  • Separate paragraphs
  • Double line breaks
  • Remove excessive line beaks

There are a number of other options such as line numbering, highlighting of the line and syntax, and inline spell check.

There is still a lot I would like to do with the project. For one thing I needs and icon. As well as build targets for Windows and OS X. Include image previews in the Image listing. Take a look at the TODO file to get a feel of what I have in mind in the near future.

For those of you who what to test it out you can find a tarball here. The dependencies are:

  • Python 2.6
  • Qt >= 4.5
  • PyQt >=4.5
  • python-markdown
  • python-enchant (optional for spell check)

Tags: , , , .



* QPlainTextEdit With In Line Spell Check

Posted on August 22nd, 2009 by John. Filed under programming.


***Update: Simplified Highlighter.highlightBlock function

One thing Qt lacks is an integrated spell check in the text entry components. For a project I’m working on this is necessary. Using python-enchant and the QSyntaxHighlighter I was able to implement this functionality. Here is how to add an in line spell check support to a QPlainTextEdit.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
 
__license__ = 'MIT'
__copyright__ = '2009, John Schember '
__docformat__ = 'restructuredtext en'
 
import re
import sys
 
import enchant
 
from PyQt4.Qt import QAction
from PyQt4.Qt import QApplication
from PyQt4.Qt import QEvent
from PyQt4.Qt import QMenu
from PyQt4.Qt import QMouseEvent
from PyQt4.Qt import QPlainTextEdit
from PyQt4.Qt import QSyntaxHighlighter
from PyQt4.Qt import QTextCharFormat
from PyQt4.Qt import QTextCursor
from PyQt4.Qt import Qt
from PyQt4.QtCore import pyqtSignal
 
class SpellTextEdit(QPlainTextEdit):
 
    def __init__(self, *args):
        QPlainTextEdit.__init__(self, *args)
 
        # Default dictionary based on the current locale.
        self.dict = enchant.Dict()
        self.highlighter = Highlighter(self.document())
        self.highlighter.setDict(self.dict)
 
    def mousePressEvent(self, event):
        if event.button() == Qt.RightButton:
            # Rewrite the mouse event to a left button event so the cursor is
            # moved to the location of the pointer.
            event = QMouseEvent(QEvent.MouseButtonPress, event.pos(),
                Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
        QPlainTextEdit.mousePressEvent(self, event)
 
    def contextMenuEvent(self, event):
        popup_menu = self.createStandardContextMenu()
 
        # Select the word under the cursor.
        cursor = self.textCursor()
        cursor.select(QTextCursor.WordUnderCursor)
        self.setTextCursor(cursor)
 
        # Check if the selected word is misspelled and offer spelling
        # suggestions if it is.
        if self.textCursor().hasSelection():
            text = unicode(self.textCursor().selectedText())
            if not self.dict.check(text):
                spell_menu = QMenu('Spelling Suggestions')
                for word in self.dict.suggest(text):
                    action = SpellAction(word, spell_menu)
                    action.correct.connect(self.correctWord)
                    spell_menu.addAction(action)
                # Only add the spelling suggests to the menu if there are
                # suggestions.
                if len(spell_menu.actions()) != 0:
                    popup_menu.insertSeparator(popup_menu.actions()[0])
                    popup_menu.insertMenu(popup_menu.actions()[0], spell_menu)
 
        popup_menu.exec_(event.globalPos())
 
    def correctWord(self, word):
        '''
        Replaces the selected text with word.
        '''
        cursor = self.textCursor()
        cursor.beginEditBlock()
 
        cursor.removeSelectedText()
        cursor.insertText(word)
 
        cursor.endEditBlock()
 
 
class Highlighter(QSyntaxHighlighter):
 
    WORDS = u'(?iu)[\w\']+'
 
    def __init__(self, *args):
        QSyntaxHighlighter.__init__(self, *args)
 
        self.dict = None
 
    def setDict(self, dict):
        self.dict = dict
 
    def highlightBlock(self, text):
        if not self.dict:
            return
 
        text = unicode(text)
 
        format = QTextCharFormat()
        format.setUnderlineColor(Qt.red)
        format.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
 
        for word_object in re.finditer(self.WORDS, text):
            if not self.dict.check(word_object.group()):
                self.setFormat(word_object.start(),
                    word_object.end() - word_object.start(), format)
 
 
class SpellAction(QAction):
 
    '''
    A special QAction that returns the text in a signal.
    '''
 
    correct = pyqtSignal(unicode)
 
    def __init__(self, *args):
        QAction.__init__(self, *args)
 
        self.triggered.connect(lambda x: self.correct.emit(
            unicode(self.text())))
 
 
def main(args=sys.argv):
    app = QApplication(args)
 
    spellEdit = SpellTextEdit()
    spellEdit.show()
 
    return app.exec_()
 
if __name__ == '__main__':
    sys.exit(main())

The SpellTextEdit’s purpose is straightforward. It will mark misspelled words. Right clicking on a word in the SpellTextEdit will cause the word to become selected and display a context menu. If the word is misspelled and there are spelling suggestions the context menu will include a sub menu of those suggestions. Selecting a suggestion will replace the misspelled text with the selection.

The Highlighter class takes text, breaks it into words, checks if they are spelled correctly and if not underlines the misspelled ones with a red squiggle. I’m using a regular expression to split the words instead of using str.split because str.split will only split on whitespace and include punctuation (e.g. “.!*) as part of the words.

SpellAction is a simple class that allows for the action’s text to be sent with the signal. This is necessary for dynamically creating the list of possible correction words in the right click menu. The SpellAction is connected to a function that replaces the selected text with the signal text.

Tags: , , , , , .



* Better QPlainTextEdit With Line Numbers

Posted on August 19th, 2009 by John. Filed under programming.


My last post was an implementation of a Qt widget which displays text with line numbers. I found that it has a few limitations. The biggest was a performance penalty when dealing with large documents. I’ve since re-factored and rewritten the class to make the performance acceptable. I’ve also cleaned up the code a bit and added a highlight to the current line.

 
'''
Text widget with support for line numbers
'''
 
from PyQt4.Qt import QFrame
from PyQt4.Qt import QHBoxLayout
from PyQt4.Qt import QPainter
from PyQt4.Qt import QPlainTextEdit
from PyQt4.Qt import QRect
from PyQt4.Qt import QTextEdit
from PyQt4.Qt import QTextFormat
from PyQt4.Qt import QVariant
from PyQt4.Qt import QWidget
from PyQt4.Qt import Qt
 
class LNTextEdit(QFrame):
 
    class NumberBar(QWidget):
 
        def __init__(self, edit):
            QWidget.__init__(self, edit)
 
            self.edit = edit
            self.adjustWidth(1)
 
        def paintEvent(self, event):
            self.edit.numberbarPaint(self, event)
            QWidget.paintEvent(self, event)
 
        def adjustWidth(self, count):
            width = self.fontMetrics().width(unicode(count))
            if self.width() != width:
                self.setFixedWidth(width)
 
        def updateContents(self, rect, scroll):
            if scroll:
                self.scroll(0, scroll)
            else:
                # It would be nice to do
                # self.update(0, rect.y(), self.width(), rect.height())
                # But we can't because it will not remove the bold on the
                # current line if word wrap is enabled and a new block is
                # selected.
                self.update()
 
 
    class PlainTextEdit(QPlainTextEdit):
 
        def __init__(self, *args):
            QPlainTextEdit.__init__(self, *args)
 
            #self.setFrameStyle(QFrame.NoFrame)
 
            self.setFrameStyle(QFrame.NoFrame)
            self.highlight()
            #self.setLineWrapMode(QPlainTextEdit.NoWrap)
 
            self.cursorPositionChanged.connect(self.highlight)
 
        def highlight(self):
            hi_selection = QTextEdit.ExtraSelection()
 
            hi_selection.format.setBackground(self.palette().alternateBase())
            hi_selection.format.setProperty(QTextFormat.FullWidthSelection, QVariant(True))
            hi_selection.cursor = self.textCursor()
            hi_selection.cursor.clearSelection()
 
            self.setExtraSelections([hi_selection])
 
        def numberbarPaint(self, number_bar, event):
            font_metrics = self.fontMetrics()
            current_line = self.document().findBlock(self.textCursor().position()).blockNumber() + 1
 
            block = self.firstVisibleBlock()
            line_count = block.blockNumber()
            painter = QPainter(number_bar)
            painter.fillRect(event.rect(), self.palette().base())
 
            # Iterate over all visible text blocks in the document.
            while block.isValid():
                line_count += 1
                block_top = self.blockBoundingGeometry(block).translated(self.contentOffset()).top()
 
                # Check if the position of the block is out side of the visible
                # area.
                if not block.isVisible() or block_top &gt;= event.rect().bottom():
                    break
 
                # We want the line number for the selected line to be bold.
                if line_count == current_line:
                    font = painter.font()
                    font.setBold(True)
                    painter.setFont(font)
                else:
                    font = painter.font()
                    font.setBold(False)
                    painter.setFont(font)
 
                # Draw the line number right justified at the position of the line.
                paint_rect = QRect(0, block_top, number_bar.width(), font_metrics.height())
                painter.drawText(paint_rect, Qt.AlignRight, unicode(line_count))
 
                block = block.next()
 
            painter.end()
 
    def __init__(self, *args):
        QFrame.__init__(self, *args)
 
        self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
 
        self.edit = self.PlainTextEdit()
        self.number_bar = self.NumberBar(self.edit)
 
        hbox = QHBoxLayout(self)
        hbox.setSpacing(0)
        hbox.setMargin(0)
        hbox.addWidget(self.number_bar)
        hbox.addWidget(self.edit)
 
        self.edit.blockCountChanged.connect(self.number_bar.adjustWidth)
        self.edit.updateRequest.connect(self.number_bar.updateContents)
 
    def getText(self):
        return unicode(self.edit.toPlainText())
 
    def setText(self, text):
        self.edit.setPlainText(text)
 
    def isModified(self):
        return self.edit.document().isModified()
 
    def setModified(self, modified):
        self.edit.document().setModified(modified)
 
    def setLineWrapMode(self, mode):
        self.edit.setLineWrapMode(mode)

Tags: , , .



* QTextEdit With Line Numbers

Posted on August 15th, 2009 by John. Filed under programming.


Here is a Qt4 widget written in Python that allows for line numbers next to a QTextEdit. Similar to what is seen in a number of text editors such as gedit and kate.

from PyQt4.Qt import QFrame, QWidget, QTextEdit, QHBoxLayout, QPainter
 
class LineTextWidget(QFrame):
 
    class NumberBar(QWidget):
 
        def __init__(self, *args):
            QWidget.__init__(self, *args)
            self.edit = None
            # This is used to update the width of the control.
            # It is the highest line that is currently visibile.
            self.highest_line = 0
 
        def setTextEdit(self, edit):
            self.edit = edit
 
        def update(self, *args):
            '''
            Updates the number bar to display the current set of numbers.
            Also, adjusts the width of the number bar if necessary.
            '''
            # The + 4 is used to compensate for the current line being bold.
            width = self.fontMetrics().width(str(self.highest_line)) + 4
            if self.width() != width:
                self.setFixedWidth(width)
            QWidget.update(self, *args)
 
        def paintEvent(self, event):
            contents_y = self.edit.verticalScrollBar().value()
            page_bottom = contents_y + self.edit.viewport().height()
            font_metrics = self.fontMetrics()
            current_block = self.edit.document().findBlock(self.edit.textCursor().position())
 
            painter = QPainter(self)
 
            line_count = 0
            # Iterate over all text blocks in the document.
            block = self.edit.document().begin()
            while block.isValid():
                line_count += 1
 
                # The top left position of the block in the document
                position = self.edit.document().documentLayout().blockBoundingRect(block).topLeft()
 
                # Check if the position of the block is out side of the visible
                # area.
                if position.y() &gt; page_bottom:
                    break
 
                # We want the line number for the selected line to be bold.
                bold = False
                if block == current_block:
                    bold = True
                    font = painter.font()
                    font.setBold(True)
                    painter.setFont(font)
 
                # Draw the line number right justified at the y position of the
                # line. 3 is a magic padding number. drawText(x, y, text).
                painter.drawText(self.width() - font_metrics.width(str(line_count)) - 3, round(position.y()) - contents_y + font_metrics.ascent(), str(line_count))
 
                # Remove the bold style if it was set previously.
                if bold:
                    font = painter.font()
                    font.setBold(False)
                    painter.setFont(font)
 
                block = block.next()
 
            self.highest_line = line_count
            painter.end()
 
            QWidget.paintEvent(self, event)
 
 
    def __init__(self, *args):
        QFrame.__init__(self, *args)
 
        self.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
 
        self.edit = QTextEdit()
        self.edit.setFrameStyle(QFrame.NoFrame)
        self.edit.setAcceptRichText(False)
 
        self.number_bar = self.NumberBar()
        self.number_bar.setTextEdit(self.edit)
 
        hbox = QHBoxLayout(self)
        hbox.setSpacing(0)
        hbox.setMargin(0)
        hbox.addWidget(self.number_bar)
        hbox.addWidget(self.edit)
 
        self.edit.installEventFilter(self)
        self.edit.viewport().installEventFilter(self)
 
    def eventFilter(self, object, event):
        # Update the line numbers for all events on the text edit and the viewport.
        # This is easier than connecting all necessary singals.
        if object in (self.edit, self.edit.viewport()):
            self.number_bar.update()
            return False
        return QFrame.eventFilter(object, event)
 
    def getTextEdit(self):
        return self.edit

Tags: , , .



* History Drop Down With Model

Posted on July 23rd, 2009 by John. Filed under programming.


Following is a bit of python code that illustrates how to create a QComboBox that attaches to a model for listing history items. The main features of this code are items entered in the text area of the combo are added to the history. Selected items and items entered that already appear in the combo are moved to the top. When MAX_ITEMS is exceeded older items (items at the bottom of the drop down) are removed.

#!/usr/bin/env python
 
from PyQt4.Qt import *
from PyQt4.QtGui import *
 
class ComboModel(QAbstractListModel):
 
    MAX_ITEMS = 5
    items = [u'123', u'456', u'789']
 
    # Required to get a working model.
    def rowCount(self, parent=QModelIndex()):
        # This is a List model meaning all elements are root elements.
        if parent and parent.isValid():
            return 0
        return len(self.items)
 
    # Required to get a working model.
    def insertRows(self, row, count, parent=QModelIndex()):
        self.beginInsertRows(parent, 0, 1)
        self.endInsertRows()
        return True
 
    def data(self, index, role):
        if role in (Qt.DisplayRole, Qt.EditRole):
            return QVariant(self.items[index.row()])
        return QVariant()
 
    def setData(self, index, value, role):
        value = unicode(value.toString())
        if value in self.items:
            # Move the item to the top of the list.
            del self.items[self.items.index(value)]
            self.items.insert(0, value)
        else:
            # Add the new item to the top of the list.
            self.items.insert(0, value)
            self.remove_items()
        self.emit(SIGNAL('dataChanged(QModelIndex, QModelIndex)'),
            self.createIndex(0, 0), self.createIndex(len(self.items) - 1, 0))
        return True
 
    def remove_items(self):
        '''
        Checks the number of items in the list and if it has been exceeded
        removes extra items.
        '''
        if len(self.items) &gt; self.MAX_ITEMS:
            count = len(self.items) - self.MAX_ITEMS
            del self.items[-count:]
 
    def order_items(self, index):
        '''
        Move the selected item tot he top of the list.
        '''
        # We only need to move the item to the top if it is not the first item.
        if index &gt; 0:
            self.emit(SIGNAL('layoutAboutToBeChanged()'))
            value = self.items[index]
            del self.items[index]
            self.items.insert(0, value)
            self.emit(SIGNAL('layoutChanged()'))
 
 
class MComboBox(QComboBox):
 
    def __init__(self, parent=None):
        QComboBox.__init__(self, parent)
        self.setEditable(True)
        # The default policy is InsertAtBottom. InsertAtTop must be set
        # otherwise the model will move the item to the top and the QComboBox
        # will select the last index. In the case of reaching max items the
        # index will be invalid because the removal is after the QComboBox has
        # stored the value of the last index.
        self.setInsertPolicy(QComboBox.InsertAtTop)
        # Without this the QComboBox will not call set data for us to check
        # for items already in the model if the item is a duplicate.
        self.setDuplicatesEnabled(True)
        self._model = ComboModel()
        self.setModel(self._model)
 
        # We need to tell the model when an item is selected so it can be moved
        # to the top of the history list.
        self.connect(self, SIGNAL('activated(int)'), self._model.order_items)
 
 
def main():
    app = QApplication([])
 
    bx = MComboBox()
    bx.show()
 
    app.exec_()
 
if __name__ == '__main__':
    main()

One thing to note is that in this example the model stores the items in a list called items. This can be replaced with some other way to retrieve the history items. For example with a connection to an SQLite DB.

Tags: , .



* QCompleter and Comma-Separated Tags

Posted on July 4th, 2009 by John. Filed under programming.


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.

Tags: , .