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