Blog

Sixth anniversary

This year's anniversary of the mimec.org website coincides with two very important events. First, I'm finally releasing version 1.0 of WebIssues. It's been the longest development cycle I've ever made, as it took about 2.5 years; in that time I (and other contributors) made about 1,000 commits into SVN, and published nine pre-release versions. The popularity of this project is also continuously growing, reaching about 15,000 downloads this year, and a few weeks ago WebIssues was one of the featured projects of SourceForge.net main page. But the most important thing is that I managed to put all the long awaited features into it, and it became a really unique, innovative project, which can compete even with commercial software.

The second, even more important event, that we're impatiently awaiting with my wife, is the birth of our first son. All indications are that it's going to happen right after Christmas. This is really going to be the biggest and most important "project" in the next few years :). Starting next year, you can expect some new photo galleries to appear on this site, especially that we're going to continue visiting various interesting places in the world, as it seems to be the most reasonable way to spend money in these uncertain times.

Hopefully I'm still going to find some time to continue working on various other things:

  • I'm planning to update the mimec.org sites, refresh their appearance a bit and update some content, for example by publishing new versions of some articles.
  • I'm also going to continue developing Saladin, as it's a great, promising project that also deserves a bit of attention.
  • There's still a lot that can be done with WebIssues, so after some time off I'm also going to return to it. I'm also seriously thinking about starting some professional services related to WebIssues, such as paid hosting, support, etc., but time will tell what will come of it.
  • There are a few other ideas that keep circling in my head, for example a bazillionth version of the Misc computing language interpreter, a novel that I recently started to write, etc.
Filed under: Blog

Status bar and elided label

Many applications use a QStatusBar to display various information at the bottom of the window. The good thing about this widget is that it can contain various other widgets, not only labels, but also buttons, progress bars, etc. But in the most common case the status bar simply contains a few labels, and lays them out in a horizontal bar layout.

Everything is fine when the information displayed in the status bar is short and the window is large enough. However one thing that we should remember about the QLabel is that it can be expanded as necessary, but in the default configuration it cannot shrink — unless it has word wrapping or auto-scaling (in case of graphic content) enabled, its preferred size is also the minimum size. The result is that the window must be at least as wide as the total widths of all status labels, and the user cannot make it smaller. But in some cases the status text can be arbitrarily long — take a web browser which displays the URL of the hovered link in the status bar, and we cannot make any assumptions about the maximum length of the link.

Qt has a powerful mechanism of layouts which offers various solutions to this problem. One solution is to set the horizontal size policy of the label to Ignored. In that case both the preferred size and the minimum size is ignored. That's fine when the status bar contains only a single label. But in many cases, there are several additional labels on the right, that should only take as much space as they need, but should also shrink when there is not enough space. The solution is to keep the Preferred size policy, but also force the label to have zero minimum width by using setMinimumWidth. (Edited on 2012-03-23: actually it must be a small value greater than zero; setting minimum width to zero has no effect.)

There is still a minor issue — when the QLabel shrinks, it doesn't elide the text (by appending ... to indicate that the text was truncated), but simply clips it. The QLabel can be extended to support elided text quite easily, and there are many ways to do that. When I first did that, based on some code that I found, it worked nicely, until I placed a few such labels side by side in a status bar. I couldn't get the layout to behave correctly, and finally I found that this problem was related to the mechanism of eliding text.

So in case you need to put together an elided label on your own: do this by simply overriding the paintEvent, and do not try to mess with setText or resizeEvent, because that may break the automatically calculated preferred size of the label. Here's a code snippet:

void ElidedLabel::paintEvent( QPaintEvent* /*e*/ )
{
    QPainter painter( this );
    drawFrame( &painter );

    QRect cr = contentsRect();
    cr.adjust( margin(), margin(), -margin(), -margin() );

    QString fullText = text();

    if ( fullText != m_lastText || cr.width() != m_lastWidth ) {
        m_elidedText = fontMetrics().elidedText( fullText, Qt::ElideRight,
            cr.width() );
        m_lastText = fullText;
        m_lastWidth = cr.width();
    }

    QStyleOption opt;
    opt.initFrom( this );

    style()->drawItemText( &painter, cr, alignment(), opt.palette, isEnabled(),
        m_elidedText, foregroundRole() );
}

Note that this code caches the elided text for better performance, using the m_elidedText, m_lastText and m_lastWidth member variables. Also note that it always uses Qt::ElideRight — you can customize this if you need. Of course this will only work in case of a single-line, plain text with no special formatting.

Filed under: Blog

Locking an SQLite database

