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