Files
WebClient/src/main/java/org/openautonomousconnection/webclient/ui/BrowserTab.java

381 lines
11 KiB
Java
Raw Normal View History

package org.openautonomousconnection.webclient.ui;
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
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;
2026-02-08 22:36:42 +01:00
import java.util.Objects;
2026-02-14 22:16:15 +01:00
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicBoolean;
2026-02-08 22:36:42 +01:00
import java.util.function.Consumer;
/**
2026-02-14 22:16:15 +01:00
* Logical browser tab: stable UI key + embedded JavaFX WebView.
*
* <p>This class merges the previous {@code BrowserTab} + {@code TabView} into one component.</p>
2026-02-08 22:36:42 +01:00
*/
2026-02-14 22:16:15 +01:00
public final class BrowserTab extends OACPanel {
2026-02-08 22:36:42 +01:00
private final String key;
2026-02-14 22:16:15 +01:00
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;
2026-02-08 22:36:42 +01:00
/**
* Creates a browser tab.
*
2026-02-14 22:16:15 +01:00
* @param key stable UI key (must match CardLayout key and titlebar tab title)
* @param initialUrl initial URL (used for logger context)
2026-02-08 22:36:42 +01:00
* @param onLocationChange callback invoked on URL changes
2026-02-14 22:16:15 +01:00
* @param luaEnabled whether Lua is enabled for this tab
* @param luaPolicy execution policy for Lua
*/
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();
}
/**
* Returns the stable tab key.
*
* @return key
2026-02-08 22:36:42 +01:00
*/
2026-02-14 22:16:15 +01:00
public String getKey() {
return key;
}
public ClientImpl getProtocolClient() {
return protocolClient;
2026-02-08 22:36:42 +01:00
}
/**
2026-02-14 22:16:15 +01:00
* Sets callback for opening current page in a new tab.
2026-02-08 22:36:42 +01:00
*
2026-02-14 22:16:15 +01:00
* @param callback callback
2026-02-08 22:36:42 +01:00
*/
2026-02-14 22:16:15 +01:00
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));
});
2026-02-08 22:36:42 +01:00
}
/**
* Loads a URL.
*
* @param url URL
*/
public void loadUrl(String url) {
2026-02-14 22:16:15 +01:00
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();
});
2026-02-08 22:36:42 +01:00
}
/**
* Goes back in history if possible.
*/
public void goBack() {
2026-02-14 22:16:15 +01:00
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);
});
2026-02-08 22:36:42 +01:00
}
/**
* Goes forward in history if possible.
*/
public void goForward() {
2026-02-14 22:16:15 +01:00
if (peekHistoryTarget(+1) == null) return;
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
Platform.runLater(() -> {
WebEngine e = engine;
if (e == null) return;
WebHistory h = e.getHistory();
if (h.getCurrentIndex() < h.getEntries().size() - 1) h.go(1);
});
2026-02-08 22:36:42 +01:00
}
/**
* Returns current location if known.
*
* @return URL or null
*/
2026-02-14 22:16:15 +01:00
public String getLocationUrl() {
return getEngineLocation();
2026-02-08 22:36:42 +01:00
}
/**
2026-02-14 22:16:15 +01:00
* Returns current engine location.
*
* @return url or null
2026-02-08 22:36:42 +01:00
*/
2026-02-14 22:16:15 +01:00
public String getEngineLocation() {
WebEngine e = engine;
return e != null ? e.getLocation() : null;
2026-02-08 22:36:42 +01:00
}
/**
2026-02-14 22:16:15 +01:00
* Applies HTML live to the current tab (no saving).
2026-02-08 22:36:42 +01:00
*
2026-02-14 22:16:15 +01:00
* @param html html text
2026-02-08 22:36:42 +01:00
*/
2026-02-14 22:16:15 +01:00
public void applyHtml(String html) {
String content = html == null ? "" : html;
Platform.runLater(() -> {
WebEngine e = engine;
if (e != null) e.loadContent(content, "text/html");
});
2026-02-08 22:36:42 +01:00
}
/**
2026-02-14 22:16:15 +01:00
* Returns the current rendered HTML (DOM serialization).
2026-02-08 22:36:42 +01:00
*
2026-02-14 22:16:15 +01:00
* @return current html (never null)
2026-02-08 22:36:42 +01:00
*/
2026-02-14 22:16:15 +01:00
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 "";
}
2026-02-08 22:36:42 +01:00
}
/**
2026-02-14 22:16:15 +01:00
* Releases resources.
2026-02-08 22:36:42 +01:00
*/
2026-02-14 22:16:15 +01:00
public void dispose() {
FxEngine le = luaEngine;
luaEngine = null;
if (le != null) {
try {
le.close();
} catch (Exception ignored) {
// Intentionally ignored
}
}
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
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);
}
});
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
disconnectProtocolQuietly();
}
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
private void fireLocationChanged(String location) {
if (location == null) return;
String s = location.trim();
if (s.isEmpty()) return;
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
try {
onLocationChanged.accept(s);
} catch (Exception ignored) {
// Must not break FX thread
2026-02-08 22:36:42 +01:00
}
2026-02-14 22:16:15 +01:00
}
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
private void installCustomContextMenu() {
final ContextMenu menu = new ContextMenu();
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
MenuItem back = new MenuItem("Back");
back.setOnAction(e -> SwingUtilities.invokeLater(this::goBack));
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
MenuItem forward = new MenuItem("Forward");
forward.setOnAction(e -> SwingUtilities.invokeLater(this::goForward));
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
MenuItem reload = new MenuItem("Reload");
reload.setOnAction(e -> SwingUtilities.invokeLater(this::reload));
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
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();
}
2026-02-08 22:36:42 +01:00
2026-02-14 22:16:15 +01:00
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.
2026-02-08 22:36:42 +01:00
}
}
2026-02-14 22:16:15 +01:00
}