diff --git a/abstract_client.cpp b/abstract_client.cpp
index 41e75c5503..0ba96360e6 100644
--- a/abstract_client.cpp
+++ b/abstract_client.cpp
@@ -39,6 +39,8 @@ along with this program. If not, see .
#include
+#include
+
#include
#include
@@ -674,7 +676,10 @@ void AbstractClient::setupWindowManagementInterface()
w->setMinimizeable(isMinimizable());
w->setFullscreenable(isFullScreenable());
w->setIcon(icon());
- w->setAppId(QString::fromUtf8(resourceName()));
+ auto updateAppId = [this, w] {
+ w->setAppId(QString::fromUtf8(m_desktopFileName.isEmpty() ? resourceName() : m_desktopFileName));
+ };
+ updateAppId();
w->setSkipTaskbar(skipTaskbar());
w->setShadeable(isShadeable());
w->setShaded(isShade());
@@ -716,11 +721,8 @@ void AbstractClient::setupWindowManagementInterface()
w->setIcon(icon());
}
);
- connect(this, &AbstractClient::windowClassChanged, w,
- [w, this] {
- w->setAppId(QString::fromUtf8(resourceName()));
- }
- );
+ connect(this, &AbstractClient::windowClassChanged, w, updateAppId);
+ connect(this, &AbstractClient::desktopFileNameChanged, w, updateAppId);
connect(this, &AbstractClient::shadeChanged, w, [w, this] { w->setShaded(isShade()); });
connect(this, &AbstractClient::transientChanged, w,
[w, this] {
@@ -1639,4 +1641,26 @@ bool AbstractClient::dockWantsInput() const
return false;
}
+void AbstractClient::setDesktopFileName(const QByteArray &name)
+{
+ if (name == m_desktopFileName) {
+ return;
+ }
+ m_desktopFileName = name;
+ emit desktopFileNameChanged();
+}
+
+QString AbstractClient::iconFromDesktopFile() const
+{
+ if (m_desktopFileName.isEmpty()) {
+ return QString();
+ }
+ QString desktopFile = QString::fromUtf8(m_desktopFileName);
+ if (!desktopFile.endsWith(QLatin1String(".desktop"))) {
+ desktopFile.append(QLatin1String(".desktop"));
+ }
+ KDesktopFile df(desktopFile);
+ return df.readIcon();
+}
+
}
diff --git a/abstract_client.h b/abstract_client.h
index b78f84ca61..058571e5cc 100644
--- a/abstract_client.h
+++ b/abstract_client.h
@@ -244,6 +244,17 @@ class KWIN_EXPORT AbstractClient : public Toplevel
* Because of that there is no notify signal.
**/
Q_PROPERTY(bool resizeable READ isResizable)
+
+ /**
+ * The desktop file name of the application this AbstractClient belongs to.
+ *
+ * This is either the base name without full path and without file extension of the
+ * desktop file for the window's application (e.g. "org.kde.foo").
+ *
+ * The application's desktop file name can also be the full path to the desktop file
+ * (e.g. "/opt/kde/share/org.kde.foo.desktop") in case it's not in a standard location.
+ **/
+ Q_PROPERTY(QByteArray desktopFileName READ desktopFileName NOTIFY desktopFileNameChanged)
public:
virtual ~AbstractClient();
@@ -599,6 +610,10 @@ public:
**/
virtual void showOnScreenEdge() = 0;
+ QByteArray desktopFileName() const {
+ return m_desktopFileName;
+ }
+
// TODO: remove boolean trap
static bool belongToSameApplication(const AbstractClient* c1, const AbstractClient* c2, bool active_hack = false);
@@ -640,6 +655,7 @@ Q_SIGNALS:
void minimizeableChanged(bool);
void shadeableChanged(bool);
void maximizeableChanged(bool);
+ void desktopFileNameChanged();
protected:
AbstractClient();
@@ -918,6 +934,9 @@ protected:
void startDecorationDoubleClickTimer();
void invalidateDecorationDoubleClickTimer();
+ void setDesktopFileName(const QByteArray &name);
+ QString iconFromDesktopFile() const;
+
private:
void handlePaletteChange();
QSharedPointer m_tabBoxClient;
@@ -985,6 +1004,8 @@ private:
QElapsedTimer doubleClickTimer;
} m_decoration;
+ QByteArray m_desktopFileName;
+
static bool s_haveResizeEffect;
};
diff --git a/autotests/integration/data/example.desktop b/autotests/integration/data/example.desktop
new file mode 100644
index 0000000000..739f5d3087
--- /dev/null
+++ b/autotests/integration/data/example.desktop
@@ -0,0 +1,3 @@
+[Desktop Entry]
+Name=An example application
+Icon=kwin
diff --git a/autotests/integration/shell_client_test.cpp b/autotests/integration/shell_client_test.cpp
index 28b65f80a8..45c9ddc880 100644
--- a/autotests/integration/shell_client_test.cpp
+++ b/autotests/integration/shell_client_test.cpp
@@ -62,6 +62,7 @@ private Q_SLOTS:
void testWindowOpensLargerThanScreen();
void testHidden_data();
void testHidden();
+ void testDesktopFileName();
};
void TestShellClient::initTestCase()
@@ -583,5 +584,37 @@ void TestShellClient::testHidden()
//QCOMPARE(workspace()->activeClient(), c);
}
+void TestShellClient::testDesktopFileName()
+{
+ // this test verifies that desktop file name is passed correctly to the window
+ QScopedPointer surface(Test::createSurface());
+ // only xdg-shell as ShellSurface misses the setter
+ QScopedPointer shellSurface(qobject_cast(Test::createShellSurface(Test::ShellSurfaceType::XdgShellV5, surface.data())));
+ shellSurface->setAppId(QByteArrayLiteral("org.kde.foo"));
+ auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue);
+ QVERIFY(c);
+ QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.foo"));
+ // the desktop file does not exist, so icon should be generic Wayland
+ QCOMPARE(c->icon().name(), QStringLiteral("wayland"));
+
+ QSignalSpy desktopFileNameChangedSpy(c, &AbstractClient::desktopFileNameChanged);
+ QVERIFY(desktopFileNameChangedSpy.isValid());
+ QSignalSpy iconChangedSpy(c, &ShellClient::iconChanged);
+ QVERIFY(iconChangedSpy.isValid());
+ shellSurface->setAppId(QByteArrayLiteral("org.kde.bar"));
+ QVERIFY(desktopFileNameChangedSpy.wait());
+ QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.bar"));
+ // icon should still be wayland
+ QCOMPARE(c->icon().name(), QStringLiteral("wayland"));
+ QVERIFY(iconChangedSpy.isEmpty());
+
+ const QString dfPath = QFINDTESTDATA("data/example.desktop");
+ shellSurface->setAppId(dfPath.toUtf8());
+ QVERIFY(desktopFileNameChangedSpy.wait());
+ QCOMPARE(iconChangedSpy.count(), 1);
+ QCOMPARE(QString::fromUtf8(c->desktopFileName()), dfPath);
+ QCOMPARE(c->icon().name(), QStringLiteral("kwin"));
+}
+
WAYLANDTEST_MAIN(TestShellClient)
#include "shell_client_test.moc"
diff --git a/client.cpp b/client.cpp
index 8ae4d9e172..4cdfbe4a03 100644
--- a/client.cpp
+++ b/client.cpp
@@ -1683,6 +1683,11 @@ void Client::getMotifHints()
void Client::getIcons()
{
// First read icons from the window itself
+ const QString themedIconName = iconFromDesktopFile();
+ if (!themedIconName.isEmpty()) {
+ setIcon(QIcon::fromTheme(themedIconName));
+ return;
+ }
QIcon icon;
auto readIcon = [this, &icon](int size, bool scale = true) {
const QPixmap pix = KWindowSystem::icon(window(), size, size, scale, KWindowSystem::NETWM | KWindowSystem::WMHints, info);
diff --git a/events.cpp b/events.cpp
index 4976369eca..d8da5a7c0a 100644
--- a/events.cpp
+++ b/events.cpp
@@ -651,6 +651,9 @@ bool Client::windowEvent(xcb_generic_event_t *e)
if (dirtyProperties2 & NET::WM2OpaqueRegion) {
getWmOpaqueRegion();
}
+ if (dirtyProperties2 & NET::WM2DesktopFileName) {
+ setDesktopFileName(QByteArray(info->desktopFileName()));
+ }
}
const uint8_t eventType = e->response_type & ~0x80;
diff --git a/manage.cpp b/manage.cpp
index 67abd95c79..bf372a4d75 100644
--- a/manage.cpp
+++ b/manage.cpp
@@ -93,7 +93,8 @@ bool Client::manage(xcb_window_t w, bool isMapped)
NET::WM2Protocols |
NET::WM2InitialMappingState |
NET::WM2IconPixmap |
- NET::WM2OpaqueRegion;
+ NET::WM2OpaqueRegion |
+ NET::WM2DesktopFileName;
auto wmClientLeaderCookie = fetchWmClientLeader();
auto skipCloseAnimationCookie = fetchSkipCloseAnimation();
@@ -142,7 +143,10 @@ bool Client::manage(xcb_window_t w, bool isMapped)
setModal((info->state() & NET::Modal) != 0); // Needs to be valid before handling groups
readTransientProperty(transientCookie);
+ setDesktopFileName(QByteArray(info->desktopFileName()));
getIcons();
+ connect(this, &Client::desktopFileNameChanged, this, &Client::getIcons);
+
m_geometryHints.read();
getMotifHints();
getWmOpaqueRegion();
diff --git a/shell_client.cpp b/shell_client.cpp
index dbf0b4da55..5edf298c24 100644
--- a/shell_client.cpp
+++ b/shell_client.cpp
@@ -108,12 +108,13 @@ void ShellClient::initSurface(T *shellSurface)
performMouseCommand(Options::MouseMove, Cursor::pos());
}
);
- connect(shellSurface, &T::windowClassChanged, this, &ShellClient::updateIcon);
setResourceClass(shellSurface->windowClass());
+ setDesktopFileName(shellSurface->windowClass());
connect(shellSurface, &T::windowClassChanged, this,
[this] (const QByteArray &windowClass) {
setResourceClass(windowClass);
+ setDesktopFileName(windowClass);
}
);
connect(shellSurface, &T::resizeRequested, this,
@@ -168,6 +169,7 @@ void ShellClient::initSurface(T *shellSurface)
void ShellClient::init()
{
+ connect(this, &ShellClient::desktopFileNameChanged, this, &ShellClient::updateIcon);
findInternalWindow();
createWindowId();
setupCompositing();
@@ -233,7 +235,6 @@ void ShellClient::init()
} else if (m_xdgShellPopup) {
connect(m_xdgShellPopup, &XdgShellPopupInterface::destroyed, this, &ShellClient::destroyClient);
}
- updateIcon();
// setup shadow integration
getShadow();
@@ -1184,19 +1185,12 @@ bool ShellClient::hasStrut() const
void ShellClient::updateIcon()
{
const QString waylandIconName = QStringLiteral("wayland");
- QString desktopFile;
- if (m_shellSurface) {
- desktopFile = QString::fromUtf8(m_shellSurface->windowClass());
+ const QString dfIconName = iconFromDesktopFile();
+ const QString iconName = dfIconName.isEmpty() ? waylandIconName : dfIconName;
+ if (iconName == icon().name()) {
+ return;
}
- if (desktopFile.isEmpty()) {
- setIcon(QIcon::fromTheme(waylandIconName));
- }
- if (!desktopFile.endsWith(QLatin1String(".desktop"))) {
- desktopFile.append(QLatin1String(".desktop"));
- }
- KDesktopFile df(desktopFile);
- const QString iconName = df.readIcon();
- setIcon(QIcon::fromTheme(iconName.isEmpty() ? waylandIconName : iconName));
+ setIcon(QIcon::fromTheme(iconName));
}
bool ShellClient::isTransient() const