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