diff options
Diffstat (limited to 'java/org/cef/browser/CefBrowserOsr.java')
-rw-r--r-- | java/org/cef/browser/CefBrowserOsr.java | 275 |
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; } } } |