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:
- Canvas has better performance, but not too good flexibility! Depends on the Swing Canvas component.
- 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)