feat(browser): 添加 JCEF 浏览器窗口功能
- 新增 BrowserWindow 类实现浏览器窗口功能- 添加 WindowOperation、WindowOperationHandler 和 WindowRegistry 类用于窗口操作管理 - 在 MainApplication 中使用 JCEF 创建主窗口 - 实现了窗口创建、关闭等基本操作
This commit is contained in:
@@ -121,6 +121,8 @@ dependencies {
|
||||
|
||||
// JavaFX 模块
|
||||
implementation 'org.openjfx:javafx-graphics:21'
|
||||
|
||||
implementation 'me.friwi:jcefmaven:122.1.10'
|
||||
}
|
||||
|
||||
// 分离依赖项到 libs 目录
|
||||
|
||||
369
src/main/java/com/axis/innovators/box/browser/BrowserWindow.java
Normal file
369
src/main/java/com/axis/innovators/box/browser/BrowserWindow.java
Normal file
@@ -0,0 +1,369 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import com.axis.innovators.box.tools.FolderCreator;
|
||||
import org.cef.*;
|
||||
import org.cef.browser.*;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.*;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.net.MalformedURLException;
|
||||
import java.util.Arrays;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class BrowserWindow extends JFrame {
|
||||
private final String windowId;
|
||||
private CefApp cefApp;
|
||||
private CefClient client;
|
||||
private CefBrowser browser;
|
||||
private final Component browserComponent;
|
||||
private final String htmlPath;
|
||||
private static boolean isInitialized = false;
|
||||
private WindowOperationHandler operationHandler;
|
||||
private static Thread cefThread;
|
||||
private CefMessageRouter msgRouter;
|
||||
public static class Builder {
|
||||
private String windowId;
|
||||
private String title = "JCEF Window";
|
||||
private Dimension size = new Dimension(800, 600);
|
||||
private WindowOperationHandler operationHandler;
|
||||
private String htmlPath;
|
||||
private Image icon;
|
||||
|
||||
public Builder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder size(int width, int height) {
|
||||
this.size = new Dimension(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder operationHandler(WindowOperationHandler handler) {
|
||||
this.operationHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder icon(Image icon) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BrowserWindow build() {
|
||||
if (this.htmlPath == null || this.htmlPath.isEmpty()) {
|
||||
throw new IllegalArgumentException("HTML paths cannot be empty");
|
||||
}
|
||||
File htmlFile = new File(this.htmlPath);
|
||||
if (!htmlFile.exists()) {
|
||||
throw new RuntimeException("The HTML file does not exist: " + htmlFile.getAbsolutePath());
|
||||
}
|
||||
return new BrowserWindow(this);
|
||||
}
|
||||
|
||||
public Builder htmlPath(String path) {
|
||||
this.htmlPath = path;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private BrowserWindow(Builder builder) {
|
||||
this.windowId = builder.windowId;
|
||||
this.htmlPath = builder.htmlPath;
|
||||
this.operationHandler = builder.operationHandler;
|
||||
|
||||
if (builder.icon != null) {
|
||||
setIconImage(builder.icon);
|
||||
}
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
setupMessageHandlers(operationHandler);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
|
||||
//System.exit(1);
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized && CefApp.getState() != CefApp.CefAppState.INITIALIZED) {
|
||||
try {
|
||||
// 1. 初始化CEF设置(禁用多窗口)
|
||||
CefSettings settings = new CefSettings();
|
||||
settings.windowless_rendering_enabled = false;
|
||||
settings.javascript_flags = "--expose-gc";
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_VERBOSE;
|
||||
String subprocessPath = FolderCreator.getLibraryFolder() + "/jcef/lib/win64/jcef_helper.exe";
|
||||
|
||||
// 验证子进程路径
|
||||
if (!new File(subprocessPath).exists()) {
|
||||
throw new IllegalStateException("jcef_helper.exe not found at: " + subprocessPath);
|
||||
}
|
||||
settings.browser_subprocess_path = subprocessPath;
|
||||
System.out.println("Subprocess Path: " + settings.browser_subprocess_path);
|
||||
|
||||
// 2. 创建CefApp和Client(严格单例)
|
||||
cefApp = CefApp.getInstance(settings);
|
||||
client = cefApp.createClient();
|
||||
|
||||
client.addDisplayHandler(new CefDisplayHandler (){
|
||||
@Override
|
||||
public void onAddressChange(CefBrowser browser, CefFrame frame, String url) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChange(CefBrowser browser, String title) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void OnFullscreenModeChange(CefBrowser browser, boolean fullscreen) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTooltip(CefBrowser browser, String text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusMessage(CefBrowser browser, String value) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onConsoleMessage(
|
||||
CefBrowser browser,
|
||||
CefSettings.LogSeverity level,
|
||||
String message,
|
||||
String source,
|
||||
int line
|
||||
) {
|
||||
// 格式化输出到 Java 控制台
|
||||
String log = String.format(
|
||||
"[Browser Console] %s %s (Line %d) -> %s",
|
||||
getLogLevelSymbol(level),
|
||||
source,
|
||||
line,
|
||||
message
|
||||
);
|
||||
System.out.println(log);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCursorChange(CefBrowser browser, int cursorType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
private String getLogLevelSymbol(CefSettings.LogSeverity level) {
|
||||
switch (level) {
|
||||
case LOGSEVERITY_ERROR: return "⛔";
|
||||
case LOGSEVERITY_WARNING: return "⚠️";
|
||||
case LOGSEVERITY_DEFAULT: return "🐞";
|
||||
default: return "ℹ️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 3. 拦截所有新窗口(关键修复点!)
|
||||
//client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
// @Override
|
||||
// public boolean onBeforePopup(CefBrowser browser,
|
||||
// CefFrame frame, String target_url, String target_frame_name) {
|
||||
// return true; // 返回true表示拦截弹窗
|
||||
// }
|
||||
//});
|
||||
|
||||
|
||||
Thread.currentThread().setName("BrowserRenderThread");
|
||||
|
||||
// 4. 加载HTML
|
||||
String fileUrl = new File(htmlPath).toURI().toURL().toString();
|
||||
System.out.println("Loading HTML from: " + fileUrl);
|
||||
|
||||
// 5. 创建浏览器组件(直接添加到内容面板)
|
||||
browser = client.createBrowser(fileUrl, false, false);
|
||||
|
||||
Component browserComponent = browser.getUIComponent();
|
||||
browser.executeJavaScript("console.log('Java -> HTML 消息测试')",null,2);
|
||||
|
||||
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
getContentPane().removeAll(); // 清空已有组件
|
||||
getContentPane().setLayout(new BorderLayout());
|
||||
|
||||
// 透明拖拽层(仅顶部可拖拽)
|
||||
JPanel dragPanel = new JPanel(new BorderLayout());
|
||||
dragPanel.setOpaque(false);
|
||||
|
||||
JPanel titleBar = new JPanel();
|
||||
titleBar.setOpaque(false);
|
||||
titleBar.setPreferredSize(new Dimension(builder.size.width, 20));
|
||||
final Point[] dragStart = new Point[1];
|
||||
titleBar.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
dragStart[0] = e.getPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
dragStart[0] = null;
|
||||
}
|
||||
});
|
||||
titleBar.addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (dragStart[0] != null) {
|
||||
Point curr = e.getLocationOnScreen();
|
||||
setLocation(curr.x - dragStart[0].x, curr.y - dragStart[0].y);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dragPanel.add(titleBar, BorderLayout.NORTH);
|
||||
getContentPane().add(dragPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(browserComponent, BorderLayout.CENTER);
|
||||
|
||||
// 7. 窗口属性设置
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size); // 直接设置尺寸,避免pack()计算错误
|
||||
setLocationRelativeTo(null);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 8. 资源释放
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true); // 强制关闭浏览器
|
||||
client.dispose();
|
||||
cefApp.dispose();
|
||||
isInitialized = false;
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
isInitialized = true;
|
||||
});
|
||||
return browserComponent;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
isInitialized = false;
|
||||
JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
System.exit(1);
|
||||
}
|
||||
} else {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void printStackTrace() {
|
||||
StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace();
|
||||
for (int i = 2; i < stackTrace.length; i++) {
|
||||
StackTraceElement element = stackTrace[i];
|
||||
System.out.println(element.getClassName() + "." + element.getMethodName() +
|
||||
"(" + (element.getFileName() != null ? element.getFileName() : "Unknown Source") +
|
||||
":" + element.getLineNumber() + ")");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b) {
|
||||
super.setVisible(b);
|
||||
}
|
||||
|
||||
public Component getBrowserComponent() {
|
||||
return browserComponent;
|
||||
}
|
||||
|
||||
private void setupMessageHandlers(WindowOperationHandler handler) {
|
||||
if (client != null) {
|
||||
msgRouter = CefMessageRouter.create();
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser,
|
||||
CefFrame frame,
|
||||
long queryId,
|
||||
String request,
|
||||
boolean persistent,
|
||||
CefQueryCallback callback) {
|
||||
if (request.startsWith("system:")) {
|
||||
String[] parts = request.split(":");
|
||||
String operation = parts.length >= 2 ? parts[1] : null;
|
||||
String targetWindow = parts.length > 2 ? parts[2] : null;
|
||||
handler.handleOperation(
|
||||
new WindowOperation(operation, targetWindow, callback) // [!code ++]
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (request.startsWith("java-response:")) {
|
||||
String[] parts = request.split(":");
|
||||
String requestId = parts[1];
|
||||
String responseData = parts.length > 2 ? parts[2] : "";
|
||||
Consumer<String> handler = WindowRegistry.getInstance().getCallback(requestId);
|
||||
if (handler != null) {
|
||||
handler.accept(responseData);
|
||||
callback.success("");
|
||||
} else {
|
||||
callback.failure(-1, "无效的请求ID");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
client.addMessageRouter(msgRouter);
|
||||
}
|
||||
}
|
||||
|
||||
public String getWindowId() {
|
||||
return windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息路由器
|
||||
* @return 消息路由器
|
||||
*/
|
||||
public CefMessageRouter getMsgRouter() {
|
||||
return msgRouter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器对象
|
||||
* @return 浏览器对象
|
||||
*/
|
||||
public CefBrowser getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
public void closeWindow() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (browser != null) {
|
||||
browser.close(true);
|
||||
}
|
||||
dispose();
|
||||
cefApp.dispose();
|
||||
WindowRegistry.getInstance().unregisterWindow(windowId);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import javax.swing.*;
|
||||
|
||||
/**
|
||||
* 这是一个简单的示例程序,用于展示如何使用JCEF来创建一个简单的浏览器窗口。
|
||||
*/
|
||||
public class MainApplication {
|
||||
public static void main(String[] args) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
//if (!CefApp.startup(args)) {
|
||||
// System.out.println("Startup initialization failed!");
|
||||
// return;
|
||||
//}
|
||||
|
||||
WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
builder.title("Axis Innovators Box")
|
||||
.size(1280, 720)
|
||||
.htmlPath("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\java\\com\\axis\\innovators\\box\\browser\\main.html")
|
||||
.operationHandler(createOperationHandler())
|
||||
.build()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
private static WindowOperationHandler createOperationHandler() {
|
||||
return new WindowOperationHandler.Builder()
|
||||
.withDefaultOperations()
|
||||
.onOperation("我是注册的指令", s -> {
|
||||
|
||||
})
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
|
||||
public class WindowOperation {
|
||||
private final String type;
|
||||
private final String targetWindow;
|
||||
private final CefQueryCallback callback;
|
||||
|
||||
public WindowOperation(String type, String targetWindow, CefQueryCallback callback) { // [!code ++]
|
||||
this.type = type;
|
||||
this.targetWindow = targetWindow;
|
||||
this.callback = callback; // [!code ++]
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getTargetWindow() {
|
||||
return targetWindow;
|
||||
}
|
||||
|
||||
public CefQueryCallback getCallback() {
|
||||
return callback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class WindowOperationHandler {
|
||||
private final WindowRegistry registry;
|
||||
private final Map<String, Consumer<String>> operations;
|
||||
private final Component attachedComponent;
|
||||
|
||||
public static class Builder {
|
||||
private WindowRegistry registry = WindowRegistry.getInstance();
|
||||
private final Map<String, Consumer<String>> operations = new ConcurrentHashMap<>();
|
||||
private Component attachedComponent;
|
||||
|
||||
public Builder attachTo(Component component) {
|
||||
this.attachedComponent = component;
|
||||
return this;
|
||||
}
|
||||
public Builder withDefaultOperations() {
|
||||
this.operations.put("open", target -> {
|
||||
registry.getWindow(target).setVisible(true);
|
||||
});
|
||||
this.operations.put("close", target -> {
|
||||
if (target != null) {
|
||||
System.out.println("Close window: " + target);
|
||||
registry.unregisterWindow(target);
|
||||
} else if (attachedComponent != null) {
|
||||
Window window = SwingUtilities.getWindowAncestor(attachedComponent);
|
||||
if (window instanceof BrowserWindow) {
|
||||
((BrowserWindow) window).closeWindow();
|
||||
}
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder onOperation(String operation, Consumer<String> handler) {
|
||||
this.operations.put(operation, handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
public WindowOperationHandler build() {
|
||||
return new WindowOperationHandler(this);
|
||||
}
|
||||
|
||||
private void handleOpen(String targetWindow) {
|
||||
registry.createNewWindow(targetWindow, builder ->
|
||||
builder.title("New Window")
|
||||
);
|
||||
}
|
||||
|
||||
private void handleClose(String targetWindow) {
|
||||
if (targetWindow != null) {
|
||||
registry.unregisterWindow(targetWindow);
|
||||
} else {
|
||||
handleCurrentWindowClose();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCurrentWindowClose() {
|
||||
if (attachedComponent != null) {
|
||||
BrowserWindow currentWindow = (BrowserWindow)
|
||||
SwingUtilities.getWindowAncestor(attachedComponent);
|
||||
if (currentWindow != null) {
|
||||
registry.unregisterWindow(currentWindow.getWindowId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private WindowOperationHandler(Builder builder) {
|
||||
this.registry = builder.registry;
|
||||
this.attachedComponent = builder.attachedComponent;
|
||||
this.operations = new ConcurrentHashMap<>(builder.operations);
|
||||
}
|
||||
|
||||
public void handleOperation(WindowOperation operation) {
|
||||
Consumer<String> handler = operations.get(operation.getType());
|
||||
if (handler != null) {
|
||||
handler.accept(operation.getTargetWindow());
|
||||
operation.getCallback().success("操作成功: " + operation.getType());
|
||||
} else {
|
||||
operation.getCallback().failure(-1, "未定义的操作: " + operation.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class WindowRegistry {
|
||||
private static WindowRegistry instance;
|
||||
private final ConcurrentMap<String, BrowserWindow> windows =
|
||||
new ConcurrentHashMap<>();
|
||||
private final Map<String, Consumer<String>> callbacks = new ConcurrentHashMap<>();
|
||||
|
||||
private WindowRegistry() {}
|
||||
|
||||
public static synchronized WindowRegistry getInstance() {
|
||||
if (instance == null) {
|
||||
instance = new WindowRegistry();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public void registerWindow(BrowserWindow window) {
|
||||
windows.put(window.getWindowId(), window);
|
||||
}
|
||||
|
||||
public void registerCallback(String requestId, Consumer<String> handler) {
|
||||
callbacks.put(requestId, handler);
|
||||
}
|
||||
|
||||
public Consumer<String> getCallback(String requestId) {
|
||||
return callbacks.remove(requestId);
|
||||
}
|
||||
|
||||
public void unregisterWindow(String windowId) {
|
||||
BrowserWindow window = windows.remove(windowId);
|
||||
if (window != null) {
|
||||
window.closeWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public BrowserWindow getWindow(String windowId) {
|
||||
return windows.get(windowId);
|
||||
}
|
||||
|
||||
public void createNewWindow(String windowId, Consumer<BrowserWindow.Builder> config) {
|
||||
BrowserWindow.Builder builder = new BrowserWindow.Builder(windowId);
|
||||
config.accept(builder);
|
||||
BrowserWindow window = builder.build();
|
||||
registerWindow(window);
|
||||
}
|
||||
}
|
||||
13
src/main/java/com/axis/innovators/box/browser/main.html
Normal file
13
src/main/java/com/axis/innovators/box/browser/main.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<script>
|
||||
// 必须定义此函数以接收 Java 消息
|
||||
function javaMessageReceived(requestId, message) {
|
||||
console.log("[HTML] 收到 Java 消息:", requestId, message);
|
||||
// 示例:将消息转为大写并返回
|
||||
const response = message.toUpperCase();
|
||||
window.cefQuery({
|
||||
request: 'java-response:' + requestId + ':' + response,
|
||||
onSuccess: function() {},
|
||||
onFailure: function() {}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
602
src/main/java/org/cef/CefApp.java
Normal file
602
src/main/java/org/cef/CefApp.java
Normal file
@@ -0,0 +1,602 @@
|
||||
// Copyright (c) 2013 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;
|
||||
|
||||
import com.axis.innovators.box.tools.LibraryLoad;
|
||||
import org.cef.callback.CefSchemeHandlerFactory;
|
||||
import org.cef.handler.CefAppHandler;
|
||||
import org.cef.handler.CefAppHandlerAdapter;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.ActionListener;
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashSet;
|
||||
|
||||
/**
|
||||
* Exposes static methods for managing the global CEF context.
|
||||
*/
|
||||
public class CefApp extends CefAppHandlerAdapter {
|
||||
public final class CefVersion {
|
||||
public final int JCEF_COMMIT_NUMBER;
|
||||
|
||||
public final int CEF_VERSION_MAJOR;
|
||||
public final int CEF_VERSION_MINOR;
|
||||
public final int CEF_VERSION_PATCH;
|
||||
public final int CEF_COMMIT_NUMBER;
|
||||
|
||||
public final int CHROME_VERSION_MAJOR;
|
||||
public final int CHROME_VERSION_MINOR;
|
||||
public final int CHROME_VERSION_BUILD;
|
||||
public final int CHROME_VERSION_PATCH;
|
||||
|
||||
private CefVersion(int jcefCommitNo, int cefMajor, int cefMinor, int cefPatch,
|
||||
int cefCommitNo, int chrMajor, int chrMin, int chrBuild, int chrPatch) {
|
||||
JCEF_COMMIT_NUMBER = jcefCommitNo;
|
||||
|
||||
CEF_VERSION_MAJOR = cefMajor;
|
||||
CEF_VERSION_MINOR = cefMinor;
|
||||
CEF_VERSION_PATCH = cefPatch;
|
||||
CEF_COMMIT_NUMBER = cefCommitNo;
|
||||
|
||||
CHROME_VERSION_MAJOR = chrMajor;
|
||||
CHROME_VERSION_MINOR = chrMin;
|
||||
CHROME_VERSION_BUILD = chrBuild;
|
||||
CHROME_VERSION_PATCH = chrPatch;
|
||||
}
|
||||
|
||||
public String getJcefVersion() {
|
||||
return CEF_VERSION_MAJOR + "." + CEF_VERSION_MINOR + "." + CEF_VERSION_PATCH + "."
|
||||
+ JCEF_COMMIT_NUMBER;
|
||||
}
|
||||
|
||||
public String getCefVersion() {
|
||||
return CEF_VERSION_MAJOR + "." + CEF_VERSION_MINOR + "." + CEF_VERSION_PATCH;
|
||||
}
|
||||
|
||||
public String getChromeVersion() {
|
||||
return CHROME_VERSION_MAJOR + "." + CHROME_VERSION_MINOR + "." + CHROME_VERSION_BUILD
|
||||
+ "." + CHROME_VERSION_PATCH;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "JCEF Version = " + getJcefVersion() + "\n"
|
||||
+ "CEF Version = " + getCefVersion() + "\n"
|
||||
+ "Chromium Version = " + getChromeVersion();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The CefAppState gives you a hint if the CefApp is already usable or not
|
||||
* usable any more. See values for details.
|
||||
*/
|
||||
public enum CefAppState {
|
||||
/**
|
||||
* No CefApp instance was created yet. Call getInstance() to create a new
|
||||
* one.
|
||||
*/
|
||||
NONE,
|
||||
|
||||
/**
|
||||
* CefApp is new created but not initialized yet. No CefClient and no
|
||||
* CefBrowser was created until now.
|
||||
*/
|
||||
NEW,
|
||||
|
||||
/**
|
||||
* CefApp is in its initializing process. Please wait until initializing is
|
||||
* finished.
|
||||
*/
|
||||
INITIALIZING,
|
||||
|
||||
/**
|
||||
* CefApp is up and running. At least one CefClient was created and the
|
||||
* message loop is running. You can use all classes and methods of JCEF now.
|
||||
*/
|
||||
INITIALIZED,
|
||||
|
||||
/**
|
||||
* CEF initialization has failed (for example due to a second process using
|
||||
* the same root_cache_path).
|
||||
*/
|
||||
INITIALIZATION_FAILED,
|
||||
|
||||
/**
|
||||
* CefApp is in its shutdown process. All CefClients and CefBrowser
|
||||
* instances will be disposed. No new CefClient or CefBrowser is allowed to
|
||||
* be created. The message loop will be performed until all CefClients and
|
||||
* all CefBrowsers are disposed completely.
|
||||
*/
|
||||
SHUTTING_DOWN,
|
||||
|
||||
/**
|
||||
* CefApp is terminated and can't be used any more. You can shutdown the
|
||||
* application safely now.
|
||||
*/
|
||||
TERMINATED
|
||||
}
|
||||
|
||||
/**
|
||||
* According the singleton pattern, this attribute keeps
|
||||
* one single object of this class.
|
||||
*/
|
||||
private static CefApp self = null;
|
||||
private static CefAppHandler appHandler_ = null;
|
||||
private static CefAppState state_ = CefAppState.NONE;
|
||||
private Timer workTimer_ = null;
|
||||
private HashSet<CefClient> clients_ = new HashSet<CefClient>();
|
||||
private CefSettings settings_ = null;
|
||||
|
||||
/**
|
||||
* To get an instance of this class, use the method
|
||||
* getInstance() instead of this CTOR.
|
||||
*
|
||||
* The CTOR is called by getInstance() as needed and
|
||||
* loads all required JCEF libraries.
|
||||
*
|
||||
* @throws UnsatisfiedLinkError
|
||||
*/
|
||||
private CefApp(String[] args, CefSettings settings) throws UnsatisfiedLinkError {
|
||||
super(args);
|
||||
if (settings != null) settings_ = settings.clone();
|
||||
if (OS.isWindows()) {
|
||||
SystemBootstrap.loadLibrary("jawt");
|
||||
LibraryLoad.loadLibrary("jcef/lib/win64/chrome_elf");
|
||||
//SystemBootstrap.loadLibrary("libcef");
|
||||
LibraryLoad.loadLibrary("jcef/lib/win64/libcef.dll");
|
||||
|
||||
// Other platforms load this library in CefApp.startup().
|
||||
//SystemBootstrap.loadLibrary("jcef");
|
||||
LibraryLoad.loadLibrary("jcef/lib/win64/jcef");
|
||||
} else if (OS.isLinux()) {
|
||||
SystemBootstrap.loadLibrary("cef");
|
||||
}
|
||||
if (appHandler_ == null) {
|
||||
appHandler_ = this;
|
||||
}
|
||||
|
||||
// Execute on the AWT event dispatching thread.
|
||||
try {
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Perform native pre-initialization.
|
||||
if (!N_PreInitialize())
|
||||
throw new IllegalStateException("Failed to pre-initialize native code");
|
||||
}
|
||||
};
|
||||
if (SwingUtilities.isEventDispatchThread())
|
||||
r.run();
|
||||
else
|
||||
SwingUtilities.invokeAndWait(r);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign an AppHandler to CefApp. The AppHandler can be used to evaluate
|
||||
* application arguments, to register your own schemes and to hook into the
|
||||
* shutdown sequence. See CefAppHandler for more details.
|
||||
*
|
||||
* This method must be called before CefApp is initialized. CefApp will be
|
||||
* initialized automatically if you call createClient() the first time.
|
||||
* @param appHandler An instance of CefAppHandler.
|
||||
*
|
||||
* @throws IllegalStateException in case of CefApp is already initialized
|
||||
*/
|
||||
public static void addAppHandler(CefAppHandler appHandler) throws IllegalStateException {
|
||||
if (getState().compareTo(CefAppState.NEW) > 0)
|
||||
throw new IllegalStateException("Must be called before CefApp is initialized");
|
||||
appHandler_ = appHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of this class.
|
||||
* @return an instance of this class
|
||||
* @throws UnsatisfiedLinkError
|
||||
*/
|
||||
public static synchronized CefApp getInstance() throws UnsatisfiedLinkError {
|
||||
return getInstance(null, null);
|
||||
}
|
||||
|
||||
public static synchronized CefApp getInstance(String[] args) throws UnsatisfiedLinkError {
|
||||
return getInstance(args, null);
|
||||
}
|
||||
|
||||
public static synchronized CefApp getInstance(CefSettings settings)
|
||||
throws UnsatisfiedLinkError {
|
||||
return getInstance(null, settings);
|
||||
}
|
||||
|
||||
public static synchronized CefApp getInstance(String[] args, CefSettings settings)
|
||||
throws UnsatisfiedLinkError {
|
||||
if (settings != null) {
|
||||
if (getState() != CefAppState.NONE && getState() != CefAppState.NEW)
|
||||
throw new IllegalStateException("Settings can only be passed to CEF"
|
||||
+ " before createClient is called the first time.");
|
||||
}
|
||||
if (self == null) {
|
||||
if (getState() == CefAppState.TERMINATED)
|
||||
throw new IllegalStateException("CefApp was terminated");
|
||||
self = new CefApp(args, settings);
|
||||
setState(CefAppState.NEW);
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
public final void setSettings(CefSettings settings) throws IllegalStateException {
|
||||
if (getState() != CefAppState.NONE && getState() != CefAppState.NEW)
|
||||
throw new IllegalStateException("Settings can only be passed to CEF"
|
||||
+ " before createClient is called the first time.");
|
||||
settings_ = settings.clone();
|
||||
}
|
||||
|
||||
public final CefVersion getVersion() {
|
||||
try {
|
||||
return N_GetVersion();
|
||||
} catch (UnsatisfiedLinkError ule) {
|
||||
ule.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current state of CefApp.
|
||||
* @return current state.
|
||||
*/
|
||||
public final static CefAppState getState() {
|
||||
synchronized (state_) {
|
||||
return state_;
|
||||
}
|
||||
}
|
||||
|
||||
private static final void setState(final CefAppState state) {
|
||||
synchronized (state_) {
|
||||
state_ = state;
|
||||
}
|
||||
// Execute on the AWT event dispatching thread.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (appHandler_ != null) appHandler_.stateHasChanged(state);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* To shutdown the system, it's important to call the dispose
|
||||
* method. Calling this method closes all client instances with
|
||||
* and all browser instances each client owns. After that the
|
||||
* message loop is terminated and CEF is shutdown.
|
||||
*/
|
||||
public synchronized final void dispose() {
|
||||
switch (getState()) {
|
||||
case NEW:
|
||||
// Nothing to do inspite of invalidating the state
|
||||
setState(CefAppState.TERMINATED);
|
||||
break;
|
||||
|
||||
case INITIALIZING:
|
||||
case INITIALIZED:
|
||||
// (3) Shutdown sequence. Close all clients and continue.
|
||||
setState(CefAppState.SHUTTING_DOWN);
|
||||
if (clients_.isEmpty()) {
|
||||
shutdown();
|
||||
} else {
|
||||
// shutdown() will be called from clientWasDisposed() when the last
|
||||
// client is gone.
|
||||
// Use a copy of the HashSet to avoid iterating during modification.
|
||||
HashSet<CefClient> clients = new HashSet<CefClient>(clients_);
|
||||
for (CefClient c : clients) {
|
||||
c.dispose();
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case NONE:
|
||||
case SHUTTING_DOWN:
|
||||
case TERMINATED:
|
||||
// Ignore shutdown, CefApp is already terminated, in shutdown progress
|
||||
// or was never created (shouldn't be possible)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new client instance and returns it to the caller.
|
||||
* One client instance is responsible for one to many browser
|
||||
* instances
|
||||
* @return a new client instance
|
||||
*/
|
||||
public synchronized CefClient createClient() {
|
||||
switch (getState()) {
|
||||
case NEW:
|
||||
setState(CefAppState.INITIALIZING);
|
||||
initialize();
|
||||
// FALL THRU
|
||||
|
||||
case INITIALIZING:
|
||||
case INITIALIZED:
|
||||
CefClient client = new CefClient();
|
||||
clients_.add(client);
|
||||
return client;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Can't crate client in state " + state_);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a scheme handler factory for the specified |scheme_name| and
|
||||
* optional |domain_name|. An empty |domain_name| value for a standard scheme
|
||||
* will cause the factory to match all domain names. The |domain_name| value
|
||||
* will be ignored for non-standard schemes. If |scheme_name| is a built-in
|
||||
* scheme and no handler is returned by |factory| then the built-in scheme
|
||||
* handler factory will be called. If |scheme_name| is a custom scheme then
|
||||
* also implement the CefApp::OnRegisterCustomSchemes() method in all
|
||||
* processes. This function may be called multiple times to change or remove
|
||||
* the factory that matches the specified |scheme_name| and optional
|
||||
* |domain_name|. Returns false if an error occurs. This function may be
|
||||
* called on any thread in the browser process.
|
||||
*/
|
||||
public boolean registerSchemeHandlerFactory(
|
||||
String schemeName, String domainName, CefSchemeHandlerFactory factory) {
|
||||
try {
|
||||
return N_RegisterSchemeHandlerFactory(schemeName, domainName, factory);
|
||||
} catch (Exception err) {
|
||||
err.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all registered scheme handler factories. Returns false on error. This
|
||||
* function may be called on any thread in the browser process.
|
||||
*/
|
||||
public boolean clearSchemeHandlerFactories() {
|
||||
try {
|
||||
return N_ClearSchemeHandlerFactories();
|
||||
} catch (Exception err) {
|
||||
err.printStackTrace();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called by a CefClient if it was disposed. This causes
|
||||
* CefApp to clean up its list of available client instances. If all clients
|
||||
* are disposed, CefApp will be shutdown.
|
||||
* @param client the disposed client.
|
||||
*/
|
||||
protected final synchronized void clientWasDisposed(CefClient client) {
|
||||
clients_.remove(client);
|
||||
if (clients_.isEmpty() && getState().compareTo(CefAppState.SHUTTING_DOWN) >= 0) {
|
||||
// Shutdown native system.
|
||||
shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the context.
|
||||
* @return true on success.
|
||||
*/
|
||||
private final void initialize() {
|
||||
// Execute on the AWT event dispatching thread.
|
||||
try {
|
||||
Runnable r = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
String library_path = getJcefLibPath();
|
||||
System.out.println("initialize on " + Thread.currentThread()
|
||||
+ " with library path " + library_path);
|
||||
|
||||
CefSettings settings = settings_ != null ? settings_ : new CefSettings();
|
||||
|
||||
// Avoid to override user values by testing on NULL
|
||||
if (OS.isMacintosh()) {
|
||||
if (settings.browser_subprocess_path == null) {
|
||||
Path path = Paths.get(library_path,
|
||||
"../Frameworks/jcef Helper.app/Contents/MacOS/jcef Helper");
|
||||
settings.browser_subprocess_path =
|
||||
path.normalize().toAbsolutePath().toString();
|
||||
}
|
||||
} else if (OS.isWindows()) {
|
||||
if (settings.browser_subprocess_path == null) {
|
||||
Path path = Paths.get(library_path, "jcef_helper.exe");
|
||||
settings.browser_subprocess_path =
|
||||
path.normalize().toAbsolutePath().toString();
|
||||
}
|
||||
} else if (OS.isLinux()) {
|
||||
if (settings.browser_subprocess_path == null) {
|
||||
Path path = Paths.get(library_path, "jcef_helper");
|
||||
settings.browser_subprocess_path =
|
||||
path.normalize().toAbsolutePath().toString();
|
||||
}
|
||||
if (settings.resources_dir_path == null) {
|
||||
Path path = Paths.get(library_path);
|
||||
settings.resources_dir_path =
|
||||
path.normalize().toAbsolutePath().toString();
|
||||
}
|
||||
if (settings.locales_dir_path == null) {
|
||||
Path path = Paths.get(library_path, "locales");
|
||||
settings.locales_dir_path =
|
||||
path.normalize().toAbsolutePath().toString();
|
||||
}
|
||||
}
|
||||
|
||||
if (N_Initialize(appHandler_, settings)) {
|
||||
setState(CefAppState.INITIALIZED);
|
||||
} else {
|
||||
setState(CefAppState.INITIALIZATION_FAILED);
|
||||
}
|
||||
}
|
||||
};
|
||||
if (SwingUtilities.isEventDispatchThread())
|
||||
r.run();
|
||||
else
|
||||
SwingUtilities.invokeAndWait(r);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is invoked by the native code (currently on Mac only) in case
|
||||
* of a termination event (e.g. someone pressed CMD+Q).
|
||||
*/
|
||||
protected final void handleBeforeTerminate() {
|
||||
System.out.println("Cmd+Q termination request.");
|
||||
// Execute on the AWT event dispatching thread. Always call asynchronously
|
||||
// so the call stack has a chance to unwind.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
CefAppHandler handler =
|
||||
(CefAppHandler) ((appHandler_ == null) ? this : appHandler_);
|
||||
if (!handler.onBeforeTerminate()) dispose();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down the context.
|
||||
*/
|
||||
private final void shutdown() {
|
||||
// Execute on the AWT event dispatching thread. Always call asynchronously
|
||||
// so the call stack has a chance to unwind.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("shutdown on " + Thread.currentThread());
|
||||
|
||||
// Shutdown native CEF.
|
||||
N_Shutdown();
|
||||
|
||||
setState(CefAppState.TERMINATED);
|
||||
CefApp.self = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a single message loop iteration. Used on all platforms except
|
||||
* Windows with windowed rendering.
|
||||
*/
|
||||
public final void doMessageLoopWork(final long delay_ms) {
|
||||
// Execute on the AWT event dispatching thread.
|
||||
SwingUtilities.invokeLater(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (getState() == CefAppState.TERMINATED) return;
|
||||
|
||||
// The maximum number of milliseconds we're willing to wait between
|
||||
// calls to DoMessageLoopWork().
|
||||
final long kMaxTimerDelay = 1000 / 30; // 30fps
|
||||
|
||||
if (workTimer_ != null) {
|
||||
workTimer_.stop();
|
||||
workTimer_ = null;
|
||||
}
|
||||
|
||||
if (delay_ms <= 0) {
|
||||
// Execute the work immediately.
|
||||
N_DoMessageLoopWork();
|
||||
|
||||
// Schedule more work later.
|
||||
doMessageLoopWork(kMaxTimerDelay);
|
||||
} else {
|
||||
long timer_delay_ms = delay_ms;
|
||||
// Never wait longer than the maximum allowed time.
|
||||
if (timer_delay_ms > kMaxTimerDelay) timer_delay_ms = kMaxTimerDelay;
|
||||
|
||||
workTimer_ = new Timer((int) timer_delay_ms, new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent evt) {
|
||||
// Timer has timed out.
|
||||
workTimer_.stop();
|
||||
workTimer_ = null;
|
||||
|
||||
N_DoMessageLoopWork();
|
||||
|
||||
// Schedule more work later.
|
||||
doMessageLoopWork(kMaxTimerDelay);
|
||||
}
|
||||
});
|
||||
workTimer_.start();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This method must be called at the beginning of the main() method to perform platform-
|
||||
* specific startup initialization. On Linux this initializes Xlib multithreading and on
|
||||
* macOS this dynamically loads the CEF framework.
|
||||
* @param args Command-line arguments massed to main().
|
||||
* @return True on successful startup.
|
||||
*/
|
||||
public static final boolean startup(String[] args) {
|
||||
if (OS.isLinux() || OS.isMacintosh()) {
|
||||
SystemBootstrap.loadLibrary("jcef");
|
||||
return N_Startup(OS.isMacintosh() ? getCefFrameworkPath(args) : null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path which contains the jcef library
|
||||
* @return The path to the jcef library
|
||||
*/
|
||||
private static final String getJcefLibPath() {
|
||||
String library_path = System.getProperty("java.library.path");
|
||||
String[] paths = library_path.split(System.getProperty("path.separator"));
|
||||
for (String path : paths) {
|
||||
File dir = new File(path);
|
||||
String[] found = dir.list(new FilenameFilter() {
|
||||
@Override
|
||||
public boolean accept(File dir, String name) {
|
||||
return (name.equalsIgnoreCase("libjcef.dylib")
|
||||
|| name.equalsIgnoreCase("libjcef.so")
|
||||
|| name.equalsIgnoreCase("jcef.dll"));
|
||||
}
|
||||
});
|
||||
if (found != null && found.length != 0) return path;
|
||||
}
|
||||
return library_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the path that contains the CEF Framework on macOS.
|
||||
* @return The path to the CEF Framework.
|
||||
*/
|
||||
private static final String getCefFrameworkPath(String[] args) {
|
||||
// Check for the path on the command-line.
|
||||
String switchPrefix = "--framework-dir-path=";
|
||||
for (String arg : args) {
|
||||
if (arg.startsWith(switchPrefix)) {
|
||||
return new File(arg.substring(switchPrefix.length())).getAbsolutePath();
|
||||
}
|
||||
}
|
||||
|
||||
// Determine the path relative to the JCEF lib location in the app bundle.
|
||||
return new File(getJcefLibPath() + "/../Frameworks/Chromium Embedded Framework.framework")
|
||||
.getAbsolutePath();
|
||||
}
|
||||
|
||||
private final static native boolean N_Startup(String pathToCefFramework);
|
||||
private final native boolean N_PreInitialize();
|
||||
private final native boolean N_Initialize(CefAppHandler appHandler, CefSettings settings);
|
||||
private final native void N_Shutdown();
|
||||
private final native void N_DoMessageLoopWork();
|
||||
private final native CefVersion N_GetVersion();
|
||||
private final native boolean N_RegisterSchemeHandlerFactory(
|
||||
String schemeName, String domainName, CefSchemeHandlerFactory factory);
|
||||
private final native boolean N_ClearSchemeHandlerFactories();
|
||||
}
|
||||
Reference in New Issue
Block a user