Browse Source

x11: Support desktop startup notification

This lets the window manager know when a program has finished launching, to stop showing a spinning cursor

Can be disabled with `x-send-startup-notification false`
rdb 3 years ago
parent
commit
60acf82f04

+ 7 - 0
panda/src/x11display/config_x11display.cxx

@@ -84,6 +84,13 @@ ConfigVariableString x_wm_class
  PRC_DESC("Specify the value to use for the res_class field of the window's "
           "WM_CLASS property."));
 
+ConfigVariableBool x_send_startup_notification
+("x-send-startup-notification", true,
+ PRC_DESC("Set this to true to send a startup notification to the window "
+          "manager automatically after the first window is opened.  This "
+          "lets the window manager know that an application has launched, so "
+          "that it no longer needs to display a spinning mouse cursor."));
+
 /**
  * Initializes the library.  This must be called at least once before any of
  * the functions or classes in this library can be used.  Normally it will be

+ 1 - 0
panda/src/x11display/config_x11display.h

@@ -36,5 +36,6 @@ extern ConfigVariableInt x_wheel_right_button;
 extern ConfigVariableInt x_cursor_size;
 extern ConfigVariableString x_wm_class_name;
 extern ConfigVariableString x_wm_class;
+extern ConfigVariableBool x_send_startup_notification;
 
 #endif

+ 9 - 0
panda/src/x11display/x11GraphicsPipe.I

@@ -57,6 +57,15 @@ get_hidden_cursor() {
   return _hidden_cursor;
 }
 
+/**
+ * Returns the startup id that may have been passed by the environment variable
+ * DESKTOP_STARTUP_ID.
+ */
+INLINE const std::string &x11GraphicsPipe::
+get_startup_id() const {
+  return _startup_id;
+}
+
 /**
  * Returns true if a form of relative mouse mode is supported on this display.
  */

+ 102 - 8
panda/src/x11display/x11GraphicsPipe.cxx

@@ -66,6 +66,20 @@ x11GraphicsPipe(const std::string &display) :
   // point to mean a decimal point.
   setlocale(LC_NUMERIC, "C");
 
+  // Also save the startup ID.  We are required to unset it by the FreeDesktop
+  // specification so that it is not propagated to child processes.
+  {
+    char *startup_id = getenv("DESKTOP_STARTUP_ID");
+    if (startup_id != nullptr) {
+      _startup_id.assign(startup_id);
+      if (x11display_cat.is_debug()) {
+        x11display_cat.debug()
+          << "Got desktop startup ID " << _startup_id << "\n";
+      }
+      unsetenv("DESKTOP_STARTUP_ID");
+    }
+  }
+
   _is_valid = false;
   _supported_types = OT_window | OT_buffer | OT_texture_buffer;
   _display = nullptr;
@@ -375,21 +389,25 @@ x11GraphicsPipe(const std::string &display) :
   }
 
   // Get some X atom numbers.
+  _utf8_string = XInternAtom(_display, "UTF8_STRING", false);
   _wm_delete_window = XInternAtom(_display, "WM_DELETE_WINDOW", false);
+  _net_startup_id = XInternAtom(_display, "_NET_STARTUP_ID", false);
+  _net_startup_info = XInternAtom(_display, "_NET_STARTUP_INFO", false);
+  _net_startup_info_begin = XInternAtom(_display, "_NET_STARTUP_INFO_BEGIN", false);
+  _net_wm_bypass_compositor = XInternAtom(_display, "_NET_WM_BYPASS_COMPOSITOR", false);
   _net_wm_pid = XInternAtom(_display, "_NET_WM_PID", false);
   _net_wm_ping = XInternAtom(_display, "_NET_WM_PING", false);
-  _net_wm_window_type = XInternAtom(_display, "_NET_WM_WINDOW_TYPE", false);
-  _net_wm_window_type_splash = XInternAtom(_display, "_NET_WM_WINDOW_TYPE_SPLASH", false);
-  _net_wm_window_type_fullscreen = XInternAtom(_display, "_NET_WM_WINDOW_TYPE_FULLSCREEN", false);
   _net_wm_state = XInternAtom(_display, "_NET_WM_STATE", false);
