001package jmri.util.swing; 002 003import java.awt.Canvas; 004import java.awt.Dimension; 005import java.awt.Font; 006import java.awt.FontMetrics; 007import java.awt.GraphicsEnvironment; 008import java.util.ArrayList; 009import java.util.Arrays; 010import java.util.List; 011import javax.swing.BoxLayout; 012import javax.swing.JComboBox; 013import javax.swing.JLabel; 014import javax.swing.JList; 015import javax.swing.JPanel; 016import javax.swing.UIManager; 017import org.slf4j.Logger; 018import org.slf4j.LoggerFactory; 019 020/** 021 * This utility class provides methods that initialise and return a JComboBox 022 * containing a specific sub-set of fonts installed on a users system. 023 * <p> 024 * Optionally, the JComboBox can be displayed with a preview of the specific 025 * font in the drop-down list itself. 026 * <hr> 027 * This file is part of JMRI. 028 * <p> 029 * JMRI is free software; you can redistribute it and/or modify it under the 030 * terms of version 2 of the GNU General Public License as published by the Free 031 * Software Foundation. See the "COPYING" file for a copy of this license. 032 * <p> 033 * JMRI is distributed in the hope that it will be useful, but WITHOUT ANY 034 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 035 * A PARTICULAR PURPOSE. See the GNU General Public License for more details. 036 * 037 * @author Matthew Harris Copyright (C) 2011 038 * @since 2.13.1 039 */ 040public class FontComboUtil { 041 042 public static final int ALL = 0; 043 public static final int MONOSPACED = 1; 044 public static final int PROPORTIONAL = 2; 045 public static final int CHARACTER = 3; 046 public static final int SYMBOL = 4; 047 048 private static final List<String> all = new ArrayList<>(); 049 private static final List<String> monospaced = new ArrayList<>(); 050 private static final List<String> proportional = new ArrayList<>(); 051 private static final List<String> character = new ArrayList<>(); 052 private static final List<String> symbol = new ArrayList<>(); 053 054 private static volatile boolean prepared = false; 055 private static volatile boolean preparing = false; 056 057 public static List<String> getFonts(int which) { 058 prepareFontLists(); 059 060 switch (which) { 061 case MONOSPACED: 062 return new ArrayList<>(monospaced); 063 case PROPORTIONAL: 064 return new ArrayList<>(proportional); 065 case CHARACTER: 066 return new ArrayList<>(character); 067 case SYMBOL: 068 return new ArrayList<>(symbol); 069 default: 070 return new ArrayList<>(all); 071 } 072 073 } 074 075 /** 076 * Determine if the specified font family is a symbol font 077 * 078 * @param font the font family to check 079 * @return true if a symbol font; false if not 080 */ 081 public static boolean isSymbolFont(String font) { 082 prepareFontLists(); 083 return symbol.contains(font); 084 } 085 086 /** 087 * Method to initialise the font lists on first access 088 */ 089 public static void prepareFontLists() { 090 if (prepared || preparing) { 091 // Normally we shouldn't get here except when the initialisation 092 // thread has taken a bit longer than normal. 093 log.debug("Subsequent call - no need to prepare"); 094 return; 095 } 096 initFonts(); 097 } 098 099 private static synchronized void initFonts() { 100 preparing = true; 101 102 log.debug("Prepare font lists..."); 103 104 // Initialise the font lists 105 initAllFonts(); 106 107 // Create a font render context to use for the comparison 108 Canvas c = new Canvas(); 109 // Loop through all available font families 110 all.forEach(s -> { 111 112 // Retrieve a plain version of the current font family 113 Font f = new Font(s, Font.PLAIN, 12); 114 FontMetrics fm = c.getFontMetrics(f); 115 116 // Fairly naive test if this is a symbol font 117// if (f.canDisplayUpTo("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")==-1) { 118 // Check that a few different characters can be displayed 119 if (f.canDisplay('F') && f.canDisplay('b') && f.canDisplay('8')) { 120 // It's not a symbol font - add to the character font list 121 character.add(s); 122 123 // Check if the widths of a 'narrow' letter (I) 124 // a 'wide' letter (W) and a 'space' ( ) are the same. 125 int w = fm.charWidth('I'); 126 if (fm.charWidth('W') == w && fm.charWidth(' ') == w) { 127 // Yes, they're all the same width - add to the monospaced list 128 monospaced.add(s); 129 } else { 130 // No, they're different widths - add to the proportional list 131 proportional.add(s); 132 } 133 } else { 134 // It's a symbol font - add to the symbol font list 135 symbol.add(s); 136 } 137 }); 138 139 log.debug("...font lists built"); 140 prepared = true; 141 } 142 143 private static synchronized void initAllFonts() { 144 if (all.isEmpty()) { 145 all.addAll(Arrays.asList(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames())); 146 } 147 } 148 149 /** 150 * Return a JComboBox containing all available font families. The list is 151 * displayed using a preview of the font at the standard size. 152 * 153 * @see #getFontCombo(int, int, boolean) 154 * @return List of all available font families as a {@link JComboBox} 155 */ 156 public static JComboBox<String> getFontCombo() { 157 initAllFonts(); 158 return getFontCombo(ALL); 159 } 160 161 /** 162 * Return a JComboBox containing all available font families. The list is 163 * displayed using a preview of the font at the standard size and with the 164 * option of the name alongside in the regular dialog font. 165 * 166 * @see #getFontCombo(int, int, boolean) 167 * @param previewOnly set to True to show only a preview in the list; False 168 * to show both name and preview 169 * @return List of specified font families as a {@link JComboBox} 170 */ 171 public static JComboBox<String> getFontCombo(boolean previewOnly) { 172 initAllFonts(); 173 return getFontCombo(ALL, previewOnly); 174 } 175 176 /** 177 * Return a JComboBox containing the specified set of font families. The 178 * list is displayed using a preview of the font at the standard size. 179 * 180 * @see #getFontCombo(int, int, boolean) 181 * @param which the set of fonts to return; {@link #MONOSPACED}, 182 * {@link #PROPORTIONAL}, {@link #CHARACTER}, {@link #SYMBOL} or 183 * {@link #ALL} 184 * @return List of specified font families as a {@link JComboBox} 185 */ 186 public static JComboBox<String> getFontCombo(int which) { 187 return getFontCombo(which, true); 188 } 189 190 /** 191 * Return a JComboBox containing the specified set of font families. The 192 * list is displayed using a preview of the font at the standard size and 193 * with the option of the name alongside in the regular dialog font. 194 * 195 * @see #getFontCombo(int, int, boolean) 196 * @param which the set of fonts to return; {@link #MONOSPACED}, 197 * {@link #PROPORTIONAL}, {@link #CHARACTER}, {@link #SYMBOL} or 198 * {@link #ALL} 199 * @param previewOnly set to True to show only a preview in the list; False 200 * to show both name and preview 201 * @return List of specified font families as a {@link JComboBox} 202 */ 203 public static JComboBox<String> getFontCombo(int which, boolean previewOnly) { 204 return getFontCombo(which, 0, previewOnly); 205 } 206 207 /** 208 * Return a JComboBox containing the specified set of font families. The 209 * list is displayed using a preview of the font at the specified point 210 * size. 211 * 212 * @see #getFontCombo(int, int, boolean) 213 * @param which the set of fonts to return; {@link #MONOSPACED}, 214 * {@link #PROPORTIONAL}, {@link #CHARACTER}, {@link #SYMBOL} or 215 * {@link #ALL} 216 * @param size point size for the preview 217 * @return List of specified font families as a {@link JComboBox} 218 */ 219 public static JComboBox<String> getFontCombo(int which, int size) { 220 return getFontCombo(which, size, true); 221 } 222 223 /** 224 * Return a JComboBox containing the specified set of font families. The 225 * list is displayed using a preview of the font at the specified point size 226 * and with the option of the name alongside in the regular dialog font. 227 * <p> 228 * Available font sets: 229 * <ul> 230 * <li>Monospaced fonts {@link #MONOSPACED} 231 * <li>Proportional fonts {@link #PROPORTIONAL} 232 * <li>Character fonts {@link #CHARACTER} 233 * <li>Symbol fonts {@link #SYMBOL} 234 * <li>All available fonts {@link #ALL} 235 * </ul> 236 * <p> 237 * Typical usage: 238 * <pre> 239 * JComboBox fontFamily = FontComboUtil.getFontCombo(FontComboUtil.MONOSPACED); 240 * fontFamily.addActionListener(new ActionListener() { 241 * public void actionPerformed(ActionEvent e) { 242 * myObject.setFontFamily((String) ((JComboBox)e.getSource()).getSelectedItem()); 243 * } 244 * }); 245 * fontFamily.setSelectedItem(myObject.getFontFamily()); 246 * </pre> 247 * 248 * @param which the set of fonts to return; {@link #MONOSPACED}, 249 * {@link #PROPORTIONAL}, {@link #CHARACTER}, {@link #SYMBOL} or 250 * {@link #ALL} 251 * @param size point size for the preview 252 * @param previewOnly true to show only a preview in the list; false to show 253 * both name and preview 254 * @return List of specified font families as a {@link JComboBox} 255 */ 256 public static JComboBox<String> getFontCombo(int which, final int size, final boolean previewOnly) { 257 prepareFontLists(); 258 // Create a JComboBox containing the specified list of font families 259 List<String> fonts = getFonts(which); 260 JComboBox<String> fontList = new JComboBox<>(fonts.toArray(new String[fonts.size()])); 261 262 // Assign a custom renderer 263 fontList.setRenderer((JList<? extends String> list, String family, // name of the current font family 264 int index, boolean isSelected, boolean hasFocus) -> { 265 JPanel p = new JPanel(); 266 p.setLayout(new BoxLayout(p, BoxLayout.X_AXIS)); 267 268 // Opaque only when rendering the actual list items 269 p.setOpaque(index > -1); 270 271 // Invert colours when item selected in the list 272 if (isSelected && index > -1) { 273 p.setBackground(list.getSelectionBackground()); 274 p.setForeground(list.getSelectionForeground()); 275 } else { 276 p.setBackground(list.getBackground()); 277 p.setForeground(list.getForeground()); 278 } 279 280 // Setup two labels: 281 // - one for the font name in regular dialog font 282 // - one for the font name in the font itself 283 JLabel name = new JLabel(family + (previewOnly || index == -1 ? "" : ": ")); 284 JLabel preview = new JLabel(family); 285 286 // Set the font of the labels 287 // Regular dialog font for the name 288 // Actual font for the preview (unless a symbol font) 289 name.setFont(list.getFont()); 290 if (isSymbolFont(family)) { 291 preview.setFont(list.getFont()); 292 preview.setText(family + " " + Bundle.getMessage("FontSymbol")); 293 } else { 294 preview.setFont(new Font(family, Font.PLAIN, size == 0 ? list.getFont().getSize() : size)); 295 } 296 297 // Set the size of the labels 298 name.setPreferredSize(new Dimension((index == -1 && !previewOnly ? name.getMaximumSize().width * 2 : name.getMaximumSize().width), name.getMaximumSize().height + 4)); 299 preview.setPreferredSize(new Dimension(name.getMaximumSize().width, preview.getMaximumSize().height)); 300 301 // Centre align both labels vertically 302 name.setAlignmentY(JLabel.CENTER_ALIGNMENT); 303 preview.setAlignmentY(JLabel.CENTER_ALIGNMENT); 304 305 // Ensure text colours align with that of the underlying panel 306 name.setForeground(p.getForeground()); 307 preview.setForeground(p.getForeground()); 308 309 // Determine which label(s) to show 310 // Always display the dialog font version as the list header 311 if (!previewOnly && index > -1) { 312 p.add(name); 313 p.add(preview); 314 } else if (index == -1) { 315 name.setPreferredSize(new Dimension(name.getPreferredSize().width + 20, name.getPreferredSize().height - 2)); 316 p.add(name); 317 } else { 318 p.add(preview); 319 } 320 321 // 'Oribble hack as CDE/Motif JComboBox doesn't seem to like 322 // displaying JPanels in the JComboBox header 323 if (UIManager.getLookAndFeel().getName().equals("CDE/Motif") && index == -1) { 324 return name; 325 } 326 return p; 327 328 }); 329 return fontList; 330 } 331 332 /** 333 * Determine if usable; starts the process of making it so if needed 334 * 335 * @return true if ready for use; false otherwise 336 */ 337 public static boolean isReady() { 338 if (!prepared && !preparing) { // prepareFontLists is synchronized; don't do it if you don't have to 339 jmri.util.ThreadingUtil.newThread( 340 () -> { 341 prepareFontLists(); 342 }, 343 "FontComboUtil Prepare").start(); 344 } 345 return prepared; 346 } 347 348 private static final Logger log = LoggerFactory.getLogger(FontComboUtil.class); 349 350}