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 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
171              */
172             AppearanceControlMenu(final String string)
173             {
174                 super(string);
175             }
176 
177             @Override
178             public boolean isFont()
179             {
180                 return true;
181             }
182 
183             @Override
184             public String toString()
185             {
186                 return "AppearanceControlMenu []";
187             }
188         }
189 
190         // Appearance menu
191         JMenu app = new AppearanceControlMenu("Appearance");
192         app.addMouseListener(new SubMenuShower(app));
193         this.appGroup = new ButtonGroup();
194         for (Appearance appearanceValue : Appearance.values())
195         {
196             this.appGroup.add(addAppearance(app, appearanceValue));
197         }
198         JMenu scale = new AppearanceControlMenu("Font size");
199         scale.addMouseListener(new SubMenuShower(scale));
200         this.scaleGroup = new ButtonGroup();
201         for (String fontScaleName : FONT_SCALES.keySet())
202         {
203             this.scaleGroup.add(addFontsize(scale, fontScaleName));
204         }
205 
206         /** PopupMenu class to only accept the font of an Appearance */
207         class AppearanceControlPopupMenu extends JPopupMenu implements AppearanceControl
208         {
209             /** */
210             private static final long serialVersionUID = 20180206L;
211 
212             @Override
213             public boolean isFont()
214             {
215                 return true;
216             }
217 
218             @Override
219             public String toString()
220             {
221                 return "AppearanceControlPopupMenu []";
222             }
223         }
224 
225         // Popup menu to change appearance
226         this.popMenu = new AppearanceControlPopupMenu();
227         this.popMenu.add(app);
228         this.popMenu.add(scale);
229         ((JPanel) getContentPane()).setComponentPopupMenu(this.popMenu);
230     }
231 
232     /**
233      * Set font scale.
234      * @param fontScaleName font scale name.
235      */
236     public void setFontScale(final String fontScaleName)
237     {
238         this.fontScaleName = fontScaleName;
239         setAppearance(getAppearance());
240     }
241 
242     /**
243      * Sets an appearance.
244      * @param appearance appearance
245      */
246     public void setAppearance(final Appearance appearance)
247     {
248         this.appearance = appearance;
249         setAppearance(this.popMenu, appearance);
250         for (Enumeration<AbstractButton> c = this.appGroup.getElements(); c.hasMoreElements();)
251         {
252             setAppearance(c.nextElement(), appearance);
253         }
254         for (Enumeration<AbstractButton> c = this.scaleGroup.getElements(); c.hasMoreElements();)
255         {
256             setAppearance(c.nextElement(), appearance);
257         }
258         setAppearance(getContentPane(), appearance);
259         this.frameProperties.setProperty("Appearance", appearance.toString());
260         this.frameProperties.setProperty("FontScale", this.fontScaleName);
261     }
262 
263     /**
264      * Sets an appearance recursively on components.
265      * @param c visual component
266      * @param appear look and feel
267      */
268     private void setAppearance(final Component c, final Appearance appear)
269     {
270         if (c instanceof AppearanceControl)
271         {
272             AppearanceControl ac = (AppearanceControl) c;
273             if (ac.isBackground())
274             {
275                 c.setBackground(appear.getBackground());
276             }
277             if (ac.isForeground())
278             {
279                 c.setForeground(appear.getForeground());
280             }
281             if (ac.isFont())
282             {
283                 changeFont(c, appear.getFont());
284             }
285             if (ac.getFontSize() != null)
286             {
287                 changeFontSize(c);
288             }
289         }
290         else if (VisualizationPanel.class.isAssignableFrom(c.getClass()))
291         {
292             // animation backdrop
293             c.setBackground(appear.getBackdrop()); // not background
294             c.setForeground(appear.getForeground());
295             changeFont(c, appear.getFont());
296             changeFontSize(c);
297         }
298         else
299         {
300             // default
301             c.setBackground(appear.getBackground());
302             c.setForeground(appear.getForeground());
303             changeFont(c, appear.getFont());
304             changeFontSize(c);
305         }
306         if (c instanceof JSlider)
307         {
308             // labels of the slider
309             Dictionary<?, ?> dictionary = ((JSlider) c).getLabelTable();
310             Enumeration<?> keys = dictionary.keys();
311             while (keys.hasMoreElements())
312             {
313                 JLabel label = (JLabel) dictionary.get(keys.nextElement());
314                 label.setForeground(appear.getForeground());
315                 label.setBackground(appear.getBackground());
316             }
317         }
318         // children
319         if (c instanceof JComponent)
320         {
321             for (Component child : ((JComponent) c).getComponents())
322             {
323                 setAppearance(child, appear);
324             }
325         }
326     }
327 
328     /**
329      * Change font on component.
330      * @param c component
331      * @param font font name
332      */
333     protected void changeFont(final Component c, final String font)
334     {
335         Font prev = c.getFont();
336         c.setFont(new Font(font, prev.getStyle(), prev.getSize()));
337     }
338 
339     /**
340      * Changes the font size of the component.
341      * @param c component.
342      */
343     protected void changeFontSize(final Component c)
344     {
345         Font prev = c.getFont();
346         int size;
347         if (c instanceof AppearanceControl)
348         {
349             AppearanceControl a = (AppearanceControl) c;
350             if (a.getFontSize() != null)
351             {
352                 size = (int) (a.getFontSize() * FONT_SCALES.get(this.fontScaleName));
353             }
354             else
355             {
356                 size = prev.getSize();
357             }
358         }
359         else
360         {
361             size = (int) (AppearanceControl.DEFAULT_FONT_SIZE * FONT_SCALES.get(this.fontScaleName));
362         }
363         c.setFont(new Font(prev.getFontName(), prev.getStyle(), size));
364     }
365 
366     /**
367      * Returns the appearance.
368      * @return appearance
369      */
370     public Appearance getAppearance()
371     {
372         return this.appearance;
373     }
374 
375     /**
376      * Adds an appearance to the menu.
377      * @param group menu to add item to
378      * @param appear appearance this item selects
379      * @return menu item
380      */
381     private JMenuItem addAppearance(final JMenu group, final Appearance appear)
382     {
383         JCheckBoxMenuItem check = new StayOpenCheckBoxMenuItem(appear.getName(), appear.equals(getAppearance()));
384         check.addMouseListener(new MouseAdapter()
385         {
386             @Override
387             public void mouseClicked(final MouseEvent e)
388             {
389                 setAppearance(appear);
390             }
391         });
392         return group.add(check);
393     }
394 
395     /**
396      * Adds an appearance to the menu.
397      * @param group menu to add item to
398      * @param fontScaleName font scale name
399      * @return menu item
400      */
401     private JMenuItem addFontsize(final JMenu group, final String fontScaleName)
402     {
403         JCheckBoxMenuItem check = new StayOpenCheckBoxMenuItem(fontScaleName, this.fontScaleName.equals(fontScaleName));
404         check.addMouseListener(new MouseAdapter()
405         {
406             @Override
407             public void mouseClicked(final MouseEvent e)
408             {
409                 setFontScale(fontScaleName);
410             }
411         });
412         return group.add(check);
413     }
414 
415     /**
416      * Sets default font in the UIManager. This should be invoked by any application before any GUI element is created.
417      */
418     public static void setDefaultFont()
419     {
420         UIManager.put("Label.font", FONT);
421         UIManager.put("Menu.font", FONT);
422         UIManager.put("MenuItem.font", FONT);
423         UIManager.put("TabbedPane.font", FONT);
424         UIManager.put("Table.font", FONT);
425         UIManager.put("TableHeader.font", FONT);
426         UIManager.put("TextField.font", FONT);
427         UIManager.put("Button.font", FONT);
428         UIManager.put("ComboBox.font", FONT);
429         UIManager.put("CheckBox.font", FONT);
430         UIManager.put("CheckBoxMenuItem.font", FONT);
431         // for full list: https://stackoverflow.com/questions/7434845/setting-the-default-font-of-swing-program
432     }
433 
434     /**
435      * Mouse listener which shows the submenu when the mouse enters the button.
436      * <p>
437      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
438      * <br>
439      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
440      * </p>
441      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
442      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
443      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
444      */
445     private class SubMenuShower extends MouseAdapter
446     {
447         /** The menu. */
448         private JMenu menu;
449 
450         /**
451          * Constructor.
452          * @param menu menu
453          */
454         SubMenuShower(final JMenu menu)
455         {
456             this.menu = menu;
457         }
458 
459         @Override
460         public void mouseEntered(final MouseEvent e)
461         {
462             MenuSelectionManager.defaultManager().setSelectedPath(
463                     new MenuElement[] {(MenuElement) this.menu.getParent(), this.menu, this.menu.getPopupMenu()});
464         }
465 
466         @Override
467         public String toString()
468         {
469             return "SubMenuShower [menu=" + this.menu + "]";
470         }
471     }
472 
473     /**
474      * Check box item that keeps the popup menu visible after clicking, so the user can click and try some options.
475      * <p>
476      * Copyright (c) 2013-2024 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
477      * <br>
478      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
479      * </p>
480      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
481      * @author <a href="https://github.com/peter-knoppers">Peter Knoppers</a>
482      * @author <a href="https://github.com/wjschakel">Wouter Schakel</a>
483      */
484     private static class StayOpenCheckBoxMenuItem extends JCheckBoxMenuItem implements AppearanceControl
485     {
486         /** */
487         private static final long serialVersionUID = 20180206L;
488 
489         /** Stored selection path. */
490         private static MenuElement[] PATH;
491         {
492             getModel().addChangeListener(new ChangeListener()
493             {
494                 @Override
495                 public void stateChanged(final ChangeEvent e)
496                 {
497                     if (getModel().isArmed() && isShowing())
498                     {
499                         setPath(MenuSelectionManager.defaultManager().getSelectedPath());
500                     }
501                 }
502             });
503         }
504 
505         /**
506          * Sets the path.
507          * @param path path
508          */
509         public static void setPath(final MenuElement[] path)
510         {
511             StayOpenCheckBoxMenuItem.PATH = path;
512         }
513 
514         /**
515          * Constructor.
516          * @param text menu item text
517          * @param selected if the item is selected
518          */
519         StayOpenCheckBoxMenuItem(final String text, final boolean selected)
520         {
521             super(text, selected);
522         }
523 
524         @Override
525         public void doClick(final int pressTime)
526         {
527             super.doClick(pressTime);
528             for (MenuElement element : PATH)
529             {
530                 if (element instanceof JComponent)
531                 {
532                     ((JComponent) element).setVisible(true);
533                 }
534             }
535             JMenu menu = (JMenu) PATH[PATH.length - 3];
536             MenuSelectionManager.defaultManager()
537                     .setSelectedPath(new MenuElement[] {(MenuElement) menu.getParent(), menu, menu.getPopupMenu()});
538         }
539 
540         @Override
541         public boolean isFont()
542         {
543             return true;
544         }
545 
546         @Override
547         public String toString()
548         {
549             return "StayOpenCheckBoxMenuItem []";
550         }
551     }
552 
553 }