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