Major refactoring for 1.1.1
[debian/openrocket] / src / net / sf / openrocket / util / GUIUtil.java
1 package net.sf.openrocket.util;
2
3 import java.awt.Component;
4 import java.awt.Container;
5 import java.awt.Image;
6 import java.awt.KeyboardFocusManager;
7 import java.awt.Point;
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;
27 import java.util.Set;
28 import java.util.Vector;
29
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;
60
61 import net.sf.openrocket.gui.Resettable;
62 import net.sf.openrocket.logging.LogHelper;
63 import net.sf.openrocket.startup.Application;
64
65 public class GUIUtil {
66         private static final LogHelper log = Application.getLogger();
67         
68         private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
69         private static final String CLOSE_ACTION_KEY = "escape:WINDOW_CLOSING";
70         
71         private static final List<Image> images = new ArrayList<Image>();
72         static {
73                 loadImage("pix/icon/icon-256.png");
74                 loadImage("pix/icon/icon-064.png");
75                 loadImage("pix/icon/icon-048.png");
76                 loadImage("pix/icon/icon-032.png");
77                 loadImage("pix/icon/icon-016.png");
78         }
79         
80         private static void loadImage(String file) {
81                 InputStream is;
82                 
83                 is = ClassLoader.getSystemResourceAsStream(file);
84                 if (is == null)
85                         return;
86                 
87                 try {
88                         Image image = ImageIO.read(is);
89                         images.add(image);
90                 } catch (IOException ignore) {
91                         ignore.printStackTrace();
92                 }
93         }
94         
95         
96
97         /**
98          * Set suitable options for a single-use disposable dialog.  This includes
99          * setting ESC to close the dialog and adding the appropriate window icons.
100          * If defaultButton is provided, it is set to the default button action.
101          * <p>
102          * The default button must be already attached to the dialog.
103          * 
104          * @param dialog                the dialog.
105          * @param defaultButton the default button of the dialog, or <code>null</code>.
106          */
107         public static void setDisposableDialogOptions(JDialog dialog, JButton defaultButton) {
108                 installEscapeCloseOperation(dialog);
109                 setWindowIcons(dialog);
110                 addModelNullingListener(dialog);
111                 if (defaultButton != null) {
112                         setDefaultButton(defaultButton);
113                 }
114         }
115         
116         
117
118         /**
119          * Add the correct action to close a JDialog when the ESC key is pressed.
120          * The dialog is closed by sending is a WINDOW_CLOSING event.
121          * 
122          * @param dialog        the dialog for which to install the action.
123          */
124         public static void installEscapeCloseOperation(final JDialog dialog) {
125                 Action dispatchClosing = new AbstractAction() {
126                         public void actionPerformed(ActionEvent event) {
127                                 dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
128                         }
129                 };
130                 JRootPane root = dialog.getRootPane();
131                 root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY);
132                 root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing);
133         }
134         
135         
136         /**
137          * Set the given button as the default button of the frame/dialog it is in.  The button
138          * must be first attached to the window component hierarchy.
139          * 
140          * @param button        the button to set as the default button.
141          */
142         public static void setDefaultButton(JButton button) {
143                 Window w = SwingUtilities.windowForComponent(button);
144                 if (w == null) {
145                         throw new IllegalArgumentException("Attach button to a window first.");
146                 }
147                 if (!(w instanceof RootPaneContainer)) {
148                         throw new IllegalArgumentException("Button not attached to RootPaneContainer, w=" + w);
149                 }
150                 ((RootPaneContainer) w).getRootPane().setDefaultButton(button);
151         }
152         
153         
154
155         /**
156          * Change the behavior of a component so that TAB and Shift-TAB cycles the focus of
157          * the components.  This is necessary for e.g. <code>JTextArea</code>.
158          * 
159          * @param c             the component to modify
160          */
161         public static void setTabToFocusing(Component c) {
162                 Set<KeyStroke> strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB")));
163                 c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes);
164                 strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB")));
165                 c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes);
166         }
167         
168         
169
170         /**
171          * Set the OpenRocket icons to the window icons.
172          * 
173          * @param window        the window to set.
174          */
175         public static void setWindowIcons(Window window) {
176                 window.setIconImages(images);
177         }
178         
179         /**
180          * Add a listener to the provided window that will call {@link #setNullModels(Component)}
181          * on the window once it is closed.  This method may only be used on single-use
182          * windows and dialogs, that will never be shown again once closed!
183          * 
184          * @param window        the window to add the listener to.
185          */
186         public static void addModelNullingListener(final Window window) {
187                 window.addWindowListener(new WindowAdapter() {
188                         @Override
189                         public void windowClosed(WindowEvent e) {
190                                 setNullModels(window);
191                         }
192                 });
193         }
194         
195         
196
197         /**
198          * Set the best available look-and-feel into use.
199          */
200         public static void setBestLAF() {
201                 /*
202                  * Set the look-and-feel.  On Linux, Motif/Metal is sometimes incorrectly used 
203                  * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
204                  * other alternatives.
205                  */
206                 try {
207                         // Set system L&F
208                         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
209                         
210                         // Check whether we have an ugly L&F
211                         LookAndFeel laf = UIManager.getLookAndFeel();
212                         if (laf == null ||
213                                         laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
214                                         laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
215                                 
216                                 // Search for better LAF
217                                 UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
218                                 String lafNames[] = {
219                                                 ".*[gG][tT][kK].*",
220                                                 ".*[wW][iI][nN].*",
221                                                 ".*[mM][aA][cC].*",
222                                                 ".*[aA][qQ][uU][aA].*",
223                                                 ".*[nN][iI][mM][bB].*"
224                                 };
225                                 
226                                 lf: for (String lafName : lafNames) {
227                                         for (UIManager.LookAndFeelInfo l : info) {
228                                                 if (l.getName().matches(lafName)) {
229                                                         UIManager.setLookAndFeel(l.getClassName());
230                                                         break lf;
231                                                 }
232                                         }
233                                 }
234                         }
235                 } catch (Exception e) {
236                         log.warn("Error setting LAF: " + e);
237                 }
238         }
239         
240         
241         /**
242          * Traverses recursively the component tree, and sets all applicable component 
243          * models to null, so as to remove the listener connections.  After calling this
244          * method the component hierarchy should no longed be used.
245          * <p>
246          * All components that use custom models should be added to this method, as
247          * there exists no standard way of removing the model from a component.
248          * 
249          * @param c             the component (<code>null</code> is ok)
250          */
251         public static void setNullModels(Component c) {
252                 if (c == null)
253                         return;
254                 
255                 // Remove various listeners
256                 for (ComponentListener l : c.getComponentListeners()) {
257                         c.removeComponentListener(l);
258                 }
259                 for (FocusListener l : c.getFocusListeners()) {
260                         c.removeFocusListener(l);
261                 }
262                 for (MouseListener l : c.getMouseListeners()) {
263                         c.removeMouseListener(l);
264                 }
265                 for (PropertyChangeListener l : c.getPropertyChangeListeners()) {
266                         c.removePropertyChangeListener(l);
267                 }
268                 for (PropertyChangeListener l : c.getPropertyChangeListeners("model")) {
269                         c.removePropertyChangeListener("model", l);
270                 }
271                 for (PropertyChangeListener l : c.getPropertyChangeListeners("action")) {
272                         c.removePropertyChangeListener("action", l);
273                 }
274                 
275                 // Remove models for known components
276                 //  Why the FSCK must this be so hard?!?!?
277                 
278                 if (c instanceof JSpinner) {
279                         
280                         JSpinner spinner = (JSpinner) c;
281                         for (ChangeListener l : spinner.getChangeListeners()) {
282                                 spinner.removeChangeListener(l);
283                         }
284                         spinner.setModel(new SpinnerNumberModel());
285                         
286                 } else if (c instanceof JSlider) {
287                         
288                         JSlider slider = (JSlider) c;
289                         for (ChangeListener l : slider.getChangeListeners()) {
290                                 slider.removeChangeListener(l);
291                         }
292                         slider.setModel(new DefaultBoundedRangeModel());
293                         
294                 } else if (c instanceof JComboBox) {
295                         
296                         JComboBox combo = (JComboBox) c;
297                         for (ActionListener l : combo.getActionListeners()) {
298                                 combo.removeActionListener(l);
299                         }
300                         combo.setModel(new DefaultComboBoxModel());
301                         
302                 } else if (c instanceof AbstractButton) {
303                         
304                         AbstractButton button = (AbstractButton) c;
305                         for (ActionListener l : button.getActionListeners()) {
306                                 button.removeActionListener(l);
307                         }
308                         button.setAction(new AbstractAction() {
309                                 @Override
310                                 public void actionPerformed(ActionEvent e) {
311                                 }
312                         });
313                         
314                 } else if (c instanceof JTable) {
315                         
316                         JTable table = (JTable) c;
317                         table.setModel(new DefaultTableModel());
318                         table.setColumnModel(new DefaultTableColumnModel());
319                         table.setSelectionModel(new DefaultListSelectionModel());
320                         
321                 } else if (c instanceof JTree) {
322                         
323                         JTree tree = (JTree) c;
324                         tree.setModel(new DefaultTreeModel(new TreeNode() {
325                                 @SuppressWarnings("unchecked")
326                                 @Override
327                                 public Enumeration children() {
328                                         return new Vector().elements();
329                                 }
330                                 
331                                 @Override
332                                 public boolean getAllowsChildren() {
333                                         return false;
334                                 }
335                                 
336                                 @Override
337                                 public TreeNode getChildAt(int childIndex) {
338                                         return null;
339                                 }
340                                 
341                                 @Override
342                                 public int getChildCount() {
343                                         return 0;
344                                 }
345                                 
346                                 @Override
347                                 public int getIndex(TreeNode node) {
348                                         return 0;
349                                 }
350                                 
351                                 @Override
352                                 public TreeNode getParent() {
353                                         return null;
354                                 }
355                                 
356                                 @Override
357                                 public boolean isLeaf() {
358                                         return true;
359                                 }
360                         }));
361                         tree.setSelectionModel(new DefaultTreeSelectionModel());
362                         
363                 } else if (c instanceof Resettable) {
364                         
365                         ((Resettable) c).resetModel();
366                         
367                 }
368                 
369                 // Recurse the component
370                 if (c instanceof Container) {
371                         Component[] cs = ((Container) c).getComponents();
372                         for (Component sub : cs)
373                                 setNullModels(sub);
374                 }
375                 
376         }
377         
378         
379
380
381         /**
382          * A mouse listener that toggles the state of a boolean value in a table model
383          * when clicked on another column of the table.
384          * <p>
385          * NOTE:  If the table model does not extend AbstractTableModel, the model must
386          * fire a change event (which in normal table usage is not necessary).
387          * 
388          * @author Sampo Niskanen <sampo.niskanen@iki.fi>
389          */
390         public static class BooleanTableClickListener extends MouseAdapter {
391                 
392                 private final JTable table;
393                 private final int clickColumn;
394                 private final int booleanColumn;
395                 
396                 
397                 public BooleanTableClickListener(JTable table) {
398                         this(table, 1, 0);
399                 }
400                 
401                 
402                 public BooleanTableClickListener(JTable table, int clickColumn, int booleanColumn) {
403                         this.table = table;
404                         this.clickColumn = clickColumn;
405                         this.booleanColumn = booleanColumn;
406                 }
407                 
408                 @Override
409                 public void mouseClicked(MouseEvent e) {
410                         if (e.getButton() != MouseEvent.BUTTON1)
411                                 return;
412                         
413                         Point p = e.getPoint();
414                         int col = table.columnAtPoint(p);
415                         if (col < 0)
416                                 return;
417                         col = table.convertColumnIndexToModel(col);
418                         if (col != clickColumn)
419                                 return;
420                         
421                         int row = table.rowAtPoint(p);
422                         if (row < 0)
423                                 return;
424                         row = table.convertRowIndexToModel(row);
425                         if (row < 0)
426                                 return;
427                         
428                         TableModel model = table.getModel();
429                         Object value = model.getValueAt(row, booleanColumn);
430                         
431                         if (!(value instanceof Boolean)) {
432                                 throw new IllegalStateException("Table value at row=" + row + " col=" +
433                                                 booleanColumn + " is not a Boolean, value=" + value);
434                         }
435                         
436                         Boolean b = (Boolean) value;
437                         b = !b;
438                         model.setValueAt(b, row, booleanColumn);
439                         if (model instanceof AbstractTableModel) {
440                                 ((AbstractTableModel) model).fireTableCellUpdated(row, booleanColumn);
441                         }
442                 }
443                 
444         }
445         
446 }