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