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