View Javadoc
1   package org.opentrafficsim.swing.gui;
2   
3   import java.awt.Component;
4   import java.awt.Font;
5   import java.awt.Frame;
6   import java.awt.event.MouseAdapter;
7   import java.awt.event.MouseEvent;
8   import java.awt.event.WindowAdapter;
9   import java.awt.event.WindowEvent;
10  import java.awt.geom.Rectangle2D;
11  import java.io.File;
12  import java.io.FileReader;
13  import java.io.FileWriter;
14  import java.io.IOException;
15  import java.util.Dictionary;
16  import java.util.Enumeration;
17  import java.util.Properties;
18  
19  import javax.imageio.ImageIO;
20  import javax.swing.ButtonGroup;
21  import javax.swing.JCheckBoxMenuItem;
22  import javax.swing.JComponent;
23  import javax.swing.JFrame;
24  import javax.swing.JLabel;
25  import javax.swing.JMenu;
26  import javax.swing.JMenuItem;
27  import javax.swing.JPanel;
28  import javax.swing.JPopupMenu;
29  import javax.swing.JSlider;
30  import javax.swing.MenuElement;
31  import javax.swing.MenuSelectionManager;
32  import javax.swing.WindowConstants;
33  import javax.swing.event.ChangeEvent;
34  import javax.swing.event.ChangeListener;
35  
36  import org.opentrafficsim.core.animation.gtu.colorer.DefaultSwitchableGtuColorer;
37  import org.opentrafficsim.core.animation.gtu.colorer.GtuColorer;
38  import org.opentrafficsim.core.dsol.OtsModelInterface;
39  
40  import nl.tudelft.simulation.dsol.swing.animation.D2.AnimationPanel;
41  
42  /**
43   * Wrap a DSOL simulation model, or any (descendant of a) JPanel in a JFrame (wrap it in a window). The window will be
44   * maximized.
45   * <p>
46   * Copyright (c) 2013-2023 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/averbraeck">Alexander Verbraeck</a>
50   * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
51   * @param <T> model type
52   */
53  public class OtsSwingApplication<T extends OtsModelInterface> extends JFrame
54  {
55      /** */
56      private static final long serialVersionUID = 20141216L;
57  
58      /** Single instance of default colorer, reachable from various places. */
59      public static final GtuColorer DEFAULT_COLORER = new DefaultSwitchableGtuColorer();
60  
61      /** the model. */
62      private final T model;
63  
64      /** whether the application has been closed or not. */
65      @SuppressWarnings("checkstyle:visibilitymodifier")
66      protected boolean closed = false;
67  
68      /** Properties for the frame appearance (not simulation related). */
69      protected Properties frameProperties;
70  
71      /** Current appearance. */
72      private Appearance appearance = Appearance.GRAY;
73  
74      /**
75       * Wrap an OtsModel in a JFrame. Uses a default GTU colorer.
76       * @param model T; the model that will be shown in the JFrame
77       * @param panel JPanel; this should be the JPanel of the simulation
78       */
79      public OtsSwingApplication(final T model, final JPanel panel)
80      {
81          this.model = model;
82          setTitle("OTS | The Open Traffic Simulator | " + model.getDescription());
83          setContentPane(panel);
84          pack();
85          setExtendedState(Frame.MAXIMIZED_BOTH);
86          setVisible(true);
87  
88          setExitOnClose(true);
89          addWindowListener(new WindowAdapter()
90          {
91              @Override
92              public void windowClosing(final WindowEvent windowEvent)
93              {
94                  OtsSwingApplication.this.closed = true;
95                  super.windowClosing(windowEvent);
96              }
97          });
98  
99          //////////////////////
100         ///// Appearance /////
101         //////////////////////
102         
103         try
104         {
105             setIconImage(ImageIO.read(Resource.getResourceAsStream("/OTS_merge.png")));
106         }
107         catch (IOException io)
108         {
109             // accept no icon set
110         }
111 
112         // Listener to write frame properties on frame close
113         String sep = System.getProperty("file.separator");
114         String propertiesFile = System.getProperty("user.home") + sep + "OTS" + sep + "properties.ini";
115         addWindowListener(new WindowAdapter()
116         {
117             /** {@inheritDoce} */
118             @Override
119             public void windowClosing(final WindowEvent windowEvent)
120             {
121                 try
122                 {
123                     File f = new File(propertiesFile);
124                     f.getParentFile().mkdirs();
125                     FileWriter writer = new FileWriter(f);
126                     OtsSwingApplication.this.frameProperties.store(writer, "OTS user settings");
127                 }
128                 catch (IOException exception)
129                 {
130                     System.err.println("Could not store properties at " + propertiesFile + ".");
131                 }
132             }
133         });
134 
135         // Set default frame properties and load properties from file (if any)
136         Properties defaults = new Properties();
137         defaults.setProperty("Appearance", "GRAY");
138         this.frameProperties = new Properties(defaults);
139         try
140         {
141             FileReader reader = new FileReader(propertiesFile);
142             this.frameProperties.load(reader);
143         }
144         catch (IOException ioe)
145         {
146             // ok, use defaults
147         }
148         this.appearance = Appearance.valueOf(this.frameProperties.getProperty("Appearance").toUpperCase());
149 
150         /** Menu class to only accept the font of an Appearance */
151         class AppearanceControlMenu extends JMenu implements AppearanceControl
152         {
153             /** */
154             private static final long serialVersionUID = 20180206L;
155 
156             /**
157              * Constructor.
158              * @param string String; string
159              */
160             AppearanceControlMenu(final String string)
161             {
162                 super(string);
163             }
164 
165             /** {@inheritDoc} */
166             @Override
167             public boolean isFont()
168             {
169                 return true;
170             }
171 
172             /** {@inheritDoc} */
173             @Override
174             public String toString()
175             {
176                 return "AppearanceControlMenu []";
177             }
178         }
179 
180         // Appearance menu
181         JMenu app = new AppearanceControlMenu("Appearance");
182         app.addMouseListener(new SubMenuShower(app));
183         ButtonGroup appGroup = new ButtonGroup();
184         for (Appearance appearanceValue : Appearance.values())
185         {
186             appGroup.add(addAppearance(app, appearanceValue));
187         }
188 
189         /** PopupMenu class to only accept the font of an Appearance */
190         class AppearanceControlPopupMenu extends JPopupMenu implements AppearanceControl
191         {
192             /** */
193             private static final long serialVersionUID = 20180206L;
194 
195             /** {@inheritDoc} */
196             @Override
197             public boolean isFont()
198             {
199                 return true;
200             }
201 
202             /** {@inheritDoc} */
203             @Override
204             public String toString()
205             {
206                 return "AppearanceControlPopupMenu []";
207             }
208         }
209 
210         // Popup menu to change appearance
211         JPopupMenu popMenu = new AppearanceControlPopupMenu();
212         popMenu.add(app);
213         panel.setComponentPopupMenu(popMenu);
214 
215         // Set the Appearance as by frame properties
216         setAppearance(getAppearance()); // color elements that were just added
217     }
218 
219     /**
220      * Sets an appearance.
221      * @param appearance Appearance; appearance
222      */
223     public void setAppearance(final Appearance appearance)
224     {
225         this.appearance = appearance;
226         setAppearance(this.getContentPane(), appearance);
227         this.frameProperties.setProperty("Appearance", appearance.toString());
228     }
229 
230     /**
231      * Sets an appearance recursively on components.
232      * @param c Component; visual component
233      * @param appear Appearance; look and feel
234      */
235     private void setAppearance(final Component c, final Appearance appear)
236     {
237         if (c instanceof AppearanceControl)
238         {
239             AppearanceControl ac = (AppearanceControl) c;
240             if (ac.isBackground())
241             {
242                 c.setBackground(appear.getBackground());
243             }
244             if (ac.isForeground())
245             {
246                 c.setForeground(appear.getForeground());
247             }
248             if (ac.isFont())
249             {
250                 changeFont(c, appear.getFont());
251             }
252         }
253         else if (c instanceof AnimationPanel)
254         {
255             // animation backdrop
256             c.setBackground(appear.getBackdrop()); // not background
257             c.setForeground(appear.getForeground());
258             changeFont(c, appear.getFont());
259         }
260         else
261         {
262             // default
263             c.setBackground(appear.getBackground());
264             c.setForeground(appear.getForeground());
265             changeFont(c, appear.getFont());
266         }
267         if (c instanceof JSlider)
268         {
269             // labels of the slider
270             Dictionary<?, ?> dictionary = ((JSlider) c).getLabelTable();
271             Enumeration<?> keys = dictionary.keys();
272             while (keys.hasMoreElements())
273             {
274                 JLabel label = (JLabel) dictionary.get(keys.nextElement());
275                 label.setForeground(appear.getForeground());
276                 label.setBackground(appear.getBackground());
277             }
278         }
279         // children
280         if (c instanceof JComponent)
281         {
282             for (Component child : ((JComponent) c).getComponents())
283             {
284                 setAppearance(child, appear);
285             }
286         }
287     }
288 
289     /**
290      * Change font on component.
291      * @param c Component; component
292      * @param font String; font name
293      */
294     private void changeFont(final Component c, final String font)
295     {
296         Font prev = c.getFont();
297         c.setFont(new Font(font, prev.getStyle(), prev.getSize()));
298     }
299 
300     /**
301      * Returns the appearance.
302      * @return Appearance; appearance
303      */
304     public Appearance getAppearance()
305     {
306         return this.appearance;
307     }
308 
309     /**
310      * Adds an appearance to the menu.
311      * @param group JMenu; menu to add item to
312      * @param appear Appearance; appearance this item selects
313      * @return JMenuItem; menu item
314      */
315     private JMenuItem addAppearance(final JMenu group, final Appearance appear)
316     {
317         JCheckBoxMenuItem check = new StayOpenCheckBoxMenuItem(appear.getName(), appear.equals(getAppearance()));
318         check.addMouseListener(new MouseAdapter()
319         {
320             /** {@inheritDoc} */
321             @Override
322             public void mouseClicked(final MouseEvent e)
323             {
324                 setAppearance(appear);
325             }
326         });
327         return group.add(check);
328     }
329 
330     /**
331      * Return the initial 'home' extent for the animation. The 'Home' button returns to this extent. Override this method when a
332      * smaller or larger part of the infra should be shown. In the default setting, all currently visible objects are shown.
333      * @return the initial and 'home' rectangle for the animation.
334      */
335     @SuppressWarnings("checkstyle:designforextension")
336     protected Rectangle2D makeAnimationRectangle()
337     {
338         return this.model.getNetwork().getExtent();
339     }
340 
341     /**
342      * @param exitOnClose boolean; set exitOnClose
343      */
344     public final void setExitOnClose(final boolean exitOnClose)
345     {
346         if (exitOnClose)
347         {
348             setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
349         }
350         else
351         {
352             setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
353         }
354     }
355 
356     /**
357      * @return closed
358      */
359     public final boolean isClosed()
360     {
361         return this.closed;
362     }
363 
364     /**
365      * @return model
366      */
367     public final T getModel()
368     {
369         return this.model;
370     }
371 
372     /**
373      * Mouse listener which shows the submenu when the mouse enters the button.
374      * <p>
375      * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
376      * <br>
377      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
378      * </p>
379      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
380      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
381      * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
382      */
383     private class SubMenuShower extends MouseAdapter
384     {
385         /** The menu. */
386         private JMenu menu;
387 
388         /**
389          * Constructor.
390          * @param menu JMenu; menu
391          */
392         SubMenuShower(final JMenu menu)
393         {
394             this.menu = menu;
395         }
396 
397         /** {@inheritDoc} */
398         @Override
399         public void mouseEntered(final MouseEvent e)
400         {
401             MenuSelectionManager.defaultManager().setSelectedPath(
402                     new MenuElement[] {(MenuElement) this.menu.getParent(), this.menu, this.menu.getPopupMenu()});
403         }
404 
405         /** {@inheritDoc} */
406         @Override
407         public String toString()
408         {
409             return "SubMenuShower [menu=" + this.menu + "]";
410         }
411     }
412 
413     /**
414      * Check box item that keeps the popup menu visible after clicking, so the user can click and try some options.
415      * <p>
416      * Copyright (c) 2013-2023 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
417      * <br>
418      * BSD-style license. See <a href="https://opentrafficsim.org/docs/license.html">OpenTrafficSim License</a>.
419      * </p>
420      * @author <a href="https://github.com/averbraeck">Alexander Verbraeck</a>
421      * @author <a href="https://tudelft.nl/staff/p.knoppers-1">Peter Knoppers</a>
422      * @author <a href="https://dittlab.tudelft.nl">Wouter Schakel</a>
423      */
424     private static class StayOpenCheckBoxMenuItem extends JCheckBoxMenuItem implements AppearanceControl
425     {
426         /** */
427         private static final long serialVersionUID = 20180206L;
428 
429         /** Stored selection path. */
430         private static MenuElement[] path;
431 
432         {
433             getModel().addChangeListener(new ChangeListener()
434             {
435 
436                 @Override
437                 public void stateChanged(final ChangeEvent e)
438                 {
439                     if (getModel().isArmed() && isShowing())
440                     {
441                         setPath(MenuSelectionManager.defaultManager().getSelectedPath());
442                     }
443                 }
444             });
445         }
446 
447         /**
448          * Sets the path.
449          * @param path MenuElement[]; path
450          */
451         public static void setPath(final MenuElement[] path)
452         {
453             StayOpenCheckBoxMenuItem.path = path;
454         }
455 
456         /**
457          * Constructor.
458          * @param text String; menu item text
459          * @param selected boolean; if the item is selected
460          */
461         StayOpenCheckBoxMenuItem(final String text, final boolean selected)
462         {
463             super(text, selected);
464         }
465 
466         /** {@inheritDoc} */
467         @Override
468         public void doClick(final int pressTime)
469         {
470             super.doClick(pressTime);
471             for (MenuElement element : path)
472             {
473                 if (element instanceof JComponent)
474                 {
475                     ((JComponent) element).setVisible(true);
476                 }
477             }
478             JMenu menu = (JMenu) path[path.length - 3];
479             MenuSelectionManager.defaultManager()
480                     .setSelectedPath(new MenuElement[] {(MenuElement) menu.getParent(), menu, menu.getPopupMenu()});
481         }
482 
483         /** {@inheritDoc} */
484         @Override
485         public boolean isFont()
486         {
487             return true;
488         }
489 
490         /** {@inheritDoc} */
491         @Override
492         public String toString()
493         {
494             return "StayOpenCheckBoxMenuItem []";
495         }
496     }
497 
498 }