X11 Intercept Window Close Event

***Note Nov, 8 2009: a few additions have been made to the code samples to make them more complete. Specifically subscribing to X11 events.

A feature request for KDocker was made a few days ago for docking when closed. Basically the person wants the window to iconify when the X button on the window decoration is clicked. It’s something I’ve been thinking about for quite some time but it’s not the easiest feature to implement. Girish thankfully pointed me onto the right path when he suggested looking into using a QX11EmbedContainer. I’ve gotten it working and it will be in the 4.3 release.

There isn’t a whole lot of documentation out there on achieving this so I’m going to detail how I’ve implemented the feature in KDocker. Do be awhare that there are a few short comings. Versions <= 4.2 will iconify all windows associated with the docked one. Meaning Gimp and Audacious (XMMS) which have multiple windows will not have the extra windows iconify when the main one is. Also, decorationless windows like audacious (possibly Google Chrome) are no longer movable by clicking on the top of the window. Alt+Left Mouse Click still works and is currently the only way to move them.

The basic overview of the implementation is: Take the user started application window and get info about it (title, size, location, window decorations …). Create a container window. Use XEmbed to put the window into the container. Set the container properties to those of the window. Intercept the WM_DELETE_WINDOW message. Forward other changes like title and icon from the window to the container.

Getting info about the window requires Xlib calls. I’m using the following to get the minimum size, current size, position, width, height and decorations. window is the window id for the application window. The decoration code requires mwmutil.h which can be found in libmotif and LessTif.

Display *display = QX11Info::display();
XSizeHints sizeHint;
long dummy;
Window root;
int x, y;
unsigned int width, height, border, depth;

XGetWMNormalHints(display, window, &sizeHint, &dummy);
XGetGeometry(display, window, &root, &x, &y, &width, &height, &border, &depth);

Atom wm_hints_atom = XInternAtom(display, _XA_MOTIF_WM_HINTS, false);
unsigned char *wm_data;
Atom wm_type;
int wm_format;
unsigned long wm_nitems, wm_bytes_after;

XGetWindowProperty(display, window, wm_hints_atom, 0, sizeof (MotifWmHints) / sizeof (long), false, AnyPropertyType, &wm_type, &wm_format, &wm_nitems, &wm_bytes_after, &wm_data);

Next create the container. I’m using QX11EmbedContainer because it is is a Qt provided XEmbed container widget.

QX11EmbedContainer *container = new QX11EmbedContainer();
container->embedClient(window);
container->show();

We need to tell X that we want to receive certain events about the container and embedded window. If we don’t set the appropriate masks the events we are interested in will never be sent to our X11EventFilter.

long mask_container = StructureNotifyMask | PropertyChangeMask | VisibilityChangeMask | FocusChangeMask;
long mask_embed = PropertyChangeMask;

XWindowAttributes attr_container;
XWindowAttributes attr_embed;

XGetWindowAttributes(display, m_container->winId(), &attr_container);
XGetWindowAttributes(display, window, &attr_embed);

if ((attr_container.your_event_mask & mask_container) != mask_container)) {
    XSelectInput(display, m_container->winId(), attr_container.your_event_mask | mask_container);
}
if ((attr_container.your_event_mask & mask_embed) != mask_embed)) {
    XSelectInput(display, window, attr_embed.your_event_mask | mask_embed);
}

Now we want to register WM_DELETE_WINDOW so that instead of closing it will be directed to us as a client message. We will check for the message in the X11EventFilter and take appropriate action. This will only work because we have reparented the window by embedding it into the container we control. Simply setting this on the window will not work.

Atom wm_delete = XInternAtom(display, "WM_DELETE_WINDOW", False);
XSetWMProtocols(display, container->winId(), &wm_delete, 1);

Take all the info we obtained from the window and apply it to our container. This way it will look and behave the same as if it was not embedded.

m_container->setMinimumSize(m_sizeHint.min_width, m_sizeHint.min_height);
m_container->setGeometry(x, y, width, height);

if (wm_type == None) {
    MotifWmHints hints;
    memset(&hints, 0, sizeof (hints));
    hints.flags = MWM_HINTS_DECORATIONS;
    hints.decorations = MWM_DECOR_ALL;
    XChangeProperty(display, m_container->winId(), wm_hints_atom, wm_hints_atom, 32, PropModeReplace, (unsigned char *) & hints, sizeof (MotifWmHints) / sizeof (long));
} else {
    MotifWmHints *hints;
    hints = (MotifWmHints *) wm_data;
    if (!(hints->flags & MWM_HINTS_DECORATIONS)) {
        hints->flags |= MWM_HINTS_DECORATIONS;
        hints->decorations = 0;
    }
    XChangeProperty(display, m_container->winId(), wm_hints_atom, wm_hints_atom, 32, PropModeReplace, (unsigned char *) hints, sizeof (MotifWmHints) / sizeof (long));
}
XFree(wm_data);

Thats it for constructing the window but there are a few important pieces we still need to worry about. Within QApplication’s x11EventFilter we need to processes some events.

bool x11EventFilter(XEvent *ev) {
    XAnyEvent *event = (XAnyEvent *) ev;
    if (event->window == m_container->winId()) {
        if (ev->type == ClientMessage && (ulong) ev->xclient.data.l[0] == XInternAtom(QX11Info::display(), "WM_DELETE_WINDOW", False)) {
            if (m_iconifyOnClose) {
                iconifyWindow();
            } else {
                close();
            }
            return true;
        }
    }

    if (event->window == window) {
        if (event->type == PropertyNotify) {
            return propertyChangeEvent(((XPropertyEvent *) event)->atom);
        }
    }
    return false
}

bool propertyChangeEvent(Atom property) {
    Display *display = QX11Info::display();
    static Atom WM_NAME = XInternAtom(display, "WM_NAME", True);
    if (property == WM_NAME) {
        updateTitle();
        return true;
    }
}

void updateTitle() {
    Display *display = QX11Info::display();
    char *windowName = 0;
    QString title;

    XFetchName(display, window, &windowName);
    title = windowName;
    if (windowName) {
        XFree(windowName);
    }

    container->setWindowTitle(title);
}

2 thoughts on “X11 Intercept Window Close Event

  1. Hey John, fantastic stuff!

    One bit that I didn’t quite understand is why you register for WM_DELETE_WINDOW. Can’t you just subclass QX11EmbedContainer and reimplement closeEvent? (you can accept/reject the QCloseEvent)

    Like

    1. WM_DELETE_WINDOW is passed to the embedded window if you do not register it, even if you re-implement the closeEvent; the embedded window will close. If you register it then you have to look for it as a client message in the X11EventFilter because the closeEvent will never be called since the WM_DELETE_WINDOW has essentially been overridden.

      Like

Comments are closed.