1 package net.sf.openrocket.util;
3 import java.awt.Component;
4 import java.awt.Container;
7 import java.awt.KeyboardFocusManager;
9 import java.awt.Window;
10 import java.awt.event.ActionEvent;
11 import java.awt.event.ActionListener;
12 import java.awt.event.ComponentListener;
13 import java.awt.event.FocusListener;
14 import java.awt.event.KeyEvent;
15 import java.awt.event.MouseAdapter;
16 import java.awt.event.MouseEvent;
17 import java.awt.event.MouseListener;
18 import java.awt.event.WindowAdapter;
19 import java.awt.event.WindowEvent;
20 import java.beans.PropertyChangeListener;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.util.ArrayList;
24 import java.util.Arrays;
25 import java.util.HashSet;
26 import java.util.List;
29 import javax.imageio.ImageIO;
30 import javax.swing.AbstractAction;
31 import javax.swing.AbstractButton;
32 import javax.swing.Action;
33 import javax.swing.BoundedRangeModel;
34 import javax.swing.ComboBoxModel;
35 import javax.swing.DefaultBoundedRangeModel;
36 import javax.swing.DefaultComboBoxModel;
37 import javax.swing.DefaultListSelectionModel;
38 import javax.swing.JButton;
39 import javax.swing.JComboBox;
40 import javax.swing.JComponent;
41 import javax.swing.JDialog;
42 import javax.swing.JRootPane;
43 import javax.swing.JSlider;
44 import javax.swing.JSpinner;
45 import javax.swing.JTable;
46 import javax.swing.JTree;
47 import javax.swing.KeyStroke;
48 import javax.swing.ListSelectionModel;
49 import javax.swing.LookAndFeel;
50 import javax.swing.RootPaneContainer;
51 import javax.swing.SpinnerModel;
52 import javax.swing.SpinnerNumberModel;
53 import javax.swing.SwingUtilities;
54 import javax.swing.UIManager;
55 import javax.swing.border.TitledBorder;
56 import javax.swing.event.ChangeListener;
57 import javax.swing.table.AbstractTableModel;
58 import javax.swing.table.DefaultTableColumnModel;
59 import javax.swing.table.DefaultTableModel;
60 import javax.swing.table.TableColumnModel;
61 import javax.swing.table.TableModel;
62 import javax.swing.tree.DefaultMutableTreeNode;
63 import javax.swing.tree.DefaultTreeModel;
64 import javax.swing.tree.DefaultTreeSelectionModel;
65 import javax.swing.tree.TreeModel;
66 import javax.swing.tree.TreeSelectionModel;
68 import net.sf.openrocket.gui.Resettable;
69 import net.sf.openrocket.logging.LogHelper;
70 import net.sf.openrocket.startup.Application;
72 public class GUIUtil {
73 private static final LogHelper log = Application.getLogger();
75 private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
76 private static final String CLOSE_ACTION_KEY = "escape:WINDOW_CLOSING";
78 private static final List<Image> images = new ArrayList<Image>();
80 loadImage("pix/icon/icon-256.png");
81 loadImage("pix/icon/icon-064.png");
82 loadImage("pix/icon/icon-048.png");
83 loadImage("pix/icon/icon-032.png");
84 loadImage("pix/icon/icon-016.png");
87 private static void loadImage(String file) {
90 is = ClassLoader.getSystemResourceAsStream(file);
95 Image image = ImageIO.read(is);
97 } catch (IOException ignore) {
98 ignore.printStackTrace();
105 * Set suitable options for a single-use disposable dialog. This includes
106 * setting ESC to close the dialog, adding the appropriate window icons and
107 * setting the location based on the platform. If defaultButton is provided,
108 * it is set to the default button action.
110 * The default button must be already attached to the dialog.
112 * @param dialog the dialog.
113 * @param defaultButton the default button of the dialog, or <code>null</code>.
115 public static void setDisposableDialogOptions(JDialog dialog, JButton defaultButton) {
116 installEscapeCloseOperation(dialog);
117 setWindowIcons(dialog);
118 addModelNullingListener(dialog);
119 dialog.setLocationByPlatform(true);
120 dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
122 if (defaultButton != null) {
123 setDefaultButton(defaultButton);
130 * Add the correct action to close a JDialog when the ESC key is pressed.
131 * The dialog is closed by sending is a WINDOW_CLOSING event.
133 * @param dialog the dialog for which to install the action.
135 public static void installEscapeCloseOperation(final JDialog dialog) {
136 Action dispatchClosing = new AbstractAction() {
138 public void actionPerformed(ActionEvent event) {
139 log.user("Closing dialog " + dialog);
140 dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
143 JRootPane root = dialog.getRootPane();
144 root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY);
145 root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing);
150 * Set the given button as the default button of the frame/dialog it is in. The button
151 * must be first attached to the window component hierarchy.
153 * @param button the button to set as the default button.
155 public static void setDefaultButton(JButton button) {
156 Window w = SwingUtilities.windowForComponent(button);
158 throw new IllegalArgumentException("Attach button to a window first.");
160 if (!(w instanceof RootPaneContainer)) {
161 throw new IllegalArgumentException("Button not attached to RootPaneContainer, w=" + w);
163 ((RootPaneContainer) w).getRootPane().setDefaultButton(button);
169 * Change the behavior of a component so that TAB and Shift-TAB cycles the focus of
170 * the components. This is necessary for e.g. <code>JTextArea</code>.
172 * @param c the component to modify
174 public static void setTabToFocusing(Component c) {
175 Set<KeyStroke> strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB")));
176 c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes);
177 strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB")));
178 c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes);
184 * Set the OpenRocket icons to the window icons.
186 * @param window the window to set.
188 public static void setWindowIcons(Window window) {
189 window.setIconImages(images);
193 * Add a listener to the provided window that will call {@link #setNullModels(Component)}
194 * on the window once it is closed. This method may only be used on single-use
195 * windows and dialogs, that will never be shown again once closed!
197 * @param window the window to add the listener to.
199 public static void addModelNullingListener(final Window window) {
200 window.addWindowListener(new WindowAdapter() {
202 public void windowClosed(WindowEvent e) {
203 log.debug("Clearing all models of window " + window);
204 setNullModels(window);
205 MemoryManagement.collectable(window);
213 * Set the best available look-and-feel into use.
215 public static void setBestLAF() {
217 * Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used
218 * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
219 * other alternatives.
223 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
225 // Check whether we have an ugly L&F
226 LookAndFeel laf = UIManager.getLookAndFeel();
228 laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
229 laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
231 // Search for better LAF
232 UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
233 String lafNames[] = {
237 ".*[aA][qQ][uU][aA].*",
238 ".*[nN][iI][mM][bB].*"
241 lf: for (String lafName : lafNames) {
242 for (UIManager.LookAndFeelInfo l : info) {
243 if (l.getName().matches(lafName)) {
244 UIManager.setLookAndFeel(l.getClassName());
250 } catch (Exception e) {
251 log.warn("Error setting LAF: " + e);
257 * Changes the size of the font of the specified component by the given amount.
259 * @param component the component for which to change the font
260 * @param size the change in the font size
262 public static void changeFontSize(JComponent component, float size) {
263 Font font = component.getFont();
264 font = font.deriveFont(font.getSize2D() + size);
265 component.setFont(font);
270 * Changes the style of the font of the specified border.
272 * @param border the component for which to change the font
273 * @param style the change in the font style
275 public static void changeFontStyle(TitledBorder border, int style) {
277 * There's been an NPE caused by the font changing, this is debug for it.
279 if (border == null) {
280 throw new BugException("border is null");
282 Font font = border.getTitleFont();
284 throw new BugException("Border font is null");
286 font = font.deriveFont(style);
288 throw new BugException("Derived font is null");
290 border.setTitleFont(font);
296 * Traverses recursively the component tree, and sets all applicable component
297 * models to null, so as to remove the listener connections. After calling this
298 * method the component hierarchy should no longed be used.
300 * All components that use custom models should be added to this method, as
301 * there exists no standard way of removing the model from a component.
303 * @param c the component (<code>null</code> is ok)
305 public static void setNullModels(Component c) {
309 // Remove various listeners
310 for (ComponentListener l : c.getComponentListeners()) {
311 c.removeComponentListener(l);
313 for (FocusListener l : c.getFocusListeners()) {
314 c.removeFocusListener(l);
316 for (MouseListener l : c.getMouseListeners()) {
317 c.removeMouseListener(l);
319 for (PropertyChangeListener l : c.getPropertyChangeListeners()) {
320 c.removePropertyChangeListener(l);
322 for (PropertyChangeListener l : c.getPropertyChangeListeners("model")) {
323 c.removePropertyChangeListener("model", l);
325 for (PropertyChangeListener l : c.getPropertyChangeListeners("action")) {
326 c.removePropertyChangeListener("action", l);
329 // Remove models for known components
330 // Why the FSCK must this be so hard?!?!?
332 if (c instanceof JSpinner) {
334 JSpinner spinner = (JSpinner) c;
335 for (ChangeListener l : spinner.getChangeListeners()) {
336 spinner.removeChangeListener(l);
338 SpinnerModel model = spinner.getModel();
339 spinner.setModel(new SpinnerNumberModel());
340 if (model instanceof Invalidatable) {
341 ((Invalidatable) model).invalidate();
344 } else if (c instanceof JSlider) {
346 JSlider slider = (JSlider) c;
347 for (ChangeListener l : slider.getChangeListeners()) {
348 slider.removeChangeListener(l);
350 BoundedRangeModel model = slider.getModel();
351 slider.setModel(new DefaultBoundedRangeModel());
352 if (model instanceof Invalidatable) {
353 ((Invalidatable) model).invalidate();
356 } else if (c instanceof JComboBox) {
358 JComboBox combo = (JComboBox) c;
359 for (ActionListener l : combo.getActionListeners()) {
360 combo.removeActionListener(l);
362 ComboBoxModel model = combo.getModel();
363 combo.setModel(new DefaultComboBoxModel());
364 if (model instanceof Invalidatable) {
365 ((Invalidatable) model).invalidate();
368 } else if (c instanceof AbstractButton) {
370 AbstractButton button = (AbstractButton) c;
371 for (ActionListener l : button.getActionListeners()) {
372 button.removeActionListener(l);
374 Action model = button.getAction();
375 button.setAction(new AbstractAction() {
377 public void actionPerformed(ActionEvent e) {
380 if (model instanceof Invalidatable) {
381 ((Invalidatable) model).invalidate();
384 } else if (c instanceof JTable) {
386 JTable table = (JTable) c;
387 TableModel model1 = table.getModel();
388 table.setModel(new DefaultTableModel());
389 if (model1 instanceof Invalidatable) {
390 ((Invalidatable) model1).invalidate();
393 TableColumnModel model2 = table.getColumnModel();
394 table.setColumnModel(new DefaultTableColumnModel());
395 if (model2 instanceof Invalidatable) {
396 ((Invalidatable) model2).invalidate();
399 ListSelectionModel model3 = table.getSelectionModel();
400 table.setSelectionModel(new DefaultListSelectionModel());
401 if (model3 instanceof Invalidatable) {
402 ((Invalidatable) model3).invalidate();
405 } else if (c instanceof JTree) {
407 JTree tree = (JTree) c;
408 TreeModel model1 = tree.getModel();
409 tree.setModel(new DefaultTreeModel(new DefaultMutableTreeNode()));
410 if (model1 instanceof Invalidatable) {
411 ((Invalidatable) model1).invalidate();
414 TreeSelectionModel model2 = tree.getSelectionModel();
415 tree.setSelectionModel(new DefaultTreeSelectionModel());
416 if (model2 instanceof Invalidatable) {
417 ((Invalidatable) model2).invalidate();
420 } else if (c instanceof Resettable) {
422 ((Resettable) c).resetModel();
426 // Recurse the component
427 if (c instanceof Container) {
428 Component[] cs = ((Container) c).getComponents();
429 for (Component sub : cs)
438 * A mouse listener that toggles the state of a boolean value in a table model
439 * when clicked on another column of the table.
441 * NOTE: If the table model does not extend AbstractTableModel, the model must
442 * fire a change event (which in normal table usage is not necessary).
444 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
446 public static class BooleanTableClickListener extends MouseAdapter {
448 private final JTable table;
449 private final int clickColumn;
450 private final int booleanColumn;
453 public BooleanTableClickListener(JTable table) {
458 public BooleanTableClickListener(JTable table, int clickColumn, int booleanColumn) {
460 this.clickColumn = clickColumn;
461 this.booleanColumn = booleanColumn;
465 public void mouseClicked(MouseEvent e) {
466 if (e.getButton() != MouseEvent.BUTTON1)
469 Point p = e.getPoint();
470 int col = table.columnAtPoint(p);
473 col = table.convertColumnIndexToModel(col);
474 if (col != clickColumn)
477 int row = table.rowAtPoint(p);
480 row = table.convertRowIndexToModel(row);
484 TableModel model = table.getModel();
485 Object value = model.getValueAt(row, booleanColumn);
487 if (!(value instanceof Boolean)) {
488 throw new IllegalStateException("Table value at row=" + row + " col=" +
489 booleanColumn + " is not a Boolean, value=" + value);
492 Boolean b = (Boolean) value;
494 model.setValueAt(b, row, booleanColumn);
495 if (model instanceof AbstractTableModel) {
496 ((AbstractTableModel) model).fireTableCellUpdated(row, booleanColumn);