1
votes

I have an application that grabs X windows and hosts them as sub-windows. It somehow implements a very basic window manager. No window manager is running. Works well except for Java applications.

For instance if the Java app has menus or popups, those pop ups are not properly repositioned. I have reproduced this with a very simple unit test. The java app is a basic JFrame with a menu bar and an menu

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JMenuBar;

public class test extends JFrame {
    private static test window;
    public test() {
        initialize();
    }

    /**
     * @param args
     */
    public static void main(String[] args) {
        System.out.println("Hello world");
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                window = new test();
                window.setVisible(true);
            }
        });
    }

     /**
     * Initialize the contents of the frame.
     */
    private void initialize() {          
        setTitle("ResizeTest");

        setBounds(100, 100, 888, 439);
        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        getContentPane().setLayout(new BorderLayout(0, 0));

        JMenuBar menuBar = new JMenuBar();
        getContentPane().add(menuBar, BorderLayout.NORTH);

        JMenu fileMenu = new JMenu("File");
        fileMenu.add("Open");
        fileMenu.add("Close");
        menuBar.add(fileMenu);
        add(menuBar);

        addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                dispose();
                System.exit(0);
            }
        });

        JPanel panel = new JPanel();
        panel.add(new JLabel("Hello world"));
        getContentPane().add(panel, BorderLayout.SOUTH);

        pack();
    }
}

Then another unit test (in C):

/*
   Simple Xlib application drawing a box in a window.
   To Compile: gcc -O2 -Wall -o test test.c -L /usr/X11R6/lib -lX11 -lm
 */ 
#include<X11/Xlib.h>
#include<stdio.h>
#include<stdlib.h> // prevents error for exit on line 18 when compiling with gcc

#include <X11/X.h>
#include <X11/Xatom.h>
#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Shell.h>

int WINDOW_ID;

void processExistingWindows(Display *display, Window parentWindow) {

    Window *children;
    Window parent;
    Window root;
    unsigned int nchildren;
    printf("Entering processExistingWindows\n");

    Window rootWindow = XDefaultRootWindow(display);
    int result = XQueryTree(display, rootWindow, &root, &parent, &children, &nchildren);
    printf("XQueryTree result is %d\n", result);
    unsigned int windowCount = 0;
    printf("Iterating through %d windows\n", nchildren);
    for (windowCount = 0; result && windowCount < nchildren; windowCount++) {
        Window currentWindow = children[windowCount];
        if ((int)currentWindow == WINDOW_ID) {
            int reparentResult = XReparentWindow(display, currentWindow, parentWindow, 0, 0);
            printf("XReparentWindow result: %d\n", reparentResult);
            printf("Reparented window: %x into %x\n", (int)currentWindow, (int)parentWindow);
        }
    }
    if (result && children != NULL) {
        XFree((char *) children);
    }
}

 int main(int argc, char** argv) {
   Display *d;
   int s;
   Window w;
   XEvent e;

   if (argc < 2) {
      printf("Please give the window ID you want to reparent\n");
      exit(1);
    }
    WINDOW_ID = atoi(argv[1]);
    printf("Will try to reparent the Window: %x \n", WINDOW_ID);

    printf("Forcing synchronous calls to the X Server\n");
    _Xdebug = 1; // To allow proper debugging by forcing synchronous calls to the X Server

                /* open connection with the server */
   d=XOpenDisplay(NULL);
   if(d==NULL) {
     printf("Cannot open display\n");
     exit(1);
   }
   s=DefaultScreen(d);

    /* create window */
   w = XCreateSimpleWindow(d, RootWindow(d, s), 0, 0, 400, 400, 1,
                         BlackPixel(d, s), WhitePixel(d, s));

    printf("Current window ID is: %x\n", (int) w);

   // Process Window Close Event through event handler so XNextEvent does Not fail
   Atom delWindow = XInternAtom( d, "WM_DELETE_WINDOW", 0 );
   XSetWMProtocols(d , w, &delWindow, 1);

                        /* select kind of events we are interested in */
   XSelectInput(d, w, ExposureMask | KeyPressMask | StructureNotifyMask );

                        /* map (show) the window */
   XMapWindow(d, w);

   processExistingWindows(d, w);
                        /* event loop */
   printf("Starting the loop\n");
   XMoveWindow(d, w, 100, 200);

   while(1) {
     XNextEvent(d, &e);
                        /* draw or redraw the window */
     if(e.type==Expose) {
       XFillRectangle(d, w, DefaultGC(d, s), 20, 20, 10, 10);
     }
                        /* exit on key press */
     if(e.type==KeyPress)
       break;

     // Handle Windows Close Event
     if(e.type==ClientMessage)
        break;
   }

                        /* destroy our window */
   XDestroyWindow(d, w);

                        /* close connection to server */
   XCloseDisplay(d);

   return 0;
 }