-  _net_wm_state_fullscreen = XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", false);
   _net_wm_state_above = XInternAtom(_display, "_NET_WM_STATE_ABOVE", false);
-  _net_wm_state_below = XInternAtom(_display, "_NET_WM_STATE_BELOW", false);
   _net_wm_state_add = XInternAtom(_display, "_NET_WM_STATE_ADD", false);
-  _net_wm_state_remove = XInternAtom(_display, "_NET_WM_STATE_REMOVE", false);
-  _net_wm_bypass_compositor = XInternAtom(_display, "_NET_WM_BYPASS_COMPOSITOR", false);
-  _net_wm_state_maximized_vert = XInternAtom(_display, "_NET_WM_STATE_MAXIMIZED_VERT", false);
+  _net_wm_state_below = XInternAtom(_display, "_NET_WM_STATE_BELOW", false);
+  _net_wm_state_fullscreen = XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", false);
   _net_wm_state_maximized_horz = XInternAtom(_display, "_NET_WM_STATE_MAXIMIZED_HORZ", false);
+  _net_wm_state_maximized_vert = XInternAtom(_display, "_NET_WM_STATE_MAXIMIZED_VERT", false);
+  _net_wm_state_remove = XInternAtom(_display, "_NET_WM_STATE_REMOVE", false);
+  _net_wm_window_type = XInternAtom(_display, "_NET_WM_WINDOW_TYPE", false);
+  _net_wm_window_type_fullscreen = XInternAtom(_display, "_NET_WM_WINDOW_TYPE_FULLSCREEN", false);
+  _net_wm_window_type_splash = XInternAtom(_display, "_NET_WM_WINDOW_TYPE_SPLASH", false);
 }
 
 /**
@@ -406,6 +424,82 @@ x11GraphicsPipe::
   }
 }
 
+/**
+ * Tells the window manager that the launch sequence with the indicated startup
+ * ID has finished.  Will only send it once, and only if DESKTOP_STARTUP_ID was
+ * passed in as an environment variable.
+ */
+void x11GraphicsPipe::
+send_startup_notification() {
+  if (_sent_startup_notification || _startup_id.empty()) {
+    return;
+  }
+
+  // Allocate enough room for the message, with room for escape characters.
+  char *message = (char *)alloca(_startup_id.size() * 2 + 14);
+  strcpy(message, "remove: ID=\"");
+
+  char *p = message + 12;
+  for (char c : _startup_id) {
+    if (c == '"' || c == '\\') {
+      *p++ = '\\';
+    }
+    *p++ = c;
+  }
+  *p++ = '\"';
+  *p++ = '\0';
+
+  if (x11display_cat.is_debug()) {
+    x11display_cat.debug()
+      << "Sending startup info message: " << message << "\n";
+  }
+
+  // It doesn't *strictly* seem to be necessary to create a window for this
+  // (just passing in the root window works too) but the spec says we should
+  // and GTK does it too, so why not?
+  XSetWindowAttributes attrs;
+  attrs.override_redirect = True;
+  attrs.event_mask = PropertyChangeMask | StructureNotifyMask;
+  X11_Window xwin = XCreateWindow(_display, _root, -100, -100, 1, 1, 0,
+                                  CopyFromParent, CopyFromParent, CopyFromParent,
+                                  CWOverrideRedirect | CWEventMask, &attrs);
+
+  XEvent xevent;
+  xevent.xclient.type = ClientMessage;
+  xevent.xclient.message_type = _net_startup_info_begin;
+  xevent.xclient.display = _display;
+  xevent.xclient.window = xwin;
+  xevent.xclient.format = 8;
+
+  const char *src = message;
+  const char *src_end = message + strlen(message) + 1;
+
+  char *dest, *dest_end;
+  while (src != src_end) {
+    dest = &xevent.xclient.data.b[0];
+    dest_end = dest + 20;
+
+    while (dest != dest_end && src != src_end) {
+      *dest = *src;
+      ++dest;
+      ++src;
+    }
+
+    while (dest != dest_end) {
+      *dest = '\0';
+      ++dest;
+    }
+
+    XSendEvent(_display, _root, False, PropertyChangeMask, &xevent);
+    xevent.xclient.message_type = _net_startup_info;
+  }
+
+  XDestroyWindow(_display, xwin);
+  XFlush(_display);
+
+  _sent_startup_notification = true;
+}
+
 /**
  * Enables raw mouse mode for this display.  Returns false if unsupported.
  */