Today I will return to the topic of SQLite once again. This time I will discuss why and how to lock an SQLite database for exclusive access to make sure that it's not modified by other processes.

Of course one of the goals of SQLite, like most database engines, is concurrency - i.e. the ability for multiple processes to read and write data to the same database. As we can read in the documentation, "in order to maximize concurrency, SQLite works to minimize the amount of time that EXCLUSIVE locks are held".

That makes perfect sense if we use SQLite with a web application, which should handle as many concurrent requests as possible. But there are some scenarios when we might want to keep the database locked for exclusive access. For example, an SQLite database is often used as a cache for various data. If we run two instances of a program and both use the same cache, it may sometimes lead to strange results. Sometimes it's just easier to prevent such situation than to try to handle it. As you can guess, that's the situation I came across in the WebIssues client.

One way to achieve this is to simply use one large transaction throughout the lifetime of the connection. The documentation advises such solution when the database is used as the application file format. However, sometimes we want all modifications to be committed as soon as possible, for example to prevent losing data on a crash, while still keeping an exclusive lock as long as the connection is open.

Fortunately, SQLite has a useful pragma statement called locking_mode. If we set it to EXCLUSIVE, the obtained locks are never released until the connection is closed. However, the name of this setting is slightly misleading, because it doesn't really obtain an exclusive lock, it just ensures that the lock is never released. So perhaps "persistent" locking mode would be a better name.

In order to lock the database, we need to perform the following sequence of commands:

  • PRAGMA locking_mode = EXCLUSIVE
  • BEGIN EXCLUSIVE
  • COMMIT

The BEGIN EXCLUSIVE command actually locks the database. It begins an empty transaction, which doesn't change anything, but ensures that the exclusive lock is acquired on the database. If the database is already locked by another process, this command will fail with the SQLITE_BUSY error.

Note that the standard Qt driver for SQLite sets the busy timeout to 5 seconds by default, so the application will freeze for a while before reporting an error. We can change it using the QSQLITE_BUSY_TIMEOUT connection option, or when using a custom driver, by simply changing the parameter of sqlite3_busy_timeout to a smaller value.

Filed under: Blog

QComboBox with separator

I haven't written for quite some time mostly because I'm getting more and more busy with WebIssues. Take a look at the online demo of the latest version, 1.0-beta2. But here's another post in my Qt series.

In version 4.4 of Qt a long awaited feature was added to the QComboBox widget. It is now possible to create separator items, which are drawn as thin, gray lines and cannot be selected. Before 4.4 it was necessary to create a custom item delegate, and also to add a few hacks in order to correctly calculate the height of the popup list and to make it impossible to select the separator using up and down arrow keys. Now we can simply use the insertSeparator method.

Perhaps it's a matter of taste, but I don't like that default separator. It has no space above and below and it is basically hardly visible. However we can change the way it looks by simply implementing a custom QItemDelegate and replacing the default delegate using setItemDelegate.

If you look at the source code of the combo box, or actually the internal QComboBoxDelegate class, you will see that a separator item has the Qt::AccessibleDescriptionRole data set to the string "separator". We can take advantage of this and increase the height of the separator to 5 pixels:

QSize sizeHint( const QStyleOptionViewItem& option, const QModelIndex& index ) const
{
    QString type = index.data( Qt::AccessibleDescriptionRole ).toString();
    if ( type == QLatin1String( "separator" ) )
        return QSize( 5, 5 );
    return QItemDelegate::sizeHint( option, index );
}

We can also override the paint method in order to draw a dark, horizontal line:

void paint( QPainter* painter, const QStyleOptionViewItem& option,
    const QModelIndex& index ) const
{
    QString type = index.data( Qt::AccessibleDescriptionRole ).toString();
    if ( type == QLatin1String( "separator" ) ) {
        QItemDelegate::paint( painter, option, index );
        int y = ( option.rect.top() + option.rect.bottom() ) / 2;
        painter->setPen(  option.palette.color( QPalette::Active, QPalette::Dark ) );
        painter->drawLine( option.rect.left(), y, option.rect.right(), y );
    } else {
        QItemDelegate::paint( painter, option, index );
    }
}

It's convenient to create a custom class which inherits QComboBox and automatically creates the custom delegate in the constructor. Then we can also add the missing, but very useful addSeparator method:

void addSeparator()
{
    insertSeparator( count() );
}

The custom delegate allows us to do much more interesting things. A nice feature of the <select> element in HTML is the ability to create groups of items. Parent items are displayed using bold font and are cannot be selected. Child items, on the other hand, are slightly indented. Here's an example:

Select element

It's easy to do something like this using a regular QComboBox in Qt. Assuming that the combo box uses the standard model, we can add the following helper method to add a "parent" item of a group:

void addParentItem( const QString& text )
{
    QStandardItem* item = new QStandardItem( text );
    item->setFlags( item->flags() & ~( Qt::ItemIsEnabled | Qt::ItemIsSelectable ) );
    item->setData( "parent", Qt::AccessibleDescriptionRole );

    QFont font = item->font();
    font.setBold( true );
    item->setFont( font );

    QStandardItemModel* itemModel = (QStandardItemModel*)model();
    itemModel->appendRow( item );
}

We make the item disabled and non-selectable in the same way as Qt does in case of separators. We also indicate that it's a parent item using the accessible description data. Finally we make the item's default font bold. The code for adding child items is even simpler:

void SeparatorComboBox::addChildItem( const QString& text, const QVariant& data )
{
    QStandardItem* item = new QStandardItem( text + QString( 4, QChar( ' ' ) ) );
    item->setData( data, Qt::UserRole );
    item->setData( "child", Qt::AccessibleDescriptionRole );

    QStandardItemModel* itemModel = (QStandardItemModel*)model();
    itemModel->appendRow( item );
}

We append 4 spaces to the item's text so that it's size is adjusted, while making the item still selectable by typing a few first letters. We also set the user data (for compatibility with addItem) and indicate that it's a child item. Now we have to slightly customize the way those special types of items are painted by the delegate:

    if ( type == QLatin1String( "parent" ) ) {
        QStyleOptionViewItem parentOption = option;
        parentOption.state |= QStyle::State_Enabled;
        QItemDelegate::paint( painter, parentOption, index );
    } else if ( type == QLatin1String( "child" ) ) {
        QStyleOptionViewItem childOption = option;
        int indent = option.fontMetrics.width( QString( 4, QChar( ' ' ) ) );
        childOption.rect.adjust( indent, 0, 0, 0 );
        childOption.textElideMode = Qt::ElideNone;
        QItemDelegate::paint( painter, childOption, index );
    }

We paint the parent item as if it was enabled, otherwise it would be grayed. We also adjust the rectangle of the child item by the width of four spaces to make the indent.

Note that the width of "text " may sometimes be slightly smaller than the sum of widths of " " and "text" because of font kerning. The longest item may sometimes appear truncated (with "..." at the end). To prevent this, we disable the elide mode.

Filed under: Blog

QCompleter with multi-selection

The QCompleter class offers an out-of-the-box functionality of automatically suggesting matching items when typing in the associated QLineEdit. It can be used as a nice replacement for QComboBox when there is a lot of items or when the user can enter a free text (not constrained to one of the items).

One important functionality that the QCompleter lack is the ability to complete a list of items separated with commas. Such completer, together with the line edit widget, could be used as a simple combo box with multi-selection, for editing comma separated tags, etc. Quick googling revealed two simple solutions: this and this. They are both written in Python, but can be easily translated to C++. The following code snippet is based on the first, simpler and much more elegant example. Obviously it's a bit more verbose that its equivalent in Python, but I personally find it easier to understand (perhaps because Python is not my native language ;).

The idea is based on the concept of "path", which is originally used to support completion based on tree models (for example the folders and files in the file system), but it can be slightly abused to support multi-selection. In this implementation, the path is not based on the model's index (because our model is a flat list of strings, not a tree), but rather on the original contents of the associated line edit widget, with the part after the last comma replaced with the selected item.

class MultiSelectCompleter : public QCompleter
{
    Q_OBJECT
public:
    MultiSelectCompleter( const QStringList& items, QObject* parent );
    ~MultiSelectCompleter();

public:
    QString pathFromIndex( const QModelIndex& index ) const;
    QStringList splitPath( const QString& path ) const;
};

MultiSelectCompleter::MultiSelectCompleter( const QStringList& items, QObject* parent )
    : QCompleter( items, parent )
{
}

MultiSelectCompleter::~MultiSelectCompleter()
{
}

QString MultiSelectCompleter::pathFromIndex( const QModelIndex& index ) const
{
    QString path = QCompleter::pathFromIndex( index );

    QString text = static_cast<QLineEdit*>( widget() )->text();

    int pos = text.lastIndexOf( ',' );
    if ( pos >= 0 )
         path = text.left( pos ) + ", " + path;

    return path;
}

QStringList MultiSelectCompleter::splitPath( const QString& path ) const
{
    int pos = path.lastIndexOf( ',' ) + 1;

    while ( pos < path.length() && path.at( pos ) == QLatin1Char( ' ' ) )
        pos++;

    return QStringList( path.mid( pos ) );
}

Filed under: Blog
Syndicate content