/*
* Copyright 2015 Brandon Borkholder
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jogamp.glg2d;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.LayoutManager2;
import java.io.Serializable;
import java.util.logging.Logger;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.GLCapabilities;
import com.jogamp.opengl.GLCapabilitiesImmutable;
import com.jogamp.opengl.GLContext;
import com.jogamp.opengl.GLDrawableFactory;
import com.jogamp.opengl.GLEventListener;
import com.jogamp.opengl.GLOffscreenAutoDrawable;
import com.jogamp.opengl.GLProfile;
import com.jogamp.opengl.Threading;
import com.jogamp.opengl.awt.GLCanvas;
import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.RepaintManager;
import com.jogamp.opengl.util.Animator;
/**
* This canvas redirects all paints to an OpenGL canvas. The drawable component
* can be any JComponent. This is a simple implementation to allow manual
* painting of a JComponent scene to OpenGL. A {@code G2DGLCanvas} is more
* appropriate when rendering a complex scene using
* {@link JComponent#paintComponent(Graphics)} and the {@code Graphics2D}
* object.
*
* <p>
* GL drawing can be enabled or disabled using the {@code setGLDrawing(boolean)}
* method. If GL drawing is enabled, all full paint requests are intercepted and
* the drawable component is drawn to the OpenGL canvas.
* </p>
*
* <p>
* Override {@link #createGLComponent(GLCapabilitiesImmutable, GLContext)} to
* create the OpenGL canvas. The returned canvas may be a {@code GLJPanel} or a
* {@code GLCanvas}. {@link #createG2DListener(JComponent)} is used to create
* the {@code GLEventListener} that will draw to the OpenGL canvas. Use
* {@link #getGLDrawable()} if you want to attach an {@code Animator}.
* Otherwise, paints will only happen when requested (either with
* {@code repaint()} or from AWT).
* </p>
*/
public class GLG2DCanvas extends JComponent {
private static final long serialVersionUID = -471481443599019888L;
protected GLAutoDrawable canvas;
protected GLCapabilitiesImmutable chosenCapabilities;
protected GLEventListener g2dglListener;
/**
* @see #removeNotify()
*/
private GLAutoDrawable sideContext;
private JComponent drawableComponent;
private boolean drawGL;
/**
* Returns the default, desired OpenGL capabilities needed for this component.
*/
public static GLCapabilities getDefaultCapabalities() {
GLCapabilities caps = new GLCapabilities(GLProfile.getGL2ES1());
caps.setRedBits(8);
caps.setGreenBits(8);
caps.setBlueBits(8);
caps.setAlphaBits(8);
caps.setDoubleBuffered(true);
caps.setHardwareAccelerated(true);
caps.setNumSamples(4);
caps.setBackgroundOpaque(false);
caps.setSampleBuffers(true);
return caps;
}
/**
* Creates a new, blank {@code G2DGLCanvas} using the default capabilities
* from {@link #getDefaultCapabalities()}.
*/
public GLG2DCanvas() {
this(getDefaultCapabalities());
}
/**
* Creates a new, blank {@code G2DGLCanvas} using the given OpenGL
* capabilities.
*/
public GLG2DCanvas(GLCapabilities capabilities) {
canvas = createGLComponent(capabilities, null);
/*
* Set both canvas and drawableComponent to be the same size, but we never
* draw the drawableComponent except into the canvas.
*/
setLayout(new GLOverlayLayout());
add((Component) canvas);
setGLDrawing(true);
RepaintManager.setCurrentManager(GLAwareRepaintManager.INSTANCE);
}
/**
* Creates a new {@code G2DGLCanvas} where {@code drawableComponent} fills the
* canvas. This uses the default capabilities from
* {@link #getDefaultCapabalities()}.
*/
public GLG2DCanvas(JComponent drawableComponent) {
this();
setDrawableComponent(drawableComponent);
}
/**
* Creates a new {@code G2DGLCanvas} where {@code drawableComponent} fills the
* canvas.
*/
public GLG2DCanvas(GLCapabilities capabilities, JComponent drawableComponent) {
this(capabilities);
setDrawableComponent(drawableComponent);
}
/**
* Returns {@code true} if the {@code drawableComonent} is drawn using OpenGL
* libraries. If {@code false}, it is using normal Java2D drawing routines.
*/
public boolean isGLDrawing() {
return drawGL;
}
/**
* Sets the drawing path, {@code true} for OpenGL, {@code false} for normal
* Java2D.
*
* @see #isGLDrawing()
*/
public void setGLDrawing(boolean drawGL) {
if (this.drawGL != drawGL) {
this.drawGL = drawGL;
((Component) canvas).setVisible(drawGL);
setOpaque(drawGL);
firePropertyChange("gldrawing", !drawGL, drawGL);
repaint();
}
}
/**
* Gets the {@code JComponent} to be drawn to the OpenGL canvas.
*/
public JComponent getDrawableComponent() {
return drawableComponent;
}
/**
* Sets the {@code JComponent} that will be drawn to the OpenGL canvas.
*/
public void setDrawableComponent(JComponent component) {
if (component == drawableComponent) {
return;
}
if (g2dglListener != null) {
canvas.removeGLEventListener(g2dglListener);
if (sideContext != null) {
sideContext.removeGLEventListener(g2dglListener);
}
}
if (drawableComponent != null) {
remove(drawableComponent);
}
drawableComponent = component;
if (drawableComponent != null) {
verifyHierarchy(drawableComponent);
g2dglListener = createG2DListener(drawableComponent);
canvas.addGLEventListener(g2dglListener);
if (sideContext != null) {
sideContext.addGLEventListener(g2dglListener);
}
add(drawableComponent);
}
}
/**
* Checks the component and all children to ensure that everything is pure
* Swing. We can only draw lightweights.
*
*
* We'll also set PopupMenus to heavyweight and fix JViewport blitting.
*/
protected void verifyHierarchy(Component comp) {
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
if (comp instanceof JComponent) {
((JComponent) comp).setDoubleBuffered(false);
}
if (!(comp instanceof JComponent)) {
Logger.getLogger(GLG2DCanvas.class.getName()).warning("Drawable component and children should be pure Swing: " +
comp + " does not inherit JComponent");
}
if (comp instanceof JViewport) {
((JViewport) comp).setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
}
if (comp instanceof Container) {
Container cont = (Container) comp;
for (int i = 0; i < cont.getComponentCount(); i++) {
verifyHierarchy(cont.getComponent(i));
}
}
}
/**
* Gets the {@code GLAutoDrawable} used for drawing. By default this is a
* {@link GLCanvas}, but can be changed by overriding
* {@link #createGLComponent(GLCapabilitiesImmutable, GLContext)}.
*
* <p>
* Use the returned {@code GLAutoDrawable} as input to an {@link Animator} to
* automate painting.
* </p>
*/
public GLAutoDrawable getGLDrawable() {
return canvas;
}
/**
* Creates a {@code Component} that is also a {@code GLAutoDrawable}. This is
* where all the drawing takes place. The advantage of a {@code GLCanvas} is
* that it is faster, but a {@code GLJPanel} is more portable. The component
* should also be disabled so that it does not receive events that should be
* sent to the {@code drawableComponent}. A {@code GLCanvas} is a heavyweight
* component and on some platforms will not pass through mouse events even
* though it is disabled. A {@code GLJPanel} supports this better.
*/
protected GLAutoDrawable createGLComponent(GLCapabilitiesImmutable capabilities, GLContext shareWith) {
GLCanvas canvas = new GLCanvas(capabilities);
if (shareWith != null) {
canvas.setSharedContext(shareWith);
}
canvas.setEnabled(false);
chosenCapabilities = (GLCapabilitiesImmutable) capabilities.cloneMutable();
return canvas;
}
/**
* Creates the GLEventListener that will draw the given component to the
* canvas.
*/
protected GLEventListener createG2DListener(JComponent drawingComponent) {
return new GLG2DSimpleEventListener(drawingComponent);
}
/**
* Calling {@link GLCanvas#removeNotify()} destroys the GLContext. We could
* mess with that internally, but this is slightly easier.
* <p>
* This method is particularly important for docking frameworks and moving the
* panel from one window to another. This is simple for normal Swing
* components, but GL contexts are destroyed when {@code removeNotify()} is
* called.
* </p>
* <p>
* Our workaround is to use context sharing. The pbuffer is initialized and by
* drawing into it at least once, we automatically share all textures, etc.
* with the new pbuffer. This pbuffer holds the data until we can initialize
* our new JOGL canvas. We share the pbuffer canvas with the new JOGL canvas
* and everything works nicely from then on.
* </p>
* <p>
* This has the unfortunate side-effect of leaking memory. I'm not sure how to
* fix this yet.
* </p>
*/
@Override
public void removeNotify() {
prepareSideContext();
remove((Component) canvas);
super.removeNotify();
canvas = createGLComponent(chosenCapabilities, sideContext.getContext());
canvas.addGLEventListener(g2dglListener);
add((Component) canvas, 0);
}
private void prepareSideContext() {
if (sideContext == null) {
GLDrawableFactory factory = canvas.getFactory();
sideContext = factory.createOffscreenAutoDrawable(null, chosenCapabilities, null, 1, 1);
((GLOffscreenAutoDrawable) sideContext).setSharedContext(canvas.getContext());
sideContext.addGLEventListener(g2dglListener);
}
Runnable work = new Runnable() {
@Override
public void run() {
sideContext.display();
}
};
if (Threading.isOpenGLThread()) {
work.run();
} else {
Threading.invokeOnOpenGLThread(false, work);
}
}
@Override
public void paint(Graphics g) {
if (isGLDrawing() && drawableComponent != null && canvas != null) {
canvas.display();
} else {
super.paint(g);
}
}
@Override
protected void paintChildren(Graphics g) {
/*
* Don't paint the drawableComponent. If we'd use a GLJPanel instead of a
* GLCanvas, we'd have to paint it here.
*/
if (!isGLDrawing()) {
super.paintChildren(g);
}
}
@Override
protected void addImpl(Component comp, Object constraints, int index) {
if (comp == canvas || comp == drawableComponent) {
super.addImpl(comp, constraints, index);
} else {
throw new IllegalArgumentException("Do not add component to this. Add them to the object in getDrawableComponent()");
}
}
/**
* Implements a simple layout where all the components are the same size as
* the parent.
*/
protected static class GLOverlayLayout implements LayoutManager2, Serializable {
private static final long serialVersionUID = -8248213786715565045L;
@Override
public Dimension preferredLayoutSize(Container parent) {
if (parent.isPreferredSizeSet() || parent.getComponentCount() == 0) {
return parent.getPreferredSize();
} else {
int x = -1, y = -1;
for (Component child : parent.getComponents()) {
Dimension dim = child.getPreferredSize();
x = Math.max(dim.width, x);
y = Math.max(dim.height, y);
}
return new Dimension(x, y);
}
}
@Override
public Dimension minimumLayoutSize(Container parent) {
if (parent.getComponentCount() == 0) {
return new Dimension(0, 0);
} else {
int x = Integer.MAX_VALUE, y = Integer.MAX_VALUE;
for (Component child : parent.getComponents()) {
Dimension dim = child.getMinimumSize();
x = Math.min(dim.width, x);
y = Math.min(dim.height, y);
}
return new Dimension(x, y);
}
}
@Override
public Dimension maximumLayoutSize(Container parent) {
if (parent.getComponentCount() == 0) {
return new Dimension(0, 0);
} else {
int x = -1, y = -1;
for (Component child : parent.getComponents()) {
Dimension dim = child.getMaximumSize();
x = Math.max(dim.width, x);
y = Math.max(dim.height, y);
}
return new Dimension(x, y);
}
}
@Override
public void layoutContainer(Container parent) {
for (Component child : parent.getComponents()) {
child.setSize(parent.getSize());
}
}
@Override
public void addLayoutComponent(String name, Component comp) {
// nop
}
@Override
public void addLayoutComponent(Component comp, Object constraints) {
// nop
}
@Override
public void removeLayoutComponent(Component comp) {
// nop
}
@Override
public void invalidateLayout(Container target) {
// nop
}
@Override
public float getLayoutAlignmentX(Container target) {
return 0.5f;
}
@Override
public float getLayoutAlignmentY(Container target) {
return 0.5f;
}
}
}