Blog

Porting the Seam, Not the Screen

I started porting a small KDE utility today and made the predictable mistake: I opened the main window source first. Ten minutes later I had changed several class names and learned almost nothing about whether the program still worked.

The useful work began when I backed up and found the seams. This program has a parser, a document object, a job that reads files, and a window that displays results. Only the last part truly cares about widgets. I built the parser and document as a small Qt 4 library before touching the screen.

The immediate payoff was a command-line test:

int main(int argc, char **argv)
{
    QCoreApplication app(argc, argv);

    if (argc != 2) {
        qCritical() << "usage:" << argv[0] << "file";
        return 1;
    }

    Document document;
    QString error;
    if (!document.load(QString::fromLocal8Bit(argv[1]), &error)) {
        qCritical() << error;
        return 1;
    }

    qDebug() << document.entries().count();
    return 0;
}

That exposed two assumptions hidden by the old interface. One parser path depended on a widget-created codec, and file loading reported errors by opening a message box. Neither belongs in the core. Loading now returns a status and an error string; the caller decides how to present it.

This matters because KDE 4 is architecture and porting work at the moment, not a desktop I would hand to an unsuspecting relative. APIs and libraries are being separated so applications can use services without importing a sack of unrelated GUI machinery. A compiling window is pleasant evidence, but it is not proof of a sound port.

Porting from the non-GUI core outward gives me executable checkpoints and makes ownership clearer. Old applications sometimes mix policy, storage, and widgets so thoroughly that extracting a core costs more than a direct compile fix. For a tiny or soon-to-be-replaced tool, the ugly route may be honest.

One more practical trick: make every compatibility warning visible during the port. I am using verbose builds and keeping a short list of intentionally deferred warnings. If the list grows without explanation, it is not a list; it is a compost heap.

I also run the command-line test from a clean build tree, not only from the source directory. That catches accidental dependence on local files before the GUI can hide it.

Tomorrow I will return to the main window. It should now have less to do, which is my favourite feature in a window class.

CMake Before Coffee

This morning I changed one header and watched the old build machinery reconsider what appeared to be western civilisation. That was enough encouragement to try the same small library with CMake.

The useful bit was not clever syntax. It was stating the target directly:

set(parser_SRCS parser.cpp token.cpp)
kde4_add_library(parser ${parser_SRCS})
target_link_libraries(parser ${QT_QTCORE_LIBRARY})
install(TARGETS parser DESTINATION ${LIB_INSTALL_DIR})

I configured outside the source tree:

mkdir build
cd build
cmake ..
make VERBOSE=1

That last command matters while converting. The generated command line tells me which include path, definition, or library I forgot. Guessing from a linker error is possible, but so is repairing a watch with a spoon.

I prefer one explicit target with its own sources and libraries over directory-wide flags. It makes dependencies visible and avoids accidentally linking every library against everything else. The caveat is that our CMake helper macros are still moving, so a tidy file today may need adjustment next week.

My practical rule is to convert one buildable directory at a time, compile it, then continue. A giant mechanical conversion produces a giant pile of errors with no useful ordering. Small green steps are less heroic and considerably faster.

The Timer That Froze the Window

I found today’s problem by dragging a window. The repaint stopped, the title bar sulked, and a progress label remained at 12 percent while the application did several seconds of perfectly respectable work.

The work was running from a QTimer slot. I had assumed that using a timer made it asynchronous. It does not. A timer merely arranges for a callback to be delivered by the event loop; the callback still runs on that loop’s thread. If the slot computes for five seconds, paint events wait five seconds. So do mouse events and, more importantly, the user’s patience.

The quick diagnostic was wonderfully primitive:

void Scanner::scanNext()
{
    qDebug() << "enter scanNext";
    scanEverything();
    qDebug() << "leave scanNext";
}

The pause sat between those messages. Splitting the operation into bounded pieces fixed the immediate problem:

