June 4, 2023 by Reion

How KDE Plasma Badge Works

I logged into my telegram in KDE Plasma today, and found that the two are integrated so well, just like the title of this article says “Badge”, please see the screenshot bwlow:

|inline|border

|inline|border

When you read some messages, telegram will request to update the number of badges in the taskbar icon:

|inline|border

This all looks perfect and got my attention, so how does it work?

Let’s first determine what D-Bus(the Linux desktop environment is all D-Bus communication) interface to transmit data, and the dbus-monitor tool can be used to monitor the transmitted content.

signal time=1685861701.302893 sender=:1.174 -> destination=(null destination) serial=353 path=/com/canonical/unity/launcherentry/2857096580; interface=com.canonical.Unity.LauncherEntry; member=Update
   string "application://telegramdesktop.desktop"
   array [
      dict entry(
         string "count"
         variant             int64 1498
      )
      dict entry(
         string "count-visible"
         variant             boolean true
      )
   ]

Yes I found out what they transmit with dbus-monitor, using the LauncherAPI defined by Ubuntu, which is com.canonical.Unity.LauncherEntry, here is the official document: https://wiki.ubuntu.com/Unity/LauncherAPI

Regarding the interface specification defined by Ubuntu, I still have some impressions, which is mentioned by @probonopd, which can be found in this: https://github.com/cyberos/cyber-dock/issues/7, haha.

plasma-desktop

Let’s get back to the topic, the implementation of Plasma is in the taskmanager module and there is a C++ class called SmartLauncherBackend.

This class calls the setupUnity() function when it is constructed, which is no doubt used to receive Update() messages from com.com.canonical.Unity.LauncherEntry.

void Backend::setupUnity()
{
    auto sessionBus = QDBusConnection::sessionBus();

    if (!sessionBus.connect({},
                            {},
                            QStringLiteral("com.canonical.Unity.LauncherEntry"),
                            QStringLiteral("Update"),
                            this,
                            SLOT(update(QString, QMap<QString, QVariant>)))) {
        qWarning() << "failed to register Update signal";
        return;
    }

    if (!sessionBus.registerObject(QStringLiteral("/Unity"), this)) {
        qWarning() << "Failed to register unity object";
        return;
    }

    if (!sessionBus.registerService(QStringLiteral("com.canonical.Unity"))) {
        qWarning() << "Failed to register unity service";
        // In case an external process uses this (e.g. Latte Dock), let it just listen.
    }

    ......
}

After receiving the Update signal, it’s hard not to get it, the next thing it does is to find the icon item on the taskbar through app_uri, and then update the UI.

For more details, you can view the code, the code location is plasma-desktop/applets/taskmanager/plugin/smartlaunchers /smartlauncherbackend.cpp.

How does Telegram send?

void MainWindow::updateUnityCounter() {
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
	qApp->setBadgeNumber(Core::App().unreadBadge());
#else // Qt >= 6.6.0
	static const auto djbStringHash = [](const std::string &string) {
		uint hash = 5381;
		for (const auto &curChar : string) {
			hash = (hash << 5) + hash + curChar;
		}
		return hash;
	};

	const auto launcherUrl = Glib::ustring(
		"application://"
			+ QGuiApplication::desktopFileName().toStdString());
	const auto counterSlice = std::min(Core::App().unreadBadge(), 9999);
	std::map<Glib::ustring, Glib::VariantBase> dbusUnityProperties;

	if (counterSlice > 0) {
		// According to the spec, it should be of 'x' D-Bus signature,
		// which corresponds to signed 64-bit integer
		// https://wiki.ubuntu.com/Unity/LauncherAPI#Low_level_DBus_API:_com.canonical.Unity.LauncherEntry
		dbusUnityProperties["count"] = Glib::Variant<int64>::create(
			counterSlice);
		dbusUnityProperties["count-visible"] =
			Glib::Variant<bool>::create(true);
	} else {
		dbusUnityProperties["count-visible"] =
			Glib::Variant<bool>::create(false);
	}

	try {
		const auto connection = Gio::DBus::Connection::get_sync(
			Gio::DBus::BusType::SESSION);

		connection->emit_signal(
			"/com/canonical/unity/launcherentry/"
				+ std::to_string(djbStringHash(launcherUrl)),
			"com.canonical.Unity.LauncherEntry",
			"Update",
			{},
			base::Platform::MakeGlibVariant(std::tuple{
				launcherUrl,
				dbusUnityProperties,
			}));
	} catch (...) {
	}
#endif // Qt < 6.6.0
}

We can see that Telegram did it in MainWindow::updateUnityCounter(). It seems easy, and the interesting thing is that Qt also supports this feature on version 6.5 or higher.

Next, let’s look at how this is implemented in Qt.

Qt

According to the official document, Qt added the setBadgeNumber method in 6.5, which can facilitate the application to set the number of badge.

/*!
    Sets the application's badge to \a number.

    Useful for providing feedback to the user about the number
    of unread messages or similar.

    The badge will be overlaid on the application's icon in the Dock
    on \macos, the home screen icon on iOS, or the task bar on Windows
    and Linux.

    If the number is outside the range supported by the platform, the
    number will be clamped to the supported range. If the number does
    not fit within the badge, the number may be visually elided.

    Setting the number to 0 will clear the badge.

    \since 6.5
    \sa applicationName
*/
void QGuiApplication::setBadgeNumber(qint64 number)
{
    QGuiApplicationPrivate::platformIntegration()->setApplicationBadge(number);
}

Qt has different implementations on different platforms, it makes good use of abstract concepts, and in Linux it is implemented in QGenericUnixServices::setApplicationBadge()

void QGenericUnixServices::setApplicationBadge(qint64 number)
{
#if QT_CONFIG(dbus)
    if (qGuiApp->desktopFileName().isEmpty()) {
        qWarning("QGuiApplication::desktopFileName() is empty");
        return;
    }


    const QString launcherUrl = QStringLiteral("application://") + qGuiApp->desktopFileName() + QStringLiteral(".desktop");
    const qint64 count = qBound(0, number, 9999);
    QVariantMap dbusUnityProperties;

    if (count > 0) {
        dbusUnityProperties[QStringLiteral("count")] = count;
        dbusUnityProperties[QStringLiteral("count-visible")] = true;
    } else {
        dbusUnityProperties[QStringLiteral("count-visible")] = false;
    }

    auto signal = QDBusMessage::createSignal(QStringLiteral("/com/canonical/unity/launcherentry/")
        + qGuiApp->applicationName(), QStringLiteral("com.canonical.Unity.LauncherEntry"), QStringLiteral("Update"));

    signal.setArguments({launcherUrl, dbusUnityProperties});

    QDBusConnection::sessionBus().send(signal);
#else
    Q_UNUSED(number)
#endif
}

It’s all unified.

Chromium & Electron

Chromium code

Electron

Unity Badge Count implementation: https://github.com/electron/electron/issues/16001