From 6437a0f912eaa4f3b0a6cb88cf8e3e61c2f3e83d Mon Sep 17 00:00:00 2001 From: Merudo Date: Tue, 31 Mar 2020 07:59:46 -0500 Subject: [PATCH] Add overlay() function to display a html overlay - Add function overlay() to display a transparent html 3.2 overlay over the map - Function is used similarly to frame() and dialog(), with [overlay():{ myhtml }] used to display "myhtml" - Discussed in #1425 --- .../maptool/client/MapToolLineParser.java | 15 ++ .../maptool/client/ui/MapToolFrame.java | 58 ++++-- .../client/ui/htmlframe/HTMLOverlay.java | 181 ++++++++++++++++++ .../maptool/client/ui/htmlframe/HTMLPane.java | 42 +++- .../client/ui/htmlframe/HTMLPanel.java | 18 +- .../maptool/client/ui/zone/ZoneRenderer.java | 17 +- 6 files changed, 300 insertions(+), 31 deletions(-) create mode 100644 src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlay.java diff --git a/src/main/java/net/rptools/maptool/client/MapToolLineParser.java b/src/main/java/net/rptools/maptool/client/MapToolLineParser.java index cb89fc24b4..6ed6376438 100644 --- a/src/main/java/net/rptools/maptool/client/MapToolLineParser.java +++ b/src/main/java/net/rptools/maptool/client/MapToolLineParser.java @@ -209,6 +209,7 @@ private enum CodeType { // Mutually exclusive code-execution options private enum OutputLoc { // Mutually exclusive output location CHAT, DIALOG, + OVERLAY, DIALOG5, FRAME, FRAME5 @@ -357,6 +358,8 @@ private enum OptionType { DIALOG5("dialog5", 1, 2, "\"\""), // HTML webView FRAME5("frame5", 1, 2, "\"\""), + // HTML overlay + OVERLAY("overlay", 0, 1, "\"\""), // Run for another token TOKEN("token", 1, 1); @@ -517,6 +520,10 @@ private void parseOptionString(String optionString, int start) throws RollOption matcher.region(start, endOfString); List paramList = new ArrayList(); boolean lastItem = false; // true if last match ended in ")" + if (")".equals(optionString.substring(start))) { + lastItem = true; + start += 1; + } while (!lastItem) { if (matcher.find()) { @@ -1028,6 +1035,11 @@ public String parseLine( frameOpts = option.getParsedParam(1, resolver, tokenInContext).toString(); outputTo = OutputLoc.FRAME5; break; + case OVERLAY: + codeType = CodeType.CODEBLOCK; + outputTo = OutputLoc.OVERLAY; + frameOpts = option.getParsedParam(0, resolver, tokenInContext).toString(); + break; /////////////////////////////////////////////////// // CODE OPTIONS /////////////////////////////////////////////////// @@ -1424,6 +1436,9 @@ public String parseLine( HTMLFrameFactory.show( frameName, false, false, frameOpts, expressionBuilder.toString()); break; + case OVERLAY: + MapTool.getFrame().getHtmlOverlay().updateContents(expressionBuilder.toString()); + break; case CHAT: builder.append(expressionBuilder); break; 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 fa68bdee42..9ce48179e9 100644 --- a/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java +++ b/src/main/java/net/rptools/maptool/client/ui/MapToolFrame.java @@ -16,18 +16,7 @@ import com.jidesoft.docking.DefaultDockableHolder; import com.jidesoft.docking.DockableFrame; -import java.awt.BorderLayout; -import java.awt.Color; -import java.awt.Component; -import java.awt.Desktop; -import java.awt.EventQueue; -import java.awt.GraphicsConfiguration; -import java.awt.GridBagConstraints; -import java.awt.GridBagLayout; -import java.awt.GridLayout; -import java.awt.IllegalComponentStateException; -import java.awt.Image; -import java.awt.Rectangle; +import java.awt.*; import java.awt.desktop.AboutEvent; import java.awt.desktop.AboutHandler; import java.awt.desktop.PreferencesEvent; @@ -123,6 +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.lookuptable.LookupTablePanel; import net.rptools.maptool.client.ui.macrobuttons.buttons.MacroButton; import net.rptools.maptool.client.ui.macrobuttons.panels.*; @@ -181,6 +171,8 @@ public class MapToolFrame extends DefaultDockableHolder private final ClientConnectionPanel connectionPanel; /** The panel showing the initiative order. */ private final InitiativePanel initiativePanel; + /** The HTML pane showing the map overlay. */ + private final HTMLOverlay htmlOverlay; private final PointerOverlay pointerOverlay; private final CommandPanel commandPanel; @@ -462,6 +454,7 @@ public MapToolFrame(JMenuBar menuBar) { connectionPanel = createConnectionPanel(); toolbox = new Toolbox(); initiativePanel = createInitiativePanel(); + htmlOverlay = new HTMLOverlay(); zoneRendererList = new CopyOnWriteArrayList(); pointerOverlay = new PointerOverlay(); @@ -512,9 +505,15 @@ public MapToolFrame(JMenuBar menuBar) { commandPanel = new CommandPanel(); MapTool.getMessageList().addObserver(commandPanel); + // Setup a JLayeredPane to display the HTML overlay over the map. + JLayeredPane zoneRenderLayered = new JLayeredPane(); + zoneRenderLayered.setLayout(new LayeredPaneLayout()); + zoneRenderLayered.add(zoneRendererPanel, JLayeredPane.DEFAULT_LAYER); + zoneRenderLayered.add(htmlOverlay, JLayeredPane.POPUP_LAYER); + rendererBorderPanel = new JPanel(new GridLayout()); rendererBorderPanel.setBorder(BorderFactory.createLineBorder(Color.darkGray)); - rendererBorderPanel.add(zoneRendererPanel); + rendererBorderPanel.add(zoneRenderLayered); toolbarPanel = new ToolbarPanel(toolbox); // Put it all together @@ -1509,6 +1508,11 @@ public ZoneRenderer getCurrentZoneRenderer() { return currentRenderer; } + /** @return the HTMLOverlay */ + public HTMLOverlay getHtmlOverlay() { + return htmlOverlay; + } + public void addZoneRenderer(ZoneRenderer renderer) { zoneRendererList.add(renderer); } @@ -2101,4 +2105,32 @@ public void actionPerformed(ActionEvent e) { } } } + + /** + * Layout for LayeredPanel where the bounds of every component is set to the size of the parent. + */ + private static class LayeredPaneLayout implements LayoutManager { + @Override + public void addLayoutComponent(String name, Component comp) {} + + @Override + public void removeLayoutComponent(Component comp) {} + + @Override + public Dimension preferredLayoutSize(Container parent) { + return parent.getSize(); + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return preferredLayoutSize(parent); + } + + @Override + public void layoutContainer(Container parent) { + for (Component comp : parent.getComponents()) { + comp.setBounds(0, 0, parent.getWidth(), parent.getHeight()); + } + } + } } diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlay.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlay.java new file mode 100644 index 0000000000..7cf1c36606 --- /dev/null +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLOverlay.java @@ -0,0 +1,181 @@ +/* + * 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 + * and specifically the Affero license + * text at . + */ +package net.rptools.maptool.client.ui.htmlframe; + +import java.awt.*; +import java.awt.dnd.DropTargetDragEvent; +import java.awt.dnd.DropTargetDropEvent; +import java.awt.dnd.DropTargetEvent; +import java.awt.dnd.DropTargetListener; +import java.awt.event.*; +import java.awt.image.BufferedImage; +import java.util.List; +import java.util.TooManyListenersException; +import javax.swing.*; +import javax.swing.text.*; +import net.rptools.maptool.client.AppPreferences; +import net.rptools.maptool.client.MapTool; +import net.rptools.maptool.client.ScreenPoint; +import net.rptools.maptool.client.TransferableHelper; +import net.rptools.maptool.client.ui.zone.ZoneRenderer; +import net.rptools.maptool.model.Token; +import net.rptools.maptool.model.ZonePoint; + +/** Represents the transparent HTML overlay over the map. */ +public class HTMLOverlay extends HTMLPane implements DropTargetListener { + /** The default rule for an invisible body tag. */ + private static final String CSS_RULE_BODY = + "body { font-family: sans-serif; font-size: %dpt; background: none}"; + + public HTMLOverlay() { + super(); + setFocusable(false); + setHighlighter(null); + setOpaque(false); + addMouseListeners(); + setCaretColor(new Color(0, 0, 0, 0)); // invisible, needed or it shows in DnD operations + + setTransferHandler(new TransferableHelper()); // set the Drag & Drop handler + try { + getDropTarget().addDropTargetListener(this); + } catch (TooManyListenersException e1) { + // Should never happen because the transfer handler fixes this problem. + } + } + + /** + * Return the rule for an invisible body. + * + * @return the rule + */ + @Override + public String getRuleBody() { + return String.format(CSS_RULE_BODY, AppPreferences.getFontSize()); + } + + @Override + public void dragEnter(DropTargetDragEvent dtde) {} + + @Override + public void dragOver(DropTargetDragEvent dtde) {} + + @Override + public void dropActionChanged(DropTargetDragEvent dtde) {} + + @Override + public void dragExit(DropTargetEvent dte) {} + + /** + * Add the tokens to the current zone renderer if a token is dropped on the overlay. + * + * @param dtde the event of the drop + */ + @Override + public void drop(DropTargetDropEvent dtde) { + ZoneRenderer zr = MapTool.getFrame().getCurrentZoneRenderer(); + Point point = SwingUtilities.convertPoint(this, dtde.getLocation(), zr); + + ZonePoint zp = new ScreenPoint((int) point.getX(), (int) point.getY()).convertToZone(zr); + TransferableHelper th = (TransferableHelper) getTransferHandler(); + List tokens = th.getTokens(); + if (tokens != null && !tokens.isEmpty()) { + zr.addTokens(tokens, zp, th.getConfigureTokens(), false); + } + } + + /** Add the mouse listeners to forward the mouse events to the current ZoneRenderer. */ + 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 mousePressed(MouseEvent e) { + passMouseEvent(e, true); + } + + @Override + public void mouseClicked(MouseEvent e) { + passMouseEvent(e, true); + } + + @Override + public void mouseEntered(MouseEvent e) { + passMouseEvent(e); + } + + @Override + public void mouseReleased(MouseEvent e) { + passMouseEvent(e); + } + + @Override + public void mouseExited(MouseEvent e) { + passMouseEvent(e); + } + }); + } + + /** + * Passes a mouse event to the ZoneRenderer. If checking for transparency, only forwards the event + * if it happened over a transparent pixel. + * + * @param e the mouse event to forward + * @param checkForTransparency whether to check for transparency + */ + private void passMouseEvent(MouseEvent e, boolean checkForTransparency) { + SwingUtilities.invokeLater( + () -> { + if (checkForTransparency && isOpaque(e.getPoint())) { + return; // don't forward + } + Component c = MapTool.getFrame().getCurrentZoneRenderer(); + c.dispatchEvent(SwingUtilities.convertMouseEvent(e.getComponent(), e, c)); + }); + } + + /** + * Passes a mouse event to the ZoneRenderer. + * + * @param e the mouse event to forward + */ + private void passMouseEvent(MouseEvent e) { + passMouseEvent(e, false); + } + + /** + * Returns true if the pixel of the component at the point is opaque. + * + * @param p the point + * @return true if the pixel is opaque + */ + public boolean isOpaque(Point p) { + Rectangle rect = getBounds(); + BufferedImage img = new BufferedImage(rect.width, rect.height, BufferedImage.TYPE_INT_ARGB); + paintAll(img.createGraphics()); + return new Color(img.getRGB(p.x, p.y), true).getAlpha() != 0; + } +} diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLPane.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLPane.java index 8ec1845b7f..ee568a94b5 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLPane.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLPane.java @@ -95,6 +95,11 @@ public void hyperlinkUpdate(HyperlinkEvent e) { ToolTipManager.sharedInstance().registerComponent(this); } + /** @return the rule for the body tag */ + public String getRuleBody() { + return String.format(CSS_RULE_BODY, AppPreferences.getFontSize()); + } + public void addActionListener(ActionListener listener) { actionListeners = AWTEventMulticaster.add(actionListeners, listener); } @@ -103,6 +108,41 @@ public void removeActionListener(ActionListener listener) { actionListeners = AWTEventMulticaster.remove(actionListeners, listener); } + /** + * Set the default cursor of the editor kit. + * + * @param cursor the cursor to set + */ + public void setEditorKitDefaultCursor(Cursor cursor) { + editorKit.setDefaultCursor(cursor); + } + + /** + * Flush the pane, set the new html, and set the caret to zero. + * + * @param html the html to set + */ + public void updateContents(final String html) { + EventQueue.invokeLater( + new Runnable() { + public void run() { + editorKit.flush(); + setText(html); + setCaretPosition(0); + } + }); + } + + /** Flushes any caching for the panel. */ + public void flush() { + EventQueue.invokeLater( + new Runnable() { + public void run() { + editorKit.flush(); + } + }); + } + /** * Handle a submit. * @@ -188,7 +228,7 @@ public void setText(String text) { style.removeStyle(s); } - style.addRule(String.format(CSS_RULE_BODY, AppPreferences.getFontSize())); + style.addRule(getRuleBody()); style.addRule(CSS_RULE_DIV); style.addRule(CSS_RULE_SPAN); parse.parse(new StringReader(text), new ParserCallBack(), true); diff --git a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLPanel.java b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLPanel.java index 0ee15feb7b..f3b9cb6b1a 100644 --- a/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLPanel.java +++ b/src/main/java/net/rptools/maptool/client/ui/htmlframe/HTMLPanel.java @@ -15,7 +15,6 @@ package net.rptools.maptool.client.ui.htmlframe; import java.awt.BorderLayout; -import java.awt.EventQueue; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; @@ -24,7 +23,6 @@ import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.KeyStroke; -import net.rptools.maptool.client.swing.MessagePanelEditorKit; /** Represents the JPanel holding the HTML pane. */ public class HTMLPanel extends JPanel implements HTMLPanelInterface { @@ -69,25 +67,13 @@ public void actionPerformed(ActionEvent e) { */ @Override public void updateContents(final String html) { - EventQueue.invokeLater( - new Runnable() { - public void run() { - ((MessagePanelEditorKit) pane.getEditorKit()).flush(); - pane.setText(html); - pane.setCaretPosition(0); - } - }); + pane.updateContents(html); } /** Flushes any caching for the panel. */ @Override public void flush() { - EventQueue.invokeLater( - new Runnable() { - public void run() { - ((MessagePanelEditorKit) pane.getEditorKit()).flush(); - } - }); + pane.flush(); } /** 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 90293959d3..780f4bc0c4 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 @@ -4523,6 +4523,7 @@ public LabelLocation(Rectangle bounds, Label label) { * * @see java.awt.dnd.DropTargetListener#dragEnter(java.awt.dnd. DropTargetDragEvent ) */ + @Override public void dragEnter(DropTargetDragEvent dtde) {} /* @@ -4530,6 +4531,7 @@ public void dragEnter(DropTargetDragEvent dtde) {} * * @see java.awt.dnd.DropTargetListener#dragExit(java.awt.dnd.DropTargetEvent) */ + @Override public void dragExit(DropTargetEvent dte) {} /* @@ -4537,9 +4539,18 @@ public void dragExit(DropTargetEvent dte) {} * * @see java.awt.dnd.DropTargetListener#dragOver (java.awt.dnd.DropTargetDragEvent) */ + @Override public void dragOver(DropTargetDragEvent dtde) {} - private void addTokens( + /** + * Adds tokens at a given zone point coordinates. + * + * @param tokens the list of tokens to add + * @param zp the zone point where to add the tokens + * @param configureTokens the list indicating if each token is to be configured + * @param showDialog whether to display a token edit dialog + */ + public void addTokens( List tokens, ZonePoint zp, List configureTokens, boolean showDialog) { GridCapabilities gridCaps = zone.getGrid().getCapabilities(); boolean isGM = MapTool.getPlayer().isGM(); @@ -4739,6 +4750,7 @@ private BufferedImage getTokenImage(Token token) { * * @see java.awt.dnd.DropTargetListener#drop (java.awt.dnd.DropTargetDropEvent) */ + @Override public void drop(DropTargetDropEvent dtde) { ZonePoint zp = new ScreenPoint((int) dtde.getLocation().getX(), (int) dtde.getLocation().getY()) @@ -4767,6 +4779,7 @@ public List getVisibleTokens() { * * @see java.awt.dnd.DropTargetListener#dropActionChanged (java.awt.dnd.DropTargetDragEvent) */ + @Override public void dropActionChanged(DropTargetDragEvent dtde) {} /** ZONE MODEL CHANGE LISTENER */ @@ -4860,7 +4873,9 @@ public void setCursor(Cursor cursor) { custom = createCustomCursor("image/cursor.png", "Group"); cursor = custom; } + // overlay and ZoneRenderer should have same cursor as map super.setCursor(cursor); + MapTool.getFrame().getHtmlOverlay().setEditorKitDefaultCursor(cursor); } private Cursor custom = null;