aboutsummaryrefslogtreecommitdiff
path: root/java/org/cef/browser/CefBrowserOsr.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/org/cef/browser/CefBrowserOsr.java')
-rw-r--r--java/org/cef/browser/CefBrowserOsr.java275
1 files changed, 271 insertions, 4 deletions
diff --git a/java/org/cef/browser/CefBrowserOsr.java b/java/org/cef/browser/CefBrowserOsr.java
index 18a9d1e..c7acf60 100644
--- a/java/org/cef/browser/CefBrowserOsr.java
+++ b/java/org/cef/browser/CefBrowserOsr.java
@@ -6,21 +6,27 @@ package org.cef.browser;
import com.jetbrains.cef.JCefAppConfig;
import com.jogamp.nativewindow.NativeSurface;
+import com.jogamp.opengl.GL;
+import com.jogamp.opengl.GL2;
import com.jogamp.opengl.GLAutoDrawable;
import com.jogamp.opengl.GLCapabilities;
import com.jogamp.opengl.GLContext;
import com.jogamp.opengl.GLEventListener;
import com.jogamp.opengl.GLProfile;
import com.jogamp.opengl.awt.GLCanvas;
+import com.jogamp.opengl.util.GLBuffers;
import org.cef.CefClient;
import org.cef.OS;
import org.cef.callback.CefDragData;
import org.cef.handler.CefRenderHandler;
+import org.cef.handler.CefScreenInfo;
import java.awt.Component;
import java.awt.Cursor;
import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.GraphicsConfiguration;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.dnd.DropTarget;
@@ -33,7 +39,22 @@ import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
+import java.awt.geom.AffineTransform;
+import java.awt.image.AffineTransformOp;
+import java.awt.image.BufferedImage;
+import java.lang.ClassNotFoundException;
+import java.lang.IllegalAccessException;
+import java.lang.IllegalArgumentException;
+import java.lang.NoSuchMethodException;
+import java.lang.SecurityException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
import java.nio.ByteBuffer;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
import javax.swing.MenuSelectionManager;
import javax.swing.SwingUtilities;
@@ -47,8 +68,12 @@ class CefBrowserOsr extends CefBrowser_N implements CefRenderHandler {
private CefRenderer renderer_;
private GLCanvas canvas_;
private long window_handle_ = 0;
+ private boolean justCreated_ = false;
private Rectangle browser_rect_ = new Rectangle(0, 0, 1, 1); // Work around CEF issue #1437.
private Point screenPoint_ = new Point(0, 0);
+ private double scaleFactor_ = 1.0;
+ private int depth = 32;
+ private int depth_per_component = 8;
private boolean isTransparent_;
CefBrowserOsr(CefClient client, String url, boolean transparent, CefRequestContext context) {
@@ -65,6 +90,7 @@ class CefBrowserOsr extends CefBrowser_N implements CefRenderHandler {
@Override
public void createImmediately() {
+ justCreated_ = true;
// Create the browser immediately.
createBrowserIfRequired(false);
}
@@ -104,11 +130,76 @@ class CefBrowserOsr extends CefBrowser_N implements CefRenderHandler {
GLProfile glprofile = GLProfile.getMaxFixedFunc(true);
GLCapabilities glcapabilities = new GLCapabilities(glprofile);
canvas_ = new GLCanvas(glcapabilities) {
+ private Method scaleFactorAccessor = null;
+ private boolean removed_ = true;
+
@Override
public void paint(Graphics g) {
createBrowserIfRequired(true);
+ if (g instanceof Graphics2D) {
+ GraphicsConfiguration config = ((Graphics2D) g).getDeviceConfiguration();
+ depth = config.getColorModel().getPixelSize();
+ depth_per_component = config.getColorModel().getComponentSize()[0];
+
+ if (OS.isMacintosh()
+ && System.getProperty("java.runtime.version").startsWith("1.8")) {
+ // This fixes a weird thing on MacOS: the scale factor being read from
+ // getTransform().getScaleX() is incorrect for Java 8 VMs; it is always
+ // 1, even though Retina display scaling of window sizes etc. is
+ // definitely ongoing somewhere in the lower levels of AWT. This isn't
+ // too big of a problem for us, because the transparent scaling handles
+ // the situation, except for one thing: the screenshot-grabbing
+ // code below, which reads from the OpenGL context, must know the real
+ // scale factor, because the image to be read is larger by that factor
+ // and thus a bigger buffer is required. This is why there's some
+ // admittedly-ugly reflection magic going on below that's able to get
+ // the real scale factor.
+ // All of this is not relevant for either Windows or MacOS JDKs > 8,
+ // for which the official "getScaleX()" approach works fine.
+ try {
+ if (scaleFactorAccessor == null) {
+ scaleFactorAccessor = getClass()
+ .getClassLoader()
+ .loadClass("sun.awt.CGraphicsDevice")
+ .getDeclaredMethod("getScaleFactor");
+ }
+ Object factor = scaleFactorAccessor.invoke(config.getDevice());
+ if (factor instanceof Integer) {
+ scaleFactor_ = ((Integer) factor).doubleValue();
+ } else {
+ scaleFactor_ = 1.0;
+ }
+ } catch (InvocationTargetException | IllegalAccessException
+ | IllegalArgumentException | NoSuchMethodException
+ | SecurityException | ClassNotFoundException exc) {
+ scaleFactor_ = 1.0;
+ }
+ } else {
+ scaleFactor_ = ((Graphics2D) g).getTransform().getScaleX();
+ }
+ }
super.paint(g);
}
+
+ @Override
+ public void addNotify() {
+ super.addNotify();
+ if (removed_) {
+ notifyAfterParentChanged();
+ removed_ = false;
+ }
+ }
+
+ @Override
+ public void removeNotify() {
+ if (!removed_) {
+ if (!isClosed()) {
+ notifyAfterParentChanged();
+ }
+ removed_ = true;
+ }
+ super.removeNotify();
+ }
};
// The GLContext will be re-initialized when changing displays, resulting in calls to
@@ -119,7 +210,7 @@ class CefBrowserOsr extends CefBrowser_N implements CefRenderHandler {
GLAutoDrawable glautodrawable, int x, int y, int width, int height) {
browser_rect_.setBounds(canvas_.getBounds()/*x, y, width, height*/); // [tav] todo: revise it
screenPoint_ = canvas_.getLocationOnScreen();
- wasResized(width, height);
+ wasResized(newWidth, newHeight);
}
@Override
@@ -275,12 +366,15 @@ class CefBrowserOsr extends CefBrowser_N implements CefRenderHandler {
}
@Override
- public void onCursorChange(CefBrowser browser, final int cursorType) {
+ public boolean onCursorChange(CefBrowser browser, final int cursorType) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
canvas_.setCursor(new Cursor(cursorType));
}
});
+
+ // OSR always handles the cursor change.
+ return true;
}
@Override
@@ -308,9 +402,182 @@ class CefBrowserOsr extends CefBrowser_N implements CefRenderHandler {
createBrowser(getClient(), windowHandle, getUrl(), true, isTransparent_, null,
getRequestContext());
}
- } else {
- // OSR windows cannot be reparented after creation.
+ } else if (hasParent && justCreated_) {
+ notifyAfterParentChanged();
setFocus(true);
+ justCreated_ = false;
+ }
+ }
+
+ private void notifyAfterParentChanged() {
+ // With OSR there is no native window to reparent but we still need to send the
+ // notification.
+ getClient().onAfterParentChanged(this);
+ }
+
+ @Override
+ public boolean getScreenInfo(CefBrowser browser, CefScreenInfo screenInfo) {
+ screenInfo.Set(scaleFactor_, depth, depth_per_component, false, browser_rect_.getBounds(),
+ browser_rect_.getBounds());
+
+ return true;
+ }
+
+ @Override
+ public CompletableFuture<BufferedImage> createScreenshot(boolean nativeResolution) {
+ int width = (int) (canvas_.getWidth() * scaleFactor_);
+ int height = (int) (canvas_.getHeight() * scaleFactor_);
+
+ // In order to grab a screenshot of the browser window, we need to get the OpenGL internals
+ // from the GLCanvas that displays the browser.
+ GL2 gl = canvas_.getGL().getGL2();
+ int textureId = renderer_.getTextureID();
+
+ // This mirrors the two ways in which CefRenderer may render images internally - either via
+ // an incrementally updated texture that is the same size as the window and simply rendered
+ // onto a textured quad by graphics hardware, in which case we capture the data directly
+ // from this texture, or by directly writing pixels into the OpenGL framebuffer, in which
+ // case we directly read those pixels back. The latter is the way chosen if there is no
+ // hardware rasterizer capability detected. We can simply distinguish both approaches by
+ // looking whether the textureId of the renderer is a valid (non-zero) one.
+ boolean useReadPixels = (textureId == 0);
+
+ // This Callable encapsulates the pixel-reading code. After running it, the screenshot
+ // BufferedImage contains the grabbed image.
+ final Callable<BufferedImage> pixelGrabberCallable = new Callable<BufferedImage>() {
+ @Override
+ public BufferedImage call() {
+ BufferedImage screenshot =
+ new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
+ ByteBuffer buffer = GLBuffers.newDirectByteBuffer(width * height * 4);
+
+ gl.getContext().makeCurrent();
+ try {
+ if (useReadPixels) {
+ // If pixels are copied directly to the framebuffer, we also directly read
+ // them back.
+ gl.glReadPixels(
+ 0, 0, width, height, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buffer);
+ } else {
+ // In this case, read the texture pixel data from the previously-retrieved
+ // texture ID
+ gl.glEnable(GL.GL_TEXTURE_2D);
+ gl.glBindTexture(GL.GL_TEXTURE_2D, textureId);
+ gl.glGetTexImage(
+ GL.GL_TEXTURE_2D, 0, GL.GL_RGBA, GL.GL_UNSIGNED_BYTE, buffer);
+ gl.glDisable(GL.GL_TEXTURE_2D);
+ }
+ } finally {
+ gl.getContext().release();
+ }
+
+ for (int y = 0; y < height; y++) {
+ for (int x = 0; x < width; x++) {
+ // The OpenGL functions only support RGBA, while Java BufferedImage uses
+ // ARGB. We must convert.
+ int r = (buffer.get() & 0xff);
+ int g = (buffer.get() & 0xff);
+ int b = (buffer.get() & 0xff);
+ int a = (buffer.get() & 0xff);
+ int argb = (a << 24) | (r << 16) | (g << 8) | (b << 0);
+ // If pixels were read from the framebuffer, we have to flip the resulting
+ // image on the Y axis, as the OpenGL framebuffer's y axis starts at the
+ // bottom of the image pointing "upwards", while BufferedImage has the
+ // origin in the upper left corner. This flipping is done when drawing into
+ // the BufferedImage.
+ screenshot.setRGB(x, useReadPixels ? (height - y - 1) : y, argb);
+ }
+ }
+
+ if (!nativeResolution && scaleFactor_ != 1.0) {
+ // HiDPI images should be resized down to "normal" levels
+ BufferedImage resized =
+ new BufferedImage((int) (screenshot.getWidth() / scaleFactor_),
+ (int) (screenshot.getHeight() / scaleFactor_),
+ BufferedImage.TYPE_INT_ARGB);
+ AffineTransform tempTransform = new AffineTransform();
+ tempTransform.scale(1.0 / scaleFactor_, 1.0 / scaleFactor_);
+ AffineTransformOp tempScaleOperation =
+ new AffineTransformOp(tempTransform, AffineTransformOp.TYPE_BILINEAR);
+ resized = tempScaleOperation.filter(screenshot, resized);
+ return resized;
+ } else {
+ return screenshot;
+ }
+ }
+ };
+
+ if (SwingUtilities.isEventDispatchThread()) {
+ // If called on the AWT event thread, just access the GL API
+ try {
+ BufferedImage screenshot = pixelGrabberCallable.call();
+ return CompletableFuture.completedFuture(screenshot);
+ } catch (Exception e) {
+ CompletableFuture<BufferedImage> future = new CompletableFuture<BufferedImage>();
+ future.completeExceptionally(e);
+ return future;
+ }
+ } else {
+ // If called from another thread, register a GLEventListener and trigger an async
+ // redraw, during which we use the GL API to grab the pixel data. An unresolved Future
+ // is returned, on which the caller can wait for a result (but not with the Event
+ // Thread, as we need that for pixel grabbing, which is why there's a safeguard in place
+ // to catch that situation if it accidentally happens).
+ CompletableFuture<BufferedImage> future = new CompletableFuture<BufferedImage>() {
+ private void safeguardGet() {
+ if (SwingUtilities.isEventDispatchThread()) {
+ throw new RuntimeException(
+ "Waiting on this Future using the AWT Event Thread is illegal, "
+ + "because it can potentially deadlock the thread.");
+ }
+ }
+
+ @Override
+ public BufferedImage get() throws InterruptedException, ExecutionException {
+ safeguardGet();
+ return super.get();
+ }
+
+ @Override
+ public BufferedImage get(long timeout, TimeUnit unit)
+ throws InterruptedException, ExecutionException, TimeoutException {
+ safeguardGet();
+ return super.get(timeout, unit);
+ }
+ };
+ canvas_.addGLEventListener(new GLEventListener() {
+ @Override
+ public void reshape(
+ GLAutoDrawable aDrawable, int aArg1, int aArg2, int aArg3, int aArg4) {
+ // ignore
+ }
+
+ @Override
+ public void init(GLAutoDrawable aDrawable) {
+ // ignore
+ }
+
+ @Override
+ public void dispose(GLAutoDrawable aDrawable) {
+ // ignore
+ }
+
+ @Override
+ public void display(GLAutoDrawable aDrawable) {
+ canvas_.removeGLEventListener(this);
+ try {
+ future.complete(pixelGrabberCallable.call());
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ }
+ });
+
+ // This repaint triggers an indirect call to the listeners' display method above, which
+ // ultimately completes the future that we return immediately.
+ canvas_.repaint();
+
+ return future;
}
}
}