1 /*
   2  * Copyright (c) 2012, 2014, Oracle and/or its affiliates.
   3  * All rights reserved. Use is subject to license terms.
   4  *
   5  * This file is available and licensed under the following license:
   6  *
   7  * Redistribution and use in source and binary forms, with or without
   8  * modification, are permitted provided that the following conditions
   9  * are met:
  10  *
  11  *  - Redistributions of source code must retain the above copyright
  12  *    notice, this list of conditions and the following disclaimer.
  13  *  - Redistributions in binary form must reproduce the above copyright
  14  *    notice, this list of conditions and the following disclaimer in
  15  *    the documentation and/or other materials provided with the distribution.
  16  *  - Neither the name of Oracle Corporation nor the names of its
  17  *    contributors may be used to endorse or promote products derived
  18  *    from this software without specific prior written permission.
  19  *
  20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  24  * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  25  * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  26  * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  27  * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  28  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  29  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  30  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  31  */
  32 package com.oracle.javafx.scenebuilder.kit.util;
  33 
  34 import java.io.File;
  35 import java.io.IOException;
  36 import java.lang.reflect.InvocationTargetException;
  37 import java.net.MalformedURLException;
  38 import java.net.URL;
  39 import java.util.ArrayList;
  40 import java.util.HashMap;
  41 import java.util.HashSet;
  42 import java.util.List;
  43 import java.util.Map;
  44 import java.util.Set;
  45 import java.util.TreeMap;
  46 import java.util.TreeSet;
  47 
  48 import javafx.beans.property.ReadOnlyProperty;
  49 import javafx.collections.FXCollections;
  50 import javafx.css.CssMetaData;
  51 import javafx.css.StyleOrigin;
  52 import javafx.css.Styleable;
  53 import javafx.css.StyleableProperty;
  54 import javafx.scene.Node;
  55 import javafx.scene.Parent;
  56 
  57 import com.oracle.javafx.scenebuilder.kit.editor.EditorController;
  58 import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform;
  59 import com.oracle.javafx.scenebuilder.kit.editor.EditorPlatform.Theme;
  60 import com.oracle.javafx.scenebuilder.kit.fxom.FXOMInstance;
  61 import com.oracle.javafx.scenebuilder.kit.metadata.property.ValuePropertyMetadata;
  62 import com.sun.javafx.css.CompoundSelector;
  63 import com.sun.javafx.css.Rule;
  64 import com.sun.javafx.css.Selector;
  65 import com.sun.javafx.css.SimpleSelector;
  66 import com.sun.javafx.css.Style;
  67 import com.sun.javafx.css.Stylesheet;
  68 import com.sun.javafx.css.parser.CSSParser;
  69 
  70 /**
  71  *
  72  * Utility classes using css internal classes (from com.sun package). Note that
  73  * the CSS Analyzer is also using extensively com.sun classes.
  74  *
  75  */
  76 public class CssInternal {
  77 
  78     private final static String[] themeUrls = {
  79         Deprecation.CASPIAN_EMBEDDED_HIGHCONTRAST_STYLESHEET,
  80         Deprecation.CASPIAN_EMBEDDED_QVGA_HIGHCONTRAST_STYLESHEET,
  81         Deprecation.CASPIAN_EMBEDDED_QVGA_STYLESHEET,
  82         Deprecation.CASPIAN_EMBEDDED_STYLESHEET,
  83         Deprecation.CASPIAN_HIGHCONTRAST_STYLESHEET,
  84         Deprecation.CASPIAN_STYLESHEET,
  85         Deprecation.MODENA_HIGHCONTRAST_BLACKONWHITE_STYLESHEET,
  86         Deprecation.MODENA_HIGHCONTRAST_WHITEONBLACK_STYLESHEET,
  87         Deprecation.MODENA_HIGHCONTRAST_YELLOWONBLACK_STYLESHEET,
  88         Deprecation.MODENA_STYLESHEET,
  89         Deprecation.MODENA_TOUCH_HIGHCONTRAST_BLACKONWHITE_STYLESHEET,
  90         Deprecation.MODENA_TOUCH_HIGHCONTRAST_WHITEONBLACK_STYLESHEET,
  91         Deprecation.MODENA_TOUCH_HIGHCONTRAST_YELLOWONBLACK_STYLESHEET,
  92         Deprecation.MODENA_TOUCH_STYLESHEET
  93     };
  94 
  95     /**
  96      * Check if the input style is from a theme stylesheet (caspian or modena).
  97      *
  98      * @param style style to be checked
  99      * @return true if the style is from a theme css.
 100      */
 101     public static boolean isThemeStyle(Style style) {
 102         return isThemeRule(style.getDeclaration().getRule());
 103     }
 104 
 105     public static boolean isCaspianTheme(Style style) {
 106         return style.getDeclaration().getRule().getStylesheet().getUrl()
 107                 .endsWith(Deprecation.CASPIAN_STYLESHEET);
 108     }
 109 
 110     public static boolean isModenaTheme(Style style) {
 111         return style.getDeclaration().getRule().getStylesheet().getUrl()
 112                 .endsWith(Deprecation.MODENA_TOUCH_STYLESHEET);
 113     }
 114 
 115     public static String getThemeDisplayName(Style style) {
 116         String themeName = ""; //NOI18N
 117         String url = style.getDeclaration().getRule().getStylesheet().getUrl();
 118         if (url.contains("modena")) {//NOI18N
 119             themeName += "modena/"; //NOI18N
 120         } else if (url.contains("caspian")) {//NOI18N
 121             themeName += "caspian/"; //NOI18N
 122         }
 123         File file = new File(url);
 124         themeName += file.getName().replace(".bss", ".css");//NOI18N
 125         if (themeName.endsWith("modena.css")) {//NOI18N
 126             themeName = "modena.css";//NOI18N
 127         } else if (themeName.endsWith("caspian.css")) {//NOI18N
 128             themeName = "caspian.css";//NOI18N
 129         }
 130         return themeName;
 131     }
 132 
 133     public static boolean isThemeRule(Rule rule) {
 134         String stylePath = rule.getStylesheet().getUrl();
 135         assert stylePath != null;
 136         for (String themeUrl : themeUrls) {
 137             if (stylePath.endsWith(themeUrl)) {
 138                 return true;
 139             }
 140         }
 141         return false;
 142     }
 143 
 144     public static boolean isThemeClass(Theme theme, String styleClass) {
 145         return getThemeStyleClasses(theme).contains(styleClass);
 146     }
 147 
 148     public static List<String> getThemeStyleClasses(Theme theme) {
 149         String themeStyleSheet = EditorPlatform.getThemeStylesheetURL(theme);
 150         Set<String> themeClasses = new HashSet<>();
 151         // For Theme css, we need to get the text css (.css) to be able to parse it.
 152         // (instead of the default binary format .bss)
 153         themeClasses.addAll(getStyleClasses(Deprecation.getThemeTextStylesheet(themeStyleSheet)));
 154         return new ArrayList<>(themeClasses);
 155     }
 156 
 157     // Return the stylesheet corresponding to a style class.
 158     // (input parameter: a map returned by getStyleClassesMap(), styleClass)
 159     public static String getStyleSheet(Map<String, String> styleClassMap, String styleClass) {
 160         return styleClassMap.get(styleClass);
 161     }
 162 
 163     public static List<String> getStyleClasses(EditorController editorController, Set<FXOMInstance> instances) {
 164         return new ArrayList<>(getStyleClassesMap(editorController, instances).keySet());
 165     }
 166 
 167     public static Map<String, String> getStyleClassesMap(EditorController editorController, Set<FXOMInstance> instances) {
 168         Map<String, String> classesMap = new TreeMap<>();
 169         Object fxRoot = null;
 170         for (FXOMInstance instance : instances) {
 171             if (fxRoot == null) {
 172                 fxRoot = instance.getFxomDocument().getSceneGraphRoot();
 173             }
 174             Object fxObject = instance.getSceneGraphObject();
 175             classesMap.putAll(getFxObjectClassesMap(fxObject, fxRoot));
 176         }
 177 
 178         // Handle the Scene stylesheets (if any)
 179         List<File> sceneStyleSheets = editorController.getSceneStyleSheets();
 180         if (sceneStyleSheets != null) {
 181             for (File stylesheet : sceneStyleSheets) {
 182                 try {
 183                     URL stylesheetUrl = stylesheet.toURI().toURL();
 184                     for (String styleClass : getStyleClasses(stylesheetUrl)) {
 185                         classesMap.put(styleClass, stylesheetUrl.toExternalForm());
 186                     }
 187                 } catch (MalformedURLException ex) {
 188                     return classesMap;
 189                 }
 190             }
 191         }
 192         return classesMap;
 193     }
 194 
 195     // Retrieve the styClasses in the fx object scene graph
 196     private static Map<String, String> getFxObjectClassesMap(Object fxObject, Object fxRoot) {
 197         Map<String, String> classesMap = new HashMap<>();
 198         classesMap.putAll(getSingleFxObjectClassesMap(fxObject));
 199         if (!(fxObject instanceof Node)) {
 200             return classesMap;
 201         }
 202         Node node = (Node) fxObject;
 203         if (node == fxRoot) {
 204             return classesMap;
 205         }
 206         // Loop on scene graph tree, and stop at root node (to avoid to handle SB nodes)
 207         while (node.getParent() != null) {
 208             node = node.getParent();
 209             classesMap.putAll(getSingleFxObjectClassesMap(node));
 210             if (node == fxRoot) {
 211                 break;
 212             }
 213         }
 214         return classesMap;
 215     }
 216 
 217     // Retrieve the styleClasses in the fx object only (not inherited ones)
 218     private static Map<String, String> getSingleFxObjectClassesMap(Object fxObject) {
 219         Map<String, String> classesMap = new HashMap<>();
 220 
 221         if (fxObject instanceof Parent) {
 222             List<String> stylesheets = ((Parent) fxObject).getStylesheets();
 223             for (String stylesheet : stylesheets) {
 224                 try {
 225                     for (String styleClass : getStyleClasses(new URL(stylesheet))) {
 226                         classesMap.put(styleClass, stylesheet);
 227                     }
 228                 } catch (MalformedURLException ex) {
 229                     return classesMap;
 230                 }
 231             }
 232         }
 233         return classesMap;
 234     }
 235 
 236     private static Set<String> getStyleClasses(final URL url) {
 237         Set<String> styleClasses = new HashSet<>();
 238         Stylesheet s;
 239         try {
 240             s = new CSSParser().parse(url);
 241         } catch (IOException ex) {
 242             System.out.println("Warning: Invalid Stylesheet " + url); //NOI18N
 243             return styleClasses;
 244         }
 245         if (s == null) {
 246             // The parsed CSS file was empty. No parsing occured.
 247             return styleClasses;
 248         }
 249         for (Rule r : s.getRules()) {
 250             for (Selector ss : r.getSelectors()) {
 251                 if (ss instanceof SimpleSelector) {
 252                     SimpleSelector simple = (SimpleSelector) ss;
 253                     styleClasses.addAll(simple.getStyleClasses());
 254                 } else {
 255                     if (ss instanceof CompoundSelector) {
 256                         CompoundSelector cs = (CompoundSelector) ss;
 257                         for (Selector selector : cs.getSelectors()) {
 258                             if (selector instanceof SimpleSelector) {
 259                                 SimpleSelector simple = (SimpleSelector) selector;
 260                                 styleClasses.addAll(simple.getStyleClasses());
 261                             }
 262                         }
 263                     }
 264                 }
 265             }
 266         }
 267         return styleClasses;
 268     }
 269 
 270     @SuppressWarnings("unchecked")
 271     public static List<String> getCssProperties(Set<Class<?>> classes) {
 272         TreeSet<String> cssProperties = new TreeSet<>();
 273         for (Class<?> clazz : classes) {
 274             if (Node.class.isAssignableFrom(clazz)) {
 275                 Object metadatas = null;
 276                 try {
 277                     metadatas = clazz.getMethod("getClassCssMetaData").invoke(null, (Object[]) null); //NOI18N
 278                 } catch (NoSuchMethodException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
 279                     assert false;
 280                 }
 281                 for (CssMetaData<? extends Styleable, ?> metadata : ((List<CssMetaData<? extends Styleable, ?>>) metadatas)) {
 282                     cssProperties.add(metadata.getProperty());
 283                     if (metadata.getSubProperties() != null) {
 284                         for (CssMetaData<? extends Styleable, ?> subMetadata : metadata.getSubProperties()) {
 285                             cssProperties.add(subMetadata.getProperty());
 286                         }
 287                     }
 288                 }
 289             }
 290         }
 291         return new ArrayList<>(cssProperties);
 292     }
 293 
 294     // If this property is ruled by CSS, return a CssPropAuthorInfo. Otherwise returns null.
 295     public static CssPropAuthorInfo getCssInfo(Object fxObject, ValuePropertyMetadata prop) {
 296         CssPropAuthorInfo info = null;
 297         Node node = null;
 298 
 299         if (fxObject instanceof Node) {
 300             node = (Node) fxObject;
 301         } else {
 302             Styleable styleable = fxObject instanceof Styleable ? (Styleable) fxObject : null;
 303             if (styleable != null) {
 304                 node = Deprecation.getNode(styleable);
 305             }
 306         }
 307         if (node != null) {
 308             info = getCssInfoForNode(node, prop);
 309         }
 310         return info;
 311     }
 312 
 313     private static CssPropAuthorInfo getCssInfoForNode(Node node, ValuePropertyMetadata prop) {
 314         @SuppressWarnings("rawtypes")
 315         Map<StyleableProperty, List<Style>> map = collectCssState(node);
 316         for (@SuppressWarnings("rawtypes") Map.Entry<StyleableProperty, List<Style>> entry : map.entrySet()) {//NOI18N
 317             StyleableProperty<?> beanProp = entry.getKey();
 318             List<Style> styles = new ArrayList<>(entry.getValue());
 319             String name = getBeanPropertyName(beanProp);
 320             if (!name.equals(prop.getName().getName())) {
 321                 continue;
 322             }
 323             if (name.equals(prop.getName().getName())) {
 324                 // If the value has an origin of Author or Inline 
 325                 // then we have a property ruled by CSS, otherwise return null
 326                 // This is in sync because the map is not empty
 327                 StyleOrigin origin = beanProp.getStyleOrigin();
 328                 if (origin == null || origin.equals(StyleOrigin.USER)
 329                         || origin.equals(StyleOrigin.USER_AGENT)) {
 330                     return null;
 331                 }
 332                 CssMetaData<?, ?> styleable = beanProp.getCssMetaData();
 333                 // Lookup the Author style
 334                 CssPropAuthorInfo info = null;
 335                 for (Style style : styles) {
 336                     Rule rule = style.getDeclaration().getRule();
 337                     assert rule != null;
 338                     // StyleOrigin can be null when the value is set to its initial value.
 339                     StyleOrigin o = rule.getOrigin();
 340                     if (o == null) {
 341                         return null;
 342                     }
 343                     if ((o.equals(StyleOrigin.AUTHOR) && (!CssInternal.isThemeStyle(style)))
 344                             || o.equals(StyleOrigin.INLINE)) {
 345                         if (info == null) {
 346                             info = new CssPropAuthorInfo(prop, beanProp, styleable);
 347                         }
 348                         info.getStyles().add(style);
 349                     }
 350                 }
 351                 return info;
 352             }
 353         }
 354         return null;
 355     }
 356 
 357     public static boolean isCssRuled(Object fxObject, ValuePropertyMetadata prop) {
 358         return getCssInfo(fxObject, prop) != null;
 359     }
 360 
 361     /**
 362      * CSS information attached to a Bean Property when styled with Author or
 363      * Inline origin.
 364      *
 365      */
 366     public static class CssPropAuthorInfo {
 367 
 368         private final ValuePropertyMetadata prop;
 369         private final CssMetaData<?, ?> styleable;
 370         private final StyleableProperty<?> value;
 371         private final Object val;
 372         private final List<Style> styles = new ArrayList<>();
 373 
 374         public CssPropAuthorInfo(ValuePropertyMetadata prop, StyleableProperty<?> value, CssMetaData<?, ?> styleable) {
 375             this(prop, value, styleable, null);
 376         }
 377 
 378         private CssPropAuthorInfo(ValuePropertyMetadata prop, StyleableProperty<?> value, CssMetaData<?, ?> styleable, Object val) {
 379             this.prop = prop;
 380             this.styleable = styleable;
 381             this.value = value;
 382             this.val = val;
 383         }
 384 
 385         public CssPropAuthorInfo(StyleableProperty<?> val, CssMetaData<?, ?> styleable, Object value) {
 386             this(null, val, styleable, value);
 387         }
 388 
 389         public StyleOrigin getOrigin() {
 390             return value.getStyleOrigin();
 391         }
 392 
 393         public URL getMainUrl() {
 394             if (getStyles().isEmpty()) {
 395                 return null;
 396             } else {
 397                 Rule rule = getStyles().get(0).getDeclaration().getRule();
 398                 if (rule == null) {
 399                     return null;
 400                 } else {
 401                     try {
 402                         return new URL(rule.getStylesheet().getUrl());
 403                     } catch (MalformedURLException ex) {
 404                         System.out.println(ex.getMessage() + " " + ex);
 405                         return null;
 406                     }
 407                 }
 408             }
 409         }
 410 
 411         public List<Style> getStyles() {
 412             return styles;
 413         }
 414 
 415         public Object getFxValue() {
 416             return val != null ? val : value.getValue();
 417         }
 418 
 419         public boolean isInline() {
 420             StyleOrigin o = getOrigin();
 421             return o != null && o.equals(StyleOrigin.INLINE);
 422         }
 423 
 424         /**
 425          * @return the prop
 426          */
 427         public ValuePropertyMetadata getProp() {
 428             return prop;
 429         }
 430 
 431         /**
 432          * @return the cssProp
 433          */
 434         public CssMetaData<?, ?> getCssProp() {
 435             return styleable;
 436         }
 437 
 438     }
 439 
 440     public static String getBeanPropertyName(StyleableProperty<?> val) {
 441         String property = null;
 442         if (val instanceof ReadOnlyProperty) {
 443             property = ((ReadOnlyProperty<?>) val).getName();
 444         }
 445         return property;
 446     }
 447 
 448     public static void attachMapToNode(Node node) {
 449         Map<StyleableProperty<?>, List<Style>> smap = new HashMap<>();
 450         Deprecation.setStyleMap(node, FXCollections.observableMap(smap));
 451     }
 452 
 453     public static void detachMapToNode(Node node) {
 454         Deprecation.setStyleMap(node, null);
 455     }
 456 
 457     @SuppressWarnings("rawtypes")
 458     public static Map<StyleableProperty, List<Style>> collectCssState(Node node) {
 459         attachMapToNode(node);
 460         // Force CSS to apply
 461         node.applyCss();
 462 
 463         Map<StyleableProperty, List<Style>> ret = new HashMap<>();
 464 //        ret.putAll(Deprecation.getStyleMap(node));
 465 
 466         Map<StyleableProperty<?>, List<Style>> map = Deprecation.getStyleMap(node);
 467         if (map != null && !map.isEmpty()) {
 468             for (Map.Entry<StyleableProperty<?>, List<Style>> entry : map.entrySet()) {
 469                 StyleableProperty<?> key = entry.getKey();
 470                 List<Style> value = entry.getValue();
 471                 if (((javafx.beans.property.Property<?>) key).getBean() == node) {
 472                     ret.put(key, value);
 473                 }
 474             }
 475         }
 476 
 477         // Attached map may impact css performance, so remove it.
 478         detachMapToNode(node);
 479         // DEBUG
 480 //        System.out.println("collectCssState() for " + node);
 481 //        for (StyleableProperty s : ret.keySet()) {
 482 //            List<Style> styles = ret.get(s);
 483 //            for (Style style : styles) {
 484 //                System.out.println(style.getDeclaration().getRule().getOrigin() + " ==> STYLE " + style.getDeclaration());
 485 //                System.out.println("--> css url = " + style.getDeclaration().getRule().getStylesheet().getUrl());
 486 //            }
 487 //        }
 488         return ret;
 489     }
 490 
 491     public static StyleOrigin getOrigin(Style style) {
 492         if (style == null || style.getDeclaration() == null) {
 493             return null;
 494         }
 495         return style.getDeclaration().getRule().getOrigin();
 496     }
 497 
 498     // From an css url, returns the theme display name
 499     public static String getThemeDisplayName(String url) {
 500         String themeName = ""; //NOI18N
 501         if (url.contains("modena")) {//NOI18N
 502             themeName += "modena/"; //NOI18N
 503         } else if (url.contains("caspian")) {//NOI18N
 504             themeName += "caspian/"; //NOI18N
 505         }
 506         File file = new File(url);
 507         themeName += file.getName().replace(".bss", ".css");//NOI18N
 508         if (themeName.endsWith("modena.css")) {//NOI18N
 509             themeName = "modena.css";//NOI18N
 510         } else if (themeName.endsWith("caspian.css")) {//NOI18N
 511             themeName = "caspian.css";//NOI18N
 512         }
 513         return themeName;
 514     }
 515 
 516 }