+ 18 - 8
panda/src/x11display/x11GraphicsPipe.h

@@ -144,6 +144,9 @@ public:
 
   INLINE X11_Cursor get_hidden_cursor();
 
+  INLINE const std::string &get_startup_id() const;
+  void send_startup_notification();
+
   INLINE bool supports_relative_mouse() const;
   INLINE bool enable_dga_mouse();
   INLINE void disable_dga_mouse();
@@ -165,21 +168,25 @@ public:
 
 public:
   // Atom specifications.
+  Atom _utf8_string;
   Atom _wm_delete_window;
+  Atom _net_startup_id;
+  Atom _net_startup_info;
+  Atom _net_startup_info_begin;
+  Atom _net_wm_bypass_compositor;
   Atom _net_wm_pid;
   Atom _net_wm_ping;
-  Atom _net_wm_window_type;
-  Atom _net_wm_window_type_splash;
-  Atom _net_wm_window_type_fullscreen;
   Atom _net_wm_state;
-  Atom _net_wm_state_fullscreen;
   Atom _net_wm_state_above;
-  Atom _net_wm_state_below;
   Atom _net_wm_state_add;
-  Atom _net_wm_state_remove;
-  Atom _net_wm_bypass_compositor;
-  Atom _net_wm_state_maximized_vert;
+  Atom _net_wm_state_below;
+  Atom _net_wm_state_fullscreen;
   Atom _net_wm_state_maximized_horz;
+  Atom _net_wm_state_maximized_vert;
+  Atom _net_wm_state_remove;
+  Atom _net_wm_window_type;
+  Atom _net_wm_window_type_fullscreen;
+  Atom _net_wm_window_type_splash;
 
   // Extension functions.
   typedef int (*pfn_XcursorGetDefaultSize)(X11_Display *);
@@ -224,6 +231,9 @@ protected:
 
   X11_Cursor _hidden_cursor;
 
+  std::string _startup_id;
+  bool _sent_startup_notification = false;
+
   typedef Bool (*pfn_XF86DGAQueryVersion)(X11_Display *, int*, int*);
   typedef Status (*pfn_XF86DGADirectVideo)(X11_Display *, int, int);
   pfn_XF86DGADirectVideo _XF86DGADirectVideo;

+ 15 - 0
panda/src/x11display/x11GraphicsWindow.cxx

@@ -1260,6 +1260,15 @@ open_window() {
     XDefineCursor(_display, _xwindow, cursor);
   }
 
+  // Set _NET_STARTUP_ID if we've got it, so that the window manager knows
+  // this window belongs to a particular launch request.
+  const std::string &startup_id = x11_pipe->get_startup_id();
+  if (!startup_id.empty() && _parent_window_handle == nullptr) {
+    XChangeProperty(_display, _xwindow, x11_pipe->_net_startup_id,
+                    x11_pipe->_utf8_string, 8, PropModeReplace,
+                    (unsigned char *)startup_id.c_str(), startup_id.size());
+  }
+
   XMapWindow(_display, _xwindow);
 
   if (_properties.get_raw_mice()) {
@@ -1279,6 +1288,12 @@ open_window() {
     _parent_window_handle->attach_child(_window_handle);
   }
 
+  // Now that we've opened a window, tell the window manager that the
+  // application has finished starting up.
+  if (!startup_id.empty() && x_send_startup_notification) {
+    x11_pipe->send_startup_notification();
+  }
+
   return true;
 }