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