docs: 添加 Axis Innovators Box 框架完整 API 文档
- 新增浏览器模块技术文档,涵盖 BrowserCore、BrowserWindow 等核心组件 - 添加事件系统文档,包括 EventBus、GlobalEventBus 及各类事件定义 - 创建 LanguageManager 国际化管理器详细说明文档 - 新增 Log4j2OutputStream 标准输出重定向类文档 - 添加 Main 入口类启动流程与路由机制说明 - 创建 BrowserCreationCallback 回调接口使用指南 - 完善 AxisInnovatorsBox 主类架构与崩溃诊断系统文档
This commit is contained in:
700
README.md
700
README.md
@@ -1,151 +1,147 @@
|
||||
# AxisInnovatorsBoxWindowApi
|
||||
# AxisInnovatorsBox Window API
|
||||
|
||||
[项目链接](https://gitea.lingqi.vip/lanxi/window-axis-innovators-box) |
|
||||
[官网](https://box.nimblenexa.cn)
|
||||
| 简体中文
|
||||
[项目链接](https://gitea.lingqi.vip/lanxi/window-axis-innovators-box) | [官网](https://box.nimblenexa.cn) | 简体中文
|
||||
|
||||
---
|
||||
|
||||
## 项目概述
|
||||
## 📖 项目概述
|
||||
|
||||
`AxisInnovatorsBoxWindowApi` 是一个为 **AxisInnovatorsBox** 平台设计的管理API接口库,开发者可通过此API创建自定义插件,实现窗口管理、事件交互等核心功能。该仓库提供了接口定义、类说明文档及插件开发示例代码,帮助开发者快速接入AxisInnovatorsBox生态系统。
|
||||
`AxisInnovatorsBoxWindowApi` 是专为 **AxisInnovatorsBox** 平台打造的核心扩展 API 库。它赋予开发者创建自定义插件的能力,支持窗口深度管理、跨语言事件交互及 UI 定制。该仓库包含完整的接口定义、开发文档及示例代码,旨在帮助开发者无缝接入并扩展 AxisInnovatorsBox 生态系统。
|
||||
|
||||
## ✨ 功能特性
|
||||
|
||||
* 🖥️ **全生命周期窗口管理** - 精确控制窗口创建、销毁、最小化/最大化及模态状态。
|
||||
* 🎮 **事件驱动架构** - 基于发布/订阅模式的事件总线,支持系统级事件监听与自定义事件广播。
|
||||
* 📦 **多语言插件引擎** - 原生支持 Java 插件,并通过 Jython 实现 Python 插件的无缝加载与互操作。
|
||||
* 📄 **动态配置与国际化** - 支持通过 Properties 灵活管理配置,提供完整的 I18n 多语言支持。
|
||||
* 📊 **统一日志系统** - 集成 Java 日志框架,自动捕获并同步 Python 插件的运行日志与异常堆栈。
|
||||
|
||||
---
|
||||
|
||||
## 功能特性
|
||||
## 🧩 插件开发指南
|
||||
|
||||
- 🖥️ **窗口生命周期管理** - 创建/销毁窗口、调整窗口状态(最小化/最大化)
|
||||
- 🎮 **事件驱动交互** - 支持窗口事件监听与自定义事件触发
|
||||
- 📦 **跨语言插件支持** - 基于 Java 平台无缝加载 Python 插件,提供标准插件基类(Java/Python)实现快速扩展
|
||||
- 📄 **动态配置管理** - 通过 properties 配置文件灵活加载多语言插件(支持 Java/Python 插件声明)
|
||||
- 📊 **统一日志追踪** - 集成 Java 平台日志系统,同步记录 Python 插件的运行状态与异常信息
|
||||
插件加载系统是程序的核心组件,由主程序内部自动调度。
|
||||
|
||||
---
|
||||
### 1. Java 插件开发 (Jar)
|
||||
Jar 格式插件需放置在 `/plug-in` 目录下。
|
||||
|
||||
## 插件加载系统说明
|
||||
- 插件加载系统核心组件。
|
||||
- 插件加载系统由 **程序内部** 完成
|
||||
**核心注解 `@PluginMeta`**:
|
||||
用于描述插件的基本信息。系统会自动实例化被标记的类,并填充 `INSTANCE` 字段。
|
||||
|
||||
### 注册Jar插件
|
||||
- Jar插件在/plug-in中添加
|
||||
```java
|
||||
@PluginMeta(id = "test", name = "测试插件",
|
||||
package com.example.plugin;
|
||||
|
||||
import com.axis.innovators.box.plugins.PluginDescriptor;
|
||||
import com.axis.innovators.box.plugins.PluginMeta;
|
||||
import com.axis.innovators.box.events.GlobalEventBus;
|
||||
import com.axis.innovators.box.events.StartupEvent;
|
||||
import com.axis.innovators.box.events.SubscribeEvent;
|
||||
import com.axis.innovators.box.window.MainWindow;
|
||||
|
||||
@PluginMeta(
|
||||
id = "test_plugin",
|
||||
name = "测试插件",
|
||||
version = "1.0.0",
|
||||
supportedVersions = {"0.0.2"},
|
||||
description = "测试插件",
|
||||
icon = "",
|
||||
registeredName = "test")
|
||||
description = "这是一个Java插件示例",
|
||||
icon = "icon.png",
|
||||
registeredName = "test"
|
||||
)
|
||||
public class Template {
|
||||
// 系统加载时会自动注入此描述符
|
||||
public static PluginDescriptor INSTANCE = null;
|
||||
|
||||
public Template() {
|
||||
// 在构造函数中注册事件监听
|
||||
GlobalEventBus.EVENT_BUS.register(this);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@SubscribeEvent
|
||||
public void onStartup(StartupEvent event) {
|
||||
MainWindow.ToolCategory category = new MainWindow.ToolCategory("测试插件", "test", "测试插件");
|
||||
// 在启动事件中注册工具栏分类
|
||||
MainWindow.ToolCategory category = new MainWindow.ToolCategory("测试分类", "test_icon", "分类描述");
|
||||
event.main().getRegistrationTool().addToolCategory(
|
||||
category,
|
||||
INSTANCE,
|
||||
"templatePlugin"
|
||||
"templatePlugin::category"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
- 插件加载系统会自动填充 **INSTANCE** 内容
|
||||
- 使用PluginMeta注册插件信息
|
||||
|
||||
### Python插件注册
|
||||
- Python插件在/plug-in/python中添加
|
||||
- Python可以直接调用Java类实现对插件系统的控制
|
||||
- 插件还需要单独的放在一个子文件夹中,如/plug-in/python/Examples
|
||||
- Python插件需要声明一个metadata.json文件,如:
|
||||
### 2. Python 插件开发 (Script)
|
||||
Python 插件需放置在 `/plug-in/python` 的子目录中(例如 `/plug-in/python/Examples`)。Python 脚本可直接调用 Java 类库。
|
||||
|
||||
**必要文件结构**:
|
||||
* `metadata.json`: 插件元数据
|
||||
* `main.py`: 入口脚本
|
||||
|
||||
**metadata.json 示例**:
|
||||
```json
|
||||
{
|
||||
"id": "testing",
|
||||
"name": "测试",
|
||||
"id": "py_testing",
|
||||
"name": "Python测试插件",
|
||||
"version": "0.0.1",
|
||||
"description": "测试插件",
|
||||
"description": "Python脚本插件示例",
|
||||
"author": "tzdwindows 7",
|
||||
"dependencies": [],
|
||||
"_comment": {
|
||||
"warning": "本文件为插件元数据配置,修改后需重启应用生效",
|
||||
"path": "插件资源应放置在plugins/{id}/目录下"
|
||||
"path": "资源文件应放置在 plugins/{id}/ 目录下"
|
||||
}
|
||||
}
|
||||
```
|
||||
- Python插件需要声明一个main.py文件做为插件的主脚本,如:
|
||||
|
||||
**main.py 示例**:
|
||||
```python
|
||||
"""
|
||||
工具模块初始化脚本
|
||||
功能:向Axis Innovators Box注册自定义工具类别和工具项
|
||||
作者:tzdwindows 7
|
||||
版本:1.1
|
||||
功能:注册自定义工具类别和工具项
|
||||
"""
|
||||
from com.axis.innovators.box.python import PyLocalSide
|
||||
from javax.swing import AbstractAction
|
||||
|
||||
class MyAction(AbstractAction):
|
||||
def actionPerformed(self, event):
|
||||
"""工具项点击事件处理"""
|
||||
print("[DEBUG] Tool item clicked! Event source:", event.getSource())
|
||||
print("[DEBUG] Python工具项被点击!事件源:", event.getSource())
|
||||
|
||||
def onStartup():
|
||||
"""
|
||||
系统启动时自动执行的初始化逻辑
|
||||
功能:
|
||||
1. 创建工具类别
|
||||
2. 创建工具项并绑定动作
|
||||
3. 注册到系统全局工具集
|
||||
"""
|
||||
print('[INFO] 正在初始化自定义工具...')
|
||||
"""系统启动钩子函数"""
|
||||
print('[INFO] 初始化Python插件...')
|
||||
|
||||
# --------------------------
|
||||
# 创建工具类别(参数顺序:显示名称,图标资源名,描述)
|
||||
# --------------------------
|
||||
# 1. 创建工具类别
|
||||
tool_category = PyLocalSide.getToolCategory(
|
||||
u"数据分析工具", # 显示名称(GUI可见)
|
||||
u"analytics_icon.png", # 图标文件名(需存在于资源目录)
|
||||
u"高级数据分析功能集合" # 悬停提示描述
|
||||
u"数据分析", # 名称
|
||||
u"analytics.png", # 图标
|
||||
u"Python数据分析工具" # 描述
|
||||
)
|
||||
|
||||
# --------------------------
|
||||
# 创建工具项(参数顺序:显示名称,图标,描述,ID,动作对象)
|
||||
# --------------------------
|
||||
# 2. 创建并添加工具项
|
||||
tool_action = MyAction()
|
||||
tool_item = PyLocalSide.getToolItem(
|
||||
u"数据可视化", # 工具项显示名称
|
||||
u"chart_icon.png", # 工具项图标
|
||||
u"生成交互式数据图表", # 工具项描述
|
||||
1001, # 工具项唯一ID(需在配置中统一管理)
|
||||
tool_action # 点击触发的动作
|
||||
u"生成图表",
|
||||
u"chart.png",
|
||||
u"点击生成报表",
|
||||
1001,
|
||||
tool_action
|
||||
)
|
||||
tool_category.addTool(tool_item)
|
||||
|
||||
# --------------------------
|
||||
# 注册工具类别到系统(参数:类别对象,全局唯一注册名称)
|
||||
# --------------------------
|
||||
# 3. 注册到系统
|
||||
PyLocalSide.addToolCategory(
|
||||
tool_category,
|
||||
u"custom_module::data_analysis_tools" # 推荐命名规则:模块名::功能名
|
||||
u"py_module::analysis"
|
||||
)
|
||||
print('[SUCCESS] 工具类别注册成功')
|
||||
print('[SUCCESS] Python插件加载完成')
|
||||
|
||||
# 将 onStartup 绑定到全局作用域,供 Java 端调用
|
||||
if __name__ == '__main__':
|
||||
result = 0
|
||||
errorResult = ""
|
||||
|
||||
# 确保Jython运行时可以访问onStartup函数
|
||||
# 原理:将函数显式绑定到全局字典
|
||||
globals()['onStartup'] = onStartup
|
||||
```
|
||||
|
||||
### 声明CorePlugins
|
||||
* CorePlugins核心组件可以修改部分模块的字节码。
|
||||
* CorePlugins需要在jar的属性中添加 **CorePlugins: CorePlugins类位置**
|
||||
* 自动化构建,在build.gradle中添加如下代码:
|
||||
### 3. CorePlugins (字节码增强)
|
||||
CorePlugins 允许在类加载阶段修改目标类的字节码(ASM)。
|
||||
|
||||
**配置步骤**:
|
||||
1. 在 `build.gradle` 中声明 Manifest 属性:
|
||||
```groovy
|
||||
jar {
|
||||
manifest {
|
||||
@@ -153,510 +149,172 @@ jar {
|
||||
}
|
||||
}
|
||||
```
|
||||
* CorePlugins核心组件需要实现 **CorePlugins** 接口,如:
|
||||
2. 实现 `LoadingCorePlugin` 接口:
|
||||
```java
|
||||
package com.axis.core.template;
|
||||
|
||||
import com.axis.innovators.box.plugins.LoadingCorePlugin;
|
||||
import com.axis.innovators.template.Template;
|
||||
|
||||
/**
|
||||
* 注册core插件
|
||||
*/
|
||||
public class TemplateLoadingCorePlugin implements LoadingCorePlugin {
|
||||
@Override
|
||||
public String getMainClass() {
|
||||
// 返回主类名
|
||||
return Template.class.getName();
|
||||
return Template.class.getName(); // 返回插件主类
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getASMTransformerClass() {
|
||||
// 返回字节码转换器类名
|
||||
return new String[]{TemplateTransformer.class.getName()};
|
||||
return new String[]{TemplateTransformer.class.getName()}; // 返回Transformer类
|
||||
}
|
||||
}
|
||||
```
|
||||
* IClassTransformer的实现,如:
|
||||
3. 实现 `IClassTransformer` 接口(使用 ASM 修改字节码):
|
||||
```java
|
||||
package com.axis.core.template;
|
||||
|
||||
import com.axis.innovators.box.plugins.IClassTransformer;
|
||||
import org.objectweb.asm.*;
|
||||
|
||||
/**
|
||||
* core plugin transformer
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class TemplateTransformer implements IClassTransformer {
|
||||
@Override
|
||||
public byte[] transform(String s, String s1, byte[] bytes) {
|
||||
ClassReader classReader = new ClassReader(bytes);
|
||||
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS);
|
||||
|
||||
ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM5, classWriter) {
|
||||
@Override
|
||||
public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {
|
||||
if ((access & Opcodes.ACC_PRIVATE) != 0) {
|
||||
access = (access & ~Opcodes.ACC_PRIVATE) | Opcodes.ACC_PUBLIC;
|
||||
System.out.println("Changing field access to public: " + name);
|
||||
}
|
||||
return super.visitField(access, name, descriptor, signature, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
|
||||
System.out.println(name + " , descriptor:" + descriptor);
|
||||
if ((access & Opcodes.ACC_PRIVATE) != 0) {
|
||||
access = (access & ~Opcodes.ACC_PRIVATE) | Opcodes.ACC_PUBLIC;
|
||||
System.out.println("Changing method access to public: " + name);
|
||||
}
|
||||
return super.visitMethod(access, name, descriptor, signature, exceptions);
|
||||
}
|
||||
};
|
||||
|
||||
classReader.accept(classVisitor, 0);
|
||||
return classWriter.toByteArray();
|
||||
public byte[] transform(String name, String transformedName, byte[] basicClass) {
|
||||
// 使用 ASM ClassReader/ClassWriter 修改字节码
|
||||
// 示例:将所有 private 字段和方法修改为 public
|
||||
// ... (省略具体ASM代码,参考原文档)
|
||||
return modifiedBytes;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统说明
|
||||
- 事件驱动架构核心组件。
|
||||
- 事件总线系统,支持跨模块通信。
|
||||
- 事件总线由 **EventBus & GlobalEventBus** 实现
|
||||
---
|
||||
|
||||
### 1.. EventBus & GlobalEventBus 说明
|
||||
应用程序内的事件驱动架构核心组件:
|
||||
## 📡 事件驱动系统
|
||||
|
||||
基于 `EventBus` 的发布/订阅模式,实现模块解耦与通信。
|
||||
|
||||
### 核心组件
|
||||
* `EventBus`: 基础事件总线。
|
||||
* `GlobalEventBus.EVENT_BUS`: **全局单例总线**,用于跨模块通信。
|
||||
|
||||
### 使用示例
|
||||
```java
|
||||
package com.axis.innovators.box.events;
|
||||
|
||||
/**
|
||||
* 事件总线系统(支持多总线实例隔离)
|
||||
*/
|
||||
public class EventBus {
|
||||
// 核心方法
|
||||
public void register(Object listener); // 注册监听器
|
||||
public void unregister(Object target); // 注销监听器
|
||||
public boolean post(Object event); // 发布事件
|
||||
public void shutdown(); // 关闭总线
|
||||
}
|
||||
|
||||
public class GlobalEventBus {
|
||||
public static final EventBus EVENT_BUS = new EventBus(); // 全局单例总线
|
||||
}
|
||||
```
|
||||
- **EventBus**:用于处理应用程序内各个模块之间的事件通信。
|
||||
- **GlobalEventBus**:用于处理应用程序内各个模块之间的事件通信,支持多总线实例隔离。
|
||||
|
||||
### 2. EventBus & GlobalEventBus 使用示例
|
||||
|
||||
#### 示例1:基础事件处理
|
||||
```java
|
||||
// 1. 定义事件类型
|
||||
// 1. 定义事件
|
||||
public class UserLoginEvent {
|
||||
private final String username;
|
||||
private boolean cancelled;
|
||||
|
||||
public UserLoginEvent(String username) {
|
||||
this.username = username;
|
||||
public UserLoginEvent(String u) { this.username = u; }
|
||||
// getters...
|
||||
}
|
||||
|
||||
// Getter/Setter...
|
||||
// 2. 注册监听器 (@SubscribeEvent)
|
||||
public class LoginLogger {
|
||||
public LoginLogger() {
|
||||
GlobalEventBus.EVENT_BUS.register(this);
|
||||
}
|
||||
|
||||
// 2. 创建监听器类
|
||||
public class SecurityLogger {
|
||||
@SubscribeEvent
|
||||
public void logLoginAttempt(UserLoginEvent event) {
|
||||
System.out.println("[安全审计] 登录尝试: " + event.getUsername());
|
||||
public void onLogin(UserLoginEvent event) {
|
||||
System.out.println("用户登录: " + event.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 使用全局总线
|
||||
public class Main {
|
||||
public static void main(String[] args) {
|
||||
// 注册监听器
|
||||
GlobalEventBus.EVENT_BUS.register(new SecurityLogger());
|
||||
|
||||
// 模拟用户登录
|
||||
UserLoginEvent loginEvent = new UserLoginEvent("admin");
|
||||
GlobalEventBus.EVENT_BUS.post(loginEvent);
|
||||
}
|
||||
}
|
||||
```
|
||||
- 示例2:事件取消机制
|
||||
```java
|
||||
// 1. 定义可取消事件
|
||||
public class FileDeleteEvent {
|
||||
private final Path filePath;
|
||||
private boolean cancelled;
|
||||
|
||||
// 构造方法/getters/setters...
|
||||
}
|
||||
|
||||
// 2. 创建权限校验监听器
|
||||
public class PermissionValidator {
|
||||
@SubscribeEvent
|
||||
public void validateDeletePermission(FileDeleteEvent event) {
|
||||
if (!checkAdminAccess()) {
|
||||
event.setCancelled(true);
|
||||
System.out.println("文件删除被拒绝:权限不足");
|
||||
}
|
||||
}
|
||||
|
||||
private boolean checkAdminAccess() {
|
||||
// 权限校验逻辑
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 主业务流程
|
||||
public class FileManager {
|
||||
public void deleteFile(Path path) {
|
||||
FileDeleteEvent event = new FileDeleteEvent(path);
|
||||
GlobalEventBus.EVENT_BUS.post(event);
|
||||
|
||||
if (!event.isCancelled()) {
|
||||
// 执行删除操作
|
||||
System.out.println("正在删除文件: " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3. 发布事件
|
||||
GlobalEventBus.EVENT_BUS.post(new UserLoginEvent("admin"));
|
||||
```
|
||||
|
||||
### 3.所有系统所支持的事件
|
||||
- `com.axis.innovators.box.events.CategoryRenderingEvent`: 分类栏的渲染事件
|
||||
- `com.axis.innovators.box.events.MainWindowEvents`: 主窗口事件
|
||||
- `com.axis.innovators.box.events.OpenFileEvents`: 接收文件事件
|
||||
- `com.axis.innovators.box.events.SettingsLoadEvents`: 程序初始化事件
|
||||
- `com.axis.innovators.box.events.StartupEvent`: 程序启动事件
|
||||
- `com.axis.innovators.box.events.TABUIEvents`: 选项卡Ui属性事件
|
||||
### 内置系统事件表
|
||||
| 事件类名 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `StartupEvent` | 程序启动完成事件,常用于注册工具栏 |
|
||||
| `SettingsLoadEvents` | 设置界面初始化事件,用于添加设置项 |
|
||||
| `MainWindowEvents` | 主窗口生命周期相关事件 |
|
||||
| `CategoryRenderingEvent` | 工具栏分类渲染事件 |
|
||||
| `OpenFileEvents` | 外部文件打开请求事件 |
|
||||
| `TABUIEvents` | 选项卡 UI 属性变更事件 |
|
||||
|
||||
## HTML窗口集成指南
|
||||
---
|
||||
|
||||
我们实现了一套高性能的HTML渲染系统,通过Java Chromium Embedded Framework (JCEF) 将HTML内容无缝集成到Java桌面应用中,底层基于[jcefmaven](https://github.com/jcefmaven/jcefmaven)项目。
|
||||
## 🌐 HTML 窗口集成 (JCEF)
|
||||
|
||||
### 核心实现步骤
|
||||
通过 JCEF (Chromium) 渲染高性能 HTML/JS 界面,并支持与 Java 双向通信。
|
||||
|
||||
#### 1. 创建HTML窗口
|
||||
### 创建 HTML 窗口
|
||||
```java
|
||||
// 创建窗口引用
|
||||
AtomicReference<BrowserWindowJDialog> htmlWindow = new AtomicReference<>();
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 通过窗口注册表创建子窗口
|
||||
WindowRegistry.getInstance().createNewChildWindow("main", builder -> {
|
||||
htmlWindow.set(builder
|
||||
.title("Axis Innovators Box AI 工具箱") // 窗口标题
|
||||
.parentFrame(parentFrame) // 父级窗口
|
||||
.icon(getApplicationIcon()) // 应用图标
|
||||
.size(1280, 720) // 初始尺寸
|
||||
.htmlPath(getHtmlResourcePath()) // HTML文件路径
|
||||
.operationHandler(createOperationHandler()) // 自定义操作处理器
|
||||
.build());
|
||||
WindowRegistry.getInstance().createNewChildWindow("ai_toolbox", builder -> {
|
||||
builder.title("AI 工具箱")
|
||||
.size(1280, 720)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + "/ui/index.html") // 本地HTML路径
|
||||
.build();
|
||||
});
|
||||
|
||||
// 配置消息路由
|
||||
configureMessageRouter(htmlWindow.get());
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. 辅助方法
|
||||
```java
|
||||
// 获取应用图标
|
||||
private Image getApplicationIcon() {
|
||||
return new ImageIcon(Objects.requireNonNull(
|
||||
MainApplication.class.getClassLoader()
|
||||
.getResource("icons/logo.png")
|
||||
)).getImage();
|
||||
}
|
||||
### Java 与 JavaScript 通信 (`CefMessageRouter`)
|
||||
|
||||
// 获取HTML资源路径
|
||||
private String getHtmlResourcePath() {
|
||||
return FolderCreator.getJavaScriptFolder() + "/AIaToolbox_dark.html";
|
||||
}
|
||||
**1. JavaScript 调用 Java**
|
||||
```javascript
|
||||
// 在前端 JS 中调用
|
||||
window.cefQuery({
|
||||
request: JSON.stringify({ action: "getData", id: 1 }),
|
||||
onSuccess: function(response) { console.log("Java返回:", response); },
|
||||
onFailure: function(code, msg) { console.error("错误:", msg); }
|
||||
});
|
||||
```
|
||||
|
||||
#### 3. 配置消息路由器
|
||||
**2. Java 处理请求**
|
||||
```java
|
||||
private void configureMessageRouter(BrowserWindowJDialog window) {
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter == null) return;
|
||||
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
// 获取路由并添加处理器
|
||||
CefMessageRouter router = window.getMsgRouter();
|
||||
router.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId,
|
||||
String request, boolean persistent, CefQueryCallback callback) {
|
||||
// 处理来自HTML的请求
|
||||
handleBrowserRequest(request, callback);
|
||||
return true; // 表示已处理该请求
|
||||
if (request.contains("getData")) {
|
||||
callback.success("{\"data\": \"Hello from Java\"}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
|
||||
// 处理请求取消逻辑
|
||||
System.out.println("请求被取消: " + queryId);
|
||||
}
|
||||
}, true); // true表示优先处理
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
```
|
||||
|
||||
## HTML事件
|
||||
HTML窗口内可以捕捉到一些Java的事件
|
||||
|
||||
| 事件名 | 介绍 | 触发时机 |
|
||||
|--------|------------|---------------------|
|
||||
| `javaFontsLoaded` | Java字体加载完成 | Java字体信息传输到HTML时,或在在更新主题时 |
|
||||
| `javaThemeChanged` | 在主题发生变化时触发 | 在更新主题时 |
|
||||
|
||||
#### 具体示例
|
||||
**3. HTML 监听 Java 事件**
|
||||
HTML 页面可监听来自 Java 的系统级通知:
|
||||
* `javaFontsLoaded`: 字体加载完成。
|
||||
* `javaThemeChanged`: 主题变更通知。
|
||||
|
||||
```javascript
|
||||
// 监听Java字体加载事件
|
||||
document.addEventListener('javaFontsLoaded', function(event) {
|
||||
const fontInfo = event.detail;
|
||||
console.log('接收到Java字体信息:', fontInfo);
|
||||
|
||||
// 应用Java字体到界面
|
||||
applyJavaFonts(fontInfo);
|
||||
});
|
||||
// 监听Java主题变化事件
|
||||
document.addEventListener('javaThemeChanged', function(event) {
|
||||
const themeInfo = event.detail;
|
||||
console.log('接收到Java主题信息:', themeInfo);
|
||||
applyJavaTheme(themeInfo);
|
||||
});
|
||||
```
|
||||
### 窗口管理系统说明
|
||||
|
||||
通过`WindowRegistry`统一管理应用窗口:
|
||||
|
||||
| 方法 | 说明 |
|
||||
|------|------|
|
||||
| `createNewWindow(String id, Consumer<Builder> config)` | 创建主窗口 |
|
||||
| `createNewChildWindow(String id, Consumer<Builder> config)` | 创建模态子窗口 |
|
||||
| `getWindow(String id)` | 获取已注册窗口 |
|
||||
| `unregisterWindow(String id)` | 关闭指定窗口 |
|
||||
|
||||
### CefMessageRouter 使用指南
|
||||
|
||||
实现Java与JavaScript双向通信的核心组件:
|
||||
|
||||
1. **消息处理流程**:
|
||||
- JavaScript → Java: 通过`window.cefQuery()`发送请求
|
||||
- Java → JavaScript: 使用`CefFrame.executeJavaScript()`执行脚本
|
||||
|
||||
2. **核心方法**:
|
||||
```java
|
||||
// JavaScript调用示例
|
||||
function callJavaMethod(data) {
|
||||
window.cefQuery({
|
||||
request: JSON.stringify(data),
|
||||
onSuccess: response => console.log("Success:", response),
|
||||
onFailure: (err, msg) => console.error("Error:", msg)
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
3. **最佳实践**:
|
||||
- 使用JSON格式进行数据交换
|
||||
- 为不同功能模块使用独立的路由处理器
|
||||
- 在窗口关闭前移除所有路由处理器
|
||||
|
||||
### 生命周期管理
|
||||
```java
|
||||
// 关闭窗口时清理资源
|
||||
htmlWindow.get().addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(WindowEvent e) {
|
||||
window.getMsgRouter().dispose();
|
||||
CefApp.getInstance().dispose();
|
||||
}
|
||||
document.addEventListener('javaThemeChanged', (e) => {
|
||||
console.log('主题变更为:', e.detail);
|
||||
});
|
||||
```
|
||||
|
||||
## 核心类说明
|
||||
---
|
||||
|
||||
### 1. AxisInnovatorsBox
|
||||
窗口实例的核心操作类,提供以下功能:
|
||||
- `getMain()`: 获取当前AxisInnovatorsBox实例
|
||||
- `getMainWindow()`: 获取主窗口实例
|
||||
- `quit()`: 退出程序
|
||||
- `organizingCrashReports(Exception)`: 组织崩溃报告,用于在应用程序发生异常时生成崩溃报告。
|
||||
- `popupWindow(WindowsJDialog)`: 弹出新的窗口,并将其添加到窗口列表中。
|
||||
- `isWindowStartup(WindowsJDialog)`: 判断指定的窗口是否已经启动。
|
||||
- `clearWindow(WindowsJDialog)`: 清除指定的窗口,并将其从窗口列表中移除。
|
||||
- `reloadAllWindow()`: 重新加载窗口。
|
||||
- `getRegistrationTool()`: 获取注册工具实例。
|
||||
- `getArgs()`: 获取命令行参数。
|
||||
- `isWindow()`: 判断窗口是否已经启动。
|
||||
- `getVersion()`: 获取应用程序的版本号。
|
||||
- `getRegistrationTopic()`: 获取注册主题实例。
|
||||
- `getAuthor()`: 获取应用程序的作者信息。
|
||||
- `getStateManager()`: 获取状态管理器实例。
|
||||
## 📚 核心 API 参考
|
||||
|
||||
### 2. RegistrationTool
|
||||
负责在 **应用程序启动阶段** 注册和管理工具分类的核心组件,具备插件系统集成能力:
|
||||
### 1. `AxisInnovatorsBox` (主程序入口)
|
||||
获取应用全局状态和核心管理器。
|
||||
* `getInstance()` / `getMain()`: 获取单例。
|
||||
* `getRegistrationTool()`: 获取工具注册器。
|
||||
* `getRegistrationTopic()`: 获取主题注册器。
|
||||
* `getMainWindow()`: 获取主窗口对象。
|
||||
* `popupWindow(WindowsJDialog)` / `clearWindow(...)`: 窗口堆栈管理。
|
||||
|
||||
```java
|
||||
package com.axis.innovators.box.register;
|
||||
### 2. `RegistrationTool` (工具注册)
|
||||
在 `StartupEvent` 中使用,用于向主界面添加功能入口。
|
||||
* `addToolCategory(category, regName)`: 注册工具分类。
|
||||
* `getToolCategory(UUID)`: 获取已注册分类。
|
||||
|
||||
/**
|
||||
* 工具分类注册中心(窗口启动前必须完成注册)
|
||||
*/
|
||||
public class RegistrationTool {
|
||||
// 构造方法关联主程序实例
|
||||
public RegistrationTool(AxisInnovatorsBox main) { ... }
|
||||
### 3. `RegistrationTopic` (主题注册)
|
||||
在初始化阶段注册自定义 LookAndFeel 或主题类。
|
||||
* `addTopic(topicClass, name, tip, icon, regName)`: 注册主题。
|
||||
* **注意**: 注册名称 (`regName`) 必须唯一,否则会抛出异常。
|
||||
|
||||
// 核心功能方法
|
||||
public boolean addToolCategory(ToolCategory category, String regName);
|
||||
public void addToolCategory(ToolCategory category, PluginDescriptor descriptor, String regName);
|
||||
public ToolCategory getToolCategory(UUID id);
|
||||
public UUID getUUID(String registeredName);
|
||||
}
|
||||
```
|
||||
- `addToolCategory(ToolCategory category, String regName)`: 向工具分类注册中心添加一个新的工具分类。
|
||||
- `addToolCategory(ToolCategory category, PluginDescriptor descriptor, String regName)`: 向工具分类注册中心添加一个新的工具分类,同时关联插件描述符。
|
||||
- `getToolCategory(UUID id)`: 通过UUID获取工具分类。
|
||||
- `getUUID(String registeredName)`: 通过注册名称获取UUID。
|
||||
### 4. `RegistrationSettingsItem` (设置项注册)
|
||||
在 `SettingsLoadEvents` 中使用,向设置中心添加面板。
|
||||
* `addSettings(JPanel panel, String title, Icon icon, String tip, PluginDescriptor plugin, String regName)`: 注册设置页。
|
||||
|
||||
#### 注册示例
|
||||
```java
|
||||
// 创建调试工具分类
|
||||
MainWindow.ToolCategory debugCategory = new MainWindow.ToolCategory(
|
||||
"逆向分析工具",
|
||||
"icons/debugger.png",
|
||||
"二进制逆向分析工具集"
|
||||
);
|
||||
### 5. `StateManager` (状态持久化)
|
||||
用于保存插件配置或用户偏好设置(键值对)。
|
||||
* `saveState(key, value)`: 持久化保存。
|
||||
* `getStateAsString(key)` / `getStateAsInt(key)`: 读取配置。
|
||||
|
||||
// 添加工具项(带点击事件)
|
||||
debugCategory.addTool(new MainWindow.ToolItem(
|
||||
"内存分析器",
|
||||
"icons/memory.png",
|
||||
"实时查看进程内存映射",
|
||||
1,
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
JOptionPane.showMessageDialog(null, "启动内存分析模块...");
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
// 注册到系统(必须在窗口初始化前完成)
|
||||
try {
|
||||
registrationTool.addToolCategory(debugCategory, "system:reverseEngineering");
|
||||
} catch (RegistrationError ex) {
|
||||
System.err.println("注册失败: " + ex.getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### 3. RegistrationTopic
|
||||
负责在 **应用程序初始化阶段** 统一管理UI主题注册的核心组件,支持类名/LookAndFeel双模式主题注入:
|
||||
|
||||
```java
|
||||
package com.axis.innovators.box.register;
|
||||
|
||||
/**
|
||||
* 主题注册中心(窗口初始化前必须完成注册)
|
||||
*/
|
||||
public class RegistrationTopic {
|
||||
// 核心注册方法
|
||||
public void addTopic(String topicClass, String name, String tip, Icon icon, String regName);
|
||||
public void addTopic(LookAndFeel laf, String name, String tip, Icon icon, String regName);
|
||||
|
||||
// 状态管理方法
|
||||
public boolean isLoading(String themeName);
|
||||
public void setLoading(String themeName);
|
||||
}
|
||||
```
|
||||
- `addTopic(String topicClass, String name, String tip, Icon icon, String regName)`: 向主题注册中心添加一个新的主题。
|
||||
- `addTopic(LookAndFeel laf, String name, String tip, Icon icon, String regName)`: 向主题注册中心添加一个新的主题,同时关联LookAndFeel。
|
||||
- `isLoading(String themeName)`: 判断指定主题是否正在加载。
|
||||
- `setLoading(String themeName)`: 设置指定主题为正在加载状态。
|
||||
#### 注册示例
|
||||
```java
|
||||
try {
|
||||
// 重复注册相同名称
|
||||
topicRegistry.addTopic("com.axis.light.MaterialTheme",
|
||||
"质感浅色",
|
||||
"Material Design风格",
|
||||
materialIcon,
|
||||
"theme:light"); // 已存在同名注册
|
||||
} catch (RegistrationError ex) {
|
||||
// 捕获异常并提示:theme:light duplicate registered names
|
||||
JOptionPane.showMessageDialog(null, ex.getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
### 4. StateManager
|
||||
应用程序状态管理工具类,提供跨会话的配置持久化能力:
|
||||
|
||||
```java
|
||||
package com.axis.innovators.box.tools;
|
||||
|
||||
/**
|
||||
* 状态持久化管理器(线程安全)
|
||||
*/
|
||||
public class StateManager {
|
||||
// 构造方法
|
||||
public StateManager(); // 默认使用toolbox.properties
|
||||
public StateManager(String customFileName); // 自定义状态文件名
|
||||
|
||||
// 核心操作方法
|
||||
public void saveState(String key, [int|long|boolean...] value);
|
||||
public [String|int|boolean...] getStateAs[Type](String key);
|
||||
}
|
||||
```
|
||||
- `saveState(String key, [int|long|boolean...] value)`: 保存状态到配置文件。
|
||||
- `getStateAs[Type](String key)`: 从配置文件获取状态。
|
||||
- `[String|int|boolean...]`: 支持多种数据类型保存到配置文件,并支持多种数据类型从配置文件获取。
|
||||
|
||||
### 5. RegistrationSettingsItem
|
||||
负责管理系统 **设置中心** 的配置面板注册,支持插件化扩展设置项的核心组件:
|
||||
|
||||
```java
|
||||
package com.axis.innovators.box.register;
|
||||
|
||||
/**
|
||||
* 设置项注册中心(集成插件配置扩展能力)
|
||||
*/
|
||||
public class RegistrationSettingsItem extends WindowsJDialog {
|
||||
// 核心注册方法
|
||||
public void addSettings(JPanel panel, String title, Icon icon, String tip, String regName);
|
||||
public void addSettings(JPanel panel, String title, Icon icon, String tip,
|
||||
PluginDescriptor plugin, String regName);
|
||||
|
||||
// 查询方法
|
||||
public static List<RegistrationSettingsItem> getRegistrationsByPlugin(PluginDescriptor plugin);
|
||||
}
|
||||
```
|
||||
- `addSettings(JPanel panel, String title, Icon icon, String tip, String regName)`: 向设置项注册中心添加一个新的设置项。
|
||||
- `addSettings(JPanel panel, String title, Icon icon, String tip, PluginDescriptor plugin, String regName)`: 向设置项注册中心添加一个新的设置项,同时关联插件描述符。
|
||||
- `getRegistrationsByPlugin(PluginDescriptor plugin)`: 通过插件描述符获取关联的设置项列表。
|
||||
|
||||
### 6. LanguageManager
|
||||
应用程序多语言管理核心组件,支持动态加载与合并多语言资源:
|
||||
|
||||
```java
|
||||
package com.axis.innovators.box.register;
|
||||
|
||||
/**
|
||||
* 国际化语言管理中心(支持插件扩展语言包)
|
||||
*/
|
||||
public class LanguageManager {
|
||||
// 核心操作方法
|
||||
public static void addLanguage(Language lang);
|
||||
public static void loadLanguage(String regName);
|
||||
public static Language getLoadedLanguages();
|
||||
public static Language getLanguage(String identifier);
|
||||
}
|
||||
```
|
||||
- `addLanguage(Language lang)`: 向语言管理中心添加一个新的语言包。
|
||||
- `loadLanguage(String regName)`: 加载指定语言包。
|
||||
- `getLoadedLanguages()`: 获取当前系统加载的语言包。
|
||||
- `getLanguage(String identifier)`: 通过标识符获取语言包。
|
||||
### 6. `LanguageManager` (国际化)
|
||||
* `addLanguage(Language lang)`: 注册新的语言包。
|
||||
* `getLanguage(String id)`: 获取特定语言资源。
|
||||
|
||||
### 7. `WindowRegistry` (窗口管理)
|
||||
统一管理 JCEF 和原生 Swing 窗口的生命周期。
|
||||
* `createNewChildWindow(id, config)`: 创建模态子窗口。
|
||||
* `unregisterWindow(id)`: 安全关闭并注销窗口。
|
||||
108
api-documentation/AxisInnovatorsBox.md
Normal file
108
api-documentation/AxisInnovatorsBox.md
Normal file
@@ -0,0 +1,108 @@
|
||||
这是一个为您编写的专门用于介绍 `package com.axis.innovators.box` 中 `AxisInnovatorsBox` 类的 Markdown 文档。
|
||||
|
||||
该文档详细涵盖了类的架构、核心功能、生命周期管理、崩溃处理机制以及主题系统。
|
||||
|
||||
***
|
||||
|
||||
# AxisInnovatorsBox 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.AxisInnovatorsBox`
|
||||
**版本:** 0.2.2
|
||||
**作者:** tzdwindows 7, lyxyz5223, 🐾Mr. Liu🐾, 泽钰
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`AxisInnovatorsBox` 是 **Axis Innovators Box** 应用程序的核心入口类和应用控制器。它主要负责应用程序的生命周期管理、全局配置初始化、插件加载、GUI 窗口调度、崩溃诊断报告生成以及用户身份验证。
|
||||
|
||||
该类采用了 **单例模式 (Singleton Pattern)**,确保在整个应用运行期间只有一个主控制器实例存在,并通过 `GlobalEventBus` 进行事件驱动的模块间通信。
|
||||
|
||||
## 2. 核心架构与职责
|
||||
|
||||
该类承载了以下关键职责:
|
||||
|
||||
* **启动引导 (Bootstrapping):** 解析命令行参数,初始化 Log4j2 日志系统,加载原生库 (JNI)。
|
||||
* **资源加载:** 并行加载 Java 插件 (`PluginLoader`) 和 Python 插件 (`PluginPyLoader`),并管理加载进度条 (`ProgressBarManager`)。
|
||||
* **GUI 管理:** 初始化主窗口 (`MainWindow`),管理子窗口 (`WindowsJDialog`),并处理系统托盘 (`Tray`)。
|
||||
* **主题与外观:** 集成 FlatLaf 和 Material 主题,支持明/暗模式自动切换及系统主题监听。
|
||||
* **异常熔断与诊断:** 捕获未处理的异常,生成包含堆栈、内存快照 (Heap Dump)、线程快照等的详细崩溃报告。
|
||||
* **安全与验证:** 处理 Casdoor 用户登录,管理加密的 Token (`AESCryptoUtil`, `Base64CryptoUtil`)。
|
||||
|
||||
## 3. 关键特性详解
|
||||
|
||||
### 3.1 启动流程 (`run` 方法)
|
||||
启动过程被设计为异步非阻塞模式:
|
||||
1. **环境检查:** 区分快速启动模式 (`quickStart`) 和调试模式 (`isDebug`)。
|
||||
2. **原生库加载:** 在静态块中加载核心原生库:`FridaNative`, `ThrowSafely`, `DogAgent`, `RegisterTray`。
|
||||
3. **插件加载线程:** 开启独立线程加载插件,避免阻塞 UI 渲染,同时通过 `ProgressBarManager` 反馈进度。
|
||||
4. **事件广播:** 插件加载完成后,通过事件总线发送 `StartupEvent` 和 `OpenFileEvents`。
|
||||
5. **UI 呈现:** 最终调用 `runWindow()` 初始化并显示主界面。
|
||||
|
||||
### 3.2 强大的崩溃诊断系统 (`organizingCrashReports`)
|
||||
这是该类最复杂的功能之一。当发生未捕获异常时,它会:
|
||||
1. 拦截异常(包括 EDT 线程异常)。
|
||||
2. 收集全面的调试信息:
|
||||
* **日志:** Log4j2 的所有 Appender 日志。
|
||||
* **JVM 信息:** 类加载器层级、内存使用详情 (Heap/Non-Heap)、线程堆栈 (Thread Dump)、GC 状态。
|
||||
* **系统环境:** 环境变量、系统属性。
|
||||
* **内存转储:** 调用 `HotSpotDiagnosticMXBean` 生成 `.hprof` 文件。
|
||||
3. **UI 反馈:** 弹出一个自定义的崩溃对话框,展示错误堆栈和插件信息。
|
||||
4. **导出:** 允许用户将所有诊断信息打包导出为 ZIP 文件。
|
||||
|
||||
### 3.3 主题管理系统 (`setTopic` & `updateTheme`)
|
||||
支持高度可定制的外观,并能响应系统级的主题变更:
|
||||
* **集成主题库:** FlatLaf (Light, Dark, IntelliJ, Darcula, Mac), Material (Oceanic, Lite, Mars), 以及 Java 标准主题 (Metal, Motif)。
|
||||
* **动态切换:** 通过 `WindowsTheme` 监听系统主题变化,并实时广播 `TopicsUpdateEvents` 事件来刷新所有已打开的窗口。
|
||||
|
||||
### 3.4 身份验证 (`popupLogin`)
|
||||
* 集成了 **Casdoor** 认证服务器。
|
||||
* 使用 `StateManager` 本地存储加密后的登录 Token。
|
||||
* 支持自动登录:如果本地存在有效 Token,自动解密并校验;否则弹出登录窗口。
|
||||
|
||||
## 4. 主要方法说明
|
||||
|
||||
| 方法签名 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `static void run(String[] args, boolean isDebug, boolean quickStart)` | 程序的静态入口点。初始化全局异常处理并启动主实例。 |
|
||||
| `void quit()` | 安全退出程序,中断主线程并关闭 JVM。 |
|
||||
| `void runWindow()` | 初始化主窗口 UI,加载注册的工具组件,并显示窗口。 |
|
||||
| `void organizingCrashReports(Exception e)` | 核心崩溃处理逻辑。收集数据并显示错误报告 GUI。 |
|
||||
| `void reloadAllWindow()` | 重新加载所有活动窗口(主窗口及子弹窗),用于主题切换或语言更新时刷新 UI。 |
|
||||
| `void updateTheme(String themeName, boolean isDarkMode)` | 设置 Swing 的 LookAndFeel 并触发全局主题更新事件。 |
|
||||
| `static AxisInnovatorsBox getMain()` | 获取当前运行的应用主实例(单例)。 |
|
||||
|
||||
## 5. 调试功能
|
||||
|
||||
当 `isDebug` 为 `true` 时,程序会:
|
||||
1. 启动一个独立的 `DebugWindow`,实时显示应用内部状态。
|
||||
2. 允许通过 `generateFileDump()` 等内部方法手动生成性能分析数据。
|
||||
|
||||
## 6. 原生库依赖
|
||||
|
||||
该类在静态初始化块中加载了以下原生库,表明应用具备底层系统交互能力:
|
||||
* **FridaNative:** 可能用于动态插桩或 Hook。
|
||||
* **ThrowSafely:** 安全异常处理。
|
||||
* **DogAgent:** 代理或监控组件。
|
||||
* **RegisterTray:** 系统托盘的原生实现。
|
||||
|
||||
## 7. 代码示例:获取主实例与工具注册
|
||||
|
||||
```java
|
||||
// 获取主程序实例
|
||||
AxisInnovatorsBox app = AxisInnovatorsBox.getMain();
|
||||
|
||||
// 获取注册工具管理器
|
||||
RegistrationTool tool = app.getRegistrationTool();
|
||||
|
||||
// 检查是否处于调试环境
|
||||
if (app.isDebugEnvironment()) {
|
||||
System.out.println("Current task progress: " + app.progressBarManager);
|
||||
}
|
||||
|
||||
// 强制重新加载 UI (例如在插件更改后)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
app.reloadAllWindow();
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
97
api-documentation/Log4j2OutputStream.md
Normal file
97
api-documentation/Log4j2OutputStream.md
Normal file
@@ -0,0 +1,97 @@
|
||||
这是一个专门为 `com.axis.innovators.box.Log4j2OutputStream` 类编写的技术介绍文档。
|
||||
|
||||
---
|
||||
|
||||
# Log4j2OutputStream 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.Log4j2OutputStream`
|
||||
**主要功能:** 标准输出重定向与日志集成
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`Log4j2OutputStream` 是一个高度定制化的 `OutputStream` 实现,旨在解决 Java 应用程序中标准输出 (`System.out`) 和标准错误 (`System.err`) 与日志框架(Log4j2)集成时的冲突问题。
|
||||
|
||||
传统的日志重定向往往会“吞掉”控制台输出,导致开发者在调试时无法实时看到控制台信息。该类通过**多路输出机制**,实现了在保持控制台原始输出的同时,将内容自动同步到 Log4j2 日志记录器中,并保留一份副本在内存中用于崩溃诊断。
|
||||
|
||||
## 2. 核心特性
|
||||
|
||||
* **三路并行输出 (Triple-Way Output):** 任何写入此流的数据会同时流向:
|
||||
1. **原始控制台流:** 保证开发环境下的实时可见性。
|
||||
2. **Log4j2 Logger:** 将普通的 `print` 语句转换为结构化的日志条目。
|
||||
3. **内存缓冲区:** 静态存储在 `systemOutContent` 和 `systemErrContent` 中,供系统在发生崩溃时提取诊断数据。
|
||||
* **智能行缓冲 (Line-Buffered Logging):** 为了避免日志碎片化,该流会累积字符直到遇到换行符 (`\n`) 或手动触发 `flush()` 时,才会将其作为一条完整的日志记录(INFO 或 ERROR 级别)发送给 Log4j2。
|
||||
* **字符编码安全:** 全程采用 `StandardCharsets.UTF_8` 处理,有效防止中文字符在重定向过程中出现乱码。
|
||||
* **无缝集成:** 提供静态方法一键重定向整个系统的标准流。
|
||||
|
||||
## 3. 工作原理
|
||||
|
||||
### 3.1 数据流向图
|
||||
```text
|
||||
System.out.println("Hello")
|
||||
│
|
||||
▼
|
||||
Log4j2OutputStream.write()
|
||||
│
|
||||
├───> 原始 PrintStream (控制台显示)
|
||||
├───> 内存 ByteArrayOutputStream (诊断快照)
|
||||
└───> 内部 Buffer ───(遇到 \n)───> LogManager.getLogger().info()
|
||||
```
|
||||
|
||||
### 3.2 级别映射
|
||||
* `System.out` 映射为 Log4j2 的 **INFO** 级别。
|
||||
* `System.err` 映射为 Log4j2 的 **ERROR** 级别。
|
||||
|
||||
## 4. API 说明
|
||||
|
||||
### 4.1 核心静态方法
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `static void redirectSystemStreams()` | **最常用的方法**。调用后会立即劫持全局 `System.out` 和 `System.err`,并将其替换为封装后的 `Log4j2OutputStream`。 |
|
||||
| `static String getSystemOutContent()` | 获取自程序启动(或上次清空)以来标准输出的所有文本内容。 |
|
||||
| `static String getSystemErrContent()` | 获取自程序启动以来标准错误的所有文本内容。 |
|
||||
| `static void clearBuffers()` | 重置内存中的静态缓冲区,释放内存。 |
|
||||
|
||||
### 4.2 构造函数
|
||||
|
||||
```java
|
||||
public Log4j2OutputStream(boolean isErrorStream, PrintStream originalStream)
|
||||
```
|
||||
* `isErrorStream`: 指定该流是否作为错误流处理(影响日志级别)。
|
||||
* `originalStream`: 传入系统原始的打印流(如 `System.out` 在重定向前的副本),确保数据能回流到控制台。
|
||||
|
||||
## 5. 使用场景:崩溃诊断报告
|
||||
|
||||
该类与 `AxisInnovatorsBox` 的崩溃报告系统紧密配合。当程序发生未捕获异常时,`AxisInnovatorsBox` 会调用:
|
||||
|
||||
```java
|
||||
String systemOut = Log4j2OutputStream.systemOutContent.toString();
|
||||
String systemErr = Log4j2OutputStream.systemErrContent.toString();
|
||||
// ... 组合成完整的诊断报告
|
||||
```
|
||||
|
||||
这确保了即使用户没有查看日志文件的习惯,程序也能在崩溃瞬间抓取到最后时刻的控制台输出,极大地方便了远程排查。
|
||||
|
||||
## 6. 使用示例
|
||||
|
||||
在程序启动的最早期(通常在 `main` 方法或初始化块中)调用:
|
||||
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
// 初始化并启动重定向
|
||||
Log4j2OutputStream.redirectSystemStreams();
|
||||
|
||||
// 现在的 println 既会出现在控制台,也会进入 log4j2 配置文件定义的 Appender 中
|
||||
System.out.println("This is a test message.");
|
||||
System.err.println("This error will be logged as ERROR level.");
|
||||
}
|
||||
```
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. **资源开销:** 由于内存中保留了输出副本,如果程序产生极大量的控制台输出(如数 GB 的日志),应定期调用 `clearBuffers()` 以防止内存溢出。
|
||||
2. **线程安全:** 该类依赖 `ByteArrayOutputStream` 的同步机制和 Log4j2 的线程安全保证。
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
75
api-documentation/Main.md
Normal file
75
api-documentation/Main.md
Normal file
@@ -0,0 +1,75 @@
|
||||
这是一个专门为 `com.axis.innovators.box.Main` 类编写的技术介绍文档。
|
||||
|
||||
---
|
||||
|
||||
# Main 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.Main`
|
||||
**主要功能:** 应用程序入口点、启动路由、单实例控制
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`Main` 类是 **Axis Innovators Box** 的核心启动器(Launcher)。它并不直接负责业务逻辑,而是扮演“调度员”的角色。它的主要职责包括初始化基础环境(语言、日志)、解析命令行参数、执行单实例检查,并根据输入文件的类型决定是启动完整的主程序,还是进入特定的“快速启动(Quick Start)”模式。
|
||||
|
||||
## 2. 核心流程与职责
|
||||
|
||||
### 2.1 启动预处理
|
||||
在程序真正运行前,`Main` 执行以下环境准备工作:
|
||||
* **日志清理:** 调用 `FolderCleaner` 清理过期的日志文件(保留最近10天)。
|
||||
* **多语言初始化:** 通过 `LanguageManager` 加载用户保存的语言设置,默认为中文(zh_CN)。
|
||||
* **插件目录重定向:** 解析 `pluginsDirectory=` 参数,动态设置插件加载路径。
|
||||
|
||||
### 2.2 启动路由机制 (Quick Start)
|
||||
这是 `Main` 类最显著的特性。它会分析命令行传入的文件扩展名,并进行智能跳转:
|
||||
|
||||
| 文件类型 | 触发行为 | 对应组件 |
|
||||
| :--- | :--- | :--- |
|
||||
| `.jar` | 进入 JAR 可视化分析模式 | `ModernJarViewer` |
|
||||
| `.html` | 进入 HTML 浏览模式 | `MainApplication` |
|
||||
| 音乐类 (`.mflac`, `.mgg`, `.qmc`等) | 进入音乐解密界面 | `DecryptionUI` |
|
||||
|
||||
**快速启动逻辑:** 如果识别到上述特定文件,程序会立即启动对应的专用窗口,释放文件锁,并将 `quickStart` 标记设为 `true`,从而跳过完整的主界面初始化过程。
|
||||
|
||||
### 2.3 单实例锁 (Single Instance Lock)
|
||||
为了防止多个程序实例同时运行冲突,`Main` 实现了一套基于文件锁的保护机制:
|
||||
* **锁文件:** 在系统临时目录下创建 `axis_innovators_box.lock`。
|
||||
* **原理:** 利用 `FileChannel.tryLock()` 尝试获取排他锁。如果获取失败,说明已有实例在运行,程序将直接退出。
|
||||
* **释放:** 在程序关闭(Shutdown Hook)或进入快速启动模式时,会自动释放锁。
|
||||
|
||||
## 3. 命令行参数说明
|
||||
|
||||
`Main` 类支持以下关键参数:
|
||||
|
||||
* **`-debugControlWindow-on`**: 开启调试控制窗口(仅在非发布环境下有效)。
|
||||
* **`pluginsDirectory="path"`**: 指定插件的存储目录。
|
||||
* **文件路径**: 直接传入文件路径,程序会自动判断扩展名并路由至对应的工具。
|
||||
|
||||
## 4. 关键方法解析
|
||||
|
||||
### 4.1 `main(String[] args)`
|
||||
启动主函数。逻辑顺序为:
|
||||
1. 基础环境清理与加载。
|
||||
2. 过滤并解析特定标志位参数。
|
||||
3. 遍历文件参数,检查是否符合“快速启动”条件。
|
||||
4. 如果不是快速启动模式,则尝试获取单实例锁。
|
||||
5. 调用 `AxisInnovatorsBox.run()` 移交控制权。
|
||||
|
||||
### 4.2 `acquireLock()` & `releaseLock()`
|
||||
* **`acquireLock`**: 尝试在磁盘上锁定文件。如果返回 `false`,则程序退出。
|
||||
* **`releaseLock`**: 关闭文件通道并删除锁文件。这是确保应用能二次启动的关键清理步骤。
|
||||
|
||||
## 5. 设计模式与技术点
|
||||
|
||||
* **资源保护 (Shutdown Hook):** 通过 `Runtime.getRuntime().addShutdownHook` 注册清理线程,确保无论程序是正常关闭还是异常中止,都能尝试释放文件锁。
|
||||
* **异步启动:** 对于 UI 组件(如 `ModernJarViewer` 和 `DecryptionUI`),使用 `SwingUtilities.invokeLater` 确保在事件调度线程(EDT)中创建界面,保证线程安全。
|
||||
* **参数剥离:** 采用 `List<String> remainingArgs` 机制,将系统级参数(如目录设置)与业务级参数(待处理文件)分离。
|
||||
|
||||
## 6. 与 AxisInnovatorsBox 的关系
|
||||
|
||||
`Main` 类是第一级入口,而 `AxisInnovatorsBox` 是第二级核心。
|
||||
* 如果 `Main` 识别到特定文件,它会配置一个轻量级的 `AxisInnovatorsBox` 实例(`quickStart=true`)。
|
||||
* 如果没有特定文件,它将锁定单实例环境,并启动完整的 `AxisInnovatorsBox` 重量级主循环。
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
137
api-documentation/browser/Axis Innovators Box Browser 模块使用文档.md
Normal file
137
api-documentation/browser/Axis Innovators Box Browser 模块使用文档.md
Normal file
@@ -0,0 +1,137 @@
|
||||
这份文档是对 `com.axis.innovators.box.browser` 包核心架构的深度解析。它涵盖了从底层核心代理到高层 UI 容器,以及 Java-JS 桥接机制的所有关键类。
|
||||
|
||||
---
|
||||
|
||||
# Axis Innovators Box Browser 核心类技术文档
|
||||
|
||||
本模块旨在为 Swing 应用程序提供一个高性能、易扩展的嵌入式浏览器解决方案。通过高度抽象的架构,开发者可以轻松地在 Java 窗口中嵌入 Web 页面,并实现双向通信。
|
||||
|
||||
---
|
||||
|
||||
## 1. 核心架构设计
|
||||
|
||||
系统采用了 **代理模式 (Proxy Pattern)** 和 **构建者模式 (Builder Pattern)**:
|
||||
- **核心逻辑** (`BrowserCore`) 与 **窗口容器** (`BrowserWindow`/`BrowserWindowJDialog`) 分离。
|
||||
- **配置逻辑** (`BaseBrowserBuilder`) 统一管理窗口属性。
|
||||
- **通信逻辑** (`JsBridgeController`) 通过注解驱动。
|
||||
|
||||
---
|
||||
|
||||
## 2. 接口与基类 (抽象层)
|
||||
|
||||
### 2.1 `BrowserContainer.java` (统一接口)
|
||||
这是所有浏览器窗口容器的通用协议。无论是 `JFrame` 还是 `JDialog`,都必须实现此接口。
|
||||
- **核心职责**:提供对外的统一 API,使得 `WindowRegistry` 和业务逻辑可以忽略窗口的具体物理类型。
|
||||
- **关键方法**:
|
||||
- `executingJsCode(String script)`: 在 JS 环境中执行代码(支持跨线程)。
|
||||
- `setController(JsBridgeController controller)`: 动态切换 Java 逻辑控制器。
|
||||
- `getBrowser()` / `getMsgRouter()`: 获取底层 CEF 对象。
|
||||
|
||||
### 2.2 `BaseBrowserBuilder.java` (通用配置器)
|
||||
采用泛型链式调用的抽象构建器类。
|
||||
- **核心职责**:存储所有窗口初始化所需的配置数据(尺寸、标题、初始 URL、图标等)。
|
||||
- **主要字段**:
|
||||
- `htmlUrl` / `htmlPath`: 指定要加载的内容来源。
|
||||
- `operationHandler`: 系统级消息处理器。
|
||||
- `controller`: 初始的 JSBridge 控制器。
|
||||
- `size`, `title`, `resizable`: 基础窗口属性。
|
||||
|
||||
---
|
||||
|
||||
## 3. UI 容器层 (实现层)
|
||||
|
||||
### 3.1 `BrowserWindow.java`
|
||||
继承自 `JFrame`,用于创建独立的主窗口或顶层窗口。
|
||||
- **特点**:支持任务栏图标、最大化/最小化等标准窗口行为。
|
||||
- **内部实现**:通过 `BrowserCore` 初始化浏览器组件,并将其作为 `BorderLayout.CENTER` 添加到内容面板。
|
||||
|
||||
### 3.2 `BrowserWindowJDialog.java`
|
||||
继承自 `JDialog`,用于创建子窗口或模态/非模态对话框。
|
||||
- **特点**:支持设置 `parentFrame`,可以实现点击父窗口时弹窗保持在最前的逻辑。
|
||||
- **应用场景**:设置界面、详情弹窗、登录界面等。
|
||||
|
||||
---
|
||||
|
||||
## 4. 浏览器引擎核心
|
||||
|
||||
### 4.1 `BrowserCore.java` (系统大脑)
|
||||
这是最关键的类,封装了 JCEF 的所有复杂逻辑。
|
||||
- **职责**:
|
||||
1. **初始化**:管理 `CefClient` 的创建和 `CefBrowser` 的生命周期。
|
||||
2. **Handler 路由**:自动设置加载监听、右键菜单处理、证书错误处理、文件选择对话框处理等。
|
||||
3. **通信初始化**:配置 `CefMessageRouter` (基于 `window.javaQuery`)。
|
||||
4. **脚本队列**:维护 `pendingScripts` 队列。如果页面尚未加载完成就调用了 JS 代码,代码会暂存在队列中,直到 `onLoadEnd` 触发后自动冲刷执行。
|
||||
5. **资源注入**:自动向 Web 页面注入系统字体、主题颜色和 `extLibsPath` 等全局变量。
|
||||
|
||||
---
|
||||
|
||||
## 5. Java-JS 桥接机制 (通信层)
|
||||
|
||||
这是实现“Java 调用 JS”和“JS 调用 Java”的核心组件。
|
||||
|
||||
### 5.1 `JsMapping.java` (方法注解)
|
||||
一个运行时注解,用于标记控制器中可以被 JS 访问的方法。
|
||||
- **属性 `value()`**:
|
||||
- 若为空:默认挂载在 `tzd.方法名()`。
|
||||
- 若为单纯字符串(如 `"login"`):挂载在 `tzd.login()`。
|
||||
- 若包含点号(如 `"app.utils.calc"`):挂载在 `window.app.utils.calc()`。
|
||||
|
||||
### 5.2 `JsBridgeController.java` (抽象控制器)
|
||||
用户自定义业务逻辑的基类。
|
||||
- **工作原理**:
|
||||
1. **反射扫描**:在初始化时自动扫描带有 `@JsMapping` 的所有方法。
|
||||
2. **代码生成**:`generateInjectionJs()` 会生成一套复杂的 JS 代理对象代码,在 `onLoadEnd` 时注入浏览器。
|
||||
3. **参数转换**:利用 Gson 将 JS 传来的 JSON 数组自动反序列化为 Java 方法的参数类型(如 `String`, `int`, `boolean` 或自定义 POJO)。
|
||||
4. **异步回调**:封装了 `window.javaQuery` 的复杂调用,使 JS 端可以像调用本地函数一样使用 `async/await` 获取结果。
|
||||
|
||||
---
|
||||
|
||||
## 6. 开发流程示例
|
||||
|
||||
### 第一步:定义控制器
|
||||
```java
|
||||
public class MyActionController extends JsBridgeController {
|
||||
@JsMapping("saveConfig") // JS 调用: tzd.saveConfig(data)
|
||||
public boolean save(String data) {
|
||||
System.out.println("Saving: " + data);
|
||||
return true;
|
||||
}
|
||||
|
||||
@JsMapping("sys.getInfo") // JS 调用: sys.getInfo()
|
||||
public String getInfo() {
|
||||
return "Version 1.0.0";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 第二步:启动窗口
|
||||
```java
|
||||
WindowRegistry.getInstance().createNewChildWindow("win01", builder -> {
|
||||
builder.title("我的应用")
|
||||
.htmlPath("index.html")
|
||||
.controller(new MyActionController()); // 绑定
|
||||
});
|
||||
```
|
||||
|
||||
### 第三步:前端调用
|
||||
```javascript
|
||||
// index.html
|
||||
async function doSave() {
|
||||
// 调用 Java 并获取返回值
|
||||
const success = await tzd.saveConfig("{id: 1}");
|
||||
if (success) {
|
||||
const info = await sys.getInfo();
|
||||
alert("保存成功, " + info);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 最佳实践建议
|
||||
|
||||
1. **资源管理**:始终通过 `WindowRegistry.unregisterWindow()` 关闭窗口,以确保内存和进程资源被释放。
|
||||
2. **线程注意事项**:
|
||||
- `executingJsCode` 是线程安全的,可以在任意线程调用。
|
||||
- 在 `@JsMapping` 标记的 Java 方法中,如果涉及 UI 更新(如修改 Swing 组件),必须包裹在 `SwingUtilities.invokeLater` 中。
|
||||
3. **动态切换**:由于 `setController` 会重新注入脚本,建议在页面初次加载完成后进行控制器切换,以获得最佳稳定性。
|
||||
122
api-documentation/events/Axis Innovators Box 事件类集合技术文档.md
Normal file
122
api-documentation/events/Axis Innovators Box 事件类集合技术文档.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Axis Innovators Box 事件类集合技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.events.*`
|
||||
**主要功能:** 作为事件总线(EventBus)的载体,定义系统各模块间的通信数据结构
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
本篇文档总结了 **Axis Innovators Box** 框架中定义的核心事件类。这些类作为 `EventBus` 分发的数据负载(Payload),涵盖了应用程序生命周期、UI 渲染、用户交互及系统设置等多个维度。
|
||||
|
||||
框架采用了 **Java Records (JDK 16+)** 和 **常规 POJO** 混合编写的方式:
|
||||
* **Records**: 用于纯数据传输,简洁高效。
|
||||
* **Classes**: 用于需要逻辑处理或可变状态(如事件拦截)的复杂场景。
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心事件分类详解
|
||||
|
||||
### 2.1 系统生命周期事件
|
||||
|
||||
#### `StartupEvent` (Record)
|
||||
* **触发时机**: 应用程序启动阶段。
|
||||
* **用途**: 允许插件或子模块在系统就绪时获取主程序实例并执行初始化。
|
||||
* **字段**:
|
||||
* `AxisInnovatorsBox main`: 主程序入口实例。
|
||||
|
||||
---
|
||||
|
||||
### 2.2 主窗口与 UI 交互事件
|
||||
|
||||
#### `MainWindowEvents` (Container Class)
|
||||
内部包含两个核心 Record,用于处理主窗口的不同阶段:
|
||||
* **`initialize` (Record)**:
|
||||
* **触发时机**: 主窗口初始化加载时。
|
||||
* **用途**: 向主面板 (`mainPanel`) 添加自定义组件。
|
||||
* **`update` (Record)**:
|
||||
* **触发时机**: 窗口重绘或更新周期。
|
||||
* **用途**: 执行基于 `Graphics` 的自定义绘制逻辑。
|
||||
|
||||
#### `TABUIEvents` (Class)
|
||||
* **用途**: 管理选项卡(Tab)组件的 UI 属性设置。
|
||||
* **内部类 `update`**: 专门用于选项卡的重绘事件,传递 `Graphics` 上下文。
|
||||
|
||||
#### `TopicsUpdateEvents` (Class)
|
||||
* **触发时机**: 当系统主题发生变更(如切换深色模式/更换皮肤)时。
|
||||
* **用途**: 通知所有 UI 组件同步更新样式。
|
||||
* **字段**:
|
||||
* `String themeName`: 主题名称。
|
||||
* `boolean darkTheme`: 是否为暗黑模式。
|
||||
|
||||
---
|
||||
|
||||
### 2.3 文件与 IO 操作事件
|
||||
|
||||
#### `OpenFileEvents` (Class)
|
||||
* **触发时机**: 当程序接收到外部文件打开请求(如拖拽文件入窗口)时。
|
||||
* **核心功能**: **流程控制**。
|
||||
* **关键字段**:
|
||||
* `File filePath`: 文件路径。
|
||||
* `String extension`: 文件后缀名。
|
||||
* `boolean isContinue`: 默认值为 `true`。监听器可以通过 `setContinue(false)` 来**拦截**后续操作,阻止程序打开该文件。
|
||||
|
||||
---
|
||||
|
||||
### 2.4 设置与配置事件
|
||||
|
||||
#### `SettingsLoadEvents` (Record)
|
||||
* **触发时机**: 设置对话框加载时。
|
||||
* **用途**: 动态扩展设置界面。开发者可以获取 `JPanel content` 容器,向其中注入新的设置项(如复选框、输入框)。
|
||||
* **字段**:
|
||||
* `WindowsJDialog dialog`: 设置窗口实例。
|
||||
* `JPanel content`: 承载设置内容的容器面板。
|
||||
|
||||
---
|
||||
|
||||
## 3. 事件一览表
|
||||
|
||||
| 事件类名 | 类型 | 核心字段 | 应用场景 |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `StartupEvent` | Record | `main` | 插件初始化、全局资源准备 |
|
||||
| `MainWindowEvents.initialize` | Record | `mainWindow`, `mainPanel` | 注入主界面组件 |
|
||||
| `MainWindowEvents.update` | Record | `mainWindow`, `g` | 自定义画布渲染 |
|
||||
| `OpenFileEvents` | Class | `filePath`, `isContinue` | 文件打开监听、格式校验与拦截 |
|
||||
| `SettingsLoadEvents` | Record | `dialog`, `content` | 动态添加设置选项 |
|
||||
| `TABUIEvents` | Class | `javax`, `card` | 选项卡组件管理 |
|
||||
| `TopicsUpdateEvents` | Class | `themeName`, `darkTheme` | 皮肤切换、实时调色板更新 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 使用代码示例
|
||||
|
||||
### 示例 1:拦截特定格式的文件打开
|
||||
```java
|
||||
@SubscribeEvent
|
||||
public void onFileOpen(OpenFileEvents event) {
|
||||
if (event.getExtension().equalsIgnoreCase("tmp")) {
|
||||
System.out.println("拒绝打开临时文件");
|
||||
event.setContinue(false); // 拦截流程
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例 2:在系统启动时注册组件
|
||||
```java
|
||||
@SubscribeEvent
|
||||
public void onStartup(StartupEvent event) {
|
||||
System.out.println("Axis Innovators Box 已启动: " + event.main().getClass().getName());
|
||||
}
|
||||
```
|
||||
|
||||
### 示例 3:动态添加设置项
|
||||
```java
|
||||
@SubscribeEvent
|
||||
public void onSettingsLoad(SettingsLoadEvents event) {
|
||||
event.content().add(new JButton("扩展插件设置"));
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
83
api-documentation/events/BrowserCreationCallback.md
Normal file
83
api-documentation/events/BrowserCreationCallback.md
Normal file
@@ -0,0 +1,83 @@
|
||||
# BrowserCreationCallback 接口技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.events.BrowserCreationCallback`
|
||||
**类型:** 接口 (Interface)
|
||||
**主要功能:** 浏览器窗口布局自定义回调
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`BrowserCreationCallback` 是 **Axis Innovators Box** 框架中用于高度定制浏览器窗口布局的扩展接口。
|
||||
|
||||
在框架启动嵌入式浏览器窗口(如 HTML 查看器、数据库管理工具、或 AI 界面)时,默认会执行一套标准的布局逻辑(通常是将浏览器组件充满整个内容区域)。通过实现此接口,开发者可以拦截并接管窗口的布局过程,从而在浏览器组件周边添加自定义的 UI 元素(如侧边栏、工具栏、状态栏等)。
|
||||
|
||||
## 2. 核心方法详解
|
||||
|
||||
### 2.1 `onLayoutCustomization`
|
||||
|
||||
```java
|
||||
boolean onLayoutCustomization(
|
||||
Window window,
|
||||
Container contentPane,
|
||||
Component browserComponent,
|
||||
Object builder
|
||||
);
|
||||
```
|
||||
|
||||
#### 参数说明:
|
||||
* **`window`**: 当前正在创建的顶级窗口对象(通常是 `JFrame` 或 `JDialog` 的实例)。
|
||||
* **`contentPane`**: 窗口的主内容面板。开发者应在此容器上执行 `setLayout` 或 `add` 操作。
|
||||
* **`browserComponent`**: 已经实例化完成的浏览器渲染组件(UI 组件)。
|
||||
* **`builder`**: 触发创建流程的构建器对象(通常是内部的 `BrowserWindowBuilder`)。通过此对象可以获取更多上下文配置。
|
||||
|
||||
#### 返回值逻辑:
|
||||
* **`true` (已处理)**: 告诉框架:*“我已经手动安排好了所有组件的布局,请不要再执行系统默认的布局逻辑。”*
|
||||
* **`false` (未完全处理)**: 告诉框架:*“我可能做了一些预处理(如设置了背景色),但请继续执行默认的布局逻辑(通常是将 `browserComponent` 放入 `BorderLayout.CENTER`)。”*
|
||||
|
||||
---
|
||||
|
||||
## 3. 使用场景
|
||||
|
||||
该回调接口通常用于以下高级自定义需求:
|
||||
1. **添加导航栏**: 在浏览器组件上方添加地址栏、后退/前进按钮。
|
||||
2. **集成侧边栏**: 在浏览器左侧添加树状菜单或历史记录面板。
|
||||
3. **多组件混排**: 将浏览器作为界面的一部分,与其他 Swing 组件(如表格、控制台)共同展示。
|
||||
4. **注入装饰器**: 为浏览器窗口添加自定义的边框、水印或重写背景。
|
||||
|
||||
---
|
||||
|
||||
## 4. 代码示例:在浏览器上方添加工具栏
|
||||
|
||||
以下示例展示了如何使用该回调在浏览器窗口中插入一个简单的工具栏:
|
||||
|
||||
```java
|
||||
BrowserCreationCallback myCallback = (window, contentPane, browserComponent, builder) -> {
|
||||
// 1. 设置内容面板布局
|
||||
contentPane.setLayout(new BorderLayout());
|
||||
|
||||
// 2. 创建自定义工具栏
|
||||
JPanel toolbar = new JPanel(new FlowLayout(FlowLayout.LEFT));
|
||||
toolbar.add(new JButton("刷新"));
|
||||
toolbar.add(new JButton("主页"));
|
||||
|
||||
// 3. 将自定义组件和浏览器组件添加到面板
|
||||
contentPane.add(toolbar, BorderLayout.NORTH);
|
||||
contentPane.add(browserComponent, BorderLayout.CENTER);
|
||||
|
||||
// 4. 返回 true,表示我们已经手动完成了布局
|
||||
return true;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 设计优势
|
||||
|
||||
* **非侵入式修改**: 无需修改框架核心代码即可改变浏览器窗口的呈现方式。
|
||||
* **灵活性**: 基于接口的设计允许每个工具(Tool)根据自身业务需求定义完全不同的布局方案。
|
||||
* **控制反转 (IoC)**: 框架负责处理繁琐的浏览器引擎初始化,而将 UI 的最终决定权交还给开发者。
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
105
api-documentation/events/EventBus.md
Normal file
105
api-documentation/events/EventBus.md
Normal file
@@ -0,0 +1,105 @@
|
||||
# EventBus 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.events.EventBus`
|
||||
**主要功能:** 发布-订阅(Publish-Subscribe)模式的事件中心
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`EventBus` 是 **Axis Innovators Box** 框架的核心通信组件。它通过事件驱动的方式实现模块间的解耦,允许应用程序的不同部分在无需相互引用的情况下进行信息交换。
|
||||
|
||||
该实现采用了 **基于注解(Annotation-based)** 的方法,通过反射动态扫描和分发事件。它不仅支持简单的事件发布,还具备完整的对象生命周期管理(注册/注销)以及跨类层级的事件监听能力。
|
||||
|
||||
## 2. 核心架构设计
|
||||
|
||||
### 2.1 存储机制
|
||||
`EventBus` 内部维护了两个关键的映射表,以保证高效的事件分发和快速的资源清理:
|
||||
1. **`eventSubscribers` (Map<Class<?>, List<Subscriber>>)**:
|
||||
* **用途**: 用于事件发布。
|
||||
* **逻辑**: 以事件的 `Class` 类型为键,快速找到所有订阅了该事件的监听者列表。
|
||||
2. **`targetSubscribers` (Map<Object, List<Subscriber>>)**:
|
||||
* **用途**: 用于对象注销。
|
||||
* **逻辑**: 以订阅对象实例为键,记录该对象注册的所有监听器,确保在对象销毁时能一次性清理所有相关引用,防止内存泄漏。
|
||||
|
||||
### 2.2 订阅者封装 (`Subscriber`)
|
||||
内部类 `Subscriber` 封装了执行事件回调所需的三个要素:
|
||||
* **Target**: 接收事件的对象实例。
|
||||
* **Method**: 标记了 `@SubscribeEvent` 的方法对象。
|
||||
* **EventType**: 该监听器关注的事件类型(方法参数类型)。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键功能详解
|
||||
|
||||
### 3.1 动态注册 (`register`)
|
||||
当一个对象调用 `register` 时,`EventBus` 会:
|
||||
1. **递归扫描**: 遍历目标对象的所有方法,包括从父类继承的方法(通过 `getSuperclass()` 向上递归)。
|
||||
2. **注解验证**: 筛选出带有 `@SubscribeEvent` 注解的方法。
|
||||
3. **参数检查**: 确保方法有且仅有一个参数(该参数即为监听的事件类型)。
|
||||
4. **建立索引**: 将符合条件的方法封装成 `Subscriber` 并存入上述两个映射表中。
|
||||
|
||||
### 3.2 事件发布 (`post`)
|
||||
当发布一个事件对象时:
|
||||
1. **类型匹配**: 获取事件的 `Class` 类型。
|
||||
2. **副本保护**: 在遍历订阅者列表前创建 `copySubs` 副本。这是为了防止在执行事件回调过程中,某个监听者尝试注销自己而引发 `ConcurrentModificationException`。
|
||||
3. **反射调用**: 通过 `setAccessible(true)` 强制调用(即使是私有方法),并将事件对象传入。
|
||||
|
||||
### 3.3 生命周期管理
|
||||
* **注销 (`unregister`)**: 彻底切断事件总线对目标对象的引用,是资源清理的关键步骤。
|
||||
* **熔断 (`shutdown`)**: 一旦总线关闭,所有的 `post` 操作将失效,确保在应用关闭阶段不再产生新的业务逻辑执行。
|
||||
|
||||
---
|
||||
|
||||
## 4. API 接口说明
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `void register(Object target)` | 扫描目标对象并注册所有合法的事件监听器。 |
|
||||
| `void unregister(Object target)` | 移除该对象在总线上的所有订阅关系。 |
|
||||
| `boolean post(Object event)` | 向所有订阅了该事件类型的监听者发送事件。 |
|
||||
| `void shutdown()` | 关闭事件总线。 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 技术亮点
|
||||
|
||||
* **继承支持**: 通过 `while (clazz != null)` 循环,支持在父类中定义通用的事件处理逻辑,子类只需注册即可继承该能力。
|
||||
* **解耦性**: 发布者完全不知道谁在处理事件,监听者也无需知道事件由谁产生,只需关注事件本身。
|
||||
* **健壮性**: 内置 `handleException` 机制,确保某个监听器执行失败时不会影响总线上其他监听器的正常运行。
|
||||
|
||||
---
|
||||
|
||||
## 6. 使用代码示例
|
||||
|
||||
### 定义事件
|
||||
```java
|
||||
public class UserLoginEvent {
|
||||
public final String username;
|
||||
public UserLoginEvent(String name) { this.username = name; }
|
||||
}
|
||||
```
|
||||
|
||||
### 注册监听器
|
||||
```java
|
||||
public class LogModule {
|
||||
@SubscribeEvent
|
||||
public void onUserLogin(UserLoginEvent event) {
|
||||
System.out.println("用户登录: " + event.username);
|
||||
}
|
||||
}
|
||||
|
||||
// 在初始化处
|
||||
EventBus bus = new EventBus();
|
||||
LogModule logModule = new LogModule();
|
||||
bus.register(logModule);
|
||||
```
|
||||
|
||||
### 发布事件
|
||||
```java
|
||||
bus.post(new UserLoginEvent("Admin"));
|
||||
```
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
101
api-documentation/events/GlobalEventBus.md
Normal file
101
api-documentation/events/GlobalEventBus.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# GlobalEventBus 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.events.GlobalEventBus`
|
||||
**主要功能:** 全局单例事件总线访问点
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`GlobalEventBus` 是 **Axis Innovators Box** 框架中 `EventBus` 机制的静态包装类。它的核心作用是提供一个**全局唯一的、线程安全**的事件分发中心。
|
||||
|
||||
在一个复杂的解耦系统中,虽然可以实例化多个 `EventBus`(用于局部通信),但通常需要一个贯穿整个应用程序生命周期的主总线。`GlobalEventBus` 通过预定义的静态实例,消除了在不同模块、控制器或类之间传递 `EventBus` 引用的复杂性,实现了真正的“随处注册,随处发布”。
|
||||
|
||||
## 2. 核心架构设计
|
||||
|
||||
### 2.1 单例模式 (Singleton)
|
||||
该类采用了最简洁的静态常量初始化方式:
|
||||
```java
|
||||
public static final EventBus EVENT_BUS = new EventBus();
|
||||
```
|
||||
* **线程安全性**: 依赖于 Java 类加载机制,保证了 `EVENT_BUS` 实例在全局范围内的唯一性且只会被初始化一次。
|
||||
* **可见性**: `public` 修饰符允许框架内任何位置的代码直接访问,无需通过复杂的依赖注入(DI)容器。
|
||||
|
||||
### 2.2 职责定位
|
||||
`GlobalEventBus` 本身不包含复杂的逻辑,它更像是一个“门户”:
|
||||
1. **持有者**: 持有 `EventBus` 的核心实例。
|
||||
2. **默认中心**: 作为系统默认的、最高层级的通信渠道。
|
||||
|
||||
---
|
||||
|
||||
## 3. 关键特性
|
||||
|
||||
* **零配置访问**: 开发者无需关心 `EventBus` 的初始化时机,直接通过 `GlobalEventBus.EVENT_BUS` 即可获取能力。
|
||||
* **一致性**: 确保了核心系统事件(如应用启动、配置变更、全局错误处理)都在同一个频道内流动。
|
||||
* **低耦合**: 模块 A 只需要知道 `GlobalEventBus` 和事件类,而无需知道模块 B 的存在,即可实现与模块 B 的交互。
|
||||
|
||||
---
|
||||
|
||||
## 4. API 使用指南
|
||||
|
||||
由于 `GlobalEventBus` 是对 `EventBus` 的静态引用,其主要操作均通过 `EVENT_BUS` 成员完成。
|
||||
|
||||
| 操作类型 | 代码示例 | 说明 |
|
||||
| :--- | :--- | :--- |
|
||||
| **注册监听** | `GlobalEventBus.EVENT_BUS.register(this);` | 在组件初始化(如 `onInit`)时调用。 |
|
||||
| **发送事件** | `GlobalEventBus.EVENT_BUS.post(new ConfigChangedEvent());` | 在任何业务逻辑发生点触发。 |
|
||||
| **取消注册** | `GlobalEventBus.EVENT_BUS.unregister(this);` | 在组件销毁(如 `onDestroy`)时调用,防止内存泄漏。 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 最佳实践
|
||||
|
||||
### 5.1 何时使用 GlobalEventBus?
|
||||
* **全局生命周期事件**: 如用户登录/登出、系统配置刷新。
|
||||
* **跨层级通信**: 例如底层的网络模块需要通知最顶层的 UI 界面显示通知。
|
||||
* **临时任务**: 不需要长期维护引用关系的瞬时事件处理。
|
||||
|
||||
### 5.2 注意事项
|
||||
1. **内存管理**: 凡是使用 `GlobalEventBus.EVENT_BUS.register()` 的对象,必须在生命周期结束时手动调用 `unregister()`,否则该对象将因为被全局静态引用持有而无法被 JVM 回收。
|
||||
2. **性能考量**: 虽然单次分发效率很高,但若全局总线上挂载了数千个监听器,频繁发布高频事件(如鼠标移动)可能会产生性能抖动。
|
||||
|
||||
---
|
||||
|
||||
## 6. 使用代码示例
|
||||
|
||||
### 场景:全局错误处理系统
|
||||
```java
|
||||
// 1. 定义全局异常事件
|
||||
public class GlobalErrorEvent {
|
||||
public final String message;
|
||||
public GlobalErrorEvent(String msg) { this.message = msg; }
|
||||
}
|
||||
|
||||
// 2. 在 UI 层注册监听
|
||||
public class MainNotificationUI {
|
||||
public MainNotificationUI() {
|
||||
GlobalEventBus.EVENT_BUS.register(this);
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public void onErrorMessage(GlobalErrorEvent event) {
|
||||
showToast("系统错误: " + event.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 在底层业务逻辑处发布
|
||||
public class DatabaseWorker {
|
||||
public void doWork() {
|
||||
try {
|
||||
// 某些业务操作...
|
||||
} catch (Exception e) {
|
||||
// 发现错误,直接向全局总线抛出,无需持有 UI 的引用
|
||||
GlobalEventBus.EVENT_BUS.post(new GlobalErrorEvent(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
102
api-documentation/events/SubscribeEvent.md
Normal file
102
api-documentation/events/SubscribeEvent.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# SubscribeEvent 注解技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.events.SubscribeEvent`
|
||||
**主要功能:** 标记事件订阅方法并定义执行优先级
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`@SubscribeEvent` 是 **Axis Innovators Box** 事件系统的核心元数据注解。它被用于标记类中的方法,使其能够被 `EventBus` 识别并注册为事件监听器(Listener)。
|
||||
|
||||
该注解的存在实现了“声明式”的事件监听。开发者无需实现复杂的接口,只需在任意方法上添加此注解,并确保方法签名符合规范(单参数),即可自动参与系统的事件流转。
|
||||
|
||||
## 2. 注解配置说明
|
||||
|
||||
### 2.1 运行时保留 (`@Retention`)
|
||||
设置为 `RetentionPolicy.RUNTIME`。这意味着注解信息在类加载后依然保留在 JVM 中,允许 `EventBus` 在运行时通过**反射(Reflection)**动态扫描到这些方法。
|
||||
|
||||
### 2.2 目标范围 (`@Target`)
|
||||
设置为 `ElementType.METHOD`。明确限定该注解仅能用于方法上。
|
||||
|
||||
---
|
||||
|
||||
## 3. 核心属性详解
|
||||
|
||||
### 3.1 `priority()`
|
||||
* **类型**: `int`
|
||||
* **默认值**: `0`
|
||||
* **功能**: 定义事件处理的先后顺序。
|
||||
* **逻辑规则**:
|
||||
* 当一个事件被发布时,`EventBus` 会根据所有订阅该事件的方法的 `priority` 值进行排序。
|
||||
* **值越大,优先级越高**,该方法将越早接收到事件通知。
|
||||
* 当优先级相同时,执行顺序取决于系统反射获取方法的自然顺序(通常不保证固定顺序)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 订阅方法规范
|
||||
|
||||
为了使 `@SubscribeEvent` 生效,标记的方法必须遵循以下约束:
|
||||
|
||||
1. **参数限制**: 方法必须有且仅有一个参数。该参数的类型即为该监听器关注的事件类型。
|
||||
2. **访问权限**: 虽然 `EventBus` 内部通过 `setAccessible(true)` 支持私有方法,但建议根据模块化设计合理设置方法的可见性。
|
||||
3. **返回类型**: 通常为 `void`。因为事件发布是单向的分发过程,`EventBus` 不会处理监听方法的返回值。
|
||||
|
||||
---
|
||||
|
||||
## 5. 使用示例
|
||||
|
||||
### 5.1 基础用法
|
||||
```java
|
||||
@SubscribeEvent
|
||||
public void onGenericEvent(MessageEvent event) {
|
||||
System.out.println("收到消息: " + event.getContent());
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 优先级应用场景
|
||||
假设有一个日志系统,需要在业务逻辑处理前记录日志,在业务逻辑处理后进行清理:
|
||||
|
||||
```java
|
||||
public class SecurityMonitor {
|
||||
// 高优先级:先检查权限
|
||||
@SubscribeEvent(priority = 100)
|
||||
public void checkPermission(UserActionEvent event) {
|
||||
if (!event.user.hasAccess()) {
|
||||
event.setCancelled(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class BusinessLogic {
|
||||
// 默认优先级:正常执行业务
|
||||
@SubscribeEvent(priority = 0)
|
||||
public void handleAction(UserActionEvent event) {
|
||||
if (!event.isCancelled()) {
|
||||
executeTask();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 技术亮点
|
||||
|
||||
* **非侵入式设计**: 业务类不需要继承任何父类或实现特定接口,保持了代码的纯净性。
|
||||
* **精细化控制**: 通过 `priority` 属性,解决了传统观察者模式中监听器执行顺序不可控的问题。
|
||||
* **自描述性**: 开发者通过阅读代码中的注解,可以清晰地识别出哪些逻辑是响应式的,增强了代码的可读性。
|
||||
|
||||
---
|
||||
|
||||
## 7. 配合 EventBus 的工作流程
|
||||
|
||||
1. **扫描**: `EventBus.register(obj)` 被调用。
|
||||
2. **识别**: 遍历 `obj` 及其父类,筛选出所有带有 `@SubscribeEvent` 的方法。
|
||||
3. **解析**: 读取 `priority` 数值以及参数类型。
|
||||
4. **排序**: 将这些信息封装为 `Subscriber` 对象并根据优先级存入订阅列表。
|
||||
5. **分发**: `post(event)` 时,按优先级从高到低依次反射调用。
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
109
api-documentation/register/LanguageManager.md
Normal file
109
api-documentation/register/LanguageManager.md
Normal file
@@ -0,0 +1,109 @@
|
||||
这是一个专门为 `com.axis.innovators.box.register.LanguageManager` 类编写的技术介绍文档。
|
||||
|
||||
---
|
||||
|
||||
# LanguageManager 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.register.LanguageManager`
|
||||
**主要功能:** 多语言国际化 (i18n) 管理器、插件语言合并系统
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`LanguageManager` 是 **Axis Innovators Box** 框架的核心组件之一,负责管理应用程序的所有文本翻译和语言环境。它不仅支持内置的系统语言(中文、英文、日文),还具备高度的可扩展性,允许外部插件动态地将自己的语言包合并到主程序的语言池中。
|
||||
|
||||
该系统采用了 **Properties** 文件作为底层存储,通过 **UTF-8** 编码确保跨平台字符兼容性,并提供了完善的语言持久化(自动保存用户选择)功能。
|
||||
|
||||
## 2. 核心架构
|
||||
|
||||
### 2.1 语言描述类 (`Language` 内部类)
|
||||
`Language` 是该管理器的基本数据单元,包含:
|
||||
* **语言名称 (Language Name):** 用于 UI 显示的易读名称(如 "简体中文")。
|
||||
* **注册名 (Registered Name):** 内部唯一标识符(如 "system:zh_CN")。
|
||||
* **属性池 (Properties):** 存储键值对翻译数据的核心容器。
|
||||
* **智能加载:** 能够从磁盘文件或 JAR 包输入流中加载数据。
|
||||
|
||||
### 2.2 存储与路径
|
||||
* **语言文件夹:** 由 `FolderCreator.getLanguageFolder()` 定义。
|
||||
* **持久化文件:** `saved_language.properties`,用于记录用户上一次关闭程序时使用的语言,实现启动自加载。
|
||||
|
||||
## 3. 核心功能特性
|
||||
|
||||
### 3.1 插件语言扩展机制 (`registerPluginLanguage`)
|
||||
这是该类最强大的功能。插件无需修改主程序代码,即可通过以下流程添加翻译:
|
||||
1. **资源读取:** 插件通过其 `PluginDescriptor` 从 JAR 包内部读取 `.properties` 文件。
|
||||
2. **动态合并:** `LanguageManager` 会根据插件指定的 `targetRegisteredName`(如 "system:zh_CN"),自动将插件的翻译键值对并入主程序的对应语言对象中。
|
||||
|
||||
### 3.2 智能合并逻辑 (`addLanguage`)
|
||||
当新语言资源并入时,系统会执行“差异化合并”:
|
||||
* **新增条目:** 如果键不存在,则添加。
|
||||
* **更新条目:** 如果键已存在,则用新值覆盖(允许插件覆盖系统默认文本)。
|
||||
* **合并报告:** 自动生成详细的日志,记录新增和更新的数量,并列出前 5 个示例键。
|
||||
|
||||
### 3.3 语言切换与持久化
|
||||
* **`loadLanguage(String name)`:** 切换当前语言并立即将选择写入磁盘。
|
||||
* **`loadSavedLanguage()`:** 在程序启动时调用,恢复用户偏好的语言环境。
|
||||
|
||||
## 4. API 说明
|
||||
|
||||
### 4.1 管理器核心方法
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `static void registerPluginLanguage(...)` | **插件专用接口**。从插件资源中加载并合并语言包。 |
|
||||
| `static void addLanguage(Language lang)` | 将一个 Language 对象合并到现有的语言列表中。 |
|
||||
| `static void loadLanguage(String name)` | 设置当前激活的语言。 |
|
||||
| `static Language getLoadedLanguages()` | 获取当前正在使用的 Language 对象。 |
|
||||
| `static List<Language> getLanguages()` | 获取系统当前支持的所有语言列表。 |
|
||||
|
||||
### 4.2 Language 对象常用方法
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `String getText(String key)` | **最常用方法**。根据键获取翻译文本。如果键不存在,则返回键名本身(防止 UI 出现空白)。 |
|
||||
| `void addText(String key, String val)` | 动态添加单条翻译。 |
|
||||
|
||||
## 5. 日志与反馈示例
|
||||
|
||||
当插件合并语言时,控制台会输出如下形式的“合并报告”,极大地方便了插件开发者的调试:
|
||||
|
||||
```text
|
||||
【语言合并报告】
|
||||
▌合并来源:MyPlugin Resource -> system:zh_CN
|
||||
▌变更统计:新增 10 条 / 更新 2 条
|
||||
▌当前总量:156 条
|
||||
▌关键示例:
|
||||
✦ 新增条目:
|
||||
▸ myplugin.button.start
|
||||
▸ myplugin.menu.settings
|
||||
✦ 更新条目:
|
||||
▸ common.ok
|
||||
```
|
||||
|
||||
## 6. 使用示例
|
||||
|
||||
### 6.1 在代码中获取翻译
|
||||
```java
|
||||
// 建议在 UI 组件中使用
|
||||
String title = LanguageManager.getLoadedLanguages().getText("mainWindow.title");
|
||||
btn.setText(title);
|
||||
```
|
||||
|
||||
### 6.2 插件注册语言
|
||||
```java
|
||||
// 在插件初始化时调用
|
||||
LanguageManager.registerPluginLanguage(
|
||||
this.descriptor,
|
||||
"assets/lang/zh_CN.properties",
|
||||
"system:zh_CN"
|
||||
);
|
||||
```
|
||||
|
||||
## 7. 注意事项
|
||||
|
||||
1. **UTF-8 编码:** 所有的 `.properties` 文件必须保存为 UTF-8 编码,否则在处理非 ASCII 字符(如中文、日文)时会出现乱码。
|
||||
2. **启动顺序:** 建议在 `Main` 类中尽早调用 `loadSavedLanguage()`,以确保主窗口初始化时能正确显示本地化文本。
|
||||
3. **Key 冲突:** 插件开发者应为 Key 添加前缀(如 `plugin.id.text`),以避免意外覆盖主程序或其他插件的翻译。
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
63
api-documentation/register/RegistrationError..md
Normal file
63
api-documentation/register/RegistrationError..md
Normal file
@@ -0,0 +1,63 @@
|
||||
这是一个专门为 `com.axis.innovators.box.register.RegistrationError` 类编写的技术介绍文档。
|
||||
|
||||
---
|
||||
|
||||
# RegistrationError 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.register.RegistrationError`
|
||||
**类型:** 自定义错误类 (Custom Error)
|
||||
**继承关系:** `java.lang.Error`
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`RegistrationError` 是 **Axis Innovators Box** 框架中专门用于处理**注册阶段致命失败**的类。与标准的 `Exception` 不同,该类继承自 `java.lang.Error`,这表明它代表的是一种不可恢复的严重问题。
|
||||
|
||||
在工具加载、插件注册或核心组件初始化过程中,如果发生了违反框架逻辑或导致系统无法继续运行的错误,框架会抛出此错误以立即中断当前非法的操作流程。
|
||||
|
||||
## 2. 为什么选择继承自 `Error`?
|
||||
|
||||
在 Java 异常体系中,通常 `Exception` 用于程序可以处理的异常情况,而 `Error` 用于指示合理的应用程序不应该试图捕获的严重问题。
|
||||
|
||||
`RegistrationError` 继承自 `Error` 的设计意图在于:
|
||||
1. **强制中断:** 注册失败通常意味着插件配置错误、依赖缺失或核心工具 ID 冲突。这种情况下,继续运行程序可能会导致不可预知的行为或数据损坏。
|
||||
2. **跳过普通捕获:** 许多代码块会使用 `catch (Exception e)` 来捕获业务异常。继承自 `Error` 可以确保注册阶段的致命问题不会被普通的业务异常处理器“默默吞掉”,从而能被最外层的崩溃处理器(如 `AxisInnovatorsBox` 中的崩溃报告系统)捕获。
|
||||
3. **语义化:** 明确标识这是一个“框架级”的初始化错误,而非普通的“业务级”异常。
|
||||
|
||||
## 3. 构造函数
|
||||
|
||||
```java
|
||||
public RegistrationError(String message) {
|
||||
super(message);
|
||||
}
|
||||
```
|
||||
|
||||
* **`message`**: 详细的错误描述信息。通常包含注册失败的工具名称、原因以及可能的解决建议。
|
||||
|
||||
## 4. 使用场景
|
||||
|
||||
该错误通常由以下组件在验证失败时抛出:
|
||||
* **RegistrationTool**: 当尝试注册一个 ID 已经存在或参数非法的工具分类时。
|
||||
* **RegistrationTopic**: 当加载的主题类无法被实例化或不符合规范时。
|
||||
* **PluginLoader**: 当插件描述文件损坏或插件版本与核心框架严重不兼容时。
|
||||
|
||||
## 5. 代码示例
|
||||
|
||||
以下是该类在注册逻辑中的典型应用:
|
||||
|
||||
```java
|
||||
public void registerTool(String toolId, ToolInstance tool) {
|
||||
if (registry.containsKey(toolId)) {
|
||||
// 如果 ID 冲突,抛出 RegistrationError,这将导致程序或该插件加载流程终止
|
||||
throw new RegistrationError("致命错误:工具 ID [" + toolId + "] 已存在,无法重复注册!");
|
||||
}
|
||||
registry.put(toolId, tool);
|
||||
}
|
||||
```
|
||||
|
||||
## 6. 与崩溃报告系统的集成
|
||||
|
||||
当 `RegistrationError` 被抛出且未被捕获时,它会触发 `AxisInnovatorsBox` 中设置的 `UncaughtExceptionHandler`。这将直接导致崩溃诊断窗口弹出,向用户显示详细的注册失败信息,并允许用户导出包含插件列表和系统状态的诊断包。
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
85
api-documentation/register/RegistrationSettingsItem.md
Normal file
85
api-documentation/register/RegistrationSettingsItem.md
Normal file
@@ -0,0 +1,85 @@
|
||||
这是一个专门为 `com.axis.innovators.box.register.RegistrationSettingsItem` 类编写的技术介绍文档。
|
||||
|
||||
---
|
||||
|
||||
# RegistrationSettingsItem 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.register.RegistrationSettingsItem`
|
||||
**继承关系:** `com.axis.innovators.box.window.WindowsJDialog`
|
||||
**主要功能:** 设置中心控制器、设置项注册引擎、应用配置管理
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`RegistrationSettingsItem` 是 **Axis Innovators Box** 的配置核心。它既是一个用于展示设置界面的对话框组件,也是一个全局的设置项注册仓库。它允许主程序和外部插件动态地将自定义的设置面板(JPanel)插入到统一的“设置中心”内。
|
||||
|
||||
此外,该类还负责处理应用的外观(主题、字体、背景图片)、性能(CUDA 加速)和国际化(语言)的持久化逻辑。
|
||||
|
||||
## 2. 核心职责
|
||||
|
||||
### 2.1 设置项注册 (Registration)
|
||||
提供标准的 API,允许不同模块注册设置页面:
|
||||
* **内置页面:** 自动初始化“常规设置”、“插件管理”、“主题选择”和“关于我们”四个核心面板。
|
||||
* **插件扩展:** 插件可以通过 `addSettings()` 方法将自己的配置界面无缝集成到主设置窗口中。
|
||||
|
||||
### 2.2 环境外观管理
|
||||
* **主题切换:** 与 `RegistrationTopic` 配合,实现 LookAndFeel 的动态切换与搜索。
|
||||
* **字体定制:** 全局修改 UI 字体,包括字体家族和大小。
|
||||
* **背景特效:** 支持设置主窗口背景图片,并提供实时的**高斯模糊/毛玻璃**特效预览与应用。
|
||||
|
||||
### 2.3 配置持久化
|
||||
使用 `StateManager`(关联文件 `app_settings`)将用户的个性化选择保存到磁盘:
|
||||
* `theme.color`: 界面主题主色调。
|
||||
* `ui.font`: 用户选择的字体。
|
||||
* `background.path` & `background.blur`: 背景图片路径及其模糊程度。
|
||||
* `ui.language`: 选定的语言环境。
|
||||
* `ai.cuda.enabled`: AI 推理的 CUDA 加速开关。
|
||||
|
||||
## 3. 关键功能模块解析
|
||||
|
||||
### 3.1 动态背景模糊系统
|
||||
该类内置了复杂的图像处理算法用于背景预览:
|
||||
* **`applyFastBlur`**: 快速盒式模糊,适用于小半径预览。
|
||||
* **`applyScaledBlur`**: 缩放模糊算法,通过降低采样率再放大的方式,在大半径模糊下保持极高的性能。
|
||||
* **`applyGaussianBlur`**: 标准高斯模糊,通过一维卷积核两次传递(水平+垂直)实现高质量效果。
|
||||
|
||||
### 3.2 插件管理面板 (`createPluginSettingsPanel`)
|
||||
实时统计并展示系统中已加载的所有插件信息:
|
||||
* 区分 **Java 插件** 和 **Python 插件**。
|
||||
* 支持双击查看详细信息,包括插件图标、物理位置(JAR 路径)、主类名及转换器信息。
|
||||
|
||||
### 3.3 全局应用逻辑 (`applyAllSettings`)
|
||||
这是一个静态方法,通常在 `AxisInnovatorsBox` 启动时调用。它会按顺序读取 `StateManager` 中的所有记录,并一次性应用到当前 JVM 环境中,确保用户每次打开应用时都能获得一致的体验。
|
||||
|
||||
## 4. API 接口说明
|
||||
|
||||
### 4.1 注册设置项
|
||||
|
||||
```java
|
||||
public void addSettings(JPanel panel, String title, Icon icon, String tip, String registeredName)
|
||||
```
|
||||
* `panel`: 要显示的设置 UI 面板。
|
||||
* `title`: 侧边栏或标签页显示的标题。
|
||||
* `registeredName`: 唯一标识符(建议格式 `pluginId:itemName`)。
|
||||
|
||||
### 4.2 设置项管理
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `static void overloading()` | 重新初始化设置列表,常用于切换语言后刷新 UI 文本。 |
|
||||
| `static void applyAllSettings()` | 从磁盘加载并应用所有已保存的配置。 |
|
||||
| `void registration(JTabbedPane tp)` | 将所有已注册的设置项填充到指定的选项卡组件中。 |
|
||||
|
||||
## 5. 交互特性
|
||||
|
||||
1. **主题搜索:** 在主题面板按 `Ctrl + F` 可调出搜索栏,快速定位已安装的主题。
|
||||
2. **实时预览:** 调整模糊滑块或选择颜色时,设置界面内的预览组件会立即更新。
|
||||
3. **热重载:** 切换主题或语言后,会触发 `AxisInnovatorsBox.getMain().reloadAllWindow()`,无需重启即可看到全局变化。
|
||||
|
||||
## 6. 设计细节
|
||||
|
||||
* **隔离性:** 使用 `UUID` 内部管理设置项,确保即使注册名相似也不会发生引用冲突。
|
||||
* **健壮性:** 在加载背景图片或字体时,会自动检查物理文件是否存在及字体在当前系统中是否可用,防止因配置损坏导致启动崩溃。
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
110
api-documentation/register/RegistrationTool.md
Normal file
110
api-documentation/register/RegistrationTool.md
Normal file
@@ -0,0 +1,110 @@
|
||||
# RegistrationTool 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.register.RegistrationTool`
|
||||
**主要功能:** 工具箱组件注册中心、功能分类管理器
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`RegistrationTool` 是 **Axis Innovators Box** 的功能调度核心。它负责管理主程序中所有的工具项(`ToolItem`)及其所属的分类(`ToolCategory`)。通过该类,开发者可以将各种功能模块(如调试工具、编程环境、AI 推理等)注册到主界面的工具栏中。
|
||||
|
||||
该类不仅内置了丰富的系统工具,还为外部插件提供了一套标准的扩展接口,支持基于命名空间的工具注册逻辑。
|
||||
|
||||
## 2. 核心职责
|
||||
|
||||
* **功能分类管理:** 将相似功能的工具组织在 `ToolCategory` 下(如“调试工具”、“AI工具”)。
|
||||
* **工具项路由:** 定义点击工具图标后的具体行为(通过 `AbstractAction` 实现窗口弹出或逻辑执行)。
|
||||
* **插件扩展支持:** 提供 API 允许插件在加载时动态注入自定义工具。
|
||||
* **唯一性校验:** 确保每个注册项拥有唯一的 `UUID` 和注册名称(Registered Name),防止冲突。
|
||||
* **平台适配:** 在初始化时根据 `BuildInformation` 检查操作系统兼容性(主要针对 Windows)。
|
||||
|
||||
## 3. 内置工具分类
|
||||
|
||||
在 Windows 系统环境下,`RegistrationTool` 默认初始化以下分类及工具:
|
||||
|
||||
### 3.1 调试工具 (Debug Tools)
|
||||
* **Frida 注入工具:** 使用 Frida 框架对目标进程进行脚本注入。
|
||||
|
||||
### 3.2 编程工具 (Programming Tools)
|
||||
* **JarApi 查看器:** 反射查看 JAR 包内的方法定义及其注解。
|
||||
* **C 语言编辑器:** 智能化的 C 语言编译与编辑环境。
|
||||
* **多语言在线执行:** 支持多种编程语言的实时在线运行,具备无限循环检测机制。
|
||||
* **数据库管理:** 跨平台的数据库管理客户端。
|
||||
* **Linux 终端:** 启动一个真实的 Linux 终端模拟环境。
|
||||
* **MySQL 控制台:** 模拟 MySQL 命令行交互界面。
|
||||
|
||||
### 3.3 AI 工具 (AI Tools)
|
||||
* **本地 AI 执行工具:** 在本地运行开源大语言模型(LLM)。支持自动加载 **CUDA** 硬件加速库(通过 `LM` 类)。
|
||||
|
||||
### 3.4 系统工具 (System Tools)
|
||||
* **任务栏主题设置:** 深度定制 Windows 任务栏的外观、颜色和透明度。
|
||||
|
||||
---
|
||||
|
||||
## 4. 插件扩展机制
|
||||
|
||||
`RegistrationTool` 支持插件化扩展,插件可以通过以下方法将自己的工具分类注册到主程序中:
|
||||
|
||||
```java
|
||||
public void addToolCategory(ToolCategory toolCategory,
|
||||
PluginDescriptor pluginDescriptor,
|
||||
String registeredName)
|
||||
```
|
||||
|
||||
**命名空间规则:**
|
||||
为了防止不同插件之间的工具冲突,系统会自动使用 `插件注册名:工具注册名` 的格式生成全局唯一的标识符。
|
||||
|
||||
---
|
||||
|
||||
## 5. API 接口说明
|
||||
|
||||
### 5.1 注册方法
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `addToolCategory(ToolCategory, String)` | 注册一个新的工具分类(系统级使用)。 |
|
||||
| `addToolCategory(ToolCategory, PluginDescriptor, String)` | **插件专用接口**。通过插件描述符注册分类。 |
|
||||
|
||||
### 5.2 查询方法
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `ToolCategory getToolCategory(UUID id)` | 根据 UUID 获取分类对象。 |
|
||||
| `UUID getUUID(String registeredName)` | 根据注册名称(如 "system:debugTools")获取其 UUID。 |
|
||||
| `List<ToolCategory> getToolCategories()` | 获取当前所有已注册的分类列表。 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 关键技术约束:注册时机
|
||||
|
||||
**生命周期限制:**
|
||||
`RegistrationTool` 的注册操作具有严格的时机限制:**必须在应用主窗口启动(`run` 方法执行完毕)之前完成。**
|
||||
|
||||
* 如果在主窗口已处于显示状态(`main.isWindow()` 为 `true`)时尝试添加工具,系统会通过日志记录 `logger.warn("Wrong time to add tools")` 并拒绝注册。
|
||||
* 这种设计确保了 UI 在渲染初期能获得完整的工具树,避免动态修改 UI 导致的线程竞争或布局闪烁。
|
||||
|
||||
---
|
||||
|
||||
## 7. 错误处理
|
||||
|
||||
如果在注册过程中检测到**重复的注册名称**,该类会抛出 `RegistrationError`。这是一种致命错误,旨在提醒开发者工具标识符冲突,必须在开发阶段解决。
|
||||
|
||||
## 8. 示例代码:添加简单工具
|
||||
|
||||
```java
|
||||
ToolCategory myCategory = new ToolCategory("我的工具", "icon.png", "说明");
|
||||
myCategory.addTool(new ToolItem("弹窗演示", "btn.png", "点击会弹出消息", 100, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
JOptionPane.showMessageDialog(null, "你好,这是注册工具!");
|
||||
}
|
||||
}));
|
||||
|
||||
// 注册到系统
|
||||
registrationTool.addToolCategory(myCategory, "my_custom_tools");
|
||||
```
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
98
api-documentation/register/RegistrationTopic.md
Normal file
98
api-documentation/register/RegistrationTopic.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# RegistrationTopic 类技术文档
|
||||
|
||||
**包路径:** `com.axis.innovators.box.register.RegistrationTopic`
|
||||
**主要功能:** 主题/外观 (LookAndFeel) 注册中心、视觉模式管理
|
||||
**作者:** tzdwindows 7
|
||||
|
||||
---
|
||||
|
||||
## 1. 概述
|
||||
|
||||
`RegistrationTopic` 是 **Axis Innovators Box** 框架中负责视觉外观管理的核心组件。它充当了一个“主题仓库”,统一管理应用程序支持的所有 `LookAndFeel`(外观)。通过该类,开发者可以注册系统原生主题、第三方现代主题(如 FlatLaf、Material UI)以及自定义的视觉样式。
|
||||
|
||||
该类不仅存储主题的实现,还记录了主题的元数据(名称、图标、描述)以及色彩属性(明/暗模式),为设置中心的 UI 渲染和全局主题切换提供了数据支持。
|
||||
|
||||
## 2. 核心职责
|
||||
|
||||
* **多模式注册:** 支持通过“类全限定名(String)”或“主题实例(LookAndFeel)”两种方式注册主题。
|
||||
* **状态追踪:** 记录当前正在加载的主题,并提供实时状态查询。
|
||||
* **明/暗模式识别:** 维护每个主题的色彩属性,允许框架根据当前主题自动调整其他组件(如代码编辑器、图标)的色调。
|
||||
* **生命周期保护:** 严格限制注册时机,确保 UI 稳定性。
|
||||
|
||||
## 3. 核心机制详解
|
||||
|
||||
### 3.1 双路注册支持
|
||||
为了兼容不同的 UI 库,`RegistrationTopic` 提供了重载的 `addTopic` 方法:
|
||||
1. **基于类名注册:** 适用于 JVM 默认提供的或在类路径下的标准主题(如 `UIManager.getSystemLookAndFeelClassName()`)。
|
||||
2. **基于实例注册:** 适用于需要初始化参数的现代主题(如 `new FlatMacDarkLaf()`),这允许主题在注册前进行预配置。
|
||||
|
||||
### 3.2 同步列表结构
|
||||
该类内部使用多个对齐的 `ArrayList` 来维护数据。每个主题在所有列表中的索引(Index)是统一的。例如,索引为 `5` 的项,其类名、图标、描述和暗黑模式标志位都存储在各自列表的第 `5` 位。
|
||||
|
||||
### 3.3 明/暗模式感知 (`isDarkMode`)
|
||||
该方法通过查找当前活跃主题(`loadTopics`)在注册列表中的位置,返回其对应的 `isDarkMode` 布尔值。这对于实现“跟随主题自动切换图标颜色”等高级 UI 特性至关重要。
|
||||
|
||||
---
|
||||
|
||||
## 4. API 接口说明
|
||||
|
||||
### 4.1 注册方法
|
||||
|
||||
| 方法签名 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `addTopic(String class, String name, String tip, Icon icon, String regName, boolean isDark)` | 通过主题类名注册。 |
|
||||
| `addTopic(LookAndFeel laf, String name, String tip, Icon icon, String regName, boolean isDark)` | 通过 LookAndFeel 实例注册。 |
|
||||
|
||||
### 4.2 状态管理与查询
|
||||
|
||||
| 方法 | 描述 |
|
||||
| :--- | :--- |
|
||||
| `void setLoading(String regName)` | 设置当前激活的主题 ID(注册名)。 |
|
||||
| `boolean isLoading(String regName)` | 检查指定的主题是否是当前正在使用的主题。 |
|
||||
| `boolean isDarkMode()` | 获取当前激活的主题是否属于暗黑模式。 |
|
||||
| `boolean isEmpty()` | 检查仓库中是否尚未注册任何主题。 |
|
||||
|
||||
---
|
||||
|
||||
## 5. 关键约束:注册时机
|
||||
|
||||
与 `RegistrationTool` 类似,`RegistrationTopic` 具有严格的**时机约束**:
|
||||
|
||||
* **限制:** 所有的主题注册必须在 `AxisInnovatorsBox.isWindow()` 为 `false` 时进行(即主窗口启动前)。
|
||||
* **违规处理:** 如果在窗口显示后尝试调用 `addTopic`,系统会记录警告日志:`logger.warn("Wrong time to add topics")`,且该注册请求会被忽略。
|
||||
* **设计目的:** 保证 Swing 的 `UIManager` 状态在初始化阶段是确定的,防止运行时切换导致的界面渲染异常或部分组件更新失败。
|
||||
|
||||
## 6. 错误处理
|
||||
|
||||
如果在注册过程中出现**重复的注册名称**(`registeredName`),该类会抛出 `RegistrationError`。这确保了每个主题在设置界面和配置文件中都有唯一的引用 ID。
|
||||
|
||||
## 7. 使用示例
|
||||
|
||||
在 `AxisInnovatorsBox` 初始化阶段注册主题:
|
||||
|
||||
```java
|
||||
RegistrationTopic topicRegistry = main.getRegistrationTopic();
|
||||
|
||||
// 1. 注册系统默认主题
|
||||
topicRegistry.addTopic(
|
||||
UIManager.getSystemLookAndFeelClassName(),
|
||||
"系统默认",
|
||||
"使用操作系统的原生外观",
|
||||
null,
|
||||
"system:native",
|
||||
false
|
||||
);
|
||||
|
||||
// 2. 注册 FlatLaf 现代暗黑主题
|
||||
topicRegistry.addTopic(
|
||||
new com.formdev.flatlaf.themes.FlatMacDarkLaf(),
|
||||
"MacOS Dark",
|
||||
"现代化的深色苹果风格界面",
|
||||
new ImageIcon("mac_dark_icon.png"),
|
||||
"system:flatMacDark",
|
||||
true
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
*文档生成时间: 2026-01-02*
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import com.axis.innovators.box.browser.bridge.JsBridgeController;
|
||||
import com.axis.innovators.box.events.BrowserCreationCallback;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public abstract class BaseBrowserBuilder<T extends BaseBrowserBuilder<T>> {
|
||||
protected String windowId;
|
||||
protected String title = "JCEF Window";
|
||||
protected Dimension size = new Dimension(800, 600);
|
||||
protected WindowOperationHandler operationHandler;
|
||||
protected String htmlPath;
|
||||
protected String htmlUrl = "";
|
||||
protected Image icon;
|
||||
protected boolean resizable = true;
|
||||
protected boolean maximizable = true;
|
||||
protected boolean minimizable = true;
|
||||
protected boolean openLinksInExternalBrowser = true;
|
||||
protected BrowserCreationCallback browserCreationCallback;
|
||||
protected JsBridgeController controller;
|
||||
public BaseBrowserBuilder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
public T title(String title) { this.title = title; return (T) this; }
|
||||
public T size(int width, int height) { this.size = new Dimension(width, height); return (T) this; }
|
||||
|
||||
public T operationHandler(WindowOperationHandler handler) { this.operationHandler = handler; return (T) this; }
|
||||
public T htmlPath(String path) { this.htmlPath = path; return (T) this; }
|
||||
public T htmlUrl(String url) { this.htmlUrl = url; return (T) this; }
|
||||
public T icon(Image icon) { this.icon = icon; return (T) this; }
|
||||
public T resizable(boolean resizable) { this.resizable = resizable; return (T) this; }
|
||||
public T controller(JsBridgeController controller) {this.controller = controller;return (T) this;}
|
||||
public T openLinksInBrowser(boolean openInBrowser) { this.openLinksInExternalBrowser = !openInBrowser; return (T) this; }
|
||||
public T setBrowserCreationCallback(BrowserCreationCallback callback) { this.browserCreationCallback = callback; return (T) this; }
|
||||
|
||||
// 抽象构建方法
|
||||
public abstract Window build();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import com.axis.innovators.box.browser.bridge.JsBridgeController;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
|
||||
/**
|
||||
* 浏览器容器接口
|
||||
* 统一 JFrame 和 JDialog 的对外 API
|
||||
*/
|
||||
public interface BrowserContainer {
|
||||
/**
|
||||
* 获取窗口唯一ID
|
||||
*/
|
||||
String getWindowId();
|
||||
|
||||
/**
|
||||
* 获取 CEF 消息路由 (兼容旧 API)
|
||||
*/
|
||||
CefMessageRouter getMsgRouter();
|
||||
|
||||
/**
|
||||
* 获取 CEF 浏览器实例
|
||||
*/
|
||||
CefBrowser getBrowser();
|
||||
|
||||
/**
|
||||
* 在当前浏览器上下文中执行 JavaScript 代码
|
||||
* 支持从任意线程调用,内部会自动处理线程调度
|
||||
*
|
||||
* @param script 要执行的 JavaScript 代码字符串
|
||||
*/
|
||||
void executingJsCode(String script);
|
||||
|
||||
/**
|
||||
* 关闭窗口
|
||||
*/
|
||||
void closeWindow();
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
void updateTheme();
|
||||
|
||||
/**
|
||||
* 设置窗口可见性
|
||||
* (JFrame 和 JDialog 原生支持此方法,接口中声明即可)
|
||||
* @param b true 显示,false 隐藏
|
||||
*/
|
||||
void setVisible(boolean b);
|
||||
|
||||
/**
|
||||
* 用于动态设置Java与JavaScript之间的通信桥梁
|
||||
* @param controller JsBridgeController 实例
|
||||
*/
|
||||
void setController(JsBridgeController controller);
|
||||
}
|
||||
621
src/main/java/com/axis/innovators/box/browser/BrowserCore.java
Normal file
621
src/main/java/com/axis/innovators/box/browser/BrowserCore.java
Normal file
@@ -0,0 +1,621 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.browser.bridge.JsBridgeController;
|
||||
import com.axis.innovators.box.events.BrowserCreationCallback;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
import org.cef.callback.*;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.misc.BoolRef;
|
||||
import org.cef.network.CefRequest;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.Vector;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* 浏览器核心逻辑代理类
|
||||
* 负责封装 CefClient, CefBrowser 以及所有 Handler 的通用逻辑
|
||||
*/
|
||||
public class BrowserCore {
|
||||
private final Window parentWindow; // 宿主窗口(JFrame 或 JDialog)
|
||||
private final String windowId;
|
||||
private CefApp cefApp;
|
||||
private CefClient client;
|
||||
private CefBrowser browser;
|
||||
private CefMessageRouter msgRouter;
|
||||
private final BaseBrowserBuilder<?> config;
|
||||
private final java.util.Queue<String> pendingScripts = new java.util.concurrent.ConcurrentLinkedQueue<>();
|
||||
private volatile boolean isPageLoaded = false;
|
||||
private JsBridgeController jsController;
|
||||
|
||||
public BrowserCore(Window parentWindow, String windowId, BaseBrowserBuilder<?> config) {
|
||||
this.parentWindow = parentWindow;
|
||||
this.windowId = windowId;
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public Component initialize() throws MalformedURLException {
|
||||
this.cefApp = CefAppManager.getInstance();
|
||||
this.client = cefApp.createClient();
|
||||
|
||||
// 1. 设置通用 Handler
|
||||
setupDisplayHandler();
|
||||
setupKeyboardHandler();
|
||||
setupRequestHandler();
|
||||
setupContextMenuHandler();
|
||||
setupDialogHandler();
|
||||
setupDownloadHandler();
|
||||
setupLifeSpanHandler();
|
||||
setupFileDialogHandler();
|
||||
setupScriptQueueHandler();
|
||||
|
||||
// 2. 消息路由
|
||||
//if (config.operationHandler != null || this.jsController != null) {
|
||||
setupMessageHandlers(config.operationHandler);
|
||||
//}
|
||||
|
||||
// 3. 创建浏览器
|
||||
String targetUrl;
|
||||
if (config.htmlUrl != null && !config.htmlUrl.isEmpty()) {
|
||||
targetUrl = config.htmlUrl;
|
||||
System.out.println("Loading URL: " + targetUrl);
|
||||
} else if (config.htmlPath != null && !config.htmlPath.isEmpty()) {
|
||||
targetUrl = new File(config.htmlPath).toURI().toURL().toString();
|
||||
System.out.println("Loading File: " + targetUrl);
|
||||
} else {
|
||||
throw new IllegalArgumentException("URL or HTML path must be provided");
|
||||
}
|
||||
|
||||
this.browser = client.createBrowser(targetUrl, false, false);
|
||||
Component browserUI = browser.getUIComponent();
|
||||
|
||||
// 4. 加载结束后的主题注入
|
||||
updateTheme();
|
||||
|
||||
return browserUI;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置或更新 JSBridge 控制器
|
||||
* 支持运行时动态切换
|
||||
*/
|
||||
public void setJsBridgeController(JsBridgeController controller) {
|
||||
this.jsController = controller;
|
||||
if (controller != null) {
|
||||
controller.attach(this);
|
||||
if (browser != null && isPageLoaded) {
|
||||
System.out.println("🔄 [BrowserCore] 动态更新 JSBridge 控制器,正在重新注入 JS...");
|
||||
String bridgeScript = controller.generateInjectionJs();
|
||||
browser.executeJavaScript(bridgeScript, browser.getURL(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 核心逻辑:设置加载监听器,处理脚本队列
|
||||
*/
|
||||
private void setupScriptQueueHandler() {
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadingStateChange(CefBrowser browser, boolean isLoading, boolean canGoBack, boolean canGoForward) {
|
||||
// 可选:在这里也可以监控加载状态
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadStart(CefBrowser browser, CefFrame frame, CefRequest.TransitionType transitionType) {
|
||||
if (frame.isMain()) {
|
||||
// 开始加载新页面时,重置状态
|
||||
isPageLoaded = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
// 只有主框架加载完毕,且没有发生严重错误,才执行脚本
|
||||
// 只有主框架加载完毕才注入
|
||||
if (frame.isMain()) {
|
||||
System.out.println("✅ [BrowserCore] 页面加载完成 (Code: " + httpStatusCode + ")");
|
||||
isPageLoaded = true;
|
||||
if (jsController != null) {
|
||||
System.out.println("💉 [BrowserCore] 正在注入 JSBridge 代码...");
|
||||
String bridgeScript = jsController.generateInjectionJs();
|
||||
frame.executeJavaScript(bridgeScript, frame.getURL(), 0);
|
||||
} else {
|
||||
System.err.println("⚠️ [BrowserCore] jsController 为空,未注入 JS 对象");
|
||||
}
|
||||
while (!pendingScripts.isEmpty()) {
|
||||
String script = pendingScripts.poll();
|
||||
if (script != null) {
|
||||
try {
|
||||
frame.executeJavaScript(script, frame.getURL(), 0);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadError(CefBrowser browser, CefFrame frame, ErrorCode errorCode, String errorText, String failedUrl) {
|
||||
if (frame.isMain()) {
|
||||
System.err.println("❌ [BrowserCore] 页面加载失败: " + errorText);
|
||||
// 加载失败通常不应该执行脚本,或者你可以选择清空队列
|
||||
// pendingScripts.clear();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// --- Handlers ---
|
||||
|
||||
private void setupDisplayHandler() {
|
||||
client.addDisplayHandler(new CefDisplayHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onConsoleMessage(CefBrowser browser, CefSettings.LogSeverity level, String message, String source, int line) {
|
||||
String symbol = switch (level) {
|
||||
case LOGSEVERITY_ERROR -> "⛔";
|
||||
case LOGSEVERITY_WARNING -> "⚠️";
|
||||
case LOGSEVERITY_DEFAULT -> "🐞";
|
||||
default -> "ℹ️";
|
||||
};
|
||||
System.out.printf("[Browser Console] %s %s (Line %d) -> %s%n", symbol, source, line, message);
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTitleChange(CefBrowser browser, String title) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (parentWindow instanceof JFrame) {
|
||||
((JFrame) parentWindow).setTitle(title);
|
||||
} else if (parentWindow instanceof JDialog) {
|
||||
((JDialog) parentWindow).setTitle(title);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupKeyboardHandler() {
|
||||
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
|
||||
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
|
||||
if (event.windows_key_code == 123) { // F12
|
||||
browser.getDevTools().createImmediately();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void setupRequestHandler() {
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request, boolean user_gesture, boolean is_redirect) {
|
||||
String url = request.getURL();
|
||||
if (url != null && url.toLowerCase().startsWith("data:")) return false;
|
||||
|
||||
// 处理外部浏览器打开
|
||||
if (user_gesture && config.openLinksInExternalBrowser) {
|
||||
openInExternalBrowser(url);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 严格匹配你提供的源码签名
|
||||
*/
|
||||
@Override
|
||||
public boolean onCertificateError(CefBrowser browser,
|
||||
CefLoadHandler.ErrorCode cert_error,
|
||||
String request_url,
|
||||
CefCallback callback) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
int option = JOptionPane.showConfirmDialog(parentWindow,
|
||||
"证书错误: " + cert_error + "\n是否继续访问?",
|
||||
"安全警告",
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.WARNING_MESSAGE);
|
||||
|
||||
if (option == JOptionPane.YES_OPTION) {
|
||||
// JCEF 122 中 CefCallback.Continue() 无参数,表示"继续/忽略错误"
|
||||
callback.Continue();
|
||||
} else {
|
||||
callback.cancel();
|
||||
}
|
||||
});
|
||||
return true; // 表示我们处理了这个事件
|
||||
}
|
||||
|
||||
/**
|
||||
* 严格匹配你提供的源码签名
|
||||
*/
|
||||
@Override
|
||||
public boolean getAuthCredentials(CefBrowser browser,
|
||||
String origin_url,
|
||||
boolean isProxy,
|
||||
String host,
|
||||
int port,
|
||||
String realm,
|
||||
String scheme,
|
||||
CefAuthCallback callback) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JTextField username = new JTextField();
|
||||
JPasswordField password = new JPasswordField();
|
||||
Object[] message = {
|
||||
"Host: " + host,
|
||||
"Realm: " + realm,
|
||||
"Username:", username,
|
||||
"Password:", password
|
||||
};
|
||||
|
||||
int option = JOptionPane.showConfirmDialog(parentWindow, message, "登录认证", JOptionPane.OK_CANCEL_OPTION);
|
||||
if (option == JOptionPane.OK_OPTION) {
|
||||
callback.Continue(username.getText(), new String(password.getPassword()));
|
||||
} else {
|
||||
callback.cancel();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupFileDialogHandler() {
|
||||
client.addDialogHandler((browser, mode, title, defaultFilePath, acceptFilters, callback) -> {
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JFileChooser fileChooser = new JFileChooser();
|
||||
fileChooser.setDialogTitle(title != null ? title : "选择文件");
|
||||
|
||||
// 设置默认路径
|
||||
if (defaultFilePath != null && !defaultFilePath.isEmpty()) {
|
||||
fileChooser.setCurrentDirectory(new File(defaultFilePath));
|
||||
}
|
||||
|
||||
// 处理模式:是否多选
|
||||
boolean isMulti = (mode == CefDialogHandler.FileDialogMode.FILE_DIALOG_OPEN_MULTIPLE);
|
||||
fileChooser.setMultiSelectionEnabled(isMulti);
|
||||
|
||||
// 打开对话框
|
||||
int result = fileChooser.showOpenDialog(parentWindow);
|
||||
if (result == JFileChooser.APPROVE_OPTION) {
|
||||
Vector<String> filePaths = new Vector<>();
|
||||
if (isMulti) {
|
||||
for (File file : fileChooser.getSelectedFiles()) {
|
||||
filePaths.add(file.getAbsolutePath());
|
||||
}
|
||||
} else {
|
||||
filePaths.add(fileChooser.getSelectedFile().getAbsolutePath());
|
||||
}
|
||||
|
||||
callback.Continue(filePaths);
|
||||
} else {
|
||||
callback.Cancel();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
private void setupContextMenuHandler() {
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame, CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame, CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
if (plainText) text = text.replaceAll("<[^>]+>", "");
|
||||
String escapedText = text.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n").replace("\r", "\\r");
|
||||
browser.executeJavaScript("if (document.activeElement) { document.activeElement.value += '" + escapedText + "'; document.dispatchEvent(new Event('input', { bubbles: true })); }", browser.getURL(), 0);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupDialogHandler() {
|
||||
client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) {
|
||||
JOptionPane.showMessageDialog(parentWindow, message_text, "警告", JOptionPane.INFORMATION_MESSAGE);
|
||||
callback.Continue(true, "");
|
||||
} else if (dialog_type == JSDialogType.JSDIALOGTYPE_CONFIRM) {
|
||||
int result = JOptionPane.showConfirmDialog(parentWindow, message_text, "确认", JOptionPane.YES_NO_OPTION);
|
||||
callback.Continue(result == JOptionPane.YES_OPTION, "");
|
||||
} else if (dialog_type == JSDialogType.JSDIALOGTYPE_PROMPT) {
|
||||
String result = JOptionPane.showInputDialog(parentWindow, message_text, default_prompt_text);
|
||||
if (result != null) callback.Continue(true, result);
|
||||
else callback.Continue(false, "");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupDownloadHandler() {
|
||||
client.addDownloadHandler(new CefDownloadHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeDownload(CefBrowser browser, CefDownloadItem downloadItem, String suggestedName, CefBeforeDownloadCallback callback) {
|
||||
callback.Continue(suggestedName, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadUpdated(CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
CefDownloadItemCallback callback) {
|
||||
if (downloadItem.isComplete()) {
|
||||
System.out.println("下载完成: " + downloadItem.getFullPath());
|
||||
} else if (downloadItem.isCanceled()) {
|
||||
System.out.println("下载取消");
|
||||
} else {
|
||||
// 获取进度百分比
|
||||
int percent = downloadItem.getPercentComplete();
|
||||
if (percent % 10 == 0) { // 减少日志输出频率
|
||||
System.out.println("下载中: " + percent + "%");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupLifeSpanHandler() {
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame, String targetUrl, String targetFrameName) {
|
||||
boolean isDataProtocol = targetUrl != null && targetUrl.toLowerCase().startsWith("data:");
|
||||
|
||||
// 策略1:使用外部浏览器打开
|
||||
if (config.openLinksInExternalBrowser && !isDataProtocol) {
|
||||
openInExternalBrowser(targetUrl);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 策略2:创建新的 Swing 窗口 (WindowRegistry)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
String popupWindowId = windowId + "_popup_" + System.currentTimeMillis();
|
||||
WindowRegistry.getInstance().createNewWindow(popupWindowId, popupBuilder -> {
|
||||
popupBuilder.title("Popup") // 可以优化为获取页面标题
|
||||
.size(parentWindow.getWidth(), parentWindow.getHeight())
|
||||
.htmlUrl(targetUrl)
|
||||
.icon(config.icon)
|
||||
.openLinksInBrowser(true);
|
||||
if (config.operationHandler != null) {
|
||||
popupBuilder.operationHandler(config.operationHandler);
|
||||
}
|
||||
});
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupMessageHandlers(WindowOperationHandler handler) {
|
||||
// 1. 配置 (保持上一轮修复的代码)
|
||||
CefMessageRouter.CefMessageRouterConfig routerConfig = new CefMessageRouter.CefMessageRouterConfig();
|
||||
routerConfig.jsQueryFunction = "javaQuery";
|
||||
routerConfig.jsCancelFunction = "javaQueryCancel";
|
||||
|
||||
// 2. 创建 (保持上一轮修复的代码)
|
||||
msgRouter = CefMessageRouter.create(routerConfig);
|
||||
|
||||
// 3. 添加处理器
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) {
|
||||
|
||||
// A. 处理 JS Bridge 请求 (Controller)
|
||||
if (jsController != null && request.startsWith("jsbridge:")) {
|
||||
return jsController.handleQuery(browser, frame, queryId, request, persistent, callback);
|
||||
}
|
||||
|
||||
// B. 处理 System 请求 (OperationHandler)
|
||||
if (request.startsWith("system:")) {
|
||||
// ★★★ [增加判空] 防止 handler 为空导致空指针异常 ★★★
|
||||
if (handler != null) {
|
||||
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));
|
||||
return true;
|
||||
} else {
|
||||
// 如果没有 handler,但这又是一个 system 请求,则报错或忽略
|
||||
System.err.println("收到 system 请求但 handler 为空: " + request);
|
||||
callback.failure(404, "No handler for system operation");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// C. 处理 Java Response
|
||||
if (request.startsWith("java-response:")) {
|
||||
String[] parts = request.split(":");
|
||||
String requestId = parts[1];
|
||||
String responseData = parts.length > 2 ? parts[2] : "";
|
||||
Consumer<String> cb = WindowRegistry.getInstance().getCallback(requestId);
|
||||
if (cb != null) {
|
||||
cb.accept(responseData);
|
||||
callback.success("");
|
||||
} else {
|
||||
callback.failure(-1, "无效的请求ID");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}, true);
|
||||
|
||||
client.addMessageRouter(msgRouter);
|
||||
}
|
||||
|
||||
private void injectJsBridge() {
|
||||
if (jsController != null && browser != null) {
|
||||
String script = jsController.generateInjectionJs();
|
||||
System.out.println("💉 [Injecting JS Bridge]");
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void openInExternalBrowser(String url) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(url));
|
||||
} catch (Exception e) {
|
||||
System.err.println("外部浏览器打开失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Theme & Font Injection ---
|
||||
|
||||
public void updateTheme() {
|
||||
if (browser == null) return;
|
||||
|
||||
// 字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
|
||||
// 主题信息
|
||||
boolean isDarkTheme = false;
|
||||
if (AxisInnovatorsBox.getMain() != null) {
|
||||
isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
}
|
||||
|
||||
injectResources(fontInfo, isDarkTheme);
|
||||
}
|
||||
|
||||
private void injectResources(String fontInfo, boolean isDarkTheme) {
|
||||
if (client == null) return;
|
||||
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
// 注入字体
|
||||
String fontScript = String.format(
|
||||
"if(typeof window.javaFontInfo === 'undefined'){ window.javaFontInfo=%s; document.dispatchEvent(new CustomEvent('javaFontsLoaded',{detail:window.javaFontInfo})); }",
|
||||
fontInfo
|
||||
);
|
||||
browser.executeJavaScript(fontScript, browser.getURL(), 0);
|
||||
|
||||
if (jsController != null) {
|
||||
String bridgeScript = jsController.generateInjectionJs();
|
||||
frame.executeJavaScript(bridgeScript, frame.getURL(), 0);
|
||||
}
|
||||
// 注入主题
|
||||
String themeInfo = String.format("{\"isDarkTheme\": %s, \"timestamp\": %d}", isDarkTheme, System.currentTimeMillis());
|
||||
String themeScript = String.format(
|
||||
"window.javaThemeInfo=%s; document.dispatchEvent(new CustomEvent('javaThemeChanged',{detail:window.javaThemeInfo}));",
|
||||
themeInfo
|
||||
);
|
||||
browser.executeJavaScript(themeScript, browser.getURL(), 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private String getSystemFontsInfo() {
|
||||
try {
|
||||
JsonObject fontInfo = new JsonObject();
|
||||
JsonObject uiFonts = new JsonObject();
|
||||
String[] fontKeys = { "Label.font", "Button.font", "TextField.font", "Panel.font", "ToolTip.font" }; // 简化列表,可按需添加
|
||||
|
||||
for (String key : fontKeys) {
|
||||
Font font = UIManager.getFont(key);
|
||||
if (font != null) {
|
||||
JsonObject fontObj = new JsonObject();
|
||||
fontObj.addProperty("name", font.getFontName());
|
||||
fontObj.addProperty("size", font.getSize());
|
||||
uiFonts.add(key, fontObj);
|
||||
}
|
||||
}
|
||||
fontInfo.add("uiFonts", uiFonts);
|
||||
return new Gson().toJson(fontInfo);
|
||||
} catch (Exception e) {
|
||||
return "{}";
|
||||
}
|
||||
}
|
||||
|
||||
public CefMessageRouter getMsgRouter() {
|
||||
return msgRouter;
|
||||
}
|
||||
|
||||
public void executingJsCode(String script) {
|
||||
// 无论哪个线程调用,都放入队列检查逻辑
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (browser == null) return;
|
||||
|
||||
if (isPageLoaded) {
|
||||
// 场景A:页面已经加载好了,直接执行
|
||||
try {
|
||||
browser.getMainFrame().executeJavaScript(script, browser.getMainFrame().getURL(), 0);
|
||||
System.out.println("⚡ [直接执行] " + script);
|
||||
} catch (Exception e) {
|
||||
System.err.println("执行JS异常: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 场景B:页面还没好,加入队列,等待 onLoadEnd 自动处理
|
||||
System.out.println("⏳ [加入队列] 页面未就绪: " + (script.length() > 20 ? script.substring(0, 20) + "..." : script));
|
||||
pendingScripts.add(script);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void dispose() {
|
||||
if (browser != null) browser.close(true);
|
||||
if (client != null) client.dispose();
|
||||
// CefApp don't dispose here, it's global
|
||||
}
|
||||
|
||||
public CefBrowser getBrowser() { return browser; }
|
||||
public CefClient getClient() { return client; }
|
||||
}
|
||||
@@ -1,880 +1,168 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.events.BrowserCreationCallback;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import com.axis.innovators.box.browser.bridge.JsBridgeController;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
import org.cef.callback.*;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.misc.BoolRef;
|
||||
import org.cef.network.CefRequest;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BrowserWindow extends JFrame {
|
||||
public class BrowserWindow extends JFrame implements BrowserContainer {
|
||||
private final BrowserCore browserCore;
|
||||
private final String windowId;
|
||||
private final String htmlUrl;
|
||||
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 BrowserCreationCallback browserCreationCallback;
|
||||
private String windowId;
|
||||
private String title = "JCEF Window";
|
||||
private Dimension size = new Dimension(800, 600);
|
||||
private WindowOperationHandler operationHandler;
|
||||
private String htmlPath;
|
||||
private Image icon;
|
||||
private boolean resizable = true; // 默认允许调整大小
|
||||
private boolean maximizable = true; // 默认允许最大化
|
||||
private boolean minimizable = true; // 默认允许最小化
|
||||
private String htmlUrl = "";
|
||||
private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器
|
||||
public static class Builder extends BaseBrowserBuilder<Builder> {
|
||||
public Builder(String windowId) { super(windowId); }
|
||||
|
||||
public Builder resizable(boolean resizable) {
|
||||
this.resizable = resizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder maximizable(boolean maximizable) {
|
||||
this.maximizable = maximizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置链接打开方式
|
||||
*
|
||||
* @param openInBrowser 是否在当前浏览器窗口中打开链接
|
||||
* true - 在当前浏览器窗口中打开链接(本地跳转)
|
||||
* false - 使用系统默认浏览器打开链接(外部跳转)
|
||||
* @return Builder实例,支持链式调用
|
||||
*
|
||||
* @apiNote 此方法控制两种不同的链接打开行为:
|
||||
* 1. 当设置为true时:
|
||||
* - 所有链接将在当前CEF浏览器窗口内打开
|
||||
*
|
||||
* 2. 当设置为false时(默认值):
|
||||
* - 所有链接将在系统默认浏览器中打开
|
||||
* - 更安全,避免潜在的安全风险
|
||||
* - 适用于简单的信息展示场景
|
||||
*
|
||||
* @implNote 内部实现说明:
|
||||
* - 实际存储的是反向值(openLinksInExternalBrowser)
|
||||
* - 这样设置是为了保持与历史版本的兼容性
|
||||
* - 方法名使用"openInBrowser"更符合用户直觉
|
||||
*
|
||||
* @example 使用示例:
|
||||
* // 在当前窗口打开链接
|
||||
* new Builder().openLinksInBrowser(true).build();
|
||||
*
|
||||
* // 使用系统浏览器打开链接(默认)
|
||||
* new Builder().openLinksInBrowser(false).build();
|
||||
*
|
||||
* @see #openLinksInExternalBrowser 内部存储字段
|
||||
* @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现
|
||||
* @see CefRequestHandler#onBeforeBrowse 导航处理实现
|
||||
*/
|
||||
public Builder openLinksInBrowser(boolean openInBrowser) {
|
||||
this.openLinksInExternalBrowser = !openInBrowser;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder minimizable(boolean minimizable) {
|
||||
this.minimizable = minimizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器创建回调
|
||||
* @param callback 回调
|
||||
*/
|
||||
public Builder setBrowserCreationCallback(BrowserCreationCallback callback){
|
||||
this.browserCreationCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口标题
|
||||
* @param title 标题
|
||||
*/
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口大小
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
public Builder size(int width, int height) {
|
||||
this.size = new Dimension(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器触发事件
|
||||
* @param handler 事件处理器
|
||||
*/
|
||||
public Builder operationHandler(WindowOperationHandler handler) {
|
||||
this.operationHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器图标
|
||||
* @param icon 图标
|
||||
*/
|
||||
public Builder icon(Image icon) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
*/
|
||||
@Override
|
||||
public BrowserWindow build() {
|
||||
if (htmlUrl.isEmpty()) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
validatePaths();
|
||||
return new BrowserWindow(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
* @param path HTML路径
|
||||
*/
|
||||
public Builder htmlPath(String path) {
|
||||
this.htmlPath = path;
|
||||
return this;
|
||||
private void validatePaths() {
|
||||
if ((htmlUrl == null || htmlUrl.isEmpty()) && (htmlPath == null || htmlPath.isEmpty())) {
|
||||
throw new IllegalArgumentException("HTML path or URL cannot be empty");
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Url
|
||||
* @param htmlUrl Url路径
|
||||
*/
|
||||
public Builder htmlUrl(String htmlUrl) {
|
||||
this.htmlUrl = htmlUrl;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private BrowserWindow(Builder builder) {
|
||||
this.windowId = builder.windowId;
|
||||
this.htmlPath = builder.htmlPath;
|
||||
this.operationHandler = builder.operationHandler;
|
||||
this.htmlUrl = builder.htmlUrl;
|
||||
if (builder.icon != null) setIconImage(builder.icon);
|
||||
|
||||
// 设置图标(如果存在)
|
||||
if (builder.icon != null) {
|
||||
setIconImage(builder.icon);
|
||||
// 初始化核心代理
|
||||
this.browserCore = new BrowserCore(this, windowId, builder);
|
||||
|
||||
if (builder.controller != null) {
|
||||
browserCore.setJsBridgeController(builder.controller);
|
||||
}
|
||||
|
||||
// 初始化浏览器组件
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
setupMessageHandlers(operationHandler);
|
||||
Component browserComponent = browserCore.initialize();
|
||||
|
||||
// 布局自定义回调
|
||||
if (builder.browserCreationCallback != null) {
|
||||
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
|
||||
this, getContentPane(), browserComponent, builder
|
||||
);
|
||||
if (handled) {
|
||||
configureWindow(builder);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 默认布局
|
||||
initDefaultLayout(builder, browserComponent);
|
||||
configureWindow(builder);
|
||||
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized) {
|
||||
isInitialized = true;
|
||||
try {
|
||||
this.cefApp = CefAppManager.getInstance();
|
||||
//CefAppManager.incrementBrowserCount();
|
||||
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 控制台
|
||||
//if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
|
||||
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 "ℹ️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
|
||||
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
|
||||
// 检测 F12
|
||||
if (event.windows_key_code == 123) {
|
||||
browser.getDevTools().createImmediately();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
|
||||
CefRequest request, boolean userGesture, boolean isRedirect) {
|
||||
|
||||
String url = request.getURL();
|
||||
|
||||
// 【关键判断】如果是 data: 协议,绝对禁止调用 Desktop.browse
|
||||
// 返回 false 让 CEF 内核自己渲染这个 Base64 内容
|
||||
if (url != null && url.toLowerCase().startsWith("data:")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 处理其他普通链接 (http/https/file)
|
||||
if (userGesture) {
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(url));
|
||||
return true; // 拦截,交给系统
|
||||
} catch (Exception e) {
|
||||
System.out.println("外部浏览器打开失败: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
return false; // 允许内部跳转
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
if (params.isEditable()) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
} else {
|
||||
browser.executeJavaScript(
|
||||
"window.getSelection().toString();",
|
||||
browser.getURL(),
|
||||
0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴操作
|
||||
* @param plainText 是否去除格式(纯文本模式)
|
||||
*/
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
|
||||
if (plainText) {
|
||||
text = text.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
String escapedText = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
String script = String.format(
|
||||
"if (document.activeElement) {\n" +
|
||||
" document.activeElement.value += '%s';\n" + // 简单追加文本
|
||||
" document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
|
||||
"}",
|
||||
escapedText
|
||||
);
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addDownloadHandler(new org.cef.handler.CefDownloadHandler() {
|
||||
@Override
|
||||
public void onBeforeDownload(org.cef.browser.CefBrowser browser,
|
||||
org.cef.callback.CefDownloadItem downloadItem,
|
||||
String suggestedName,
|
||||
org.cef.callback.CefBeforeDownloadCallback callback) {
|
||||
callback.Continue(suggestedName, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadUpdated(org.cef.browser.CefBrowser browser,
|
||||
org.cef.callback.CefDownloadItem downloadItem,
|
||||
org.cef.callback.CefDownloadItemCallback callback) {
|
||||
}
|
||||
});
|
||||
|
||||
client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onJSDialog(CefBrowser browser, String origin_url, JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
|
||||
if (dialog_type == JSDialogType.JSDIALOGTYPE_ALERT) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(
|
||||
BrowserWindow.this,
|
||||
message_text,
|
||||
"警告",
|
||||
JOptionPane.INFORMATION_MESSAGE
|
||||
);
|
||||
callback.Continue(true, "");
|
||||
});
|
||||
return true;
|
||||
} else if (dialog_type == JSDialogType.JSDIALOGTYPE_CONFIRM) { // 处理 confirm()
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
int result = JOptionPane.showConfirmDialog(
|
||||
BrowserWindow.this,
|
||||
message_text,
|
||||
"确认",
|
||||
JOptionPane.YES_NO_OPTION,
|
||||
JOptionPane.QUESTION_MESSAGE
|
||||
);
|
||||
|
||||
// 如果用户点击 YES (确定),则传回 true
|
||||
boolean confirmed = (result == JOptionPane.YES_OPTION);
|
||||
callback.Continue(confirmed, "");
|
||||
});
|
||||
return true;
|
||||
} else if (dialog_type == JSDialogType.JSDIALOGTYPE_PROMPT) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
Object result = JOptionPane.showInputDialog(
|
||||
BrowserWindow.this,
|
||||
message_text,
|
||||
"输入",
|
||||
JOptionPane.QUESTION_MESSAGE,
|
||||
null,
|
||||
null,
|
||||
default_prompt_text
|
||||
);
|
||||
String input = (result != null) ? result.toString() : null;
|
||||
if (input != null) {
|
||||
callback.Continue(true, input);
|
||||
} else {
|
||||
callback.Continue(false, "");
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
// 默认行为:如果不是以上三种类型,交给 CEF 默认处理
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 3. 拦截所有新窗口(关键修复点!)
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
boolean isDataProtocol = targetUrl != null && targetUrl.toLowerCase().startsWith("data:");
|
||||
if (builder.openLinksInExternalBrowser && !isDataProtocol) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("外部浏览器打开失败: " + e.getMessage());
|
||||
}
|
||||
return true; // 拦截默认行为
|
||||
}
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
String popupWindowId = windowId + "_popup_" + System.currentTimeMillis();
|
||||
WindowRegistry.getInstance().createNewWindow(popupWindowId, popupBuilder -> {
|
||||
popupBuilder.title(getTitle()) // 继承标题
|
||||
.size(getWidth(), getHeight()) // 继承大小
|
||||
.htmlUrl(targetUrl) // 传入 data: URL
|
||||
.icon(builder.icon) // 继承图标
|
||||
.openLinksInBrowser(true); // 新窗口内链接强制内部打开
|
||||
if (builder.operationHandler != null) {
|
||||
popupBuilder.operationHandler(builder.operationHandler);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return true; // 拦截 CEF 默认弹窗,由 Java Swing 窗口接管
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Thread.currentThread().setName("BrowserRenderThread");
|
||||
|
||||
// 4. 加载HTML
|
||||
if (htmlUrl.isEmpty()){
|
||||
String fileUrl = new File(htmlPath).toURI().toURL().toString();
|
||||
System.out.println("Loading HTML from: " + fileUrl);
|
||||
|
||||
// 5. 创建浏览器组件(直接添加到内容面板)
|
||||
browser = client.createBrowser(fileUrl, false, false);
|
||||
} else {
|
||||
System.out.println("Loading Url from: " + htmlUrl);
|
||||
browser = client.createBrowser(htmlUrl, false, false);
|
||||
}
|
||||
|
||||
Component browserComponent = browser.getUIComponent();
|
||||
if (builder.browserCreationCallback != null) {
|
||||
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
|
||||
this, // 当前窗口
|
||||
getContentPane(), // 内容面板
|
||||
browserComponent, // 浏览器组件
|
||||
builder // 构建器对象
|
||||
);
|
||||
|
||||
// 如果回调返回true,跳过默认布局
|
||||
if (handled) {
|
||||
// 设置窗口基本属性
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 添加资源释放监听器
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
return browserComponent; // 直接返回,跳过默认布局
|
||||
}
|
||||
}
|
||||
|
||||
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
|
||||
updateTheme();
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
getContentPane().removeAll();
|
||||
private void initDefaultLayout(Builder builder, Component browserComponent) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
WindowDragListener dragListener = new WindowDragListener(this);
|
||||
titleBar.addMouseListener(dragListener);
|
||||
titleBar.addMouseMotionListener(dragListener);
|
||||
|
||||
dragPanel.add(titleBar, BorderLayout.NORTH);
|
||||
getContentPane().add(dragPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(browserComponent, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
// 7. 窗口属性设置
|
||||
private void configureWindow(Builder builder) {
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(null);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
setResizable(builder.resizable);
|
||||
|
||||
// 8. 资源释放
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
closeWindow();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
|
||||
});
|
||||
return browserComponent;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
isInitialized = false;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 1. 获取Java字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
if (AxisInnovatorsBox.getMain() != null) {
|
||||
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
injectFontInfoToPage(browser, fontInfo, isDarkTheme);
|
||||
}
|
||||
|
||||
// 2. 注入主题信息
|
||||
//injectThemeInfoToPage(browser, isDarkTheme);
|
||||
|
||||
//// 3. 刷新浏览器
|
||||
//SwingUtilities.invokeLater(() -> {
|
||||
// browser.reload();
|
||||
//});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Java字体信息(从UIManager获取)
|
||||
*/
|
||||
private String getSystemFontsInfo() {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
JsonObject fontInfo = new JsonObject();
|
||||
JsonObject uiFonts = new JsonObject();
|
||||
|
||||
String[] fontKeys = {
|
||||
"Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
|
||||
"CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
|
||||
"TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
|
||||
"FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
|
||||
"Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
|
||||
"PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
|
||||
"Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
|
||||
"OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
|
||||
};
|
||||
|
||||
for (String key : fontKeys) {
|
||||
Font font = UIManager.getFont(key);
|
||||
if (font != null) {
|
||||
JsonObject fontObj = new JsonObject();
|
||||
fontObj.addProperty("name", font.getFontName());
|
||||
fontObj.addProperty("family", font.getFamily());
|
||||
fontObj.addProperty("size", font.getSize());
|
||||
fontObj.addProperty("style", font.getStyle());
|
||||
fontObj.addProperty("bold", font.isBold());
|
||||
fontObj.addProperty("italic", font.isItalic());
|
||||
fontObj.addProperty("plain", font.isPlain());
|
||||
uiFonts.add(key, fontObj);
|
||||
}
|
||||
}
|
||||
|
||||
fontInfo.add("uiFonts", uiFonts);
|
||||
fontInfo.addProperty("timestamp", System.currentTimeMillis());
|
||||
fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
|
||||
|
||||
return gson.toJson(fontInfo);
|
||||
} catch (Exception e) {
|
||||
return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入主题信息到页面
|
||||
*/
|
||||
private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String themeInfo = String.format(
|
||||
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
|
||||
isDarkTheme,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
// 最简单的脚本 - 直接设置和分发事件
|
||||
String script = String.format(
|
||||
"window.javaThemeInfo = %s;" +
|
||||
"console.log('主题信息已设置:', window.javaThemeInfo);" +
|
||||
"" +
|
||||
"var event = new CustomEvent('javaThemeChanged', {" +
|
||||
" detail: window.javaThemeInfo" +
|
||||
"});" +
|
||||
"document.dispatchEvent(event);" +
|
||||
"console.log('javaThemeChanged事件已分发');",
|
||||
themeInfo);
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入字体信息到页面并设置字体
|
||||
*/
|
||||
private void injectFontInfoToPage(CefBrowser browser, String fontInfo,boolean isDarkTheme) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
// 使用更简单的脚本来注入字体信息
|
||||
String script =
|
||||
"if (typeof window.javaFontInfo === 'undefined') {" +
|
||||
" window.javaFontInfo = " + fontInfo + ";" +
|
||||
" console.log('Java font information has been loaded:', window.javaFontInfo);" +
|
||||
" " +
|
||||
" var event = new CustomEvent('javaFontsLoaded', {" +
|
||||
" detail: window.javaFontInfo" +
|
||||
" });" +
|
||||
" document.dispatchEvent(event);" +
|
||||
" console.log('The javaFontsLoaded event is dispatched');" +
|
||||
"}";
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
// 添加调试信息
|
||||
browser.executeJavaScript(
|
||||
"console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" +
|
||||
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
|
||||
String themeInfo = String.format(
|
||||
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
|
||||
isDarkTheme,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
script = String.format(
|
||||
"window.javaThemeInfo = %s;" +
|
||||
"console.log('主题信息已设置:', window.javaThemeInfo);" +
|
||||
"" +
|
||||
"var event = new CustomEvent('javaThemeChanged', {" +
|
||||
" detail: window.javaThemeInfo" +
|
||||
"});" +
|
||||
"document.dispatchEvent(event);" +
|
||||
"console.log('javaThemeChanged事件已分发');",
|
||||
themeInfo);
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
if (b) {
|
||||
if (browser != null) {
|
||||
updateTheme();
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void executingJsCode(String script) {
|
||||
if (browserCore != null) {
|
||||
browserCore.executingJsCode(script);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息路由器
|
||||
* @return 消息路由器
|
||||
* 保持 API 兼容性:将调用转发给 Core
|
||||
*/
|
||||
@Override
|
||||
public CefMessageRouter getMsgRouter() {
|
||||
return msgRouter;
|
||||
if (browserCore == null) return null;
|
||||
return browserCore.getMsgRouter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器对象
|
||||
* @return 浏览器对象
|
||||
*/
|
||||
@Override
|
||||
public CefBrowser getBrowser() {
|
||||
return browser;
|
||||
if (browserCore == null) return null;
|
||||
return browserCore.getBrowser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeWindow() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (browser != null) {
|
||||
browser.close(true);
|
||||
if (browserCore != null) {
|
||||
browserCore.dispose();
|
||||
}
|
||||
dispose();
|
||||
cefApp.dispose();
|
||||
WindowRegistry.getInstance().unregisterWindow(windowId);
|
||||
});
|
||||
dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTheme() {
|
||||
if (browserCore != null) {
|
||||
browserCore.updateTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setController(JsBridgeController controller) {
|
||||
browserCore.setJsBridgeController(controller);
|
||||
}
|
||||
|
||||
// 提取拖拽逻辑内部类
|
||||
private static class WindowDragListener extends MouseAdapter {
|
||||
private final Window window;
|
||||
private Point dragStart;
|
||||
|
||||
public WindowDragListener(Window window) { this.window = window; }
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) { dragStart = e.getPoint(); }
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) { dragStart = null; }
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (dragStart != null) {
|
||||
Point curr = e.getLocationOnScreen();
|
||||
window.setLocation(curr.x - dragStart.x, curr.y - dragStart.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,834 +1,164 @@
|
||||
package com.axis.innovators.box.browser;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.events.BrowserCreationCallback;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonObject;
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import com.axis.innovators.box.browser.bridge.JsBridgeController;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
import org.cef.callback.CefContextMenuParams;
|
||||
import org.cef.callback.CefJSDialogCallback;
|
||||
import org.cef.callback.CefMenuModel;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.*;
|
||||
import org.cef.misc.BoolRef;
|
||||
import org.cef.network.CefRequest;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.datatransfer.Clipboard;
|
||||
import java.awt.datatransfer.DataFlavor;
|
||||
import java.awt.datatransfer.UnsupportedFlavorException;
|
||||
import java.awt.event.*;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import static org.cef.callback.CefMenuModel.MenuId.MENU_ID_USER_FIRST;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class BrowserWindowJDialog extends JDialog {
|
||||
public class BrowserWindowJDialog extends JDialog implements BrowserContainer {
|
||||
private final BrowserCore browserCore;
|
||||
private final String windowId;
|
||||
private final String htmlUrl;
|
||||
private CefApp cefApp;
|
||||
private CefClient client;
|
||||
private CefBrowser browser;
|
||||
private final Component browserComponent;
|
||||
private final String htmlPath;
|
||||
//private 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 static class Builder extends BaseBrowserBuilder<Builder> {
|
||||
private JFrame parentFrame;
|
||||
private boolean resizable = true; // 默认允许调整大小
|
||||
private boolean maximizable = true; // 默认允许最大化
|
||||
private boolean minimizable = true; // 默认允许最小化
|
||||
private String htmlUrl = "";
|
||||
private BrowserCreationCallback browserCreationCallback;
|
||||
private boolean openLinksInExternalBrowser = true; // 默认使用外部浏览器
|
||||
|
||||
public Builder resizable(boolean resizable) {
|
||||
this.resizable = resizable;
|
||||
return this;
|
||||
}
|
||||
public Builder(String windowId) { super(windowId); }
|
||||
|
||||
public Builder maximizable(boolean maximizable) {
|
||||
this.maximizable = maximizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder minimizable(boolean minimizable) {
|
||||
this.minimizable = minimizable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder(String windowId) {
|
||||
this.windowId = windowId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口标题
|
||||
* @param title 标题
|
||||
*/
|
||||
public Builder title(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置链接打开方式
|
||||
*
|
||||
* @param openInBrowser 是否在当前浏览器窗口中打开链接
|
||||
* true - 在当前浏览器窗口中打开链接(本地跳转)
|
||||
* false - 使用系统默认浏览器打开链接(外部跳转)
|
||||
* @return Builder实例,支持链式调用
|
||||
*
|
||||
* @apiNote 此方法控制两种不同的链接打开行为:
|
||||
* 1. 当设置为true时:
|
||||
* - 所有链接将在当前CEF浏览器窗口内打开
|
||||
*
|
||||
* 2. 当设置为false时(默认值):
|
||||
* - 所有链接将在系统默认浏览器中打开
|
||||
* - 更安全,避免潜在的安全风险
|
||||
* - 适用于简单的信息展示场景
|
||||
*
|
||||
* @implNote 内部实现说明:
|
||||
* - 实际存储的是反向值(openLinksInExternalBrowser)
|
||||
* - 这样设置是为了保持与历史版本的兼容性
|
||||
* - 方法名使用"openInBrowser"更符合用户直觉
|
||||
*
|
||||
* @example 使用示例:
|
||||
* // 在当前窗口打开链接
|
||||
* new Builder().openLinksInBrowser(true).build();
|
||||
*
|
||||
* // 使用系统浏览器打开链接(默认)
|
||||
* new Builder().openLinksInBrowser(false).build();
|
||||
*
|
||||
* @see #openLinksInExternalBrowser 内部存储字段
|
||||
* @see CefLifeSpanHandler#onBeforePopup 弹窗处理实现
|
||||
* @see CefRequestHandler#onBeforeBrowse 导航处理实现
|
||||
*/
|
||||
public Builder openLinksInBrowser(boolean openInBrowser) {
|
||||
this.openLinksInExternalBrowser = !openInBrowser;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器创建回调
|
||||
* @param callback 回调
|
||||
*/
|
||||
public Builder setBrowserCreationCallback(BrowserCreationCallback callback){
|
||||
this.browserCreationCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口大小
|
||||
* @param width 宽度
|
||||
* @param height 高度
|
||||
*/
|
||||
public Builder size(int width, int height) {
|
||||
this.size = new Dimension(width, height);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器窗口父窗口
|
||||
* @param parent 父窗口
|
||||
*/
|
||||
public Builder parentFrame(JFrame parent) {
|
||||
this.parentFrame = parent;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 设置浏览器触发事件
|
||||
* @param handler 事件处理器
|
||||
*/
|
||||
public Builder operationHandler(WindowOperationHandler handler) {
|
||||
this.operationHandler = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置浏览器图标
|
||||
* @param icon 图标
|
||||
*/
|
||||
public Builder icon(Image icon) {
|
||||
this.icon = icon;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
*/
|
||||
@Override
|
||||
public BrowserWindowJDialog build() {
|
||||
if (htmlUrl.isEmpty()) {
|
||||
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());
|
||||
}
|
||||
if ((htmlUrl == null || htmlUrl.isEmpty()) && (htmlPath == null || htmlPath.isEmpty())) {
|
||||
throw new IllegalArgumentException("HTML path or URL cannot be empty");
|
||||
}
|
||||
return new BrowserWindowJDialog(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置HTML路径
|
||||
* @param path HTML路径
|
||||
*/
|
||||
public Builder htmlPath(String path) {
|
||||
this.htmlPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用Url
|
||||
* @param htmlUrl Url路径
|
||||
*/
|
||||
public Builder htmlUrl(String htmlUrl) {
|
||||
this.htmlUrl = htmlUrl;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private BrowserWindowJDialog(Builder builder) {
|
||||
// 根据父窗口是否存在,设置是否为模态对话框
|
||||
super(builder.parentFrame, builder.title, builder.parentFrame != null);
|
||||
this.windowId = builder.windowId;
|
||||
this.htmlPath = builder.htmlPath;
|
||||
this.htmlUrl = builder.htmlUrl;
|
||||
this.operationHandler = builder.operationHandler;
|
||||
if (builder.icon != null) setIconImage(builder.icon);
|
||||
|
||||
// 设置图标(如果存在)
|
||||
if (builder.icon != null) {
|
||||
setIconImage(builder.icon);
|
||||
this.browserCore = new BrowserCore(this, windowId, builder);
|
||||
|
||||
if (builder.controller != null) {
|
||||
browserCore.setJsBridgeController(builder.controller);
|
||||
}
|
||||
|
||||
// 初始化浏览器组件
|
||||
try {
|
||||
this.browserComponent = initializeCef(builder);
|
||||
if (operationHandler != null) {
|
||||
setupMessageHandlers(operationHandler);
|
||||
Component browserComponent = browserCore.initialize();
|
||||
|
||||
if (builder.browserCreationCallback != null) {
|
||||
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
|
||||
this, getContentPane(), browserComponent, builder
|
||||
);
|
||||
if (handled) {
|
||||
configureWindow(builder);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
initDefaultLayout(builder, browserComponent);
|
||||
configureWindow(builder);
|
||||
|
||||
} catch (Exception e) {
|
||||
JOptionPane.showMessageDialog(this, "初始化失败: " + e.getMessage());
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
private static boolean isInitialized = false;
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized) {
|
||||
isInitialized = true;
|
||||
try {
|
||||
this.cefApp = CefAppManager.getInstance();
|
||||
//CefAppManager.incrementBrowserCount();
|
||||
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 控制台
|
||||
//if (level != CefSettings.LogSeverity.LOGSEVERITY_WARNING) {
|
||||
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 "ℹ️";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (AxisInnovatorsBox.getMain() != null && AxisInnovatorsBox.getMain().isDebugEnvironment()) {
|
||||
client.addKeyboardHandler(new CefKeyboardHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onKeyEvent(CefBrowser browser, CefKeyEvent event) {
|
||||
// 检测 F12
|
||||
if (event.windows_key_code == 123) {
|
||||
browser.getDevTools().createImmediately();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
// 处理弹出窗口:根据配置决定打开方式
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
return true; // 拦截弹窗
|
||||
} else {
|
||||
// 在当前浏览器中打开
|
||||
browser.loadURL(targetUrl);
|
||||
return true; // 拦截弹窗并在当前窗口打开
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addRequestHandler(new CefRequestHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame,
|
||||
CefRequest request, boolean userGesture, boolean isRedirect) {
|
||||
// 处理主窗口导航
|
||||
if (userGesture) {
|
||||
if (builder.openLinksInExternalBrowser) {
|
||||
// 使用默认浏览器打开
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(request.getURL()));
|
||||
return true; // 取消内置浏览器导航
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 允许在当前浏览器中打开
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
client.addContextMenuHandler(new CefContextMenuHandlerAdapter() {
|
||||
@Override
|
||||
public void onBeforeContextMenu(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, CefMenuModel model) {
|
||||
model.clear();
|
||||
if (!params.getSelectionText().isEmpty() || params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST, "复制");
|
||||
}
|
||||
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MENU_ID_USER_FIRST + 1, "粘贴");
|
||||
model.addItem(MENU_ID_USER_FIRST + 2, "粘贴纯文本");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(CefBrowser browser, CefFrame frame,
|
||||
CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
if (commandId == MENU_ID_USER_FIRST) {
|
||||
if (params.isEditable()) {
|
||||
browser.executeJavaScript("document.execCommand('copy');", browser.getURL(), 0);
|
||||
} else {
|
||||
browser.executeJavaScript(
|
||||
"window.getSelection().toString();",
|
||||
browser.getURL(),
|
||||
0
|
||||
);
|
||||
}
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 1) {
|
||||
pasteContent(browser, false);
|
||||
return true;
|
||||
} else if (commandId == MENU_ID_USER_FIRST + 2) {
|
||||
pasteContent(browser, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理粘贴操作
|
||||
* @param plainText 是否去除格式(纯文本模式)
|
||||
*/
|
||||
private void pasteContent(CefBrowser browser, boolean plainText) {
|
||||
try {
|
||||
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
|
||||
if (clipboard.isDataFlavorAvailable(DataFlavor.stringFlavor)) {
|
||||
String text = (String) clipboard.getData(DataFlavor.stringFlavor);
|
||||
|
||||
if (plainText) {
|
||||
text = text.replaceAll("<[^>]+>", "");
|
||||
}
|
||||
|
||||
String escapedText = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("'", "\\'")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "\\r");
|
||||
|
||||
String script = String.format(
|
||||
"if (document.activeElement) {\n" +
|
||||
" document.activeElement.value += '%s';\n" + // 简单追加文本
|
||||
" document.dispatchEvent(new Event('input', { bubbles: true }));\n" + // 触发输入事件
|
||||
"}",
|
||||
escapedText
|
||||
);
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
}
|
||||
} catch (UnsupportedFlavorException | IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 添加 alert 弹窗监控处理
|
||||
client.addJSDialogHandler(new CefJSDialogHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onJSDialog(CefBrowser browser, String origin_url, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) {
|
||||
if (dialog_type == CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_ALERT) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
JOptionPane.showMessageDialog(
|
||||
BrowserWindowJDialog.this,
|
||||
message_text,
|
||||
"警告",
|
||||
JOptionPane.INFORMATION_MESSAGE
|
||||
);
|
||||
});
|
||||
callback.Continue(true, "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 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
|
||||
if (htmlUrl.isEmpty()) {
|
||||
String fileUrl = new File(htmlPath).toURI().toURL().toString();
|
||||
System.out.println("Loading HTML from: " + fileUrl);
|
||||
|
||||
// 5. 创建浏览器组件(直接添加到内容面板)
|
||||
browser = client.createBrowser(fileUrl, false, false);
|
||||
} else {
|
||||
System.out.println("Loading HTML from: " + htmlUrl);
|
||||
browser = client.createBrowser(htmlUrl, false, false);
|
||||
}
|
||||
|
||||
Component browserComponent = browser.getUIComponent();
|
||||
|
||||
if (builder.browserCreationCallback != null) {
|
||||
boolean handled = builder.browserCreationCallback.onLayoutCustomization(
|
||||
this, // 当前窗口
|
||||
getContentPane(), // 内容面板
|
||||
browserComponent, // 浏览器组件
|
||||
builder // 构建器对象
|
||||
);
|
||||
|
||||
// 如果回调返回true,跳过默认布局
|
||||
if (handled) {
|
||||
// 设置窗口基本属性
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(builder.parentFrame);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
|
||||
// 添加资源释放监听器
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
return browserComponent; // 直接返回,跳过默认布局
|
||||
}
|
||||
}
|
||||
|
||||
updateTheme();
|
||||
|
||||
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
getContentPane().removeAll();
|
||||
private void initDefaultLayout(Builder builder, Component browserComponent) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
WindowDragListener dragListener = new WindowDragListener(this);
|
||||
titleBar.addMouseListener(dragListener);
|
||||
titleBar.addMouseMotionListener(dragListener);
|
||||
|
||||
dragPanel.add(titleBar, BorderLayout.NORTH);
|
||||
getContentPane().add(dragPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(browserComponent, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
// 7. 窗口属性设置
|
||||
private void configureWindow(Builder builder) {
|
||||
setTitle(builder.title);
|
||||
setSize(builder.size);
|
||||
setLocationRelativeTo(builder.parentFrame);
|
||||
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
setResizable(builder.resizable);
|
||||
|
||||
// 8. 资源释放
|
||||
addWindowListener(new WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(WindowEvent e) {
|
||||
browser.close(true);
|
||||
client.dispose();
|
||||
closeWindow();
|
||||
}
|
||||
});
|
||||
|
||||
setVisible(true);
|
||||
|
||||
});
|
||||
return browserComponent;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
JOptionPane.showMessageDialog(null, "初始化失败: " + e.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
isInitialized = false;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
dispose();
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 1. 获取Java字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
injectFontInfoToPage(browser, fontInfo);
|
||||
|
||||
// 2. 注入主题信息
|
||||
if (AxisInnovatorsBox.getMain() != null) {
|
||||
boolean isDarkTheme = AxisInnovatorsBox.getMain().getRegistrationTopic().isDarkMode();
|
||||
injectThemeInfoToPage(browser, isDarkTheme);
|
||||
}
|
||||
|
||||
//// 3. 刷新浏览器
|
||||
//SwingUtilities.invokeLater(() -> {
|
||||
// browser.reload();
|
||||
//});
|
||||
|
||||
}
|
||||
private void injectThemeInfoToPage(CefBrowser browser, boolean isDarkTheme) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
String themeInfo = String.format(
|
||||
"{\"isDarkTheme\": %s, \"timestamp\": %d}",
|
||||
isDarkTheme,
|
||||
System.currentTimeMillis()
|
||||
);
|
||||
|
||||
String script =
|
||||
"window.javaThemeInfo = " + themeInfo + ";\n" +
|
||||
"console.log('Java theme information has been loaded:', window.javaThemeInfo);\n" +
|
||||
"\n" +
|
||||
"if (typeof applyJavaTheme === 'function') {\n" +
|
||||
" applyJavaTheme(window.javaThemeInfo);\n" +
|
||||
"}\n" +
|
||||
"\n" +
|
||||
"var event = new CustomEvent('javaThemeChanged', {\n" +
|
||||
" detail: window.javaThemeInfo\n" +
|
||||
"});\n" +
|
||||
"document.dispatchEvent(event);\n" +
|
||||
"console.log('The javaThemeChanged event is dispatched');";
|
||||
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
browser.executeJavaScript(
|
||||
"console.log('Theme information injection is complete,window.javaThemeInfo:', typeof window.javaThemeInfo);" +
|
||||
"console.log('Number of theme event listeners:', document.eventListeners ? document.eventListeners('javaThemeChanged') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取Java字体信息(从UIManager获取)
|
||||
*/
|
||||
private String getSystemFontsInfo() {
|
||||
try {
|
||||
Gson gson = new Gson();
|
||||
JsonObject fontInfo = new JsonObject();
|
||||
JsonObject uiFonts = new JsonObject();
|
||||
|
||||
String[] fontKeys = {
|
||||
"Label.font", "Button.font", "ToggleButton.font", "RadioButton.font",
|
||||
"CheckBox.font", "ColorChooser.font", "ComboBox.font", "EditorPane.font",
|
||||
"TextArea.font", "TextField.font", "PasswordField.font", "TextPane.font",
|
||||
"FormattedTextField.font", "Table.font", "TableHeader.font", "List.font",
|
||||
"Tree.font", "TabbedPane.font", "MenuBar.font", "Menu.font", "MenuItem.font",
|
||||
"PopupMenu.font", "CheckBoxMenuItem.font", "RadioButtonMenuItem.font",
|
||||
"Spinner.font", "ToolBar.font", "TitledBorder.font", "OptionPane.messageFont",
|
||||
"OptionPane.buttonFont", "Panel.font", "Viewport.font", "ToolTip.font"
|
||||
};
|
||||
|
||||
for (String key : fontKeys) {
|
||||
Font font = UIManager.getFont(key);
|
||||
if (font != null) {
|
||||
JsonObject fontObj = new JsonObject();
|
||||
fontObj.addProperty("name", font.getFontName());
|
||||
fontObj.addProperty("family", font.getFamily());
|
||||
fontObj.addProperty("size", font.getSize());
|
||||
fontObj.addProperty("style", font.getStyle());
|
||||
fontObj.addProperty("bold", font.isBold());
|
||||
fontObj.addProperty("italic", font.isItalic());
|
||||
fontObj.addProperty("plain", font.isPlain());
|
||||
uiFonts.add(key, fontObj);
|
||||
}
|
||||
}
|
||||
|
||||
fontInfo.add("uiFonts", uiFonts);
|
||||
fontInfo.addProperty("timestamp", System.currentTimeMillis());
|
||||
fontInfo.addProperty("lookAndFeel", UIManager.getLookAndFeel().getName());
|
||||
|
||||
return gson.toJson(fontInfo);
|
||||
} catch (Exception e) {
|
||||
return "{\"error\": \"无法获取UIManager字体信息: " + e.getMessage() + "\"}";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注入字体信息到页面并设置字体
|
||||
*/
|
||||
private void injectFontInfoToPage(CefBrowser browser, String fontInfo) {
|
||||
if (client == null) {
|
||||
return;
|
||||
}
|
||||
client.addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
// 使用更简单的脚本来注入字体信息
|
||||
String script =
|
||||
"if (typeof window.javaFontInfo === 'undefined') {" +
|
||||
" window.javaFontInfo = " + fontInfo + ";" +
|
||||
" console.log('Java font information has been loaded:', window.javaFontInfo);" +
|
||||
" " +
|
||||
" var event = new CustomEvent('javaFontsLoaded', {" +
|
||||
" detail: window.javaFontInfo" +
|
||||
" });" +
|
||||
" document.dispatchEvent(event);" +
|
||||
" console.log('The javaFontsLoaded event is dispatched');" +
|
||||
"}";
|
||||
|
||||
System.out.println("正在注入字体信息到页面...");
|
||||
browser.executeJavaScript(script, browser.getURL(), 0);
|
||||
|
||||
// 添加调试信息
|
||||
browser.executeJavaScript(
|
||||
"console.log('Font information injection is complete,window.javaFontInfo:', typeof window.javaFontInfo);" +
|
||||
"console.log('Number of event listeners:', document.eventListeners ? document.eventListeners('javaFontsLoaded') : '无法获取');",
|
||||
browser.getURL(), 0
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
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) {
|
||||
if (b) {
|
||||
if (browser != null) {
|
||||
updateTheme();
|
||||
}
|
||||
}
|
||||
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 消息路由器
|
||||
*/
|
||||
@Override
|
||||
public CefMessageRouter getMsgRouter() {
|
||||
return msgRouter;
|
||||
if (browserCore == null) return null;
|
||||
return browserCore.getMsgRouter();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取浏览器对象
|
||||
* @return 浏览器对象
|
||||
*/
|
||||
@Override
|
||||
public void executingJsCode(String script) {
|
||||
if (browserCore != null) {
|
||||
browserCore.executingJsCode(script);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CefBrowser getBrowser() {
|
||||
return browser;
|
||||
if (browserCore == null) return null;
|
||||
return browserCore.getBrowser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void closeWindow() {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
if (browser != null) {
|
||||
browser.close(true);
|
||||
if (browserCore != null) {
|
||||
browserCore.dispose();
|
||||
}
|
||||
dispose();
|
||||
cefApp.dispose();
|
||||
WindowRegistry.getInstance().unregisterWindow(windowId);
|
||||
});
|
||||
dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateTheme() {
|
||||
if (browserCore != null) {
|
||||
browserCore.updateTheme();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setController(JsBridgeController controller) {
|
||||
browserCore.setJsBridgeController(controller);
|
||||
}
|
||||
|
||||
// 复用拖拽逻辑(实际项目中可以将 WindowDragListener 提取为单独的公共类)
|
||||
private static class WindowDragListener extends MouseAdapter {
|
||||
private final Window window;
|
||||
private Point dragStart;
|
||||
public WindowDragListener(Window window) { this.window = window; }
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) { dragStart = e.getPoint(); }
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) { dragStart = null; }
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
if (dragStart != null) {
|
||||
Point curr = e.getLocationOnScreen();
|
||||
window.setLocation(curr.x - dragStart.x, curr.y - dragStart.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,12 +42,10 @@ public class MainApplication {
|
||||
private static long ctxHandle;
|
||||
private static boolean isSystem = true;
|
||||
public static void main(String[] args) {
|
||||
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
|
||||
TerminalManager.popupRealLinuxWindow();
|
||||
}
|
||||
|
||||
public static void popupSimulatingLinuxWindow(JFrame parent){
|
||||
|
||||
}
|
||||
/**
|
||||
* 初始化数据库连接
|
||||
*/
|
||||
@@ -234,20 +232,17 @@ public class MainApplication {
|
||||
* @param parent 父窗口
|
||||
*/
|
||||
public static void popupSimulatingSQLWindow(JFrame parent) {
|
||||
AtomicReference<BrowserWindowJDialog> window = new AtomicReference<>();
|
||||
initDatabase();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
WindowRegistry.getInstance().createNewChildWindow("main", builder ->
|
||||
window.set(builder.title("SQL Command Line Client")
|
||||
BrowserWindowJDialog window = WindowRegistry.getInstance().createNewChildWindow("main",builder ->
|
||||
builder.title("SQL Command Line Client")
|
||||
.parentFrame(parent)
|
||||
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
|
||||
.size(900, 600)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "SQLTerminal.html")
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
.operationHandler(createOperationHandler()));
|
||||
|
||||
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter != null) {
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
// 在 popupSimulatingSQLWindow 方法的 msgRouter.addHandler 内部:
|
||||
@@ -297,19 +292,17 @@ public class MainApplication {
|
||||
modelHandle = LM.llamaLoadModelFromFile(LM.DEEP_SEEK);
|
||||
ctxHandle = LM.createContext(modelHandle);
|
||||
|
||||
AtomicReference<BrowserWindowJDialog> window = new AtomicReference<>();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
WindowRegistry.getInstance().createNewChildWindow("main", builder ->
|
||||
window.set(builder.title("Axis Innovators Box AI 工具箱")
|
||||
BrowserWindowJDialog window = WindowRegistry.getInstance().createNewChildWindow("main", builder ->
|
||||
builder.title("Axis Innovators Box AI 工具箱")
|
||||
.parentFrame(parent)
|
||||
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
|
||||
.size(1280, 720)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "AIaToolbox_dark.html")
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
|
||||
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter != null) {
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
@@ -331,18 +324,16 @@ public class MainApplication {
|
||||
}
|
||||
|
||||
public static void popupCCodeEditorWindow() {
|
||||
AtomicReference<BrowserWindow> window = new AtomicReference<>();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
window.set(builder.title("TzdC 代码编辑器")
|
||||
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
builder.title("TzdC 代码编辑器")
|
||||
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
|
||||
.size(1487, 836)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CCodeEditor.html")
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
|
||||
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter != null) {
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
@@ -382,18 +373,15 @@ public class MainApplication {
|
||||
}
|
||||
|
||||
public static void popupCodeEditorWindow() {
|
||||
AtomicReference<BrowserWindow> window = new AtomicReference<>();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
window.set(builder.title("代码编辑器")
|
||||
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder ->builder.title("代码编辑器")
|
||||
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
|
||||
.size(1487, 836)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "CodeEditor.html")
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
|
||||
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter != null) {
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
@@ -447,18 +435,16 @@ public class MainApplication {
|
||||
* 弹出html预览窗口
|
||||
*/
|
||||
public static void popupHTMLWindow(String path) {
|
||||
AtomicReference<BrowserWindow> window = new AtomicReference<>();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
window.set(builder.title("Axis Innovators Box HTML查看器")
|
||||
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
builder.title("Axis Innovators Box HTML查看器")
|
||||
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
|
||||
.size(1487, 836)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "HtmlViewer.html")
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
|
||||
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter != null) {
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
@@ -603,23 +589,21 @@ public class MainApplication {
|
||||
System.err.println("预加载 JDBC 驱动时发生异常: " + t.getMessage());
|
||||
}
|
||||
|
||||
AtomicReference<BrowserWindow> window = new AtomicReference<>();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
window.set(builder.title("Axis Innovators Box 数据库管理工具")
|
||||
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder ->
|
||||
builder.title("Axis Innovators Box 数据库管理工具")
|
||||
.icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage())
|
||||
.size(1487, 836)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "DatabaseTool.html")
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
|
||||
if (window.get() == null) {
|
||||
if (window == null) {
|
||||
System.err.println("popupDataBaseWindow: window 创建失败,window.get() == null");
|
||||
return;
|
||||
}
|
||||
|
||||
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter != null) {
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
|
||||
@@ -15,10 +15,11 @@ import java.util.function.Consumer;
|
||||
|
||||
public class WindowRegistry {
|
||||
private static WindowRegistry instance;
|
||||
private final ConcurrentMap<String, BrowserWindow> windows =
|
||||
new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<String, BrowserWindowJDialog> childWindows =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
// 建议:统一使用 BrowserContainer 接口存储,这样可以合并两个 Map
|
||||
// 这里为了兼容你现有的代码,暂时保留分开的 Map,但逻辑上可以合并
|
||||
private final ConcurrentMap<String, BrowserWindow> windows = new ConcurrentHashMap<>();
|
||||
private final ConcurrentMap<String, BrowserWindowJDialog> childWindows = new ConcurrentHashMap<>();
|
||||
private final Map<String, Consumer<String>> callbacks = new ConcurrentHashMap<>();
|
||||
|
||||
private WindowRegistry() {}
|
||||
@@ -47,100 +48,91 @@ public class WindowRegistry {
|
||||
}
|
||||
|
||||
public void unregisterWindow(String windowId) {
|
||||
// 先尝试从主窗口移除
|
||||
BrowserWindow window = windows.remove(windowId);
|
||||
if (window != null) {
|
||||
window.closeWindow();
|
||||
return;
|
||||
}
|
||||
// 如果不是主窗口,尝试从子窗口移除
|
||||
BrowserWindowJDialog child = childWindows.remove(windowId);
|
||||
if (child != null) {
|
||||
child.closeWindow();
|
||||
}
|
||||
}
|
||||
|
||||
public BrowserWindow getWindow(String windowId) {
|
||||
return windows.get(windowId);
|
||||
// 泛型获取方法,尝试查找两种类型的窗口
|
||||
public BrowserContainer getWindow(String windowId) {
|
||||
if (windows.containsKey(windowId)) return windows.get(windowId);
|
||||
if (childWindows.containsKey(windowId)) return childWindows.get(windowId);
|
||||
return null;
|
||||
}
|
||||
|
||||
public void update() {
|
||||
for (BrowserWindow window : windows.values()) {
|
||||
if (window != null) {
|
||||
window.updateTheme();
|
||||
}
|
||||
}
|
||||
|
||||
for (BrowserWindowJDialog window : childWindows.values()) {
|
||||
if (window != null) {
|
||||
window.updateTheme();
|
||||
}
|
||||
}
|
||||
windows.values().forEach(BrowserWindow::updateTheme);
|
||||
childWindows.values().forEach(BrowserWindowJDialog::updateTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的窗口
|
||||
* @param windowId 窗口ID
|
||||
* @param config 窗口配置
|
||||
* @return 返回创建的 BrowserWindow 实例
|
||||
*/
|
||||
public void createNewWindow(String windowId, Consumer<BrowserWindow.Builder> config) {
|
||||
public BrowserWindow createNewWindow(String windowId, Consumer<BrowserWindow.Builder> config) {
|
||||
BrowserWindow.Builder builder = new BrowserWindow.Builder(windowId);
|
||||
config.accept(builder);
|
||||
BrowserWindow window = builder.build();
|
||||
registerWindow(window);
|
||||
|
||||
loadExtLibsPath(window);
|
||||
// 1. 这里只运行配置逻辑,不要在 config 中调用 build()
|
||||
config.accept(builder);
|
||||
|
||||
// 2. 这里统一进行 build,只创建一次实例
|
||||
BrowserWindow window = builder.build();
|
||||
|
||||
registerWindow(window);
|
||||
loadExtLibsPath(window); // 使用统一的接口方法
|
||||
return window; // 返回实例
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的子窗口
|
||||
* @param windowId 窗口ID
|
||||
* @param config 窗口配置
|
||||
* @return 返回创建的 BrowserWindowJDialog 实例
|
||||
*/
|
||||
public void createNewChildWindow(String windowId, Consumer<BrowserWindowJDialog.Builder> config) {
|
||||
public BrowserWindowJDialog createNewChildWindow(String windowId, Consumer<BrowserWindowJDialog.Builder> config) {
|
||||
BrowserWindowJDialog.Builder builder = new BrowserWindowJDialog.Builder(windowId);
|
||||
config.accept(builder);
|
||||
BrowserWindowJDialog window = builder.build();
|
||||
registerChildWindow(window);
|
||||
|
||||
loadExtLibsPath(window);
|
||||
return window;
|
||||
}
|
||||
|
||||
private void loadExtLibsPath(BrowserWindow window) {
|
||||
/**
|
||||
* 统一的注入逻辑,接收 BrowserContainer 接口
|
||||
* 这样 BrowserWindow 和 BrowserWindowJDialog 都可以复用此方法
|
||||
*/
|
||||
private void loadExtLibsPath(BrowserContainer window) {
|
||||
CefBrowser cefBrowser = window.getBrowser();
|
||||
if (cefBrowser == null) return;
|
||||
|
||||
if (cefBrowser != null)
|
||||
|
||||
// 使用 CefClient 的调度方法(如果可用)或直接添加 LoadHandler
|
||||
// 使用 client 注入或者直接 browser 注入
|
||||
// 这里的逻辑只需要写一遍
|
||||
cefBrowser.getClient().addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
if (frame.isMain()) {
|
||||
try {
|
||||
String extLibsPath = FolderCreator.getJavaScriptFolder() + "\\" + "extLibs";
|
||||
String extLibsPath = FolderCreator.getJavaScriptFolder() + File.separator + "extLibs";
|
||||
File extLibsDir = new File(extLibsPath);
|
||||
if (!extLibsDir.exists() || !extLibsDir.isDirectory()) {
|
||||
throw new IOException("extLibs目录无效: " + extLibsPath);
|
||||
}
|
||||
String script = "window.extLibsPath = " + JSONObject.valueToString(extLibsPath) + ";";
|
||||
browser.executeJavaScript(script, frame.getURL(), 0);
|
||||
} catch (Exception e) {
|
||||
System.err.println("注入extLibsPath失败: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
private void loadExtLibsPath(BrowserWindowJDialog window) {
|
||||
CefBrowser cefBrowser = window.getBrowser();
|
||||
// 注意:这里可能需要转义反斜杠,或者直接使用 /
|
||||
String safePath = extLibsPath.replace("\\", "\\\\");
|
||||
|
||||
if (cefBrowser != null)
|
||||
// 使用 CefClient 的调度方法(如果可用)或直接添加 LoadHandler
|
||||
cefBrowser.getClient().addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
if (frame.isMain()) {
|
||||
try {
|
||||
String extLibsPath = FolderCreator.getJavaScriptFolder() + "\\" + "extLibs";
|
||||
File extLibsDir = new File(extLibsPath);
|
||||
if (!extLibsDir.exists() || !extLibsDir.isDirectory()) {
|
||||
throw new IOException("extLibs目录无效: " + extLibsPath);
|
||||
// 仅打印日志,不抛出导致程序崩溃的异常,或者视需求而定
|
||||
System.err.println("extLibs目录不存在: " + extLibsPath);
|
||||
}
|
||||
String script = "window.extLibsPath = " + JSONObject.valueToString(extLibsPath) + ";";
|
||||
|
||||
// 使用 JSON 库处理字符串转义是最安全的
|
||||
String jsonPath = JSONObject.valueToString(extLibsPath);
|
||||
String script = "window.extLibsPath = " + jsonPath + "; console.log('ExtLibs path set:', window.extLibsPath);";
|
||||
|
||||
browser.executeJavaScript(script, frame.getURL(), 0);
|
||||
} catch (Exception e) {
|
||||
System.err.println("注入extLibsPath失败: " + e.getMessage());
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
package com.axis.innovators.box.browser.bridge;
|
||||
|
||||
import com.axis.innovators.box.browser.BrowserCore;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonParser;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.CefMessageRouterHandlerAdapter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JS 桥接控制器基类
|
||||
* 用户继承此类并在其中编写带有 @JsMapping 的方法
|
||||
*/
|
||||
public abstract class JsBridgeController {
|
||||
private final Map<String, Method> methodRegistry = new HashMap<>();
|
||||
private final Gson gson = new Gson();
|
||||
private BrowserCore browserCore;
|
||||
|
||||
public JsBridgeController() {
|
||||
scanMethods();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定 BrowserCore,用于执行回调或JS注入
|
||||
*/
|
||||
public void attach(BrowserCore core) {
|
||||
this.browserCore = core;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描子类中所有带 @JsMapping 的方法
|
||||
*/
|
||||
private void scanMethods() {
|
||||
Method[] methods = this.getClass().getDeclaredMethods();
|
||||
for (Method method : methods) {
|
||||
if (method.isAnnotationPresent(JsMapping.class)) {
|
||||
JsMapping annotation = method.getAnnotation(JsMapping.class);
|
||||
String path = annotation.value();
|
||||
|
||||
// 默认规则:如果注解为空,使用 tzd.<方法名>
|
||||
if (path.isEmpty()) {
|
||||
path = "tzd." + method.getName();
|
||||
}
|
||||
// 如果没有包含点且不为空,默认为 tzd.<value>
|
||||
else if (!path.contains(".")) {
|
||||
path = "tzd." + path;
|
||||
}
|
||||
// 否则直接使用定义的全路径 (如 "ggg.ddd")
|
||||
|
||||
methodRegistry.put(path, method);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理来自 CEF 的路由消息
|
||||
* 格式协议: "jsbridge:methodPath:[argsJson]"
|
||||
*/
|
||||
public boolean handleQuery(CefBrowser browser, CefFrame frame, long queryId, String request, boolean persistent, CefQueryCallback callback) {
|
||||
if (!request.startsWith("jsbridge:")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 解析请求:jsbridge:tzd.test:[1, "abc"]
|
||||
try {
|
||||
String content = request.substring("jsbridge:".length());
|
||||
int firstColon = content.indexOf(":");
|
||||
if (firstColon == -1) return false;
|
||||
|
||||
String methodPath = content.substring(0, firstColon);
|
||||
String argsJson = content.substring(firstColon + 1);
|
||||
|
||||
Method method = methodRegistry.get(methodPath);
|
||||
if (method == null) {
|
||||
callback.failure(404, "Method not found: " + methodPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 参数反序列化与调用
|
||||
Object[] args = parseArgs(method, argsJson);
|
||||
Object result = method.invoke(this, args);
|
||||
|
||||
// 返回结果
|
||||
String response = result != null ? gson.toJson(result) : "null";
|
||||
callback.success(response);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
callback.failure(500, "Java execution error: " + e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据方法签名将 JSON 数组转为 Java 对象数组
|
||||
*/
|
||||
private Object[] parseArgs(Method method, String jsonArrayStr) {
|
||||
if (jsonArrayStr == null || jsonArrayStr.isEmpty()) return new Object[0];
|
||||
|
||||
JsonArray jsonArray = JsonParser.parseString(jsonArrayStr).getAsJsonArray();
|
||||
Class<?>[] paramTypes = method.getParameterTypes();
|
||||
|
||||
if (jsonArray.size() != paramTypes.length) {
|
||||
throw new IllegalArgumentException("Argument count mismatch");
|
||||
}
|
||||
|
||||
Object[] args = new Object[paramTypes.length];
|
||||
for (int i = 0; i < paramTypes.length; i++) {
|
||||
args[i] = gson.fromJson(jsonArray.get(i), paramTypes[i]);
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成注入到前端的 JavaScript 代码
|
||||
* 自动构建对象层级(如 var tzd = {}; tzd.ggg = {}; ...)
|
||||
*/
|
||||
public String generateInjectionJs() {
|
||||
StringBuilder js = new StringBuilder();
|
||||
js.append("(function() {");
|
||||
js.append(" function _javaCall(path, args) {");
|
||||
js.append(" return new Promise(function(resolve, reject) {");
|
||||
js.append(" window.javaQuery({");
|
||||
js.append(" request: 'jsbridge:' + path + ':' + JSON.stringify(args),");
|
||||
js.append(" onSuccess: function(res) { resolve(JSON.parse(res)); },");
|
||||
js.append(" onFailure: function(code, msg) { reject(msg); }");
|
||||
js.append(" });");
|
||||
js.append(" });");
|
||||
js.append(" };");
|
||||
js.append(" function _ensureNs(ns) {");
|
||||
js.append(" var parts = ns.split('.'); var root = window;");
|
||||
js.append(" for(var i=0; i<parts.length; i++) {");
|
||||
js.append(" if(!root[parts[i]]) root[parts[i]] = {};");
|
||||
js.append(" root = root[parts[i]];");
|
||||
js.append(" }");
|
||||
js.append(" return root;");
|
||||
js.append(" };");
|
||||
|
||||
for (String path : methodRegistry.keySet()) {
|
||||
int lastDot = path.lastIndexOf('.');
|
||||
String ns = (lastDot > -1) ? path.substring(0, lastDot) : "window";
|
||||
String funcName = (lastDot > -1) ? path.substring(lastDot + 1) : path;
|
||||
|
||||
// 生成代码:构建命名空间并挂载函数
|
||||
if (!"window".equals(ns)) {
|
||||
js.append(String.format(" _ensureNs('%s');", ns));
|
||||
}
|
||||
|
||||
// 挂载函数代理
|
||||
js.append(String.format(" window.%s = window.%s || {};", ns, ns)); // 安全检查
|
||||
js.append(String.format(" %s.%s = function() {", (ns.equals("window") ? "window" : ns), funcName));
|
||||
js.append(String.format(" return _javaCall('%s', Array.prototype.slice.call(arguments));", path));
|
||||
js.append(" };");
|
||||
}
|
||||
|
||||
js.append("})();");
|
||||
return js.toString();
|
||||
}
|
||||
|
||||
protected BrowserCore getBrowserCore() {
|
||||
return browserCore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.axis.innovators.box.browser.bridge;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 标记该方法可以被 JavaScript 调用
|
||||
* <p>
|
||||
* 用法示例:
|
||||
* 1. @JsMapping -> JS: tzd.methodName()
|
||||
* 2. @JsMapping("myFunc") -> JS: tzd.myFunc()
|
||||
* 3. @JsMapping("app.utils.calc") -> JS: app.utils.calc()
|
||||
*/
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.METHOD)
|
||||
public @interface JsMapping {
|
||||
String value() default "";
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.axis.innovators.box.browser.test;
|
||||
|
||||
import com.axis.innovators.box.browser.BrowserWindow;
|
||||
import com.axis.innovators.box.browser.bridge.JsBridgeController;
|
||||
import com.axis.innovators.box.browser.bridge.JsMapping;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.io.File;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 测试启动入口
|
||||
*/
|
||||
public class BrowserTestMain {
|
||||
|
||||
public static void main(String[] args) {
|
||||
String htmlPath = new File("C:\\Users\\Administrator\\MCreatorWorkspaces\\AxisInnovatorsBox\\src\\main\\java\\com\\axis\\innovators\\box\\browser\\test\\test_bridge.html").getAbsolutePath();
|
||||
System.setOut(new PrintStream(System.out, true, StandardCharsets.UTF_8));
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
new BrowserWindow.Builder("test_window_001")
|
||||
.title("JSBridge 功能测试")
|
||||
.size(1024, 768)
|
||||
.htmlPath(htmlPath)
|
||||
.build().setController(new MyTestController());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义业务控制器
|
||||
*/
|
||||
public static class MyTestController extends JsBridgeController {
|
||||
|
||||
/**
|
||||
* 1. 默认映射
|
||||
* JS 调用: tzd.getJavaTime()
|
||||
*/
|
||||
@JsMapping
|
||||
public String getJavaTime() {
|
||||
return "Java Time: " + new Date().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 2. 自定义方法名
|
||||
* JS 调用: tzd.hello("World")
|
||||
*/
|
||||
@JsMapping("hello")
|
||||
public String sayHello(String name) {
|
||||
System.out.println("Java 收到名字: " + name);
|
||||
return "你好, " + name + "! (来自 Java)";
|
||||
}
|
||||
|
||||
/**
|
||||
* 3. 自定义深层路径
|
||||
* JS 调用: sys.math.add(10, 20)
|
||||
*/
|
||||
@JsMapping("sys.math.add")
|
||||
public int add(int a, int b) {
|
||||
System.out.println("Java 计算: " + a + " + " + b);
|
||||
return a + b;
|
||||
}
|
||||
|
||||
/**
|
||||
* 4. 操作浏览器窗口
|
||||
* JS 调用: win.ctrl.setTitle("新标题")
|
||||
*/
|
||||
@JsMapping("win.ctrl.setTitle")
|
||||
public void setWindowTitle(String title) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
Window window = SwingUtilities.getWindowAncestor(getBrowserCore().getBrowser().getUIComponent());
|
||||
if (window instanceof JFrame) {
|
||||
((JFrame) window).setTitle(title);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>JSBridge 测试页面</title>
|
||||
<style>
|
||||
body { font-family: 'Segoe UI', sans-serif; padding: 20px; background-color: #f4f4f9; }
|
||||
h2 { color: #333; }
|
||||
.card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); margin-bottom: 20px; }
|
||||
button {
|
||||
padding: 10px 20px; margin: 5px; cursor: pointer;
|
||||
background-color: #007bff; color: white; border: none; border-radius: 4px; font-size: 14px;
|
||||
}
|
||||
button:hover { background-color: #0056b3; }
|
||||
button.action { background-color: #28a745; }
|
||||
#console {
|
||||
background: #2d2d2d; color: #00ff00; padding: 15px;
|
||||
height: 300px; overflow-y: auto; border-radius: 4px; font-family: monospace;
|
||||
}
|
||||
.log-line { margin: 5px 0; border-bottom: 1px solid #444; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h2>Java <-> JavaScript 交互测试</h2>
|
||||
|
||||
<div class="card">
|
||||
<h3>1. 基础调用 (tzd 命名空间)</h3>
|
||||
<button onclick="testGetTime()">获取 Java 时间 (tzd.getJavaTime)</button>
|
||||
<button onclick="testHello()">发送问候 (tzd.hello)</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>2. 自定义路径与计算</h3>
|
||||
<button onclick="testMath()">计算 15 + 25 (sys.math.add)</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>3. 控制宿主窗口</h3>
|
||||
<button class="action" onclick="changeTitle()">修改窗口标题 (win.ctrl.setTitle)</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>运行日志</h3>
|
||||
<div id="console"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 日志辅助函数
|
||||
function log(msg) {
|
||||
const consoleDiv = document.getElementById('console');
|
||||
const line = document.createElement('div');
|
||||
line.className = 'log-line';
|
||||
line.innerText = `[${new Date().toLocaleTimeString()}] ${msg}`;
|
||||
consoleDiv.appendChild(line);
|
||||
consoleDiv.scrollTop = consoleDiv.scrollHeight;
|
||||
}
|
||||
|
||||
// --- 测试用例 ---
|
||||
|
||||
async function testGetTime() {
|
||||
try {
|
||||
// 等待 Java 返回结果
|
||||
log("调用 tzd.getJavaTime()...");
|
||||
let time = await tzd.getJavaTime();
|
||||
log("✅ Java 返回: " + time);
|
||||
} catch (e) {
|
||||
log("❌ 错误: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
async function testHello() {
|
||||
try {
|
||||
log("调用 tzd.hello('Web User')...");
|
||||
let response = await tzd.hello("Web User");
|
||||
log("✅ Java 返回: " + response);
|
||||
} catch (e) {
|
||||
log("❌ 错误: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
async function testMath() {
|
||||
try {
|
||||
let a = 15, b = 25;
|
||||
log(`调用 sys.math.add(${a}, ${b})...`);
|
||||
// 注意:Controller 定义了 @JsMapping("sys.math.add")
|
||||
let result = await sys.math.add(a, b);
|
||||
log("✅ 计算结果: " + result);
|
||||
} catch (e) {
|
||||
log("❌ 错误: " + e);
|
||||
}
|
||||
}
|
||||
|
||||
async function changeTitle() {
|
||||
try {
|
||||
let newTitle = "标题被 JS 修改了 - " + Math.floor(Math.random() * 100);
|
||||
log("调用 win.ctrl.setTitle...");
|
||||
await win.ctrl.setTitle(newTitle);
|
||||
log("✅ 标题已修改为: " + newTitle);
|
||||
} catch (e) {
|
||||
log("❌ 错误: " + e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -11,6 +11,7 @@ import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.browser.CefMessageRouter;
|
||||
import org.cef.callback.CefQueryCallback;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.cef.handler.CefMessageRouterHandlerAdapter;
|
||||
import org.json.JSONObject;
|
||||
|
||||
@@ -37,23 +38,20 @@ public class TerminalManager {
|
||||
* 启动真实的终端窗口
|
||||
*/
|
||||
public static void popupRealLinuxWindow() {
|
||||
AtomicReference<BrowserWindow> window = new AtomicReference<>();
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 1. 创建窗口
|
||||
WindowRegistry.getInstance().createNewWindow("real_terminal", builder ->
|
||||
window.set(builder.title("Terminal Linux")
|
||||
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("real_terminal", builder -> {
|
||||
builder.title("Terminal Linux")
|
||||
.size(900, 600)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "LinuxTerminal.html")
|
||||
.operationHandler(createOperationHandler())
|
||||
.build())
|
||||
);
|
||||
.operationHandler(createOperationHandler());
|
||||
});
|
||||
|
||||
// 2. 初始化 PTY 进程
|
||||
startPtyProcess(window.get().getBrowser());
|
||||
startPtyProcess(window.getBrowser());
|
||||
|
||||
// 3. 注册 JCEF 消息处理器
|
||||
CefMessageRouter msgRouter = window.get().getMsgRouter();
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter != null) {
|
||||
msgRouter.addHandler(new CefMessageRouterHandlerAdapter() {
|
||||
@Override
|
||||
@@ -62,10 +60,8 @@ public class TerminalManager {
|
||||
try {
|
||||
JSONObject json = new JSONObject(request);
|
||||
if ("terminalInput".equals(json.optString("type"))) {
|
||||
// 接收前端的按键数据
|
||||
String data = json.getString("data");
|
||||
if (ptyInput != null) {
|
||||
// 写入到 Shell 进程的标准输入
|
||||
ptyInput.write(data.getBytes(StandardCharsets.UTF_8));
|
||||
ptyInput.flush();
|
||||
}
|
||||
@@ -77,14 +73,20 @@ public class TerminalManager {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {}
|
||||
}, true);
|
||||
}
|
||||
|
||||
// 窗口关闭时杀死进程
|
||||
window.get().addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
window.getBrowser().getClient().addLoadHandler(new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser browser, CefFrame frame, int httpStatusCode) {
|
||||
if (frame.isMain()) {
|
||||
System.out.println("页面加载完成,开始初始化终端逻辑...");
|
||||
startPtyProcess(browser);
|
||||
}
|
||||
}
|
||||
});
|
||||
window.addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(java.awt.event.WindowEvent windowEvent) {
|
||||
stopPtyProcess();
|
||||
|
||||
@@ -45,21 +45,15 @@ public class ModernJarViewer {
|
||||
// --- CEF Window Initialization ---
|
||||
// 修改:增加 jarPath 参数
|
||||
public static void popupSimulatingWindow(JFrame parent, String jarPath) {
|
||||
AtomicReference<BrowserWindow> windowRef = new AtomicReference<>();
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 1. 创建浏览器窗口
|
||||
WindowRegistry.getInstance().createNewWindow("main", builder -> {
|
||||
BrowserWindow window = builder.title("Java Decompiler Pro")
|
||||
BrowserWindow window = WindowRegistry.getInstance().createNewWindow("main", builder -> {
|
||||
builder.title("Java Decompiler Pro")
|
||||
.size(1400, 900)
|
||||
.htmlPath(FolderCreator.getJavaScriptFolder() + File.separator + "HtmlJarViewer.html")
|
||||
.operationHandler(new WindowOperationHandler.Builder().withDefaultOperations().build())
|
||||
.build();
|
||||
windowRef.set(window);
|
||||
.operationHandler(new WindowOperationHandler.Builder().withDefaultOperations().build());
|
||||
});
|
||||
|
||||
// 2. 配置消息路由 (JS -> Java Bridge)
|
||||
BrowserWindow window = windowRef.get();
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
|
||||
if (msgRouter != null) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Auto-generated build information
|
||||
version=0.0.1
|
||||
buildTimestamp=2026-01-02T18:36:04.5818254
|
||||
buildTimestamp=2026-01-03T08:33:49.8039508
|
||||
buildSystem=WINDOWS
|
||||
|
||||
Reference in New Issue
Block a user