Swing Canvas - JMenuBar [SOLVED]

Hi all,

I am having problem with the swing JMenu component on the JFrame with JME canvas embedded in it.
I found the example “JME3 Canvas in a Swing GUI” extremely helpful and got it working fine. The JME app displays in the app window fine:

The problem I am having is that once I add the JMenuBar to the frame, it stays behind the JME canvas:

Any idea?? Any help much appreciated!

Here is the code:

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.math.Vector3f;
import com.jme3.renderer.RenderManager;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeCanvasContext;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.KeyEvent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;

/**
 * test
 * @author normenhansen
 */
public class Main extends SimpleApplication {

    public static void main(String[] args) {
    java.awt.EventQueue.invokeLater(new Runnable() {
        public void run() {
            
            AppSettings settings = new AppSettings(true);
            settings.setWidth(640);
            settings.setHeight(480);
            
            Main canvasApplication = new Main();
            canvasApplication.setSettings(settings);
            canvasApplication.createCanvas(); // create canvas!
            JmeCanvasContext ctx = (JmeCanvasContext) canvasApplication.getContext();
            ctx.setSystemListener(canvasApplication);
            Dimension dim = new Dimension(640, 480);
            ctx.getCanvas().setPreferredSize(dim);
            
            JFrame window = new JFrame("Swing Application");
            window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            
            JPanel panel = new JPanel(new FlowLayout()); // a panel
            // add the JME canvas
            panel.add(ctx.getCanvas());
            
            // Create basic menubar
            window.add(panel);
            JMenuBar menubar = new JMenuBar();
            JMenu file = new JMenu("File");
            file.setMnemonic(KeyEvent.VK_F);
            JMenuItem eMenuItem = new JMenuItem("Exit");
            eMenuItem.setMnemonic(KeyEvent.VK_E);
            eMenuItem.setToolTipText("Exit application");
            file.add(eMenuItem);
            menubar.add(file);
            window.setJMenuBar(menubar);
            
            window.pack();
            window.setVisible(true);
            
            canvasApplication.startCanvas();
        }
      });
    }

    @Override
    public void simpleInitApp() {
        // activate windowed input behaviour
        flyCam.setDragToRotate(true);
        
        Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        geom.setMaterial(mat);

        rootNode.attachChild(geom);
    }
}

You might try adding it to BorderLayout.CENTER.

…and I’m old school so I’m used to doing it:
window.getContentPane().add(panel, BorderLayout.CENTER);

Hi pspeed,

thanks for quick respond. I did replace the line of code as you suggested. This unfortunately didn’t help. The same problem remains:

Personally, I use AwtPanels instead because I understood canvas support to be a little dodgy… but it may be that things aren’t properly sized yet when pack() is done. You could try doing a swingutilities invoke from simpleInitApp() that calls pack() again maybe.

I did following edits based on your suggestion:

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
import com.jme3.math.ColorRGBA;
import com.jme3.scene.Geometry;
import com.jme3.scene.shape.Box;
import com.jme3.system.AppSettings;
import com.jme3.system.JmeCanvasContext;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.event.KeyEvent;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

/**
 * test
 * @author normenhansen
 */
public class Main extends SimpleApplication {
    static JFrame window;
    
    public static void main(String[] args) {
    java.awt.EventQueue.invokeLater(new Runnable() {
        public void run() {
            
            AppSettings settings = new AppSettings(true);
            settings.setWidth(640);
            settings.setHeight(480);
            
            Main canvasApplication = new Main();
            canvasApplication.setSettings(settings);
            canvasApplication.createCanvas(); // create canvas!
            JmeCanvasContext ctx = (JmeCanvasContext) canvasApplication.getContext();
            ctx.setSystemListener(canvasApplication);
            Dimension dim = new Dimension(640, 480);
            ctx.getCanvas().setPreferredSize(dim);
            
            window = new JFrame("Swing Application");
            window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            
            JPanel panel = new JPanel(new FlowLayout()); // a panel
            // add the JME canvas
            panel.add(ctx.getCanvas());
            
            // Create basic menubar
            window.getContentPane().add(panel, BorderLayout.CENTER);
            JMenuBar menubar = new JMenuBar();
            JMenu file = new JMenu("File");
            file.setMnemonic(KeyEvent.VK_F);
            JMenuItem eMenuItem = new JMenuItem("Exit");
            eMenuItem.setMnemonic(KeyEvent.VK_E);
            eMenuItem.setToolTipText("Exit application");
            file.add(eMenuItem);
            menubar.add(file);
            window.setJMenuBar(menubar);
            
            window.pack();
            window.setVisible(true);
            
            canvasApplication.startCanvas();
            
        }
      });
    }

    @Override
    public void simpleInitApp() {
        // activate windowed input behaviour
        flyCam.setDragToRotate(true);
        
        Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);

        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setColor("Color", ColorRGBA.Blue);
        geom.setMaterial(mat);

        rootNode.attachChild(geom);
        
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                window.pack();
            }
        });
    }
}

That didn’t help:

However what I noticed is that by replacing

window.add(panel);

with

window.getContentPane().add(panel, BorderLayout.CENTER);

there is an extra space added to the bottom of the screen which wasn’t there previously so we are getting closer:

Maybe don’t do this. FlowLayout is a bit dodgy.

Just add the canvas directly instead of wrapping it?

ok, I did replace the existing code:

            JPanel panel = new JPanel(new FlowLayout()); // a panel
            // add the JME canvas
            panel.add(ctx.getCanvas());
            
            // Create basic menubar
            window.getContentPane().add(panel, BorderLayout.CENTER);

