TestAwtPanels: lwjgl2 vs lwjgl3 + Swing/AWT

Hello everyone,
I have a question. Is it correct that class TestAwtPanels only works with LWJGL2, and with LWJGL3 it returns the following error message?

nov 14, 2024 7:36:47 PM com.jme3.system.JmeDesktopSystem initialize
INFO: Running on jMonkeyEngine 3.7.0-stable
 * Branch: HEAD
 * Git Hash: bc6cdf5
 * Build Date: 2024-10-21
nov 14, 2024 7:36:50 PM com.jme3.system.lwjgl.LwjglContext printContextInitInfo
INFO: LWJGL 3.3.2+13 context running on thread jME3 Main
 * Graphics Adapter: GLFW 3.4.0 Win32 WGL Null EGL OSMesa VisualC DLL
nov 14, 2024 7:36:50 PM com.jme3.system.lwjgl.LwjglCanvas printContextInitInfo
INFO: Initializing LWJGL3-AWT with jMonkeyEngine
 *  Double Buffer: true
 *  Stereo: false
 *  Red Size: 8
 *  Rreen Size: 8
 *  Blue Size: 8
 *  Alpha Size: 0
 *  Depth Size: 24
 *  Stencil Size: 0
 *  Accum Red Size: 0
 *  Accum Green Size: 0
 *  Accum Blue Size: 0
 *  Accum Alpha Size: 0
 *  Sample Buffers: 0
 *  Share Context: null
 *  Major Version: 3
 *  Minor Version: 2
 *  Forward Compatible: false
 *  Profile: COMPATIBILITY
 *  API: GL
 *  Debug: false
 *  Swap Interval: 1
 *  SRGB (Gamma Correction): true
 *  Pixel Format Float: false
 *  Context Release Behavior: null
 *  Color Samples NV: 0
 *  Swap Group NV: 0
 *  Swap Barrier NV: 0
 *  Robustness: false
 *  Lose Context On Reset: false
 *  Context Reset Isolation: false
nov 14, 2024 7:36:50 PM com.jme3.renderer.opengl.GLRenderer loadCapabilitiesCommon
INFO: OpenGL Renderer Information
 * Vendor: Intel
 * Renderer: Intel(R) Iris(R) Xe Graphics
 * OpenGL Version: 3.2.0 - Build 31.0.101.4032
 * GLSL Version: 1.50 - Build 31.0.101.4032
 * Profile: Core
nov 14, 2024 7:36:50 PM com.jme3.renderer.opengl.GLRenderer setMainFrameBufferSrgb
WARNING: Driver claims that default framebuffer is not sRGB capable. Enabling anyway.
nov 14, 2024 7:36:51 PM com.jme3.audio.openal.ALAudioRenderer initOpenAL
INFO: Audio Renderer Information
 * Device: OpenAL Soft
 * Vendor: OpenAL Community
 * Renderer: OpenAL Soft
 * Version: 1.1 ALSOFT 1.23.1
 * Supported channels: 64
 * ALC extensions: ALC_ENUMERATE_ALL_EXT ALC_ENUMERATION_EXT ALC_EXT_CAPTURE ALC_EXT_DEDICATED ALC_EXT_disconnect ALC_EXT_EFX ALC_EXT_thread_local_context ALC_SOFT_device_clock ALC_SOFT_HRTF ALC_SOFT_loopback ALC_SOFT_loopback_bformat ALC_SOFT_output_limiter ALC_SOFT_output_mode ALC_SOFT_pause_device ALC_SOFT_reopen_device
 * AL extensions: AL_EXT_ALAW AL_EXT_BFORMAT AL_EXT_DOUBLE AL_EXT_EXPONENT_DISTANCE AL_EXT_FLOAT32 AL_EXT_IMA4 AL_EXT_LINEAR_DISTANCE AL_EXT_MCFORMATS AL_EXT_MULAW AL_EXT_MULAW_BFORMAT AL_EXT_MULAW_MCFORMATS AL_EXT_OFFSET AL_EXT_source_distance_model AL_EXT_SOURCE_RADIUS AL_EXT_STATIC_BUFFER AL_EXT_STEREO_ANGLES AL_LOKI_quadriphonic AL_SOFT_bformat_ex AL_SOFTX_bformat_hoa AL_SOFT_block_alignment AL_SOFT_buffer_length_query AL_SOFT_callback_buffer AL_SOFTX_convolution_reverb AL_SOFT_deferred_updates AL_SOFT_direct_channels AL_SOFT_direct_channels_remix AL_SOFT_effect_target AL_SOFT_events AL_SOFT_gain_clamp_ex AL_SOFTX_hold_on_disconnect AL_SOFT_loop_points AL_SOFTX_map_buffer AL_SOFT_MSADPCM AL_SOFT_source_latency AL_SOFT_source_length AL_SOFT_source_resampler AL_SOFT_source_spatialize AL_SOFT_source_start_delay AL_SOFT_UHJ AL_SOFT_UHJ_ex
nov 14, 2024 7:36:51 PM com.jme3.audio.openal.ALAudioRenderer initOpenAL
INFO: Audio effect extension version: 1.0
nov 14, 2024 7:36:51 PM com.jme3.audio.openal.ALAudioRenderer initOpenAL
INFO: Audio max auxiliary sends: 2
nov 14, 2024 7:36:53 PM com.jme3.app.LegacyApplication handleError
SEVERE: Exception while creating the OpenGL context
java.awt.AWTException: sRGB color space requested but WGL_EXT_framebuffer_sRGB is unavailable
	at org.lwjgl.opengl.awt.PlatformWin32GLCanvas.create(PlatformWin32GLCanvas.java:438)
	at org.lwjgl.opengl.awt.PlatformWin32GLCanvas.create(PlatformWin32GLCanvas.java:156)
	at com.jme3.system.lwjgl.LwjglCanvas$LwjglAWTGLCanvas.beforeRender(LwjglCanvas.java:248)
	at com.jme3.system.lwjgl.LwjglCanvas.runLoop(LwjglCanvas.java:623)
	at com.jme3.system.lwjgl.LwjglWindow.run(LwjglWindow.java:719)
	at java.base/java.lang.Thread.run(Thread.java:834)

