release 0.9.6
[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
63 public class GUIUtil {
64
65         private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
66         private static final String CLOSE_ACTION_KEY =  "escape:WINDOW_CLOSING"; 
67         
68     private static final List<Image> images = new ArrayList<Image>();
69     static {
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");
75     }
76     private static void loadImage(String file) {
77         InputStream is;
78  
79         is = ClassLoader.getSystemResourceAsStream(file);
80         if (is == null)
81                 return;
82         
83         try {
84                 Image image = ImageIO.read(is);
85                 images.add(image);
86         } catch (IOException ignore) {
87                 ignore.printStackTrace();
88         }
89     }
90     
91
92     
93     /**
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.
97      * <p>
98      * The default button must be already attached to the dialog.
99      * 
100      * @param dialog            the dialog.
101      * @param defaultButton     the default button of the dialog, or <code>null</code>.
102      */
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);
109         }
110     }
111     
112         
113         
114         /**
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.
117          * 
118          * @param dialog        the dialog for which to install the action.
119          */
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)); 
124                 } 
125             }; 
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); 
129         }
130         
131         
132         /**
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.
135          * 
136          * @param button        the button to set as the default button.
137          */
138         public static void setDefaultButton(JButton button) {
139                 Window w = SwingUtilities.windowForComponent(button);
140                 if (w == null) {
141                         throw new IllegalArgumentException("Attach button to a window first.");
142                 }
143                 if (!(w instanceof RootPaneContainer)) {
144                         throw new IllegalArgumentException("Button not attached to RootPaneContainer, w="+w);
145                 }
146                 ((RootPaneContainer)w).getRootPane().setDefaultButton(button);
147         }
148
149         
150         
151         /**
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>.
154          * 
155          * @param c             the component to modify
156          */
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);
162     }
163
164     
165     
166     /**
167      * Set the OpenRocket icons to the window icons.
168      * 
169      * @param window    the window to set.
170      */
171     public static void setWindowIcons(Window window) {
172         window.setIconImages(images);
173     }
174     
175     /**
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!
179      * 
180      * @param window    the window to add the listener to.
181      */
182     public static void addModelNullingListener(final Window window) {
183         window.addWindowListener(new WindowAdapter() {
184                         @Override
185                         public void windowClosed(WindowEvent e) {
186                                 setNullModels(window);
187                         }
188         });
189     }
190
191
192     
193     /**
194      * Set the best available look-and-feel into use.
195      */
196     public static void setBestLAF() {
197                 /*
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.
201                  */
202                 try {
203                         // Set system L&F
204                         UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
205                         
206                         // Check whether we have an ugly L&F
207                         LookAndFeel laf = UIManager.getLookAndFeel();
208                         if (laf == null ||
209                                         laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
210                                         laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
211                                 
212                                 // Search for better LAF
213                                 UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
214                                 String lafNames[] = {
215                                                 ".*[gG][tT][kK].*",
216                                                 ".*[wW][iI][nN].*",
217                                                 ".*[mM][aA][cC].*",
218                                                 ".*[aA][qQ][uU][aA].*",
219                                                 ".*[nN][iI][mM][bB].*"
220                                 };
221                                 
222                                 lf: for (String lafName: lafNames) {
223                                         for (UIManager.LookAndFeelInfo l: info) {
224                                                 if (l.getName().matches(lafName)) {
225                                                         UIManager.setLookAndFeel(l.getClassName());
226                                                         break lf;
227                                                 }
228                                         }                                       
229                                 }
230                         }
231                 } catch (Exception e) {
232                         System.err.println("Error setting LAF: " + e);
233                 }
234     }
235     
236     
237         /**
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.
241          * <p>
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.
244          * 
245          * @param c             the component (<code>null</code> is ok)
246          */
247         public static void setNullModels(Component c) {
248                 if (c==null)
249                         return;
250                 
251                 // Remove various listeners
252                 for (ComponentListener l: c.getComponentListeners()) {
253                         c.removeComponentListener(l);
254                 }
255                 for (FocusListener l: c.getFocusListeners()) {
256                         c.removeFocusListener(l);
257                 }
258                 for (MouseListener l: c.getMouseListeners()) {
259                         c.removeMouseListener(l);
260                 }
261                 for (PropertyChangeListener l: c.getPropertyChangeListeners()) {
262                         c.removePropertyChangeListener(l);
263                 }
264                 for (PropertyChangeListener l: c.getPropertyChangeListeners("model")) {
265                         c.removePropertyChangeListener("model", l);
266                 }
267                 for (PropertyChangeListener l: c.getPropertyChangeListeners("action")) {
268                         c.removePropertyChangeListener("action", l);
269                 }
270                 
271                 // Remove models for known components
272                 //  Why the FSCK must this be so hard?!?!?
273         
274                 if (c instanceof JSpinner) {
275                         
276                         JSpinner spinner = (JSpinner)c;
277                         for (ChangeListener l: spinner.getChangeListeners()) {
278                                 spinner.removeChangeListener(l);
279                         }
280                         spinner.setModel(new SpinnerNumberModel());
281                         
282                 } else if (c instanceof JSlider) {
283                         
284                         JSlider slider = (JSlider)c;
285                         for (ChangeListener l: slider.getChangeListeners()) {
286                                 slider.removeChangeListener(l);
287                         }
288                         slider.setModel(new DefaultBoundedRangeModel());
289                         
290                 } else if (c instanceof JComboBox) {
291                         
292                         JComboBox combo = (JComboBox)c;
293                         for (ActionListener l: combo.getActionListeners()) {
294                                 combo.removeActionListener(l);
295                         }
296                         combo.setModel(new DefaultComboBoxModel());
297                         
298                 } else if (c instanceof AbstractButton) {
299                         
300                         AbstractButton button = (AbstractButton)c;
301                         for (ActionListener l: button.getActionListeners()) {
302                                 button.removeActionListener(l);
303                         }
304                         button.setAction(new AbstractAction() {
305                                 @Override
306                                 public void actionPerformed(ActionEvent e) { }
307                         });
308                         
309                 } else if (c instanceof JTable) {
310                         
311                         JTable table = (JTable)c;
312                         table.setModel(new DefaultTableModel());
313                         table.setColumnModel(new DefaultTableColumnModel());
314                         table.setSelectionModel(new DefaultListSelectionModel());
315                         
316                 } else if (c instanceof JTree) {
317                         
318                         JTree tree = (JTree)c;
319                         tree.setModel(new DefaultTreeModel(new TreeNode() {
320                                 @SuppressWarnings("unchecked")
321                                 @Override
322                                 public Enumeration children() {
323                                         return new Vector().elements();
324                                 }
325                                 @Override
326                                 public boolean getAllowsChildren() {
327                                         return false;
328                                 }
329                                 @Override
330                                 public TreeNode getChildAt(int childIndex) {
331                                         return null;
332                                 }
333                                 @Override
334                                 public int getChildCount() {
335                                         return 0;
336                                 }
337                                 @Override
338                                 public int getIndex(TreeNode node) {
339                                         return 0;
340                                 }
341                                 @Override
342                                 public TreeNode getParent() {
343                                         return null;
344                                 }
345                                 @Override
346                                 public boolean isLeaf() {
347                                         return true;
348                                 }
349                         }));
350                         tree.setSelectionModel(new DefaultTreeSelectionModel());
351                         
352                 } else if (c instanceof Resettable) {
353                         
354                         ((Resettable)c).resetModel();
355                         
356                 }
357         
358                 // Recurse the component
359                 if (c instanceof Container) {
360                         Component[] cs = ((Container)c).getComponents();
361                         for (Component sub: cs)
362                                 setNullModels(sub);
363                 }
364         
365         }
366         
367         
368
369     
370     /**
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.
373      * <p>
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).
376      * 
377      * @author Sampo Niskanen <sampo.niskanen@iki.fi>
378      */
379     public static class BooleanTableClickListener extends MouseAdapter {
380         
381         private final JTable table;
382         private final int clickColumn;
383         private final int booleanColumn;
384         
385         
386         public BooleanTableClickListener(JTable table) {
387                 this(table, 1, 0);
388         }
389
390         
391         public BooleanTableClickListener(JTable table, int clickColumn, int booleanColumn) {
392                 this.table = table;
393                 this.clickColumn = clickColumn;
394                 this.booleanColumn = booleanColumn;
395         }
396         
397                 @Override
398                 public void mouseClicked(MouseEvent e) {
399                         if (e.getButton() != MouseEvent.BUTTON1)
400                                 return;
401                         
402                         Point p = e.getPoint();
403                         int col = table.columnAtPoint(p);
404                         if (col < 0)
405                                 return;
406                         col = table.convertColumnIndexToModel(col);
407                         if (col != clickColumn) 
408                                 return;
409                         
410                         int row = table.rowAtPoint(p);
411                         if (row < 0)
412                                 return;
413                         row = table.convertRowIndexToModel(row);
414                         if (row < 0)
415                                 return;
416                         
417                         TableModel model = table.getModel();
418                         Object value = model.getValueAt(row, booleanColumn);
419                         
420                         if (!(value instanceof Boolean)) {
421                                 throw new IllegalStateException("Table value at row="+row+" col="+
422                                                 booleanColumn + " is not a Boolean, value=" +value);
423                         }
424                         
425                         Boolean b = (Boolean)value;
426                         b = !b;
427                         model.setValueAt(b, row, booleanColumn);
428                         if (model instanceof AbstractTableModel) {
429                                 ((AbstractTableModel)model).fireTableCellUpdated(row, booleanColumn);
430                         }
431                 }
432
433     }
434     
435 }