Just start the Java application. It is properly displayed and the menu shows where it is expected.

The grab the window ID (using xwininfo -root -tree -int) and start the unit test with this window ID as an argument to reparent it within a basic window. The Java frame is properly moved and and reparented to the window, however the menu still pops up at the previous JFrame location!

It is like the JFrame is properly repositioned with its origin relative to its new parent window, but this new location is not taken into account by the menu/popups.

This is odd. If instead of doing a XReparentWindow I do a XMoveWindow, then the menu appears where it should.

I googled a fair bit this issue and nothing brings any clear solution. There are a lot of talking about the hardcoded support of tiling window manager in the JVM, which is apparently addressed by OpenJDK. But none of the versions I have taken have worked (Java 6, 7, 8 and OpenJDK 7).

Although it looks to me like a plain bug in the JDK (I have other C/C++ based applications hosted this way which work fine with menu/popups), the fact that it works using a window manager (like FVWM) proves that it can work. I also looked at the implementation in twm and it does not look different.

Has anyone encountered (and solved) this issue?

Thanks!


EDIT

After hours of debugging it indeed comes from Java and how it handles the Window Managers (or absence of). The interesting portion of the code is within XDecoratedPeer.java and XWM.java.

Java tries to guess what window manager is running and will adapt its behaviour based on this window manager (they not all treat the client applications the same way). In our case there is no window manager running so Java will default to No/Other WM.

Once started a Java application will never recheck for a change of window manager. Therefore from within the host we cannot ‘fake’ any particular WM to change the behaviour of the guest as it is too late.

In the absence of a WM Java will still react to the various X events it receives (click, move, reparent ..) but expects a very strict succession of events.

In our case the ReparentNotifyEvent is not sufficient to trigger a full recomputation of the location of the windows within the JVM. So after the reparenting from the host, the guest still thinks it is at its former location. This is not immediately visible because all the windows are drawn relative to each other by the X Server, but all the popups (and all the window directly created by Java, ie having the override_redirect flag set) will be drawn at the wrong location.

What Java needs to recalculate its screen coordinate is a ConfigureNotifyEvent (which contains the absolute screen coordinates).

However this event has to have two characteristics: - Not be synthetic (ie being explicitly sent through a XSendEvent() call) - Have a serial that differs from the ReparentNotify event. That is the tricky bit and I don’t know how to do this, besides inserting a sleep(1) between the XReparentWindow and the XSendEvent. XFlush(), XSync(), XNoOp(), etc .. don’t work as they don’t create a new request, therefore not incrementing the serial.

All the versions of Java I have looked at have pretty much the same behaviour. Anyway .. with this change the Java client properly recomputes its screen location and the popups are now properly displayed.

1
Why not just run a real window manager? - n. 1.8e9-where's-my-share m.
Unfortunately that is not an option .... - Denis Gauthier

1 Answers

0
votes

So I finally managed to get it to work.

Here is the sequence of actions that need to be done:

// We need to move the child window before reparenting it to avoid some nasty offsets
XMoveWindow(display, childWindowId, 0, 0);

// Do the reparenting
XReparentWindow(display, childWindowId, parentWindowId, 0, 0);

// Ask the XServer to take ownership back if we die
XFixesChangeSaveSet(display, childWindowId, SetModeInsert, SaveSetRoot, SaveSetUnmap);

// We have to explicitly notify the Java child of its location change.
XEvent client_event;
XWindowAttributes childAttributes;
XWindowAttributes parentAttributes;
XGetWindowAttributes(display, childWindowId, &childAttributes);
XGetWindowAttributes(display, parentWindowId, &parentAttributes);
WindowDimension windowDecorationSize = // Your decoration if applicable

client_event.type = ConfigureNotify;
client_event.xconfigure.send_event = True;
client_event.xconfigure.display = display;
client_event.xconfigure.event = childWindowId;
client_event.xconfigure.window = childWindowId ;
client_event.xconfigure.x = parentAttributes.x + windowDecorationSize.width;
client_event.xconfigure.y = parentAttributes.y + windowDecorationSize.height;
client_event.xconfigure.width = childAttributes.width;
client_event.xconfigure.height = childAttributes.height;
client_event.xconfigure.border_width = 0;
client_event.xconfigure.above = None;
client_event.xconfigure.override_redirect = True;   // Set to true to filter the event out in the processing of the parent Java frame 

XSendEvent(display, childWindowId, False, StructureNotifyMask, &client_event);