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 }