From e753c5cc80d002bb643c0a93f58d018269702944 Mon Sep 17 00:00:00 2001
From: Gatis Paeglis <gatis.paeglis@qt.io>
Date: Wed, 7 Nov 2018 14:49:37 +0100
Subject: [PATCH] xcb: implement missing bits from ICCCM 4.1.4 WM_STATE
 handling

ICCCM 4.1.4 says:

Clients that want to re-use a client window (e.g. by mapping it again) after
withdrawing it must wait for the withdrawal to be complete before proceeding.
The preferred method for doing this is for clients to wait for a windown manager
to update or remove the WM_STATE property.

This patch implements the required logic. Qt 4 had something similar. Without
this patch we are calling various setter functions on a native window while it
is in an undefined state.

Fixes: QTBUG-69515
Change-Id: I377a66ad3d5e43f14465d0ea670b2f43f96ed7d3
---
 src/plugins/platforms/xcb/qxcbwindow.cpp | 116 +++++++++++++++++++++++++++----
 src/plugins/platforms/xcb/qxcbwindow.h   |  15 ++++
 2 files changed, 116 insertions(+), 15 deletions(-)

diff --git a/src/plugins/platforms/xcb/qxcbwindow.cpp b/src/plugins/platforms/xcb/qxcbwindow.cpp
index 76d3545d352..1ae8cfea810 100644
--- a/src/plugins/platforms/xcb/qxcbwindow.cpp
+++ b/src/plugins/platforms/xcb/qxcbwindow.cpp
@@ -585,6 +585,10 @@
 
     propagateSizeHints();
 
+	
+    if (shouldDeferTask(Task::SetGeometry))
+        return;
+
     QXcbScreen *currentScreen = xcbScreen();
     QXcbScreen *newScreen = parent() ? parentScreen() : static_cast<QXcbScreen*>(screenForGeometry(rect));
 
