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 }