nov 14, 2024 7:37:02 PM com.jme3.system.lwjgl.LwjglCanvas$LwjglAWTGLCanvas removeNotify
WARNING: Windows does not support this functionality: remove(__canvas__)

lwjgl2 supports Swing/AWT.

lwjgl3 does not support Swing/AWT… it’s a limitation of glfw or whatever.

…and one of the things that keeps me on lwjgl2.

2 Likes

In LWJGL 3 we are using LWJGL3-AWT to provide the support. It works kinda. You can just switch off the gamma to get over this. I don’t know if there is something more deeper to this.

 *  SRGB (Gamma Correction): true
WARNING: Driver claims that default framebuffer is not sRGB capable. Enabling anyway.
3 Likes

Thanks for your answers guys. I have a couple more doubts.


  1. The TestSafeCanvas class centers the JME Canvas within the JFrame, but it doesn’t expand to fill the entire window. What could be causing this behavior?

* Here is a code snippet from the TestSafeCanvas class (see the original file for more details):

        final TestSafeCanvas app = new TestSafeCanvas();
        AppSettings settings = new AppSettings(true);
        settings.setWidth(640);
        settings.setHeight(480);
   
        app.setSettings(settings);
        app.setPauseOnLostFocus(false);
        app.createCanvas();
        app.startCanvas(true);

        JmeCanvasContext context = (JmeCanvasContext) app.getContext();
        Canvas canvas = context.getCanvas();
        canvas.setSize(settings.getWidth(), settings.getHeight());

        ...
        // In this case the JME Canvas is not centered in the JFrame
        frame.getContentPane().add(canvas);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

  1. What are the advantages and disadvantages of using TestSafeCanvas and TestAwtPanels with AwtPanelsContext as a custom renderer? Which approach offers better performance and flexibility?

* Here is a code snippet from the TestAwtPanels class (see the original file for more details):

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

        final AwtPanelsContext ctx = (AwtPanelsContext) app.getContext();
        AwtPanel panel = ctx.createPanel(PaintMode.Accelerated);
        panel.setPreferredSize(new Dimension(400, 300));
        ctx.setInputSource(panel);

        ...
        // In this case the JME Canvas is centered in the JFrame
        frame.getContentPane().setLayout(new BorderLayout());
        frame.getContentPane().add(panel, BorderLayout.CENTER);
        frame.pack();
        frame.setLocation(null);
        frame.setVisible(true);

Last year, I was working on a lightweight framework that integrates JME, Swing and Jfx all in one environment (The Hybrid Jme Project), I was facing similar issues, and I overcame it by adding the canvas to a JFxPanel (which is a JPanel with a support for JFx components), before adding it to the JFrame (so, you will attach the JPanel to the JFrame at the end)… I literally cannot remember what was the exact issue, but I think it’s something with the Swing Canvas dimensions that cannot be redrawn once created, only the canvas components could be updated so far, but I cannot remember the exact issue, I might be wrong, but this approach works so far…

EDIT:
In your case, if you aren’t using JavaFx, then JPanel should work just fine.

I’m using Java Swing, not JavaFX. Regarding the TestSafeCanvas class, I have already tried this solution and it does not work.

Hi @capdevon

The TestSafeCanvas class centers the JME Canvas within the JFrame, but it doesn’t expand to fill the entire window

It’s strange that the TestSafeCanvas class doesn’t work correctly (at least I don’t have problems with it), are you running this example directly from jme3? (from the screenshot I think not; correct me if I’m wrong)

Note

This particular class breaks when you remove and re-add the canvas in the component (Windows)

What could be causing this behavior?

Oddly enough, apparently when you embed a GL canvas in a component that does not have a BorderLayout layout (or another that does not trigger a listener that notifies such changes when resizing said component) it will not resize the drawing area . especially if we use GroupLayout.

You can create a component (JPanel or a custom one) with a BorderLayout layout and only containing the jme3 canvas (add the canvas in the center of the component).

AppSettings settings = new AppSettings(true);
settings.setWidth(640);
settings.setHeight(480);

MyAppCanvas app = new MyAppCanvas();
app.setPauseOnLostFocus(false);
app.setSettings(settings);
app.createCanvas();
app.startCanvas(true);

JmeCanvasContext context = (JmeCanvasContext) app.getContext();
Canvas canvas = context.getCanvas();
canvas.setPreferredSize(new Dimension(settings.getWidth(), settings.getHeight()));

JPanel myRootPane = new JPanel();
myRootPane.setLayout(new BorderLayout());
myRootPane.add(canvas, BorderLayout.CENTER);

JFrame frame = new JFrame("JME3 - AWT/Swing Canvas");
frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
frame.addWindowListener(new WindowAdapter() {
	@Override
	public void windowClosing(WindowEvent e) {
		app.stop();
	}
});
frame.getContentPane().add(myRootPane);
frame.pack();

frame.setLocationRelativeTo(null);
frame.setVisible(true);

Hi @SwiftWolf ,

Have you had a chance to run your code? Does the Canvas center correctly in the Frame as expected?

I’ve tried your BorderLayout suggestion with lwjgl2, but the Canvas still isn’t centering. Any other ideas?

Here is the test class. Can you run it on your computer and tell me what result you get?

Thank you

package jme3test.awt;

