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