1 package net.sf.openrocket.util;
3 import java.awt.Component;
4 import java.awt.Container;
6 import java.awt.KeyboardFocusManager;
8 import java.awt.Window;
9 import java.awt.event.ActionEvent;
10 import java.awt.event.ActionListener;
11 import java.awt.event.ComponentListener;
12 import java.awt.event.FocusListener;
13 import java.awt.event.KeyEvent;
14 import java.awt.event.MouseAdapter;
15 import java.awt.event.MouseEvent;
16 import java.awt.event.MouseListener;
17 import java.awt.event.WindowAdapter;
18 import java.awt.event.WindowEvent;
19 import java.beans.PropertyChangeListener;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Enumeration;
25 import java.util.HashSet;
26 import java.util.List;
28 import java.util.Vector;
30 import javax.imageio.ImageIO;
31 import javax.swing.AbstractAction;
32 import javax.swing.AbstractButton;
33 import javax.swing.Action;
34 import javax.swing.DefaultBoundedRangeModel;
35 import javax.swing.DefaultComboBoxModel;
36 import javax.swing.DefaultListSelectionModel;
37 import javax.swing.JButton;
38 import javax.swing.JComboBox;
39 import javax.swing.JComponent;
40 import javax.swing.JDialog;
41 import javax.swing.JRootPane;
42 import javax.swing.JSlider;
43 import javax.swing.JSpinner;
44 import javax.swing.JTable;
45 import javax.swing.JTree;
46 import javax.swing.KeyStroke;
47 import javax.swing.LookAndFeel;
48 import javax.swing.RootPaneContainer;
49 import javax.swing.SpinnerNumberModel;
50 import javax.swing.SwingUtilities;
51 import javax.swing.UIManager;
52 import javax.swing.event.ChangeListener;
53 import javax.swing.table.AbstractTableModel;
54 import javax.swing.table.DefaultTableColumnModel;
55 import javax.swing.table.DefaultTableModel;
56 import javax.swing.table.TableModel;
57 import javax.swing.tree.DefaultTreeModel;
58 import javax.swing.tree.DefaultTreeSelectionModel;
59 import javax.swing.tree.TreeNode;
61 import net.sf.openrocket.gui.Resettable;
63 public class GUIUtil {
65 private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
66 private static final String CLOSE_ACTION_KEY = "escape:WINDOW_CLOSING";
68 private static final List<Image> images = new ArrayList<Image>();
70 loadImage("pix/icon/icon-256.png");
71 loadImage("pix/icon/icon-064.png");
72 loadImage("pix/icon/icon-048.png");
73 loadImage("pix/icon/icon-032.png");
74 loadImage("pix/icon/icon-016.png");
76 private static void loadImage(String file) {
79 is = ClassLoader.getSystemResourceAsStream(file);
84 Image image = ImageIO.read(is);
86 } catch (IOException ignore) {
87 ignore.printStackTrace();
94 * Set suitable options for a single-use disposable dialog. This includes
95 * setting ESC to close the dialog and adding the appropriate window icons.
96 * If defaultButton is provided, it is set to the default button action.
98 * The default button must be already attached to the dialog.
100 * @param dialog the dialog.
101 * @param defaultButton the default button of the dialog, or <code>null</code>.
103 public static void setDisposableDialogOptions(JDialog dialog, JButton defaultButton) {
104 installEscapeCloseOperation(dialog);
105 setWindowIcons(dialog);
106 addModelNullingListener(dialog);
107 if (defaultButton != null) {
108 setDefaultButton(defaultButton);
115 * Add the correct action to close a JDialog when the ESC key is pressed.
116 * The dialog is closed by sending is a WINDOW_CLOSING event.
118 * @param dialog the dialog for which to install the action.
120 public static void installEscapeCloseOperation(final JDialog dialog) {
121 Action dispatchClosing = new AbstractAction() {
122 public void actionPerformed(ActionEvent event) {
123 dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
126 JRootPane root = dialog.getRootPane();
127 root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY);
128 root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing);
133 * Set the given button as the default button of the frame/dialog it is in. The button
134 * must be first attached to the window component hierarchy.
136 * @param button the button to set as the default button.
138 public static void setDefaultButton(JButton button) {
139 Window w = SwingUtilities.windowForComponent(button);
141 throw new IllegalArgumentException("Attach button to a window first.");
143 if (!(w instanceof RootPaneContainer)) {
144 throw new IllegalArgumentException("Button not attached to RootPaneContainer, w="+w);
146 ((RootPaneContainer)w).getRootPane().setDefaultButton(button);
152 * Change the behavior of a component so that TAB and Shift-TAB cycles the focus of
153 * the components. This is necessary for e.g. <code>JTextArea</code>.
155 * @param c the component to modify
157 public static void setTabToFocusing(Component c) {
158 Set<KeyStroke> strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB")));
159 c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes);
160 strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB")));
161 c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes);
167 * Set the OpenRocket icons to the window icons.
169 * @param window the window to set.
171 public static void setWindowIcons(Window window) {
172 window.setIconImages(images);
176 * Add a listener to the provided window that will call {@link #setNullModels(Component)}
177 * on the window once it is closed. This method may only be used on single-use
178 * windows and dialogs, that will never be shown again once closed!
180 * @param window the window to add the listener to.
182 public static void addModelNullingListener(final Window window) {
183 window.addWindowListener(new WindowAdapter() {
185 public void windowClosed(WindowEvent e) {
186 setNullModels(window);
194 * Set the best available look-and-feel into use.
196 public static void setBestLAF() {
198 * Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used
199 * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
200 * other alternatives.
204 UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
206 // Check whether we have an ugly L&F
207 LookAndFeel laf = UIManager.getLookAndFeel();
209 laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
210 laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
212 // Search for better LAF
213 UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
214 String lafNames[] = {
218 ".*[aA][qQ][uU][aA].*",
219 ".*[nN][iI][mM][bB].*"
222 lf: for (String lafName: lafNames) {
223 for (UIManager.LookAndFeelInfo l: info) {
224 if (l.getName().matches(lafName)) {
225 UIManager.setLookAndFeel(l.getClassName());
231 } catch (Exception e) {
232 System.err.println("Error setting LAF: " + e);
238 * Traverses recursively the component tree, and sets all applicable component
239 * models to null, so as to remove the listener connections. After calling this
240 * method the component hierarchy should no longed be used.
242 * All components that use custom models should be added to this method, as
243 * there exists no standard way of removing the model from a component.
245 * @param c the component (<code>null</code> is ok)
247 public static void setNullModels(Component c) {
251 // Remove various listeners
252 for (ComponentListener l: c.getComponentListeners()) {
253 c.removeComponentListener(l);
255 for (FocusListener l: c.getFocusListeners()) {
256 c.removeFocusListener(l);
258 for (MouseListener l: c.getMouseListeners()) {
259 c.removeMouseListener(l);
261 for (PropertyChangeListener l: c.getPropertyChangeListeners()) {
262 c.removePropertyChangeListener(l);
264 for (PropertyChangeListener l: c.getPropertyChangeListeners("model")) {
265 c.removePropertyChangeListener("model", l);
267 for (PropertyChangeListener l: c.getPropertyChangeListeners("action")) {
268 c.removePropertyChangeListener("action", l);
271 // Remove models for known components
272 // Why the FSCK must this be so hard?!?!?
274 if (c instanceof JSpinner) {
276 JSpinner spinner = (JSpinner)c;
277 for (ChangeListener l: spinner.getChangeListeners()) {
278 spinner.removeChangeListener(l);
280 spinner.setModel(new SpinnerNumberModel());
282 } else if (c instanceof JSlider) {
284 JSlider slider = (JSlider)c;
285 for (ChangeListener l: slider.getChangeListeners()) {
286 slider.removeChangeListener(l);
288 slider.setModel(new DefaultBoundedRangeModel());
290 } else if (c instanceof JComboBox) {
292 JComboBox combo = (JComboBox)c;
293 for (ActionListener l: combo.getActionListeners()) {
294 combo.removeActionListener(l);
296 combo.setModel(new DefaultComboBoxModel());
298 } else if (c instanceof AbstractButton) {
300 AbstractButton button = (AbstractButton)c;
301 for (ActionListener l: button.getActionListeners()) {
302 button.removeActionListener(l);
304 button.setAction(new AbstractAction() {
306 public void actionPerformed(ActionEvent e) { }
309 } else if (c instanceof JTable) {
311 JTable table = (JTable)c;
312 table.setModel(new DefaultTableModel());
313 table.setColumnModel(new DefaultTableColumnModel());
314 table.setSelectionModel(new DefaultListSelectionModel());
316 } else if (c instanceof JTree) {
318 JTree tree = (JTree)c;
319 tree.setModel(new DefaultTreeModel(new TreeNode() {
320 @SuppressWarnings("unchecked")
322 public Enumeration children() {
323 return new Vector().elements();
326 public boolean getAllowsChildren() {
330 public TreeNode getChildAt(int childIndex) {
334 public int getChildCount() {
338 public int getIndex(TreeNode node) {
342 public TreeNode getParent() {
346 public boolean isLeaf() {
350 tree.setSelectionModel(new DefaultTreeSelectionModel());
352 } else if (c instanceof Resettable) {
354 ((Resettable)c).resetModel();
358 // Recurse the component
359 if (c instanceof Container) {
360 Component[] cs = ((Container)c).getComponents();
361 for (Component sub: cs)
371 * A mouse listener that toggles the state of a boolean value in a table model
372 * when clicked on another column of the table.
374 * NOTE: If the table model does not extend AbstractTableModel, the model must
375 * fire a change event (which in normal table usage is not necessary).
377 * @author Sampo Niskanen <sampo.niskanen@iki.fi>
379 public static class BooleanTableClickListener extends MouseAdapter {
381 private final JTable table;
382 private final int clickColumn;
383 private final int booleanColumn;
386 public BooleanTableClickListener(JTable table) {
391 public BooleanTableClickListener(JTable table, int clickColumn, int booleanColumn) {
393 this.clickColumn = clickColumn;
394 this.booleanColumn = booleanColumn;
398 public void mouseClicked(MouseEvent e) {
399 if (e.getButton() != MouseEvent.BUTTON1)
402 Point p = e.getPoint();
403 int col = table.columnAtPoint(p);
406 col = table.convertColumnIndexToModel(col);
407 if (col != clickColumn)
410 int row = table.rowAtPoint(p);
413 row = table.convertRowIndexToModel(row);
417 TableModel model = table.getModel();
418 Object value = model.getValueAt(row, booleanColumn);
420 if (!(value instanceof Boolean)) {
421 throw new IllegalStateException("Table value at row="+row+" col="+
422 booleanColumn + " is not a Boolean, value=" +value);
425 Boolean b = (Boolean)value;
427 model.setValueAt(b, row, booleanColumn);
428 if (model instanceof AbstractTableModel) {
429 ((AbstractTableModel)model).fireTableCellUpdated(row, booleanColumn);