1 /* 2 * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.oracle.javafx.jmx; 27 28 import java.awt.image.BufferedImage; 29 import java.io.File; 30 import java.util.ArrayList; 31 import java.util.Iterator; 32 import java.util.LinkedHashMap; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.concurrent.CountDownLatch; 36 import javax.imageio.ImageIO; 37 38 import javafx.collections.ObservableList; 39 import javafx.geometry.Bounds; 40 import javafx.scene.Node; 41 import javafx.scene.Parent; 42 import javafx.scene.Scene; 43 import javafx.scene.image.Image; 44 import javafx.stage.Window; 45 import javafx.embed.swing.SwingFXUtils; 46 47 import com.oracle.javafx.jmx.json.JSONDocument; 48 import javafx.css.CssMetaData; 49 import javafx.css.Styleable; 50 import javafx.css.StyleableProperty; 51 import com.sun.javafx.jmx.HighlightRegion; 52 import com.sun.javafx.jmx.MXNodeAlgorithm; 53 import com.sun.javafx.jmx.MXNodeAlgorithmContext; 54 import com.sun.javafx.scene.SceneHelper; 55 import com.sun.javafx.stage.WindowHelper; 56 import com.sun.javafx.tk.TKPulseListener; 57 import com.sun.javafx.tk.TKScene; 58 import com.sun.javafx.tk.Toolkit; 59 import com.sun.media.jfxmedia.AudioClip; 60 import com.sun.media.jfxmedia.MediaManager; 61 import com.sun.media.jfxmedia.MediaPlayer; 62 import com.sun.media.jfxmedia.events.PlayerStateEvent.PlayerState; 63 64 /** 65 * Default implementation of {@link SGMXBean} interface. 66 */ 67 public class SGMXBeanImpl implements SGMXBean, MXNodeAlgorithm { 68 69 private static final String SGMX_NOT_PAUSED_TEXT = "Scene-graph is not PAUSED."; 70 private static final String SGMX_CALL_GETSGTREE_FIRST = "You need to call getSGTree() first."; 71 72 private boolean paused = false; 73 74 private Map<Integer, Window> windowMap = null; 75 private JSONDocument jwindows = null; 76 private Map<Integer, Node> nodeMap = null; 77 private JSONDocument[] jsceneGraphs = null; 78 private Map<Scene, BufferedImage> scene2Image = null; 79 80 private List<MediaPlayer> playersToResume = null; 81 82 /** 83 * {@inheritDoc} 84 */ 85 @Override 86 public void pause() { 87 if (paused) { 88 return; 89 } 90 paused = true; 91 releaseAllStateObject(); 92 Toolkit tk = Toolkit.getToolkit(); 93 tk.pauseScenes(); 94 pauseMedia(); 95 } 96 97 /** 98 * {@inheritDoc} 99 */ 100 @Override 101 public void resume() { 102 if (!paused) { 103 return; 104 } 105 paused = false; 106 releaseAllStateObject(); 107 Toolkit tk = Toolkit.getToolkit(); 108 tk.resumeScenes(); 109 resumeMedia(); 110 } 111 112 /** 113 * {@inheritDoc} 114 */ 115 @Override 116 public void step() throws IllegalStateException { 117 if (!paused) { 118 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 119 } 120 121 releaseAllStateObject(); 122 123 Toolkit tk = Toolkit.getToolkit(); 124 final CountDownLatch onePulseLatch = new CountDownLatch(1); 125 126 tk.setLastTkPulseListener(new TKPulseListener() { 127 @Override public void pulse() { 128 onePulseLatch.countDown(); 129 } 130 }); 131 132 tk.resumeScenes(); 133 134 try { 135 onePulseLatch.await(); 136 } catch (InterruptedException e) { } 137 138 tk.pauseScenes(); 139 tk.setLastTkPulseListener(null); 140 } 141 142 /** 143 * {@inheritDoc} 144 */ 145 @Override 146 public String getWindows() throws IllegalStateException { 147 if (!paused) { 148 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 149 } 150 importWindowsIfNeeded(); 151 return jwindows.toJSON(); 152 } 153 154 /** 155 * {@inheritDoc} 156 */ 157 @Override 158 public String getSGTree(int windowId) throws IllegalStateException { 159 if (!paused) { 160 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 161 } 162 importWindowsIfNeeded(); 163 if (nodeMap == null) { 164 nodeMap = new LinkedHashMap<Integer, Node>(); 165 } 166 if (jsceneGraphs[windowId] == null) { 167 final Window window = windowMap.get(windowId); 168 this.importSGTree(window.getScene().getRoot(), windowId); 169 } 170 return jsceneGraphs[windowId].toJSON(); 171 } 172 173 /** 174 * {@inheritDoc} 175 */ 176 @Override 177 public void addHighlightedNode(int nodeId) throws IllegalStateException { 178 if (!paused) { 179 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 180 } 181 Toolkit.getToolkit().getHighlightedRegions().add( 182 createHighlightRegion(nodeId)); 183 SceneHelper.getPeer(getNode(nodeId).getScene()).markDirty(); 184 } 185 186 /** 187 * {@inheritDoc} 188 */ 189 @Override 190 public void removeHighlightedNode(int nodeId) throws IllegalStateException { 191 if (!paused) { 192 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 193 } 194 Toolkit.getToolkit().getHighlightedRegions().remove( 195 createHighlightRegion(nodeId)); 196 SceneHelper.getPeer(getNode(nodeId).getScene()).markDirty(); 197 } 198 199 /** 200 * {@inheritDoc} 201 */ 202 @Override 203 public void addHighlightedRegion(int windowId, double x, double y, double w, double h) 204 throws IllegalStateException 205 { 206 if (!paused) { 207 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 208 } 209 TKScene scenePeer = SceneHelper.getPeer(getScene(windowId)); 210 Toolkit.getToolkit().getHighlightedRegions().add( 211 new HighlightRegion(scenePeer, x, y, w, h)); 212 scenePeer.markDirty(); 213 } 214 215 /** 216 * {@inheritDoc} 217 */ 218 @Override 219 public void removeHighlightedRegion(int windowId, double x, double y, double w, double h) 220 throws IllegalStateException 221 { 222 if (!paused) { 223 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 224 } 225 TKScene scenePeer = SceneHelper.getPeer(getScene(windowId)); 226 Toolkit.getToolkit().getHighlightedRegions().remove( 227 new HighlightRegion(scenePeer, x, y, w, h)); 228 scenePeer.markDirty(); 229 } 230 231 private Node getNode(int nodeId) { 232 if (nodeMap == null) { 233 throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST); 234 } 235 Node node = nodeMap.get(nodeId); 236 if (node == null) { 237 throw new IllegalArgumentException("Wrong node id."); 238 } 239 return node; 240 } 241 242 private Scene getScene(int windowId) { 243 if (windowMap == null) { 244 throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST); 245 } 246 Window window = windowMap.get(windowId); 247 if (window == null) { 248 throw new IllegalArgumentException("Wrong window id."); 249 } 250 return window.getScene(); 251 } 252 253 private HighlightRegion createHighlightRegion(int nodeId) { 254 Node node = getNode(nodeId); 255 Bounds bounds = node.localToScene(node.getBoundsInLocal()); 256 return new HighlightRegion(SceneHelper.getPeer(node.getScene()), 257 bounds.getMinX(), 258 bounds.getMinY(), 259 bounds.getWidth(), 260 bounds.getHeight()); 261 } 262 263 /** 264 * {@inheritDoc} 265 */ 266 @Override 267 public String makeScreenShot(int nodeId) throws IllegalStateException { 268 if (!paused) { 269 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 270 } 271 if (nodeMap == null) { 272 throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST); 273 } 274 275 Node node = nodeMap.get(nodeId); 276 if (node == null) { 277 return null; 278 } 279 280 Scene scene = node.getScene(); 281 Bounds sceneBounds = node.localToScene(node.getBoundsInLocal()); 282 return getScreenShotPath(scene, sceneBounds.getMinX(), sceneBounds.getMinY(), 283 sceneBounds.getWidth(), sceneBounds.getHeight()); 284 } 285 286 /** 287 * {@inheritDoc} 288 */ 289 @Override 290 public String makeScreenShot(int windowId, double x, double y, double w, double h) 291 throws IllegalStateException 292 { 293 if (!paused) { 294 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 295 } 296 if (nodeMap == null) { 297 throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST); 298 } 299 Scene scene = getScene(windowId); 300 return getScreenShotPath(scene, x, y, w, h); 301 } 302 303 private String getScreenShotPath(Scene scene, double x, double y, double w, double h) { 304 if (scene2Image == null) { 305 scene2Image = new LinkedHashMap<Scene, BufferedImage>(); 306 } 307 308 BufferedImage bufferedImage = scene2Image.get(scene); 309 if (bufferedImage == null) { 310 Image fxImage = scene.snapshot(null); 311 bufferedImage = SwingFXUtils.fromFXImage(fxImage, null); 312 scene2Image.put(scene, bufferedImage); 313 } 314 315 BufferedImage nodeImage = bufferedImage.getSubimage((int)x, (int)y, (int)w, (int)h); 316 317 File tmpFile = null; 318 try { 319 tmpFile = File.createTempFile("jfx", ".png"); 320 ImageIO.write(nodeImage, "PNG", tmpFile); 321 tmpFile.deleteOnExit(); 322 return tmpFile.getAbsolutePath(); 323 } catch (Exception e) { 324 e.printStackTrace(); 325 } 326 return null; 327 } 328 329 private void releaseAllStateObject() { 330 clearWindowMap(); 331 jwindows = null; 332 clearNodeMap(); 333 jsceneGraphs = null; 334 clearScene2Image(); 335 } 336 337 private void clearWindowMap() { 338 if (windowMap != null) { 339 windowMap.clear(); 340 windowMap = null; 341 } 342 } 343 344 private void clearNodeMap() { 345 if (nodeMap != null) { 346 nodeMap.clear(); 347 nodeMap = null; 348 } 349 } 350 351 private void clearScene2Image() { 352 if (scene2Image != null) { 353 scene2Image.clear(); 354 scene2Image = null; 355 } 356 } 357 358 private void importWindowsIfNeeded() { 359 if (windowMap == null) { 360 windowMap = new LinkedHashMap<Integer, Window>(); 361 this.importWindows(); 362 } 363 } 364 365 private void importWindows() { 366 int windowCount = 0; 367 final List<Window> windows = Window.getWindows(); 368 369 jwindows = JSONDocument.createArray(); 370 for (Window window : windows) { 371 windowMap.put(windowCount, window); 372 373 final JSONDocument jwindow = JSONDocument.createObject(); 374 jwindow.setNumber("id", windowCount); 375 jwindow.setString("type", WindowHelper.getMXWindowType(window)); 376 jwindows.array().add(jwindow); 377 windowCount++; 378 } 379 380 jsceneGraphs = new JSONDocument[windowCount]; 381 } 382 383 private void importSGTree(Node sgRoot, int windowId) { 384 if (sgRoot == null) { 385 return; 386 } 387 jsceneGraphs[windowId] = (JSONDocument)sgRoot.impl_processMXNode(this, 388 new MXNodeAlgorithmContext(nodeMap.size())); 389 } 390 391 /** 392 * {@inheritDoc} 393 */ 394 @Override 395 public String getCSSInfo(int nodeId) throws IllegalStateException { 396 if (!paused) { 397 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 398 } 399 if (nodeMap == null) { 400 throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST); 401 } 402 403 Node node = nodeMap.get(nodeId); 404 if (node == null) { 405 return null; 406 } 407 408 JSONDocument d = new JSONDocument(JSONDocument.Type.OBJECT); 409 410 List<CssMetaData<? extends Styleable, ?>> styleables = node.getCssMetaData(); 411 412 for (CssMetaData sp: styleables) { 413 processCssMetaData(sp, node, d); 414 } 415 416 return d.toJSON(); 417 } 418 419 private static void processCssMetaData(CssMetaData sp, Node node, JSONDocument d) { 420 421 List<CssMetaData> subProps = sp.getSubProperties(); 422 if (subProps != null && !subProps.isEmpty()) { 423 for (CssMetaData subSp: subProps) { 424 processCssMetaData(subSp, node, d); 425 } 426 } 427 428 try { 429 StyleableProperty writable = sp.getStyleableProperty(node); 430 Object value = writable != null ? writable.getValue() : null; 431 if (value != null) { 432 d.setString(sp.getProperty(), value.toString()); 433 } else { 434 d.setString(sp.getProperty(), "null"); 435 } 436 } catch (Exception e) { 437 System.out.println(e); 438 e.printStackTrace(); 439 } 440 } 441 442 private static String upcaseFirstLetter(String s) { 443 return s.substring(0, 1).toUpperCase() + s.substring(1); 444 } 445 446 /** 447 * {@inheritDoc} 448 */ 449 @Override 450 public String getBounds(int nodeId) throws IllegalStateException { 451 if (!paused) { 452 throw new IllegalStateException(SGMX_NOT_PAUSED_TEXT); 453 } 454 if (nodeMap == null) { 455 throw new IllegalStateException(SGMX_CALL_GETSGTREE_FIRST); 456 } 457 458 Node node = nodeMap.get(nodeId); 459 if (node == null) { 460 return null; 461 } 462 463 Bounds sceneBounds = node.localToScene(node.getBoundsInLocal()); 464 JSONDocument d = JSONDocument.createObject(); 465 d.setNumber("x", sceneBounds.getMinX()); 466 d.setNumber("y", sceneBounds.getMinY()); 467 d.setNumber("w", sceneBounds.getWidth()); 468 d.setNumber("h", sceneBounds.getHeight()); 469 470 return d.toJSON(); 471 } 472 473 /** 474 * {@inheritDoc} 475 */ 476 @Override 477 public Object processLeafNode(Node node, MXNodeAlgorithmContext ctx) { 478 return createJSONDocument(node, ctx); 479 } 480 481 /** 482 * {@inheritDoc} 483 */ 484 @Override 485 public Object processContainerNode(Parent parent, MXNodeAlgorithmContext ctx) { 486 JSONDocument d = createJSONDocument(parent, ctx); 487 488 final ObservableList<Node> children = parent.getChildrenUnmodifiable(); 489 490 JSONDocument childrenDoc = JSONDocument.createArray(children.size()); 491 d.set("children", childrenDoc); 492 493 for (int i = 0; i < children.size(); i++) { 494 childrenDoc.set(i, (JSONDocument)children.get(i).impl_processMXNode(this, ctx)); 495 } 496 return d; 497 } 498 499 private JSONDocument createJSONDocument(Node n, MXNodeAlgorithmContext ctx) { 500 int id = ctx.getNextInt(); 501 502 nodeMap.put(id, n); 503 504 JSONDocument d = JSONDocument.createObject(); 505 d.setNumber("id", id); 506 d.setString("class", n.getClass().getSimpleName()); 507 return d; 508 } 509 510 private void pauseMedia() { 511 AudioClip.stopAllClips(); 512 513 List<MediaPlayer> allPlayers = MediaManager.getAllMediaPlayers(); 514 if (allPlayers == null) { 515 return; 516 } 517 if ((!allPlayers.isEmpty()) && (playersToResume == null)) { 518 playersToResume = new ArrayList<MediaPlayer>(); 519 } 520 for (MediaPlayer player: allPlayers) { 521 if (player.getState() == PlayerState.PLAYING) { 522 player.pause(); 523 playersToResume.add(player); 524 } 525 } 526 } 527 528 private void resumeMedia() { 529 if (playersToResume == null) { 530 return; 531 } 532 for (MediaPlayer player: playersToResume) { 533 player.play(); 534 } 535 playersToResume.clear(); 536 } 537 }