with:

           window.getContentPane().add(ctx.getCanvas(), BorderLayout.CENTER);

This didn’t solve the issue, but it did remove the margin space around the canvas. It also kept the space at the bottom which I guess is the height of the JMenuBar:

I think we are close. Any other suggestions??

The JMenuBar item is accessible by the way and renders ok:

I’m running out of ideas and since I don’t use canvas I don’t know if it’s supposed to work properly or have any easy reference to a working app.

I can provide advice on AwtPanels but I’m not sure if that works for you or not.

Can I display the JME Canvas in AWT panel and still be able to use swing components for menu bars, popups, internal frames on the top of the AWT panel?

I have a swing application that uses JME’s AwtPanels class instead of canvas. It works fine and I’m able to lay out my app fine. I don’t know why canvas is not working for you and it could just be something simple that I don’t know… since I’ve never used it before I can’t really say.

For my app, I manage this in an app state:

public class AwtPanelState extends BaseAppState {
 
    // Other than initial setup these fields are _only_ accessed
    // from the swing thread except Viewport attachment
    private final Container container;
    private final Object constraints;   
    private volatile AwtPanel panel;
 
    private List<Runnable> enabledCommands = new CopyOnWriteArrayList<>(); 
    
    public AwtPanelState( Container container, Object constraints ) {
        this.container = container;
        this.constraints = constraints;               
    }
 
    public void addEnabledCommand( Runnable cmd ) {
        enabledCommands.add(cmd);
    }
    
    protected void initialize( Application app ) {
        try {        
            SwingUtilities.invokeAndWait(new InitializeCommand());
        } catch( InterruptedException | InvocationTargetException e ) {
            throw new RuntimeException("Error creating panel on swing thread", e);
        }

        // Can't unattach them so we might as well do it on init
        panel.attachTo(true, app.getViewPort(), app.getGuiViewPort());        
    }
    
    protected void onEnable() {
        SwingUtilities.invokeLater(new AttachPanelCommand());
    }
    
    protected void onDisable() {
        SwingUtilities.invokeLater(new DetachPanelCommand());
    }
    
    protected void cleanup( Application app ) {
    }
 
    private class InitializeCommand implements Runnable {
        public void run() {
 
            AwtPanelsContext ctx = (AwtPanelsContext)getApplication().getContext();
            panel = ctx.createPanel(PaintMode.Accelerated);
            panel.setPreferredSize(new Dimension(1280, 720));
            panel.setMinimumSize(new Dimension(400, 300));
            panel.setBackground(Color.black);
            ctx.setInputSource(panel);
        }
    }
 
    private class AttachPanelCommand implements Runnable {
        public void run() {
            // Add it to the container provided
            container.add(panel, constraints);
            
            for( Runnable r : enabledCommands ) {
                r.run();
            }
        }
    }
    
    private class DetachPanelCommand implements Runnable {
        public void run() {
            container.remove(panel);
        }
    }
}

Then in my application, I initialize it like:

public class TestApp extends SimpleApplication {

    private volatile JFrame mainFrame;

    public static void main(String[] args) throws Exception {

        JFrame.setDefaultLookAndFeelDecorated(true);
        JPopupMenu.setDefaultLightWeightPopupEnabled(false);
        UIManager.setLookAndFeel(new SubstanceGraphiteGlassLookAndFeel());

        final TestApp app = new TestApp();
        app.setShowSettings(false);

        AppSettings settings = new AppSettings(true);
        settings.setCustomRenderer(AwtPanelsContext.class);
        settings.setFrameRate(60);
        app.setSettings(settings);
        app.start();
    }

    public TestApp() throws Exception {
        super(new StatsAppState(), new DebugKeysAppState(), new BasicProfilerState(false),
              new FlyCamAppState());

        // Have to create the frame on the AWT EDT.
        SwingUtilities.invokeAndWait(new Runnable() {
            public void run() {
                mainFrame = new JFrame("Test App");
                mainFrame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
                mainFrame.addWindowListener(new WindowAdapter() {
                    @Override
                    public void windowClosed(WindowEvent e) {
                        stop();
                    }
                });

                mainFrame.setJMenuBar(createMainMenu());
                stateManager.attach(new AwtPanelState(rmainFrame, BorderLayout.CENTER));
            }
        });

        stateManager.getState(AwtPanelState.class).addEnabledCommand(new Runnable() {
            public void run() {
                if( !mainFrame.isVisible() ) {
                    // By now we should have the panel inside
                    mainFrame.pack();
                    mainFrame.setLocationRelativeTo(null);
                    mainFrame.setVisible(true);
                }
            }
        });

    }
...
    @Override
    public void simpleInitApp() {
        flyCam.setDragToRotate(true);

...

…something like that. I removed a bunch of my own stuff but I think the important bits are still there.

1 Like

if you want to use BorderLayout constants, first set the layout to BorderLayout. also put the menu bar to eg. BorderLayout.North

3 Likes

The_Leo, this is brilliant. Works like a charm! Thanks for help!

window.add(menubar, BorderLayout.NORTH);

2 Likes

Content pane should already have border layout and the setMenuBar() should already be putting the menu bar in the right place with respect to the internal JRootPane.

Something is funky that this isn’t working as written.

He shouldn’t have had to add the menu bar to the north of the content pane… because the menu bar shouldn’t even be in the content pane.