import com.jme3.app.SimpleApplication;
import com.jme3.material.Material;
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.Canvas;
import java.awt.Dimension;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class TestSafeCanvas extends SimpleApplication {

    public static void main(String[] args) {
        AppSettings settings = new AppSettings(true);
        settings.setResolution(640, 480);

        final TestSafeCanvas app = new TestSafeCanvas();
        app.setPauseOnLostFocus(false);
        app.setSettings(settings);
        app.createCanvas();
        app.startCanvas(true);

        JmeCanvasContext context = (JmeCanvasContext) app.getContext();
        Canvas canvas = context.getCanvas();
        canvas.setPreferredSize(new Dimension(settings.getWidth(), settings.getHeight()));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }

        JFrame frame = new JFrame("Test");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                app.stop();
            }
        });
        
        JPanel container = new JPanel();
        container.setLayout(new BorderLayout());
        container.add(canvas, BorderLayout.CENTER);

        frame.getContentPane().add(container);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    @Override
    public void simpleInitApp() {
        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.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
        geom.setMaterial(mat);
        rootNode.attachChild(geom);
    }
}

Is it normal to share code without testing it :thinking:?

On both Linux and Windows I have no problems centering it.

First I would like to ask you a few things.

  1. Are you using lwjgl2?
  2. Do you have the same problem with lwjgl2 or does it only happen with lwjgl3?

If you are with lwjgl3, can you try resizing the canvas manually and check that the java.awt.event.ComponentAdapter#componentResized(ComponentEvent e) listener fires correctly?

I have no problems on both systems, I can run it without problems (Copying the code you provided).

[ Windows 11 ]


[ Linux ]

Hi @SwiftWolf ,
I think I found the source of the confusion around the test class results. It seems the behavior depends on whether we’re using LWJGL 2 or 3:

  • LWJGL 2:
    • The Canvas isn’t centered in the Frame, regardless of using a BorderLayout.CENTER Panel or manual resizing.
    • Mouse behavior: Click/release with flyCam.setDragToRotate(true) doesn’t re-center the cursor during camera rotation.
  • LWJGL 3:
    • We need to set settings.setGammaCorrection(false) to avoid program crashes.
    • The Canvas is centered by default (no need for BorderLayout.CENTER Panel).
    • Mouse behavior: Click/release with flyCam.setDragToRotate(true) re-centers the cursor for camera rotation.

This difference in behavior explains why some configurations work in one and not the other.

Note: The mouse problem is caused by the different behavior of the libraries when FlyByCamera enables/disables cursor visibility. This behavior is currently being discussed in an open issue here.


1. Here is code and screenshots of the test with lwjgl2

(run with jme3.7.0-stable, Windows 11):

public class Test_SafeCanvas extends SimpleApplication {

    public static void main(String[] args) {
        AppSettings settings = new AppSettings(true);
        settings.setResolution(640, 480);

        final Test_SafeCanvas app = new Test_SafeCanvas();
        app.setPauseOnLostFocus(false);
        app.setSettings(settings);
        app.createCanvas();
        app.startCanvas(true);

        JmeCanvasContext context = (JmeCanvasContext) app.getContext();
        Canvas canvas = context.getCanvas();
        canvas.setPreferredSize(new Dimension(settings.getWidth(), settings.getHeight()));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }

        JFrame frame = new JFrame("Test - LWJGL2");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                app.stop();
            }
        });
        
        JPanel container = new JPanel();
        container.setLayout(new BorderLayout());
        container.add(canvas, BorderLayout.CENTER);

        frame.getContentPane().add(container);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    @Override
    public void simpleInitApp() {
        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.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
        geom.setMaterial(mat);
        rootNode.attachChild(geom);
    }

}


2. Here is code and screenshots of the test with lwjgl3

(run with jme3.7.0-stable, Windows 11):

public class Test_SafeCanvas extends SimpleApplication {

    public static void main(String[] args) {
        AppSettings settings = new AppSettings(true);
        settings.setGammaCorrection(false); /* for lwjgl3 */
        settings.setResolution(640, 480);

        final Test_SafeCanvas app = new Test_SafeCanvas();
        app.setPauseOnLostFocus(false);
        app.setSettings(settings);
        app.createCanvas();
        app.startCanvas(true);

        JmeCanvasContext context = (JmeCanvasContext) app.getContext();
        Canvas canvas = context.getCanvas();
        canvas.setPreferredSize(new Dimension(settings.getWidth(), settings.getHeight()));

        try {
            Thread.sleep(3000);
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }

        JFrame frame = new JFrame("Test - LWJGL3");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        frame.addWindowListener(new WindowAdapter() {
            @Override
            public void windowClosing(WindowEvent e) {
                app.stop();
            }
        });
        
        // This panel is not necessary-to center the Canvas
//        JPanel container = new JPanel();
//        container.setLayout(new BorderLayout());
//        container.add(canvas, BorderLayout.CENTER);

        frame.getContentPane().add(canvas);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    @Override
    public void simpleInitApp() {
        flyCam.setDragToRotate(true);
        flyCam.setMoveSpeed(10f);
        viewPort.setBackgroundColor(ColorRGBA.DarkGray);

        Box b = new Box(1, 1, 1);
        Geometry geom = new Geometry("Box", b);
        Material mat = new Material(assetManager, "Common/MatDefs/Misc/Unshaded.j3md");
        mat.setTexture("ColorMap", assetManager.loadTexture("Interface/Logo/Monkey.jpg"));
        geom.setMaterial(mat);
        rootNode.attachChild(geom);
    }

}

Edit:

It remains to clarify my second doubt. Any suggestions?

3 Likes

Hi @capdevon

So far, my editor uses LWJGL2 + Swing Canvas (Openjdk17), because when I started writing this editor, LWJGL3 did not support Swing Canvas.

I did not use any additional Swing libraries, nor did I use any layout managers. I don’t like those layout managers very much.

I defined a free layout component “Region” by myself, and all custom components are basically based on this component.

public abstract class Region extends JPanel implements RoundCorner {
    
    protected int width;
    protected int height;
    private final AtomicBoolean needUpdateLayout = new AtomicBoolean(false);
    private final LayoutUpdater layoutUpdater = new LayoutUpdater();
    private final Dimension tempSize = new Dimension();
    private int preferWidth = -1;
    private int preferHeight = -1;
    private boolean forceUpdateLayout;
    
    public Region() {
        this(null);
    }
    
