From d116e80cbfd4690861102ea5eddc1de9bcde3be6 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Sat, 22 Mar 2025 21:05:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(browser):=20=E6=B7=BB=E5=8A=A0=20JCEF=20?= =?UTF-8?q?=E6=B5=8F=E8=A7=88=E5=99=A8=E7=AA=97=E5=8F=A3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 BrowserWindow 类实现浏览器窗口功能- 添加 WindowOperation、WindowOperationHandler 和 WindowRegistry 类用于窗口操作管理 - 在 MainApplication 中使用 JCEF 创建主窗口 - 实现了窗口创建、关闭等基本操作 --- build.gradle | 2 + .../innovators/box/browser/BrowserWindow.java | 369 +++++++++++ .../box/browser/MainApplication.java | 34 + .../box/browser/WindowOperation.java | 27 + .../box/browser/WindowOperationHandler.java | 94 +++ .../box/browser/WindowRegistry.java | 51 ++ .../com/axis/innovators/box/browser/main.html | 13 + src/main/java/org/cef/CefApp.java | 602 ++++++++++++++++++ 8 files changed, 1192 insertions(+) create mode 100644 src/main/java/com/axis/innovators/box/browser/BrowserWindow.java create mode 100644 src/main/java/com/axis/innovators/box/browser/MainApplication.java create mode 100644 src/main/java/com/axis/innovators/box/browser/WindowOperation.java create mode 100644 src/main/java/com/axis/innovators/box/browser/WindowOperationHandler.java create mode 100644 src/main/java/com/axis/innovators/box/browser/WindowRegistry.java create mode 100644 src/main/java/com/axis/innovators/box/browser/main.html create mode 100644 src/main/java/org/cef/CefApp.java diff --git a/build.gradle b/build.gradle index 1650a6d..07ec1aa 100644 --- a/build.gradle +++ b/build.gradle @@ -121,6 +121,8 @@ dependencies { // JavaFX 模块 implementation 'org.openjfx:javafx-graphics:21' + + implementation 'me.friwi:jcefmaven:122.1.10' } // 分离依赖项到 libs 目录 diff --git a/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java b/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java new file mode 100644 index 0000000..da6560f --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java @@ -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 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); + }); + } +} + diff --git a/src/main/java/com/axis/innovators/box/browser/MainApplication.java b/src/main/java/com/axis/innovators/box/browser/MainApplication.java new file mode 100644 index 0000000..4e6bb93 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/MainApplication.java @@ -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(); + } +} diff --git a/src/main/java/com/axis/innovators/box/browser/WindowOperation.java b/src/main/java/com/axis/innovators/box/browser/WindowOperation.java new file mode 100644 index 0000000..f9f4d00 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/WindowOperation.java @@ -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; + } +} diff --git a/src/main/java/com/axis/innovators/box/browser/WindowOperationHandler.java b/src/main/java/com/axis/innovators/box/browser/WindowOperationHandler.java new file mode 100644 index 0000000..d23115f --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/WindowOperationHandler.java @@ -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> operations; + private final Component attachedComponent; + + public static class Builder { + private WindowRegistry registry = WindowRegistry.getInstance(); + private final Map> 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 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 handler = operations.get(operation.getType()); + if (handler != null) { + handler.accept(operation.getTargetWindow()); + operation.getCallback().success("操作成功: " + operation.getType()); + } else { + operation.getCallback().failure(-1, "未定义的操作: " + operation.getType()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/browser/WindowRegistry.java b/src/main/java/com/axis/innovators/box/browser/WindowRegistry.java new file mode 100644 index 0000000..dfce264 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/WindowRegistry.java @@ -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 windows = + new ConcurrentHashMap<>(); + private final Map> 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 handler) { + callbacks.put(requestId, handler); + } + + public Consumer 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 config) { + BrowserWindow.Builder builder = new BrowserWindow.Builder(windowId); + config.accept(builder); + BrowserWindow window = builder.build(); + registerWindow(window); + } +} diff --git a/src/main/java/com/axis/innovators/box/browser/main.html b/src/main/java/com/axis/innovators/box/browser/main.html new file mode 100644 index 0000000..b923f71 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/main.html @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/src/main/java/org/cef/CefApp.java b/src/main/java/org/cef/CefApp.java new file mode 100644 index 0000000..bd23199 --- /dev/null +++ b/src/main/java/org/cef/CefApp.java @@ -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 clients_ = new HashSet(); + 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 clients = new HashSet(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(); +}