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