View Javadoc
1   package org.opentrafficsim.swing.gui;
2   
3   import java.awt.Component;
4   import java.awt.Font;
5   import java.awt.event.MouseAdapter;
6   import java.awt.event.MouseEvent;
7   import java.awt.event.WindowAdapter;
8   import java.awt.event.WindowEvent;
9   import java.io.File;
10  import java.io.FileReader;
11  import java.io.FileWriter;
12  import java.io.IOException;
13  import java.util.Dictionary;
14  import java.util.Enumeration;
15  import java.util.LinkedHashMap;
16  import java.util.Map;
17  import java.util.Properties;
18  
19  import javax.imageio.ImageIO;
20  import javax.swing.AbstractButton;
21  import javax.swing.ButtonGroup;
22  import javax.swing.JCheckBoxMenuItem;
23  import javax.swing.JComponent;
24  import javax.swing.JFrame;
25  import javax.swing.JLabel;
26  import javax.swing.JMenu;
27  import javax.swing.JMenuItem;
28  import javax.swing.JPanel;
29  import javax.swing.JPopupMenu;
30  import javax.swing.JSlider;
31  import javax.swing.MenuElement;
32  import javax.swing.MenuSelectionManager;
33  import javax.swing.UIManager;
34  import javax.swing.event.ChangeEvent;
35  import javax.swing.event.ChangeListener;
36  
37  import org.opentrafficsim.base.Resource;
38  
39  import nl.tudelft.simulation.dsol.swing.animation.d2.VisualizationPanel;
40  
41  /**
42   * Application with global appearance control. Subclasses should call {@code AppearanceApplication.setDefaultFont();} before any
43   * GUI elements are created (unless this is the first GUI element). Subclasses should call
44   * {@code setAppearance(getAppearance());} once all elements have been added to the GUI.
45   * <p>
46   * Copyright (c) 2023-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved. <br>
47   * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
48   * </p>
49   * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
50   */
51  public class AppearanceApplication extends JFrame
52  {
53  
54      /** */
55      private static final long serialVersionUID = 20231017L;
56  
57      /** Font. */
58      private static final Font FONT = new Font("Dialog", Font.PLAIN, AppearanceControl.DEFAULT_FONT_SIZE);
59  
60      /** Map of font scales. */
61      private static final Map<String, Double> FONT_SCALES = new LinkedHashMap<>();
62  
63      static
64      {
65          FONT_SCALES.put("Small", 10.0 / 12.0);
66          FONT_SCALES.put("Normal", 1.0);
67          FONT_SCALES.put("Large", 14.0 / 12.0);
68          FONT_SCALES.put("Very large", 16.0 / 12.0);
69      }
70  
71      /** Properties for the frame appearance (not simulation related). */
72      protected Properties frameProperties;
73  
74      /** Popup menu with options. */
75      private final JPopupMenu popMenu;
76  
77      /** Group of appearance items. */
78      private final ButtonGroup appGroup;
79  
80      /** Group of font scale items. */
81      private final ButtonGroup scaleGroup;
82  
83      /** Current appearance. */
84      private Appearance appearance = Appearance.GRAY;
85  
86      /** Current font scale. */
87      private String fontScaleName = "Normal";
88  
89      /**
90       * Constructor that uses the default content pane.
91       */
92      public AppearanceApplication()
93      {
94          this(null);
95      }
96  
97      /**
98       * Constructor that sets the content pane.
99       * @param panel JPanel; content pane.
100      */
101     public AppearanceApplication(final JPanel panel)
102     {
103         /*
104          * Any application is supposed to invoke this before any GUI element is made. However, this may be the first GUI element
105          * whereas no subclass can call this as there are only calls to super. Hence we need to call it here too.
106          */
107         AppearanceApplication.setDefaultFont();
108 
109         if (panel != null)
110         {
111             setContentPane(panel);
112         }
113         try
114         {
115             setIconImage(ImageIO.read(Resource.getResourceAsStream("/OTS_merge.png")));
116         }
117         catch (IOException io)
118         {
119             // accept no icon set
120         }
121 
122         // Listener to write frame properties on frame close
123         String sep = System.getProperty("file.separator");
124         String propertiesFile = System.getProperty("user.home") + sep + "OTS" + sep + "properties.ini";
125         addWindowListener(new WindowAdapter()
126         {
127             /** {@inheritDoce} */
128             @Override
129             public void windowClosing(final WindowEvent windowEvent)
130             {
131                 try
132                 {
133                     File f = new File(propertiesFile);
134                     f.getParentFile().mkdirs();
135                     FileWriter writer = new FileWriter(f);
136                     AppearanceApplication.this.frameProperties.store(writer, "OTS user settings");
137                 }
138                 catch (IOException exception)
139                 {
140                     System.err.println("Could not store properties at " + propertiesFile + ".");
141                 }
142             }
143         });
144 
145         // Set default frame properties and load properties from file (if any)
146         Properties defaults = new Properties();
147         defaults.setProperty("Appearance", "GRAY");
148         defaults.setProperty("FontScale", "Normal");
149         this.frameProperties = new Properties(defaults);
150         try
151         {
152             FileReader reader = new FileReader(propertiesFile);
153             this.frameProperties.load(reader);
154         }
155         catch (IOException ioe)
156         {
157             // ok, use defaults
158         }
159         this.appearance = Appearance.valueOf(this.frameProperties.getProperty("Appearance").toUpperCase());
160         this.fontScaleName = this.frameProperties.getProperty("FontScale");
161 
162         /** Menu class to only accept the font of an Appearance */
163         class AppearanceControlMenu extends JMenu implements AppearanceControl
164         {
165             /** */
166             private static final long serialVersionUID = 20180206L;
167 
168             /**
169              * Constructor.
170              * @param string String; string
171              */
172             AppearanceControlMenu(final String string)
173             {
174                 super(string);
175             }
176 
177             /** {@inheritDoc} */
178             @Override
179             public boolean isFont()
180             {
181                 return true;
182             }
183 
184             /** {@inheritDoc} */
185             @Override
186             public String toString()
187             {
188                 return "AppearanceControlMenu []";
189             }
190         }
191 
192         // Appearance menu
193         JMenu app = new AppearanceControlMenu("Appearance");
194         app.addMouseListener(new SubMenuShower(app));
195         this.appGroup = new ButtonGroup();
196         for (Appearance appearanceValue : Appearance.values())
197         {
198             this.appGroup.add(addAppearance(app, appearanceValue));
199         }
200         JMenu scale = new AppearanceControlMenu("Font size");
201         scale.addMouseListener(new SubMenuShower(scale));
202         this.scaleGroup = new ButtonGroup();
203         for (String fontScaleName : FONT_SCALES.keySet())
204         {
205             this.scaleGroup.add(addFontsize(scale, fontScaleName));
206         }
207 
208         /** PopupMenu class to only accept the font of an Appearance */
209         class AppearanceControlPopupMenu extends JPopupMenu implements AppearanceControl
210         {
211             /** */
212             private static final long serialVersionUID = 20180206L;
213 
214             /** {@inheritDoc} */
215             @Override
216             public boolean isFont()
217             {
218                 return true;
219             }
220 
221             /** {@inheritDoc} */
222             @Override
223             public String toString()
224             {
225                 return "AppearanceControlPopupMenu []";
226             }
227         }
228 
229         // Popup menu to change appearance
230         this.popMenu = new AppearanceControlPopupMenu();
231         this.popMenu.add(app);
232         this.popMenu.add(scale);
233         ((JPanel) getContentPane()).setComponentPopupMenu(this.popMenu);
234     }
235 
236     /**
237      * Set font scale.
238      * @param fontScaleName String; font scale name.
239      */
240     public void setFontScale(final String fontScaleName)
241     {
242         this.fontScaleName = fontScaleName;
243         setAppearance(getAppearance());
244     }
245 
246     /**
247      * Sets an appearance.
248      * @param appearance Appearance; appearance
249      */
250     public void setAppearance(final Appearance appearance)
251     {
252         this.appearance = appearance;
253         setAppearance(this.popMenu, appearance);
254         for (Enumeration<AbstractButton> c = this.appGroup.getElements(); c.hasMoreElements();)
255         {
256             setAppearance(c.nextElement(), appearance);
257         }
258         for (Enumeration<AbstractButton> c = this.scaleGroup.getElements(); c.hasMoreElements();)
259         {
260             setAppearance(c.nextElement(), appearance);
261         }
262         setAppearance(getContentPane(), appearance);
263         this.frameProperties.setProperty("Appearance", appearance.toString());
264         this.frameProperties.setProperty("FontScale", this.fontScaleName);
265     }
266 
267     /**
268      * Sets an appearance recursively on components.
269      * @param c Component; visual component
270      * @param appear Appearance; look and feel
271      */
272     private void setAppearance(final Component c, final Appearance appear)
273     {
274         if (c instanceof AppearanceControl)
275         {
276             AppearanceControl ac = (AppearanceControl) c;
277             if (ac.isBackground())
278             {
279                 c.setBackground(appear.getBackground());
280             }
281             if (ac.isForeground())
282             {
283                 c.setForeground(appear.getForeground());
284             }
285             if (ac.isFont())
286             {
287                 changeFont(c, appear.getFont());
288             }
289             if (ac.getFontSize() != null)
290             {
291                 changeFontSize(c);
292             }
293         }
294         else if (VisualizationPanel.class.isAssignableFrom(c.getClass()))
295         {
296             // animation backdrop
297             c.setBackground(appear.getBackdrop()); // not background
298             c.setForeground(appear.getForeground());
299             changeFont(c, appear.getFont());
300             changeFontSize(c);
301         }
302         else
303         {
304             // default
305             c.setBackground(appear.getBackground());
306             c.setForeground(appear.getForeground());
307             changeFont(c, appear.getFont());
308             changeFontSize(c);
309         }
310         if (c instanceof JSlider)
311         {
312             // labels of the slider
313             Dictionary<?, ?> dictionary = ((JSlider) c).getLabelTable();
314             Enumeration<?> keys = dictionary.keys();
315             while (keys.hasMoreElements())
316             {
317                 JLabel label = (JLabel) dictionary.get(keys.nextElement());
318                 label.setForeground(appear.getForeground());
319                 label.setBackground(appear.getBackground());
320             }
321         }
322         // children
323         if (c instanceof JComponent)
324         {
325             for (Component child : ((JComponent) c).getComponents())
326             {
327                 setAppearance(child, appear);
328             }
329         }
330     }
331 
332     /**
333      * Change font on component.
334      * @param c Component; component
335      * @param font String; font name
336      */
337     protected void changeFont(final Component c, final String font)
338     {
339         Font prev = c.getFont();
340         c.setFont(new Font(font, prev.getStyle(), prev.getSize()));
341     }
342     
343     /**
344      * Changes the font size of the component.
345      * @param c Component; component.
346      */
347     protected void changeFontSize(final Component c)
348     {
349         Font prev = c.getFont();
350         int size;
351         if (c instanceof AppearanceControl)
352         {
353             AppearanceControl a = (AppearanceControl) c;
354             if (a.getFontSize() != null)
355             {
356                 size = (int) (a.getFontSize() * FONT_SCALES.get(this.fontScaleName));
357             }
358             else
359             {
360                 size = prev.getSize();
361             }
362         }
363         else
364         {
365             size = (int) (AppearanceControl.DEFAULT_FONT_SIZE * FONT_SCALES.get(this.fontScaleName));
366         }
367         c.setFont(new Font(prev.getFontName(), prev.getStyle(), size));
368     }
369 
370     /**
371      * Returns the appearance.
372      * @return Appearance; appearance
373      */
374     public Appearance getAppearance()
375     {
376         return this.appearance;
377     }
378 
379     /**
380      * Adds an appearance to the menu.
381      * @param group JMenu; menu to add item to
382      * @param appear Appearance; appearance this item selects
383      * @return JMenuItem; menu item
384      */
385     private JMenuItem addAppearance(final JMenu group, final Appearance appear)
386     {
387         JCheckBoxMenuItem check = new StayOpenCheckBoxMenuItem(appear.getName(), appear.equals(getAppearance()));
388         check.addMouseListener(new MouseAdapter()
389         {
390             /** {@inheritDoc} */
391             @Override
392             public void mouseClicked(final MouseEvent e)
393             {
394                 setAppearance(appear);
395             }
396         });
397         return group.add(check);
398     }
399 
400     /**
401      * Adds an appearance to the menu.
402      * @param group JMenu; menu to add item to
403      * @param fontScaleName String; font scale name
404      * @return JMenuItem; menu item
405      */
406     private JMenuItem addFontsize(final JMenu group, final String fontScaleName)
407     {
408         JCheckBoxMenuItem check = new StayOpenCheckBoxMenuItem(fontScaleName, this.fontScaleName.equals(fontScaleName));
409         check.addMouseListener(new MouseAdapter()
410         {
411             /** {@inheritDoc} */
412             @Override
413             public void mouseClicked(final MouseEvent e)
414             {
415                 setFontScale(fontScaleName);
416             }
417         });
418         return group.add(check);
419     }
420 
421     /**
422      * Sets default font in the UIManager. This should be invoked by any application before any GUI element is created.
423      */
424     public static void setDefaultFont()
425     {
426         UIManager.put("Label.font", FONT);
427         UIManager.put("Menu.font", FONT);
428         UIManager.put("MenuItem.font", FONT);
429         UIManager.put("TabbedPane.font", FONT);
430         UIManager.put("Table.font", FONT);
431         UIManager.put("TableHeader.font", FONT);
432         UIManager.put("TextField.font", FONT);
433         UIManager.put("Button.font", FONT);
434         UIManager.put("ComboBox.font", FONT);
435         UIManager.put("CheckBox.font", FONT);
436         UIManager.put("CheckBoxMenuItem.font", FONT);
437         // for full list: https://stackoverflow.com/questions/7434845/setting-the-default-font-of-swing-program
438     }
439 
440     /**
441      * Mouse listener which shows the submenu when the mouse enters the button.
442      * <p>
443      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
444      * <br>
445      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
446      * </p>
447      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
448      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
449      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
450      */
451     private class SubMenuShower extends MouseAdapter
452     {
453         /** The menu. */
454         private JMenu menu;
455 
456         /**
457          * Constructor.
458          * @param menu JMenu; menu
459          */
460         SubMenuShower(final JMenu menu)
461         {
462             this.menu = menu;
463         }
464 
465         /** {@inheritDoc} */
466         @Override
467         public void mouseEntered(final MouseEvent e)
468         {
469             MenuSelectionManager.defaultManager().setSelectedPath(
470                     new MenuElement[] {(MenuElement) this.menu.getParent(), this.menu, this.menu.getPopupMenu()});
471         }
472 
473         /** {@inheritDoc} */
474         @Override
475         public String toString()
476         {
477             return "SubMenuShower [menu=" + this.menu + "]";
478         }
479     }
480 
481     /**
482      * Check box item that keeps the popup menu visible after clicking, so the user can click and try some options.
483      * <p>
484      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
485      * <br>
486      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
487      * </p>
488      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
489      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
490      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
491      */
492     private static class StayOpenCheckBoxMenuItem extends JCheckBoxMenuItem implements AppearanceControl
493     {
494         /** */
495         private static final long serialVersionUID = 20180206L;
496 
497         /** Stored selection path. */
498         private static MenuElement[] PATH;
499         {
500             getModel().addChangeListener(new ChangeListener()
501             {
502                 /** {@inheritDoc} */
503                 @Override
504                 public void stateChanged(final ChangeEvent e)
505                 {
506                     if (getModel().isArmed() && isShowing())
507                     {
508                         setPath(MenuSelectionManager.defaultManager().getSelectedPath());
509                     }
510                 }
511             });
512         }
513 
514         /**
515          * Sets the path.
516          * @param path MenuElement[]; path
517          */
518         public static void setPath(final MenuElement[] path)
519         {
520             StayOpenCheckBoxMenuItem.PATH = path;
521         }
522 
523         /**
524          * Constructor.
525          * @param text String; menu item text
526          * @param selected boolean; if the item is selected
527          */
528         StayOpenCheckBoxMenuItem(final String text, final boolean selected)
529         {
530             super(text, selected);
531         }
532 
533         /** {@inheritDoc} */
534         @Override
535         public void doClick(final int pressTime)
536         {
537             super.doClick(pressTime);
538             for (MenuElement element : PATH)
539             {
540                 if (element instanceof JComponent)
541                 {
542                     ((JComponent) element).setVisible(true);
543                 }
544             }
545             JMenu menu = (JMenu) PATH[PATH.length - 3];
546             MenuSelectionManager.defaultManager()
547                     .setSelectedPath(new MenuElement[] {(MenuElement) menu.getParent(), menu, menu.getPopupMenu()});
548         }
549 
550         /** {@inheritDoc} */
551         @Override
552         public boolean isFont()
553         {
554             return true;
555         }
556 
557         /** {@inheritDoc} */
558         @Override
559         public String toString()
560         {
561             return "StayOpenCheckBoxMenuItem []";
562         }
563     }
564 
565 }