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 extends ModernUIPane { /** * 冒号 */ 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 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 map) { setMap(map); browser.navigation().loadUrl(encodeWindowsPath(url)); } private void setMap(Map 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 executeJsObject(Frame frame, String name) { return Optional.ofNullable(frame.executeJavaScript(name)); } /** * 执行js脚本并返回,使用范围包含{@link JxUIPane#executeJavaScript(String)},可以代替使用 * * @param name 变量命名 * @return js对象 */ public

Optional

executeJS(String name) { if (browser != null) { Optional 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 参数 */ public static class Builder extends ModernUIPane.Builder { private String namespace; private String variable; private String expression; private InjectJsCallback callback; private Pair listenerPair; private final Map namespacePropertyMap; private final Map propertyMap; private final Map buildPropertyMap; private Object variableProperty; private Map parameterMap; private AssembleComponent component; private String url; private String html; public Builder() { // 为了兼容继承关系,但又不允许创建,用这个方式先处理一下 super((ModernUIPane) 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 prepare(InjectJsCallback callback) { this.callback = callback; return this; } @Override public Builder prepareForV6(ScriptContextListener contextListener) { return this; } @Override public Builder prepareForV6(LoadListener loadListener) { return this; } @Override public JxUIPane.Builder prepareForV7(InjectJsCallback callback) { prepare(callback); return this; } @Override public JxUIPane.Builder prepareForV7(Class event, Observer listener) { listenerPair = new Pair<>(event, listener); return this; } /** * 加载jar包中的资源 * * @param path 资源路径 */ public JxUIPane.Builder withEMB(final String path) { this.url = EMB_TAG + SCHEME_HEADER + path; return this; } /** * 加载jar包中的资源 * * @param path 资源路径 */ public JxUIPane.Builder withEMB(final String path, Map map) { this.parameterMap = map; this.url = EMB_TAG + SCHEME_HEADER + path; return this; } /** * 加载url指向的资源 * * @param url 文件的地址 */ public JxUIPane.Builder withURL(final String url) { this.url = url; return this; } /** * 加载url指向的资源 * * @param url 文件的地址 */ public JxUIPane.Builder withURL(final String url, Map map) { this.parameterMap = map; this.url = url; return this; } /** * 加载Atom组件 * * @param component Atom组件 */ public JxUIPane.Builder withComponent(AssembleComponent component) { return withComponent(component, null); } /** * 加载Atom组件 * * @param component Atom组件 */ public JxUIPane.Builder withComponent(AssembleComponent component, Map map) { this.parameterMap = map; this.component = component; this.url = COMPONENT_TAG; return this; } /** * 加载html文本内容 * * @param html 要加载html文本内容 */ public JxUIPane.Builder withHTML(String html) { this.html = html; return this; } /** * 设置该前端页面做数据交换所使用的对象 * 相当于: * const namespace = "Pool"; * 调用: * window[namespace]; * 默认下结构如: * window.Pool * * @param namespace 对象名 */ public JxUIPane.Builder 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 variable(String name) { this.variable = name; return this; } /** * js端往java端传数据时执行的函数表达式 * * @param expression 函数表达式 */ public JxUIPane.Builder 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 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 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 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 bindWindow(String property, PropertyBuild obj) { buildPropertyMap.put(property, obj); return this; } /** * 构建 */ public JxUIPane build() { JxUIPane 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 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(); }); } } }