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