帆软报表设计器源代码。
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.
 
 
 
 

572 lines
19 KiB

package com.fr.design.jxbrowser;
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.InjectJsCallback;
import com.teamdev.jxbrowser.chromium.events.LoadListener;
import com.teamdev.jxbrowser.chromium.events.ScriptContextListener;
import com.teamdev.jxbrowser.event.Observer;
import com.teamdev.jxbrowser.frame.Frame;
import com.teamdev.jxbrowser.js.JsObject;
import com.teamdev.jxbrowser.view.swing.BrowserView;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.SwingUtilities;
import java.awt.BorderLayout;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
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 String COLON = ":";
private static final String COLON_ESCAPE = "\\:";
private Browser browser;
private String namespace = "Pool";
private String variable = "data";
private String expression = "update()";
private JxUIPane() {
super();
}
private void initialize() {
setLayout(new BorderLayout());
if (browser != null) {
return;
}
initDebugIfNeeded();
// 使用公共引擎创建浏览器
browser = JxEngine.getPublicEngineInstance().newBrowser();
add(BrowserView.newInstance(browser), BorderLayout.CENTER);
}
/**
* 按需初始化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 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 新的地址
*/
@Override
public void redirect(String url) {
browser.navigation().loadUrl(encodeWindowsPath(url));
}
/**
* 转向一个新的地址,相当于重新加载
*
* @param url 新的地址
* @param map 初始化参数
*/
@Override
public void redirect(String url, Map<String, String> map) {
setMap(map);
browser.navigation().loadUrl(encodeWindowsPath(url));
}
private void setMap(Map<String, String> map) {
JxEngine.getInstance().setMap(map);
}
private void setComponent(AssembleComponent component) {
JxEngine.getInstance().setComponent(component);
}
@Override
protected String title4PopupWindow() {
return "ModernUI7";
}
@Override
public void populate(final T t) {
setInjectJsCallback(params -> {
executeJsObject(params.frame(), WINDOW + DOT + namespace)
.ifPresent(ns -> ns.putProperty(variable, t));
return InjectJsCallback.Response.proceed();
});
}
@Override
@Nullable
public T update() {
if (browser.mainFrame().isPresent()) {
return browser.mainFrame().get().executeJavaScript("window." + namespace + "." + expression);
}
return null;
}
/**
* 关闭浏览器
*/
public void disposeBrowser() {
if (browser != null) {
browser.close();
browser = null;
JxEngine.getInstance().clearMap();
JxEngine.getInstance().clearComponent();
}
}
/**
* 清理浏览器缓存
*/
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对象
* 注意:类内部使用,用于简化编码,提供 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 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.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;
}
/**
* 注入一个回调,回调的js会在初始化进行执行
*
* @param callback 回调
* @return builder
*/
public Builder<T> prepare(InjectJsCallback callback) {
this.callback = callback;
return this;
}
@Override
public Builder<T> prepareForV6(ScriptContextListener contextListener) {
return this;
}
@Override
public Builder<T> prepareForV6(LoadListener loadListener) {
return this;
}
@Override
public JxUIPane.Builder<T> prepareForV7(InjectJsCallback callback) {
prepare(callback);
return this;
}
@Override
public JxUIPane.Builder<T> prepareForV7(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<>();
pane.namespace = namespace;
pane.variable = variable;
pane.expression = expression;
pane.setMap(parameterMap);
pane.setComponent(component);
pane.initialize();
injectJs(pane);
if (!Objects.isNull(listenerPair)) {
pane.browser.navigation().on(listenerPair.getFirst(), listenerPair.getSecond());
}
if (StringUtils.isNotEmpty(this.url)) {
pane.browser.navigation().loadUrl(encodeWindowsPath(this.url));
} else if (StringUtils.isNotEmpty(this.html)) {
pane.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();
});
}
}
}