diff --git a/autotests/integration/x11_client_test.cpp b/autotests/integration/x11_client_test.cpp --- a/autotests/integration/x11_client_test.cpp +++ b/autotests/integration/x11_client_test.cpp @@ -50,6 +50,7 @@ void cleanup(); void testCaptionSimplified(); + void testCaptionNonPrintable(); void testFullscreenLayerWithActiveWaylandWindow(); void testFocusInWithWaylandLastActiveWindow(); void testX11WindowId(); @@ -145,6 +146,58 @@ c.reset(); } +void X11ClientTest::testCaptionNonPrintable() +{ + // this test verifies that non-printable characters in captions get stripped + // but printable non-BMP characters, such as Emoji, are kept + // BUG: 376813 + QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); + QVERIFY(windowAddedSpy.isValid()); + + // create an xcb window + QScopedPointer c(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(c.data())); + const QRect windowGeometry(0, 0, 100, 200); + xcb_window_t w = xcb_generate_id(c.data()); + xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), + windowGeometry.x(), + windowGeometry.y(), + windowGeometry.width(), + windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(hints)); + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); + NETWinInfo winInfo(c.data(), w, rootWindow(), NET::Properties(), NET::Properties2()); + const QByteArray origTitle = QByteArrayLiteral("\bTesting non\302\255printable:\177, emoij:\360\237\230\203, non-characters:\357\277\276"); + const QByteArray expectedTitle = QByteArrayLiteral("Testing nonprintable:, emoij:\360\237\230\203, non-characters:"); + winInfo.setName(origTitle.constData()); + xcb_map_window(c.data(), w); + xcb_flush(c.data()); + + // we should get a client for it + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + QVERIFY(windowCreatedSpy.wait()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->caption() != QString::fromUtf8(origTitle)); + QCOMPARE(client->caption(), QString::fromUtf8(expectedTitle); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); + xcb_destroy_window(c.data(), w); + c.reset(); +} + void X11ClientTest::testFullscreenLayerWithActiveWaylandWindow() { // this test verifies that an X11 fullscreen window does not stay in the active layer diff --git a/client.cpp b/client.cpp --- a/client.cpp +++ b/client.cpp @@ -1463,13 +1463,26 @@ void Client::setCaption(const QString& _s, bool force) { - if (!force && _s == cap_normal) - return; QString s(_s); - for (int i = 0; i < s.length(); ++i) - if (!s[i].isPrint()) - s[i] = QChar(u' '); + for (int i = 0; i < s.length(); ) { + if (!s[i].isPrint()) { + if (QChar(s[i]).isHighSurrogate() && i + 1 < s.length() && QChar(s[i + 1]).isLowSurrogate()) { + const uint uc = QChar::surrogateToUcs4(s[i], s[i + 1]); + if (!QChar::isPrint(uc)) { + s.remove(i, 2); + } else { + i += 2; + } + continue; + } + s.remove(i, 1); + continue; + } + ++i; + } const bool changed = (s != cap_normal); + if (!force && !changed) + return; cap_normal = s; if (!force && !changed) { emit captionChanged();