diff --git a/panda/src/x11display/config_x11display.cxx b/panda/src/x11display/config_x11display.cxx index d31c4c1c23..1744a8a556 100644 --- a/panda/src/x11display/config_x11display.cxx +++ b/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 diff --git a/panda/src/x11display/config_x11display.h b/panda/src/x11display/config_x11display.h index 157f09c5fc..d45ff1cd66 100644 --- a/panda/src/x11display/config_x11display.h +++ b/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 diff --git a/panda/src/x11display/x11GraphicsPipe.I b/panda/src/x11display/x11GraphicsPipe.I index 3f608bad5b..f39ed81fb3 100644 --- a/panda/src/x11display/x11GraphicsPipe.I +++ b/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. */ diff --git a/panda/src/x11display/x11GraphicsPipe.cxx b/panda/src/x11display/x11GraphicsPipe.cxx index 632cdcb055..ed5c6a05b5 100644 --- a/panda/src/x11display/x11GraphicsPipe.cxx +++ b/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. */ diff --git a/panda/src/x11display/x11GraphicsPipe.h b/panda/src/x11display/x11GraphicsPipe.h index d46218077e..b0ffcf10b5 100644 --- a/panda/src/x11display/x11GraphicsPipe.h +++ b/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; diff --git a/panda/src/x11display/x11GraphicsWindow.cxx b/panda/src/x11display/x11GraphicsWindow.cxx index 0595bc72d6..eae17632f0 100644 --- a/panda/src/x11display/x11GraphicsWindow.cxx +++ b/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; }