programming

Issues and Tins

Today I released version 1.1 of WebIssues, a major release which introduces lots of very nice and useful features. I must admit that I'm relieved that this project is finally over. It's also perfect timing, because I'm getting seriously involved in the indie game which I announced some time ago. The game is now officially called Mister Tins and it even already has a Facebook page and a blog (where I will probably post more often than here in the nearest future).

What started as a quick test project, has now become a playable and quite enjoyable game (at least for me). It's still very far from the first official release, but at least now I'm convinced that this is really what I want to do. It's what I always wanted to do and what I probably should have done a long time ago.

I'm not saying that I regret what I've been doing for the last few years. I definitely wouldn't be half as good programmer if it wasn't for WebIssues. I would even go as far as saying that I wouldn't be half the person I am today if it wasn't for all the open source projects I've been involved in. All this technical and non-technical experience should now pay off with this new project.

Of course, there's no guarantee that I will succeed. I know that there is a lot of potential in what I'm doing, and the whole idea of the game, while simple, seems quite innovative. On the other hand, there are many factors involved, and not all of them depend on me. A lot of luck is needed to provide what people need exactly in the right moment. Also, creating games is a team sport, and experience taught me that finding the right team members is not easy an task. But the best thing is that I've reached one very major goal, which was releasing WebIssues 1.1, and I can immediately concentrate on the next goal, without losing the momentum that I have.

Filed under: Blog

Disabling a QLabel with links

Qt doesn't have an equivalent of .NET's LinkLabel control, because the regular QLabel supports HTML content, including links. It's easy to create a link label using an anchor tag (i.e. <a href="...">...</a>). You can then handle the linkActivated signal or simply set the openExternalLinks property to true if you want the label to automatically open the link target in a web browser. However, there is one small limitation of the standard label widget: when you disable it, the links no longer work, but they are still drawn the same way as regular links (i.e. as blue underlined text).

Recently I had to slighty modify the notification schedule editor in WebIssues which looks like this:

img1

The links on the right make it easier to check or uncheck all options at once. However, the entire schedule needs to be disabled if sending summary emails is turned off:

img2

Note that the links are drawn in the same way as the regular disabled label and the checkboxes. How to achieve such effect?

After some research it turned out that tweaking the label's palette and/or style sheet doesn't do the job as expected. The workaround is to include the appropriate style directly in the anchor tag, for example:

