Usable Browser
This commit is contained in:
@@ -1,37 +1,152 @@
|
||||
package org.openautonomousconnection.webclient.ui;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Worker;
|
||||
import javafx.embed.swing.JFXPanel;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import javafx.scene.input.ContextMenuEvent;
|
||||
import javafx.scene.web.WebEngine;
|
||||
import javafx.scene.web.WebHistory;
|
||||
import javafx.scene.web.WebView;
|
||||
import org.openautonomousconnection.luascript.security.LuaExecutionPolicy;
|
||||
import org.openautonomousconnection.oacswing.component.OACPanel;
|
||||
import org.openautonomousconnection.webclient.ClientImpl;
|
||||
import org.openautonomousconnection.webclient.lua.WebLogger;
|
||||
import org.openautonomousconnection.webclient.settings.FxEngine;
|
||||
import org.w3c.dom.Document;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.xml.transform.OutputKeys;
|
||||
import javax.xml.transform.Transformer;
|
||||
import javax.xml.transform.TransformerFactory;
|
||||
import javax.xml.transform.dom.DOMSource;
|
||||
import javax.xml.transform.stream.StreamResult;
|
||||
import java.awt.*;
|
||||
import java.io.StringWriter;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* A logical browser tab consisting of a key (tab id), a TabView (WebView),
|
||||
* and navigation helpers.
|
||||
* Logical browser tab: stable UI key + embedded JavaFX WebView.
|
||||
*
|
||||
* <p>This class merges the previous {@code BrowserTab} + {@code TabView} into one component.</p>
|
||||
*/
|
||||
public class BrowserTab {
|
||||
public final class BrowserTab extends OACPanel {
|
||||
|
||||
private final String key;
|
||||
private final TabView view;
|
||||
|
||||
private final AtomicBoolean initialized = new AtomicBoolean(false);
|
||||
private final Consumer<String> onLocationChanged;
|
||||
private final WebLogger webLogger;
|
||||
private final ClientImpl protocolClient;
|
||||
|
||||
private final boolean luaEnabled;
|
||||
private final LuaExecutionPolicy luaPolicy;
|
||||
|
||||
private volatile Runnable openInNewTab;
|
||||
|
||||
private JFXPanel fxPanel;
|
||||
private WebView webView;
|
||||
private WebEngine engine;
|
||||
private volatile FxEngine luaEngine;
|
||||
|
||||
/**
|
||||
* Creates a browser tab.
|
||||
*
|
||||
* @param initialUrl initial URL (used for initial location value)
|
||||
* @param key stable UI key (must match CardLayout key and titlebar tab title)
|
||||
* @param initialUrl initial URL (used for logger context)
|
||||
* @param onLocationChange callback invoked on URL changes
|
||||
* @param luaEnabled whether Lua is enabled for this tab
|
||||
* @param luaPolicy execution policy for Lua
|
||||
*/
|
||||
public BrowserTab(String initialUrl, Consumer<String> onLocationChange) {
|
||||
this.key = Objects.requireNonNull(initialUrl, "initialUrl"); // placeholder key overwritten by BrowserUI
|
||||
this.view = new TabView(onLocationChange, initialUrl);
|
||||
public BrowserTab(String key, String initialUrl, Consumer<String> onLocationChange, boolean luaEnabled, LuaExecutionPolicy luaPolicy, ClientImpl protocolClient) {
|
||||
super();
|
||||
this.key = Objects.requireNonNull(key, "key");
|
||||
this.onLocationChanged = Objects.requireNonNull(onLocationChange, "onLocationChange");
|
||||
this.protocolClient = Objects.requireNonNull(protocolClient, "protocolClient");
|
||||
this.webLogger = new WebLogger(Objects.requireNonNull(initialUrl, "initialUrl"), protocolClient);
|
||||
this.luaEnabled = luaEnabled;
|
||||
this.luaPolicy = Objects.requireNonNull(luaPolicy, "luaPolicy");
|
||||
setLayout(new BorderLayout());
|
||||
}
|
||||
|
||||
private static String serializeDom(Document doc) throws Exception {
|
||||
TransformerFactory tf = TransformerFactory.newInstance();
|
||||
Transformer t = tf.newTransformer();
|
||||
t.setOutputProperty(OutputKeys.METHOD, "html");
|
||||
t.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
|
||||
t.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
|
||||
t.setOutputProperty(OutputKeys.INDENT, "yes");
|
||||
|
||||
StringWriter sw = new StringWriter(64 * 1024);
|
||||
t.transform(new DOMSource(doc), new StreamResult(sw));
|
||||
return sw.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the key (tab id) used by the UI host.
|
||||
* Returns the stable tab key.
|
||||
*
|
||||
* @param key tab key
|
||||
* @return this
|
||||
* @return key
|
||||
*/
|
||||
public BrowserTab withKey(String key) {
|
||||
// The BrowserUI uses titles as keys; keep logic simple.
|
||||
return new BrowserTabKeyed(key, view);
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public ClientImpl getProtocolClient() {
|
||||
return protocolClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets callback for opening current page in a new tab.
|
||||
*
|
||||
* @param callback callback
|
||||
*/
|
||||
public void setOpenInNewTabCallback(Runnable callback) {
|
||||
this.openInNewTab = callback;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addNotify() {
|
||||
super.addNotify();
|
||||
|
||||
if (!initialized.compareAndSet(false, true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
fxPanel = new JFXPanel();
|
||||
add(fxPanel, BorderLayout.CENTER);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
webView = new WebView();
|
||||
webView.setContextMenuEnabled(false);
|
||||
|
||||
engine = webView.getEngine();
|
||||
engine.setJavaScriptEnabled(false);
|
||||
|
||||
engine.locationProperty().addListener((obs, oldV, newV) -> fireLocationChanged(newV));
|
||||
engine.getLoadWorker().stateProperty().addListener((obs, oldS, newS) -> {
|
||||
if (newS == Worker.State.RUNNING || newS == Worker.State.SUCCEEDED) {
|
||||
fireLocationChanged(engine.getLocation());
|
||||
}
|
||||
});
|
||||
|
||||
installCustomContextMenu();
|
||||
|
||||
if (luaEnabled) {
|
||||
luaEngine = new FxEngine(engine, webView, luaPolicy, webLogger);
|
||||
luaEngine.install();
|
||||
} else {
|
||||
luaEngine = null;
|
||||
}
|
||||
|
||||
fxPanel.setScene(new Scene(webView));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -40,28 +155,54 @@ public class BrowserTab {
|
||||
* @param url URL
|
||||
*/
|
||||
public void loadUrl(String url) {
|
||||
view.loadUrl(url);
|
||||
String target = Objects.requireNonNull(url, "url").trim();
|
||||
if (target.isEmpty()) return;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e != null) e.load(target);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the current page.
|
||||
*/
|
||||
public void reload() {
|
||||
String current = getEngineLocation();
|
||||
if (current == null || current.isBlank()) return;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e != null) e.reload();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes back in history if possible.
|
||||
*/
|
||||
public void goBack() {
|
||||
view.back();
|
||||
if (peekHistoryTarget(-1) == null) return;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e == null) return;
|
||||
WebHistory h = e.getHistory();
|
||||
if (h.getCurrentIndex() > 0) h.go(-1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes forward in history if possible.
|
||||
*/
|
||||
public void goForward() {
|
||||
view.forward();
|
||||
}
|
||||
if (peekHistoryTarget(+1) == null) return;
|
||||
|
||||
/**
|
||||
* Reloads the page.
|
||||
*/
|
||||
public void reload() {
|
||||
view.reload();
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e == null) return;
|
||||
WebHistory h = e.getHistory();
|
||||
if (h.getCurrentIndex() < h.getEntries().size() - 1) h.go(1);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,88 +210,171 @@ public class BrowserTab {
|
||||
*
|
||||
* @return URL or null
|
||||
*/
|
||||
public String getLocation() {
|
||||
return view.getEngineLocation();
|
||||
public String getLocationUrl() {
|
||||
return getEngineLocation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns current engine location.
|
||||
*
|
||||
* @return url or null
|
||||
*/
|
||||
public String getEngineLocation() {
|
||||
WebEngine e = engine;
|
||||
return e != null ? e.getLocation() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies HTML live to the current tab (no saving).
|
||||
*
|
||||
* @param html html text
|
||||
*/
|
||||
public void applyHtml(String html) {
|
||||
String content = html == null ? "" : html;
|
||||
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e != null) e.loadContent(content, "text/html");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current rendered HTML (DOM serialization).
|
||||
*
|
||||
* @return current html (never null)
|
||||
*/
|
||||
public String getCurrentHtml() {
|
||||
CompletableFuture<String> fut = new CompletableFuture<>();
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
WebEngine e = engine;
|
||||
if (e == null) {
|
||||
fut.complete("");
|
||||
return;
|
||||
}
|
||||
Document doc = e.getDocument();
|
||||
if (doc == null) {
|
||||
fut.complete("");
|
||||
return;
|
||||
}
|
||||
fut.complete(serializeDom(doc));
|
||||
} catch (Throwable t) {
|
||||
fut.completeExceptionally(t);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return fut.join();
|
||||
} catch (Throwable t) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases resources.
|
||||
*/
|
||||
public void dispose() {
|
||||
view.dispose();
|
||||
FxEngine le = luaEngine;
|
||||
luaEngine = null;
|
||||
if (le != null) {
|
||||
try {
|
||||
le.close();
|
||||
} catch (Exception ignored) {
|
||||
// Intentionally ignored
|
||||
}
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
WebEngine e = engine;
|
||||
if (e != null) {
|
||||
try {
|
||||
e.load(null);
|
||||
} catch (Exception ignored) {
|
||||
// Intentionally ignored
|
||||
}
|
||||
}
|
||||
engine = null;
|
||||
webView = null;
|
||||
if (fxPanel != null) {
|
||||
fxPanel.setScene(null);
|
||||
}
|
||||
});
|
||||
|
||||
disconnectProtocolQuietly();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the tab key.
|
||||
*
|
||||
* @return key
|
||||
*/
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
private void fireLocationChanged(String location) {
|
||||
if (location == null) return;
|
||||
String s = location.trim();
|
||||
if (s.isEmpty()) return;
|
||||
|
||||
/**
|
||||
* Returns the Swing component that renders the web content.
|
||||
*
|
||||
* @return tab view
|
||||
*/
|
||||
public TabView getView() {
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal keyed wrapper so BrowserUI can store a stable key without re-creating the WebView.
|
||||
*/
|
||||
private static final class BrowserTabKeyed extends BrowserTab {
|
||||
|
||||
private final String fixedKey;
|
||||
private final TabView fixedView;
|
||||
|
||||
private BrowserTabKeyed(String fixedKey, TabView fixedView) {
|
||||
super("about:blank", s -> {
|
||||
});
|
||||
this.fixedKey = Objects.requireNonNull(fixedKey, "fixedKey");
|
||||
this.fixedView = Objects.requireNonNull(fixedView, "fixedView");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKey() {
|
||||
return fixedKey;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TabView getView() {
|
||||
return fixedView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadUrl(String url) {
|
||||
fixedView.loadUrl(url);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goBack() {
|
||||
fixedView.back();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goForward() {
|
||||
fixedView.forward();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reload() {
|
||||
fixedView.reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLocation() {
|
||||
return fixedView.getEngineLocation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
fixedView.dispose();
|
||||
try {
|
||||
onLocationChanged.accept(s);
|
||||
} catch (Exception ignored) {
|
||||
// Must not break FX thread
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void installCustomContextMenu() {
|
||||
final ContextMenu menu = new ContextMenu();
|
||||
|
||||
MenuItem back = new MenuItem("Back");
|
||||
back.setOnAction(e -> SwingUtilities.invokeLater(this::goBack));
|
||||
|
||||
MenuItem forward = new MenuItem("Forward");
|
||||
forward.setOnAction(e -> SwingUtilities.invokeLater(this::goForward));
|
||||
|
||||
MenuItem reload = new MenuItem("Reload");
|
||||
reload.setOnAction(e -> SwingUtilities.invokeLater(this::reload));
|
||||
|
||||
MenuItem copyLink = new MenuItem("Copy Link");
|
||||
copyLink.setOnAction(e -> {
|
||||
WebEngine e2 = engine;
|
||||
String loc = e2 != null ? e2.getLocation() : null;
|
||||
if (loc == null) return;
|
||||
ClipboardContent cc = new ClipboardContent();
|
||||
cc.putString(loc);
|
||||
Clipboard.getSystemClipboard().setContent(cc);
|
||||
});
|
||||
|
||||
MenuItem openNewTab = new MenuItem("Open in New Tab");
|
||||
openNewTab.setOnAction(e -> {
|
||||
Runnable r = this.openInNewTab;
|
||||
if (r != null) SwingUtilities.invokeLater(r);
|
||||
});
|
||||
|
||||
menu.getItems().addAll(back, forward, reload, copyLink, openNewTab);
|
||||
|
||||
webView.addEventHandler(ContextMenuEvent.CONTEXT_MENU_REQUESTED, ev -> {
|
||||
menu.hide();
|
||||
menu.show(webView, ev.getScreenX(), ev.getScreenY());
|
||||
ev.consume();
|
||||
});
|
||||
}
|
||||
|
||||
private String peekHistoryTarget(int delta) {
|
||||
WebEngine e = engine;
|
||||
if (e == null) return null;
|
||||
WebHistory h = e.getHistory();
|
||||
int idx = h.getCurrentIndex() + delta;
|
||||
if (idx < 0 || idx >= h.getEntries().size()) return null;
|
||||
return h.getEntries().get(idx).getUrl();
|
||||
}
|
||||
|
||||
private void disconnectProtocolQuietly() {
|
||||
try {
|
||||
if (protocolClient.getClientServerConnection() != null) {
|
||||
protocolClient.getClientServerConnection().disconnect();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Best-effort shutdown.
|
||||
}
|
||||
try {
|
||||
if (protocolClient.getClientINSConnection() != null) {
|
||||
protocolClient.getClientINSConnection().disconnect();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
// Best-effort shutdown.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user