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