    public Region(String name) {
        super.setName(name);
        super.setLayout(null);
        super.setOpaque(false);
    }
    
    @Override
    public final void setPreferredSize(Dimension preferredSize) {
        setSize(preferredSize.width, preferredSize.height);
    }
    
    @Override
    public final void setSize(Dimension d) {
        setSize(d.width, d.height);
    }
    
    @Override
    public void setSize(int width, int height) {        
        // save performance.
        if (forceUpdateLayout || width != tempSize.width || height != tempSize.height) {
            forceUpdateLayout = false;
            tempSize.setSize(width, height);
            super.setPreferredSize(tempSize);
            super.setSize(width, height); 
            this.width = width;
            this.height = height;
            updateLayout();
        }
    }
    
    @Override
    protected void addImpl(Component comp, Object constraints, int index) {
        super.addImpl(comp, constraints, index); 
        forceUpdateLayout = true;
    }
    
    @Override
    public void remove(int index) {
        super.remove(index);
        forceUpdateLayout = true;
    }
    
    @Override
    public void removeAll() {
        super.removeAll(); 
        forceUpdateLayout = true;
    }
    
    public void setNeedUpdateLayout() {
        if (needUpdateLayout.get())
            return;
        needUpdateLayout.set(true);
        SwingUtilities.invokeLater(layoutUpdater);
    }
    
    private class LayoutUpdater implements Runnable {
        @Override
        public void run() {
            if (needUpdateLayout.getAndSet(false)) {
                updateLayout();
            }
        }
    }
    
    public void setPreferWidth(int preferWidth) {
        this.preferWidth = preferWidth;
    }
    
    public void setPreferHeight(int preferHeight) {
        this.preferHeight = preferHeight;
    }

    public int getPreferWidth() {
        return preferWidth;
    }
    
    public int getPreferHeight() {
        return preferHeight;
    }

    @Override
    public void setRoundCorner(int[] corner) {
        // for subclass
    }
    
    /**
     * update the layout.
     */
    protected abstract void updateLayout();
}

The most important method is “updateLayout”

Thanks to this component, all levels of components in my entire interface can be adjusted completely freely. Although writing layout code is a bit cumbersome, it is easy to understand and adjust.
So far, this method works well and I am very satisfied with it.

Here are some reference codes about how my editor is initialized:

public class Main extends JFrame {
    private final static Logger LOG = Logger.getLogger(Main.class.getName());
    private static Main main;
    private final int width = 1280;
    private final int height = 720;
    private static MainLayout mainLayout; 
    private static App app;
    private static Canvas canvas;
    
    public static void main(String[] args) {
        // Initialize logger first.
        App.initializeLogger();
        UILookAndFeel.initLookAndFeel();
        UILookAndFeel.initDefaultFont();
        UIService.runOnUI(() -> {
            Main.main = new Main();
            main.setTitle(AppVersion.getAppName() 
                    + "-" + AppVersion.getAppVersion() 
                    + "-" + AppVersion.getEditorName());
            main.setLocationRelativeTo(null);
            main.setExtendedState(Frame.MAXIMIZED_BOTH);
            main.setVisible(true);
        });
    }
    
    public Main() {
        mainLayout = new MainLayout(this);
        super.setLayout(null);
        super.setBackground(StyleConstants.REGION_BACKGROUND);
        super.setContentPane(mainLayout);
        super.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
                Insets is = getInsets();
                Dimension size = new Dimension(e.getComponent().getWidth() - is.left - is.right
                        , e.getComponent().getHeight() - is.top - is.bottom);
                mainLayout.setSize(size);
            }
        });
        super.setSize(width, height);
        
        initApp();
        initLogo();
        GlobalKeyManager.initialize();
    }
    
    @Override
    protected void processWindowEvent(WindowEvent e) {  
        if (e.getID() == WindowEvent.WINDOW_CLOSING)  {
            exit();
            return;
        }
        super.processWindowEvent(e); 
    } 
    
    private void initApp() {
        AppSettings settings = new AppSettings(true);
        settings.setRenderer("CUSTOM" + se.ui.SeCanvasContext.class.getName());
        settings.setVSync(ConfigManager.isVSync());
        settings.setFrameRate(ConfigManager.getFrameRate());
        settings.setSamples(ConfigManager.getNumSamples());
        settings.setGammaCorrection(ConfigManager.isGammaCorrection());
        settings.setResolution(ConfigManager.getScreenWidth(), ConfigManager.getScreenHeight());
        
        app = new App(AppMode.EDITOR);
        app.setSettings(settings);
        app.setPauseOnLostFocus(false);
        app.createCanvas();
        app.startCanvas();
        canvas = ((JmeCanvasContext) app.getContext()).getCanvas();
        canvas.setBackground(StyleConstants.REGION_BACKGROUND);
        
        ThreadManager.submit(() -> {
            waitEditor();
            
            UIService.runOnCore(() -> {
                File dataRoot = UIService.loadData();
                if (dataRoot != null && dataRoot.exists() && dataRoot.isDirectory()) {
                    SystemApi.setDataRoot(dataRoot);
                }
                
                EditApi.attachAppState(new ShortcutAppState());
                EditApi.switchMove();

                UIService.runOnUI(() -> {
                    mainLayout.onEditorInitialized(canvas, (result) -> {
                        UIService.runOnCore(() -> {
                            EditApi.loadScene(ConfigManager.getStartScene());
                        });
                    });
                });
            });
        });
    }
    
    private void initLogo() {
        try {
            List<? extends Image> list = Arrays.asList(
                    ImageIO.read(Main.class.getResourceAsStream(ResConstants.LOGO32))
                  , ImageIO.read(Main.class.getResourceAsStream(ResConstants.LOGO64))
            );
            setIconImages(list);
        } catch (IOException e) {
            LOG.log(Level.WARNING, "Could not load logo! logo16={0}, logo32={1}"
                    , new Object[]{ResConstants.LOGO32, ResConstants.LOGO64});
        }
    }
    
    public final static Main getInstance() {
        return main;
    }
    
    public final static MainLayout getMainLayout() {
        return mainLayout;
    }
    
    public final static Canvas getCanvas() {
        return canvas;
    }
    
    public final static App getApp() {
        return app;
    }
    
    public final static void exit() {
        if (EditApi.isSceneModified()) {
            UIService.runOnCore(() -> {
                boolean xmlFormat = EditApi.isXmlFormatScenePath();
                UIService.runOnUI(() -> {
                    SaveDialog.showSaveDialog(mainLayout, (String savedScenePath) -> {
                        exitActual();
                    }, xmlFormat);
                });
            });
        } else {
            exitActual();
        }
    }
    
    private static void exitActual() {
        LOG.log(Level.INFO, "Exit app!");
        System.exit(0);
    }
    
    private static void waitEditor() {
        while (!EditApi.isInitialized() || !SystemApi.isInitialized()) {
            try {
                Thread.sleep(20);
            } catch (InterruptedException ex) {
                Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
            }
        }
    }
}
public class MainLayout extends Region {

