1 /*
   2  * Copyright (c) 2011, 2018, 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 package javafx.scene;
  26 
  27 import java.util.ArrayList;
  28 import java.util.Collections;
  29 import java.util.HashMap;
  30 import java.util.HashSet;
  31 import java.util.List;
  32 import java.util.Locale;
  33 import java.util.Map;
  34 import java.util.Map.Entry;
  35 import java.util.Set;
  36 
  37 import javafx.beans.property.ObjectProperty;
  38 import javafx.beans.property.SimpleObjectProperty;
  39 import javafx.beans.value.WritableValue;
  40 import com.sun.javafx.css.CascadingStyle;
  41 import javafx.css.CssMetaData;
  42 import javafx.css.CssParser;
  43 import javafx.css.FontCssMetaData;
  44 import javafx.css.ParsedValue;
  45 import javafx.css.PseudoClass;
  46 import javafx.css.Rule;
  47 import javafx.css.Selector;
  48 import javafx.css.Style;
  49 import javafx.css.StyleConverter;
  50 import javafx.css.StyleOrigin;
  51 import javafx.css.Styleable;
  52 import javafx.css.StyleableProperty;
  53 import javafx.css.Stylesheet;
  54 import javafx.scene.text.Font;
  55 import javafx.scene.text.FontPosture;
  56 import javafx.scene.text.FontWeight;
  57 
  58 import com.sun.javafx.css.CalculatedValue;
  59 import com.sun.javafx.css.ParsedValueImpl;
  60 import com.sun.javafx.css.PseudoClassState;
  61 import com.sun.javafx.css.StyleCache;
  62 import com.sun.javafx.css.StyleCacheEntry;
  63 import com.sun.javafx.css.StyleManager;
  64 import com.sun.javafx.css.StyleMap;
  65 import javafx.css.converter.FontConverter;
  66 import com.sun.javafx.util.Logging;
  67 import com.sun.javafx.util.Utils;
  68 
  69 import com.sun.javafx.logging.PlatformLogger;
  70 import com.sun.javafx.logging.PlatformLogger.Level;
  71 
  72 import static com.sun.javafx.css.CalculatedValue.*;
  73 
  74 /**
  75  * The StyleHelper is a helper class used for applying CSS information to Nodes.
  76  */
  77 final class CssStyleHelper {
  78 
  79     private static final PlatformLogger LOGGER = com.sun.javafx.util.Logging.getCSSLogger();
  80 
  81     private CssStyleHelper() {
  82         this.triggerStates = new PseudoClassState();
  83     }
  84 
  85     /**
  86      * Creates a new StyleHelper.
  87      */
  88     static CssStyleHelper createStyleHelper(final Node node) {
  89 
  90         // need to know how far we are to root in order to init arrays.
  91         // TODO: should we hang onto depth to avoid this nonsense later?
  92         // TODO: is there some other way of knowing how far from the root a node is?
  93         Styleable parent = node;
  94         int depth = 0;
  95         while(parent != null) {
  96             depth++;
  97             parent = parent.getStyleableParent();
  98         }
  99 
 100         // The List<CacheEntry> should only contain entries for those
 101         // pseudo-class states that have styles. The StyleHelper's
 102         // pseudoclassStateMask is a bitmask of those pseudoclasses that
 103         // appear in the node's StyleHelper's smap. This list of
 104         // pseudo-class masks is held by the StyleCacheKey. When a node is
 105         // styled, its pseudoclasses and the pseudoclasses of its parents
 106         // are gotten. By comparing the actual pseudo-class state to the
 107         // pseudo-class states that apply, a CacheEntry can be created or
 108         // fetched using only those pseudoclasses that matter.
 109         final PseudoClassState[] triggerStates = new PseudoClassState[depth];
 110 
 111         final StyleMap styleMap =
 112                 StyleManager.getInstance().findMatchingStyles(node, node.getSubScene(), triggerStates);
 113 
 114         //
 115         // reuse the existing styleHelper if possible.
 116         //
 117         if ( canReuseStyleHelper(node, styleMap) ) {
 118 
 119             //
 120             // RT-33080
 121             //
 122             // If we're reusing a style helper, clear the fontSizeCache in case either this node or some parent
 123             // node has changed font from a user calling setFont.
 124             //
 125             // It may be the case that the node's font has changed from a call to setFont, which will
 126             // trigger a REAPPLY. If the REAPPLY comes because of a change in font, then the fontSizeCache
 127             // needs to be invalidated (cleared) so that new values will be looked up for all transition states.
 128             //
 129             if (node.styleHelper.cacheContainer != null && node.styleHelper.isUserSetFont(node)) {
 130                 node.styleHelper.cacheContainer.fontSizeCache.clear();
 131             }
 132             node.styleHelper.cacheContainer.forceSlowpath = true;
 133             node.styleHelper.triggerStates.addAll(triggerStates[0]);
 134             node.styleHelper.firstStyleableAncestor = findFirstStyleableAncestor(node);
 135             updateParentTriggerStates(node, depth, triggerStates);
 136             return node.styleHelper;
 137 
 138         }
 139 
 140         if (styleMap == null || styleMap.isEmpty()) {
 141 
 142             boolean mightInherit = false;
 143 
 144             final List<CssMetaData<? extends Styleable, ?>> props = node.getCssMetaData();
 145 
 146             final int pMax = props != null ? props.size() : 0;
 147             for (int p=0; p<pMax; p++) {
 148 
 149                 final CssMetaData<? extends Styleable, ?> prop = props.get(p);
 150                 if (prop.isInherits()) {
 151                     mightInherit = true;
 152                     break;
 153                 }
 154             }
 155 
 156             if (mightInherit == false) {
 157 
 158                 // If this node had a style helper, then reset properties to their initial value
 159                 // since the node won't have a style helper after this call
 160                 if (node.styleHelper != null) {
 161                     node.styleHelper.resetToInitialValues(node, null);
 162                 }
 163 
 164                 //
 165                 // This node didn't have a StyleHelper before and it doesn't need one now since there are
 166                 // no styles in the StyleMap and no inherited styles.
 167                 return null;
 168             }
 169 
 170         }
 171 
 172         final CssStyleHelper helper = new CssStyleHelper();
 173         helper.triggerStates.addAll(triggerStates[0]);
 174 
 175         updateParentTriggerStates(node, depth, triggerStates);
 176 
 177         helper.cacheContainer = new CacheContainer(node, styleMap, depth);
 178 
 179         helper.firstStyleableAncestor = findFirstStyleableAncestor(node);
 180 
 181         // If this node had a style helper, then reset properties to their initial value
 182         // since the style map might now be different
 183         if (node.styleHelper != null) {
 184             StyleMap oldMap = node.styleHelper.getStyleMap(node);
 185             StyleMap newMap = helper.getStyleMap(node);
 186 
 187             if (oldMap != newMap) {
 188                 node.styleHelper.resetToInitialValues(node, null);
 189             } else {
 190                 node.styleHelper.resetToInitialValues(node, newMap);
 191             }
 192         }
 193         return helper;
 194     }
 195 
 196     private static void updateParentTriggerStates(Styleable styleable, int depth, PseudoClassState[] triggerStates) {
 197         // make sure parent's transition states include the pseudo-classes
 198         // found when matching selectors
 199         Styleable parent = styleable.getStyleableParent();
 200         for(int n=1; n<depth; n++) {
 201 
 202             // TODO: this means that a style like .menu-item:hover won't work. Need to separate CssStyleHelper tree from scene-graph tree
 203             if (parent instanceof Node == false) {
 204                 parent=parent.getStyleableParent();
 205                 continue;
 206             }
 207             Node parentNode = (Node)parent;
 208 
 209             final PseudoClassState triggerState = triggerStates[n];
 210 
 211             // if there is nothing in triggerState, then continue since there
 212             // isn't any pseudo-class state that might trigger a state change
 213             if (triggerState != null && triggerState.size() > 0) {
 214 
 215                 // Create a StyleHelper for the parent, if necessary.
 216                 // TODO : check why calling createStyleHelper(parentNode) does not work here?
 217                 if (parentNode.styleHelper == null) {
 218                     parentNode.styleHelper = new CssStyleHelper();
 219                     parentNode.styleHelper.firstStyleableAncestor = findFirstStyleableAncestor(parentNode) ;
 220                 }
 221                 parentNode.styleHelper.triggerStates.addAll(triggerState);
 222 
 223             }
 224 
 225             parent=parent.getStyleableParent();
 226         }
 227 
 228     }
 229     //
 230     // return true if the fontStyleableProperty's origin is USER
 231     //
 232     private boolean isUserSetFont(Styleable node) {
 233 
 234         if (node == null) return false; // should never happen, but just to be safe...
 235 
 236         CssMetaData<Styleable, Font> fontCssMetaData = cacheContainer != null ? cacheContainer.fontProp : null;
 237         if (fontCssMetaData != null) {
 238             StyleableProperty<Font> fontStyleableProperty = fontCssMetaData != null ? fontCssMetaData.getStyleableProperty(node) : null;
 239             if (fontStyleableProperty != null && fontStyleableProperty.getStyleOrigin() == StyleOrigin.USER) return true;
 240         }
 241 
 242         Styleable styleableParent = firstStyleableAncestor;
 243         CssStyleHelper parentStyleHelper = getStyleHelper(firstStyleableAncestor);
 244 
 245         if (parentStyleHelper != null) {
 246             return parentStyleHelper.isUserSetFont(styleableParent);
 247         } else {
 248             return false;
 249         }
 250     }
 251 
 252     private static CssStyleHelper getStyleHelper(Node n) {
 253         return (n != null)? n.styleHelper : null;
 254     }
 255 
 256     private static Node findFirstStyleableAncestor(Styleable st) {
 257         Node ancestor = null;
 258         Styleable parent = st.getStyleableParent();
 259         while (parent != null) {
 260             if (parent instanceof Node) {
 261                 if (((Node) parent).styleHelper != null) {
 262                     ancestor = (Node) parent;
 263                     break;
 264                 }
 265             }
 266             parent = parent.getStyleableParent();
 267         }
 268 
 269         return ancestor;
 270     }
 271 
 272     //
 273     // return the value of the property
 274     //
 275     private static boolean isTrue(WritableValue<Boolean> booleanProperty) {
 276         return booleanProperty != null && booleanProperty.getValue();
 277     }
 278 
 279     //
 280     // set the value of the property to true
 281     //
 282     private static void setTrue(WritableValue<Boolean> booleanProperty) {
 283         if (booleanProperty != null) booleanProperty.setValue(true);
 284     }
 285 
 286     //
 287     // return true if the Node's current styleHelper can be reused.
 288     //
 289     private static boolean canReuseStyleHelper(final Node node, final StyleMap styleMap) {
 290 
 291         // Obviously, we cannot reuse the node's style helper if it doesn't have one.
 292         if (node == null || node.styleHelper == null) {
 293             return false;
 294         }
 295 
 296         // If we have a styleHelper but the new styleMap is null, then we don't need a styleHelper at all
 297         if (styleMap == null) {
 298             return false;
 299         }
 300 
 301         StyleMap currentMap = node.styleHelper.getStyleMap(node);
 302 
 303         // We cannot reuse the style helper if the styleMap is not the same instance as the current one
 304         // Note: check instance equality!
 305         if (currentMap != styleMap) {
 306             return false;
 307         }
 308 
 309         // If the style maps are the same instance, we can re-use the current styleHelper if the cacheContainer is null.
 310         // Under this condition, there are no styles for this node _and_ no styles inherit.
 311         if (node.styleHelper.cacheContainer == null) {
 312             return true;
 313         }
 314 
 315         //
 316         // The current map might be the same, but one of the node's parent's maps might have changed which
 317         // might cause some calculated values to change. To see if we can re-use the style-helper, we need to
 318         // check if the StyleMap id's have changed, which we can do by inspecting the cacheContainer's styleCacheKey
 319         // since it is made up of the current set of StyleMap ids.
 320         //
 321 
 322         Styleable parent = node.getStyleableParent();
 323 
 324         // if the node's parent is null and the style maps are the same, then we can certainly reuse the style-helper
 325         if (parent == null) {
 326             return true;
 327         }
 328 
 329         CssStyleHelper parentHelper = getStyleHelper(node.styleHelper.firstStyleableAncestor);
 330 
 331         if (parentHelper != null && parentHelper.cacheContainer != null) {
 332 
 333             int[] parentIds = parentHelper.cacheContainer.styleCacheKey.getStyleMapIds();
 334             int[] nodeIds = node.styleHelper.cacheContainer.styleCacheKey.getStyleMapIds();
 335 
 336             if (parentIds.length == nodeIds.length - 1) {
 337 
 338                 boolean isSame = true;
 339 
 340                 // check that all of the style map ids are the same.
 341                 for (int i = 0; i < parentIds.length; i++) {
 342                     if (nodeIds[i + 1] != parentIds[i]) {
 343                         isSame = false;
 344                         break;
 345                     }
 346                 }
 347 
 348                 return isSame;
 349 
 350             }
 351         }
 352 
 353         return false;
 354     }
 355 
 356     /* This is the first Styleable parent (of Node this StyleHelper belongs to)
 357      * having a valid StyleHelper */
 358     private Node firstStyleableAncestor;
 359 
 360     private CacheContainer cacheContainer;
 361 
 362     private final static class CacheContainer {
 363 
 364         // Set internal internalState structures
 365         private CacheContainer(
 366                 Node node,
 367                 final StyleMap styleMap,
 368                 int depth) {
 369 
 370             int ctr = 0;
 371             int[] smapIds = new int[depth];
 372             smapIds[ctr++] = this.smapId = styleMap.getId();
 373 
 374             //
 375             // Create a set of StyleMap id's from the parent's smapIds.
 376             // The resulting smapIds array may have less than depth elements.
 377             // If a parent doesn't have a styleHelper or the styleHelper's
 378             // internal state is null, then that parent doesn't contribute
 379             // to the selection of a style. Any Node that has the same
 380             // set of smapId's can potentially share previously calculated
 381             // values.
 382             //
 383             Styleable parent = node.getStyleableParent();
 384             for(int d=1; d<depth; d++) {
 385 
 386                 // TODO: won't work for something like .menu-item:hover. Need to separate CssStyleHelper tree from scene-graph tree
 387                 if ( parent instanceof Node) {
 388                     Node parentNode = (Node)parent;
 389                 final CssStyleHelper helper = parentNode.styleHelper;
 390                     if (helper != null && helper.cacheContainer != null) {
 391                         smapIds[ctr++] = helper.cacheContainer.smapId;
 392                     }
 393                 }
 394                 parent = parent.getStyleableParent();
 395 
 396             }
 397 
 398             this.styleCacheKey = new StyleCache.Key(smapIds, ctr);
 399 
 400             CssMetaData<Styleable,Font> styleableFontProperty = null;
 401 
 402             final List<CssMetaData<? extends Styleable, ?>> props = node.getCssMetaData();
 403             final int pMax = props != null ? props.size() : 0;
 404             for (int p=0; p<pMax; p++) {
 405                 final CssMetaData<? extends Styleable, ?> prop = props.get(p);
 406 
 407                 if ("-fx-font".equals(prop.getProperty())) {
 408                     // unchecked!
 409                     styleableFontProperty = (CssMetaData<Styleable, Font>) prop;
 410                     break;
 411                 }
 412             }
 413 
 414             this.fontProp = styleableFontProperty;
 415             this.fontSizeCache = new HashMap<>();
 416 
 417             this.cssSetProperties = new HashMap<>();
 418 
 419         }
 420 
 421         private StyleMap getStyleMap(Styleable styleable) {
 422             if (styleable != null) {
 423                 SubScene subScene =  (styleable instanceof Node) ? ((Node) styleable).getSubScene() : null;
 424                 return StyleManager.getInstance().getStyleMap(styleable, subScene, smapId);
 425             } else {
 426                 return StyleMap.EMPTY_MAP;
 427             }
 428 
 429         }
 430 
 431         // This is the key we use to find the shared cache
 432         private final StyleCache.Key styleCacheKey;
 433 
 434         // If the node has a fontProperty, we hang onto the CssMetaData for it
 435         // so we can get at it later.
 436         // TBD - why not the fontProperty itself?
 437         private final CssMetaData<Styleable,Font> fontProp;
 438 
 439         // The id of StyleMap that contains the styles that apply to this node
 440         private final int smapId;
 441 
 442         // All nodes with the same set of styles share the same cache of
 443         // calculated values. But one node might have a different font-size
 444         // than another so the values are stored in cache by font-size.
 445         // This map associates a style cache entry with the font to use when
 446         // getting a value from or putting a value into cache.
 447         private final Map<StyleCacheEntry.Key, CalculatedValue> fontSizeCache;
 448 
 449         // Any properties that have been set by this style helper are tracked
 450         // here so the property can be reset without expanding properties that
 451         // were not set by css.
 452         private final Map<CssMetaData, CalculatedValue> cssSetProperties;
 453 
 454         private boolean forceSlowpath = false;
 455     }
 456 
 457     private void resetToInitialValues(final Styleable styleable, StyleMap smap) {
 458 
 459         // This method is invoked on a styleHelper before it is being reset
 460         // to null or a new StyleHelper.
 461         //
 462         // if smap is null/empty -- it means node will not have any new styles
 463         // in immediate future.
 464         //
 465         // Action taken in this method :
 466         // reset all styleableProperties  in cssSetProperties to initial values
 467 
 468         // if smap is a valid StyleMap -- it means node will have styles from
 469         // new stylemap in immediate future
 470         //
 471         // Action taken in this method :
 472         // reset styleableProperties in cssSetProperties in case they are not
 473         // present in smap
 474 
 475         if (cacheContainer == null ||
 476                 cacheContainer.cssSetProperties == null ||
 477                 cacheContainer.cssSetProperties.isEmpty()) return;
 478 
 479         // RT-31714 - make a copy of the entry set and clear the cssSetProperties immediately.
 480         Set<Entry<CssMetaData, CalculatedValue>> entrySet =
 481                 new HashSet<>(cacheContainer.cssSetProperties.entrySet());
 482         cacheContainer.cssSetProperties.clear();
 483 
 484         if (smap == null || smap.isEmpty() ) {
 485             for (Entry<CssMetaData, CalculatedValue> resetValues : entrySet) {
 486                 final CssMetaData metaData = resetValues.getKey();
 487                 setStyleableProperty(metaData.getStyleableProperty(styleable), resetValues.getValue());
 488             }
 489         } else {
 490             Map<String, List<CascadingStyle>> newStyles = smap.getCascadingStyles();
 491 
 492             for (Entry<CssMetaData, CalculatedValue> resetValues : entrySet) {
 493                 final CssMetaData metaData = resetValues.getKey();
 494 
 495                 if (newStyles.containsKey(metaData.getProperty()) == false) {
 496                     // earlier set property is not available in new Stylemap - reset it to initial value
 497                     setStyleableProperty(metaData.getStyleableProperty(styleable), resetValues.getValue());
 498                 }
 499             }
 500         }
 501     }
 502 
 503     private void setStyleableProperty(StyleableProperty property, CalculatedValue value) {
 504         final StyleOrigin styleOrigin = property.getStyleOrigin();
 505         if (styleOrigin != null && styleOrigin != StyleOrigin.USER) {
 506             property.applyStyle(value.getOrigin(), value.getValue());
 507         }
 508     }
 509 
 510     private StyleMap getStyleMap(Styleable styleable) {
 511         if (cacheContainer == null || styleable == null) return null;
 512         return cacheContainer.getStyleMap(styleable);
 513     }
 514 
 515     /**
 516      * A Set of all the pseudo-class states which, if they change, need to
 517      * cause the Node to be set to UPDATE its CSS styles on the next pulse.
 518      * For example, your stylesheet might have:
 519      * <pre><code>
 520      * .button { ... }
 521      * .button:hover { ... }
 522      * .button *.label { text-fill: black }
 523      * .button:hover *.label { text-fill: blue }
 524      * </code></pre>
 525      * In this case, the first 2 rules apply to the Button itself, but the
 526      * second two rules apply to the label within a Button. When the hover
 527      * changes on the Button, however, we must mark the Button as needing
 528      * an UPDATE. StyleHelper though only contains styles for the first two
 529      * rules for Button. The pseudoclassStateMask would in this case have
 530      * only a single bit set for "hover". In this way the StyleHelper associated
 531      * with the Button would know whether a change to "hover" requires the
 532      * button and all children to be update. Other pseudo-class state changes
 533      * that are not in this hash set are ignored.
 534      * *
 535      * Called "triggerStates" since they would trigger a CSS update.
 536      */
 537     private PseudoClassState triggerStates = new PseudoClassState();
 538 
 539     boolean pseudoClassStateChanged(PseudoClass pseudoClass) {
 540         return triggerStates.contains(pseudoClass);
 541     }
 542 
 543     /**
 544      * Dynamic pseudo-class state of the node and its parents.
 545      * Only valid during a pulse.
 546      *
 547      * The StyleCacheEntry to choose depends on the Node's pseudo-class state
 548      * and the pseudo-class state of its parents. Without the parent
 549      * pseudo-class state, the fact that the the node in this pseudo-class state
 550      * matched foo:blah bar { } is lost.
 551      */
 552     // TODO: this should work on Styleable, not Node
 553     private Set<PseudoClass>[] getTransitionStates(final Node node) {
 554 
 555         // if cacheContainer is null, then CSS just doesn't apply to this node
 556         if (cacheContainer == null) return null;
 557 
 558         int depth = 0;
 559         Node parent = node;
 560         while (parent != null) {
 561             depth += 1;
 562             parent = parent.getParent();
 563         }
 564 
 565         //
 566         // StyleHelper#triggerStates is the set of pseudo-classes that appear
 567         // in the style maps of this StyleHelper. Calculated values are
 568         // cached by pseudo-class state, but only the pseudo-class states
 569         // that mater are used in the search. So we take the transition states
 570         // and intersect them with triggerStates to remove the
 571         // transition states that don't matter when it comes to matching states
 572         // on a  selector. For example if the style map contains only
 573         // .foo:hover { -fx-fill: red; } then only the hover state matters
 574         // but the transtion state could be [hover, focused]
 575         //
 576         final Set<PseudoClass>[] retainedStates = new PseudoClassState[depth];
 577 
 578         //
 579         // Note Well: The array runs from leaf to root. That is,
 580         // retainedStates[0] is the pseudo-class state for node and
 581         // retainedStates[1..(states.length-1)] are the retainedStates for the
 582         // node's parents.
 583         //
 584 
 585         int count = 0;
 586         parent = node;
 587         while (parent != null) { // This loop traverses through all ancestors till root
 588             final CssStyleHelper helper = (parent instanceof Node) ? parent.styleHelper : null;
 589             if (helper != null) {
 590                 final Set<PseudoClass> pseudoClassState = parent.pseudoClassStates;
 591                 retainedStates[count] = new PseudoClassState();
 592                 retainedStates[count].addAll(pseudoClassState);
 593                 // retainAll method takes the intersection of pseudoClassState and helper.triggerStates
 594                 retainedStates[count].retainAll(helper.triggerStates);
 595                 count += 1;
 596             }
 597             parent = parent.getParent();
 598         }
 599 
 600         final Set<PseudoClass>[] transitionStates = new PseudoClassState[count];
 601         System.arraycopy(retainedStates, 0, transitionStates, 0, count);
 602 
 603         return transitionStates;
 604 
 605     }
 606 
 607     /**
 608      * Called by the Node whenever it has transitioned from one set of
 609      * pseudo-class states to another. This function will then lookup the
 610      * new values for each of the styleable variables on the Node, and
 611      * then either set the value directly or start an animation based on
 612      * how things are specified in the CSS file. Currently animation support
 613      * is disabled until the new parser comes online with support for
 614      * animations and that support is detectable via the API.
 615      */
 616     void transitionToState(final Node node) {
 617 
 618         if (cacheContainer == null) {
 619             return;
 620         }
 621 
 622         //
 623         // If styleMap is null, then StyleManager has blown it away and we need to reapply CSS.
 624         //
 625         final StyleMap styleMap = getStyleMap(node);
 626         if (styleMap == null) {
 627             cacheContainer = null;
 628             node.reapplyCSS();
 629             return;
 630         }
 631 
 632         // if the style-map is empty, then we are only looking for inherited styles.
 633         final boolean inheritOnly = styleMap.isEmpty();
 634 
 635         //
 636         // Styles that need lookup can be cached provided none of the styles
 637         // are from Node.style.
 638         //
 639         final StyleCache sharedCache = StyleManager.getInstance().getSharedCache(node, node.getSubScene(), cacheContainer.styleCacheKey);
 640 
 641         if (sharedCache == null) {
 642             // Shared cache was blown away by StyleManager.
 643             // Therefore, this CssStyleHelper is no good.
 644             cacheContainer = null;
 645             node.reapplyCSS();
 646             return;
 647 
 648         }
 649 
 650         final Set<PseudoClass>[] transitionStates = getTransitionStates(node);
 651 
 652         final StyleCacheEntry.Key fontCacheKey = new StyleCacheEntry.Key(transitionStates, Font.getDefault());
 653         CalculatedValue cachedFont = cacheContainer.fontSizeCache.get(fontCacheKey);
 654 
 655         if (cachedFont == null) {
 656 
 657             cachedFont = lookupFont(node, "-fx-font", styleMap, cachedFont);
 658 
 659             if (cachedFont == SKIP) cachedFont = getCachedFont(node.getStyleableParent());
 660             if (cachedFont == null) cachedFont = new CalculatedValue(Font.getDefault(), null, false);
 661 
 662             cacheContainer.fontSizeCache.put(fontCacheKey,cachedFont);
 663 
 664         }
 665 
 666         final Font fontForRelativeSizes = (Font)cachedFont.getValue();
 667 
 668         final StyleCacheEntry.Key cacheEntryKey = new StyleCacheEntry.Key(transitionStates, fontForRelativeSizes);
 669         StyleCacheEntry cacheEntry = sharedCache.getStyleCacheEntry(cacheEntryKey);
 670 
 671         // if the cacheEntry already exists, take the fastpath
 672         final boolean fastpath = cacheEntry != null;
 673 
 674         if (cacheEntry == null) {
 675             cacheEntry = new StyleCacheEntry();
 676             sharedCache.addStyleCacheEntry(cacheEntryKey, cacheEntry);
 677         }
 678 
 679         final List<CssMetaData<? extends Styleable,  ?>> styleables = node.getCssMetaData();
 680 
 681         // Used in the for loop below, and a convenient place to stop when debugging.
 682         final int max = styleables.size();
 683 
 684         final boolean isForceSlowpath = cacheContainer.forceSlowpath;
 685         cacheContainer.forceSlowpath = false;
 686 
 687         // For each property that is settable, we need to do a lookup and
 688         // transition to that value.
 689         for(int n=0; n<max; n++) {
 690 
 691             @SuppressWarnings("unchecked") // this is a widening conversion
 692             final CssMetaData<Styleable,Object> cssMetaData =
 693                     (CssMetaData<Styleable,Object>)styleables.get(n);
 694 
 695             // Don't bother looking up styles that don't inherit.
 696             if (inheritOnly && cssMetaData.isInherits() == false) {
 697                 continue;
 698             }
 699 
 700             // Skip the lookup if we know there isn't a chance for this property
 701             // to be set (usually due to a "bind").
 702             if (!cssMetaData.isSettable(node)) continue;
 703 
 704             final String property = cssMetaData.getProperty();
 705 
 706             CalculatedValue calculatedValue = cacheEntry.get(property);
 707 
 708             // If there is no calculatedValue and we're on the fast path,
 709             // take the slow path if cssFlags is REAPPLY (RT-31691)
 710             final boolean forceSlowpath =
 711                     fastpath && calculatedValue == null && isForceSlowpath;
 712 
 713             final boolean addToCache =
 714                     (!fastpath && calculatedValue == null) || forceSlowpath;
 715 
 716             if (fastpath && !forceSlowpath) {
 717 
 718                 // If the cache contains SKIP, then there was an
 719                 // exception thrown from applyStyle
 720                 if (calculatedValue == SKIP) {
 721                     continue;
 722                 }
 723 
 724             } else if (calculatedValue == null) {
 725 
 726                 // slowpath!
 727                 calculatedValue = lookup(node, cssMetaData, styleMap, transitionStates[0],
 728                         node, cachedFont);
 729 
 730                 // lookup is not supposed to return null.
 731                 if (calculatedValue == null) {
 732                     assert false : "lookup returned null for " + property;
 733                     continue;
 734                 }
 735 
 736             }
 737 
 738             // StyleableProperty#applyStyle might throw an exception and it is called
 739             // from two places in this try block.
 740             try {
 741 
 742                 //
 743                 // RT-19089
 744                 // If the current value of the property was set by CSS
 745                 // and there is no style for the property, then reset this
 746                 // property to its initial value. If it was not set by CSS
 747                 // then leave the property alone.
 748                 //
 749                 if (calculatedValue == null || calculatedValue == SKIP) {
 750 
 751                     // cssSetProperties keeps track of the StyleableProperty's that were set by CSS in the previous state.
 752                     // If this property is not in cssSetProperties map, then the property was not set in the previous state.
 753                     // This accomplishes two things. First, it lets us know if the property was set in the previous state
 754                     // so it can be reset in this state if there is no value for it. Second, it calling
 755                     // CssMetaData#getStyleableProperty which is rather expensive as it may cause expansion of lazy
 756                     // properties.
 757                     CalculatedValue initialValue = cacheContainer.cssSetProperties.get(cssMetaData);
 758 
 759                     // if the current value was set by CSS and there
 760                     // is no calculated value for the property, then
 761                     // there was no style for the property in the current
 762                     // state, so reset the property to its initial value.
 763                     if (initialValue != null) {
 764 
 765                         StyleableProperty styleableProperty = cssMetaData.getStyleableProperty(node);
 766                         if (styleableProperty.getStyleOrigin() != StyleOrigin.USER) {
 767                             styleableProperty.applyStyle(initialValue.getOrigin(), initialValue.getValue());
 768                         }
 769                     }
 770 
 771                     continue;
 772 
 773                 }
 774 
 775                 if (addToCache) {
 776 
 777                     // If we're not on the fastpath, then add the calculated
 778                     // value to cache.
 779                     cacheEntry.put(property, calculatedValue);
 780                 }
 781 
 782                 StyleableProperty styleableProperty = cssMetaData.getStyleableProperty(node);
 783 
 784                 // need to know who set the current value - CSS, the user, or init
 785                 final StyleOrigin originOfCurrentValue = styleableProperty.getStyleOrigin();
 786 
 787 
 788                 // RT-10522:
 789                 // If the user set the property and there is a style and
 790                 // the style came from the user agent stylesheet, then
 791                 // skip the value. A style from a user agent stylesheet should
 792                 // not override the user set style.
 793                 //
 794                 final StyleOrigin originOfCalculatedValue = calculatedValue.getOrigin();
 795 
 796                 // A calculated value should never have a null style origin since that would
 797                 // imply the style didn't come from a stylesheet or in-line style.
 798                 if (originOfCalculatedValue == null) {
 799                     assert false : styleableProperty.toString();
 800                     continue;
 801                 }
 802 
 803                 if (originOfCurrentValue == StyleOrigin.USER) {
 804                     if (originOfCalculatedValue == StyleOrigin.USER_AGENT) {
 805                         continue;
 806                     }
 807                 }
 808 
 809                 final Object value = calculatedValue.getValue();
 810                 final Object currentValue = styleableProperty.getValue();
 811 
 812                 // RT-21185: Only apply the style if something has changed.
 813                 if ((originOfCurrentValue != originOfCalculatedValue)
 814                         || (currentValue != null
 815                         ? currentValue.equals(value) == false
 816                         : value != null)) {
 817 
 818                     if (LOGGER.isLoggable(Level.FINER)) {
 819                         LOGGER.finer(property + ", call applyStyle: " + styleableProperty + ", value =" +
 820                                 String.valueOf(value) + ", originOfCalculatedValue=" + originOfCalculatedValue);
 821                     }
 822 
 823                     styleableProperty.applyStyle(originOfCalculatedValue, value);
 824 
 825                     if (cacheContainer.cssSetProperties.containsKey(cssMetaData) == false) {
 826                         // track this property
 827                         CalculatedValue initialValue = new CalculatedValue(currentValue, originOfCurrentValue, false);
 828                         cacheContainer.cssSetProperties.put(cssMetaData, initialValue);
 829                     }
 830 
 831                 }
 832 
 833             } catch (Exception e) {
 834 
 835                 StyleableProperty styleableProperty = cssMetaData.getStyleableProperty(node);
 836 
 837                 final String msg = String.format("Failed to set css [%s] on [%s] due to '%s'\n",
 838                         cssMetaData.getProperty(), styleableProperty, e.getMessage());
 839 
 840                 List<CssParser.ParseError> errors = null;
 841                 if ((errors = StyleManager.getErrors()) != null) {
 842                     final CssParser.ParseError error = new CssParser.ParseError.PropertySetError(cssMetaData, node, msg);
 843                     errors.add(error);
 844                 }
 845 
 846                 PlatformLogger logger = Logging.getCSSLogger();
 847                 if (logger.isLoggable(Level.WARNING)) {
 848                     logger.warning(msg);
 849                 }
 850 
 851                 // RT-27155: if setting value raises exception, reset value
 852                 // the value to initial and thereafter skip setting the property
 853                 cacheEntry.put(property, SKIP);
 854 
 855                 CalculatedValue cachedValue = null;
 856                 if (cacheContainer != null && cacheContainer.cssSetProperties != null) {
 857                     cachedValue = cacheContainer.cssSetProperties.get(cssMetaData);
 858                 }
 859                 Object value = (cachedValue != null) ? cachedValue.getValue() : cssMetaData.getInitialValue(node);
 860                 StyleOrigin origin = (cachedValue != null) ? cachedValue.getOrigin() : null;
 861                 try {
 862                     styleableProperty.applyStyle(origin, value);
 863                 } catch (Exception ebad) {
 864                     // This would be bad.
 865                     if (logger.isLoggable(Level.SEVERE)) {
 866                         logger.severe(String.format("Could not reset [%s] on [%s] due to %s\n" ,
 867                                 cssMetaData.getProperty(), styleableProperty, e.getMessage()));
 868                     }
 869                 }
 870 
 871             }
 872 
 873         }
 874     }
 875 
 876     /**
 877      * Gets the CSS CascadingStyle for the property of this node in these pseudo-class
 878      * states. A null style may be returned if there is no style information
 879      * for this combination of input parameters.
 880      *
 881      *
 882      * @param styleable
 883      * @param property
 884      * @param styleMap
 885      * @param states   @return
 886      * */
 887     private CascadingStyle getStyle(final Styleable styleable, final String property, final StyleMap styleMap, final Set<PseudoClass> states){
 888 
 889         if (styleMap == null || styleMap.isEmpty()) return null;
 890 
 891         final Map<String, List<CascadingStyle>> cascadingStyleMap = styleMap.getCascadingStyles();
 892         if (cascadingStyleMap == null || cascadingStyleMap.isEmpty()) return null;
 893 
 894         // Get all of the Styles which may apply to this particular property
 895         List<CascadingStyle> styles = cascadingStyleMap.get(property);
 896 
 897         // If there are no styles for this property then we can just bail
 898         if ((styles == null) || styles.isEmpty()) return null;
 899 
 900         // Go looking for the style. We do this by visiting each CascadingStyle in
 901         // order finding the first that matches the current node & set of
 902         // pseudo-class states. We use an iteration style that avoids creating
 903         // garbage iterators (and wish javac did it for us...)
 904        CascadingStyle style = null;
 905         final int max = (styles == null) ? 0 : styles.size();
 906         for (int i=0; i<max; i++) {
 907             final CascadingStyle s = styles.get(i);
 908             final Selector sel = s == null ? null : s.getSelector();
 909             if (sel == null) continue; // bail if the selector is null.
 910 //System.out.println(node.toString() + "\n\tstates=" + PseudoClassSet.getPseudoClasses(states) + "\n\tstateMatches? " + sel.stateMatches(node, states) + "\n\tsel=" + sel.toString());
 911             if (sel.stateMatches(styleable, states)) {
 912                 style = s;
 913                 break;
 914             }
 915         }
 916 
 917         return style;
 918     }
 919 
 920     /**
 921      * The main workhorse of this class, the lookup method walks up the CSS
 922      * style tree looking for the style information for the Node, the
 923      * property associated with the given styleable, in these states for this font.
 924      *
 925      *
 926      *
 927      *
 928      * @param styleable
 929      * @param states
 930      * @param originatingStyleable
 931      * @return
 932      */
 933     private CalculatedValue lookup(final Styleable styleable,
 934                                    final CssMetaData cssMetaData,
 935                                    final StyleMap styleMap,
 936                                    final Set<PseudoClass> states,
 937                                    final Styleable originatingStyleable,
 938                                    final CalculatedValue cachedFont) {
 939 
 940         if (cssMetaData.getConverter() == FontConverter.getInstance()) {
 941             return lookupFont(styleable, cssMetaData.getProperty(), styleMap, cachedFont);
 942         }
 943 
 944         final String property = cssMetaData.getProperty();
 945 
 946         // Get the CascadingStyle which may apply to this particular property
 947         CascadingStyle style = getStyle(styleable, property, styleMap, states);
 948 
 949         // If no style was found and there are no sub styleables, then there
 950         // are no matching styles for this property. We will then either SKIP
 951         // or we will INHERIT. We will inspect the default value for the styleable,
 952         // and if it is INHERIT then we will inherit otherwise we just skip it.
 953         final List<CssMetaData<? extends Styleable, ?>> subProperties = cssMetaData.getSubProperties();
 954         final int numSubProperties = (subProperties != null) ? subProperties.size() : 0;
 955         if (style == null) {
 956 
 957             if (numSubProperties == 0) {
 958 
 959                 return handleNoStyleFound(styleable, cssMetaData,
 960                         styleMap, states, originatingStyleable, cachedFont);
 961 
 962             } else {
 963 
 964                 // If style is null then it means we didn't successfully find the
 965                 // property we were looking for. However, there might be sub styleables,
 966                 // in which case we should perform a lookup for them. For example,
 967                 // there might not be a style for "font", but there might be one
 968                 // for "font-size" or "font-weight". So if the style is null, then
 969                 // we need to check with the sub-styleables.
 970 
 971                 // Build up a list of all SubProperties which have a constituent part.
 972                 // I default the array to be the size of the number of total
 973                 // sub styleables to avoid having the array grow.
 974                 Map<CssMetaData,Object> subs = null;
 975                 StyleOrigin origin = null;
 976 
 977                 boolean isRelative = false;
 978 
 979                 for (int i=0; i<numSubProperties; i++) {
 980                     CssMetaData subkey = subProperties.get(i);
 981                     CalculatedValue constituent =
 982                         lookup(styleable, subkey, styleMap, states,
 983                                 originatingStyleable, cachedFont);
 984                     if (constituent != SKIP) {
 985                         if (subs == null) {
 986                             subs = new HashMap<>();
 987                         }
 988                         subs.put(subkey, constituent.getValue());
 989 
 990                         // origin of this style is the most specific
 991                         if ((origin != null && constituent.getOrigin() != null)
 992                                 ? origin.compareTo(constituent.getOrigin()) < 0
 993                                 : constituent.getOrigin() != null) {
 994                             origin = constituent.getOrigin();
 995                         }
 996 
 997                         // if the constiuent uses relative sizes, then
 998                         // isRelative is true;
 999                         isRelative = isRelative || constituent.isRelative();
1000 
1001                     }
1002                 }
1003 
1004                 // If there are no subkeys which apply...
1005                 if (subs == null || subs.isEmpty()) {
1006                     return handleNoStyleFound(styleable, cssMetaData,
1007                             styleMap, states, originatingStyleable, cachedFont);
1008                 }
1009 
1010                 try {
1011                     final StyleConverter keyType = cssMetaData.getConverter();
1012                     Object ret = keyType.convert(subs);
1013                     return new CalculatedValue(ret, origin, isRelative);
1014                 } catch (ClassCastException cce) {
1015                     final String msg = formatExceptionMessage(styleable, cssMetaData, null, cce);
1016                     List<CssParser.ParseError> errors = null;
1017                     if ((errors = StyleManager.getErrors()) != null) {
1018                         final CssParser.ParseError error = new CssParser.ParseError.PropertySetError(cssMetaData, styleable, msg);
1019                         errors.add(error);
1020                     }
1021                     if (LOGGER.isLoggable(Level.WARNING)) {
1022                         LOGGER.warning(msg);
1023                         LOGGER.fine("caught: ", cce);
1024                         LOGGER.fine("styleable = " + cssMetaData);
1025                         LOGGER.fine("node = " + styleable.toString());
1026                     }
1027                     return SKIP;
1028                 }
1029             }
1030 
1031         } else { // style != null
1032 
1033             // RT-10522:
1034             // If the user set the property and there is a style and
1035             // the style came from the user agent stylesheet, then
1036             // skip the value. A style from a user agent stylesheet should
1037             // not override the user set style.
1038             if (style.getOrigin() == StyleOrigin.USER_AGENT) {
1039 
1040                 StyleableProperty styleableProperty = cssMetaData.getStyleableProperty(originatingStyleable);
1041                 // if styleableProperty is null, then we're dealing with a sub-property.
1042                 if (styleableProperty != null && styleableProperty.getStyleOrigin() == StyleOrigin.USER) {
1043                     return SKIP;
1044                 }
1045             }
1046 
1047             // If there was a style found, then we want to check whether the
1048             // value was "inherit". If so, then we will simply inherit.
1049             final ParsedValue cssValue = style.getParsedValue();
1050             if (cssValue != null && "inherit".equals(cssValue.getValue())) {
1051                 style = getInheritedStyle(styleable, property);
1052                 if (style == null) return SKIP;
1053             }
1054         }
1055 
1056 //        System.out.println("lookup " + property +
1057 //                ", selector = \'" + style.selector.toString() + "\'" +
1058 //                ", node = " + node.toString());
1059 
1060         return calculateValue(style, styleable, cssMetaData, styleMap, states,
1061                 originatingStyleable, cachedFont);
1062     }
1063 
1064     /**
1065      * Called when there is no style found.
1066      */
1067     private CalculatedValue handleNoStyleFound(final Styleable styleable,
1068                                                final CssMetaData cssMetaData,
1069                                                final StyleMap styleMap, Set<PseudoClass> pseudoClassStates, Styleable originatingStyleable,
1070                                                final CalculatedValue cachedFont) {
1071 
1072         if (cssMetaData.isInherits()) {
1073 
1074 
1075             StyleableProperty styleableProperty = cssMetaData.getStyleableProperty(styleable);
1076             StyleOrigin origin = styleableProperty != null ? styleableProperty.getStyleOrigin() : null;
1077 
1078             // RT-16308: if there is no matching style and the user set
1079             // the property, do not look for inherited styles.
1080             if (origin == StyleOrigin.USER) {
1081 
1082                     return SKIP;
1083 
1084             }
1085 
1086             CascadingStyle style = getInheritedStyle(styleable, cssMetaData.getProperty());
1087             if (style == null) return SKIP;
1088 
1089             CalculatedValue cv =
1090                     calculateValue(style, styleable, cssMetaData,
1091                             styleMap, pseudoClassStates, originatingStyleable,
1092                                    cachedFont);
1093 
1094             return cv;
1095 
1096         } else {
1097 
1098             // Not inherited. There is no style
1099             return SKIP;
1100 
1101         }
1102     }
1103     /**
1104      * Called when we must getInheritedStyle a value from a parent node in the scenegraph.
1105      */
1106     private CascadingStyle getInheritedStyle(
1107             final Styleable styleable,
1108             final String property) {
1109 
1110         Styleable parent = ((Node)styleable).styleHelper.firstStyleableAncestor;
1111         CssStyleHelper parentStyleHelper = getStyleHelper((Node) parent);
1112 
1113         if (parent != null && parentStyleHelper != null) {
1114 
1115             StyleMap parentStyleMap = parentStyleHelper.getStyleMap(parent);
1116             Set<PseudoClass> transitionStates = ((Node)parent).pseudoClassStates;
1117             CascadingStyle cascadingStyle = parentStyleHelper.getStyle(parent, property, parentStyleMap, transitionStates);
1118 
1119             if (cascadingStyle != null) {
1120 
1121                 final ParsedValue cssValue = cascadingStyle.getParsedValue();
1122 
1123                 if ("inherit".equals(cssValue.getValue())) {
1124                     return getInheritedStyle(parent, property);
1125                 }
1126                 return cascadingStyle;
1127             }
1128         }
1129 
1130         return null;
1131     }
1132 
1133 
1134     // helps with self-documenting the code
1135     private static final Set<PseudoClass> NULL_PSEUDO_CLASS_STATE = null;
1136 
1137     /**
1138      * Find the property among the styles that pertain to the Node
1139      */
1140     private CascadingStyle resolveRef(final Styleable styleable, final String property, final StyleMap styleMap, final Set<PseudoClass> states) {
1141 
1142         final CascadingStyle style = getStyle(styleable, property, styleMap, states);
1143         if (style != null) {
1144             return style;
1145         } else {
1146             // if style is null, it may be because there isn't a style for this
1147             // node in this state, or we may need to look up the parent chain
1148             if (states != null && states.size() > 0) {
1149                 // if states > 0, then we need to check this node again,
1150                 // but without any states.
1151                 return resolveRef(styleable,property, styleMap, NULL_PSEUDO_CLASS_STATE);
1152             } else {
1153                 // TODO: This block was copied from inherit. Both should use same code somehow.
1154 
1155                 Styleable styleableParent = ((Node)styleable).styleHelper.firstStyleableAncestor;
1156                 CssStyleHelper parentStyleHelper = getStyleHelper((Node) styleableParent);
1157 
1158                 if (styleableParent == null || parentStyleHelper == null) {
1159                     return null;
1160                 }
1161 
1162                 StyleMap parentStyleMap = parentStyleHelper.getStyleMap(styleableParent);
1163                 Set<PseudoClass> styleableParentPseudoClassStates =
1164                     styleableParent instanceof Node
1165                         ? ((Node)styleableParent).pseudoClassStates
1166                         : styleable.getPseudoClassStates();
1167 
1168                 return parentStyleHelper.resolveRef(styleableParent, property,
1169                         parentStyleMap, styleableParentPseudoClassStates);
1170             }
1171         }
1172     }
1173 
1174     // to resolve a lookup, we just need to find the parsed value.
1175     private ParsedValue resolveLookups(
1176             final Styleable styleable,
1177             final ParsedValue parsedValue,
1178             final StyleMap styleMap, Set<PseudoClass> states,
1179             final ObjectProperty<StyleOrigin> whence,
1180             Set<ParsedValue> resolves) {
1181 
1182         //
1183         // either the value itself is a lookup, or the value contain a lookup
1184         //
1185         if (parsedValue.isLookup()) {
1186 
1187             // The value we're looking for should be a Paint, one of the
1188             // containers for linear, radial or ladder, or a derived color.
1189             final Object val = parsedValue.getValue();
1190             if (val instanceof String) {
1191 
1192                 final String sval = ((String) val).toLowerCase(Locale.ROOT);
1193 
1194                 CascadingStyle resolved =
1195                     resolveRef(styleable, sval, styleMap, states);
1196 
1197                 if (resolved != null) {
1198 
1199                     if (resolves.contains(resolved.getParsedValue())) {
1200 
1201                         if (LOGGER.isLoggable(Level.WARNING)) {
1202                             LOGGER.warning("Loop detected in " + resolved.getRule().toString() + " while resolving '" + sval + "'");
1203                         }
1204                         throw new IllegalArgumentException("Loop detected in " + resolved.getRule().toString() + " while resolving '" + sval + "'");
1205 
1206                     } else {
1207                         resolves.add(parsedValue);
1208                     }
1209 
1210                     // The origin of this parsed value is the greatest of
1211                     // any of the resolved reference. If a resolved reference
1212                     // comes from an inline style, for example, then the value
1213                     // calculated from the resolved lookup should have inline
1214                     // as its origin. Otherwise, an inline style could be
1215                     // stored in shared cache.
1216                     final StyleOrigin wOrigin = whence.get();
1217                     final StyleOrigin rOrigin = resolved.getOrigin();
1218                     if (rOrigin != null && (wOrigin == null ||  wOrigin.compareTo(rOrigin) < 0)) {
1219                         whence.set(rOrigin);
1220                     }
1221 
1222                     // the resolved value may itself need to be resolved.
1223                     // For example, if the value "color" resolves to "base",
1224                     // then "base" will need to be resolved as well.
1225                     ParsedValue pv = resolveLookups(styleable, resolved.getParsedValue(), styleMap, states, whence, resolves);
1226 
1227                     if (resolves != null) {
1228                         resolves.remove(parsedValue);
1229                     }
1230 
1231                     return pv;
1232 
1233                 }
1234             }
1235         }
1236 
1237         // If the value doesn't contain any values that need lookup, then bail
1238         if (!parsedValue.isContainsLookups()) {
1239             return parsedValue;
1240         }
1241 
1242         final Object val = parsedValue.getValue();
1243 
1244         if (val instanceof ParsedValue[][]) {
1245 
1246             // If ParsedValue is a layered sequence of values, resolve the lookups for each.
1247             final ParsedValue[][] layers = (ParsedValue[][])val;
1248             ParsedValue[][] resolved = new ParsedValue[layers.length][0];
1249             for (int l=0; l<layers.length; l++) {
1250                 resolved[l] = new ParsedValue[layers[l].length];
1251                 for (int ll=0; ll<layers[l].length; ll++) {
1252                     if (layers[l][ll] == null) continue;
1253                     resolved[l][ll] =
1254                         resolveLookups(styleable, layers[l][ll], styleMap, states, whence, resolves);
1255                 }
1256             }
1257 
1258             resolves.clear();
1259 
1260             return new ParsedValueImpl(resolved, parsedValue.getConverter(), false);
1261 
1262         } else if (val instanceof ParsedValueImpl[]) {
1263 
1264             // If ParsedValue is a sequence of values, resolve the lookups for each.
1265             final ParsedValue[] layer = (ParsedValue[])val;
1266             ParsedValue[] resolved = new ParsedValue[layer.length];
1267             for (int l=0; l<layer.length; l++) {
1268                 if (layer[l] == null) continue;
1269                 resolved[l] =
1270                     resolveLookups(styleable, layer[l], styleMap, states, whence, resolves);
1271             }
1272 
1273             resolves.clear();
1274 
1275             return new ParsedValueImpl(resolved, parsedValue.getConverter(), false);
1276 
1277         }
1278 
1279         return parsedValue;
1280 
1281     }
1282 
1283     private String getUnresolvedLookup(final ParsedValue resolved) {
1284 
1285         Object value = resolved.getValue();
1286 
1287         if (resolved.isLookup() && value instanceof String) {
1288             return (String)value;
1289         }
1290 
1291         if (value instanceof ParsedValue[][]) {
1292             final ParsedValue[][] layers = (ParsedValue[][])value;
1293             for (int l=0; l<layers.length; l++) {
1294                 for (int ll=0; ll<layers[l].length; ll++) {
1295                     if (layers[l][ll] == null) continue;
1296                     String unresolvedLookup = getUnresolvedLookup(layers[l][ll]);
1297                     if (unresolvedLookup != null) return unresolvedLookup;
1298                 }
1299             }
1300 
1301         } else if (value instanceof ParsedValue[]) {
1302         // If ParsedValue is a sequence of values, resolve the lookups for each.
1303             final ParsedValue[] layer = (ParsedValue[])value;
1304             for (int l=0; l<layer.length; l++) {
1305                 if (layer[l] == null) continue;
1306                 String unresolvedLookup = getUnresolvedLookup(layer[l]);
1307                 if (unresolvedLookup != null) return unresolvedLookup;
1308             }
1309         }
1310 
1311         return null;
1312     }
1313 
1314     private String formatUnresolvedLookupMessage(Styleable styleable, CssMetaData cssMetaData, Style style, ParsedValue resolved, ClassCastException cce) {
1315 
1316         // Find value that could not be looked up. If the resolved value does not contain lookups, then the
1317         // ClassCastException is not because of trying to convert a String (which is the missing lookup)
1318         // to some value, but is because the convert method got some wrong value - like a paint when it should be a color.
1319         // See RT-33319 for an example of this.
1320         String missingLookup = resolved != null && resolved.isContainsLookups() ? getUnresolvedLookup(resolved) : null;
1321 
1322         StringBuilder sbuf = new StringBuilder();
1323         if (missingLookup != null) {
1324             sbuf.append("Could not resolve '")
1325                     .append(missingLookup)
1326                     .append("'")
1327                     .append(" while resolving lookups for '")
1328                     .append(cssMetaData.getProperty())
1329                     .append("'");
1330         } else {
1331             sbuf.append("Caught '")
1332                     .append(cce)
1333                     .append("'")
1334                     .append(" while converting value for '")
1335                     .append(cssMetaData.getProperty())
1336                     .append("'");
1337         }
1338 
1339         final Rule rule = style != null ? style.getDeclaration().getRule(): null;
1340         final Stylesheet stylesheet = rule != null ? rule.getStylesheet() : null;
1341         final String url = stylesheet != null ? stylesheet.getUrl() : null;
1342         if (url != null) {
1343             sbuf.append(" from rule '")
1344                 .append(style.getSelector())
1345                 .append("' in stylesheet ").append(url);
1346         } else if (stylesheet != null && StyleOrigin.INLINE == stylesheet.getOrigin()) {
1347             sbuf.append(" from inline style on " )
1348                 .append(styleable.toString());
1349         }
1350 
1351         return sbuf.toString();
1352     }
1353 
1354     private String formatExceptionMessage(Styleable styleable, CssMetaData cssMetaData, Style style, Exception e) {
1355 
1356         StringBuilder sbuf = new StringBuilder();
1357         sbuf.append("Caught ")
1358             .append(String.valueOf(e));
1359 
1360         if (cssMetaData != null) {
1361             sbuf.append("'")
1362                 .append(" while calculating value for '")
1363                 .append(cssMetaData.getProperty())
1364                 .append("'");
1365         }
1366 
1367         if (style != null) {
1368 
1369             final Rule rule = style.getDeclaration().getRule();
1370             final Stylesheet stylesheet = rule != null ? rule.getStylesheet() : null;
1371             final String url = stylesheet != null ? stylesheet.getUrl() : null;
1372 
1373             if (url != null) {
1374                 sbuf.append(" from rule '")
1375                         .append(style.getSelector())
1376                         .append("' in stylesheet ").append(url);
1377             } else if (styleable != null && stylesheet != null && StyleOrigin.INLINE == stylesheet.getOrigin()) {
1378                 sbuf.append(" from inline style on " )
1379                         .append(styleable.toString());
1380             } else {
1381                 sbuf.append(" from style '")
1382                     .append(String.valueOf(style))
1383                     .append("'");
1384             }
1385         }
1386 
1387         return sbuf.toString();
1388     }
1389 
1390 
1391     private CalculatedValue calculateValue(
1392             final CascadingStyle style,
1393             final Styleable styleable,
1394             final CssMetaData cssMetaData,
1395             final StyleMap styleMap, final Set<PseudoClass> states,
1396             final Styleable originatingStyleable,
1397             final CalculatedValue fontFromCacheEntry) {
1398 
1399         final ParsedValue cssValue = style.getParsedValue();
1400         if (cssValue != null && !("null".equals(cssValue.getValue()) || "none".equals(cssValue.getValue()))) {
1401 
1402             ParsedValue resolved = null;
1403             try {
1404 
1405                 ObjectProperty<StyleOrigin> whence = new SimpleObjectProperty<>(style.getOrigin());
1406                 resolved = resolveLookups(styleable, cssValue, styleMap, states, whence, new HashSet<>());
1407 
1408                 final String property = cssMetaData.getProperty();
1409 
1410                 // The computed value
1411                 Object val = null;
1412                 boolean isFontProperty =
1413                         "-fx-font".equals(property) ||
1414                         "-fx-font-size".equals(property);
1415 
1416                 boolean isRelative = ParsedValueImpl.containsFontRelativeSize(resolved, isFontProperty);
1417 
1418                 //
1419                 // Avoid using a font calculated from a relative size
1420                 // to calculate a font with a relative size.
1421                 // For example:
1422                 // Assume the default font size is 13 and we have a style with
1423                 // -fx-font-size: 1.5em, then the cacheEntry font value will
1424                 // have a size of 13*1.5=19.5.
1425                 // Now, when converting that same font size again in response
1426                 // to looking up a value for -fx-font, we do not want to use
1427                 // 19.5 as the font for relative size conversion since this will
1428                 // yield a font 19.5*1.5=29.25 when really what we want is
1429                 // a font size of 19.5.
1430                 // In this situation, then, we use the font from the parent's
1431                 // cache entry.
1432                 Font fontForFontRelativeSizes = null;
1433 
1434                 if (isRelative && isFontProperty &&
1435                     (fontFromCacheEntry == null || fontFromCacheEntry.isRelative())) {
1436 
1437                     Styleable parent = styleable;
1438                     CalculatedValue childsCachedFont = fontFromCacheEntry;
1439                     do {
1440 
1441                         CalculatedValue parentsCachedFont = getCachedFont(parent.getStyleableParent());
1442 
1443                         if (parentsCachedFont != null)  {
1444 
1445                             if (parentsCachedFont.isRelative()) {
1446 
1447                                 //
1448                                 // If the cached fonts are the same, then the cached font came from the same
1449                                 // style and we need to keep looking. Otherwise, use the font we found.
1450                                 //
1451                                 if (childsCachedFont == null || parentsCachedFont.equals(childsCachedFont)) {
1452                                     childsCachedFont = parentsCachedFont;
1453                                 } else {
1454                                     fontForFontRelativeSizes = (Font)parentsCachedFont.getValue();
1455                                 }
1456 
1457                             } else  {
1458                                 // fontValue.isRelative() == false!
1459                                 fontForFontRelativeSizes = (Font)parentsCachedFont.getValue();
1460                             }
1461 
1462                         }
1463 
1464                     } while(fontForFontRelativeSizes == null &&
1465                             (parent = parent.getStyleableParent()) != null);
1466                 }
1467 
1468                 // did we get a fontValue from the preceding block?
1469                 // if not, get it from our cacheEntry or choose the default
1470                 if (fontForFontRelativeSizes == null) {
1471                     if (fontFromCacheEntry != null && fontFromCacheEntry.isRelative() == false) {
1472                         fontForFontRelativeSizes = (Font)fontFromCacheEntry.getValue();
1473                     } else {
1474                         fontForFontRelativeSizes = Font.getDefault();
1475                     }
1476                 }
1477 
1478                 final StyleConverter cssMetaDataConverter = cssMetaData.getConverter();
1479                 // RT-37727 - handling of properties that are insets is wonky. If the property is -fx-inset, then
1480                 // there isn't an issue because the converter assigns the InsetsConverter to the ParsedValue.
1481                 // But -my-insets will parse as an array of numbers and the parser will assign the Size sequence
1482                 // converter to it. So, if the CssMetaData says it uses InsetsConverter, use the InsetsConverter
1483                 // and not the parser assigned converter.
1484                 if (cssMetaDataConverter == StyleConverter.getInsetsConverter()) {
1485                     if (resolved.getValue() instanceof ParsedValue) {
1486                         // If you give the parser "-my-insets: 5;" you end up with a ParsedValue<ParsedValue<?,Size>, Number>
1487                         // and not a ParsedValue<ParsedValue[], Number[]> so here we wrap the value into an array
1488                         // to make the InsetsConverter happy.
1489                         resolved = new ParsedValueImpl(new ParsedValue[] {(ParsedValue)resolved.getValue()}, null, false);
1490                     }
1491                     val = cssMetaDataConverter.convert(resolved, fontForFontRelativeSizes);
1492                 }
1493                 else if (resolved.getConverter() != null)
1494                     val = resolved.convert(fontForFontRelativeSizes);
1495                 else
1496                     val = cssMetaData.getConverter().convert(resolved, fontForFontRelativeSizes);
1497 
1498                 final StyleOrigin origin = whence.get();
1499                 return new CalculatedValue(val, origin, isRelative);
1500 
1501             } catch (ClassCastException cce) {
1502                 final String msg = formatUnresolvedLookupMessage(styleable, cssMetaData, style.getStyle(),resolved, cce);
1503                 List<CssParser.ParseError> errors = null;
1504                 if ((errors = StyleManager.getErrors()) != null) {
1505                     final CssParser.ParseError error = new CssParser.ParseError.PropertySetError(cssMetaData, styleable, msg);
1506                     errors.add(error);
1507                 }
1508                 if (LOGGER.isLoggable(Level.WARNING)) {
1509                     LOGGER.warning(msg);
1510                     LOGGER.fine("node = " + styleable.toString());
1511                     LOGGER.fine("cssMetaData = " + cssMetaData);
1512                     LOGGER.fine("styles = " + getMatchingStyles(styleable, cssMetaData));
1513                 }
1514                 return SKIP;
1515             } catch (IllegalArgumentException iae) {
1516                 final String msg = formatExceptionMessage(styleable, cssMetaData, style.getStyle(), iae);
1517                 List<CssParser.ParseError> errors = null;
1518                 if ((errors = StyleManager.getErrors()) != null) {
1519                     final CssParser.ParseError error = new CssParser.ParseError.PropertySetError(cssMetaData, styleable, msg);
1520                     errors.add(error);
1521                 }
1522                 if (LOGGER.isLoggable(Level.WARNING)) {
1523                     LOGGER.warning(msg);
1524                     LOGGER.fine("caught: ", iae);
1525                     LOGGER.fine("styleable = " + cssMetaData);
1526                     LOGGER.fine("node = " + styleable.toString());
1527                 }
1528                 return SKIP;
1529             } catch (NullPointerException npe) {
1530                 final String msg = formatExceptionMessage(styleable, cssMetaData, style.getStyle(), npe);
1531                 List<CssParser.ParseError> errors = null;
1532                 if ((errors = StyleManager.getErrors()) != null) {
1533                     final CssParser.ParseError error = new CssParser.ParseError.PropertySetError(cssMetaData, styleable, msg);
1534                     errors.add(error);
1535                 }
1536                 if (LOGGER.isLoggable(Level.WARNING)) {
1537                     LOGGER.warning(msg);
1538                     LOGGER.fine("caught: ", npe);
1539                     LOGGER.fine("styleable = " + cssMetaData);
1540                     LOGGER.fine("node = " + styleable.toString());
1541                 }
1542                 return SKIP;
1543             }
1544 
1545         }
1546         // either cssValue was null or cssValue's value was "null" or "none"
1547         return new CalculatedValue(null, style.getOrigin(), false);
1548 
1549     }
1550 
1551     private static final CssMetaData dummyFontProperty =
1552             new FontCssMetaData<Node>("-fx-font", Font.getDefault()) {
1553 
1554         @Override
1555         public boolean isSettable(Node node) {
1556             return true;
1557         }
1558 
1559         @Override
1560         public StyleableProperty<Font> getStyleableProperty(Node node) {
1561             return null;
1562         }
1563     };
1564 
1565     private CalculatedValue getCachedFont(final Styleable styleable) {
1566 
1567         if (styleable instanceof Node == false) return null;
1568 
1569         CalculatedValue cachedFont = null;
1570 
1571         Node parent = (Node)styleable;
1572 
1573         final CssStyleHelper parentHelper = parent.styleHelper;
1574 
1575         // if there is no parentHelper,
1576         // or there is a parentHelper but no cacheContainer,
1577         // then look to the next parent
1578         if (parentHelper == null || parentHelper.cacheContainer == null) {
1579 
1580             cachedFont = getCachedFont(parent.getStyleableParent());
1581 
1582         // there is a parent helper and a cacheContainer,
1583         } else  {
1584 
1585             CacheContainer parentCacheContainer = parentHelper.cacheContainer;
1586             if ( parentCacheContainer != null
1587                     && parentCacheContainer.fontSizeCache != null
1588                     && parentCacheContainer.fontSizeCache.isEmpty() == false) {
1589 
1590                 Set<PseudoClass>[] transitionStates = parentHelper.getTransitionStates(parent);
1591                 StyleCacheEntry.Key parentCacheEntryKey = new StyleCacheEntry.Key(transitionStates, Font.getDefault());
1592                 cachedFont = parentCacheContainer.fontSizeCache.get(parentCacheEntryKey);
1593             }
1594 
1595             if (cachedFont == null)  {
1596                 StyleMap smap = parentHelper.getStyleMap(parent);
1597                 cachedFont = parentHelper.lookupFont(parent, "-fx-font", smap, null);
1598             }
1599         }
1600 
1601         return cachedFont != SKIP ? cachedFont : null;
1602     }
1603 
1604     /*package access for testing*/ FontPosture getFontPosture(Font font) {
1605         if (font == null) return FontPosture.REGULAR;
1606 
1607         String fontName = font.getName().toLowerCase(Locale.ROOT);
1608 
1609         if (fontName.contains("italic")) {
1610             return FontPosture.ITALIC;
1611         }
1612 
1613         return FontPosture.REGULAR;
1614     }
1615 
1616     /*package access for testing*/ FontWeight getFontWeight(Font font) {
1617         if (font == null) return FontWeight.NORMAL;
1618 
1619         String fontName = font.getName().toLowerCase(Locale.ROOT);
1620 
1621         if (fontName.contains("bold")) {
1622             if (fontName.contains("extra")) return FontWeight.EXTRA_BOLD;
1623             if (fontName.contains("ultra")) return FontWeight.EXTRA_BOLD;
1624             else if (fontName.contains("semi")) return FontWeight.SEMI_BOLD;
1625             else if (fontName.contains("demi")) return FontWeight.SEMI_BOLD;
1626             else return FontWeight.BOLD;
1627 
1628         } else if (fontName.contains("light")) {
1629             if (fontName.contains("extra")) return FontWeight.EXTRA_LIGHT;
1630             if (fontName.contains("ultra")) return FontWeight.EXTRA_LIGHT;
1631             else return FontWeight.LIGHT;
1632 
1633         } else if (fontName.contains("black")) {
1634             return FontWeight.BLACK;
1635 
1636         } else if (fontName.contains("heavy")) {
1637             return FontWeight.BLACK;
1638 
1639         } else if (fontName.contains("medium")) {
1640             return FontWeight.MEDIUM;
1641         }
1642 
1643         return FontWeight.NORMAL;
1644 
1645     }
1646 
1647     /*package access for testing*/ String getFontFamily(Font font) {
1648         if (font == null) return Font.getDefault().getFamily();
1649         return font.getFamily();
1650     }
1651 
1652 
1653     /*package access for testing*/ Font deriveFont(
1654             Font font,
1655             String fontFamily,
1656             FontWeight fontWeight,
1657             FontPosture fontPosture,
1658             double fontSize) {
1659 
1660         if (font != null && fontFamily == null) fontFamily = getFontFamily(font);
1661         else if (fontFamily != null) fontFamily = Utils.stripQuotes(fontFamily);
1662 
1663         if (font != null && fontWeight == null) fontWeight = getFontWeight(font);
1664         if (font != null && fontPosture == null) fontPosture = getFontPosture(font);
1665         if (font != null && fontSize <= 0) fontSize = font.getSize();
1666 
1667         return  Font.font(
1668                 fontFamily,
1669                 fontWeight,
1670                 fontPosture,
1671                 fontSize);
1672     }
1673 
1674     /**
1675      * Look up a font property. This is handled separately from lookup since
1676      * font is inherited and has sub-properties. One should expect that the
1677      * text font for the following would be 16px Arial. The lookup method would
1678      * give 16px system since it would look <em>only</em> for font-size,
1679      * font-family, etc <em>only</em> if the lookup on font failed.
1680      * <pre>
1681      * Text text = new Text("Hello World");
1682      * text.setStyle("-fx-font-size: 16px;");
1683      * Group group = new Group();
1684      * group.setStyle("-fx-font: 12px Arial;");
1685      * group.getChildren().add(text);
1686      * </pre>
1687      */
1688      /*package access for testing*/ CalculatedValue lookupFont(
1689             final Styleable styleable,
1690             final String property,
1691             final StyleMap styleMap,
1692             final CalculatedValue cachedFont)
1693     {
1694 
1695         StyleOrigin origin = null;
1696 
1697         // How far from this node did we travel to find a font shorthand?
1698         // Don't look past this distance for other font properties.
1699         int distance = 0;
1700 
1701         // Did we find a style?
1702         boolean foundStyle = false;
1703 
1704         String family = null;
1705         double size = -1;
1706         FontWeight weight = null;
1707         FontPosture posture = null;
1708 
1709         CalculatedValue cvFont = cachedFont;
1710 
1711 
1712         Set<PseudoClass> states = styleable instanceof Node ? ((Node)styleable).pseudoClassStates : styleable.getPseudoClassStates();
1713 
1714         // RT-20145 - if looking for font size and the node has a font,
1715         // use the font property's value if it was set by the user and
1716         // there is not an inline or author style.
1717 
1718         if (cacheContainer.fontProp != null) {
1719             StyleableProperty<Font> styleableProp = cacheContainer.fontProp.getStyleableProperty(styleable);
1720             StyleOrigin fpOrigin = styleableProp.getStyleOrigin();
1721             Font font = styleableProp.getValue();
1722             if (font == null) font = Font.getDefault();
1723             if (fpOrigin == StyleOrigin.USER) {
1724                 origin = fpOrigin;
1725                 family = getFontFamily(font);
1726                 size = font.getSize();
1727                 weight = getFontWeight(font);
1728                 posture = getFontPosture(font);
1729                 cvFont = new CalculatedValue(font, fpOrigin, false);
1730             }
1731         }
1732 
1733         CalculatedValue parentCachedFont = getCachedFont(styleable.getStyleableParent());
1734         if (parentCachedFont == null) parentCachedFont = new CalculatedValue(Font.getDefault(), null, false);
1735 
1736         //
1737         // Look up the font- properties
1738         //
1739         CascadingStyle fontShorthand = getStyle(styleable, property, styleMap, states);
1740 
1741         // don't look past current node for font shorthand if user set the font
1742         if (fontShorthand == null && origin != StyleOrigin.USER) {
1743 
1744             Styleable parent = styleable != null ? styleable.getStyleableParent() : null;
1745 
1746             while (parent != null) { // This loop traverses through all ancestors till root
1747 
1748                 CssStyleHelper parentStyleHelper = parent instanceof Node ? ((Node)parent).styleHelper : null;
1749                 if (parentStyleHelper != null) {
1750 
1751                     distance += 1;
1752 
1753                     StyleMap parentStyleMap = parentStyleHelper.getStyleMap(parent);
1754                     Set<PseudoClass> transitionStates = ((Node)parent).pseudoClassStates;
1755                     CascadingStyle cascadingStyle = parentStyleHelper.getStyle(parent, property, parentStyleMap, transitionStates);
1756 
1757                     if (cascadingStyle != null) {
1758 
1759                         final ParsedValue cssValue = cascadingStyle.getParsedValue();
1760 
1761                         if ("inherit".equals(cssValue.getValue()) == false) {
1762                             fontShorthand = cascadingStyle;
1763                             break;
1764                         }
1765                     }
1766 
1767                 }
1768 
1769                 parent = parent.getStyleableParent();
1770 
1771             }
1772 
1773         }
1774 
1775         if (fontShorthand != null) {
1776 
1777             //
1778             // If we don't have an existing font, or if the origin of the
1779             // existing font is less than that of the shorthand, then
1780             // take the shorthand. If the origins compare equals, then take
1781             // the shorthand since the fontProp value will not have been
1782             // updated yet.
1783             //
1784             if (origin == null || origin.compareTo(fontShorthand.getOrigin()) <= 0) {
1785 
1786                 final CalculatedValue cv =
1787                         calculateValue(fontShorthand, styleable, dummyFontProperty,
1788                                 styleMap, states, styleable, parentCachedFont);
1789 
1790                 // cv could be SKIP
1791                 if (cv.getValue() instanceof Font) {
1792                     origin = cv.getOrigin();
1793                     Font font = (Font)cv.getValue();
1794                     family = getFontFamily(font);
1795                     size = font.getSize();
1796                     weight = getFontWeight(font);
1797                     posture = getFontPosture(font);
1798                     cvFont = cv;
1799                     foundStyle = true;
1800                 }
1801 
1802             }
1803         }
1804 
1805         CascadingStyle fontSize = getStyle(styleable, property.concat("-size"), styleMap, states);
1806         if (fontSize != null) {
1807             // if we have a font shorthand and it is more specific than font-size, then don't use the font-size style
1808             if (fontShorthand != null && fontShorthand.compareTo(fontSize) < 0) {
1809                 fontSize = null;
1810             } else if (origin == StyleOrigin.USER) {
1811                 // If fontSize is an inline or author-stylesheet style, use it.
1812                 // Otherwise, fontSize is a user-agent stylesheet style and should not override the USER style.
1813                 if (StyleOrigin.USER.compareTo(fontSize.getOrigin()) > 0) {
1814                     fontSize = null;
1815                 }
1816             }
1817         } else if (origin != StyleOrigin.USER) {
1818             //
1819             // If we don't have a font-size, see if there is an inherited font-size.
1820             // If lookupInheritedFontProperty returns other than null, then we know that font-size is closer (more specific)
1821             // than the font shorthand
1822             //
1823             fontSize = lookupInheritedFontProperty(styleable, property.concat("-size"), styleMap, distance, fontShorthand);
1824         }
1825 
1826         if (fontSize != null) {
1827 
1828             // The logic above ensures that, if fontSize is not null, then it is either
1829             // 1) a style matching this node and is more specific than the font shorthand or
1830             // 2) an inherited style that is more specific than the font shorthand
1831             // and, therefore, we can use the fontSize style
1832 
1833             final CalculatedValue cv =
1834                     calculateValue(fontSize, styleable, dummyFontProperty,
1835                             styleMap, states, styleable, parentCachedFont);
1836 
1837             if (cv.getValue() instanceof Double) {
1838                 if (origin == null || origin.compareTo(fontSize.getOrigin()) <= 0) {
1839 
1840                     origin = cv.getOrigin();
1841                 }
1842                 size = (Double) cv.getValue();
1843 
1844                 if (cvFont != null) {
1845                     boolean isRelative = cvFont.isRelative() || cv.isRelative();
1846                     Font font = deriveFont((Font) cvFont.getValue(), family, weight, posture, size);
1847                     cvFont = new CalculatedValue(font, origin, isRelative);
1848                 } else {
1849                     boolean isRelative = cv.isRelative();
1850                     Font font = deriveFont(Font.getDefault(), family, weight, posture, size);
1851                     cvFont = new CalculatedValue(font, origin, isRelative);
1852                 }
1853                 foundStyle = true;
1854             }
1855 
1856         }
1857 
1858         // if cachedFont is null, then we're in this method to look up a font for the CacheContainer's fontSizeCache
1859         // and we only care about font-size or the size from font shorthand.
1860         if (cachedFont == null) {
1861             return (cvFont != null) ? cvFont : SKIP;
1862         }
1863 
1864         CascadingStyle fontWeight = getStyle(styleable, property.concat("-weight"), styleMap, states);
1865         if (fontWeight != null) {
1866             // if we have a font shorthand and it is more specific than font-weight, then don't use the font-weight style
1867             if (fontShorthand != null && fontShorthand.compareTo(fontWeight) < 0) {
1868                 fontWeight = null;
1869             }
1870 
1871         } else if (origin != StyleOrigin.USER) {
1872             //
1873             // If we don't have a font-weight, see if there is an inherited font-weight.
1874             // If lookupInheritedFontProperty returns other than null, then we know that font-weight is closer (more specific)
1875             // than the font shorthand
1876             //
1877             fontWeight = lookupInheritedFontProperty(styleable, property.concat("-weight"), styleMap, distance, fontShorthand);
1878         }
1879 
1880         if (fontWeight != null) {
1881 
1882             // The logic above ensures that, if fontWeight is not null, then it is either
1883             // 1) a style matching this node and is more specific than the font shorthand or
1884             // 2) an inherited style that is more specific than the font shorthand
1885             // and, therefore, we can use the fontWeight style
1886 
1887             final CalculatedValue cv =
1888                     calculateValue(fontWeight, styleable, dummyFontProperty,
1889                             styleMap, states, styleable, null);
1890 
1891             if (cv.getValue() instanceof FontWeight) {
1892                 if (origin == null || origin.compareTo(fontWeight.getOrigin()) <= 0) {
1893                     origin = cv.getOrigin();
1894                 }
1895                 weight = (FontWeight)cv.getValue();
1896                 foundStyle = true;
1897             }
1898         }
1899 
1900 
1901         CascadingStyle fontStyle = getStyle(styleable, property.concat("-style"), styleMap, states);
1902         if (fontStyle != null) {
1903             // if we have a font shorthand and it is more specific than font-style, then don't use the font-style style
1904             if (fontShorthand != null && fontShorthand.compareTo(fontStyle) < 0) {
1905                 fontStyle = null;
1906             }
1907 
1908         } else if (origin != StyleOrigin.USER) {
1909             //
1910             // If we don't have a font-style, see if there is an inherited font-style.
1911             // If lookupInheritedFontProperty returns other than null, then we know that font-style is closer (more specific)
1912             // than the font shorthand
1913             //
1914             fontStyle = lookupInheritedFontProperty(styleable, property.concat("-style"), styleMap, distance, fontShorthand);
1915         }
1916 
1917         if (fontStyle != null) {
1918 
1919             // The logic above ensures that, if fontStyle is not null, then it is either
1920             // 1) a style matching this node and is more specific than the font shorthand or
1921             // 2) an inherited style that is more specific than the font shorthand
1922             // and, therefore, we can use the fontStyle style
1923 
1924             final CalculatedValue cv =
1925                     calculateValue(fontStyle, styleable, dummyFontProperty,
1926                             styleMap, states, styleable, null);
1927 
1928             if (cv.getValue() instanceof FontPosture) {
1929                 if (origin == null || origin.compareTo(fontStyle.getOrigin()) <= 0) {
1930                     origin = cv.getOrigin();
1931                 }
1932                 posture = (FontPosture)cv.getValue();
1933                 foundStyle = true;
1934             }
1935 
1936         }
1937 
1938         CascadingStyle fontFamily = getStyle(styleable, property.concat("-family"), styleMap, states);
1939         if (fontFamily != null) {
1940             // if we have a font shorthand and it is more specific than font-family, then don't use the font-family style
1941             if (fontShorthand != null && fontShorthand.compareTo(fontFamily) < 0) {
1942                 fontFamily = null;
1943             }
1944 
1945         } else if (origin != StyleOrigin.USER) {
1946             //
1947             // If we don't have a font-family, see if there is an inherited font-family.
1948             // If lookupInheritedFontProperty returns other than null, then we know that font-family is closer (more specific)
1949             // than the font shorthand
1950             //
1951             fontFamily = lookupInheritedFontProperty(styleable, property.concat("-family"), styleMap, distance, fontShorthand);
1952         }
1953 
1954         if (fontFamily != null) {
1955 
1956             // The logic above ensures that, if fontFamily is not null, then it is either
1957             // 1) a style matching this node and is more specific than the font shorthand or
1958             // 2) an inherited style that is more specific than the font shorthand
1959             // and, therefore, we can use the fontFamily style
1960 
1961             final CalculatedValue cv =
1962                     calculateValue(fontFamily, styleable, dummyFontProperty,
1963                             styleMap, states, styleable, null);
1964 
1965             if (cv.getValue() instanceof String) {
1966                 if (origin == null || origin.compareTo(fontFamily.getOrigin()) <= 0) {
1967                     origin = cv.getOrigin();
1968                 }
1969                 family = (String)cv.getValue();
1970                 foundStyle = true;
1971             }
1972 
1973         }
1974 
1975         if (foundStyle) {
1976 
1977             Font font = cvFont != null ? (Font)cvFont.getValue() : Font.getDefault();
1978             Font derivedFont = deriveFont(font, family, weight, posture, size);
1979             return new CalculatedValue(derivedFont,origin,false);
1980 
1981         }
1982 
1983         return SKIP;
1984     }
1985 
1986     private CascadingStyle lookupInheritedFontProperty(
1987             final Styleable styleable,
1988             final String property,
1989             final StyleMap styleMap,
1990             final int distance,
1991             CascadingStyle fontShorthand) {
1992 
1993         Styleable parent = styleable != null ? styleable.getStyleableParent() : null;
1994 
1995         int nlooks = distance;
1996         while (parent != null && nlooks > 0) { // This loop traverses through all ancestors till root
1997 
1998             CssStyleHelper parentStyleHelper = parent instanceof Node ? ((Node)parent).styleHelper : null;
1999             if (parentStyleHelper != null) {
2000 
2001                 nlooks -= 1;
2002 
2003                 StyleMap parentStyleMap = parentStyleHelper.getStyleMap((parent));
2004                 Set<PseudoClass> transitionStates = ((Node)parent).pseudoClassStates;
2005                 CascadingStyle cascadingStyle = parentStyleHelper.getStyle(parent, property, parentStyleMap, transitionStates);
2006 
2007                 if (cascadingStyle != null) {
2008 
2009                     // If we are closer to the node than the font shorthand, then font shorthand doesn't matter.
2010                     // If the font shorthand and this style are the same distance, then we need to compare.
2011                     if (fontShorthand != null && nlooks == 0) {
2012                         if (fontShorthand.compareTo(cascadingStyle) < 0) {
2013                             return null;
2014                         }
2015                     }
2016 
2017                     final ParsedValue cssValue = cascadingStyle.getParsedValue();
2018 
2019                     if ("inherit".equals(cssValue.getValue()) == false) {
2020                         return cascadingStyle;
2021                     }
2022                 }
2023 
2024             }
2025 
2026             parent = parent.getStyleableParent();
2027 
2028         }
2029 
2030         return null;
2031     }
2032 
2033 
2034     /**
2035      * Called from Node NodeHelper.getMatchingStyles
2036      * @param styleable
2037      * @param styleableProperty
2038      * @return
2039      */
2040     static List<Style> getMatchingStyles(final Styleable styleable, final CssMetaData styleableProperty) {
2041 
2042         if (!(styleable instanceof Node)) return Collections.<Style>emptyList();
2043 
2044         Node node = (Node)styleable;
2045         final CssStyleHelper helper = (node.styleHelper != null) ? node.styleHelper : createStyleHelper(node);
2046 
2047         if (helper != null) {
2048             return helper.getMatchingStyles(node, styleableProperty, false);
2049         }
2050         else {
2051             return Collections.<Style>emptyList();
2052         }
2053     }
2054 
2055     static Map<StyleableProperty<?>, List<Style>> getMatchingStyles(Map<StyleableProperty<?>, List<Style>> map, final Node node) {
2056 
2057         final CssStyleHelper helper = (node.styleHelper != null) ? node.styleHelper : createStyleHelper(node);
2058         if (helper != null) {
2059             if (map == null) map = new HashMap<>();
2060             for (CssMetaData metaData : node.getCssMetaData()) {
2061                 List<Style> styleList = helper.getMatchingStyles(node, metaData, true);
2062                 if (styleList != null && !styleList.isEmpty()) {
2063                     StyleableProperty prop = metaData.getStyleableProperty(node);
2064                     map.put(prop, styleList);
2065                 }
2066             }
2067         }
2068 
2069         if (node instanceof Parent) {
2070             for (Node child : ((Parent)node).getChildren()) {
2071                 map = getMatchingStyles(map, child);
2072             }
2073         }
2074 
2075         return map;
2076     }
2077 
2078     private List<Style> getMatchingStyles(final Styleable node, final CssMetaData styleableProperty, boolean matchState) {
2079 
2080         final List<CascadingStyle> styleList = new ArrayList<>();
2081 
2082         getMatchingStyles(node, styleableProperty, styleList, matchState);
2083 
2084         List<CssMetaData<? extends Styleable, ?>> subProperties = styleableProperty.getSubProperties();
2085         if (subProperties != null) {
2086             for (int n=0,nMax=subProperties.size(); n<nMax; n++) {
2087                 final CssMetaData subProperty = subProperties.get(n);
2088                 getMatchingStyles(node, subProperty, styleList, matchState);
2089             }
2090         }
2091 
2092         Collections.sort(styleList);
2093 
2094         final List<Style> matchingStyles = new ArrayList<>(styleList.size());
2095         for (int n=0,nMax=styleList.size(); n<nMax; n++) {
2096             final Style style = styleList.get(n).getStyle();
2097             if (!matchingStyles.contains(style)) matchingStyles.add(style);
2098         }
2099 
2100         return matchingStyles;
2101     }
2102 
2103     private void getMatchingStyles(final Styleable node, final CssMetaData styleableProperty, final List<CascadingStyle> styleList, boolean matchState) {
2104 
2105         if (node != null) {
2106 
2107             String property = styleableProperty.getProperty();
2108             Node _node = node instanceof Node ? (Node)node : null;
2109             final StyleMap smap = getStyleMap(_node);
2110             if (smap == null) return;
2111 
2112             if (matchState) {
2113                 CascadingStyle cascadingStyle = getStyle(node, styleableProperty.getProperty(), smap, _node.pseudoClassStates);
2114                 if (cascadingStyle != null) {
2115                     styleList.add(cascadingStyle);
2116                     final ParsedValue parsedValue = cascadingStyle.getParsedValue();
2117                     getMatchingLookupStyles(node, parsedValue, styleList, matchState);
2118                 }
2119             }  else {
2120 
2121                 Map<String, List<CascadingStyle>> cascadingStyleMap = smap.getCascadingStyles();
2122                 // StyleMap.getCascadingStyles() does not return null
2123                 List<CascadingStyle> styles = cascadingStyleMap.get(property);
2124 
2125                 if (styles != null) {
2126                     styleList.addAll(styles);
2127                     for (int n=0, nMax=styles.size(); n<nMax; n++) {
2128                         final CascadingStyle style = styles.get(n);
2129                         final ParsedValue parsedValue = style.getParsedValue();
2130                         getMatchingLookupStyles(node, parsedValue, styleList, matchState);
2131                     }
2132                 }
2133             }
2134 
2135             if (styleableProperty.isInherits()) {
2136                 Styleable parent = node.getStyleableParent();
2137                 while (parent != null) { // This loop traverses through all ancestors till root
2138                     CssStyleHelper parentHelper = parent instanceof Node
2139                             ? ((Node)parent).styleHelper
2140                             : null;
2141                     if (parentHelper != null) {
2142                         parentHelper.getMatchingStyles(parent, styleableProperty, styleList, matchState);
2143                     }
2144                     parent = parent.getStyleableParent();
2145                 }
2146             }
2147 
2148         }
2149 
2150     }
2151 
2152     // Pretty much a duplicate of resolveLookups, but without the state
2153     private void getMatchingLookupStyles(final Styleable node, final ParsedValue parsedValue, final List<CascadingStyle> styleList, boolean matchState) {
2154 
2155         if (parsedValue.isLookup()) {
2156 
2157             Object value = parsedValue.getValue();
2158 
2159             if (value instanceof String) {
2160 
2161                 final String property = (String)value;
2162                 // gather up any and all styles that contain this value as a property
2163                 Styleable parent = node;
2164                 do {
2165 
2166                     final Node _parent = parent instanceof Node ? (Node)parent : null;
2167                     final CssStyleHelper helper = _parent != null
2168                             ? _parent.styleHelper
2169                             : null;
2170                     if (helper != null) {
2171 
2172                         StyleMap styleMap = helper.getStyleMap(parent);
2173                         if (styleMap == null || styleMap.isEmpty()) continue;
2174 
2175                         final int start = styleList.size();
2176 
2177                         if (matchState) {
2178                             CascadingStyle cascadingStyle = helper.resolveRef(_parent, property, styleMap, _parent.pseudoClassStates);
2179                             if (cascadingStyle != null) {
2180                                 styleList.add(cascadingStyle);
2181                             }
2182                         } else {
2183                             final Map<String, List<CascadingStyle>> smap = styleMap.getCascadingStyles();
2184                             // getCascadingStyles does not return null
2185                             List<CascadingStyle> styles = smap.get(property);
2186 
2187                             if (styles != null) {
2188                                 styleList.addAll(styles);
2189                             }
2190 
2191                         }
2192 
2193                         final int end = styleList.size();
2194 
2195                         for (int index=start; index<end; index++) {
2196                             final CascadingStyle style = styleList.get(index);
2197                             getMatchingLookupStyles(parent, style.getParsedValue(), styleList, matchState);
2198                         }
2199                     }
2200 
2201                 } while ((parent = parent.getStyleableParent()) != null); // This loop traverses through all ancestors till root
2202 
2203             }
2204         }
2205 
2206         // If the value doesn't contain any values that need lookup, then bail
2207         if (!parsedValue.isContainsLookups()) {
2208             return;
2209         }
2210 
2211         final Object val = parsedValue.getValue();
2212         if (val instanceof ParsedValue[][]) {
2213         // If ParsedValue is a layered sequence of values, resolve the lookups for each.
2214             final ParsedValue[][] layers = (ParsedValue[][])val;
2215             for (int l=0; l<layers.length; l++) {
2216                 for (int ll=0; ll<layers[l].length; ll++) {
2217                     if (layers[l][ll] == null) continue;
2218                         getMatchingLookupStyles(node, layers[l][ll], styleList, matchState);
2219                 }
2220             }
2221 
2222         } else if (val instanceof ParsedValue[]) {
2223         // If ParsedValue is a sequence of values, resolve the lookups for each.
2224             final ParsedValue[] layer = (ParsedValue[])val;
2225             for (int l=0; l<layer.length; l++) {
2226                 if (layer[l] == null) continue;
2227                     getMatchingLookupStyles(node, layer[l], styleList, matchState);
2228             }
2229         }
2230 
2231     }
2232 
2233 }