Blog

A Panel Made of Rectangles

I spent the evening moving three coloured rectangles around a blank window. This sounds less impressive than “working on the KDE 4 desktop,” which is precisely why it is a more accurate description.

The prototype is testing a Plasma idea: applets live in a containment, and the containment decides how available space is assigned. The applet should express useful size hints rather than assume that the panel is horizontal, 32 pixels high, and forever attached to the bottom edge.

My first attempt stored absolute geometry. Rotating the panel exposed the mistake immediately. The second stores constraints and lets the containment lay things out:

QSize Applet::minimumSize() const
{
    return QSize(24, 24);
}

QSize Applet::preferredSize() const
{
    return QSize(96, 32);
}

The prototype then reruns layout when its own size or form changes. Nothing here proves the final API. It proves that geometry policy has to live somewhere other than each applet’s collection of special cases.

Painting is the second experiment. Keeping content separate from the surrounding frame means a theme can alter margins and background without every applet learning the new decoration. That also means the layout must account for frame insets before assigning content space. I forgot this and produced a clock whose final digit lived just outside civilisation.

I prefer small visual prototypes with deliberately fake data at this stage. They make resizing, invalidation, and composition bugs obvious before a real calendar or device monitor introduces its own failures. The caveat is that rectangles do not exercise accessibility, input, localisation, or expensive content. A prototype can validate one mechanism while quietly avoiding four others.

I am saving each experiment as a tiny runnable case. When the graphics or layout API changes, these cases answer whether the concept still works without requiring a complete desktop session to survive long enough for the test.

There is no finished Plasma desktop hiding behind these rectangles. There are unresolved questions about applet lifecycle, configuration, persistence, and rendering. The useful result tonight is narrower: containment-driven layout survives changing orientation better than hard-coded applet geometry.

Tomorrow I may replace one rectangle with a clock. This will increase the prototype’s accuracy by one time zone and its bug count by an unknown but probably larger number.

Converting One CMake Target at a Time

Today’s CMake conversion failed at the final link with an undefined reference from a file I had not changed. Naturally, I first blamed the linker. Linkers enjoy this arrangement because they never have to attend the meeting.

The actual cause was dependency order. The old build inherited libraries from a parent directory, so this target had never declared what it used. Moving it exposed the omission.

I started recording requirements next to the target:

set(indexer_SRCS
    indexer.cpp
    document.cpp
    tokenizer.cpp
)

kde4_add_executable(indexer ${indexer_SRCS})
target_link_libraries(indexer
    ${KDE4_KDECORE_LIBS}
    searchcore
)

Then I configured from an empty build directory. Reusing yesterday’s cache can conceal a missing check or preserve a path that no clean machine has. During conversion, this sequence is worth the extra minute:

rm -rf build
mkdir build
cd build
cmake -DCMAKE_BUILD_TYPE=debugfull ..
make VERBOSE=1

I am not recommending casual rm -rf; I am recommending a disposable directory whose purpose is printed on the tin. Source remains elsewhere.

Generated files need similar care. If a header is produced during the build, its output belongs in the binary tree and the target must depend on the generation step. Writing generated headers into the source tree makes an apparently clean checkout depend on debris from an earlier build. It also makes parallel builds unusually creative.

I prefer explicit source lists over collecting every *.cpp automatically. Adding a file should change the build description, which gives review and version control a chance to notice it. The caveat is maintenance: source lists can drift if developers forget them, and globbing can be reasonable for generated collections. For hand-written application code, explicit still wins for me.

I am applying the same rule to include directories. A target should receive only paths it needs; a global include path can let one directory compile by accident against another directory’s private header.

The best conversion checkpoint is not “CMake completed.” It is a clean configure, complete build, and one small executable run from the binary tree. Configuration only proves that CMake understood my instructions, not that my instructions were wise.

One target now builds without inherited flags. Nine remain. This is slower to describe than a bulk conversion and faster to debug than one.

HAL Is a Source, Not a Policy

I plugged in a USB disk this afternoon and my test application offered to open it twice. Unplugging it removed only one entry. This is an efficient way to make hardware discovery look supernatural.

The bug was mundane: startup enumeration and hotplug notification both created an object, while neither path checked whether the same device identifier was already present. HAL supplies facts about devices and changes, but it does not decide how my application should represent them.

I now funnel both paths through one function:

void DeviceList::addDevice(const QString &udi)
{
    if (devices.contains(udi))
        return;

    Device device = backend->device(udi);
    if (!device.isValid())
        return;

    devices.insert(udi, device);
    emit deviceAdded(udi);
}

Removal uses the same identifier and emits only after the internal state is consistent. That ordering lets slots query the list without observing a ghost.

The larger KDE 4 question is where HAL ends and a hardware abstraction such as Solid begins. I do not want every application parsing HAL properties or knowing which backend discovered a battery. Applications should ask stable questions: is this a storage volume, can it be mounted, is this network interface active? Solid can translate those questions to the platform mechanism.