    private final MenuModule menuModule;
    private final AssetZone assetZone;
    private final EditZone editModule;
    private final WindowZone windowZone;
    
    private final SplitRegion assetSplit;
    private final SplitRegion windowSplit;
    
    private int assetZoneWidth = StyleConstants.LAYOUT_ASSET_WIDTH;
    private int windowZoneWidth = StyleConstants.LAYOUT_WINDOW_WIDTH;
    
    // Drag to adjust the width of the window.
    private boolean dragStarted;
    private Point startLoc;
    private Point startLocOnScreen;
    
    public MainLayout(Main main) {
        super();
        menuModule = new MenuModule(main);
        
        assetZone = new AssetZone();
        editModule = new EditZone();
        windowZone = new WindowZone();
        assetSplit = new SplitRegion();
        windowSplit = new SplitRegion();

        MouseAdapter ma = new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (e.getSource() == windowSplit) {
                    windowSplit.setHighLight(true);
                } else if (e.getSource() == assetSplit) {
                    assetSplit.setHighLight(true);
                }
            }
            
            @Override
            public void mouseDragged(MouseEvent e) {
                JComponent dragComponent = (JComponent) e.getSource();
                if (!dragStarted) {
                    startLocOnScreen = dragComponent.getLocationOnScreen();
                    startLoc = dragComponent.getLocation();
                    dragStarted = true;
                    return;
                }
                int xMoved = e.getLocationOnScreen().x - startLocOnScreen.x;
                dragComponent.setLocation(startLoc.x + xMoved, startLoc.y);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (dragStarted) {
                    dragStarted = false;
                    int xMoved = e.getLocationOnScreen().x - startLocOnScreen.x;
                    // Limit the size of the area to prevent users from making the area too large and unable to restore.
                    int maxWidth = width / 2;
                    if (e.getSource() == windowSplit) {
                        windowZoneWidth -= xMoved;
                        if (windowZoneWidth > maxWidth) {
                            windowZoneWidth = maxWidth;
                        }
                    } else if (e.getSource() == assetSplit) {
                        assetZoneWidth += xMoved;
                        if (assetZoneWidth > maxWidth) {
                            assetZoneWidth = maxWidth;
                        }
                    }
                    setNeedUpdateLayout();
                }
                windowSplit.setHighLight(false);
                assetSplit.setHighLight(false);
            }
        };
        assetSplit.addMouseListener(ma);
        assetSplit.addMouseMotionListener(ma);
        windowSplit.addMouseListener(ma);
        windowSplit.addMouseMotionListener(ma);
        
        super.add(assetSplit);
        super.add(windowSplit);
        super.add(menuModule);
        super.add(assetZone);
        super.add(windowZone);
        super.add(editModule);
    }
    
    void onEditorInitialized(Canvas canvas, Callback callback) {
        menuModule.onEditorInitialized();
        windowZone.onEditorInitialized();
        assetZone.onEditorInitialized();
        editModule.onEditorInitialized(canvas);
        
        UIService.runOnCore(() -> {
            
            // Listener data events on UI thread.
            SystemApi.addDataListener((File dataRoot) -> {
                UIService.runOnUI(() -> {
                    assetZone.notifyDataRootChanged();
                    reloadStartScene();
                });
            });
            
            SystemApi.addSceneListener(new SceneListener() {
                @Override
                public void onSceneCleanStart() {
                    windowZone.onCleanStart();
                }

                @Override
                public void onSceneCleanEnd() {
                    windowZone.onCleanEnd();
                }

                @Override
                public void onSceneLoadStart() {
                    editModule.onLoadStart();
                    windowZone.onLoadStart();
                }

                @Override
                public void onSceneLoadEnd() {
                    editModule.onLoadEnd();
                    windowZone.onLoadEnd();
                }
            });
            
            SystemApi.addSceneListener(new SceneListener() {
                @Override
                public void onSceneChanged(String newScenePath) {
                    editModule.onSceneChanged(newScenePath);
                    windowZone.onSceneChanged(newScenePath);
                }
            });
            callback.onDone(true);
        });
    }

    @Override
    protected void updateLayout() {
        if (height <= 0)
            return;
        
        int menusH = StyleConstants.ITEM_HEIGHT;
        int assetPanelW = assetZoneWidth;
        int assetPanelH = height - menusH;
        int winW = windowZoneWidth;
        int winH = height - menusH;
        int windowsDragW = StyleConstants.SPLIT_BAR_SIZE;
        int windowsDragH = height;
        int editModuleW = width - assetPanelW - winW;
        int editModuleH = height - menusH;
        int assetDragW = StyleConstants.SPLIT_BAR_SIZE;
        int assetDragH = height;
        
        menuModule.setSize(width, menusH);
        assetZone.setSize(assetPanelW, assetPanelH);
        editModule.setSize(editModuleW, editModuleH);
        windowZone.setSize(winW, winH);
        windowSplit.setSize(windowsDragW, windowsDragH);
        assetSplit.setSize(assetDragW, assetDragH);
        
        int x = 0;
        int y = 0;
        menuModule.setLocation(x, y);
        y += menusH;
        
        assetZone.setLocation(x, y);
        x += assetPanelW;
        assetSplit.setLocation(x - assetDragW, y);
        
        editModule.setLocation(x, y);
        x += editModuleW;
        
        windowZone.setLocation(x, y);
        windowSplit.setLocation(x, y);
    }
    
    public DataModule getAssetsModule() {
        return assetZone.getAssetsModule();
    }
    
    public ItemModule getItemModule() {
        return assetZone.getItemModule();
    }
    
    public EditZone getEditRegion() {
        return editModule;
    }
    
    private void reloadStartScene() {
        UIService.runOnCore(() -> {
            String startScene = ConfigManager.getStartScene();
            EditApi.loadScene(startScene);
        });
    }
}
public class EditZone extends Region {
    
