Aurélien Gâteau

Fun with event loops and QObject::deleteLater()

written on Wednesday, September 29, 2010

Today I was tracking the reason behind LP bug #632419 and his upstream brother KDE bug #242637: when the Message Indicator Plasma widget is inactive, its text is painted over the other items in the list:

Screenshot of KDE bug #242637
Ugly, isn't it?

After much digging I realized that the list of passive widgets is a two-column grid, the first column contains the widget itself, the second column contains a label to display the widget name. What was happening is that for some reason, the label would not be deleted when the widget switched from "Passive" (aka "show me in the passive list") to "Active" (aka "show me in the system tray"). This caused an accumulation of labels over time, since a new label was created every time the widget switched to "Passive" mode, but never deleted when the widget switched back to "Active" mode.

Oddly enough, this was only happening with the Message Indicator widget. The code to delete the label looked correct (kdebase/workspace/plasma/generic/applets/systemtray/ui/taskarea.cpp, method: TaskArea::addWidgetToTask()):

    if (task->hidden() == Task::NotHidden) {
        if (d->hiddenTasks.contains(task)) {
            widget->setParentItem(this);
            for (int i = 0; i < d->hiddenTasksLayout->count(); ++i) {
                if (d->hiddenTasksLayout->itemAt(i) == d->hiddenTasks.value(task)) {
                    d->hiddenTasksLayout->removeAt(i);
                    break;
                }
            }
            disconnect(task, 0, d->hiddenTasks.value(task), 0);
            d->hiddenTasks.value(task)->deleteLater();
            d->hiddenTasks.remove(task);
            d->hiddenRelayoutTimer->start(250);
        }
    }

The label is deleted in line 11 using QObject::deleteLater(). I added a kDebug() line to the destructor of the label: the label was not being deleted... I then replaced the deleteLater() with a simple "delete" and... it worked! The destructor was called and there was no more ugly text overlap.

I was quite happy to have fixed the bug, but a question remained: Why did deleteLater() fail to call the destructor?

Then I had an epiphany... deleteLater() documentation says the following:

Note that entering and leaving a new event loop (e.g., by opening a modal dialog) will not perform the deferred deletion; for the object to be deleted, the control must return to the event loop from which deleteLater() was called.

In my case, the call to deleteLater() was a result of a status change in the Message Indicator widget, which itself happens when libindicate-qt emits Qt signals to notify of changes in Message-Indicator-enabled applications. libindicate-qt knows about these changes because it is a wrapper around libindicate, a dbus-glib based library... which runs its own event loop... It turned out my signals were emitted from within libindicate event loop, which probably does not know what to do with a deferred delete event.

Having realized that, I was able to apply a proper fix: emit libindicate-qt signals using Qt::QueuedConnection, this way slots connected to these signals are not called from within libindicate event loop. After applying this change and reverting the system tray change, it was still working. That's a much nicer fix, which I will push tomorrow.

This post was tagged kde, message indicator, qobject::deletelater and tips