QColor color = palette().color( QPalette::Disabled, QPalette::WindowText );
label->setText( QString( "<a href=\"%1\" style=\"color: %2; text-decoration: none;\">%3</a>" )
    .arg( linkUrl, color.name(), linkCaption );

Note that the disabled text color from the current palette is passed to the style definition so that it looks the same as the regular disabled label.

To make it easier, I wrote a very simple subclass of QLabel which automatically generates the correct HTML when it becomes enabled or disabled:

class LinkLabel : public QLabel
{
    Q_OBJECT
public:
    LinkLabel( const QString& caption, QWidget* parent ) : QLabel( parent ),
        m_linkUrl( "#" ),
        m_linkCaption( caption )
    {
        setTextFormat( Qt::RichText );
        updateLink();
    }

    ~LinkLabel() { }

public:
    void setLinkUrl( const QString& url )
    {
        m_linkUrl = url;
        updateLink();
    }

    const QString& linkUrl() const { return m_linkUrl; }

    void setLinkCaption( const QString& caption )
    {
        m_linkCaption = caption;
        updateLink();
    }

    const QString& linkCaption() const { return m_linkCaption; }

protected: // overrides
    void changeEvent( QEvent* e )
    {
        QLabel::changeEvent( e );
        if ( e->type() == QEvent::EnabledChange || e->type() == QEvent::PaletteChange )
            updateLink();
    }

private:
    void updateLink()
    {
        QString style;
        if ( !isEnabled() ) {
            QColor color = palette().color( QPalette::Disabled, QPalette::WindowText );
            style = QString( " style=\"color: %1; text-decoration: none;\"" ).arg( color.name() );
        }
        setText( QString( "<a href=\"%1\"%2>%3</a>" ).arg( Qt::escape( m_linkUrl ), style,
            Qt::escape( m_linkCaption ) ) );
    }

private:
    QString m_linkUrl;
    QString m_linkCaption;
};

Note that in this example the link's URL defaults to "#", which is convenient if the link is handled internally using the linkActivated signal. Of course you can overload the contstructor so that it takes the URL as a parameter. Also it's not very difficult to modify the code so that a single label could contain multiple links.

Filed under: Blog

QTextBrowser vs. QWebView pt. 2

Last week I briefly compared QTextBrowser and QWebView, the two Qt widgets which can be used for displaying rich text and HTML documents (and explained the differences between rich text and HTML). I concluded that QWebView should be used to display external web content, and for creating complex layouts, where QTextBrowser has limited capabilities. However, I also said that QWebView has its own problems. Today I will focus on these.

The entire QtWebKit component is based on WebKit, the web browser engine which also powers Safari and Chrome. However, keep in mind that the version used by Qt (especially Qt4) is quite old (last changes are from 2011). Also, in addition to common code, there are also lots of platform dependent components, so the Qt version of WebKit is not entirely the same as the one used by these browsers. What this means is that if you embed a QWebView in your application, it won't automatically have the same capabilities as Google Chrome.

The most annoying thing in QtWebKit is that printing is currently quite broken. When you print issue history or any other report in version 1.1 of the WebIssues Desktop Client, you can notice that sometimes lines are cut in half by page breaks. The same happens when exporting the report as a PDF document. This bug is already fixed upstream, but it still occurs in Qt 4.8. Unfortunately it seems that even in the latest versions of Qt printing is still broken.

Another thing to watch for when printing, though that's not really a bug, is the fact that printing the page immediately after calling setHtml() will sometimes produce no result. If the page contains any references to other resources, they are always loaded asynchronously, even if they are local files or embedded resources (which can be accessed using the qrc: protocol). The solution is to handle the loadFinished() signal from the QWebFrame.

QtWebKit has problems with certain Unicode characters. In version 1.0 of WebIssues I used the → character in history of changes, but in version 1.1 I had to replace it with », because → breaks the text layout. This is similar to this bug, however in my case this occurred not only when printing, but also QWebView rendered the text incorrectly.

Also tabs are incorrectly rendered by the QtWebKit and appear as spaces. This bug is apparently fixed in Qt 5, but not in Qt 4.8, so in the WebIssues Desktop Client I have to convert all tabs to spaces, because tabs may occur in issue comments. This isn't perfect, especially when using a non-monospace font, but it works in most cases.

I also found than the hasSelection() property of QWebView always returns true, even if there is really no text selected. Since I'm using this to determine whether the Copy action should be enabled, I'm simply checking if the built-in Copy action is enabled instead, in other words I'm calling pageAction( QWebPage::Copy )->isEnabled(). Fortunately the selectionChanged() signal is emitted correctly.

In general, I think that the whole API of QtWebKit module is quite limited, especially when compared to QTextBrowser and QTextDocument. Even though QtWebKit was first introduced in Qt 4.4, many useful functions are only available in the most recent versions. However, it is sufficient for my current needs, so despite the various bugs and problems, I'm generally satisfied with the migration from QTextBrowser to QWebView.

Filed under: Blog

QTextBrowser vs. QWebView

There are two classes in Qt that can display HTML content: QTextBrowser and QWebView. They seem similar, but in fact they are quite different. The most obvious difference is that QTextBrowser is part of QtGui, while QWebView belongs to the QtWebKit module. It's a pretty big library, about 12.5 MB (32-bit Windows DLL), which is more than QtCore and QtGui combined. I'm going to try to explain why there's such difference.

Although QTextBrowser can display a piece of HTML content, technically it's a rich text viewer, not an HTML viewer. It may seems like the same thing, and in fact there are many similarities. A rich text document consists of blocks of text (similar to HTML paragraphs) and frames (similar to HTML div's). It also supports tables, lists and images. However, the layout of the rich text document is much simpler than that of HTML. The text of the document simply flows from top to bottom. There is no concept of absolute positioning, floating frames, etc. You can forget about tableless layouts and most CSS styles. Even the margin and padding settings are not always respected and it takes some experimentation to get the spacing right.

The QTextBrowser can export the rich text in HTML format, and import it back without loosing information. However, this is not fully standard compliant HTML, and when opened in a regular browser, it may not necessarily look the same as in the QTextBrowser. What's worse, although HTML can be imported from an external source, only a limited subset of HTML tags, attributes and CSS properties will be recognized, and the document will almost surely look very different than in a browser.

Side note: The difference between rich text model and HTML model is not just specific to Qt. Also many common word processors, including MS Word, have a similar limitation. You can import HTML to Word, but the layout of a web page will not be strictly preserved. And because MS Outlook uses the same engine as Word to render HTML emails, it also only supports a limited subset of HTML and CSS. This makes it difficult to create HTML emails which look good in all email clients including Outlook.