    private Canvas canvas;
    private SceneEdit sceneEdit;
    private DropTarget dropTarget;
    
    public EditZone() {
        super();
        super.setFocusable(true);
        super.addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                EditZone.this.requestFocusInWindow();
            }
        });
    }

    public void onEditorInitialized(Canvas canvas) {
        this.canvas = canvas;
        createDragAndDrop(canvas);
        
        sceneEdit = new SceneEdit(canvas);
        sceneEdit.onEditorInitialized();
        super.add(sceneEdit);
        updateLayout();
        repaint();
    }
    
    public void onSceneChanged(String newScenePath) {
        if (sceneEdit != null) {
            sceneEdit.onSceneChanged(newScenePath);
        }
    }
    
    public void onLoadStart() {
        if (sceneEdit != null) {
            sceneEdit.onLoadStart();
        }
    }

    public void onLoadEnd() {
        if (sceneEdit != null) {
            sceneEdit.onLoadEnd();
        }
    }
    
    @Override
    protected void updateLayout() {
        if (sceneEdit != null) {
            sceneEdit.setSize(width, height);
            sceneEdit.setLocation(0, 0);
        }
    }
    
    public void updatePropertyPanel(EntityId entityId) {
        if (sceneEdit == null) 
            return;
        sceneEdit.updatePropertyPanel(entityId);
    }
    
    public SceneEdit getSceneEdit() {
        return this.sceneEdit;
    }
    
    private void createDragAndDrop(Canvas canvas) {
        DropTargetAdapter dta = new DropTargetAdapter() {
            @Override
            public void drop(DropTargetDropEvent dtde) {
                try {
                    Transferable tf = dtde.getTransferable();
                    if (tf.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) {
                        dtde.acceptDrop(DnDConstants.ACTION_COPY_OR_MOVE);
                        List list = (List) tf.getTransferData(DataFlavor.javaFileListFlavor);
                        onDropFiles(list, dtde.getLocation());
                        dtde.dropComplete(true);
                    } else {
                        dtde.rejectDrop();
                    }
                } catch (Exception e) {
                    Logger.getLogger(EditZone.class.getName()).log(Level.SEVERE, null, e);
                }
            }
        };
        dropTarget = new DropTarget(canvas, DnDConstants.ACTION_COPY_OR_MOVE, dta);
    }
    
    private void onDropFiles(List<File> files, Point location) {
        if (files == null || files.isEmpty())
            return;
        Vector2f screenPos = new Vector2f(location.x, canvas.getHeight() - location.y);
        UIService.runOnCore(() -> {
            EditApi.addFiles(files, screenPos);
        });
    }
}
public class SceneEdit extends BorderRegion {
    
    private final Title title;
    private final ToolbarPanel toolbar;
    private final CanvasPanel canvasPanel;
    private final PropertyPanel property;
    private final SceneToolbar sceneToolbar;
    
    private final SceneSplitRegion toolbarDrag = new SceneSplitRegion();
    private final SceneSplitRegion propertyDrag = new SceneSplitRegion();
    
    private int toolbarWidth = StyleConstants.LAYOUT_TOOLBAR_WIDTH;
    private int propertyWidth = StyleConstants.LAYOUT_PROPERTY_WIDTH;
    
    private boolean dragStarted;
    private Point startLoc;
    private Point startLocOnScreen;
    
