001    /*
002     * $Id: SwingXUtilities.java 3359 2009-06-12 18:37:03Z kschaefe $
003     *
004     * Copyright 2008 Sun Microsystems, Inc., 4150 Network Circle,
005     * Santa Clara, California 95054, U.S.A. All rights reserved.
006     *
007     * This library is free software; you can redistribute it and/or
008     * modify it under the terms of the GNU Lesser General Public
009     * License as published by the Free Software Foundation; either
010     * version 2.1 of the License, or (at your option) any later version.
011     * 
012     * This library is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015     * Lesser General Public License for more details.
016     * 
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this library; if not, write to the Free Software
019     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020     */
021    package org.jdesktop.swingx;
022    
023    import java.awt.Color;
024    import java.awt.Component;
025    import java.awt.Container;
026    import java.awt.Font;
027    import java.awt.Frame;
028    import java.awt.Window;
029    import java.awt.event.InputEvent;
030    import java.io.IOException;
031    import java.io.StringReader;
032    import java.lang.reflect.Method;
033    import java.util.Locale;
034    
035    import javax.swing.InputMap;
036    import javax.swing.JComponent;
037    import javax.swing.JPopupMenu;
038    import javax.swing.KeyStroke;
039    import javax.swing.MenuElement;
040    import javax.swing.RepaintManager;
041    import javax.swing.SwingUtilities;
042    import javax.swing.plaf.ComponentInputMapUIResource;
043    import javax.swing.plaf.UIResource;
044    import javax.swing.text.html.HTMLDocument;
045    
046    /**
047     * A collection of utility methods for Swing(X) classes.
048     * 
049     * <ul>
050     * PENDING JW: think about location of this class and/or its methods, Options:
051     * 
052     *  <li> move this class to the swingx utils package which already has a bunch of xxUtils
053     *  <li> move methods between xxUtils classes as appropriate (one window/comp related util)
054     *  <li> keep here in swingx (consistent with swingutilities in core)
055     * </ul>
056     * @author Karl George Schaefer
057     */
058    public final class SwingXUtilities {
059        private SwingXUtilities() {
060            //does nothing
061        }
062    
063    
064        /**
065         * A helper for creating and updating key bindings for components with
066         * mnemonics. The {@code pressed} action will be invoked when the mnemonic
067         * is activated.
068         * <p>
069         * TODO establish an interface for the mnemonic properties, such as {@code
070         * MnemonicEnabled} and change signature to {@code public static <T extends
071         * JComponent & MnemonicEnabled> void updateMnemonicBinding(T c, String
072         * pressed)}
073         * 
074         * @param c
075         *            the component bindings to update
076         * @param pressed
077         *            the name of the action in the action map to invoke when the
078         *            mnemonic is pressed
079         * @throws NullPointerException
080         *             if the component is {@code null}
081         */
082        public static void updateMnemonicBinding(JComponent c, String pressed) {
083            updateMnemonicBinding(c, pressed, null);
084        }
085        
086        /**
087         * A helper for creating and updating key bindings for components with
088         * mnemonics. The {@code pressed} action will be invoked when the mnemonic
089         * is activated and the {@code released} action will be invoked when the
090         * mnemonic is deactivated.
091         * <p>
092         * TODO establish an interface for the mnemonic properties, such as {@code
093         * MnemonicEnabled} and change signature to {@code public static <T extends
094         * JComponent & MnemonicEnabled> void updateMnemonicBinding(T c, String
095         * pressed, String released)}
096         * 
097         * @param c
098         *            the component bindings to update
099         * @param pressed
100         *            the name of the action in the action map to invoke when the
101         *            mnemonic is pressed
102         * @param released
103         *            the name of the action in the action map to invoke when the
104         *            mnemonic is released (if the action is a toggle style, then
105         *            this parameter should be {@code null})
106         * @throws NullPointerException
107         *             if the component is {@code null}
108         */
109        public static void updateMnemonicBinding(JComponent c, String pressed, String released) {
110            Class<?> clazz = c.getClass();
111            int m = -1;
112            
113            try {
114                Method mtd = clazz.getMethod("getMnemonic");
115                m = (Integer) mtd.invoke(c);
116            } catch (RuntimeException e) {
117                throw e;
118            } catch (Exception e) {
119                throw new IllegalArgumentException("unable to access mnemonic", e);
120            }
121            
122            InputMap map = SwingUtilities.getUIInputMap(c,
123                    JComponent.WHEN_IN_FOCUSED_WINDOW);
124            
125            if (m != 0) {
126                if (map == null) {
127                    map = new ComponentInputMapUIResource(c);
128                    SwingUtilities.replaceUIInputMap(c,
129                            JComponent.WHEN_IN_FOCUSED_WINDOW, map);
130                }
131                
132                map.clear();
133                
134                //TODO is ALT_MASK right for all platforms?
135                map.put(KeyStroke.getKeyStroke(m, InputEvent.ALT_MASK, false),
136                        pressed);
137                map.put(KeyStroke.getKeyStroke(m, InputEvent.ALT_MASK, true),
138                        released);
139                map.put(KeyStroke.getKeyStroke(m, 0, true), released);
140            } else {
141                if (map != null) {
142                    map.clear();
143                }
144            }
145        }
146        
147        private static Component[] getChildren(Component c) {
148            Component[] children = null;
149            
150            if (c instanceof MenuElement) {
151                MenuElement[] elements = ((MenuElement) c).getSubElements();
152                children = new Component[elements.length];
153                
154                for (int i = 0; i < elements.length; i++) {
155                    children[i] = elements[i].getComponent();
156                }
157            } else if (c instanceof Container) {
158                children = ((Container) c).getComponents();
159            }
160            
161            return children;
162        }
163        
164        /**
165         * Enables or disables of the components in the tree starting with {@code c}.
166         * 
167         * @param c
168         *                the starting component
169         * @param enabled
170         *                {@code true} if the component is to enabled; {@code false} otherwise
171         */
172        public static void setComponentTreeEnabled(Component c, boolean enabled) {
173            c.setEnabled(enabled);
174            
175            Component[] children = getChildren(c);
176                
177            if (children != null) {
178                for(int i = 0; i < children.length; i++) {
179                    setComponentTreeEnabled(children[i], enabled);
180                }
181            }
182        }
183        
184        /**
185         * Sets the locale for an entire component hierarchy to the specified
186         * locale.
187         * 
188         * @param c
189         *                the starting component
190         * @param locale
191         *                the locale to set
192         */
193        public static void setComponentTreeLocale(Component c, Locale locale) {
194            c.setLocale(locale);
195            
196            Component[] children = getChildren(c);
197            
198            if (children != null) {
199                for(int i = 0; i < children.length; i++) {
200                    setComponentTreeLocale(children[i], locale);
201                }
202            }
203        }
204    
205        /**
206         * Sets the background for an entire component hierarchy to the specified
207         * color.
208         * 
209         * @param c
210         *                the starting component
211         * @param color
212         *                the color to set
213         */
214        public static void setComponentTreeBackground(Component c, Color color) {
215            c.setBackground(color);
216            
217            Component[] children = getChildren(c);
218            
219            if (children != null) {
220                for(int i = 0; i < children.length; i++) {
221                    setComponentTreeBackground(children[i], color);
222                }
223            }
224        }
225    
226        /**
227         * Sets the foreground for an entire component hierarchy to the specified
228         * color.
229         * 
230         * @param c
231         *                the starting component
232         * @param color
233         *                the color to set
234         */
235        public static void setComponentTreeForeground(Component c, Color color) {
236            c.setForeground(color);
237            
238            Component[] children = getChildren(c);
239            
240            if (children != null) {
241                for(int i = 0; i < children.length; i++) {
242                    setComponentTreeForeground(children[i], color);
243                }
244            }
245        }
246    
247        /**
248         * Sets the font for an entire component hierarchy to the specified font.
249         * 
250         * @param c
251         *            the starting component
252         * @param font
253         *            the font to set
254         */
255        public static void setComponentTreeFont(Component c, Font font) {
256            c.setFont(font);
257            
258            Component[] children = getChildren(c);
259            
260            if (children != null) {
261                for(int i = 0; i < children.length; i++) {
262                    setComponentTreeFont(children[i], font);
263                }
264            }
265        }
266    
267        private static String STYLESHEET = 
268            "body { margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0;"
269            + " font-family: %s; font-size: %dpt;  }"
270            + "a, p, li { margin-top: 0; margin-bottom: 0; margin-left: 0;"
271            + " margin-right: 0; font-family: %s; font-size: %dpt;  }";
272        
273        /**
274         * Sets the font used for HTML displays to the specified font. Components
275         * that display HTML do not necessarily honor font properties, since the
276         * HTML document can override these values. Calling {@code setHtmlFont}
277         * after the data is set will force the HTML display to use the font
278         * specified to this method.
279         * 
280         * @param doc
281         *            the HTML document to update
282         * @param font
283         *            the font to use
284         * @throws NullPointerException
285         *             if any parameter is {@code null}
286         */
287        public static void setHtmlFont(HTMLDocument doc, Font font) {
288            String stylesheet = String.format(STYLESHEET, font.getName(),
289                    font.getSize(), font.getName(), font.getSize());
290            
291            try {
292                doc.getStyleSheet().loadRules(new StringReader(stylesheet), null);
293            } catch (IOException e) {
294                //this should never happen with our sheet
295                throw new IllegalStateException(e);
296            }
297        }
298        
299        /**
300         * Updates the componentTreeUI of all top-level windows of the 
301         * current application.
302         * 
303         */
304        public static void updateAllComponentTreeUIs() {
305            for (Frame frame : Frame.getFrames()) {
306                updateAllComponentTreeUIs(frame);
307            }
308            
309        }
310    
311    
312    
313        /**
314         * Updates the componentTreeUI of the given window and all its
315         * owned windows, recursively.
316         * 
317         * 
318         * @param window the window to update
319         */
320        public static void updateAllComponentTreeUIs(Window window) {
321            SwingUtilities.updateComponentTreeUI(window);
322            for (Window owned : window.getOwnedWindows()) {
323                updateAllComponentTreeUIs(owned);
324            }
325        }
326    
327        /**
328         * An improved version of
329         * {@link SwingUtilities#getAncestorOfClass(Class, Component)}. This method
330         * traverses {@code JPopupMenu} invoker and uses generics to return an
331         * appropriately typed object.
332         * 
333         * @param <T>
334         *            the type of ancestor to find
335         * @param clazz
336         *            the class instance of the ancestor to find
337         * @param c
338         *            the component to start the search from
339         * @return an ancestor of the correct type or {@code null} if no such
340         *         ancestor exists. This method also returns {@code null} if any
341         *         parameter is {@code null}.
342         */
343        @SuppressWarnings("unchecked")
344        public static <T> T getAncestor(Class<T> clazz, Component c) {
345            if (clazz == null || c == null) {
346                return null;
347            }
348            
349            Component parent = c.getParent();
350    
351            while (parent != null && !(clazz.isInstance(parent))) {
352                parent = parent instanceof JPopupMenu
353                        ? ((JPopupMenu) parent).getInvoker() : parent.getParent();
354            }
355            
356            return (T) parent;
357        }
358    
359        /**
360         * Returns whether the component is part of the parent's
361         * container hierarchy. If a parent in the chain is of type 
362         * JPopupMenu, the parent chain of its invoker is walked.
363         * 
364         * @param focusOwner
365         * @param parent
366         * @return true if the component is contained under the parent's 
367         *    hierarchy, coping with JPopupMenus.
368         */
369        public static boolean isDescendingFrom(Component focusOwner, Component parent) {
370            while (focusOwner !=  null) {
371                if (focusOwner instanceof JPopupMenu) {
372                    focusOwner = ((JPopupMenu) focusOwner).getInvoker();
373                    if (focusOwner == null) {
374                        return false;
375                    }
376                }
377                if (focusOwner == parent) {
378                    return true;
379                }
380                focusOwner = focusOwner.getParent();
381            }
382            return false;
383        }
384    
385        /**
386         * Obtains a {@code TranslucentRepaintManager} from the specified manager.
387         * If the current manager is a {@code TranslucentRepaintManager} or a
388         * {@code ForwardingRepaintManager} that contains a {@code
389         * TranslucentRepaintManager}, then the passed in manager is returned.
390         * Otherwise a new repaint manager is created and returned.
391         * 
392         * @param delegate
393         *            the current repaint manager
394         * @return a non-{@code null} {@code TranslucentRepaintManager}
395         * @throws NullPointerException if {@code delegate} is {@code null}
396         */
397        static RepaintManager getTranslucentRepaintManager(RepaintManager delegate) {
398            RepaintManager manager = delegate;
399            
400            while (manager != null && !manager.getClass().isAnnotationPresent(TranslucentRepaintManager.class)) {
401                if (manager instanceof ForwardingRepaintManager) {
402                    manager = ((ForwardingRepaintManager) manager).getDelegateManager();
403                } else {
404                    manager = null;
405                }
406            }
407            
408            return manager == null ? new RepaintManagerX(delegate) : delegate;
409        }
410        
411        /**
412         * Checks and returns whether the given property should be replaced
413         * by the UI's default value. 
414         * 
415         * @param property the property to check.
416         * @return true if the given property should be replaced by the UI's
417         *   default value, false otherwise. 
418         */
419        public static boolean isUIInstallable(Object property) {
420           return (property == null) || (property instanceof UIResource);
421        }
422    
423    }