From ddbb6efdb1b2d258d63ccbea0b10c0eecef063f9 Mon Sep 17 00:00:00 2001 From: Merudo Date: Mon, 13 Apr 2020 19:21:20 -0500 Subject: [PATCH] Change overlay to support multiple overlays - Change so overlays must be given a name, as for frames or dialogs - Add parameter "zorder" to overlay(). The overlays will be drawn according to the zOrder (high zOrder overlays are drawn over low zOrder overlays) - Move HTMLJFXPanel WebView code to HTMLWebViewManager - Separate HTMLOverlay into HTMLOverlayManager and HTMLOverlayPanel - Feature discussed in #1425 --- .../java/net/rptools/lib/swing/SwingUtil.java | 59 +- .../net/rptools/maptool/client/MapTool.java | 3 +- .../maptool/client/MapToolLineParser.java | 24 +- .../maptool/client/tool/MeasureTool.java | 30 +- .../maptool/client/ui/MapToolFrame.java | 16 +- .../client/ui/htmlframe/HTMLDialog.java | 2 +- .../client/ui/htmlframe/HTMLFrame.java | 2 +- .../client/ui/htmlframe/HTMLFrameFactory.java | 29 +- .../client/ui/htmlframe/HTMLJFXPanel.java | 578 +---------------- ...MLOverlay.java => HTMLOverlayManager.java} | 291 ++++----- .../client/ui/htmlframe/HTMLOverlayPanel.java | 345 ++++++++++ .../ui/htmlframe/HTMLWebViewManager.java | 597 ++++++++++++++++++ .../maptool/client/ui/zone/ZoneRenderer.java | 2 +- 13 files changed, 1198 insertions(+), 780 deletions(-) rename src/main/java/net/rptools/maptool/client/ui/htmlframe/{HTMLOverlay.java => HTMLOverlayManager.java} (50%) create mode 100644 src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlayPanel.java create mode 100644 src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLWebViewManager.java diff --git a/src/main/java/net/rptools/lib/swing/SwingUtil.java b/src/main/java/net/rptools/lib/swing/SwingUtil.java index 516c36c923..458c3f1cdb 100644 --- a/src/main/java/net/rptools/lib/swing/SwingUtil.java +++ b/src/main/java/net/rptools/lib/swing/SwingUtil.java @@ -33,20 +33,26 @@ import java.io.IOException; import java.util.LinkedList; import java.util.List; +import javafx.application.Platform; +import javafx.scene.ImageCursor; import javax.swing.JComponent; import javax.swing.SwingUtilities; import net.rptools.lib.image.ImageUtil; +import net.rptools.maptool.client.tool.MeasureTool; /** */ public class SwingUtil { public static Cursor emptyCursor; + public static javafx.scene.Cursor emptyCursorFX; + private static final String PATH_EMPTY = "net/rptools/lib/swing/image/empty.png"; static { try { emptyCursor = Toolkit.getDefaultToolkit() - .createCustomCursor( - ImageUtil.getImage("net/rptools/lib/swing/image/empty.png"), new Point(0, 0), ""); + .createCustomCursor(ImageUtil.getImage(PATH_EMPTY), new Point(0, 0), ""); + Platform.runLater( + () -> emptyCursorFX = new ImageCursor(new javafx.scene.image.Image(PATH_EMPTY), 0, 0)); } catch (IOException ioe) { ioe.printStackTrace(); } @@ -291,4 +297,53 @@ public static JComponent getComponent(JComponent container, String name) { public static boolean hasComponent(JComponent container, String name) { return getComponent(container, name) != null; } + + /** + * Returns the JavaFX cursor equivalent of a Swing cursor + * + * @param cursor the Swing cursor + * @return the JavaFX cursor + */ + public static javafx.scene.Cursor swingCursorToFX(Cursor cursor) { + if (cursor == null) { + return javafx.scene.Cursor.DEFAULT; + } + if (cursor == emptyCursor) { + return emptyCursorFX; + } + if (cursor == MeasureTool.getMeasureCursor()) { + return MeasureTool.getMeasureCursorFX(); + } + + switch (cursor.getType()) { + case Cursor.CROSSHAIR_CURSOR: + return javafx.scene.Cursor.CROSSHAIR; + case Cursor.E_RESIZE_CURSOR: + return javafx.scene.Cursor.E_RESIZE; + case Cursor.HAND_CURSOR: + return javafx.scene.Cursor.HAND; + case Cursor.MOVE_CURSOR: + return javafx.scene.Cursor.MOVE; + case Cursor.N_RESIZE_CURSOR: + return javafx.scene.Cursor.N_RESIZE; + case Cursor.NE_RESIZE_CURSOR: + return javafx.scene.Cursor.NE_RESIZE; + case Cursor.NW_RESIZE_CURSOR: + return javafx.scene.Cursor.NW_RESIZE; + case Cursor.S_RESIZE_CURSOR: + return javafx.scene.Cursor.S_RESIZE; + case Cursor.SE_RESIZE_CURSOR: + return javafx.scene.Cursor.SE_RESIZE; + case Cursor.SW_RESIZE_CURSOR: + return javafx.scene.Cursor.SW_RESIZE; + case Cursor.TEXT_CURSOR: + return javafx.scene.Cursor.TEXT; + case Cursor.W_RESIZE_CURSOR: + return javafx.scene.Cursor.W_RESIZE; + case Cursor.WAIT_CURSOR: + return javafx.scene.Cursor.WAIT; + default: + return javafx.scene.Cursor.DEFAULT; + } + } } diff --git a/src/main/java/net/rptools/maptool/client/MapTool.java b/src/main/java/net/rptools/maptool/client/MapTool.java index bf997b0b84..6d81ac50cf 100644 --- a/src/main/java/net/rptools/maptool/client/MapTool.java +++ b/src/main/java/net/rptools/maptool/client/MapTool.java @@ -998,7 +998,8 @@ public static void setCampaign(Campaign campaign, GUID defaultRendererId) { AssetManager.updateRepositoryList(); MapTool.getFrame().getCampaignPanel().reset(); MapTool.getFrame().getGmPanel().reset(); - MapTool.getFrame().getHtmlOverlay().closeRequest(); // overlay vanishes after campaign change + // overlay vanishes after campaign change + MapTool.getFrame().getOverlayPanel().removeAllOverlays(); UserDefinedMacroFunctions.getInstance().loadCampaignLibFunctions(); } diff --git a/src/main/java/net/rptools/maptool/client/MapToolLineParser.java b/src/main/java/net/rptools/maptool/client/MapToolLineParser.java index 612a552907..5e2d9d0df2 100644 --- a/src/main/java/net/rptools/maptool/client/MapToolLineParser.java +++ b/src/main/java/net/rptools/maptool/client/MapToolLineParser.java @@ -39,6 +39,7 @@ import net.rptools.maptool.client.functions.ReturnFunction.ReturnFunctionException; import net.rptools.maptool.client.functions.json.JSONMacroFunctions; import net.rptools.maptool.client.ui.htmlframe.HTMLFrameFactory; +import net.rptools.maptool.client.ui.htmlframe.HTMLFrameFactory.FrameType; import net.rptools.maptool.client.ui.macrobuttons.buttons.MacroButtonPrefs; import net.rptools.maptool.client.ui.zone.ZoneRenderer; import net.rptools.maptool.language.I18N; @@ -359,7 +360,7 @@ private enum OptionType { // HTML webView FRAME5("frame5", 1, 2, "\"\""), // HTML overlay - OVERLAY("overlay", 0, 1, "\"\""), + OVERLAY("overlay", 1, 2, "\"\""), // Run for another token TOKEN("token", 1, 1); @@ -1037,8 +1038,9 @@ public String parseLine( break; case OVERLAY: codeType = CodeType.CODEBLOCK; + frameName = option.getParsedParam(0, resolver, tokenInContext).toString(); + frameOpts = option.getParsedParam(1, resolver, tokenInContext).toString(); outputTo = OutputLoc.OVERLAY; - frameOpts = option.getParsedParam(0, resolver, tokenInContext).toString(); break; /////////////////////////////////////////////////// // CODE OPTIONS @@ -1431,26 +1433,30 @@ public String parseLine( switch (outputTo) { case FRAME: HTMLFrameFactory.show( - frameName, true, false, frameOpts, expressionBuilder.toString()); + frameName, FrameType.FRAME, false, frameOpts, expressionBuilder.toString()); break; case DIALOG: HTMLFrameFactory.show( - frameName, false, false, frameOpts, expressionBuilder.toString()); + frameName, FrameType.DIALOG, false, frameOpts, expressionBuilder.toString()); break; case OVERLAY: - MapTool.getFrame() - .getHtmlOverlay() - .updateContents(expressionBuilder.toString(), true); + HTMLFrameFactory.show( + frameName, FrameType.OVERLAY, true, frameOpts, expressionBuilder.toString()); break; case CHAT: builder.append(expressionBuilder); break; case FRAME5: - HTMLFrameFactory.show(frameName, true, true, frameOpts, expressionBuilder.toString()); + HTMLFrameFactory.show( + frameName, FrameType.FRAME, true, frameOpts, expressionBuilder.toString()); break; case DIALOG5: HTMLFrameFactory.show( - frameName, false, true, frameOpts, expressionBuilder.toString()); + frameName, + HTMLFrameFactory.FrameType.DIALOG, + true, + frameOpts, + expressionBuilder.toString()); break; } diff --git a/src/main/java/net/rptools/maptool/client/tool/MeasureTool.java b/src/main/java/net/rptools/maptool/client/tool/MeasureTool.java index 35c741e7a6..c86cacfeac 100644 --- a/src/main/java/net/rptools/maptool/client/tool/MeasureTool.java +++ b/src/main/java/net/rptools/maptool/client/tool/MeasureTool.java @@ -25,6 +25,8 @@ import java.io.IOException; import java.text.NumberFormat; import java.util.Map; +import javafx.application.Platform; +import javafx.scene.ImageCursor; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ImageIcon; @@ -48,24 +50,38 @@ public class MeasureTool extends DefaultTool implements ZoneOverlay { private ZoneWalker walker; private Path gridlessPath; - private Cursor measureCursor; + private static Cursor measureCursor; + private static javafx.scene.Cursor measureCursorFX; + + private static final String PATH_RULER_IMG = + "net/rptools/maptool/client/image/tool/ruler-blue.png"; + private static final String PATH_MEASURE_IMG = + "net/rptools/maptool/client/image/cursor-tape-measure.png"; public MeasureTool() { try { - setIcon( - new ImageIcon( - ImageUtil.getImage("net/rptools/maptool/client/image/tool/ruler-blue.png"))); + setIcon(new ImageIcon(ImageUtil.getImage(PATH_RULER_IMG))); measureCursor = Toolkit.getDefaultToolkit() .createCustomCursor( - ImageUtil.getImage("net/rptools/maptool/client/image/cursor-tape-measure.png"), - new Point(2, 28), - CURSOR_NAME); + ImageUtil.getImage(PATH_MEASURE_IMG), new Point(2, 28), CURSOR_NAME); + Platform.runLater( + () -> + measureCursorFX = + new ImageCursor(new javafx.scene.image.Image(PATH_MEASURE_IMG), 2, 28)); } catch (IOException ioe) { ioe.printStackTrace(); } } + public static Cursor getMeasureCursor() { + return measureCursor; + } + + public static javafx.scene.Cursor getMeasureCursorFX() { + return measureCursorFX; + } + @Override protected void attachTo(ZoneRenderer renderer) { renderer.setCursor(measureCursor); diff --git a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java index 12fbd858c9..46414e8495 100644 --- a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java +++ b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java @@ -112,7 +112,7 @@ import net.rptools.maptool.client.ui.drawpanel.DrawPanelTreeCellRenderer; import net.rptools.maptool.client.ui.drawpanel.DrawPanelTreeModel; import net.rptools.maptool.client.ui.drawpanel.DrawablesPanel; -import net.rptools.maptool.client.ui.htmlframe.HTMLOverlay; +import net.rptools.maptool.client.ui.htmlframe.HTMLOverlayPanel; import net.rptools.maptool.client.ui.lookuptable.LookupTablePanel; import net.rptools.maptool.client.ui.macrobuttons.buttons.MacroButton; import net.rptools.maptool.client.ui.macrobuttons.panels.*; @@ -172,7 +172,7 @@ public class MapToolFrame extends DefaultDockableHolder /** The panel showing the initiative order. */ private final InitiativePanel initiativePanel; /** The HTML pane showing the map overlay. */ - private final HTMLOverlay htmlOverlay; + private HTMLOverlayPanel overlayPanel; private final PointerOverlay pointerOverlay; private final CommandPanel commandPanel; @@ -456,7 +456,7 @@ public MapToolFrame(JMenuBar menuBar) { connectionPanel = createConnectionPanel(); toolbox = new Toolbox(); initiativePanel = createInitiativePanel(); - htmlOverlay = new HTMLOverlay(); + overlayPanel = new HTMLOverlayPanel(); zoneRendererList = new CopyOnWriteArrayList(); pointerOverlay = new PointerOverlay(); @@ -512,8 +512,8 @@ public MapToolFrame(JMenuBar menuBar) { rendererBorderPanel.add(zoneRendererPanel); toolbarPanel = new ToolbarPanel(toolbox); - zoneRendererPanel.add(htmlOverlay, PositionalLayout.Position.CENTER, 0); - htmlOverlay.setVisible(false); // disabled by default + zoneRendererPanel.add(overlayPanel, PositionalLayout.Position.CENTER, 0); + overlayPanel.setVisible(false); // disabled by default // Put it all together setJMenuBar(menuBar); @@ -1524,9 +1524,9 @@ public ZoneRenderer getCurrentZoneRenderer() { return currentRenderer; } - /** @return the HTMLOverlay */ - public HTMLOverlay getHtmlOverlay() { - return htmlOverlay; + /** @return the HTML Overlay Panel */ + public HTMLOverlayPanel getOverlayPanel() { + return overlayPanel; } public void addZoneRenderer(ZoneRenderer renderer) { diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLDialog.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLDialog.java index f6946c126f..d8aaafed75 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLDialog.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLDialog.java @@ -155,7 +155,7 @@ public void windowClosing(WindowEvent e) { */ public void addHTMLPanel(boolean scrollBar, boolean isHTML5) { if (isHTML5) { - panel = new HTMLJFXPanel(this); + panel = new HTMLJFXPanel(this, new HTMLWebViewManager()); } else { panel = new HTMLPanel(this, scrollBar); } diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrame.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrame.java index 4bbac50fc8..e456efd5a9 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrame.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrame.java @@ -162,7 +162,7 @@ public boolean getTemporary() { */ public void addHTMLPanel(boolean isHTML5) { if (isHTML5) { - panel = new HTMLJFXPanel(this); + panel = new HTMLJFXPanel(this, new HTMLWebViewManager()); } else { panel = new HTMLPanel(this, true); } diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrameFactory.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrameFactory.java index 27fe747036..a1fb3e4636 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrameFactory.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLFrameFactory.java @@ -27,19 +27,25 @@ public class HTMLFrameFactory { private HTMLFrameFactory() {} + public enum FrameType { + FRAME, + DIALOG, + OVERLAY + } + private static HTMLFrameFactory.Listener listener; /** * Shows a dialog or frame based on the options. * * @param name The name of the dialog or frame. - * @param isFrame Is it a frame. + * @param frameType The type of the frame. * @param isHTML5 Does it use HTML5 (JavaFX) or HTML 3.2 (Swing). * @param properties The properties that determine the attributes of the frame or dialog. * @param html The html contents of frame or dialog. */ public static void show( - String name, boolean isFrame, boolean isHTML5, String properties, String html) { + String name, FrameType frameType, boolean isHTML5, String properties, String html) { if (listener == null) { listener = new HTMLFrameFactory.Listener(); } @@ -47,6 +53,7 @@ public static void show( boolean temporary = false; int width = -1; int height = -1; + int zOrder = 0; String title = name; String tabTitle = null; Object frameValue = null; @@ -94,6 +101,12 @@ public static void show( } catch (NumberFormatException e) { // Ignoring the value; shouldn't we warn the user? } + } else if (keyLC.equals("zorder")) { + try { + zOrder = Integer.parseInt(value); + } catch (NumberFormatException e) { + // Ignoring the value; shouldn't we warn the user? + } } else if (keyLC.equals("title")) { title = value; } else if (keyLC.equals("noframe")) { @@ -127,10 +140,10 @@ public static void show( } } if (tabTitle == null) tabTitle = title; // if tabTitle not set, make it same as title - if (isFrame) { + if (frameType == FrameType.FRAME) { HTMLFrame.showFrame( name, title, tabTitle, width, height, temporary, scrollReset, isHTML5, frameValue, html); - } else { + } else if (frameType == FrameType.DIALOG) { HTMLDialog.showDialog( name, title, @@ -144,6 +157,8 @@ public static void show( isHTML5, frameValue, html); + } else if (frameType == FrameType.OVERLAY) { + MapTool.getFrame().getOverlayPanel().showOverlay(name, zOrder, html); } } @@ -151,21 +166,21 @@ public static void show( public static void selectedListChanged() { HTMLFrame.doSelectedChanged(); HTMLDialog.doSelectedChanged(); - MapTool.getFrame().getHtmlOverlay().doSelectedChanged(); + MapTool.getFrame().getOverlayPanel().doSelectedChanged(); } /** A new token has been impersonated or cleared. */ public static void impersonateToken() { HTMLFrame.doImpersonatedChanged(); HTMLDialog.doImpersonatedChanged(); - MapTool.getFrame().getHtmlOverlay().doImpersonatedChanged(); + MapTool.getFrame().getOverlayPanel().doImpersonatedChanged(); } /** One of the tokens has changed. */ public static void tokenChanged(Token token) { HTMLFrame.doTokenChanged(token); HTMLDialog.doTokenChanged(token); - MapTool.getFrame().getHtmlOverlay().doTokenChanged(token); + MapTool.getFrame().getOverlayPanel().doTokenChanged(token); } public static class Listener implements ModelChangeListener, AppEventListener { diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLJFXPanel.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLJFXPanel.java index 6ccad96263..ad2685e206 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLJFXPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLJFXPanel.java @@ -14,135 +14,42 @@ */ package net.rptools.maptool.client.ui.htmlframe; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import java.awt.*; import java.awt.event.ActionListener; -import java.math.BigDecimal; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.Optional; import javafx.application.Platform; -import javafx.beans.value.ObservableValue; -import javafx.concurrent.Worker; import javafx.embed.swing.JFXPanel; import javafx.scene.Scene; -import javafx.scene.control.ButtonType; -import javafx.scene.control.TextInputDialog; import javafx.scene.input.*; import javafx.scene.layout.StackPane; import javafx.scene.web.*; -import javafx.stage.Stage; -import javafx.stage.StageStyle; import javax.swing.*; -import net.rptools.maptool.client.AppPreferences; -import net.rptools.maptool.client.MapTool; -import net.rptools.maptool.client.functions.MacroLinkFunction; -import net.rptools.maptool.client.functions.json.JSONMacroFunctions; -import net.rptools.maptool.language.I18N; -import net.rptools.maptool.model.TextMessage; -import net.rptools.parser.ParserException; -import netscape.javascript.JSObject; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.w3c.dom.*; -import org.w3c.dom.events.EventListener; -import org.w3c.dom.events.EventTarget; -import org.w3c.dom.html.*; /** Class handles JFXPanel that contains a WebView that can display HTML5. */ public class HTMLJFXPanel extends JFXPanel implements HTMLPanelInterface { - /** The logger. */ - private static final Logger log = LogManager.getLogger(HTMLJFXPanel.class); - - /** The action listeners for the container. */ - private ActionListener actionListeners; - /** The WebView that displays HTML5. */ - public WebView webView; - - /** The WebEngine of the WebView. */ - private WebEngine webEngine; - - /** Whether the scrolling should be reset. */ - private boolean scrollReset = true; - /** The horizontal scrolling. */ - private int scrollX = 0; - /** The vertical scrolling. */ - private int scrollY = 0; - /** Whether the WebView has been flushed out. */ - private boolean isFlushed = true; - - /** The bridge from Javascript to Java. */ - private static final JavaBridge bridge = new JavaBridge(); - - /** Represents a bridge from Javascript to Java. */ - public static class JavaBridge { - /** Name of the Bridge. */ - private static final String NAME = "MapTool"; - /** - * Display a self-only message in the chat window. - * - * @param text the message to display - */ - public void log(String text) { - MapTool.addMessage(TextMessage.me(null, text)); - } - } - - /** Meta-tag that blocks external file access. */ - private static final String SCRIPT_BLOCK_EXT = - "\n"; - - /** The default rule for the body tag. */ - static final String CSS_BODY = - "body { font-family: sans-serif; font-size: %dpt; background: #ECE9D8;}"; - /** The default rule for the div tag. */ - static final String CSS_DIV = "div {margin-bottom: 5px}"; - /** The default rule for the span tag. */ - static final String CSS_SPAN = "span.roll {background:#efefef}"; - - /** JS that scroll the view to an element from its Id. */ - private static final String SCRIPT_ANCHOR = - "element = document.getElementById('%s'); if(element != null) {element.scrollIntoView();}"; - - /** JS that directs the console.log function to the Java bridge function "log". */ - private static final String SCRIPT_REPLACE_LOG = - "console.log = function(message){" + JavaBridge.NAME + ".log(message);};"; - - /** JS that replace the form.submit() in JS by a function that works. */ - private static final String SCRIPT_REPLACE_SUBMIT = - "HTMLFormElement.prototype.submit = function(){this.dispatchEvent(new Event('submit'));};"; + HTMLWebViewManager webViewManager; /** * Creates a new HTMLJFXPanel. * * @param container The container that will hold the HTML panel. */ - HTMLJFXPanel(final HTMLPanelContainer container) { - Platform.runLater(() -> setupScene(container)); + HTMLJFXPanel(final HTMLPanelContainer container, HTMLWebViewManager webViewManager) { + this.webViewManager = webViewManager; + Platform.runLater(() -> setupScene(container, new WebView())); } - void setupScene(final HTMLPanelContainer container) { - webView = new WebView(); - webView.setContextMenuEnabled(false); // disable "reload' right click menu. - webEngine = webView.getEngine(); - webEngine.getLoadWorker().stateProperty().addListener(this::changed); - - // For alert / confirm / prompt JS events. - webEngine.setOnAlert(HTMLJFXPanel::showAlert); - webEngine.setConfirmHandler(HTMLJFXPanel::showConfirm); - webEngine.setPromptHandler(HTMLJFXPanel::showPrompt); - webEngine.setCreatePopupHandler(HTMLJFXPanel::showPopup); - webEngine.setOnError(HTMLJFXPanel::showError); + /** + * Setup the JavaFX scene that will hold the WebView + * + * @param container the container to close on escape + */ + void setupScene(HTMLPanelContainer container, WebView webview) { + webViewManager.setupWebView(webview); StackPane root = new StackPane(); // VBox would create empty space at bottom on resize root.setStyle("-fx-background-color: rgba(0, 0, 0, 0);"); // set stackpane transparent - - webView.setPickOnBounds(false); root.setPickOnBounds(false); - - root.getChildren().add(webView); + root.getChildren().add(webViewManager.getWebView()); Scene scene = new Scene(root); scene.setFill(javafx.scene.paint.Color.TRANSPARENT); // set scene transparent @@ -158,10 +65,6 @@ void setupScene(final HTMLPanelContainer container) { this.setScene(scene); // set the scene on the JFXPanel } - public WebEngine getWebEngine() { - return webEngine; - } - @Override public void addToContainer(HTMLPanelContainer container) { container.add(this); @@ -174,467 +77,16 @@ public void removeFromContainer(HTMLPanelContainer container) { @Override public void addActionListener(ActionListener listener) { - actionListeners = AWTEventMulticaster.add(actionListeners, listener); + webViewManager.addActionListener(listener); } @Override public void flush() { - Platform.runLater( - () -> { - // Stores the x,y scrolling of the previous WebView - scrollX = getHScrollValue(); - scrollY = getVScrollValue(); - - // Delete cache for navigate back - webEngine.load("about:blank"); - // Delete cookies - java.net.CookieHandler.setDefault(new java.net.CookieManager()); - - isFlushed = true; - }); + Platform.runLater(() -> webViewManager.flush()); } @Override public void updateContents(final String html, boolean scrollReset) { - if (log.isDebugEnabled()) { - log.debug("setting text in WebView: " + html); - } - Platform.runLater( - () -> { - this.scrollReset = scrollReset; - // If the WebView has been flushed, the scrolling has already been stored - if (!scrollReset && !isFlushed) { - scrollX = getHScrollValue(); - scrollY = getVScrollValue(); - } - isFlushed = false; - webEngine.loadContent(SCRIPT_BLOCK_EXT + HTMLPanelInterface.fixHTML(html)); - }); - } - - /** - * Show an alert message. - * - * @param event the event of the alert - */ - private static void showAlert(WebEvent event) { - javafx.scene.control.Dialog alert = new javafx.scene.control.Dialog<>(); - alert.getDialogPane().setContentText(event.getData()); - alert.getDialogPane().getButtonTypes().add(ButtonType.OK); - alert.showAndWait(); - } - - /** - * Show a confirmation box. - * - * @param message the message to display. - * @return boolean true if OK was pressed, false otherwise. - */ - private static boolean showConfirm(String message) { - javafx.scene.control.Dialog confirm = new javafx.scene.control.Dialog<>(); - confirm.getDialogPane().setContentText(message); - confirm.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); - - Optional result = confirm.showAndWait(); - return (result.isPresent() && result.get() == ButtonType.OK); - } - - /** - * Shows a prompt for a value. - * - * @param promptData the promptData object holding the default value and text - * @return string holding the value entered by the user, or a null - */ - private static String showPrompt(PromptData promptData) { - TextInputDialog dialog = new TextInputDialog(promptData.getDefaultValue()); - dialog.setTitle(I18N.getText("lineParser.dialogTitleNoToken")); - dialog.setContentText(promptData.getMessage()); - return dialog.showAndWait().orElse(null); - } - - /** - * Shows a popup window. - * - * @param popupFeatures the popup features - * @return the webEngine of the popup - */ - private static WebEngine showPopup(PopupFeatures popupFeatures) { - Stage stage = new Stage((StageStyle.UTILITY)); - WebView webViewPopup = new WebView(); - stage.setScene(new Scene(webViewPopup, 300, 300)); - stage.show(); - return webViewPopup.getEngine(); - } - - /** - * Shows an error message in the chat window. - * - * @param event the error event - */ - private static void showError(WebErrorEvent event) { - // Hide error "User data directory is already in use", directory not used anyway - if (event.getEventType() != WebErrorEvent.USER_DATA_DIRECTORY_ALREADY_IN_USE) { - MapTool.addMessage(TextMessage.me(null, event.getMessage())); - } - } - - String getCSSRule() { - return String.format(CSS_BODY, AppPreferences.getFontSize()) + CSS_SPAN + CSS_DIV; - } - - /** - * Check if the worker succeeded, then deal with the MapTool macro references and add event - * listeners for the buttons and hyperlinks. - * - * @param observable what is observed. - * @param oldState the previous state of the worker. - * @param newState the new state of the worker. - */ - private void changed( - ObservableValue observable, - Worker.State oldState, - Worker.State newState) { - if (newState == Worker.State.SUCCEEDED) { - handlePage(); - } - } - - void handlePage() { - // Redirect console.log to the JavaBridge - JSObject window = (JSObject) webEngine.executeScript("window"); - window.setMember(JavaBridge.NAME, bridge); - webEngine.executeScript(SCRIPT_REPLACE_LOG); - - // Replace the broken javascript form.submit method - webEngine.executeScript(SCRIPT_REPLACE_SUBMIT); - - // Event listener for the href macro link clicks. - EventListener listenerA = this::fixHref; - // Event listener for form submission. - EventListener listenerSubmit = this::getDataAndSubmit; - - Document doc = webEngine.getDocument(); - NodeList nodeList; - - // Add default CSS as first element of the head tag - Element styleNode = doc.createElement("style"); - Text styleContent = doc.createTextNode(getCSSRule()); - styleNode.appendChild(styleContent); - Node head = doc.getDocumentElement().getElementsByTagName("head").item(0); - Node nodeCSS = head.insertBefore(styleNode, head.getFirstChild()); - - // Deal with CSS and events of . - nodeList = doc.getElementsByTagName("link"); - for (int i = 0; i < nodeList.getLength(); i++) { - fixLink(nodeList.item(i).getAttributes(), nodeCSS, doc); - } - - // Set the title if using . - nodeList = doc.getElementsByTagName("title"); - if (nodeList.getLength() > 0) { - doChangeTitle(nodeList.item(0).getTextContent()); - } - - // Handle the <meta> tags. - nodeList = doc.getElementsByTagName("meta"); - for (int i = 0; i < nodeList.getLength(); i++) { - handleMetaTag((Element) nodeList.item(i)); - } - - // Add event handlers for <a> hyperlinks. - nodeList = doc.getElementsByTagName("a"); - for (int i = 0; i < nodeList.getLength(); i++) { - EventTarget node = (EventTarget) nodeList.item(i); - node.addEventListener("click", listenerA, false); - } - - // Add event handlers for hyperlinks for maps. - nodeList = doc.getElementsByTagName("area"); - for (int i = 0; i < nodeList.getLength(); i++) { - EventTarget node = (EventTarget) nodeList.item(i); - node.addEventListener("click", listenerA, false); - } - - // Set the "submit" handler to get the data on submission not based on buttons - nodeList = doc.getElementsByTagName("form"); - for (int i = 0; i < nodeList.getLength(); i++) { - EventTarget target = (EventTarget) nodeList.item(i); - target.addEventListener("submit", listenerSubmit, false); - } - - // Set the "submit" handler to get the data on submission based on input - nodeList = doc.getElementsByTagName("input"); - for (int i = 0; i < nodeList.getLength(); i++) { - String type = ((Element) nodeList.item(i)).getAttribute("type"); - if ("image".equals(type) || "submit".equals(type)) { - EventTarget target = (EventTarget) nodeList.item(i); - target.addEventListener("click", listenerSubmit, false); - } - } - // Set the "submit" handler to get the data on submission based on button - nodeList = doc.getElementsByTagName("button"); - for (int i = 0; i < nodeList.getLength(); i++) { - String type = ((Element) nodeList.item(i)).getAttribute("type"); - if (type == null || "submit".equals(type)) { - EventTarget target = (EventTarget) nodeList.item(i); - target.addEventListener("click", listenerSubmit, false); - } - } - // Restores the previous scrolling. - if (!scrollReset) { - scrollTo(scrollX, scrollY); - } - } - - /** - * Handle a request to register a macro callback. - * - * @param type The type of event. - * @param link The link to the macro. - */ - private void doRegisterMacro(String type, String link) { - if (actionListeners != null) { - if (log.isDebugEnabled()) { - log.debug("registerMacro event: type='" + type + "' link='" + link + "'"); - } - actionListeners.actionPerformed( - new HTMLActionEvent.RegisterMacroActionEvent(this, type, link)); - } - } - - /** - * Handles the CSS and the events of a link. For a stylesheet link with a macro location as a - * href, the CSS sheet is attached at the end of the refNode. If the href instead starts with - * "macro", register the href as a callback macro. - * - * @param attr the attributes of the link tag - * @param refNode the node to append the new CSS rules to - * @param doc the document to update with the modified link - */ - private void fixLink(NamedNodeMap attr, Node refNode, Document doc) { - Node rel = attr.getNamedItem("rel"); - Node type = attr.getNamedItem("type"); - Node href = attr.getNamedItem("href"); - - if (rel != null && type != null && href != null) { - String content = href.getTextContent(); - if (rel.getTextContent().equalsIgnoreCase("stylesheet")) { - String[] vals = content.split("@"); - if (vals.length != 2) { - return; - } - try { - String cssText = MapTool.getParser().getTokenLibMacro(vals[0], vals[1]); - Element styleNode = doc.createElement("style"); - Text styleContent = doc.createTextNode(cssText); - styleNode.appendChild(styleContent); - // Append the style sheet node to the refNode - refNode.appendChild(styleNode); - } catch (ParserException e) { - // Do nothing - } - } else if (type.getTextContent().equalsIgnoreCase("macro")) { - if (rel.getTextContent().equalsIgnoreCase("onChangeImpersonated")) { - doRegisterMacro("onChangeImpersonated", content); - } else if (rel.getTextContent().equalsIgnoreCase("onChangeSelection")) { - doRegisterMacro("onChangeSelection", content); - } else if (rel.getTextContent().equalsIgnoreCase("onChangeToken")) { - doRegisterMacro("onChangeToken", content); - } - } - } - } - - /** - * Handles the href events. MacroLinks are executed, external links open the browsers, and anchor - * links scroll the browser to the link. - * - * @param event the href event triggered - */ - private void fixHref(org.w3c.dom.events.Event event) { - if (log.isDebugEnabled()) { - log.debug("Responding to hyperlink event: " + event.getType() + " " + event.toString()); - } - - final String href = ((Element) event.getCurrentTarget()).getAttribute("href"); - if (href != null && !href.equals("")) { - String href2 = href.trim().toLowerCase(); - if (href2.startsWith("macro")) { - // ran as macroLink; - SwingUtilities.invokeLater(() -> MacroLinkFunction.runMacroLink(href)); - } else if (href2.startsWith("#")) { - // Java bug JDK-8199014 workaround - webEngine.executeScript(String.format(SCRIPT_ANCHOR, href.substring(1))); - } else if (!href2.startsWith("javascript")) { - // non-macrolink, non-anchor link, non-javascript code - MapTool.showDocument(href); // show in usual browser - } - event.preventDefault(); // don't change webview - } - } - - /** - * Handle a change in title. - * - * @param title The title to change to. - */ - private void doChangeTitle(String title) { - if (actionListeners != null) { - if (log.isDebugEnabled()) { - log.debug("changeTitle event: " + title); - } - actionListeners.actionPerformed(new HTMLActionEvent.ChangeTitleActionEvent(this, title)); - } - } - - /** - * Handle any meta tag information in the html. - * - * @param element the element of the meta tag. - */ - private void handleMetaTag(Element element) { - String name = element.getAttribute("name"); - String content = element.getAttribute("content"); - - if (actionListeners != null && name != null && content != null) { - if (log.isDebugEnabled()) { - log.debug("metaTag found: name='" + name + "' content='" + content + "'"); - } - actionListeners.actionPerformed(new HTMLActionEvent.MetaTagActionEvent(this, name, content)); - } - } - - /** - * Get the data of the form and submit it as a json. - * - * @param event the event of the form submission - */ - private void getDataAndSubmit(org.w3c.dom.events.Event event) { - boolean formnovalidate = false; // if true, the form validation is bypassed - HTMLFormElement form = null; - Element target = (Element) event.getCurrentTarget(); - JsonObject jObj = new JsonObject(); - // Get the form based on the target of the event - if (target instanceof HTMLFormElement) { - form = (HTMLFormElement) target; - } else if (target instanceof HTMLInputElement) { - HTMLInputElement input = (HTMLInputElement) target; - form = input.getForm(); - addToObject(jObj, input.getName(), input.getValue()); - formnovalidate = input.getAttribute("formnovalidate") != null; - } else if (target instanceof HTMLButtonElement) { - HTMLButtonElement button = (HTMLButtonElement) target; - form = button.getForm(); - addToObject(jObj, button.getName(), button.getValue()); - formnovalidate = button.getAttribute("formnovalidate") != null; - } - if (form == null) return; - - // Check for non-macrolinktext action - String action = form.getAction(); - if (action == null || action.startsWith("javascript:")) { - return; - } - - // Check for validity - boolean novalidate = form.getAttribute("novalidate") != null; - if (!formnovalidate && !novalidate) { - JSObject jsObject = (JSObject) form; - if (!(boolean) jsObject.call("checkValidity")) { - return; - } - } - - event.preventDefault(); // prevent duplicated form submit - - // Gets the data from the form - final HTMLCollection collection = form.getElements(); - for (int i = 0; i < collection.getLength(); i++) { - String name, value; - if (collection.item(i) instanceof HTMLInputElement) { - HTMLInputElement element = (HTMLInputElement) collection.item(i); - String type = element.getType().toLowerCase(); - if (type.equals("checkbox") || type.equals("radio")) { - if (element.getChecked()) { - name = element.getName(); - value = element.getValue(); - } else continue; // skip unchecked elements - } else if (type.equals("submit") || type.equals("image")) { - continue; // skip input button/images that were not pressed - } else { - name = element.getName(); - value = element.getValue(); - } - } else if (collection.item(i) instanceof HTMLSelectElement) { - HTMLSelectElement element = (HTMLSelectElement) collection.item(i); - name = element.getName(); - value = element.getValue(); - } else if (collection.item(i) instanceof HTMLTextAreaElement) { - HTMLTextAreaElement element = (HTMLTextAreaElement) collection.item(i); - name = element.getName(); - value = element.getValue(); - } else continue; // skip elements not containing data - addToObject(jObj, name, value); - } - String data = URLEncoder.encode(jObj.toString(), StandardCharsets.UTF_8); - doSubmit("json", action, data); - } - - /** - * Convenience method to put name and value in the object. - * - * @param jObj the JsonObject to put the data in - * @param name the name - * @param value the value - */ - private static void addToObject(JsonObject jObj, String name, String value) { - if (name != null && !"".equals(name)) { - value = value == null ? "" : value; - try { - BigDecimal number = new BigDecimal(value); - jObj.addProperty(name, number); - } catch (NumberFormatException nfe) { - JsonElement json = JSONMacroFunctions.getInstance().asJsonElement(value); - jObj.add(name, json); - } - } - } - - /** - * Handle a submit. - * - * @param method The method of the submit. - * @param action The action for the submit. - * @param data The data from the form. - */ - private void doSubmit(String method, String action, String data) { - if (actionListeners != null) { - if (log.isDebugEnabled()) { - log.debug( - "submit event: method='" + method + "' action='" + action + "' data='" + data + "'"); - } - actionListeners.actionPerformed( - new HTMLActionEvent.FormActionEvent(this, method, action, data)); - } - } - - /** Returns the vertical scroll value, i.e. thumb position. */ - private int getVScrollValue() { - return (Integer) webEngine.executeScript("document.body.scrollTop"); - } - - /** Returns the horizontal scroll value, i.e. thumb position. */ - private int getHScrollValue() { - return (Integer) webEngine.executeScript("document.body.scrollLeft"); - } - - /** - * Scrolls the WebView. - * - * @param x the horizontal scrolling - * @param y the vertical scrolling - */ - private void scrollTo(int x, int y) { - webEngine.executeScript("window.scrollTo(" + x + ", " + y + ")"); + Platform.runLater(() -> webViewManager.updateContents(html, scrollReset)); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlay.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlayManager.java similarity index 50% rename from src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlay.java rename to src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlayManager.java index c262595bfb..adb83f8f74 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlay.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlayManager.java @@ -15,29 +15,30 @@ package net.rptools.maptool.client.ui.htmlframe; import java.awt.*; -import java.awt.event.*; -import java.awt.image.BufferedImage; +import java.awt.event.ActionEvent; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.HashMap; import java.util.Map; -import javafx.application.Platform; -import javax.swing.*; +import javafx.geometry.Rectangle2D; +import javafx.scene.SnapshotParameters; +import javafx.scene.image.WritableImage; +import javafx.scene.transform.Translate; +import javafx.scene.web.WebView; import net.rptools.maptool.client.AppPreferences; import net.rptools.maptool.client.MapTool; import net.rptools.maptool.client.functions.MacroLinkFunction; -import net.rptools.maptool.client.tool.DefaultTool; -import net.rptools.maptool.client.ui.Tool; -import net.rptools.maptool.model.Token; import netscape.javascript.JSObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; -/** Represents the transparent HTML overlay over the map. */ -public class HTMLOverlay extends HTMLJFXPanel implements HTMLPanelContainer { +/** The manager for the HTMLWebView of an overlay. */ +public class HTMLOverlayManager extends HTMLWebViewManager + implements Comparable<HTMLOverlayManager>, HTMLPanelContainer { /** The logger. */ - private static final Logger log = LogManager.getLogger(HTMLOverlay.class); + private static final Logger log = LogManager.getLogger(HTMLOverlayManager.class); /** The default rule for an invisible body tag. */ private static final String CSS_BODY = @@ -54,14 +55,46 @@ public class HTMLOverlay extends HTMLJFXPanel implements HTMLPanelContainer { private static final String SCRIPT_GET_POINTERMAP = "window.getComputedStyle(this).getPropertyValue('--pointermap')"; + /** The ZOrder of the overlay. */ + private int zOrder; + + /** The name of the overlay. */ + private final String name; + + /** The value stored in the overlay. */ + private Object value; + /** The map of the macro callbacks. */ - private final Map<String, String> macroCallbacks = new HashMap<>(); + private final Map<String, String> macroCallbacks = new HashMap<String, String>(); - public HTMLOverlay() { - super(null); - addMouseListeners(); // mouse listeners to transmit to the ZR + HTMLOverlayManager(String name, int zOrder) { addActionListener(this); // add the action listeners for form events - setBackground(new Color(0, 0, 0, 0)); // transparent overlay + this.name = name; + this.zOrder = zOrder; + } + + @Override + public void setupWebView(WebView webView) { + super.setupWebView(webView); + } + + /** @return the zOrder of the overlay. */ + int getZOrder() { + return zOrder; + } + + /** + * Sets the zOrder of the overlay. + * + * @param zOrder the zOrder + */ + void setZOrder(int zOrder) { + this.zOrder = zOrder; + } + + @Override + public int compareTo(@NotNull HTMLOverlayManager o) { + return getZOrder() - o.getZOrder(); } @Override @@ -69,59 +102,51 @@ void handlePage() { super.handlePage(); makeWebEngineTransparent(); // transparent WebView - // Set the listener for the cursor to switch the webView cursor to the correct one. This is - // caused by WebView attempting to return to its default instead of our tool cursor. - webView + getWebView() .cursorProperty() - .addListener((obs, oldCursor, newCursor) -> blockCursorChange(newCursor)); + .addListener((obs, oldCursor, newCursor) -> updateOverlayCursor(newCursor)); } /** - * Blocks the cursor change of WebView, if it is a default cursor different from the one of the - * ZoneRenderer. + * Updates the overlay cursor to match the WebView cursor, if needs be. * * @param newCursor the cursor that WebView tries to impose */ - private void blockCursorChange(javafx.scene.Cursor newCursor) { + private void updateOverlayCursor(javafx.scene.Cursor newCursor) { if (newCursor != null && "DEFAULT".equals(newCursor.toString())) { - Cursor cursor = MapTool.getFrame().getCurrentZoneRenderer().getCursor(); - if (!cursor.getName().equals("Default Cursor")) { - webView.setCursor(null); - this.setCursor(cursor); + // Only changes to the default cursor if all WebViews have the default cursor + if (MapTool.getFrame().getOverlayPanel().areWebViewCursorsDefault()) { + Cursor cursor = MapTool.getFrame().getCurrentZoneRenderer().getCursor(); + MapTool.getFrame().getOverlayPanel().setOverlayCursor(cursor); } + } else if (newCursor != null) { + MapTool.getFrame().getOverlayPanel().setOverlayCursor(newCursor); } } - /** - * Pass the mouse event to the map, if valid. Takes a MouseEvent caught by the Overlay, check if - * it was done on an HTML element enabling a click on the map. If so, forward the mouse event to - * the ZoneRenderer. - * - * @param swingEvent the mouse event from Swing that could be passed - */ - private void validateAndPassEvent(MouseEvent swingEvent) { - Platform.runLater( - () -> { - boolean passClick = isPassClick(swingEvent.getX(), swingEvent.getY()); - if (passClick) { - SwingUtilities.invokeLater(() -> passMouseEvent(swingEvent)); - } - }); + /** @return the rule for an invisible body. */ + @Override + String getCSSRule() { + return String.format(CSS_BODY, AppPreferences.getFontSize()) + + CSS_SPAN + + CSS_DIV + + CSS_POINTERMAP; } - /** - * Sets up the initial drag start. If the mouse press is a right click and the tool could be - * dragging the map, sets up the initial drag start. This is required or the map will "jump" if - * performing a right click on the overlay followed by a drag. - * - * @param e the mouse press event - */ - private void maySetMapDragStart(MouseEvent e) { - if (SwingUtilities.isRightMouseButton(e)) { - Tool tool = MapTool.getFrame().getToolbox().getSelectedTool(); - if (tool instanceof DefaultTool) { - ((DefaultTool) tool).setDragStart(e.getX(), e.getY()); - } + /** Makes the webEngine transparent through reflection. */ + private void makeWebEngineTransparent() { + try { + Field f = getWebEngine().getClass().getDeclaredField("page"); + f.setAccessible(true); + Object page = f.get(getWebEngine()); + Method m = page.getClass().getMethod("setBackgroundColor", int.class); + m.setAccessible(true); + m.invoke(page, (new Color(0, 0, 0, 0)).getRGB()); + } catch (NoSuchFieldException + | IllegalAccessException + | NoSuchMethodException + | InvocationTargetException e) { + e.printStackTrace(); } } @@ -132,118 +157,38 @@ private void maySetMapDragStart(MouseEvent e) { * @param y the y coordinate of the click * @return false if the element has --pointermap=block, true otherwise */ - private boolean isPassClick(int x, int y) { + boolean isClickBlocked(int x, int y) { JSObject element = (JSObject) getWebEngine().executeScript(String.format(SCRIPT_GET_FROM_POINT, x, y)); if (element == null) { - return true; + return false; } else { String pe = (String) element.eval(SCRIPT_GET_POINTERMAP); - return "blockopaque".equals(pe) ? !isOpaque(x, y) : !"block".equals(pe); + return "blockopaque".equals(pe) ? isOpaque(x, y) : "block".equals(pe); } } + private static Rectangle2D onePixel = new Rectangle2D(0, 0, 1, 1); + /** - * Returns whether the overlay is opaque (alpha > 0) at the x,y pixel. Method provided by gthanop - * at https://stackoverflow.com/questions/60906929 + * Returns whether the overlay is opaque (alpha > 0) at the x,y pixel. * * @param x the x coordinate of the pixel * @param y the y coordinate of the pixel * @return true if alpha isn't 0, false if it is */ private boolean isOpaque(int x, int y) { - if (!getBounds().contains(x, y)) return false; // no overlay outside the bounds - final BufferedImage bimg = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB); - final Graphics2D g2d = bimg.createGraphics(); - g2d.translate(-x, -y); // pixel of interest is now at 0,0 - printAll(g2d); // draw a 1,1 pixel at 0,0 - g2d.dispose(); - Color c = new Color(bimg.getRGB(0, 0), true); - bimg.flush(); - return c.getAlpha() != 0; + SnapshotParameters sp = new SnapshotParameters(); + sp.setTransform(new Translate(-x, -y)); + sp.setViewport(onePixel); + sp.setFill(javafx.scene.paint.Color.TRANSPARENT); + WritableImage image = getWebView().snapshot(sp, null); + return image.getPixelReader().getColor(0, 0).getOpacity() != 0; } - /** @return the rule for an invisible body. */ @Override - String getCSSRule() { - return String.format(CSS_BODY, AppPreferences.getFontSize()) - + CSS_SPAN - + CSS_DIV - + CSS_POINTERMAP; - } - - /** Run the callback macro for "onChangeSelection". */ - void doSelectedChanged() { - HTMLPanelContainer.selectedChanged(macroCallbacks); - } - - /** Run the callback macro for "onChangeImpersonated". */ - void doImpersonatedChanged() { - HTMLPanelContainer.impersonatedChanged(macroCallbacks); - } - - /** Run the callback macro for "onChangeToken". */ - void doTokenChanged(Token token) { - HTMLPanelContainer.tokenChanged(token, macroCallbacks); - } - - /** - * Add the mouse listeners to forward the mouse events to the current ZoneRenderer. Clicks, mouse - * press, and mouse released get validated first to see if they need forwarding. - */ - private void addMouseListeners() { - addMouseWheelListener(this::passMouseEvent); - addMouseMotionListener( - new MouseMotionAdapter() { - @Override - public void mouseMoved(MouseEvent e) { - passMouseEvent(e); - } - - @Override - public void mouseDragged(MouseEvent e) { - passMouseEvent(e); - } - }); - addMouseListener( - new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - validateAndPassEvent(e); - } - - @Override - public void mousePressed(MouseEvent e) { - maySetMapDragStart(e); // may set map dragstart x and y, even if on overlay - validateAndPassEvent(e); - e.consume(); // workaround for java bug JDK-8200224 - } - - @Override - public void mouseReleased(MouseEvent e) { - passMouseEvent(e); - } - - @Override - public void mouseEntered(MouseEvent e) { - passMouseEvent(e); - } - - @Override - public void mouseExited(MouseEvent e) { - passMouseEvent(e); - } - }); - } - - /** - * Passes a mouse event to the ZoneRenderer. - * - * @param e the mouse event to forward - */ - private void passMouseEvent(MouseEvent e) { - Component c = MapTool.getFrame().getCurrentZoneRenderer(); - c.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, c)); + public boolean isVisible() { + return getWebView().isVisible(); } @Override @@ -251,9 +196,14 @@ public Map<String, String> macroCallbacks() { return macroCallbacks; } + @Override + public void setVisible(boolean visible) { + getWebView().setVisible(visible); + } + @Override public boolean getTemporary() { - return false; + return true; } @Override @@ -261,11 +211,21 @@ public void setTemporary(boolean temp) {} @Override public Object getValue() { + return value; + } + + @Override + public void setValue(Object value) { + this.value = value; + } + + @Override + public Component add(Component component) { return null; } @Override - public void setValue(Object value) {} + public void remove(Component component) {} /** * Act when an action is performed. @@ -295,38 +255,9 @@ public void actionPerformed(ActionEvent e) { } } - @Override - public void updateContents(final String html, boolean scrollReset) { - macroCallbacks.clear(); // clear the old callbacks - super.updateContents(html, true); - getDropTarget().setActive(false); // disables drop on overlay, drop goes to map - if ("".equals(html)) { - closeRequest(); // turn off the overlay - } else { - setVisible(true); - } - } - @Override public void closeRequest() { - flush(); setVisible(false); - } - - /** Makes the webEngine transparent through reflection. */ - private void makeWebEngineTransparent() { - try { - Field f = getWebEngine().getClass().getDeclaredField("page"); - f.setAccessible(true); - Object page = f.get(getWebEngine()); - Method m = page.getClass().getMethod("setBackgroundColor", int.class); - m.setAccessible(true); - m.invoke(page, (new Color(0, 0, 0, 0)).getRGB()); - } catch (NoSuchFieldException - | IllegalAccessException - | NoSuchMethodException - | InvocationTargetException e) { - e.printStackTrace(); - } + flush(); } } diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlayPanel.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlayPanel.java new file mode 100644 index 0000000000..25ccb4427a --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlayPanel.java @@ -0,0 +1,345 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * <http://www.gnu.org/licenses/> and specifically the Affero license + * text at <http://www.gnu.org/licenses/agpl.html>. + */ +package net.rptools.maptool.client.ui.htmlframe; + +import java.awt.*; +import java.awt.event.*; +import java.util.*; +import java.util.List; +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.embed.swing.JFXPanel; +import javafx.scene.Cursor; +import javafx.scene.Node; +import javafx.scene.Scene; +import javafx.scene.layout.Background; +import javafx.scene.layout.Region; +import javafx.scene.layout.StackPane; +import javafx.scene.web.WebView; +import javax.swing.*; +import net.rptools.lib.swing.SwingUtil; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.tool.DefaultTool; +import net.rptools.maptool.client.ui.Tool; +import net.rptools.maptool.model.Token; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** Represents the JFXPanel that will contains the map overlays. */ +public class HTMLOverlayPanel extends JFXPanel { + /** The logger. */ + private static final Logger log = LogManager.getLogger(HTMLOverlayManager.class); + + /** The ordered map of the overlays. */ + private Map<String, HTMLOverlayManager> overlays = new LinkedHashMap<>(); + + /** The StackPane holding all the overlays. */ + private StackPane root; + + /** The Region used to catch the clicks on the overlays. */ + private Region front; + + /** Creates a new HTMLJFXPanel. */ + public HTMLOverlayPanel() { + super(); + addMouseListeners(); // mouse listeners to transmit to the ZR + setBackground(new Color(0, 0, 0, 0)); // transparent overlay + + Platform.runLater(this::setupScene); + setVisible(false); // disabled by default + } + + /** Setups the scene of the JFXPanel. */ + void setupScene() { + front = new Region(); + front.setBackground(Background.EMPTY); + front.setPickOnBounds(true); // catches the clicks + front.addEventFilter( + javafx.scene.input.MouseEvent.ANY, + event -> { + // Passes the mouse event to all overlays + for (HTMLOverlayManager overlay : overlays.values()) { + overlay.getWebView().fireEvent(event); + } + }); + + root = new StackPane(front); + root.setStyle("-fx-background-color: rgba(0, 0, 0, 0);"); // set stackpane transparent + + Scene scene = new Scene(root); + scene.setFill(javafx.scene.paint.Color.TRANSPARENT); // set scene transparent + this.setScene(scene); + } + + /** + * Sets the overlay cursor to a JavaFX cursor. + * + * @param cursor the cursor to set + */ + public void setOverlayCursor(Cursor cursor) { + front.setCursor(cursor); + } + + /** + * Sets the overlay cursor to a Swing cursor. + * + * @param cursor the cursor to set + */ + public void setOverlayCursor(java.awt.Cursor cursor) { + front.setCursor(SwingUtil.swingCursorToFX(cursor)); + } + + /** @return whether all overlay WebViews have the default cursor. */ + public boolean areWebViewCursorsDefault() { + for (HTMLOverlayManager overlay : overlays.values()) { + Cursor cursor = overlay.getWebView().getCursor(); + if (cursor == null || !"DEFAULT".equals(cursor.toString())) { + return false; + } + } + return true; + } + + /** + * Removes one overlay. + * + * @param name The name of the overlay. + */ + void removeOverlay(String name) { + if (overlays.containsKey(name)) { + root.getChildren().remove(overlays.get(name).getWebView()); + overlays.remove(name); + if (overlays.isEmpty()) { + setVisible(false); // hide overlay panel if all are gone + } + } + } + + /** Removes all overlays. */ + public void removeAllOverlays() { + this.setVisible(false); + Platform.runLater( + () -> { + ObservableList<Node> listChildren = root.getChildren(); + for (HTMLOverlayManager overlay : overlays.values()) { + listChildren.remove(overlay.getWebView()); + } + overlays.clear(); + setVisible(false); + }); + } + + /** + * Shows an overlay. + * + * @param name the name of the overlay + * @param zOrder the zOrder of the overlay + * @param html the HTML of the overlay + */ + public void showOverlay(String name, int zOrder, String html) { + getDropTarget().setActive(false); // disables drop on overlay, drop goes to map + setVisible(true); + Platform.runLater( + () -> { + HTMLOverlayManager overlayManager; + boolean needsSorting = false; + if (overlays.containsKey(name)) { + overlayManager = overlays.get(name); + if ("".equals(html)) { + // Blank removes the overlay + removeOverlay(name); + return; + } else if (zOrder != overlayManager.getZOrder()) { + overlayManager.setZOrder(zOrder); + needsSorting = true; + } + } else { + overlayManager = new HTMLOverlayManager(name, zOrder); + overlayManager.setupWebView(new WebView()); + overlays.put(name, overlayManager); + root.getChildren().add(overlayManager.getWebView()); + needsSorting = true; + } + if (needsSorting) { + sortOverlays(); + } + overlayManager.updateContents(html, true); + }); + } + + /** Sorts all overlays according to their zOrder. */ + private void sortOverlays() { + overlays = MapUtil.sortByValue(overlays); + for (Map.Entry<String, HTMLOverlayManager> entries : overlays.entrySet()) { + entries.getValue().getWebView().toFront(); + } + front.toFront(); + } + + /** Utility class to return a sorted copy of a map. */ + public static class MapUtil { + /** + * Returns a sorted copy of a map + * + * @param map the map + * @param <K> the keys of the map + * @param <V> the values of the map + * @return the sorted copy + */ + public static <K, V extends Comparable<? super V>> Map<K, V> sortByValue(Map<K, V> map) { + List<Map.Entry<K, V>> list = new ArrayList<>(map.entrySet()); + list.sort(Map.Entry.comparingByValue()); + + Map<K, V> result = new LinkedHashMap<>(); + for (Map.Entry<K, V> entry : list) { + result.put(entry.getKey(), entry.getValue()); + } + return result; + } + } + + /** + * Checks if the mouse event needs to be blocked, if not send the event to ZoneRenderer + * + * @param e the mouse event + */ + private void mayPassClick(MouseEvent e) { + Platform.runLater( + () -> { + if (!isClickBlocked(e)) { + SwingUtilities.invokeLater(() -> passMouseEvent(e)); + } + }); + } + + /** + * Returns true if one overlay blocks the mouse event, false otherwise. + * + * @param e the mouse event + * @return whether the mouse event should be blocked + */ + private boolean isClickBlocked(MouseEvent e) { + boolean blocked = false; + for (HTMLOverlayManager overlay : overlays.values()) { + if (overlay.isClickBlocked(e.getX(), e.getY())) { + blocked = true; + break; + } + } + return blocked; + } + + /** + * Add the mouse listeners to forward the mouse events to the current ZoneRenderer. Clicks and + * mouse press get validated first to see if they need forwarding. + */ + private void addMouseListeners() { + addMouseWheelListener(this::passMouseEvent); + addMouseMotionListener( + new MouseMotionAdapter() { + @Override + public void mouseMoved(MouseEvent e) { + passMouseEvent(e); + } + + @Override + public void mouseDragged(MouseEvent e) { + passMouseEvent(e); + } + }); + addMouseListener( + new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + mayPassClick(e); + } + + @Override + public void mousePressed(MouseEvent e) { + maySetMapDragStart(e); // may set map dragstart x and y, even if on overlay + mayPassClick(e); + e.consume(); // workaround for java bug JDK-8200224 + } + + @Override + public void mouseReleased(MouseEvent e) { + passMouseEvent(e); + } + + @Override + public void mouseEntered(MouseEvent e) { + passMouseEvent(e); + } + + @Override + public void mouseExited(MouseEvent e) { + passMouseEvent(e); + } + }); + } + + /** + * Passes a mouse event to the ZoneRenderer. + * + * @param e the mouse event to forward + */ + void passMouseEvent(MouseEvent e) { + Component c = MapTool.getFrame().getCurrentZoneRenderer(); + c.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, c)); + } + + /** + * Sets up the initial drag start. If the mouse press is a right click and the tool could be + * dragging the map, sets up the initial drag start. This is required or the map will "jump" if + * performing a right click on the overlay followed by a drag. + * + * @param e the mouse press event + */ + private void maySetMapDragStart(MouseEvent e) { + if (SwingUtilities.isRightMouseButton(e)) { + Tool tool = MapTool.getFrame().getToolbox().getSelectedTool(); + if (tool instanceof DefaultTool) { + ((DefaultTool) tool).setDragStart(e.getX(), e.getY()); + } + } + } + + /** Run all callback macros for "onTokenChanged". */ + public void doTokenChanged(Token token) { + for (HTMLOverlayManager overlay : overlays.values()) { + if (overlay.getWebView().isVisible()) { + HTMLPanelContainer.tokenChanged(token, overlay.macroCallbacks()); + } + } + } + + /** Run all callback macros for "onChangeImpersonated". */ + public void doImpersonatedChanged() { + for (HTMLOverlayManager overlay : overlays.values()) { + if (overlay.getWebView().isVisible()) { + HTMLPanelContainer.impersonatedChanged(overlay.macroCallbacks()); + } + } + } + + /** Run all callback macros for "onChangeSelection". */ + public void doSelectedChanged() { + for (HTMLOverlayManager overlay : overlays.values()) { + if (overlay.getWebView().isVisible()) { + HTMLPanelContainer.selectedChanged(overlay.macroCallbacks()); + } + } + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLWebViewManager.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLWebViewManager.java new file mode 100644 index 0000000000..a3dbbe9c57 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLWebViewManager.java @@ -0,0 +1,597 @@ +/* + * This software Copyright by the RPTools.net development team, and + * licensed under the Affero GPL Version 3 or, at your option, any later + * version. + * + * MapTool Source Code is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty + * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * + * You should have received a copy of the GNU Affero General Public + * License * along with this source Code. If not, please visit + * <http://www.gnu.org/licenses/> and specifically the Affero license + * text at <http://www.gnu.org/licenses/agpl.html>. + */ +package net.rptools.maptool.client.ui.htmlframe; + +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import java.awt.*; +import java.awt.event.ActionListener; +import java.math.BigDecimal; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import javafx.beans.value.ObservableValue; +import javafx.concurrent.Worker; +import javafx.scene.Scene; +import javafx.scene.control.ButtonType; +import javafx.scene.control.TextInputDialog; +import javafx.scene.web.*; +import javafx.stage.Stage; +import javafx.stage.StageStyle; +import javax.swing.*; +import net.rptools.maptool.client.AppPreferences; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.functions.MacroLinkFunction; +import net.rptools.maptool.client.functions.json.JSONMacroFunctions; +import net.rptools.maptool.language.I18N; +import net.rptools.maptool.model.TextMessage; +import net.rptools.parser.ParserException; +import netscape.javascript.JSObject; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.w3c.dom.*; +import org.w3c.dom.events.EventListener; +import org.w3c.dom.events.EventTarget; +import org.w3c.dom.html.*; + +/** The manager for a WebView that can display HTML5. */ +public class HTMLWebViewManager { + /** The logger. */ + private static final Logger log = LogManager.getLogger(HTMLWebViewManager.class); + + /** The action listeners for the container. */ + private ActionListener actionListeners; + + /** The WebView that displays HTML5. */ + WebView webView; + + /** The WebEngine of the WebView. */ + private WebEngine webEngine; + + /** Whether the scrolling to be reset. */ + private boolean scrollReset = true; + /** The horizontal scrolling. */ + private int scrollX = 0; + /** The vertical scrolling. */ + private int scrollY = 0; + /** Whether the WebView has been flushed out. */ + private boolean isFlushed = true; + + /** The bridge from Javascript to Java. */ + private static final JavaBridge bridge = new JavaBridge(); + + /** Represents a bridge from Javascript to Java. */ + public static class JavaBridge { + /** Name of the Bridge. */ + private static final String NAME = "MapTool"; + /** + * Display a self-only message in the chat window. + * + * @param text the message to display + */ + public void log(String text) { + MapTool.addMessage(TextMessage.me(null, text)); + } + } + + /** Meta-tag that blocks external file access. */ + private static final String SCRIPT_BLOCK_EXT = + "<meta http-equiv=\"Content-Security-Policy\" content=\"default-src asset:; style-src 'unsafe-inline'; script-src 'unsafe-inline' 'unsafe-eval'\">\n"; + + /** The default rule for the body tag. */ + static final String CSS_BODY = + "body { font-family: sans-serif; font-size: %dpt; background: #ECE9D8;}"; + /** The default rule for the div tag. */ + static final String CSS_DIV = "div {margin-bottom: 5px}"; + /** The default rule for the span tag. */ + static final String CSS_SPAN = "span.roll {background:#efefef}"; + + /** JS that scroll the view to an element from its Id. */ + private static final String SCRIPT_ANCHOR = + "element = document.getElementById('%s'); if(element != null) {element.scrollIntoView();}"; + + /** JS that directs the console.log function to the Java bridge function "log". */ + private static final String SCRIPT_REPLACE_LOG = + "console.log = function(message){" + JavaBridge.NAME + ".log(message);};"; + + /** JS that replace the form.submit() in JS by a function that works. */ + private static final String SCRIPT_REPLACE_SUBMIT = + "HTMLFormElement.prototype.submit = function(){this.dispatchEvent(new Event('submit'));};"; + + HTMLWebViewManager() {} + + /** + * Setup the WebView + * + * @param webView the webView to manage + */ + public void setupWebView(WebView webView) { + this.webView = webView; + this.webView.setContextMenuEnabled(false); // disable "reload' right click menu. + this.webView.setPickOnBounds(false); + + webEngine = webView.getEngine(); + webEngine.getLoadWorker().stateProperty().addListener(this::changed); + + // For alert / confirm / prompt JS events. + webEngine.setOnAlert(HTMLWebViewManager::showAlert); + webEngine.setConfirmHandler(HTMLWebViewManager::showConfirm); + webEngine.setPromptHandler(HTMLWebViewManager::showPrompt); + webEngine.setCreatePopupHandler(HTMLWebViewManager::showPopup); + webEngine.setOnError(HTMLWebViewManager::showError); + } + + public WebView getWebView() { + return webView; + } + + public WebEngine getWebEngine() { + return webEngine; + } + + public void addActionListener(ActionListener listener) { + actionListeners = AWTEventMulticaster.add(actionListeners, listener); + } + + public void flush() { + // Stores the x,y scrolling of the previous WebView + scrollX = getHScrollValue(); + scrollY = getVScrollValue(); + + // Delete cache for navigate back + webEngine.load("about:blank"); + // Delete cookies + java.net.CookieHandler.setDefault(new java.net.CookieManager()); + + isFlushed = true; + } + + public void updateContents(final String html, boolean scrollReset) { + if (log.isDebugEnabled()) { + log.debug("setting text in WebView: " + html); + } + this.scrollReset = scrollReset; + // If the WebView has been flushed, the scrolling has already been stored + if (!scrollReset && !isFlushed) { + scrollX = getHScrollValue(); + scrollY = getVScrollValue(); + } + isFlushed = false; + webEngine.loadContent(SCRIPT_BLOCK_EXT + HTMLPanelInterface.fixHTML(html)); + } + + /** + * Show an alert message. + * + * @param event the event of the alert + */ + private static void showAlert(WebEvent<String> event) { + javafx.scene.control.Dialog<ButtonType> alert = new javafx.scene.control.Dialog<>(); + alert.getDialogPane().setContentText(event.getData()); + alert.getDialogPane().getButtonTypes().add(ButtonType.OK); + alert.showAndWait(); + } + + /** + * Show a confirmation box. + * + * @param message the message to display. + * @return boolean true if OK was pressed, false otherwise. + */ + private static boolean showConfirm(String message) { + javafx.scene.control.Dialog<ButtonType> confirm = new javafx.scene.control.Dialog<>(); + confirm.getDialogPane().setContentText(message); + confirm.getDialogPane().getButtonTypes().addAll(ButtonType.OK, ButtonType.CANCEL); + + Optional<ButtonType> result = confirm.showAndWait(); + return (result.isPresent() && result.get() == ButtonType.OK); + } + + /** + * Shows a prompt for a value. + * + * @param promptData the promptData object holding the default value and text + * @return string holding the value entered by the user, or a null + */ + private static String showPrompt(PromptData promptData) { + TextInputDialog dialog = new TextInputDialog(promptData.getDefaultValue()); + dialog.setTitle(I18N.getText("lineParser.dialogTitleNoToken")); + dialog.setContentText(promptData.getMessage()); + return dialog.showAndWait().orElse(null); + } + + /** + * Shows a popup window. + * + * @param popupFeatures the popup features + * @return the webEngine of the popup + */ + private static WebEngine showPopup(PopupFeatures popupFeatures) { + Stage stage = new Stage((StageStyle.UTILITY)); + WebView webViewPopup = new WebView(); + stage.setScene(new Scene(webViewPopup, 300, 300)); + stage.show(); + return webViewPopup.getEngine(); + } + + /** + * Shows an error message in the chat window. + * + * @param event the error event + */ + private static void showError(WebErrorEvent event) { + MapTool.addMessage(TextMessage.me(null, event.getMessage())); + } + + String getCSSRule() { + return String.format(CSS_BODY, AppPreferences.getFontSize()) + CSS_SPAN + CSS_DIV; + } + + /** + * Check if the worker succeeded, then deal with the MapTool macro references and add event + * listeners for the buttons and hyperlinks. + * + * @param observable what is observed. + * @param oldState the previous state of the worker. + * @param newState the new state of the worker. + */ + private void changed( + ObservableValue<? extends Worker.State> observable, + Worker.State oldState, + Worker.State newState) { + if (newState == Worker.State.SUCCEEDED) { + handlePage(); + } + } + + void handlePage() { + // Redirect console.log to the JavaBridge + JSObject window = (JSObject) webEngine.executeScript("window"); + window.setMember(JavaBridge.NAME, bridge); + webEngine.executeScript(SCRIPT_REPLACE_LOG); + + // Replace the broken javascript form.submit method + webEngine.executeScript(SCRIPT_REPLACE_SUBMIT); + + // Event listener for the href macro link clicks. + EventListener listenerA = this::fixHref; + // Event listener for form submission. + EventListener listenerSubmit = this::getDataAndSubmit; + + Document doc = webEngine.getDocument(); + NodeList nodeList; + + // Add default CSS as first element of the head tag + Element styleNode = doc.createElement("style"); + Text styleContent = doc.createTextNode(getCSSRule()); + styleNode.appendChild(styleContent); + Node head = doc.getDocumentElement().getElementsByTagName("head").item(0); + Node nodeCSS = head.insertBefore(styleNode, head.getFirstChild()); + + // Deal with CSS and events of <link>. + nodeList = doc.getElementsByTagName("link"); + for (int i = 0; i < nodeList.getLength(); i++) { + fixLink(nodeList.item(i).getAttributes(), nodeCSS, doc); + } + + // Set the title if using <title>. + nodeList = doc.getElementsByTagName("title"); + if (nodeList.getLength() > 0) { + doChangeTitle(nodeList.item(0).getTextContent()); + } + + // Handle the <meta> tags. + nodeList = doc.getElementsByTagName("meta"); + for (int i = 0; i < nodeList.getLength(); i++) { + handleMetaTag((Element) nodeList.item(i)); + } + + // Add event handlers for <a> hyperlinks. + nodeList = doc.getElementsByTagName("a"); + for (int i = 0; i < nodeList.getLength(); i++) { + EventTarget node = (EventTarget) nodeList.item(i); + node.addEventListener("click", listenerA, false); + } + + // Add event handlers for hyperlinks for maps. + nodeList = doc.getElementsByTagName("area"); + for (int i = 0; i < nodeList.getLength(); i++) { + EventTarget node = (EventTarget) nodeList.item(i); + node.addEventListener("click", listenerA, false); + } + + // Set the "submit" handler to get the data on submission not based on buttons + nodeList = doc.getElementsByTagName("form"); + for (int i = 0; i < nodeList.getLength(); i++) { + EventTarget target = (EventTarget) nodeList.item(i); + target.addEventListener("submit", listenerSubmit, false); + } + + // Set the "submit" handler to get the data on submission based on input + nodeList = doc.getElementsByTagName("input"); + for (int i = 0; i < nodeList.getLength(); i++) { + String type = ((Element) nodeList.item(i)).getAttribute("type"); + if ("image".equals(type) || "submit".equals(type)) { + EventTarget target = (EventTarget) nodeList.item(i); + target.addEventListener("click", listenerSubmit, false); + } + } + // Set the "submit" handler to get the data on submission based on button + nodeList = doc.getElementsByTagName("button"); + for (int i = 0; i < nodeList.getLength(); i++) { + String type = ((Element) nodeList.item(i)).getAttribute("type"); + if (type == null || "submit".equals(type)) { + EventTarget target = (EventTarget) nodeList.item(i); + target.addEventListener("click", listenerSubmit, false); + } + } + // Restores the previous scrolling. + if (!scrollReset) { + scrollTo(scrollX, scrollY); + } + } + + /** + * Handle a request to register a macro callback. + * + * @param type The type of event. + * @param link The link to the macro. + */ + private void doRegisterMacro(String type, String link) { + if (actionListeners != null) { + if (log.isDebugEnabled()) { + log.debug("registerMacro event: type='" + type + "' link='" + link + "'"); + } + actionListeners.actionPerformed( + new HTMLActionEvent.RegisterMacroActionEvent(this, type, link)); + } + } + + /** + * Handles the CSS and the events of a link. For a stylesheet link with a macro location as a + * href, the CSS sheet is attached at the end of the refNode. If the href instead starts with + * "macro", register the href as a callback macro. + * + * @param attr the attributes of the link tag + * @param refNode the node to append the new CSS rules to + * @param doc the document to update with the modified link + */ + private void fixLink(NamedNodeMap attr, Node refNode, Document doc) { + Node rel = attr.getNamedItem("rel"); + Node type = attr.getNamedItem("type"); + Node href = attr.getNamedItem("href"); + + if (rel != null && type != null && href != null) { + String content = href.getTextContent(); + if (rel.getTextContent().equalsIgnoreCase("stylesheet")) { + String[] vals = content.split("@"); + if (vals.length != 2) { + return; + } + try { + String cssText = MapTool.getParser().getTokenLibMacro(vals[0], vals[1]); + Element styleNode = doc.createElement("style"); + Text styleContent = doc.createTextNode(cssText); + styleNode.appendChild(styleContent); + // Append the style sheet node to the refNode + refNode.appendChild(styleNode); + } catch (ParserException e) { + // Do nothing + } + } else if (type.getTextContent().equalsIgnoreCase("macro")) { + if (rel.getTextContent().equalsIgnoreCase("onChangeImpersonated")) { + doRegisterMacro("onChangeImpersonated", content); + } else if (rel.getTextContent().equalsIgnoreCase("onChangeSelection")) { + doRegisterMacro("onChangeSelection", content); + } else if (rel.getTextContent().equalsIgnoreCase("onChangeToken")) { + doRegisterMacro("onChangeToken", content); + } + } + } + } + + /** + * Handles the href events. MacroLinks are executed, external links open the browsers, and anchor + * links scroll the browser to the link. + * + * @param event the href event triggered + */ + private void fixHref(org.w3c.dom.events.Event event) { + if (log.isDebugEnabled()) { + log.debug("Responding to hyperlink event: " + event.getType() + " " + event.toString()); + } + + final String href = ((Element) event.getCurrentTarget()).getAttribute("href"); + if (href != null && !href.equals("")) { + String href2 = href.trim().toLowerCase(); + if (href2.startsWith("macro")) { + // ran as macroLink; + SwingUtilities.invokeLater(() -> MacroLinkFunction.runMacroLink(href)); + } else if (href2.startsWith("#")) { + // Java bug JDK-8199014 workaround + webEngine.executeScript(String.format(SCRIPT_ANCHOR, href.substring(1))); + } else if (!href2.startsWith("javascript")) { + // non-macrolink, non-anchor link, non-javascript code + MapTool.showDocument(href); // show in usual browser + } + event.preventDefault(); // don't change webview + } + } + + /** + * Handle a change in title. + * + * @param title The title to change to. + */ + private void doChangeTitle(String title) { + if (actionListeners != null) { + if (log.isDebugEnabled()) { + log.debug("changeTitle event: " + title); + } + actionListeners.actionPerformed(new HTMLActionEvent.ChangeTitleActionEvent(this, title)); + } + } + + /** + * Handle any meta tag information in the html. + * + * @param element the element of the meta tag. + */ + private void handleMetaTag(Element element) { + String name = element.getAttribute("name"); + String content = element.getAttribute("content"); + + if (actionListeners != null && name != null && content != null) { + if (log.isDebugEnabled()) { + log.debug("metaTag found: name='" + name + "' content='" + content + "'"); + } + actionListeners.actionPerformed(new HTMLActionEvent.MetaTagActionEvent(this, name, content)); + } + } + + /** + * Get the data of the form and submit it as a json. + * + * @param event the event of the form submission + */ + private void getDataAndSubmit(org.w3c.dom.events.Event event) { + boolean formnovalidate = false; // if true, the form validation is bypassed + HTMLFormElement form = null; + Element target = (Element) event.getCurrentTarget(); + JsonObject jObj = new JsonObject(); + // Get the form based on the target of the event + if (target instanceof HTMLFormElement) { + form = (HTMLFormElement) target; + } else if (target instanceof HTMLInputElement) { + HTMLInputElement input = (HTMLInputElement) target; + form = input.getForm(); + addToObject(jObj, input.getName(), input.getValue()); + formnovalidate = input.getAttribute("formnovalidate") != null; + } else if (target instanceof HTMLButtonElement) { + HTMLButtonElement button = (HTMLButtonElement) target; + form = button.getForm(); + addToObject(jObj, button.getName(), button.getValue()); + formnovalidate = button.getAttribute("formnovalidate") != null; + } + if (form == null) return; + + // Check for non-macrolinktext action + String action = form.getAction(); + if (action == null || action.startsWith("javascript:")) { + return; + } + + // Check for validity + boolean novalidate = form.getAttribute("novalidate") != null; + if (!formnovalidate && !novalidate) { + JSObject jsObject = (JSObject) form; + if (!(boolean) jsObject.call("checkValidity")) { + return; + } + } + + event.preventDefault(); // prevent duplicated form submit + + // Gets the data from the form + final HTMLCollection collection = form.getElements(); + for (int i = 0; i < collection.getLength(); i++) { + String name, value; + if (collection.item(i) instanceof HTMLInputElement) { + HTMLInputElement element = (HTMLInputElement) collection.item(i); + String type = element.getType().toLowerCase(); + if (type.equals("checkbox") || type.equals("radio")) { + if (element.getChecked()) { + name = element.getName(); + value = element.getValue(); + } else continue; // skip unchecked elements + } else if (type.equals("submit") || type.equals("image")) { + continue; // skip input button/images that were not pressed + } else { + name = element.getName(); + value = element.getValue(); + } + } else if (collection.item(i) instanceof HTMLSelectElement) { + HTMLSelectElement element = (HTMLSelectElement) collection.item(i); + name = element.getName(); + value = element.getValue(); + } else if (collection.item(i) instanceof HTMLTextAreaElement) { + HTMLTextAreaElement element = (HTMLTextAreaElement) collection.item(i); + name = element.getName(); + value = element.getValue(); + } else continue; // skip elements not containing data + addToObject(jObj, name, value); + } + String data = URLEncoder.encode(jObj.toString(), StandardCharsets.UTF_8); + doSubmit("json", action, data); + } + + /** + * Convenience method to put name and value in the object. + * + * @param jObj the JsonObject to put the data in + * @param name the name + * @param value the value + */ + private static void addToObject(JsonObject jObj, String name, String value) { + if (name != null && !"".equals(name)) { + value = value == null ? "" : value; + try { + BigDecimal number = new BigDecimal(value); + jObj.addProperty(name, number); + } catch (NumberFormatException nfe) { + JsonElement json = JSONMacroFunctions.getInstance().asJsonElement(value); + jObj.add(name, json); + } + } + } + + /** + * Handle a submit. + * + * @param method The method of the submit. + * @param action The action for the submit. + * @param data The data from the form. + */ + private void doSubmit(String method, String action, String data) { + if (actionListeners != null) { + if (log.isDebugEnabled()) { + log.debug( + "submit event: method='" + method + "' action='" + action + "' data='" + data + "'"); + } + actionListeners.actionPerformed( + new HTMLActionEvent.FormActionEvent(this, method, action, data)); + } + } + + /** Returns the vertical scroll value, i.e. thumb position. */ + private int getVScrollValue() { + return (Integer) webEngine.executeScript("document.body.scrollTop"); + } + + /** Returns the horizontal scroll value, i.e. thumb position. */ + private int getHScrollValue() { + return (Integer) webEngine.executeScript("document.body.scrollLeft"); + } + + /** + * Scrolls the WebView. + * + * @param x the horizontal scrolling + * @param y the vertical scrolling + */ + private void scrollTo(int x, int y) { + webEngine.executeScript("window.scrollTo(" + x + ", " + y + ")"); + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java index 297378a88d..8a6c0c5678 100644 --- a/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java +++ b/src/main/java/net/rptools/maptool/client/ui/zone/ZoneRenderer.java @@ -4910,7 +4910,7 @@ public void setCursor(Cursor cursor) { } // Overlay and ZoneRenderer should have same cursor super.setCursor(cursor); - MapTool.getFrame().getHtmlOverlay().setCursor(cursor); + MapTool.getFrame().getOverlayPanel().setOverlayCursor(cursor); } private Cursor custom = null;