    public SceneEdit(Canvas canvas) {
        super();
        super.setPaintBorder(true, false, false, true);
        super.setHighLightAllBorder(true);
        this.title = new Title();
        this.canvasPanel = new CanvasPanel(canvas);
        this.sceneToolbar = new SceneToolbar();
        this.property = new PropertyPanel(this);
        this.toolbar = new ToolbarPanel(this);
        super.add(title);
        super.add(toolbarDrag);
        super.add(toolbar);
        super.add(propertyDrag);
        super.add(property);
        super.add(sceneToolbar);
        super.add(canvasPanel);

        MouseAdapter ma = new MouseAdapter() {
            
            @Override
            public void mousePressed(MouseEvent e) {
                if (e.getSource() == toolbarDrag) {
                    toolbarDrag.setHighLight(true);
                } else if (e.getSource() == propertyDrag) {
                    propertyDrag.setHighLight(true);
                }
            }
            
            @Override
            public void mouseDragged(MouseEvent e) {
                JComponent dragComponent = (JComponent) e.getSource();
                if (!dragStarted) {
                    if (e.getSource() == toolbarDrag) {
                        if (!isToolbarVisible()) {
                            setToolbarVisible(true);
                            toolbarWidth = StyleConstants.SPLIT_BAR_SIZE;
                        }
                    } else if (e.getSource() == propertyDrag) {
                        if (!isPropertyVisible()) {
                            setPropertyVisible(true);
                            propertyWidth = StyleConstants.SPLIT_BAR_SIZE;
                        }
                    }
                    startLocOnScreen = dragComponent.getLocationOnScreen();
                    startLoc = dragComponent.getLocation();
                    dragStarted = true;
                    return;
                }
                int xMoved = e.getLocationOnScreen().x - startLocOnScreen.x;
                dragComponent.setLocation(startLoc.x + xMoved, startLoc.y);
            }

            @Override
            public void mouseReleased(MouseEvent e) {
                if (dragStarted) {
                    dragStarted = false;
                    int xMoved = e.getLocationOnScreen().x - startLocOnScreen.x;
                    // Limit the size of the area to prevent users from making the area too large and unable to restore.
                    int maxWidth = width / 2;
                    if (e.getSource() == toolbarDrag) {
                        toolbarWidth += xMoved;
                        if (toolbarWidth > maxWidth) {
                            toolbarWidth = maxWidth;
                        }
                        if (toolbarWidth <= StyleConstants.SPLIT_BAR_SIZE) {
                            setToolbarVisible(false);
                        }
                    } else if (e.getSource() == propertyDrag) {
                        propertyWidth -= xMoved;
                        if (propertyWidth > maxWidth) {
                            propertyWidth = maxWidth;
                        }
                        if (propertyWidth <= StyleConstants.SPLIT_BAR_SIZE) {
                            setPropertyVisible(false);
                        }
                    }
                    setNeedUpdateLayout();
                }
                toolbarDrag.setHighLight(false);
                propertyDrag.setHighLight(false);
            }
            
            @Override
            public void mouseClicked(MouseEvent e) {
                if (!UIUtils.isMouseLeftSingleEvent(e)) return;
                if (e.getSource() == toolbarDrag) {
                    setToolbarVisible(!isToolbarVisible());
                } else if (e.getSource() == propertyDrag) {
                    setPropertyVisible(!isPropertyVisible());
                }
            }
        };
        
        toolbarDrag.addMouseListener(ma);
        toolbarDrag.addMouseMotionListener(ma);
        toolbarDrag.setToolTipText("Shortcut: T");
        toolbarDrag.setCursor(new Cursor(Cursor.E_RESIZE_CURSOR));
        toolbarDrag.displayRight();
        
        propertyDrag.addMouseListener(ma);
        propertyDrag.addMouseMotionListener(ma);
        propertyDrag.setToolTipText("Shortcut: N");
        propertyDrag.setCursor(new Cursor(Cursor.E_RESIZE_CURSOR));
        propertyDrag.displayRight();
        
        toolbar.setToolbarVisible(false); // Hide for default.
    }
    
    public void onEditorInitialized() {
        sceneToolbar.onEditorInitialized();
        property.onEditorInitialized();
        toolbar.onEditorInitialized();
    }
    
    public void onSceneChanged(String newScenePath) {
        UIService.runOnUI(() -> {
            setTitle(newScenePath);
        });
    }
    
    public void onLoadStart() {
        sceneToolbar.onLoadStart();
        property.onLoadStart();
        toolbar.onLoadStart();
    }

    public void onLoadEnd() {
        sceneToolbar.onLoadEnd();
        property.onLoadEnd();
        toolbar.onLoadEnd();
    }
  
    @Override
    protected void updateLayout() {
        boolean propertyVisible = isPropertyVisible();
        boolean toolbarVisible = isToolbarVisible();
        int titleH = StyleConstants.ITEM_HEIGHT;
        int toolbarDragW = StyleConstants.SPLIT_BAR_SIZE;
        int propertyDragW = StyleConstants.SPLIT_BAR_SIZE;
        int sceneToolbarH = 35;
        
        int toolbarW = toolbarVisible ? toolbarWidth : toolbarDragW;
        int toolbarH = height - titleH - sceneToolbarH;
        
        int propertyW = propertyVisible ? propertyWidth : propertyDragW;
        int propertyH = height - titleH - sceneToolbarH;
        
        int canvasPanelW = width - toolbarW - propertyW;
        // Reduce by 1px to prevent covering the top highlight of the scene toolbar
        int canvasPanelH = height - titleH - sceneToolbarH - 1; 
        
        title.setSize(width, titleH);
        toolbar.setSize(toolbarW, toolbarH);
        toolbarDrag.setSize(toolbarDragW, toolbarH);
        canvasPanel.setSize(canvasPanelW, canvasPanelH);
        propertyDrag.setSize(propertyDragW, propertyH);
        property.setSize(propertyW, propertyH);
        sceneToolbar.setSize(width, sceneToolbarH);

        int x = 0;
        int y = 0;
        title.setLocation(x, y);
        y += titleH;
        
        toolbar.setLocation(0, y);
        x += toolbarW - toolbarDragW;
        
        toolbarDrag.setLocation(x, y);
        x += toolbarDragW;
        
        canvasPanel.setLocation(x, y);
        x += canvasPanelW;
        
        propertyDrag.setLocation(x, y);
        property.setLocation(x, y);
        
        x = 0;
        y = titleH + canvasPanelH;
        sceneToolbar.setLocation(x, y);
    }
    
    private void setTitle(String title) {
        this.title.title.setText(title);
    }
    
    public boolean isPropertyVisible() {
        return property.isPropertyVisible();
    }
    
    public void setPropertyVisible(boolean visible) {
        if (visible == property.isPropertyVisible())
            return;
        property.setPropertyVisible(visible);
        if (visible) {
            propertyDrag.displayRight();
            if (propertyWidth <= StyleConstants.SPLIT_BAR_SIZE) {
                propertyWidth = StyleConstants.LAYOUT_PROPERTY_WIDTH;
            }
        } else {
            propertyDrag.displayLeft();
        }
        setNeedUpdateLayout();
    }
    
    public boolean isToolbarVisible() {
        return toolbar.isToolbarVisible();
    }
    
    public void setToolbarVisible(boolean visible) {
        if (visible == toolbar.isToolbarVisible())
            return;
        toolbar.setToolbarVisible(visible);
        if (visible) {
            toolbarDrag.displayLeft();
            if (toolbarWidth <= StyleConstants.SPLIT_BAR_SIZE) {
                toolbarWidth = StyleConstants.LAYOUT_TOOLBAR_WIDTH;
            }
        } else {
            toolbarDrag.displayRight();
        }
        setNeedUpdateLayout();
    }
    