This obviously doesn't mean that you shouldn't use QTextBrowser at all. First of all, it's got an excellent cursor-based API which lets you create rich documents very efficiently without writing a single piece of HTML markup. This is excellent for creating various reports, etc. Just ensure that when you make a lot of changes in the document, it shouldn't be connected to the browser, otherwise updating it will be slow. Also don't forget that rich text is also supported out-of-the by many other widgets, for example QLabel or QToolTip. Not to mention that you can also edit rich text using the QTextEdit widget (which is, in fact, inherited by QTextBrowser).

QWebView, on the other hand, is a full blown, standards compliant HTML browser. Actually it's based on the same code which powers Chrome and Safari browers. It works natively with HTML and supports all tags, attributes and CSS properties. It also has a built-in JavaScript interpreter. You should definitely use QWebView when you need to display external web content, create complex layouts or use dynamic, scripted content. This comes at the cost of the extra 12.5 MB linked library and slightly higher resource usage. It's hard to measue the difference in performance. Simple documents work very fast in both controls, and complex documents can only be handled well by QWebView.

Despite many advantages, QtWebKit is far from being perfect. In the next part I will write about various bugs and problems I've encountered so far while porting the WebIssues Desktop Client from QTextBrowser to QWebView.

Filed under: Blog

Positioning multiple windows in Qt

It's been a while since the last post, but I've been quite busy with WebIssues. Now that the first beta of version 1.1 is released, it's time to catch up with other things. Actually I started writing this post some time ago, but then my HDD crashed, so I had to do it once again.

QWidget provides the saveGeometry/restoreGeometry pair of methods for a convenient way of storing and restoring the position of a window. This works fine when our application has just one top level window. But what if the application can open multiple windows of a given type? Consider a chat or email application, where each conversation or message can be opened in a separate window. It should be possible to store the position and size of the windows, but we also want to make sure that when the user opens multiple windows, they are displayed at slightly different positions, so that they don't cover one another entirely.

Doing this manually is not an easy task. Besides handling the maximized state of the window, restoreGeometry also ensures that the window is not displayed off screen when a display device is disconnected or its resolution has changed since the geometry was saved. Instead of implementing a custom mechanism, we can take advantage of saveGeometry/restoreGeometry, with a slight modification which makes multiple windows behave correctly.

The idea is that in addition to the geometry (which is serialized as a QByteArray) we also store a boolean flag called "offset". Whenever a new window is opened, we remember its position and set the offset to true, meaning that when another window is opened, an offset should be added to the stored position. When a window is closed, we remember its final position and set the offset to false. Another window will be then opened at the exact position of the old one. One way of doing this is handling the show and hide events:

void MyWindow::showEvent( QShowEvent* e )
{
    if ( !e->spontaneous() )
        storeGeometry( true );
}

void MyWindow::hideEvent( QHideEvent* e )
{
    if ( !e->spontaneous() )
        storeGeometry( false );
}

void MyWindow::storeGeometry( bool offset )
{
    QSettings settings;
    settings.setValue( "MyWindowGeometry", saveGeometry() );
    settings.setValue( "MyWindowOffset", offset );
}

Note that we ignore spontaneous events, which are fired when the window is minimized or restored by the user. All that's left is restoring the window's geometry. This can be done, for example, in the constructor of the window's class:

    QSettings settings;
    restoreGeometry( settings.value( "MyWindowGeometry" ).toByteArray() );
    if ( settings.value( "MyWindowOffset" ).toBool() ) {
        QPoint position = pos() + QPoint( 40, 40 );
        QRect available = QApplication::desktop()->availableGeometry( this );
        QRect frame = frameGeometry();
        if ( position.x() + frame.width() > available.right() )
            position.rx() = available.left();
        if ( position.y() + frame.height() > available.bottom() - 20 )
            position.ry() = available.top();
        move( position );
    }

We add an arbitrary offset of 40 pixels to the window's X and Y position. Then we ensure that the window doesn't span outside the right and bottom edge of the screen and otherwise move it to the left or top edge, respectively.

Note that this is not a perfect solution. If the user opens and closes the windows in certain order, they will overlap (I leave it as an excercise to the reader to come up with such scenario). But it nicely handles the two most common scenarios: reopening a closed window in the same location and ensuring that multiple windows, opened one after another, have slightly different positions.

Filed under: Blog
Syndicate content