import java.awt.EventQueue; import java.awt.GraphicsDevice; import java.awt.GraphicsEnvironment; import java.awt.Rectangle; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.ComponentListener; import java.awt.event.ContainerAdapter; import java.awt.event.ContainerEvent; import java.awt.event.ContainerListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.event.WindowListener; import javax.media.opengl.GLCanvas; import javax.swing.JFrame; /** * Class to exhibit issue with JOGL on Linux/Xorg/NVidia/TwinView. * * This will cause an IllegalArgumentException to be thrown when adding a GLCanvas to * a JFrame (the JFrame's content pane): * * java.lang.IllegalArgumentException: adding a container to a container on a different GraphicsDevice at java.awt.Component.checkGD(Component.java:965) at java.awt.Container.addImpl(Container.java:1027) at java.awt.Container.add(Container.java:352) at JOGLWithNVidiaTwinView$2.run(JOGLWithNVidiaTwinView.java:88) at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:209) at java.awt.EventQueue.dispatchEvent(EventQueue.java:597) at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:273) at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:183) at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:173) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:168) at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:160) at java.awt.EventDispatchThread.run(EventDispatchThread.java:121) */ public class JOGLWithNVidiaTwinView { /** * @param args Not used */ public static void main(String[] args) { final XScreenDevice[] twinViewPair = XScreenDevice.findTwinViewScreens(); if (twinViewPair == null) { System.out.println("No TwinView screen pair was found, exiting."); System.exit(1); } /* * Create a new Window on one head. */ final JFrame frame = new JFrame(twinViewPair[0].device.getDefaultConfiguration()); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setSize(200, 200); /* * Set the frame visible. We will wait until the window is opened before * moving it. */ try { openAndWait(frame); System.out.println("Frame is on device with ID: " + frame.getGraphicsConfiguration().getDevice().getIDstring()); System.out.flush(); addCanvas(frame); System.out.println("Moving window to second device..."); System.out.flush(); /* * Get the bounds of the second device and move the frame there. */ final Rectangle secondDeviceBounds = twinViewPair[1].device.getDefaultConfiguration().getBounds(); moveTo(frame, secondDeviceBounds); System.out.println("Frame is on device with ID: " + frame.getGraphicsConfiguration().getDevice().getIDstring()); System.out.println("Adding GLCanvas..."); System.out.flush(); addCanvas(frame); } catch (InterruptedException ie) { System.out.println("Interrupted..."); } } /** * Creates and adds a new GLCanvas to the specified JFrame, blocking * until the operation is complete (do not call this from the Event Thread) * * @param frame * @throws InterruptedException */ static void addCanvas(final JFrame frame) throws InterruptedException { final boolean[] added = new boolean[] { false }; final GLCanvas canvas = new GLCanvas(); final ContainerListener listener = new ContainerAdapter() { @Override public void componentAdded(ContainerEvent e) { if (e.getComponent() == canvas) { synchronized (added) { added[0] = true; added.notifyAll(); } } } }; EventQueue.invokeLater(new Runnable() { public void run() { final GLCanvas canvas = new GLCanvas(); try { frame.getContentPane().add(canvas); } catch (IllegalArgumentException iae) { System.out.println("Caught exception while adding canvas: "); System.out.flush(); iae.printStackTrace(); } } }); try { synchronized (added) { while (!added[0]) added.wait(); } } finally { frame.removeContainerListener(listener); } } /** * Moves the specified frame somewhere (top-left corner) of the specified rectangle, blocking until * the operation is complete (do not call this from the Event Thread). * * @param frame * @param target * @throws InterruptedException */ static void moveTo(final JFrame frame, final Rectangle target) throws InterruptedException { final boolean[] moved = new boolean[] { target.contains(frame.getX(), frame.getY(), frame.getWidth(), frame .getHeight()) }; final ComponentListener listener = new ComponentAdapter() { public void componentMoved(ComponentEvent e) { synchronized (moved) { if (moved[0] = target.contains(frame.getX(), frame.getY(), frame.getWidth(), frame.getHeight())) { moved.notifyAll(); System.out.println("Moved to: " + frame.getX() + ", " + frame.getY()); } else { System.out.println("Moved somewhere unexpected: " + frame.getX() + ", " + frame.getY()); } } } }; frame.addComponentListener(listener); EventQueue.invokeLater(new Runnable() { public void run() { frame.setLocation(target.x, target.y); } }); try { synchronized (moved) { while (!moved[0]) moved.wait(); } } finally { frame.removeComponentListener(listener); } } /** * Sets the specified frame visible and waits for a corresponding WindowEvent. * Do not call from Event Thread. * * @param frame * @throws InterruptedException */ static void openAndWait(JFrame frame) throws InterruptedException { final boolean[] open = new boolean[] { frame.isVisible() }; final WindowListener listener = new WindowAdapter() { public void windowOpened(WindowEvent e) { synchronized (open) { open[0] = true; open.notifyAll(); } } }; frame.addWindowListener(listener); frame.setVisible(true); try { synchronized (open) { while (!open[0]) open.wait(); } } finally { frame.removeWindowListener(listener); } } /** * Class to extract some (Unix, or possible Linux-specific) * info from a GraphicsDevice instance's idString. */ static class XScreenDevice { public final GraphicsDevice device; public final String hostName; public final String serverID; public final String displayID; public XScreenDevice(GraphicsDevice device) { this.device = device; String idStr = device.getIDstring(); final int colonIdx = idStr.indexOf(':'); if (colonIdx < 0) { throw new IllegalArgumentException("The specified device (id: " + idStr + ") does not appear to be an X screen device."); } hostName = idStr.substring(0, colonIdx); idStr = idStr.substring(colonIdx + 1); final int dotIdx = idStr.indexOf('.'); if (dotIdx < 0) { displayID = ""; serverID = idStr; } else { serverID = idStr.substring(0, dotIdx); displayID = idStr.substring(dotIdx + 1); } } /** * Checks whether the specified device might be involved in a TwinView * configuration with this device. This means that the hostNames and * serverIDs must be identical, and the displayID and device must be * different. * * @param device A non-null XScreenDevice. * @return true if the specified device is (probably) in * a TwinView configuration with this device, false otherwise. */ public boolean isTwinViewComplement(XScreenDevice device) { return device.device != this.device && device.hostName.equals(this.hostName) && device.hostName.equals(this.hostName) && !device.displayID.equals(this.displayID); } public String toString() { return "X Screen Device -- id: " + device.getIDstring() + " (host=" + hostName + ", serverID=" + serverID + ", displayID=" + displayID + ")"; } /** * Identifies a pair of TwinView screens in the local graphics * environment. A TwinView pair (for the purposes of this method) has X * display IDs comprised of identical hostnames, followed by a colon, * identical server ids, followed by a '.', and unique display ids. For * example, ':0.0' and ':0.1' would be a pair. * * * @return A pair of XScreenDevice instances representing two distinct * GraphicsDevices in the local environment that are believed to * be a TwinView pair, or null if none are found. */ public static XScreenDevice[] findTwinViewScreens() { final GraphicsEnvironment graphicsEnv = GraphicsEnvironment.getLocalGraphicsEnvironment(); final GraphicsDevice[] screenDevices = graphicsEnv.getScreenDevices(); if (screenDevices == null || screenDevices.length < 2) { /* * Not enough devices for a TwinView pair. */ return null; } for (int firstScreenIdx = 1; firstScreenIdx < screenDevices.length; firstScreenIdx++) { final XScreenDevice firstDevice; try { firstDevice = new XScreenDevice(screenDevices[firstScreenIdx]); System.out.println("Searching for TwinView complement to: " + firstDevice); } catch (IllegalArgumentException iae) { System.out.println("Device: " + screenDevices[firstScreenIdx].getIDstring() + " does not appear to be an X screen"); continue; } for (int secondScreenIdx = firstScreenIdx - 1; secondScreenIdx >= 0; secondScreenIdx--) { try { final XScreenDevice secondDevice = new XScreenDevice(screenDevices[secondScreenIdx]); System.out.println("\tTesting: " + secondDevice); if (firstDevice.isTwinViewComplement(secondDevice)) { System.out.println("TwinView Pair Identified!"); return new XScreenDevice[] { firstDevice, secondDevice }; } } catch (IllegalArgumentException iae) { /* * Already warned above... */ } } } /* * None found. */ return null; } } }