@@ -707,6 +711,9 @@
 
 void QXcbWindow::show()
 {
+    if (shouldDeferTask(Task::Map))	
+        return;
+
     if (window()->isTopLevel()) {
 
         // update WM_NORMAL_HINTS
@@ -757,6 +764,9 @@
 
 void QXcbWindow::hide()
 {
+    if (shouldDeferTask(Task::Unmap))
+        return;
+
     xcb_unmap_window(xcb_connection(), m_window);
 
     // send synthetic UnmapNotify event according to icccm 4.1.4
@@ -942,6 +952,9 @@
 
 void QXcbWindow::setWindowFlags(Qt::WindowFlags flags)
 {
+    if (shouldDeferTask(Task::SetWindowFlags))
+        return;
+
     Qt::WindowType type = static_cast<Qt::WindowType>(int(flags & Qt::WindowType_Mask));
 
     if (type == Qt::ToolTip)
@@ -971,6 +984,8 @@
 
     setTransparentForMouseEvents(flags & Qt::WindowTransparentForInput);
     updateDoesNotAcceptFocus(flags & Qt::WindowDoesNotAcceptFocus);
+
+    m_isWmManagedWindow = !(flags & Qt::X11BypassWindowManagerHint);
 }
 
 void QXcbWindow::setMotifWmHints(Qt::WindowFlags flags)
@@ -1163,6 +1178,9 @@
 void QXcbWindow::setWindowState(Qt::WindowStates state)
 {
     if (state == m_windowState)
+        return;
+
+    if (shouldDeferTask(Task::SetWindowState))	
         return;
 
     if ((m_windowState & Qt::WindowMinimized) && !(state & Qt::WindowMinimized)) {
@@ -1888,6 +1906,10 @@
     if (event->window == m_window) {
         m_mapped = false;
         QWindowSystemInterface::handleExposeEvent(window(), QRegion());
+        if (!m_isWmManagedWindow) {
+            m_wmStateValid = true;
+            handleDeferredTasks();
+        }
     }
 }
 
@@ -2213,32 +2235,97 @@
     handleLeaveNotifyEvent(event->root_x, event->root_y, event->mode, event->detail, event->time);
 }
 
+bool QXcbWindow::shouldDeferTask(Task task)	
+{
+    if (m_wmStateValid)
+        return false;
+
+    m_deferredTasks.append(task);
+    return true;
+}
+		
+void QXcbWindow::handleDeferredTasks()	
+{
+    Q_ASSERT(m_wmStateValid == true);
+    if (m_deferredTasks.isEmpty())
+        return;
+	
+    bool map = false;
+    bool unmap = false;
+	
+    QVector<Task> tasks;
+    for (auto taskIt = m_deferredTasks.rbegin(); taskIt != m_deferredTasks.rend(); ++taskIt) {
+        if (!tasks.contains(*taskIt))
+            tasks.prepend(*taskIt);
+    }
+	
+    for (Task task : tasks) {
+        switch (task) {
+        case Task::Map:
+            map = true;
+            unmap = false;
+            break;
+        case Task::Unmap:
+            unmap = true;
+            map = false;
+            break;
+        case Task::SetGeometry:
+            setGeometry(geometry());
+            break;
+        case Task::SetWindowFlags:
+            setWindowFlags(window()->flags());
+            break;
+        case Task::SetWindowState:
+            setWindowState(window()->windowState());
+            break;
+        }
+    }
+    m_deferredTasks.clear();
+	
+    if (map) {
+        Q_ASSERT(unmap == false);
+        show();
+    }
+    if (unmap) {
+        Q_ASSERT(map == false);
+        hide();
+    }
+}
+
 void QXcbWindow::handlePropertyNotifyEvent(const xcb_property_notify_event_t *event)
 {
     connection()->setTime(event->time);
 
-    const bool propertyDeleted = event->state == XCB_PROPERTY_DELETE;
-
-    if (event->atom == atom(QXcbAtom::_NET_WM_STATE) || event->atom == atom(QXcbAtom::WM_STATE)) {
-        if (propertyDeleted)
+    const bool wmStateChanged = event->atom == atom(QXcbAtom::WM_STATE);
+    const bool netWmStateChanged = event->atom == atom(QXcbAtom::_NET_WM_STATE);
+    if (netWmStateChanged || wmStateChanged) {
+        if (wmStateChanged && !m_wmStateValid) {
+            // ICCCM 4.1.4
+            // Clients that want to re-use a client window (e.g. by mapping it again)
+            // after withdrawing it must wait for the withdrawal to be complete before
+            // proceeding. The preferred method for doing this is for clients to wait for
+            // a window manager to update or remove the WM_STATE property.
+            m_wmStateValid = true;
+            handleDeferredTasks();
+        }
+        if (event->state == XCB_PROPERTY_DELETE)
             return;
 
-        Qt::WindowStates newState = Qt::WindowNoState;
-
-        if (event->atom == atom(QXcbAtom::WM_STATE)) { // WM_STATE: Quick check for 'Minimize'.
+        if (wmStateChanged) {
             auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(),
                                      0, m_window, atom(QXcbAtom::WM_STATE),
                                      XCB_ATOM_ANY, 0, 1024);
             if (reply && reply->format == 32 && reply->type == atom(QXcbAtom::WM_STATE)) {
-                const quint32 *data = (const quint32 *)xcb_get_property_value(reply.get());
-                if (reply->length != 0)
-                    m_minimized = (data[0] == XCB_ICCCM_WM_STATE_ICONIC
-                                   || (data[0] == XCB_ICCCM_WM_STATE_WITHDRAWN && m_minimized));
+                auto data = static_cast<const quint32 *>(xcb_get_property_value(reply.get()));
+                if (reply->length != 0) {
+                    const bool changedToWithdrawn = data[0] == XCB_ICCCM_WM_STATE_WITHDRAWN;
+                    const bool changedToIconic = data[0] == XCB_ICCCM_WM_STATE_ICONIC;
+                    m_minimized = changedToIconic || (changedToWithdrawn && m_minimized);
+                }
             }
         }
-        if (m_minimized)
-            newState = Qt::WindowMinimized;
-
+        // _NET_WM_STATE handling
+        Qt::WindowStates newState = m_minimized ? Qt::WindowMinimized : Qt::WindowNoState;
         const NetWmStates states = netWmStates();
         if (states & NetWmStateFullScreen)
             newState |= Qt::WindowFullScreen;
@@ -2252,7 +2339,7 @@
             if ((m_windowState & Qt::WindowMinimized) && connection()->mouseGrabber() == this)
                 connection()->setMouseGrabber(nullptr);
         }
-        return;
+
     } else if (event->atom == atom(QXcbAtom::_NET_FRAME_EXTENTS)) {
         m_dirtyFrameMargins = true;
     }

diff --git a/src/plugins/platforms/xcb/qxcbwindow.h b/src/plugins/platforms/xcb/qxcbwindow.h
index 13b37db028a..5df64f3610a 100644
--- a/src/plugins/platforms/xcb/qxcbwindow.h
+++ b/src/plugins/platforms/xcb/qxcbwindow.h
@@ -73,6 +73,14 @@
 
     Q_DECLARE_FLAGS(NetWmStates, NetWmState)
 
+    enum Task {
+        Map,
+        Unmap,
+        SetGeometry,
+        SetWindowFlags,
+        SetWindowState	
+    };
+
     QXcbWindow(QWindow *window);
     ~QXcbWindow();
 
@@ -144,6 +152,9 @@
 
     QXcbWindow *toWindow() override;
 
+    bool shouldDeferTask(Task task);	
+    void handleDeferredTasks();
+
     void handleMouseEvent(xcb_timestamp_t time, const QPoint &local, const QPoint &global,
                           Qt::KeyboardModifiers modifiers, QEvent::Type type, Qt::MouseEventSource source);
 
@@ -276,6 +287,10 @@
     SyncState m_syncState = NoSyncNeeded;
 
     QXcbSyncWindowRequest *m_pendingSyncRequest = nullptr;
+
+    bool m_wmStateValid = true;
+    QVector<Task> m_deferredTasks;	
+    bool m_isWmManagedWindow = true;
 };
 
 class QXcbForeignWindow : public QXcbWindow
