1 /* 2 * Copyright (c) 2012, 2014, Oracle and/or its affiliates. 3 * All rights reserved. Use is subject to license terms. 4 * 5 * This file is available and licensed under the following license: 6 * 7 * Redistribution and use in source and binary forms, with or without 8 * modification, are permitted provided that the following conditions 9 * are met: 10 * 11 * - Redistributions of source code must retain the above copyright 12 * notice, this list of conditions and the following disclaimer. 13 * - Redistributions in binary form must reproduce the above copyright 14 * notice, this list of conditions and the following disclaimer in 15 * the documentation and/or other materials provided with the distribution. 16 * - Neither the name of Oracle Corporation nor the names of its 17 * contributors may be used to endorse or promote products derived 18 * from this software without specific prior written permission. 19 * 20 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 */ 32 package com.oracle.javafx.scenebuilder.kit.editor.panel.content; 33 34 import com.oracle.javafx.scenebuilder.kit.editor.i18n.I18N; 35 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMDocument; 36 37 import java.util.List; 38 39 import javafx.animation.FadeTransition; 40 import javafx.application.ConditionalFeature; 41 import javafx.application.Platform; 42 import javafx.beans.value.ChangeListener; 43 import javafx.geometry.BoundingBox; 44 import javafx.geometry.Bounds; 45 import javafx.scene.Group; 46 import javafx.scene.Node; 47 import javafx.scene.Parent; 48 import javafx.scene.Scene; 49 import javafx.scene.SubScene; 50 import javafx.scene.control.Label; 51 import javafx.scene.control.ScrollPane; 52 import javafx.scene.layout.Region; 53 import javafx.scene.layout.StackPane; 54 import javafx.scene.shape.Rectangle; 55 import javafx.util.Duration; 56 57 /** 58 * 59 */ 60 class WorkspaceController { 61 62 private static final double AUTORESIZE_SIZE = 500.0; 63 64 private ScrollPane scrollPane; 65 private Group scalingGroup; 66 private SubScene contentSubScene; 67 private Group contentGroup; 68 private Label backgroundPane; 69 private Rectangle extensionRect; 70 private boolean autoResize3DContent = true; 71 private double scaling = 1.0; 72 private RuntimeException layoutException; 73 74 private FXOMDocument fxomDocument; 75 76 public void panelControllerDidLoadFxml(ScrollPane scrollPane, 77 Group scalingGroup, SubScene contentSubScene, Group contentGroup, Label backgroundPane, 78 Rectangle extensionRect) { 79 assert scrollPane != null; 80 assert backgroundPane != null; 81 assert scalingGroup != null; 82 assert contentSubScene != null; 83 assert contentGroup != null; 84 assert extensionRect != null; 85 86 this.scrollPane = scrollPane; 87 this.scalingGroup = scalingGroup; 88 this.contentSubScene = contentSubScene; 89 this.contentGroup = contentGroup; 90 this.backgroundPane = backgroundPane; 91 this.extensionRect = extensionRect; 92 93 // Add scene listener to panelRoot.sceneProperty() 94 this.scrollPane.sceneProperty().addListener((ChangeListener<Scene>) (ov, t, t1) -> sceneDidChange()); 95 96 // Make scalingGroup invisible. 97 // We'll turn it visible once content panel is displayed in a Scene 98 this.scalingGroup.setVisible(false); 99 100 // Remove sample content from contentGroup 101 this.contentGroup.getChildren().clear(); 102 103 updateContentGroup(); 104 updateScalingGroup(); 105 } 106 107 public void setFxomDocument(FXOMDocument fxomDocument) { 108 if (this.fxomDocument != fxomDocument) { 109 this.fxomDocument = fxomDocument; 110 sceneGraphDidChange(); 111 } 112 } 113 114 public void sceneGraphDidChange() { 115 if (this.scrollPane != null) { 116 updateContentGroup(); 117 updateScalingGroup(); 118 } 119 } 120 121 public boolean isAutoResize3DContent() { 122 return autoResize3DContent; 123 } 124 125 public void setAutoResize3DContent(boolean autoResize3DContent) { 126 127 this.autoResize3DContent = autoResize3DContent; 128 if ((scrollPane != null) && (scrollPane.getScene() != null)) { 129 adjustWorkspace(); 130 } 131 } 132 133 public double getScaling() { 134 return scaling; 135 } 136 137 public void setScaling(double scaling) { 138 this.scaling = scaling; 139 updateScalingGroup(); 140 } 141 142 public List<String> getThemeStyleSheets() { 143 final List<String> result; 144 if (contentGroup.getStylesheets().isEmpty()) { 145 result = null; 146 } else { 147 result = contentGroup.getStylesheets(); 148 } 149 return result; 150 } 151 152 public void setThemeStyleSheet(String themeStyleSheet) { 153 assert themeStyleSheet != null; 154 contentSubScene.setUserAgentStylesheet(themeStyleSheet); 155 } 156 157 public void setPreviewStyleSheets(List<String> previewStyleSheets) { 158 contentGroup.getStylesheets().clear(); 159 contentGroup.getStylesheets().addAll(previewStyleSheets); 160 contentGroup.applyCss(); 161 } 162 163 public void layoutContent(boolean applyCSS) { 164 if (scrollPane != null) { 165 try { 166 if (applyCSS) { 167 contentSubScene.getRoot().applyCss(); 168 } 169 scrollPane.layout(); 170 layoutException = null; 171 } catch(RuntimeException x) { 172 layoutException = x; 173 } 174 } 175 } 176 177 public RuntimeException getLayoutException() { 178 return layoutException; 179 } 180 181 public void beginInteraction() { 182 assert scalingGroup.getParent().isManaged(); 183 assert scrollPane.getContent() instanceof StackPane; 184 185 // Makes the user design and enclosing group unmanaged so 186 // that they no longer influence the scroll pane viewport. 187 scalingGroup.getParent().setManaged(false); 188 189 // Renders the top stack pane fully rigid 190 final StackPane contentPane = (StackPane) scrollPane.getContent(); 191 assert contentPane.getMinWidth() == Region.USE_PREF_SIZE; 192 assert contentPane.getMinHeight() == Region.USE_PREF_SIZE; 193 assert contentPane.getPrefWidth() == Region.USE_COMPUTED_SIZE; 194 assert contentPane.getPrefHeight() == Region.USE_COMPUTED_SIZE; 195 assert contentPane.getMaxWidth() == Double.MAX_VALUE; 196 assert contentPane.getMaxHeight() == Double.MAX_VALUE; 197 contentPane.setPrefWidth(contentPane.getWidth()); 198 contentPane.setPrefHeight(contentPane.getHeight()); 199 contentPane.setMaxWidth(Region.USE_PREF_SIZE); 200 contentPane.setMaxHeight(Region.USE_PREF_SIZE); 201 } 202 203 public void endInteraction() { 204 assert scalingGroup.getParent().isManaged() == false; 205 206 // Reverts the top stack pane : it now adjusts to the size of its children 207 final StackPane contentPane = (StackPane) scrollPane.getContent(); 208 assert contentPane.getMinWidth() == Region.USE_PREF_SIZE; 209 assert contentPane.getMinHeight() == Region.USE_PREF_SIZE; 210 assert contentPane.getPrefWidth() != Region.USE_COMPUTED_SIZE; 211 assert contentPane.getPrefHeight() != Region.USE_COMPUTED_SIZE; 212 assert contentPane.getMaxWidth() == Region.USE_PREF_SIZE; 213 assert contentPane.getMaxHeight() == Region.USE_PREF_SIZE; 214 contentPane.setPrefWidth(Region.USE_COMPUTED_SIZE); 215 contentPane.setPrefHeight(Region.USE_COMPUTED_SIZE); 216 contentPane.setMaxWidth(Double.MAX_VALUE); 217 contentPane.setMaxHeight(Double.MAX_VALUE); 218 219 // Reverts scalingGroup setup 220 scalingGroup.getParent().setManaged(true); 221 } 222 223 /* 224 * Private 225 */ 226 227 private void sceneDidChange() { 228 assert this.scrollPane != null; 229 230 if (scrollPane.getScene() != null) { 231 assert scalingGroup.isVisible() == false; 232 233 // Here we'd like to layout the user scene graph immediately 234 // i.e. invoke: 235 // 1) layoutContent() // to relayout user scene graph 236 // 2) adjustWorkspace() // to size the content workspace 237 // 238 // However invoking layoutContent() from here (scene change listener) 239 // does not work very well (see RT-32326). 240 // 241 // So we do these two steps in runLater(). 242 // Until they are done, scalingGroup is kept invisible to avoid 243 // visual artifacts. After the two steps are done, we turn the 244 // visible by calling revealScalingGroup(). 245 246 Platform.runLater(() -> { 247 layoutContent(true /* applyCSS */); 248 adjustWorkspace(); 249 revealScalingGroup(); 250 }); 251 } else { 252 assert scalingGroup.isVisible(); 253 scalingGroup.setVisible(false); 254 } 255 } 256 257 258 private void updateContentGroup() { 259 260 261 /* 262 * fxomRoot 263 */ 264 265 final String statusMessageText, statusStyleClass; 266 contentGroup.getChildren().clear(); 267 268 if (fxomDocument == null) { 269 statusMessageText = "FXOMDocument is null"; //NOI18N 270 statusStyleClass = "stage-prompt"; //NOI18N 271 } else if (fxomDocument.getFxomRoot() == null) { 272 statusMessageText = I18N.getString("content.label.status.invitation"); 273 statusStyleClass = "stage-prompt"; //NOI18N 274 } else { 275 final Object userSceneGraph = fxomDocument.getSceneGraphRoot(); 276 if (userSceneGraph instanceof Node) { 277 final Node rootNode = (Node) userSceneGraph; 278 assert rootNode.getParent() == null; 279 contentGroup.getChildren().add(rootNode); 280 layoutContent(true /* applyCSS */); 281 if (layoutException == null) { 282 statusMessageText = ""; //NOI18N 283 statusStyleClass = "stage-prompt-default"; //NOI18N 284 } else { 285 contentGroup.getChildren().clear(); 286 statusMessageText = I18N.getString("content.label.status.cannot.display"); 287 statusStyleClass = "stage-prompt"; //NOI18N 288 } 289 } else { 290 statusMessageText = I18N.getString("content.label.status.cannot.display"); 291 statusStyleClass = "stage-prompt"; //NOI18N 292 } 293 } 294 295 backgroundPane.setText(statusMessageText); 296 backgroundPane.getStyleClass().clear(); 297 backgroundPane.getStyleClass().add(statusStyleClass); 298 299 // If layoutException != null, then this layout call is required 300 // so that backgroundPane updates its message... Strange... 301 backgroundPane.layout(); 302 303 adjustWorkspace(); 304 } 305 306 private void updateScalingGroup() { 307 if (scalingGroup != null) { 308 final double actualScaling; 309 if (fxomDocument == null) { 310 actualScaling = 1.0; 311 } else if (fxomDocument.getSceneGraphRoot() == null) { 312 actualScaling = 1.0; 313 } else { 314 actualScaling = scaling; 315 } 316 scalingGroup.setScaleX(actualScaling); 317 scalingGroup.setScaleY(actualScaling); 318 319 if (Platform.isSupported(ConditionalFeature.SCENE3D)) { 320 scalingGroup.setScaleZ(actualScaling); 321 } 322 // else { 323 // leave scaleZ unchanged else it breaks zooming when running 324 // with the software pipeline (see DTL-6661). 325 // } 326 } 327 } 328 329 private void adjustWorkspace() { 330 final Bounds backgroundBounds, extensionBounds; 331 332 final Object userSceneGraph; 333 if (fxomDocument == null) { 334 userSceneGraph = null; 335 } else { 336 userSceneGraph = fxomDocument.getSceneGraphRoot(); 337 } 338 if ((userSceneGraph instanceof Node) && (layoutException == null)) { 339 final Node rootNode = (Node) userSceneGraph; 340 341 final Bounds rootBounds = rootNode.getLayoutBounds(); 342 343 if (rootBounds.isEmpty() 344 || (rootBounds.getWidth() == 0.0) 345 || (rootBounds.getHeight() == 0.0)) { 346 backgroundBounds = new BoundingBox(0.0, 0.0, 0.0, 0.0); 347 extensionBounds = new BoundingBox(0.0, 0.0, 0.0, 0.0); 348 } else { 349 final double scale; 350 if ((rootBounds.getDepth() > 0) && autoResize3DContent) { 351 // Content is 3D 352 final double scaleX = AUTORESIZE_SIZE / rootBounds.getWidth(); 353 final double scaleY = AUTORESIZE_SIZE / rootBounds.getHeight(); 354 final double scaleZ = AUTORESIZE_SIZE / rootBounds.getDepth(); 355 scale = Math.min(scaleX, Math.min(scaleY, scaleZ)); 356 } else { 357 scale = 1.0; 358 } 359 contentGroup.setScaleX(scale); 360 contentGroup.setScaleY(scale); 361 contentGroup.setScaleZ(scale); 362 363 final Bounds contentBounds = rootNode.localToParent(rootBounds); 364 backgroundBounds = new BoundingBox(0.0, 0.0, 365 contentBounds.getMinX() + contentBounds.getWidth(), 366 contentBounds.getMinY() + contentBounds.getHeight()); 367 368 369 final Bounds unclippedRootBounds = computeUnclippedBounds(rootNode); 370 assert unclippedRootBounds.getHeight() != 0.0; 371 assert unclippedRootBounds.getWidth() != 0.0; 372 assert rootNode.getParent() == contentGroup; 373 374 final Bounds unclippedContentBounds = rootNode.localToParent(unclippedRootBounds); 375 extensionBounds = computeExtensionBounds(backgroundBounds, unclippedContentBounds); 376 } 377 } else { 378 backgroundBounds = new BoundingBox(0.0, 0.0, 320.0, 150.0); 379 extensionBounds = new BoundingBox(0.0, 0.0, 0.0, 0.0); 380 } 381 382 backgroundPane.setPrefWidth(backgroundBounds.getWidth()); 383 backgroundPane.setPrefHeight(backgroundBounds.getHeight()); 384 extensionRect.setX(extensionBounds.getMinX()); 385 extensionRect.setY(extensionBounds.getMinY()); 386 extensionRect.setWidth(extensionBounds.getWidth()); 387 extensionRect.setHeight(extensionBounds.getHeight()); 388 389 contentSubScene.setWidth(contentGroup.getLayoutBounds().getWidth()); 390 contentSubScene.setHeight(contentGroup.getLayoutBounds().getHeight()); 391 } 392 393 private static Bounds computeUnclippedBounds(Node node) { 394 final Bounds layoutBounds; 395 double minX, minY, maxX, maxY, minZ, maxZ; 396 397 assert node != null; 398 assert node.getLayoutBounds().isEmpty() == false; 399 400 layoutBounds = node.getLayoutBounds(); 401 minX = layoutBounds.getMinX(); 402 minY = layoutBounds.getMinY(); 403 maxX = layoutBounds.getMaxX(); 404 maxY = layoutBounds.getMaxY(); 405 minZ = layoutBounds.getMinZ(); 406 maxZ = layoutBounds.getMaxZ(); 407 408 if (node instanceof Parent) { 409 final Parent parent = (Parent) node; 410 411 for (Node child : parent.getChildrenUnmodifiable()) { 412 final Bounds childBounds = child.getBoundsInParent(); 413 minX = Math.min(minX, childBounds.getMinX()); 414 minY = Math.min(minY, childBounds.getMinY()); 415 maxX = Math.max(maxX, childBounds.getMaxX()); 416 maxY = Math.max(maxY, childBounds.getMaxY()); 417 minZ = Math.min(minZ, childBounds.getMinZ()); 418 maxZ = Math.max(maxZ, childBounds.getMaxZ()); 419 } 420 } 421 422 assert minX <= maxX; 423 assert minY <= maxY; 424 assert minZ <= maxZ; 425 426 return new BoundingBox(minX, minY, minZ, maxX-minX, maxY-minY, maxZ-minZ); 427 } 428 429 430 private static Bounds computeExtensionBounds(Bounds backgroundBounds, 431 Bounds unclippedContentBounds) { 432 final Bounds totalBounds = unionOfBounds(backgroundBounds, unclippedContentBounds); 433 final double backgroundCenterX, backgroundCenterY; 434 backgroundCenterX = (backgroundBounds.getMinX() + backgroundBounds.getMaxX()) / 2.0; 435 backgroundCenterY = (backgroundBounds.getMinY() + backgroundBounds.getMaxY()) / 2.0; 436 assert totalBounds.contains(backgroundCenterX, backgroundCenterY); 437 double extensionHalfWidth, extensionHalfHeight; 438 extensionHalfWidth = Math.max( 439 backgroundCenterX - totalBounds.getMinX(), 440 totalBounds.getMaxX() - backgroundCenterX); 441 extensionHalfHeight = Math.max( 442 backgroundCenterY - totalBounds.getMinY(), 443 totalBounds.getMaxY() - backgroundCenterY); 444 445 // We a few pixels in order the parent ring of root object 446 // to fit inside the extension rect. 447 extensionHalfWidth += 20.0; 448 extensionHalfHeight += 20.0; 449 450 return new BoundingBox( 451 backgroundCenterX - extensionHalfWidth, 452 backgroundCenterY - extensionHalfHeight, 453 extensionHalfWidth * 2, 454 extensionHalfHeight * 2); 455 } 456 457 458 private static Bounds unionOfBounds(Bounds b1, Bounds b2) { 459 final Bounds result; 460 461 if (b1.isEmpty()) { 462 result = b2; 463 } else if (b2.isEmpty()) { 464 result = b1; 465 } else { 466 final double minX = Math.min(b1.getMinX(), b2.getMinX()); 467 final double minY = Math.min(b1.getMinY(), b2.getMinY()); 468 final double minZ = Math.min(b1.getMinZ(), b2.getMinZ()); 469 final double maxX = Math.max(b1.getMaxX(), b2.getMaxX()); 470 final double maxY = Math.max(b1.getMaxY(), b2.getMaxY()); 471 final double maxZ = Math.max(b1.getMaxZ(), b2.getMaxZ()); 472 473 assert minX <= maxX; 474 assert minY <= maxY; 475 assert minZ <= maxZ; 476 477 result = new BoundingBox(minX, minY, minZ, maxX-minX, maxY-minY, maxZ-minZ); 478 } 479 480 return result; 481 } 482 483 484 private void revealScalingGroup() { 485 assert scalingGroup.isVisible() == false; 486 487 scalingGroup.setVisible(true); 488 scalingGroup.setOpacity(0.0); 489 490 FadeTransition showHost = new FadeTransition(Duration.millis(300), scalingGroup); 491 showHost.setFromValue(0.0); 492 showHost.setToValue(1.0); 493 showHost.play(); 494 } 495 }