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 }