001/*
002 * $Id: SwingXUtilities.java 3886 2010-11-16 16:28:58Z 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 */
021package org.jdesktop.swingx;
022
023import java.awt.Color;
024import java.awt.Component;
025import java.awt.Container;
026import java.awt.Dimension;
027import java.awt.Font;
028import java.awt.Graphics2D;
029import java.awt.Insets;
030import java.awt.Point;
031import java.awt.Rectangle;
032import java.awt.Window;
033import java.awt.event.InputEvent;
034import java.awt.event.MouseEvent;
035import java.io.IOException;
036import java.io.StringReader;
037import java.lang.reflect.InvocationTargetException;
038import java.lang.reflect.Method;
039import java.util.Locale;
040import java.util.concurrent.Callable;
041import java.util.concurrent.ExecutionException;
042import java.util.concurrent.FutureTask;
043
044import javax.swing.InputMap;
045import javax.swing.JComponent;
046import javax.swing.JList;
047import javax.swing.JPopupMenu;
048import javax.swing.KeyStroke;
049import javax.swing.ListCellRenderer;
050import javax.swing.ListModel;
051import javax.swing.ListSelectionModel;
052import javax.swing.MenuElement;
053import javax.swing.RepaintManager;
054import javax.swing.SwingUtilities;
055import javax.swing.plaf.ComponentInputMapUIResource;
056import javax.swing.plaf.UIResource;
057import javax.swing.text.html.HTMLDocument;
058
059import org.jdesktop.swingx.painter.Painter;
060import org.jdesktop.swingx.plaf.PainterUIResource;
061
062/**
063 * A collection of utility methods for Swing(X) classes.
064 * 
065 * <ul>
066 * PENDING JW: think about location of this class and/or its methods, Options:
067 * 
068 *  <li> move this class to the swingx utils package which already has a bunch of xxUtils
069 *  <li> move methods between xxUtils classes as appropriate (one window/comp related util)
070 *  <li> keep here in swingx (consistent with swingutilities in core)
071 * </ul>
072 * @author Karl George Schaefer
073 */
074public final class SwingXUtilities {
075    private SwingXUtilities() {
076        //does nothing
077    }
078
079
080    /**
081     * A helper for creating and updating key bindings for components with
082     * mnemonics. The {@code pressed} action will be invoked when the mnemonic
083     * is activated.
084     * <p>
085     * TODO establish an interface for the mnemonic properties, such as {@code
086     * MnemonicEnabled} and change signature to {@code public static <T extends
087     * JComponent & MnemonicEnabled> void updateMnemonicBinding(T c, String
088     * pressed)}
089     * 
090     * @param c
091     *            the component bindings to update
092     * @param pressed
093     *            the name of the action in the action map to invoke when the
094     *            mnemonic is pressed
095     * @throws NullPointerException
096     *             if the component is {@code null}
097     */
098    public static void updateMnemonicBinding(JComponent c, String pressed) {
099        updateMnemonicBinding(c, pressed, null);
100    }
101    
102    /**
103     * A helper for creating and updating key bindings for components with
104     * mnemonics. The {@code pressed} action will be invoked when the mnemonic
105     * is activated and the {@code released} action will be invoked when the
106     * mnemonic is deactivated.
107     * <p>
108     * TODO establish an interface for the mnemonic properties, such as {@code
109     * MnemonicEnabled} and change signature to {@code public static <T extends
110     * JComponent & MnemonicEnabled> void updateMnemonicBinding(T c, String
111     * pressed, String released)}
112     * 
113     * @param c
114     *            the component bindings to update
115     * @param pressed
116     *            the name of the action in the action map to invoke when the
117     *            mnemonic is pressed
118     * @param released
119     *            the name of the action in the action map to invoke when the
120     *            mnemonic is released (if the action is a toggle style, then
121     *            this parameter should be {@code null})
122     * @throws NullPointerException
123     *             if the component is {@code null}
124     */
125    public static void updateMnemonicBinding(JComponent c, String pressed, String released) {
126        Class<?> clazz = c.getClass();
127        int m = -1;
128        
129        try {
130            Method mtd = clazz.getMethod("getMnemonic");
131            m = (Integer) mtd.invoke(c);
132        } catch (RuntimeException e) {
133            throw e;
134        } catch (Exception e) {
135            throw new IllegalArgumentException("unable to access mnemonic", e);
136        }
137        
138        InputMap map = SwingUtilities.getUIInputMap(c,
139                JComponent.WHEN_IN_FOCUSED_WINDOW);
140        
141        if (m != 0) {
142            if (map == null) {
143                map = new ComponentInputMapUIResource(c);
144                SwingUtilities.replaceUIInputMap(c,
145                        JComponent.WHEN_IN_FOCUSED_WINDOW, map);
146            }
147            
148            map.clear();
149            
150            //TODO is ALT_MASK right for all platforms?
151            map.put(KeyStroke.getKeyStroke(m,  InputEvent.ALT_MASK, false),
152                    pressed);
153            map.put(KeyStroke.getKeyStroke(m, InputEvent.ALT_MASK, true),
154                    released);
155            map.put(KeyStroke.getKeyStroke(m, 0, true), released);
156        } else {
157            if (map != null) {
158                map.clear();
159            }
160        }
161    }
162    
163    static <C extends JComponent & BackgroundPaintable> void installBackground(C comp, Color color) {
164        if (isUIInstallable(color)) {
165            //only handle UIResource, if null then painter isn't painted; this allows optimized code paths
166            if (comp.getBackgroundPainter() instanceof UIResource) {
167                comp.setBackgroundPainter(new PainterUIResource<JComponent>(new BackgroundPainter(color)));
168            }
169            //does nothing otherwise; do not install UIResource Color over a non-UIResource Painter
170        } else {
171            comp.setBackgroundPainter(new BackgroundPainter(color));
172        }
173    }
174    
175    @SuppressWarnings("unchecked")
176    static <C extends JComponent & BackgroundPaintable> void paintBackground(C comp, Graphics2D g) {
177        Painter<? super C> painter = comp.getBackgroundPainter();
178        
179        if (painter instanceof BackgroundPainter) {
180            //ignore paintBorderInsets for BackgroundPainter
181            painter.paint(g, comp, comp.getWidth(), comp.getHeight());
182        } else if (painter != null) {
183            if (comp.isPaintBorderInsets()) {
184                painter.paint(g, comp, comp.getWidth(), comp.getHeight());
185            } else {
186                Insets insets = comp.getInsets();
187                g.translate(insets.left, insets.top);
188                painter.paint(g, comp, comp.getWidth() - insets.left - insets.right,
189                        comp.getHeight() - insets.top - insets.bottom);
190                g.translate(-insets.left, -insets.top);
191            }
192        }
193    }
194    
195    private static Component[] getChildren(Component c) {
196        Component[] children = null;
197        
198        if (c instanceof MenuElement) {
199            MenuElement[] elements = ((MenuElement) c).getSubElements();
200            children = new Component[elements.length];
201            
202            for (int i = 0; i < elements.length; i++) {
203                children[i] = elements[i].getComponent();
204            }
205        } else if (c instanceof Container) {
206            children = ((Container) c).getComponents();
207        }
208        
209        return children;
210    }
211    
212    /**
213     * Enables or disables of the components in the tree starting with {@code c}.
214     * 
215     * @param c
216     *                the starting component
217     * @param enabled
218     *                {@code true} if the component is to enabled; {@code false} otherwise
219     */
220    public static void setComponentTreeEnabled(Component c, boolean enabled) {
221        c.setEnabled(enabled);
222        
223        Component[] children = getChildren(c);
224            
225        if (children != null) {
226            for(int i = 0; i < children.length; i++) {
227                setComponentTreeEnabled(children[i], enabled);
228            }
229        }
230    }
231    
232    /**
233     * Sets the locale for an entire component hierarchy to the specified
234     * locale.
235     * 
236     * @param c
237     *                the starting component
238     * @param locale
239     *                the locale to set
240     */
241    public static void setComponentTreeLocale(Component c, Locale locale) {
242        c.setLocale(locale);
243        
244        Component[] children = getChildren(c);
245        
246        if (children != null) {
247            for(int i = 0; i < children.length; i++) {
248                setComponentTreeLocale(children[i], locale);
249            }
250        }
251    }
252
253    /**
254     * Sets the background for an entire component hierarchy to the specified
255     * color.
256     * 
257     * @param c
258     *                the starting component
259     * @param color
260     *                the color to set
261     */
262    public static void setComponentTreeBackground(Component c, Color color) {
263        c.setBackground(color);
264        
265        Component[] children = getChildren(c);
266        
267        if (children != null) {
268            for(int i = 0; i < children.length; i++) {
269                setComponentTreeBackground(children[i], color);
270            }
271        }
272    }
273
274    /**
275     * Sets the foreground for an entire component hierarchy to the specified
276     * color.
277     * 
278     * @param c
279     *                the starting component
280     * @param color
281     *                the color to set
282     */
283    public static void setComponentTreeForeground(Component c, Color color) {
284        c.setForeground(color);
285        
286        Component[] children = getChildren(c);
287        
288        if (children != null) {
289            for(int i = 0; i < children.length; i++) {
290                setComponentTreeForeground(children[i], color);
291            }
292        }
293    }
294
295    /**
296     * Sets the font for an entire component hierarchy to the specified font.
297     * 
298     * @param c
299     *            the starting component
300     * @param font
301     *            the font to set
302     */
303    public static void setComponentTreeFont(Component c, Font font) {
304        c.setFont(font);
305        
306        Component[] children = getChildren(c);
307        
308        if (children != null) {
309            for(int i = 0; i < children.length; i++) {
310                setComponentTreeFont(children[i], font);
311            }
312        }
313    }
314
315    private static String STYLESHEET = 
316        "body { margin-top: 0; margin-bottom: 0; margin-left: 0; margin-right: 0;"
317        + " font-family: %s; font-size: %dpt;  }"
318        + "a, p, li { margin-top: 0; margin-bottom: 0; margin-left: 0;"
319        + " margin-right: 0; font-family: %s; font-size: %dpt;  }";
320    
321    /**
322     * Sets the font used for HTML displays to the specified font. Components
323     * that display HTML do not necessarily honor font properties, since the
324     * HTML document can override these values. Calling {@code setHtmlFont}
325     * after the data is set will force the HTML display to use the font
326     * specified to this method.
327     * 
328     * @param doc
329     *            the HTML document to update
330     * @param font
331     *            the font to use
332     * @throws NullPointerException
333     *             if any parameter is {@code null}
334     */
335    public static void setHtmlFont(HTMLDocument doc, Font font) {
336        String stylesheet = String.format(STYLESHEET, font.getName(),
337                font.getSize(), font.getName(), font.getSize());
338        
339        try {
340            doc.getStyleSheet().loadRules(new StringReader(stylesheet), null);
341        } catch (IOException e) {
342            //this should never happen with our sheet
343            throw new IllegalStateException(e);
344        }
345    }
346    
347    /**
348     * Updates the componentTreeUI of all top-level windows of the 
349     * current application.
350     * 
351     */
352    public static void updateAllComponentTreeUIs() {
353//        for (Frame frame : Frame.getFrames()) {
354//            updateAllComponentTreeUIs(frame);
355//        }
356        // JW: updated to new 1.6 api - returns all windows, owned and ownerless
357        for (Window window: Window.getWindows()) {
358            SwingUtilities.updateComponentTreeUI(window);
359        }
360    }
361
362
363
364    /**
365     * Updates the componentTreeUI of the given window and all its
366     * owned windows, recursively.
367     * 
368     * 
369     * @param window the window to update
370     */
371    public static void updateAllComponentTreeUIs(Window window) {
372        SwingUtilities.updateComponentTreeUI(window);
373        for (Window owned : window.getOwnedWindows()) {
374            updateAllComponentTreeUIs(owned);
375        }
376    }
377
378    /**
379     * A version of {@link SwingUtilities#invokeLater(Runnable)} that supports return values.
380     * 
381     * @param <T>
382     *            the return type of the callable
383     * @param callable
384     *            the callable to execute
385     * @return a future task for accessing the return value
386     * @see Callable
387     */
388    public static <T> FutureTask<T> invokeLater(Callable<T> callable) {
389        FutureTask<T> task = new FutureTask<T>(callable);
390        
391        SwingUtilities.invokeLater(task);
392        
393        return task;
394    }
395
396    /**
397     * A version of {@link SwingUtilities#invokeAndWait(Runnable)} that supports return values.
398     * 
399     * @param <T>
400     *            the return type of the callable
401     * @param callable
402     *            the callable to execute
403     * @return the value returned by the callable
404     * @throws InterruptedException
405     *             if we're interrupted while waiting for the event dispatching thread to finish
406     *             executing {@code callable.call()}
407     * @throws InvocationTargetException
408     *                if an exception is thrown while running {@code callable}
409     * @see Callable
410     */
411    public static <T> T invokeAndWait(Callable<T> callable) throws InterruptedException,
412            InvocationTargetException {
413        try {
414            //blocks until future returns
415            return invokeLater(callable).get();
416        } catch (ExecutionException e) {
417            Throwable t = e.getCause();
418            
419            if (t instanceof RuntimeException) {
420                throw (RuntimeException) t;
421            } else if (t instanceof InvocationTargetException) {
422                throw (InvocationTargetException) t;
423            } else {
424                throw new InvocationTargetException(t);
425            }
426        }
427    }
428
429    /**
430     * An improved version of
431     * {@link SwingUtilities#getAncestorOfClass(Class, Component)}. This method
432     * traverses {@code JPopupMenu} invoker and uses generics to return an
433     * appropriately typed object.
434     * 
435     * @param <T>
436     *            the type of ancestor to find
437     * @param clazz
438     *            the class instance of the ancestor to find
439     * @param c
440     *            the component to start the search from
441     * @return an ancestor of the correct type or {@code null} if no such
442     *         ancestor exists. This method also returns {@code null} if any
443     *         parameter is {@code null}.
444     */
445    @SuppressWarnings("unchecked")
446    public static <T> T getAncestor(Class<T> clazz, Component c) {
447        if (clazz == null || c == null) {
448            return null;
449        }
450        
451        Component parent = c.getParent();
452
453        while (parent != null && !(clazz.isInstance(parent))) {
454            parent = parent instanceof JPopupMenu
455                    ? ((JPopupMenu) parent).getInvoker() : parent.getParent();
456        }
457        
458        return (T) parent;
459    }
460
461    /**
462     * Returns whether the component is part of the parent's
463     * container hierarchy. If a parent in the chain is of type 
464     * JPopupMenu, the parent chain of its invoker is walked.
465     * 
466     * @param focusOwner
467     * @param parent
468     * @return true if the component is contained under the parent's 
469     *    hierarchy, coping with JPopupMenus.
470     */
471    public static boolean isDescendingFrom(Component focusOwner, Component parent) {
472        while (focusOwner !=  null) {
473            if (focusOwner instanceof JPopupMenu) {
474                focusOwner = ((JPopupMenu) focusOwner).getInvoker();
475                if (focusOwner == null) {
476                    return false;
477                }
478            }
479            if (focusOwner == parent) {
480                return true;
481            }
482            focusOwner = focusOwner.getParent();
483        }
484        return false;
485    }
486
487    /**
488     * Obtains a {@code TranslucentRepaintManager} from the specified manager.
489     * If the current manager is a {@code TranslucentRepaintManager} or a
490     * {@code ForwardingRepaintManager} that contains a {@code
491     * TranslucentRepaintManager}, then the passed in manager is returned.
492     * Otherwise a new repaint manager is created and returned.
493     * 
494     * @param delegate
495     *            the current repaint manager
496     * @return a non-{@code null} {@code TranslucentRepaintManager}
497     * @throws NullPointerException if {@code delegate} is {@code null}
498     */
499    static RepaintManager getTranslucentRepaintManager(RepaintManager delegate) {
500        RepaintManager manager = delegate;
501        
502        while (manager != null && !manager.getClass().isAnnotationPresent(TranslucentRepaintManager.class)) {
503            if (manager instanceof ForwardingRepaintManager) {
504                manager = ((ForwardingRepaintManager) manager).getDelegateManager();
505            } else {
506                manager = null;
507            }
508        }
509        
510        return manager == null ? new RepaintManagerX(delegate) : delegate;
511    }
512    
513    /**
514     * Checks and returns whether the given property should be replaced
515     * by the UI's default value. 
516     * 
517     * @param property the property to check.
518     * @return true if the given property should be replaced by the UI's
519     *   default value, false otherwise. 
520     */
521    public static boolean isUIInstallable(Object property) {
522       return (property == null) || (property instanceof UIResource);
523    }
524
525//---- methods c&p'ed from SwingUtilities2 to reduce dependencies on sun packages
526    
527    /**
528     * Updates lead and anchor selection index without changing the selection.
529     * 
530     * Note: this is c&p'ed from SwingUtilities2 to not have any direct
531     * dependency.
532     * 
533     * @param selectionModel the selection model to change lead/anchor
534     * @param lead the lead selection index
535     * @param anchor the anchor selection index
536     */
537    public static void setLeadAnchorWithoutSelection(
538            ListSelectionModel selectionModel, int lead, int anchor) {
539        if (anchor == -1) {
540            anchor = lead;
541        }
542        if (lead == -1) {
543            selectionModel.setAnchorSelectionIndex(-1);
544            selectionModel.setLeadSelectionIndex(-1);
545        } else {
546            if (selectionModel.isSelectedIndex(lead))
547                selectionModel.addSelectionInterval(lead, lead);
548            else {
549                selectionModel.removeSelectionInterval(lead, lead);
550            }
551            selectionModel.setAnchorSelectionIndex(anchor);
552        }
553    }
554
555    public static boolean shouldIgnore(MouseEvent mouseEvent,
556            JComponent component) {
557        return ((component == null) || (!(component.isEnabled()))
558                || (!(SwingUtilities.isLeftMouseButton(mouseEvent))) 
559                || (mouseEvent.isConsumed()));
560    }
561
562    
563    public static int loc2IndexFileList(JList list, Point point) {
564        int i = list.locationToIndex(point);
565        if (i != -1) {
566            Object localObject = list
567                    .getClientProperty("List.isFileList");
568            if ((localObject instanceof Boolean)
569                    && (((Boolean) localObject).booleanValue())
570    // PENDING JW: this isn't aware of sorting/filtering - fix!
571                    && (!(pointIsInActualBounds(list, i, point)))) {
572                i = -1;
573            }
574        }
575        return i;
576    }
577
578    // PENDING JW: this isn't aware of sorting/filtering - fix!
579    private static boolean pointIsInActualBounds(JList list, int index,
580            Point point) {
581        ListCellRenderer renderer = list.getCellRenderer();
582        ListModel model = list.getModel();
583        Object element = model.getElementAt(index);
584        Component comp = renderer.getListCellRendererComponent(list, element,
585                index, false, false);
586
587        Dimension prefSize = comp.getPreferredSize();
588        Rectangle cellBounds = list.getCellBounds(index, index);
589        if (!(comp.getComponentOrientation().isLeftToRight())) {
590            cellBounds.x += cellBounds.width - prefSize.width;
591        }
592        cellBounds.width = prefSize.width;
593
594        return cellBounds.contains(point);
595    }
596
597    public static void adjustFocus(JComponent component) {
598        if ((!(component.hasFocus())) && (component.isRequestFocusEnabled()))
599            component.requestFocus();
600    }
601
602    public static int convertModifiersToDropAction(int modifiers,
603            int sourcActions) {
604        // PENDING JW: c'p from a decompiled SunDragSourceContextPeer
605        // PENDING JW: haha ... completely readable, right ;-)
606        int i = 0;
607
608        switch (modifiers & 0xC0) {
609        case 192:
610            i = 1073741824;
611            break;
612        case 128:
613            i = 1;
614            break;
615        case 64:
616            i = 2;
617            break;
618        default:
619            if ((sourcActions & 0x2) != 0) {
620                i = 2;
621                break;
622            }
623            if ((sourcActions & 0x1) != 0) {
624                i = 1;
625                break;
626            }
627            if ((sourcActions & 0x40000000) == 0)
628                break;
629            i = 1073741824;
630        }
631
632        // label88:
633        return (i & sourcActions);
634    }
635
636}