1 /*
   2  * Copyright (c) 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.editor.panel.inspector.popupeditors;
  33 
  34 import com.oracle.javafx.scenebuilder.kit.editor.EditorController;
  35 import com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.editors.AutoSuggestEditor;
  36 import com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.editors.BoundedDoubleEditor;
  37 import com.oracle.javafx.scenebuilder.kit.editor.panel.inspector.editors.EditorUtils;
  38 import com.oracle.javafx.scenebuilder.kit.metadata.property.ValuePropertyMetadata;
  39 
  40 import java.lang.ref.WeakReference;
  41 import java.util.ArrayList;
  42 import java.util.Arrays;
  43 import java.util.Collections;
  44 import java.util.Comparator;
  45 import java.util.HashMap;
  46 import java.util.HashSet;
  47 import java.util.List;
  48 import java.util.Map;
  49 import java.util.Objects;
  50 import java.util.Set;
  51 import java.util.TreeMap;
  52 import java.util.TreeSet;
  53 
  54 import javafx.beans.value.ChangeListener;
  55 import javafx.event.ActionEvent;
  56 import javafx.event.EventHandler;
  57 import javafx.fxml.FXML;
  58 import javafx.scene.Node;
  59 import javafx.scene.Parent;
  60 import javafx.scene.layout.StackPane;
  61 import javafx.scene.text.Font;
  62 
  63 /**
  64  * Font popup editor.
  65  */
  66 public class FontPopupEditor extends PopupEditor {
  67 
  68     @FXML
  69     private StackPane familySp;
  70     @FXML
  71     private StackPane styleSp;
  72     @FXML
  73     private StackPane sizeSp;
  74 
  75     private Parent root;
  76     private Font font = Font.getDefault();
  77     private FamilyEditor familyEditor;
  78     private StyleEditor styleEditor;
  79     private BoundedDoubleEditor sizeEditor;
  80     private EditorController editorController;
  81 
  82     public FontPopupEditor(ValuePropertyMetadata propMeta, Set<Class<?>> selectedClasses, EditorController editorController) {
  83         super(propMeta, selectedClasses);
  84         initialize(editorController);
  85     }
  86 
  87     private void initialize(EditorController editorController) {
  88         this.editorController = editorController;
  89     }
  90 
  91     private void setStyle() {
  92         styleEditor.reset("", "", new ArrayList<>(getStyles(EditorUtils.toString(familyEditor.getValue()), false, editorController)));//NOI18N
  93         styleEditor.setUpdateFromModel(true);
  94         styleEditor.setValue(font.getStyle());
  95         styleEditor.setUpdateFromModel(false);
  96     }
  97 
  98     private void commit() {
  99         if (isUpdateFromModel()) {
 100             return;
 101         }
 102         font = getFont();
 103         assert font != null;
 104 //        System.out.println("Committing: " + font + " - preview: " + getValueAsString());
 105         commitValue(font);
 106     }
 107 
 108     private Font getFont() {
 109         Font oldFont = font;
 110         Object sizeObj = sizeEditor.getValue();
 111         assert sizeObj instanceof Double;
 112         Font newFont = getFont(EditorUtils.toString(familyEditor.getValue()), EditorUtils.toString(styleEditor.getValue()),
 113                 (Double) sizeObj, editorController);
 114         if (newFont != null) {
 115             return newFont;
 116         } else {
 117             return oldFont;
 118         }
 119     }
 120 
 121     @Override
 122     public Object getValue() {
 123         return font;
 124     }
 125 
 126     public void reset(ValuePropertyMetadata propMeta, Set<Class<?>> selectedClasses, EditorController editorController) {
 127         super.reset(propMeta, selectedClasses);
 128         this.editorController = editorController;
 129     }
 130 
 131     //
 132     // Interface from PopupEditor
 133     // Methods called by PopupEditor.
 134     //
 135     @Override
 136     public void initializePopupContent() {
 137         root = EditorUtils.loadPopupFxml("FontPopupEditor.fxml", this); //NOI18N
 138         // Add the editors in the scene graph
 139         familyEditor = new FamilyEditor("", "", getFamilies(editorController), editorController);//NOI18N
 140         familySp.getChildren().add(familyEditor.getValueEditor());
 141         styleEditor = new StyleEditor("", "", new ArrayList<>(), editorController);//NOI18N
 142         styleSp.getChildren().add(styleEditor.getValueEditor());
 143         sizeEditor = new BoundedDoubleEditor("", "", getPredefinedFontSizes(), 1.0, 96.0, true);//NOI18N
 144         commitOnFocusLost(sizeEditor);
 145         sizeSp.getChildren().add(sizeEditor.getValueEditor());
 146 
 147         familyEditor.valueProperty().addListener((ChangeListener<Object>) (ov, oldVal, newVal) -> {
 148             if (familyEditor.isUpdateFromModel()) {
 149                 // nothing to do
 150                 return;
 151             }
 152             commit();
 153             setStyle();
 154         });
 155 
 156         styleEditor.valueProperty().addListener((ChangeListener<Object>) (ov, oldVal, newVal) -> {
 157             if (styleEditor.isUpdateFromModel()) {
 158                 // nothing to do
 159                 return;
 160             }
 161             commit();
 162         });
 163 
 164         sizeEditor.valueProperty().addListener((ChangeListener<Object>) (ov, oldVal, newVal) -> {
 165             if (sizeEditor.isUpdateFromModel()) {
 166                 // nothing to do
 167                 return;
 168             }
 169             commit();
 170         });
 171 
 172         sizeEditor.transientValueProperty().addListener((ChangeListener<Object>) (ov, oldVal, newVal) -> transientValue(getFont()));
 173     }
 174 
 175     @Override
 176     public String getPreviewString(Object value) {
 177         // value should never be null
 178         assert value instanceof Font;
 179         Font fontVal = (Font) value;
 180         if (isIndeterminate()) {
 181             return "-"; //NOI18N
 182         } else {
 183             String size = EditorUtils.valAsStr(fontVal.getSize());
 184             return fontVal.getFamily() + " " + size + "px" //NOI18N
 185                     + (!fontVal.getName().equals(fontVal.getFamily()) && !"Regular".equals(fontVal.getStyle()) ? //NOI18N
 186                     " (" + fontVal.getStyle() + ")" : ""); //NOI18N
 187         }
 188     }
 189 
 190     @Override
 191     public void setPopupContentValue(Object value) {
 192         assert value instanceof Font;
 193         font = (Font) value;
 194         familyEditor.setUpdateFromModel(true);
 195         familyEditor.setValue(font.getFamily());
 196         familyEditor.setUpdateFromModel(false);
 197         setStyle();
 198         sizeEditor.setUpdateFromModel(true);
 199         sizeEditor.setValue(font.getSize());
 200         sizeEditor.setUpdateFromModel(false);
 201     }
 202 
 203     @Override
 204     public Node getPopupContentNode() {
 205         return root;
 206     }
 207 
 208     private static class FamilyEditor extends AutoSuggestEditor {
 209 
 210         private List<String> families;
 211         private String family = null;
 212 
 213         public FamilyEditor(String name, String defaultValue, List<String> families, EditorController editorController) {
 214             super(name, defaultValue, families);
 215             initialize(families, editorController);
 216         }
 217 
 218         private void initialize(List<String> families, EditorController editorController) {
 219             this.families = families;
 220             EventHandler<ActionEvent> onActionListener = event -> {
 221                 if (Objects.equals(family, getTextField().getText())) {
 222                     // no change
 223                     return;
 224                 }
 225                 family = getTextField().getText();
 226                 if (family.isEmpty() || !FamilyEditor.this.families.contains(family)) {
 227                     editorController.getMessageLog().logWarningMessage(
 228                             "inspector.font.invalidfamily", family); //NOI18N
 229                     return;
 230                 }
 231 //                    System.out.println("Setting family from '" + valueProperty().get() + "' to '" + value + "'");
 232                 valueProperty().setValue(family);
 233             };
 234 
 235             setTextEditorBehavior(this, getTextField(), onActionListener);
 236             commitOnFocusLost(this);
 237         }
 238 
 239         @Override
 240         public Object getValue() {
 241             return getTextField().getText();
 242         }
 243 
 244         @SuppressWarnings("unused")
 245         public List<String> getFamilies() {
 246             return families;
 247         }
 248     }
 249 
 250     private static class StyleEditor extends AutoSuggestEditor {
 251 
 252         private String style = null;
 253 
 254         public StyleEditor(String name, String defaultValue, List<String> suggestedList, EditorController editorController) {
 255             super(name, defaultValue, suggestedList);
 256             initialize(editorController);
 257         }
 258 
 259         private void initialize(EditorController editorController) {
 260             EventHandler<ActionEvent> onActionListener = event -> {
 261                 if (Objects.equals(style, getTextField().getText())) {
 262                     // no change
 263                     return;
 264                 }
 265                 style = getTextField().getText();
 266                 if (style.isEmpty() || !getSuggestedList().contains(style)) {
 267                     editorController.getMessageLog().logWarningMessage(
 268                             "inspector.font.invalidstyle", style); //NOI18N
 269                     return;
 270                 }
 271                 valueProperty().setValue(style);
 272             };
 273 
 274             setTextEditorBehavior(this, getTextField(), onActionListener);
 275             commitOnFocusLost(this);
 276         }
 277 
 278         @Override
 279         public Object getValue() {
 280             return getTextField().getText();
 281         }
 282     }
 283 
 284     private static void commitOnFocusLost(AutoSuggestEditor autoSuggestEditor) {
 285         autoSuggestEditor.getTextField().focusedProperty().addListener((ChangeListener<Boolean>) (ov, oldVal, newVal) -> {
 286             if (!newVal) {
 287                 autoSuggestEditor.getCommitListener().handle(null);
 288             }
 289         });
 290     }
 291 
 292     /*
 293      *
 294      * Utilities methods for Font handling
 295      *
 296      */
 297     private static WeakReference<Map<String, Map<String, Font>>> fontCache
 298             = new WeakReference<>(null);
 299 
 300     // Automagically discover which font will require the work around for RT-23021.
 301     private static volatile Map<String, String> pathologicalFonts = null;
 302 
 303     private static final Comparator<Font> fontComparator
 304             = (t, t1) -> {
 305         int cmp = t.getName().compareTo(t1.getName());
 306         if (cmp != 0) {
 307             return cmp;
 308         }
 309         return t.toString().compareTo(t1.toString());
 310     };
 311 
 312     public static Set<Font> getAllFonts() {
 313         Font f = Font.getDefault();
 314         double defSize = f.getSize();
 315         Set<Font> allFonts = new TreeSet<>(fontComparator);
 316         for (String familly : Font.getFamilies()) {
 317             //System.out.println("*** FAMILY: " + familly); //NOI18N
 318             for (String name : Font.getFontNames(familly)) {
 319                 Font font = new Font(name, defSize);
 320                 allFonts.add(font);
 321                 //System.out.println("\t\""+name+"\" -- name=\""+font.getName()+"\", familly=\""+font.getFamily()+"\", style=\""+font.getStyle()+"\""); //NOI18N
 322             }
 323         }
 324         // some font will not appear with the code above: we also need to use getAllNames!
 325         for (String name : Font.getFontNames()) {
 326             Font font = new Font(name, defSize);
 327             allFonts.add(font);
 328         }
 329         return allFonts;
 330     }
 331 
 332     public static List<String> getFamilies(EditorController editorController) {
 333 //        System.out.println("Getting font families...");
 334         return new ArrayList<>(getFontMap(editorController).keySet());
 335     }
 336 
 337     public static Set<String> getStyles(String family, boolean canBeUnknown, EditorController editorController) {
 338         Map<String, Font> styles = getFontMap(editorController).get(family);
 339         if (styles == null) {
 340             assert !canBeUnknown;
 341             styles = Collections.emptyMap();
 342         }
 343         return styles.keySet();
 344     }
 345 
 346     public static Font getFont(String family, String style, EditorController editorController) {
 347         Map<String, Font> styles = getFontMap(editorController).get(family);
 348         if (styles == null) {
 349             styles = Collections.emptyMap();
 350         }
 351 
 352         if (styles.get(style) == null) {
 353             // The requested style does not exist for this font:
 354             // pick up the first style
 355             style = styles.keySet().iterator().next();
 356         }
 357         return styles.get(style);
 358     }
 359 
 360     public static Font getFont(String family, String style, double size, EditorController editorController) {
 361         final Font font = getFont(family, style, editorController);
 362         if (font == null) {
 363             return null;
 364         }
 365         return getFont(font, size);
 366     }
 367 
 368     public static Font getFont(Font font, double size) {
 369         if (font == null) {
 370             assert false;
 371             font = Font.getDefault();
 372         }
 373         if (Math.abs(font.getSize() - size) < .0000001) {
 374             return font;
 375         }
 376 
 377         return new Font(getPersistentName(font), size);
 378     }
 379 
 380     public static Map<String, String> getPathologicalFonts() {
 381         if (pathologicalFonts == null) {
 382             final double size = Font.getDefault().getSize();
 383             final String defaultName = Font.getDefault().getName();
 384             Map<String, String> problems = new HashMap<>();
 385             final Set<String> allNames = new HashSet<>(Font.getFontNames());
 386             for (String familly : Font.getFamilies()) {
 387                 allNames.addAll(Font.getFontNames(familly));
 388             }
 389             for (String name : allNames) {
 390                 Font f = new Font(name, size);
 391                 if (f.getName().equals(name)) {
 392                     continue;
 393                 }
 394                 if (f.getName().equals(defaultName) || f.getName().equals("System")) { //NOI18N
 395                     continue; //NOI18N
 396                 }
 397                 final Font f2 = new Font(f.getName(), size);
 398                 if (f2.getName().equals(f.getName())) {
 399                     continue;
 400                 }
 401                 problems.put(f.getName(), name);
 402             }
 403             pathologicalFonts = Collections.unmodifiableMap(problems);
 404         }
 405         return pathologicalFonts;
 406     }
 407 
 408     public static String getPersistentName(Font font) {
 409         // The block below is an ugly workaround for
 410         // RT-23021: Inconsitent naming for fonts in the 'Tahoma' family.
 411         final Map<String, String> problems = getPathologicalFonts();
 412         if (problems.containsKey(font.getName())) { // e.g. font.getName() is "Tahoma Bold" //NOI18N
 413             final Font test = new Font(font.getName(), font.getSize());
 414             if (test.getName().equals(font.getName())) {
 415                 // OK
 416                 return font.getName();
 417             } else {
 418                 final String alternateName = problems.get(font.getName()); // e.g: "Tahoma Negreta" //NOI18N
 419                 assert alternateName != null;
 420                 final Font test2 = new Font(alternateName, font.getSize()); //NOI18N
 421                 if (test2.getName().equals(font.getName())) {
 422                     // OK
 423                     return alternateName; // e.g: "Tahoma Negreta" //NOI18N
 424                 }
 425             }
 426         }
 427         return font.getName();
 428     }
 429 
 430     private static Map<String, Map<String, Font>> getFontMap(EditorController editorController) {
 431         Map<String, Map<String, Font>> fonts = fontCache.get();
 432         if (fonts == null) {
 433             fonts = makeFontMap(editorController);
 434             fontCache = new WeakReference<>(fonts);
 435         }
 436         return fonts;
 437     }
 438 
 439     private static Map<String, Map<String, Font>> makeFontMap(EditorController editorController) {
 440         final Set<Font> fonts = getAllFonts();
 441         final Map<String, Map<String, Set<Font>>> fontTree = new TreeMap<>();
 442 
 443         for (Font f : fonts) {
 444             Map<String, Set<Font>> familyStyleMap = fontTree.get(f.getFamily());
 445             if (familyStyleMap == null) {
 446                 familyStyleMap = new TreeMap<>();
 447                 fontTree.put(f.getFamily(), familyStyleMap);
 448             }
 449             Set<Font> styleFonts = familyStyleMap.get(f.getStyle());
 450             if (styleFonts == null) {
 451                 styleFonts = new HashSet<>();
 452                 familyStyleMap.put(f.getStyle(), styleFonts);
 453             }
 454             styleFonts.add(f);
 455         }
 456 
 457         final Map<String, Map<String, Font>> res = new TreeMap<>();
 458         for (Map.Entry<String, Map<String, Set<Font>>> e1 : fontTree.entrySet()) {
 459             final String family = e1.getKey();
 460             final Map<String, Set<Font>> styleMap = e1.getValue();
 461             final Map<String, Font> resMap = new TreeMap<>();
 462             for (Map.Entry<String, Set<Font>> e2 : styleMap.entrySet()) {
 463                 final String style = e2.getKey();
 464                 final Set<Font> fontSet = e2.getValue();
 465                 int size = fontSet.size();
 466                 assert 1 <= size;
 467                 if (1 < size) {
 468                     editorController.getMessageLog().logWarningMessage(
 469                             "inspector.font.samefamilystyle", styleMap.get(style)); //NOI18N
 470                 }
 471                 resMap.put(style, styleMap.get(style).iterator().next());
 472             }
 473             res.put(family, Collections.<String, Font>unmodifiableMap(resMap));
 474         }
 475         return Collections.<String, Map<String, Font>>unmodifiableMap(res);
 476     }
 477 
 478     private static List<String> getPredefinedFontSizes() {
 479         String[] predefinedFontSizes
 480                 = {"9", "10", "11", "12", "13", "14", "18", "24", "36", "48", "64", "72", "96"};//NOI18N
 481         return Arrays.asList(predefinedFontSizes);
 482     }
 483 
 484 }