帆软报表设计器源代码。
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

691 lines
23 KiB

package com.fr.design.jxbrowser;
import com.fr.concurrent.NamedThreadFactory;
import com.fr.design.DesignerEnvManager;
import com.fr.design.gui.ibutton.UIButton;
import com.fr.design.gui.itoolbar.UIToolbar;
import com.fr.design.i18n.Toolkit;
import com.fr.design.ui.ModernUIConstants;
import com.fr.design.ui.ModernUIPane;
import com.fr.stable.StringUtils;
import com.fr.stable.collections.combination.Pair;
import com.fr.stable.os.OperatingSystem;
import com.fr.web.struct.AssembleComponent;
import com.teamdev.jxbrowser.browser.Browser;
import com.teamdev.jxbrowser.browser.callback.CertificateErrorCallback;
import com.teamdev.jxbrowser.browser.callback.InjectJsCallback;
import com.teamdev.jxbrowser.event.Observer;
import com.teamdev.jxbrowser.frame.Frame;
import com.teamdev.jxbrowser.js.JsObject;
import com.teamdev.jxbrowser.net.callback.BeforeStartTransactionCallback;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import java.awt.BorderLayout;
import java.awt.Desktop;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
import static com.fine.swing.ui.layout.Layouts.cell;
import static com.fine.swing.ui.layout.Layouts.column;
import static com.fine.swing.ui.layout.Layouts.flex;
import static com.fine.swing.ui.layout.Layouts.row;
import static com.fr.design.ui.ModernUIConstants.COMPONENT_TAG;
import static com.fr.design.ui.ModernUIConstants.DEFAULT_EXPRESSION;
import static com.fr.design.ui.ModernUIConstants.DEFAULT_NAMESPACE;
import static com.fr.design.ui.ModernUIConstants.DEFAULT_VARIABLE;
import static com.fr.design.ui.ModernUIConstants.DOT;
import static com.fr.design.ui.ModernUIConstants.EMB_TAG;
import static com.fr.design.ui.ModernUIConstants.SCHEME_HEADER;
import static com.fr.design.ui.ModernUIConstants.WINDOW;
/**
* 基于v7 jxbrowser 实现
* 用于加载 html5 的Swing容器,可以在设计选项设置中打开调试窗口,
* 示例可查看:com.fr.design.ui.JxUIPaneTest
*
* @author vito
* @since 11.0
* Created on 2023-06-12
*/
public class JxUIPane<T> extends ModernUIPane<T> {
public static final ExecutorService DEFAULT_EXECUTOR =
Executors.newSingleThreadExecutor(new NamedThreadFactory("jx-simple", true));
/**
* 冒号
*/
public static final String COLON = ":";
private static final String COLON_ESCAPE = "\\:";
private Browser browser;
private String namespace = "Pool";
private String variable = "data";
private String expression = "update()";
private final JxEngine jxEngine;
private Consumer<Browser> initCallback = null;
private JxUIPane(JxEngine jxEngine) {
this.jxEngine = jxEngine;
}
private void initialize(Consumer<Browser> consumer) {
setLayout(new BorderLayout());
if (browser != null) {
return;
}
hackInITInnovationLinuxDesktop();
initCallback = consumer;
initDebugIfNeeded();
asyncInitBrowser();
}
/**
* 启动 jxbrowser 引擎,过程包含解压文件等过程,异步操作。
*/
private void asyncInitBrowser() {
JProgressBar jProgressBar = showProgressBar();
new SwingWorker<Browser, Void>() {
@Override
protected Browser doInBackground() {
browser = jxEngine.getEngine().newBrowser();
if (jxEngine.isDisableWebSecurity()) {
// 忽略证书验证,兼容有些情况下自定义证书与实际域名不匹配的情况。
// 虽然不是个正确的方式,但真有这么用的还是兼容一下
browser.set(CertificateErrorCallback.class, (params, action) -> action.allow());
}
return browser;
}
@Override
protected void done() {
jProgressBar.setVisible(false);
try {
Browser mBrowser = get();
add(BrowserView.newInstance(mBrowser), BorderLayout.CENTER);
if (initCallback != null) {
initCallback.accept(mBrowser);
}
initCallback = null;
revalidate();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
}
}.execute();
}
/**
* hack:部分 Linux 信创桌面打开需要先初始化 Desktop
*/
private static void hackInITInnovationLinuxDesktop() {
if (OperatingSystem.isLinux()) {
Desktop.getDesktop();
}
}
/**
* 加载组件时显示一个进度条
*/
private @NotNull JProgressBar showProgressBar() {
JProgressBar jProgressBar = new JProgressBar();
jProgressBar.setIndeterminate(true);
add(row(
flex(),
column(flex(), cell(jProgressBar), flex()),
flex()
).getComponent(), BorderLayout.CENTER);
return jProgressBar;
}
/**
* 添加自定义XHR请求头,只在自定义引擎下生效,
* 公共引擎暂不支持
*
* @param headers 自定义头
*/
public void addXHRHeaders(Map<String, String> headers) {
warpCallback(browser -> {
if (JxEngine.getInstance() != jxEngine) {
jxEngine.addXHRHeaders(headers);
}
});
}
/**
* 异步链式调用
*
* @param then 后续任务
*/
private void warpCallback(Consumer<Browser> then) {
if (initCallback != null) {
initCallback = initCallback.andThen(then);
} else {
then.accept(browser);
}
}
/**
* 按需初始化debug界面UI
*/
private void initDebugIfNeeded() {
if (DesignerEnvManager.getEnvManager().isOpenDebug()) {
UIToolbar toolbar = new UIToolbar();
add(toolbar, BorderLayout.NORTH);
UIButton openDebugButton = new UIButton(Toolkit.i18nText("Fine-Design_Basic_Open_Debug_Window"));
openDebugButton.addActionListener(e -> browser.devTools().show());
toolbar.add(openDebugButton);
UIButton reloadButton = new UIButton(Toolkit.i18nText("Fine-Design_Basic_Reload"));
reloadButton.addActionListener(e -> browser.navigation().reloadIgnoringCache());
toolbar.add(reloadButton);
UIButton closeButton = new UIButton(Toolkit.i18nText("Fine-Design_Basic_Close_Window"));
closeButton.addActionListener(e -> SwingUtilities.getWindowAncestor(JxUIPane.this).setVisible(false));
toolbar.add(closeButton);
}
}
/**
* 在初始化时进行注入JS的方法,只被build调用
*
* @param injectJsCallback 回调
*/
private void initInjectJs(InjectJsCallback injectJsCallback) {
browser.set(InjectJsCallback.class, params -> {
// 初始化的时候,就把命名空间对象初始化好,确保window.a.b.c("a.b.c"为命名空间)对象都是初始化过的
params.frame().executeJavaScript(String.format(ModernUIConstants.SCRIPT_INIT_NAME_SPACE, namespace));
return injectJsCallback.on(params);
});
}
/**
* 设置 InjectJsCallback。
* 这个方法解决重复InjectJsCallback被覆盖的问题。
* 用于本类内部方法使用,如{@link #populate(Object)}
*
* @param injectJsCallback 回调
*/
private synchronized void setInjectJsCallback(InjectJsCallback injectJsCallback) {
Optional<InjectJsCallback> callback = browser.get(InjectJsCallback.class);
if (callback.isPresent()) {
browser.set(InjectJsCallback.class, params -> {
callback.get().on(params);
return injectJsCallback.on(params);
});
} else {
browser.set(InjectJsCallback.class, injectJsCallback);
}
}
/**
* 转向一个新的地址,相当于重新加载
*
* @param url 新的地址
*/
public void redirect(String url) {
warpCallback(browser -> browser.navigation().loadUrl(encodeWindowsPath(url)));
}
/**
* 转向一个新的地址,相当于重新加载
*
* @param url 新的地址
* @param map 初始化参数
*/
public void redirect(String url, Map<String, String> map) {
setMap(map);
warpCallback(browser -> browser.navigation().loadUrl(encodeWindowsPath(url)));
}
private void setMap(Map<String, String> map) {
jxEngine.setMap(map);
}
private void setComponent(AssembleComponent component) {
jxEngine.setComponent(component);
}
@Override
protected String title4PopupWindow() {
return "ModernUI7";
}
/**
* 更新数据到界面
*
* @param t 数据类
*/
public void populate(final T t) {
warpCallback(browser -> setInjectJsCallback(params -> {
executeJsObject(params.frame(), WINDOW + DOT + namespace)
.ifPresent(ns -> ns.putProperty(variable, t));
return InjectJsCallback.Response.proceed();
}));
}
@Nullable
public T update() {
if (browser.mainFrame().isPresent()) {
return browser.mainFrame().get().executeJavaScript(WINDOW + DOT + namespace + DOT + expression);
}
return null;
}
/**
* 关闭浏览器
*/
public void disposeBrowser() {
if (browser != null) {
browser.close();
browser = null;
jxEngine.clearMap();
jxEngine.clearComponent();
jxEngine.getEngine().network().remove(BeforeStartTransactionCallback.class);
}
}
/**
* 清理浏览器缓存
*/
public void clearCache() {
if (browser != null) {
browser.engine().httpCache().clear();
}
}
/**
* 执行一段js
*
* @param javaScript 待执行的js脚本
* @see JxUIPane#executeJS(String)
*/
public void executeJavaScript(String javaScript) {
if (browser != null) {
browser.mainFrame().ifPresent(frame -> frame.executeJavaScript(javaScript));
}
}
/**
* 执行一段js
*
* @param javaScript 待执行的js脚本
* @see JxUIPane#executeJS(String)
*/
public <T> void executeJavaScript(String javaScript, Consumer<T> consumer) {
if (browser != null) {
browser.mainFrame().ifPresent(frame -> frame.executeJavaScript(javaScript, consumer));
}
}
/**
* 获取js对象
* 注意:类内部使用,用于简化编码,提供 Optional 包装
*
* @param frame 页面frame对象
* @param name 变量命名
* @return js对象
*/
private static Optional<JsObject> executeJsObject(Frame frame, String name) {
return Optional.ofNullable(frame.executeJavaScript(name));
}
/**
* 执行js脚本并返回,使用范围包含{@link JxUIPane#executeJavaScript(String)},可以代替使用
*
* @param name 变量命名
* @return js对象
*/
public <P> Optional<P> executeJS(String name) {
if (browser != null) {
Optional<Frame> frame = browser.mainFrame();
if (frame.isPresent()) {
return Optional.ofNullable(frame.get().executeJavaScript(name));
}
}
return Optional.empty();
}
/**
* 由于自定义scheme目前走的是url,因此路径会被自动转化,比如windows路径下对冒号问题
* C:\\abc 变成 /C/abc,这里对冒号进行编码转义
*/
private static String encodeWindowsPath(String path) {
if (OperatingSystem.isWindows() && path.startsWith(EMB_TAG + SCHEME_HEADER)) {
String s = path.split(EMB_TAG + SCHEME_HEADER)[1];
return EMB_TAG + SCHEME_HEADER + s.replace(COLON, COLON_ESCAPE);
}
return path;
}
/**
* JxUIPane 的建造者
*
* @param <T> 参数
*/
public static class Builder<T> extends ModernUIPane.Builder<T> {
private JxEngine jxEngine;
private String namespace;
private String variable;
private String expression;
private InjectJsCallback callback;
private Pair<Class, Observer> listenerPair;
private final Map<String, Object> namespacePropertyMap;
private final Map<String, Object> propertyMap;
private final Map<String, PropertyBuild> buildPropertyMap;
private Object variableProperty;
private Map<String, String> parameterMap;
private AssembleComponent component;
private String url;
private String html;
public Builder() {
// 为了兼容继承关系,但又不允许创建,用这个方式先处理一下
super((ModernUIPane<T>) null);
this.jxEngine = JxEngine.getInstance();
this.namespace = DEFAULT_NAMESPACE;
this.variable = DEFAULT_VARIABLE;
this.expression = DEFAULT_EXPRESSION;
this.callback = null;
this.listenerPair = null;
this.namespacePropertyMap = new HashMap<>();
this.propertyMap = new HashMap<>();
this.buildPropertyMap = new HashMap<>();
this.variableProperty = null;
this.parameterMap = null;
this.component = null;
this.url = StringUtils.EMPTY;
this.html = StringUtils.EMPTY;
}
/**
* 自定义引擎
*
* @param jxEngine 引擎
* @return builder
*/
public Builder<T> engine(JxEngine jxEngine) {
this.jxEngine = jxEngine;
return this;
}
/**
* 注入一个回调,回调的js会在初始化进行执行
*
* @param callback 回调
* @return builder
*/
public Builder<T> prepare(InjectJsCallback callback) {
this.callback = callback;
return this;
}
/**
* 注册一个监听器
*
* @param event 事件
* @param listener 监听器
* @return builder
*/
public JxUIPane.Builder<T> prepare(Class event, Observer listener) {
listenerPair = new Pair<>(event, listener);
return this;
}
/**
* 加载jar包中的资源
*
* @param path 资源路径
*/
public JxUIPane.Builder<T> withEMB(final String path) {
this.url = EMB_TAG + SCHEME_HEADER + path;
return this;
}
/**
* 加载jar包中的资源
*
* @param path 资源路径
*/
public JxUIPane.Builder<T> withEMB(final String path, Map<String, String> map) {
this.parameterMap = map;
this.url = EMB_TAG + SCHEME_HEADER + path;
return this;
}
/**
* 加载url指向的资源
*
* @param url 文件的地址
*/
public JxUIPane.Builder<T> withURL(final String url) {
this.url = url;
return this;
}
/**
* 加载url指向的资源
*
* @param url 文件的地址
*/
public JxUIPane.Builder<T> withURL(final String url, Map<String, String> map) {
this.parameterMap = map;
this.url = url;
return this;
}
/**
* 加载Atom组件
*
* @param component Atom组件
*/
public JxUIPane.Builder<T> withComponent(AssembleComponent component) {
return withComponent(component, null);
}
/**
* 加载Atom组件
*
* @param component Atom组件
*/
public JxUIPane.Builder<T> withComponent(AssembleComponent component, Map<String, String> map) {
this.parameterMap = map;
this.component = component;
this.url = COMPONENT_TAG;
return this;
}
/**
* 加载html文本内容
*
* @param html 要加载html文本内容
*/
public JxUIPane.Builder<T> withHTML(String html) {
this.html = html;
return this;
}
/**
* 设置该前端页面做数据交换所使用的对象
* 相当于:
* const namespace = "Pool";
* 调用:
* window[namespace];
* 默认下结构如:
* window.Pool
*
* @param namespace 对象名
*/
public JxUIPane.Builder<T> namespace(String namespace) {
this.namespace = namespace;
return this;
}
/**
* java端往js端传数据时使用的变量名字
* 默认值为 data
* 相当于:
* const variable = "data";
* 调用:
* window[namespace][variable];
* 默认下结构如:
* window.Pool.data
*
* @param name 变量的名字
*/
public JxUIPane.Builder<T> variable(String name) {
this.variable = name;
return this;
}
/**
* js端往java端传数据时执行的函数表达式
*
* @param expression 函数表达式
*/
public JxUIPane.Builder<T> expression(String expression) {
this.expression = expression;
return this;
}
/**
* 注入一个java对象到js中,绑定在全局变量window的指定变量variable。
* variable 可由 {@link #namespace(String)} 设置,默认值为 data
* 这个方法仅在在加载的网页上执行 JavaScript 之前注入
* 相当于:
* window[namespace][property] = javaObject
* 默认下:
* window.Pool[property] = javaObject
*
* @param obj java对象
* @return 链式对象
*/
public JxUIPane.Builder<T> bindNamespace(String property, @Nullable Object obj) {
this.namespacePropertyMap.put(property, obj);
return this;
}
/**
* 注入一个java对象到js中,绑定在全局变量window的指定变量variable。
* variable 可由 {@link #variable(String)} 设置,默认值为 data
* 这个方法仅在在加载的网页上执行 JavaScript 之前注入
* 相当于:
* window[namespace][variable] = javaObject
* 默认下:
* window.Pool.data = javaObject
*
* @param obj java对象
* @return 链式对象
*/
public JxUIPane.Builder<T> bindVariable(@NotNull Object obj) {
this.variableProperty = obj;
return this;
}
/**
* 注入一个java对象到js中,绑定在全局变量 window的
* property指定的变量。这个方法仅在在加载的网页上执
* 行 JavaScript 之前注入
* 相当于:
* window[property] = javaObject
*
* @param property 属性
* @param obj java对象
* @return 链式对象
* @see #bindWindow(String, PropertyBuild)
*/
public JxUIPane.Builder<T> bindWindow(String property, @Nullable Object obj) {
this.propertyMap.put(property, obj);
return this;
}
/**
* 注入一个java对象到js中。绑定在全局变量 window的property指定的变量。
* PropertyBuild用于动态生成绑定属性。个方法仅在在加载的网页上执行
* JavaScript 之前注入
* 相当于:
* window[property] = javaObject
*
* @param property 属性构建器
* @param obj java对象
* @return 链式对象
* @see #bindWindow(String, Object)
*/
public JxUIPane.Builder<T> bindWindow(String property, PropertyBuild obj) {
buildPropertyMap.put(property, obj);
return this;
}
/**
* 构建
*/
public JxUIPane<T> build() {
JxUIPane<T> pane = new JxUIPane<>(jxEngine);
pane.namespace = namespace;
pane.variable = variable;
pane.expression = expression;
pane.setMap(parameterMap);
pane.setComponent(component);
pane.initialize(browser -> {
injectJs(pane);
if (!Objects.isNull(listenerPair)) {
browser.navigation().on(listenerPair.getFirst(), listenerPair.getSecond());
}
if (StringUtils.isNotEmpty(url)) {
browser.navigation().loadUrl(encodeWindowsPath(url));
} else if (StringUtils.isNotEmpty(html)) {
browser.mainFrame().ifPresent(f -> f.loadHtml(html));
}
});
return pane;
}
/**
* 由于 InjectJsCallback 的回调机制,在初始化期间,只有
* 在 InjectJsCallback 中 putProperty 才能生效。
* 因此,嵌套回调分别做默认初始化、putProperty、外置初始化
*/
private void injectJs(JxUIPane<T> pane) {
pane.initInjectJs(params -> {
Frame frame = params.frame();
if (!propertyMap.isEmpty()) {
propertyMap.forEach((key, value) ->
executeJsObject(frame, WINDOW)
.ifPresent(window -> window.putProperty(key, value)));
}
if (!buildPropertyMap.isEmpty()) {
buildPropertyMap.forEach((key, value) ->
executeJsObject(frame, WINDOW)
.ifPresent(window -> window.putProperty(key, value.build(window))));
}
if (!namespacePropertyMap.isEmpty()) {
namespacePropertyMap.forEach((key, value) ->
executeJsObject(frame, WINDOW + DOT + namespace)
.ifPresent(pool -> pool.putProperty(key, value)));
}
if (variableProperty != null) {
executeJsObject(frame, WINDOW + DOT + namespace)
.ifPresent(pool -> pool.putProperty(variable, variableProperty));
}
if (callback != null) {
return callback.on(params);
}
return InjectJsCallback.Response.proceed();
});
}
}
}