Compare commits
59 Commits
52231cc
...
7e97da60ff
| Author | SHA1 | Date | |
|---|---|---|---|
| 7e97da60ff | |||
|
|
0ad6835fed | ||
|
|
c5097f91be | ||
|
|
5c66838b3e | ||
|
|
e06c59c8d1 | ||
|
|
a725e7eb23 | ||
|
|
f2cb74379e | ||
|
|
401263cd2b | ||
|
|
71aa2b8699 | ||
|
|
43aab9f0fd | ||
|
|
5775bc5d7e | ||
|
|
3add504321 | ||
|
|
a9c2d202d3 | ||
|
|
1f5752257e | ||
|
|
cdc0843174 | ||
|
|
331d836d62 | ||
|
|
d2bb534d26 | ||
|
|
210ac72a38 | ||
|
|
7ac960be5e | ||
|
|
2278c5d0c7 | ||
|
|
fec5de1276 | ||
|
|
6a3eb89aaf | ||
|
|
b3c50ca794 | ||
|
|
879069a9f4 | ||
|
|
27744d4b5c | ||
|
|
1bc2634afb | ||
|
|
082478cdb6 | ||
|
|
b501da0254 | ||
|
|
fb1db942ed | ||
|
|
22c3661d6e | ||
|
|
16af846e48 | ||
|
|
9cde0192fd | ||
|
|
22af92cd84 | ||
|
|
424c00ede9 | ||
|
|
becf789cb8 | ||
|
|
52ed33b5c8 | ||
|
|
173c30f277 | ||
|
|
3cf7f5883c | ||
|
|
1e0aa62ca8 | ||
|
|
efc73c935d | ||
|
|
9eede23a94 | ||
|
|
8f40542ab0 | ||
|
|
167bf6405f | ||
|
|
adf659853d | ||
|
|
f24e78ab95 | ||
|
|
000ab3488b | ||
|
|
d254e57e1f | ||
|
|
3d3b626c73 | ||
|
|
86a9e9e81d | ||
|
|
75f765bb47 | ||
|
|
0ec498f6eb | ||
|
|
e6df4fe4b2 | ||
|
|
b23662b861 | ||
|
|
5df14e353a | ||
|
|
a30c306cf1 | ||
|
|
628389150c | ||
|
|
2904258983 | ||
|
|
ba5c07746a | ||
|
|
37ef4029b4 |
8
.gitignore
vendored
8
.gitignore
vendored
@@ -40,3 +40,11 @@ bin/
|
||||
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
|
||||
### logs ###
|
||||
*.log
|
||||
logs/
|
||||
|
||||
### JCEF Dlls ###
|
||||
library/*/
|
||||
|
||||
|
||||
2
.idea/gradle.xml
generated
2
.idea/gradle.xml
generated
@@ -5,7 +5,7 @@
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleHome" value="" />
|
||||
<option name="gradleJvm" value="corretto-20" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
|
||||
662
README.md
Normal file
662
README.md
Normal file
@@ -0,0 +1,662 @@
|
||||
# AxisInnovatorsBoxWindowApi
|
||||
|
||||
[项目链接](https://gitea.nimblenexa.cn/lanxi/window-axis-innovators-box) |
|
||||
[官网](https://box.nimblenexa.cn)
|
||||
| 简体中文
|
||||
|
||||
---
|
||||
|
||||
## 项目概述
|
||||
|
||||
`AxisInnovatorsBoxWindowApi` 是一个为 **AxisInnovatorsBox** 平台设计的管理API接口库,开发者可通过此API创建自定义插件,实现窗口管理、事件交互等核心功能。该仓库提供了接口定义、类说明文档及插件开发示例代码,帮助开发者快速接入AxisInnovatorsBox生态系统。
|
||||
|
||||
---
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🖥️ **窗口生命周期管理** - 创建/销毁窗口、调整窗口状态(最小化/最大化)
|
||||
- 🎮 **事件驱动交互** - 支持窗口事件监听与自定义事件触发
|
||||
- 📦 **跨语言插件支持** - 基于 Java 平台无缝加载 Python 插件,提供标准插件基类(Java/Python)实现快速扩展
|
||||
- 📄 **动态配置管理** - 通过 properties 配置文件灵活加载多语言插件(支持 Java/Python 插件声明)
|
||||
- 📊 **统一日志追踪** - 集成 Java 平台日志系统,同步记录 Python 插件的运行状态与异常信息
|
||||
|
||||
---
|
||||
|
||||
## 插件加载系统说明
|
||||
- 插件加载系统核心组件。
|
||||
- 插件加载系统由 **程序内部** 完成
|
||||
|
||||
### 注册Jar插件
|
||||
- Jar插件在/plug-in中添加
|
||||
```java
|
||||
@PluginMeta(id = "test", name = "测试插件",
|
||||
supportedVersions = {"0.0.2"},
|
||||
description = "测试插件",
|
||||
icon = "",
|
||||
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", "测试插件");
|
||||
event.main().getRegistrationTool().addToolCategory(
|
||||
category,
|
||||
INSTANCE,
|
||||
"templatePlugin"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
- 插件加载系统会自动填充 **INSTANCE** 内容
|
||||
- 使用PluginMeta注册插件信息
|
||||
|
||||
### Python插件注册
|
||||
- Python插件在/plug-in/python中添加
|
||||
- Python可以直接调用Java类实现对插件系统的控制
|
||||
- 插件还需要单独的放在一个子文件夹中,如/plug-in/python/Examples
|
||||
- Python插件需要声明一个metadata.json文件,如:
|
||||
```json
|
||||
{
|
||||
"id": "testing",
|
||||
"name": "测试",
|
||||
"version": "0.0.1",
|
||||
"description": "测试插件",
|
||||
"author": "tzdwindows 7",
|
||||
"dependencies": [],
|
||||
"_comment": {
|
||||
"warning": "本文件为插件元数据配置,修改后需重启应用生效",
|
||||
"path": "插件资源应放置在plugins/{id}/目录下"
|
||||
}
|
||||
}
|
||||
```
|
||||
- Python插件需要声明一个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())
|
||||
|
||||
def onStartup():
|
||||
"""
|
||||
系统启动时自动执行的初始化逻辑
|
||||
功能:
|
||||
1. 创建工具类别
|
||||
2. 创建工具项并绑定动作
|
||||
3. 注册到系统全局工具集
|
||||
"""
|
||||
print('[INFO] 正在初始化自定义工具...')
|
||||
|
||||
# --------------------------
|
||||
# 创建工具类别(参数顺序:显示名称,图标资源名,描述)
|
||||
# --------------------------
|
||||
tool_category = PyLocalSide.getToolCategory(
|
||||
u"数据分析工具", # 显示名称(GUI可见)
|
||||
u"analytics_icon.png", # 图标文件名(需存在于资源目录)
|
||||
u"高级数据分析功能集合" # 悬停提示描述
|
||||
)
|
||||
|
||||
# --------------------------
|
||||
# 创建工具项(参数顺序:显示名称,图标,描述,ID,动作对象)
|
||||
# --------------------------
|
||||
tool_action = MyAction()
|
||||
tool_item = PyLocalSide.getToolItem(
|
||||
u"数据可视化", # 工具项显示名称
|
||||
u"chart_icon.png", # 工具项图标
|
||||
u"生成交互式数据图表", # 工具项描述
|
||||
1001, # 工具项唯一ID(需在配置中统一管理)
|
||||
tool_action # 点击触发的动作
|
||||
)
|
||||
tool_category.addTool(tool_item)
|
||||
|
||||
# --------------------------
|
||||
# 注册工具类别到系统(参数:类别对象,全局唯一注册名称)
|
||||
# --------------------------
|
||||
PyLocalSide.addToolCategory(
|
||||
tool_category,
|
||||
u"custom_module::data_analysis_tools" # 推荐命名规则:模块名::功能名
|
||||
)
|
||||
print('[SUCCESS] 工具类别注册成功')
|
||||
|
||||
if __name__ == '__main__':
|
||||
result = 0
|
||||
errorResult = ""
|
||||
|
||||
# 确保Jython运行时可以访问onStartup函数
|
||||
# 原理:将函数显式绑定到全局字典
|
||||
globals()['onStartup'] = onStartup
|
||||
```
|
||||
|
||||
### 声明CorePlugins
|
||||
* CorePlugins核心组件可以修改部分模块的字节码。
|
||||
* CorePlugins需要在jar的属性中添加 **CorePlugins: CorePlugins类位置**
|
||||
* 自动化构建,在build.gradle中添加如下代码:
|
||||
```groovy
|
||||
jar {
|
||||
manifest {
|
||||
attributes 'CorePlugin': 'com.axis.core.template.TemplateLoadingCorePlugin'
|
||||
}
|
||||
}
|
||||
```
|
||||
* CorePlugins核心组件需要实现 **CorePlugins** 接口,如:
|
||||
```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();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getASMTransformerClass() {
|
||||
// 返回字节码转换器类名
|
||||
return new String[]{TemplateTransformer.class.getName()};
|
||||
}
|
||||
}
|
||||
```
|
||||
* IClassTransformer的实现,如:
|
||||
```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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 事件系统说明
|
||||
- 事件驱动架构核心组件。
|
||||
- 事件总线系统,支持跨模块通信。
|
||||
- 事件总线由 **EventBus & GlobalEventBus** 实现
|
||||
|
||||
### 1.. EventBus & GlobalEventBus 说明
|
||||
应用程序内的事件驱动架构核心组件:
|
||||
|
||||
```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. 定义事件类型
|
||||
public class UserLoginEvent {
|
||||
private final String username;
|
||||
private boolean cancelled;
|
||||
|
||||
public UserLoginEvent(String username) {
|
||||
this.username = username;
|
||||
}
|
||||
|
||||
// Getter/Setter...
|
||||
}
|
||||
|
||||
// 2. 创建监听器类
|
||||
public class SecurityLogger {
|
||||
@SubscribeEvent
|
||||
public void logLoginAttempt(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.所有系统所支持的事件
|
||||
- `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属性事件
|
||||
|
||||
## HTML窗口集成指南
|
||||
|
||||
我们实现了一套高性能的HTML渲染系统,通过Java Chromium Embedded Framework (JCEF) 将HTML内容无缝集成到Java桌面应用中,底层基于[jcefmaven](https://github.com/jcefmaven/jcefmaven)项目。
|
||||
|
||||
### 核心实现步骤
|
||||
|
||||
#### 1. 创建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());
|
||||
});
|
||||
|
||||
// 配置消息路由
|
||||
configureMessageRouter(htmlWindow.get());
|
||||
});
|
||||
```
|
||||
|
||||
#### 2. 辅助方法
|
||||
```java
|
||||
// 获取应用图标
|
||||
private Image getApplicationIcon() {
|
||||
return new ImageIcon(Objects.requireNonNull(
|
||||
MainApplication.class.getClassLoader()
|
||||
.getResource("icons/logo.png")
|
||||
)).getImage();
|
||||
}
|
||||
|
||||
// 获取HTML资源路径
|
||||
private String getHtmlResourcePath() {
|
||||
return FolderCreator.getJavaScriptFolder() + "/AIaToolbox_dark.html";
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 配置消息路由器
|
||||
```java
|
||||
private void configureMessageRouter(BrowserWindowJDialog window) {
|
||||
CefMessageRouter msgRouter = window.getMsgRouter();
|
||||
if (msgRouter == null) return;
|
||||
|
||||
msgRouter.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; // 表示已处理该请求
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) {
|
||||
// 处理请求取消逻辑
|
||||
System.out.println("请求被取消: " + queryId);
|
||||
}
|
||||
}, true); // true表示优先处理
|
||||
}
|
||||
```
|
||||
|
||||
## HTML事件
|
||||
HTML窗口内可以捕捉到一些Java的事件
|
||||
|
||||
| 事件名 | 介绍 | 触发时机 |
|
||||
|--------|------------|---------------------|
|
||||
| `javaFontsLoaded` | Java字体加载完成 | Java字体信息传输到HTML时,或在在更新主题时 |
|
||||
| `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();
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 核心类说明
|
||||
|
||||
### 1. AxisInnovatorsBox
|
||||
窗口实例的核心操作类,提供以下功能:
|
||||
- `getMain()`: 获取当前AxisInnovatorsBox实例
|
||||
- `getMainWindow()`: 获取主窗口实例
|
||||
- `quit()`: 退出程序
|
||||
- `organizingCrashReports(Exception)`: 组织崩溃报告,用于在应用程序发生异常时生成崩溃报告。
|
||||
- `popupWindow(WindowsJDialog)`: 弹出新的窗口,并将其添加到窗口列表中。
|
||||
- `isWindowStartup(WindowsJDialog)`: 判断指定的窗口是否已经启动。
|
||||
- `clearWindow(WindowsJDialog)`: 清除指定的窗口,并将其从窗口列表中移除。
|
||||
- `reloadAllWindow()`: 重新加载窗口。
|
||||
- `getRegistrationTool()`: 获取注册工具实例。
|
||||
- `getArgs()`: 获取命令行参数。
|
||||
- `isWindow()`: 判断窗口是否已经启动。
|
||||
- `getVersion()`: 获取应用程序的版本号。
|
||||
- `getRegistrationTopic()`: 获取注册主题实例。
|
||||
- `getAuthor()`: 获取应用程序的作者信息。
|
||||
- `getStateManager()`: 获取状态管理器实例。
|
||||
|
||||
### 2. RegistrationTool
|
||||
负责在 **应用程序启动阶段** 注册和管理工具分类的核心组件,具备插件系统集成能力:
|
||||
|
||||
```java
|
||||
package com.axis.innovators.box.register;
|
||||
|
||||
/**
|
||||
* 工具分类注册中心(窗口启动前必须完成注册)
|
||||
*/
|
||||
public class RegistrationTool {
|
||||
// 构造方法关联主程序实例
|
||||
public RegistrationTool(AxisInnovatorsBox main) { ... }
|
||||
|
||||
// 核心功能方法
|
||||
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。
|
||||
|
||||
#### 注册示例
|
||||
```java
|
||||
// 创建调试工具分类
|
||||
MainWindow.ToolCategory debugCategory = new MainWindow.ToolCategory(
|
||||
"逆向分析工具",
|
||||
"icons/debugger.png",
|
||||
"二进制逆向分析工具集"
|
||||
);
|
||||
|
||||
// 添加工具项(带点击事件)
|
||||
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)`: 通过标识符获取语言包。
|
||||
|
||||
234
build.gradle
234
build.gradle
@@ -1,3 +1,5 @@
|
||||
import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform
|
||||
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'application'
|
||||
@@ -13,6 +15,7 @@ configurations {
|
||||
proguardLib
|
||||
}
|
||||
|
||||
|
||||
// JDK 版本检查
|
||||
def requiredJavaVersion = 20
|
||||
def currentJavaVersion = JavaVersion.current().majorVersion.toInteger()
|
||||
@@ -30,92 +33,169 @@ repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
//tasks.named("bootJar") {
|
||||
// enabled = false
|
||||
//}
|
||||
|
||||
dependencies {
|
||||
// === 构建工具 ===
|
||||
proguardLib files('libs/proguard.jar')
|
||||
|
||||
// === 测试框架 ===
|
||||
testImplementation platform('org.junit:junit-bom:5.10.0')
|
||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||
implementation 'org.commonmark:commonmark:0.24.0'
|
||||
implementation 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.1'
|
||||
implementation 'com.google.code.gson:gson:2.8.9'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
|
||||
// === 开发工具 ===
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
|
||||
// === 本地库文件 ===
|
||||
implementation files('libs/JNC-1.0-jnc.jar')
|
||||
implementation files('libs/dog api 1.3.jar')
|
||||
implementation files('libs/DesktopWallpaperSdk-1.0-SNAPSHOT.jar')
|
||||
|
||||
// === DJL API ===
|
||||
implementation platform('ai.djl:bom:0.35.0')
|
||||
implementation 'ai.djl:api'
|
||||
implementation 'ai.djl:model-zoo'
|
||||
implementation 'ai.djl.pytorch:pytorch-model-zoo:0.35.0'
|
||||
implementation 'ai.djl.pytorch:pytorch-engine'
|
||||
implementation 'ai.djl:basicdataset'
|
||||
implementation 'ai.djl.onnxruntime:onnxruntime-engine'
|
||||
runtimeOnly 'ai.djl.pytorch:pytorch-native-cpu:2.7.1'
|
||||
runtimeOnly 'ai.djl.onnxruntime:onnxruntime-native-cpu:1.3.0'
|
||||
// === 核心工具库 ===
|
||||
implementation 'com.google.code.gson:gson:2.10.1' // 统一版本
|
||||
implementation 'org.apache.logging.log4j:log4j-api:2.20.0'
|
||||
implementation 'org.apache.logging.log4j:log4j-core:2.20.0'
|
||||
implementation 'org.apache.logging.log4j:log4j-slf4j-impl:2.20.0'
|
||||
implementation 'commons-io:commons-io:2.18.0' // 统一版本
|
||||
implementation 'com.google.guava:guava:31.1-jre'
|
||||
implementation 'net.java.dev.jna:jna:5.13.0'
|
||||
implementation 'net.java.dev.jna:jna-platform:5.13.0'
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'org.apache.commons:commons-compress:1.23.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
|
||||
|
||||
// === 字节码操作 ===
|
||||
implementation 'org.ow2.asm:asm:9.7.1'
|
||||
implementation 'org.ow2.asm:asm-commons:9.7.1'
|
||||
implementation 'org.ow2.asm:asm-analysis:9.7.1'
|
||||
implementation 'org.ow2.asm:asm-util:9.7.1'
|
||||
implementation 'org.ow2.asm:asm-tree:9.7.1'
|
||||
implementation 'net.bytebuddy:byte-buddy:1.17.6'
|
||||
|
||||
// === 反编译工具 ===
|
||||
implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0' // 统一版本
|
||||
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0' // 统一版本
|
||||
implementation 'org.benf:cfr:0.152'
|
||||
|
||||
// === Java 解析与分析 ===
|
||||
implementation 'com.github.javaparser:javaparser-core:3.25.1'
|
||||
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.25.9'
|
||||
|
||||
// === 文本处理 ===
|
||||
implementation 'org.commonmark:commonmark:0.24.0'
|
||||
implementation 'org.commonjava.googlecode.markdown4j:markdown4j:2.2-cj-1.1'
|
||||
implementation 'com.vladsch.flexmark:flexmark:0.64.8'
|
||||
|
||||
// === Web 和网络 ===
|
||||
implementation 'org.jsoup:jsoup:1.17.2'
|
||||
implementation 'commons-io:commons-io:2.14.0'
|
||||
implementation 'org.json:json:20231013' // 统一版本
|
||||
implementation 'org.openjfx:javafx-web:17'
|
||||
|
||||
// === UI 框架 ===
|
||||
implementation 'com.formdev:flatlaf:3.2.1'
|
||||
implementation 'com.formdev:flatlaf-extras:3.2.1'
|
||||
implementation 'com.formdev:flatlaf-intellij-themes:3.2.1'
|
||||
implementation 'io.github.vincenzopalazzo:material-ui-swing:1.1.2'
|
||||
implementation 'org.python:jython-standalone:2.7.3'
|
||||
implementation 'org.graalvm.python:python-embedding:24.2.1'
|
||||
implementation files('libs/JNC-1.0-jnc.jar')
|
||||
implementation files('libs/dog api 1.3.jar')
|
||||
implementation files('libs/DesktopWallpaperSdk-1.0-SNAPSHOT.jar')
|
||||
|
||||
// JavaFX
|
||||
implementation 'org.openjfx:javafx-controls:21'
|
||||
implementation 'org.openjfx:javafx-graphics:21'
|
||||
implementation 'org.fxmisc.richtext:richtextfx:0.11.0'
|
||||
implementation 'org.bitbucket.mstrobel:procyon-core:0.5.36'
|
||||
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.5.36'
|
||||
implementation 'org.controlsfx:controlsfx:11.1.2'
|
||||
implementation 'com.dlsc.formsfx:formsfx-core:11.6.0'
|
||||
implementation 'com.dustinredmond.fxtrayicon:FXTrayIcon:4.0.1'
|
||||
|
||||
// === 代码编辑器 ===
|
||||
implementation 'com.fifesoft:rsyntaxtextarea:3.5.4'
|
||||
implementation 'com.fifesoft:rstaui:3.3.1'
|
||||
implementation 'com.fifesoft:languagesupport:3.3.0'
|
||||
implementation 'com.fifesoft:autocomplete:3.3.2'
|
||||
implementation 'org.apache.commons:commons-compress:1.23.0'
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
|
||||
implementation 'org.controlsfx:controlsfx:11.1.2'
|
||||
implementation 'com.dlsc.formsfx:formsfx-core:11.6.0'
|
||||
|
||||
// === 图形和游戏引擎 ===
|
||||
// LWJGL
|
||||
implementation 'org.lwjgl:lwjgl:3.3.6'
|
||||
implementation 'org.lwjgl:lwjgl-stb:3.3.6'
|
||||
implementation 'org.lwjgl:lwjgl-glfw:3.3.6'
|
||||
implementation 'org.lwjgl:lwjgl-opengl:3.3.6'
|
||||
implementation 'org.lwjgl:lwjgl-jawt:3.3.5'
|
||||
|
||||
// Lwjgl natives
|
||||
if (DefaultNativePlatform.currentOperatingSystem.isWindows()) {
|
||||
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-windows'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-windows'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-windows'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-windows'
|
||||
} else if (DefaultNativePlatform.currentOperatingSystem.isLinux()) {
|
||||
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-linux'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-linux'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-linux'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-linux'
|
||||
} else if (DefaultNativePlatform.currentOperatingSystem.isMacOsX()) {
|
||||
runtimeOnly 'org.lwjgl:lwjgl:3.3.6:natives-macos'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-glfw:3.3.6:natives-macos'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-opengl:3.3.6:natives-macos'
|
||||
runtimeOnly 'org.lwjgl:lwjgl-stb:3.3.6:natives-macos'
|
||||
}
|
||||
|
||||
// 其他图形库
|
||||
implementation 'com.badlogicgames.gdx:gdx:1.12.1'
|
||||
implementation 'org.joml:joml:1.10.7'
|
||||
implementation 'com.kitfox.svg:svg-salamander:1.0'
|
||||
implementation 'net.sourceforge.plantuml:plantuml:8059'
|
||||
implementation 'com.google.code.gson:gson:2.10.1'
|
||||
implementation 'org.openjfx:javafx-controls:21'
|
||||
implementation 'org.benf:cfr:0.152'
|
||||
implementation 'com.github.javaparser:javaparser-core:3.25.1'
|
||||
implementation 'com.1stleg:jnativehook:2.1.0'
|
||||
implementation 'org.json:json:20230618'
|
||||
implementation 'org.lwjgl:lwjgl:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl-glfw:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl-opengl:3.3.1'
|
||||
implementation 'org.lwjgl:lwjgl:3.3.1:natives-windows'
|
||||
implementation 'org.lwjgl:lwjgl-glfw:3.3.1:natives-windows'
|
||||
implementation 'org.lwjgl:lwjgl-opengl:3.3.1:natives-windows'
|
||||
implementation 'com.twelvemonkeys.imageio:imageio-psd:3.12.0'
|
||||
|
||||
// === 图像处理 ===
|
||||
implementation 'com.madgag:animated-gif-lib:1.4'
|
||||
implementation 'org.bytedeco:javacv-platform:1.5.7'
|
||||
implementation 'org.bytedeco:javacpp-platform:1.5.7'
|
||||
implementation 'com.madgag:animated-gif-lib:1.4'
|
||||
implementation 'org.openjfx:javafx-web:17'
|
||||
runtimeOnly 'com.mysql:mysql-connector-j'
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
implementation 'com.kitfox.svg:svg-salamander:1.0'
|
||||
implementation 'com.vladsch.flexmark:flexmark:0.64.8'
|
||||
|
||||
// === 编程语言支持 ===
|
||||
implementation 'org.python:jython-standalone:2.7.3'
|
||||
implementation 'org.graalvm.python:python-embedding:24.2.1'
|
||||
|
||||
// === 系统交互 ===
|
||||
implementation 'com.github.kwhat:jnativehook:2.2.2'
|
||||
implementation 'com.dustinredmond.fxtrayicon:FXTrayIcon:4.0.1'
|
||||
implementation 'org.openjfx:javafx-graphics:21'
|
||||
implementation 'me.friwi:jcefmaven:122.1.10'
|
||||
implementation 'com.alphacephei:vosk:0.3.45'
|
||||
implementation 'net.java.dev.jna:jna:5.13.0'
|
||||
implementation 'net.java.dev.jna:jna-platform:5.13.0'
|
||||
implementation 'org.apache.commons:commons-math3:3.6.1'
|
||||
implementation 'com.google.guava:guava:31.1-jre'
|
||||
implementation 'com.github.javaparser:javaparser-symbol-solver-core:3.25.9'
|
||||
implementation 'org.bitbucket.mstrobel:procyon-core:0.6.0'
|
||||
implementation 'org.bitbucket.mstrobel:procyon-compilertools:0.6.0'
|
||||
implementation 'com.belerweb:pinyin4j:2.5.1'
|
||||
implementation 'commons-io:commons-io:2.18.0'
|
||||
implementation 'com.1stleg:jnativehook:2.1.0'
|
||||
|
||||
// === 数据库 ===
|
||||
runtimeOnly 'com.mysql:mysql-connector-j'
|
||||
implementation 'mysql:mysql-connector-java:8.0.33'
|
||||
implementation 'com.h2database:h2:2.2.220'
|
||||
implementation 'org.xerial:sqlite-jdbc:3.41.2.1'
|
||||
implementation 'org.postgresql:postgresql:42.6.0'
|
||||
|
||||
// === 音频处理 ===
|
||||
implementation 'jflac:jflac:1.3'
|
||||
implementation 'com.github.axet:TarsosDSP:2.4'
|
||||
implementation 'org.json:json:20231013'
|
||||
implementation 'com.googlecode.soundlibs:mp3spi:1.9.5-1'
|
||||
implementation 'com.googlecode.soundlibs:vorbisspi:1.0.3-2'
|
||||
implementation 'com.googlecode.soundlibs:jorbis:0.0.17-2'
|
||||
|
||||
// === 语音识别 ===
|
||||
implementation 'com.alphacephei:vosk:0.3.45'
|
||||
|
||||
// === 浏览器引擎 ===
|
||||
implementation 'me.friwi:jcefmaven:122.1.10'
|
||||
|
||||
// === 中文处理 ===
|
||||
implementation 'com.belerweb:pinyin4j:2.5.1'
|
||||
|
||||
// === 安全认证 ===
|
||||
implementation 'cn.dev33:sa-token-spring-boot-starter:1.44.0'
|
||||
implementation 'org.casbin:casdoor-java-sdk:1.37.0'
|
||||
}
|
||||
|
||||
configurations.all {
|
||||
configurations.configureEach {
|
||||
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
|
||||
}
|
||||
|
||||
@@ -167,9 +247,9 @@ application {
|
||||
mainClass = 'com.axis.innovators.box.Main'
|
||||
}
|
||||
|
||||
task runClient(type: JavaExec) {
|
||||
group = "application"
|
||||
description = "运行 com.axis.innovators.box.Main"
|
||||
tasks.register('runClient', JavaExec) {
|
||||
group = "run-toolboxProgram"
|
||||
description = "执行工具箱程序"
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
mainClass = "com.axis.innovators.box.Main"
|
||||
jvmArgs = [
|
||||
@@ -177,3 +257,43 @@ task runClient(type: JavaExec) {
|
||||
"-Djava.system.class.loader=com.axis.innovators.box.plugins.BoxClassLoader"
|
||||
]
|
||||
}
|
||||
|
||||
tasks.register('test2DModelLayerPanel', JavaExec) {
|
||||
group = "test-model"
|
||||
description = "运行 2D Model Layer Panel 测试"
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
mainClass = "com.chuangzhou.vivid2D.test.ModelLayerPanelTest"
|
||||
jvmArgs = [
|
||||
"-Dfile.encoding=UTF-8"
|
||||
]
|
||||
}
|
||||
|
||||
tasks.register('testModelRenderLightingTest', JavaExec) {
|
||||
group = "test-model"
|
||||
description = "运行 2D Model 高亮灯光测试"
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
mainClass = "com.chuangzhou.vivid2D.test.ModelRenderLightingTest"
|
||||
jvmArgs = [
|
||||
"-Dfile.encoding=UTF-8"
|
||||
]
|
||||
}
|
||||
|
||||
tasks.register('testModelTest', JavaExec) {
|
||||
group = "test-model"
|
||||
description = "运行 2D Model 保存和完整性测试"
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
mainClass = "com.chuangzhou.vivid2D.test.ModelTest"
|
||||
jvmArgs = [
|
||||
"-Dfile.encoding=UTF-8"
|
||||
]
|
||||
}
|
||||
|
||||
tasks.register('testModelTest2', JavaExec) {
|
||||
group = "test-model"
|
||||
description = "运行 2D Model 物理基准测试"
|
||||
classpath = sourceSets.main.runtimeClasspath
|
||||
mainClass = "com.chuangzhou.vivid2D.test.ModelTest2"
|
||||
jvmArgs = [
|
||||
"-Dfile.encoding=UTF-8"
|
||||
]
|
||||
}
|
||||
0
gradle.properties
Normal file
0
gradle.properties
Normal file
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,6 +1,6 @@
|
||||
#Tue Feb 04 17:20:23 CST 2025
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-8.5-bin.zip
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
@@ -584,6 +584,59 @@
|
||||
ignoreUnescapedHTML: true
|
||||
});
|
||||
|
||||
// 监听Java字体加载事件
|
||||
document.addEventListener('javaFontsLoaded', function(event) {
|
||||
const fontInfo = event.detail;
|
||||
console.log('接收到Java字体信息:', fontInfo);
|
||||
|
||||
// 应用Java字体到界面
|
||||
applyJavaFonts(fontInfo);
|
||||
});
|
||||
|
||||
// 应用Java字体的函数
|
||||
function applyJavaFonts(fontInfo) {
|
||||
const uiFonts = fontInfo.uiFonts || {};
|
||||
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
|
||||
|
||||
if (defaultFont && defaultFont.family) {
|
||||
const fontFamily = defaultFont.family;
|
||||
const fontSize = defaultFont.size || 14;
|
||||
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
|
||||
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
|
||||
|
||||
// 创建字体样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
body, html {
|
||||
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
|
||||
}
|
||||
.message.user .bubble {
|
||||
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
|
||||
}
|
||||
.message.ai .bubble {
|
||||
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
|
||||
font-size: ${fontSize}px !important;
|
||||
}
|
||||
input, button {
|
||||
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
|
||||
}
|
||||
code, pre {
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', 'Courier New', monospace !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// 添加到文档头
|
||||
document.head.appendChild(style);
|
||||
|
||||
console.log('Java字体已应用到DeepSeek界面:', fontFamily, fontSize + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果字体信息已经存在,立即应用
|
||||
if (typeof window.javaFontInfo !== 'undefined') {
|
||||
applyJavaFonts(window.javaFontInfo);
|
||||
}
|
||||
|
||||
// CEF通信桥接
|
||||
window.javaQuery = window.cefQuery ? (request, success, error) => {
|
||||
window.cefQuery({
|
||||
|
||||
@@ -624,6 +624,59 @@
|
||||
);
|
||||
}
|
||||
|
||||
// 监听Java字体加载事件
|
||||
document.addEventListener('javaFontsLoaded', function(event) {
|
||||
const fontInfo = event.detail;
|
||||
console.log('接收到Java字体信息:', fontInfo);
|
||||
|
||||
// 应用Java字体到界面
|
||||
applyJavaFonts(fontInfo);
|
||||
});
|
||||
|
||||
// 应用Java字体的函数
|
||||
function applyJavaFonts(fontInfo) {
|
||||
const uiFonts = fontInfo.uiFonts || {};
|
||||
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
|
||||
|
||||
if (defaultFont && defaultFont.family) {
|
||||
const fontFamily = defaultFont.family;
|
||||
const fontSize = defaultFont.size || 14;
|
||||
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
|
||||
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
|
||||
|
||||
// 创建字体样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
body, html {
|
||||
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
|
||||
}
|
||||
.message.user .bubble {
|
||||
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
|
||||
}
|
||||
.message.ai .bubble {
|
||||
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
|
||||
font-size: ${fontSize}px !important;
|
||||
}
|
||||
input, button {
|
||||
font-family: '${fontFamily}', 'Helvetica Neue', Helvetica, Arial, 'PingFang SC', 'Microsoft YaHei', sans-serif !important;
|
||||
}
|
||||
code, pre {
|
||||
font-family: 'JetBrains Mono', 'Consolas', 'Monaco', 'Courier New', monospace !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// 添加到文档头
|
||||
document.head.appendChild(style);
|
||||
|
||||
console.log('Java字体已应用到DeepSeek界面:', fontFamily, fontSize + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果字体信息已经存在,立即应用
|
||||
if (typeof window.javaFontInfo !== 'undefined') {
|
||||
applyJavaFonts(window.javaFontInfo);
|
||||
}
|
||||
|
||||
function showError(requestId, message) {
|
||||
const stream = streams.get(requestId);
|
||||
if (stream) {
|
||||
|
||||
@@ -122,6 +122,61 @@
|
||||
});
|
||||
});
|
||||
|
||||
// 监听Java字体加载事件
|
||||
document.addEventListener('javaFontsLoaded', function(event) {
|
||||
const fontInfo = event.detail;
|
||||
console.log('接收到Java字体信息:', fontInfo);
|
||||
|
||||
// 应用Java字体到编辑器
|
||||
applyJavaFonts(fontInfo);
|
||||
});
|
||||
|
||||
// 应用Java字体的函数
|
||||
function applyJavaFonts(fontInfo) {
|
||||
const uiFonts = fontInfo.uiFonts || {};
|
||||
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
|
||||
|
||||
if (defaultFont && defaultFont.family) {
|
||||
const fontFamily = defaultFont.family;
|
||||
const fontSize = defaultFont.size || 14;
|
||||
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
|
||||
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
|
||||
|
||||
// 创建字体样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
body, html {
|
||||
font-family: '${fontFamily}', 'JetBrains Mono', 'Consolas', monospace !important;
|
||||
}
|
||||
#output {
|
||||
font-family: '${fontFamily}', 'JetBrains Mono', monospace !important;
|
||||
font-size: ${fontSize}px !important;
|
||||
}
|
||||
#executeBtn {
|
||||
font-family: '${fontFamily}', 'JetBrains Mono', monospace !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// 添加到文档头
|
||||
document.head.appendChild(style);
|
||||
|
||||
// 更新Monaco编辑器字体
|
||||
if (window.editor) {
|
||||
editor.updateOptions({
|
||||
fontFamily: fontFamily,
|
||||
fontSize: fontSize
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Java字体已应用到编辑器:', fontFamily, fontSize + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果字体信息已经存在,立即应用
|
||||
if (typeof window.javaFontInfo !== 'undefined') {
|
||||
applyJavaFonts(window.javaFontInfo);
|
||||
}
|
||||
|
||||
monaco.languages.register({ id: 'c' });
|
||||
|
||||
// C语言关键字配置
|
||||
|
||||
@@ -129,7 +129,60 @@
|
||||
editor.setValue(getDefaultCode(lang));
|
||||
}
|
||||
|
||||
// 监听Java字体加载事件
|
||||
document.addEventListener('javaFontsLoaded', function(event) {
|
||||
const fontInfo = event.detail;
|
||||
console.log('接收到Java字体信息:', fontInfo);
|
||||
|
||||
// 应用Java字体到编辑器
|
||||
applyJavaFonts(fontInfo);
|
||||
});
|
||||
|
||||
// 应用Java字体的函数
|
||||
function applyJavaFonts(fontInfo) {
|
||||
const uiFonts = fontInfo.uiFonts || {};
|
||||
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
|
||||
|
||||
if (defaultFont && defaultFont.family) {
|
||||
const fontFamily = defaultFont.family;
|
||||
const fontSize = defaultFont.size || 14;
|
||||
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
|
||||
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
|
||||
|
||||
// 创建字体样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
body, html {
|
||||
font-family: '${fontFamily}', 'Microsoft YaHei', sans-serif !important;
|
||||
}
|
||||
#output {
|
||||
font-family: '${fontFamily}', 'Microsoft YaHei', monospace !important;
|
||||
font-size: ${fontSize}px !important;
|
||||
}
|
||||
.btn, #language-select, #status {
|
||||
font-family: '${fontFamily}', 'Microsoft YaHei', sans-serif !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// 添加到文档头
|
||||
document.head.appendChild(style);
|
||||
|
||||
// 更新Monaco编辑器字体
|
||||
if (window.editor) {
|
||||
editor.updateOptions({
|
||||
fontFamily: fontFamily,
|
||||
fontSize: fontSize
|
||||
});
|
||||
}
|
||||
|
||||
console.log('Java字体已应用到编辑器:', fontFamily, fontSize + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果字体信息已经存在,立即应用
|
||||
if (typeof window.javaFontInfo !== 'undefined') {
|
||||
applyJavaFonts(window.javaFontInfo);
|
||||
}
|
||||
|
||||
function runCode() {
|
||||
const code = editor.getValue();
|
||||
|
||||
2015
javascript/DatabaseTool.html
Normal file
2015
javascript/DatabaseTool.html
Normal file
File diff suppressed because it is too large
Load Diff
@@ -446,6 +446,62 @@
|
||||
}
|
||||
})();
|
||||
|
||||
// 监听Java字体加载事件
|
||||
document.addEventListener('javaFontsLoaded', function(event) {
|
||||
const fontInfo = event.detail;
|
||||
console.log('接收到Java字体信息:', fontInfo);
|
||||
|
||||
// 应用Java字体到编辑器
|
||||
applyJavaFonts(fontInfo);
|
||||
});
|
||||
|
||||
// 应用Java字体的函数
|
||||
function applyJavaFonts(fontInfo) {
|
||||
const uiFonts = fontInfo.uiFonts || {};
|
||||
const defaultFont = fontInfo.defaultFont || uiFonts['Label.font'] || {};
|
||||
|
||||
if (defaultFont && defaultFont.family) {
|
||||
const fontFamily = defaultFont.family;
|
||||
const fontSize = defaultFont.size || 14;
|
||||
const fontWeight = defaultFont.bold ? 'bold' : 'normal';
|
||||
const fontStyle = defaultFont.italic ? 'italic' : 'normal';
|
||||
|
||||
// 创建字体样式
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
body, html {
|
||||
font-family: '${fontFamily}', 'Fira Code', 'JetBrains Mono', monospace !important;
|
||||
}
|
||||
.toolbar, button, select {
|
||||
font-family: '${fontFamily}', 'Fira Code', 'JetBrains Mono', monospace !important;
|
||||
}
|
||||
.log-item {
|
||||
font-family: '${fontFamily}', 'Fira Code', 'JetBrains Mono', monospace !important;
|
||||
font-size: ${fontSize}px !important;
|
||||
}
|
||||
.CodeMirror {
|
||||
font-family: '${fontFamily}', 'Fira Code', 'JetBrains Mono', monospace !important;
|
||||
font-size: ${fontSize}px !important;
|
||||
}
|
||||
`;
|
||||
|
||||
// 添加到文档头
|
||||
document.head.appendChild(style);
|
||||
|
||||
// 更新CodeMirror编辑器字体
|
||||
if (window.editor) {
|
||||
editor.refresh(); // 刷新编辑器以应用新字体
|
||||
}
|
||||
|
||||
console.log('Java字体已应用到HTML编辑器:', fontFamily, fontSize + 'px');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果字体信息已经存在,立即应用
|
||||
if (typeof window.javaFontInfo !== 'undefined') {
|
||||
applyJavaFonts(window.javaFontInfo);
|
||||
}
|
||||
|
||||
window.javaMessageHandler = {
|
||||
loadContent: (content, fileName) => {
|
||||
editor.setValue(content);
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
#Current Loaded Language
|
||||
#Sat Aug 16 18:11:03 CST 2025
|
||||
#Sun Oct 05 18:45:33 CST 2025
|
||||
loadedLanguage=system\:zh_CN
|
||||
|
||||
@@ -41,6 +41,9 @@ material_oceanic_theme.default.tip=\u57FA\u4E8E MaterialLookAndFeel \u7684\u6D45
|
||||
flatLightLaf_theme.system.topicName=flatLightLaf\u98CE\u683C
|
||||
flatLightLaf_theme.default.tip=flatLightLaf\u98CE\u683C
|
||||
|
||||
blur.system.topicName=\u6A21\u7CCA\u98CE\u683C
|
||||
blur.default.tip=\u6A21\u7CCA\u4E3B\u9898\uFF0C\u9002\u7528\u4E8E\u9AD8\u5BF9\u6BD4\u5EA6\u7684\u4E3B\u9898
|
||||
|
||||
mainWindow.title=\u8F74\u521B\u5DE5\u5177\u7BB1 v1.0
|
||||
mainWindow.title.2=\u8F74\u521B\u5DE5\u5177\u7BB1
|
||||
mainWindow.settings.title=\u7CFB\u7EDF\u8BBE\u7F6E
|
||||
@@ -91,7 +94,7 @@ fridaWindow.settings.font=\u5B57\u4F53
|
||||
fridaWindow.settings.size=\u5927\u5C0F
|
||||
fridaWindow.settings.theme=\u4E3B\u9898
|
||||
fridaWindow.menu.settingsMenu.settingsItem.1=\u8BBE\u7F6E
|
||||
fridaWindow.menu.help.about=\u5173\u4E8E
|
||||
fridaWindow.menu.help.about=\u5173\u4E8E\u6211\u4EEC
|
||||
|
||||
localWindow.newBtn=\u65B0\u5BF9\u8BDD
|
||||
localWindow.saveBtn=\u4FDD\u5B58\u8BB0\u5F55
|
||||
@@ -165,10 +168,10 @@ fridaWindow.font.preview=\u793A\u4F8B\u6587\u672C\uFF1Aconsole.log("Hello Frida"
|
||||
fridaWindow.about.title=\u5173\u4E8E
|
||||
fridaWindow.about.content=\u57FA\u4E8EFrida\u7684\u73B0\u4EE3\u6CE8\u5165\u5DE5\u5177\n\n\u7248\u6743\u6240\u6709 \u00A9 {0}
|
||||
|
||||
settings.1.title=\u63D2\u4EF6
|
||||
settings.1.title=\u63D2\u4EF6\u7BA1\u7406
|
||||
settings.2.title=\u57FA\u7840\u8BBE\u7F6E
|
||||
settings.3.title=\u5173\u4E8E
|
||||
settings.4.title=\u4E3B\u9898
|
||||
settings.3.title=\u5173\u4E8E\u6211\u4EEC
|
||||
settings.4.title=\u4E3B\u9898\u8BBE\u7F6E
|
||||
settings.1.tip=\u63D2\u4EF6\u7BA1\u7406
|
||||
settings.2.tip=\u5916\u89C2\u8BBE\u7F6E
|
||||
settings.3.tip=\u7248\u672C\u4FE1\u606F
|
||||
@@ -183,6 +186,8 @@ settings.1.scrollPane=\u5DF2\u52A0\u8F7D\u63D2\u4EF6\u5217\u8868
|
||||
settings.2.color=\u754C\u9762\u4E3B\u9898\u989C\u8272\uFF1A
|
||||
settings.2.colorBtn=\u9009\u62E9\u989C\u8272
|
||||
settings.2.colorBtn.color=\u9009\u62E9\u4E3B\u9898\u989C\u8272
|
||||
settings.2.colorBtn.cancel=\u53D6\u6D88
|
||||
settings.2.colorBtn.apply=\u5E94\u7528
|
||||
settings.2.font=\u754C\u9762\u5B57\u4F53\uFF1A
|
||||
settings.2.fontBtn=\u9009\u62E9\u5B57\u4F53
|
||||
settings.2.showConfirmDialog=\u9009\u62E9\u5B57\u4F53
|
||||
@@ -194,6 +199,17 @@ settings.2.language=\u754C\u9762\u8BED\u8A00\uFF1A
|
||||
settings.2.language.error=\u672A\u77E5\u8BED\u8A00
|
||||
settings.3.infoArea.1=\u8F6F\u4EF6\u7248\u672C:
|
||||
settings.3.infoArea.2=\u5F00\u53D1\u4F5C\u8005:
|
||||
settings.3.infoArea.description=\u8FD9\u662F\u4E00\u4E2A\u529F\u80FD\u5F3A\u5927\u7684\u521B\u65B0\u5DE5\u5177\u7BB1\uFF0C\u96C6\u6210\u4E86\u591A\u79CD\u5B9E\u7528\u5DE5\u5177\u3002
|
||||
settings.3.infoArea.features=\u4E3B\u8981\u529F\u80FD
|
||||
settings.3.infoArea.feature1=\u6570\u636E\u5206\u6790\u548C\u53EF\u89C6\u5316
|
||||
settings.3.infoArea.feature2=\u81EA\u52A8\u5316\u5904\u7406
|
||||
settings.3.infoArea.feature3=\u81EA\u5B9A\u4E49\u63D2\u4EF6\u652F\u6301
|
||||
settings.3.infoArea.email=tzdwindows 7
|
||||
settings.3.infoArea.website=https://axisInnovatorsBox.com
|
||||
settings.3.infoArea.copyright=\u7248\u6743\u6240\u6709 \u00A9 2025 Axis Innovators. \u4FDD\u7559\u6240\u6709\u6743\u5229\u3002
|
||||
settings.3.infoArea.requirement1=Windows 10 \u6216\u66F4\u9AD8\u7248\u672C
|
||||
settings.3.infoArea.requirement2=\u81F3\u5C11 4GB \u5185\u5B58
|
||||
settings.3.infoArea.support=\u5982\u6709\u95EE\u9898\u8BF7\u8054\u7CFB\u6280\u672F\u652F\u6301\u56E2\u961F
|
||||
settings.4.no_theme=\u6CA1\u6709\u53EF\u7528\u7684\u4E3B\u9898
|
||||
settings.4.search=\u641C\u7D22
|
||||
settings.4.search_empty=\u8BF7\u8F93\u5165\u641C\u7D22\u5185\u5BB9\uFF01
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
library/TaskbarTranslucentConsole.exe
Normal file
BIN
library/TaskbarTranslucentConsole.exe
Normal file
Binary file not shown.
BIN
library/TaskbarTranslucentManager.dll
Normal file
BIN
library/TaskbarTranslucentManager.dll
Normal file
Binary file not shown.
Binary file not shown.
BIN
library/WallpaperOptimization.dll
Normal file
BIN
library/WallpaperOptimization.dll
Normal file
Binary file not shown.
BIN
library/chrome_elf.dll
Normal file
BIN
library/chrome_elf.dll
Normal file
Binary file not shown.
@@ -7,12 +7,21 @@
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* Class: com_axis_innovators_box_tools_RegisterTray
|
||||
* Method: register
|
||||
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
|
||||
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
|
||||
*/
|
||||
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
|
||||
(JNIEnv*, jclass, jstring, jobject, jstring, jobject);
|
||||
|
||||
/*
|
||||
* Class: com_axis_innovators_box_tools_RegisterTray
|
||||
* Method: registerEx
|
||||
* Signature: (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Ljava/lang/String;Lcom/axis/innovators/box/tools/RegisterTray/Event;)J
|
||||
*/
|
||||
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_registerEx
|
||||
(JNIEnv*, jclass, jstring, jobject, jstring, jstring, jobject);
|
||||
|
||||
/*
|
||||
|
||||
@@ -4,288 +4,790 @@
|
||||
#include <jni.h>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
#include <algorithm>
|
||||
#include <uxtheme.h>
|
||||
#include <windowsx.h>
|
||||
|
||||
#pragma comment(lib, "uxtheme.lib")
|
||||
|
||||
#include "com_axis_innovators_box_tools_RegisterTray.h"
|
||||
|
||||
// 调试输出
|
||||
#define DEBUG_LOG(msg) OutputDebugStringW(L"[Tray] " msg L"\n")
|
||||
// 调试输出宏
|
||||
#define DEBUG_LOG(msg) { OutputDebugStringW(L"[Tray] " msg L"\n"); }
|
||||
|
||||
// 全局 JVM 缓存(懒取)
|
||||
static std::atomic<JavaVM*> gJvm{ nullptr };
|
||||
|
||||
struct MenuItemData {
|
||||
jint menuId;
|
||||
jobject eventObjGlobal; // global ref to the Event object for this item
|
||||
jmethodID onClickMethod;
|
||||
jobject eventObj;
|
||||
int menuId;
|
||||
std::wstring title;
|
||||
};
|
||||
|
||||
struct TrayData {
|
||||
HMENU hMenu = NULL;
|
||||
std::vector<MenuItemData> menuItems;
|
||||
jobject eventObj = NULL;
|
||||
jmethodID onClickMethod = NULL;
|
||||
HWND hwnd = NULL;
|
||||
UINT trayId = 0;
|
||||
HICON hIcon = NULL;
|
||||
std::vector<MenuItemData> menuItems;
|
||||
jobject eventObjGlobal = NULL; // global ref for the primary event callback
|
||||
jmethodID onClickMethod = NULL;
|
||||
std::wstring name;
|
||||
std::wstring iconPath;
|
||||
std::wstring description;
|
||||
// 使用 Win32 线程句柄替代 std::thread
|
||||
HANDLE threadHandle = NULL;
|
||||
DWORD threadId = 0;
|
||||
std::mutex mutex;
|
||||
std::atomic<bool> running{ false };
|
||||
};
|
||||
|
||||
std::vector<TrayData*> trayDataList;
|
||||
static std::vector<TrayData*> gTrayList;
|
||||
static std::mutex gTrayListMutex;
|
||||
|
||||
HICON LoadTrayIcon(const wchar_t* path) {
|
||||
HICON hIcon = (HICON)LoadImageW(
|
||||
NULL, path, IMAGE_ICON,
|
||||
GetSystemMetrics(SM_CXSMICON),
|
||||
GetSystemMetrics(SM_CYSMICON),
|
||||
LR_LOADFROMFILE | LR_SHARED
|
||||
);
|
||||
return hIcon ? hIcon : LoadIconW(NULL, IDI_APPLICATION);
|
||||
// ---------- 主题/字体 ----------
|
||||
|
||||
struct ThemeColors {
|
||||
COLORREF bg;
|
||||
COLORREF hover;
|
||||
COLORREF text;
|
||||
COLORREF border;
|
||||
};
|
||||
|
||||
static bool IsLightTheme()
|
||||
{
|
||||
DWORD val = 1; // 默认为浅色
|
||||
DWORD cb = sizeof(val);
|
||||
LONG r = RegGetValueW(
|
||||
HKEY_CURRENT_USER,
|
||||
L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize",
|
||||
L"AppsUseLightTheme",
|
||||
RRF_RT_REG_DWORD, nullptr, &val, &cb);
|
||||
return (r != ERROR_SUCCESS) ? true : (val != 0);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
TrayData* pData = (TrayData*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
|
||||
static ThemeColors GetThemeColors()
|
||||
{
|
||||
if (IsLightTheme()) {
|
||||
// 浅色主题
|
||||
return ThemeColors{
|
||||
RGB(245, 245, 245), // 背景
|
||||
RGB(225, 225, 225), // 悬停
|
||||
RGB(32, 32, 32), // 文字
|
||||
RGB(210, 210, 210) // 边框
|
||||
};
|
||||
}
|
||||
else {
|
||||
// 深色主题
|
||||
return ThemeColors{
|
||||
RGB(30, 30, 30), // 背景
|
||||
RGB(50, 50, 50), // 悬停
|
||||
RGB(230, 230, 230), // 文字
|
||||
RGB(60, 60, 60) // 边框
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static HFONT CreatePopupUIFont()
|
||||
{
|
||||
LOGFONTW lf{};
|
||||
// 使用系统图标标题字体作为基准
|
||||
SystemParametersInfoW(SPI_GETICONTITLELOGFONT, sizeof(lf), &lf, 0);
|
||||
// 替换为 Segoe UI,更现代
|
||||
wcscpy_s(lf.lfFaceName, L"Segoe UI");
|
||||
lf.lfHeight = -12; // 约 9pt @96DPI
|
||||
lf.lfWeight = FW_NORMAL;
|
||||
lf.lfQuality = CLEARTYPE_NATURAL_QUALITY; // 开启更自然的 ClearType
|
||||
return CreateFontIndirectW(&lf);
|
||||
}
|
||||
|
||||
// ---------- 前向声明 ----------
|
||||
LRESULT CALLBACK TrayWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
LRESULT CALLBACK PopupWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
|
||||
DWORD WINAPI TrayMessageThreadProc(LPVOID lpParam);
|
||||
void TrayMessageThread(TrayData* td);
|
||||
|
||||
// 注册窗口类(线程安全)
|
||||
void EnsureWindowClassesRegistered(HINSTANCE hInstance) {
|
||||
static std::atomic_bool registered{ false };
|
||||
bool expected = false;
|
||||
if (!registered.compare_exchange_strong(expected, true)) return;
|
||||
|
||||
WNDCLASSEXW wc = { 0 };
|
||||
wc.cbSize = sizeof(wc);
|
||||
wc.lpfnWndProc = TrayWndProc;
|
||||
wc.hInstance = hInstance;
|
||||
wc.lpszClassName = L"ModernTray_TrayWindowClass";
|
||||
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
|
||||
WNDCLASSEXW popup = { 0 };
|
||||
popup.cbSize = sizeof(popup);
|
||||
popup.lpfnWndProc = PopupWndProc;
|
||||
popup.hInstance = hInstance;
|
||||
popup.lpszClassName = L"ModernTray_PopupWindowClass";
|
||||
popup.hCursor = LoadCursor(NULL, IDC_ARROW);
|
||||
popup.hbrBackground = NULL; // 自绘背景
|
||||
|
||||
RegisterClassExW(&wc);
|
||||
RegisterClassExW(&popup);
|
||||
}
|
||||
|
||||
// 加载图标(文件或默认)
|
||||
HICON LoadTrayIconSafe(const std::wstring& path) {
|
||||
if (!path.empty()) {
|
||||
HICON h = (HICON)LoadImageW(NULL, path.c_str(), IMAGE_ICON,
|
||||
GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON),
|
||||
LR_LOADFROMFILE | LR_SHARED);
|
||||
if (h) return h;
|
||||
}
|
||||
return LoadIconW(NULL, IDI_APPLICATION);
|
||||
}
|
||||
|
||||
// 查找 TrayData
|
||||
TrayData* FindTrayById(UINT id) {
|
||||
//std::lock_guard<std::mutex> lk(gTrayListMutex);
|
||||
for (auto t : gTrayList) if (t->trayId == id) return t;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// 计算窗口尺寸(基于菜单文本)
|
||||
SIZE CalcPopupSize(HDC hdc, const std::vector<MenuItemData>& items, HFONT hFont, int paddingX = 14, int paddingY = 10, int itemHeight = 28) {
|
||||
SIZE sz = { 0, 0 };
|
||||
int maxw = 0;
|
||||
|
||||
HFONT old = (HFONT)SelectObject(hdc, hFont);
|
||||
|
||||
for (const auto& it : items) {
|
||||
if (it.title.empty()) continue;
|
||||
RECT rc = { 0,0,0,0 };
|
||||
DrawTextW(hdc, it.title.c_str(), -1, &rc, DT_SINGLELINE | DT_LEFT | DT_CALCRECT);
|
||||
int w = rc.right - rc.left;
|
||||
if (w > maxw) maxw = w;
|
||||
}
|
||||
|
||||
SelectObject(hdc, old);
|
||||
|
||||
sz.cx = maxw + paddingX * 2;
|
||||
if (sz.cx < 140) sz.cx = 140; // 最小宽度
|
||||
sz.cy = (int)items.size() * itemHeight + paddingY; // 顶部/底部各留 paddingY/2 的感觉
|
||||
return sz;
|
||||
}
|
||||
|
||||
// 绘制圆角背景与文本(WM_PAINT 在 PopupWndProc 使用)
|
||||
void PaintPopup(HWND hwnd, const std::vector<MenuItemData>& items, int hoverIndex) {
|
||||
PAINTSTRUCT ps;
|
||||
HDC hdc = BeginPaint(hwnd, &ps);
|
||||
|
||||
RECT rc;
|
||||
GetClientRect(hwnd, &rc);
|
||||
|
||||
ThemeColors c = GetThemeColors();
|
||||
|
||||
// 背景(圆角 + 边框)
|
||||
HBRUSH bg = CreateSolidBrush(c.bg);
|
||||
HBRUSH hover = CreateSolidBrush(c.hover);
|
||||
HPEN border = CreatePen(PS_SOLID, 1, c.border);
|
||||
|
||||
HRGN rgn = CreateRoundRectRgn(rc.left, rc.top, rc.right, rc.bottom, 12, 12);
|
||||
SelectClipRgn(hdc, rgn);
|
||||
|
||||
HGDIOBJ oldPen = SelectObject(hdc, border);
|
||||
HGDIOBJ oldBrush = SelectObject(hdc, bg);
|
||||
RoundRect(hdc, rc.left, rc.top, rc.right, rc.bottom, 12, 12);
|
||||
|
||||
// 文本 & 悬停绘制
|
||||
SetBkMode(hdc, TRANSPARENT);
|
||||
SetTextColor(hdc, c.text);
|
||||
static HFONT sFont = nullptr;
|
||||
if (!sFont) sFont = CreatePopupUIFont();
|
||||
HFONT oldFont = (HFONT)SelectObject(hdc, sFont);
|
||||
|
||||
int y = 6;
|
||||
const int itemH = 28;
|
||||
for (size_t i = 0; i < items.size(); ++i) {
|
||||
RECT itrc = { 8, y, rc.right - 8, y + itemH - 2 };
|
||||
if ((int)i == hoverIndex) {
|
||||
FillRect(hdc, &itrc, hover);
|
||||
}
|
||||
DrawTextW(hdc, items[i].title.c_str(), -1, &itrc, DT_VCENTER | DT_SINGLELINE | DT_LEFT | DT_NOPREFIX);
|
||||
y += itemH;
|
||||
}
|
||||
|
||||
SelectObject(hdc, oldFont);
|
||||
SelectObject(hdc, oldBrush);
|
||||
SelectObject(hdc, oldPen);
|
||||
|
||||
DeleteObject(bg);
|
||||
DeleteObject(hover);
|
||||
DeleteObject(border);
|
||||
DeleteObject(rgn);
|
||||
|
||||
EndPaint(hwnd, &ps);
|
||||
}
|
||||
|
||||
// ---------- Popup window helpers ----------
|
||||
struct PopupContext {
|
||||
TrayData* tray;
|
||||
std::vector<MenuItemData> items;
|
||||
int hoverIndex;
|
||||
POINT origin;
|
||||
};
|
||||
|
||||
// 存储 PopupContext 到窗口属性(使用 GWLP_USERDATA)
|
||||
static inline PopupContext* GetPopupContext(HWND hwnd) {
|
||||
return (PopupContext*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
|
||||
}
|
||||
|
||||
// PopupWndProc 实现:自绘菜单,鼠标移动、点击处理
|
||||
LRESULT CALLBACK PopupWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
PopupContext* ctx = GetPopupContext(hwnd);
|
||||
static const UINT kGuardTimerId = 1; // 初始防抖计时器
|
||||
static const UINT kGuardMs = 120; // 防抖时长
|
||||
|
||||
switch (msg) {
|
||||
case WM_USER + 1:
|
||||
switch (lParam) {
|
||||
case WM_RBUTTONUP:
|
||||
if (pData && pData->hMenu) {
|
||||
case WM_CREATE:
|
||||
SetTimer(hwnd, kGuardTimerId, kGuardMs, nullptr);
|
||||
return 0;
|
||||
|
||||
case WM_TIMER:
|
||||
if (wParam == kGuardTimerId) {
|
||||
KillTimer(hwnd, kGuardTimerId);
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
|
||||
case WM_ACTIVATE:
|
||||
if (LOWORD(wParam) == WA_INACTIVE) {
|
||||
// 防抖期内忽略一次失焦
|
||||
if (!KillTimer(hwnd, kGuardTimerId)) {
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
SetTimer(hwnd, kGuardTimerId, kGuardMs, nullptr);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_MOUSEMOVE: {
|
||||
if (!ctx) break;
|
||||
int my = GET_Y_LPARAM(lParam);
|
||||
const int itemH = 28;
|
||||
int count = (int)ctx->items.size();
|
||||
if (count <= 0) break;
|
||||
int idx = std::max(0, std::min(count - 1, (my - 6) / itemH));
|
||||
if (idx != ctx->hoverIndex) {
|
||||
ctx->hoverIndex = idx;
|
||||
InvalidateRect(hwnd, NULL, TRUE);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_LBUTTONDOWN: {
|
||||
if (!ctx) break;
|
||||
int my = GET_Y_LPARAM(lParam);
|
||||
const int itemH = 28;
|
||||
int count = (int)ctx->items.size();
|
||||
if (count > 0) {
|
||||
int idx = std::max(0, std::min(count - 1, (my - 6) / itemH));
|
||||
if (idx >= 0 && idx < count) {
|
||||
MenuItemData sel = ctx->items[idx];
|
||||
TrayData* td = ctx->tray;
|
||||
JavaVM* jvm = gJvm.load();
|
||||
if (!jvm) {
|
||||
JNI_GetCreatedJavaVMs(&jvm, 1, NULL);
|
||||
gJvm.store(jvm);
|
||||
}
|
||||
if (jvm && sel.eventObjGlobal && sel.onClickMethod) {
|
||||
JNIEnv* env = nullptr;
|
||||
if (jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
|
||||
env->CallVoidMethod(sel.eventObjGlobal, sel.onClickMethod, (jlong)td->trayId);
|
||||
jvm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
DestroyWindow(hwnd);
|
||||
return 0;
|
||||
}
|
||||
|
||||
case WM_PAINT:
|
||||
if (ctx) PaintPopup(hwnd, ctx->items, ctx->hoverIndex);
|
||||
return 0;
|
||||
|
||||
case WM_NCDESTROY:
|
||||
if (ctx) {
|
||||
delete ctx;
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, 0);
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
|
||||
default:
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
}
|
||||
|
||||
// 创建并显示自定义弹出菜单(在托盘窗口线程上下文调用)
|
||||
void ShowCustomPopup(TrayData* td, int x, int y) {
|
||||
if (!td) return;
|
||||
HINSTANCE hInst = GetModuleHandleW(NULL);
|
||||
|
||||
HWND popup = CreateWindowExW(
|
||||
WS_EX_TOOLWINDOW | WS_EX_TOPMOST,
|
||||
L"ModernTray_PopupWindowClass", L"",
|
||||
WS_POPUP,
|
||||
x, y, 200, 200,
|
||||
td->hwnd /* owner */, NULL, hInst, NULL
|
||||
);
|
||||
if (!popup) return;
|
||||
|
||||
PopupContext* ctx = new PopupContext();
|
||||
ctx->tray = td;
|
||||
ctx->items = td->menuItems; // 保证是完整列表
|
||||
ctx->hoverIndex = -1;
|
||||
SetWindowLongPtrW(popup, GWLP_USERDATA, (LONG_PTR)ctx);
|
||||
|
||||
// 计算尺寸并防止出屏
|
||||
HDC hdc = GetDC(popup);
|
||||
static HFONT sFont = nullptr;
|
||||
if (!sFont) sFont = CreatePopupUIFont();
|
||||
SIZE sz = CalcPopupSize(hdc, ctx->items, sFont);
|
||||
ReleaseDC(popup, hdc);
|
||||
|
||||
RECT work{};
|
||||
SystemParametersInfoW(SPI_GETWORKAREA, 0, &work, 0);
|
||||
|
||||
LONG px = std::min(
|
||||
std::max(static_cast<LONG>(x), work.left),
|
||||
std::max(work.right - static_cast<LONG>(sz.cx), work.left)
|
||||
);
|
||||
LONG py = std::min(
|
||||
std::max(static_cast<LONG>(y), work.top),
|
||||
std::max(work.bottom - static_cast<LONG>(sz.cy), work.top)
|
||||
);
|
||||
|
||||
SetWindowPos(popup, HWND_TOPMOST, px, py, sz.cx, sz.cy, SWP_SHOWWINDOW | SWP_NOACTIVATE);
|
||||
|
||||
// 圆角
|
||||
HRGN r = CreateRoundRectRgn(0, 0, sz.cx + 1, sz.cy + 1, 12, 12);
|
||||
SetWindowRgn(popup, r, TRUE);
|
||||
|
||||
// 抢前台以降低“瞬间失焦”概率
|
||||
if (td && td->hwnd) SetForegroundWindow(td->hwnd);
|
||||
ShowWindow(popup, SW_SHOWNORMAL);
|
||||
SetForegroundWindow(popup);
|
||||
SetFocus(popup);
|
||||
|
||||
// 仅用于悬停高亮(不再用离开即关闭)
|
||||
TRACKMOUSEEVENT tme{ sizeof(TRACKMOUSEEVENT) };
|
||||
tme.dwFlags = TME_LEAVE;
|
||||
tme.hwndTrack = popup;
|
||||
TrackMouseEvent(&tme);
|
||||
|
||||
UpdateWindow(popup);
|
||||
}
|
||||
|
||||
// ---------- Tray 窗口处理函数 ----------
|
||||
LRESULT CALLBACK TrayWndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
|
||||
TrayData* td = (TrayData*)GetWindowLongPtrW(hwnd, GWLP_USERDATA);
|
||||
|
||||
if (msg == (WM_USER + 1)) {
|
||||
if (lParam == WM_RBUTTONUP) {
|
||||
if (td && td->hwnd) {
|
||||
SetForegroundWindow(td->hwnd);
|
||||
}
|
||||
else {
|
||||
SetForegroundWindow(hwnd);
|
||||
}
|
||||
POINT pt;
|
||||
GetCursorPos(&pt);
|
||||
SetForegroundWindow(hwnd);
|
||||
TrackPopupMenuEx(pData->hMenu,
|
||||
TPM_RIGHTALIGN | TPM_BOTTOMALIGN,
|
||||
pt.x, pt.y, hwnd, NULL);
|
||||
PostMessageW(hwnd, WM_NULL, 0, 0);
|
||||
}
|
||||
if (td) ShowCustomPopup(td, pt.x, pt.y);
|
||||
return 0;
|
||||
case WM_LBUTTONDOWN:
|
||||
if (pData && pData->onClickMethod) {
|
||||
JavaVM* jvm;
|
||||
JNIEnv* env;
|
||||
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK &&
|
||||
jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
|
||||
env->CallVoidMethod(pData->eventObj, pData->onClickMethod, (jlong)pData->trayId);
|
||||
}
|
||||
else if (lParam == WM_LBUTTONDOWN) {
|
||||
if (td && td->eventObjGlobal && td->onClickMethod) {
|
||||
JavaVM* jvm = gJvm.load();
|
||||
if (!jvm) {
|
||||
JNI_GetCreatedJavaVMs(&jvm, 1, NULL);
|
||||
gJvm.store(jvm);
|
||||
}
|
||||
if (jvm) {
|
||||
JNIEnv* env = nullptr;
|
||||
if (jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
|
||||
env->CallVoidMethod(td->eventObjGlobal, td->onClickMethod, (jlong)td->trayId);
|
||||
jvm->DetachCurrentThread();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case WM_COMMAND: {
|
||||
int menuId = LOWORD(wParam);
|
||||
if (pData) {
|
||||
for (auto& item : pData->menuItems) {
|
||||
if (item.menuId == menuId) {
|
||||
JavaVM* jvm;
|
||||
JNIEnv* env;
|
||||
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK &&
|
||||
jvm->AttachCurrentThread((void**)&env, NULL) == JNI_OK) {
|
||||
env->CallVoidMethod(item.eventObj, item.onClickMethod, (jlong)pData->trayId);
|
||||
jvm->DetachCurrentThread();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
switch (msg) {
|
||||
case WM_CREATE:
|
||||
return 0;
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
return DefWindowProcW(hwnd, msg, wParam, lParam);
|
||||
}
|
||||
}
|
||||
|
||||
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
|
||||
(JNIEnv* env, jclass, jstring name, jobject menuItems, jstring icon, jstring, jobject event) {
|
||||
// 注册窗口类
|
||||
WNDCLASSEXW wc = { sizeof(WNDCLASSEXW) };
|
||||
wc.lpfnWndProc = WndProc;
|
||||
wc.hInstance = GetModuleHandleW(NULL);
|
||||
wc.lpszClassName = L"TrayWindowClass";
|
||||
if (!RegisterClassExW(&wc)) return -1;
|
||||
// 线程入口(Win32 线程入口)
|
||||
DWORD WINAPI TrayMessageThreadProc(LPVOID lpParam) {
|
||||
TrayData* td = (TrayData*)lpParam;
|
||||
if (!td) return 0;
|
||||
TrayMessageThread(td);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 创建消息窗口
|
||||
HWND hwnd = CreateWindowExW(0, L"TrayWindowClass", L"", 0, 0, 0, 0, 0,
|
||||
HWND_MESSAGE, NULL, NULL, NULL);
|
||||
if (!hwnd) return -1;
|
||||
// 线程主体:为单个托盘创建窗口、图标并运行消息循环
|
||||
void TrayMessageThread(TrayData* td) {
|
||||
if (!td) return;
|
||||
td->running.store(true);
|
||||
|
||||
TrayData* pData = new TrayData();
|
||||
pData->hwnd = hwnd;
|
||||
pData->trayId = GetTickCount();
|
||||
pData->hMenu = CreatePopupMenu();
|
||||
HINSTANCE hInst = GetModuleHandleW(NULL);
|
||||
EnsureWindowClassesRegistered(hInst);
|
||||
|
||||
// 解析菜单项
|
||||
jclass listClass = env->GetObjectClass(menuItems);
|
||||
jint size = env->CallIntMethod(menuItems, env->GetMethodID(listClass, "size", "()I"));
|
||||
|
||||
for (int i = 0; i < size; ++i) {
|
||||
jobject item = env->CallObjectMethod(menuItems,
|
||||
env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;"), i);
|
||||
|
||||
jstring name = (jstring)env->GetObjectField(item,
|
||||
env->GetFieldID(env->GetObjectClass(item), "name", "Ljava/lang/String;"));
|
||||
jobject eventObj = env->GetObjectField(item,
|
||||
env->GetFieldID(env->GetObjectClass(item), "event",
|
||||
"Lcom/axis/innovators/box/tools/RegisterTray$Event;"));
|
||||
|
||||
const jchar* nameChars = env->GetStringChars(name, NULL);
|
||||
std::wstring menuName(nameChars, nameChars + env->GetStringLength(name));
|
||||
env->ReleaseStringChars(name, nameChars);
|
||||
|
||||
MenuItemData menuItem;
|
||||
menuItem.menuId = 1000 + i;
|
||||
menuItem.eventObj = env->NewGlobalRef(eventObj);
|
||||
jclass eventClass = env->GetObjectClass(eventObj);
|
||||
menuItem.onClickMethod = env->GetMethodID(eventClass, "onClick", "(J)V");
|
||||
|
||||
AppendMenuW(pData->hMenu, MF_STRING, menuItem.menuId, menuName.c_str());
|
||||
pData->menuItems.push_back(menuItem);
|
||||
// 创建消息窗口(消息窗口必须在本线程创建)
|
||||
HWND hwnd = CreateWindowExW(0, L"ModernTray_TrayWindowClass", L"", 0,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
HWND_MESSAGE, NULL, hInst, NULL);
|
||||
if (!hwnd) {
|
||||
//DEBUG_LOG(L"CreateWindowExW 失败");
|
||||
td->running.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// 事件回调
|
||||
jclass eventClass = env->GetObjectClass(event);
|
||||
pData->onClickMethod = env->GetMethodID(eventClass, "onClick", "(J)V");
|
||||
pData->eventObj = env->NewGlobalRef(event);
|
||||
|
||||
// 配置托盘
|
||||
NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) };
|
||||
nid.hWnd = hwnd;
|
||||
nid.uID = pData->trayId;
|
||||
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
|
||||
nid.uCallbackMessage = WM_USER + 1;
|
||||
td->hwnd = hwnd;
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)td);
|
||||
|
||||
// 加载图标
|
||||
const wchar_t* iconPath = (const wchar_t*)env->GetStringChars(icon, NULL);
|
||||
pData->hIcon = LoadTrayIcon(iconPath);
|
||||
nid.hIcon = pData->hIcon;
|
||||
env->ReleaseStringChars(icon, (const jchar*)iconPath);
|
||||
td->hIcon = LoadTrayIconSafe(td->iconPath);
|
||||
|
||||
// 设置提示
|
||||
const wchar_t* tip = (const wchar_t*)env->GetStringChars(name, NULL);
|
||||
wcsncpy_s(nid.szTip, _countof(nid.szTip), tip, _TRUNCATE);
|
||||
env->ReleaseStringChars(name, (const jchar*)tip);
|
||||
|
||||
if (!Shell_NotifyIconW(NIM_ADD, &nid)) {
|
||||
delete pData;
|
||||
return -1;
|
||||
// 添加到系统托盘
|
||||
NOTIFYICONDATAW nid = { 0 };
|
||||
nid.cbSize = sizeof(nid);
|
||||
nid.hWnd = hwnd;
|
||||
nid.uID = td->trayId;
|
||||
nid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP;
|
||||
nid.uCallbackMessage = WM_USER + 1;
|
||||
nid.hIcon = td->hIcon;
|
||||
// szTip
|
||||
{
|
||||
std::wstring tip = td->name.empty() ? L"Tray" : td->name;
|
||||
wcsncpy_s(nid.szTip, tip.c_str(), _TRUNCATE);
|
||||
}
|
||||
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA, (LONG_PTR)pData);
|
||||
trayDataList.push_back(pData);
|
||||
if (!Shell_NotifyIconW(NIM_ADD, &nid)) {
|
||||
//DEBUG_LOG(L"Shell_NotifyIconW(NIM_ADD) 失败");
|
||||
}
|
||||
|
||||
// 启动消息循环
|
||||
// 消息循环
|
||||
MSG msg;
|
||||
while (GetMessageW(&msg, NULL, 0, 0)) {
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessageW(&msg);
|
||||
}
|
||||
|
||||
return (jlong)pData->trayId;
|
||||
}
|
||||
// 在退出前删除托盘图标(再一次保险)
|
||||
Shell_NotifyIconW(NIM_DELETE, &nid);
|
||||
|
||||
TrayData* FindTrayData(UINT trayId) {
|
||||
for (auto data : trayDataList) {
|
||||
if (data->trayId == trayId) return data;
|
||||
// 清理窗口句柄(如果尚未)
|
||||
if (td->hwnd) {
|
||||
DestroyWindow(td->hwnd);
|
||||
td->hwnd = NULL;
|
||||
}
|
||||
return nullptr;
|
||||
|
||||
td->running.store(false);
|
||||
}
|
||||
|
||||
// ---------- JNI 接口实现 ----------
|
||||
|
||||
// 辅助:将 jstring 转 wchar_t string
|
||||
static std::wstring JStringToWString(JNIEnv* env, jstring js) {
|
||||
if (!js) return L"";
|
||||
const jchar* chars = env->GetStringChars(js, NULL);
|
||||
jsize len = env->GetStringLength(js);
|
||||
std::wstring ws(chars, chars + len);
|
||||
env->ReleaseStringChars(js, chars);
|
||||
return ws;
|
||||
}
|
||||
|
||||
/*
|
||||
* public static native long register(String name, List<Item> value, String icon, Event event);
|
||||
*/
|
||||
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_register
|
||||
(JNIEnv* env, jclass, jstring jname, jobject jlist, jstring jicon, jobject jevent) {
|
||||
// 转换输入
|
||||
std::wstring name = JStringToWString(env, jname);
|
||||
std::wstring iconPath = JStringToWString(env, jicon);
|
||||
|
||||
// 创建 TrayData
|
||||
TrayData* td = new TrayData();
|
||||
td->trayId = (UINT)GetTickCount();
|
||||
td->name = name;
|
||||
td->iconPath = iconPath;
|
||||
|
||||
// 缓存 JVM
|
||||
JavaVM* jvm = nullptr;
|
||||
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK && jvm) gJvm.store(jvm);
|
||||
|
||||
// event 全局引用与方法
|
||||
if (jevent) {
|
||||
td->eventObjGlobal = env->NewGlobalRef(jevent);
|
||||
jclass evc = env->GetObjectClass(jevent);
|
||||
td->onClickMethod = env->GetMethodID(evc, "onClick", "(J)V");
|
||||
}
|
||||
|
||||
// 解析列表 items(更鲁棒的遍历)
|
||||
if (jlist) {
|
||||
jclass listClass = env->GetObjectClass(jlist);
|
||||
jmethodID sizeMid = env->GetMethodID(listClass, "size", "()I");
|
||||
jmethodID getMid = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;");
|
||||
jint size = env->CallIntMethod(jlist, sizeMid);
|
||||
|
||||
jint parsed = 0;
|
||||
for (jint i = 0; i < size; ++i) {
|
||||
jobject item = env->CallObjectMethod(jlist, getMid, i);
|
||||
if (!item) continue;
|
||||
jclass itemClass = env->GetObjectClass(item);
|
||||
|
||||
jfieldID nameF = env->GetFieldID(itemClass, "name", "Ljava/lang/String;");
|
||||
jstring jtitle = (jstring)env->GetObjectField(item, nameF);
|
||||
std::wstring title = JStringToWString(env, jtitle);
|
||||
|
||||
jfieldID eventF = env->GetFieldID(itemClass, "event", "Lcom/axis/innovators/box/tools/RegisterTray$Event;");
|
||||
jobject ievent = env->GetObjectField(item, eventF);
|
||||
|
||||
jobject globalEvent = nullptr;
|
||||
jmethodID onClickMid = nullptr;
|
||||
if (ievent) {
|
||||
globalEvent = env->NewGlobalRef(ievent);
|
||||
jclass evc = env->GetObjectClass(ievent);
|
||||
onClickMid = env->GetMethodID(evc, "onClick", "(J)V");
|
||||
}
|
||||
|
||||
MenuItemData mid;
|
||||
mid.menuId = 1000 + i;
|
||||
mid.eventObjGlobal = globalEvent;
|
||||
mid.onClickMethod = onClickMid;
|
||||
mid.title = title;
|
||||
td->menuItems.push_back(mid);
|
||||
++parsed;
|
||||
}
|
||||
// 打印数量,便于确认 Java 侧是否传入了多项
|
||||
wchar_t buf[128];
|
||||
swprintf_s(buf, L"[register] parsed menu items = %d", (int)parsed);
|
||||
//DEBUG_LOG(buf);
|
||||
}
|
||||
|
||||
// 放入全局列表
|
||||
{
|
||||
//std::lock_guard<std::mutex> lk(gTrayListMutex);
|
||||
gTrayList.push_back(td);
|
||||
}
|
||||
|
||||
// 启动窗口线程
|
||||
td->threadHandle = CreateThread(NULL, 0, TrayMessageThreadProc, td, 0, &td->threadId);
|
||||
if (!td->threadHandle) {
|
||||
DEBUG_LOG(L"CreateThread failed");
|
||||
// 清理
|
||||
if (td->eventObjGlobal) {
|
||||
env->DeleteGlobalRef(td->eventObjGlobal);
|
||||
td->eventObjGlobal = NULL;
|
||||
}
|
||||
for (auto& mi : td->menuItems) {
|
||||
if (mi.eventObjGlobal) {
|
||||
env->DeleteGlobalRef(mi.eventObjGlobal);
|
||||
mi.eventObjGlobal = NULL;
|
||||
}
|
||||
}
|
||||
td->menuItems.clear();
|
||||
delete td;
|
||||
return (jlong)0;
|
||||
}
|
||||
|
||||
return (jlong)td->trayId;
|
||||
}
|
||||
|
||||
/*
|
||||
* public static native long registerEx(String name, List<Item> value, String icon, String description, Event event);
|
||||
*/
|
||||
JNIEXPORT jlong JNICALL Java_com_axis_innovators_box_tools_RegisterTray_registerEx
|
||||
(JNIEnv* env, jclass, jstring jname, jobject jlist, jstring jicon, jstring jdesc, jobject jevent) {
|
||||
// 功能与 register 相同,只是多了 description
|
||||
std::wstring name = JStringToWString(env, jname);
|
||||
std::wstring iconPath = JStringToWString(env, jicon);
|
||||
std::wstring desc = JStringToWString(env, jdesc);
|
||||
|
||||
TrayData* td = new TrayData();
|
||||
td->trayId = (UINT)GetTickCount();
|
||||
td->name = name;
|
||||
td->iconPath = iconPath;
|
||||
td->description = desc;
|
||||
|
||||
// 缓存 JVM
|
||||
JavaVM* jvm = nullptr;
|
||||
if (JNI_GetCreatedJavaVMs(&jvm, 1, NULL) == JNI_OK && jvm) gJvm.store(jvm);
|
||||
|
||||
// event
|
||||
if (jevent) {
|
||||
td->eventObjGlobal = env->NewGlobalRef(jevent);
|
||||
jclass evc = env->GetObjectClass(jevent);
|
||||
td->onClickMethod = env->GetMethodID(evc, "onClick", "(J)V");
|
||||
}
|
||||
|
||||
// 解析 list
|
||||
if (jlist) {
|
||||
jclass listClass = env->GetObjectClass(jlist);
|
||||
jmethodID sizeMid = env->GetMethodID(listClass, "size", "()I");
|
||||
jmethodID getMid = env->GetMethodID(listClass, "get", "(I)Ljava/lang/Object;");
|
||||
jint size = env->CallIntMethod(jlist, sizeMid);
|
||||
|
||||
jint parsed = 0;
|
||||
for (jint i = 0; i < size; ++i) {
|
||||
jobject item = env->CallObjectMethod(jlist, getMid, i);
|
||||
if (!item) continue;
|
||||
jclass itemClass = env->GetObjectClass(item);
|
||||
|
||||
jfieldID nameF = env->GetFieldID(itemClass, "name", "Ljava/lang/String;");
|
||||
jstring jtitle = (jstring)env->GetObjectField(item, nameF);
|
||||
std::wstring title = JStringToWString(env, jtitle);
|
||||
|
||||
jfieldID eventF = env->GetFieldID(itemClass, "event", "Lcom/axis/innovators/box/tools/RegisterTray$Event;");
|
||||
jobject ievent = env->GetObjectField(item, eventF);
|
||||
|
||||
jobject globalEvent = nullptr;
|
||||
jmethodID onClickMid = nullptr;
|
||||
if (ievent) {
|
||||
globalEvent = env->NewGlobalRef(ievent);
|
||||
jclass evc = env->GetObjectClass(ievent);
|
||||
onClickMid = env->GetMethodID(evc, "onClick", "(J)V");
|
||||
}
|
||||
|
||||
MenuItemData mid;
|
||||
mid.menuId = 2000 + i;
|
||||
mid.eventObjGlobal = globalEvent;
|
||||
mid.onClickMethod = onClickMid;
|
||||
mid.title = title;
|
||||
td->menuItems.push_back(mid);
|
||||
++parsed;
|
||||
}
|
||||
wchar_t buf[128];
|
||||
swprintf_s(buf, L"[registerEx] parsed menu items = %d", (int)parsed);
|
||||
//DEBUG_LOG(buf);
|
||||
}
|
||||
|
||||
{
|
||||
//std::lock_guard<std::mutex> lk(gTrayListMutex);
|
||||
gTrayList.push_back(td);
|
||||
}
|
||||
|
||||
td->threadHandle = CreateThread(NULL, 0, TrayMessageThreadProc, td, 0, &td->threadId);
|
||||
if (!td->threadHandle) {
|
||||
//DEBUG_LOG(L"CreateThread failed (registerEx)");
|
||||
// 清理
|
||||
if (td->eventObjGlobal) {
|
||||
env->DeleteGlobalRef(td->eventObjGlobal);
|
||||
td->eventObjGlobal = NULL;
|
||||
}
|
||||
for (auto& mi : td->menuItems) {
|
||||
if (mi.eventObjGlobal) {
|
||||
env->DeleteGlobalRef(mi.eventObjGlobal);
|
||||
mi.eventObjGlobal = NULL;
|
||||
}
|
||||
}
|
||||
td->menuItems.clear();
|
||||
delete td;
|
||||
return (jlong)0;
|
||||
}
|
||||
|
||||
return (jlong)td->trayId;
|
||||
}
|
||||
|
||||
/*
|
||||
* public static native void unregister(long id);
|
||||
*/
|
||||
JNIEXPORT void JNICALL Java_com_axis_innovators_box_tools_RegisterTray_unregister
|
||||
(JNIEnv* env, jclass clazz, jlong id) {
|
||||
const UINT trayId = static_cast<UINT>(id);
|
||||
TrayData* pData = FindTrayData(trayId);
|
||||
|
||||
if (!pData) {
|
||||
OutputDebugStringW(L"[unregister] 找不到对应的托盘数据");
|
||||
(JNIEnv* env, jclass, jlong id) {
|
||||
UINT trayId = (UINT)id;
|
||||
TrayData* td = nullptr;
|
||||
{
|
||||
//std::lock_guard<std::mutex> lk(gTrayListMutex);
|
||||
auto it = std::find_if(gTrayList.begin(), gTrayList.end(), [trayId](TrayData* t) { return t->trayId == trayId; });
|
||||
if (it != gTrayList.end()) {
|
||||
td = *it;
|
||||
gTrayList.erase(it);
|
||||
}
|
||||
}
|
||||
if (!td) {
|
||||
DEBUG_LOG(L"[unregister] 找不到对应的托盘数据");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 删除系统托盘图标
|
||||
NOTIFYICONDATAW nid = { sizeof(NOTIFYICONDATAW) };
|
||||
nid.hWnd = pData->hwnd;
|
||||
nid.uID = trayId;
|
||||
|
||||
if (!Shell_NotifyIconW(NIM_DELETE, &nid)) {
|
||||
DWORD err = GetLastError();
|
||||
wchar_t errMsg[256];
|
||||
swprintf_s(errMsg, _countof(errMsg), L"[unregister] 销毁窗口失败 (错误码: 0x%08X)", err);
|
||||
OutputDebugStringW(errMsg);
|
||||
// 发布消息让线程结束(销毁窗口会退出消息循环)
|
||||
if (td->hwnd) {
|
||||
PostMessageW(td->hwnd, WM_CLOSE, 0, 0);
|
||||
}
|
||||
|
||||
// 2. 释放图标资源
|
||||
if (pData->hIcon) {
|
||||
if (!DestroyIcon(pData->hIcon)) {
|
||||
OutputDebugStringW(L"[unregister] 销毁图标失败");
|
||||
// 等待线程退出
|
||||
if (td->threadHandle) {
|
||||
if (GetCurrentThreadId() != td->threadId) {
|
||||
DWORD waitRes = WaitForSingleObject(td->threadHandle, 5000);
|
||||
if (waitRes == WAIT_TIMEOUT) {
|
||||
DEBUG_LOG(L"[unregister] WaitForSingleObject 超时");
|
||||
}
|
||||
}
|
||||
else {
|
||||
OutputDebugStringW(L"[unregister] 图标资源已释放");
|
||||
for (int i = 0; i < 100 && td->running.load(); ++i) Sleep(10);
|
||||
}
|
||||
pData->hIcon = NULL;
|
||||
}
|
||||
|
||||
// 3. 销毁菜单
|
||||
if (pData->hMenu) {
|
||||
if (!DestroyMenu(pData->hMenu)) {
|
||||
OutputDebugStringW(L"[unregister] 销毁菜单失败");
|
||||
CloseHandle(td->threadHandle);
|
||||
td->threadHandle = NULL;
|
||||
td->threadId = 0;
|
||||
}
|
||||
else {
|
||||
OutputDebugStringW(L"[unregister] 菜单已销毁");
|
||||
}
|
||||
pData->hMenu = NULL;
|
||||
for (int i = 0; i < 50 && td->running.load(); ++i) Sleep(20);
|
||||
}
|
||||
|
||||
// 4. 销毁窗口
|
||||
if (pData->hwnd) {
|
||||
if (!DestroyWindow(pData->hwnd)) {
|
||||
DWORD err = GetLastError();
|
||||
wchar_t errMsg[256];
|
||||
swprintf_s(errMsg, _countof(errMsg), // 修改点
|
||||
L"[unregister] 销毁窗口失败 (错误码: 0x%08X)",
|
||||
err);
|
||||
OutputDebugStringW(errMsg);
|
||||
}
|
||||
else {
|
||||
OutputDebugStringW(L"[unregister] 窗口已销毁");
|
||||
}
|
||||
pData->hwnd = NULL;
|
||||
// 删除通知区图标(保险)
|
||||
NOTIFYICONDATAW nid = { 0 };
|
||||
nid.cbSize = sizeof(nid);
|
||||
nid.hWnd = td->hwnd;
|
||||
nid.uID = td->trayId;
|
||||
Shell_NotifyIconW(NIM_DELETE, &nid);
|
||||
|
||||
// 释放 icon
|
||||
if (td->hIcon) {
|
||||
DestroyIcon(td->hIcon);
|
||||
td->hIcon = NULL;
|
||||
}
|
||||
|
||||
// 5. 释放JNI全局引用
|
||||
if (pData->eventObj) {
|
||||
env->DeleteGlobalRef(pData->eventObj);
|
||||
pData->eventObj = NULL;
|
||||
OutputDebugStringW(L"[unregister] 事件全局引用已释放");
|
||||
// 删除全局引用
|
||||
if (td->eventObjGlobal) {
|
||||
env->DeleteGlobalRef(td->eventObjGlobal);
|
||||
td->eventObjGlobal = NULL;
|
||||
}
|
||||
|
||||
// 6. 释放菜单项全局引用
|
||||
for (auto& item : pData->menuItems) {
|
||||
if (item.eventObj) {
|
||||
env->DeleteGlobalRef(item.eventObj);
|
||||
item.eventObj = NULL;
|
||||
for (auto& mi : td->menuItems) {
|
||||
if (mi.eventObjGlobal) {
|
||||
env->DeleteGlobalRef(mi.eventObjGlobal);
|
||||
mi.eventObjGlobal = NULL;
|
||||
}
|
||||
}
|
||||
pData->menuItems.clear();
|
||||
OutputDebugStringW(L"[unregister] 菜单项资源已清理");
|
||||
td->menuItems.clear();
|
||||
|
||||
// 7. 从全局列表移除
|
||||
auto it = std::remove_if(trayDataList.begin(), trayDataList.end(),
|
||||
[pData](TrayData* data) { return data == pData; });
|
||||
|
||||
if (it != trayDataList.end()) {
|
||||
trayDataList.erase(it, trayDataList.end());
|
||||
OutputDebugStringW(L"[unregister] 已从全局列表移除");
|
||||
if (td->hwnd) {
|
||||
DestroyWindow(td->hwnd);
|
||||
td->hwnd = NULL;
|
||||
}
|
||||
|
||||
// 8. 释放内存
|
||||
delete pData;
|
||||
OutputDebugStringW(L"[unregister] 内存已释放");
|
||||
|
||||
// 9. 强制重绘任务栏
|
||||
HWND taskbar = FindWindowW(L"Shell_TrayWnd", NULL);
|
||||
if (taskbar) {
|
||||
RedrawWindow(taskbar, NULL, NULL,
|
||||
RDW_INVALIDATE | RDW_ERASE | RDW_ALLCHILDREN);
|
||||
}
|
||||
delete td;
|
||||
DEBUG_LOG(L"[unregister] 已完成清理");
|
||||
}
|
||||
|
||||
BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
// DllMain
|
||||
BOOL APIENTRY DllMain(HMODULE, DWORD, LPVOID) {
|
||||
return TRUE;
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
package com.axis.innovators.box;
|
||||
|
||||
import com.axis.innovators.box.browser.WindowRegistry;
|
||||
import com.axis.innovators.box.events.GlobalEventBus;
|
||||
import com.axis.innovators.box.events.OpenFileEvents;
|
||||
import com.axis.innovators.box.events.StartupEvent;
|
||||
import com.axis.innovators.box.events.TopicsUpdateEvents;
|
||||
import com.axis.innovators.box.util.WindowsTheme;
|
||||
import com.axis.innovators.box.window.*;
|
||||
import com.axis.innovators.box.plugins.PluginDescriptor;
|
||||
import com.axis.innovators.box.plugins.PluginLoader;
|
||||
@@ -12,12 +15,17 @@ import com.axis.innovators.box.register.RegistrationSettingsItem;
|
||||
import com.axis.innovators.box.register.RegistrationTool;
|
||||
import com.axis.innovators.box.register.RegistrationTopic;
|
||||
import com.axis.innovators.box.tools.*;
|
||||
import com.axis.innovators.box.tools.Crypto.AESCryptoUtil;
|
||||
import com.axis.innovators.box.tools.Crypto.Base64CryptoUtil;
|
||||
import com.axis.innovators.box.util.PythonResult;
|
||||
import com.axis.innovators.box.util.Tray;
|
||||
import com.axis.innovators.box.util.UserLocalInformation;
|
||||
import com.axis.innovators.box.verification.UserTags;
|
||||
import com.axis.innovators.box.verification.LoginResult;
|
||||
import com.axis.innovators.box.verification.CasdoorServer;
|
||||
import com.axis.innovators.box.verification.LoginData;
|
||||
import com.formdev.flatlaf.themes.FlatMacDarkLaf;
|
||||
import com.formdev.flatlaf.themes.FlatMacLightLaf;
|
||||
import com.jogamp.nativewindow.swt.SWTAccessor;
|
||||
import com.sun.javafx.stage.WindowHelper;
|
||||
import com.sun.management.HotSpotDiagnosticMXBean;
|
||||
import mdlaf.MaterialLookAndFeel;
|
||||
import mdlaf.themes.JMarsDarkTheme;
|
||||
@@ -31,6 +39,7 @@ import org.apache.logging.log4j.core.appender.FileAppender;
|
||||
import org.apache.logging.log4j.core.appender.RollingFileAppender;
|
||||
import org.apache.logging.log4j.core.config.Configuration;
|
||||
import org.api.dog.agent.VirtualMachine;
|
||||
import org.casbin.casdoor.entity.User;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.swing.*;
|
||||
@@ -39,9 +48,12 @@ import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.event.WindowAdapter;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.beans.PropertyChangeEvent;
|
||||
import java.beans.PropertyChangeListener;
|
||||
import java.io.*;
|
||||
import java.lang.instrument.Instrumentation;
|
||||
import java.lang.management.*;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.List;
|
||||
@@ -56,9 +68,12 @@ import java.util.zip.ZipOutputStream;
|
||||
*/
|
||||
public class AxisInnovatorsBox {
|
||||
private static final Logger logger = LogManager.getLogger(AxisInnovatorsBox.class);
|
||||
private static final String VERSIONS = "0.1.2";
|
||||
private static final String VERSIONS = "0.2.2";
|
||||
private static final String[] AUTHOR = new String[]{
|
||||
"tzdwindows 7"
|
||||
"tzdwindows 7",
|
||||
"lyxyz5223",
|
||||
"\uD83D\uDC3EMr. Liu\uD83D\uDC3E",
|
||||
"泽钰"
|
||||
};
|
||||
|
||||
/** 我是总任务数 **/
|
||||
@@ -69,6 +84,7 @@ public class AxisInnovatorsBox {
|
||||
LanguageManager.getLoadedLanguages().getText("progressBarManager.title"),
|
||||
totalTasks);
|
||||
private static AxisInnovatorsBox main;
|
||||
private final boolean quickStart;
|
||||
private MainWindow ex;
|
||||
private Thread thread;
|
||||
private final String[] args;
|
||||
@@ -77,17 +93,90 @@ public class AxisInnovatorsBox {
|
||||
private final RegistrationTopic registrationTopic = new RegistrationTopic(this);
|
||||
private final List<WindowsJDialog> windowsJDialogList = new ArrayList<>();
|
||||
private final StateManager stateManager = new StateManager();
|
||||
private UserTags userTags;
|
||||
private final boolean isDebug;
|
||||
private static DebugWindow debugWindow;
|
||||
|
||||
public AxisInnovatorsBox(String[] args, boolean isDebug) {
|
||||
private static LoginData loginData;
|
||||
|
||||
public AxisInnovatorsBox(String[] args, boolean isDebug, boolean quickStart) {
|
||||
this.args = args;
|
||||
this.isDebug = isDebug;
|
||||
this.quickStart = quickStart;
|
||||
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||
organizingCrashReports(throwable instanceof Exception ?
|
||||
(Exception) throwable : new Exception(throwable));
|
||||
});
|
||||
|
||||
if (quickStart){
|
||||
initLog4j2();
|
||||
setTopic();
|
||||
main = this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前环境是否为快速启动环境
|
||||
*/
|
||||
public boolean getQuickStart() {
|
||||
return quickStart;
|
||||
}
|
||||
|
||||
/**
|
||||
* 弹出登录窗口
|
||||
*/
|
||||
private void popupLogin() {
|
||||
// 加载登录信息,如果没有,弹出登录弹窗,后续可以删掉默认弹出
|
||||
// TODO: login window should not be show when AxisInnovatorsBox initialize,
|
||||
// it should be show when user click login button.
|
||||
try {
|
||||
String excryptedKey = "loginToken";
|
||||
try {
|
||||
excryptedKey = Base64CryptoUtil.base64Encode(excryptedKey);
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to encrypt key", e);
|
||||
}
|
||||
String encryptedToken = stateManager.getState(excryptedKey);
|
||||
String token = null;
|
||||
if (encryptedToken != null && !encryptedToken.isEmpty()) {
|
||||
try {
|
||||
token = AESCryptoUtil.decrypt(encryptedToken);
|
||||
} catch (Exception ex) {
|
||||
logger.error("Token 解密失败", ex);
|
||||
token = null;
|
||||
}
|
||||
}
|
||||
if (token == null || token.isEmpty()) {
|
||||
LoginResult loginResult = CasdoorLoginWindow.showLoginDialogAndGetLoginResult();
|
||||
if (loginResult == null) {
|
||||
// 用户取消登录
|
||||
JOptionPane.showMessageDialog(null, "取消登录", "登录",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
} else if (loginResult.success()) {
|
||||
loginData = loginResult.loginData();
|
||||
String encrypted = AESCryptoUtil.encrypt(loginResult.token());
|
||||
stateManager.saveState(excryptedKey, encrypted);
|
||||
logger.info(
|
||||
"Login result: token: " + loginResult.token() + ", user: " + loginResult.user());
|
||||
JOptionPane.showMessageDialog(null, "登录成功", "登录",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
} else {
|
||||
// 登录失败,弹出错误提醒,这里只是输出登录错误信息
|
||||
logger.error("Login error: " + loginResult.message());
|
||||
JOptionPane.showMessageDialog(null, "登录失败: \n" + loginResult.message(), "登录失败",
|
||||
JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
} else {
|
||||
CasdoorServer casdoorServer = new CasdoorServer();
|
||||
User user = casdoorServer.parseJwtToken(token);
|
||||
loginData = new LoginData(token, user);
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
logger.error("InterruptedException: Failed to load login information", e);
|
||||
} catch (InvocationTargetException e) {
|
||||
logger.error("InvocationTargetException: Failed to load login information", e);
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception: Failed to load login information", e);
|
||||
} // 添加了更详细的异常处理
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,8 +193,9 @@ public class AxisInnovatorsBox {
|
||||
LibraryLoad.loadLibrary("FridaNative");
|
||||
LibraryLoad.loadLibrary("ThrowSafely");
|
||||
LibraryLoad.loadLibrary("DogAgent");
|
||||
LibraryLoad.loadLibrary("RegisterTray");
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to load the 'FridaNative' library", e);
|
||||
logger.error("Failed to load library", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,6 +228,7 @@ public class AxisInnovatorsBox {
|
||||
* 组织崩溃报告
|
||||
*/
|
||||
public void organizingCrashReports(Exception e) {
|
||||
e.printStackTrace();
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
String systemOut = Log4j2OutputStream.systemOutContent.toString();
|
||||
String systemErr = Log4j2OutputStream.systemErrContent.toString();
|
||||
@@ -629,6 +720,28 @@ public class AxisInnovatorsBox {
|
||||
return tempFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行监控主题
|
||||
*/
|
||||
public void runMonitorTopics(){
|
||||
WindowsTheme.setThemeChangeListener((themeName, darkTheme) -> {
|
||||
logger.info("主题变更: {}, 暗主题: {}", themeName, darkTheme);
|
||||
if (registrationTopic.isDarkMode() == darkTheme){
|
||||
return;
|
||||
}
|
||||
try {
|
||||
updateTheme(themeName,darkTheme);
|
||||
} catch (UnsupportedLookAndFeelException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
if (SwingUtilities.isEventDispatchThread()) {
|
||||
reloadAllWindow();
|
||||
} else {
|
||||
SwingUtilities.invokeLater(this::reloadAllWindow);
|
||||
}
|
||||
});
|
||||
}
|
||||
private void addFileToZip(ZipOutputStream zos, File file, String entryPath) throws IOException {
|
||||
if (!file.exists()) {
|
||||
return;
|
||||
@@ -720,7 +833,7 @@ public class AxisInnovatorsBox {
|
||||
private void setTopic() {
|
||||
try {
|
||||
|
||||
boolean isDarkMode = SystemInfoUtil.isSystemDarkMode();
|
||||
boolean isDarkMode = WindowsTheme.isDarkTheme();
|
||||
|
||||
// 1. 默认系统主题
|
||||
main.registrationTopic.addTopic(
|
||||
@@ -840,14 +953,32 @@ public class AxisInnovatorsBox {
|
||||
true
|
||||
);
|
||||
|
||||
//main.registrationTopic.addTopic(
|
||||
// new BlurTopic(),
|
||||
// LanguageManager.getLoadedLanguages().getText("blur.system.topicName"),
|
||||
// LanguageManager.getLoadedLanguages().getText("blur.default.tip"),
|
||||
// LoadIcon.loadIcon(MainWindow.class, "logo.png", 64),
|
||||
// "system:blur",true
|
||||
//);
|
||||
|
||||
updateTheme("",isDarkMode);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to load the system facade class", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
* @param themeName 主题名称
|
||||
* @param isDarkMode 是否是暗主题
|
||||
*/
|
||||
public void updateTheme(String themeName,boolean isDarkMode) throws UnsupportedLookAndFeelException {
|
||||
LookAndFeel defaultLaf = isDarkMode ? new FlatMacDarkLaf() : new FlatMacLightLaf();
|
||||
UIManager.setLookAndFeel(defaultLaf);
|
||||
main.registrationTopic.setLoading(
|
||||
isDarkMode ? "system:flatMacDark_theme" : "system:flatMacLight_theme"
|
||||
);
|
||||
} catch (Exception e) {
|
||||
logger.warn("Failed to load the system facade class", e);
|
||||
}
|
||||
GlobalEventBus.EVENT_BUS.post(new TopicsUpdateEvents(themeName,isDarkMode));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -893,6 +1024,7 @@ public class AxisInnovatorsBox {
|
||||
windowsJDialog.revalidate();
|
||||
windowsJDialog.repaint();
|
||||
}
|
||||
WindowRegistry.getInstance().update();
|
||||
ex.initUI();
|
||||
ex.updateTheme();
|
||||
ex.revalidate();
|
||||
@@ -926,25 +1058,48 @@ public class AxisInnovatorsBox {
|
||||
debugWindow.setVisible(true);
|
||||
}
|
||||
|
||||
public static void run(String[] args, boolean isDebug) {
|
||||
public static void run(String[] args, boolean isDebug, boolean quickStart) {
|
||||
Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
|
||||
if (main != null) {
|
||||
main.organizingCrashReports(throwable instanceof Exception ?
|
||||
(Exception) throwable : new Exception(throwable));
|
||||
} else {
|
||||
new AxisInnovatorsBox(args, isDebug)
|
||||
new AxisInnovatorsBox(args, isDebug,true)
|
||||
.organizingCrashReports(throwable instanceof Exception ?
|
||||
(Exception) throwable : new Exception(throwable));
|
||||
}
|
||||
});
|
||||
|
||||
// 设置EDT(事件调度线程)的异常处理器
|
||||
// Set the exception handler for EDT(event dispatcher thread)
|
||||
System.setProperty("sun.awt.exception.handler", EDTCrashHandler.class.getName());
|
||||
|
||||
main = new AxisInnovatorsBox(args,isDebug);
|
||||
// Check if AxisInnovatorsBox is started
|
||||
// If it's started, and it's not a quick start, don't allow it
|
||||
// because it's already started
|
||||
// Stop loading if the current running context is the quickStart context
|
||||
if (AxisInnovatorsBox.getMain() != null
|
||||
&& !AxisInnovatorsBox.getMain().getQuickStart() || quickStart) {
|
||||
// Manually created if it is a quickStart context and the AxisInnovatorsBox instance in the context is empty
|
||||
if (AxisInnovatorsBox.getMain() == null && quickStart) {
|
||||
new AxisInnovatorsBox(args,isDebug,true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
main = new AxisInnovatorsBox(args,isDebug,false);
|
||||
try {
|
||||
main.initLog4j2();
|
||||
main.setTopic();
|
||||
main.runMonitorTopics();
|
||||
//main.popupLogin();
|
||||
main.thread = new Thread(() -> {
|
||||
try {
|
||||
// 主任务1:加载插件
|
||||
logger.info("Loaded plugins Started");
|
||||
main.progressBarManager.updateMainProgress(++main.completedTasks);
|
||||
PluginLoader.loadPlugins();
|
||||
PluginPyLoader.loadAllPlugins();
|
||||
logger.info("Loaded plugins End");
|
||||
|
||||
List<Map<String, String>> validFiles = ArgsParser.parseArgs(args);
|
||||
for (Map<String, String> fileInfo : validFiles) {
|
||||
@@ -957,23 +1112,6 @@ public class AxisInnovatorsBox {
|
||||
}
|
||||
}
|
||||
|
||||
main.thread = new Thread(() -> {
|
||||
try {
|
||||
UserLocalInformation userLocalInformation = new UserLocalInformation(main);
|
||||
main.userTags = userLocalInformation.getUserTags();
|
||||
if (main.userTags == null) {
|
||||
// 登录窗口
|
||||
main.userTags = LoginWindow.createAndShow();
|
||||
userLocalInformation.setUserTags(main.userTags);
|
||||
}
|
||||
|
||||
// 主任务1:加载插件
|
||||
logger.info("Loaded plugins Started");
|
||||
main.progressBarManager.updateMainProgress(++main.completedTasks);
|
||||
PluginLoader.loadPlugins();
|
||||
PluginPyLoader.loadAllPlugins();
|
||||
logger.info("Loaded plugins End");
|
||||
|
||||
main.progressBarManager.close();
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
@@ -1052,9 +1190,16 @@ public class AxisInnovatorsBox {
|
||||
}
|
||||
|
||||
ex.initUI();
|
||||
RegistrationSettingsItem.applyAllSettings();
|
||||
isWindow = true;
|
||||
ex.setVisible(true);
|
||||
|
||||
Toolkit.getDefaultToolkit().addPropertyChangeListener("win.xpstyle.themeName",
|
||||
evt -> {
|
||||
logger.info("系统主题发生变化: {}", evt.getNewValue());
|
||||
ex.updateTheme();
|
||||
});
|
||||
|
||||
if (isDebug) {
|
||||
SwingUtilities.invokeLater(this::createDebugWindow);
|
||||
}
|
||||
@@ -1092,13 +1237,6 @@ public class AxisInnovatorsBox {
|
||||
return AUTHOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户标签
|
||||
* @return 用户标签
|
||||
*/
|
||||
public UserTags getUserTags() {
|
||||
return userTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态管理器
|
||||
|
||||
@@ -6,33 +6,121 @@ import org.apache.logging.log4j.Logger;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 将输出传递给 Log4j2 的日志记录器
|
||||
* 将输出传递给 Log4j2 的日志记录器,同时保持控制台输出
|
||||
* 修复问题:控制台输出被Log4j2覆盖
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class Log4j2OutputStream extends OutputStream {
|
||||
private static final Logger logger = LogManager.getLogger();
|
||||
|
||||
// 恢复静态变量
|
||||
public static final ByteArrayOutputStream systemOutContent = new ByteArrayOutputStream();
|
||||
public static final ByteArrayOutputStream systemErrContent = new ByteArrayOutputStream();
|
||||
|
||||
private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
|
||||
private final boolean isErrorStream;
|
||||
private final PrintStream originalStream; // 保存原始的控制台流
|
||||
|
||||
public Log4j2OutputStream(boolean isErrorStream, PrintStream originalStream) {
|
||||
this.isErrorStream = isErrorStream;
|
||||
this.originalStream = originalStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) {
|
||||
// 写入原始控制台
|
||||
originalStream.write(b);
|
||||
|
||||
buffer.write(b);
|
||||
// 将内容同时写入对应的静态变量
|
||||
if (isErrorStream) {
|
||||
systemErrContent.write(b);
|
||||
} else {
|
||||
systemOutContent.write(b);
|
||||
logger.info(String.valueOf((char) b));
|
||||
}
|
||||
|
||||
// 遇到换行符时刷新缓冲区到日志
|
||||
if (b == '\n') {
|
||||
flush();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) {
|
||||
// 写入原始控制台
|
||||
originalStream.write(b, off, len);
|
||||
|
||||
buffer.write(b, off, len);
|
||||
// 将内容同时写入对应的静态变量
|
||||
if (isErrorStream) {
|
||||
systemErrContent.write(b, off, len);
|
||||
} else {
|
||||
systemOutContent.write(b, off, len);
|
||||
String message = new String(b, off, len).trim();
|
||||
}
|
||||
|
||||
// 检查是否包含换行符
|
||||
for (int i = off; i < off + len; i++) {
|
||||
if (b[i] == '\n') {
|
||||
flush();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
originalStream.flush();
|
||||
|
||||
String message = buffer.toString(StandardCharsets.UTF_8).trim();
|
||||
if (!message.isEmpty()) {
|
||||
if (isErrorStream) {
|
||||
logger.error(message);
|
||||
} else {
|
||||
logger.info(message);
|
||||
}
|
||||
}
|
||||
buffer.reset(); // 清空缓冲区
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
flush();
|
||||
originalStream.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重定向 System.out 和 System.err 到 Log4j2
|
||||
* 重定向 System.out 和 System.err 到 Log4j2,同时保持控制台输出
|
||||
*/
|
||||
public static void redirectSystemStreams() {
|
||||
System.setOut(new PrintStream(new Log4j2OutputStream(), true));
|
||||
System.setErr(new PrintStream(new Log4j2OutputStream(), true));
|
||||
// 保存原始流
|
||||
PrintStream originalOut = System.out;
|
||||
PrintStream originalErr = System.err;
|
||||
|
||||
// System.out 使用 INFO 级别,同时输出到原始控制台
|
||||
System.setOut(new PrintStream(new Log4j2OutputStream(false, originalOut), true, StandardCharsets.UTF_8));
|
||||
// System.err 使用 ERROR 级别,同时输出到原始控制台
|
||||
System.setErr(new PrintStream(new Log4j2OutputStream(true, originalErr), true, StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空静态缓冲区内容
|
||||
*/
|
||||
public static void clearBuffers() {
|
||||
systemOutContent.reset();
|
||||
systemErrContent.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取输出内容
|
||||
*/
|
||||
public static String getSystemOutContent() {
|
||||
return systemOutContent.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
public static String getSystemErrContent() {
|
||||
return systemErrContent.toString(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import com.axis.innovators.box.register.LanguageManager;
|
||||
import com.axis.innovators.box.tools.ArgsParser;
|
||||
import com.axis.innovators.box.tools.FolderCleaner;
|
||||
import com.axis.innovators.box.tools.FolderCreator;
|
||||
import org.QQdecryption.ui.DecryptionUI;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.io.File;
|
||||
@@ -14,8 +15,7 @@ import java.io.RandomAccessFile;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.channels.OverlappingFileLockException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
@@ -26,33 +26,49 @@ public class Main {
|
||||
private static FileLock lock = null;
|
||||
private static RandomAccessFile lockFile = null;
|
||||
private static FileChannel lockChannel = null;
|
||||
|
||||
private final static boolean releaseEnvironments = false;
|
||||
public static void main(String[] args) {
|
||||
// 清理日志文件(最大日志为10)
|
||||
FolderCleaner.cleanFolder(FolderCreator.getLogsFolder(), 10);
|
||||
|
||||
// 加载保存的语言
|
||||
LanguageManager.loadSavedLanguage();
|
||||
// 如果没有加载的语言,则加载默认的语言
|
||||
if (LanguageManager.getLoadedLanguages() == null) {
|
||||
LanguageManager.loadLanguage("system:zh_CN");
|
||||
}
|
||||
|
||||
// 检查是否包含调试控制台参数
|
||||
boolean debugWindowEnabled = false;
|
||||
for (int i = 0; i < args.length; i++) {
|
||||
if ("-debugControlWindow-on".equals(args[i])) {
|
||||
String pluginsDirectory = null;
|
||||
|
||||
List<String> remainingArgs = new ArrayList<>();
|
||||
|
||||
for (String arg : args) {
|
||||
if (!releaseEnvironments && "-debugControlWindow-on".equals(arg)) {
|
||||
debugWindowEnabled = true;
|
||||
// 移除此参数避免干扰后续处理
|
||||
String[] newArgs = new String[args.length - 1];
|
||||
System.arraycopy(args, 0, newArgs, 0, i);
|
||||
System.arraycopy(args, i + 1, newArgs, i, args.length - i - 1);
|
||||
args = newArgs;
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
List<Map<String, String>> validFiles = ArgsParser.parseArgs(args);
|
||||
if (arg.startsWith("pluginsDirectory=")) {
|
||||
pluginsDirectory = arg.substring("pluginsDirectory=".length());
|
||||
if (pluginsDirectory.startsWith("\"") && pluginsDirectory.endsWith("\"")) {
|
||||
pluginsDirectory = pluginsDirectory.substring(1, pluginsDirectory.length() - 1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
remainingArgs.add(arg);
|
||||
}
|
||||
|
||||
if (pluginsDirectory != null && !pluginsDirectory.isEmpty()) {
|
||||
System.out.println("Setting up the plugin directory: " + pluginsDirectory);
|
||||
FolderCreator.setPluginPath(pluginsDirectory);
|
||||
}
|
||||
|
||||
boolean quickStart = false;
|
||||
|
||||
String[] processedArgs = remainingArgs.toArray(new String[0]);
|
||||
|
||||
final Set<String> mUSICEXTS = new HashSet<>(Arrays.asList(
|
||||
".mflac", ".mgg", ".qmc0", ".qmc3", ".qmcflac", ".qmcogg",
|
||||
".tkm", ".qmc2", ".bkcmp3", ".bkcflac", ".ogg"
|
||||
));
|
||||
List<Map<String, String>> validFiles = ArgsParser.parseArgs(processedArgs);
|
||||
for (Map<String, String> fileInfo : validFiles) {
|
||||
String extension = fileInfo.get("extension");
|
||||
String path = fileInfo.get("path");
|
||||
@@ -60,26 +76,35 @@ public class Main {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
try {
|
||||
UIManager.setLookAndFeel(new com.formdev.flatlaf.FlatDarculaLaf());
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
ModernJarViewer viewer = new ModernJarViewer(null, path);
|
||||
viewer.setVisible(true);
|
||||
});
|
||||
releaseLock(); // 释放锁(窗口模式)
|
||||
return;
|
||||
quickStart = true;
|
||||
}
|
||||
|
||||
if (".html".equals(extension)) {
|
||||
MainApplication.popupHTMLWindow(path);
|
||||
releaseLock();
|
||||
return;
|
||||
quickStart = true;
|
||||
}
|
||||
|
||||
if (extension != null && mUSICEXTS.contains(extension.toLowerCase(Locale.ROOT))) {
|
||||
final String p = path;
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
DecryptionUI ui = new DecryptionUI(p);
|
||||
ui.setVisible(true);
|
||||
});
|
||||
releaseLock();
|
||||
quickStart = true;
|
||||
}
|
||||
}
|
||||
if (!acquireLock()) {
|
||||
return;
|
||||
}
|
||||
AxisInnovatorsBox.run(args, debugWindowEnabled);
|
||||
|
||||
AxisInnovatorsBox.run(processedArgs, debugWindowEnabled,quickStart);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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;
|
||||
@@ -8,9 +11,11 @@ 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 javax.swing.*;
|
||||
@@ -218,7 +223,6 @@ public class BrowserWindow extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private Component initializeCef(Builder builder) throws MalformedURLException {
|
||||
if (!isInitialized) {
|
||||
isInitialized = true;
|
||||
@@ -289,6 +293,20 @@ public class BrowserWindow extends JFrame {
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -408,6 +426,24 @@ public class BrowserWindow extends JFrame {
|
||||
}
|
||||
});
|
||||
|
||||
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(
|
||||
BrowserWindow.this,
|
||||
message_text,
|
||||
"警告",
|
||||
JOptionPane.INFORMATION_MESSAGE
|
||||
);
|
||||
});
|
||||
callback.Continue(true, "");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 3. 拦截所有新窗口(关键修复点!)
|
||||
@@ -468,6 +504,7 @@ public class BrowserWindow extends JFrame {
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
|
||||
updateTheme();
|
||||
|
||||
// 6. 配置窗口布局(确保只添加一次)
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
@@ -539,6 +576,153 @@ public class BrowserWindow extends JFrame {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 1. 获取Java字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
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++) {
|
||||
@@ -551,6 +735,11 @@ public class BrowserWindow extends JFrame {
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b) {
|
||||
if (b) {
|
||||
if (browser != null) {
|
||||
updateTheme();
|
||||
}
|
||||
}
|
||||
super.setVisible(b);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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;
|
||||
@@ -8,10 +11,13 @@ 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.*;
|
||||
@@ -85,6 +91,7 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置链接打开方式
|
||||
*
|
||||
@@ -294,6 +301,20 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
@@ -413,6 +434,26 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
}
|
||||
});
|
||||
|
||||
// 添加 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. 拦截所有新窗口(关键修复点!)
|
||||
@@ -427,6 +468,7 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
|
||||
Thread.currentThread().setName("BrowserRenderThread");
|
||||
|
||||
|
||||
// 4. 加载HTML
|
||||
if (htmlUrl.isEmpty()) {
|
||||
String fileUrl = new File(htmlPath).toURI().toURL().toString();
|
||||
@@ -471,6 +513,8 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
}
|
||||
}
|
||||
|
||||
updateTheme();
|
||||
|
||||
CefMessageRouter.CefMessageRouterConfig config = new CefMessageRouter.CefMessageRouterConfig();
|
||||
config.jsQueryFunction = "javaQuery";// 定义方法
|
||||
config.jsCancelFunction = "javaQueryCancel";// 定义取消方法
|
||||
@@ -510,6 +554,8 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
dragPanel.add(titleBar, BorderLayout.NORTH);
|
||||
getContentPane().add(dragPanel, BorderLayout.CENTER);
|
||||
getContentPane().add(browserComponent, BorderLayout.CENTER);
|
||||
@@ -546,6 +592,148 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 更新主题
|
||||
*/
|
||||
public void updateTheme() {
|
||||
// 1. 获取Java字体信息
|
||||
String fontInfo = getSystemFontsInfo();
|
||||
injectFontInfoToPage(browser, fontInfo);
|
||||
|
||||
// 2. 注入主题信息
|
||||
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++) {
|
||||
@@ -558,6 +746,11 @@ public class BrowserWindowJDialog extends JDialog {
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean b) {
|
||||
if (b) {
|
||||
if (browser != null) {
|
||||
updateTheme();
|
||||
}
|
||||
}
|
||||
super.setVisible(b);
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +56,9 @@ public class CefAppManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Cef
|
||||
*/
|
||||
private static void initializeDefaultSettings() {
|
||||
initLock.lock();
|
||||
try {
|
||||
@@ -81,6 +84,9 @@ public class CefAppManager {
|
||||
String processType,
|
||||
CefCommandLine commandLine
|
||||
) {
|
||||
//commandLine.appendSwitch("disable-dev-tools");
|
||||
//commandLine.appendSwitch("disable-view-source");
|
||||
|
||||
LanguageManager.loadSavedLanguage();
|
||||
LanguageManager.Language currentLang = LanguageManager.getLoadedLanguages();
|
||||
if (currentLang != null){
|
||||
@@ -109,6 +115,10 @@ public class CefAppManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断地区主题是否为黑色主题
|
||||
* @return 是否
|
||||
*/
|
||||
private static boolean isDarkTheme() {
|
||||
if (AxisInnovatorsBox.getMain() == null){
|
||||
return false;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -57,6 +57,20 @@ public class WindowRegistry {
|
||||
return windows.get(windowId);
|
||||
}
|
||||
|
||||
public void update() {
|
||||
for (BrowserWindow window : windows.values()) {
|
||||
if (window != null) {
|
||||
window.updateTheme();
|
||||
}
|
||||
}
|
||||
|
||||
for (BrowserWindowJDialog window : childWindows.values()) {
|
||||
if (window != null) {
|
||||
window.updateTheme();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个新的窗口
|
||||
* @param windowId 窗口ID
|
||||
|
||||
@@ -21,6 +21,13 @@ public class CodeExecutor {
|
||||
void onOutput(String newOutput);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行代码
|
||||
* @param code 代码字符串
|
||||
* @param language 代码类型
|
||||
* @param listener 回调,代码的输出回调
|
||||
* @return 返回i执行结果
|
||||
*/
|
||||
public static String executeCode(String code, String language, OutputListener listener) {
|
||||
switch (language.toLowerCase()) {
|
||||
case "python":
|
||||
@@ -35,6 +42,10 @@ public class CodeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行Java代码
|
||||
* @return 返回执行结果
|
||||
*/
|
||||
public static String executeJavaCode(String code, OutputListener listener) {
|
||||
Path tempDir = null;
|
||||
try {
|
||||
@@ -176,6 +187,9 @@ public class CodeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行C代码
|
||||
*/
|
||||
private static String executeC(String code, OutputListener listener) {
|
||||
Path tempDir = null;
|
||||
Path cFile = null;
|
||||
@@ -316,63 +330,6 @@ public class CodeExecutor {
|
||||
}
|
||||
}
|
||||
|
||||
private static String captureProcessOutput(Process process, OutputListener listener)
|
||||
throws IOException {
|
||||
StringBuilder totalOutput = new StringBuilder();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream()))) {
|
||||
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
totalOutput.append(line).append("\n");
|
||||
if (listener != null) {
|
||||
listener.onOutput(line + "\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
return totalOutput.toString();
|
||||
}
|
||||
|
||||
// 输出监控线程
|
||||
private static class OutputMonitor implements Runnable {
|
||||
private final BufferedReader reader;
|
||||
private final OutputListener listener;
|
||||
private volatile boolean running = true;
|
||||
private final StringBuilder totalOutput = new StringBuilder();
|
||||
|
||||
public OutputMonitor(BufferedReader reader, OutputListener listener) {
|
||||
this.reader = reader;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
try {
|
||||
while (running) {
|
||||
if (reader.ready()) {
|
||||
String line = reader.readLine();
|
||||
if (line != null) {
|
||||
totalOutput.append(line).append("\n");
|
||||
if (listener != null) {
|
||||
listener.onOutput(line + "\n");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Thread.sleep(50);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
}
|
||||
|
||||
public String getTotalOutput() {
|
||||
return totalOutput.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用方法
|
||||
public static void main(String[] args) {
|
||||
|
||||
@@ -0,0 +1,400 @@
|
||||
package com.axis.innovators.box.browser.util;
|
||||
|
||||
import java.sql.*;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
|
||||
/**
|
||||
* 数据库连接管理器
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class DatabaseConnectionManager {
|
||||
private static final java.util.Map<String, Connection> connections = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
private static final java.util.Map<String, DatabaseInfo> connectionInfo = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
public static class DatabaseInfo {
|
||||
public String driver;
|
||||
public String url;
|
||||
public String host;
|
||||
public String port;
|
||||
public String database;
|
||||
public String username;
|
||||
|
||||
public DatabaseInfo(String driver, String url, String host, String port, String database, String username) {
|
||||
this.driver = driver;
|
||||
this.url = url;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
this.database = database;
|
||||
this.username = username;
|
||||
}
|
||||
}
|
||||
|
||||
public static String connect(String driver, String host, String port,
|
||||
String database, String username, String password) throws SQLException {
|
||||
String connectionId = "conn_" + System.currentTimeMillis();
|
||||
|
||||
String drv = driver == null ? "" : driver.toLowerCase();
|
||||
|
||||
// 规范化 database 路径(特别是 Windows 反斜杠问题)
|
||||
if (database != null) {
|
||||
database = database.replace("\\", "/");
|
||||
} else {
|
||||
database = "";
|
||||
}
|
||||
|
||||
// 先显式加载驱动,避免因为 classloader 问题找不到驱动
|
||||
try {
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
Class.forName("com.mysql.cj.jdbc.Driver");
|
||||
break;
|
||||
case "postgresql":
|
||||
Class.forName("org.postgresql.Driver");
|
||||
break;
|
||||
case "sqlite":
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
break;
|
||||
case "oracle":
|
||||
Class.forName("oracle.jdbc.OracleDriver");
|
||||
break;
|
||||
case "h2":
|
||||
Class.forName("org.h2.Driver");
|
||||
break;
|
||||
default:
|
||||
// 不抛出,使后续 URL 构造仍可检查类型
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("JDBC 驱动未找到,请确认对应驱动已加入 classpath: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
String url = buildConnectionUrl(driver, host, port, database);
|
||||
|
||||
Connection connection;
|
||||
Properties props = new Properties();
|
||||
if (username != null && !username.isEmpty()) props.setProperty("user", username);
|
||||
if (password != null && !password.isEmpty()) props.setProperty("password", password);
|
||||
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
props.setProperty("useSSL", "false");
|
||||
props.setProperty("serverTimezone", "UTC");
|
||||
props.setProperty("allowPublicKeyRetrieval", "true");
|
||||
props.setProperty("useUnicode", "true");
|
||||
props.setProperty("characterEncoding", "UTF-8");
|
||||
connection = DriverManager.getConnection(url, props);
|
||||
break;
|
||||
case "postgresql":
|
||||
connection = DriverManager.getConnection(url, props);
|
||||
break;
|
||||
case "sqlite":
|
||||
// sqlite 不需要 props,URL 已经是文件路径(已做过替换)
|
||||
connection = DriverManager.getConnection(url);
|
||||
break;
|
||||
case "oracle":
|
||||
connection = DriverManager.getConnection(url, props);
|
||||
break;
|
||||
case "h2":
|
||||
// H2 使用默认用户 sa / 空密码(如果需要可调整)
|
||||
connection = DriverManager.getConnection(url, "sa", "");
|
||||
break;
|
||||
default:
|
||||
throw new SQLException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
|
||||
connections.put(connectionId, connection);
|
||||
connectionInfo.put(connectionId, new DatabaseInfo(driver, url, host, port, database, username));
|
||||
return connectionId;
|
||||
}
|
||||
|
||||
public static void disconnect(String connectionId) throws SQLException {
|
||||
Connection connection = connections.get(connectionId);
|
||||
if (connection != null && !connection.isClosed()) {
|
||||
connection.close();
|
||||
}
|
||||
connections.remove(connectionId);
|
||||
connectionInfo.remove(connectionId);
|
||||
}
|
||||
|
||||
public static Connection getConnection(String connectionId) {
|
||||
return connections.get(connectionId);
|
||||
}
|
||||
|
||||
public static DatabaseInfo getConnectionInfo(String connectionId) {
|
||||
return connectionInfo.get(connectionId);
|
||||
}
|
||||
|
||||
private static String buildConnectionUrl(String driver, String host, String port, String database) {
|
||||
String drv = driver == null ? "" : driver.toLowerCase();
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
return "jdbc:mysql://" + host + ":" + port + "/" + database;
|
||||
case "postgresql":
|
||||
return "jdbc:postgresql://" + host + ":" + port + "/" + database;
|
||||
case "sqlite":
|
||||
// 对于 SQLite,database 可能是绝对路径或相对文件名,先把反斜杠替成正斜杠
|
||||
if (database == null || database.isEmpty()) {
|
||||
return "jdbc:sqlite::memory:";
|
||||
}
|
||||
String normalized = database.replace("\\", "/");
|
||||
// 如果看起来像相对文件名(不含冒号也不以 / 开头),则当作相对于用户目录的路径
|
||||
if (!normalized.contains(":") && !normalized.startsWith("/")) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
normalized = userHome + "/" + normalized;
|
||||
}
|
||||
return "jdbc:sqlite:" + normalized;
|
||||
case "oracle":
|
||||
return "jdbc:oracle:thin:@" + host + ":" + port + ":" + database;
|
||||
case "h2":
|
||||
// H2 文件路径同样做反斜杠处理
|
||||
if (database == null || database.isEmpty()) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
return "jdbc:h2:file:" + userHome + "/.axis_innovators_box/databases/h2db";
|
||||
} else {
|
||||
String norm = database.replace("\\", "/");
|
||||
// 如果传入仅是名字(无斜杠或冒号),则存到用户目录下
|
||||
if (!norm.contains("/") && !norm.contains(":")) {
|
||||
String userHome = System.getProperty("user.home").replace("\\", "/");
|
||||
norm = userHome + "/.axis_innovators_box/databases/" + norm;
|
||||
}
|
||||
return "jdbc:h2:file:" + norm;
|
||||
}
|
||||
default:
|
||||
throw new IllegalArgumentException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 在服务器上创建数据库(MySQL / PostgreSQL / Oracle(示例))
|
||||
* @param driver mysql | postgresql | oracle
|
||||
* @param host 数据库主机
|
||||
* @param port 端口
|
||||
* @param dbName 要创建的数据库名(或 schema 名)
|
||||
* @param adminUser 管理员用户名(用于创建数据库)
|
||||
* @param adminPassword 管理员密码
|
||||
* @return 如果创建成功返回一个简短消息,否则抛出 SQLException
|
||||
* @throws SQLException
|
||||
*/
|
||||
public static String createDatabaseOnServer(String driver, String host, String port,
|
||||
String dbName, String adminUser, String adminPassword) throws SQLException {
|
||||
if (driver == null) throw new SQLException("driver 不能为空");
|
||||
String drv = driver.toLowerCase().trim();
|
||||
|
||||
// 简单校验 dbName(避免注入)——只允许字母数字下划线
|
||||
if (dbName == null || !dbName.matches("[A-Za-z0-9_]+")) {
|
||||
throw new SQLException("不合法的数据库名: " + dbName);
|
||||
}
|
||||
|
||||
try {
|
||||
switch (drv) {
|
||||
case "mysql":
|
||||
// 加载驱动(如果尚未加载)
|
||||
try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("MySQL 驱动未找到,请加入 mysql-connector-java 到 classpath", e);
|
||||
}
|
||||
// 连接到服务器的默认库(不指定数据库)以执行 CREATE DATABASE
|
||||
String mysqlUrl = "jdbc:mysql://" + host + ":" + port + "/?useSSL=false&serverTimezone=UTC";
|
||||
try (Connection conn = DriverManager.getConnection(mysqlUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
String sql = "CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci";
|
||||
st.executeUpdate(sql);
|
||||
}
|
||||
return "MySQL 数据库创建成功: " + dbName;
|
||||
|
||||
case "postgresql":
|
||||
case "postgres":
|
||||
try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("Postgres 驱动未找到,请加入 postgresql 到 classpath", e);
|
||||
}
|
||||
// 连接到默认 postgres 数据库以创建新数据库
|
||||
String pgUrl = "jdbc:postgresql://" + host + ":" + port + "/postgres";
|
||||
try (Connection conn = DriverManager.getConnection(pgUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
String sql = "CREATE DATABASE " + dbName + " WITH ENCODING 'UTF8'";
|
||||
st.executeUpdate(sql);
|
||||
}
|
||||
return "PostgreSQL 数据库创建成功: " + dbName;
|
||||
|
||||
case "oracle":
|
||||
// Oracle 数据库“创建数据库”通常由 DBA 完成(复杂),这里示例创建用户/模式(更常见)
|
||||
try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) {
|
||||
throw new SQLException("Oracle 驱动未找到,请把 ojdbc.jar 加入 classpath", e);
|
||||
}
|
||||
// 需使用具有足够权限的账户(通常为 sys or system),并且 URL 需要正确(SID / ServiceName)
|
||||
// 下面示例假设通过 SID 连接: jdbc:oracle:thin:@host:port:SID
|
||||
String oracleUrl = "jdbc:oracle:thin:@" + host + ":" + port + ":" + "ORCL"; // 把 ORCL 换成实际 SID
|
||||
try (Connection conn = DriverManager.getConnection(oracleUrl, adminUser, adminPassword);
|
||||
Statement st = conn.createStatement()) {
|
||||
// 创建 user(schema)示例
|
||||
String pwd = adminPassword; // 实际应使用独立密码,不推荐用 adminPassword
|
||||
String createUser = "CREATE USER " + dbName + " IDENTIFIED BY \"" + pwd + "\"";
|
||||
String grant = "GRANT CONNECT, RESOURCE TO " + dbName;
|
||||
st.executeUpdate(createUser);
|
||||
st.executeUpdate(grant);
|
||||
} catch (SQLException ex) {
|
||||
// Oracle 操作更容易失败,给出提示
|
||||
throw new SQLException("Oracle: 无法创建用户/模式,请检查权限和 URL(通常需由 DBA 操作): " + ex.getMessage(), ex);
|
||||
}
|
||||
return "Oracle 用户/模式创建成功(注意:真正的 DB 实例通常由 DBA 管理): " + dbName;
|
||||
|
||||
default:
|
||||
throw new SQLException("不支持的数据库类型: " + driver);
|
||||
}
|
||||
} catch (SQLException se) {
|
||||
// 透传 SQLException,调用方会拿到 message 并反馈给前端
|
||||
throw se;
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("创建数据库时发生异常: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
public static String createLocalDatabase(String driver, String dbName) throws SQLException {
|
||||
switch (driver.toLowerCase()) {
|
||||
case "sqlite":
|
||||
// 创建目录并构造规范化路径(确保路径使用正斜杠)
|
||||
String dbFileName = dbName.endsWith(".db") ? dbName : (dbName + ".db");
|
||||
java.nio.file.Path dbDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases");
|
||||
try {
|
||||
java.nio.file.Files.createDirectories(dbDir);
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("无法创建数据库目录: " + e.getMessage(), e);
|
||||
}
|
||||
String dbPath = dbDir.resolve(dbFileName).toAbsolutePath().toString().replace("\\", "/");
|
||||
|
||||
// 显式加载 sqlite 驱动(避免 No suitable driver)
|
||||
try {
|
||||
Class.forName("org.sqlite.JDBC");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("未找到 sqlite 驱动,请确认 sqlite-jdbc 已加入 classpath", e);
|
||||
}
|
||||
|
||||
// 直接使用 connect 构建连接(connect 中会通过 buildConnectionUrl 处理 path)
|
||||
String connectionId = connect("sqlite", "", "", dbPath, "", "");
|
||||
|
||||
// 创建示例表
|
||||
createSampleTables(connectionId);
|
||||
|
||||
return connectionId;
|
||||
|
||||
case "h2":
|
||||
java.nio.file.Path h2Dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases");
|
||||
try {
|
||||
java.nio.file.Files.createDirectories(h2Dir);
|
||||
} catch (Exception e) {
|
||||
throw new SQLException("无法创建数据库目录: " + e.getMessage(), e);
|
||||
}
|
||||
String h2Path = h2Dir.resolve(dbName).toAbsolutePath().toString().replace("\\", "/");
|
||||
String h2Url = "jdbc:h2:file:" + h2Path;
|
||||
|
||||
try {
|
||||
Class.forName("org.h2.Driver");
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new SQLException("未找到 H2 驱动,请确认 h2.jar 已加入 classpath", e);
|
||||
}
|
||||
|
||||
Connection h2Conn = DriverManager.getConnection(h2Url, "sa", "");
|
||||
String h2ConnectionId = "conn_" + System.currentTimeMillis();
|
||||
connections.put(h2ConnectionId, h2Conn);
|
||||
connectionInfo.put(h2ConnectionId, new DatabaseInfo("h2", h2Url, "localhost", "", dbName, "sa"));
|
||||
|
||||
createSampleTables(h2ConnectionId);
|
||||
|
||||
return h2ConnectionId;
|
||||
|
||||
default:
|
||||
throw new SQLException("不支持创建本地数据库类型: " + driver);
|
||||
}
|
||||
}
|
||||
|
||||
private static void createSampleTables(String connectionId) throws SQLException {
|
||||
Connection conn = getConnection(connectionId);
|
||||
DatabaseInfo info = getConnectionInfo(connectionId);
|
||||
|
||||
if ("sqlite".equals(info.driver) || "h2".equals(info.driver)) {
|
||||
try (Statement stmt = conn.createStatement()) {
|
||||
// 创建用户表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS users (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"username VARCHAR(50) NOT NULL UNIQUE, " +
|
||||
"email VARCHAR(100) NOT NULL, " +
|
||||
"password VARCHAR(100) NOT NULL, " +
|
||||
"status VARCHAR(20) DEFAULT 'active', " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
|
||||
")");
|
||||
|
||||
// 创建产品表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS products (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"name VARCHAR(100) NOT NULL, " +
|
||||
"description TEXT, " +
|
||||
"price DECIMAL(10,2) NOT NULL, " +
|
||||
"stock INTEGER DEFAULT 0, " +
|
||||
"category VARCHAR(50), " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
|
||||
")");
|
||||
|
||||
// 创建订单表
|
||||
stmt.execute("CREATE TABLE IF NOT EXISTS orders (" +
|
||||
"id INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
"user_id INTEGER, " +
|
||||
"product_id INTEGER, " +
|
||||
"quantity INTEGER NOT NULL, " +
|
||||
"total_price DECIMAL(10,2) NOT NULL, " +
|
||||
"status VARCHAR(20) DEFAULT 'pending', " +
|
||||
"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
|
||||
"FOREIGN KEY (user_id) REFERENCES users(id), " +
|
||||
"FOREIGN KEY (product_id) REFERENCES products(id)" +
|
||||
")");
|
||||
|
||||
// 插入示例数据
|
||||
insertSampleData(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void insertSampleData(Connection conn) throws SQLException {
|
||||
// 检查是否已有数据
|
||||
try (Statement checkStmt = conn.createStatement();
|
||||
ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM users")) {
|
||||
if (rs.next() && rs.getInt(1) == 0) {
|
||||
// 插入用户数据
|
||||
try (PreparedStatement pstmt = conn.prepareStatement(
|
||||
"INSERT INTO users (username, email, password) VALUES (?, ?, ?)")) {
|
||||
String[][] users = {
|
||||
{"admin", "admin@example.com", "password123"},
|
||||
{"user1", "user1@example.com", "password123"},
|
||||
{"user2", "user2@example.com", "password123"}
|
||||
};
|
||||
|
||||
for (String[] user : users) {
|
||||
pstmt.setString(1, user[0]);
|
||||
pstmt.setString(2, user[1]);
|
||||
pstmt.setString(3, user[2]);
|
||||
pstmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// 插入产品数据
|
||||
try (PreparedStatement pstmt = conn.prepareStatement(
|
||||
"INSERT INTO products (name, description, price, stock, category) VALUES (?, ?, ?, ?, ?)")) {
|
||||
Object[][] products = {
|
||||
{"笔记本电脑", "高性能笔记本电脑", 5999.99, 50, "电子"},
|
||||
{"智能手机", "最新款智能手机", 3999.99, 100, "电子"},
|
||||
{"办公椅", "舒适办公椅", 299.99, 30, "家居"},
|
||||
{"咖啡机", "全自动咖啡机", 899.99, 20, "家电"}
|
||||
};
|
||||
|
||||
for (Object[] product : products) {
|
||||
pstmt.setString(1, (String) product[0]);
|
||||
pstmt.setString(2, (String) product[1]);
|
||||
pstmt.setDouble(3, (Double) product[2]);
|
||||
pstmt.setInt(4, (Integer) product[3]);
|
||||
pstmt.setString(5, (String) product[4]);
|
||||
pstmt.executeUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.axis.innovators.box.events;
|
||||
|
||||
/**
|
||||
* 当主题变更时被调用
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class TopicsUpdateEvents {
|
||||
private final String themeName;
|
||||
private final boolean darkTheme;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
* @param themeName 主题名称
|
||||
* @param darkTheme 是否是暗主题
|
||||
*/
|
||||
public TopicsUpdateEvents(String themeName, boolean darkTheme) {
|
||||
this.themeName = themeName;
|
||||
this.darkTheme = darkTheme;
|
||||
}
|
||||
|
||||
public String getThemeName() {
|
||||
return themeName;
|
||||
}
|
||||
|
||||
public boolean isDarkTheme() {
|
||||
return darkTheme;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.axis.innovators.box.login;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class LoginStpUtil {
|
||||
public static void login(Object id){
|
||||
StpUtil.login(id);
|
||||
}
|
||||
|
||||
|
||||
public static void main(String[] args) {
|
||||
LoginStpUtil.login(1);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import com.axis.innovators.box.window.FridaWindow;
|
||||
import com.axis.innovators.box.window.JarApiProfilingWindow;
|
||||
import com.axis.innovators.box.window.MainWindow;
|
||||
import com.axis.innovators.box.plugins.PluginDescriptor;
|
||||
import com.axis.innovators.box.window.TaskbarAppearanceWindow;
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.tzd.lm.LM;
|
||||
@@ -47,7 +48,6 @@ public class RegistrationTool {
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
MainWindow.ToolCategory programmingToolsCategory = new MainWindow.ToolCategory("编程工具",
|
||||
"programming/programming.png",
|
||||
"编程工具");
|
||||
@@ -82,6 +82,16 @@ public class RegistrationTool {
|
||||
}
|
||||
}));
|
||||
|
||||
programmingToolsCategory.addTool(new MainWindow.ToolItem("数据库管理工具", "programming/programming_dark.png",
|
||||
"用于管理数据库" +
|
||||
"\n作者:tzdwindows 7", ++id, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
// Window owner = SwingUtilities.windowForComponent((Component) e.getSource());
|
||||
MainApplication.popupDataBaseWindow();
|
||||
}
|
||||
}));
|
||||
|
||||
MainWindow.ToolCategory aICategory = new MainWindow.ToolCategory("AI工具",
|
||||
"ai/ai.png",
|
||||
"人工智能/大语言模型");
|
||||
@@ -108,9 +118,44 @@ public class RegistrationTool {
|
||||
}
|
||||
}));
|
||||
|
||||
MainWindow.ToolCategory hahahah = new MainWindow.ToolCategory(
|
||||
"good工具",
|
||||
"haha/ok.png",
|
||||
"good "
|
||||
);
|
||||
hahahah.addTool(new MainWindow.ToolItem("123", "ai/local/local_main.png",
|
||||
"456789" +
|
||||
"\n作者:Vinfya", ++id, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
// 在这里写
|
||||
// 这个就是弹窗Ok
|
||||
JOptionPane.showMessageDialog(null, "你好...");
|
||||
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
|
||||
MainWindow.ToolCategory systemCategory = new MainWindow.ToolCategory("系统工具",
|
||||
"windows/windows.png",
|
||||
"系统工具");
|
||||
systemCategory.addTool(new MainWindow.ToolItem("任务栏主题设置", "windows/windowsOptimization/windowsOptimization.png",
|
||||
"可以设置Windows任务栏的颜色等各种信息",++id, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
Window owner = SwingUtilities.windowForComponent((Component) e.getSource());
|
||||
TaskbarAppearanceWindow taskbarAppearanceWindow = new TaskbarAppearanceWindow(owner);
|
||||
main.popupWindow(taskbarAppearanceWindow);
|
||||
}
|
||||
}));
|
||||
|
||||
addToolCategory(debugCategory, "system:debugTools");
|
||||
addToolCategory(aICategory,"system:fridaTools");
|
||||
addToolCategory(programmingToolsCategory, "system:programmingTools");
|
||||
addToolCategory(systemCategory, "system:systemTools");
|
||||
addToolCategory(hahahah, "system:mc");
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.axis.innovators.box.tools.Crypto;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
public class AESCryptoUtil {
|
||||
private static final String KEY_FILE = System.getProperty("user.home") + "/.lingqi/.axis_box_key";
|
||||
|
||||
// 获取密钥(Base64字符串,长度16字节)
|
||||
public static byte[] getKeyBytes() throws Exception {
|
||||
Path path = Paths.get(KEY_FILE);
|
||||
if (Files.exists(path)) {
|
||||
byte[] encrypted = Base64.getDecoder().decode(Files.readAllBytes(path));
|
||||
return WindowsDPAPIUtil.unprotect(encrypted);
|
||||
} else {
|
||||
// 首次生成密钥
|
||||
byte[] keyBytes = new byte[16];
|
||||
new SecureRandom().nextBytes(keyBytes);
|
||||
byte[] encrypted = WindowsDPAPIUtil.protect(keyBytes);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.write(path, Base64.getEncoder().encode(encrypted));
|
||||
return keyBytes;
|
||||
}
|
||||
}
|
||||
|
||||
// 加密
|
||||
public static String encrypt(String data) throws Exception {
|
||||
byte[] keyBytes = getKeyBytes();
|
||||
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
|
||||
byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
}
|
||||
|
||||
// 解密
|
||||
public static String decrypt(String encrypted) throws Exception {
|
||||
byte[] keyBytes = getKeyBytes();
|
||||
SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
|
||||
Cipher cipher = Cipher.getInstance("AES");
|
||||
cipher.init(Cipher.DECRYPT_MODE, keySpec);
|
||||
byte[] decoded = Base64.getDecoder().decode(encrypted);
|
||||
byte[] decrypted = cipher.doFinal(decoded);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.axis.innovators.box.tools.Crypto;
|
||||
|
||||
import java.util.Base64;
|
||||
|
||||
public class Base64CryptoUtil {
|
||||
|
||||
public static String base64Encode(String input) {
|
||||
return Base64.getEncoder().encodeToString(input.getBytes());
|
||||
}
|
||||
|
||||
public static String base64Decode(String input) {
|
||||
byte[] decodedBytes = Base64.getDecoder().decode(input);
|
||||
return new String(decodedBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.axis.innovators.box.tools.Crypto;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Base64;
|
||||
|
||||
public class HashUtil {
|
||||
public static String sha256(String input) throws Exception {
|
||||
MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
byte[] hash = digest.digest(input.getBytes("UTF-8"));
|
||||
return Base64.getEncoder().encodeToString(hash); // 输出为Base64字符串
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.axis.innovators.box.tools.Crypto;
|
||||
|
||||
import com.sun.jna.platform.win32.Crypt32Util;
|
||||
|
||||
public class WindowsDPAPIUtil {
|
||||
// 加密
|
||||
public static byte[] protect(byte[] data) {
|
||||
return Crypt32Util.cryptProtectData(data);
|
||||
}
|
||||
|
||||
// 解密
|
||||
public static byte[] unprotect(byte[] encrypted) {
|
||||
return Crypt32Util.cryptUnprotectData(encrypted);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,13 @@ public class FolderCreator {
|
||||
public static final String LANGUAGE_PATH = "language";
|
||||
public static final String CONFIGURATION_PATH = "state";
|
||||
public static final String JAVA_SCRIPT_PATH = "javascript";
|
||||
|
||||
public static String PLUGIN_PATH_ = "";
|
||||
|
||||
public static void setPluginPath(String pluginPath) {
|
||||
PLUGIN_PATH_ = pluginPath;
|
||||
}
|
||||
|
||||
public static String getConfigurationFolder() {
|
||||
String folder = createFolder(CONFIGURATION_PATH);
|
||||
if (folder == null) {
|
||||
@@ -46,6 +53,9 @@ public class FolderCreator {
|
||||
return folder;
|
||||
}
|
||||
public static String getPluginFolder() {
|
||||
if (!PLUGIN_PATH_.isEmpty()){
|
||||
return PLUGIN_PATH_;
|
||||
}
|
||||
String folder = createFolder(PLUGIN_PATH);
|
||||
if (folder == null) {
|
||||
logger.error("Plugin folder creation failed, please use administrator privileges to execute this procedure");
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.axis.innovators.box.tools;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
|
||||
public class LoadResource {
|
||||
|
||||
public static String readString(String srcPath) {
|
||||
try {
|
||||
java.net.URL url = LoadResource.class.getResource(srcPath);
|
||||
if (url == null) {
|
||||
LogManager.getLogger(LoadResource.class).error("资源文件未找到: " + srcPath);
|
||||
return null;
|
||||
}
|
||||
java.nio.file.Path path = java.nio.file.Paths.get(url.toURI());
|
||||
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
LogManager.getLogger(LoadResource.class).error("读取资源文件失败", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static String loadString(Class<?> clazz, String srcPath) {
|
||||
try {
|
||||
java.net.URL url = clazz.getResource(srcPath);
|
||||
if (url == null) {
|
||||
LogManager.getLogger(LoadResource.class).error("资源文件未找到: " + srcPath);
|
||||
return null;
|
||||
}
|
||||
java.nio.file.Path path = java.nio.file.Paths.get(url.toURI());
|
||||
return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
LogManager.getLogger(LoadResource.class).error("读取资源文件失败", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -12,10 +12,6 @@ import java.util.List;
|
||||
*/
|
||||
public class RegisterTray {
|
||||
|
||||
static {
|
||||
LibraryLoad.loadLibrary("RegisterTray");
|
||||
}
|
||||
|
||||
/**
|
||||
* 托盘菜单项构建器(流畅接口)
|
||||
*/
|
||||
@@ -26,39 +22,28 @@ public class RegisterTray {
|
||||
* 添加菜单项
|
||||
* @param id 菜单项唯一标识符(需大于0)
|
||||
* @param label 菜单显示文本
|
||||
* @param onClick 点击事件处理器
|
||||
* @param onClick 点击事件处理器(接收 itemId)
|
||||
* @return 当前构建器实例
|
||||
*/
|
||||
public MenuBuilder addItem(int id, String label, MenuItemClickListener onClick) {
|
||||
items.add(new Item(
|
||||
id,
|
||||
label,
|
||||
"", "", "",
|
||||
(Event) combinedId -> {
|
||||
int itemId = (int)(combinedId >> 32);
|
||||
onClick.onClick(itemId);
|
||||
// 为每一项创建一个 Event 回调对象(native 端会在点击时回调此 Event.onClick(long))
|
||||
// 这里我们忽略 native 传回的 trayId,只把 itemId 传给上层的 MenuItemClickListener
|
||||
Event ev = new Event() {
|
||||
@Override
|
||||
public void onClick(long trayId) {
|
||||
try {
|
||||
onClick.onClick(id);
|
||||
} catch (Throwable t) {
|
||||
t.printStackTrace();
|
||||
}
|
||||
));
|
||||
return this;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加菜单项
|
||||
* @param id 菜单项唯一标识符(需大于0)
|
||||
* @param label 菜单显示文本
|
||||
* @param onClick 点击事件处理器
|
||||
* @return 当前构建器实例
|
||||
*/
|
||||
public MenuBuilder addItem(MenuBuilder builder,int id, String label, MenuItemClickListener onClick) {
|
||||
this.items = builder.items;
|
||||
items.add(new Item(
|
||||
id,
|
||||
label,
|
||||
"", "", "",
|
||||
(Event) combinedId -> {
|
||||
int itemId = (int)(combinedId >> 32);
|
||||
onClick.onClick(itemId);
|
||||
}
|
||||
ev
|
||||
));
|
||||
return this;
|
||||
}
|
||||
@@ -72,6 +57,7 @@ public class RegisterTray {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 托盘配置器(流畅接口)
|
||||
*/
|
||||
@@ -143,7 +129,6 @@ public class RegisterTray {
|
||||
title,
|
||||
menuItems,
|
||||
iconPath,
|
||||
tooltip,
|
||||
clickListener::onClick
|
||||
);
|
||||
} catch (Exception e) {
|
||||
@@ -185,8 +170,13 @@ public class RegisterTray {
|
||||
}
|
||||
}
|
||||
|
||||
public static native long register(String name, List<Item> value,
|
||||
String icon, String description, Event event);
|
||||
public static native long register(String name, List<Item> items, String icon, Event event);
|
||||
|
||||
/**
|
||||
* 更强的变体:允许提供 description(可以用于 tooltip 或弹出顶部信息)。
|
||||
* 推荐使用 registerEx 来获取现代化圆角弹出菜单。
|
||||
*/
|
||||
public static native long registerEx(String name, List<Item> items, String icon, String description, Event event);
|
||||
public static native void unregister(long id);
|
||||
|
||||
public interface Event {
|
||||
|
||||
@@ -60,22 +60,17 @@ public class Tray {
|
||||
* @throws RegisterTray.TrayException 抛出错误
|
||||
*/
|
||||
public static void load(TrayLabels trayLabels) throws RegisterTray.TrayException {
|
||||
if (trayLabels == null || trayLabelsList.contains(trayLabels)){
|
||||
if (trayLabels == null || trayLabelsList.contains(trayLabels)) {
|
||||
System.err.println("trayLabels is null or trayLabelsList contains trayLabels");
|
||||
return;
|
||||
}
|
||||
trayLabelsList.add(trayLabels);
|
||||
|
||||
if (menuBuilders == null) {
|
||||
RegisterTray.MenuBuilder menuBuilder = new RegisterTray.MenuBuilder()
|
||||
.addItem(trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
|
||||
menuBuilders = menuBuilder;
|
||||
menuItems = menuBuilder.build();
|
||||
} else {
|
||||
menuBuilders = new RegisterTray.MenuBuilder()
|
||||
.addItem(menuBuilders, trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
|
||||
menuItems = menuBuilders.build();
|
||||
menuBuilders = new RegisterTray.MenuBuilder();
|
||||
}
|
||||
menuBuilders.addItem(trayLabels.id, trayLabels.name, itemId -> trayLabels.action.run());
|
||||
menuItems = menuBuilders.build();
|
||||
}
|
||||
|
||||
public static void addAction(Runnable action){
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.axis.innovators.box.util;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.verification.OnlineVerification;
|
||||
import com.axis.innovators.box.verification.UserTags;
|
||||
import com.axis.innovators.box.verification.VerificationService;
|
||||
|
||||
/**
|
||||
* 用于存储用户信息
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class UserLocalInformation {
|
||||
private final AxisInnovatorsBox main;
|
||||
|
||||
public UserLocalInformation(AxisInnovatorsBox main){
|
||||
this.main = main;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户信息
|
||||
* @param userTags 用户信息
|
||||
*/
|
||||
public void setUserTags(UserTags userTags){
|
||||
OnlineVerification onlineVerification = userTags.getUser();
|
||||
main.getStateManager().saveState("password", onlineVerification.password);
|
||||
main.getStateManager().saveState("verification", onlineVerification.onlineVerification);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
* @return 用户信息
|
||||
*/
|
||||
public UserTags getUserTags(){
|
||||
String verification = main.getStateManager().getState("verification");
|
||||
String password = main.getStateManager().getState("password");
|
||||
if (verification == null || password == null){
|
||||
return null;
|
||||
}
|
||||
OnlineVerification onlineVerification = OnlineVerification.validateLogin(
|
||||
verification,
|
||||
password);
|
||||
if (onlineVerification == null){
|
||||
return null;
|
||||
}
|
||||
return VerificationService.determineUserType(onlineVerification);
|
||||
}
|
||||
}
|
||||
64
src/main/java/com/axis/innovators/box/util/WindowsTheme.java
Normal file
64
src/main/java/com/axis/innovators/box/util/WindowsTheme.java
Normal file
@@ -0,0 +1,64 @@
|
||||
package com.axis.innovators.box.util;
|
||||
|
||||
/**
|
||||
* Windows主题变更监听器
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class WindowsTheme {
|
||||
|
||||
/**
|
||||
* 设置主题改变监听器
|
||||
* @param listener 监听器
|
||||
*/
|
||||
public static native void setThemeChangeListener(ThemeChangeListener listener);
|
||||
|
||||
/**
|
||||
* 主题改变监听器
|
||||
*/
|
||||
public interface ThemeChangeListener {
|
||||
/**
|
||||
* 当主题改变时调用
|
||||
* @param themeName 主题名称
|
||||
* @param darkTheme 是否是暗主题
|
||||
*/
|
||||
void themeChanged(String themeName,boolean darkTheme);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前是否为暗色主题
|
||||
* @return true-暗色主题, false-亮色主题
|
||||
*/
|
||||
public static native boolean isDarkTheme();
|
||||
|
||||
/**
|
||||
* 获取当前主题名称
|
||||
* @return 主题名称
|
||||
*/
|
||||
public static native String getThemeName();
|
||||
|
||||
//public static void main(String[] args) {
|
||||
// try {
|
||||
// System.load("C:\\Users\\Administrator\\source\\repos\\RegisterTray\\x64\\Release\\RegisterTray.dll");
|
||||
//
|
||||
// // 先测试直接获取主题信息的方法
|
||||
// System.out.println("Current topic Name: " + WindowsTheme.getThemeName());
|
||||
// System.out.println("Is it a dark theme: " + WindowsTheme.isDarkTheme());
|
||||
//
|
||||
// WindowsTheme.setThemeChangeListener(new WindowsTheme.ThemeChangeListener() {
|
||||
// @Override
|
||||
// public void themeChanged(String themeName, boolean darkTheme) {
|
||||
// System.out.println("The theme has changed: " + themeName + " is dark theme: " + darkTheme);
|
||||
// }
|
||||
// });
|
||||
//
|
||||
// System.out.println("Start listening for theme changes... Press Enter to exit");
|
||||
//
|
||||
// System.in.read();
|
||||
//
|
||||
// } catch (Exception e) {
|
||||
// e.printStackTrace();
|
||||
// } finally {
|
||||
// WindowsTheme.setThemeChangeListener(null);
|
||||
// }
|
||||
//}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package com.axis.innovators.box.verification;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.casbin.casdoor.config.Config;
|
||||
import org.casbin.casdoor.entity.User;
|
||||
import org.casbin.casdoor.service.AuthService;
|
||||
|
||||
import com.axis.innovators.box.tools.LoadResource;
|
||||
|
||||
import config.CasdoorConfig;
|
||||
|
||||
public class CasdoorServer {
|
||||
private static final Logger logger = LogManager.getLogger(CasdoorServer.class);
|
||||
|
||||
private final AuthService authService;
|
||||
private final Config config;
|
||||
private final String certificate;
|
||||
|
||||
public CasdoorServer() {
|
||||
this.certificate = LoadResource.readString("/cert/casdoor_cert.pem");
|
||||
this.config = new Config(
|
||||
CasdoorConfig.CASDOOR_API_URL,
|
||||
CasdoorConfig.CASDOOR_CLIENT_ID,
|
||||
CasdoorConfig.CASDOOR_CLIENT_SECRET,
|
||||
this.certificate,
|
||||
CasdoorConfig.CASDOOR_ORGANIZATION_NAME,
|
||||
CasdoorConfig.CASDOOR_APPLICATION_NAME);
|
||||
this.authService = new AuthService(this.config);
|
||||
}
|
||||
|
||||
public AuthService getAuthService() {
|
||||
return authService;
|
||||
}
|
||||
|
||||
public Config getConfig() {
|
||||
return config;
|
||||
}
|
||||
|
||||
public String getCertificate() {
|
||||
return certificate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录 URL
|
||||
* @return 登录 URL
|
||||
*/
|
||||
public String getSigninUrl() {
|
||||
return authService.getSigninUrl(CasdoorConfig.CASDOOR_LOGIN_REDIRECT_URI);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取注册 URL
|
||||
* @return 注册 URL
|
||||
*/
|
||||
public String getSignupUrl() {
|
||||
String redirectUrl = CasdoorConfig.CASDOOR_SIGNUP_REDIRECT_URI;
|
||||
if (redirectUrl == null) {
|
||||
redirectUrl = getSigninUrl(); // 如果没有设置注册回调地址,则使用登录回调地址
|
||||
}
|
||||
return authService.getSignupUrl(redirectUrl);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OAuth Token
|
||||
* @param code 登录回调的 code 参数
|
||||
* @param state 登录回调的 state 参数
|
||||
* @return
|
||||
*/
|
||||
public String getOAuthToken(String code, String state) {
|
||||
try {
|
||||
return authService.getOAuthToken(code, state);
|
||||
} catch (Exception e) {
|
||||
logger.error("获取 OAuth Token 失败: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 JWT Token
|
||||
* @param token 令牌
|
||||
* @return User 用户信息
|
||||
*/
|
||||
public User parseJwtToken(String token) {
|
||||
try {
|
||||
return authService.parseJwtToken(token);
|
||||
} catch (Exception e) {
|
||||
logger.error("解析 JWT Token 失败: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息,同 parseJwtToken
|
||||
* @param token 令牌
|
||||
* @return User 用户信息
|
||||
*/
|
||||
public User getUserInfo(String token) {
|
||||
return parseJwtToken(token);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.axis.innovators.box.verification;
|
||||
|
||||
import org.casbin.casdoor.entity.User;
|
||||
|
||||
public class LoginData {
|
||||
private String token;
|
||||
private User user;
|
||||
|
||||
public LoginData(String token, User user) {
|
||||
this.token = token;
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
public String getToken() {
|
||||
return token;
|
||||
}
|
||||
|
||||
public User getUser() {
|
||||
return user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.axis.innovators.box.verification;
|
||||
|
||||
import org.casbin.casdoor.entity.User;
|
||||
|
||||
public class LoginResult extends Result {
|
||||
|
||||
public LoginResult(boolean success, String message, LoginData data) {
|
||||
super(success, message, data);
|
||||
}
|
||||
|
||||
public String token() {
|
||||
LoginData loginData = (LoginData)this.m_data;
|
||||
return loginData != null ? loginData.getToken() : null;
|
||||
}
|
||||
|
||||
public User user() {
|
||||
LoginData loginData = (LoginData) this.m_data;
|
||||
return loginData != null ? loginData.getUser() : null;
|
||||
}
|
||||
|
||||
public LoginData loginData() {
|
||||
return (LoginData) this.m_data;
|
||||
}
|
||||
|
||||
public LoginData data() {
|
||||
return (LoginData) this.m_data;
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.axis.innovators.box.verification;
|
||||
|
||||
/**
|
||||
* 在线验证用户身份
|
||||
* 用于报错用户的验证信息
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class OnlineVerification {
|
||||
public String onlineVerification;
|
||||
public String password;
|
||||
/* 我是错误信息,要返回错误请修改我 */
|
||||
public static String errorMessage = "用户不存在";
|
||||
|
||||
/**
|
||||
* 验证登录
|
||||
* @param identifier 账号
|
||||
* @param password 密码
|
||||
*/
|
||||
OnlineVerification(String identifier, String password){
|
||||
this.onlineVerification = identifier;
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证登录
|
||||
* @param identifier 账号
|
||||
* @param password 密码
|
||||
* @return 验证结果,如果返回null则表示验证失败,使用errorMessage获取验证失败的原因
|
||||
*/
|
||||
public static OnlineVerification validateLogin(String identifier, String password){
|
||||
return new OnlineVerification(identifier, password);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.axis.innovators.box.verification;
|
||||
|
||||
public class Result {
|
||||
protected final boolean m_success;
|
||||
protected final String m_message;
|
||||
protected final Object m_data;
|
||||
|
||||
public Result(boolean success, String message, Object data) {
|
||||
this.m_success = success;
|
||||
this.m_message = message;
|
||||
this.m_data = data;
|
||||
}
|
||||
|
||||
public boolean success() {
|
||||
return m_success;
|
||||
}
|
||||
|
||||
public String message() {
|
||||
return m_message;
|
||||
}
|
||||
|
||||
public Object data() {
|
||||
return m_data;
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.axis.innovators.box.verification;
|
||||
|
||||
/**
|
||||
* 用户标签组
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public enum UserTags {
|
||||
/**
|
||||
* 没有登录的标签
|
||||
*/
|
||||
None,
|
||||
/**
|
||||
* 普通用户标签
|
||||
*/
|
||||
RegularUsers,
|
||||
/**
|
||||
* 管理员标签
|
||||
*/
|
||||
AdminUsers,
|
||||
/**
|
||||
* VIP用户标签
|
||||
*/
|
||||
VipUsers,
|
||||
/**
|
||||
* SVip用户标签
|
||||
*/
|
||||
SVipUsers,
|
||||
/**
|
||||
* 企业用户标签
|
||||
*/
|
||||
EnterpriseUsers;
|
||||
|
||||
private OnlineVerification onlineVerification;
|
||||
|
||||
/**
|
||||
* 设置用户组信息
|
||||
* @param onlineVerification 用户验证结果信息
|
||||
*/
|
||||
void setUser(OnlineVerification onlineVerification) {
|
||||
this.onlineVerification = onlineVerification;
|
||||
}
|
||||
|
||||
public OnlineVerification getUser() {
|
||||
return onlineVerification;
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
package com.axis.innovators.box.verification;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class VerificationService {
|
||||
|
||||
/**
|
||||
* 确定用户类型
|
||||
* @param identifier 用户
|
||||
* @return 用户类型
|
||||
*/
|
||||
public static UserTags determineUserType(OnlineVerification identifier) {
|
||||
UserTags userTags = UserTags.RegularUsers;
|
||||
userTags.setUser(identifier);
|
||||
return userTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送密码重置链接给用户
|
||||
* @param text
|
||||
* @return
|
||||
*/
|
||||
public static boolean sendPasswordReset(String text) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册用户
|
||||
* @param text
|
||||
* @param text1
|
||||
* @param pwd
|
||||
* @return
|
||||
*/
|
||||
public static boolean registerUser(String text, String text1, String pwd) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.axis.innovators.box.verification.api;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* 用户API接口
|
||||
* @author lyxyz5223
|
||||
*/
|
||||
public interface UserApi {
|
||||
/**
|
||||
* 发送验证码
|
||||
* @param email 用户邮箱
|
||||
* @return 发送结果
|
||||
*/
|
||||
CompletableFuture<UserApiResult> sendVerificationCode(String email);
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
* @param email 用户邮箱
|
||||
* @param verificationCode 验证码
|
||||
* @return 验证结果
|
||||
*/
|
||||
CompletableFuture<UserApiResult> verifyCode(String email, String verificationCode);
|
||||
|
||||
/**
|
||||
* 检验登录token有效性,用于检验登录状态
|
||||
* @param token 登录token
|
||||
* @return 检验结果
|
||||
*/
|
||||
CompletableFuture<UserApiResult> checkTokenValidity(String token);
|
||||
|
||||
/**
|
||||
* 申请新的token,可以定期申请新的token保证安全性
|
||||
* @param oldToken 旧的登录token
|
||||
* @return 申请结果
|
||||
*/
|
||||
CompletableFuture<UserApiResult> applyNewToken(String oldToken);
|
||||
|
||||
/**
|
||||
* 登录接口
|
||||
* @param username 用户名
|
||||
* @param password 密码(加密)
|
||||
* @return { token: String, expiresIn: Number } 登录结果(含token和有效期,临近到期需要手动申请新的token,每次登陆服务器将更新token有效期)
|
||||
*/
|
||||
CompletableFuture<UserApiResult> login(String username, String password);
|
||||
|
||||
/**
|
||||
* 登出接口
|
||||
* @param username 用户名
|
||||
* @return 登出结果
|
||||
*/
|
||||
CompletableFuture<UserApiResult> logout(String username);
|
||||
|
||||
/**
|
||||
* 注册接口
|
||||
* @param username 用户名
|
||||
* @param password 密码(加密)
|
||||
* @param email 邮箱
|
||||
* @return 注册结果
|
||||
*/
|
||||
CompletableFuture<UserApiResult> register(String username, String password, String email);
|
||||
|
||||
/**
|
||||
* 申请重置密码接口
|
||||
* @param email 用户邮箱
|
||||
* @return { resetToken: String, expiresIn: Number } 重置结果(包含有效期内的重置token和有效期,24小时内有效)
|
||||
*/
|
||||
CompletableFuture<UserApiResult> postResetPasswordRequest(String email);
|
||||
|
||||
/**
|
||||
* 重置密码接口
|
||||
* @param email 用户邮箱
|
||||
* @param resetToken 重置token(有效期24小时内)
|
||||
* @param newPassword 新密码(加密)
|
||||
* @return 重置结果
|
||||
*/
|
||||
CompletableFuture<UserApiResult> resetPassword(String email, String resetToken, String newPassword);
|
||||
|
||||
/**
|
||||
* 修改密码接口
|
||||
* @param username 用户名
|
||||
* @param oldPassword 旧密码(加密)
|
||||
* @param newPassword 新密码(加密)
|
||||
* @return 修改结果
|
||||
*/
|
||||
CompletableFuture<UserApiResult> changePassword(String username, String oldPassword, String newPassword);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.axis.innovators.box.verification.api;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class UserApiImpl implements UserApi {
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> sendVerificationCode(String email) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Code sent", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> verifyCode(String email, String verificationCode) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Code verified", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> checkTokenValidity(String token) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Token is valid", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> applyNewToken(String oldToken) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "New token applied", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> login(String username, String password) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Login successful", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> logout(String username) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Logout successful", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> register(String username, String password, String email) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Registration successful", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> postResetPasswordRequest(String email) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Reset password request sent", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> resetPassword(String email, String resetToken, String newPassword) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Password reset successful", null));
|
||||
}
|
||||
|
||||
@Override
|
||||
public CompletableFuture<UserApiResult> changePassword(String username, String oldPassword, String newPassword) {
|
||||
// Implementation here
|
||||
return CompletableFuture.completedFuture(new UserApiResult(true, "Password changed successfully", null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.axis.innovators.box.verification.api;
|
||||
|
||||
public class UserApiResult {
|
||||
private final boolean m_success;
|
||||
private final String m_message;
|
||||
private final Object m_data;
|
||||
|
||||
public UserApiResult(boolean success, String message, Object data) {
|
||||
this.m_success = success;
|
||||
this.m_message = message;
|
||||
this.m_data = data;
|
||||
}
|
||||
|
||||
public boolean success() {
|
||||
return m_success;
|
||||
}
|
||||
|
||||
public String message() {
|
||||
return m_message;
|
||||
}
|
||||
|
||||
public Object data() {
|
||||
return m_data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
package com.axis.innovators.box.window;
|
||||
|
||||
import com.axis.innovators.box.AxisInnovatorsBox;
|
||||
import com.axis.innovators.box.browser.CefAppManager;
|
||||
import com.axis.innovators.box.tools.LoadResource;
|
||||
import com.axis.innovators.box.verification.CasdoorServer;
|
||||
import com.axis.innovators.box.verification.LoginData;
|
||||
import com.axis.innovators.box.verification.LoginResult;
|
||||
import com.axis.innovators.box.verification.Result;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.awt.Desktop;
|
||||
import java.net.URI;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
import org.apache.logging.log4j.LogManager;
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.casbin.casdoor.entity.User;
|
||||
import javax.swing.*;
|
||||
import config.CasdoorConfig;
|
||||
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.browser.CefFrame;
|
||||
import org.cef.handler.CefContextMenuHandlerAdapter;
|
||||
import org.cef.handler.CefKeyboardHandlerAdapter;
|
||||
import org.cef.handler.CefLifeSpanHandlerAdapter;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.cef.callback.CefMenuModel;
|
||||
import org.cef.callback.CefMenuModel.MenuId;
|
||||
import org.cef.callback.CefContextMenuParams;
|
||||
|
||||
|
||||
public class CasdoorLoginWindow {
|
||||
private final Logger logger = LogManager.getLogger(CasdoorLoginWindow.class);
|
||||
private final CasdoorServer casdoorServer;
|
||||
|
||||
private CefBrowser browser;
|
||||
private HttpServer server;
|
||||
private JDialog dialog;
|
||||
private LoginResult loginResult = null;
|
||||
private boolean windowVisible = true;
|
||||
private boolean isModal = true;
|
||||
|
||||
public CasdoorLoginWindow() {
|
||||
casdoorServer = new CasdoorServer();
|
||||
}
|
||||
|
||||
private void startLocalCallbackServer() {
|
||||
try {
|
||||
server = HttpServer.create(new java.net.InetSocketAddress(CasdoorConfig.CASDOOR_WEB_SERVER_PORT), 0);
|
||||
server.createContext("/casdoor/callback", this::handleCallback);
|
||||
server.setExecutor(java.util.concurrent.Executors.newSingleThreadExecutor());
|
||||
server.start();
|
||||
} catch (IOException e) {
|
||||
System.err.println("本地回调服务启动失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleCallback(HttpExchange exchange) throws IOException {
|
||||
String query = exchange.getRequestURI().getQuery();
|
||||
AtomicReference<String> code = new AtomicReference<>("");
|
||||
AtomicReference<String> state = new AtomicReference<>("");
|
||||
if (query != null) {
|
||||
for (String param : query.split("&")) {
|
||||
String[] kv = param.split("=");
|
||||
if (kv.length == 2) {
|
||||
if (kv[0].equals("code"))
|
||||
code.set(kv[1]);
|
||||
if (kv[0].equals("state"))
|
||||
state.set(kv[1]);
|
||||
logger.info("Received callback with code: " + code.get() + ", state: " + state.get());
|
||||
loginResult = parseUserInfo(code.get(), state.get());
|
||||
String response = "Login success, please close this window.";
|
||||
exchange.sendResponseHeaders(200, response.getBytes().length);
|
||||
try (OutputStream os = exchange.getResponseBody()) {
|
||||
os.write(response.getBytes());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initUI() {
|
||||
if (browser == null) {
|
||||
try {
|
||||
CefApp cefApp = CefAppManager.getInstance();
|
||||
CefClient client = cefApp.createClient();
|
||||
|
||||
browser = client.createBrowser(casdoorServer.getSigninUrl(), false, false);
|
||||
|
||||
if (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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 🔹 页面加载完成后注入 JS / CSS
|
||||
client.addLoadHandler(new org.cef.handler.CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadEnd(CefBrowser cefBrowser, org.cef.browser.CefFrame frame, int httpStatusCode) {
|
||||
if (frame != null && !frame.isMain()) return;
|
||||
String url = cefBrowser.getURL();
|
||||
try {
|
||||
String js =
|
||||
"(function(){"
|
||||
+ "if(window._axis_toolbar_injected) return; window._axis_toolbar_injected = true;"
|
||||
+ "var style = document.createElement('style'); style.innerHTML = '"
|
||||
+ ".axis-toolbar{position:fixed;right:16px;display:flex;gap:8px;z-index:2147483647;transition:transform .36s cubic-bezier(.2,.9,.2,1),opacity .28s;transform:translateY(20px) scale(.98);opacity:0;} "
|
||||
+ ".axis-toolbar.show{transform:translateY(0) scale(1);opacity:1;} "
|
||||
+ ".axis-btn{width:40px;height:40px;border-radius:10px;border:none;padding:6px;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(6px);background:rgba(255,255,255,0.08);box-shadow:0 6px 18px rgba(0,0,0,0.18);cursor:pointer;font-family:Segoe UI,SegoeUI,Arial,sans-serif;} "
|
||||
+ ".axis-btn:hover{transform:translateY(-2px) scale(1.03);} "
|
||||
+ ".axis-btn svg{width:20px;height:20px;display:block;} "
|
||||
+ "@keyframes axisPageOut{0%{transform:scale(1);opacity:1}100%{transform:scale(0.96);opacity:0}}"
|
||||
+ "@keyframes axisPageIn{0%{transform:scale(1.04);opacity:0}100%{transform:scale(1);opacity:1}}"
|
||||
+ ".axis-page-out{animation:axisPageOut .3s ease-out forwards;pointer-events:none;}"
|
||||
+ ".axis-page-in{animation:axisPageIn .4s ease-out forwards;}"
|
||||
+ "';"
|
||||
+ "document.head.appendChild(style);"
|
||||
+ "var toolbar = document.createElement('div'); toolbar.className = 'axis-toolbar';"
|
||||
+ "function makeBtn(title, svg, onclick){ var b = document.createElement('button'); b.className='axis-btn'; b.title=title; b.innerHTML = svg; b.addEventListener('click', function(e){ e.preventDefault(); try{ onclick(); }catch(ex){} }); return b; }"
|
||||
+ "var svgBack = '<svg viewBox=\\'0 0 24 24\\' fill=\\'none\\' xmlns=\\'http://www.w3.org/2000/svg\\'><path d=\\'M15 6L9 12l6 6\\' stroke=\\'currentColor\\' stroke-width=\\'2\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\'/></svg>';"
|
||||
+ "var svgFwd = '<svg viewBox=\\'0 0 24 24\\' fill=\\'none\\' xmlns=\\'http://www.w3.org/2000/svg\\'><path d=\\'M9 18l6-6-6-6\\' stroke=\\'currentColor\\' stroke-width=\\'2\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\'/></svg>';"
|
||||
+ "var svgReload = '<svg viewBox=\\'0 0 24 24\\' fill=\\'none\\' xmlns=\\'http://www.w3.org/2000/svg\\'><path d=\\'M21 12a9 9 0 10-3 6.7\\' stroke=\\'currentColor\\' stroke-width=\\'2\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\'/><path d=\\'M21 3v6h-6\\' stroke=\\'currentColor\\' stroke-width=\\'2\\' stroke-linecap=\\'round\\' stroke-linejoin=\\'round\\'/></svg>';"
|
||||
+ "var bBack = makeBtn('后退', svgBack, function(){ try{ history.back(); }catch(e){} });"
|
||||
+ "var bFwd = makeBtn('前进', svgFwd, function(){ try{ history.forward(); }catch(e){} });"
|
||||
+ "var bReload = makeBtn('重新加载', svgReload, function(){ try{ location.reload(); }catch(e){} });"
|
||||
+ "toolbar.appendChild(bBack); toolbar.appendChild(bFwd); toolbar.appendChild(bReload); document.body.appendChild(toolbar);"
|
||||
+ "setTimeout(function(){ try{ var footer = document.querySelector('footer'); var bottom = 24; if(footer && footer.offsetHeight) bottom = footer.offsetHeight + 16; toolbar.style.bottom = bottom + 'px'; }catch(e){} toolbar.classList.add('show'); }, 60);"
|
||||
+ "var isAnimating = false;"
|
||||
+ "document.addEventListener('click', function(e){"
|
||||
+ " try{"
|
||||
+ " if(isAnimating) return;"
|
||||
+ " var a = e.target.closest && e.target.closest('a');"
|
||||
+ " if(!a) return;"
|
||||
+ " if(a.target=='_blank') return;"
|
||||
+ " var href = a.href;"
|
||||
+ " if(!href) return;"
|
||||
+ " var same = (new URL(href, location.href)).origin === location.origin;"
|
||||
+ " if(!same) return;"
|
||||
+ " e.preventDefault();"
|
||||
+ " isAnimating = true;"
|
||||
+ " document.documentElement.classList.add('axis-page-out');"
|
||||
+ " setTimeout(function(){"
|
||||
+ " location.href = href;"
|
||||
+ " isAnimating = false;"
|
||||
+ " }, 300);"
|
||||
+ " }catch(ex){}"
|
||||
+ "}, true);"
|
||||
+ "function runPageInAnimation() {"
|
||||
+ " document.documentElement.classList.remove('axis-page-out');"
|
||||
+ " document.documentElement.classList.add('axis-page-in');"
|
||||
+ " setTimeout(function(){"
|
||||
+ " document.documentElement.classList.remove('axis-page-in');"
|
||||
+ " }, 400);"
|
||||
+ "}"
|
||||
+ "window.addEventListener('pageshow', function(event){"
|
||||
+ " if (event.persisted) runPageInAnimation();"
|
||||
+ "});"
|
||||
+ "window.addEventListener('DOMContentLoaded', runPageInAnimation);"
|
||||
+ "})();";
|
||||
cefBrowser.executeJavaScript(js, url, 0);
|
||||
} catch (Throwable ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
client.addLifeSpanHandler(new CefLifeSpanHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforePopup(CefBrowser browser, CefFrame frame,
|
||||
String targetUrl, String targetFrameName) {
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(targetUrl));
|
||||
} catch (Exception e) {
|
||||
System.out.println("Failed to open external browser: " + e.getMessage());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// 🔹 自定义右键菜单
|
||||
client.addContextMenuHandler(new org.cef.handler.CefContextMenuHandlerAdapter() {
|
||||
|
||||
@Override
|
||||
public void onBeforeContextMenu(org.cef.browser.CefBrowser browser, org.cef.browser.CefFrame frame,
|
||||
org.cef.callback.CefContextMenuParams params, org.cef.callback.CefMenuModel model) {
|
||||
model.clear(); // 清空默认菜单
|
||||
if (browser.canGoBack()) {
|
||||
model.addItem(MenuId.MENU_ID_BACK, "后退");
|
||||
}
|
||||
if (browser.canGoForward()) {
|
||||
model.addItem(MenuId.MENU_ID_FORWARD, "前进");
|
||||
}
|
||||
// 仅有选中内容时显示“复制”
|
||||
String selectionText = params.getSelectionText();
|
||||
if (selectionText != null && !selectionText.trim().isEmpty()) {
|
||||
model.addItem(MenuId.MENU_ID_COPY, "复制");
|
||||
}
|
||||
// 仅在可编辑区域显示“粘贴”
|
||||
if (params.isEditable()) {
|
||||
model.addItem(MenuId.MENU_ID_PASTE, "粘贴");
|
||||
}
|
||||
model.addItem(MenuId.MENU_ID_RELOAD, "刷新");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContextMenuCommand(org.cef.browser.CefBrowser browser, org.cef.browser.CefFrame frame,
|
||||
org.cef.callback.CefContextMenuParams params, int commandId, int eventFlags) {
|
||||
return false; // 使用默认处理
|
||||
}
|
||||
});
|
||||
} catch (Throwable e) {
|
||||
logger.error("Failed to initialize CefBrowser", e);
|
||||
openCasdoorLoginPageInDefaultBrowser();
|
||||
String message = "浏览器初始化失败,已在默认浏览器打开登录页面,请手动完成登录。\n或者手动复制下面链接在浏览器打开进行登录:\n"
|
||||
+ casdoorServer.getSigninUrl();
|
||||
JTextArea textArea = new JTextArea(message);
|
||||
textArea.setEditable(false);
|
||||
textArea.setLineWrap(true);
|
||||
textArea.setWrapStyleWord(true);
|
||||
textArea.setBackground(null);
|
||||
textArea.setSize(504,835);
|
||||
JOptionPane.showMessageDialog(dialog, new JScrollPane(textArea), "内嵌浏览器初始化失败", JOptionPane.WARNING_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
dialog = new JDialog();
|
||||
dialog.setTitle("AXIS 认证");
|
||||
dialog.setSize(504,835);
|
||||
dialog.setLocationRelativeTo(null);
|
||||
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
|
||||
|
||||
JPanel panel = new JPanel();
|
||||
panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
|
||||
|
||||
if (browser != null) {
|
||||
panel.add(browser.getUIComponent());
|
||||
}
|
||||
dialog.add(panel);
|
||||
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(java.awt.event.WindowEvent e) {
|
||||
if (server != null) {
|
||||
server.stop(0);
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void openLoginAndListen() {
|
||||
if (server == null) {
|
||||
startLocalCallbackServer();
|
||||
}
|
||||
if (browser != null) {
|
||||
browser.loadURL(casdoorServer.getSigninUrl());
|
||||
}
|
||||
}
|
||||
|
||||
private void openCasdoorLoginPageInDefaultBrowser() {
|
||||
String loginUrl = casdoorServer.getSigninUrl();
|
||||
try {
|
||||
Desktop.getDesktop().browse(new URI(loginUrl));
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(dialog, "无法打开浏览器: " + ex.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private LoginResult parseUserInfo(String code, String state) {
|
||||
if (code.isEmpty() || state.isEmpty()) {
|
||||
return new LoginResult(false, "Login failed with error: Invalid code or state.", null);
|
||||
}
|
||||
try {
|
||||
String token = casdoorServer.getOAuthToken(code, state);
|
||||
User user = casdoorServer.parseJwtToken(token);
|
||||
return new LoginResult(true, "Login successful.", new LoginData(token, user));
|
||||
} catch (Exception ex) {
|
||||
logger.error("解析登录信息失败: " + ex.getMessage());
|
||||
return new LoginResult(false, "Login failed with error: " + ex.getMessage(), null);
|
||||
}
|
||||
}
|
||||
|
||||
private void resetResult() {
|
||||
loginResult = null;
|
||||
}
|
||||
|
||||
public LoginResult exec() {
|
||||
resetResult();
|
||||
initUI();
|
||||
openLoginAndListen();
|
||||
dialog.setModal(true);
|
||||
isModal = true;
|
||||
dialog.setVisible(windowVisible);
|
||||
return getLoginResult();
|
||||
}
|
||||
|
||||
public CompletableFuture<LoginResult> show() {
|
||||
resetResult();
|
||||
initUI();
|
||||
CompletableFuture<LoginResult> future = new CompletableFuture<>();
|
||||
dialog.addWindowListener(new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosed(java.awt.event.WindowEvent e) {
|
||||
future.complete(getLoginResult());
|
||||
}
|
||||
});
|
||||
openLoginAndListen();
|
||||
dialog.setModal(false);
|
||||
isModal = false;
|
||||
dialog.setVisible(true);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public boolean isModal() {
|
||||
return isModal;
|
||||
}
|
||||
|
||||
public void setVisible(boolean b) {
|
||||
this.windowVisible = b;
|
||||
dialog.setVisible(b);
|
||||
}
|
||||
|
||||
public LoginResult getLoginResult() {
|
||||
return loginResult;
|
||||
}
|
||||
|
||||
public static LoginResult showLoginDialogAndGetLoginResult() throws InterruptedException, InvocationTargetException {
|
||||
AtomicReference<LoginResult> result = new AtomicReference<>();
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
CasdoorLoginWindow window = new CasdoorLoginWindow();
|
||||
result.set(window.exec());
|
||||
});
|
||||
return result.get();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||
Result result = showLoginDialogAndGetLoginResult();
|
||||
System.out.println("Login result: " + result);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,539 +0,0 @@
|
||||
package com.axis.innovators.box.window;
|
||||
|
||||
import com.axis.innovators.box.verification.OnlineVerification;
|
||||
import com.axis.innovators.box.verification.UserTags;
|
||||
import com.axis.innovators.box.verification.VerificationService;
|
||||
import com.formdev.flatlaf.FlatDarculaLaf;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.LineBorder;
|
||||
import javax.swing.text.JTextComponent;
|
||||
import java.awt.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* 重构后的现代化单窗口登录/注册/找回密码界面(带平滑切换动画)
|
||||
* 保留原有验证逻辑接口调用(OnlineVerification / VerificationService)
|
||||
*
|
||||
* 说明:
|
||||
* - 单窗口(JDialog)内使用滑动动画切换视图(仿微软登录体验)。
|
||||
* - 所有子界面(登录/注册/找回密码)都在同一容器中切换,不再弹新窗口。
|
||||
* - 按钮与输入框固定宽度,避免被挤压变形。
|
||||
*
|
||||
* 注意:需要 flatlaf 依赖以呈现更现代的外观。
|
||||
*/
|
||||
public class LoginWindow {
|
||||
private static final AtomicReference<UserTags> loginResult = new AtomicReference<>(UserTags.None);
|
||||
|
||||
private final JDialog dialog;
|
||||
private final JLayeredPane layeredPane;
|
||||
private final int DIALOG_WIDTH = 460;
|
||||
private final int DIALOG_HEIGHT = 560;
|
||||
|
||||
// 登录面板中的控件需要在类域以便访问
|
||||
private JTextField loginEmailField;
|
||||
private JPasswordField loginPasswordField;
|
||||
|
||||
public static UserTags createAndShow() throws InterruptedException, InvocationTargetException {
|
||||
AtomicReference<UserTags> result = new AtomicReference<>(UserTags.None);
|
||||
SwingUtilities.invokeAndWait(() -> {
|
||||
LoginWindow window = new LoginWindow();
|
||||
window.dialog.setVisible(true);
|
||||
result.set(loginResult.get());
|
||||
if (result.get() == UserTags.None) {
|
||||
System.exit(0);
|
||||
}
|
||||
});
|
||||
return result.get();
|
||||
}
|
||||
|
||||
public LoginWindow() {
|
||||
setupLookAndFeel();
|
||||
dialog = new JDialog((Frame) null, "AXIS 安全认证", true);
|
||||
dialog.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
|
||||
dialog.setSize(DIALOG_WIDTH, DIALOG_HEIGHT);
|
||||
dialog.setResizable(false);
|
||||
dialog.setLocationRelativeTo(null);
|
||||
|
||||
// 根容器:深色背景并居中卡片
|
||||
JPanel root = new JPanel(new GridBagLayout());
|
||||
root.setBackground(new Color(0x202225));
|
||||
root.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
|
||||
dialog.setContentPane(root);
|
||||
|
||||
// 卡片容器(居中)
|
||||
JPanel cardWrapper = new JPanel(null) {
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
return new Dimension(DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40);
|
||||
}
|
||||
};
|
||||
cardWrapper.setOpaque(false);
|
||||
cardWrapper.setPreferredSize(new Dimension(DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40));
|
||||
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = 0;
|
||||
root.add(cardWrapper, gbc);
|
||||
|
||||
// 分层面板用于动画
|
||||
layeredPane = new JLayeredPane();
|
||||
layeredPane.setBounds(0, 0, DIALOG_WIDTH - 40, DIALOG_HEIGHT - 40);
|
||||
cardWrapper.add(layeredPane);
|
||||
|
||||
// 卡片背景(圆角)
|
||||
JPanel backgroundCard = new JPanel();
|
||||
backgroundCard.setBackground(new Color(0x2A2E31));
|
||||
backgroundCard.setBorder(new RoundBorder(16, new Color(0x3A3F42)));
|
||||
backgroundCard.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight());
|
||||
backgroundCard.setLayout(null);
|
||||
layeredPane.add(backgroundCard, Integer.valueOf(0));
|
||||
|
||||
// 创建三个面板(宽度等于容器宽度),初始将登录面板放置在0位置
|
||||
JPanel loginPanel = buildLoginPanel();
|
||||
JPanel registerPanel = buildRegisterPanel();
|
||||
JPanel forgotPanel = buildForgotPanel();
|
||||
|
||||
int w = layeredPane.getWidth();
|
||||
int h = layeredPane.getHeight();
|
||||
|
||||
loginPanel.setBounds(0, 0, w, h);
|
||||
registerPanel.setBounds(w, 0, w, h); // 右侧预置
|
||||
forgotPanel.setBounds(w * 2, 0, w, h);
|
||||
|
||||
layeredPane.add(loginPanel, Integer.valueOf(1));
|
||||
layeredPane.add(registerPanel, Integer.valueOf(1));
|
||||
layeredPane.add(forgotPanel, Integer.valueOf(1));
|
||||
|
||||
dialog.pack();
|
||||
// ensure layered sizes match after pack
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
layeredPane.setBounds(0, 0, cardWrapper.getWidth(), cardWrapper.getHeight());
|
||||
backgroundCard.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight());
|
||||
int nw = layeredPane.getWidth(), nh = layeredPane.getHeight();
|
||||
loginPanel.setBounds(0, 0, nw, nh);
|
||||
registerPanel.setBounds(nw, 0, nw, nh);
|
||||
forgotPanel.setBounds(nw * 2, 0, nw, nh);
|
||||
});
|
||||
}
|
||||
|
||||
// 切换动画:direction = 1 表示向左滑动进入下一页(当前 -> 右侧),-1 表示向右滑动进入上一页
|
||||
private void slideTo(int targetIndex) {
|
||||
Component[] comps = layeredPane.getComponents();
|
||||
// 每个面板宽度
|
||||
int w = layeredPane.getWidth();
|
||||
// 当前最左边的x位置(找到最左的那个主要面板)
|
||||
// 我们采用简单方式:目标面板应该位于 x = targetIndex * w (0,1,2)
|
||||
int targetX = -targetIndex * w; // 我们会将所有面板整体平移,使目标显示在 x=0
|
||||
|
||||
// 获取当前 offset (使用第一个面板的 x 来代表整体偏移)
|
||||
int startOffset = 0;
|
||||
// find current offset by checking bounds of first panel (assume index 0 is login)
|
||||
if (comps.length > 0) {
|
||||
startOffset = comps[0].getX();
|
||||
}
|
||||
|
||||
int start = startOffset;
|
||||
int end = targetX;
|
||||
|
||||
int duration = 300; // ms
|
||||
int fps = 60;
|
||||
int delay = 1000 / fps;
|
||||
int steps = Math.max(1, duration / delay);
|
||||
final int[] step = {0};
|
||||
|
||||
Timer timer = new Timer(delay, null);
|
||||
timer.addActionListener((ActionEvent e) -> {
|
||||
step[0]++;
|
||||
double t = (double) step[0] / steps;
|
||||
// ease in-out cubic
|
||||
double tt = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
||||
int cur = (int) Math.round(start + (end - start) * tt);
|
||||
|
||||
// 平移所有在 layeredPane 中(除背景)组件
|
||||
for (Component c : layeredPane.getComponents()) {
|
||||
if (c instanceof JPanel && c.isVisible()) {
|
||||
// 计算原始索引根据名字
|
||||
String name = c.getName();
|
||||
// 我们之前把panel放在 x = idx * w ; 现在把它设置为 idx*w + cur
|
||||
int idx = 0;
|
||||
if ("login".equals(name)) idx = 0;
|
||||
else if ("register".equals(name)) idx = 1;
|
||||
else if ("forgot".equals(name)) idx = 2;
|
||||
c.setLocation(idx * w + cur, 0);
|
||||
}
|
||||
}
|
||||
|
||||
layeredPane.repaint();
|
||||
|
||||
if (step[0] >= steps) {
|
||||
timer.stop();
|
||||
}
|
||||
});
|
||||
timer.setInitialDelay(0);
|
||||
timer.start();
|
||||
}
|
||||
|
||||
private JPanel buildLoginPanel() {
|
||||
JPanel p = new JPanel(null);
|
||||
p.setOpaque(false);
|
||||
p.setName("login");
|
||||
int w = DIALOG_WIDTH - 40;
|
||||
int h = DIALOG_HEIGHT - 40;
|
||||
|
||||
// 标题区
|
||||
JLabel appTitle = new JLabel("AXIS");
|
||||
appTitle.setFont(new Font("微软雅黑", Font.BOLD, 28));
|
||||
appTitle.setForeground(Color.WHITE);
|
||||
appTitle.setBounds(28, 20, w - 56, 36);
|
||||
|
||||
JLabel subtitle = new JLabel("安全认证 — 请登录以继续");
|
||||
subtitle.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
subtitle.setForeground(new Color(0xA7AEB5));
|
||||
subtitle.setBounds(28, 56, w - 56, 18);
|
||||
|
||||
p.add(appTitle);
|
||||
p.add(subtitle);
|
||||
|
||||
// 卡片内控件起始 y
|
||||
int startY = 96;
|
||||
int fieldW = Math.min(360, w - 56);
|
||||
int fieldX = (w - fieldW) / 2;
|
||||
|
||||
// Email
|
||||
JLabel emailLabel = new JLabel("账号");
|
||||
emailLabel.setForeground(new Color(0x99A0A7));
|
||||
emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
|
||||
emailLabel.setBounds(fieldX, startY, fieldW, 18);
|
||||
|
||||
loginEmailField = new JTextField();
|
||||
styleInput(loginEmailField);
|
||||
loginEmailField.setBounds(fieldX, startY + 22, fieldW, 44);
|
||||
loginEmailField.putClientProperty("JTextField.placeholderText", "邮箱或手机号");
|
||||
|
||||
// Password
|
||||
JLabel pwdLabel = new JLabel("密码");
|
||||
pwdLabel.setForeground(new Color(0x99A0A7));
|
||||
pwdLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
|
||||
pwdLabel.setBounds(fieldX, startY + 22 + 44 + 12, fieldW, 18);
|
||||
|
||||
loginPasswordField = new JPasswordField();
|
||||
styleInput(loginPasswordField);
|
||||
loginPasswordField.setBounds(fieldX, startY + 22 + 44 + 12 + 20, fieldW - 48, 44);
|
||||
loginPasswordField.putClientProperty("JTextField.placeholderText", "请输入密码");
|
||||
|
||||
// eye toggle
|
||||
JToggleButton eyeBtn = new JToggleButton("显示");
|
||||
eyeBtn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
eyeBtn.setFocusable(false);
|
||||
eyeBtn.setBorderPainted(false);
|
||||
eyeBtn.setContentAreaFilled(true);
|
||||
eyeBtn.setBackground(new Color(0x39424A));
|
||||
eyeBtn.setForeground(Color.WHITE);
|
||||
eyeBtn.setBounds(fieldX + fieldW - 44, startY + 22 + 44 + 12 + 20, 44, 44);
|
||||
eyeBtn.addActionListener(e -> {
|
||||
if (eyeBtn.isSelected()) loginPasswordField.setEchoChar((char) 0);
|
||||
else loginPasswordField.setEchoChar('•');
|
||||
});
|
||||
|
||||
// 登录按钮(占满宽度)
|
||||
JButton loginBtn = new JButton("立即登录");
|
||||
stylePrimaryButton(loginBtn);
|
||||
loginBtn.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 22, fieldW, 48);
|
||||
loginBtn.addActionListener(e -> doLogin());
|
||||
|
||||
// 链接区域(注册 / 忘记密码) — 点击切换到对应面板
|
||||
JButton toRegister = createTextLink("注册账号");
|
||||
toRegister.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 22 + 60, 120, 20);
|
||||
toRegister.addActionListener(e -> slideTo(1));
|
||||
|
||||
JButton toForgot = createTextLink("忘记密码");
|
||||
toForgot.setBounds(fieldX + fieldW - 120, startY + 22 + 44 + 12 + 20 + 44 + 22 + 60, 120, 20);
|
||||
toForgot.addActionListener(e -> slideTo(2));
|
||||
|
||||
// footer
|
||||
JLabel footer = new JLabel("使用你的 AXIS 账户进行登录。");
|
||||
footer.setForeground(new Color(0x8F969C));
|
||||
footer.setFont(new Font("微软雅黑", Font.PLAIN, 11));
|
||||
footer.setBounds(fieldX, h - 44, fieldW, 18);
|
||||
|
||||
p.add(emailLabel);
|
||||
p.add(loginEmailField);
|
||||
p.add(pwdLabel);
|
||||
p.add(loginPasswordField);
|
||||
p.add(eyeBtn);
|
||||
p.add(loginBtn);
|
||||
p.add(toRegister);
|
||||
p.add(toForgot);
|
||||
p.add(footer);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private JPanel buildRegisterPanel() {
|
||||
JPanel p = new JPanel(null);
|
||||
p.setOpaque(false);
|
||||
p.setName("register");
|
||||
int w = DIALOG_WIDTH - 40;
|
||||
int h = DIALOG_HEIGHT - 40;
|
||||
|
||||
JLabel title = new JLabel("创建账号");
|
||||
title.setFont(new Font("微软雅黑", Font.BOLD, 24));
|
||||
title.setForeground(Color.WHITE);
|
||||
title.setBounds(28, 20, w - 56, 36);
|
||||
|
||||
JLabel desc = new JLabel("快速创建你的 AXIS 帐号");
|
||||
desc.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
desc.setForeground(new Color(0xA7AEB5));
|
||||
desc.setBounds(28, 56, w - 56, 18);
|
||||
|
||||
p.add(title);
|
||||
p.add(desc);
|
||||
|
||||
int startY = 96;
|
||||
int fieldW = Math.min(360, w - 56);
|
||||
int fieldX = (w - fieldW) / 2;
|
||||
|
||||
// 用户名
|
||||
JLabel nameLabel = new JLabel("用户名");
|
||||
nameLabel.setForeground(new Color(0x99A0A7));
|
||||
nameLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
|
||||
nameLabel.setBounds(fieldX, startY, fieldW, 18);
|
||||
|
||||
JTextField nameField = new JTextField();
|
||||
styleInput(nameField);
|
||||
nameField.setBounds(fieldX, startY + 22, fieldW, 44);
|
||||
|
||||
// 邮箱
|
||||
JLabel emailLabel = new JLabel("邮箱");
|
||||
emailLabel.setForeground(new Color(0x99A0A7));
|
||||
emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
|
||||
emailLabel.setBounds(fieldX, startY + 22 + 44 + 12, fieldW, 18);
|
||||
|
||||
JTextField emailField = new JTextField();
|
||||
styleInput(emailField);
|
||||
emailField.setBounds(fieldX, startY + 22 + 44 + 12 + 20, fieldW, 44);
|
||||
|
||||
// 密码
|
||||
JLabel pwdLabel = new JLabel("密码");
|
||||
pwdLabel.setForeground(new Color(0x99A0A7));
|
||||
pwdLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
|
||||
pwdLabel.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12, fieldW, 18);
|
||||
|
||||
JPasswordField pwdField = new JPasswordField();
|
||||
styleInput(pwdField);
|
||||
pwdField.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20, fieldW, 44);
|
||||
|
||||
// 确认密码
|
||||
JLabel confirmLabel = new JLabel("确认密码");
|
||||
confirmLabel.setForeground(new Color(0x99A0A7));
|
||||
confirmLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
|
||||
confirmLabel.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12, fieldW, 18);
|
||||
|
||||
JPasswordField confirmField = new JPasswordField();
|
||||
styleInput(confirmField);
|
||||
confirmField.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12 + 20, fieldW, 44);
|
||||
|
||||
// 注册按钮
|
||||
JButton regBtn = new JButton("创建账号");
|
||||
stylePrimaryButton(regBtn);
|
||||
regBtn.setBounds(fieldX, startY + 22 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 12 + 20 + 44 + 18, fieldW, 48);
|
||||
|
||||
regBtn.addActionListener(e -> {
|
||||
String name = nameField.getText().trim();
|
||||
String email = emailField.getText().trim();
|
||||
String pwd = new String(pwdField.getPassword());
|
||||
String confirm = new String(confirmField.getPassword());
|
||||
|
||||
if (name.isEmpty() || email.isEmpty() || pwd.isEmpty() || confirm.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(dialog, "请完整填写注册信息", "注册错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (!pwd.equals(confirm)) {
|
||||
JOptionPane.showMessageDialog(dialog, "两次输入的密码不一致", "注册错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
boolean success = VerificationService.registerUser(name, email, pwd);
|
||||
if (success) {
|
||||
JOptionPane.showMessageDialog(dialog, "注册成功,请登录", "注册成功", JOptionPane.INFORMATION_MESSAGE);
|
||||
slideTo(0); // 回到登录页面
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(dialog, "注册失败,请检查信息", "注册错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
|
||||
// 返回登录链接
|
||||
JButton back = createTextLink("返回登录");
|
||||
back.setBounds(fieldX, regBtn.getY() + regBtn.getHeight() + 12, 120, 20);
|
||||
back.addActionListener(e -> slideTo(0));
|
||||
|
||||
p.add(nameLabel);
|
||||
p.add(nameField);
|
||||
p.add(emailLabel);
|
||||
p.add(emailField);
|
||||
p.add(pwdLabel);
|
||||
p.add(pwdField);
|
||||
p.add(confirmLabel);
|
||||
p.add(confirmField);
|
||||
p.add(regBtn);
|
||||
p.add(back);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private JPanel buildForgotPanel() {
|
||||
JPanel p = new JPanel(null);
|
||||
p.setOpaque(false);
|
||||
p.setName("forgot");
|
||||
int w = DIALOG_WIDTH - 40;
|
||||
int h = DIALOG_HEIGHT - 40;
|
||||
|
||||
JLabel title = new JLabel("找回密码");
|
||||
title.setFont(new Font("微软雅黑", Font.BOLD, 24));
|
||||
title.setForeground(Color.WHITE);
|
||||
title.setBounds(28, 20, w - 56, 36);
|
||||
|
||||
JLabel desc = new JLabel("通过注册邮箱重置密码");
|
||||
desc.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
desc.setForeground(new Color(0xA7AEB5));
|
||||
desc.setBounds(28, 56, w - 56, 18);
|
||||
|
||||
p.add(title);
|
||||
p.add(desc);
|
||||
|
||||
int startY = 110;
|
||||
int fieldW = Math.min(360, w - 56);
|
||||
int fieldX = (w - fieldW) / 2;
|
||||
|
||||
JLabel emailLabel = new JLabel("注册邮箱");
|
||||
emailLabel.setForeground(new Color(0x99A0A7));
|
||||
emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 13));
|
||||
emailLabel.setBounds(fieldX, startY, fieldW, 18);
|
||||
|
||||
JTextField emailField = new JTextField();
|
||||
styleInput(emailField);
|
||||
emailField.setBounds(fieldX, startY + 22, fieldW, 44);
|
||||
|
||||
JButton sendBtn = new JButton("发送重置邮件");
|
||||
stylePrimaryButton(sendBtn);
|
||||
sendBtn.setBounds(fieldX, startY + 22 + 44 + 22, fieldW, 48);
|
||||
|
||||
sendBtn.addActionListener(e -> {
|
||||
String email = emailField.getText().trim();
|
||||
if (email.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(dialog, "请输入注册邮箱", "错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
if (VerificationService.sendPasswordReset(email)) {
|
||||
JOptionPane.showMessageDialog(dialog, "重置邮件已发送,请查收", "成功", JOptionPane.INFORMATION_MESSAGE);
|
||||
slideTo(0);
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(dialog, "发送失败或邮箱未注册", "失败", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
});
|
||||
|
||||
JButton back = createTextLink("返回登录");
|
||||
back.setBounds(fieldX, sendBtn.getY() + sendBtn.getHeight() + 12, 120, 20);
|
||||
back.addActionListener(e -> slideTo(0));
|
||||
|
||||
p.add(emailLabel);
|
||||
p.add(emailField);
|
||||
p.add(sendBtn);
|
||||
p.add(back);
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
private void doLogin() {
|
||||
String email = loginEmailField.getText().trim();
|
||||
String password = new String(loginPasswordField.getPassword()).trim();
|
||||
|
||||
if (email.isEmpty() || password.isEmpty()) {
|
||||
JOptionPane.showMessageDialog(dialog, "请输入完整的登录信息", "验证错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
OnlineVerification onlineVerification = OnlineVerification.validateLogin(email, password);
|
||||
if (onlineVerification == null) {
|
||||
String err = OnlineVerification.errorMessage != null && !OnlineVerification.errorMessage.trim().isEmpty()
|
||||
? OnlineVerification.errorMessage
|
||||
: "验证失败,请重试";
|
||||
JOptionPane.showMessageDialog(dialog, err, "验证错误", JOptionPane.ERROR_MESSAGE);
|
||||
return;
|
||||
}
|
||||
|
||||
loginResult.set(VerificationService.determineUserType(onlineVerification));
|
||||
dialog.dispose();
|
||||
}
|
||||
|
||||
|
||||
// 通用输入框样式
|
||||
private void styleInput(JComponent comp) {
|
||||
comp.setFont(new Font("微软雅黑", Font.PLAIN, 14));
|
||||
comp.setBackground(new Color(0x2F3336));
|
||||
comp.setForeground(new Color(0xE8ECEF));
|
||||
comp.setBorder(BorderFactory.createCompoundBorder(
|
||||
new RoundBorder(8, new Color(0x3A3F42)),
|
||||
BorderFactory.createEmptyBorder(10, 12, 10, 12)
|
||||
));
|
||||
if (comp instanceof JTextComponent) ((JTextComponent) comp).setCaretColor(new Color(0x9AA0A6));
|
||||
}
|
||||
|
||||
// 主要操作按钮样式(主色)
|
||||
private void stylePrimaryButton(AbstractButton b) {
|
||||
b.setFont(new Font("微软雅黑", Font.BOLD, 14));
|
||||
b.setForeground(Color.WHITE);
|
||||
b.setBackground(new Color(0x2B79D0));
|
||||
b.setBorderPainted(false);
|
||||
b.setFocusPainted(false);
|
||||
b.setOpaque(true);
|
||||
b.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
}
|
||||
|
||||
private JButton createTextLink(String text) {
|
||||
JButton btn = new JButton(text);
|
||||
btn.setFont(new Font("微软雅黑", Font.PLAIN, 12));
|
||||
btn.setForeground(new Color(0x79A6FF));
|
||||
btn.setBorderPainted(false);
|
||||
btn.setContentAreaFilled(false);
|
||||
btn.setFocusPainted(false);
|
||||
btn.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
return btn;
|
||||
}
|
||||
|
||||
private void setupLookAndFeel() {
|
||||
try {
|
||||
UIManager.setLookAndFeel(new FlatDarculaLaf());
|
||||
UIManager.put("Component.arc", 12);
|
||||
UIManager.put("TextComponent.arc", 12);
|
||||
UIManager.put("Button.arc", 10);
|
||||
|
||||
UIManager.put("Panel.background", new Color(0x202225));
|
||||
UIManager.put("TextComponent.background", new Color(0x2F3336));
|
||||
UIManager.put("TextComponent.foreground", new Color(0xE8ECEF));
|
||||
} catch (UnsupportedLookAndFeelException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
// 圆角边框
|
||||
private static class RoundBorder extends LineBorder {
|
||||
private final int radius;
|
||||
|
||||
public RoundBorder(int radius, Color color) {
|
||||
super(color, 1);
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2.setColor(lineColor);
|
||||
g2.drawRoundRect(x, y, width - 1, height - 1, radius, radius);
|
||||
g2.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,14 +14,18 @@ import javax.swing.border.Border;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import javax.swing.plaf.FontUIResource;
|
||||
import javax.swing.plaf.LayerUI;
|
||||
import javax.swing.plaf.PanelUI;
|
||||
import javax.swing.plaf.basic.BasicScrollBarUI;
|
||||
import javax.swing.plaf.basic.BasicTabbedPaneUI;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.geom.AffineTransform;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.awt.image.ConvolveOp;
|
||||
import java.awt.image.Kernel;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@@ -34,6 +38,8 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
* - 侧栏颜色使用 #3C3F41(按你的要求)
|
||||
* - 搜索框以居中为主,聚焦时带放大动画与圆角外观
|
||||
* - 尽量避免离屏绘制遗留(使用 component.printAll() 截图)
|
||||
* - 添加背景图片和玻璃模糊效果支持
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class MainWindow extends JFrame {
|
||||
private static final Logger logger = LogManager.getLogger(MainWindow.class);
|
||||
@@ -56,27 +62,18 @@ public class MainWindow extends JFrame {
|
||||
private JPanel sideBar;
|
||||
private JPanel contentPanel;
|
||||
private RoundedSearchField searchField;
|
||||
private JLabel status;
|
||||
|
||||
// 背景图片相关字段
|
||||
private Image backgroundImage;
|
||||
private float blurAmount = 0.0f;
|
||||
private float backgroundOpacity = 1.0f;
|
||||
private BufferedImage cachedBlurredBackground;
|
||||
private Dimension cachedBackgroundSize;
|
||||
|
||||
// settings dialog
|
||||
private WindowsJDialog dialog;
|
||||
|
||||
// 侧边栏颜色(比面板背景稍暗)
|
||||
private static Color SIDEBAR_COLOR = Optional.ofNullable(UIManager.getColor("Panel.background"))
|
||||
.orElse(new Color(0x3C3F41));
|
||||
|
||||
// 卡片背景(深色模式适配,比侧边栏稍亮)
|
||||
private static Color CARD_BG = Optional.ofNullable(UIManager.getColor("control"))
|
||||
.map(bg -> ThemeColors.brighten(bg, 0.1f))
|
||||
.orElse(new Color(0x4A4D50));
|
||||
|
||||
// 卡片边框(使用系统边框色或稍亮颜色)
|
||||
private static Color CARD_BORDER = Optional.ofNullable(UIManager.getColor("controlHighlight"))
|
||||
.orElse(new Color(0x5C5F61));
|
||||
|
||||
// 文本颜色(直接使用系统主题的文本颜色)
|
||||
private static Color TEXT_COLOR = Optional.ofNullable(UIManager.getColor("textText"))
|
||||
.orElse(new Color(0xE0E0E0));
|
||||
|
||||
public MainWindow() {
|
||||
// 增强字体设置逻辑:优先使用系统支持的中文字体
|
||||
String[] fontNames = {"Microsoft YaHei", "微软雅黑", "PingFang SC", "SimHei", "宋体", "新宋体", "SansSerif"};
|
||||
@@ -123,13 +120,137 @@ public class MainWindow extends JFrame {
|
||||
if (layeredPane != null && cardsPanel != null) {
|
||||
cardsPanel.setBounds(0, 0, layeredPane.getWidth(), layeredPane.getHeight());
|
||||
}
|
||||
// 窗口大小改变时重新生成模糊背景
|
||||
if (backgroundImage != null) {
|
||||
cachedBlurredBackground = null;
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
//setLocationRelativeTo(null);
|
||||
|
||||
addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentShown(ComponentEvent e) {
|
||||
// 确保只执行一次
|
||||
removeComponentListener(this);
|
||||
setLocationRelativeTo(null);
|
||||
}
|
||||
});
|
||||
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
setSize(1200, 800);
|
||||
setSize(1060, 670);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置背景图片和玻璃模糊效果
|
||||
* @param backgroundImage 背景图片
|
||||
* @param blurAmount 模糊程度 (0.0f - 1.0f),0为不模糊,1为最大模糊
|
||||
* @param opacity 透明度 (0.0f - 1.0f),0为完全透明,1为完全不透明
|
||||
*/
|
||||
public void setBackgroundWithGlassEffect(Image backgroundImage, float blurAmount, float opacity) {
|
||||
this.backgroundImage = backgroundImage;
|
||||
this.blurAmount = Math.max(0.0f, Math.min(1.0f, blurAmount));
|
||||
this.backgroundOpacity = Math.max(0.0f, Math.min(1.0f, opacity));
|
||||
this.cachedBlurredBackground = null;
|
||||
this.cachedBackgroundSize = null;
|
||||
AxisInnovatorsBox.getMain().reloadAllWindow();
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除背景图片
|
||||
*/
|
||||
public void removeBackground() {
|
||||
this.backgroundImage = null;
|
||||
this.cachedBlurredBackground = null;
|
||||
this.cachedBackgroundSize = null;
|
||||
|
||||
// 这是段重复的石山代码我不想改了,作用用py想都知道是更新窗口渲染
|
||||
// 别问我为什么不用AxisInnovatorsBox.getMain().reloadAllWindow();
|
||||
// 因为AxisInnovatorsBox.getMain().reloadAllWindow();会广播到所有窗口
|
||||
getContentPane().removeAll();
|
||||
repaint();
|
||||
initUI();
|
||||
updateTheme();
|
||||
revalidate();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用高斯模糊到图片
|
||||
*/
|
||||
private BufferedImage applyGaussianBlur(BufferedImage image, float blurFactor) {
|
||||
if (blurFactor <= 0.0f) return image;
|
||||
|
||||
// 根据模糊因子计算模糊半径 (1-15像素)
|
||||
int radius = Math.max(1, (int)(blurFactor * 15));
|
||||
|
||||
// 确保半径为奇数
|
||||
if (radius % 2 == 0) radius++;
|
||||
|
||||
int size = radius * 2 + 1;
|
||||
float[] data = new float[size * size];
|
||||
|
||||
// 计算高斯核
|
||||
float sigma = radius / 3.0f;
|
||||
float twoSigmaSquare = 2.0f * sigma * sigma;
|
||||
float sigmaRoot = (float) Math.sqrt(twoSigmaSquare * Math.PI);
|
||||
float total = 0.0f;
|
||||
|
||||
for (int i = -radius; i <= radius; i++) {
|
||||
for (int j = -radius; j <= radius; j++) {
|
||||
float distance = i * i + j * j;
|
||||
data[(i + radius) * size + (j + radius)] =
|
||||
(float) Math.exp(-distance / twoSigmaSquare) / sigmaRoot;
|
||||
total += data[(i + radius) * size + (j + radius)];
|
||||
}
|
||||
}
|
||||
|
||||
// 归一化
|
||||
for (int i = 0; i < data.length; i++) {
|
||||
data[i] /= total;
|
||||
}
|
||||
|
||||
Kernel kernel = new Kernel(size, size, data);
|
||||
ConvolveOp convolve = new ConvolveOp(kernel, ConvolveOp.EDGE_NO_OP, null);
|
||||
return convolve.filter(image, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建带模糊效果的背景图片
|
||||
*/
|
||||
private BufferedImage createBlurredBackground(Dimension size) {
|
||||
if (backgroundImage == null) return null;
|
||||
|
||||
// 如果尺寸相同且已有缓存,直接返回缓存
|
||||
if (cachedBlurredBackground != null && cachedBackgroundSize != null &&
|
||||
cachedBackgroundSize.equals(size)) {
|
||||
return cachedBlurredBackground;
|
||||
}
|
||||
|
||||
// 创建背景图片
|
||||
BufferedImage bgImage = new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = bgImage.createGraphics();
|
||||
|
||||
// 设置渲染质量
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
|
||||
// 绘制原始背景图片(缩放以适应窗口)
|
||||
g2d.drawImage(backgroundImage, 0, 0, size.width, size.height, null);
|
||||
g2d.dispose();
|
||||
|
||||
// 应用模糊效果
|
||||
if (blurAmount > 0.0f) {
|
||||
bgImage = applyGaussianBlur(bgImage, blurAmount);
|
||||
}
|
||||
|
||||
// 缓存结果
|
||||
cachedBlurredBackground = bgImage;
|
||||
cachedBackgroundSize = new Dimension(size);
|
||||
|
||||
return bgImage;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,13 +279,56 @@ public class MainWindow extends JFrame {
|
||||
|
||||
setTitle(LanguageManager.getLoadedLanguages().getText("mainWindow.title"));
|
||||
|
||||
// 主容器
|
||||
JPanel mainPanel = new JPanel(new BorderLayout());
|
||||
// 主容器 - 使用自定义面板以支持背景绘制
|
||||
JPanel mainPanel = new JPanel(new BorderLayout()) {
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
|
||||
// 如果有背景图片,绘制背景
|
||||
if (backgroundImage != null) {
|
||||
Graphics2D g2d = (Graphics2D) g.create();
|
||||
|
||||
// 设置透明度
|
||||
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, backgroundOpacity));
|
||||
|
||||
// 获取带模糊效果的背景
|
||||
Dimension size = getSize();
|
||||
BufferedImage bg = createBlurredBackground(size);
|
||||
|
||||
if (bg != null) {
|
||||
g2d.drawImage(bg, 0, 0, null);
|
||||
}
|
||||
|
||||
g2d.dispose();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder());
|
||||
mainPanel.setOpaque(true);
|
||||
|
||||
// 设置背景色(如果有背景图片,则使用半透明背景)
|
||||
if (backgroundImage != null) {
|
||||
// 当有背景图片时,使用半透明的背景色
|
||||
Color panelBg = UIManager.getColor("Panel.background");
|
||||
if (panelBg != null) {
|
||||
// 降低背景色的不透明度,让背景图片透出来
|
||||
Color semiTransparentBg = new Color(
|
||||
panelBg.getRed(),
|
||||
panelBg.getGreen(),
|
||||
panelBg.getBlue(),
|
||||
(int)(200 * backgroundOpacity) // 调整透明度
|
||||
);
|
||||
mainPanel.setBackground(semiTransparentBg);
|
||||
}
|
||||
} else {
|
||||
// 没有背景图片时使用正常背景色
|
||||
Color panelBg = UIManager.getColor("Panel.background");
|
||||
if (panelBg == null) panelBg = new Color(245, 246, 248);
|
||||
mainPanel.setBackground(panelBg);
|
||||
}
|
||||
|
||||
mainPanel.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
|
||||
mainPanel.add(createHeader(), BorderLayout.NORTH);
|
||||
@@ -288,7 +452,19 @@ public class MainWindow extends JFrame {
|
||||
JComponent contentPane = (JComponent) content;
|
||||
Color panelBg = UIManager.getColor("Panel.background");
|
||||
if (panelBg == null) panelBg = new Color(245, 246, 248);
|
||||
|
||||
// 如果有背景图片,使用半透明背景
|
||||
if (backgroundImage != null) {
|
||||
Color semiTransparentBg = new Color(
|
||||
panelBg.getRed(),
|
||||
panelBg.getGreen(),
|
||||
panelBg.getBlue(),
|
||||
(int)(200 * backgroundOpacity)
|
||||
);
|
||||
contentPane.setBackground(semiTransparentBg);
|
||||
} else {
|
||||
contentPane.setBackground(panelBg);
|
||||
}
|
||||
contentPane.repaint();
|
||||
}
|
||||
|
||||
@@ -309,7 +485,12 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
|
||||
if (sideBar != null) {
|
||||
if (backgroundImage != null) {
|
||||
sideBar.setOpaque(false);
|
||||
} else {
|
||||
sideBar.setOpaque(true);
|
||||
sideBar.setBackground(getSidebarColor());
|
||||
}
|
||||
sideBar.repaint();
|
||||
}
|
||||
|
||||
@@ -539,7 +720,7 @@ public class MainWindow extends JFrame {
|
||||
private JPanel createSideBar() {
|
||||
JPanel sidebar = new JPanel(new BorderLayout());
|
||||
sidebar.setOpaque(true);
|
||||
sidebar.setBackground(SIDEBAR_COLOR);
|
||||
//sidebar.setBackground(SIDEBAR_COLOR);
|
||||
sidebar.setPreferredSize(new Dimension(220, getHeight()));
|
||||
sidebar.setBorder(null);
|
||||
|
||||
@@ -574,7 +755,7 @@ public class MainWindow extends JFrame {
|
||||
JScrollPane listScroll = new JScrollPane(listPanel);
|
||||
listScroll.setBorder(null);
|
||||
listScroll.setOpaque(false);
|
||||
listScroll.getViewport().setOpaque(false);
|
||||
listScroll.getViewport().setOpaque(false); // 确保视口透明
|
||||
listScroll.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
|
||||
listScroll.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
|
||||
listScroll.getVerticalScrollBar().setUI(new CustomScrollBarUI());
|
||||
@@ -680,7 +861,8 @@ public class MainWindow extends JFrame {
|
||||
));
|
||||
|
||||
// 样式
|
||||
button.setForeground(TEXT_COLOR);
|
||||
button.setForeground(Optional.ofNullable(UIManager.getColor("textText"))
|
||||
.orElse(new Color(0xE0E0E0)));
|
||||
// 初始设为不填充,由 updateSelection 决定是否填充背景
|
||||
button.setOpaque(false);
|
||||
button.setContentAreaFilled(true); // 允许根据 opaque/background 填充(mouse/selected 状态会切换 opaque)
|
||||
@@ -695,7 +877,14 @@ public class MainWindow extends JFrame {
|
||||
boolean selected = Objects.equals(currentCategoryId, category.getId().toString());
|
||||
if (selected) {
|
||||
button.setOpaque(true);
|
||||
/**
|
||||
* Java我草泥马,你马死了
|
||||
*/
|
||||
if (backgroundImage == null) {
|
||||
button.setBackground(SELECT_FILL);
|
||||
} else {
|
||||
button.setBackground(new Color(0, 120, 215, 255));
|
||||
}
|
||||
} else {
|
||||
button.setOpaque(false);
|
||||
// 为避免残留,仍设置透明背景(透明颜色)
|
||||
@@ -749,7 +938,15 @@ public class MainWindow extends JFrame {
|
||||
|
||||
// ---------- 工具卡/面板 ----------
|
||||
private JPanel createToolsPanel(ToolCategory category) {
|
||||
JPanel panel = new JPanel(new WrapLayout(FlowLayout.LEFT, 16, 16));
|
||||
JPanel panel = new JPanel(new WrapLayout(FlowLayout.LEFT, 16, 16)) {
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
// 计算容器宽度以恰好容纳3个卡片和间隙
|
||||
int cardWidth = 240; // 卡片宽度
|
||||
int gap = 16;
|
||||
return new Dimension(cardWidth * 3 + gap * 2, super.getPreferredSize().height);
|
||||
}
|
||||
};
|
||||
panel.setOpaque(false);
|
||||
panel.setBorder(null);
|
||||
|
||||
@@ -764,6 +961,36 @@ public class MainWindow extends JFrame {
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断设置界面是否打开
|
||||
* @return 设置界面是否可见
|
||||
*/
|
||||
public boolean isSettingsVisible(){
|
||||
if (dialog == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!dialog.isDisplayable()) {
|
||||
dialog = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!dialog.isVisible()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (dialog.getParent() != this && dialog.getOwner() != this) {
|
||||
dialog = null;
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
dialog = null;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private JPanel createToolCard(ToolItem tool) {
|
||||
JPanel card = new JPanel() {
|
||||
@Override
|
||||
@@ -778,6 +1005,14 @@ public class MainWindow extends JFrame {
|
||||
Color cardBorder = getCardBorder();
|
||||
Color shadowColor = isDarkTheme() ? new Color(30, 30, 30) : Color.BLACK;
|
||||
|
||||
// 如果有背景图片,卡片背景使用半透明
|
||||
if (backgroundImage != null) {
|
||||
cardBg = new Color(cardBg.getRed(), cardBg.getGreen(), cardBg.getBlue(),
|
||||
(int)(200 * backgroundOpacity));
|
||||
cardBorder = new Color(cardBorder.getRed(), cardBorder.getGreen(),
|
||||
cardBorder.getBlue(), (int)(200 * backgroundOpacity));
|
||||
}
|
||||
|
||||
// 1. 绘制阴影(根据主题调整透明度)
|
||||
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, isDarkTheme() ? 0.12f : 0.06f));
|
||||
g2.setColor(shadowColor);
|
||||
@@ -826,8 +1061,10 @@ public class MainWindow extends JFrame {
|
||||
titleLabel.setForeground(UIManager.getColor("Label.foreground"));
|
||||
JTextArea descArea = new JTextArea(tool.getDescription());
|
||||
descArea.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei", "SansSerif", 13).getName(), Font.PLAIN, 13));
|
||||
titleLabel.setForeground(TEXT_COLOR);
|
||||
descArea.setForeground(TEXT_COLOR.darker());
|
||||
titleLabel.setForeground(Optional.ofNullable(UIManager.getColor("textText"))
|
||||
.orElse(new Color(0xE0E0E0)));
|
||||
descArea.setForeground(Optional.ofNullable(UIManager.getColor("textText"))
|
||||
.orElse(new Color(0xE0E0E0)).darker());
|
||||
descArea.setForeground(new Color(100,100,105));
|
||||
descArea.setLineWrap(true);
|
||||
descArea.setWrapStyleWord(true);
|
||||
@@ -925,6 +1162,7 @@ public class MainWindow extends JFrame {
|
||||
card.repaint();
|
||||
if (currentElevation == targetElevation && Math.abs(cardScales.get(card) - targetScale) < 0.002f) ((Timer)e.getSource()).stop();
|
||||
}
|
||||
|
||||
}).start();
|
||||
}
|
||||
|
||||
@@ -1035,13 +1273,21 @@ public class MainWindow extends JFrame {
|
||||
JPanel footer = new JPanel(new BorderLayout());
|
||||
footer.setOpaque(false);
|
||||
footer.setBorder(BorderFactory.createEmptyBorder(8, 8, 8, 8));
|
||||
JLabel status = new JLabel("Ready");
|
||||
status = new JLabel("Ready");
|
||||
status.setFont(new Font(selectFont("Segoe UI", "Microsoft YaHei", "SansSerif", 12).getName(), Font.PLAIN, 12));
|
||||
status.setForeground(UIManager.getColor("Label.foreground"));
|
||||
footer.add(status, BorderLayout.WEST);
|
||||
return footer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态
|
||||
* @param text 状态名称
|
||||
*/
|
||||
public void updataStatus(String text) {
|
||||
status.setText(text);
|
||||
}
|
||||
|
||||
private void updateSideSelection(String categoryId) {
|
||||
for (Map.Entry<String, JButton> e : sideButtons.entrySet()) {
|
||||
String id = e.getKey();
|
||||
@@ -1138,36 +1384,126 @@ public class MainWindow extends JFrame {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- showSettings 恢复实现 ----------
|
||||
// ---------- showSettings 实现 ----------
|
||||
public void showSettings() {
|
||||
if (dialog == null || AxisInnovatorsBox.getMain().isWindowStartup(dialog)) {
|
||||
dialog = new WindowsJDialog(this,
|
||||
LanguageManager.getLoadedLanguages().getText("mainWindow.settings.title"), true);
|
||||
}
|
||||
dialog.setTitle(LanguageManager.getLoadedLanguages().getText("mainWindow.settings.title"));
|
||||
dialog.setSize(680, 480);
|
||||
dialog.setSize(750, 550);
|
||||
dialog.setLocationRelativeTo(this);
|
||||
|
||||
JPanel content = new JPanel(new BorderLayout());
|
||||
content.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
content.setOpaque(false);
|
||||
// 使用 JLayer + LayerUI 来对整个内容做统一的淡入 + 下滑(仿 Apple 风格)动画,
|
||||
// 这样子组件也会跟随一起动画,而不是只有背景绘制发生变化。
|
||||
JPanel inner = new JPanel(new BorderLayout());
|
||||
inner.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12));
|
||||
inner.setOpaque(false); // 背景在 LayerUI 中绘制以便做阴影/圆角
|
||||
|
||||
|
||||
JTabbedPane tabbedPane = new JTabbedPane(JTabbedPane.LEFT);
|
||||
tabbedPane.setOpaque(false);
|
||||
tabbedPane.setBorder(BorderFactory.createEmptyBorder(6, 6, 6, 6));
|
||||
|
||||
JTabbedPane tabbedPane = new JTabbedPane();
|
||||
List<RegistrationSettingsItem> registrationSettingsItemList
|
||||
= RegistrationSettingsItem.getRegistrationSettingsItemList();
|
||||
for (RegistrationSettingsItem registrationSettingsItem : registrationSettingsItemList) {
|
||||
registrationSettingsItem.registration(tabbedPane);
|
||||
}
|
||||
|
||||
content.add(tabbedPane, BorderLayout.CENTER);
|
||||
GlobalEventBus.EVENT_BUS.post(new SettingsLoadEvents(dialog, content));
|
||||
dialog.add(content);
|
||||
inner.add(tabbedPane, BorderLayout.CENTER);
|
||||
GlobalEventBus.EVENT_BUS.post(new SettingsLoadEvents(dialog, inner));
|
||||
|
||||
// LayerUI 实现:负责绘制圆角背景、阴影,并在 paint 中使用 alpha + translate 来实现动画
|
||||
class FadeLayerUI extends LayerUI<JComponent> {
|
||||
private float alpha = 0f;
|
||||
private int translateY = 20;
|
||||
|
||||
public void setAlpha(float alpha) { this.alpha = Math.max(0f, Math.min(1f, alpha)); }
|
||||
public void setTranslateY(int y) { this.translateY = y; }
|
||||
|
||||
@Override
|
||||
public void paint (Graphics g, JComponent c) {
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
int w = c.getWidth();
|
||||
int h = c.getHeight();
|
||||
|
||||
// 平滑处理
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 先绘制柔和阴影(透明度与 alpha 关联)
|
||||
int arc = 16;
|
||||
float shadowAlpha = Math.min(0.35f, alpha * 0.35f);
|
||||
if (shadowAlpha > 0f) {
|
||||
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, shadowAlpha));
|
||||
g2.setColor(new Color(0, 0, 0, 255));
|
||||
// 画一个稍微向下偏移的矩形作为阴影基础
|
||||
g2.fillRoundRect(8, 10 + translateY/3, w - 16, h - 20, arc, arc);
|
||||
}
|
||||
|
||||
// 应用下滑位移 + 透明度到后续的内容绘制(包括子组件)
|
||||
g2.translate(0, translateY);
|
||||
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, alpha));
|
||||
|
||||
Color panelBg = UIManager.getColor("Panel.background");
|
||||
if (panelBg == null)
|
||||
panelBg = new Color(245, 246, 248);
|
||||
// 绘制半透明/圆角的主背景
|
||||
g2.setColor(panelBg);
|
||||
g2.fillRoundRect(0, 0, w, h, arc, arc);
|
||||
|
||||
// 把转换后的 Graphics 传给 super.paint 来绘制子组件(子组件会被 alpha & translate 影响)
|
||||
super.paint(g2, c);
|
||||
g2.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
FadeLayerUI fadeUI = new FadeLayerUI();
|
||||
JLayer<JComponent> layered = new JLayer<>(inner, fadeUI);
|
||||
layered.setOpaque(false);
|
||||
|
||||
dialog.getContentPane().removeAll();
|
||||
dialog.getContentPane().setBackground(new Color(0,0,0,0)); // 保持透明(视平台支持情况)
|
||||
dialog.add(layered);
|
||||
|
||||
// 动画 Timer:350ms 的淡入 + 下滑(使用 ease-out 曲线)
|
||||
final int duration = 350; // ms
|
||||
final long start = System.currentTimeMillis();
|
||||
Timer timer = new Timer(16, null); // ~60fps
|
||||
timer.addActionListener(new ActionListener() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
float t = (System.currentTimeMillis() - start) / (float) duration;
|
||||
if (t >= 1f) t = 1f;
|
||||
// ease-out (cubic)
|
||||
float eased = 1 - (float) Math.pow(1 - t, 3);
|
||||
|
||||
fadeUI.setAlpha(eased);
|
||||
// translate 从 20 -> 0
|
||||
fadeUI.setTranslateY((int) ((1 - eased) * 20));
|
||||
|
||||
layered.repaint();
|
||||
|
||||
if (t >= 1f) {
|
||||
((Timer) e.getSource()).stop();
|
||||
}
|
||||
}
|
||||
});
|
||||
timer.setCoalesce(true);
|
||||
|
||||
// 初始状态隐藏内容(alpha=0),启动动画
|
||||
fadeUI.setAlpha(0f);
|
||||
fadeUI.setTranslateY(20);
|
||||
layered.setVisible(true);
|
||||
timer.start();
|
||||
|
||||
dialog.revalidate();
|
||||
if (AxisInnovatorsBox.getMain().isWindowStartup(dialog)) {
|
||||
AxisInnovatorsBox.getMain().popupWindow(dialog);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---------- 内部类:ToolCategory, ToolItem 等(保持原结构) ----------
|
||||
public static class ToolCategory {
|
||||
private final String name;
|
||||
@@ -1176,24 +1512,29 @@ public class MainWindow extends JFrame {
|
||||
private final String description;
|
||||
private final UUID id = UUID.randomUUID();
|
||||
private final List<ToolItem> tools = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 分类
|
||||
* @param name 分类名称
|
||||
* @param icon 分类的图标
|
||||
* @param description 分类的描述
|
||||
*/
|
||||
public ToolCategory(String name, String icon, String description) {
|
||||
this.name = name; this.icon = icon; this.description = description; this.iconImage = null;
|
||||
}
|
||||
public ToolCategory(String name, ImageIcon icon, String description) {
|
||||
this.name = name; this.iconImage = icon; this.description = description; this.icon = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在当前分类中创建一个新的工具卡片
|
||||
* @param tool 工具卡片
|
||||
*/
|
||||
public void addTool(ToolItem tool) { tools.add(tool); }
|
||||
public String getDescription() { return description; }
|
||||
public String getIcon() {
|
||||
if (isDarkTheme()) {
|
||||
int lastDotIndex = 0;
|
||||
if (icon != null) {
|
||||
lastDotIndex = icon.lastIndexOf('.');
|
||||
}
|
||||
if (lastDotIndex > 0) {
|
||||
return icon.substring(0, lastDotIndex) + "_dark" + icon.substring(lastDotIndex);
|
||||
}
|
||||
return icon + "_dark";
|
||||
return icon.replace(".png", "_dark.png");
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
@@ -1210,6 +1551,15 @@ public class MainWindow extends JFrame {
|
||||
private final String description;
|
||||
private final int id;
|
||||
private final Action action;
|
||||
|
||||
/**
|
||||
* 创建一个新的工具
|
||||
* @param title 工具的标题
|
||||
* @param icon 工具的图标
|
||||
* @param description 工具的介绍
|
||||
* @param id 工具的id(不要重复即可)
|
||||
* @param action 点击工具卡片后触发的事件
|
||||
*/
|
||||
public ToolItem(String title, String icon, String description, int id, Action action) {
|
||||
this.title = title; this.icon = icon; this.description = description; this.id = id; this.action = action; this.imageIcon = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,657 @@
|
||||
package com.axis.innovators.box.window;
|
||||
|
||||
import com.axis.innovators.box.window.WindowsJDialog;
|
||||
import org.tzd.explorer.LocalCall;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.event.ChangeEvent;
|
||||
import javax.swing.event.ChangeListener;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.geom.Point2D;
|
||||
import java.awt.RadialGradientPaint;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class TaskbarAppearanceWindow extends WindowsJDialog {
|
||||
private final JComboBox<LocalCall.TaskbarAccentMode> modeCombo;
|
||||
private final JButton colorButton;
|
||||
private final JSlider alphaSlider;
|
||||
private final JButton applyButton;
|
||||
private final JButton helpButton;
|
||||
private final PreviewPanel previewPanel;
|
||||
|
||||
private final JButton sysTextColorButton;
|
||||
private final JButton applySysTextColorButton;
|
||||
private Color sysTextColor = Color.BLACK;
|
||||
|
||||
// 动画平滑过渡参数
|
||||
private Color currentColor = new Color(0, 0, 0);
|
||||
private Color targetColor = new Color(0, 0, 0);
|
||||
private int currentAlpha = 255; // 用于动画(实际 0-255)
|
||||
private int targetAlpha = 255; // 用于动画(实际 0-255)
|
||||
private LocalCall.TaskbarAccentMode currentMode = LocalCall.TaskbarAccentMode.ACCENT_DISABLED;
|
||||
private LocalCall.TaskbarAccentMode targetMode = LocalCall.TaskbarAccentMode.ACCENT_DISABLED;
|
||||
private Timer animationTimer = new Timer(16, e -> {});
|
||||
|
||||
// UI 元素引用(需要动态修改标签)
|
||||
private JPanel alphaLabeledPanel;
|
||||
private JLabel alphaLabel;
|
||||
|
||||
private static final Map<LocalCall.TaskbarAccentMode, String> MODE_LABELS = Map.of(
|
||||
LocalCall.TaskbarAccentMode.ACCENT_DISABLED, "禁用特效(无额外效果)",
|
||||
LocalCall.TaskbarAccentMode.ACCENT_ENABLE_GRADIENT, "渐变(纯色/渐变)",
|
||||
LocalCall.TaskbarAccentMode.ACCENT_ENABLE_TRANSPARENTGRADIENT, "透明渐变(半透明)",
|
||||
LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND, "毛玻璃模糊(高档模糊)",
|
||||
LocalCall.TaskbarAccentMode.ACCENT_ENABLE_ACRYLICBLURBEHIND, "亚克力模糊(Fluent 风格)",
|
||||
LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE, "无效状态(保留)"
|
||||
);
|
||||
|
||||
public static void main(String[] args) {
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
TaskbarAppearanceWindow w = new TaskbarAppearanceWindow(null);
|
||||
w.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
public TaskbarAppearanceWindow(Window owner) {
|
||||
super(owner, "任务栏外观设置", ModalityType.MODELESS);
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
|
||||
// 基本窗口设置
|
||||
setSize(1220, 750);
|
||||
setLocationRelativeTo(owner);
|
||||
((JComponent) getContentPane()).setBorder(new EmptyBorder(12, 12, 12, 12));
|
||||
setLayout(new BorderLayout(12, 12));
|
||||
|
||||
// 顶部标题
|
||||
JPanel top = new JPanel(new BorderLayout());
|
||||
top.setOpaque(false);
|
||||
JLabel header = new JLabel("任务栏外观 - 高级设置");
|
||||
header.setFont(header.getFont().deriveFont(Font.BOLD, 18f));
|
||||
JLabel sub = new JLabel("可实时预览任务栏效果。");
|
||||
sub.setFont(sub.getFont().deriveFont(12f));
|
||||
top.add(header, BorderLayout.NORTH);
|
||||
top.add(sub, BorderLayout.SOUTH);
|
||||
add(top, BorderLayout.NORTH);
|
||||
|
||||
// 左侧:控制区
|
||||
JPanel left = new JPanel();
|
||||
left.setLayout(new BoxLayout(left, BoxLayout.Y_AXIS));
|
||||
left.setOpaque(false);
|
||||
left.setPreferredSize(new Dimension(400, getHeight()));
|
||||
left.setMaximumSize(new Dimension(420, Integer.MAX_VALUE));
|
||||
left.setBorder(new EmptyBorder(6, 6, 6, 6));
|
||||
|
||||
// 模式选择
|
||||
modeCombo = new JComboBox<>(LocalCall.TaskbarAccentMode.values());
|
||||
modeCombo.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
|
||||
modeCombo.setRenderer((list, value, index, isSelected, cellHasFocus) -> {
|
||||
JLabel lab = new JLabel();
|
||||
if (value instanceof LocalCall.TaskbarAccentMode) {
|
||||
LocalCall.TaskbarAccentMode m = (LocalCall.TaskbarAccentMode) value;
|
||||
lab.setText(MODE_LABELS.getOrDefault(m, m.name()));
|
||||
} else {
|
||||
lab.setText(String.valueOf(value));
|
||||
}
|
||||
lab.setOpaque(isSelected);
|
||||
if (isSelected) {
|
||||
lab.setBackground(new Color(0, 120, 215, 40));
|
||||
}
|
||||
lab.setBorder(new EmptyBorder(4, 4, 4, 4));
|
||||
return lab;
|
||||
});
|
||||
left.add(labeledComponent("模式", modeCombo));
|
||||
left.add(Box.createVerticalStrut(10));
|
||||
|
||||
// 颜色选择
|
||||
colorButton = new JButton("选择任务栏颜色");
|
||||
colorButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
|
||||
colorButton.setPreferredSize(new Dimension(200, 32));
|
||||
colorButton.setFocusPainted(false);
|
||||
colorButton.addActionListener(e -> {
|
||||
Color chosen = JColorChooser.showDialog(this, "选择任务栏颜色", targetColor);
|
||||
if (chosen != null) {
|
||||
setTargetColor(chosen);
|
||||
}
|
||||
});
|
||||
left.add(labeledComponent("颜色", colorButton));
|
||||
left.add(Box.createVerticalStrut(10));
|
||||
|
||||
// alpha 滑块(我们要能动态修改其标签和范围)
|
||||
alphaSlider = new JSlider(0, 255, 255);
|
||||
alphaSlider.setMajorTickSpacing(64);
|
||||
alphaSlider.setMinorTickSpacing(16);
|
||||
alphaSlider.setPaintTicks(true);
|
||||
alphaSlider.setPaintLabels(true);
|
||||
alphaSlider.setMaximumSize(new Dimension(Integer.MAX_VALUE, 64));
|
||||
alphaSlider.addChangeListener(new ChangeListener() {
|
||||
@Override
|
||||
public void stateChanged(ChangeEvent e) {
|
||||
// 根据当前模式决定如何解释滑块值:如果是毛玻璃(degree 0-64),映射到 0-255;否则直接为 alpha
|
||||
int v = alphaSlider.getValue();
|
||||
if (targetMode == LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND) {
|
||||
// degree -> alpha (scale 0-64 to 0-255)
|
||||
float ratio = Math.max(0f, Math.min(1f, v / 64f));
|
||||
targetAlpha = Math.round(ratio * 255f);
|
||||
} else {
|
||||
targetAlpha = clamp(v);
|
||||
}
|
||||
targetSliderChanged(v);
|
||||
}
|
||||
});
|
||||
// 用一个可变标签面板包裹
|
||||
alphaLabeledPanel = createAlphaLabeledPanel("不透明度 (Alpha)", alphaSlider);
|
||||
left.add(alphaLabeledPanel);
|
||||
left.add(Box.createVerticalStrut(10));
|
||||
|
||||
// 去掉字节序提示(按要求删除)
|
||||
// left.add(Box.createVerticalStrut(10)); // 不再添加提示
|
||||
|
||||
// 按钮行
|
||||
JPanel btnRow = new JPanel(new FlowLayout(FlowLayout.RIGHT, 8, 0));
|
||||
btnRow.setOpaque(false);
|
||||
applyButton = new JButton("应用到任务栏");
|
||||
helpButton = new JButton("查看帮助");
|
||||
Dimension smallBtn = new Dimension(140, 30);
|
||||
applyButton.setPreferredSize(smallBtn);
|
||||
helpButton.setPreferredSize(smallBtn);
|
||||
applyButton.setMaximumSize(smallBtn);
|
||||
helpButton.setMaximumSize(smallBtn);
|
||||
applyButton.setFocusable(false);
|
||||
helpButton.setFocusable(false);
|
||||
applyButton.setMargin(new Insets(6, 10, 6, 10));
|
||||
helpButton.setMargin(new Insets(6, 10, 6, 10));
|
||||
btnRow.add(helpButton);
|
||||
btnRow.add(applyButton);
|
||||
btnRow.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
left.add(btnRow);
|
||||
|
||||
left.add(Box.createVerticalStrut(14));
|
||||
|
||||
// 系统文字颜色(实验性)
|
||||
sysTextColorButton = new JButton("选择系统文字颜色");
|
||||
sysTextColorButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
|
||||
sysTextColorButton.setPreferredSize(new Dimension(180, 30));
|
||||
sysTextColorButton.setBackground(sysTextColor);
|
||||
sysTextColorButton.setForeground(contrast(sysTextColor));
|
||||
sysTextColorButton.setFocusPainted(false);
|
||||
sysTextColorButton.addActionListener(e -> {
|
||||
Color chosen = JColorChooser.showDialog(this, "选择要设置的系统文字颜色", sysTextColor);
|
||||
if (chosen != null) {
|
||||
sysTextColor = chosen;
|
||||
sysTextColorButton.setBackground(sysTextColor);
|
||||
sysTextColorButton.setForeground(contrast(sysTextColor));
|
||||
}
|
||||
});
|
||||
left.add(labeledComponent("系统文字颜色(实验性)", sysTextColorButton));
|
||||
left.add(Box.createVerticalStrut(8));
|
||||
applySysTextColorButton = new JButton("应用系统文字颜色(修改注册表)");
|
||||
applySysTextColorButton.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
applySysTextColorButton.setPreferredSize(new Dimension(220, 30));
|
||||
applySysTextColorButton.setMaximumSize(new Dimension(Integer.MAX_VALUE, 34));
|
||||
applySysTextColorButton.setFocusPainted(false);
|
||||
left.add(applySysTextColorButton);
|
||||
|
||||
left.add(Box.createVerticalGlue());
|
||||
|
||||
// 右侧:实时预览区
|
||||
previewPanel = new PreviewPanel();
|
||||
previewPanel.setPreferredSize(new Dimension(720, 520));
|
||||
JPanel previewWrap = new JPanel(new BorderLayout());
|
||||
previewWrap.setBorder(BorderFactory.createTitledBorder("实时预览"));
|
||||
previewWrap.add(previewPanel, BorderLayout.CENTER);
|
||||
|
||||
// 主布局
|
||||
JSplitPane split = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, left, previewWrap);
|
||||
split.setResizeWeight(0);
|
||||
split.setDividerLocation(420);
|
||||
split.setOneTouchExpandable(false);
|
||||
add(split, BorderLayout.CENTER);
|
||||
|
||||
// 事件绑定:模式切换时更新控件状态
|
||||
modeCombo.addActionListener(e -> {
|
||||
LocalCall.TaskbarAccentMode sel = (LocalCall.TaskbarAccentMode) modeCombo.getSelectedItem();
|
||||
if (sel != null) {
|
||||
setTargetMode(sel);
|
||||
updateControlsForMode(sel);
|
||||
}
|
||||
});
|
||||
|
||||
// apply 按钮行为(在后台线程执行 LocalCall)
|
||||
applyButton.addActionListener(e -> {
|
||||
setControlsEnabled(false);
|
||||
LocalCall.TaskbarAccentMode mode = targetMode;
|
||||
String colorArg = buildColorArg(targetColor); // 0xRRGGBB
|
||||
int alphaToSend = targetAlpha; // targetAlpha 已经是 0-255(如果毛玻璃,按 degree 映射)
|
||||
boolean restartExplorerAlways = false;
|
||||
if (mode == LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE) {
|
||||
restartExplorerAlways = true;
|
||||
}
|
||||
|
||||
boolean finalRestartExplorerAlways = restartExplorerAlways;
|
||||
SwingWorker<Void, Void> worker = new SwingWorker<>() {
|
||||
Exception ex = null;
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
try {
|
||||
LocalCall.setTaskbarAppearance(mode, colorArg, alphaToSend, finalRestartExplorerAlways);
|
||||
} catch (IOException | InterruptedException e1) {
|
||||
ex = e1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
protected void done() {
|
||||
setControlsEnabled(true);
|
||||
if (ex != null) {
|
||||
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
|
||||
"应用到任务栏失败: " + ex.getMessage(),
|
||||
"错误", JOptionPane.ERROR_MESSAGE);
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
|
||||
"已尝试应用设置。若未生效可能需要 Explorer 重启或注销。", "完成",
|
||||
JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
});
|
||||
|
||||
// 帮助按钮
|
||||
helpButton.addActionListener(e -> {
|
||||
SwingWorker<Void, Void> w = new SwingWorker<>() {
|
||||
Exception ex = null;
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
try {
|
||||
LocalCall.showTaskbarHelp();
|
||||
} catch (IOException | InterruptedException ioException) {
|
||||
ex = ioException;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
protected void done() {
|
||||
if (ex != null) {
|
||||
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
|
||||
"调用帮助失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
};
|
||||
w.execute();
|
||||
});
|
||||
|
||||
// 应用系统文字颜色
|
||||
applySysTextColorButton.addActionListener(e -> {
|
||||
applySysTextColorButton.setEnabled(false);
|
||||
sysTextColorButton.setEnabled(false);
|
||||
SwingWorker<Void, Void> worker = new SwingWorker<>() {
|
||||
Exception ex = null;
|
||||
@Override
|
||||
protected Void doInBackground() {
|
||||
try {
|
||||
setWindowsTextColor(sysTextColor);
|
||||
} catch (IOException | InterruptedException ioException) {
|
||||
ex = ioException;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@Override
|
||||
protected void done() {
|
||||
applySysTextColorButton.setEnabled(true);
|
||||
sysTextColorButton.setEnabled(true);
|
||||
if (ex != null) {
|
||||
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
|
||||
"应用系统文字颜色失败:" + ex.getMessage(),
|
||||
"错误", JOptionPane.ERROR_MESSAGE);
|
||||
} else {
|
||||
JOptionPane.showMessageDialog(TaskbarAppearanceWindow.this,
|
||||
"已尝试修改注册表并刷新用户外观参数(可能需要注销/重启以完全生效)。",
|
||||
"提示", JOptionPane.INFORMATION_MESSAGE);
|
||||
}
|
||||
}
|
||||
};
|
||||
worker.execute();
|
||||
});
|
||||
|
||||
// 双击预览快速体验
|
||||
previewPanel.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
modeCombo.setSelectedItem(LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND);
|
||||
// 设置滑块为中等程度示例
|
||||
alphaSlider.setValue(32);
|
||||
applyButton.doClick();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化目标并启动动画 Timer
|
||||
setTargetColor(currentColor);
|
||||
// 初始化 slider 为 255(默认)
|
||||
alphaSlider.setValue(255);
|
||||
// 默认模式更新 UI
|
||||
updateControlsForMode(currentMode);
|
||||
|
||||
animationTimer = new Timer(16, e -> {
|
||||
boolean changed = false;
|
||||
int rCur = currentColor.getRed(), gCur = currentColor.getGreen(), bCur = currentColor.getBlue();
|
||||
int rT = targetColor.getRed(), gT = targetColor.getGreen(), bT = targetColor.getBlue();
|
||||
int rNew = lerp(rCur, rT, 0.12f);
|
||||
int gNew = lerp(gCur, gT, 0.12f);
|
||||
int bNew = lerp(bCur, bT, 0.12f);
|
||||
Color newColor = new Color(clamp(rNew), clamp(gNew), clamp(bNew));
|
||||
if (!newColor.equals(currentColor)) {
|
||||
currentColor = newColor;
|
||||
changed = true;
|
||||
}
|
||||
int aNew = lerp(currentAlpha, targetAlpha, 0.14f);
|
||||
if (aNew != currentAlpha) {
|
||||
currentAlpha = aNew;
|
||||
changed = true;
|
||||
}
|
||||
if (currentMode != targetMode) {
|
||||
if (Math.abs(currentAlpha - targetAlpha) < 3 &&
|
||||
Math.abs(currentColor.getRed() - targetColor.getRed()) < 3 &&
|
||||
Math.abs(currentColor.getGreen() - targetColor.getGreen()) < 3 &&
|
||||
Math.abs(currentColor.getBlue() - targetColor.getBlue()) < 3) {
|
||||
currentMode = targetMode;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if (changed) {
|
||||
previewPanel.setPreviewState(currentColor, currentAlpha, currentMode);
|
||||
} else {
|
||||
animationTimer.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Enable / disable 控件
|
||||
private void setControlsEnabled(boolean enabled) {
|
||||
// 模式选择保持可用(用户可以改变)
|
||||
modeCombo.setEnabled(true);
|
||||
//applyButton.setEnabled(enabled);
|
||||
helpButton.setEnabled(enabled);
|
||||
colorButton.setEnabled(enabled && targetMode != LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND
|
||||
&& targetMode != LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE);
|
||||
alphaSlider.setEnabled(enabled && targetMode != LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE);
|
||||
applySysTextColorButton.setEnabled(enabled);
|
||||
sysTextColorButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
// 带标签的组件包装(左对齐)
|
||||
private JPanel labeledComponent(String label, JComponent comp) {
|
||||
JPanel p = new JPanel();
|
||||
p.setLayout(new BorderLayout(8, 8));
|
||||
p.setOpaque(false);
|
||||
JLabel l = new JLabel(label);
|
||||
l.setPreferredSize(new Dimension(140, 24));
|
||||
p.add(l, BorderLayout.WEST);
|
||||
p.add(comp, BorderLayout.CENTER);
|
||||
p.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
return p;
|
||||
}
|
||||
|
||||
// 创建 alpha 带可变标签的面板
|
||||
private JPanel createAlphaLabeledPanel(String labelText, JComponent comp) {
|
||||
JPanel p = new JPanel(new BorderLayout(8, 8));
|
||||
p.setOpaque(false);
|
||||
alphaLabel = new JLabel(labelText);
|
||||
alphaLabel.setPreferredSize(new Dimension(140, 24));
|
||||
p.add(alphaLabel, BorderLayout.WEST);
|
||||
p.add(comp, BorderLayout.CENTER);
|
||||
p.setAlignmentX(Component.LEFT_ALIGNMENT);
|
||||
return p;
|
||||
}
|
||||
|
||||
private void setTargetColor(Color c) {
|
||||
if (c == null) return;
|
||||
targetColor = c;
|
||||
colorButton.setBackground(c);
|
||||
colorButton.setForeground(contrast(c));
|
||||
previewPanel.setPreviewState(targetColor, targetAlpha, targetMode);
|
||||
startAnimationIfNeeded();
|
||||
}
|
||||
|
||||
// 当滑块值改变(v 为滑块上的值:可能是 0-255 或 0-64)
|
||||
private void targetSliderChanged(int v) {
|
||||
// 更新预览(targetAlpha 已在 ChangeListener 中计算)
|
||||
previewPanel.setPreviewState(targetColor, targetAlpha, targetMode);
|
||||
startAnimationIfNeeded();
|
||||
}
|
||||
|
||||
private void setTargetMode(LocalCall.TaskbarAccentMode mode) {
|
||||
if (mode == null) return;
|
||||
targetMode = mode;
|
||||
// 如果进入毛玻璃模式,确保 targetAlpha 基于当前 slider(映射)
|
||||
if (targetMode == LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND) {
|
||||
int degree = alphaSlider.getValue();
|
||||
float ratio = Math.max(0f, Math.min(1f, degree / 64f));
|
||||
targetAlpha = Math.round(ratio * 255f);
|
||||
}
|
||||
previewPanel.setPreviewState(targetColor, targetAlpha, targetMode);
|
||||
startAnimationIfNeeded();
|
||||
}
|
||||
|
||||
private void updateControlsForMode(LocalCall.TaskbarAccentMode mode) {
|
||||
// 默认启用全部(模式选择除外)
|
||||
boolean disableAll = (mode == LocalCall.TaskbarAccentMode.ACCENT_INVALID_STATE);
|
||||
boolean isBlur = (mode == LocalCall.TaskbarAccentMode.ACCENT_ENABLE_BLURBEHIND);
|
||||
|
||||
// 如果无效状态:什么都不能设置(除模式之外)
|
||||
colorButton.setEnabled(!disableAll && !isBlur);
|
||||
sysTextColorButton.setEnabled(!disableAll);
|
||||
applySysTextColorButton.setEnabled(!disableAll);
|
||||
helpButton.setEnabled(!disableAll);
|
||||
//applyButton.setEnabled(!disableAll);
|
||||
|
||||
// 配置滑块范围与标签
|
||||
if (isBlur) {
|
||||
// 毛玻璃:滑块行为为 程度 0-64
|
||||
alphaLabel.setText("程度 (0-64)");
|
||||
alphaSlider.setMinimum(0);
|
||||
alphaSlider.setMaximum(64);
|
||||
alphaSlider.setMajorTickSpacing(16);
|
||||
alphaSlider.setMinorTickSpacing(4);
|
||||
alphaSlider.setPaintLabels(true);
|
||||
// 如果之前值超范围则设置为上限
|
||||
if (alphaSlider.getValue() > 64) alphaSlider.setValue(32);
|
||||
// 计算 targetAlpha(映射到 0-255)
|
||||
int degree = alphaSlider.getValue();
|
||||
float ratio = Math.max(0f, Math.min(1f, degree / 64f));
|
||||
targetAlpha = Math.round(ratio * 255f);
|
||||
} else {
|
||||
// 常规模式:不透明度 0-255
|
||||
alphaLabel.setText("不透明度 (Alpha)");
|
||||
alphaSlider.setMinimum(0);
|
||||
alphaSlider.setMaximum(255);
|
||||
alphaSlider.setMajorTickSpacing(64);
|
||||
alphaSlider.setMinorTickSpacing(16);
|
||||
alphaSlider.setPaintLabels(true);
|
||||
// 若当前 slider 值超过 255,调整
|
||||
if (alphaSlider.getValue() > 255) alphaSlider.setValue(255);
|
||||
targetAlpha = clamp(alphaSlider.getValue());
|
||||
}
|
||||
|
||||
// 预览和控件可用性
|
||||
alphaSlider.setEnabled(!disableAll);
|
||||
setControlsEnabled(!disableAll);
|
||||
|
||||
// 立即更新预览状态
|
||||
previewPanel.setPreviewState(targetColor, targetAlpha, mode);
|
||||
startAnimationIfNeeded();
|
||||
}
|
||||
|
||||
private void startAnimationIfNeeded() {
|
||||
if (!animationTimer.isRunning()) animationTimer.start();
|
||||
}
|
||||
|
||||
private static int lerp(int a, int b, float t) {
|
||||
return Math.round(a + (b - a) * t);
|
||||
}
|
||||
|
||||
private static int clamp(int v) {
|
||||
return Math.min(255, Math.max(0, v));
|
||||
}
|
||||
|
||||
private static Color contrast(Color bg) {
|
||||
double luminance = (0.299 * bg.getRed() + 0.587 * bg.getGreen() + 0.114 * bg.getBlue()) / 255.0;
|
||||
return luminance > 0.6 ? Color.BLACK : Color.WHITE;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建传入本地工具的颜色字符串(固定为 0xRRGGBB)
|
||||
* 说明:为避免字节序混淆,Java 端始终发送 RGB(不包含 alpha),例如 #0099CC -> 0x0099CC
|
||||
*/
|
||||
private static String buildColorArg(Color c) {
|
||||
if (c == null) return "0x000000";
|
||||
int r = c.getRed();
|
||||
int g = c.getGreen();
|
||||
int b = c.getBlue();
|
||||
return String.format("0x%02X%02X%02X", r, g, b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过修改注册表设置系统文字颜色(实验性)
|
||||
*/
|
||||
private static void setWindowsTextColor(Color c) throws IOException, InterruptedException {
|
||||
String rgb = String.format("%d %d %d", c.getRed(), c.getGreen(), c.getBlue());
|
||||
String key = "HKCU\\Control Panel\\Colors";
|
||||
String[] values = new String[]{"WindowText", "HilightText", "ButtonText"};
|
||||
for (String v : values) {
|
||||
String cmd = String.format("reg add \"%s\" /v %s /t REG_SZ /d \"%s\" /f", key, v, rgb);
|
||||
Process p = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c", cmd});
|
||||
p.waitFor();
|
||||
}
|
||||
Process refresh = Runtime.getRuntime().exec(new String[]{"cmd.exe", "/c",
|
||||
"RUNDLL32.EXE user32.dll,UpdatePerUserSystemParameters"});
|
||||
refresh.waitFor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 预览面板:绘制模拟任务栏效果(更现代的表现)
|
||||
*/
|
||||
private static class PreviewPanel extends JPanel {
|
||||
private Color previewColor = new Color(0, 0, 0);
|
||||
private int previewAlpha = 255;
|
||||
private LocalCall.TaskbarAccentMode previewMode = LocalCall.TaskbarAccentMode.ACCENT_DISABLED;
|
||||
|
||||
PreviewPanel() {
|
||||
setOpaque(true);
|
||||
setBackground(new Color(0xF4F7FB));
|
||||
}
|
||||
|
||||
void setPreviewState(Color color, int alpha, LocalCall.TaskbarAccentMode mode) {
|
||||
this.previewColor = color != null ? color : new Color(0,0,0);
|
||||
this.previewAlpha = clamp(alpha);
|
||||
this.previewMode = mode != null ? mode : LocalCall.TaskbarAccentMode.ACCENT_DISABLED;
|
||||
repaint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
int w = getWidth();
|
||||
int h = getHeight();
|
||||
Graphics2D g2 = (Graphics2D) g.create();
|
||||
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 背景渐变
|
||||
GradientPaint bg = new GradientPaint(0, 0, new Color(0xEAF3FF), 0, h, new Color(0xE6F0FF));
|
||||
g2.setPaint(bg);
|
||||
g2.fillRect(0, 0, w, h);
|
||||
|
||||
// 任务栏容器
|
||||
int barH = Math.max(76, h / 6);
|
||||
int barY = h - barH - 36;
|
||||
int margin = 36;
|
||||
int barX = margin;
|
||||
int barW = w - margin * 2;
|
||||
RoundRectangle2D.Float barRect = new RoundRectangle2D.Float(barX, barY, barW, barH, 18, 18);
|
||||
|
||||
// 根据模式绘制
|
||||
switch (previewMode) {
|
||||
case ACCENT_DISABLED:
|
||||
Color solid = new Color(previewColor.getRed(), previewColor.getGreen(), previewColor.getBlue(),
|
||||
Math.max(200, previewAlpha));
|
||||
g2.setPaint(solid);
|
||||
g2.fill(barRect);
|
||||
break;
|
||||
case ACCENT_ENABLE_GRADIENT:
|
||||
case ACCENT_ENABLE_TRANSPARENTGRADIENT:
|
||||
Color c1 = new Color(previewColor.getRed(), previewColor.getGreen(), previewColor.getBlue(),
|
||||
Math.max(60, previewAlpha / 2));
|
||||
Color c2 = new Color(clamp(previewColor.getRed() + 30), clamp(previewColor.getGreen() + 30),
|
||||
clamp(previewColor.getBlue() + 30), Math.max(60, previewAlpha / 2));
|
||||
GradientPaint gp = new GradientPaint(barX, barY, c1, barX + barW, barY + barH, c2);
|
||||
g2.setPaint(gp);
|
||||
g2.fill(barRect);
|
||||
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.08f));
|
||||
g2.fill(new RoundRectangle2D.Float(barX, barY, barW, barH / 2f, 16, 16));
|
||||
g2.setComposite(AlphaComposite.SrcOver);
|
||||
break;
|
||||
case ACCENT_ENABLE_BLURBEHIND:
|
||||
Color base = new Color(previewColor.getRed(), previewColor.getGreen(), previewColor.getBlue(),
|
||||
Math.max(8, previewAlpha / 3));
|
||||
g2.setPaint(base);
|
||||
g2.fill(barRect);
|
||||
Point2D center = new Point2D.Float(barX + barW / 2f, barY + barH / 2f);
|
||||
float radius = Math.max(barW, barH);
|
||||
RadialGradientPaint rgp = new RadialGradientPaint(center, radius,
|
||||
new float[]{0f, 1f},
|
||||
new Color[]{new Color(255, 255, 255, Math.min(100, previewAlpha / 2)),
|
||||
new Color(0, 0, 0, Math.min(40, previewAlpha / 6))});
|
||||
g2.setPaint(rgp);
|
||||
g2.fill(barRect);
|
||||
g2.setStroke(new BasicStroke(1.2f));
|
||||
for (int i = 0; i < 5; i++) {
|
||||
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.04f + i * 0.02f));
|
||||
g2.draw(new RoundRectangle2D.Float(barX + i, barY + i, barW - i * 2f, barH - i * 2f, 16, 16));
|
||||
}
|
||||
g2.setComposite(AlphaComposite.SrcOver);
|
||||
break;
|
||||
case ACCENT_ENABLE_ACRYLICBLURBEHIND:
|
||||
case ACCENT_INVALID_STATE:
|
||||
default:
|
||||
Color acr = new Color(previewColor.getRed(), previewColor.getGreen(), previewColor.getBlue(),
|
||||
Math.max(20, previewAlpha / 2));
|
||||
g2.setPaint(acr);
|
||||
g2.fill(barRect);
|
||||
GradientPaint spot = new GradientPaint(barX, barY, new Color(255, 255, 255, 140),
|
||||
barX + barW / 2, barY + barH / 2, new Color(255, 255, 255, 0));
|
||||
g2.setPaint(spot);
|
||||
g2.fill(new RoundRectangle2D.Float(barX, barY, barW / 2f, barH, 16, 16));
|
||||
for (int i = 0; i < barW; i += 10) {
|
||||
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.03f));
|
||||
g2.drawLine(barX + i, barY + 6, barX + i + 6, barY + barH - 6);
|
||||
}
|
||||
g2.setComposite(AlphaComposite.SrcOver);
|
||||
break;
|
||||
}
|
||||
|
||||
// 绘制图标占位
|
||||
int iconY = barY + 14;
|
||||
int iconSize = Math.min(36, barH - 28);
|
||||
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.95f));
|
||||
g2.setColor(new Color(255, 255, 255, 200));
|
||||
g2.fillOval(barX + 16, iconY, iconSize, iconSize);
|
||||
|
||||
int trayX = barX + barW - 18 - (iconSize + 10) * 4;
|
||||
for (int i = 0; i < 4; i++) {
|
||||
int x = trayX + i * (iconSize + 10);
|
||||
g2.fillRoundRect(x, iconY, iconSize, iconSize, 8, 8);
|
||||
}
|
||||
|
||||
g2.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
8
src/main/java/com/chuangzhou/vivid2D/Main.java
Normal file
8
src/main/java/com/chuangzhou/vivid2D/Main.java
Normal file
@@ -0,0 +1,8 @@
|
||||
package com.chuangzhou.vivid2D;
|
||||
|
||||
public class Main {
|
||||
|
||||
public static void main(String[] args) {
|
||||
|
||||
}
|
||||
}
|
||||
221
src/main/java/com/chuangzhou/vivid2D/ai/ModelManagement.java
Normal file
221
src/main/java/com/chuangzhou/vivid2D/ai/ModelManagement.java
Normal file
@@ -0,0 +1,221 @@
|
||||
package com.chuangzhou.vivid2D.ai;
|
||||
|
||||
import com.chuangzhou.vivid2D.ai.anime_face_segmentation.AnimeModelWrapper;
|
||||
import com.chuangzhou.vivid2D.ai.anime_segmentation.Anime2VividModelWrapper;
|
||||
import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetVividModelWrapper;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 模型管理器 - 负责模型的注册、分类和检索
|
||||
*/
|
||||
public class ModelManagement {
|
||||
private final Map<String, Class<?>> models = new ConcurrentHashMap<>();
|
||||
private final Map<String, List<String>> modelsByCategory = new ConcurrentHashMap<>();
|
||||
private final List<String> modelDisplayNames = new ArrayList<>();
|
||||
private final Map<String, String> displayNameToRegistrationName = new ConcurrentHashMap<>();
|
||||
|
||||
private ModelManagement() {
|
||||
initializeDefaultCategories();
|
||||
registerDefaultModels();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认分类
|
||||
*/
|
||||
private void initializeDefaultCategories() {
|
||||
modelsByCategory.put("Image Segmentation", new ArrayList<>());
|
||||
modelsByCategory.put("Image Processing", new ArrayList<>());
|
||||
modelsByCategory.put("Image Generation", new ArrayList<>());
|
||||
modelsByCategory.put("Image Inpainting", new ArrayList<>());
|
||||
modelsByCategory.put("Image Completion", new ArrayList<>());
|
||||
modelsByCategory.put("Face Analysis", new ArrayList<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册默认模型
|
||||
*/
|
||||
private void registerDefaultModels() {
|
||||
registerModel("segmentation:anime_face", "Anime Face Segmentation",
|
||||
AnimeModelWrapper.class, "Image Segmentation");
|
||||
registerModel("segmentation:anime", "Anime Image Segmentation",
|
||||
Anime2VividModelWrapper.class, "Image Segmentation");
|
||||
registerModel("segmentation:face_parsing", "Face Parsing",
|
||||
BiSeNetVividModelWrapper.class, "Image Segmentation");
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册模型
|
||||
* @param modelRegistrationName 注册名称,格式必须为 "category:model_name"
|
||||
* @param modelDisplayName 模型显示名称
|
||||
* @param modelClass 模型类
|
||||
* @param category 模型类别
|
||||
*/
|
||||
public void registerModel(String modelRegistrationName, String modelDisplayName,
|
||||
Class<?> modelClass, String category) {
|
||||
if (!isValidRegistrationName(modelRegistrationName)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid registration name format. Expected 'category:model_name', got: " + modelRegistrationName);
|
||||
}
|
||||
if (models.containsKey(modelRegistrationName)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Model registration name already exists: " + modelRegistrationName);
|
||||
}
|
||||
if (displayNameToRegistrationName.containsKey(modelDisplayName)) {
|
||||
throw new IllegalArgumentException(
|
||||
"Model display name already exists: " + modelDisplayName);
|
||||
}
|
||||
if (!modelsByCategory.containsKey(category)) {
|
||||
modelsByCategory.put(category, new ArrayList<>());
|
||||
}
|
||||
models.put(modelRegistrationName, modelClass);
|
||||
displayNameToRegistrationName.put(modelDisplayName, modelRegistrationName);
|
||||
modelDisplayNames.add(modelDisplayName);
|
||||
modelsByCategory.get(category).add(modelRegistrationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证注册名称格式
|
||||
*/
|
||||
private boolean isValidRegistrationName(String name) {
|
||||
return name != null && name.matches("^[a-zA-Z0-9_]+:[a-zA-Z0-9_]+$");
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过显示名称获取模型类
|
||||
*/
|
||||
public Class<?> getModel(String modelDisplayName) {
|
||||
String registrationName = displayNameToRegistrationName.get(modelDisplayName);
|
||||
return registrationName != null ? models.get(registrationName) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过索引获取模型类
|
||||
*/
|
||||
public Class<?> getModel(int modelIndex) {
|
||||
if (modelIndex >= 0 && modelIndex < modelDisplayNames.size()) {
|
||||
String displayName = modelDisplayNames.get(modelIndex);
|
||||
return getModel(displayName);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过注册名称获取模型类
|
||||
*/
|
||||
public Class<?> getModelByRegistrationName(String registrationName) {
|
||||
return models.get(registrationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过类名获取模型类
|
||||
*/
|
||||
public Class<?> getModelByClassName(String className) {
|
||||
for (Class<?> modelClass : models.values()) {
|
||||
if (modelClass.getName().equals(className)) {
|
||||
return modelClass;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有模型的显示名称
|
||||
*/
|
||||
public List<String> getAllModelDisplayNames() {
|
||||
return Collections.unmodifiableList(modelDisplayNames);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有模型的注册名称
|
||||
*/
|
||||
public Set<String> getAllModelRegistrationNames() {
|
||||
return Collections.unmodifiableSet(models.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按类别获取模型注册名称
|
||||
*/
|
||||
public List<String> getModelsByCategory(String category) {
|
||||
return Collections.unmodifiableList(
|
||||
modelsByCategory.getOrDefault(category, new ArrayList<>())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有可用的类别
|
||||
*/
|
||||
public Set<String> getAllCategories() {
|
||||
return Collections.unmodifiableSet(modelsByCategory.keySet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型数量
|
||||
*/
|
||||
public int getModelCount() {
|
||||
return modelDisplayNames.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型显示名称对应的注册名称
|
||||
*/
|
||||
public String getRegistrationName(String modelDisplayName) {
|
||||
return displayNameToRegistrationName.get(modelDisplayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型注册名称对应的显示名称
|
||||
*/
|
||||
public String getDisplayName(String registrationName) {
|
||||
for (Map.Entry<String, String> entry : displayNameToRegistrationName.entrySet()) {
|
||||
if (entry.getValue().equals(registrationName)) {
|
||||
return entry.getKey();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模型是否存在
|
||||
*/
|
||||
public boolean containsModel(String modelDisplayName) {
|
||||
return displayNameToRegistrationName.containsKey(modelDisplayName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查注册名称是否存在
|
||||
*/
|
||||
public boolean containsRegistrationName(String registrationName) {
|
||||
return models.containsKey(registrationName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除模型
|
||||
*/
|
||||
public boolean removeModel(String modelDisplayName) {
|
||||
String registrationName = displayNameToRegistrationName.get(modelDisplayName);
|
||||
if (registrationName != null) {
|
||||
// 从所有存储中移除
|
||||
models.remove(registrationName);
|
||||
displayNameToRegistrationName.remove(modelDisplayName);
|
||||
modelDisplayNames.remove(modelDisplayName);
|
||||
|
||||
// 从类别中移除
|
||||
for (List<String> categoryModels : modelsByCategory.values()) {
|
||||
categoryModels.remove(registrationName);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final class InstanceHolder {
|
||||
private static final ModelManagement instance = new ModelManagement();
|
||||
}
|
||||
|
||||
public static ModelManagement getInstance() {
|
||||
return InstanceHolder.instance;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.chuangzhou.vivid2D.ai;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Map;
|
||||
|
||||
public class SegmentationResult {
|
||||
// 分割掩码图(每个像素的颜色为对应类别颜色)
|
||||
private final BufferedImage maskImage;
|
||||
|
||||
// 类别索引 -> 类别名称
|
||||
private final Map<Integer, String> labels;
|
||||
|
||||
// 类别名称 -> ARGB 颜色
|
||||
private final Map<String, Integer> palette;
|
||||
|
||||
public SegmentationResult(BufferedImage maskImage, Map<Integer, String> labels, Map<String, Integer> palette) {
|
||||
this.maskImage = maskImage;
|
||||
this.labels = labels;
|
||||
this.palette = palette;
|
||||
}
|
||||
|
||||
public BufferedImage getMaskImage() {
|
||||
return maskImage;
|
||||
}
|
||||
|
||||
public Map<Integer, String> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getPalette() {
|
||||
return palette;
|
||||
}
|
||||
}
|
||||
154
src/main/java/com/chuangzhou/vivid2D/ai/Segmenter.java
Normal file
154
src/main/java/com/chuangzhou/vivid2D/ai/Segmenter.java
Normal file
@@ -0,0 +1,154 @@
|
||||
package com.chuangzhou.vivid2D.ai;
|
||||
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.ndarray.types.DataType;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.translate.Batchifier;
|
||||
import ai.djl.translate.TranslateException;
|
||||
import ai.djl.translate.Translator;
|
||||
import ai.djl.translate.TranslatorContext;
|
||||
import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetLabelPalette;
|
||||
import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetSegmentationResult;
|
||||
import com.chuangzhou.vivid2D.ai.face_parsing.BiSeNetSegmenter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
public abstract class Segmenter implements AutoCloseable {
|
||||
// 内部类,用于从Translator安全地传出数据
|
||||
public static class SegmentationData {
|
||||
public final int[] indices;
|
||||
public final long[] shape;
|
||||
|
||||
public SegmentationData(int[] indices, long[] shape) {
|
||||
this.indices = indices;
|
||||
this.shape = shape;
|
||||
}
|
||||
}
|
||||
|
||||
private String engine = "PyTorch";
|
||||
protected final ZooModel<Image, Segmenter.SegmentationData> modelWrapper;
|
||||
protected final Predictor<Image, Segmenter.SegmentationData> predictor;
|
||||
protected final List<String> labels;
|
||||
protected final Map<String, Integer> palette;
|
||||
|
||||
public Segmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
|
||||
this.labels = new ArrayList<>(labels);
|
||||
this.palette = BiSeNetLabelPalette.defaultPalette();
|
||||
|
||||
Translator<Image, Segmenter.SegmentationData> translator = new Translator<Image, Segmenter.SegmentationData>() {
|
||||
@Override
|
||||
public NDList processInput(TranslatorContext ctx, Image input) {
|
||||
return Segmenter.this.processInput(ctx, input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) {
|
||||
return Segmenter.this.processOutput(ctx, list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Batchifier getBatchifier() {
|
||||
return Segmenter.this.getBatchifier();
|
||||
}
|
||||
};
|
||||
|
||||
Criteria<Image, Segmenter.SegmentationData> criteria = Criteria.builder()
|
||||
.setTypes(Image.class, Segmenter.SegmentationData.class)
|
||||
.optModelPath(modelDir)
|
||||
.optEngine(engine)
|
||||
.optTranslator(translator)
|
||||
.build();
|
||||
|
||||
this.modelWrapper = criteria.loadModel();
|
||||
this.predictor = modelWrapper.newPredictor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理模型输入
|
||||
* @param ctx translator 上下文
|
||||
* @param input 图片
|
||||
* @return 模型输入
|
||||
*/
|
||||
public abstract NDList processInput(TranslatorContext ctx, Image input);
|
||||
|
||||
/**
|
||||
* 处理模型输出
|
||||
* @param ctx translator 上下文
|
||||
* @param list 模型输出
|
||||
* @return 模型输出
|
||||
*/
|
||||
public abstract Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list);
|
||||
|
||||
/**
|
||||
* 获取批量处理方式
|
||||
* @return 批量处理方式
|
||||
*/
|
||||
public Batchifier getBatchifier(){
|
||||
return null;
|
||||
}
|
||||
|
||||
public SegmentationResult segment(File imgFile) throws TranslateException, IOException {
|
||||
Image img = ImageFactory.getInstance().fromFile(imgFile.toPath());
|
||||
|
||||
// predict 方法现在直接返回安全的 Java 对象
|
||||
Segmenter.SegmentationData data = predictor.predict(img);
|
||||
|
||||
long[] shp = data.shape;
|
||||
int[] indices = data.indices;
|
||||
|
||||
int height, width;
|
||||
if (shp.length == 2) {
|
||||
height = (int) shp[0];
|
||||
width = (int) shp[1];
|
||||
} else {
|
||||
throw new RuntimeException("Unexpected classMap shape from SegmentationData: " + Arrays.toString(shp));
|
||||
}
|
||||
|
||||
// 后续处理完全基于 Java 对象,不再有 Native resource 问题
|
||||
BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
Map<Integer, String> labelsMap = new HashMap<>();
|
||||
for (int i = 0; i < labels.size(); i++) {
|
||||
labelsMap.put(i, labels.get(i));
|
||||
}
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int idx = indices[y * width + x];
|
||||
String label = labelsMap.getOrDefault(idx, "unknown");
|
||||
int argb = palette.getOrDefault(label, 0xFF00FF00);
|
||||
mask.setRGB(x, y, argb);
|
||||
}
|
||||
}
|
||||
|
||||
return new SegmentationResult(mask, labelsMap, palette);
|
||||
}
|
||||
|
||||
public void setEngine(String engine) {
|
||||
this.engine = engine;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
predictor.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
try {
|
||||
modelWrapper.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
113
src/main/java/com/chuangzhou/vivid2D/ai/VividModelWrapper.java
Normal file
113
src/main/java/com/chuangzhou/vivid2D/ai/VividModelWrapper.java
Normal file
@@ -0,0 +1,113 @@
|
||||
package com.chuangzhou.vivid2D.ai;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class VividModelWrapper<s extends Segmenter> implements AutoCloseable{
|
||||
protected final s segmenter;
|
||||
protected final List<String> labels; // index -> name
|
||||
protected final Map<String, Integer> palette; // name -> ARGB
|
||||
|
||||
protected VividModelWrapper(s segmenter, List<String> labels, Map<String, Integer> palette) {
|
||||
this.segmenter = segmenter;
|
||||
this.labels = labels;
|
||||
this.palette = palette;
|
||||
}
|
||||
|
||||
public List<String> getLabels() {
|
||||
return Collections.unmodifiableList(labels);
|
||||
}
|
||||
|
||||
public Map<String, Integer> getPalette() {
|
||||
return Collections.unmodifiableMap(palette);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接返回分割结果(SegmentationResult)
|
||||
*/
|
||||
public SegmentationResult segment(File inputImage) throws Exception {
|
||||
return segmenter.segment(inputImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。
|
||||
* 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
|
||||
* <p>
|
||||
* 返回值:Map<labelName, ResultFiles>,ResultFiles 包含 maskFile、overlayFile(两个 PNG)
|
||||
*/
|
||||
public abstract Map<String, ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception;
|
||||
|
||||
protected static String safeFileName(String s) {
|
||||
return s.replaceAll("[^a-zA-Z0-9_\\-\\.]", "_");
|
||||
}
|
||||
|
||||
protected static Set<String> parseTargetsSet(Set<String> in) {
|
||||
if (in == null || in.isEmpty()) return Collections.emptySet();
|
||||
// 若包含单个 "all"
|
||||
if (in.size() == 1) {
|
||||
String only = in.iterator().next();
|
||||
if ("all".equalsIgnoreCase(only.trim())) {
|
||||
return Set.of("all");
|
||||
}
|
||||
}
|
||||
// 直接返回 trim 后的小写不变集合(保持用户传入的名字)
|
||||
Set<String> out = new LinkedHashSet<>();
|
||||
for (String s : in) {
|
||||
if (s != null) out.add(s.trim());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭底层资源
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
segmenter.close();
|
||||
} catch (Exception ignore) {}
|
||||
}
|
||||
|
||||
/* ================= helper: 从 modelDir 读取 synset.txt ================= */
|
||||
|
||||
protected static Optional<List<String>> loadLabelsFromSynset(Path modelDir) {
|
||||
Path syn = modelDir.resolve("synset.txt");
|
||||
if (Files.exists(syn)) {
|
||||
try {
|
||||
List<String> lines = Files.readAllLines(syn);
|
||||
List<String> cleaned = new ArrayList<>();
|
||||
for (String l : lines) {
|
||||
String s = l.trim();
|
||||
if (!s.isEmpty()) cleaned.add(s);
|
||||
}
|
||||
if (!cleaned.isEmpty()) return Optional.of(cleaned);
|
||||
} catch (IOException ignore) {}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 存放结果文件路径
|
||||
*/
|
||||
public static class ResultFiles {
|
||||
private final File maskFile;
|
||||
private final File overlayFile;
|
||||
|
||||
public ResultFiles(File maskFile, File overlayFile) {
|
||||
this.maskFile = maskFile;
|
||||
this.overlayFile = overlayFile;
|
||||
}
|
||||
|
||||
public File getMaskFile() {
|
||||
return maskFile;
|
||||
}
|
||||
|
||||
public File getOverlayFile() {
|
||||
return overlayFile;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Anime-Face-Segmentation UNet 模型的标签和颜色调色板。
|
||||
* 基于 Anime-Face-Segmentation 项目的 util.py 中的颜色定义。
|
||||
* 标签索引必须与模型输出索引一致(0-6)。
|
||||
*/
|
||||
public class AnimeLabelPalette {
|
||||
|
||||
/**
|
||||
* Anime-Face-Segmentation UNet 模型的标准标签(7个类别,索引 0-6)
|
||||
*/
|
||||
public static List<String> defaultLabels() {
|
||||
return Arrays.asList(
|
||||
"background", // 0 - 青色 (0,255,255)
|
||||
"hair", // 1 - 蓝色 (255,0,0)
|
||||
"eye", // 2 - 红色 (0,0,255)
|
||||
"mouth", // 3 - 白色 (255,255,255)
|
||||
"face", // 4 - 绿色 (0,255,0)
|
||||
"skin", // 5 - 黄色 (255,255,0)
|
||||
"clothes" // 6 - 紫色 (255,0,255)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回对应的调色板:类别名 -> ARGB 颜色值。
|
||||
* 颜色值基于 util.py 中的 PALETTE 数组的 RGB 值转换为 ARGB 格式 (0xFFRRGGBB)。
|
||||
*/
|
||||
public static Map<String, Integer> defaultPalette() {
|
||||
Map<String, Integer> map = new HashMap<>();
|
||||
// 索引 0: background -> (0,255,255) 青色
|
||||
map.put("background", 0xFF00FFFF);
|
||||
// 索引 1: hair -> (255,0,0) 蓝色
|
||||
map.put("hair", 0xFFFF0000);
|
||||
// 索引 2: eye -> (0,0,255) 红色
|
||||
map.put("eye", 0xFF0000FF);
|
||||
// 索引 3: mouth -> (255,255,255) 白色
|
||||
map.put("mouth", 0xFFFFFFFF);
|
||||
// 索引 4: face -> (0,255,0) 绿色
|
||||
map.put("face", 0xFF00FF00);
|
||||
// 索引 5: skin -> (255,255,0) 黄色
|
||||
map.put("skin", 0xFFFFFF00);
|
||||
// 索引 6: clothes -> (255,0,255) 紫色
|
||||
map.put("clothes", 0xFFFF00FF);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取类别索引到名称的映射
|
||||
*/
|
||||
public static Map<Integer, String> getIndexToLabelMap() {
|
||||
List<String> labels = defaultLabels();
|
||||
Map<Integer, String> map = new HashMap<>();
|
||||
for (int i = 0; i < labels.size(); i++) {
|
||||
map.put(i, labels.get(i));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
|
||||
|
||||
import com.chuangzhou.vivid2D.ai.VividModelWrapper;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AnimeModelWrapper - 专门为 Anime-Face-Segmentation 模型封装的 Wrapper
|
||||
*/
|
||||
public class AnimeModelWrapper extends VividModelWrapper<AnimeSegmenter> {
|
||||
|
||||
private AnimeModelWrapper(AnimeSegmenter segmenter, List<String> labels, Map<String, Integer> palette) {
|
||||
super(segmenter, labels, palette);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模型
|
||||
*/
|
||||
public static AnimeModelWrapper load(Path modelDir) throws Exception {
|
||||
List<String> labels = loadLabelsFromSynset(modelDir).orElseGet(AnimeLabelPalette::defaultLabels);
|
||||
AnimeSegmenter segmenter = new AnimeSegmenter(modelDir, labels);
|
||||
Map<String, Integer> palette = AnimeLabelPalette.defaultPalette();
|
||||
return new AnimeModelWrapper(segmenter, labels, palette);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接返回分割结果(在丢给底层 segmenter 前会做通用预处理:RGB 转换 + 等比 letterbox 缩放到模型输入尺寸)
|
||||
*/
|
||||
public AnimeSegmentationResult segment(File inputImage) throws Exception {
|
||||
File pre = null;
|
||||
try {
|
||||
pre = preprocessAndSave(inputImage);
|
||||
// 将预处理后的临时文件丢给底层 segmenter
|
||||
return segmenter.segment(pre);
|
||||
} finally {
|
||||
if (pre != null && pre.exists()) {
|
||||
try { Files.deleteIfExists(pre.toPath()); } catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分割并保存结果
|
||||
*/
|
||||
public Map<String, ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception {
|
||||
if (!Files.exists(outDir)) {
|
||||
Files.createDirectories(outDir);
|
||||
}
|
||||
|
||||
AnimeSegmentationResult res = segment(inputImage);
|
||||
BufferedImage original = ImageIO.read(inputImage);
|
||||
BufferedImage maskImage = res.getMaskImage();
|
||||
|
||||
int maskW = maskImage.getWidth();
|
||||
int maskH = maskImage.getHeight();
|
||||
|
||||
// 解析 targets
|
||||
Set<String> realTargets = parseTargetsSet(targets);
|
||||
Map<String, ResultFiles> saved = new LinkedHashMap<>();
|
||||
|
||||
for (String target : realTargets) {
|
||||
if (!palette.containsKey(target)) {
|
||||
// 尝试忽略大小写匹配
|
||||
String finalTarget = target;
|
||||
Optional<String> matched = palette.keySet().stream()
|
||||
.filter(k -> k.equalsIgnoreCase(finalTarget))
|
||||
.findFirst();
|
||||
if (matched.isPresent()) target = matched.get();
|
||||
else {
|
||||
System.err.println("Warning: unknown label '" + target + "' - skip.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
int targetColor = palette.get(target);
|
||||
|
||||
// 1) 生成透明背景的二值掩码(只保留 target 像素)
|
||||
BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < maskH; y++) {
|
||||
for (int x = 0; x < maskW; x++) {
|
||||
int c = maskImage.getRGB(x, y);
|
||||
if (c == targetColor) {
|
||||
partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
|
||||
} else {
|
||||
partMask.setRGB(x, y, 0x00000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 将 mask 缩放到与原图一致(如果需要),并生成 overlay(半透明)
|
||||
BufferedImage maskResized = partMask;
|
||||
if (original.getWidth() != maskW || original.getHeight() != maskH) {
|
||||
maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = maskResized.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = overlay.createGraphics();
|
||||
g2.drawImage(original, 0, 0, null);
|
||||
// 半透明颜色(alpha = 0x88)
|
||||
int rgbOnly = (targetColor & 0x00FFFFFF);
|
||||
int translucent = (0x88 << 24) | rgbOnly;
|
||||
BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < colorOverlay.getHeight(); y++) {
|
||||
for (int x = 0; x < colorOverlay.getWidth(); x++) {
|
||||
int mc = maskResized.getRGB(x, y);
|
||||
if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
|
||||
colorOverlay.setRGB(x, y, translucent);
|
||||
} else {
|
||||
colorOverlay.setRGB(x, y, 0x00000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
g2.drawImage(colorOverlay, 0, 0, null);
|
||||
g2.dispose();
|
||||
|
||||
// 保存
|
||||
String safe = safeFileName(target);
|
||||
File maskOut = outDir.resolve(safe + "_mask.png").toFile();
|
||||
File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
|
||||
|
||||
ImageIO.write(maskResized, "png", maskOut);
|
||||
ImageIO.write(overlay, "png", overlayOut);
|
||||
|
||||
saved.put(target, new ResultFiles(maskOut, overlayOut));
|
||||
}
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专门提取眼睛的方法(在丢给底层 segmenter 前做预处理)
|
||||
*/
|
||||
public ResultFiles extractEyes(File inputImage, Path outDir) throws Exception {
|
||||
if (!Files.exists(outDir)) {
|
||||
Files.createDirectories(outDir);
|
||||
}
|
||||
|
||||
File pre = null;
|
||||
BufferedImage eyes;
|
||||
try {
|
||||
pre = preprocessAndSave(inputImage);
|
||||
eyes = segmenter.extractEyes(pre);
|
||||
} finally {
|
||||
if (pre != null && pre.exists()) {
|
||||
try { Files.deleteIfExists(pre.toPath()); } catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
|
||||
File eyesMask = outDir.resolve("eyes_mask.png").toFile();
|
||||
ImageIO.write(eyes, "png", eyesMask);
|
||||
|
||||
// 创建眼睛的 overlay(原有逻辑,保持不变)
|
||||
BufferedImage original = ImageIO.read(inputImage);
|
||||
BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = overlay.createGraphics();
|
||||
g2.drawImage(original, 0, 0, null);
|
||||
|
||||
// 缩放眼睛掩码到原图尺寸
|
||||
BufferedImage eyesResized = eyes;
|
||||
if (original.getWidth() != eyes.getWidth() || original.getHeight() != eyes.getHeight()) {
|
||||
eyesResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = eyesResized.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(eyes, 0, 0, original.getWidth(), original.getHeight(), null);
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
int eyeColor = palette.getOrDefault("eye", 0xFF00FF); // 若没有 eye,给个显眼默认色
|
||||
int rgbOnly = (eyeColor & 0x00FFFFFF);
|
||||
int translucent = (0x88 << 24) | rgbOnly;
|
||||
|
||||
BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < colorOverlay.getHeight(); y++) {
|
||||
for (int x = 0; x < colorOverlay.getWidth(); x++) {
|
||||
int mc = eyesResized.getRGB(x, y);
|
||||
if ((mc & 0x00FFFFFF) == (eyeColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
|
||||
colorOverlay.setRGB(x, y, translucent);
|
||||
} else {
|
||||
colorOverlay.setRGB(x, y, 0x00000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
g2.drawImage(colorOverlay, 0, 0, null);
|
||||
g2.dispose();
|
||||
|
||||
File eyesOverlay = outDir.resolve("eyes_overlay.png").toFile();
|
||||
ImageIO.write(overlay, "png", eyesOverlay);
|
||||
|
||||
return new ResultFiles(eyesMask, eyesOverlay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭底层资源
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
segmenter.close();
|
||||
} catch (Exception ignore) {}
|
||||
}
|
||||
|
||||
// ========== 新增:预处理并保存到临时文件 ==========
|
||||
private File preprocessAndSave(File inputImage) throws IOException {
|
||||
BufferedImage img = ImageIO.read(inputImage);
|
||||
if (img == null) throw new IOException("无法读取图片: " + inputImage);
|
||||
|
||||
// 转成标准 RGB(去掉 alpha / 保证三通道)
|
||||
BufferedImage rgb = new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g = rgb.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(img, 0, 0, null);
|
||||
g.dispose();
|
||||
|
||||
// 获取模型输入尺寸(尝试反射读取,找不到则使用默认 512x512)
|
||||
int[] size = getModelInputSize();
|
||||
int targetW = size[0], targetH = size[1];
|
||||
|
||||
// 等比缩放并居中填充(letterbox),背景用白色
|
||||
double scale = Math.min((double) targetW / rgb.getWidth(), (double) targetH / rgb.getHeight());
|
||||
int newW = Math.max(1, (int) Math.round(rgb.getWidth() * scale));
|
||||
int newH = Math.max(1, (int) Math.round(rgb.getHeight() * scale));
|
||||
|
||||
BufferedImage resized = new BufferedImage(targetW, targetH, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2 = resized.createGraphics();
|
||||
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g2.setColor(Color.WHITE);
|
||||
g2.fillRect(0, 0, targetW, targetH);
|
||||
int x = (targetW - newW) / 2;
|
||||
int y = (targetH - newH) / 2;
|
||||
g2.drawImage(rgb, x, y, newW, newH, null);
|
||||
g2.dispose();
|
||||
|
||||
// 保存为临时 PNG 文件(确保无压缩失真)
|
||||
File tmp = Files.createTempFile("anime_pre_", ".png").toFile();
|
||||
ImageIO.write(resized, "png", tmp);
|
||||
return tmp;
|
||||
}
|
||||
|
||||
// ========== 新增:尝试通过反射从 segmenter 上读取模型输入尺寸 ==========
|
||||
private int[] getModelInputSize() {
|
||||
// 默认值
|
||||
int defaultSize = 512;
|
||||
int w = defaultSize, h = defaultSize;
|
||||
|
||||
try {
|
||||
Class<?> cls = segmenter.getClass();
|
||||
|
||||
// 尝试方法 getInputWidth/getInputHeight
|
||||
try {
|
||||
Method mw = cls.getMethod("getInputWidth");
|
||||
Method mh = cls.getMethod("getInputHeight");
|
||||
Object ow = mw.invoke(segmenter);
|
||||
Object oh = mh.invoke(segmenter);
|
||||
if (ow instanceof Number && oh instanceof Number) {
|
||||
int iw = ((Number) ow).intValue();
|
||||
int ih = ((Number) oh).intValue();
|
||||
if (iw > 0 && ih > 0) {
|
||||
return new int[]{iw, ih};
|
||||
}
|
||||
}
|
||||
} catch (NoSuchMethodException ignored) {}
|
||||
|
||||
// 尝试方法 getInputSize 返回 int[] 或 Dimension
|
||||
try {
|
||||
Method ms = cls.getMethod("getInputSize");
|
||||
Object os = ms.invoke(segmenter);
|
||||
if (os instanceof int[] && ((int[]) os).length >= 2) {
|
||||
int iw = ((int[]) os)[0];
|
||||
int ih = ((int[]) os)[1];
|
||||
if (iw > 0 && ih > 0) return new int[]{iw, ih};
|
||||
} else if (os != null) {
|
||||
// 处理 java.awt.Dimension
|
||||
try {
|
||||
Method gw = os.getClass().getMethod("getWidth");
|
||||
Method gh = os.getClass().getMethod("getHeight");
|
||||
Object ow2 = gw.invoke(os);
|
||||
Object oh2 = gh.invoke(os);
|
||||
if (ow2 instanceof Number && oh2 instanceof Number) {
|
||||
int iw = ((Number) ow2).intValue();
|
||||
int ih = ((Number) oh2).intValue();
|
||||
if (iw > 0 && ih > 0) return new int[]{iw, ih};
|
||||
}
|
||||
} catch (Exception ignored2) {}
|
||||
}
|
||||
} catch (NoSuchMethodException ignored) {}
|
||||
|
||||
// 尝试字段 inputWidth/inputHeight
|
||||
try {
|
||||
try {
|
||||
java.lang.reflect.Field fw = cls.getDeclaredField("inputWidth");
|
||||
java.lang.reflect.Field fh = cls.getDeclaredField("inputHeight");
|
||||
fw.setAccessible(true); fh.setAccessible(true);
|
||||
Object ow = fw.get(segmenter);
|
||||
Object oh = fh.get(segmenter);
|
||||
if (ow instanceof Number && oh instanceof Number) {
|
||||
int iw = ((Number) ow).intValue();
|
||||
int ih = ((Number) oh).intValue();
|
||||
if (iw > 0 && ih > 0) return new int[]{iw, ih};
|
||||
}
|
||||
} catch (NoSuchFieldException ignoredField) {}
|
||||
} catch (Exception ignored) {}
|
||||
|
||||
} catch (Exception ignored) {
|
||||
// 任何反射异常都回退到默认值
|
||||
}
|
||||
|
||||
return new int[]{w, h};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
|
||||
|
||||
import com.chuangzhou.vivid2D.ai.SegmentationResult;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 动漫分割结果容器
|
||||
*/
|
||||
public class AnimeSegmentationResult extends SegmentationResult {
|
||||
// 分割掩码图(每个像素的颜色为对应类别颜色)
|
||||
private final BufferedImage maskImage;
|
||||
|
||||
// 分割概率图(每个像素的类别概率分布)
|
||||
private final float[][][] probabilityMap;
|
||||
|
||||
// 类别索引 -> 类别名称
|
||||
private final Map<Integer, String> labels;
|
||||
|
||||
// 类别名称 -> ARGB 颜色
|
||||
private final Map<String, Integer> palette;
|
||||
|
||||
public AnimeSegmentationResult(BufferedImage maskImage, float[][][] probabilityMap,
|
||||
Map<Integer, String> labels, Map<String, Integer> palette) {
|
||||
super(maskImage, labels, palette);
|
||||
this.maskImage = maskImage;
|
||||
this.probabilityMap = probabilityMap;
|
||||
this.labels = labels;
|
||||
this.palette = palette;
|
||||
}
|
||||
|
||||
public BufferedImage getMaskImage() {
|
||||
return maskImage;
|
||||
}
|
||||
|
||||
public float[][][] getProbabilityMap() {
|
||||
return probabilityMap;
|
||||
}
|
||||
|
||||
public Map<Integer, String> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
public Map<String, Integer> getPalette() {
|
||||
return palette;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定类别的概率图
|
||||
*/
|
||||
public float[][] getClassProbability(int classIndex) {
|
||||
if (probabilityMap == null) return null;
|
||||
int height = probabilityMap.length;
|
||||
int width = probabilityMap[0].length;
|
||||
float[][] result = new float[height][width];
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
result[y][x] = probabilityMap[y][x][classIndex];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package com.chuangzhou.vivid2D.ai.anime_face_segmentation;
|
||||
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.ndarray.types.DataType;
|
||||
import ai.djl.ndarray.types.Shape;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.translate.Batchifier;
|
||||
import ai.djl.translate.TranslateException;
|
||||
import ai.djl.translate.Translator;
|
||||
import ai.djl.translate.TranslatorContext;
|
||||
import com.chuangzhou.vivid2D.ai.Segmenter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* AnimeSegmenter: 专门为 Anime-Face-Segmentation UNet 模型设计的分割器
|
||||
*/
|
||||
public class AnimeSegmenter extends Segmenter {
|
||||
|
||||
// 模型默认输入大小(与训练时一致)。若模型不同可以修改为实际值或让 caller 通过构造参数传入。
|
||||
private static final int MODEL_INPUT_W = 512;
|
||||
private static final int MODEL_INPUT_H = 512;
|
||||
|
||||
// 内部类,用于从Translator安全地传出数据
|
||||
public static class SegmentationData {
|
||||
final int[] indices; // 类别索引 [H * W]
|
||||
final float[][][] probMap; // 概率图 [H][W][C]
|
||||
final long[] shape; // 形状 [H, W]
|
||||
|
||||
public SegmentationData(int[] indices, float[][][] probMap, long[] shape) {
|
||||
this.indices = indices;
|
||||
this.probMap = probMap;
|
||||
this.shape = shape;
|
||||
}
|
||||
}
|
||||
|
||||
private final ZooModel<Image, SegmentationData> modelWrapper;
|
||||
private final Predictor<Image, SegmentationData> predictor;
|
||||
private final Map<String, Integer> palette;
|
||||
|
||||
public AnimeSegmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
|
||||
super(modelDir, labels);
|
||||
this.palette = AnimeLabelPalette.defaultPalette();
|
||||
|
||||
Translator<Image, SegmentationData> translator = new Translator<Image, SegmentationData>() {
|
||||
@Override
|
||||
public NDList processInput(TranslatorContext ctx, Image input) {
|
||||
NDManager manager = ctx.getNDManager();
|
||||
|
||||
// 如果图片已经是模型输入大小则不再 resize(避免重复缩放导致失真)
|
||||
Image toUse = input;
|
||||
if (!(input.getWidth() == MODEL_INPUT_W && input.getHeight() == MODEL_INPUT_H)) {
|
||||
toUse = input.resize(MODEL_INPUT_W, MODEL_INPUT_H, true);
|
||||
}
|
||||
|
||||
// 转换为NDArray并预处理
|
||||
NDArray array = toUse.toNDArray(manager);
|
||||
// DJL 返回 HWC 格式数组,转换为 CHW,并标准化到 [0,1]
|
||||
array = array.transpose(2, 0, 1) // HWC -> CHW
|
||||
.toType(DataType.FLOAT32, false)
|
||||
.div(255f) // 归一化到[0,1]
|
||||
.expandDims(0); // 添加batch维度 [1,3,H,W]
|
||||
|
||||
return new NDList(array);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SegmentationData processOutput(TranslatorContext ctx, NDList list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
throw new IllegalStateException("Model did not return any output.");
|
||||
}
|
||||
|
||||
NDArray output = list.get(0); // 期望形状 [1,C,H,W] 或 [1,C,W,H](以训练时一致为准)
|
||||
|
||||
// 确保维度:把 output 视作 [1, C, H, W]
|
||||
Shape outShape = output.getShape();
|
||||
if (outShape.dimension() != 4) {
|
||||
throw new IllegalStateException("Unexpected output shape: " + outShape);
|
||||
}
|
||||
|
||||
// 1. 获取类别索引(argmax) -> [H, W]
|
||||
NDArray squeezed = output.squeeze(0); // [C,H,W]
|
||||
NDArray classMap = squeezed.argMax(0).toType(DataType.INT32, false); // argMax over channel维度
|
||||
|
||||
// 2. 获取概率图(softmax 输出或模型已经输出概率),转换为 [H,W,C]
|
||||
NDArray probabilities = squeezed.transpose(1, 2, 0) // [H,W,C]
|
||||
.toType(DataType.FLOAT32, false);
|
||||
|
||||
// 3. 转换为Java数组
|
||||
long[] shape = classMap.getShape().getShape(); // [H, W]
|
||||
int[] indices = classMap.toIntArray();
|
||||
long[] probShape = probabilities.getShape().getShape(); // [H, W, C]
|
||||
int height = (int) probShape[0];
|
||||
int width = (int) probShape[1];
|
||||
int classes = (int) probShape[2];
|
||||
float[] flatProbMap = probabilities.toFloatArray();
|
||||
float[][][] probMap = new float[height][width][classes];
|
||||
for (int i = 0; i < height; i++) {
|
||||
for (int j = 0; j < width; j++) {
|
||||
for (int k = 0; k < classes; k++) {
|
||||
int index = i * width * classes + j * classes + k;
|
||||
probMap[i][j][k] = flatProbMap[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new SegmentationData(indices, probMap, shape);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Batchifier getBatchifier() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
Criteria<Image, SegmentationData> criteria = Criteria.builder()
|
||||
.setTypes(Image.class, SegmentationData.class)
|
||||
.optModelPath(modelDir)
|
||||
.optEngine("PyTorch")
|
||||
.optTranslator(translator)
|
||||
.build();
|
||||
|
||||
this.modelWrapper = criteria.loadModel();
|
||||
this.predictor = modelWrapper.newPredictor();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NDList processInput(TranslatorContext ctx, Image input) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public AnimeSegmentationResult segment(File imgFile) throws TranslateException, IOException {
|
||||
Image img = ImageFactory.getInstance().fromFile(imgFile.toPath());
|
||||
|
||||
// 预测并获取分割数据
|
||||
SegmentationData data = predictor.predict(img);
|
||||
|
||||
long[] shp = data.shape;
|
||||
int[] indices = data.indices;
|
||||
float[][][] probMap = data.probMap;
|
||||
|
||||
int height = (int) shp[0];
|
||||
int width = (int) shp[1];
|
||||
|
||||
// 创建掩码图像
|
||||
BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
Map<Integer, String> labelsMap = AnimeLabelPalette.getIndexToLabelMap();
|
||||
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int idx = indices[y * width + x];
|
||||
String label = labelsMap.getOrDefault(idx, "unknown");
|
||||
int argb = palette.getOrDefault(label, 0xFF00FF00); // 默认绿色
|
||||
mask.setRGB(x, y, argb);
|
||||
}
|
||||
}
|
||||
|
||||
return new AnimeSegmentationResult(mask, probMap, labelsMap, palette);
|
||||
}
|
||||
|
||||
/**
|
||||
* 专门针对眼睛的分割方法
|
||||
*/
|
||||
public BufferedImage extractEyes(File imgFile) throws TranslateException, IOException {
|
||||
AnimeSegmentationResult result = segment(imgFile);
|
||||
BufferedImage mask = result.getMaskImage();
|
||||
BufferedImage eyeMask = new BufferedImage(mask.getWidth(), mask.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
int eyeColor = palette.get("eye");
|
||||
|
||||
for (int y = 0; y < mask.getHeight(); y++) {
|
||||
for (int x = 0; x < mask.getWidth(); x++) {
|
||||
int rgb = mask.getRGB(x, y);
|
||||
if (rgb == eyeColor) {
|
||||
eyeMask.setRGB(x, y, eyeColor);
|
||||
} else {
|
||||
eyeMask.setRGB(x, y, 0x00000000); // 透明
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return eyeMask;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
predictor.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
try {
|
||||
modelWrapper.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.chuangzhou.vivid2D.ai.anime_segmentation;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 动漫分割模型的标签和颜色调色板。
|
||||
* 这是一个二分类模型:背景和前景(动漫人物)
|
||||
*/
|
||||
public class Anime2LabelPalette {
|
||||
|
||||
/**
|
||||
* 动漫分割模型的标准标签(2个类别)
|
||||
*/
|
||||
public static List<String> defaultLabels() {
|
||||
return Arrays.asList(
|
||||
"background", // 0
|
||||
"foreground" // 1
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回动漫分割模型的调色板
|
||||
*/
|
||||
public static Map<String, Integer> defaultPalette() {
|
||||
Map<String, Integer> map = new HashMap<>();
|
||||
// 索引 0: background - 黑色
|
||||
map.put("background", 0xFF000000);
|
||||
// 索引 1: foreground - 白色
|
||||
map.put("foreground", 0xFFFFFFFF);
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 专门为动漫分割模型设计的调色板(可视化更友好)
|
||||
*/
|
||||
public static Map<String, Integer> animeSegmentationPalette() {
|
||||
Map<String, Integer> map = new HashMap<>();
|
||||
// 背景 - 透明
|
||||
map.put("background", 0x00000000);
|
||||
// 前景 - 红色(用于可视化)
|
||||
map.put("foreground", 0xFFFF0000);
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.chuangzhou.vivid2D.ai.anime_segmentation;
|
||||
|
||||
import com.chuangzhou.vivid2D.ai.SegmentationResult;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 动漫分割结果容器
|
||||
*/
|
||||
public class Anime2SegmentationResult extends SegmentationResult {
|
||||
public Anime2SegmentationResult(BufferedImage maskImage, Map<Integer, String> labels, Map<String, Integer> palette) {
|
||||
super(maskImage, labels, palette);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.chuangzhou.vivid2D.ai.anime_segmentation;
|
||||
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.ndarray.types.DataType;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.translate.TranslateException;
|
||||
import ai.djl.translate.TranslatorContext;
|
||||
import com.chuangzhou.vivid2D.ai.SegmentationResult;
|
||||
import com.chuangzhou.vivid2D.ai.Segmenter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Anime2Segmenter: 专门用于动漫分割模型
|
||||
* 处理 anime-segmentation 模型的二值分割输出
|
||||
*/
|
||||
public class Anime2Segmenter extends Segmenter {
|
||||
public Anime2Segmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
|
||||
super(modelDir, labels);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NDList processInput(TranslatorContext ctx, Image input) {
|
||||
NDManager manager = ctx.getNDManager();
|
||||
|
||||
// 调整输入图像尺寸到模型期望的大小 (1024x1024)
|
||||
Image resized = input.resize(1024, 1024, true);
|
||||
NDArray array = resized.toNDArray(manager);
|
||||
|
||||
// 转换为 CHW 格式并归一化
|
||||
array = array.transpose(2, 0, 1).toType(DataType.FLOAT32, false);
|
||||
array = array.div(255f);
|
||||
array = array.expandDims(0); // 添加batch维度
|
||||
|
||||
return new NDList(array);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SegmentationData processOutput(TranslatorContext ctx, NDList list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
throw new IllegalStateException("Model did not return any output.");
|
||||
}
|
||||
NDArray out = list.get(0);
|
||||
// 动漫分割模型输出形状: [1, 1, H, W] - 单通道概率图
|
||||
// 应用sigmoid并二值化
|
||||
NDArray probabilities = out.div(out.neg().exp().add(1));
|
||||
NDArray binaryMask = probabilities.gt(0.5).toType(DataType.INT32, false);
|
||||
if (binaryMask.getShape().dimension() == 4) {
|
||||
binaryMask = binaryMask.squeeze(0).squeeze(0);
|
||||
}
|
||||
long[] finalShape = binaryMask.getShape().getShape();
|
||||
int[] indices = binaryMask.toIntArray();
|
||||
return new SegmentationData(indices, finalShape);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SegmentationResult segment(File imgFile) throws TranslateException, IOException {
|
||||
Image img = ImageFactory.getInstance().fromFile(imgFile.toPath());
|
||||
Segmenter.SegmentationData data = predictor.predict(img);
|
||||
long[] shp = data.shape;
|
||||
int[] indices = data.indices;
|
||||
int height, width;
|
||||
if (shp.length == 2) {
|
||||
height = (int) shp[0];
|
||||
width = (int) shp[1];
|
||||
} else {
|
||||
throw new RuntimeException("Unexpected classMap shape from SegmentationData: " + Arrays.toString(shp));
|
||||
}
|
||||
BufferedImage mask = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
Map<Integer, String> labelsMap = new HashMap<>();
|
||||
for (int i = 0; i < labels.size(); i++) {
|
||||
labelsMap.put(i, labels.get(i));
|
||||
}
|
||||
for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int idx = indices[y * width + x];
|
||||
String label = labelsMap.getOrDefault(idx, "unknown");
|
||||
int argb = palette.getOrDefault(label, 0xFFFF0000);
|
||||
mask.setRGB(x, y, argb);
|
||||
}
|
||||
}
|
||||
return new SegmentationResult(mask, labelsMap, palette);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
predictor.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
try {
|
||||
modelWrapper.close();
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.chuangzhou.vivid2D.ai.anime_segmentation;
|
||||
|
||||
import com.chuangzhou.vivid2D.ai.SegmentationResult;
|
||||
import com.chuangzhou.vivid2D.ai.VividModelWrapper;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Anime2VividModelWrapper - 对之前 Anime2Segmenter 的封装,提供更便捷的API
|
||||
* <p>
|
||||
* 用法示例:
|
||||
* Anime2VividModelWrapper wrapper = Anime2VividModelWrapper.load(Paths.get("/path/to/modelDir"));
|
||||
* Map<String, Anime2VividModelWrapper.ResultFiles> out = wrapper.segmentAndSave(
|
||||
* new File("input.jpg"),
|
||||
* Set.of("foreground"), // 动漫分割主要关注前景
|
||||
* Paths.get("outDir")
|
||||
* );
|
||||
* // out contains 每个目标标签对应的 mask+overlay 文件路径
|
||||
* wrapper.close();
|
||||
*/
|
||||
public class Anime2VividModelWrapper extends VividModelWrapper<Anime2Segmenter> {
|
||||
|
||||
private Anime2VividModelWrapper(Anime2Segmenter segmenter, List<String> labels, Map<String, Integer> palette) {
|
||||
super(segmenter, labels, palette);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 modelDir/synset.txt(每行一个标签),若不存在则使用 Anime2LabelPalette.defaultLabels()
|
||||
* 并创建 Anime2Segmenter 实例。
|
||||
*/
|
||||
public static Anime2VividModelWrapper load(Path modelDir) throws Exception {
|
||||
List<String> labels = loadLabelsFromSynset(modelDir).orElseGet(Anime2LabelPalette::defaultLabels);
|
||||
Anime2Segmenter s = new Anime2Segmenter(modelDir, labels);
|
||||
Map<String, Integer> palette = Anime2LabelPalette.animeSegmentationPalette();
|
||||
return new Anime2VividModelWrapper(s, labels, palette);
|
||||
}
|
||||
|
||||
public List<String> getLabels() {
|
||||
return Collections.unmodifiableList(labels);
|
||||
}
|
||||
|
||||
public Map<String, Integer> getPalette() {
|
||||
return Collections.unmodifiableMap(palette);
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接返回分割结果(Anime2SegmentationResult)
|
||||
*/
|
||||
public SegmentationResult segment(File inputImage) throws Exception {
|
||||
return segmenter.segment(inputImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。
|
||||
* 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
|
||||
* <p>
|
||||
* 返回值:Map<labelName, ResultFiles>,ResultFiles 包含 maskFile、overlayFile(两个 PNG)
|
||||
*/
|
||||
public Map<String, VividModelWrapper.ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception {
|
||||
if (!Files.exists(outDir)) {
|
||||
Files.createDirectories(outDir);
|
||||
}
|
||||
|
||||
SegmentationResult res = segment(inputImage);
|
||||
BufferedImage original = ImageIO.read(inputImage);
|
||||
BufferedImage maskImage = res.getMaskImage();
|
||||
|
||||
int maskW = maskImage.getWidth();
|
||||
int maskH = maskImage.getHeight();
|
||||
|
||||
// 解析 targets
|
||||
Set<String> realTargets = parseTargetsSet(targets);
|
||||
Map<String, ResultFiles> saved = new LinkedHashMap<>();
|
||||
|
||||
for (String target : realTargets) {
|
||||
if (!palette.containsKey(target)) {
|
||||
String finalTarget = target;
|
||||
Optional<String> matched = palette.keySet().stream()
|
||||
.filter(k -> k.equalsIgnoreCase(finalTarget))
|
||||
.findFirst();
|
||||
if (matched.isPresent()) target = matched.get();
|
||||
else {
|
||||
System.err.println("Warning: unknown label '" + target + "' - skip.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
int targetColor = palette.get(target);
|
||||
|
||||
// 1) 生成透明背景的二值掩码(只保留 target 像素)
|
||||
BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < maskH; y++) {
|
||||
for (int x = 0; x < maskW; x++) {
|
||||
int c = maskImage.getRGB(x, y);
|
||||
if (c == targetColor) {
|
||||
partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
|
||||
} else {
|
||||
partMask.setRGB(x, y, 0x00000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 将 mask 缩放到与原图一致(如果需要),并生成 overlay(半透明)
|
||||
BufferedImage maskResized = partMask;
|
||||
if (original.getWidth() != maskW || original.getHeight() != maskH) {
|
||||
maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = maskResized.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = overlay.createGraphics();
|
||||
g2.drawImage(original, 0, 0, null);
|
||||
// 半透明颜色(alpha = 0x88)
|
||||
int rgbOnly = (targetColor & 0x00FFFFFF);
|
||||
int translucent = (0x88 << 24) | rgbOnly;
|
||||
BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < colorOverlay.getHeight(); y++) {
|
||||
for (int x = 0; x < colorOverlay.getWidth(); x++) {
|
||||
int mc = maskResized.getRGB(x, y);
|
||||
if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
|
||||
colorOverlay.setRGB(x, y, translucent);
|
||||
} else {
|
||||
colorOverlay.setRGB(x, y, 0x00000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
g2.drawImage(colorOverlay, 0, 0, null);
|
||||
g2.dispose();
|
||||
|
||||
// 保存
|
||||
String safe = safeFileName(target);
|
||||
File maskOut = outDir.resolve(safe + "_mask.png").toFile();
|
||||
File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
|
||||
|
||||
ImageIO.write(maskResized, "png", maskOut);
|
||||
ImageIO.write(overlay, "png", overlayOut);
|
||||
|
||||
saved.put(target, new ResultFiles(maskOut, overlayOut));
|
||||
}
|
||||
|
||||
return saved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.chuangzhou.vivid2D.ai.face_parsing;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* BiSeNet 人脸解析模型的标准标签和颜色调色板。
|
||||
* 颜色值基于 zllrunning/face-parsing.PyTorch 仓库的 test.py 文件。
|
||||
* 标签索引必须与模型输出索引一致(0-18)。
|
||||
*/
|
||||
public class BiSeNetLabelPalette {
|
||||
|
||||
/**
|
||||
* BiSeNet 人脸解析模型的标准标签(19个类别,索引 0-18)
|
||||
*/
|
||||
public static List<String> defaultLabels() {
|
||||
return Arrays.asList(
|
||||
"background", // 0
|
||||
"skin", // 1
|
||||
"nose", // 2
|
||||
"eye_left", // 3
|
||||
"eye_right", // 4
|
||||
"eyebrow_left", // 5
|
||||
"eyebrow_right",// 6
|
||||
"ear_left", // 7
|
||||
"ear_right", // 8
|
||||
"mouth", // 9
|
||||
"lip_upper", // 10
|
||||
"lip_lower", // 11
|
||||
"hair", // 12
|
||||
"hat", // 13
|
||||
"earring", // 14
|
||||
"necklace", // 15
|
||||
"clothes", // 16
|
||||
"facial_hair",// 17
|
||||
"neck" // 18
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个对应的调色板:类别名 -> ARGB 颜色值。
|
||||
* 颜色值基于 test.py 中 part_colors 数组的 RGB 值转换为 ARGB 格式 (0xFFRRGGBB)。
|
||||
*/
|
||||
public static Map<String, Integer> defaultPalette() {
|
||||
Map<String, Integer> map = new HashMap<>();
|
||||
// 索引 0: background
|
||||
map.put("background", 0xFF000000); // 黑色
|
||||
|
||||
// 索引 1-18: 对应 part_colors 数组的前 18 个颜色
|
||||
// 注意:这里假设 part_colors[i-1] 对应 索引 i 的标签。
|
||||
// 索引 1: skin -> [255, 0, 0]
|
||||
map.put("skin", 0xFFFF0000);
|
||||
// 索引 2: nose -> [255, 85, 0]
|
||||
map.put("nose", 0xFFFF5500);
|
||||
// 索引 3: eye_left -> [255, 170, 0]
|
||||
map.put("eye_left", 0xFFFFAA00);
|
||||
// 索引 4: eye_right -> [255, 0, 85]
|
||||
map.put("eye_right", 0xFFFF0055);
|
||||
// 索引 5: eyebrow_left -> [255, 0, 170]
|
||||
map.put("eyebrow_left",0xFFFF00AA);
|
||||
// 索引 6: eyebrow_right -> [0, 255, 0]
|
||||
map.put("eyebrow_right",0xFF00FF00);
|
||||
// 索引 7: ear_left -> [85, 255, 0]
|
||||
map.put("ear_left", 0xFF55FF00);
|
||||
// 索引 8: ear_right -> [170, 255, 0]
|
||||
map.put("ear_right", 0xFFAAFF00);
|
||||
// 索引 9: mouth -> [0, 255, 85]
|
||||
map.put("mouth", 0xFF00FF55);
|
||||
// 索引 10: lip_upper -> [0, 255, 170]
|
||||
map.put("lip_upper", 0xFF00FFAA);
|
||||
// 索引 11: lip_lower -> [0, 0, 255]
|
||||
map.put("lip_lower", 0xFF0000FF);
|
||||
// 索引 12: hair -> [85, 0, 255]
|
||||
map.put("hair", 0xFF5500FF);
|
||||
// 索引 13: hat -> [170, 0, 255]
|
||||
map.put("hat", 0xFFAA00FF);
|
||||
// 索引 14: earring -> [0, 85, 255]
|
||||
map.put("earring", 0xFF0055FF);
|
||||
// 索引 15: necklace -> [0, 170, 255]
|
||||
map.put("necklace", 0xFF00AAFF);
|
||||
// 索引 16: clothes -> [255, 255, 0]
|
||||
map.put("clothes", 0xFFFFFF00);
|
||||
// 索引 17: facial_hair -> [255, 85, 85]
|
||||
map.put("facial_hair", 0xFFFF5555);
|
||||
// 索引 18: neck -> [255, 170, 170]
|
||||
map.put("neck", 0xFFFFAAAA);
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.chuangzhou.vivid2D.ai.face_parsing;
|
||||
|
||||
import com.chuangzhou.vivid2D.ai.SegmentationResult;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 分割结果容器
|
||||
*/
|
||||
public class BiSeNetSegmentationResult extends SegmentationResult {
|
||||
public BiSeNetSegmentationResult(BufferedImage maskImage, Map<Integer, String> labels, Map<String, Integer> palette) {
|
||||
super(maskImage, labels, palette);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.chuangzhou.vivid2D.ai.face_parsing;
|
||||
|
||||
import ai.djl.MalformedModelException;
|
||||
import ai.djl.inference.Predictor;
|
||||
import ai.djl.modality.cv.Image;
|
||||
import ai.djl.modality.cv.ImageFactory;
|
||||
import ai.djl.ndarray.NDArray;
|
||||
import ai.djl.ndarray.NDList;
|
||||
import ai.djl.ndarray.NDManager;
|
||||
import ai.djl.ndarray.types.DataType;
|
||||
import ai.djl.repository.zoo.Criteria;
|
||||
import ai.djl.repository.zoo.ModelNotFoundException;
|
||||
import ai.djl.repository.zoo.ZooModel;
|
||||
import ai.djl.translate.Batchifier;
|
||||
import ai.djl.translate.TranslateException;
|
||||
import ai.djl.translate.Translator;
|
||||
import ai.djl.translate.TranslatorContext;
|
||||
import com.chuangzhou.vivid2D.ai.SegmentationResult;
|
||||
import com.chuangzhou.vivid2D.ai.Segmenter;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* Segmenter: 加载模型并对图片做语义分割
|
||||
*
|
||||
* 说明:
|
||||
* - Translator.processOutput 在翻译器层就把模型输出处理成 (H, W) 的类别索引 NDArray,
|
||||
* 并把该 NDArray 拷贝到 persistentManager 中返回,从而避免后续 native 资源被释放的问题。
|
||||
* - 这里改为在 Translator 内部把 classMap 转为 Java int[](通过 classMap.toIntArray()),
|
||||
* 再用 persistentManager.create(int[], shape) 创建新的 NDArray 返回,确保安全。
|
||||
*/
|
||||
public class BiSeNetSegmenter extends Segmenter {
|
||||
|
||||
public BiSeNetSegmenter(Path modelDir, List<String> labels) throws IOException, MalformedModelException, ModelNotFoundException {
|
||||
super(modelDir, labels);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NDList processInput(TranslatorContext ctx, Image input) {
|
||||
NDManager manager = ctx.getNDManager();
|
||||
NDArray array = input.toNDArray(manager);
|
||||
array = array.transpose(2, 0, 1).toType(DataType.FLOAT32, false);
|
||||
array = array.div(255f);
|
||||
array = array.expandDims(0);
|
||||
return new NDList(array);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Segmenter.SegmentationData processOutput(TranslatorContext ctx, NDList list) {
|
||||
if (list == null || list.isEmpty()) {
|
||||
throw new IllegalStateException("Model did not return any output.");
|
||||
}
|
||||
|
||||
NDArray out = list.get(0);
|
||||
NDArray classMap;
|
||||
|
||||
// 1. 解析模型输出,得到类别图谱 (classMap)
|
||||
long[] shape = out.getShape().getShape();
|
||||
if (shape.length == 4 && shape[1] > 1) {
|
||||
classMap = out.argMax(1);
|
||||
} else if (shape.length == 3) {
|
||||
classMap = (shape[0] == 1) ? out : out.argMax(0);
|
||||
} else if (shape.length == 2) {
|
||||
classMap = out;
|
||||
} else {
|
||||
throw new IllegalStateException("Unexpected output shape: " + Arrays.toString(shape));
|
||||
}
|
||||
|
||||
if (classMap.getShape().dimension() == 3) {
|
||||
classMap = classMap.squeeze(0);
|
||||
}
|
||||
|
||||
// 2. *** 关键步骤 ***
|
||||
// 在 NDArray 仍然有效的上下文中,将其转换为 Java 原生类型
|
||||
|
||||
// 首先,确保数据类型是 INT32
|
||||
NDArray int32ClassMap = classMap.toType(DataType.INT32, false);
|
||||
|
||||
// 然后,获取形状和 int[] 数组
|
||||
long[] finalShape = int32ClassMap.getShape().getShape();
|
||||
int[] indices = int32ClassMap.toIntArray();
|
||||
|
||||
// 3. 将 Java 对象封装并返回
|
||||
return new SegmentationData(indices, finalShape);
|
||||
}
|
||||
|
||||
@Override
|
||||
public SegmentationResult segment(File imgFile) throws TranslateException, IOException {
|
||||
return super.segment(imgFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package com.chuangzhou.vivid2D.ai.face_parsing;
|
||||
|
||||
import com.chuangzhou.vivid2D.ai.SegmentationResult;
|
||||
import com.chuangzhou.vivid2D.ai.VividModelWrapper;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* VividModelWrapper - 对之前 Segmenter / SegmenterExample 的封装
|
||||
*
|
||||
* 用法示例:
|
||||
* VividModelWrapper wrapper = VividModelWrapper.load(Paths.get("/path/to/modelDir"));
|
||||
* Map<String, VividModelWrapper.ResultFiles> out = wrapper.segmentAndSave(
|
||||
* new File("input.jpg"),
|
||||
* Set.of("eye","face"), // 或 Set.of(all labels...);若想全部传 "all" 可以用 helper parseTargets
|
||||
* Paths.get("outDir")
|
||||
* );
|
||||
* // out contains 每个目标标签对应的 mask+overlay 文件路径
|
||||
* wrapper.close();
|
||||
*/
|
||||
public class BiSeNetVividModelWrapper extends VividModelWrapper<BiSeNetSegmenter> {
|
||||
|
||||
private BiSeNetVividModelWrapper(BiSeNetSegmenter segmenter, List<String> labels, Map<String, Integer> palette) {
|
||||
super(segmenter, labels, palette);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 modelDir/synset.txt(每行一个标签),若不存在则使用 LabelPalette.defaultLabels()
|
||||
* 并创建 Segmenter 实例。
|
||||
*/
|
||||
public static BiSeNetVividModelWrapper load(Path modelDir) throws Exception {
|
||||
List<String> labels = loadLabelsFromSynset(modelDir).orElseGet(BiSeNetLabelPalette::defaultLabels);
|
||||
BiSeNetSegmenter s = new BiSeNetSegmenter(modelDir, labels);
|
||||
Map<String, Integer> palette = BiSeNetLabelPalette.defaultPalette();
|
||||
return new BiSeNetVividModelWrapper(s, labels, palette);
|
||||
}
|
||||
|
||||
public List<String> getLabels() {
|
||||
return super.getLabels();
|
||||
}
|
||||
|
||||
public Map<String, Integer> getPalette() {
|
||||
return super.getPalette();
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接返回分割结果(SegmentationResult)
|
||||
*/
|
||||
public SegmentationResult segment(File inputImage) throws Exception {
|
||||
return segmenter.segment(inputImage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把指定 targets(标签名集合)从输入图片中分割并保存到 outDir。
|
||||
* 如果 targets 包含单个元素 "all"(忽略大小写),则保存所有标签。
|
||||
* <p>
|
||||
* 返回值:Map<labelName, ResultFiles>,ResultFiles 包含 maskFile、overlayFile(两个 PNG)
|
||||
*/
|
||||
public Map<String, VividModelWrapper.ResultFiles> segmentAndSave(File inputImage, Set<String> targets, Path outDir) throws Exception {
|
||||
if (!Files.exists(outDir)) {
|
||||
Files.createDirectories(outDir);
|
||||
}
|
||||
SegmentationResult res = segment(inputImage);
|
||||
BufferedImage original = ImageIO.read(inputImage);
|
||||
BufferedImage maskImage = res.getMaskImage();
|
||||
int maskW = maskImage.getWidth();
|
||||
int maskH = maskImage.getHeight();
|
||||
Set<String> realTargets = parseTargetsSet(targets);
|
||||
Map<String, ResultFiles> saved = new LinkedHashMap<>();
|
||||
for (String target : realTargets) {
|
||||
if (!palette.containsKey(target)) {
|
||||
String finalTarget = target;
|
||||
Optional<String> matched = palette.keySet().stream()
|
||||
.filter(k -> k.equalsIgnoreCase(finalTarget))
|
||||
.findFirst();
|
||||
if (matched.isPresent()) target = matched.get();
|
||||
else {
|
||||
System.err.println("Warning: unknown label '" + target + "' - skip.");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
int targetColor = palette.get(target);
|
||||
BufferedImage partMask = new BufferedImage(maskW, maskH, BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < maskH; y++) {
|
||||
for (int x = 0; x < maskW; x++) {
|
||||
int c = maskImage.getRGB(x, y);
|
||||
if (c == targetColor) {
|
||||
partMask.setRGB(x, y, targetColor | 0xFF000000); // 保证不透明
|
||||
} else {
|
||||
partMask.setRGB(x, y, 0x00000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
BufferedImage maskResized = partMask;
|
||||
if (original.getWidth() != maskW || original.getHeight() != maskH) {
|
||||
maskResized = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g = maskResized.createGraphics();
|
||||
g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
g.drawImage(partMask, 0, 0, original.getWidth(), original.getHeight(), null);
|
||||
g.dispose();
|
||||
}
|
||||
BufferedImage overlay = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2 = overlay.createGraphics();
|
||||
g2.drawImage(original, 0, 0, null);
|
||||
int rgbOnly = (targetColor & 0x00FFFFFF);
|
||||
int translucent = (0x88 << 24) | rgbOnly;
|
||||
BufferedImage colorOverlay = new BufferedImage(overlay.getWidth(), overlay.getHeight(), BufferedImage.TYPE_INT_ARGB);
|
||||
for (int y = 0; y < colorOverlay.getHeight(); y++) {
|
||||
for (int x = 0; x < colorOverlay.getWidth(); x++) {
|
||||
int mc = maskResized.getRGB(x, y);
|
||||
if ((mc & 0x00FFFFFF) == (targetColor & 0x00FFFFFF) && ((mc >>> 24) != 0)) {
|
||||
colorOverlay.setRGB(x, y, translucent);
|
||||
} else {
|
||||
colorOverlay.setRGB(x, y, 0x00000000);
|
||||
}
|
||||
}
|
||||
}
|
||||
g2.drawImage(colorOverlay, 0, 0, null);
|
||||
g2.dispose();
|
||||
String safe = safeFileName(target);
|
||||
File maskOut = outDir.resolve(safe + "_mask.png").toFile();
|
||||
File overlayOut = outDir.resolve(safe + "_overlay.png").toFile();
|
||||
ImageIO.write(maskResized, "png", maskOut);
|
||||
ImageIO.write(overlay, "png", overlayOut);
|
||||
saved.put(target, new ResultFiles(maskOut, overlayOut));
|
||||
}
|
||||
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭底层资源
|
||||
*/
|
||||
@Override
|
||||
public void close() {
|
||||
try {
|
||||
segmenter.close();
|
||||
} catch (Exception ignore) {}
|
||||
}
|
||||
}
|
||||
1397
src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java
Normal file
1397
src/main/java/com/chuangzhou/vivid2D/render/ModelRender.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,336 @@
|
||||
package com.chuangzhou.vivid2D.render;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import org.joml.Vector2f;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
|
||||
/**
|
||||
* 现代化选择框渲染器(性能优化版)
|
||||
* 主要优化点:
|
||||
* 1) 复用 Tesselator 单例 BufferBuilder,减少频繁的 GPU 资源创建/销毁
|
||||
* 2) 批量提交顶点:把同一 primitive(LINES / TRIANGLES / LINE_LOOP)与同一颜色的顶点尽量合并到一次 begin/end
|
||||
* 3) 手柄使用实心矩形(两三角形)批量绘制,保持美观且高效
|
||||
* 4) 增加轻微外发光(透明大边框)和阴影感以达到“现代”外观
|
||||
* <p>
|
||||
* 注意:本类依赖你工程中已有的 RenderSystem/Tesselator/BufferBuilder/BufferUploader 实现。
|
||||
*/
|
||||
public class MultiSelectionBoxRenderer {
|
||||
|
||||
// 常量定义(视觉可调)
|
||||
public static final float DEFAULT_CORNER_SIZE = 10.0f;
|
||||
public static final float DEFAULT_BORDER_THICKNESS = 6.0f;
|
||||
public static final float DEFAULT_DASH_LENGTH = 10.0f;
|
||||
public static final float DEFAULT_GAP_LENGTH = 6.0f;
|
||||
public static final float ROTATION_HANDLE_DISTANCE = 28.0f;
|
||||
public static final float HANDLE_ROUNDNESS = 1.5f; // 保留,用于未来改进圆角手柄
|
||||
|
||||
// 颜色(更现代的配色)
|
||||
public static final Vector4f DASHED_BORDER_COLOR = new Vector4f(1.0f, 0.85f, 0.0f, 1.0f); // 黄色虚线
|
||||
public static final Vector4f SOLID_BORDER_COLOR_OUTER = new Vector4f(0.0f, 0.85f, 0.95f, 0.18f); // 轻微外发光
|
||||
public static final Vector4f SOLID_BORDER_COLOR_MAIN = new Vector4f(0.0f, 0.92f, 0.94f, 1.0f); // 主边框,青色
|
||||
public static final Vector4f SOLID_BORDER_COLOR_INNER = new Vector4f(1.0f, 1.0f, 1.0f, 0.9f); // 内边框,接近白
|
||||
public static final Vector4f HANDLE_COLOR = new Vector4f(1.0f, 1.0f, 1.0f, 1.0f); // 手柄白
|
||||
public static final Vector4f MULTI_SELECTION_HANDLE_COLOR = new Vector4f(1.0f, 0.9f, 0.0f, 1.0f); // 黄色手柄
|
||||
public static final Vector4f CENTER_POINT_COLOR = new Vector4f(1.0f, 0.2f, 0.2f, 1.0f); // 中心点红
|
||||
public static final Vector4f ROTATION_HANDLE_COLOR = new Vector4f(0.14f, 0.95f, 0.3f, 1.0f); // 绿色旋转手柄
|
||||
public static final Vector4f SHADOW_COLOR = new Vector4f(0f, 0f, 0f, 0.18f); // 阴影/背板
|
||||
|
||||
/**
|
||||
* 绘制单选状态下的选择框(高效批处理)
|
||||
*/
|
||||
public static void drawSelectBox(BoundingBox bounds, Vector2f pivot) {
|
||||
if (!bounds.isValid()) return;
|
||||
|
||||
float minX = bounds.getMinX();
|
||||
float minY = bounds.getMinY();
|
||||
float maxX = bounds.getMaxX();
|
||||
float maxY = bounds.getMaxY();
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder bb = tesselator.getBuilder();
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// 1) 阴影底板(轻微偏移)
|
||||
bb.begin(RenderSystem.GL_TRIANGLES, 6);
|
||||
bb.setColor(SHADOW_COLOR);
|
||||
addFilledQuadTriangles(bb, minX + 4f, minY + 4f, maxX + 4f, maxY + 4f);
|
||||
tesselator.end();
|
||||
|
||||
// 2) 外发光边框(更柔和)
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
|
||||
bb.setColor(SOLID_BORDER_COLOR_OUTER);
|
||||
bb.vertex(minX - 6.0f, minY - 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(maxX + 6.0f, minY - 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(maxX + 6.0f, maxY + 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(minX - 6.0f, maxY + 6.0f, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 3) 主边框 + 内边框(两个 LINE_LOOP)
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
|
||||
bb.setColor(SOLID_BORDER_COLOR_MAIN);
|
||||
bb.vertex(minX - 1.0f, minY - 1.0f, 0.0f, 0.0f);
|
||||
bb.vertex(maxX + 1.0f, minY - 1.0f, 0.0f, 0.0f);
|
||||
bb.vertex(maxX + 1.0f, maxY + 1.0f, 0.0f, 0.0f);
|
||||
bb.vertex(minX - 1.0f, maxY + 1.0f, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 4);
|
||||
bb.setColor(SOLID_BORDER_COLOR_INNER);
|
||||
bb.vertex(minX, minY, 0.0f, 0.0f);
|
||||
bb.vertex(maxX, minY, 0.0f, 0.0f);
|
||||
bb.vertex(maxX, maxY, 0.0f, 0.0f);
|
||||
bb.vertex(minX, maxY, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 4) 手柄(一次性 TRIANGLES 批次绘制所有手柄)
|
||||
// 8 个手柄(四角 + 四边中点),每个 6 个顶点
|
||||
bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8);
|
||||
bb.setColor(HANDLE_COLOR);
|
||||
addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE); // 左上
|
||||
addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE); // 右上
|
||||
addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE); // 左下
|
||||
addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE); // 右下
|
||||
|
||||
addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS); // 上中
|
||||
addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS); // 下中
|
||||
addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 左中
|
||||
addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS); // 右中
|
||||
tesselator.end();
|
||||
|
||||
// 5) 中心点(十字 + 圆环)
|
||||
// 十字:LINES
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(CENTER_POINT_COLOR);
|
||||
bb.vertex(pivot.x - 6.0f, pivot.y, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x + 6.0f, pivot.y, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x, pivot.y - 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x, pivot.y + 6.0f, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 圆环:LINE_LOOP
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
|
||||
bb.setColor(CENTER_POINT_COLOR);
|
||||
float radius = 6.0f * 0.85f;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
float angle = (float) (i * 2f * Math.PI / 16f);
|
||||
bb.vertex(pivot.x + (float) Math.cos(angle) * radius, pivot.y + (float) Math.sin(angle) * radius, 0.0f, 0.0f);
|
||||
}
|
||||
tesselator.end();
|
||||
|
||||
// 6) 旋转手柄(连线 + 圆 + 箭头),分三次提交但数量小
|
||||
float topY = bounds.getMinY();
|
||||
float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE;
|
||||
|
||||
// 连线
|
||||
bb.begin(GL11.GL_LINES, 2);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
bb.vertex(pivot.x, topY, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x, rotationHandleY, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 圆
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
float handleRadius = 6.0f;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
float angle = (float) (i * 2f * Math.PI / 16f);
|
||||
bb.vertex(pivot.x + (float) Math.cos(angle) * handleRadius, rotationHandleY + (float) Math.sin(angle) * handleRadius, 0.0f, 0.0f);
|
||||
}
|
||||
tesselator.end();
|
||||
|
||||
// 箭头(两条交叉线,提示旋转)
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
float arrow = 4.0f;
|
||||
bb.vertex(pivot.x - arrow, rotationHandleY - arrow, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x + arrow, rotationHandleY + arrow, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x + arrow, rotationHandleY - arrow, 0.0f, 0.0f);
|
||||
bb.vertex(pivot.x - arrow, rotationHandleY + arrow, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* 绘制多选框(现代化外观,批量提交)
|
||||
*/
|
||||
public static void drawMultiSelectionBox(BoundingBox multiBounds) {
|
||||
if (!multiBounds.isValid()) return;
|
||||
|
||||
float minX = multiBounds.getMinX();
|
||||
float minY = multiBounds.getMinY();
|
||||
float maxX = multiBounds.getMaxX();
|
||||
float maxY = multiBounds.getMaxY();
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder bb = tesselator.getBuilder();
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// 虚线边框 - 将所有虚线段放在同一个 GL_LINES 批次
|
||||
int estimatedSegments = Math.max(4,
|
||||
(int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH)));
|
||||
bb.begin(GL11.GL_LINES, estimatedSegments * 2);
|
||||
bb.setColor(DASHED_BORDER_COLOR);
|
||||
addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
tesselator.end();
|
||||
|
||||
// 手柄(一次性 TRIANGLES 批次)
|
||||
bb.begin(RenderSystem.GL_TRIANGLES, 6 * 8);
|
||||
bb.setColor(MULTI_SELECTION_HANDLE_COLOR);
|
||||
addHandleQuad(bb, minX, minY, DEFAULT_CORNER_SIZE);
|
||||
addHandleQuad(bb, maxX, minY, DEFAULT_CORNER_SIZE);
|
||||
addHandleQuad(bb, minX, maxY, DEFAULT_CORNER_SIZE);
|
||||
addHandleQuad(bb, maxX, maxY, DEFAULT_CORNER_SIZE);
|
||||
addHandleQuad(bb, (minX + maxX) / 2f, minY, DEFAULT_BORDER_THICKNESS);
|
||||
addHandleQuad(bb, (minX + maxX) / 2f, maxY, DEFAULT_BORDER_THICKNESS);
|
||||
addHandleQuad(bb, minX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS);
|
||||
addHandleQuad(bb, maxX, (minY + maxY) / 2f, DEFAULT_BORDER_THICKNESS);
|
||||
tesselator.end();
|
||||
|
||||
// 中心点
|
||||
Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f);
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(CENTER_POINT_COLOR);
|
||||
bb.vertex(center.x - 6.0f, center.y, 0.0f, 0.0f);
|
||||
bb.vertex(center.x + 6.0f, center.y, 0.0f, 0.0f);
|
||||
bb.vertex(center.x, center.y - 6.0f, 0.0f, 0.0f);
|
||||
bb.vertex(center.x, center.y + 6.0f, 0.0f, 0.0f);
|
||||
tesselator.end();
|
||||
|
||||
// 旋转手柄(沿用单选逻辑)
|
||||
drawMultiSelectionRotationHandle(bb, minX, minY, maxX, maxY);
|
||||
}
|
||||
|
||||
// ------ 辅助顶点生成方法(批量写入当前 begin() 的 BufferBuilder) ------
|
||||
|
||||
// 向当前 TRIANGLES 批次添加一个填充矩形(两三角形)
|
||||
private static void addFilledQuadTriangles(BufferBuilder bb, float x0, float y0, float x1, float y1) {
|
||||
// 三角形 1
|
||||
bb.vertex(x0, y0, 0.0f, 0.0f);
|
||||
bb.vertex(x1, y0, 0.0f, 0.0f);
|
||||
bb.vertex(x1, y1, 0.0f, 0.0f);
|
||||
// 三角形 2
|
||||
bb.vertex(x1, y1, 0.0f, 0.0f);
|
||||
bb.vertex(x0, y1, 0.0f, 0.0f);
|
||||
bb.vertex(x0, y0, 0.0f, 0.0f);
|
||||
}
|
||||
|
||||
// 向当前 TRIANGLES 批次添加一个手柄方块(中心在 cx,cy,边长 size)
|
||||
private static void addHandleQuad(BufferBuilder bb, float cx, float cy, float size) {
|
||||
float half = size / 2f;
|
||||
addFilledQuadTriangles(bb, cx - half, cy - half, cx + half, cy + half);
|
||||
}
|
||||
|
||||
// 向当前 LINES 批次添加一段虚线(将多个线段顶点 push 到当前 begin())
|
||||
private static void addDashedLineVertices(BufferBuilder bb, float startX, float startY, float endX, float endY,
|
||||
float dashLen, float gapLen) {
|
||||
float dx = endX - startX;
|
||||
float dy = endY - startY;
|
||||
float len = (float) Math.sqrt(dx * dx + dy * dy);
|
||||
if (len < 0.001f) return;
|
||||
float dirX = dx / len, dirY = dy / len;
|
||||
float seg = dashLen + gapLen;
|
||||
int count = (int) Math.ceil(len / seg);
|
||||
for (int i = 0; i < count; i++) {
|
||||
float s = i * seg;
|
||||
if (s >= len) break;
|
||||
float e = Math.min(s + dashLen, len);
|
||||
float sx = startX + dirX * s;
|
||||
float sy = startY + dirY * s;
|
||||
float ex = startX + dirX * e;
|
||||
float ey = startY + dirY * e;
|
||||
bb.vertex(sx, sy, 0.0f, 0.0f);
|
||||
bb.vertex(ex, ey, 0.0f, 0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
// 适配:在 multi selection 中把旋转手柄渲染写入到传入的 bb(会在函数内部使用 tesselator.end())
|
||||
public static void drawMultiSelectionRotationHandle(BufferBuilder bb, float minX, float minY, float maxX, float maxY) {
|
||||
Vector2f center = new Vector2f((minX + maxX) / 2f, (minY + maxY) / 2f);
|
||||
drawRotationHandle(center, new BoundingBox(minX, minY, maxX, maxY));
|
||||
}
|
||||
|
||||
// 单独绘制旋转手柄(内部会 new / begin / end,因为包含多种 primitive)
|
||||
private static void drawRotationHandle(Vector2f pivot, BoundingBox bounds) {
|
||||
float centerX = pivot.x;
|
||||
float centerY = pivot.y;
|
||||
float topY = bounds.getMinY();
|
||||
|
||||
boolean pivotInBounds = (centerX >= bounds.getMinX() && centerX <= bounds.getMaxX() &&
|
||||
centerY >= bounds.getMinY() && centerY <= bounds.getMaxY());
|
||||
if (!pivotInBounds) {
|
||||
centerX = (bounds.getMinX() + bounds.getMaxX()) * 0.5f;
|
||||
centerY = (bounds.getMinY() + bounds.getMaxY()) * 0.5f;
|
||||
topY = bounds.getMinY();
|
||||
}
|
||||
|
||||
float rotationHandleY = topY - ROTATION_HANDLE_DISTANCE;
|
||||
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder bb = t.getBuilder();
|
||||
|
||||
// 连线
|
||||
bb.begin(GL11.GL_LINES, 2);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
bb.vertex(centerX, topY, 0.0f, 0.0f);
|
||||
bb.vertex(centerX, rotationHandleY, 0.0f, 0.0f);
|
||||
t.end();
|
||||
|
||||
// 圆环
|
||||
bb.begin(RenderSystem.GL_LINE_LOOP, 16);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
float r = 6.0f;
|
||||
for (int i = 0; i < 16; i++) {
|
||||
float ang = (float) (i * 2f * Math.PI / 16f);
|
||||
bb.vertex(centerX + (float) Math.cos(ang) * r, rotationHandleY + (float) Math.sin(ang) * r, 0.0f, 0.0f);
|
||||
}
|
||||
t.end();
|
||||
|
||||
// 箭头
|
||||
bb.begin(GL11.GL_LINES, 4);
|
||||
bb.setColor(ROTATION_HANDLE_COLOR);
|
||||
float arrow = 4.0f;
|
||||
bb.vertex(centerX - arrow, rotationHandleY - arrow, 0.0f, 0.0f);
|
||||
bb.vertex(centerX + arrow, rotationHandleY + arrow, 0.0f, 0.0f);
|
||||
bb.vertex(centerX + arrow, rotationHandleY - arrow, 0.0f, 0.0f);
|
||||
bb.vertex(centerX - arrow, rotationHandleY + arrow, 0.0f, 0.0f);
|
||||
t.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅绘制简化的多选虚线边框(保留单次批量绘制)
|
||||
*/
|
||||
public static void drawSimpleMultiSelectionBox(BoundingBox multiBounds) {
|
||||
if (!multiBounds.isValid()) return;
|
||||
|
||||
float minX = multiBounds.getMinX();
|
||||
float minY = multiBounds.getMinY();
|
||||
float maxX = multiBounds.getMaxX();
|
||||
float maxY = multiBounds.getMaxY();
|
||||
|
||||
Tesselator tesselator = Tesselator.getInstance();
|
||||
BufferBuilder bb = tesselator.getBuilder();
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
int est = Math.max(4,
|
||||
(int) Math.ceil((2f * ((maxX - minX) + (maxY - minY))) / (DEFAULT_DASH_LENGTH + DEFAULT_GAP_LENGTH)));
|
||||
bb.begin(GL11.GL_LINES, est * 2);
|
||||
bb.setColor(DASHED_BORDER_COLOR);
|
||||
addDashedLineVertices(bb, minX, minY, maxX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, maxX, minY, maxX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, maxX, maxY, minX, maxY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
addDashedLineVertices(bb, minX, maxY, minX, minY, DEFAULT_DASH_LENGTH, DEFAULT_GAP_LENGTH);
|
||||
tesselator.end();
|
||||
}
|
||||
}
|
||||
336
src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java
Normal file
336
src/main/java/com/chuangzhou/vivid2D/render/TextRenderer.java
Normal file
@@ -0,0 +1,336 @@
|
||||
package com.chuangzhou.vivid2D.render;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.BufferBuilder;
|
||||
import com.chuangzhou.vivid2D.render.systems.buffer.Tesselator;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderManagement;
|
||||
import com.chuangzhou.vivid2D.render.systems.sources.ShaderProgram;
|
||||
import org.joml.Vector4f;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.opengl.GL12;
|
||||
import org.lwjgl.opengl.GL30;
|
||||
import org.lwjgl.opengl.GL33;
|
||||
import org.lwjgl.stb.STBTTAlignedQuad;
|
||||
import org.lwjgl.stb.STBTTBakedChar;
|
||||
import org.lwjgl.system.MemoryStack;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
import static org.lwjgl.stb.STBTruetype.stbtt_BakeFontBitmap;
|
||||
import static org.lwjgl.stb.STBTruetype.stbtt_GetBakedQuad;
|
||||
|
||||
/**
|
||||
* 支持 ASCII + 中文的 OpenGL 文本渲染器
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public final class TextRenderer {
|
||||
private static final Logger logger = LoggerFactory.getLogger(TextRenderer.class);
|
||||
|
||||
private final int bitmapWidth;
|
||||
private final int bitmapHeight;
|
||||
private final int firstChar;
|
||||
private final int charCount;
|
||||
|
||||
private STBTTBakedChar.Buffer asciiCharData;
|
||||
private STBTTBakedChar.Buffer chineseCharData;
|
||||
private int asciiTextureId;
|
||||
private int chineseTextureId;
|
||||
|
||||
private boolean initialized = false;
|
||||
|
||||
// 中文字符起始编码(选择一个不冲突的范围)
|
||||
private static final int CHINESE_FIRST_CHAR = 0x4E00; // CJK Unified Ideographs 常用汉字起始范围
|
||||
private static final int CHINESE_CHAR_COUNT = 20000;
|
||||
|
||||
public TextRenderer(int bitmapWidth, int bitmapHeight, int firstChar, int charCount) {
|
||||
this.bitmapWidth = bitmapWidth;
|
||||
this.bitmapHeight = bitmapHeight;
|
||||
this.firstChar = firstChar;
|
||||
this.charCount = charCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化字体渲染器
|
||||
*/
|
||||
public void initialize(ByteBuffer fontData, float fontHeight) {
|
||||
if (initialized) {
|
||||
logger.warn("TextRenderer already initialized");
|
||||
return;
|
||||
}
|
||||
if (fontData == null || fontData.capacity() == 0 || fontHeight <= 0) {
|
||||
logger.error("Invalid font data or font height");
|
||||
return;
|
||||
}
|
||||
|
||||
ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader");
|
||||
if (shader == null) {
|
||||
logger.error("TextShader not found");
|
||||
return;
|
||||
}
|
||||
|
||||
shader.use();
|
||||
|
||||
try {
|
||||
// 烘焙 ASCII
|
||||
asciiCharData = STBTTBakedChar.malloc(charCount);
|
||||
ByteBuffer asciiBitmap = ByteBuffer.allocateDirect(bitmapWidth * bitmapHeight);
|
||||
int asciiRes = stbtt_BakeFontBitmap(fontData, fontHeight, asciiBitmap,
|
||||
bitmapWidth, bitmapHeight, firstChar, asciiCharData);
|
||||
if (asciiRes <= 0) {
|
||||
logger.error("ASCII font bake failed, result: {}", asciiRes);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
asciiTextureId = createTextureFromBitmap(bitmapWidth, bitmapHeight, asciiBitmap);
|
||||
if (asciiTextureId == 0) {
|
||||
logger.error("Failed to create ASCII texture");
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
// 烘焙中文 - 使用更大的纹理和正确的字符范围
|
||||
int chineseTexSize = 4096; // 中文字符需要更大的纹理
|
||||
// 分配足够的空间来存储 CHINESE_CHAR_COUNT 个字符的数据
|
||||
chineseCharData = STBTTBakedChar.malloc(CHINESE_CHAR_COUNT);
|
||||
ByteBuffer chineseBitmap = ByteBuffer.allocateDirect(chineseTexSize * chineseTexSize);
|
||||
|
||||
// 关键:烘焙从 CHINESE_FIRST_CHAR 开始的 CHINESE_CHAR_COUNT 个连续字符
|
||||
int chineseRes = stbtt_BakeFontBitmap(fontData, fontHeight, chineseBitmap,
|
||||
chineseTexSize, chineseTexSize, CHINESE_FIRST_CHAR, chineseCharData);
|
||||
if (chineseRes <= 0) {
|
||||
logger.error("Chinese font bake failed, result: {}", chineseRes);
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
chineseTextureId = createTextureFromBitmap(chineseTexSize, chineseTexSize, chineseBitmap);
|
||||
if (chineseTextureId == 0) {
|
||||
logger.error("Failed to create Chinese texture");
|
||||
cleanup();
|
||||
return;
|
||||
}
|
||||
|
||||
initialized = true;
|
||||
logger.debug("TextRenderer initialized, ASCII tex={}, Chinese tex={}", asciiTextureId, chineseTextureId);
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Exception during TextRenderer init: {}", e.getMessage(), e);
|
||||
cleanup();
|
||||
} finally {
|
||||
shader.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染文字
|
||||
*/
|
||||
public void renderText(String text, float x, float y, Vector4f color) {
|
||||
renderText(text, x, y, color, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一行文字的宽度(单位:像素)
|
||||
*/
|
||||
public float getTextWidth(String text) {
|
||||
return getTextWidth(text, 1.0f);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取一行文字的宽度(带缩放)
|
||||
*/
|
||||
public float getTextWidth(String text, float scale) {
|
||||
if (!initialized || text == null || text.isEmpty()) return 0f;
|
||||
|
||||
float width = 0f;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
if (c >= firstChar && c < firstChar + charCount) {
|
||||
STBTTBakedChar bakedChar = asciiCharData.get(c - firstChar);
|
||||
width += bakedChar.xadvance() * scale;
|
||||
} else {
|
||||
// 修复中文索引逻辑:检查字符是否在烘焙的连续范围内
|
||||
if (c >= CHINESE_FIRST_CHAR && c < CHINESE_FIRST_CHAR + CHINESE_CHAR_COUNT) {
|
||||
int idx = c - CHINESE_FIRST_CHAR; // 关键:使用 Unicode 差值作为索引
|
||||
STBTTBakedChar bakedChar = chineseCharData.get(idx);
|
||||
width += bakedChar.xadvance() * scale;
|
||||
} else {
|
||||
// 对于未找到的字符,使用空格宽度
|
||||
width += 0.5f * scale; // 估计值
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return width;
|
||||
}
|
||||
|
||||
public void renderText(String text, float x, float y, Vector4f color, float scale) {
|
||||
if (!initialized || text == null || text.isEmpty()) return;
|
||||
if (scale <= 0f) scale = 1.0f;
|
||||
|
||||
RenderSystem.assertOnRenderThread();
|
||||
RenderSystem.pushState();
|
||||
try {
|
||||
ShaderProgram shader = ShaderManagement.getShaderProgram("TextShader");
|
||||
if (shader == null) {
|
||||
logger.error("TextShader not found");
|
||||
return;
|
||||
}
|
||||
shader.use();
|
||||
ShaderManagement.setUniformVec4(shader, "uColor", color);
|
||||
ShaderManagement.setUniformInt(shader, "uTexture", 0);
|
||||
|
||||
RenderSystem.enableBlend();
|
||||
RenderSystem.blendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
|
||||
RenderSystem.disableDepthTest();
|
||||
|
||||
try (MemoryStack stack = MemoryStack.stackPush()) {
|
||||
STBTTAlignedQuad q = STBTTAlignedQuad.malloc(stack);
|
||||
float[] xpos = {x};
|
||||
float[] ypos = {y};
|
||||
|
||||
Tesselator t = Tesselator.getInstance();
|
||||
BufferBuilder builder = t.getBuilder();
|
||||
|
||||
// 按字符类型分组渲染以减少纹理切换
|
||||
int currentTexture = -1;
|
||||
boolean batchStarted = false;
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
int targetTexture;
|
||||
STBTTBakedChar.Buffer charBuffer;
|
||||
int texWidth, texHeight;
|
||||
|
||||
if (c >= firstChar && c < firstChar + charCount) {
|
||||
targetTexture = asciiTextureId;
|
||||
charBuffer = asciiCharData;
|
||||
texWidth = bitmapWidth;
|
||||
texHeight = bitmapHeight;
|
||||
stbtt_GetBakedQuad(charBuffer, texWidth, texHeight, c - firstChar, xpos, ypos, q, true);
|
||||
} else {
|
||||
// 修复中文索引逻辑:检查字符是否在烘焙的连续范围内
|
||||
if (c >= CHINESE_FIRST_CHAR && c < CHINESE_FIRST_CHAR + CHINESE_CHAR_COUNT) {
|
||||
targetTexture = chineseTextureId;
|
||||
charBuffer = chineseCharData;
|
||||
texWidth = 4096;
|
||||
texHeight = 4096;
|
||||
// 关键修复:索引是字符的 Unicode 减去起始 Unicode
|
||||
int idx = c - CHINESE_FIRST_CHAR;
|
||||
stbtt_GetBakedQuad(charBuffer, texWidth, texHeight, idx, xpos, ypos, q, true);
|
||||
} else {
|
||||
continue; // 跳过不支持的字符
|
||||
}
|
||||
}
|
||||
|
||||
// 如果纹理改变,结束当前批次
|
||||
if (targetTexture != currentTexture) {
|
||||
if (batchStarted) {
|
||||
t.end();
|
||||
batchStarted = false;
|
||||
}
|
||||
RenderSystem.bindTexture(targetTexture);
|
||||
currentTexture = targetTexture;
|
||||
}
|
||||
|
||||
// 开始新批次(如果需要)
|
||||
if (!batchStarted) {
|
||||
builder.begin(RenderSystem.DRAW_TRIANGLES, (text.length() - i) * 6);
|
||||
batchStarted = true;
|
||||
}
|
||||
|
||||
// 应用缩放并计算顶点
|
||||
float sx0 = x + (q.x0() - x) * scale;
|
||||
float sx1 = x + (q.x1() - x) * scale;
|
||||
float sy0 = y + (q.y0() - y) * scale;
|
||||
float sy1 = y + (q.y1() - y) * scale;
|
||||
|
||||
builder.vertex(sx0, sy0, q.s0(), q.t0());
|
||||
builder.vertex(sx1, sy0, q.s1(), q.t0());
|
||||
builder.vertex(sx0, sy1, q.s0(), q.t1());
|
||||
|
||||
builder.vertex(sx1, sy0, q.s1(), q.t0());
|
||||
builder.vertex(sx1, sy1, q.s1(), q.t1());
|
||||
builder.vertex(sx0, sy1, q.s0(), q.t1());
|
||||
}
|
||||
|
||||
// 结束最后一个批次
|
||||
if (batchStarted) {
|
||||
t.end();
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
logger.error("Error rendering text: {}", e.getMessage(), e);
|
||||
} finally {
|
||||
RenderSystem.popState();
|
||||
}
|
||||
}
|
||||
|
||||
private int createTextureFromBitmap(int width, int height, ByteBuffer pixels) {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
try {
|
||||
int textureId = RenderSystem.genTextures();
|
||||
RenderSystem.bindTexture(textureId);
|
||||
|
||||
RenderSystem.pixelStore(GL11.GL_UNPACK_ALIGNMENT, 1);
|
||||
RenderSystem.texImage2D(GL11.GL_TEXTURE_2D, 0, GL30.GL_R8, width, height, 0,
|
||||
GL11.GL_RED, GL11.GL_UNSIGNED_BYTE, pixels);
|
||||
|
||||
RenderSystem.setTextureMinFilter(GL11.GL_LINEAR);
|
||||
RenderSystem.setTextureMagFilter(GL11.GL_LINEAR);
|
||||
RenderSystem.setTextureWrapS(GL12.GL_CLAMP_TO_EDGE);
|
||||
RenderSystem.setTextureWrapT(GL12.GL_CLAMP_TO_EDGE);
|
||||
|
||||
// 设置纹理swizzle以便单通道纹理在着色器中显示为白色
|
||||
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_R, GL11.GL_RED);
|
||||
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_G, GL11.GL_RED);
|
||||
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_B, GL11.GL_RED);
|
||||
RenderSystem.texParameteri(GL11.GL_TEXTURE_2D, GL33.GL_TEXTURE_SWIZZLE_A, GL11.GL_RED);
|
||||
|
||||
RenderSystem.pixelStore(GL11.GL_UNPACK_ALIGNMENT, 4);
|
||||
RenderSystem.bindTexture(0);
|
||||
|
||||
return textureId;
|
||||
} catch (Exception e) {
|
||||
logger.error("Failed to create texture from bitmap: {}", e.getMessage(), e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void cleanup() {
|
||||
RenderSystem.assertOnRenderThread();
|
||||
if (asciiTextureId != 0) {
|
||||
RenderSystem.deleteTextures(asciiTextureId);
|
||||
asciiTextureId = 0;
|
||||
}
|
||||
if (chineseTextureId != 0) {
|
||||
RenderSystem.deleteTextures(chineseTextureId);
|
||||
chineseTextureId = 0;
|
||||
}
|
||||
if (asciiCharData != null) {
|
||||
asciiCharData.free();
|
||||
asciiCharData = null;
|
||||
}
|
||||
if (chineseCharData != null) {
|
||||
chineseCharData.free();
|
||||
chineseCharData = null;
|
||||
}
|
||||
initialized = false;
|
||||
logger.debug("TextRenderer cleaned up");
|
||||
}
|
||||
|
||||
public boolean isInitialized() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
public int getAsciiTextureId() {
|
||||
return asciiTextureId;
|
||||
}
|
||||
|
||||
public int getChineseTextureId() {
|
||||
return chineseTextureId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
public class ModelAIPanel {
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
|
||||
/**
|
||||
* 模型点击事件监听器接口
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public interface ModelClickListener {
|
||||
/**
|
||||
* 当点击模型时触发
|
||||
*
|
||||
* @param mesh 被点击的网格,如果点击在空白处则为 null
|
||||
* @param modelX 模型坐标系中的 X 坐标
|
||||
* @param modelY 模型坐标系中的 Y 坐标
|
||||
* @param screenX 屏幕坐标系中的 X 坐标
|
||||
* @param screenY 屏幕坐标系中的 Y 坐标
|
||||
*/
|
||||
void onModelClicked(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY);
|
||||
|
||||
/**
|
||||
* 当鼠标在模型上移动时触发
|
||||
*
|
||||
* @param mesh 鼠标下方的网格,如果不在任何网格上则为 null
|
||||
* @param modelX 模型坐标系中的 X 坐标
|
||||
* @param modelY 模型坐标系中的 Y 坐标
|
||||
* @param screenX 屏幕坐标系中的 X 坐标
|
||||
* @param screenY 屏幕坐标系中的 Y 坐标
|
||||
*/
|
||||
default void onModelHover(Mesh2D mesh, float modelX, float modelY, int screenX, int screenY) {
|
||||
}
|
||||
|
||||
default void onLiquifyModeExited() {
|
||||
}
|
||||
|
||||
default void onLiquifyModeEntered(Mesh2D targetMesh, ModelPart liquifyTargetPart) {
|
||||
}
|
||||
|
||||
default void onSecondaryVertexModeEntered(Mesh2D secondaryVertexTargetMesh) {
|
||||
}
|
||||
|
||||
default void onSecondaryVertexModeExited() {
|
||||
}
|
||||
|
||||
default void onPuppetModeEntered(Mesh2D puppetTargetMesh) {
|
||||
}
|
||||
|
||||
default void onPuppetModeExited() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,845 @@
|
||||
// ModelLayerPanel.java (现代化重构)
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.LayerOperationManager;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.ThumbnailManager;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.*;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerCellRenderer;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.renderer.LayerReorderTransferHandler;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.EmptyBorder;
|
||||
import javax.swing.border.TitledBorder;
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.geom.RoundRectangle2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ModelLayerPanel extends JPanel {
|
||||
private Model2D model;
|
||||
private ModelRenderPanel renderPanel;
|
||||
|
||||
private DefaultListModel<ModelPart> listModel;
|
||||
private JList<ModelPart> layerList;
|
||||
|
||||
// 现代化UI组件
|
||||
private ModernButton addButton;
|
||||
private ModernButton removeButton;
|
||||
private ModernButton upButton;
|
||||
private ModernButton downButton;
|
||||
private ModernButton bindTextureButton;
|
||||
|
||||
private JSlider opacitySlider;
|
||||
private JLabel opacityValueLabel;
|
||||
private boolean isDragging = false;
|
||||
private ModelPart draggedPart = null;
|
||||
private Vector2f dragStartPosition = null;
|
||||
|
||||
private volatile boolean ignoreSliderEvents = false;
|
||||
|
||||
// 使用重构后的工具类
|
||||
private ThumbnailManager thumbnailManager;
|
||||
private PSDImporter psdImporter;
|
||||
private LayerOperationManager operationManager;
|
||||
|
||||
// 现代化颜色方案
|
||||
private static final Color BACKGROUND_COLOR = new Color(45, 45, 48);
|
||||
private static final Color SURFACE_COLOR = new Color(62, 62, 66);
|
||||
private static final Color ACCENT_COLOR = new Color(0, 122, 204);
|
||||
private static final Color TEXT_COLOR = new Color(241, 241, 241);
|
||||
private static final Color BORDER_COLOR = new Color(87, 87, 87);
|
||||
|
||||
public ModelLayerPanel(Model2D model) {
|
||||
this(model, null);
|
||||
}
|
||||
|
||||
public ModelLayerPanel(Model2D model, ModelRenderPanel renderPanel) {
|
||||
this.model = model;
|
||||
this.renderPanel = renderPanel;
|
||||
|
||||
// 设置现代化外观
|
||||
setupModernLookAndFeel();
|
||||
|
||||
// 初始化工具类
|
||||
this.thumbnailManager = new ThumbnailManager(renderPanel);
|
||||
this.psdImporter = new PSDImporter(model, renderPanel, this);
|
||||
this.operationManager = new LayerOperationManager(model);
|
||||
|
||||
initComponents();
|
||||
reloadFromModel();
|
||||
generateAllThumbnails();
|
||||
}
|
||||
|
||||
private void setupModernLookAndFeel() {
|
||||
setBackground(BACKGROUND_COLOR);
|
||||
setBorder(new EmptyBorder(10, 10, 10, 10));
|
||||
|
||||
// 设置现代化UI默认值
|
||||
UIManager.put("List.background", SURFACE_COLOR);
|
||||
UIManager.put("List.foreground", TEXT_COLOR);
|
||||
UIManager.put("List.selectionBackground", ACCENT_COLOR);
|
||||
UIManager.put("List.selectionForeground", Color.WHITE);
|
||||
UIManager.put("ScrollPane.background", SURFACE_COLOR);
|
||||
UIManager.put("ScrollPane.border", BorderFactory.createLineBorder(BORDER_COLOR));
|
||||
UIManager.put("Slider.background", SURFACE_COLOR);
|
||||
UIManager.put("Slider.foreground", ACCENT_COLOR);
|
||||
}
|
||||
|
||||
// ============== 缩略图相关方法 ==============
|
||||
private void generateAllThumbnails() {
|
||||
if (model == null) return;
|
||||
|
||||
thumbnailManager.clearCache();
|
||||
for (int i = 0; i < listModel.getSize(); i++) {
|
||||
ModelPart part = listModel.get(i);
|
||||
thumbnailManager.generateThumbnail(part);
|
||||
}
|
||||
layerList.repaint();
|
||||
}
|
||||
|
||||
private void refreshSelectedThumbnail() {
|
||||
ModelPart selected = layerList.getSelectedValue();
|
||||
if (selected != null) {
|
||||
thumbnailManager.generateThumbnail(selected);
|
||||
layerList.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 现代化组件初始化 ==============
|
||||
private void initComponents() {
|
||||
setLayout(new BorderLayout(10, 10));
|
||||
listModel = new DefaultListModel<>();
|
||||
layerList = createModernList();
|
||||
|
||||
// 创建现代化布局
|
||||
createHeaderPanel();
|
||||
createCenterPanel();
|
||||
createControlPanel();
|
||||
}
|
||||
|
||||
private JList<ModelPart> createModernList() {
|
||||
JList<ModelPart> list = new JList<>(listModel);
|
||||
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
list.setBackground(SURFACE_COLOR);
|
||||
list.setForeground(TEXT_COLOR);
|
||||
list.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
|
||||
list.setFixedCellHeight(70); // 增加行高以显示缩略图
|
||||
|
||||
// 使用独立的渲染器
|
||||
LayerCellRenderer cellRenderer = new LayerCellRenderer(this, thumbnailManager);
|
||||
cellRenderer.attachMouseListener(list, listModel);
|
||||
list.setCellRenderer(cellRenderer);
|
||||
|
||||
// 使用独立的拖拽处理器
|
||||
list.setDragEnabled(true);
|
||||
list.setTransferHandler(new LayerReorderTransferHandler(this));
|
||||
list.setDropMode(DropMode.INSERT);
|
||||
|
||||
// 双击重命名
|
||||
list.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
if (e.getClickCount() == 2) {
|
||||
int idx = list.locationToIndex(e.getPoint());
|
||||
if (idx >= 0) {
|
||||
showRenameDialog(listModel.get(idx));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 选择变更监听器
|
||||
list.addListSelectionListener(e -> updateUIState());
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private void createHeaderPanel() {
|
||||
JPanel headerPanel = new JPanel(new BorderLayout());
|
||||
headerPanel.setBackground(BACKGROUND_COLOR);
|
||||
headerPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 10, 0));
|
||||
|
||||
JLabel titleLabel = new JLabel("图层管理");
|
||||
titleLabel.setForeground(TEXT_COLOR);
|
||||
titleLabel.setFont(titleLabel.getFont().deriveFont(Font.BOLD, 16f));
|
||||
|
||||
headerPanel.add(titleLabel, BorderLayout.WEST);
|
||||
add(headerPanel, BorderLayout.NORTH);
|
||||
}
|
||||
|
||||
private void createCenterPanel() {
|
||||
JScrollPane scrollPane = new JScrollPane(layerList);
|
||||
scrollPane.setBorder(createModernBorder("图层列表"));
|
||||
scrollPane.getViewport().setBackground(SURFACE_COLOR);
|
||||
|
||||
// 自定义滚动条
|
||||
JScrollBar verticalScrollBar = scrollPane.getVerticalScrollBar();
|
||||
//verticalScrollBar.setUI(new ModernScrollBarUI());
|
||||
|
||||
add(scrollPane, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
private void createControlPanel() {
|
||||
JPanel controlPanel = new JPanel(new BorderLayout(10, 10));
|
||||
controlPanel.setBackground(BACKGROUND_COLOR);
|
||||
|
||||
// 顶部按钮面板
|
||||
controlPanel.add(createButtonPanel(), BorderLayout.NORTH);
|
||||
// 底部设置面板
|
||||
controlPanel.add(createSettingsPanel(), BorderLayout.SOUTH);
|
||||
|
||||
add(controlPanel, BorderLayout.SOUTH);
|
||||
}
|
||||
|
||||
private JPanel createButtonPanel() {
|
||||
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 8, 8));
|
||||
buttonPanel.setBackground(BACKGROUND_COLOR);
|
||||
buttonPanel.setBorder(createModernBorder("操作"));
|
||||
|
||||
// 创建现代化按钮
|
||||
addButton = createIconButton("⊕", "添加图层", this::showAddMenu);
|
||||
removeButton = createIconButton("⊖", "删除选中图层", this::onRemoveLayer);
|
||||
upButton = createIconButton("↑", "上移图层", this::moveSelectedUp);
|
||||
downButton = createIconButton("↓", "下移图层", this::moveSelectedDown);
|
||||
bindTextureButton = createIconButton("📷", "绑定贴图", this::bindTextureToSelectedPart);
|
||||
|
||||
// 初始禁用状态
|
||||
removeButton.setEnabled(false);
|
||||
upButton.setEnabled(false);
|
||||
downButton.setEnabled(false);
|
||||
bindTextureButton.setEnabled(false);
|
||||
|
||||
buttonPanel.add(addButton);
|
||||
buttonPanel.add(removeButton);
|
||||
buttonPanel.add(upButton);
|
||||
buttonPanel.add(downButton);
|
||||
buttonPanel.add(bindTextureButton);
|
||||
|
||||
return buttonPanel;
|
||||
}
|
||||
|
||||
private JPanel createSettingsPanel() {
|
||||
JPanel settingsPanel = new JPanel(new BorderLayout(10, 5));
|
||||
settingsPanel.setBackground(BACKGROUND_COLOR);
|
||||
settingsPanel.setBorder(createModernBorder("图层设置"));
|
||||
|
||||
// 不透明度控制
|
||||
JPanel opacityPanel = new JPanel(new BorderLayout(8, 0));
|
||||
opacityPanel.setBackground(BACKGROUND_COLOR);
|
||||
|
||||
JLabel opacityLabel = new JLabel("不透明度:");
|
||||
opacityLabel.setForeground(TEXT_COLOR);
|
||||
|
||||
opacitySlider = createModernSlider();
|
||||
opacityValueLabel = new JLabel("100%");
|
||||
opacityValueLabel.setForeground(TEXT_COLOR);
|
||||
opacityValueLabel.setPreferredSize(new Dimension(40, 20));
|
||||
|
||||
opacitySlider.addChangeListener(e -> {
|
||||
if (ignoreSliderEvents) return;
|
||||
onOpacityChanged();
|
||||
});
|
||||
|
||||
opacityPanel.add(opacityLabel, BorderLayout.WEST);
|
||||
opacityPanel.add(opacitySlider, BorderLayout.CENTER);
|
||||
opacityPanel.add(opacityValueLabel, BorderLayout.EAST);
|
||||
|
||||
settingsPanel.add(opacityPanel, BorderLayout.CENTER);
|
||||
return settingsPanel;
|
||||
}
|
||||
|
||||
private JSlider createModernSlider() {
|
||||
JSlider slider = new JSlider(0, 100, 100);
|
||||
slider.setBackground(BACKGROUND_COLOR);
|
||||
slider.setForeground(ACCENT_COLOR);
|
||||
|
||||
// 自定义滑块UI
|
||||
//slider.setUI(new ModernSliderUI());
|
||||
|
||||
return slider;
|
||||
}
|
||||
|
||||
private ModernButton createIconButton(String icon, String tooltip, Runnable action) {
|
||||
ModernButton button = new ModernButton(icon);
|
||||
button.setToolTipText(tooltip);
|
||||
button.addActionListener(e -> action.run());
|
||||
return button;
|
||||
}
|
||||
|
||||
private void showAddMenu() {
|
||||
JPopupMenu addMenu = new ModernPopupMenu();
|
||||
|
||||
String[] menuItems = {
|
||||
"📄 创建空图层",
|
||||
"🖼️ 从文件创建图层",
|
||||
"🎨 创建透明图层",
|
||||
"---",
|
||||
"📂 从PSD文件导入"
|
||||
};
|
||||
|
||||
Runnable[] actions = {
|
||||
this::createEmptyPart,
|
||||
this::createPartWithTextureFromFile,
|
||||
this::createPartWithTransparentTexture,
|
||||
null,
|
||||
this::importPSDFile
|
||||
};
|
||||
|
||||
for (int i = 0; i < menuItems.length; i++) {
|
||||
if (menuItems[i].equals("---")) {
|
||||
addMenu.add(new JSeparator());
|
||||
} else {
|
||||
JMenuItem item = new ModernMenuItem(menuItems[i]);
|
||||
if (actions[i] != null) {
|
||||
int finalI = i;
|
||||
item.addActionListener(e -> actions[finalI].run());
|
||||
}
|
||||
addMenu.add(item);
|
||||
}
|
||||
}
|
||||
|
||||
addMenu.show(addButton, 0, addButton.getHeight());
|
||||
}
|
||||
|
||||
private void createEmptyPart() {
|
||||
String name = JOptionPane.showInputDialog(this, "新图层名称:", "新图层");
|
||||
if (name == null || name.trim().isEmpty()) return;
|
||||
|
||||
operationManager.addLayer(name);
|
||||
reloadFromModel();
|
||||
|
||||
// 选中新创建的部件
|
||||
ModelPart newPart = findPartByName(name);
|
||||
if (newPart != null) {
|
||||
selectPart(newPart);
|
||||
thumbnailManager.generateThumbnail(newPart);
|
||||
}
|
||||
}
|
||||
|
||||
private ModelPart findPartByName(String name) {
|
||||
if (model == null) return null;
|
||||
Map<String, ModelPart> partMap = model.getPartMap();
|
||||
return partMap != null ? partMap.get(name) : null;
|
||||
}
|
||||
|
||||
public Map<String, ModelPart> getModelPartMap() {
|
||||
if (model == null) return null;
|
||||
return model.getPartMap();
|
||||
}
|
||||
|
||||
// ============== 现代化对话框方法 ==============
|
||||
private void showRenameDialog(ModelPart part) {
|
||||
String newName = (String) JOptionPane.showInputDialog(
|
||||
this,
|
||||
"输入新名称:",
|
||||
"重命名图层",
|
||||
JOptionPane.PLAIN_MESSAGE,
|
||||
null,
|
||||
null,
|
||||
part.getName()
|
||||
);
|
||||
|
||||
if (newName != null && !newName.trim().isEmpty()) {
|
||||
renamePart(part, newName);
|
||||
reloadFromModel();
|
||||
refreshSelectedThumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
// ============== 原有业务方法(保持不变) ==============
|
||||
public void setModel(Model2D model) {
|
||||
this.model = model;
|
||||
this.psdImporter = new PSDImporter(model, renderPanel, this);
|
||||
this.operationManager = new LayerOperationManager(model);
|
||||
reloadFromModel();
|
||||
generateAllThumbnails();
|
||||
}
|
||||
|
||||
public void setRenderPanel(ModelRenderPanel panel) {
|
||||
this.renderPanel = panel;
|
||||
this.thumbnailManager = new ThumbnailManager(panel);
|
||||
this.psdImporter = new PSDImporter(model, panel, this);
|
||||
}
|
||||
|
||||
private void importPSDFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
chooser.setFileFilter(new javax.swing.filechooser.FileNameExtensionFilter("PSD文件", "psd"));
|
||||
int r = chooser.showOpenDialog(this);
|
||||
if (r == JFileChooser.APPROVE_OPTION) {
|
||||
psdImporter.importPSDFile(chooser.getSelectedFile());
|
||||
}
|
||||
}
|
||||
|
||||
private void updateUIState() {
|
||||
ModelPart sel = layerList.getSelectedValue();
|
||||
boolean hasSelection = sel != null;
|
||||
|
||||
if (hasSelection) {
|
||||
updateOpacitySlider(sel);
|
||||
}
|
||||
|
||||
removeButton.setEnabled(hasSelection);
|
||||
upButton.setEnabled(hasSelection);
|
||||
downButton.setEnabled(hasSelection);
|
||||
bindTextureButton.setEnabled(hasSelection);
|
||||
}
|
||||
|
||||
private void updateOpacitySlider(ModelPart part) {
|
||||
float opacity = extractOpacity(part);
|
||||
int value = Math.round(opacity * 100);
|
||||
|
||||
ignoreSliderEvents = true;
|
||||
try {
|
||||
opacitySlider.setValue(value);
|
||||
opacityValueLabel.setText(value + "%");
|
||||
} finally {
|
||||
ignoreSliderEvents = false;
|
||||
}
|
||||
}
|
||||
|
||||
private float extractOpacity(ModelPart part) {
|
||||
try {
|
||||
Method method = part.getClass().getMethod("getOpacity");
|
||||
Object value = method.invoke(part);
|
||||
if (value instanceof Float) return (Float) value;
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
Field field = part.getClass().getDeclaredField("opacity");
|
||||
field.setAccessible(true);
|
||||
Object value = field.get(part);
|
||||
if (value instanceof Float) return (Float) value;
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
private void onOpacityChanged() {
|
||||
ModelPart sel = layerList.getSelectedValue();
|
||||
if (sel == null) return;
|
||||
|
||||
int value = opacitySlider.getValue();
|
||||
opacityValueLabel.setText(value + "%");
|
||||
|
||||
setPartOpacity(sel, value / 100.0f);
|
||||
|
||||
if (model != null) model.markNeedsUpdate();
|
||||
layerList.repaint();
|
||||
refreshSelectedThumbnail();
|
||||
}
|
||||
|
||||
private void setPartOpacity(ModelPart part, float opacity) {
|
||||
try {
|
||||
Method method = part.getClass().getMethod("setOpacity", float.class);
|
||||
method.invoke(part, opacity);
|
||||
} catch (Exception e) {
|
||||
try {
|
||||
Field field = part.getClass().getDeclaredField("opacity");
|
||||
field.setAccessible(true);
|
||||
field.setFloat(part, opacity);
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
// 现代化边框
|
||||
private TitledBorder createModernBorder(String title) {
|
||||
TitledBorder border = BorderFactory.createTitledBorder(
|
||||
BorderFactory.createLineBorder(BORDER_COLOR, 1, true),
|
||||
title
|
||||
);
|
||||
border.setTitleColor(TEXT_COLOR);
|
||||
border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD, 12f));
|
||||
return border;
|
||||
}
|
||||
|
||||
public Texture createTextureFromBufferedImage(BufferedImage img, String texName) {
|
||||
BufferedImage flippedImage = flipImageVertically(img);
|
||||
return Texture.createFromBufferedImage(texName, flippedImage);
|
||||
}
|
||||
|
||||
private BufferedImage flipImageVertically(BufferedImage img) {
|
||||
int width = img.getWidth();
|
||||
int height = img.getHeight();
|
||||
BufferedImage flipped = new BufferedImage(width, height, img.getType());
|
||||
Graphics2D g = flipped.createGraphics();
|
||||
g.drawImage(img, 0, height, width, -height, null);
|
||||
g.dispose();
|
||||
return flipped;
|
||||
}
|
||||
|
||||
public void refreshCurrentThumbnail() {
|
||||
refreshSelectedThumbnail();
|
||||
}
|
||||
|
||||
// 现代化按钮类
|
||||
private class ModernButton extends JButton {
|
||||
public ModernButton(String text) {
|
||||
super(text);
|
||||
setupModernStyle();
|
||||
}
|
||||
|
||||
private void setupModernStyle() {
|
||||
setBackground(SURFACE_COLOR);
|
||||
setForeground(TEXT_COLOR);
|
||||
setBorder(BorderFactory.createCompoundBorder(
|
||||
BorderFactory.createLineBorder(BORDER_COLOR, 1, true),
|
||||
BorderFactory.createEmptyBorder(8, 12, 8, 12)
|
||||
));
|
||||
setFocusPainted(false);
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
if (isEnabled()) {
|
||||
setBackground(ACCENT_COLOR);
|
||||
setForeground(Color.WHITE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
setBackground(SURFACE_COLOR);
|
||||
setForeground(TEXT_COLOR);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 现代化菜单项类
|
||||
private class ModernMenuItem extends JMenuItem {
|
||||
public ModernMenuItem(String text) {
|
||||
super(text);
|
||||
setBackground(SURFACE_COLOR);
|
||||
setForeground(TEXT_COLOR);
|
||||
setBorder(BorderFactory.createEmptyBorder(8, 12, 8, 12));
|
||||
|
||||
addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseEntered(MouseEvent e) {
|
||||
setBackground(ACCENT_COLOR);
|
||||
setForeground(Color.WHITE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
setBackground(SURFACE_COLOR);
|
||||
setForeground(TEXT_COLOR);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 现代化弹出菜单
|
||||
private class ModernPopupMenu extends JPopupMenu {
|
||||
public ModernPopupMenu() {
|
||||
setBackground(SURFACE_COLOR);
|
||||
setBorder(BorderFactory.createLineBorder(BORDER_COLOR));
|
||||
}
|
||||
}
|
||||
|
||||
// 其余原有方法保持不变...
|
||||
// (reloadFromModel, performVisualReorder, bindTextureToSelectedPart等)
|
||||
|
||||
public void reloadFromModel() {
|
||||
ModelPart selected = layerList.getSelectedValue();
|
||||
|
||||
listModel.clear();
|
||||
if (model == null) return;
|
||||
try {
|
||||
List<ModelPart> parts = model.getParts();
|
||||
if (parts != null) {
|
||||
for (int i = parts.size() - 1; i >= 0; i--) {
|
||||
listModel.addElement(parts.get(i));
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
|
||||
if (selected != null) {
|
||||
for (int i = 0; i < listModel.getSize(); i++) {
|
||||
if (listModel.get(i) == selected) {
|
||||
layerList.setSelectedIndex(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void performVisualReorder(int visualFrom, int visualTo) {
|
||||
if (model == null) return;
|
||||
try {
|
||||
int size = listModel.getSize();
|
||||
if (visualFrom < 0 || visualFrom >= size) return;
|
||||
if (visualTo < 0) visualTo = 0;
|
||||
if (visualTo > size - 1) visualTo = size - 1;
|
||||
|
||||
ModelPart moved = listModel.get(visualFrom);
|
||||
if (!isDragging) {
|
||||
isDragging = true;
|
||||
draggedPart = moved;
|
||||
dragStartPosition = new Vector2f(moved.getPosition());
|
||||
}
|
||||
|
||||
List<ModelPart> visual = new ArrayList<>(size);
|
||||
for (int i = 0; i < size; i++) visual.add(listModel.get(i));
|
||||
moved = visual.remove(visualFrom);
|
||||
visual.add(visualTo, moved);
|
||||
|
||||
ignoreSliderEvents = true;
|
||||
try {
|
||||
listModel.clear();
|
||||
for (ModelPart p : visual) listModel.addElement(p);
|
||||
} finally {
|
||||
ignoreSliderEvents = false;
|
||||
}
|
||||
|
||||
operationManager.moveLayer(visual);
|
||||
selectPart(moved);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void selectPart(ModelPart part) {
|
||||
if (part == null) return;
|
||||
for (int i = 0; i < listModel.getSize(); i++) {
|
||||
if (listModel.get(i) == part) {
|
||||
layerList.setSelectedIndex(i);
|
||||
layerList.ensureIndexIsVisible(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void renamePart(ModelPart part, String newName) {
|
||||
if (part == null) return;
|
||||
part.setName(newName);
|
||||
}
|
||||
|
||||
public Model2D getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
private void bindTextureToSelectedPart() {
|
||||
ModelPart sel = layerList.getSelectedValue();
|
||||
if (sel == null) return;
|
||||
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
int r = chooser.showOpenDialog(this);
|
||||
if (r != JFileChooser.APPROVE_OPTION) return;
|
||||
File f = chooser.getSelectedFile();
|
||||
try {
|
||||
BufferedImage img = null;
|
||||
try {
|
||||
img = ImageIO.read(f);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
Mesh2D targetMesh = null;
|
||||
try {
|
||||
List<Mesh2D> list = sel.getMeshes();
|
||||
if (!list.isEmpty() && list.get(0) != null) {
|
||||
targetMesh = list.get(0);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (targetMesh == null) {
|
||||
if (img == null) {
|
||||
img = new BufferedImage(128, 128, BufferedImage.TYPE_INT_ARGB);
|
||||
}
|
||||
targetMesh = MeshTextureUtil.createQuadForImage(img, sel.getName() + "_mesh");
|
||||
try {
|
||||
sel.addMesh(targetMesh);
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
final Mesh2D meshToBind = targetMesh;
|
||||
final String filePath = f.getAbsolutePath();
|
||||
final String texName = sel.getName() + "_tex";
|
||||
|
||||
if (renderPanel != null) {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
Texture texture = Texture.createFromFile(texName, filePath);
|
||||
meshToBind.setTexture(texture);
|
||||
model.addTexture(texture);
|
||||
model.markNeedsUpdate();
|
||||
} catch (Throwable ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (img == null) img = ImageIO.read(f);
|
||||
Texture mem = MeshTextureUtil.tryCreateTextureFromImageMemory(img, texName);
|
||||
if (mem != null) {
|
||||
meshToBind.setTexture(mem);
|
||||
model.addTexture(mem);
|
||||
model.markNeedsUpdate();
|
||||
} else {
|
||||
System.err.println("无法在无 GL 上下文中创建纹理: " + filePath);
|
||||
}
|
||||
}
|
||||
|
||||
reloadFromModel();
|
||||
selectPart(sel);
|
||||
refreshSelectedThumbnail();
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "绑定贴图失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private void onRemoveLayer() {
|
||||
ModelPart sel = layerList.getSelectedValue();
|
||||
if (sel == null) return;
|
||||
int r = JOptionPane.showConfirmDialog(this, "确认删除图层:" + sel.getName() + " ?", "确认删除", JOptionPane.YES_NO_OPTION);
|
||||
if (r != JOptionPane.YES_OPTION) return;
|
||||
|
||||
try {
|
||||
operationManager.removeLayer(sel);
|
||||
thumbnailManager.removeThumbnail(sel);
|
||||
reloadFromModel();
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "删除失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
private void moveSelectedUp() {
|
||||
int idx = layerList.getSelectedIndex();
|
||||
if (idx <= 0) return;
|
||||
performVisualReorder(idx, idx - 1);
|
||||
}
|
||||
|
||||
private void moveSelectedDown() {
|
||||
int idx = layerList.getSelectedIndex();
|
||||
if (idx < 0 || idx >= listModel.getSize() - 1) return;
|
||||
performVisualReorder(idx, idx + 1);
|
||||
}
|
||||
|
||||
private void createPartWithTextureFromFile() {
|
||||
JFileChooser chooser = new JFileChooser();
|
||||
int r = chooser.showOpenDialog(this);
|
||||
if (r != JFileChooser.APPROVE_OPTION) return;
|
||||
File f = chooser.getSelectedFile();
|
||||
try {
|
||||
BufferedImage img = ImageIO.read(f);
|
||||
if (img == null) throw new IOException("无法读取图片:" + f.getAbsolutePath());
|
||||
String name = JOptionPane.showInputDialog(this, "新图层名称:", f.getName());
|
||||
if (name == null || name.trim().isEmpty()) name = f.getName();
|
||||
|
||||
// 创建部件与 Mesh
|
||||
ModelPart part = model.createPart(name);
|
||||
Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh");
|
||||
mesh.createDefaultSecondaryVertices();
|
||||
part.addMesh(mesh);
|
||||
|
||||
// 创建纹理
|
||||
if (renderPanel != null) {
|
||||
final String texName = name + "_tex";
|
||||
final String filePath = f.getAbsolutePath();
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
Texture texture = Texture.createFromFile(texName, filePath);
|
||||
List<Mesh2D> partMeshes = part.getMeshes();
|
||||
Mesh2D actualMesh = null;
|
||||
if (partMeshes != null && !partMeshes.isEmpty()) {
|
||||
actualMesh = partMeshes.get(partMeshes.size() - 1);
|
||||
}
|
||||
|
||||
if (actualMesh != null) {
|
||||
actualMesh.setTexture(texture);
|
||||
} else {
|
||||
mesh.setTexture(texture);
|
||||
}
|
||||
|
||||
model.addTexture(texture);
|
||||
model.markNeedsUpdate();
|
||||
} catch (Throwable ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Texture memTex = MeshTextureUtil.tryCreateTextureFromImageMemory(img, name + "_tex");
|
||||
if (memTex != null) {
|
||||
mesh.setTexture(memTex);
|
||||
model.addTexture(memTex);
|
||||
model.markNeedsUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
reloadFromModel();
|
||||
selectPart(part);
|
||||
thumbnailManager.generateThumbnail(part);
|
||||
} catch (Exception ex) {
|
||||
JOptionPane.showMessageDialog(this, "创建带贴图图层失败: " + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE);
|
||||
}
|
||||
}
|
||||
|
||||
public void endDragOperation() {
|
||||
if (isDragging && draggedPart != null && dragStartPosition != null) {
|
||||
Vector2f endPosition = draggedPart.getPosition();
|
||||
if (!endPosition.equals(dragStartPosition)) {
|
||||
recordDragOperation(draggedPart, dragStartPosition, endPosition);
|
||||
}
|
||||
isDragging = false;
|
||||
draggedPart = null;
|
||||
dragStartPosition = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void recordDragOperation(ModelPart part, Vector2f startPos, Vector2f endPos) {
|
||||
OperationHistoryManager manager = OperationHistoryManager.getInstance();
|
||||
if (manager != null) {
|
||||
manager.recordOperation("DRAG_PART", part, startPos, endPos);
|
||||
}
|
||||
}
|
||||
|
||||
private void createPartWithTransparentTexture() {
|
||||
String name = JOptionPane.showInputDialog(this, "新图层名称(透明):", "透明图层");
|
||||
if (name == null || name.trim().isEmpty()) return;
|
||||
int w = 128, h = 128;
|
||||
try {
|
||||
String wh = JOptionPane.showInputDialog(this, "输入尺寸(宽x高,例如 128x128)或留空使用 128x128:", "128x128");
|
||||
if (wh != null && wh.contains("x")) {
|
||||
String[] sp = wh.split("x");
|
||||
w = Math.max(1, Integer.parseInt(sp[0].trim()));
|
||||
h = Math.max(1, Integer.parseInt(sp[1].trim()));
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
|
||||
ModelPart part = model.createPart(name);
|
||||
Mesh2D mesh = MeshTextureUtil.createQuadForImage(img, name + "_mesh");
|
||||
part.addMesh(mesh);
|
||||
|
||||
Texture memTex = MeshTextureUtil.tryCreateTextureFromImageMemory(img, name + "_tex");
|
||||
if (memTex != null) {
|
||||
mesh.setTexture(memTex);
|
||||
model.addTexture(memTex);
|
||||
}
|
||||
|
||||
model.markNeedsUpdate();
|
||||
reloadFromModel();
|
||||
selectPart(part);
|
||||
thumbnailManager.generateThumbnail(part);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,675 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.awt.manager.*;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.PuppetDeformationTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.SelectionTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.Tool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.VertexDeformationTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.LiquifyTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager;
|
||||
import com.chuangzhou.vivid2D.render.model.util.tools.LiquifyTargetPartRander;
|
||||
import com.chuangzhou.vivid2D.render.model.util.tools.PuppetDeformationRander;
|
||||
import com.chuangzhou.vivid2D.render.model.util.tools.VertexDeformationRander;
|
||||
import com.chuangzhou.vivid2D.render.systems.Camera;
|
||||
import com.chuangzhou.vivid2D.test.TestModelGLPanel;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.Timer;
|
||||
import javax.swing.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* vivid2D 模型的 Java 渲染面板
|
||||
*
|
||||
* <p>该类提供了 vivid2D 模型在 Java 环境下的图形渲染功能,
|
||||
* 包含基本的 2D 图形绘制、模型显示和交互操作。</p>
|
||||
*
|
||||
* @author tzdwindows 7
|
||||
* @version 1.1
|
||||
* @see TestModelGLPanel
|
||||
* @since 2025-10-13
|
||||
*/
|
||||
public class ModelRenderPanel extends JPanel {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ModelRenderPanel.class);
|
||||
|
||||
private final GLContextManager glContextManager;
|
||||
private final MouseManagement mouseManagement;
|
||||
private final CameraManagement cameraManagement;
|
||||
private final WorldManagement worldManagement;
|
||||
private final AtomicReference<Model2D> modelRef = new AtomicReference<>();
|
||||
private final KeyboardManager keyboardManager;
|
||||
private final CopyOnWriteArrayList<ModelClickListener> clickListeners = new CopyOnWriteArrayList<>();
|
||||
private final StatusRecordManagement statusRecordManagement;
|
||||
private final RanderToolsManager randerToolsManager = RanderToolsManager.getInstance();
|
||||
public static final float BORDER_THICKNESS = 6.0f;
|
||||
public static final float CORNER_SIZE = 12.0f;
|
||||
// ================== 摄像机控制相关字段 ==================
|
||||
private final Timer doubleClickTimer;
|
||||
private volatile long lastClickTime = 0;
|
||||
private static final int DOUBLE_CLICK_INTERVAL = 300; // 双击间隔(毫秒)
|
||||
private final ToolManagement toolManagement;
|
||||
|
||||
// ================== 摄像机控制方法 ==================
|
||||
|
||||
/**
|
||||
* 获取摄像机实例
|
||||
*/
|
||||
public Camera getCamera() {
|
||||
return ModelRender.getCamera();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置摄像机
|
||||
*/
|
||||
public void resetCamera() {
|
||||
glContextManager.executeInGLContext(ModelRender::resetCamera);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数:使用模型路径
|
||||
*/
|
||||
public ModelRenderPanel(String modelPath, int width, int height) {
|
||||
this.glContextManager = new GLContextManager(modelPath, width, height);
|
||||
this.statusRecordManagement = new StatusRecordManagement(this, OperationHistoryGlobal.getInstance());
|
||||
this.keyboardManager = new KeyboardManager(this);
|
||||
this.worldManagement = new WorldManagement(this, glContextManager);
|
||||
this.cameraManagement = new CameraManagement(this, glContextManager, worldManagement);
|
||||
this.mouseManagement = new MouseManagement(this, glContextManager, cameraManagement, keyboardManager);
|
||||
this.toolManagement = new ToolManagement(this, randerToolsManager);
|
||||
|
||||
// 注册所有工具
|
||||
toolManagement.registerTool(new PuppetDeformationTool(this),new PuppetDeformationRander());
|
||||
toolManagement.registerTool(new VertexDeformationTool(this),new VertexDeformationRander());
|
||||
toolManagement.registerTool(new LiquifyTool(this), new LiquifyTargetPartRander());
|
||||
initialize();
|
||||
|
||||
keyboardManager.initKeyboardShortcuts();
|
||||
|
||||
doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> {
|
||||
handleSingleClick();
|
||||
});
|
||||
doubleClickTimer.setRepeats(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数:使用已加载模型
|
||||
*/
|
||||
public ModelRenderPanel(Model2D model, int width, int height) {
|
||||
this.glContextManager = new GLContextManager(model, width, height);
|
||||
this.modelRef.set(model);
|
||||
this.statusRecordManagement = new StatusRecordManagement(this, OperationHistoryGlobal.getInstance());
|
||||
this.keyboardManager = new KeyboardManager(this);
|
||||
this.worldManagement = new WorldManagement(this, glContextManager);
|
||||
this.cameraManagement = new CameraManagement(this, glContextManager, worldManagement);
|
||||
this.mouseManagement = new MouseManagement(this, glContextManager, cameraManagement, keyboardManager);
|
||||
this.toolManagement = new ToolManagement(this, randerToolsManager);
|
||||
|
||||
// 注册所有工具
|
||||
toolManagement.registerTool(new PuppetDeformationTool(this),new PuppetDeformationRander());
|
||||
toolManagement.registerTool(new VertexDeformationTool(this),new VertexDeformationRander());
|
||||
toolManagement.registerTool(new LiquifyTool(this), new LiquifyTargetPartRander());
|
||||
|
||||
initialize();
|
||||
keyboardManager.initKeyboardShortcuts();
|
||||
doubleClickTimer = new Timer(DOUBLE_CLICK_INTERVAL, e -> {
|
||||
// 单单击超时处理
|
||||
handleSingleClick();
|
||||
});
|
||||
doubleClickTimer.setRepeats(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理双击事件
|
||||
*/
|
||||
private void handleDoubleClick(MouseEvent e) {
|
||||
float[] modelCoords = worldManagement.screenToModelCoordinates(e.getX(), e.getY());
|
||||
|
||||
// 如果有激活的工具,优先交给工具处理
|
||||
if (toolManagement.hasActiveTool() && modelCoords != null) {
|
||||
glContextManager.executeInGLContext(() -> {
|
||||
toolManagement.handleMouseDoubleClicked(e, modelCoords[0], modelCoords[1]);
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单单击事件
|
||||
*/
|
||||
private void handleSingleClick() {
|
||||
// 单单击逻辑已迁移到各个工具中
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加模型点击监听器
|
||||
*/
|
||||
public void addModelClickListener(ModelClickListener listener) {
|
||||
clickListeners.add(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除模型点击监听器
|
||||
*/
|
||||
public void removeModelClickListener(ModelClickListener listener) {
|
||||
clickListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的模型部件
|
||||
* @return 选中的模型部件列表
|
||||
*/
|
||||
public List<ModelPart> getSelectedParts() {
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
if (currentTool instanceof SelectionTool) {
|
||||
return ((SelectionTool) currentTool).getSelectedParts();
|
||||
}
|
||||
return java.util.Collections.emptyList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的网格
|
||||
*/
|
||||
public Mesh2D getSelectedMesh() {
|
||||
// 委托给工具管理系统的当前工具
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
if (currentTool instanceof SelectionTool) {
|
||||
return ((SelectionTool) currentTool).getSelectedMesh();
|
||||
} else if (toolManagement.getDefaultTool() instanceof SelectionTool selectedMesh) {
|
||||
return selectedMesh.getSelectedMesh();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的所有网格
|
||||
*/
|
||||
public java.util.Set<Mesh2D> getSelectedMeshes() {
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
if (currentTool instanceof SelectionTool) {
|
||||
return ((SelectionTool) currentTool).getSelectedMeshes();
|
||||
} else if (toolManagement.getDefaultTool() instanceof SelectionTool selectedMeshes) {
|
||||
return selectedMeshes.getSelectedMeshes();
|
||||
}
|
||||
return java.util.Collections.emptySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空所有选中的网格
|
||||
*/
|
||||
public void clearSelectedMeshes() {
|
||||
glContextManager.executeInGLContext(() -> {
|
||||
// 委托给工具管理系统的当前工具
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
if (currentTool instanceof SelectionTool) {
|
||||
((SelectionTool) currentTool).clearSelectedMeshes();
|
||||
} else {
|
||||
toolManagement.switchToDefaultTool();
|
||||
}
|
||||
|
||||
logger.debug("清空所有选中网格");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 全选所有网格
|
||||
*/
|
||||
public void selectAllMeshes() {
|
||||
glContextManager.executeInGLContext(() -> {
|
||||
// 委托给工具管理系统的当前工具
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
if (currentTool instanceof SelectionTool) {
|
||||
((SelectionTool) currentTool).selectAllMeshes();
|
||||
logger.info("已全选网格");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中的部件
|
||||
*/
|
||||
public ModelPart getSelectedPart() {
|
||||
Mesh2D selectedMesh = getSelectedMesh();
|
||||
return selectedMesh != null ? findPartByMesh(selectedMesh) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取鼠标悬停的网格
|
||||
*/
|
||||
public Mesh2D getHoveredMesh() {
|
||||
// 委托给工具管理系统的当前工具
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
if (currentTool instanceof SelectionTool) {
|
||||
return ((SelectionTool) currentTool).getHoveredMesh();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取液化工具实例
|
||||
*/
|
||||
public LiquifyTool getLiquifyTool() {
|
||||
Tool tool = toolManagement.getTool("液化工具");
|
||||
return tool instanceof LiquifyTool ? (LiquifyTool) tool : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化画笔大小
|
||||
*/
|
||||
public void setLiquifyBrushSize(float size) {
|
||||
LiquifyTool liquifyTool = getLiquifyTool();
|
||||
if (liquifyTool != null) {
|
||||
liquifyTool.setLiquifyBrushSize(size);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化画笔强度
|
||||
*/
|
||||
public void setLiquifyBrushStrength(float strength) {
|
||||
LiquifyTool liquifyTool = getLiquifyTool();
|
||||
if (liquifyTool != null) {
|
||||
liquifyTool.setLiquifyBrushStrength(strength);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化模式
|
||||
*/
|
||||
public void setLiquifyMode(ModelPart.LiquifyMode mode) {
|
||||
LiquifyTool liquifyTool = getLiquifyTool();
|
||||
if (liquifyTool != null) {
|
||||
liquifyTool.setLiquifyMode(mode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前液化状态
|
||||
*/
|
||||
public boolean isInLiquifyMode() {
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
return currentTool instanceof LiquifyTool;
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setLayout(new BorderLayout());
|
||||
setPreferredSize(new Dimension(glContextManager.getWidth(), glContextManager.getHeight()));
|
||||
|
||||
// 添加鼠标监听器
|
||||
mouseManagement.addMouseListeners();
|
||||
|
||||
// 创建渲染线程
|
||||
glContextManager.startRendering();
|
||||
|
||||
this.addComponentListener(new ComponentAdapter() {
|
||||
@Override
|
||||
public void componentResized(ComponentEvent e) {
|
||||
int w = getWidth();
|
||||
int h = getHeight();
|
||||
if (w <= 0 || h <= 0) return;
|
||||
if (w == glContextManager.getWidth() && h == glContextManager.getHeight()) return;
|
||||
resize(w, h);
|
||||
}
|
||||
});
|
||||
|
||||
glContextManager.setRepaintCallback(this::repaint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标按下事件
|
||||
*/
|
||||
public void handleMousePressed(MouseEvent e) {
|
||||
if (!glContextManager.isContextInitialized()) return;
|
||||
final int screenX = e.getX();
|
||||
final int screenY = e.getY();
|
||||
requestFocusInWindow();
|
||||
|
||||
// 首先处理中键拖拽(摄像机控制),在任何模式下都可用
|
||||
if (SwingUtilities.isMiddleMouseButton(e)) {
|
||||
glContextManager.setCameraDragging(true);
|
||||
cameraManagement.setLastCameraDragX(screenX);
|
||||
cameraManagement.setLastCameraDragY(screenY);
|
||||
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
|
||||
return;
|
||||
}
|
||||
|
||||
float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY);
|
||||
|
||||
// 如果有激活的工具,优先交给工具处理
|
||||
if (toolManagement.hasActiveTool() && modelCoords != null) {
|
||||
glContextManager.executeInGLContext(() -> toolManagement.handleMousePressed(e, modelCoords[0], modelCoords[1]));
|
||||
}
|
||||
}
|
||||
|
||||
public ToolManagement getToolManagement() {
|
||||
return toolManagement;
|
||||
}
|
||||
|
||||
public void switchTool(String toolName) {
|
||||
glContextManager.executeInGLContext(() -> toolManagement.switchTool(toolName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到液化工具
|
||||
*/
|
||||
public void switchToLiquifyTool() {
|
||||
switchTool("液化工具");
|
||||
}
|
||||
|
||||
public Tool getCurrentTool() {
|
||||
return toolManagement.getCurrentTool();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标拖拽事件
|
||||
*/
|
||||
public void handleMouseDragged(MouseEvent e) {
|
||||
if (glContextManager.isCameraDragging()) {
|
||||
final int screenX = e.getX();
|
||||
final int screenY = e.getY();
|
||||
// 计算鼠标移动距离
|
||||
final int deltaX = screenX - cameraManagement.getLastCameraDragX();
|
||||
final int deltaY = screenY - cameraManagement.getLastCameraDragY();
|
||||
|
||||
// 更新最后拖拽位置
|
||||
cameraManagement.setLastCameraDragX(screenX);
|
||||
cameraManagement.setLastCameraDragY(screenY);
|
||||
|
||||
// 确保在 GL 上下文线程中执行摄像机移动
|
||||
glContextManager.executeInGLContext(() -> {
|
||||
try {
|
||||
Camera camera = ModelRender.getCamera();
|
||||
float zoom = camera.getZoom();
|
||||
|
||||
float worldDeltaX = -deltaX / zoom;
|
||||
float worldDeltaY = -deltaY / zoom;
|
||||
|
||||
// 应用摄像机移动
|
||||
camera.move(worldDeltaX, worldDeltaY);
|
||||
} catch (Exception ex) {
|
||||
logger.error("处理摄像机拖拽时出错", ex);
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final float[][] modelCoords = {worldManagement.screenToModelCoordinates(e.getX(), e.getY())};
|
||||
|
||||
// 如果有激活的工具,优先交给工具处理
|
||||
if (toolManagement.hasActiveTool() && modelCoords[0] != null) {
|
||||
glContextManager.executeInGLContext(() -> toolManagement.handleMouseDragged(e, modelCoords[0][0], modelCoords[0][1]));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标释放事件
|
||||
*/
|
||||
public void handleMouseReleased(MouseEvent e) {
|
||||
// 首先处理摄像机拖拽释放
|
||||
if (glContextManager.isCameraDragging() && SwingUtilities.isMiddleMouseButton(e)) {
|
||||
glContextManager.setCameraDragging(false);
|
||||
// 恢复悬停状态的光标
|
||||
updateCursorForHoverState();
|
||||
return;
|
||||
}
|
||||
|
||||
float[] modelCoords = worldManagement.screenToModelCoordinates(e.getX(), e.getY());
|
||||
|
||||
// 如果有激活的工具,优先交给工具处理
|
||||
if (toolManagement.hasActiveTool() && modelCoords != null) {
|
||||
toolManagement.handleMouseReleased(e, modelCoords[0], modelCoords[1]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标点击事件
|
||||
*/
|
||||
public void handleMouseClick(MouseEvent e) {
|
||||
if (!glContextManager.isContextInitialized()) return;
|
||||
|
||||
final int screenX = e.getX();
|
||||
final int screenY = e.getY();
|
||||
|
||||
long currentTime = System.currentTimeMillis();
|
||||
boolean isDoubleClick = (currentTime - lastClickTime) < DOUBLE_CLICK_INTERVAL;
|
||||
lastClickTime = currentTime;
|
||||
|
||||
if (isDoubleClick) {
|
||||
// 取消单单击计时器
|
||||
doubleClickTimer.stop();
|
||||
handleDoubleClick(e);
|
||||
} else {
|
||||
float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY);
|
||||
|
||||
// 如果有激活的工具,优先交给工具处理
|
||||
if (toolManagement.hasActiveTool() && modelCoords != null) {
|
||||
toolManagement.handleMouseClicked(e, modelCoords[0], modelCoords[1]);
|
||||
return;
|
||||
}
|
||||
|
||||
glContextManager.executeInGLContext(() -> {
|
||||
try {
|
||||
if (modelCoords == null) return;
|
||||
|
||||
float modelX = modelCoords[0];
|
||||
float modelY = modelCoords[1];
|
||||
|
||||
logger.debug("点击位置:({}, {})", modelX, modelY);
|
||||
|
||||
// 触发点击事件
|
||||
for (ModelClickListener listener : clickListeners) {
|
||||
try {
|
||||
listener.onModelClicked(null, modelX, modelY, screenX, screenY);
|
||||
} catch (Exception ex) {
|
||||
logger.error("点击事件监听器执行出错", ex);
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
logger.error("处理鼠标点击时出错", ex);
|
||||
}
|
||||
});
|
||||
doubleClickTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标移动事件
|
||||
*/
|
||||
public void handleMouseMove(MouseEvent e) {
|
||||
if (!glContextManager.isContextInitialized()) return;
|
||||
|
||||
final int screenX = e.getX();
|
||||
final int screenY = e.getY();
|
||||
|
||||
if (glContextManager.isCameraDragging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
float[] modelCoords = worldManagement.screenToModelCoordinates(screenX, screenY);
|
||||
|
||||
// 如果有激活的工具,优先交给工具处理
|
||||
if (toolManagement.hasActiveTool() && modelCoords != null) {
|
||||
toolManagement.handleMouseMoved(e, modelCoords[0], modelCoords[1]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据悬停状态更新光标(无坐标版本,用于鼠标释放后)
|
||||
*/
|
||||
private void updateCursorForHoverState() {
|
||||
Point mousePos = getMousePosition();
|
||||
if (mousePos != null) {
|
||||
float[] modelCoords = worldManagement.screenToModelCoordinates(mousePos.x, mousePos.y);
|
||||
if (modelCoords != null) {
|
||||
// 委托给工具管理系统的当前工具
|
||||
Tool currentTool = toolManagement.getCurrentTool();
|
||||
if (currentTool != null) {
|
||||
setCursor(currentTool.getToolCursor());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 鼠标不在面板内,恢复默认光标
|
||||
setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void paintComponent(Graphics g) {
|
||||
super.paintComponent(g);
|
||||
|
||||
Graphics2D g2d = (Graphics2D) g.create();
|
||||
try {
|
||||
BufferedImage imgToDraw = glContextManager.getCurrentFrame()
|
||||
!= null ? glContextManager.getCurrentFrame() : glContextManager.getLastFrame();
|
||||
int panelW = getWidth();
|
||||
int panelH = getHeight();
|
||||
if (imgToDraw != null) {
|
||||
g2d.drawImage(imgToDraw, 0, 0, panelW, panelH, null);
|
||||
} else {
|
||||
g2d.setColor(Color.DARK_GRAY);
|
||||
g2d.fillRect(0, 0, panelW, panelH);
|
||||
}
|
||||
if (modelRef.get() == null) {
|
||||
g2d.setColor(new Color(255, 255, 0, 200));
|
||||
g2d.drawString("模型未加载", 10, 20);
|
||||
}
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置模型
|
||||
*/
|
||||
public void setModel(Model2D model) {
|
||||
glContextManager.executeInGLContext(() -> {
|
||||
modelRef.set(model);
|
||||
logger.info("模型已更新");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前渲染的模型
|
||||
*/
|
||||
public Model2D getModel() {
|
||||
return modelRef.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新设置面板大小
|
||||
* <p>
|
||||
* 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲,
|
||||
* 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。
|
||||
*/
|
||||
public void resize(int newWidth, int newHeight) {
|
||||
// 更新 Swing 尺寸
|
||||
setPreferredSize(new Dimension(newWidth, newHeight));
|
||||
revalidate();
|
||||
glContextManager.resize(newWidth, newHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局操作历史管理器
|
||||
*/
|
||||
public StatusRecordManagement getStatusRecordManagement() {
|
||||
return statusRecordManagement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 GL 上下文管理器
|
||||
*/
|
||||
public GLContextManager getGlContextManager() {
|
||||
return glContextManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取鼠标管理器
|
||||
*/
|
||||
public MouseManagement getMouseManagement() {
|
||||
return mouseManagement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相机管理器
|
||||
*/
|
||||
public CameraManagement getCameraManagement() {
|
||||
return cameraManagement;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取键盘管理器
|
||||
*/
|
||||
public KeyboardManager getKeyboardManager() {
|
||||
return keyboardManager;
|
||||
}
|
||||
|
||||
// ================== 保留的辅助方法 ==================
|
||||
|
||||
/**
|
||||
* 通过网格查找对应的 ModelPart
|
||||
*/
|
||||
public ModelPart findPartByMesh(Mesh2D mesh) {
|
||||
Model2D model = modelRef.get();
|
||||
if (model == null) return null;
|
||||
for (ModelPart part : model.getParts()) {
|
||||
ModelPart found = findPartByMeshRecursive(part, mesh);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归查找包含指定网格的部件
|
||||
*/
|
||||
private ModelPart findPartByMeshRecursive(ModelPart part, Mesh2D targetMesh) {
|
||||
if (part == null || targetMesh == null) return null;
|
||||
|
||||
// 检查当前部件的网格
|
||||
for (Mesh2D mesh : part.getMeshes()) {
|
||||
if (mesh == targetMesh) {
|
||||
return part;
|
||||
}
|
||||
}
|
||||
|
||||
// 递归检查子部件
|
||||
for (ModelPart child : part.getChildren()) {
|
||||
ModelPart found = findPartByMeshRecursive(child, targetMesh);
|
||||
if (found != null) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public enum DragMode {
|
||||
NONE, // 无拖拽
|
||||
MOVE, // 移动部件
|
||||
RESIZE_LEFT, // 调整左边
|
||||
RESIZE_RIGHT, // 调整右边
|
||||
RESIZE_TOP, // 调整上边
|
||||
RESIZE_BOTTOM, // 调整下边
|
||||
RESIZE_TOP_LEFT, // 调整左上角
|
||||
RESIZE_TOP_RIGHT, // 调整右上角
|
||||
RESIZE_BOTTOM_LEFT, // 调整左下角
|
||||
RESIZE_BOTTOM_RIGHT, // 调整右下角
|
||||
ROTATE, // 新增:旋转
|
||||
MOVE_PIVOT, // 新增:移动中心点
|
||||
MOVE_SECONDARY_VERTEX, // 新增:移动二级顶点
|
||||
MOVE_PUPPET_PIN // 新增:移动 puppetPin
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,716 @@
|
||||
package com.chuangzhou.vivid2D.render.awt;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelEvent;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.DocumentEvent;
|
||||
import javax.swing.event.DocumentListener;
|
||||
import java.awt.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author tzdwindows 7
|
||||
*/
|
||||
public class TransformPanel extends JPanel implements ModelEvent {
|
||||
private final ModelRenderPanel renderPanel;
|
||||
private final List<ModelPart> selectedParts = new ArrayList<>();
|
||||
private boolean isMultiSelection = false;
|
||||
|
||||
// 位置控制
|
||||
private JTextField positionXField;
|
||||
private JTextField positionYField;
|
||||
|
||||
// 旋转控制
|
||||
private JTextField rotationField;
|
||||
|
||||
// 缩放控制
|
||||
private JTextField scaleXField;
|
||||
private JTextField scaleYField;
|
||||
|
||||
// 中心点控制
|
||||
private JTextField pivotXField;
|
||||
private JTextField pivotYField;
|
||||
|
||||
// 按钮
|
||||
private JButton flipXButton;
|
||||
private JButton flipYButton;
|
||||
private JButton rotate90CWButton;
|
||||
private JButton rotate90CCWButton;
|
||||
private JButton resetScaleButton;
|
||||
|
||||
private boolean updatingUI = false; // 防止UI更新时触发事件
|
||||
private javax.swing.Timer transformTimer; // 用于延迟处理变换输入
|
||||
|
||||
private final OperationHistoryGlobal operationHistory;
|
||||
|
||||
public TransformPanel(ModelRenderPanel renderPanel) {
|
||||
this.renderPanel = renderPanel;
|
||||
this.operationHistory = OperationHistoryGlobal.getInstance();
|
||||
initComponents();
|
||||
setupListeners();
|
||||
updateUIState();
|
||||
}
|
||||
|
||||
private void initComponents() {
|
||||
setLayout(new GridBagLayout());
|
||||
GridBagConstraints gbc = new GridBagConstraints();
|
||||
gbc.insets = new Insets(3, 5, 3, 5);
|
||||
gbc.fill = GridBagConstraints.HORIZONTAL;
|
||||
|
||||
int row = 0;
|
||||
|
||||
// 位置控制
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("位置 X:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = row;
|
||||
positionXField = new JTextField("0.0");
|
||||
add(positionXField, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("Y:"), gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.gridy = row++;
|
||||
positionYField = new JTextField("0.0");
|
||||
add(positionYField, gbc);
|
||||
|
||||
// 旋转控制
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("旋转角度:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = row;
|
||||
rotationField = new JTextField("0.0");
|
||||
add(rotationField, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
gbc.gridwidth = 2;
|
||||
rotate90CWButton = new JButton("+90°");
|
||||
rotate90CWButton.setToolTipText("顺时针旋转90度");
|
||||
add(rotate90CWButton, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 4;
|
||||
rotate90CCWButton = new JButton("-90°");
|
||||
rotate90CCWButton.setToolTipText("逆时针旋转90度");
|
||||
add(rotate90CCWButton, gbc);
|
||||
|
||||
// 缩放控制
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 1;
|
||||
add(new JLabel("缩放 X:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = row;
|
||||
scaleXField = new JTextField("1.0");
|
||||
add(scaleXField, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("Y:"), gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.gridy = row;
|
||||
scaleYField = new JTextField("1.0");
|
||||
add(scaleYField, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 2;
|
||||
flipXButton = new JButton("水平翻转");
|
||||
add(flipXButton, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
gbc.gridwidth = 2;
|
||||
flipYButton = new JButton("垂直翻转");
|
||||
add(flipYButton, gbc);
|
||||
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 4;
|
||||
resetScaleButton = new JButton("重置缩放");
|
||||
resetScaleButton.setToolTipText("重置为1:1缩放");
|
||||
add(resetScaleButton, gbc);
|
||||
|
||||
// 中心点控制
|
||||
gbc.gridx = 0;
|
||||
gbc.gridy = ++row;
|
||||
gbc.gridwidth = 1;
|
||||
add(new JLabel("中心点 X:"), gbc);
|
||||
|
||||
gbc.gridx = 1;
|
||||
gbc.gridy = row;
|
||||
pivotXField = new JTextField("0.0");
|
||||
add(pivotXField, gbc);
|
||||
|
||||
gbc.gridx = 2;
|
||||
gbc.gridy = row;
|
||||
add(new JLabel("Y:"), gbc);
|
||||
|
||||
gbc.gridx = 3;
|
||||
gbc.gridy = row;
|
||||
pivotYField = new JTextField("0.0");
|
||||
add(pivotYField, gbc);
|
||||
|
||||
// Set border
|
||||
setBorder(BorderFactory.createTitledBorder("变换控制"));
|
||||
|
||||
// 初始化定时器,用于延迟处理变换输入
|
||||
transformTimer = new javax.swing.Timer(300, e -> applyTransformChanges());
|
||||
transformTimer.setRepeats(false); // 只执行一次
|
||||
}
|
||||
|
||||
private void setupListeners() {
|
||||
// 为所有文本框添加文档监听器
|
||||
DocumentListener documentListener = new DocumentListener() {
|
||||
@Override
|
||||
public void insertUpdate(DocumentEvent e) {
|
||||
scheduleTransformUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeUpdate(DocumentEvent e) {
|
||||
scheduleTransformUpdate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changedUpdate(DocumentEvent e) {
|
||||
scheduleTransformUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
positionXField.getDocument().addDocumentListener(documentListener);
|
||||
positionYField.getDocument().addDocumentListener(documentListener);
|
||||
rotationField.getDocument().addDocumentListener(documentListener);
|
||||
scaleXField.getDocument().addDocumentListener(documentListener);
|
||||
scaleYField.getDocument().addDocumentListener(documentListener);
|
||||
pivotXField.getDocument().addDocumentListener(documentListener);
|
||||
pivotYField.getDocument().addDocumentListener(documentListener);
|
||||
|
||||
// 添加焦点监听,当失去焦点时立即应用
|
||||
java.awt.event.FocusAdapter focusAdapter = new java.awt.event.FocusAdapter() {
|
||||
@Override
|
||||
public void focusLost(java.awt.event.FocusEvent e) {
|
||||
transformTimer.stop();
|
||||
applyTransformChanges();
|
||||
}
|
||||
};
|
||||
|
||||
positionXField.addFocusListener(focusAdapter);
|
||||
positionYField.addFocusListener(focusAdapter);
|
||||
rotationField.addFocusListener(focusAdapter);
|
||||
scaleXField.addFocusListener(focusAdapter);
|
||||
scaleYField.addFocusListener(focusAdapter);
|
||||
pivotXField.addFocusListener(focusAdapter);
|
||||
pivotYField.addFocusListener(focusAdapter);
|
||||
|
||||
// 添加回车键监听
|
||||
java.awt.event.ActionListener enterListener = e -> {
|
||||
transformTimer.stop();
|
||||
applyTransformChanges();
|
||||
};
|
||||
|
||||
positionXField.addActionListener(enterListener);
|
||||
positionYField.addActionListener(enterListener);
|
||||
rotationField.addActionListener(enterListener);
|
||||
scaleXField.addActionListener(enterListener);
|
||||
scaleYField.addActionListener(enterListener);
|
||||
pivotXField.addActionListener(enterListener);
|
||||
pivotYField.addActionListener(enterListener);
|
||||
|
||||
// 旋转按钮监听器修改(支持多选)
|
||||
rotate90CWButton.addActionListener(e -> {
|
||||
if (!selectedParts.isEmpty()) {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
Map<ModelPart, Float> oldRotations = new HashMap<>();
|
||||
Map<ModelPart, Float> newRotations = new HashMap<>();
|
||||
|
||||
for (ModelPart part : selectedParts) {
|
||||
float oldRotation = part.getRotation();
|
||||
oldRotations.put(part, oldRotation);
|
||||
|
||||
float currentRotation = (float) Math.toDegrees(oldRotation);
|
||||
float newRotation = normalizeAngle(currentRotation + 90.0f);
|
||||
part.setRotation((float) Math.toRadians(newRotation));
|
||||
|
||||
newRotations.put(part, part.getRotation());
|
||||
}
|
||||
|
||||
// 记录多选操作历史
|
||||
recordMultiPartOperation("ROTATION",
|
||||
new HashMap<>(oldRotations),
|
||||
new HashMap<>(newRotations));
|
||||
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
rotate90CCWButton.addActionListener(e -> {
|
||||
if (!selectedParts.isEmpty()) {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
Map<ModelPart, Float> oldRotations = new HashMap<>();
|
||||
Map<ModelPart, Float> newRotations = new HashMap<>();
|
||||
|
||||
for (ModelPart part : selectedParts) {
|
||||
float oldRotation = part.getRotation();
|
||||
oldRotations.put(part, oldRotation);
|
||||
|
||||
float currentRotation = (float) Math.toDegrees(oldRotation);
|
||||
float newRotation = normalizeAngle(currentRotation - 90.0f);
|
||||
part.setRotation((float) Math.toRadians(newRotation));
|
||||
|
||||
newRotations.put(part, part.getRotation());
|
||||
}
|
||||
|
||||
// 记录多选操作历史
|
||||
recordMultiPartOperation("ROTATION",
|
||||
new HashMap<>(oldRotations),
|
||||
new HashMap<>(newRotations));
|
||||
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 翻转按钮监听器修改(支持多选)
|
||||
flipXButton.addActionListener(e -> {
|
||||
if (!selectedParts.isEmpty()) {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
|
||||
Map<ModelPart, Vector2f> newScales = new HashMap<>();
|
||||
|
||||
for (ModelPart part : selectedParts) {
|
||||
Vector2f oldScale = new Vector2f(part.getScale());
|
||||
oldScales.put(part, oldScale);
|
||||
|
||||
float currentScaleX = part.getScaleX();
|
||||
float currentScaleY = part.getScaleY();
|
||||
part.setScale(currentScaleX * -1, currentScaleY);
|
||||
|
||||
newScales.put(part, new Vector2f(part.getScale()));
|
||||
}
|
||||
|
||||
// 记录多选操作历史
|
||||
recordMultiPartOperation("SCALE",
|
||||
new HashMap<>(oldScales),
|
||||
new HashMap<>(newScales));
|
||||
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
flipYButton.addActionListener(e -> {
|
||||
if (!selectedParts.isEmpty()) {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
|
||||
Map<ModelPart, Vector2f> newScales = new HashMap<>();
|
||||
|
||||
for (ModelPart part : selectedParts) {
|
||||
Vector2f oldScale = new Vector2f(part.getScale());
|
||||
oldScales.put(part, oldScale);
|
||||
|
||||
float currentScaleX = part.getScaleX();
|
||||
float currentScaleY = part.getScaleY();
|
||||
part.setScale(currentScaleX, currentScaleY * -1);
|
||||
|
||||
newScales.put(part, new Vector2f(part.getScale()));
|
||||
}
|
||||
|
||||
// 记录多选操作历史
|
||||
recordMultiPartOperation("SCALE",
|
||||
new HashMap<>(oldScales),
|
||||
new HashMap<>(newScales));
|
||||
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 重置缩放按钮监听器修改(支持多选)
|
||||
resetScaleButton.addActionListener(e -> {
|
||||
if (!selectedParts.isEmpty()) {
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
Map<ModelPart, Vector2f> oldScales = new HashMap<>();
|
||||
Map<ModelPart, Vector2f> newScales = new HashMap<>();
|
||||
|
||||
for (ModelPart part : selectedParts) {
|
||||
Vector2f oldScale = new Vector2f(part.getScale());
|
||||
oldScales.put(part, oldScale);
|
||||
|
||||
part.setScale(1.0f, 1.0f);
|
||||
|
||||
newScales.put(part, new Vector2f(part.getScale()));
|
||||
}
|
||||
|
||||
// 记录多选操作历史
|
||||
recordMultiPartOperation("SCALE",
|
||||
new HashMap<>(oldScales),
|
||||
new HashMap<>(newScales));
|
||||
|
||||
renderPanel.repaint();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录多部件操作历史
|
||||
*/
|
||||
private void recordMultiPartOperation(String operationType, Map<ModelPart, Object> oldValues, Map<ModelPart, Object> newValues) {
|
||||
if (operationHistory != null && !selectedParts.isEmpty()) {
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(new ArrayList<>(selectedParts));
|
||||
params.add(oldValues);
|
||||
params.add(newValues);
|
||||
operationHistory.recordOperation("MULTI_" + operationType, params.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量应用变换到所有选中部件
|
||||
*/
|
||||
private void applyTransformToAllParts(float posX, float posY, float rotationDegrees,
|
||||
float scaleX, float scaleY, float pivotX, float pivotY) {
|
||||
// 记录变换前的状态
|
||||
Map<ModelPart, Object> oldStates = new HashMap<>();
|
||||
Map<ModelPart, Object> newStates = new HashMap<>();
|
||||
|
||||
for (ModelPart part : selectedParts) {
|
||||
// 记录旧状态
|
||||
Object[] oldState = new Object[]{
|
||||
new Vector2f(part.getPosition()),
|
||||
part.getRotation(),
|
||||
new Vector2f(part.getScale()),
|
||||
new Vector2f(part.getPivot())
|
||||
};
|
||||
oldStates.put(part, oldState);
|
||||
|
||||
// 应用变换
|
||||
part.setPosition(posX, posY);
|
||||
part.setRotation((float) Math.toRadians(rotationDegrees));
|
||||
part.setScale(scaleX, scaleY);
|
||||
part.setPivot(pivotX, pivotY);
|
||||
|
||||
// 记录新状态
|
||||
Object[] newState = new Object[]{
|
||||
new Vector2f(part.getPosition()),
|
||||
part.getRotation(),
|
||||
new Vector2f(part.getScale()),
|
||||
new Vector2f(part.getPivot())
|
||||
};
|
||||
newStates.put(part, newState);
|
||||
}
|
||||
|
||||
// 记录批量操作历史
|
||||
recordMultiPartOperation("BATCH_TRANSFORM", oldStates, newStates);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事件监听器实现 - 当任何选中部件的属性变化时更新UI
|
||||
*/
|
||||
@Override
|
||||
public void trigger(String eventName, Object source) {
|
||||
if (!(source instanceof ModelPart) || !selectedParts.contains(source)) return;
|
||||
|
||||
SwingUtilities.invokeLater(() -> {
|
||||
// 如果是多选,只更新UI但不记录历史(避免循环触发)
|
||||
if (selectedParts.size() > 1) {
|
||||
updatingUI = true;
|
||||
updateUIForMultiSelection();
|
||||
updatingUI = false;
|
||||
} else if (selectedParts.size() == 1) {
|
||||
updatingUI = true;
|
||||
try {
|
||||
ModelPart part = (ModelPart) source;
|
||||
switch (eventName) {
|
||||
case "position":
|
||||
Vector2f position = part.getPosition();
|
||||
positionXField.setText(String.format("%.2f", position.x));
|
||||
positionYField.setText(String.format("%.2f", position.y));
|
||||
break;
|
||||
case "rotation":
|
||||
float currentRotation = (float) Math.toDegrees(part.getRotation());
|
||||
currentRotation = normalizeAngle(currentRotation);
|
||||
rotationField.setText(String.format("%.2f", currentRotation));
|
||||
break;
|
||||
case "scale":
|
||||
Vector2f scale = part.getScale();
|
||||
scaleXField.setText(String.format("%.2f", scale.x));
|
||||
scaleYField.setText(String.format("%.2f", scale.y));
|
||||
break;
|
||||
case "pivot":
|
||||
Vector2f pivot = part.getPivot();
|
||||
pivotXField.setText(String.format("%.2f", pivot.x));
|
||||
pivotYField.setText(String.format("%.2f", pivot.y));
|
||||
break;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
updatingUI = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 调度变换更新(延迟处理)
|
||||
*/
|
||||
private void scheduleTransformUpdate() {
|
||||
if (updatingUI || selectedParts.isEmpty()) return;
|
||||
transformTimer.stop();
|
||||
transformTimer.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 将角度标准化到0-360度范围内
|
||||
*/
|
||||
private float normalizeAngle(float degrees) {
|
||||
degrees = degrees % 360;
|
||||
if (degrees < 0) {
|
||||
degrees += 360;
|
||||
}
|
||||
return degrees;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用所有变换更改(支持多选)
|
||||
*/
|
||||
private void applyTransformChanges() {
|
||||
if (updatingUI || selectedParts.isEmpty()) return;
|
||||
|
||||
renderPanel.getGlContextManager().executeInGLContext(() -> {
|
||||
try {
|
||||
float posX = Float.parseFloat(positionXField.getText());
|
||||
float posY = Float.parseFloat(positionYField.getText());
|
||||
float rotationDegrees = Float.parseFloat(rotationField.getText());
|
||||
rotationDegrees = normalizeAngle(rotationDegrees);
|
||||
float scaleX = Float.parseFloat(scaleXField.getText());
|
||||
float scaleY = Float.parseFloat(scaleYField.getText());
|
||||
float pivotX = Float.parseFloat(pivotXField.getText());
|
||||
float pivotY = Float.parseFloat(pivotYField.getText());
|
||||
|
||||
// 批量应用到所有选中部件
|
||||
applyTransformToAllParts(posX, posY, rotationDegrees, scaleX, scaleY, pivotX, pivotY);
|
||||
|
||||
renderPanel.repaint();
|
||||
} catch (NumberFormatException ex) {
|
||||
// 输入无效时恢复之前的值
|
||||
SwingUtilities.invokeLater(this::updateUIFromSelectedParts);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从选中的部件更新UI(支持多选)
|
||||
*/
|
||||
private void updateUIFromSelectedParts() {
|
||||
if (selectedParts.isEmpty()) return;
|
||||
|
||||
updatingUI = true;
|
||||
try {
|
||||
if (selectedParts.size() == 1) {
|
||||
// 单选:显示具体值
|
||||
ModelPart part = selectedParts.get(0);
|
||||
updateUIFromSinglePart(part);
|
||||
isMultiSelection = false;
|
||||
} else {
|
||||
// 多选:显示特殊标识或平均值
|
||||
updateUIForMultiSelection();
|
||||
isMultiSelection = true;
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
updatingUI = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从单个部件更新UI
|
||||
*/
|
||||
private void updateUIFromSinglePart(ModelPart part) {
|
||||
// 更新位置
|
||||
Vector2f position = part.getPosition();
|
||||
positionXField.setText(String.format("%.2f", position.x));
|
||||
positionYField.setText(String.format("%.2f", position.y));
|
||||
|
||||
// 更新旋转
|
||||
float currentRotation = (float) Math.toDegrees(part.getRotation());
|
||||
currentRotation = normalizeAngle(currentRotation);
|
||||
rotationField.setText(String.format("%.2f", currentRotation));
|
||||
|
||||
// 更新缩放
|
||||
Vector2f scale = part.getScale();
|
||||
scaleXField.setText(String.format("%.2f", scale.x));
|
||||
scaleYField.setText(String.format("%.2f", scale.y));
|
||||
|
||||
// 更新中心点
|
||||
Vector2f pivot = part.getPivot();
|
||||
pivotXField.setText(String.format("%.2f", pivot.x));
|
||||
pivotYField.setText(String.format("%.2f", pivot.y));
|
||||
}
|
||||
|
||||
/**
|
||||
* 多选时的UI显示
|
||||
*/
|
||||
private void updateUIForMultiSelection() {
|
||||
// 多选时显示特殊值或平均值
|
||||
positionXField.setText("[多选]");
|
||||
positionYField.setText("[多选]");
|
||||
rotationField.setText("[多选]");
|
||||
scaleXField.setText("[多选]");
|
||||
scaleYField.setText("[多选]");
|
||||
pivotXField.setText("[多选]");
|
||||
pivotYField.setText("[多选]");
|
||||
|
||||
// 或者计算平均值(可选)
|
||||
// calculateAndDisplayAverageValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置选中的部件(支持多选)
|
||||
*/
|
||||
public void setSelectedParts(List<ModelPart> parts) {
|
||||
// 移除旧部件的事件监听
|
||||
for (ModelPart oldPart : selectedParts) {
|
||||
oldPart.removeEvent(this);
|
||||
}
|
||||
|
||||
this.selectedParts.clear();
|
||||
if (parts != null) {
|
||||
this.selectedParts.addAll(parts);
|
||||
|
||||
// 添加新部件的事件监听
|
||||
for (ModelPart newPart : selectedParts) {
|
||||
newPart.addEvent(this);
|
||||
}
|
||||
}
|
||||
|
||||
updateUIState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加选中部件
|
||||
*/
|
||||
public void addSelectedPart(ModelPart part) {
|
||||
if (part != null && !selectedParts.contains(part)) {
|
||||
selectedParts.add(part);
|
||||
part.addEvent(this);
|
||||
updateUIState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除选中部件
|
||||
*/
|
||||
public void removeSelectedPart(ModelPart part) {
|
||||
if (part != null && selectedParts.contains(part)) {
|
||||
selectedParts.remove(part);
|
||||
part.removeEvent(this);
|
||||
updateUIState();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空选中部件
|
||||
*/
|
||||
public void clearSelectedParts() {
|
||||
for (ModelPart part : selectedParts) {
|
||||
part.removeEvent(this);
|
||||
}
|
||||
selectedParts.clear();
|
||||
updateUIState();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中部件
|
||||
*/
|
||||
public ModelPart getSelectedPart() {
|
||||
return selectedParts.isEmpty() ? null : selectedParts.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有选中部件
|
||||
*/
|
||||
public List<ModelPart> getSelectedParts() {
|
||||
return new ArrayList<>(selectedParts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前选中部件数量
|
||||
*/
|
||||
public int getSelectedPartsCount() {
|
||||
return selectedParts.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否是多选状态
|
||||
*/
|
||||
public boolean isMultiSelection() {
|
||||
return isMultiSelection;
|
||||
}
|
||||
|
||||
private void updateUIState() {
|
||||
updatingUI = true;
|
||||
if (!selectedParts.isEmpty()) {
|
||||
updateUIFromSelectedParts();
|
||||
setControlsEnabled(true);
|
||||
} else {
|
||||
// 清空所有字段
|
||||
positionXField.setText("0.00");
|
||||
positionYField.setText("0.00");
|
||||
rotationField.setText("0.00");
|
||||
scaleXField.setText("1.00");
|
||||
scaleYField.setText("1.00");
|
||||
pivotXField.setText("0.00");
|
||||
pivotYField.setText("0.00");
|
||||
setControlsEnabled(false);
|
||||
isMultiSelection = false;
|
||||
}
|
||||
updatingUI = false;
|
||||
}
|
||||
|
||||
private void setControlsEnabled(boolean enabled) {
|
||||
positionXField.setEnabled(enabled);
|
||||
positionYField.setEnabled(enabled);
|
||||
rotationField.setEnabled(enabled);
|
||||
scaleXField.setEnabled(enabled);
|
||||
scaleYField.setEnabled(enabled);
|
||||
pivotXField.setEnabled(enabled);
|
||||
pivotYField.setEnabled(enabled);
|
||||
flipXButton.setEnabled(enabled);
|
||||
flipYButton.setEnabled(enabled);
|
||||
rotate90CWButton.setEnabled(enabled);
|
||||
rotate90CCWButton.setEnabled(enabled);
|
||||
resetScaleButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeNotify() {
|
||||
super.removeNotify();
|
||||
// 清理定时器资源和事件监听
|
||||
if (transformTimer != null) {
|
||||
transformTimer.stop();
|
||||
}
|
||||
// 移除所有部件的事件监听
|
||||
for (ModelPart part : selectedParts) {
|
||||
part.removeEvent(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.systems.Camera;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
public class CameraManagement {
|
||||
private final ModelRenderPanel modelRenderPanel;
|
||||
private final GLContextManager glContextManager;
|
||||
private final WorldManagement worldManagement;
|
||||
|
||||
private volatile int lastCameraDragX, lastCameraDragY;
|
||||
private final Vector2f rotationCenter = new Vector2f();
|
||||
|
||||
public static final float ZOOM_STEP = 1.15f; // 每格滚轮的指数因子(>1 放大)
|
||||
public static final float ZOOM_MIN = 0.1f;
|
||||
public static final float ZOOM_MAX = 8.0f;
|
||||
public static final float ROTATION_HANDLE_DISTANCE = 30.0f;
|
||||
|
||||
public CameraManagement(ModelRenderPanel modelRenderPanel, GLContextManager glContextManager, WorldManagement worldManagement){
|
||||
this.modelRenderPanel = modelRenderPanel;
|
||||
this.glContextManager = glContextManager;
|
||||
this.worldManagement = worldManagement;
|
||||
}
|
||||
|
||||
public void resizingApplications(int screenX, int screenY, int notches, boolean fine){
|
||||
glContextManager.executeInGLContext(() -> {
|
||||
Camera camera = ModelRender.getCamera();
|
||||
float oldZoom = camera.getZoom();
|
||||
float[] worldPosBefore = worldManagement.screenToModelCoordinates(screenX, screenY);
|
||||
if (worldPosBefore == null) return;
|
||||
double step = fine ? Math.pow(ZOOM_STEP, 0.25) : ZOOM_STEP;
|
||||
float newZoom = oldZoom;
|
||||
if (notches > 0) { // 缩小
|
||||
newZoom /= (float) Math.pow(step, notches);
|
||||
} else { // 放大
|
||||
newZoom *= (float) Math.pow(step, -notches);
|
||||
}
|
||||
newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newZoom));
|
||||
if (Math.abs(newZoom - oldZoom) < 1e-6f) {
|
||||
return;
|
||||
}
|
||||
camera.setZoom(newZoom);
|
||||
float[] worldPosAfter = worldManagement.screenToModelCoordinates(screenX, screenY);
|
||||
if (worldPosAfter == null) {
|
||||
camera.setZoom(oldZoom);
|
||||
return;
|
||||
}
|
||||
float panX = worldPosBefore[0] - worldPosAfter[0];
|
||||
float panY = worldPosBefore[1] - worldPosAfter[1];
|
||||
camera.move(panX, panY);
|
||||
glContextManager.setDisplayScale(newZoom);
|
||||
glContextManager.setTargetScale(newZoom);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前缩放因子(模型单位与屏幕像素的比例)
|
||||
*/
|
||||
public float calculateScaleFactor() {
|
||||
int panelWidth = modelRenderPanel.getWidth();
|
||||
int panelHeight = modelRenderPanel.getHeight();
|
||||
|
||||
if (panelWidth <= 0 || panelHeight <= 0 || glContextManager.getHeight() <= 0 || glContextManager.getHeight() <= 0) {
|
||||
return 1.0f;
|
||||
}
|
||||
|
||||
// 计算面板与离屏缓冲区的比例
|
||||
float scaleX = (float) panelWidth / glContextManager.getWidth();
|
||||
float scaleY = (float) panelHeight / glContextManager.getHeight();
|
||||
|
||||
// 基本面板缩放(保持与现有逻辑一致)
|
||||
float base = Math.min(scaleX, scaleY);
|
||||
|
||||
// 乘以平滑的 displayScale,使视觉上缩放与检测区域一致
|
||||
return base * glContextManager.displayScale;
|
||||
}
|
||||
|
||||
public int getLastCameraDragX() {
|
||||
return lastCameraDragX;
|
||||
}
|
||||
|
||||
public int getLastCameraDragY() {
|
||||
return lastCameraDragY;
|
||||
}
|
||||
|
||||
public void setLastCameraDragX(int lastCameraDragX) {
|
||||
this.lastCameraDragX = lastCameraDragX;
|
||||
}
|
||||
|
||||
public void setLastCameraDragY(int lastCameraDragY) {
|
||||
this.lastCameraDragY = lastCameraDragY;
|
||||
}
|
||||
|
||||
public Vector2f getRotationCenter() {
|
||||
return rotationCenter;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,593 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.systems.RenderSystem;
|
||||
import org.lwjgl.glfw.GLFW;
|
||||
import org.lwjgl.opengl.GL;
|
||||
import org.lwjgl.opengl.GL11;
|
||||
import org.lwjgl.system.MemoryUtil;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.IntBuffer;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
|
||||
public class GLContextManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(GLContextManager.class);
|
||||
private long windowId;
|
||||
private volatile boolean running = true;
|
||||
private Thread renderThread;
|
||||
// 改为可变的宽高以支持动态重建离屏上下文缓冲
|
||||
private volatile int width;
|
||||
private volatile int height;
|
||||
|
||||
private BufferedImage currentFrame;
|
||||
private volatile boolean contextInitialized = false;
|
||||
private final CompletableFuture<Void> contextReady = new CompletableFuture<>();
|
||||
private final String modelPath;
|
||||
private final AtomicReference<Model2D> modelRef = new AtomicReference<>();
|
||||
|
||||
private BufferedImage lastFrame = null;
|
||||
private ByteBuffer pixelBuffer = null;
|
||||
private int[] pixelInts = null;
|
||||
private int[] argbInts = null;
|
||||
public volatile float displayScale = 1.0f; // 当前可视缩放(用于检测阈值/角点等)
|
||||
public volatile float targetScale = 1.0f; // 目标缩放(鼠标滚轮/程序改变时设置)
|
||||
|
||||
// 任务队列,用于在 GL 上下文线程执行代码
|
||||
private final BlockingQueue<Runnable> glTaskQueue = new LinkedBlockingQueue<>();
|
||||
private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor();
|
||||
|
||||
private volatile boolean cameraDragging = false;
|
||||
private static final float ZOOM_SMOOTHING = 0.18f; // 0..1, 越大收敛越快(建议 0.12-0.25)
|
||||
private RepaintCallback repaintCallback;
|
||||
|
||||
public GLContextManager(String modelPath, int width, int height) {
|
||||
this.modelPath = modelPath;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
|
||||
public GLContextManager(Model2D model, int width, int height) {
|
||||
this.modelPath = null;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.modelRef.set(model);
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建离屏 OpenGL 上下文
|
||||
*/
|
||||
private void createOffscreenContext() throws Exception {
|
||||
// 设置窗口提示
|
||||
GLFW.glfwDefaultWindowHints();
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MAJOR, 3);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_CONTEXT_VERSION_MINOR, 3);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_PROFILE, GLFW.GLFW_OPENGL_CORE_PROFILE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_OPENGL_FORWARD_COMPAT, GL11.GL_TRUE);
|
||||
GLFW.glfwWindowHint(GLFW.GLFW_SAMPLES, 4);
|
||||
|
||||
// 创建离屏窗口(像素尺寸以当前 width/height 为准)
|
||||
windowId = GLFW.glfwCreateWindow(width, height, "Offscreen Render", MemoryUtil.NULL, MemoryUtil.NULL);
|
||||
if (windowId == MemoryUtil.NULL) {
|
||||
throw new Exception("无法创建离屏 OpenGL 上下文");
|
||||
}
|
||||
|
||||
// 设置为当前上下文并初始化
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
GL.createCapabilities();
|
||||
|
||||
logger.info("OpenGL context created successfully");
|
||||
|
||||
// 然后初始化 RenderSystem
|
||||
RenderSystem.beginInitialization();
|
||||
RenderSystem.initRenderThread();
|
||||
|
||||
// 使用 RenderSystem 设置视口
|
||||
RenderSystem.viewport(0, 0, width, height);
|
||||
|
||||
// 分配像素读取缓冲
|
||||
int pixelCount = Math.max(1, width * height);
|
||||
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
|
||||
pixelBuffer.order(ByteOrder.nativeOrder());
|
||||
pixelInts = new int[pixelCount];
|
||||
argbInts = new int[pixelCount];
|
||||
|
||||
// 初始化 ModelRender
|
||||
ModelRender.initialize();
|
||||
|
||||
RenderSystem.finishInitialization();
|
||||
|
||||
// 在正确的上下文中加载模型(可能会耗时)
|
||||
loadModelInContext();
|
||||
|
||||
// 标记上下文已初始化并完成通知(只 complete 一次)
|
||||
contextInitialized = true;
|
||||
contextReady.complete(null);
|
||||
|
||||
logger.info("Offscreen context initialization completed");
|
||||
}
|
||||
|
||||
public void setRepaintCallback(RepaintCallback callback) {
|
||||
this.repaintCallback = callback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 OpenGL 上下文中加载模型
|
||||
*/
|
||||
private void loadModelInContext() {
|
||||
try {
|
||||
if (modelPath != null) {
|
||||
Model2D model = Model2D.loadFromFile(modelPath);
|
||||
modelRef.set(model);
|
||||
logger.info("模型加载成功: {}", modelPath);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("模型加载失败: {}", e.getMessage(), e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动渲染线程
|
||||
*/
|
||||
public void startRendering() {
|
||||
// 初始化 GLFW
|
||||
if (!GLFW.glfwInit()) {
|
||||
throw new RuntimeException("无法初始化 GLFW");
|
||||
}
|
||||
renderThread = new Thread(() -> {
|
||||
try {
|
||||
createOffscreenContext();
|
||||
|
||||
// 等待上下文就绪后再开始渲染循环(contextReady 由 createOffscreenContext 完成)
|
||||
contextReady.get();
|
||||
|
||||
// 确保当前线程一直持有该 GL 上下文(避免在每个任务/帧中重复 makeCurrent)
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
|
||||
final long targetNs = 1_000_000_000L / 60L; // 60 FPS
|
||||
while (running && !GLFW.glfwWindowShouldClose(windowId)) {
|
||||
long start = System.nanoTime();
|
||||
|
||||
processGLTasks();
|
||||
displayScale += (targetScale - displayScale) * ZOOM_SMOOTHING;
|
||||
renderFrame();
|
||||
|
||||
long elapsed = System.nanoTime() - start;
|
||||
long sleepNs = targetNs - elapsed;
|
||||
if (sleepNs > 0) {
|
||||
LockSupport.parkNanos(sleepNs);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("渲染线程异常", e);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
renderThread.setDaemon(true);
|
||||
renderThread.setName("GL-Render-Thread");
|
||||
renderThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单帧并读取到 BufferedImage
|
||||
*/
|
||||
private void renderFrame() {
|
||||
if (!contextInitialized || windowId == 0) return;
|
||||
// 确保在当前上下文中
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
Model2D currentModel = modelRef.get();
|
||||
if (currentModel != null) {
|
||||
try {
|
||||
// 使用 RenderSystem 清除缓冲区
|
||||
RenderSystem.setClearColor(0.18f, 0.18f, 0.25f, 1f);
|
||||
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
|
||||
// 渲染模型
|
||||
ModelRender.render(1.0f / 60f, currentModel);
|
||||
// 读取像素数据到 BufferedImage
|
||||
readPixelsToImage();
|
||||
} catch (Exception e) {
|
||||
System.err.println("渲染错误: " + e.getMessage());
|
||||
renderErrorFrame(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
// 没有模型时显示默认背景
|
||||
renderDefaultBackground();
|
||||
}
|
||||
|
||||
// 在 Swing EDT 中更新显示
|
||||
if (repaintCallback != null) {
|
||||
repaintCallback.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染默认背景
|
||||
*/
|
||||
private void renderDefaultBackground() {
|
||||
RenderSystem.setClearColor(0.1f, 0.1f, 0.15f, 1f);
|
||||
RenderSystem.clear(RenderSystem.GL_COLOR_BUFFER_BIT | RenderSystem.GL_DEPTH_BUFFER_BIT);
|
||||
readPixelsToImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染错误帧
|
||||
*/
|
||||
private void renderErrorFrame(String errorMessage) {
|
||||
GL11.glClearColor(0.3f, 0.1f, 0.1f, 1f);
|
||||
GL11.glClear(GL11.GL_COLOR_BUFFER_BIT | GL11.GL_DEPTH_BUFFER_BIT);
|
||||
readPixelsToImage();
|
||||
BufferedImage errorImage = new BufferedImage(Math.max(1, width), Math.max(1, height), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = errorImage.createGraphics();
|
||||
g2d.setColor(Color.DARK_GRAY);
|
||||
g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight());
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.drawString("渲染错误: " + errorMessage, 10, 20);
|
||||
g2d.dispose();
|
||||
currentFrame = errorImage;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取 OpenGL 像素数据到 BufferedImage
|
||||
*/
|
||||
private void readPixelsToImage() {
|
||||
try {
|
||||
final int w = Math.max(1, this.width);
|
||||
final int h = Math.max(1, this.height);
|
||||
final int pixelCount = w * h;
|
||||
|
||||
// 确保缓冲区大小匹配(可能在 resize 后需要重建)
|
||||
if (pixelBuffer == null || pixelInts == null || pixelInts.length != pixelCount) {
|
||||
if (pixelBuffer != null) {
|
||||
try {
|
||||
MemoryUtil.memFree(pixelBuffer);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
|
||||
pixelBuffer.order(ByteOrder.nativeOrder());
|
||||
pixelInts = new int[pixelCount];
|
||||
argbInts = new int[pixelCount];
|
||||
}
|
||||
|
||||
pixelBuffer.clear();
|
||||
// 从 GPU 读取 RGBA 字节到本地缓冲 - 使用 RenderSystem
|
||||
RenderSystem.readPixels(0, 0, w, h, RenderSystem.GL_RGBA, RenderSystem.GL_UNSIGNED_BYTE, pixelBuffer);
|
||||
|
||||
// 以 int 批量读取(依赖本机字节序),然后转换为带 alpha 的 ARGB 并垂直翻转
|
||||
IntBuffer ib = pixelBuffer.asIntBuffer();
|
||||
ib.get(pixelInts, 0, pixelCount);
|
||||
|
||||
// 转换并翻转(RGBA -> ARGB)
|
||||
for (int y = 0; y < h; y++) {
|
||||
int srcRow = (h - y - 1) * w;
|
||||
int dstRow = y * w;
|
||||
for (int x = 0; x < w; x++) {
|
||||
int rgba = pixelInts[srcRow + x];
|
||||
|
||||
// 提取字节(考虑 native order,按 RGBA 存放)
|
||||
int r = (rgba >> 0) & 0xFF;
|
||||
int g = (rgba >> 8) & 0xFF;
|
||||
int b = (rgba >> 16) & 0xFF;
|
||||
int a = (rgba >> 24) & 0xFF;
|
||||
|
||||
// 组合为 ARGB (BufferedImage 使用 ARGB)
|
||||
argbInts[dstRow + x] = (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用一次 setRGB 写入 BufferedImage(比逐像素 setRGB 快)
|
||||
BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
|
||||
image.setRGB(0, 0, w, h, argbInts, 0, w);
|
||||
|
||||
currentFrame = image;
|
||||
lastFrame = image;
|
||||
} catch (Exception e) {
|
||||
logger.error("读取像素数据错误", e);
|
||||
// 创建错误图像(保持原逻辑)
|
||||
BufferedImage errorImage = new BufferedImage(Math.max(1, this.width), Math.max(1, this.height), BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = errorImage.createGraphics();
|
||||
g2d.setColor(Color.BLACK);
|
||||
g2d.fillRect(0, 0, errorImage.getWidth(), errorImage.getHeight());
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.drawString("像素读取失败", 10, 20);
|
||||
g2d.dispose();
|
||||
currentFrame = errorImage;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 GL 上下文任务队列
|
||||
*/
|
||||
private void processGLTasks() {
|
||||
Runnable task;
|
||||
while ((task = glTaskQueue.poll()) != null) {
|
||||
try {
|
||||
// 在渲染线程中执行,渲染线程已将上下文设为 current
|
||||
task.run();
|
||||
} catch (Exception e) {
|
||||
logger.error("执行 GL 任务时出错", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新设置面板大小
|
||||
* <p>
|
||||
* 说明:当 Swing 面板被放大时,需要同时调整离屏 GLFW 窗口像素大小、GL 视口以及重分配像素读取缓冲,
|
||||
* 否则将把较小分辨率的图像拉伸到更大面板上导致模糊。
|
||||
*/
|
||||
public void resize(int newWidth, int newHeight) {
|
||||
executeInGLContext(() -> {
|
||||
if (contextInitialized && windowId != 0) {
|
||||
this.width = Math.max(1, newWidth);
|
||||
this.height = Math.max(1, newHeight);
|
||||
GLFW.glfwMakeContextCurrent(windowId);
|
||||
GLFW.glfwSetWindowSize(windowId, this.width, this.height);
|
||||
RenderSystem.viewport(0, 0, this.width, this.height);
|
||||
ModelRender.setViewport(this.width, this.height);
|
||||
try {
|
||||
if (pixelBuffer != null) {
|
||||
MemoryUtil.memFree(pixelBuffer);
|
||||
pixelBuffer = null;
|
||||
}
|
||||
} catch (Throwable ignored) {}
|
||||
int pixelCount = Math.max(1, this.width * this.height);
|
||||
pixelBuffer = MemoryUtil.memAlloc(pixelCount * 4);
|
||||
pixelBuffer.order(ByteOrder.nativeOrder());
|
||||
pixelInts = new int[pixelCount];
|
||||
argbInts = new int[pixelCount];
|
||||
currentFrame = null;
|
||||
} else {
|
||||
this.width = Math.max(1, newWidth);
|
||||
this.height = Math.max(1, newHeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 等待渲染上下文准备就绪
|
||||
*/
|
||||
public CompletableFuture<Void> waitForContext() {
|
||||
return contextReady;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查渲染上下文是否已初始化
|
||||
* @return true 表示已初始化,false 表示未初始化
|
||||
*/
|
||||
public boolean isContextInitialized() {
|
||||
return contextInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否正在运行
|
||||
*/
|
||||
public boolean isRunning() {
|
||||
return running && contextInitialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
public void dispose() {
|
||||
running = false;
|
||||
cameraDragging = false;
|
||||
// 停止任务执行器
|
||||
taskExecutor.shutdown();
|
||||
|
||||
if (renderThread != null) {
|
||||
try {
|
||||
renderThread.join(2000);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
cleanup();
|
||||
}
|
||||
|
||||
private void cleanup() {
|
||||
// 清理 ModelRender
|
||||
try {
|
||||
if (ModelRender.isInitialized()) {
|
||||
ModelRender.cleanup();
|
||||
logger.info("ModelRender 已清理");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.error("清理 ModelRender 时出错: {}", e.getMessage());
|
||||
}
|
||||
|
||||
if (windowId != 0) {
|
||||
try {
|
||||
GLFW.glfwDestroyWindow(windowId);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
windowId = 0;
|
||||
}
|
||||
|
||||
// 释放像素缓冲
|
||||
try {
|
||||
if (pixelBuffer != null) {
|
||||
MemoryUtil.memFree(pixelBuffer);
|
||||
pixelBuffer = null;
|
||||
}
|
||||
} catch (Throwable t) {
|
||||
logger.warn("释放 pixelBuffer 时出错: {}", t.getMessage());
|
||||
}
|
||||
|
||||
// 终止 GLFW(注意:如果应用中还有其他 GLFW 窗口,这里会影响它们)
|
||||
try {
|
||||
GLFW.glfwTerminate();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
|
||||
logger.info("OpenGL 资源已清理");
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 GL 上下文线程上异步执行任务
|
||||
*
|
||||
* @param task 要在 GL 上下文线程中执行的任务
|
||||
* @return CompletableFuture 用于获取任务执行结果
|
||||
*/
|
||||
public CompletableFuture<Void> executeInGLContext(Runnable task) {
|
||||
CompletableFuture<Void> future = new CompletableFuture<>();
|
||||
|
||||
if (!running) {
|
||||
future.completeExceptionally(new IllegalStateException("渲染线程已停止"));
|
||||
return future;
|
||||
}
|
||||
|
||||
// 等待上下文就绪后再提交任务
|
||||
contextReady.thenRun(() -> {
|
||||
try {
|
||||
// 使用 put 保证任务不会被丢弃,如果队列已满会阻塞调用者直到可入队
|
||||
glTaskQueue.put(() -> {
|
||||
try {
|
||||
task.run();
|
||||
future.complete(null);
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 GL 上下文线程上异步执行任务并返回结果
|
||||
*
|
||||
* @param task 要在 GL 上下文线程中执行的有返回值的任务
|
||||
* @return CompletableFuture 用于获取任务执行结果
|
||||
*/
|
||||
public <T> CompletableFuture<T> executeInGLContext(Callable<T> task) {
|
||||
CompletableFuture<T> future = new CompletableFuture<>();
|
||||
|
||||
if (!running) {
|
||||
future.completeExceptionally(new IllegalStateException("渲染线程已停止"));
|
||||
return future;
|
||||
}
|
||||
|
||||
contextReady.thenRun(() -> {
|
||||
try {
|
||||
boolean offered = glTaskQueue.offer(() -> {
|
||||
try {
|
||||
T result = task.call();
|
||||
future.complete(result);
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
}, 5, TimeUnit.SECONDS);
|
||||
|
||||
if (!offered) {
|
||||
future.completeExceptionally(new TimeoutException("任务队列已满,无法在5秒内添加任务"));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
future.completeExceptionally(new IllegalStateException("任务提交被中断", e));
|
||||
} catch (Exception e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步在 GL 上下文线程上执行任务(会阻塞当前线程直到任务完成)
|
||||
*
|
||||
* @param task 要在 GL 上下文线程中执行的任务
|
||||
* @throws Exception 如果任务执行出错
|
||||
*/
|
||||
public void executeInGLContextSync(Runnable task) throws Exception {
|
||||
if (!running) {
|
||||
throw new IllegalStateException("渲染线程已停止");
|
||||
}
|
||||
|
||||
CompletableFuture<Void> future = executeInGLContext(task);
|
||||
future.get(10, TimeUnit.SECONDS); // 设置超时时间
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步在 GL 上下文线程上执行任务并返回结果(会阻塞当前线程直到任务完成)
|
||||
*
|
||||
* @param task 要在 GL 上下文线程中执行的有返回值的任务
|
||||
* @return 任务执行结果
|
||||
* @throws Exception 如果任务执行出错或超时
|
||||
*/
|
||||
public <T> T executeInGLContextSync(Callable<T> task) throws Exception {
|
||||
if (!running) {
|
||||
throw new IllegalStateException("渲染线程已停止");
|
||||
}
|
||||
|
||||
CompletableFuture<T> future = executeInGLContext(task);
|
||||
return future.get(10, TimeUnit.SECONDS); // 设置超时时间
|
||||
}
|
||||
|
||||
public void setDisplayScale(float scale) {
|
||||
this.displayScale = scale;
|
||||
}
|
||||
|
||||
public void setTargetScale(float scale) {
|
||||
this.targetScale = scale;
|
||||
}
|
||||
|
||||
public float getDisplayScale() {
|
||||
return displayScale;
|
||||
}
|
||||
|
||||
public float getTargetScale() {
|
||||
return targetScale;
|
||||
}
|
||||
|
||||
public interface RepaintCallback {
|
||||
void repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前帧
|
||||
* @return 当前帧
|
||||
*/
|
||||
public BufferedImage getCurrentFrame() {
|
||||
return currentFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一帧
|
||||
* @return 上一帧
|
||||
*/
|
||||
public BufferedImage getLastFrame() {
|
||||
return lastFrame;
|
||||
}
|
||||
|
||||
public boolean isCameraDragging() {
|
||||
return cameraDragging;
|
||||
}
|
||||
|
||||
public void setCameraDragging(boolean cameraDragging) {
|
||||
this.cameraDragging = cameraDragging;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,423 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.LiquifyTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.Tool;
|
||||
import com.chuangzhou.vivid2D.render.model.util.tools.LiquifyTargetPartRander;
|
||||
import com.chuangzhou.vivid2D.render.systems.Camera;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.swing.*;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyAdapter;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.KeyListener;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class KeyboardManager {
|
||||
private static final Logger logger = LoggerFactory.getLogger(KeyboardManager.class);
|
||||
private final ModelRenderPanel panel;
|
||||
private volatile boolean shiftPressed = false;
|
||||
private volatile boolean ctrlPressed = false;
|
||||
|
||||
// 存储自定义快捷键
|
||||
private final Map<String, KeyStroke> customShortcuts = new HashMap<>();
|
||||
private final Map<String, AbstractAction> customActions = new HashMap<>();
|
||||
|
||||
public KeyboardManager(ModelRenderPanel panel){
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化键盘快捷键
|
||||
*/
|
||||
public void initKeyboardShortcuts() {
|
||||
// 获取输入映射和动作映射
|
||||
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
ActionMap actionMap = panel.getActionMap();
|
||||
|
||||
// 撤回快捷键:Ctrl+Z
|
||||
registerShortcut("undo", KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.getStatusRecordManagement().undo();
|
||||
}
|
||||
});
|
||||
|
||||
// 重做快捷键:Ctrl+Y 或 Ctrl+Shift+Z
|
||||
registerShortcut("redo", KeyStroke.getKeyStroke(KeyEvent.VK_Y, KeyEvent.CTRL_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.getStatusRecordManagement().redo();
|
||||
}
|
||||
});
|
||||
registerShortcut("redo2", KeyStroke.getKeyStroke(KeyEvent.VK_Z, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.getStatusRecordManagement().redo();
|
||||
}
|
||||
});
|
||||
|
||||
// 清除历史记录:Ctrl+Shift+Delete
|
||||
registerShortcut("clearHistory", KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, KeyEvent.CTRL_DOWN_MASK | KeyEvent.SHIFT_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.getStatusRecordManagement().clearHistory();
|
||||
}
|
||||
});
|
||||
|
||||
// 摄像机重置快捷键:Ctrl+R
|
||||
registerShortcut("resetCamera", KeyStroke.getKeyStroke(KeyEvent.VK_R, KeyEvent.CTRL_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.resetCamera();
|
||||
logger.info("重置摄像机");
|
||||
}
|
||||
});
|
||||
|
||||
// 摄像机启用/禁用快捷键:Ctrl+E
|
||||
registerShortcut("toggleCamera", KeyStroke.getKeyStroke(KeyEvent.VK_E, KeyEvent.CTRL_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
Camera camera = ModelRender.getCamera();
|
||||
boolean newState = !camera.isEnabled();
|
||||
camera.setEnabled(newState);
|
||||
logger.info("{}摄像机", newState ? "启用" : "禁用");
|
||||
}
|
||||
});
|
||||
|
||||
// 注册工具快捷键
|
||||
registerToolShortcuts();
|
||||
|
||||
// 设置键盘监听器
|
||||
setupKeyListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工具快捷键
|
||||
*/
|
||||
private void registerToolShortcuts() {
|
||||
// 木偶变形工具快捷键:Ctrl+P
|
||||
registerShortcut("puppetTool", KeyStroke.getKeyStroke(KeyEvent.VK_P, KeyEvent.CTRL_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.switchTool("木偶变形工具");
|
||||
logger.info("切换到木偶变形工具");
|
||||
}
|
||||
});
|
||||
|
||||
// 顶点变形工具快捷键:Ctrl+T
|
||||
registerShortcut("vertexTool", KeyStroke.getKeyStroke(KeyEvent.VK_T, KeyEvent.CTRL_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.switchTool("顶点变形工具");
|
||||
logger.info("切换到顶点变形工具");
|
||||
}
|
||||
});
|
||||
|
||||
// 选择工具快捷键:Ctrl+S
|
||||
registerShortcut("selectionTool", KeyStroke.getKeyStroke(KeyEvent.VK_S, KeyEvent.CTRL_DOWN_MASK),
|
||||
new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.switchTool("选择工具");
|
||||
logger.info("切换到选择工具");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册自定义快捷键
|
||||
* @param actionName 动作名称
|
||||
* @param keyStroke 按键组合
|
||||
* @param action 对应的动作
|
||||
*/
|
||||
public void registerShortcut(String actionName, KeyStroke keyStroke, AbstractAction action) {
|
||||
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
ActionMap actionMap = panel.getActionMap();
|
||||
|
||||
// 如果已存在相同的快捷键,先移除
|
||||
if (customShortcuts.containsKey(actionName)) {
|
||||
KeyStroke oldKeyStroke = customShortcuts.get(actionName);
|
||||
inputMap.remove(oldKeyStroke);
|
||||
}
|
||||
|
||||
// 注册新的快捷键
|
||||
inputMap.put(keyStroke, actionName);
|
||||
actionMap.put(actionName, action);
|
||||
|
||||
// 保存到自定义快捷键映射
|
||||
customShortcuts.put(actionName, keyStroke);
|
||||
customActions.put(actionName, action);
|
||||
|
||||
logger.debug("注册快捷键: {} -> {}", keyStroke, actionName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销自定义快捷键
|
||||
* @param actionName 动作名称
|
||||
*/
|
||||
public void unregisterShortcut(String actionName) {
|
||||
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
ActionMap actionMap = panel.getActionMap();
|
||||
|
||||
if (customShortcuts.containsKey(actionName)) {
|
||||
KeyStroke keyStroke = customShortcuts.get(actionName);
|
||||
inputMap.remove(keyStroke);
|
||||
actionMap.remove(actionName);
|
||||
|
||||
customShortcuts.remove(actionName);
|
||||
customActions.remove(actionName);
|
||||
|
||||
logger.debug("注销快捷键: {}", actionName);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工具快捷键
|
||||
* @param toolName 工具名称
|
||||
* @param keyStroke 按键组合
|
||||
*/
|
||||
public void registerToolShortcut(String toolName, KeyStroke keyStroke) {
|
||||
String actionName = "tool_" + toolName;
|
||||
registerShortcut(actionName, keyStroke, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.switchTool(toolName);
|
||||
logger.info("切换到工具: {}", toolName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工具循环切换快捷键
|
||||
* @param keyStroke 按键组合
|
||||
*/
|
||||
public void registerToolCycleShortcut(KeyStroke keyStroke) {
|
||||
registerShortcut("cycleTools", keyStroke, new AbstractAction() {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
panel.getToolManagement().switchToPreviousTool();
|
||||
Tool currentTool = panel.getCurrentTool();
|
||||
if (currentTool != null) {
|
||||
logger.info("切换到上一个工具: {}", currentTool.getToolName());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键盘监听器
|
||||
*/
|
||||
private void setupKeyListeners() {
|
||||
panel.addKeyListener(new KeyAdapter() {
|
||||
@Override
|
||||
public void keyPressed(KeyEvent e) {
|
||||
handleKeyPressed(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void keyReleased(KeyEvent e) {
|
||||
handleKeyReleased(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理按键按下
|
||||
*/
|
||||
private void handleKeyPressed(KeyEvent e) {
|
||||
int keyCode = e.getKeyCode();
|
||||
|
||||
// 更新修饰键状态
|
||||
if (keyCode == KeyEvent.VK_SHIFT) {
|
||||
shiftPressed = true;
|
||||
} else if (keyCode == KeyEvent.VK_CONTROL) {
|
||||
ctrlPressed = true;
|
||||
// 液化模式下按住Ctrl显示顶点
|
||||
if (panel.getCurrentTool() instanceof LiquifyTool liquifyTool) {
|
||||
LiquifyTargetPartRander rander = liquifyTool.getAssociatedRanderTools();
|
||||
if (rander != null) {
|
||||
rander.setAlgorithmEnabled("isRenderVertices",true);
|
||||
logger.debug("液化模式下按住Ctrl,开启顶点渲染");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 处理功能快捷键
|
||||
if (ctrlPressed) {
|
||||
switch (keyCode) {
|
||||
case KeyEvent.VK_A:
|
||||
// Ctrl+A 全选
|
||||
e.consume();
|
||||
panel.selectAllMeshes();
|
||||
logger.debug("全选所有网格");
|
||||
break;
|
||||
case KeyEvent.VK_D:
|
||||
// Ctrl+D 取消选择
|
||||
e.consume();
|
||||
panel.clearSelectedMeshes();
|
||||
logger.debug("取消所有选择");
|
||||
break;
|
||||
case KeyEvent.VK_1:
|
||||
// Ctrl+1 切换到第一个工具
|
||||
e.consume();
|
||||
switchToToolByIndex(0);
|
||||
break;
|
||||
case KeyEvent.VK_2:
|
||||
// Ctrl+2 切换到第二个工具
|
||||
e.consume();
|
||||
switchToToolByIndex(1);
|
||||
break;
|
||||
case KeyEvent.VK_3:
|
||||
// Ctrl+3 切换到第三个工具
|
||||
e.consume();
|
||||
switchToToolByIndex(2);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 单独按键处理
|
||||
switch (keyCode) {
|
||||
case KeyEvent.VK_ESCAPE:
|
||||
// ESC 键取消所有选择或退出工具
|
||||
e.consume();
|
||||
if (panel.getToolManagement().hasActiveTool() &&
|
||||
!panel.getToolManagement().getCurrentTool().getToolName().equals("选择工具")) {
|
||||
// 如果有激活的工具且不是选择工具,切换到选择工具
|
||||
panel.switchTool("选择工具");
|
||||
logger.info("按ESC键切换到选择工具");
|
||||
} else {
|
||||
// 否则取消所有选择
|
||||
panel.clearSelectedMeshes();
|
||||
|
||||
logger.info("按ESC键取消所有选择");
|
||||
}
|
||||
break;
|
||||
case KeyEvent.VK_SPACE:
|
||||
// 空格键临时切换到手型工具(用于移动视图)
|
||||
if (!e.isConsumed()) {
|
||||
// 这里可以添加空格键拖拽视图的功能
|
||||
// 需要与鼠标管理中键拖拽功能配合
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理按键释放
|
||||
*/
|
||||
private void handleKeyReleased(KeyEvent e) {
|
||||
int keyCode = e.getKeyCode();
|
||||
|
||||
// 更新修饰键状态
|
||||
if (keyCode == KeyEvent.VK_SHIFT) {
|
||||
shiftPressed = false;
|
||||
} else if (keyCode == KeyEvent.VK_CONTROL) {
|
||||
ctrlPressed = false;
|
||||
// 液化模式下松开Ctrl隐藏顶点
|
||||
if (panel.getCurrentTool() instanceof LiquifyTool liquifyTool) {
|
||||
LiquifyTargetPartRander rander = liquifyTool.getAssociatedRanderTools();
|
||||
if (rander != null) {
|
||||
rander.setAlgorithmEnabled("isRenderVertices",false);
|
||||
logger.debug("液化模式下松开Ctrl,关闭顶点渲染");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据索引切换到工具
|
||||
*/
|
||||
private void switchToToolByIndex(int index) {
|
||||
java.util.List<Tool> tools = panel.getToolManagement().getRegisteredTools();
|
||||
if (index >= 0 && index < tools.size()) {
|
||||
Tool tool = tools.get(index);
|
||||
panel.switchTool(tool.getToolName());
|
||||
logger.info("切换到工具: {}", tool.getToolName());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有注册的快捷键信息
|
||||
*/
|
||||
public Map<String, String> getShortcutInfo() {
|
||||
Map<String, String> info = new HashMap<>();
|
||||
for (Map.Entry<String, KeyStroke> entry : customShortcuts.entrySet()) {
|
||||
info.put(entry.getKey(), entry.getValue().toString());
|
||||
}
|
||||
return info;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载所有快捷键
|
||||
*/
|
||||
public void reloadShortcuts() {
|
||||
// 清除所有自定义快捷键
|
||||
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
ActionMap actionMap = panel.getActionMap();
|
||||
|
||||
for (String actionName : customShortcuts.keySet()) {
|
||||
KeyStroke keyStroke = customShortcuts.get(actionName);
|
||||
inputMap.remove(keyStroke);
|
||||
actionMap.remove(actionName);
|
||||
}
|
||||
|
||||
customShortcuts.clear();
|
||||
customActions.clear();
|
||||
|
||||
// 重新初始化
|
||||
initKeyboardShortcuts();
|
||||
logger.info("重新加载所有快捷键");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Shift键状态
|
||||
*/
|
||||
public boolean getIsShiftPressed(){
|
||||
return shiftPressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Ctrl键状态
|
||||
*/
|
||||
public boolean getIsCtrlPressed(){
|
||||
return ctrlPressed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
public void dispose() {
|
||||
// 移除所有键盘监听器
|
||||
for (KeyListener listener : panel.getKeyListeners()) {
|
||||
panel.removeKeyListener(listener);
|
||||
}
|
||||
|
||||
// 清除所有快捷键
|
||||
InputMap inputMap = panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
|
||||
ActionMap actionMap = panel.getActionMap();
|
||||
|
||||
for (String actionName : customShortcuts.keySet()) {
|
||||
KeyStroke keyStroke = customShortcuts.get(actionName);
|
||||
inputMap.remove(keyStroke);
|
||||
actionMap.remove(actionName);
|
||||
}
|
||||
|
||||
customShortcuts.clear();
|
||||
customActions.clear();
|
||||
|
||||
logger.info("键盘管理器已清理");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class LayerOperationManager {
|
||||
private final Model2D model;
|
||||
|
||||
public LayerOperationManager(Model2D model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public void addLayer(String name) {
|
||||
model.createPart(name);
|
||||
model.markNeedsUpdate();
|
||||
}
|
||||
|
||||
public void removeLayer(ModelPart part) {
|
||||
if (part == null) return;
|
||||
|
||||
List<ModelPart> parts = model.getParts();
|
||||
if (parts != null) parts.remove(part);
|
||||
|
||||
Map<String, ModelPart> partMap = model.getPartMap();
|
||||
if (partMap != null) partMap.remove(part.getName());
|
||||
|
||||
model.markNeedsUpdate();
|
||||
}
|
||||
|
||||
public void moveLayer(List<ModelPart> visualOrder) {
|
||||
List<ModelPart> newModelParts = new ArrayList<>(visualOrder.size());
|
||||
for (int i = visualOrder.size() - 1; i >= 0; i--) {
|
||||
newModelParts.add(visualOrder.get(i));
|
||||
}
|
||||
replaceModelPartsList(newModelParts);
|
||||
model.markNeedsUpdate();
|
||||
}
|
||||
|
||||
public void setLayerOpacity(ModelPart part, float opacity) {
|
||||
part.setOpacity(opacity);
|
||||
model.markNeedsUpdate();
|
||||
}
|
||||
|
||||
public void setLayerVisibility(ModelPart part, boolean visible) {
|
||||
part.setVisible(visible);
|
||||
model.markNeedsUpdate();
|
||||
}
|
||||
|
||||
private void replaceModelPartsList(List<ModelPart> newParts) {
|
||||
if (model == null) return;
|
||||
try {
|
||||
java.lang.reflect.Field partsField = model.getClass().getDeclaredField("parts");
|
||||
partsField.setAccessible(true);
|
||||
Object old = partsField.get(model);
|
||||
if (old instanceof List) {
|
||||
((List) old).clear();
|
||||
((List) old).addAll(newParts);
|
||||
} else {
|
||||
partsField.set(model, newParts);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
public class MouseManagement {
|
||||
|
||||
private final ModelRenderPanel modelRenderPanel;
|
||||
private final GLContextManager glContextManager;
|
||||
private final CameraManagement cameraManagement;
|
||||
private final KeyboardManager keyboardManager;
|
||||
|
||||
public MouseManagement(ModelRenderPanel modelRenderPanel,
|
||||
GLContextManager glContextManager,
|
||||
CameraManagement cameraManagement,
|
||||
KeyboardManager keyboardManager){
|
||||
this.modelRenderPanel = modelRenderPanel;
|
||||
this.glContextManager = glContextManager;
|
||||
this.cameraManagement = cameraManagement;
|
||||
this.keyboardManager = keyboardManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加鼠标事件监听器
|
||||
*/
|
||||
public void addMouseListeners() {
|
||||
modelRenderPanel.addMouseListener(new MouseAdapter() {
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent e) {
|
||||
modelRenderPanel.handleMouseClick(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mousePressed(MouseEvent e) {
|
||||
modelRenderPanel.handleMousePressed(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseReleased(MouseEvent e) {
|
||||
modelRenderPanel.handleMouseReleased(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseExited(MouseEvent e) {
|
||||
modelRenderPanel.setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
});
|
||||
|
||||
modelRenderPanel.addMouseWheelListener(new MouseWheelListener() {
|
||||
@Override
|
||||
public void mouseWheelMoved(MouseWheelEvent e) {
|
||||
if (!glContextManager.isContextInitialized()) return;
|
||||
final int screenX = e.getX();
|
||||
final int screenY = e.getY();
|
||||
final int notches = e.getWheelRotation();
|
||||
final boolean fine = e.isShiftDown();
|
||||
cameraManagement.resizingApplications(screenX, screenY, notches, fine);
|
||||
}
|
||||
});
|
||||
|
||||
modelRenderPanel.addMouseMotionListener(new MouseMotionAdapter() {
|
||||
@Override
|
||||
public void mouseMoved(MouseEvent e) {
|
||||
modelRenderPanel.handleMouseMove(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void mouseDragged(MouseEvent e) {
|
||||
modelRenderPanel.handleMouseDragged(e);
|
||||
}
|
||||
});
|
||||
|
||||
modelRenderPanel.addMouseWheelListener(e -> {
|
||||
int notches = e.getWheelRotation();
|
||||
boolean fine = (e.isShiftDown() || keyboardManager.getIsShiftPressed()); // 支持 Shift 更精细控制
|
||||
double step = fine ? Math.pow(CameraManagement.ZOOM_STEP, 0.25) : CameraManagement.ZOOM_STEP;
|
||||
if (notches > 0) {
|
||||
// 滚轮下:缩小
|
||||
glContextManager.targetScale *= Math.pow(1.0 / step, notches);
|
||||
} else if (notches < 0) {
|
||||
// 滚轮上:放大
|
||||
glContextManager.targetScale *= Math.pow(step, -notches);
|
||||
}
|
||||
glContextManager.targetScale = Math.max(CameraManagement.ZOOM_MIN, Math.min(CameraManagement.ZOOM_MAX, glContextManager.targetScale));
|
||||
});
|
||||
modelRenderPanel.setFocusable(true);
|
||||
modelRenderPanel.requestFocusInWindow();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.util.OperationHistoryGlobal;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class StatusRecordManagement {
|
||||
private final ModelRenderPanel panel;
|
||||
private final OperationHistoryGlobal operationHistory;
|
||||
|
||||
public StatusRecordManagement(ModelRenderPanel panel, OperationHistoryGlobal operationHistory){
|
||||
this.operationHistory = operationHistory;
|
||||
this.panel = panel;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重做操作
|
||||
*/
|
||||
public void redo() {
|
||||
if (operationHistory != null && operationHistory.canRedo()) {
|
||||
panel.getGlContextManager().executeInGLContext(() -> {
|
||||
boolean success = operationHistory.redo();
|
||||
if (success) {
|
||||
panel.repaint();
|
||||
System.out.println("重做: " + operationHistory.getRedoDescription());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
System.out.println("没有可重做的操作");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录位置变化操作
|
||||
*/
|
||||
private void recordPositionChange(ModelPart part, Vector2f oldPosition, Vector2f newPosition) {
|
||||
if (operationHistory != null && part != null) {
|
||||
operationHistory.recordOperation("SET_POSITION", part, oldPosition, newPosition);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录缩放变化操作
|
||||
*/
|
||||
private void recordScaleChange(ModelPart part, Vector2f oldScale, Vector2f newScale) {
|
||||
if (operationHistory != null && part != null) {
|
||||
operationHistory.recordOperation("SET_SCALE", part, oldScale, newScale);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录旋转变化操作
|
||||
*/
|
||||
private void recordRotationChange(ModelPart part, float oldRotation, float newRotation) {
|
||||
if (operationHistory != null && part != null) {
|
||||
operationHistory.recordOperation("SET_ROTATION", part, oldRotation, newRotation);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录中心点变化操作
|
||||
*/
|
||||
private void recordPivotChange(ModelPart part, Vector2f oldPivot, Vector2f newPivot) {
|
||||
if (operationHistory != null && part != null) {
|
||||
operationHistory.recordOperation("SET_PIVOT", part, oldPivot, newPivot);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录拖拽结束操作
|
||||
*/
|
||||
public void recordDragEnd(List<ModelPart> parts, Map<ModelPart, Vector2f> startPositions) {
|
||||
if (operationHistory != null && parts != null && !parts.isEmpty()) {
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(parts);
|
||||
params.add(startPositions);
|
||||
// 添加当前位置
|
||||
for (ModelPart part : parts) {
|
||||
params.add(part.getPosition());
|
||||
}
|
||||
operationHistory.recordOperation("DRAG_PART_END", params.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录调整大小结束操作
|
||||
*/
|
||||
public void recordResizeEnd(List<ModelPart> parts, Map<ModelPart, Vector2f> startScales) {
|
||||
if (operationHistory != null && parts != null && !parts.isEmpty()) {
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(parts);
|
||||
params.add(startScales);
|
||||
// 添加当前缩放
|
||||
for (ModelPart part : parts) {
|
||||
params.add(part.getScale());
|
||||
}
|
||||
operationHistory.recordOperation("RESIZE_PART_END", params.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录旋转结束操作
|
||||
*/
|
||||
public void recordRotateEnd(List<ModelPart> parts, Map<ModelPart, Float> startRotations) {
|
||||
if (operationHistory != null && parts != null && !parts.isEmpty()) {
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(parts);
|
||||
params.add(startRotations);
|
||||
// 添加当前旋转
|
||||
for (ModelPart part : parts) {
|
||||
params.add(part.getRotation());
|
||||
}
|
||||
operationHistory.recordOperation("ROTATE_PART_END", params.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录移动中心点结束操作
|
||||
*/
|
||||
public void recordMovePivotEnd(List<ModelPart> parts, Map<ModelPart, Vector2f> startPivots) {
|
||||
if (operationHistory != null && parts != null && !parts.isEmpty()) {
|
||||
List<Object> params = new ArrayList<>();
|
||||
params.add(parts);
|
||||
params.add(startPivots);
|
||||
// 添加当前中心点
|
||||
for (ModelPart part : parts) {
|
||||
params.add(part.getPivot());
|
||||
}
|
||||
operationHistory.recordOperation("MOVE_PIVOT_END", params.toArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回操作
|
||||
*/
|
||||
public void undo() {
|
||||
if (operationHistory != null && operationHistory.canUndo()) {
|
||||
panel.getGlContextManager().executeInGLContext(() -> {
|
||||
boolean success = operationHistory.undo();
|
||||
if (success) {
|
||||
panel.repaint();
|
||||
System.out.println("撤回: " + operationHistory.getUndoDescription());
|
||||
}
|
||||
});
|
||||
} else {
|
||||
System.out.println("没有可撤回的操作");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除操作历史
|
||||
*/
|
||||
public void clearHistory() {
|
||||
if (operationHistory != null) {
|
||||
operationHistory.clearHistory();
|
||||
System.out.println("操作历史已清除");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Texture;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ThumbnailManager {
|
||||
private static final int THUMBNAIL_WIDTH = 48;
|
||||
private static final int THUMBNAIL_HEIGHT = 48;
|
||||
|
||||
private final Map<ModelPart, BufferedImage> thumbnailCache = new HashMap<>();
|
||||
private ModelRenderPanel renderPanel;
|
||||
|
||||
public ThumbnailManager(ModelRenderPanel renderPanel) {
|
||||
this.renderPanel = renderPanel;
|
||||
}
|
||||
|
||||
public BufferedImage getThumbnail(ModelPart part) {
|
||||
return thumbnailCache.get(part);
|
||||
}
|
||||
|
||||
public void generateThumbnail(ModelPart part) {
|
||||
if (renderPanel == null) return;
|
||||
|
||||
try {
|
||||
BufferedImage thumbnail = renderPanel.getGlContextManager()
|
||||
.executeInGLContext(() -> renderPartThumbnail(part))
|
||||
.get();
|
||||
|
||||
if (thumbnail != null) {
|
||||
thumbnailCache.put(part, thumbnail);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
thumbnailCache.put(part, createDefaultThumbnail());
|
||||
}
|
||||
}
|
||||
|
||||
public void removeThumbnail(ModelPart part) {
|
||||
thumbnailCache.remove(part);
|
||||
}
|
||||
|
||||
public void clearCache() {
|
||||
thumbnailCache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染单个部件的缩略图
|
||||
*/
|
||||
private BufferedImage renderPartThumbnail(ModelPart part) {
|
||||
if (renderPanel == null) return createDefaultThumbnail();
|
||||
|
||||
try {
|
||||
return createThumbnailForPart(part);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return createDefaultThumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
private BufferedImage createThumbnailForPart(ModelPart part) {
|
||||
BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = thumbnail.createGraphics();
|
||||
|
||||
// 设置抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
|
||||
|
||||
// 绘制背景
|
||||
g2d.setColor(new Color(40, 40, 40));
|
||||
g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
|
||||
|
||||
try {
|
||||
// 尝试获取部件的纹理
|
||||
Texture texture = null;
|
||||
List<Mesh2D> meshes = part.getMeshes();
|
||||
if (meshes != null && !meshes.isEmpty()) {
|
||||
for (Mesh2D mesh : meshes) {
|
||||
texture = mesh.getTexture();
|
||||
if (texture != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (texture != null && !texture.isDisposed()) {
|
||||
// 获取纹理的 BufferedImage
|
||||
BufferedImage textureImage = textureToBufferedImage(texture);
|
||||
if (textureImage != null) {
|
||||
// 计算缩放比例以保持宽高比
|
||||
int imgWidth = textureImage.getWidth();
|
||||
int imgHeight = textureImage.getHeight();
|
||||
|
||||
if (imgWidth > 0 && imgHeight > 0) {
|
||||
float scale = Math.min(
|
||||
(float)(THUMBNAIL_WIDTH - 8) / imgWidth,
|
||||
(float)(THUMBNAIL_HEIGHT - 8) / imgHeight
|
||||
);
|
||||
|
||||
int scaledWidth = (int)(imgWidth * scale);
|
||||
int scaledHeight = (int)(imgHeight * scale);
|
||||
int x = (THUMBNAIL_WIDTH - scaledWidth) / 2;
|
||||
int y = (THUMBNAIL_HEIGHT - scaledHeight) / 2;
|
||||
|
||||
// 绘制纹理图片
|
||||
g2d.drawImage(textureImage, x, y, scaledWidth, scaledHeight, null);
|
||||
|
||||
// 绘制边框
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.drawRect(x, y, scaledWidth - 1, scaledHeight - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("生成缩略图失败: " + part.getName() + " - " + e.getMessage());
|
||||
}
|
||||
|
||||
// 如果部件不可见,绘制红色斜线覆盖
|
||||
if (!part.isVisible()) {
|
||||
g2d.setColor(new Color(255, 0, 0, 128)); // 半透明红色
|
||||
g2d.setStroke(new BasicStroke(3));
|
||||
g2d.drawLine(2, 2, THUMBNAIL_WIDTH - 2, THUMBNAIL_HEIGHT - 2);
|
||||
g2d.drawLine(THUMBNAIL_WIDTH - 2, 2, 2, THUMBNAIL_HEIGHT - 2);
|
||||
}
|
||||
|
||||
g2d.dispose();
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将Texture转换为BufferedImage
|
||||
*/
|
||||
private BufferedImage textureToBufferedImage(Texture texture) {
|
||||
try {
|
||||
// 确保纹理有像素数据缓存
|
||||
texture.ensurePixelDataCached();
|
||||
|
||||
if (!texture.hasPixelData()) {
|
||||
System.err.println("纹理没有像素数据: " + texture.getName());
|
||||
return null;
|
||||
}
|
||||
|
||||
byte[] pixelData = texture.getPixelData();
|
||||
if (pixelData == null || pixelData.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
int width = texture.getWidth();
|
||||
int height = texture.getHeight();
|
||||
Texture.TextureFormat format = texture.getFormat();
|
||||
int components = format.getComponents();
|
||||
|
||||
// 创建BufferedImage
|
||||
BufferedImage image;
|
||||
switch (components) {
|
||||
case 1: // 单通道
|
||||
image = new BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY);
|
||||
break;
|
||||
case 3: // RGB
|
||||
image = new BufferedImage(width, height, BufferedImage.TYPE_3BYTE_BGR);
|
||||
break;
|
||||
case 4: // RGBA
|
||||
image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
|
||||
break;
|
||||
default:
|
||||
System.err.println("不支持的纹理格式组件数量: " + components);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 将像素数据复制到BufferedImage,同时翻转垂直方向
|
||||
if (components == 4) {
|
||||
// RGBA格式 - 垂直翻转
|
||||
int[] pixels = new int[width * height];
|
||||
for (int y = 0; y < height; y++) {
|
||||
int srcY = height - 1 - y; // 翻转Y坐标
|
||||
for (int x = 0; x < width; x++) {
|
||||
int srcIndex = (srcY * width + x) * 4;
|
||||
int dstIndex = y * width + x;
|
||||
|
||||
int r = pixelData[srcIndex] & 0xFF;
|
||||
int g = pixelData[srcIndex + 1] & 0xFF;
|
||||
int b = pixelData[srcIndex + 2] & 0xFF;
|
||||
int a = pixelData[srcIndex + 3] & 0xFF;
|
||||
pixels[dstIndex] = (a << 24) | (r << 16) | (g << 8) | b;
|
||||
}
|
||||
}
|
||||
image.setRGB(0, 0, width, height, pixels, 0, width);
|
||||
} else if (components == 3) {
|
||||
// RGB格式 - 垂直翻转
|
||||
for (int y = 0; y < height; y++) {
|
||||
int srcY = height - 1 - y; // 翻转Y坐标
|
||||
for (int x = 0; x < width; x++) {
|
||||
int srcIndex = (srcY * width + x) * 3;
|
||||
int r = pixelData[srcIndex] & 0xFF;
|
||||
int g = pixelData[srcIndex + 1] & 0xFF;
|
||||
int b = pixelData[srcIndex + 2] & 0xFF;
|
||||
int rgb = (r << 16) | (g << 8) | b;
|
||||
image.setRGB(x, y, rgb);
|
||||
}
|
||||
}
|
||||
} else if (components == 1) {
|
||||
// 单通道格式 - 垂直翻转
|
||||
for (int y = 0; y < height; y++) {
|
||||
int srcY = height - 1 - y; // 翻转Y坐标
|
||||
for (int x = 0; x < width; x++) {
|
||||
int srcIndex = srcY * width + x;
|
||||
int gray = pixelData[srcIndex] & 0xFF;
|
||||
int rgb = (gray << 16) | (gray << 8) | gray;
|
||||
image.setRGB(x, y, rgb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("转换纹理到BufferedImage失败: " + texture.getName() + " - " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private BufferedImage createDefaultThumbnail() {
|
||||
BufferedImage thumbnail = new BufferedImage(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = thumbnail.createGraphics();
|
||||
|
||||
g2d.setColor(new Color(60, 60, 60));
|
||||
g2d.fillRect(0, 0, THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT);
|
||||
|
||||
g2d.setColor(Color.GRAY);
|
||||
g2d.drawRect(2, 2, THUMBNAIL_WIDTH - 5, THUMBNAIL_HEIGHT - 5);
|
||||
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.drawString("?", THUMBNAIL_WIDTH/2 - 4, THUMBNAIL_HEIGHT/2 + 4);
|
||||
|
||||
g2d.dispose();
|
||||
return thumbnail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.SelectionTool;
|
||||
import com.chuangzhou.vivid2D.render.awt.tools.Tool;
|
||||
import com.chuangzhou.vivid2D.render.model.util.manager.RanderToolsManager;
|
||||
import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 工具管理器
|
||||
* 负责注册、管理和切换各种编辑工具
|
||||
*/
|
||||
public class ToolManagement {
|
||||
private static final Logger logger = LoggerFactory.getLogger(ToolManagement.class);
|
||||
|
||||
private final ModelRenderPanel renderPanel;
|
||||
private final Map<String, Tool> registeredTools;
|
||||
private final RanderToolsManager randerToolsManager;
|
||||
private Tool currentTool = null;
|
||||
private Tool previousTool = null;
|
||||
|
||||
// 默认工具(选择工具)
|
||||
private final Tool defaultTool;
|
||||
|
||||
public ToolManagement(ModelRenderPanel renderPanel, RanderToolsManager randerToolsManager) {
|
||||
this.renderPanel = renderPanel;
|
||||
this.registeredTools = new ConcurrentHashMap<>();
|
||||
this.randerToolsManager = randerToolsManager;
|
||||
|
||||
// 创建默认选择工具
|
||||
this.defaultTool = new SelectionTool(renderPanel);
|
||||
registerTool(defaultTool);
|
||||
|
||||
// 设置默认工具为当前工具
|
||||
switchTool(defaultTool.getToolName());
|
||||
}
|
||||
|
||||
// ================== 工具注册管理 ==================
|
||||
|
||||
/**
|
||||
* 注册工具
|
||||
*/
|
||||
public void registerTool(Tool tool, RanderTools randerTools) {
|
||||
if (tool == null) {
|
||||
logger.warn("尝试注册空工具");
|
||||
return;
|
||||
}
|
||||
|
||||
String toolName = tool.getToolName();
|
||||
if (registeredTools.containsKey(toolName)) {
|
||||
logger.warn("工具已存在: {}", toolName);
|
||||
return;
|
||||
}
|
||||
registeredTools.put(toolName, tool);
|
||||
randerToolsManager.bindToolWithRanderTools(tool, randerTools);
|
||||
tool.setAssociatedRanderTools(randerTools);
|
||||
logger.info("注册工具: {}", toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册工具
|
||||
*/
|
||||
public void registerTool(Tool tool) {
|
||||
if (tool == null) {
|
||||
logger.warn("尝试注册空工具");
|
||||
return;
|
||||
}
|
||||
|
||||
String toolName = tool.getToolName();
|
||||
if (registeredTools.containsKey(toolName)) {
|
||||
logger.warn("工具已存在: {}", toolName);
|
||||
return;
|
||||
}
|
||||
|
||||
registeredTools.put(toolName, tool);
|
||||
logger.info("注册工具: {}", toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注销工具
|
||||
*/
|
||||
public void unregisterTool(String toolName) {
|
||||
Tool tool = registeredTools.get(toolName);
|
||||
if (tool == null) {
|
||||
logger.warn("工具不存在: {}", toolName);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果要注销的工具是当前工具,先停用它
|
||||
if (currentTool == tool) {
|
||||
switchToDefaultTool();
|
||||
}
|
||||
|
||||
tool.dispose();
|
||||
registeredTools.remove(toolName);
|
||||
logger.info("注销工具: {}", toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有注册的工具
|
||||
*/
|
||||
public List<Tool> getRegisteredTools() {
|
||||
return new ArrayList<>(registeredTools.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据名称获取工具
|
||||
*/
|
||||
public Tool getTool(String toolName) {
|
||||
return registeredTools.get(toolName);
|
||||
}
|
||||
|
||||
// ================== 工具切换管理 ==================
|
||||
|
||||
/**
|
||||
* 切换到指定工具
|
||||
*/
|
||||
public boolean switchTool(String toolName) {
|
||||
Tool targetTool = registeredTools.get(toolName);
|
||||
if (targetTool == null) {
|
||||
logger.warn("工具不存在: {}", toolName);
|
||||
return false;
|
||||
}
|
||||
|
||||
return switchTool(targetTool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到指定工具实例
|
||||
*/
|
||||
public boolean switchTool(Tool tool) {
|
||||
if (tool == null) {
|
||||
logger.warn("尝试切换到空工具");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查工具是否可用
|
||||
if (!tool.isAvailable()) {
|
||||
logger.warn("工具不可用: {}", tool.getToolName());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 如果已经是当前工具,直接返回
|
||||
if (currentTool == tool) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 停用当前工具
|
||||
if (currentTool != null) {
|
||||
currentTool.deactivate();
|
||||
previousTool = currentTool;
|
||||
}
|
||||
|
||||
// 激活新工具
|
||||
currentTool = tool;
|
||||
currentTool.activate();
|
||||
|
||||
// 更新光标
|
||||
updateCursor();
|
||||
|
||||
logger.info("切换到工具: {}", currentTool.getToolName());
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到默认工具
|
||||
*/
|
||||
public void switchToDefaultTool() {
|
||||
switchTool(defaultTool);
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到上一个工具
|
||||
*/
|
||||
public void switchToPreviousTool() {
|
||||
if (previousTool != null && previousTool.isAvailable()) {
|
||||
switchTool(previousTool);
|
||||
} else {
|
||||
switchToDefaultTool();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前工具
|
||||
*/
|
||||
public Tool getCurrentTool() {
|
||||
return currentTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一个工具
|
||||
*/
|
||||
public Tool getPreviousTool() {
|
||||
return previousTool;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认工具
|
||||
*/
|
||||
public Tool getDefaultTool() {
|
||||
return defaultTool;
|
||||
}
|
||||
|
||||
// ================== 事件转发 ==================
|
||||
|
||||
/**
|
||||
* 处理鼠标按下事件
|
||||
*/
|
||||
public void handleMousePressed(MouseEvent e, float modelX, float modelY) {
|
||||
if (currentTool != null) {
|
||||
currentTool.onMousePressed(e, modelX, modelY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标释放事件
|
||||
*/
|
||||
public void handleMouseReleased(MouseEvent e, float modelX, float modelY) {
|
||||
if (currentTool != null) {
|
||||
currentTool.onMouseReleased(e, modelX, modelY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标拖拽事件
|
||||
*/
|
||||
public void handleMouseDragged(MouseEvent e, float modelX, float modelY) {
|
||||
if (currentTool != null) {
|
||||
currentTool.onMouseDragged(e, modelX, modelY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标移动事件
|
||||
*/
|
||||
public void handleMouseMoved(MouseEvent e, float modelX, float modelY) {
|
||||
if (currentTool != null) {
|
||||
currentTool.onMouseMoved(e, modelX, modelY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标点击事件
|
||||
*/
|
||||
public void handleMouseClicked(MouseEvent e, float modelX, float modelY) {
|
||||
if (currentTool != null) {
|
||||
currentTool.onMouseClicked(e, modelX, modelY);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理鼠标双击事件
|
||||
*/
|
||||
public void handleMouseDoubleClicked(MouseEvent e, float modelX, float modelY) {
|
||||
if (currentTool != null) {
|
||||
currentTool.onMouseDoubleClicked(e, modelX, modelY);
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 工具状态管理 ==================
|
||||
|
||||
/**
|
||||
* 更新光标
|
||||
*/
|
||||
private void updateCursor() {
|
||||
if (currentTool != null) {
|
||||
Cursor cursor = currentTool.getToolCursor();
|
||||
if (cursor != null) {
|
||||
renderPanel.setCursor(cursor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有工具处于激活状态
|
||||
*/
|
||||
public boolean hasActiveTool() {
|
||||
return currentTool != null && currentTool.isActive();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用所有工具
|
||||
*/
|
||||
public void deactivateAllTools() {
|
||||
for (Tool tool : registeredTools.values()) {
|
||||
if (tool.isActive()) {
|
||||
tool.deactivate();
|
||||
}
|
||||
}
|
||||
currentTool = null;
|
||||
renderPanel.setCursor(Cursor.getDefaultCursor());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理所有工具资源
|
||||
*/
|
||||
public void dispose() {
|
||||
deactivateAllTools();
|
||||
for (Tool tool : registeredTools.values()) {
|
||||
tool.dispose();
|
||||
}
|
||||
registeredTools.clear();
|
||||
logger.info("工具管理器已清理");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具统计信息
|
||||
*/
|
||||
public String getToolStatistics() {
|
||||
int activeCount = 0;
|
||||
for (Tool tool : registeredTools.values()) {
|
||||
if (tool.isActive()) {
|
||||
activeCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return String.format("工具统计: 注册%d个, 激活%d个, 当前工具: %s",
|
||||
registeredTools.size(), activeCount,
|
||||
currentTool != null ? currentTool.getToolName() : "无");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.manager;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.ModelRender;
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import org.joml.Vector2f;
|
||||
|
||||
public class WorldManagement {
|
||||
private final ModelRenderPanel modelRenderPanel;
|
||||
private final GLContextManager glContextManager;
|
||||
|
||||
public WorldManagement(ModelRenderPanel modelRenderPanel, GLContextManager glContextManager) {
|
||||
this.modelRenderPanel = modelRenderPanel;
|
||||
this.glContextManager = glContextManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将屏幕坐标转换为模型坐标
|
||||
*/
|
||||
public float[] screenToModelCoordinates(int screenX, int screenY) {
|
||||
if (!glContextManager.isContextInitialized() || glContextManager.getWidth() <= 0 || glContextManager.getHeight() <= 0) return null;
|
||||
float glX = (float) screenX * glContextManager.getWidth() / modelRenderPanel.getWidth();
|
||||
float glY = (float) screenY * glContextManager.getHeight() / modelRenderPanel.getHeight();
|
||||
float ndcX = (2.0f * glX) / glContextManager.getWidth() - 1.0f;
|
||||
float ndcY = 1.0f - (2.0f * glY) / glContextManager.getHeight();
|
||||
Vector2f camOffset = ModelRender.getCameraOffset();
|
||||
float zoom = ModelRender.getCamera().getZoom();
|
||||
float modelX = (ndcX * glContextManager.getWidth() / (2.0f * zoom)) + camOffset.x;
|
||||
float modelY = (ndcY * glContextManager.getHeight() / (-2.0f * zoom)) + camOffset.y;
|
||||
return new float[]{modelX, modelY};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,400 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.tools;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.model.Model2D;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.tools.LiquifyTargetPartRander;
|
||||
import org.joml.Vector2f;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* 液化工具
|
||||
* 用于对网格进行液化变形操作
|
||||
*/
|
||||
public class LiquifyTool extends Tool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(LiquifyTool.class);
|
||||
|
||||
private ModelPart liquifyTargetPart = null;
|
||||
private Mesh2D liquifyTargetMesh = null;
|
||||
|
||||
private float liquifyBrushSize = 50.0f;
|
||||
private float liquifyBrushStrength = 2.0f;
|
||||
private ModelPart.LiquifyMode currentLiquifyMode = ModelPart.LiquifyMode.PUSH;
|
||||
|
||||
private boolean renderEnabled = false;
|
||||
|
||||
public LiquifyTool(ModelRenderPanel renderPanel) {
|
||||
super(renderPanel, "液化工具", "对网格进行液化变形操作");
|
||||
}
|
||||
|
||||
// ================== 启动和关闭方法 ==================
|
||||
|
||||
/**
|
||||
* 启动液化工具渲染
|
||||
* 启用液化覆盖层和顶点渲染
|
||||
*/
|
||||
public void startLiquifyRendering() {
|
||||
if (associatedRanderTools == null) {
|
||||
logger.warn("液化渲染器未初始化");
|
||||
return;
|
||||
}
|
||||
|
||||
// 启用渲染算法
|
||||
enableRenderingAlgorithms();
|
||||
renderEnabled = true;
|
||||
|
||||
logger.info("启动液化工具渲染");
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭液化工具渲染
|
||||
* 禁用所有液化相关的渲染效果
|
||||
*/
|
||||
public void stopLiquifyRendering() {
|
||||
if (associatedRanderTools == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 禁用渲染算法
|
||||
disableRenderingAlgorithms();
|
||||
renderEnabled = false;
|
||||
|
||||
logger.info("关闭液化工具渲染");
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用渲染算法
|
||||
*/
|
||||
private void enableRenderingAlgorithms() {
|
||||
if (associatedRanderTools != null) {
|
||||
// 使用正确的方法设置算法状态
|
||||
associatedRanderTools.setAlgorithmEnabled("showLiquifyOverlay", true);
|
||||
|
||||
// 根据当前状态决定是否显示顶点
|
||||
boolean showVertices = renderPanel.getKeyboardManager().getIsCtrlPressed();
|
||||
associatedRanderTools.setAlgorithmEnabled("isRenderVertices", showVertices);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 禁用渲染算法
|
||||
*/
|
||||
private void disableRenderingAlgorithms() {
|
||||
if (associatedRanderTools != null) {
|
||||
associatedRanderTools.setAlgorithmEnabled("showLiquifyOverlay", false);
|
||||
associatedRanderTools.setAlgorithmEnabled("isRenderVertices", false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 切换顶点显示状态
|
||||
*/
|
||||
public void toggleVertexRendering() {
|
||||
if (associatedRanderTools != null && renderEnabled) {
|
||||
boolean currentState = associatedRanderTools.isAlgorithmEnabled("isRenderVertices");
|
||||
associatedRanderTools.setAlgorithmEnabled("isRenderVertices", !currentState);
|
||||
logger.info("切换顶点显示状态: {}", !currentState);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置顶点显示状态
|
||||
*/
|
||||
public void setVertexRendering(boolean enabled) {
|
||||
if (associatedRanderTools != null && renderEnabled) {
|
||||
associatedRanderTools.setAlgorithmEnabled("isRenderVertices", enabled);
|
||||
logger.info("设置顶点显示状态: {}", enabled);
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 工具生命周期方法 ==================
|
||||
|
||||
@Override
|
||||
public void activate() {
|
||||
if (isActive) return;
|
||||
|
||||
isActive = true;
|
||||
|
||||
// 尝试获取选中的网格作为液化目标
|
||||
if (!renderPanel.getSelectedMeshes().isEmpty()) {
|
||||
liquifyTargetMesh = renderPanel.getSelectedMesh();
|
||||
liquifyTargetPart = renderPanel.findPartByMesh(liquifyTargetMesh);
|
||||
} else {
|
||||
// 如果没有选中的网格,尝试获取第一个可见网格
|
||||
liquifyTargetMesh = findFirstVisibleMesh();
|
||||
liquifyTargetPart = renderPanel.findPartByMesh(liquifyTargetMesh);
|
||||
}
|
||||
|
||||
if (liquifyTargetPart != null) {
|
||||
liquifyTargetPart.setStartLiquefy(true);
|
||||
// 启动渲染
|
||||
startLiquifyRendering();
|
||||
logger.info("激活液化工具: {}", liquifyTargetMesh != null ? liquifyTargetMesh.getName() : "null");
|
||||
} else {
|
||||
logger.warn("没有找到可用的网格用于液化");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
|
||||
// 停止渲染
|
||||
stopLiquifyRendering();
|
||||
|
||||
if (liquifyTargetPart != null) {
|
||||
liquifyTargetPart.setStartLiquefy(false);
|
||||
}
|
||||
liquifyTargetMesh = null;
|
||||
liquifyTargetPart = null;
|
||||
|
||||
logger.info("停用液化工具");
|
||||
}
|
||||
|
||||
// ================== 事件处理方法 ==================
|
||||
|
||||
@Override
|
||||
public void onMousePressed(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || liquifyTargetPart == null) return;
|
||||
|
||||
// 液化模式下,左键按下直接开始液化操作
|
||||
if (e.getButton() == MouseEvent.BUTTON1) {
|
||||
applyLiquifyEffect(modelX, modelY);
|
||||
}
|
||||
|
||||
// 右键可以切换顶点显示
|
||||
if (e.getButton() == MouseEvent.BUTTON3) {
|
||||
toggleVertexRendering();
|
||||
renderPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseReleased(MouseEvent e, float modelX, float modelY) {
|
||||
// 液化工具不需要特殊的释放处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseDragged(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || liquifyTargetPart == null) return;
|
||||
|
||||
// 液化模式下拖拽时连续应用液化效果
|
||||
if (e.getButton() == MouseEvent.BUTTON1) {
|
||||
applyLiquifyEffect(modelX, modelY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseMoved(MouseEvent e, float modelX, float modelY) {
|
||||
// 液化工具不需要特殊的移动处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseClicked(MouseEvent e, float modelX, float modelY) {
|
||||
// 单单击已在 pressed 中处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive) return;
|
||||
|
||||
// 双击空白处退出液化模式
|
||||
if (liquifyTargetPart == null || !isOverTargetMesh(modelX, modelY)) {
|
||||
// 切换到选择工具
|
||||
renderPanel.getToolManagement().switchToDefaultTool();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getToolCursor() {
|
||||
return createLiquifyCursor();
|
||||
}
|
||||
|
||||
// ================== 工具特定方法 ==================
|
||||
|
||||
/**
|
||||
* 应用液化效果
|
||||
*/
|
||||
private void applyLiquifyEffect(float modelX, float modelY) {
|
||||
if (liquifyTargetPart == null) return;
|
||||
|
||||
Vector2f brushCenter = new Vector2f(modelX, modelY);
|
||||
|
||||
// 判断是否按住Ctrl键,决定是否创建顶点
|
||||
boolean createVertices = renderPanel.getKeyboardManager().getIsCtrlPressed();
|
||||
|
||||
liquifyTargetPart.applyLiquify(brushCenter, liquifyBrushSize,
|
||||
liquifyBrushStrength, currentLiquifyMode, 1, createVertices);
|
||||
|
||||
// 强制重绘
|
||||
renderPanel.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在目标网格上
|
||||
*/
|
||||
private boolean isOverTargetMesh(float modelX, float modelY) {
|
||||
if (liquifyTargetMesh == null) return false;
|
||||
|
||||
// 更新边界框
|
||||
liquifyTargetMesh.updateBounds();
|
||||
return liquifyTargetMesh.containsPoint(modelX, modelY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找第一个可见的网格
|
||||
*/
|
||||
private Mesh2D findFirstVisibleMesh() {
|
||||
Model2D model = renderPanel.getModel();
|
||||
if (model == null) return null;
|
||||
|
||||
java.util.List<ModelPart> parts = model.getParts();
|
||||
if (parts == null || parts.isEmpty()) return null;
|
||||
|
||||
for (ModelPart part : parts) {
|
||||
if (part != null && part.isVisible()) {
|
||||
java.util.List<Mesh2D> meshes = part.getMeshes();
|
||||
if (meshes != null && !meshes.isEmpty()) {
|
||||
for (Mesh2D mesh : meshes) {
|
||||
if (mesh != null && mesh.isVisible()) {
|
||||
return mesh;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建液化模式光标
|
||||
*/
|
||||
private Cursor createLiquifyCursor() {
|
||||
// 创建自定义液化光标(圆圈)
|
||||
int size = 32;
|
||||
BufferedImage cursorImg = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = cursorImg.createGraphics();
|
||||
|
||||
// 设置抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制透明背景
|
||||
g2d.setColor(new Color(0, 0, 0, 0));
|
||||
g2d.fillRect(0, 0, size, size);
|
||||
|
||||
// 绘制圆圈
|
||||
int center = size / 2;
|
||||
int radius = (int) (liquifyBrushSize * 0.1f); // 根据画笔大小缩放光标
|
||||
|
||||
// 外圈
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.setStroke(new BasicStroke(2f));
|
||||
g2d.drawOval(center - radius, center - radius, radius * 2, radius * 2);
|
||||
|
||||
// 内圈
|
||||
g2d.setColor(new Color(255, 100, 100, 150));
|
||||
g2d.setStroke(new BasicStroke(1f));
|
||||
g2d.drawOval(center - radius / 2, center - radius / 2, radius, radius);
|
||||
|
||||
// 中心点
|
||||
g2d.setColor(Color.RED);
|
||||
g2d.fillOval(center - 2, center - 2, 4, 4);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "LiquifyCursor");
|
||||
}
|
||||
|
||||
// ================== 配置方法 ==================
|
||||
|
||||
/**
|
||||
* 设置液化画笔大小
|
||||
*/
|
||||
public void setLiquifyBrushSize(float size) {
|
||||
this.liquifyBrushSize = Math.max(1.0f, Math.min(500.0f, size));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化画笔强度
|
||||
*/
|
||||
public void setLiquifyBrushStrength(float strength) {
|
||||
this.liquifyBrushStrength = Math.max(0.0f, Math.min(2.0f, strength));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置液化模式
|
||||
*/
|
||||
public void setLiquifyMode(ModelPart.LiquifyMode mode) {
|
||||
this.currentLiquifyMode = mode;
|
||||
}
|
||||
|
||||
// ================== 获取工具状态 ==================
|
||||
|
||||
public ModelPart getLiquifyTargetPart() {
|
||||
return liquifyTargetPart;
|
||||
}
|
||||
|
||||
public Mesh2D getLiquifyTargetMesh() {
|
||||
return liquifyTargetMesh;
|
||||
}
|
||||
|
||||
public float getLiquifyBrushSize() {
|
||||
return liquifyBrushSize;
|
||||
}
|
||||
|
||||
public float getLiquifyBrushStrength() {
|
||||
return liquifyBrushStrength;
|
||||
}
|
||||
|
||||
public ModelPart.LiquifyMode getCurrentLiquifyMode() {
|
||||
return currentLiquifyMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取渲染器实例
|
||||
*/
|
||||
public LiquifyTargetPartRander getAssociatedRanderTools() {
|
||||
return (LiquifyTargetPartRander) associatedRanderTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查渲染是否启用
|
||||
*/
|
||||
public boolean isRenderEnabled() {
|
||||
return renderEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否显示顶点
|
||||
*/
|
||||
public boolean isVertexRenderingEnabled() {
|
||||
return associatedRanderTools != null &&
|
||||
associatedRanderTools.getAlgorithmEnabled().getOrDefault("isRenderVertices", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否显示液化覆盖层
|
||||
*/
|
||||
public boolean isLiquifyOverlayEnabled() {
|
||||
return associatedRanderTools != null &&
|
||||
associatedRanderTools.getAlgorithmEnabled().getOrDefault("showLiquifyOverlay", false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
// 清理资源
|
||||
stopLiquifyRendering();
|
||||
associatedRanderTools = null;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.tools;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.model.ModelPart;
|
||||
import com.chuangzhou.vivid2D.render.model.util.Mesh2D;
|
||||
import com.chuangzhou.vivid2D.render.model.util.PuppetPin;
|
||||
import com.chuangzhou.vivid2D.render.model.util.BoundingBox;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
/**
|
||||
* 木偶变形工具
|
||||
* 用于通过控制点对网格进行变形
|
||||
*/
|
||||
public class PuppetDeformationTool extends Tool {
|
||||
private static final Logger logger = LoggerFactory.getLogger(PuppetDeformationTool.class);
|
||||
|
||||
private Mesh2D targetMesh = null;
|
||||
private PuppetPin selectedPin = null;
|
||||
private PuppetPin hoveredPin = null;
|
||||
|
||||
private static final float PIN_TOLERANCE = 8.0f;
|
||||
private ModelRenderPanel.DragMode currentDragMode = ModelRenderPanel.DragMode.NONE;
|
||||
private float dragStartX, dragStartY;
|
||||
|
||||
public PuppetDeformationTool(ModelRenderPanel renderPanel) {
|
||||
super(renderPanel, "木偶变形工具", "通过控制点对网格进行变形操作");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate() {
|
||||
if (isActive) return;
|
||||
|
||||
// 检查是否有选中的网格
|
||||
if (renderPanel.getSelectedMeshes().isEmpty()) {
|
||||
logger.warn("请先选择一个网格以进入木偶变形工具模式");
|
||||
return;
|
||||
}
|
||||
|
||||
isActive = true;
|
||||
targetMesh = renderPanel.getSelectedMesh();
|
||||
|
||||
if (targetMesh != null) {
|
||||
// 显示木偶控制点
|
||||
associatedRanderTools.setAlgorithmEnabled("showPuppetPins", true);
|
||||
targetMesh.setShowPuppetPins(true);
|
||||
|
||||
// 如果没有木偶控制点,创建默认的四个角点
|
||||
if (targetMesh.getPuppetPinCount() == 0) {
|
||||
createDefaultPuppetPins();
|
||||
}
|
||||
|
||||
// 预计算权重
|
||||
targetMesh.precomputeAllPuppetWeights();
|
||||
}
|
||||
|
||||
logger.info("激活木偶变形工具: {}", targetMesh != null ? targetMesh.getName() : "null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deactivate() {
|
||||
if (!isActive) return;
|
||||
|
||||
isActive = false;
|
||||
if (targetMesh != null) {
|
||||
associatedRanderTools.setAlgorithmEnabled("showPuppetPins", false);
|
||||
targetMesh.setShowPuppetPins(false);
|
||||
}
|
||||
targetMesh = null;
|
||||
selectedPin = null;
|
||||
hoveredPin = null;
|
||||
|
||||
logger.info("停用木偶变形工具");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMousePressed(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || targetMesh == null) return;
|
||||
|
||||
// 选择木偶控制点
|
||||
PuppetPin clickedPin = findPuppetPinAtPosition(modelX, modelY);
|
||||
if (clickedPin != null) {
|
||||
targetMesh.setSelectedPuppetPin(clickedPin);
|
||||
selectedPin = clickedPin;
|
||||
|
||||
// 开始拖拽
|
||||
currentDragMode = ModelRenderPanel.DragMode.MOVE_PUPPET_PIN;
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
|
||||
logger.debug("开始移动木偶控制点: ID={}", clickedPin.getId());
|
||||
} else {
|
||||
// 点击空白处,取消选择
|
||||
targetMesh.setSelectedPuppetPin(null);
|
||||
selectedPin = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseReleased(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive) return;
|
||||
|
||||
currentDragMode = ModelRenderPanel.DragMode.NONE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseDragged(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || selectedPin == null) return;
|
||||
|
||||
if (currentDragMode == ModelRenderPanel.DragMode.MOVE_PUPPET_PIN) {
|
||||
selectedPin.setPosition(modelX, modelY);
|
||||
dragStartX = modelX;
|
||||
dragStartY = modelY;
|
||||
targetMesh.updateVerticesFromPuppetPins();
|
||||
targetMesh.markDirty();
|
||||
renderPanel.repaint();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseMoved(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || targetMesh == null) return;
|
||||
|
||||
// 更新悬停的木偶控制点
|
||||
PuppetPin newHoveredPin = findPuppetPinAtPosition(modelX, modelY);
|
||||
|
||||
if (newHoveredPin != hoveredPin) {
|
||||
hoveredPin = newHoveredPin;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseClicked(MouseEvent e, float modelX, float modelY) {
|
||||
// 单单击不需要特殊处理
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY) {
|
||||
if (!isActive || targetMesh == null) return;
|
||||
|
||||
// 检查是否双击了木偶控制点
|
||||
PuppetPin clickedPin = findPuppetPinAtPosition(modelX, modelY);
|
||||
if (clickedPin != null) {
|
||||
// 双击木偶控制点:删除该控制点
|
||||
deletePuppetPin(clickedPin);
|
||||
} else {
|
||||
// 双击空白处:创建新的木偶控制点
|
||||
createPuppetPinAt(modelX, modelY);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getToolCursor() {
|
||||
return createPuppetCursor();
|
||||
}
|
||||
|
||||
// ================== 工具特定方法 ==================
|
||||
|
||||
/**
|
||||
* 创建默认的四个角点木偶控制点
|
||||
*/
|
||||
private void createDefaultPuppetPins() {
|
||||
if (targetMesh == null) return;
|
||||
|
||||
BoundingBox bounds = targetMesh.getBounds();
|
||||
if (bounds == null || !bounds.isValid()) return;
|
||||
|
||||
float minX = bounds.getMinX();
|
||||
float minY = bounds.getMinY();
|
||||
float maxX = bounds.getMaxX();
|
||||
float maxY = bounds.getMaxY();
|
||||
|
||||
// 创建四个角点
|
||||
targetMesh.addPuppetPin(minX, minY, 0.0f, 1.0f); // 左下
|
||||
targetMesh.addPuppetPin(maxX, minY, 1.0f, 1.0f); // 右下
|
||||
targetMesh.addPuppetPin(maxX, maxY, 1.0f, 0.0f); // 右上
|
||||
targetMesh.addPuppetPin(minX, maxY, 0.0f, 0.0f); // 左上
|
||||
|
||||
logger.debug("为网格 {} 创建了4个默认木偶控制点", targetMesh.getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定位置创建木偶控制点
|
||||
*/
|
||||
private void createPuppetPinAt(float x, float y) {
|
||||
if (targetMesh == null) return;
|
||||
|
||||
// 计算UV坐标(基于边界框)
|
||||
BoundingBox bounds = targetMesh.getBounds();
|
||||
if (bounds == null || !bounds.isValid()) return;
|
||||
|
||||
float u = (x - bounds.getMinX()) / bounds.getWidth();
|
||||
float v = (y - bounds.getMinY()) / bounds.getHeight();
|
||||
|
||||
// 限制UV在0-1范围内
|
||||
u = Math.max(0.0f, Math.min(1.0f, u));
|
||||
v = Math.max(0.0f, Math.min(1.0f, v));
|
||||
|
||||
PuppetPin newPin = targetMesh.addPuppetPin(x, y, u, v);
|
||||
logger.info("创建木偶控制点: ID={}, 位置({}, {}), UV({}, {})",
|
||||
newPin.getId(), x, y, u, v);
|
||||
|
||||
renderPanel.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除木偶控制点
|
||||
*/
|
||||
private void deletePuppetPin(PuppetPin pin) {
|
||||
if (targetMesh == null || pin == null) return;
|
||||
|
||||
boolean removed = targetMesh.removePuppetPin(pin);
|
||||
if (removed) {
|
||||
if (selectedPin == pin) {
|
||||
selectedPin = null;
|
||||
}
|
||||
if (hoveredPin == pin) {
|
||||
hoveredPin = null;
|
||||
}
|
||||
logger.info("删除木偶控制点: ID={}", pin.getId());
|
||||
}
|
||||
|
||||
renderPanel.repaint();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在指定位置查找木偶控制点
|
||||
*/
|
||||
private PuppetPin findPuppetPinAtPosition(float x, float y) {
|
||||
if (targetMesh == null) return null;
|
||||
|
||||
float tolerance = PIN_TOLERANCE / calculateScaleFactor();
|
||||
return targetMesh.selectPuppetPinAt(x, y, tolerance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算当前缩放因子
|
||||
*/
|
||||
private float calculateScaleFactor() {
|
||||
return renderPanel.getCameraManagement().calculateScaleFactor();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建木偶工具光标
|
||||
*/
|
||||
private Cursor createPuppetCursor() {
|
||||
int size = 32;
|
||||
BufferedImage cursorImg = new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
|
||||
Graphics2D g2d = cursorImg.createGraphics();
|
||||
|
||||
// 设置抗锯齿
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
|
||||
// 绘制透明背景
|
||||
g2d.setColor(new Color(0, 0, 0, 0));
|
||||
g2d.fillRect(0, 0, size, size);
|
||||
|
||||
// 绘制木偶图标
|
||||
int center = size / 2;
|
||||
|
||||
// 外圈(蓝色)
|
||||
g2d.setColor(Color.BLUE);
|
||||
g2d.setStroke(new BasicStroke(2f));
|
||||
g2d.drawOval(center - 6, center - 6, 12, 12);
|
||||
|
||||
// 内圈
|
||||
g2d.setColor(new Color(0, 0, 200, 150));
|
||||
g2d.setStroke(new BasicStroke(1f));
|
||||
g2d.drawOval(center - 3, center - 3, 6, 6);
|
||||
|
||||
// 中心点
|
||||
g2d.setColor(Color.BLUE);
|
||||
g2d.fillOval(center - 1, center - 1, 2, 2);
|
||||
|
||||
g2d.dispose();
|
||||
|
||||
return Toolkit.getDefaultToolkit().createCustomCursor(cursorImg, new Point(center, center), "PuppetCursor");
|
||||
}
|
||||
|
||||
// ================== 获取工具状态 ==================
|
||||
|
||||
public Mesh2D getTargetMesh() {
|
||||
return targetMesh;
|
||||
}
|
||||
|
||||
public PuppetPin getSelectedPin() {
|
||||
return selectedPin;
|
||||
}
|
||||
|
||||
public PuppetPin getHoveredPin() {
|
||||
return hoveredPin;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
147
src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java
Normal file
147
src/main/java/com/chuangzhou/vivid2D/render/awt/tools/Tool.java
Normal file
@@ -0,0 +1,147 @@
|
||||
package com.chuangzhou.vivid2D.render.awt.tools;
|
||||
|
||||
import com.chuangzhou.vivid2D.render.awt.ModelRenderPanel;
|
||||
import com.chuangzhou.vivid2D.render.model.util.tools.RanderTools;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
|
||||
/**
|
||||
* 工具抽象基类
|
||||
* 所有编辑工具都应继承此类
|
||||
*/
|
||||
public abstract class Tool {
|
||||
protected ModelRenderPanel renderPanel;
|
||||
protected String toolName;
|
||||
protected String toolDescription;
|
||||
protected boolean isActive = false;
|
||||
|
||||
// 关联的渲染工具对象
|
||||
protected RanderTools associatedRanderTools;
|
||||
|
||||
public Tool(ModelRenderPanel renderPanel, String toolName, String toolDescription) {
|
||||
this.renderPanel = renderPanel;
|
||||
this.toolName = toolName;
|
||||
this.toolDescription = toolDescription;
|
||||
}
|
||||
|
||||
// ================== 生命周期方法 ==================
|
||||
|
||||
/**
|
||||
* 激活工具
|
||||
*/
|
||||
public abstract void activate();
|
||||
|
||||
/**
|
||||
* 停用工具
|
||||
*/
|
||||
public abstract void deactivate();
|
||||
|
||||
/**
|
||||
* 工具是否处于激活状态
|
||||
*/
|
||||
public boolean isActive() {
|
||||
return isActive;
|
||||
}
|
||||
|
||||
// ================== 事件处理方法 ==================
|
||||
|
||||
/**
|
||||
* 处理鼠标按下事件
|
||||
*/
|
||||
public abstract void onMousePressed(MouseEvent e, float modelX, float modelY);
|
||||
|
||||
/**
|
||||
* 处理鼠标释放事件
|
||||
*/
|
||||
public abstract void onMouseReleased(MouseEvent e, float modelX, float modelY);
|
||||
|
||||
/**
|
||||
* 处理鼠标拖拽事件
|
||||
*/
|
||||
public abstract void onMouseDragged(MouseEvent e, float modelX, float modelY);
|
||||
|
||||
/**
|
||||
* 处理鼠标移动事件
|
||||
*/
|
||||
public abstract void onMouseMoved(MouseEvent e, float modelX, float modelY);
|
||||
|
||||
/**
|
||||
* 处理鼠标点击事件
|
||||
*/
|
||||
public abstract void onMouseClicked(MouseEvent e, float modelX, float modelY);
|
||||
|
||||
/**
|
||||
* 处理鼠标双击事件
|
||||
*/
|
||||
public abstract void onMouseDoubleClicked(MouseEvent e, float modelX, float modelY);
|
||||
|
||||
// ================== 工具状态方法 ==================
|
||||
|
||||
/**
|
||||
* 获取工具名称
|
||||
*/
|
||||
public String getToolName() {
|
||||
return toolName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具描述
|
||||
*/
|
||||
public String getToolDescription() {
|
||||
return toolDescription;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具光标
|
||||
*/
|
||||
public abstract Cursor getToolCursor();
|
||||
|
||||
/**
|
||||
* 工具是否可用
|
||||
*/
|
||||
public boolean isAvailable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理工具资源
|
||||
*/
|
||||
public void dispose() {
|
||||
// 子类可重写此方法清理资源
|
||||
if (associatedRanderTools != null) {
|
||||
associatedRanderTools = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ================== 新增方法:与RanderToolsManager集成 ==================
|
||||
|
||||
/**
|
||||
* 设置关联的渲染工具
|
||||
* @param randerTools 渲染工具对象
|
||||
*/
|
||||
public void setAssociatedRanderTools(RanderTools randerTools) {
|
||||
this.associatedRanderTools = randerTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取关联的渲染工具
|
||||
* @return 关联的渲染工具对象,可能为null
|
||||
*/
|
||||
public RanderTools getAssociatedRanderTools() {
|
||||
return associatedRanderTools;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有关联的渲染工具
|
||||
* @return true如果有关联的渲染工具
|
||||
*/
|
||||
public boolean hasAssociatedRanderTools() {
|
||||
return associatedRanderTools != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return toolName;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user