java/org/cef/browser/CefBrowserWr.java (442 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 org.cef.CefBrowserSettings;
import org.cef.CefClient;
import org.cef.OS;
import org.cef.handler.CefWindowHandler;
import org.cef.handler.CefWindowHandlerAdapter;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.MenuSelectionManager;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.ToolTipManager;
import com.jetbrains.cef.JdkEx;
import org.cef.misc.CefLog;
import sun.awt.AWTAccessor;
/**
* This class represents a windowed rendered browser.
* The visibility of this class is "package". To create a new
* CefBrowser instance, please use CefBrowserFactory.
*/
class CefBrowserWr extends CefBrowser_N {
private static final boolean USE_CANVAS = OS.isWindows() || OS.isLinux();
private Canvas canvas_ = null;
private Component component_ = null;
private Rectangle content_rect_ = new Rectangle(0, 0, 0, 0);
private long window_handle_ = 0;
private boolean justCreated_ = false;
private double scaleFactor_ = 1.0;
private long delayCreationUntilMs_ = 0; // used only for testing
private Timer delayedUpdate_ = new Timer(100, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
if (isClosed()) return;
if (delayCreationUntilMs_ > 0 &&
System.currentTimeMillis() < delayCreationUntilMs_ &&
getNativeRef("CefBrowser") == 0
) {
CefLog.Debug("delay native browser creation (need wait %d ms)", delayCreationUntilMs_ - System.currentTimeMillis());
delayedUpdate_.restart();
return;
}
if (AWTAccessor.getComponentAccessor().getPeer(component_) == null || // not in UI yet
createBrowserIfRequired(true)) // has just created UI
{
delayedUpdate_.restart();
} else {
// If on Mac, this is needed due to the quirk described below
// (in org.cef.browser.CefBrowserWr.CefBrowserWr(...).new JPanel()
// {...}.paint(Graphics)). If on Linux, this is needed to invoke an
// XMoveResizeWindow call shortly after the UI was created. That seems to be
// necessary to actually get a windowed renderer to display something.
if (OS.isMacintosh() || OS.isLinux()) doUpdate();
}
}
});
}
});
private CefWindowHandlerAdapter win_handler_ = new CefWindowHandlerAdapter() {
private Point lastPos = new Point(-1, -1);
private long[] nextClick = new long[MouseInfo.getNumberOfButtons()];
private int[] clickCnt = new int[MouseInfo.getNumberOfButtons()];
@Override
public Rectangle getRect(CefBrowser browser) {
synchronized (content_rect_) {
return content_rect_;
}
}
@Override
public void onMouseEvent(CefBrowser browser, int event, final int screenX,
final int screenY, final int modifier, final int button) {
final Point pt = new Point(screenX, screenY);
if (event == MouseEvent.MOUSE_MOVED) {
// Remove mouse-moved events if the position of the cursor hasn't
// changed.
if (pt.equals(lastPos)) return;
lastPos = pt;
// Change mouse-moved event to mouse-dragged event if the left mouse
// button is pressed.
if ((modifier & MouseEvent.BUTTON1_DOWN_MASK) != 0)
event = MouseEvent.MOUSE_DRAGGED;
}
final int finalEvent = event;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
Component parent = SwingUtilities.getRoot(component_);
if (parent == null) {
return;
}
double scaleX = parent.getGraphicsConfiguration().getDefaultTransform().getScaleX();
double scaleY = parent.getGraphicsConfiguration().getDefaultTransform().getScaleY();
if (JdkEx.isJetBrainsJDK() && OS.isWindows()) {
Point parentPt = parent.getLocationOnScreen();
// In JBR/win the device [x, y] is preserved in device space (width/height is scaled).
Rectangle devBounds = parent.getGraphicsConfiguration().getDevice().getDefaultConfiguration().getBounds();
int scaledScreenX = devBounds.x + (int) Math.round((screenX - devBounds.x) / scaleX);
int scaledScreenY = devBounds.y + (int) Math.round((screenY - devBounds.y) / scaleY);
pt.x = scaledScreenX - parentPt.x;
pt.y = scaledScreenY - parentPt.y;
}
else {
pt.x = (int)Math.round(pt.x / scaleX);
pt.y = (int)Math.round(pt.y / scaleY);
SwingUtilities.convertPointFromScreen(pt, parent);
}
int clickCnt = 0;
long now = new Date().getTime();
if (finalEvent == MouseEvent.MOUSE_WHEEL) {
int scrollType = MouseWheelEvent.WHEEL_UNIT_SCROLL;
int rotation = button > 0 ? 1 : -1;
Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(new MouseWheelEvent(parent, finalEvent, now,
modifier, pt.x, pt.y, 0, false, scrollType, 3, rotation));
} else {
clickCnt = getClickCount(finalEvent, button);
Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(new MouseEvent(parent, finalEvent, now, modifier,
pt.x, pt.y, screenX, screenY, clickCnt, false, button));
}
// Always fire a mouse-clicked event after a mouse-released event.
if (finalEvent == MouseEvent.MOUSE_RELEASED) {
Toolkit.getDefaultToolkit().getSystemEventQueue().postEvent(
new MouseEvent(parent, MouseEvent.MOUSE_CLICKED, now, modifier,
pt.x, pt.y, screenX, screenY, clickCnt, false, button));
}
}
});
}
public int getClickCount(int event, int button) {
// avoid exceptions by using modulo
int idx = button % nextClick.length;
switch (event) {
case MouseEvent.MOUSE_PRESSED:
long currTime = new Date().getTime();
if (currTime > nextClick[idx]) {
nextClick[idx] = currTime
+ (Integer) Toolkit.getDefaultToolkit().getDesktopProperty(
"awt.multiClickInterval");
clickCnt[idx] = 1;
} else {
clickCnt[idx]++;
}
// FALL THRU
case MouseEvent.MOUSE_RELEASED:
return clickCnt[idx];
default:
return 0;
}
}
};
CefBrowserWr(
CefClient client, String url, CefRequestContext context, CefBrowserSettings settings) {
this(client, url, context, null, null, settings);
}
@SuppressWarnings("serial")
private CefBrowserWr(CefClient client, String url, CefRequestContext context,
CefBrowserWr parent, Point inspectAt, CefBrowserSettings settings) {
super(client, url, context, parent, inspectAt, settings);
delayedUpdate_.setRepeats(false);
delayCreationUntilMs_ = Long.getLong("jcef.debug.cefbrowserwr.delay_creation", 0);
if (delayCreationUntilMs_ > 0) delayCreationUntilMs_ += System.currentTimeMillis();
// Disabling lightweight of popup menu is required because
// otherwise it will be displayed behind the content of component_
JPopupMenu.setDefaultLightWeightPopupEnabled(false);
ToolTipManager.sharedInstance().setLightWeightPopupEnabled(false);
// We're using a JComponent instead of a Canvas now because the
// JComponent has clipping informations, which aren't accessible for Canvas.
component_ = new JPanel(new BorderLayout()) {
private MouseListener mouseListener;
private MouseWheelListener mouseWheelListener;
private MouseMotionListener mouseMotionListener;
private boolean removed_ = true;
{
addPropertyChangeListener("graphicsConfiguration", e -> updateScale());
}
@Override
public void addMouseListener(MouseListener l) {
mouseListener = l;
if (canvas_ != null)
canvas_.addMouseListener(l);
super.addMouseListener(l);
}
@Override
public void addMouseWheelListener(MouseWheelListener l) {
mouseWheelListener = l;
if (canvas_ != null)
canvas_.addMouseWheelListener(l);
super.addMouseWheelListener(l);
}
@Override
public void addMouseMotionListener(MouseMotionListener l) {
mouseMotionListener = l;
if (canvas_ != null)
canvas_.addMouseMotionListener(l);
super.addMouseMotionListener(l);
}
@Override
public void removeMouseListener(MouseListener l) {
mouseListener = null;
if (canvas_ != null)
canvas_.removeMouseListener(l);
super.removeMouseListener(l);
}
@Override
public void removeMouseMotionListener(MouseMotionListener l) {
mouseMotionListener = null;
if (canvas_ != null)
canvas_.removeMouseMotionListener(l);
super.removeMouseMotionListener(l);
}
@Override
public void removeMouseWheelListener(MouseWheelListener l) {
mouseWheelListener = null;
if (canvas_ != null)
canvas_.removeMouseWheelListener(l);
super.removeMouseWheelListener(l);
}
private void addCanvas() {
canvas_ = new BrowserCanvas();
if (mouseListener != null) canvas_.addMouseListener(mouseListener);
if (mouseWheelListener != null) canvas_.addMouseWheelListener(mouseWheelListener);
if (mouseMotionListener != null) canvas_.addMouseMotionListener(mouseMotionListener);
this.add(canvas_, BorderLayout.CENTER);
}
private void removeCanvas() {
if (canvas_ == null) return;
if (mouseListener != null) canvas_.removeMouseListener(mouseListener);
if (mouseWheelListener != null) canvas_.removeMouseWheelListener(mouseWheelListener);
if (mouseMotionListener != null) canvas_.removeMouseMotionListener(mouseMotionListener);
this.remove(canvas_);
canvas_ = null;
}
@Override
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
wasResized((int) (width * scaleFactor_), (int) (height * scaleFactor_));
}
@Override
public void setBounds(Rectangle r) {
setBounds(r.x, r.y, r.width, r.height);
}
@Override
public void setSize(int width, int height) {
super.setSize(width, height);
wasResized((int) (width * scaleFactor_), (int) (height * scaleFactor_));
}
@Override
public void setSize(Dimension d) {
setSize(d.width, d.height);
}
@Override
public void paint(Graphics g) {
// If the user resizes the UI component, the new size and clipping
// informations are forwarded to the native code.
// But on Mac the last resize information doesn't resize the native UI
// accurately (sometimes the native UI is too small). An easy way to
// solve this, is to send the last Update-Information again. Therefore
// we're setting up a delayedUpdate timer which is reset each time
// paint is called. This prevents the us of sending the UI update too
// often.
doUpdate();
delayedUpdate_.restart();
}
@Override
public void addNotify() {
super.addNotify();
updateScale();
if (removed_) {
if (USE_CANVAS) {
// Recreate canvas to prevent its blinking at toplevel's [0,0].
// NOTE: generally it's a bad idea to add components inside addNotify
// Also it'd be better to add component before super.addNotify (because it perfroms some processing with
// all children - send notifications, set listeners etc)
addCanvas();
}
setParent(getWindowHandle(this), canvas_);
removed_ = false;
}
delayedUpdate_.restart();
}
@Override
public void removeNotify() {
if (!removed_) {
if (!isClosed()) {
setParent(0, null);
}
removed_ = true;
if (USE_CANVAS) {
removeCanvas();
}
}
super.removeNotify();
}
};
// Initial minimal size of the component. Otherwise the UI won't work
// accordingly in panes like JSplitPane.
component_.setMinimumSize(new Dimension(0, 0));
component_.setFocusable(true);
component_.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);
}
});
component_.addHierarchyBoundsListener(new HierarchyBoundsListener() {
@Override
public void ancestorResized(HierarchyEvent e) {
doUpdate();
}
@Override
public void ancestorMoved(HierarchyEvent e) {
doUpdate();
notifyMoveOrResizeStarted();
}
});
component_.addHierarchyListener(new HierarchyListener() {
@Override
public void hierarchyChanged(HierarchyEvent e) {
if ((e.getChangeFlags() & HierarchyEvent.SHOWING_CHANGED) != 0) {
setWindowVisibility(e.getChanged().isVisible());
}
}
});
}
// On windows we have to use a Canvas because its a heavyweight component
// and we need its native HWND as parent for the browser UI. The same
// technique is used on Linux as well.
private class BrowserCanvas extends Canvas {
@Override
public void paint(Graphics g) {
g.setColor(component_.getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
}
}
private void updateScale() {
if (!OS.isMacintosh()) scaleFactor_ = shouldUpscale() ? JCefAppConfig.getDeviceScaleFactor(component_) : 1;
}
@Override
public void createImmediately() {
justCreated_ = true;
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
// Create the browser immediately. It will be parented to the Java
// window once it becomes available.
createBrowserIfRequired(false);
}
});
}
@Override
public Component getUIComponent() {
return component_;
}
@Override
public CefWindowHandler getWindowHandler() {
return win_handler_;
}
@Override
protected CefBrowser createDevToolsBrowser(CefClient client, String url,
CefRequestContext context, CefBrowser parent, Point inspectAt) {
return new CefBrowserWr(client, url, context, (CefBrowserWr) this, inspectAt, null);
}
private synchronized long getWindowHandle() {
if (window_handle_ == 0 && OS.isMacintosh()) {
window_handle_ = getWindowHandle(component_);
}
return window_handle_;
}
static long getWindowHandle(Component component) {
if (OS.isMacintosh()) {
try {
Class<?> cls = Class.forName("org.cef.browser.mac.CefBrowserWindowMac");
CefBrowserWindow browserWindow = (CefBrowserWindow) cls.newInstance();
if (browserWindow != null) {
return browserWindow.getWindowHandle(component);
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return 0;
}
private void doUpdate() {
if (isClosed()) return;
Rectangle vr = ((JPanel) component_).getVisibleRect();
Rectangle clipping = new Rectangle((int) (vr.getX() * scaleFactor_),
(int) (vr.getY() * scaleFactor_), (int) (vr.getWidth() * scaleFactor_),
(int) (vr.getHeight() * scaleFactor_));
if (OS.isMacintosh()) {
Container parent = component_.getParent();
Point contentPos = component_.getLocation();
while (parent != null) {
Container next = parent.getParent();
if (next != null && next instanceof Window) break;
Point parentPos = parent.getLocation();
contentPos.translate(parentPos.x, parentPos.y);
parent = next;
}
contentPos.translate(clipping.x, clipping.y);
Point browserPos = clipping.getLocation();
browserPos.x *= -1;
browserPos.y *= -1;
synchronized (content_rect_) {
content_rect_ = new Rectangle(contentPos, clipping.getSize());
Rectangle browserRect = new Rectangle(browserPos, component_.getSize());
updateUI(content_rect_, browserRect);
}
} else {
synchronized (content_rect_) {
Rectangle bounds = null != canvas_ ? canvas_.getBounds() : component_.getBounds();
// On Linux, content_rect_ scaling downgrades, namely:
// - should not be scaled in JRE-managed HiDPI mode
// - should be downscaled in IDE-managed HiDPI mode
double scale = OS.isLinux() ?
(shouldUpscale() ? 1 : 1 / JCefAppConfig.getDeviceScaleFactor(component_)) :
scaleFactor_;
content_rect_ = new Rectangle(
(int) (bounds.getX() * scale),
(int) (bounds.getY() * scale),
(int) (bounds.getWidth() * scale),
(int) (bounds.getHeight() * scale));
updateUI(clipping, content_rect_);
}
}
}
private boolean createBrowserIfRequired(boolean hasParent) {
if (isClosed()) return false;
long windowHandle = 0;
Component canvas = null;
if (hasParent) {
windowHandle = getWindowHandle();
canvas = (OS.isWindows() || OS.isLinux()) ? canvas_ : component_;
}
if (getNativeRef("CefBrowser") == 0) {
if (getParentBrowser() != null) {
createDevTools(getParentBrowser(), getClient(), windowHandle, false, false, canvas,
getInspectAt());
return true;
} else {
createBrowser(getClient(), windowHandle, getUrl(), false, false, canvas);
return true;
}
} else if (hasParent && justCreated_) {
setParent(windowHandle, canvas);
// setFocus(true); do not request focus on show
justCreated_ = false;
}
return false;
}
@Override
public CompletableFuture<BufferedImage> createScreenshot(boolean nativeResolution) {
throw new UnsupportedOperationException("Unsupported for windowed rendering");
}
private static boolean shouldUpscale() {
return JCefAppConfig.getForceDeviceScaleFactor() == -1;
}
@Override
public void setWindowlessFrameRate(int frameRate) {
throw new UnsupportedOperationException(
"You can only set windowless framerate on OSR browser");
}
@Override
public CompletableFuture<Integer> getWindowlessFrameRate() {
throw new UnsupportedOperationException(
"You can only get windowless framerate on OSR browser");
}
@Override
public boolean isWindowless() {
return false;
}
}