    public void updatePropertyPanel(EntityId entityId) {
        property.updateEntity(entityId);
    }
    
    private class Title extends BorderRegion {
        
        private final JLabel title = new JLabel("");

        public Title() {
            super();
            super.add(title);
            title.setForeground(StyleConstants.FONT_NORMAL);
        }
        
        @Override
        protected void updateLayout() {
            int vSpace = StyleConstants.ITEM_VSPACE;
            title.setSize(width - vSpace * 2, height);
            title.setLocation(vSpace, 0);
        }
        
    }
    
    private class SceneSplitRegion extends SplitRegion {
        private final JLabel right = new JLabel(ImageUtils.loadIcon(IconConstants.ARROW_RIGHT));
        private final JLabel left = new JLabel(ImageUtils.loadIcon(IconConstants.ARROW_LEFT));
        public SceneSplitRegion() {
            super();
            super.add(right, 0);
            super.add(left, 0);
            right.setSize(16, 16);
            left.setSize(16, 16);
        }

        @Override
        protected void updateLayout() {
            super.updateLayout();
            int xOffset = (StyleConstants.SPLIT_BAR_SIZE - left.getWidth()) / 2;
            right.setLocation(xOffset, (height - right.getHeight()) / 2);
            left.setLocation(xOffset, (height - left.getHeight()) / 2);
        }
        
        void displayLeft() {
            left.setVisible(true);
            right.setVisible(false);
        }
        
        void displayRight() {
            left.setVisible(false);
            right.setVisible(true);
        }
    }
}

In addition, regarding the advantages and disadvantages of Canvas and AwtPanel, I have tried them before. In my personal opinion:

  1. Canvas has better performance, but not too good flexibility! Depends on the Swing Canvas component.
  2. AwtPanel is more flexible, but the performance is not so good, especially when the scene is large, the performance is seriously degraded, which seems to be related to the need to re-read data from the GPU back to the memory, refer to “AwtPanel.drawFrameInThread()”

For performance reasons, I gave up AwtPanel and chose Canvas.

I hope this helps you!

(Some translations are from Google Translate, don’t mind my poor English)

1 Like

There is something special about CanvasPanel

/**
 * This custom panel is used as the container for canvas.
 * Since the canvas is a heavy-weight component, it may cover other components, 
 * which can cause display issues.
 * 
 * It is also used to correct the scaling issues of the canvas on the Windows OS.
 * If the monitor's scaling is over 100%, such as 125%,150%, or above, the 
 * canvas will be displayed in the incorrect size. 
 * This issue appeared on win10 (I have no test on other systems)
 * 
 * @author foxcc
 */
public class CanvasPanel extends Region {
    
    private final Canvas canvas;
    private final boolean windowsOS;
    private boolean onResizing;
    
    public CanvasPanel(Canvas canvas) {
        super();
        super.add(canvas);
        this.canvas = canvas;
        windowsOS = JmeSystem.getPlatform().getOs() == Platform.Os.Windows;
    }

    public void setOnResizing(boolean onResizing) {
        this.onResizing = onResizing;
    }
    
    @Override
    protected void updateLayout() {
        canvas.setSize(width, height);
        canvas.setLocation(0, 0);
        
        // To fix the scaling issues on windows
        if (windowsOS && !onResizing) {
            GraphicsConfiguration gf = GraphicsEnvironment
                    .getLocalGraphicsEnvironment().getDefaultScreenDevice()
                    .getDefaultConfiguration();
            AffineTransform at = gf.getDefaultTransform();
            int boundW = (int) Math.floor(width * at.getScaleX());
            int boundH = (int) Math.floor(height * at.getScaleY());
            canvas.setBounds(0, 0, boundW, boundH);
        }
    }
}
1 Like

Thank you very much, @FoxCC , for sharing your knowledge. I couldn’t understand why the JME Canvas wasn’t centered in the JFrame on my PC. Thanks to your example code, I found out that the problem was due to the display setting on my laptop being scaled to 125%. I wonder if this is a bug with lwjgl2. This monitor configuration and the differing behaviors of the lwjgl2 and lwjgl3 libraries caused a lot of confusion. I also want to thank @SwiftWolf for his support.

  • Here is the test result with OS Windows 11, Display Scale 125%, JME Canvas (settings resolution 800x600), lwjgl2, class Test_SafeCanvas (see previous post) :

  • Here is the test result with OS Windows 11, Display Scale 100%, JME Canvas (settings resolution 800x600), lwjgl2, class Test_SafeCanvas (see previous post):

Yes, indeed, when using the AwtPanel, I feel that the graphics look more pixelated (as seen in the SDK) and have a lower resolution compared to Canvas. However, this is just my speculation, as I haven’t seen the internal code.

One last curiosity, what value did you set in the Renderer parameter?

AppSettings settings = new AppSettings(true);
settings.setRenderer("CUSTOM" + se.ui.SeCanvasContext.class.getName());

Glad to be of some help.

I don’t know the exact reason for the Canvas issue!
I don’t know much about lwjgl2 and lwjgl3.
I just found a simple solution to this problem from somewhere, and it works well so far for me.

About the

settings.setRenderer("CUSTOM" + se.ui.SeCanvasContext.class.getName())

It was pretty simple, I just wrote a SeCanvasContext (inherited from LwjglCanvas) and caught the exception in the runLoop, displayed a crash warning, and tried to save a temporary scene to avoid the user losing their work.

    @Override
    protected void runLoop() {
        try {
            
            if (crashExit) {
                displayCrashWarning();
                return;
            }
            
            super.runLoop();
            count = 0;
            
        } catch (Throwable e) {
            LOG.log(Level.WARNING, "RunLoop exception(Canvas)!!!error=" + e.getMessage(), e);
            count++;
            if (count >= maxCount) {
                count = 0;
                if (crashSave) {
                    crashSave();
                    crashExit = true;
                }
            }
        }
    }
1 Like