3

I'm writing a sort of web applet emulator. I read a web page, find the applet parameters, download the applet and run it. It is very important that the applet runs in its own process (i.e. not the emulator process). It should, however, render in the emulator process window.

How does the Java plugin do it? When the separate_jvm flag is set, the plugin loads the applet in a separate JVM process but the applet still appears in the same browser panel.

I've made some progress by creating a loader class that, on another JVM, adds the target Applet to an undecorated, invisible frame and messages the frame's window handle to the emulator JVM. The latter binds it to a Canvas instance with user32.SetParent via JNA, and the display works perfectly.

However, only mouse events are being sent: keyboard input is not forwarded. The applet reports Component#isFocusOwner as false, and requestFocusInWindow does not make it the focus owner, returning false. How can I pass keyboard focus to the Applet window handle? My current approach involves a server (the emulator), which receives window handles from the client (the applet). Only mouse events appear to work, since the Applet cannot gain focus.

The server class handles the display of the applet.

import com.sun.jna.*;
import com.sun.jna.platform.win32.User32;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.*;
import static com.sun.jna.platform.win32.User32.*;

public class Loader {
    private static final String APPLET_DIRECTORY = ""; // TODO: Set this to the directory containing the compiled applet

    private static ServerSocket serverSocket;
    private static JFrame frame;
    private static Canvas nativeDisplayCanvas;