void Scanner::scanNext()
{
    for (int i = 0; i != 50 && hasMore(); ++i)
        scanOne();

    if (hasMore())
        QTimer::singleShot(0, this, SLOT(scanNext()));
}

A zero-duration timer does not promise instant execution. It puts more work back into event processing, allowing pending paints and input to run between batches. The batch size is deliberately boring: small enough to keep the interface alive, large enough not to drown in scheduling overhead.

I prefer this incremental approach when the job naturally divides into independent records. It keeps all GUI objects on their owning thread and makes cancellation straightforward. The caveat is that a single expensive record still blocks everything. In that case the computation belongs on a worker thread, with results sent back rather than widgets touched directly.

Calling QCoreApplication::processEvents() inside the loop is tempting, but it permits other callbacks to enter while the current operation is unfinished. That can expose partially updated state or let the user start the same action twice. Returning to the event loop between explicit batches gives me a cleaner boundary: each batch leaves the object consistent before yielding.

That boundary also gives cancellation a predictable checkpoint between records, rather than during an arbitrary mutation.

Qt 4’s event loop is not magic concurrency dust. This is disappointing only until one remembers that magic concurrency dust would probably deadlock before breakfast.

What the Desktop Taught Me This Year

I spent much of this year fixing things that I first tried to solve at the wrong layer. A missing button event sent me into desktop settings. A wandering disk name produced a worse shell script. A driver race invited more logging. My instincts have been very energetic, if not consistently useful.

The better method is to follow the mechanism. Kernel devices appear through the device model and sysfs; udev applies naming policy. Input travels from the kernel through X.Org to applications. Qt objects have explicit parent ownership. Deferred kernel work has context and lifetime rules. Git history is a graph of objects, not a remote folder with ceremony.

KDE 3.5 ties the user-facing side together with unusual steadiness. Its maturity is not one clever feature but the accumulated reliability of applications, libraries, and conventions.

My practical conclusion for the year is to identify the boundary before editing the configuration or code. Ask what layer owns the decision, what evidence crosses that boundary, and what lifetime the data has. Then make one change and test it.

This sounds obvious when written in December. In February it apparently required an oops. Education remains committed to memorable examples.

KDE 3.5 and the Value of Finish

KDE 3.5 has been released, and I upgraded the main workstation after using development builds on another machine. The practical question was not whether the version number justified celebration. It was whether I could move my working session without spending the evening negotiating with it.

My naive plan was to keep every cached file and preference because testing had gone well elsewhere. Experience intervened. I backed up the configuration, upgraded the packages, and started with the existing account, but kept a clean account ready as a control. That made it possible to distinguish release problems from my personal museum of settings.

The existing account worked with only a small amount of cleanup. More importantly, the desktop preserved habits established across the 3.x series. Konsole, Konqueror, the panel, and common dialogs did not demand a new operating philosophy. Applications still felt related through shared shortcuts, appearance, and services.

This is the benefit of finishing a line of software rather than constantly replacing its foundation. KDE 3.5 contains visible improvements, but its strongest feature is accumulated correction. Years of reports have covered odd displays, unusual input setups, stale sessions, broken files, and people who click buttons in an order no designer predicted.

Maturity does not mean absence of bugs. It means the common paths are dependable, failure is more localized, and the model has become familiar enough that diagnosis starts from evidence rather than folklore. When an application misbehaves, I can usually decide whether to inspect its configuration, KDE output, X.Org, or the kernel instead of rebooting the entire stack ceremonially.

For C++ desktop development, the platform also feels coherent. Qt 3.3 and KDE libraries offer established patterns for ownership, events, actions, and user interface conventions. Consistency reduces application code and user surprise at the same time, which is a rare two-for-one bargain.

There is still excess. Some menus and control panels would benefit from deciding which choices matter most. Flexibility is admirable, but every checkbox is also a tiny maintenance promise.

My verdict is that KDE 3.5 demonstrates the value of patient integration. It does not need to astonish me each morning. It needs to restore my terminals, open my files, and stay predictable while I break something much more interesting in kernel code. On that measure, this release is in very good shape.