1 /*
   2  * Copyright (c) 2011, 2013, 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 javafx.scene;
  27 
  28 import javafx.collections.ObservableList;
  29 import com.sun.javafx.geom.BaseBounds;
  30 import com.sun.javafx.geom.transform.BaseTransform;
  31 import com.sun.javafx.jmx.MXNodeAlgorithm;
  32 import com.sun.javafx.jmx.MXNodeAlgorithmContext;
  33 import com.sun.javafx.sg.prism.NGGroup;
  34 import com.sun.javafx.sg.prism.NGNode;
  35 import org.junit.Rule;
  36 import org.junit.Test;
  37 import org.junit.rules.ExpectedException;
  38 import static org.junit.Assert.assertEquals;
  39 import static org.junit.Assert.assertNull;
  40 import static org.junit.Assert.assertSame;
  41 import static org.junit.Assert.assertTrue;
  42 import static org.junit.Assert.fail;
  43 
  44 
  45 /**
  46  * Tests structural aspects of scene graph manipulation. See RT-4095.
  47  *
  48  * The following notation is used in test names to indicate various
  49  * relationships:
  50  *   CL = clip child
  51  *   G  = Group child
  52  *   S  = Scene child
  53  *
  54  * Several relationships are checked in each test, typically in
  55  * the following order:
  56  *
  57  * parent's clip
  58  * parent's children (for Group and Region)
  59  * relationships of second parent (if any)
  60  * child's clipParent
  61  * child's parent
  62  * child's scene
  63  */
  64 
  65 public class StructureTest {
  66     @Rule
  67     public ExpectedException thrown = ExpectedException.none();
  68 
  69     // TODO:
  70     //  - various nasty observableArrayList updates to Group.content and Scene.content.
  71     //  - various bind expressions.
  72 
  73     /////////////////////////
  74     // Setup and teardown. //
  75     /////////////////////////
  76 
  77     //////////////////////
  78     // Helper Functions //
  79     //////////////////////
  80 
  81     int occurs(Node child, ObservableList<Node> content) {
  82         int count = 0;
  83         if (content != null) {
  84             for (Node node : content) {
  85                 if (node == child) {
  86                     count++; 
  87                 }
  88             }
  89         }
  90         return count;
  91     }
  92     
  93     /**
  94      * Checks whether child occurs exactly once in the content observableArrayList.
  95      */
  96     boolean occursOnce(Node child, ObservableList<Node> content) {
  97         return 1 == occurs(child, content);
  98     }
  99 
 100     /**
 101      * Checks whether child does not occur in the content observableArrayList.
 102      */
 103     boolean occursZero(Node child, ObservableList<Node> content) {
 104         return 0 == occurs(child, content);
 105     }
 106 
 107     /**
 108      * Checks whether the child node appears exactly once in the
 109      * Group's content observableArrayList.
 110      */
 111     boolean isChild(Node child, Group group) {
 112         return occursOnce(child, group.getChildren());
 113     }
 114 
 115     /**
 116      * Checks whether the child node is the root of the Scene.
 117      */
 118     boolean isRoot(Parent root, Scene scene) {
 119         return root == scene.getRoot();
 120     }
 121 
 122     /**
 123      * Checks whether the child node does not appear in the
 124      * Group's content observableArrayList.
 125      *
 126      * We use assertTrue(notChild(child, parent)) instead of
 127      * assertFalse(isChild(child, parent)) because we want
 128      * to catch cases where child occurs more than once in
 129      * the parent's content observableArrayList.
 130      */
 131     boolean notChild(Node child, Group group) {
 132         return occursZero(child, group.getChildren());
 133     }
 134 
 135     /**
 136      * Checks whether the child node does not appear as the root of the
 137      * Scene.
 138      */
 139     boolean notRoot(Parent root, Scene scene) {
 140         return root != scene.getRoot();
 141     }
 142 
 143     /////////////////////////////////////
 144     // Simple Structural Relationships //
 145     /////////////////////////////////////
 146 
 147     @Test
 148     public void testOrphan() {
 149         Node n = new StubNode();
 150 
 151         assertNull("clipParent is null", n.getClipParent());
 152         assertNull("parent is null", n.getParent());
 153         assertNull("scene is null", n.getScene());
 154     }
 155 
 156     @Test
 157     public void testSimpleCL() {
 158         StubNode parent = new StubNode();
 159         StubNode child = new StubNode();
 160         parent.setClip(child);
 161 
 162         assertSame("parent.clip is child", child, parent.getClip());
 163         assertSame("child.clipParent is parent", parent, child.getClipParent());
 164         assertNull("child.parent is null", child.getParent());
 165         assertNull("scene is null", child.getScene());
 166     }
 167 
 168     @Test
 169     public void testSimpleG() {
 170         StubNode child = new StubNode();
 171         Group group = new Group(child);
 172 
 173         assertNull("group.clip is null", group.getClip());
 174         assertTrue("isChild of group", isChild(child, group));
 175         assertNull("child.clipParent is null", child.getClipParent());
 176         assertSame("child.parent is parent", group, child.getParent());
 177         assertNull("child.getScene() is null", child.getScene());
 178     }
 179 
 180     @Test
 181     public void testSimpleS() {
 182         StubParent root = new StubParent();
 183         Scene scene = new Scene(root);
 184 
 185         assertTrue("isChild of scene", isRoot(root, scene));
 186         assertNull("child.clipParent is null", root.getClipParent());
 187         assertSame("child.getScene() is scene", scene, root.getScene());
 188     }
 189 
 190     @Test
 191     public void testSceneInsertGroup1() {
 192         StubNode child = new StubNode();
 193         Group group = new Group();
 194         Scene scene = new Scene(group);
 195         group.getChildren().add(child);
 196         
 197         assertSame("group.getScene() is scene", scene, group.getScene());
 198         assertSame("child.getScene() is scene", scene, child.getScene());
 199     }
 200 
 201     @Test
 202     public void testSceneInsertGroup2() {
 203         StubNode child = new StubNode();
 204         Group group = new Group();
 205         Scene scene = new Scene(group);
 206         group.getChildren().add(child);
 207         
 208 
 209         assertSame("group.getScene() is scene", scene, group.getScene());
 210         assertSame("child.getScene() is scene", scene, child.getScene());
 211     }
 212 
 213     @Test public void testUnparentCL() {
 214         StubNode child = new StubNode();
 215         StubNode parent = new StubNode();
 216         parent.setClip(child);
 217         parent.setClip(null);
 218 
 219         assertNull("parent.clip is null", parent.getClip());
 220         assertNull("child.clipParent is null", child.getClipParent());
 221     }
 222 
 223     @Test public void testUnparentG() {
 224         StubNode child = new StubNode();
 225         Group parent = new Group(child);
 226         
 227         parent.getChildren().remove(child);
 228         
 229 
 230         assertEquals("parent.content is zero size", 0, parent.getChildren().size());
 231         assertNull("child.parent is null", child.getParent());
 232     }
 233 
 234     ////////////////////////////////////
 235     // Illegal Structure Change Tests //
 236     ////////////////////////////////////
 237 
 238     // Test attempts to switch from one part of the scene graph to another.
 239     // This is the cross product: {CL,CU,G,S}x{CL,CU,G,S} so there
 240     // are sixteen cases.
 241 
 242     @Test public void testSwitchCLCL() {
 243         StubNode child = new StubNode();
 244         StubNode p1 = new StubNode();
 245         p1.setClip(child);
 246         StubNode p2 = new StubNode();
 247         thrown.expect(IllegalArgumentException.class);
 248         try {
 249             p2.setClip(child);
 250         } catch (final IllegalArgumentException e) {
 251             assertSame("p1.clip is child", child, p1.getClip());
 252             assertNull("p2.clip is null", p2.getClip());
 253             assertSame("child.clipParent is p1",
 254                        p1, child.getClipParent());
 255             assertNull("child.parent is null", child.getParent());
 256             assertNull("child.getScene() is null", child.getScene());
 257             throw e;
 258         }
 259     }
 260 
 261     @Test public void testSwitchCLG() {
 262         StubNode child = new StubNode();
 263         StubNode p1 = new StubNode();
 264         p1.setClip(child);
 265         Group p2 = new Group();
 266         ObservableList<Node> content = p2.getChildren();
 267         try {
 268             content.add(child);
 269             fail("IllegalArgument should have been thrown.");
 270         } catch (IllegalArgumentException iae) {
 271             // expected
 272         }
 273         
 274         assertSame("p1.clip is child", child, p1.getClip());
 275         assertNull("p2.clip is null", p2.getClip());
 276         assertTrue("notChild of p2", notChild(child, p2));
 277         assertSame("child.clipParent is p1", p1, child.getClipParent());
 278         assertNull("child.parent is null", child.getParent());
 279         assertNull("child.getScene() is null", child.getScene());
 280     }
 281 
 282     @Test public void testSwitchCLS() {
 283         StubParent clipNode = new StubParent();
 284         StubNode p1 = new StubNode();
 285         p1.setClip(clipNode);
 286         try {
 287             Scene p2 = new Scene(clipNode);
 288             fail("IllegalArgument should have been thrown.");
 289         } catch (Throwable t) {
 290             //expected
 291         }
 292         assertSame("p1.clip is child", clipNode, p1.getClip());
 293         assertSame("child.clipParent is p1", p1, clipNode.getClipParent());
 294         assertNull("child.parent is null", clipNode.getParent());
 295         assertNull("child.getScene() is null", clipNode.getScene());
 296     }
 297 
 298     @Test public void testSwitchGCL() {
 299         StubNode child = new StubNode();
 300         Group p1 = new Group(child);
 301         StubNode p2 = new StubNode();
 302         thrown.expect(IllegalArgumentException.class);
 303         try {
 304             p2.setClip(child);
 305         } catch (final IllegalArgumentException e) {
 306             assertNull("p1.clip is null", p1.getClip());
 307             assertTrue("isChild of p1", isChild(child, p1));
 308             assertNull("p2.clip is null", p2.getClip());
 309             assertNull("child.clipParent is null", child.getClipParent());
 310             assertSame("child.parent is p1", p1, child.getParent());
 311             assertNull("child.getScene() is null", child.getScene());
 312             throw e;
 313         }
 314     }
 315 
 316 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED
 317 
 318 //    @Test public void testSwitchGG() {
 319 //        var child = new StubNode();
 320 //        var p1 = Group { content: [ child ] };
 321 //        setHandler();
 322 //        var p2 = Group { content: [ child ] };
 323 //
 324 //        assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException);
 325 //        assertNull("p1.clip is null", p1.clip);
 326 //        assertTrue("isChild of p1", isChild(child, p1));
 327 //        assertNull("p2.clip is null", p2.clip);
 328 //        assertTrue("notChild of p2", notChild(child, p2));
 329 //        assertNull("child.clipParent is null", child.getClipParent());
 330 //        assertSame("child.parent is p1", p1, child.parent);
 331 //        assertNull("child.getScene() is null", child.getScene());
 332 //    }
 333 
 334 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED
 335 
 336 //    @Test public void testSwitchGS() {
 337 //        var child = new StubNode();
 338 //        var p1 = Group { content: [ child ] };
 339 //        setHandler();
 340 //        var p2 = Scene { content: [ child ] };
 341 //
 342 //        assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException);
 343 //        assertNull("p1.clip is null", p1.clip);
 344 //        assertTrue("isChild of p1", isChild(child, p1));
 345 //        assertTrue("notChild of p2", notChild(child, p2));
 346 //        assertNull("child.clipParent is null", child.getClipParent());
 347 //        assertSame("child.parent is p1", p1, child.parent);
 348 //        assertNull("child.getScene() is null", child.getScene());
 349 //    }
 350 
 351 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST OF STOPGAP POLICY
 352 
 353     @Test public void testSwitchGGStopgap() {
 354         StubNode child = new StubNode();
 355         Group p1 = new Group(child);
 356         Group p2 = new Group(child);
 357 
 358         assertTrue("notChild of p1", notChild(child, p1));
 359         assertTrue("isChild of p2", isChild(child, p2));
 360         assertSame("child.parent is p2", p2, child.getParent());
 361     }
 362 
 363     @Test public void testSwitchSCL() {
 364         StubParent root = new StubParent();
 365         Scene scene = new Scene(root);
 366         StubNode p2 = new StubNode();
 367         thrown.expect(IllegalArgumentException.class);
 368         try {
 369             p2.setClip(root);
 370         } catch (final IllegalArgumentException e) {
 371             assertTrue("isRoot of scene", isRoot(root, scene));
 372             assertNull("p2.clip is null", p2.getClip());
 373             assertNull("root.clipParent is null", root.getClipParent());
 374             assertSame("root.getScene() is scene", scene, root.getScene());
 375             throw e;
 376         }
 377     }
 378 
 379 
 380 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED
 381 
 382 //    @Test public void testSwitchSG() {
 383 //        var child = new StubNode();
 384 //        var p1 = Scene { content: [ child ] };
 385 //        setHandler();
 386 //        var p2 = Group { content: [ child ] };
 387 //
 388 //        assertTrue("caught IllegalArgumentException", caught instanceof IllegalArgumentException);
 389 //        assertTrue("isChild of p1", isChild(child, p1));
 390 //        assertNull("p2.clip is null", p2.clip);
 391 //        assertTrue("notChild of p2", notChild(child, p2));
 392 //        assertNull("child.clipParent is null", child.getClipParent());
 393 //        assertSame("child.parent is p1.impl_root", p1.impl_root, child.parent);
 394 //        assertSame("child.getScene() is p1", p1, child.getScene());
 395 //    }
 396 
 397 // TODO XXX TEMPORARY STOPGAP POLICY RT-4095 -- TEST DISABLED
 398 
 399 //    @Test public void testSwitchSS() {
 400 //        var child = new StubNode();
 401 //        var p1 = Scene { content: [ child ] };
 402 //        setHandler();
 403 //        var p2 = Scene { content: [ child ] };
 404 //
 405 //        assertTrue("isChild of p1", isChild(child, p1));
 406 //        assertTrue("notChild of p2", notChild(child, p2));
 407 //        assertNull("child.clipParent is null", child.getClipParent());
 408 //        assertSame("child.parent is p1.impl_root", p1.impl_root, child.parent);
 409 //        assertSame("child.getScene() is p1", p1, child.getScene());
 410 //    }
 411 
 412     @Test public void testGroupInsert() {
 413         StubNode n0 = new StubNode();
 414         n0.setId("n0");
 415         StubNode n1 = new StubNode();
 416         n1.setId("n1");
 417         StubNode n2 = new StubNode();
 418         n2.setId("n2");
 419         Group g = new Group(n0, n1, n2);
 420         
 421         ObservableList<Node> content = g.getChildren();
 422         try {
 423             content.add(n1);
 424             fail("IllegalArgument should have been thrown.");
 425         } catch (IllegalArgumentException iae) {
 426             // expected
 427         }
 428         
 429         assertEquals("g.content is size 3", 3, g.getChildren().size());
 430         assertSame("g.content[0] is n0", n0, g.getChildren().get(0));
 431         assertSame("g.content[1] is n1", n1, g.getChildren().get(1));
 432         assertSame("g.content[2] is n2", n2, g.getChildren().get(2));
 433         
 434     }
 435 
 436 
 437     @Test public void testGroupReplace1() {
 438         StubNode n0 = new StubNode();
 439         n0.setId("n0");
 440         StubNode n1 = new StubNode();
 441         n1.setId("n1");
 442         StubNode n2 = new StubNode();
 443         n2.setId("n2");
 444         Group g = new Group(n0, n1, n2);
 445         
 446         g.getChildren().remove(1);
 447         ObservableList<Node> n = javafx.collections.FXCollections.<Node>observableArrayList();
 448         n.addAll(n1,n1);
 449         ObservableList<Node> content = g.getChildren();
 450         try {
 451             content.addAll(1, n);
 452             fail("IllegalArgument should have been thrown.");
 453         } catch (IllegalArgumentException iae) {
 454             // expected
 455         }
 456 
 457         assertEquals("g.content is size 2", 2, g.getChildren().size());
 458         assertSame("g.content[0] is n0", n0, g.getChildren().get(0));
 459         assertSame("g.content[1] is n2", n2, g.getChildren().get(1));
 460     }
 461 
 462     @Test public void testGroupReplace2() {
 463         StubNode n0 = new StubNode();
 464         n0.setId("n0");
 465         StubNode n1 = new StubNode();
 466         n1.setId("n1");
 467         StubNode n2 = new StubNode();
 468         n2.setId("n2");
 469         Group g = new Group(n0, n1, n2);
 470         
 471         try {
 472             g.getChildren().set(1, n0);
 473             fail("No exception thrown.");
 474         } catch (IllegalArgumentException e) {
 475             //Expected
 476         }
 477 
 478         assertEquals("g.content is size 3", 3, g.getChildren().size());
 479         assertSame("g.content[0] is n0", n0, g.getChildren().get(0));
 480         assertSame("g.content[1] is n1", n1, g.getChildren().get(1));
 481         assertSame("g.content[2] is n2", n2, g.getChildren().get(2));
 482     }
 483 
 484     @Test public void testGroupReplace3() {
 485         StubNode n0 = new StubNode();
 486         n0.setId("n0");
 487         StubNode n1 = new StubNode();
 488         n1.setId("n1");
 489         StubNode n2 = new StubNode();
 490         n2.setId("n2");
 491         Group g = new Group(n0, n1, n2);
 492         g.getChildren().set(1, n1);
 493         g.getChildren().set(2, n2);
 494 
 495         assertEquals("g.content is size 3", 3, g.getChildren().size());
 496         assertSame("g.content[0] is n0", n0, g.getChildren().get(0));
 497         assertSame("g.content[1] is n1", n1, g.getChildren().get(1));
 498         assertSame("g.content[2] is n2", n2, g.getChildren().get(2));
 499     }
 500 
 501     ///////////////////////
 502     // Circularity Tests //
 503     ///////////////////////
 504 
 505     // General form is: given an existing relationship of one kind, add
 506     // another relationship of some kind that would cause a circularity.
 507     // This is the cross product: {CL,CU,G}x{CL,CU,G}. Also test degenerate
 508     // circularities where a node is its own clip, its own child.
 509     // The Scene relationship does not occur here, because a Scene cannot
 510     // participate in any circular relationship as it has no parent.
 511 
 512     // Test only {CL,G}x{CL,G} for now.
 513 
 514     @Test public void testCircularCLCL() {
 515         StubNode node1 = new StubNode();
 516         StubNode node2 = new StubNode();
 517         node2.setClip(node1);
 518         thrown.expect(IllegalArgumentException.class);
 519         try {
 520             node1.setClip(node2);
 521         } catch (final IllegalArgumentException e) {
 522             assertNull("node1.clip is null", node1.getClip());
 523             assertSame("node1.clipParent is node2",
 524                        node2,
 525                        node1.getClipParent());
 526             assertSame("node2.clip is node1", node1, node2.getClip());
 527             assertNull("node2.clipParent is null", node2.getClipParent());
 528             throw e;
 529         }
 530     }
 531 
 532     @Test public void testCircularCLG() {
 533         StubNode node1 = new StubNode();
 534         Group node2 = new Group(node1);
 535         thrown.expect(IllegalArgumentException.class);
 536         try {
 537             node1.setClip(node2);
 538         } catch (final IllegalArgumentException e) {
 539             assertNull("node1.clip is null", node1.getClip());
 540             assertNull("node1.clipParent is null", node1.getClipParent());
 541             assertSame("node1.parent is node2", node2, node1.getParent());
 542             assertNull("node2.clip is null", node2.getClip());
 543             assertNull("node2.clipParent is null", node2.getClipParent());
 544             assertTrue("node1 is child of node2", isChild(node1, node2));
 545             throw e;
 546         }
 547     }
 548 
 549     @Test public void testCircularGCL() {
 550         Group node1 = new Group();
 551         StubNode node2 = new StubNode();
 552         node2.setClip(node1);
 553 
 554         ObservableList<Node> content = node1.getChildren();
 555         try {
 556             content.add(node2);
 557             fail("IllegalArgument should have been thrown.");
 558         } catch (IllegalArgumentException iae) {
 559             // expected
 560         }
 561 
 562         assertNull("node1.clip is null", node1.getClip());
 563         assertSame("node1.clipParent is node2", node2, node1.getClipParent());
 564         assertTrue("node2 is not child of node1", notChild(node2, node1));
 565         assertSame("node2.clip is node1", node1, node2.getClip());
 566         assertNull("node2.clipParent is null", node2.getClipParent());
 567         assertNull("node2.parent is null", node2.getParent());
 568     }
 569 
 570     @Test public void testCircularGG() {
 571         Group node1 = new Group();
 572         Group node2 = new Group(node1);
 573         
 574         ObservableList<Node> content = node1.getChildren();
 575         try {
 576             content.add(node2);
 577             fail("IllegalArgument should have been thrown.");
 578         } catch (IllegalArgumentException iae) {
 579             // expected
 580         }
 581 
 582         assertSame("node1.parent is node2", node2, node1.getParent());
 583         assertTrue("node2 is not a child of node1", notChild(node2, node1));
 584         assertNull("node2.parent is null", node2.getParent());
 585         assertTrue("node1 is child of node2", isChild(node1, node2));
 586     }
 587 
 588     @Test public void testCircularSelfCL() {
 589         StubNode node1 = new StubNode();
 590         thrown.expect(IllegalArgumentException.class);
 591         try {
 592             node1.setClip(node1);
 593         } catch (final IllegalArgumentException e) {
 594             assertNull("node1.clip is null", node1.getClip());
 595             assertNull("node1.clipParent is null", node1.getClipParent());
 596             throw e;
 597         }
 598     }
 599 
 600     @Test public void testCircularSelfG() {
 601         Group node1 = new Group();
 602 
 603         ObservableList<Node> content = node1.getChildren();
 604         try {
 605             content.add(node1);
 606             fail("IllegalArgument should have been thrown.");
 607         } catch (IllegalArgumentException iae) {
 608             // expected
 609         }
 610         
 611         assertTrue("node1 is not a child of itself", notChild(node1, node1));
 612         assertNull("node1.parent is null", node1.getParent());
 613     }
 614 
 615     //////////////////////////
 616     // Bound Variable Tests //
 617     //////////////////////////
 618 
 619     // Test various cases where a structure variable (Node.clip,
 620     // Group.content, Scene.content) is initialized to a bind-expression.
 621     // If the trigger attempts to roll back a change to a variable
 622     // initialized this way, the attempt will fail and will throw an
 623     // exception. This will leave the invariant violation in place!
 624     // We can't do anything about this without language support, so
 625     // don't test these cases for now.
 626 
 627 // FAILS:
 628 //    @Test public void testBindClip() {
 629 //        var c:Node = null;
 630 //        var p1 = StubNode { clip: bind c id: "p1" };
 631 //        var p2 = StubNode { clip: bind c id: "p2" };
 632 //        c = StubNode { id: "c" };
 633 //
 634 //        println("testBindClip");
 635 //        println("p1 = {p1}");
 636 //        println("p2 = {p2}");
 637 //        println("c = {c}");
 638 //        println("p1.clip = {p1.clip}");
 639 //        println("p2.clip = {p2.clip}");
 640 //        println("c.clipParent = {c.getClipParent()}");
 641 //    }
 642 
 643 }
 644 
 645 
 646 ////////////////////
 647 // Helper Classes //
 648 ////////////////////
 649 
 650 //
 651 // * A stub node that contains as little functionality as possible.
 652 // *
 653 class StubNode extends Node {
 654     
 655     // * Returning null from impl_createPGNode() causes crashes,
 656     // * so return a PGGroup.
 657     // *
 658     @Override
 659     protected NGNode impl_createPeer() {
 660         return new NGGroup();
 661     }
 662 
 663     @Override
 664     public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 665         return bounds;
 666     }
 667 
 668     @Override
 669     public boolean impl_computeContains(double localX, double localY) {
 670         // TODO: Missing code.
 671         return false;
 672     }
 673 
 674     @Override
 675     public Object impl_processMXNode(MXNodeAlgorithm alg, MXNodeAlgorithmContext ctx) {
 676         return null;
 677     }
 678 }
 679 
 680 class StubParent extends Parent {
 681 
 682     // * Returning null from impl_createPGNode() causes crashes,
 683     // * so return a PGGroup.
 684     // *
 685     @Override
 686     protected NGNode impl_createPeer() {
 687         return new NGGroup();
 688     }
 689 
 690     @Override
 691     public BaseBounds impl_computeGeomBounds(BaseBounds bounds, BaseTransform tx) {
 692         return bounds;
 693     }
 694 
 695     @Override
 696     public boolean impl_computeContains(double localX, double localY) {
 697         // TODO: Missing code.
 698         return false;
 699     }
 700 }
 701