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