package org.opentrafficsim.swing.gui;
import java.awt.Component;
import java.awt.Font;
import java.awt.Frame;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Dictionary;
import java.util.Enumeration;
import java.util.Properties;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBoxMenuItem;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JSlider;
import javax.swing.MenuElement;
import javax.swing.MenuSelectionManager;
import javax.swing.WindowConstants;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.opentrafficsim.core.animation.gtu.colorer.DefaultSwitchableGTUColorer;
import org.opentrafficsim.core.animation.gtu.colorer.GTUColorer;
import org.opentrafficsim.core.dsol.OTSModelInterface;
import nl.tudelft.simulation.dsol.swing.animation.D2.AnimationPanel;
/**
* Wrap a DSOL simulation model, or any (descendant of a) JPanel in a JFrame (wrap it in a window). The window will be
* maximized.
*
* Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
* BSD-style license. See OpenTrafficSim License.
*
* $LastChangedDate: 2018-09-19 13:55:45 +0200 (Wed, 19 Sep 2018) $, @version $Revision: 4006 $, by $Author: averbraeck $,
* initial version 16 dec. 2014
* @author Alexander Verbraeck
* @author Peter Knoppers
* @param model type
*/
public class OTSSwingApplication extends JFrame
{
/** */
private static final long serialVersionUID = 20141216L;
/** Single instance of default colorer, reachable from various places. */
public static final GTUColorer DEFAULT_COLORER = new DefaultSwitchableGTUColorer();
/** the model. */
private final T model;
/** whether the application has been closed or not. */
@SuppressWarnings("checkstyle:visibilitymodifier")
protected boolean closed = false;
/** Properties for the frame appearance (not simulation related). */
protected Properties frameProperties;
/** Current appearance. */
private Appearance appearance = Appearance.GRAY;
/**
* Wrap an OTSModel in a JFrame. Uses a default GTU colorer.
* @param model T; the model that will be shown in the JFrame
* @param panel JPanel; this should be the JPanel of the simulation
*/
public OTSSwingApplication(final T model, final JPanel panel)
{
this.model = model;
setTitle("OTS | The Open Traffic Simulator | " + model.getDescription());
setContentPane(panel);
pack();
setExtendedState(Frame.MAXIMIZED_BOTH);
setVisible(true);
setExitOnClose(true);
addWindowListener(new WindowAdapter()
{
@Override
public void windowClosing(final WindowEvent windowEvent)
{
OTSSwingApplication.this.closed = true;
super.windowClosing(windowEvent);
}
});
//////////////////////
///// Appearance /////
//////////////////////
// Listener to write frame properties on frame close
String sep = System.getProperty("file.separator");
String propertiesFile = System.getProperty("user.home") + sep + "OTS" + sep + "properties.ini";
addWindowListener(new WindowAdapter()
{
/** {@inheritDoce} */
@Override
public void windowClosing(final WindowEvent windowEvent)
{
try
{
File f = new File(propertiesFile);
f.getParentFile().mkdirs();
FileWriter writer = new FileWriter(f);
OTSSwingApplication.this.frameProperties.store(writer, "OTS user settings");
}
catch (IOException exception)
{
System.err.println("Could not store properties at " + propertiesFile + ".");
}
}
});
// Set default frame properties and load properties from file (if any)
Properties defaults = new Properties();
defaults.setProperty("Appearance", "GRAY");
this.frameProperties = new Properties(defaults);
try
{
FileReader reader = new FileReader(propertiesFile);
this.frameProperties.load(reader);
}
catch (IOException ioe)
{
// ok, use defaults
}
this.appearance = Appearance.valueOf(this.frameProperties.getProperty("Appearance").toUpperCase());
/** Menu class to only accept the font of an Appearance */
class AppearanceControlMenu extends JMenu implements AppearanceControl
{
/** */
private static final long serialVersionUID = 20180206L;
/**
* Constructor.
* @param string String; string
*/
AppearanceControlMenu(final String string)
{
super(string);
}
/** {@inheritDoc} */
@Override
public boolean isFont()
{
return true;
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "AppearanceControlMenu []";
}
}
// Appearance menu
JMenu app = new AppearanceControlMenu("Appearance");
app.addMouseListener(new SubMenuShower(app));
ButtonGroup appGroup = new ButtonGroup();
for (Appearance appearanceValue : Appearance.values())
{
appGroup.add(addAppearance(app, appearanceValue));
}
/** PopupMenu class to only accept the font of an Appearance */
class AppearanceControlPopupMenu extends JPopupMenu implements AppearanceControl
{
/** */
private static final long serialVersionUID = 20180206L;
/** {@inheritDoc} */
@Override
public boolean isFont()
{
return true;
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "AppearanceControlPopupMenu []";
}
}
// Popup menu to change appearance
JPopupMenu popMenu = new AppearanceControlPopupMenu();
popMenu.add(app);
panel.setComponentPopupMenu(popMenu);
// Set the Appearance as by frame properties
setAppearance(getAppearance()); // color elements that were just added
}
/**
* Sets an appearance.
* @param appearance Appearance; appearance
*/
public void setAppearance(final Appearance appearance)
{
this.appearance = appearance;
setAppearance(this.getContentPane(), appearance);
this.frameProperties.setProperty("Appearance", appearance.toString());
}
/**
* Sets an appearance recursively on components.
* @param c Component; visual component
* @param appear Appearance; look and feel
*/
private void setAppearance(final Component c, final Appearance appear)
{
if (c instanceof AppearanceControl)
{
AppearanceControl ac = (AppearanceControl) c;
if (ac.isBackground())
{
c.setBackground(appear.getBackground());
}
if (ac.isForeground())
{
c.setForeground(appear.getForeground());
}
if (ac.isFont())
{
changeFont(c, appear.getFont());
}
}
else if (c instanceof AnimationPanel)
{
// animation backdrop
c.setBackground(appear.getBackdrop()); // not background
c.setForeground(appear.getForeground());
changeFont(c, appear.getFont());
}
else
{
// default
c.setBackground(appear.getBackground());
c.setForeground(appear.getForeground());
changeFont(c, appear.getFont());
}
if (c instanceof JSlider)
{
// labels of the slider
Dictionary, ?> dictionary = ((JSlider) c).getLabelTable();
Enumeration> keys = dictionary.keys();
while (keys.hasMoreElements())
{
JLabel label = (JLabel) dictionary.get(keys.nextElement());
label.setForeground(appear.getForeground());
label.setBackground(appear.getBackground());
}
}
// children
if (c instanceof JComponent)
{
for (Component child : ((JComponent) c).getComponents())
{
setAppearance(child, appear);
}
}
}
/**
* Change font on component.
* @param c Component; component
* @param font String; font name
*/
private void changeFont(final Component c, final String font)
{
Font prev = c.getFont();
c.setFont(new Font(font, prev.getStyle(), prev.getSize()));
}
/**
* Returns the appearance.
* @return Appearance; appearance
*/
public Appearance getAppearance()
{
return this.appearance;
}
/**
* Adds an appearance to the menu.
* @param group JMenu; menu to add item to
* @param appear Appearance; appearance this item selects
* @return JMenuItem; menu item
*/
private JMenuItem addAppearance(final JMenu group, final Appearance appear)
{
JCheckBoxMenuItem check = new StayOpenCheckBoxMenuItem(appear.getName(), appear.equals(getAppearance()));
check.addMouseListener(new MouseAdapter()
{
/** {@inheritDoc} */
@Override
public void mouseClicked(final MouseEvent e)
{
setAppearance(appear);
}
});
return group.add(check);
}
/**
* Return the initial 'home' extent for the animation. The 'Home' button returns to this extent. Override this method when a
* smaller or larger part of the infra should be shown. In the default setting, all currently visible objects are shown.
* @return the initial and 'home' rectangle for the animation.
*/
@SuppressWarnings("checkstyle:designforextension")
protected Rectangle2D makeAnimationRectangle()
{
return this.model.getNetwork().getExtent();
}
/**
* @param exitOnClose boolean; set exitOnClose
*/
public final void setExitOnClose(final boolean exitOnClose)
{
if (exitOnClose)
{
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
}
else
{
setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);
}
}
/**
* @return closed
*/
public final boolean isClosed()
{
return this.closed;
}
/**
* @return model
*/
public final T getModel()
{
return this.model;
}
/**
* Mouse listener which shows the submenu when the mouse enters the button.
*
* Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
*
* BSD-style license. See OpenTrafficSim License.
*
* @version $Revision$, $LastChangedDate$, by $Author$, initial version 6 feb. 2018
* @author Alexander Verbraeck
* @author Peter Knoppers
* @author Wouter Schakel
*/
private class SubMenuShower extends MouseAdapter
{
/** The menu. */
private JMenu menu;
/**
* Constructor.
* @param menu JMenu; menu
*/
SubMenuShower(final JMenu menu)
{
this.menu = menu;
}
/** {@inheritDoc} */
@Override
public void mouseEntered(final MouseEvent e)
{
MenuSelectionManager.defaultManager().setSelectedPath(
new MenuElement[] {(MenuElement) this.menu.getParent(), this.menu, this.menu.getPopupMenu()});
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "SubMenuShower [menu=" + this.menu + "]";
}
}
/**
* Check box item that keeps the popup menu visible after clicking, so the user can click and try some options.
*
* Copyright (c) 2013-2022 Delft University of Technology, PO Box 5, 2600 AA, Delft, the Netherlands. All rights reserved.
*
* BSD-style license. See OpenTrafficSim License.
*
* @version $Revision$, $LastChangedDate$, by $Author$, initial version 6 feb. 2018
* @author Alexander Verbraeck
* @author Peter Knoppers
* @author Wouter Schakel
*/
private static class StayOpenCheckBoxMenuItem extends JCheckBoxMenuItem implements AppearanceControl
{
/** */
private static final long serialVersionUID = 20180206L;
/** Stored selection path. */
private static MenuElement[] path;
{
getModel().addChangeListener(new ChangeListener()
{
@Override
public void stateChanged(final ChangeEvent e)
{
if (getModel().isArmed() && isShowing())
{
setPath(MenuSelectionManager.defaultManager().getSelectedPath());
}
}
});
}
/**
* Sets the path.
* @param path MenuElement[]; path
*/
public static void setPath(final MenuElement[] path)
{
StayOpenCheckBoxMenuItem.path = path;
}
/**
* Constructor.
* @param text String; menu item text
* @param selected boolean; if the item is selected
*/
StayOpenCheckBoxMenuItem(final String text, final boolean selected)
{
super(text, selected);
}
/** {@inheritDoc} */
@Override
public void doClick(final int pressTime)
{
super.doClick(pressTime);
for (MenuElement element : path)
{
if (element instanceof JComponent)
{
((JComponent) element).setVisible(true);
}
}
JMenu menu = (JMenu) path[path.length - 3];
MenuSelectionManager.defaultManager()
.setSelectedPath(new MenuElement[] {(MenuElement) menu.getParent(), menu, menu.getPopupMenu()});
}
/** {@inheritDoc} */
@Override
public boolean isFont()
{
return true;
}
/** {@inheritDoc} */
@Override
public String toString()
{
return "StayOpenCheckBoxMenuItem []";
}
}
}