2026-02-10 23:13:58 +01:00
|
|
|
package org.openautonomousconnection.webclient.ui;
|
2026-02-08 22:36:42 +01:00
|
|
|
|
|
|
|
|
import org.openautonomousconnection.oacswing.component.OACButton;
|
|
|
|
|
import org.openautonomousconnection.oacswing.component.OACFrame;
|
|
|
|
|
import org.openautonomousconnection.oacswing.component.OACPanel;
|
|
|
|
|
import org.openautonomousconnection.oacswing.component.OACTextField;
|
|
|
|
|
import org.openautonomousconnection.oacswing.component.design.DesignManager;
|
|
|
|
|
|
|
|
|
|
import javax.swing.*;
|
|
|
|
|
import java.awt.*;
|
|
|
|
|
import java.util.LinkedHashMap;
|
|
|
|
|
import java.util.Map;
|
|
|
|
|
import java.util.Objects;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* A simple multi-tab browser UI:
|
|
|
|
|
* - Tab headers live in the custom title bar (OACTitleBar / OACTabbedPane).
|
|
|
|
|
* - Tab content (JavaFX WebView) lives in the frame content area.
|
|
|
|
|
* - Address bar controls the currently selected tab.
|
|
|
|
|
*/
|
|
|
|
|
public class BrowserUI extends OACFrame {
|
|
|
|
|
|
|
|
|
|
private static final int TITLE_BAR_HEIGHT = 42;
|
|
|
|
|
|
|
|
|
|
private final OACTextField addressField;
|
|
|
|
|
private final OACButton goButton;
|
|
|
|
|
private final OACButton newTabButton;
|
|
|
|
|
private final OACButton closeTabButton;
|
|
|
|
|
private final OACButton backButton;
|
|
|
|
|
private final OACButton forwardButton;
|
|
|
|
|
private final OACButton reloadButton;
|
|
|
|
|
|
|
|
|
|
private final CardLayout cardLayout;
|
|
|
|
|
private final OACPanel pageHost;
|
|
|
|
|
|
|
|
|
|
private final Map<String, BrowserTab> tabsByKey = new LinkedHashMap<>();
|
|
|
|
|
private int tabCounter = 0;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Creates the browser UI and wires tab selection and address bar to tab content.
|
|
|
|
|
*/
|
|
|
|
|
public BrowserUI() {
|
|
|
|
|
super("OAC Browser");
|
|
|
|
|
|
|
|
|
|
// Content pane must be offset because your title bar is an overlay in the layered pane.
|
|
|
|
|
JComponent content = (JComponent) getContentPane();
|
|
|
|
|
content.setLayout(new BorderLayout());
|
|
|
|
|
content.setBorder(BorderFactory.createEmptyBorder(TITLE_BAR_HEIGHT, 0, 0, 0));
|
|
|
|
|
|
|
|
|
|
// Address bar (top of content area)
|
|
|
|
|
OACPanel navBar = new OACPanel(new BorderLayout(8, 0));
|
|
|
|
|
navBar.setBorder(BorderFactory.createEmptyBorder(8, 10, 8, 10));
|
|
|
|
|
|
|
|
|
|
OACPanel leftControls = new OACPanel(new FlowLayout(FlowLayout.LEFT, 6, 0));
|
|
|
|
|
backButton = new OACButton("←");
|
|
|
|
|
forwardButton = new OACButton("→");
|
|
|
|
|
reloadButton = new OACButton("⟳");
|
|
|
|
|
leftControls.add(backButton);
|
|
|
|
|
leftControls.add(forwardButton);
|
|
|
|
|
leftControls.add(reloadButton);
|
|
|
|
|
|
|
|
|
|
addressField = new OACTextField();
|
|
|
|
|
goButton = new OACButton("Go");
|
|
|
|
|
|
|
|
|
|
OACPanel rightControls = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
|
|
|
|
newTabButton = new OACButton("+");
|
|
|
|
|
closeTabButton = new OACButton("✕");
|
|
|
|
|
rightControls.add(newTabButton);
|
|
|
|
|
rightControls.add(closeTabButton);
|
|
|
|
|
|
|
|
|
|
navBar.add(leftControls, BorderLayout.WEST);
|
|
|
|
|
navBar.add(addressField, BorderLayout.CENTER);
|
|
|
|
|
navBar.add(goButton, BorderLayout.EAST);
|
|
|
|
|
navBar.add(rightControls, BorderLayout.EAST);
|
|
|
|
|
|
|
|
|
|
// Fix: BorderLayout only allows one EAST; wrap Go+RightControls in a single panel
|
|
|
|
|
OACPanel east = new OACPanel(new FlowLayout(FlowLayout.RIGHT, 6, 0));
|
|
|
|
|
|
|
|
|
|
east.add(goButton);
|
|
|
|
|
east.add(newTabButton);
|
|
|
|
|
east.add(closeTabButton);
|
|
|
|
|
navBar.add(east, BorderLayout.EAST);
|
|
|
|
|
|
|
|
|
|
content.add(navBar, BorderLayout.NORTH);
|
|
|
|
|
|
|
|
|
|
// Page host
|
|
|
|
|
cardLayout = new CardLayout();
|
|
|
|
|
pageHost = new OACPanel(cardLayout);
|
|
|
|
|
content.add(pageHost, BorderLayout.CENTER);
|
|
|
|
|
|
|
|
|
|
// Wire title bar tab selection to content host
|
|
|
|
|
getTitleBar().getTabs().addChangeListener(e -> onHeaderTabChanged());
|
|
|
|
|
|
|
|
|
|
// Wire address bar actions
|
|
|
|
|
addressField.addActionListener(e -> navigateCurrent(addressField.getText()));
|
|
|
|
|
goButton.addActionListener(e -> navigateCurrent(addressField.getText()));
|
|
|
|
|
|
|
|
|
|
// Wire navigation buttons
|
|
|
|
|
backButton.addActionListener(e -> {
|
|
|
|
|
BrowserTab tab = getCurrentTab();
|
|
|
|
|
if (tab != null) tab.goBack();
|
|
|
|
|
});
|
|
|
|
|
forwardButton.addActionListener(e -> {
|
|
|
|
|
BrowserTab tab = getCurrentTab();
|
|
|
|
|
if (tab != null) tab.goForward();
|
|
|
|
|
});
|
|
|
|
|
reloadButton.addActionListener(e -> {
|
|
|
|
|
BrowserTab tab = getCurrentTab();
|
|
|
|
|
if (tab != null) tab.reload();
|
|
|
|
|
});
|
|
|
|
|
|
2026-02-10 23:13:58 +01:00
|
|
|
newTabButton.addActionListener(e -> openNewTab("web://info.oac/"));
|
2026-02-08 22:36:42 +01:00
|
|
|
closeTabButton.addActionListener(e -> closeCurrentTab());
|
|
|
|
|
|
|
|
|
|
// Create first tab
|
|
|
|
|
DesignManager.apply(this);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Opens a new tab and navigates to the given URL.
|
|
|
|
|
*
|
|
|
|
|
* @param url initial URL
|
|
|
|
|
*/
|
|
|
|
|
public void openNewTab(String url) {
|
|
|
|
|
String key = nextTabKey();
|
|
|
|
|
BrowserTab tab = new BrowserTab(url, newLocation -> SwingUtilities.invokeLater(() -> {
|
|
|
|
|
BrowserTab current = getCurrentTab();
|
|
|
|
|
if (current != null && Objects.equals(current.getKey(), key)) {
|
|
|
|
|
addressField.setText(newLocation);
|
|
|
|
|
}
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
tabsByKey.put(key, tab);
|
|
|
|
|
|
|
|
|
|
// Real page content in center host
|
|
|
|
|
pageHost.add(tab.getView(), key);
|
|
|
|
|
|
|
|
|
|
// Header tab in title bar: DO NOT place the real page here (title bar is only ~42px high).
|
|
|
|
|
getTitleBar().addTab(key, new OACPanel());
|
|
|
|
|
|
|
|
|
|
// Select it
|
|
|
|
|
int idx = getTitleBar().getTabs().getTabCount() - 1;
|
|
|
|
|
getTitleBar().getTabs().setSelectedIndex(idx);
|
|
|
|
|
|
|
|
|
|
// Navigate
|
2026-02-10 23:13:58 +01:00
|
|
|
tab.loadUrl(url);
|
2026-02-08 22:36:42 +01:00
|
|
|
|
|
|
|
|
// Show content
|
|
|
|
|
cardLayout.show(pageHost, key);
|
|
|
|
|
pageHost.revalidate();
|
|
|
|
|
pageHost.repaint();
|
|
|
|
|
|
|
|
|
|
// Update address field immediately
|
|
|
|
|
addressField.setText(url);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Navigates the current tab to the given input (URL or host).
|
|
|
|
|
*
|
|
|
|
|
* @param input user input
|
|
|
|
|
*/
|
|
|
|
|
public void navigateCurrent(String input) {
|
|
|
|
|
BrowserTab tab = getCurrentTab();
|
|
|
|
|
if (tab == null) return;
|
|
|
|
|
|
|
|
|
|
String url = normalizeUrl(input);
|
|
|
|
|
addressField.setText(url);
|
2026-02-10 23:13:58 +01:00
|
|
|
tab.loadUrl(url);
|
2026-02-08 22:36:42 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Closes the currently selected tab.
|
|
|
|
|
*/
|
|
|
|
|
public void closeCurrentTab() {
|
|
|
|
|
int idx = getTitleBar().getTabs().getSelectedIndex();
|
|
|
|
|
if (idx < 0) return;
|
|
|
|
|
|
|
|
|
|
String key = getTitleBar().getTabs().getTitleAt(idx);
|
|
|
|
|
|
|
|
|
|
BrowserTab removed = tabsByKey.remove(key);
|
|
|
|
|
if (removed != null) {
|
|
|
|
|
removed.dispose();
|
|
|
|
|
pageHost.remove(removed.getView());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
getTitleBar().getTabs().removeTabAt(idx);
|
|
|
|
|
|
|
|
|
|
// If no tabs left, open a new one
|
|
|
|
|
if (getTitleBar().getTabs().getTabCount() == 0) {
|
2026-02-10 23:13:58 +01:00
|
|
|
openNewTab("web://info.oac/");
|
2026-02-08 22:36:42 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Show selected tab content
|
|
|
|
|
onHeaderTabChanged();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void onHeaderTabChanged() {
|
|
|
|
|
BrowserTab tab = getCurrentTab();
|
|
|
|
|
if (tab == null) return;
|
|
|
|
|
|
|
|
|
|
cardLayout.show(pageHost, tab.getKey());
|
|
|
|
|
pageHost.revalidate();
|
|
|
|
|
pageHost.repaint();
|
|
|
|
|
|
|
|
|
|
// Sync address bar
|
|
|
|
|
String loc = tab.getLocation();
|
|
|
|
|
if (loc != null && !loc.isBlank()) {
|
|
|
|
|
addressField.setText(loc);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public BrowserTab getCurrentTab() {
|
|
|
|
|
int idx = getTitleBar().getTabs().getSelectedIndex();
|
|
|
|
|
if (idx < 0) return null;
|
|
|
|
|
|
|
|
|
|
String key = getTitleBar().getTabs().getTitleAt(idx);
|
|
|
|
|
return tabsByKey.get(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private String nextTabKey() {
|
|
|
|
|
tabCounter++;
|
|
|
|
|
return "Tab " + tabCounter;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String normalizeUrl(String input) {
|
|
|
|
|
String s = input == null ? "" : input.trim();
|
2026-02-10 23:13:58 +01:00
|
|
|
if (s.isEmpty()) return "web://info.oac/";
|
|
|
|
|
if (s.startsWith("web://")) {
|
|
|
|
|
// Ensure trailing slash for "host only" URLs
|
|
|
|
|
String rest = s.substring("web://".length());
|
|
|
|
|
if (!rest.contains("/")) return s + "/";
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
return "web://" + s + (s.contains("/") ? "" : "/");
|
2026-02-08 22:36:42 +01:00
|
|
|
}
|
|
|
|
|
}
|