    public static void main(String[] argv) throws Exception {
        nativeDisplayCanvas = new Canvas();
        frame = new JFrame("Frame redirect");
        frame.setLayout(new BorderLayout());
        frame.add(nativeDisplayCanvas, BorderLayout.CENTER);
        frame.setSize(300, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

        (new Thread() {
            public void run() {
                try {
                    serve();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();

        spawnAltJVM(APPLET_DIRECTORY, "AppletDemo");
    }

    public static void serve() throws Exception {
        serverSocket = new ServerSocket(6067);
        serverSocket.setSoTimeout(10000);

        while (true) {
            try {
                System.out.println("Waiting for applet on port " + serverSocket.getLocalPort() + "...");
                Socket server = serverSocket.accept();
                System.out.println("Connected to " + server.getRemoteSocketAddress());
                BufferedReader in = new BufferedReader(new InputStreamReader(server.getInputStream()));
                DataOutputStream out = new DataOutputStream(server.getOutputStream());
                while (true) {
                    String msg = in.readLine();
                    if (msg != null && msg.startsWith("child_hwnd")) {
                        windowCreatedHandler(msg);
                        out.writeUTF("hwnd_recv\n");
                        out.flush();
                    }
                }
            } catch (IOException ex) {
                System.out.println("Something happened to the socket...");
                break;
            }
        }
    }

    public static void windowCreatedHandler(String message) {
        String[] tokens = message.split(":");
        final User32 user32 = User32.INSTANCE;
        HWND child_applet = new HWND(Pointer.createConstant(Long.parseLong(tokens[1])));
        final HWND child_frame = new HWND(Pointer.createConstant(Long.parseLong(tokens[2])));

        frame.addComponentListener(
                new ComponentAdapter() {
                    @Override
                    public void componentResized(ComponentEvent e) {
                        user32.SetWindowPos(child_frame, new HWND(Pointer.NULL), 0, 0, frame.getWidth(), frame.getHeight(), 0);
                    }
                }
        );
        HWND parent = new HWND(Native.getComponentPointer(nativeDisplayCanvas));

        user32.SetParent(child_applet, parent);

        int style = user32.GetWindowLong(child_frame, GWL_STYLE) & ~WS_POPUP | (WS_CHILD | WS_VISIBLE);
        user32.SetWindowLong(child_applet, GWL_STYLE, style);
        user32.SetWindowPos(child_applet, new HWND(Pointer.NULL), 0, 0, nativeDisplayCanvas.getWidth(), nativeDisplayCanvas.getHeight(), 0);
    }

    public static void spawnAltJVM(String cp, String clazz) throws IOException, InterruptedException, ClassNotFoundException {
        ProcessBuilder processBuilder = new ProcessBuilder(System.getProperty("java.home") + File.separator + "bin" + File.separator + "java", "-cp", cp, clazz);
        Process applet = processBuilder.start();
        final BufferedReader in = new BufferedReader(new InputStreamReader(applet.getInputStream()));
        final BufferedReader err = new BufferedReader(new InputStreamReader(applet.getErrorStream()));
        (new Thread() {
            public void run() {
                while (true) {
                    try {
                        System.out.println("[client] " + in.readLine());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}

Meanwhile, the client class just instantiates and messages the handles.

import sun.awt.windows.WComponentPeer;
import javax.swing.*;
import java.applet.Applet;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.Socket;
import java.util.concurrent.LinkedBlockingDeque;

public class AppletDemo extends Applet {
    private Canvas canvas;
    private static Color backgroundColor = Color.RED;

    public AppletDemo() {
        setLayout(new BorderLayout());
        canvas = new Canvas();
        add(canvas, BorderLayout.CENTER);
        setBackground(Color.CYAN);
        canvas.addKeyListener(new KeyAdapter() {
            @Override
            public void keyTyped(KeyEvent e) {
                refreshColors();
            }
        });
        canvas.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                refreshColors();
            }
        });
    }

    private void refreshColors() {
        SwingUtilities.invokeLater(
                new Runnable() {
                    @Override
                    public void run() {
                        backgroundColor = (backgroundColor == Color.RED ? Color.GREEN : Color.RED);
                        canvas.setBackground(backgroundColor);
                    }
                }
        );
    }

    public static void main(String[] argv) throws Exception {
        System.setErr(System.out);

        final AppletDemo app = new AppletDemo();
        Frame frame = new Frame("AppletViewer");
        frame.setLayout(new BorderLayout());
        frame.add(app, BorderLayout.CENTER);
        frame.setUndecorated(true);
        frame.pack(); // Create the native peers
        frame.setSize(300, 200);

        final Socket client = new Socket("localhost", 6067);
        final LinkedBlockingDeque<String> messageQueue = new LinkedBlockingDeque<>();
        final DataOutputStream out = new DataOutputStream(client.getOutputStream());
        final BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
        (new Thread() {
            public void run() {
                while (true) {
                    try {
                        out.writeBytes(messageQueue.take() + "\n");
                        out.flush();
                    } catch (IOException | InterruptedException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }).start();
        (new Thread() {
            public void run() {
                while (true) {
                    try {
                        if ("hwnd_recv".equals(in.readLine())) {
                            // Attempt to grab focus in the other process' frame
                            System.out.println("Trying to request focus...");
                            System.out.println(app.requestFocusInWindow());
                        }
                    } catch (IOException ex) {
                        ex.printStackTrace();
                    }
                }
            }
        }).start();

        messageQueue.add("child_hwnd:" + ((WComponentPeer) app.getPeer()).getHWnd() + ":" + ((WComponentPeer) frame.getPeer()).getHWnd());
    }
}

They're both a bit lengthy because they require some socket work, but they are compilable and should demonstrate the issue. They require JNA to compile. I've shortened them as much as possible at the cost of some good practices.

When Loader is ran, a window redirecting the AppletDemo's canvas should appear. Mouse events are sent: the canvas toggles between red and green on a mouse press. Ideally, the same behavior should occur for keystrokes too.

I've used WinSpy to get the handles of a notepad.exe window and text pane, and hardcoding the handles into Loader. Keyboard focus works perfectly with the multiline edit control, but not with the toplevel window itself. Why? Is this related to the issue I'm having?

I opened up a Chrome window running an applet in WinSpy, and found that the plugin creates no dummy Frame — the applet canvas is directly set as a child of Chrome. However, I haven't been able to create a native peer for the Applet, since it seems to require it to be displayable.

I've read about the dangers of cross-process parent/child or owner/owned window relationship, but I can't think of a better way to graft the child applet into the emulator.

Xyene
  • 2,304
  • 20
  • 36
  • *"The applet states `Component#isFocusOwner` as false, and `requestFocus` does not make it the focus owner."* From the docs.. *"..Because the focus behavior of this method is platform-dependent, developers are strongly encouraged to use `requestFocusInWindow` when possible."* – Andrew Thompson Jan 08 '15 at 00:00
  • @AndrewThompson Thanks, I switched to `requestFocusInWindow` but the problem persists. Specifically, `requestFocusInWindow` returns `false`. – Xyene Jan 08 '15 at 00:30
  • `requestFocusInWindow` must be called at a specific time. For better help sooner, post an [MCVE](http://stackoverflow.com/help/mcve) (Minimal Complete Verifiable Example) or [SSCCE](http://www.sscce.org/) (Short, Self Contained, Correct Example). – Andrew Thompson Jan 08 '15 at 00:38
  • @AndrewThompson I've edited my question to include compilable code. Granted it's a bit long, but I've shortened it as much as I can while retaining the issue. – Xyene Jan 08 '15 at 01:40
  • *"They're both a bit lengthy because they require some socket work,"* To show the problem? As in, without the socket work it focuses correctly? If not, it should be stripped out. That is the point of an MCVE/SSCCE, it removes the irrelevant cruft to focus *only* on the problem code. – Andrew Thompson Jan 08 '15 at 01:53
  • It focuses correctly if the parent of the Applet is in the same process; the issue seems to be caused by having a secondary process create it and adding it into a `Canvas` created by the main process through `user32.SetParent`. Somehow the window handle of the secondary process has to make its way to the parent, and a socket is more convenient than copy/pasting a printed handle from the child process into the parent. – Xyene Jan 08 '15 at 01:57
  • This doesn't look very portable... – tbodt Jan 14 '15 at 01:18
  • @tbodt I'd think any solution to this problem would be platform specific (it's tagged [winapi] for that reason), though I hope there is a simpler approach than mine. – Xyene Jan 14 '15 at 04:00

3 Answers3

1

Since what you really want is to create the applet as a child window, the easy solution would be to convince the applet to be your children, not forcefully adopting it, and working against both Windows and the JVM.

Luckily, the Sun/Oracle Java VM comes with a class called WComponentFrame (Windows-only as implied from the name). It can constructed from an hwnd, which you can send from your parent process. The applet can then be added as a child of your window.

import sun.awt.windows.WComponentPeer;

frame = new WEmbeddedFrame(hwnd);
frame.setLayout(new BorderLayout());
frame.add(applet, BorderLayout.CENTER);
quantum
  • 3,672
  • 29
  • 51
0

It looks like you are trying to pass the event to the Canvas object, which you do not explicitly setFocusable(true) for.

If this is the case, then in your AppletDemo constructor, try:

canvas.setFocusable(true);
canvas.requestFocus();

Also it seems like you want to pass key events to your Applet rather than your Canvas from your question.

In this case, try this in your AppletDemo constructor:

this.setFocusable(true);
this.requestFocus();

After that, you should receive keyboard input by default to the component that is focused.

OsAbdel
  • 16
  • 1
0

With JNA it is as easy as

HWND hwnd1 = User32.INSTANCE.FindWindow(null, "JFrame1");
HWND hwnd2 = User32.INSTANCE.FindWindow(null, "JFrame2");
HWND hwnd3 = User32.INSTANCE.SetParent(hwnd2, hwnd1);

see also

Good or evil - SetParent() win32 API between different processes

weberjn
  • 1,840
  • 20
  • 24