forked from RPTools/maptool
/
HTMLOverlayPanel.java
389 lines (356 loc) · 12.2 KB
/
HTMLOverlayPanel.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
/*
* 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.awt.image.BufferedImage;
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.AppMenuBar;
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 {
enum mousePassResult {
BLOCK,
PASS,
CHECK_OPACITY
}
/** 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()) {
if (overlay.isVisible()) {
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()) {
if (overlay.isVisible()) {
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);
AppMenuBar.removeFromOverlayMenu(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());
AppMenuBar.removeFromOverlayMenu(overlay.getName());
}
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());
AppMenuBar.addToOverlayMenu(overlayManager);
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;
}
}
/**
* Determines if the mouse event should be forwarded, and if so dispatch it to the ZoneRenderer.
*
* @param e the mouse event
*/
private void mayPassClick(MouseEvent e) {
Platform.runLater(
() -> {
// get the result based on the most restrictive CSS of all overlays
mousePassResult result = getMousePassResult(e);
if (result != mousePassResult.BLOCK) {
SwingUtilities.invokeLater(
() -> {
if (result == mousePassResult.PASS || !isOpaque(e.getX(), e.getY())) {
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 mousePassResult getMousePassResult(MouseEvent e) {
mousePassResult globalResult = mousePassResult.PASS;
for (HTMLOverlayManager overlay : overlays.values()) {
mousePassResult result = overlay.getMousePassResult(e.getX(), e.getY());
if (result == mousePassResult.BLOCK) {
globalResult = mousePassResult.BLOCK;
break;
} else if (result == mousePassResult.CHECK_OPACITY) {
globalResult = mousePassResult.CHECK_OPACITY;
}
}
return globalResult;
}
/**
* Returns whether the overlay is opaque (alpha > 0) at the x,y pixel. Method provided by gthanop
* at https://stackoverflow.com/questions/60906929
*
* @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;
}
/**
* 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());
}
}
}
}