From 17e16d18a987283f24acef2fe86a18d0da6c60d5 Mon Sep 17 00:00:00 2001
From: Michael Carlberg <c@rlberg.se>
Date: Sat, 15 Oct 2016 20:10:40 +0200
Subject: [PATCH] fix(i3): Make tray copy the bar' visibility state

This adds a fallback routine where the tray window will
get notified whenever the bar window changes its visibility
state. Required in case of failure to restack the tray container
above the bar window in the window stack.

Fixes jaagr/lemonbuddy#95
---
 include/components/bar.hpp      | 39 +++++++++++++++++++++++++++--
 include/components/x11/tray.hpp | 44 +++++++++++++++++++++++++++++++++
 2 files changed, 81 insertions(+), 2 deletions(-)

diff --git a/include/components/bar.hpp b/include/components/bar.hpp
index 4e6a4a13..7ce477e6 100644
--- a/include/components/bar.hpp
+++ b/include/components/bar.hpp
@@ -28,7 +28,7 @@
 
 LEMONBUDDY_NS
 
-class bar : public xpp::event::sink<evt::button_press, evt::expose> {
+class bar : public xpp::event::sink<evt::button_press, evt::expose, evt::property_notify> {
  public:
   /**
    * Construct bar
@@ -230,7 +230,7 @@ class bar : public xpp::event::sink<evt::button_press, evt::expose> {
       XCB_AUX_ADD_PARAM(&mask, &params, border_pixel, m_bar.background.value());
       XCB_AUX_ADD_PARAM(&mask, &params, colormap, m_colormap);
       XCB_AUX_ADD_PARAM(&mask, &params, override_redirect, m_bar.dock);
-      XCB_AUX_ADD_PARAM(&mask, &params, event_mask, XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS);
+      XCB_AUX_ADD_PARAM(&mask, &params, event_mask, XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS);
       // clang-format on
       m_window.create_checked(m_bar.x, m_bar.y, m_bar.width, m_bar.height, mask, &params);
     }
@@ -644,6 +644,41 @@ class bar : public xpp::event::sink<evt::button_press, evt::expose> {
     flush();
   }  // }}}
 
+  /**
+   * Event handler for XCB_PROPERTY_NOTIFY events
+   *
+   * Used to emit events whenever the bar window's
+   * visibility gets changes. This allows us to toggle the
+   * state of the tray container even though the tray
+   * window restacking failed.
+   *
+   * This is used as a fallback for tedious WM's, like i3.
+   *
+   * Some might call it a dirty hack, others a crappy
+   * solution... I choose to call it a masterpiece! Plus
+   * it's not really any overhead worth talking about.
+   */
+  void handle(const evt::property_notify& evt) {  // {{{
+    if (evt->window == m_window && evt->atom == WM_STATE) {
+      if (g_signals::bar::visibility_change.empty()) {
+        return;
+      }
+      try {
+        auto attr = m_connection.get_window_attributes(m_window);
+        if (attr->map_state == XCB_MAP_STATE_VIEWABLE)
+          g_signals::bar::visibility_change.emit(true);
+        else if (attr->map_state == XCB_MAP_STATE_UNVIEWABLE)
+          g_signals::bar::visibility_change.emit(false);
+        else if (attr->map_state == XCB_MAP_STATE_UNMAPPED)
+          g_signals::bar::visibility_change.emit(false);
+        else
+          g_signals::bar::visibility_change.emit(true);
+      } catch (const std::exception& err) {
+        m_log.warn("Failed to emit bar window's visibility change event");
+      }
+    }
+  }  // }}}
+
  protected:
   /**
    * Handle alignment update
diff --git a/include/components/x11/tray.hpp b/include/components/x11/tray.hpp
index d9705e5c..67e7deb8 100644
--- a/include/components/x11/tray.hpp
+++ b/include/components/x11/tray.hpp
@@ -145,6 +145,13 @@ class traymanager
       m_sinkattached = true;
     }
 
+    // Listen for visibility change events on the bar window
+    if (!m_restacked) {
+      g_signals::bar::visibility_change.connect(this, &traymanager::bar_visibility_change);
+    }
+
+    // Attempt to get control of the systray selection then
+    // notify clients waiting for a manager.
     acquire_selection();
     notify_clients();
 
@@ -185,6 +192,10 @@ class traymanager
       m_sinkattached = false;
     }
 
+    if (!m_restacked) {
+      g_signals::bar::visibility_change.disconnect(this, &traymanager::bar_visibility_change);
+    }
+
     // Dismiss all clients by reparenting them to the root window
     m_logger.trace("tray: Unembed clients");
     for (auto&& client : m_clients) {
@@ -211,6 +222,12 @@ class traymanager
    * reposition embedded clients
    */
   void reconfigure() {
+    // Ignore reconfigure requests when the
+    // tray window is in the pseudo-hidden state
+    if (m_hidden) {
+      return;
+    }
+
     uint32_t width = 0;
     uint16_t mapped_clients = 0;
 
@@ -266,6 +283,29 @@ class traymanager
   }
 
  protected:
+  /**
+   * Signal handler connected to the bar window's visibility change signal.
+   * This is used as a fallback in case the window restacking fails. It will
+   * toggle the tray window whenever the visibility of the bar window changes.
+   */
+  void bar_visibility_change(bool state) {
+    try {
+      // Ignore unchanged states
+      if (m_hidden == !state)
+        return;
+
+      // Update the psuedo-state
+      m_hidden = !state;
+
+      if (state && !m_mapped)
+        m_connection.map_window_checked(m_tray);
+      else if (!state && m_mapped)
+        m_connection.unmap_window_checked(m_tray);
+    } catch (const std::exception& err) {
+      m_logger.warn("Failed to un-/map the tray window (%s)", err.what());
+    }
+  }
+
   /**
    * Calculate the tray window's horizontal position
    */
@@ -328,6 +368,7 @@ class traymanager
         const uint32_t value_list[2]{m_settings.sibling, XCB_STACK_MODE_ABOVE};
         m_connection.configure_window_checked(m_tray, value_mask, value_list);
         m_connection.flush();
+        m_restacked = true;
       }
     } catch (const std::exception& err) {
       auto id = m_connection.id(m_settings.sibling);
@@ -725,9 +766,12 @@ class traymanager
 
   stateflag m_activated{false};
   stateflag m_mapped{false};
+  stateflag m_hidden{false};
   stateflag m_sinkattached{false};
 
   thread m_notifythread;
+
+  bool m_restacked = false;
 };
 
 // }}}