1 package net.sf.openrocket.gui.util;
3 import java.awt.Component;
4 import java.awt.Container;
5 import java.awt.Dimension;
8 import java.awt.KeyboardFocusManager;
10 import java.awt.Toolkit;
11 import java.awt.Window;
12 import java.awt.event.ActionEvent;
13 import java.awt.event.ActionListener;
14 import java.awt.event.ComponentAdapter;
15 import java.awt.event.ComponentEvent;
16 import java.awt.event.ComponentListener;
17 import java.awt.event.FocusListener;
18 import java.awt.event.KeyEvent;
19 import java.awt.event.MouseAdapter;
20 import java.awt.event.MouseEvent;
21 import java.awt.event.MouseListener;
22 import java.awt.event.WindowAdapter;
23 import java.awt.event.WindowEvent;
24 import java.beans.PropertyChangeListener;
25 import java.io.IOException;
26 import java.io.InputStream;
27 import java.util.ArrayList;
28 import java.util.Arrays;
29 import java.util.HashSet;
30 import java.util.List;
33 import javax.imageio.ImageIO;
34 import javax.swing.AbstractAction;
35 import javax.swing.AbstractButton;
36 import javax.swing.Action;
37 import javax.swing.BoundedRangeModel;
38 import javax.swing.ComboBoxModel;
39 import javax.swing.DefaultBoundedRangeModel;
40 import javax.swing.DefaultComboBoxModel;
41 import javax.swing.DefaultListSelectionModel;
42 import javax.swing.JButton;
43 import javax.swing.JComboBox;
44 import javax.swing.JComponent;
45 import javax.swing.JDialog;
46 import javax.swing.JFrame;
47 import javax.swing.JLabel;
48 import javax.swing.JRootPane;
49 import javax.swing.JSlider;
50 import javax.swing.JSpinner;
51 import javax.swing.JTable;
52 import javax.swing.JTree;
53 import javax.swing.KeyStroke;
54 import javax.swing.ListSelectionModel;
55 import javax.swing.LookAndFeel;
56 import javax.swing.RootPaneContainer;
57 import javax.swing.SpinnerModel;
58 import javax.swing.SpinnerNumberModel;
59 import javax.swing.SwingUtilities;
60 import javax.swing.UIManager;
61 import javax.swing.border.TitledBorder;
62 import javax.swing.event.ChangeListener;
63 import javax.swing.table.AbstractTableModel;
64 import javax.swing.table.DefaultTableColumnModel;
65 import javax.swing.table.DefaultTableModel;
66 import javax.swing.table.TableColumnModel;
67 import javax.swing.table.TableModel;
68 import javax.swing.tree.DefaultMutableTreeNode;
69 import javax.swing.tree.DefaultTreeModel;
70 import javax.swing.tree.DefaultTreeSelectionModel;
71 import javax.swing.tree.TreeModel;
72 import javax.swing.tree.TreeSelectionModel;
74 import net.sf.openrocket.gui.Resettable;
75 import net.sf.openrocket.logging.LogHelper;
76 import net.sf.openrocket.startup.Application;
77 import net.sf.openrocket.util.BugException;
78 import net.sf.openrocket.util.Invalidatable;
79 import net.sf.openrocket.util.MemoryManagement;
80 import net.sf.openrocket.util.Prefs;
82 public class GUIUtil {
83 private static final LogHelper log = Application.getLogger();
85 private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
86 private static final String CLOSE_ACTION_KEY = "escape:WINDOW_CLOSING";
88 private static final List<Image> images = new ArrayList<Image>();
90 loadImage("pix/icon/icon-256.png");
91 loadImage("pix/icon/icon-064.png");
92 loadImage("pix/icon/icon-048.png");
93 loadImage("pix/icon/icon-032.png");
94 loadImage("pix/icon/icon-016.png");
97 private static void loadImage(String file) {
100 is = ClassLoader.getSystemResourceAsStream(file);
105 Image image = ImageIO.read(is);
107 } catch (IOException ignore) {
108 ignore.printStackTrace();
113 * Return the DPI setting of the monitor. This is either the setting provided
114 * by the system or a user-specified DPI setting.
116 * @return the DPI setting to use.
118 public static double getDPI() {
119 int dpi = Application.getPreferences().getInt("DPI", 0); // Tenths of a dpi
122 dpi = Toolkit.getDefaultToolkit().getScreenResolution() * 10;
134 * Set suitable options for a single-use disposable dialog. This includes
135 * setting ESC to close the dialog, adding the appropriate window icons and
136 * setting the location based on the platform. If defaultButton is provided,
137 * it is set to the default button action.
139 * The default button must be already attached to the dialog.
141 * @param dialog the dialog.
142 * @param defaultButton the default button of the dialog, or <code>null</code>.
144 public static void setDisposableDialogOptions(JDialog dialog, JButton defaultButton) {
145 installEscapeCloseOperation(dialog);
146 setWindowIcons(dialog);
147 addModelNullingListener(dialog);
148 dialog.setLocationByPlatform(true);
149 dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
151 if (defaultButton != null) {
152 setDefaultButton(defaultButton);
159 * Add the correct action to close a JDialog when the ESC key is pressed.
160 * The dialog is closed by sending is a WINDOW_CLOSING event.
162 * @param dialog the dialog for which to install the action.
164 public static void installEscapeCloseOperation(final JDialog dialog) {
165 Action dispatchClosing = new AbstractAction() {
167 public void actionPerformed(ActionEvent event) {
168 log.user("Closing dialog " + dialog);
169 dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
172 JRootPane root = dialog.getRootPane();
173 root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY);
174 root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing);
179 * Set the given button as the default button of the frame/dialog it is in. The button
180 * must be first attached to the window component hierarchy.
182 * @param button the button to set as the default button.
184 public static void setDefaultButton(JButton button) {
185 Window w = SwingUtilities.windowForComponent(button);
187 throw new IllegalArgumentException("Attach button to a window first.");
189 if (!(w instanceof RootPaneContainer)) {
190 throw new IllegalArgumentException("Button not attached to RootPaneContainer, w=" + w);
192 ((RootPaneContainer) w).getRootPane().setDefaultButton(button);
198 * Change the behavior of a component so that TAB and Shift-TAB cycles the focus of
199 * the components. This is necessary for e.g. <code>JTextArea</code>.
201 * @param c the component to modify
203 public static void setTabToFocusing(Component c) {
204 Set<KeyStroke> strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB")));
205 c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes);
206 strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB")));
207 c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes);
213 * Set the OpenRocket icons to the window icons.
215 * @param window the window to set.
217 public static void setWindowIcons(Window window) {
218 window.setIconImages(images);
222 * Add a listener to the provided window that will call {@link #setNullModels(Component)}
223 * on the window once it is closed. This method may only be used on single-use
224 * windows and dialogs, that will never be shown again once closed!
226 * @param window the window to add the listener to.
228 public static void addModelNullingListener(final Window window) {
229 window.addWindowListener(new WindowAdapter() {
231 public void windowClosed(WindowEvent e) {
232 log.debug("Clearing all models of window " + window);
233 setNullModels(window);
234 MemoryManagement.collectable(window);
242 * Set the best available look-and-feel into use.
244 public static void setBestLAF() {
246 * Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used
247 * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
248 * other alternatives.
252 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
254 // Check whether we have an ugly L&F
255 LookAndFeel laf = UIManager.getLookAndFeel();
257 laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
258 laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
260 // Search for better LAF
261 UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
262 String lafNames[] = {
266 ".*[aA][qQ][uU][aA].*",
267 ".*[nN][iI][mM][bB].*"
270 lf: for (String lafName : lafNames) {
271 for (UIManager.LookAndFeelInfo l : info) {
272 if (l.getName().matches(lafName)) {
273 UIManager.setLookAndFeel(l.getClassName());
279 } catch (Exception e) {
280 log.warn("Error setting LAF: " + e);
286 * Changes the size of the font of the specified component by the given amount.
288 * @param component the component for which to change the font
289 * @param size the change in the font size
291 public static void changeFontSize(JComponent component, float size) {
292 Font font = component.getFont();
293 font = font.deriveFont(font.getSize2D() + size);
294 component.setFont(font);
300 * Automatically remember the size of a window. This stores the window size in the user
301 * preferences when resizing/maximizing the window and sets the state on the first call.
303 public static void rememberWindowSize(final Window window) {
304 window.addComponentListener(new ComponentAdapter() {
306 public void componentResized(ComponentEvent e) {
307 log.debug("Storing size of " + window.getClass().getName() + ": " + window.getSize());
308 ((Prefs) Application.getPreferences()).setWindowSize(window.getClass(), window.getSize());
309 if (window instanceof JFrame) {
310 if ((((JFrame) window).getExtendedState() & JFrame.MAXIMIZED_BOTH) == JFrame.MAXIMIZED_BOTH) {
311 log.debug("Storing maximized state of " + window.getClass().getName());
312 ((Prefs) Application.getPreferences()).setWindowMaximized(window.getClass());
318 if (((Prefs) Application.getPreferences()).isWindowMaximized(window.getClass())) {
319 if (window instanceof JFrame) {
320 ((JFrame) window).setExtendedState(JFrame.MAXIMIZED_BOTH);
323 Dimension dim = ((Prefs) Application.getPreferences()).getWindowSize(window.getClass());
332 * Automatically remember the position of a window. The position is stored in the user preferences
333 * every time the window is moved and set from there when first calling this method.
335 public static void rememberWindowPosition(final Window window) {
336 window.addComponentListener(new ComponentAdapter() {
338 public void componentMoved(ComponentEvent e) {
339 ((Prefs) Application.getPreferences()).setWindowPosition(window.getClass(), window.getLocation());
343 // Set window position according to preferences, and set prefs when moving
344 Point position = ((Prefs) Application.getPreferences()).getWindowPosition(window.getClass());
345 if (position != null) {
346 window.setLocationByPlatform(false);
347 window.setLocation(position);
353 * Changes the style of the font of the specified border.
355 * @param border the component for which to change the font
356 * @param style the change in the font style
358 public static void changeFontStyle(TitledBorder border, int style) {
360 * The fix of JRE bug #4129681 causes a TitledBorder occasionally to
361 * return a null font. We try to work around the issue by detecting it
362 * and reverting to the font of a JLabel instead.
364 Font font = border.getTitleFont();
366 log.error("Border font is null, reverting to JLabel font");
367 font = new JLabel().getFont();
369 log.error("JLabel font is null, not modifying font");
373 font = font.deriveFont(style);
375 throw new BugException("Derived font is null");
377 border.setTitleFont(font);
383 * Traverses recursively the component tree, and sets all applicable component
384 * models to null, so as to remove the listener connections. After calling this
385 * method the component hierarchy should no longed be used.
387 * All components that use custom models should be added to this method, as
388 * there exists no standard way of removing the model from a component.
390 * @param c the component (<code>null</code> is ok)
392 public static void setNullModels(Component c) {
396 // Remove various listeners
397 for (ComponentListener l : c.getComponentListeners()) {
398 c.removeComponentListener(l);
400 for (FocusListener l : c.getFocusListeners()) {
401 c.removeFocusListener(l);
403 for (MouseListener l : c.getMouseListeners()) {
404 c.removeMouseListener(l);
406 for (PropertyChangeListener l : c.getPropertyChangeListeners()) {
407 c.removePropertyChangeListener(l);
409 for (PropertyChangeListener l : c.getPropertyChangeListeners("model")) {
410 c.removePropertyChangeListener("model", l);
412 for (PropertyChangeListener l : c.getPropertyChangeListeners("action")) {
413 c.removePropertyChangeListener("action", l);
416 // Remove models for known components
417 // Why the FSCK must this be so hard?!?!?
419 if (c instanceof JSpinner) {
421 JSpinner spinner = (JSpinner) c;
422 for (ChangeListener l : spinner.getChangeListeners()) {
423 spinner.removeChangeListener(l);
425 SpinnerModel model = spinner.getModel();
426 spinner.setModel(new SpinnerNumberModel());
427 if (model instanceof Invalidatable) {
428 ((Invalidatable) model).invalidate();
431 } else if (c instanceof JSlider) {
433 JSlider slider = (JSlider) c;
434 for (ChangeListener l : slider.getChangeListeners()) {
435 slider.removeChangeListener(l);
437 BoundedRangeModel model = slider.getModel();
438 slider.setModel(new DefaultBoundedRangeModel());
439 if (model instanceof Invalidatable) {
440 ((Invalidatable) model).invalidate();
443 } else if (c instanceof JComboBox) {
445 JComboBox combo = (JComboBox) c;
446 for (ActionListener l : combo.getActionListeners()) {
447 combo.removeActionListener(l);
449 ComboBoxModel model = combo.getModel();
450 combo.setModel(new DefaultComboBoxModel());
451 if (model instanceof Invalidatable) {
452 ((Invalidatable) model).invalidate();
455 } else if (c instanceof AbstractButton) {
457 AbstractButton button = (AbstractButton) c;
458 for (ActionListener l : button.getActionListeners()) {
459 button.removeActionListener(l);
461 Action model = button.getAction();
462 button.setAction(new AbstractAction() {
464 public void actionPerformed(ActionEvent e) {
467 if (model instanceof Invalidatable) {
468 ((Invalidatable) model).invalidate();
471 } else if (c instanceof JTable) {
473 JTable table = (JTable) c;
474 TableModel model1 = table.getModel();
475 table.setModel(new DefaultTableModel());
476 if (model1 instanceof Invalidatable) {
477 ((Invalidatable) model1).invalidate();
480 TableColumnModel model2 = table.getColumnModel();
481 table.setColumnModel(new DefaultTableColumnModel());
482 if (model2 instanceof Invalidatable) {
483 ((Invalidatable) model2).invalidate();
486 ListSelectionModel model3 = table.getSelectionModel();
487 table.setSelectionModel(new DefaultListSelectionModel());
488 if (model3 instanceof Invalidatable) {
489 ((Invalidatable) model3).invalidate();
492 } else if (c instanceof JTree) {
494 JTree tree = (JTree) c;
495 TreeModel model1 = tree.getModel();
496 tree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode()));
497 if (model1 instanceof Invalidatable) {
498 ((Invalidatable) model1).invalidate();
501 TreeSelectionModel model2 = tree.getSelectionModel();
502 tree.setSelectionModel(new DefaultTreeSelectionModel());
503 if (model2 instanceof Invalidatable) {
504 ((Invalidatable) model2).invalidate();
507 } else if (c instanceof Resettable) {
509 ((Resettable) c).resetModel();
513 // Recurse the component
514 if (c instanceof Container) {
515 Component[] cs = ((Container) c).getComponents();
516 for (Component sub : cs)
525 * A mouse listener that toggles the state of a boolean value in a table model
526 * when clicked on another column of the table.
528 * NOTE: If the table model does not extend AbstractTableModel, the model must
529 * fire a change event (which in normal table usage is not necessary).
531 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
533 public static class BooleanTableClickListener extends MouseAdapter {
535 private final JTable table;
536 private final int clickColumn;
537 private final int booleanColumn;
540 public BooleanTableClickListener(JTable table) {
545 public BooleanTableClickListener(JTable table, int clickColumn, int booleanColumn) {
547 this.clickColumn = clickColumn;
548 this.booleanColumn = booleanColumn;
552 public void mouseClicked(MouseEvent e) {
553 if (e.getButton() != MouseEvent.BUTTON1)
556 Point p = e.getPoint();
557 int col = table.columnAtPoint(p);
560 col = table.convertColumnIndexToModel(col);
561 if (col != clickColumn)
564 int row = table.rowAtPoint(p);
567 row = table.convertRowIndexToModel(row);
571 TableModel model = table.getModel();
572 Object value = model.getValueAt(row, booleanColumn);
574 if (!(value instanceof Boolean)) {
575 throw new IllegalStateException("Table value at row=" + row + " col=" +
576 booleanColumn + " is not a Boolean, value=" + value);
579 Boolean b = (Boolean) value;
581 model.setValueAt(b, row, booleanColumn);
582 if (model instanceof AbstractTableModel) {
583 ((AbstractTableModel) model).fireTableCellUpdated(row, booleanColumn);