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}