I prefer keeping the backend thin and putting application policy above Solid. A media player may automatically inspect an audio disc; a file manager may offer actions; a backup tool may ignore both and look only for a labelled volume. The same hardware event should not imply the same decision.

For testing, the backend now accepts a recorded sequence of add and remove events. Replaying “add, add, remove” verifies the duplicate guard without requiring me to develop exceptional timing with a USB plug. It also makes event ordering assumptions visible in a small test rather than in a user’s device list.

The caveat is that abstraction can hide information we have not yet learned to model. During early porting I am logging the raw UDI and selected properties beside the abstract device type. When a device is classified incorrectly, that evidence is valuable. I would remove such noise from a normal release, assuming we eventually have one that does not greet a single disk as twins.

Model/View Without the Fog

I triggered a crash today by deleting the third row of a list. The list had only two visible rows, which was the first clue that my idea of the data and the view’s idea had stopped speaking to each other.

The old code filled a widget item by item and kept pointers to those items in the document. Qt 4’s model/view classes ask for a cleaner bargain: the model owns the shape and meaning of the data, while views ask questions through QModelIndex. That index is a coordinate plus model identity, not the object itself.

For a flat list, the smallest useful model is not large:

class ContactModel : public QAbstractListModel
{
public:
    ContactModel(QObject *parent = 0)
        : QAbstractListModel(parent)
    {
    }

    int rowCount(const QModelIndex &parent = QModelIndex()) const
    {
        return parent.isValid() ? 0 : contacts.count();
    }

    QVariant data(const QModelIndex &index, int role) const
    {
        if (!index.isValid() || index.model() != this ||
            index.row() < 0 || index.row() >= contacts.count() ||
            index.column() != 0)
            return QVariant();

        if (role == Qt::DisplayRole)
            return contacts.at(index.row()).name;

        return QVariant();
    }

private:
    QList<Contact> contacts;
};

The parent check is important even for a list. A valid parent asks for children beneath an item; this model has none. Returning the full count there can make a view believe it has discovered a tree of infinite enthusiasm.

The more interesting part is changing data. The view cannot infer that a row appeared because a QList grew. The model must announce structural changes around the actual mutation:

void ContactModel::append(const Contact &contact)
{
    int row = contacts.count();
    beginInsertRows(QModelIndex(), row, row);
    contacts.append(contact);
    endInsertRows();
}

Those calls let attached views adjust selections, editors, and cached geometry coherently. For an ordinary value change, I modify the value and emit dataChanged(index, index) instead. Resetting the whole model after every edit appears easier, but it discards selection and makes modest updates expensive.

My crash came from removal code that changed the collection first and emitted signals afterwards. During that gap, a view requested an index using the old shape against the new data. Correct order is not decorative:

void ContactModel::remove(int row)
{
    if (row < 0 || row >= contacts.count())
        return;

    beginRemoveRows(QModelIndex(), row, row);
    contacts.removeAt(row);
    endRemoveRows();
}

Selections add another wrinkle. A normal QModelIndex is intended for immediate use. If I must retain an index while rows can move, I use QPersistentModelIndex, and even then I ask whether storing a domain identifier would be clearer. Persistent indexes survive model notifications, not arbitrary lies told by the model.

Sorting and filtering belong in a proxy when possible. QSortFilterProxyModel sits between source and view, translating indexes in both directions. That means code receiving a selected proxy index must map it before addressing source data:

QModelIndex sourceIndex = proxy->mapToSource(view->currentIndex());
Contact contact = model->contactAt(sourceIndex.row());

Forgetting this works beautifully until sorting changes the order. These are the bugs that wait politely through a demonstration and introduce themselves to the first user.

I prefer models that expose domain data through roles and operations, with widgets knowing as little as possible about storage. One model can then drive a list, a combo box, and perhaps a custom view without three synchronisation routines. The caveat is that model/view has real ceremony. A fixed list of five labels does not need a heroic subclass; QStringListModel may be enough.

Editable models need one more honest contract. flags() must advertise editable indexes, and setData() must validate the role and value before changing storage. Returning true means the edit happened, not merely that the method received a pleasant visit:

bool ContactModel::setData(const QModelIndex &index,
                           const QVariant &value, int role)
{
    if (role != Qt::EditRole || !index.isValid() ||
        index.model() != this || index.row() < 0 ||
        index.row() >= contacts.count() || index.column() != 0)
        return false;

    contacts[index.row()].name = value.toString();
    emit dataChanged(index, index);
    return true;
}

The model and bounds checks matter here too. An index from another model is not permission to address this model’s storage, even when its row happens to fit. The delegate asks the model to commit; it does not gain ownership of the underlying record.

There is also a testing advantage. I can exercise rowCount, data, insertion, and removal with QCoreApplication, no screen required. My current test records the emitted row ranges and checks the resulting names. It is not glamorous, but neither is debugging a stale selection at midnight.

Qt 4’s mechanism got less foggy when I stopped treating the model as a collection adapter. The view trusts its signals and invariants. Once I stopped lying to it, the disappearing third row quit crashing, though it remains philosophically ambitious.

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.