package net.sf.openrocket.gui.util;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Image;
import java.awt.KeyboardFocusManager;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.swing.AbstractAction;
import javax.swing.AbstractButton;
import javax.swing.Action;
import javax.swing.BoundedRangeModel;
import javax.swing.ComboBoxModel;
import javax.swing.DefaultBoundedRangeModel;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JRootPane;
import javax.swing.JSlider;
import javax.swing.JSpinner;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.KeyStroke;
import javax.swing.ListSelectionModel;
import javax.swing.LookAndFeel;
import javax.swing.RootPaneContainer;
import javax.swing.SpinnerModel;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.TitledBorder;
import javax.swing.event.ChangeListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreeSelectionModel;
import net.sf.openrocket.gui.Resettable;
import net.sf.openrocket.logging.Markers;
import net.sf.openrocket.startup.Application;
import net.sf.openrocket.util.BugException;
import net.sf.openrocket.util.Invalidatable;
import net.sf.openrocket.util.MemoryManagement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class GUIUtil {
private static final Logger log = LoggerFactory.getLogger(GUIUtil.class);
private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
private static final String CLOSE_ACTION_KEY = "escape:WINDOW_CLOSING";
private static final List<Image> images = new ArrayList<Image>();
static {
loadImage("pix/icon/icon-256.png");
loadImage("pix/icon/icon-064.png");
loadImage("pix/icon/icon-048.png");
loadImage("pix/icon/icon-032.png");
loadImage("pix/icon/icon-016.png");
}
private static void loadImage(String file) {
InputStream is;
is = ClassLoader.getSystemResourceAsStream(file);
if (is == null)
return;
try {
Image image = ImageIO.read(is);
images.add(image);
} catch (IOException ignore) {
ignore.printStackTrace();
}
}
/**
* Return the DPI setting of the monitor. This is either the setting provided
* by the system or a user-specified DPI setting.
*
* @return the DPI setting to use.
*/
public static double getDPI() {
int dpi = Application.getPreferences().getInt("DPI", 0); // Tenths of a dpi
if (dpi < 10) {
dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10;
}
if (dpi < 10)
dpi = 960;
return (dpi) / 10.0;
}
/**
* Set suitable options for a single-use disposable dialog. This includes
* setting ESC to close the dialog, adding the appropriate window icons and
* setting the location based on the platform. If defaultButton is provided,
* it is set to the default button action.
* <p>
* The default button must be already attached to the dialog.
*
* @param dialog the dialog.
* @param defaultButton the default button of the dialog, or <code>null</code>.
*/
public static void setDisposableDialogOptions(JDialog dialog, JButton defaultButton) {
installEscapeCloseOperation(dialog);
setWindowIcons(dialog);
addModelNullingListener(dialog);
dialog.setLocationRelativeTo(dialog.getOwner());
dialog.setLocationByPlatform(true);
dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
dialog.pack();
if (defaultButton != null) {
setDefaultButton(defaultButton);
}
}
/**
* Add the correct action to close a JDialog when the ESC key is pressed.
* The dialog is closed by sending is a WINDOW_CLOSING event.
*
* @param dialog the dialog for which to install the action.
*/
public static void installEscapeCloseOperation(final JDialog dialog) {
Action dispatchClosing = new AbstractAction() {
@Override
public void actionPerformed(ActionEvent event) {
log.info(Markers.USER_MARKER, "Closing dialog " + dialog);
dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
}
};
JRootPane root = dialog.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY);
root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing);
}
/**
* Set the given button as the default button of the frame/dialog it is in. The button
* must be first attached to the window component hierarchy.
*
* @param button the button to set as the default button.
*/
public static void setDefaultButton(JButton button) {
Window w = SwingUtilities.windowForComponent(button);
if (w == null) {
throw new IllegalArgumentException("Attach button to a window first.");
}
if (!(w instanceof RootPaneContainer)) {
throw new IllegalArgumentException("Button not attached to RootPaneContainer, w=" + w);
}
((RootPaneContainer) w).getRootPane().setDefaultButton(button);
}
/**
* Change the behavior of a component so that TAB and Shift-TAB cycles the focus of
* the components. This is necessary for e.g. <code>JTextArea</code>.
*
* @param c the component to modify
*/
public static void setTabToFocusing(Component c) {
Set<KeyStroke> strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB")));
c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes);
strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB")));
c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes);
}
/**
* Set the OpenRocket icons to the window icons.
*
* @param window the window to set.
*/
public static void setWindowIcons(Window window) {
window.setIconImages(images);
}
/**
* Add a listener to the provided window that will call {@link #setNullModels(Component)}
* on the window once it is closed. This method may only be used on single-use
* windows and dialogs, that will never be shown again once closed!
*
* @param window the window to add the listener to.
*/
public static void addModelNullingListener(final Window window) {
window.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
log.debug("Clearing all models of window " + window);
setNullModels(window);
MemoryManagement.collectable(window);
}
});
}
/**
* Set the best available look-and-feel into use.
*/
public static void setBestLAF() {
/*
* Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used
* which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
* other alternatives.
*/
try {
// Set system L&F
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
// Check whether we have an ugly L&F
LookAndFeel laf = UIManager.getLookAndFeel();
if (laf == null ||
laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
// Search for better LAF
UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
String lafNames[] = {
".*[gG][tT][kK].*",
".*[wW][iI][nN].*",
".*[mM][aA][cC].*",
".*[aA][qQ][uU][aA].*",
".*[nN][iI][mM][bB].*"
};
lf: for (String lafName : lafNames) {
for (UIManager.LookAndFeelInfo l : info) {
if (l.getName().matches(lafName)) {
UIManager.setLookAndFeel(l.getClassName());
break lf;
}
}
}
}
} catch (Exception e) {
log.warn("Error setting LAF: " + e);
}
}
/**
* Changes the size of the font of the specified component by the given amount.
*
* @param component the component for which to change the font
* @param size the change in the font size
*/
public static void changeFontSize(JComponent component, float size) {
Font font = component.getFont();
font = font.deriveFont(font.getSize2D() + size);
component.setFont(font);
}
/**
* Automatically remember the size of a window. This stores the window size in the user
* preferences when resizing/maximizing the window and sets the state on the first call.
*/
public static void rememberWindowSize(final Window window) {
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
log.debug("Storing size of " + window.getClass().getName() + ": " + window.getSize());
((SwingPreferences) Application.getPreferences()).setWindowSize(window.getClass(), window.getSize());
if (window instanceof JFrame) {
if ((((JFrame) window).getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH) {
log.debug("Storing maximized state of " + window.getClass().getName());
((SwingPreferences) Application.getPreferences()).setWindowMaximized(window.getClass());
}
}
}
});
if (((SwingPreferences) Application.getPreferences()).isWindowMaximized(window.getClass())) {
if (window instanceof JFrame) {
((JFrame) window).setExtendedState(JFrame.MAXIMIZED_BOTH);
}
} else {
Dimension dim = ((SwingPreferences) Application.getPreferences()).getWindowSize(window.getClass());
if (dim != null) {
window.setSize(dim);
}
}
}
/**
* Automatically remember the position of a window. The position is stored in the user preferences
* every time the window is moved and set from there when first calling this method.
*/
public static void rememberWindowPosition(final Window window) {
window.addComponentListener(new ComponentAdapter() {
@Override
public void componentMoved(ComponentEvent e) {
((SwingPreferences) Application.getPreferences()).setWindowPosition(window.getClass(), window.getLocation());
}
});
// Set window position according to preferences, and set prefs when moving
Point position = ((SwingPreferences) Application.getPreferences()).getWindowPosition(window.getClass());
if (position != null) {
window.setLocationByPlatform(false);
window.setLocation(position);
}
}
public static void setAutomaticColumnTableWidths(JTable table, int max) {
int columns = table.getColumnCount();
int widths[] = new int[columns];
Arrays.fill(widths, 1);
for (int row = 0; row < table.getRowCount(); row++) {
for (int col = 0; col < columns; col++) {
Object value = table.getValueAt(row, col);
//System.out.println("row=" + row + " col=" + col + " : " + value);
widths[col] = Math.max(widths[col], value == null ? 0 : value.toString().length());
}
}
for (int col = 0; col < columns; col++) {
System.err.println("Setting column " + col + " to width " + widths[col]);
table.getColumnModel().getColumn(col).setPreferredWidth(Math.min(widths[col], max) * 100);
}
}
/**
* Changes the style of the font of the specified border.
*
* @param border the component for which to change the font
* @param style the change in the font style
*/
public static void changeFontStyle(TitledBorder border, int style) {
/*
* The fix of JRE bug #4129681 causes a TitledBorder occasionally to
* return a null font. We try to work around the issue by detecting it
* and reverting to the font of a JLabel instead.
*/
Font font = border.getTitleFont();
if (font == null) {
log.warn("JRE bug workaround : Border font is null, reverting to JLabel font");
font = new JLabel().getFont();
if (font == null) {
log.warn("JRE bug workaround : JLabel font is null, not modifying font");
return;
}
}
font = font.deriveFont(style);
if (font == null) {
throw new BugException("Derived font is null");
}
border.setTitleFont(font);
}
/**
* Changes the style of the font of the specified label.
*
* @param label the component for which to change the font
* @param style the change in the font style
*/
public static void changeFontStyle(JLabel label, int style) {
Font font = label.getFont();
font = font.deriveFont(style);
label.setFont(font);
}
/**
* Traverses recursively the component tree, and sets all applicable component
* models to null, so as to remove the listener connections. After calling this
* method the component hierarchy should no longed be used.
* <p>
* All components that use custom models should be added to this method, as
* there exists no standard way of removing the model from a component.
*
* @param c the component (<code>null</code> is ok)
*/
public static void setNullModels(Component c) {
if (c == null)
return;
// Remove various listeners
for (ComponentListener l : c.getComponentListeners()) {
c.removeComponentListener(l);
}
for (FocusListener l : c.getFocusListeners()) {
c.removeFocusListener(l);
}
for (MouseListener l : c.getMouseListeners()) {
c.removeMouseListener(l);
}
for (PropertyChangeListener l : c.getPropertyChangeListeners()) {
c.removePropertyChangeListener(l);
}
for (PropertyChangeListener l : c.getPropertyChangeListeners("model")) {
c.removePropertyChangeListener("model", l);
}
for (PropertyChangeListener l : c.getPropertyChangeListeners("action")) {
c.removePropertyChangeListener("action", l);
}
// Remove models for known components
// Why the FSCK must this be so hard?!?!?
if (c instanceof JSpinner) {
JSpinner spinner = (JSpinner) c;
for (ChangeListener l : spinner.getChangeListeners()) {
spinner.removeChangeListener(l);
}
SpinnerModel model = spinner.getModel();
spinner.setModel(new SpinnerNumberModel());
if (model instanceof Invalidatable) {
((Invalidatable) model).invalidate();
}
} else if (c instanceof JSlider) {
JSlider slider = (JSlider) c;
for (ChangeListener l : slider.getChangeListeners()) {
slider.removeChangeListener(l);
}
BoundedRangeModel model = slider.getModel();
slider.setModel(new DefaultBoundedRangeModel());
if (model instanceof Invalidatable) {
((Invalidatable) model).invalidate();
}
} else if (c instanceof JComboBox) {
JComboBox combo = (JComboBox) c;
for (ActionListener l : combo.getActionListeners()) {
combo.removeActionListener(l);
}
ComboBoxModel model = combo.getModel();
combo.setModel(new DefaultComboBoxModel());
if (model instanceof Invalidatable) {
((Invalidatable) model).invalidate();
}
} else if (c instanceof AbstractButton) {
AbstractButton button = (AbstractButton) c;
for (ActionListener l : button.getActionListeners()) {
button.removeActionListener(l);
}
Action model = button.getAction();
button.setAction(new AbstractAction() {
@Override
public void actionPerformed(ActionEvent e) {
}
});
if (model instanceof Invalidatable) {
((Invalidatable) model).invalidate();
}
} else if (c instanceof JTable) {
JTable table = (JTable) c;
TableModel model1 = table.getModel();
table.setModel(new DefaultTableModel());
if (model1 instanceof Invalidatable) {
((Invalidatable) model1).invalidate();
}
TableColumnModel model2 = table.getColumnModel();
table.setColumnModel(new DefaultTableColumnModel());
if (model2 instanceof Invalidatable) {
((Invalidatable) model2).invalidate();
}
ListSelectionModel model3 = table.getSelectionModel();
table.setSelectionModel(new DefaultListSelectionModel());
if (model3 instanceof Invalidatable) {
((Invalidatable) model3).invalidate();
}
} else if (c instanceof JTree) {
JTree tree = (JTree) c;
TreeModel model1 = tree.getModel();
tree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode()));
if (model1 instanceof Invalidatable) {
((Invalidatable) model1).invalidate();
}
TreeSelectionModel model2 = tree.getSelectionModel();
tree.setSelectionModel(new DefaultTreeSelectionModel());
if (model2 instanceof Invalidatable) {
((Invalidatable) model2).invalidate();
}
} else if (c instanceof Resettable) {
((Resettable) c).resetModel();
}
// Recurse the component
if (c instanceof Container) {
Component[] cs = ((Container) c).getComponents();
for (Component sub : cs)
setNullModels(sub);
}
}
/**
* A mouse listener that toggles the state of a boolean value in a table model
* when clicked on another column of the table.
* <p>
* NOTE: If the table model does not extend AbstractTableModel, the model must
* fire a change event (which in normal table usage is not necessary).
*
* @author Sampo Niskanen <sampo.niskanen@iki.fi>
*/
public static class BooleanTableClickListener extends MouseAdapter {
private final JTable table;
private final int clickColumn;
private final int booleanColumn;
public BooleanTableClickListener(JTable table) {
this(table, 1, 0);
}
public BooleanTableClickListener(JTable table, int clickColumn, int booleanColumn) {
this.table = table;
this.clickColumn = clickColumn;
this.booleanColumn = booleanColumn;
}
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() != MouseEvent.BUTTON1)
return;
Point p = e.getPoint();
int col = table.columnAtPoint(p);
if (col < 0)
return;
col = table.convertColumnIndexToModel(col);
if (col != clickColumn)
return;
int row = table.rowAtPoint(p);
if (row < 0)
return;
row = table.convertRowIndexToModel(row);
if (row < 0)
return;
TableModel model = table.getModel();
Object value = model.getValueAt(row, booleanColumn);
if (!(value instanceof Boolean)) {
throw new IllegalStateException("Table value at row=" + row + " col=" +
booleanColumn + " is not a Boolean, value=" + value);
}
Boolean b = (Boolean) value;
b = !b;
model.setValueAt(b, row, booleanColumn);
if (model instanceof AbstractTableModel) {
((AbstractTableModel) model).fireTableCellUpdated(row, booleanColumn);
}
}
}
}