java/org/cef/browser/CefBrowserOsr.java (527 lines of code) (raw):
// Copyright (c) 2014 The Chromium Embedded Framework Authors. All rights
// reserved. Use of this source code is governed by a BSD-style license that
// can be found in the LICENSE file.
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.CefBrowserSettings;
import org.cef.CefClient;
import org.cef.OS;
import org.cef.callback.CefDragData;
import org.cef.handler.CefAcceleratedPaintInfo;
import org.cef.handler.CefRenderHandler;
import org.cef.handler.CefScreenInfo;
import org.cef.misc.CefRange;
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.datatransfer.StringSelection;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureRecognizer;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceAdapter;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DropTarget;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
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.ArrayList;
import java.util.Arrays;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import javax.swing.MenuSelectionManager;
import javax.swing.SwingUtilities;
/**
* This class represents an off-screen rendered browser.
* The visibility of this class is "package". To create a new
* CefBrowser instance, please use CefBrowserFactory.
*/
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_;
private CopyOnWriteArrayList<Consumer<CefPaintEvent>> onPaintListeners =
new CopyOnWriteArrayList<>();
CefBrowserOsr(CefClient client, String url, boolean transparent, CefRequestContext context,
CefBrowserSettings settings) {
this(client, url, transparent, context, null, null, settings);
}
private CefBrowserOsr(CefClient client, String url, boolean transparent,
CefRequestContext context, CefBrowserOsr parent, Point inspectAt,
CefBrowserSettings settings) {
super(client, url, context, parent, inspectAt, settings);
isTransparent_ = transparent;
renderer_ = new CefRenderer(transparent);
createGLCanvas();
}
@Override
public void createImmediately() {
justCreated_ = true;
// Create the browser immediately.
createBrowserIfRequired(false);
}
@Override
public Component getUIComponent() {
return canvas_;
}
@Override
public CefRenderHandler getRenderHandler() {
return this;
}
@Override
protected CefBrowser createDevToolsBrowser(CefClient client, String url,
CefRequestContext context, CefBrowser parent, Point inspectAt) {
return new CefBrowserOsr(
client, url, isTransparent_, context, this, inspectAt, null);
}
private synchronized long getWindowHandle() {
if (window_handle_ == 0) {
NativeSurface surface = canvas_.getNativeSurface();
if (surface != null) {
surface.lockSurface();
window_handle_ = getWindowHandle(surface.getSurfaceHandle());
surface.unlockSurface();
if (!OS.isMacintosh()) assert (window_handle_ != 0);
}
}
return window_handle_;
}
@SuppressWarnings("serial")
private void createGLCanvas() {
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
// dispose/init/reshape.
canvas_.addGLEventListener(new GLEventListener() {
@Override
public void reshape(
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);
}
@Override
public void init(GLAutoDrawable glautodrawable) {
renderer_.initialize(glautodrawable.getGL().getGL2());
}
@Override
public void dispose(GLAutoDrawable glautodrawable) {
renderer_.cleanup(glautodrawable.getGL().getGL2());
}
@Override
public void display(GLAutoDrawable glautodrawable) {
renderer_.render(glautodrawable.getGL().getGL2());
}
});
canvas_.addMouseListener(new MouseListener() {
@Override
public void mousePressed(MouseEvent e) {
sendMouseEvent(e);
}
@Override
public void mouseReleased(MouseEvent e) {
sendMouseEvent(e);
}
@Override
public void mouseEntered(MouseEvent e) {
sendMouseEvent(e);
}
@Override
public void mouseExited(MouseEvent e) {
sendMouseEvent(e);
}
@Override
public void mouseClicked(MouseEvent e) {
sendMouseEvent(e);
}
});
canvas_.addMouseMotionListener(new MouseMotionListener() {
@Override
public void mouseMoved(MouseEvent e) {
sendMouseEvent(e);
}
@Override
public void mouseDragged(MouseEvent e) {
sendMouseEvent(e);
}
});
canvas_.addMouseWheelListener(new MouseWheelListener() {
@Override
public void mouseWheelMoved(MouseWheelEvent e) {
sendMouseWheelEvent(e);
}
});
canvas_.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
sendKeyEvent(e);
}
@Override
public void keyPressed(KeyEvent e) {
sendKeyEvent(e);
}
@Override
public void keyReleased(KeyEvent e) {
sendKeyEvent(e);
}
});
canvas_.setFocusable(true);
canvas_.addFocusListener(new FocusListener() {
@Override
public void focusLost(FocusEvent e) {
setFocus(false);
}
@Override
public void focusGained(FocusEvent e) {
// Dismiss any Java menus that are currently displayed.
MenuSelectionManager.defaultManager().clearSelectedPath();
setFocus(true);
}
});
// Connect the Canvas with a drag and drop listener.
new DropTarget(canvas_, new CefDropTargetListener(this));
}
@Override
public Rectangle getViewRect(CefBrowser browser) {
return browser_rect_;
}
@Override
public Point getScreenPoint(CefBrowser browser, Point viewPoint) {
Point screenPoint = new Point(screenPoint_);
screenPoint.translate(viewPoint.x, viewPoint.y);
return screenPoint;
}
@Override
public double getDeviceScaleFactor(CefBrowser browser) {
return JCefAppConfig.getDeviceScaleFactor(browser.getUIComponent());
}
@Override
public void onPopupShow(CefBrowser browser, boolean show) {
if (!show) {
renderer_.clearPopupRects();
invalidate();
}
}
@Override
public void onPopupSize(CefBrowser browser, Rectangle size) {
renderer_.onPopupSize(size);
}
@Override
public void addOnPaintListener(Consumer<CefPaintEvent> listener) {
onPaintListeners.add(listener);
}
@Override
public void setOnPaintListener(Consumer<CefPaintEvent> listener) {
onPaintListeners.clear();
onPaintListeners.add(listener);
}
@Override
public void removeOnPaintListener(Consumer<CefPaintEvent> listener) {
onPaintListeners.remove(listener);
}
@Override
public void onPaint(CefBrowser browser, boolean popup, Rectangle[] dirtyRects,
ByteBuffer buffer, int width, int height) {
// if window is closing, canvas_ or opengl context could be null
final GLContext context = canvas_ != null ? canvas_.getContext() : null;
if (context == null) {
return;
}
// This result can occur due to GLContext re-initialization when changing displays.
if (context.makeCurrent() == GLContext.CONTEXT_NOT_CURRENT) {
return;
}
renderer_.onPaint(canvas_.getGL().getGL2(), popup, dirtyRects, buffer, width, height);
context.release();
SwingUtilities.invokeLater(new Runnable() {
public void run() {
canvas_.display();
}
});
if (!onPaintListeners.isEmpty()) {
CefPaintEvent paintEvent =
new CefPaintEvent(browser, popup, dirtyRects, buffer, width, height);
for (Consumer<CefPaintEvent> l : onPaintListeners) {
l.accept(paintEvent);
}
}
}
@Override
public void onAcceleratedPaint(CefBrowser browser, boolean popup, Rectangle[] dirtyRects, CefAcceleratedPaintInfo info) {
}
@Override
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;
}
private static final class SyntheticDragGestureRecognizer extends DragGestureRecognizer {
public SyntheticDragGestureRecognizer(Component c, int action, MouseEvent triggerEvent) {
super(new DragSource(), c, action);
appendEvent(triggerEvent);
}
protected void registerListeners() {}
protected void unregisterListeners() {}
};
private static int getDndAction(int mask) {
// Default to copy if multiple operations are specified.
int action = DnDConstants.ACTION_NONE;
if ((mask & CefDragData.DragOperations.DRAG_OPERATION_COPY)
== CefDragData.DragOperations.DRAG_OPERATION_COPY) {
action = DnDConstants.ACTION_COPY;
} else if ((mask & CefDragData.DragOperations.DRAG_OPERATION_MOVE)
== CefDragData.DragOperations.DRAG_OPERATION_MOVE) {
action = DnDConstants.ACTION_MOVE;
} else if ((mask & CefDragData.DragOperations.DRAG_OPERATION_LINK)
== CefDragData.DragOperations.DRAG_OPERATION_LINK) {
action = DnDConstants.ACTION_LINK;
}
return action;
}
@Override
public boolean startDragging(CefBrowser browser, CefDragData dragData, int mask, int x, int y) {
int action = getDndAction(mask);
MouseEvent triggerEvent =
new MouseEvent(canvas_, MouseEvent.MOUSE_DRAGGED, 0, 0, x, y, 0, false);
DragGestureEvent ev = new DragGestureEvent(
new SyntheticDragGestureRecognizer(canvas_, action, triggerEvent), action,
new Point(x, y), new ArrayList<>(Arrays.asList(triggerEvent)));
DragSource.getDefaultDragSource().startDrag(ev, /*dragCursor=*/null,
new StringSelection(dragData.getFragmentText()), new DragSourceAdapter() {
@Override
public void dragDropEnd(DragSourceDropEvent dsde) {
dragSourceEndedAt(dsde.getLocation(), action);
dragSourceSystemDragEnded();
}
});
return true;
}
@Override
public void updateDragCursor(CefBrowser browser, int operation) {
// TODO: Consider calling onCursorChange() if we want different cursors based on
// |operation|.
}
private void createBrowserIfRequired(boolean hasParent) {
long windowHandle = 0;
if (hasParent) {
windowHandle = getWindowHandle();
}
if (getNativeRef("CefBrowser") == 0) {
if (getParentBrowser() != null) {
createDevTools(getParentBrowser(), getClient(), windowHandle, true, isTransparent_,
null, getInspectAt());
} else {
createBrowser(getClient(), windowHandle, getUrl(), true, isTransparent_, null);
}
} 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) Math.ceil(canvas_.getWidth() * scaleFactor_);
int height = (int) Math.ceil(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;
}
}
@Override
public boolean isWindowless() {
return true;
}
}