001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2016
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv.util;
030
031import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
032import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newHashSet;
033
034import static javax.swing.GroupLayout.DEFAULT_SIZE;
035import static javax.swing.GroupLayout.PREFERRED_SIZE;
036import static javax.swing.GroupLayout.Alignment.BASELINE;
037import static javax.swing.GroupLayout.Alignment.LEADING;
038import static javax.swing.LayoutStyle.ComponentPlacement.RELATED;
039
040import java.awt.Color;
041import java.awt.Component;
042import java.awt.Container;
043import java.awt.Dimension;
044import java.awt.Font;
045import java.awt.Graphics;
046import java.awt.Image;
047import java.awt.Rectangle;
048import java.awt.event.HierarchyEvent;
049import java.lang.reflect.InvocationTargetException;
050import java.lang.reflect.Method;
051import java.util.ArrayList;
052import java.util.Collection;
053import java.util.Collections;
054import java.util.EventObject;
055import java.util.HashMap;
056import java.util.HashSet;
057import java.util.List;
058import java.util.Map;
059import java.util.Map.Entry;
060import java.util.Set;
061import java.util.regex.Pattern;
062
063import javax.swing.GroupLayout;
064import javax.swing.GroupLayout.ParallelGroup;
065import javax.swing.GroupLayout.SequentialGroup;
066import javax.swing.ImageIcon;
067import javax.swing.JButton;
068import javax.swing.JComboBox;
069import javax.swing.JComponent;
070import javax.swing.JLabel;
071import javax.swing.JMenuItem;
072import javax.swing.JPanel;
073import javax.swing.JTextField;
074import javax.swing.SwingConstants;
075import javax.swing.SwingUtilities;
076import javax.swing.UIDefaults;
077import javax.swing.text.JTextComponent;
078
079import org.slf4j.Logger;
080import org.slf4j.LoggerFactory;
081
082import ucar.unidata.idv.MapViewManager;
083import ucar.unidata.idv.ViewManager;
084import ucar.unidata.idv.ui.IdvComponentGroup;
085import ucar.unidata.idv.ui.IdvComponentHolder;
086import ucar.unidata.idv.ui.IdvWindow;
087import ucar.unidata.idv.ui.WindowInfo;
088import ucar.unidata.ui.ComponentHolder;
089import ucar.unidata.ui.MultiFrame;
090import ucar.unidata.util.GuiUtils;
091
092import edu.wisc.ssec.mcidasv.Constants;
093import edu.wisc.ssec.mcidasv.McIDASV;
094import edu.wisc.ssec.mcidasv.ViewManagerManager;
095import edu.wisc.ssec.mcidasv.ui.McvComponentGroup;
096import edu.wisc.ssec.mcidasv.ui.McvComponentHolder;
097import edu.wisc.ssec.mcidasv.ui.UIManager;
098
099/**
100 * McIDAS-V's collection of GUI utility methods
101 */
102public class McVGuiUtils implements Constants {
103
104//    private static final Logger logger = LoggerFactory.getLogger(McVGuiUtils.class);
105
106    /** 
107     * Estimated number of {@link ucar.unidata.idv.ViewManager ViewManagers}.
108     * This value is only used as a last resort ({@link McIDASV#getStaticMcv()} failing). 
109     */
110    private static final int ESTIMATED_VM_COUNT = 32;
111
112    public enum Width { HALF, SINGLE, ONEHALF, DOUBLE, TRIPLE, QUADRUPLE, DOUBLEDOUBLE }
113
114    public enum Position { LEFT, RIGHT, CENTER }
115
116    public enum Prefer { TOP, BOTTOM, NEITHER }
117
118    public enum TextColor { NORMAL, STATUS }
119
120    private McVGuiUtils() {}
121
122    /**
123     * Use this class to create a panel with a background image
124     * @author davep
125     *
126     */
127    public static class IconPanel extends JPanel {
128        private Image img;
129
130        public IconPanel(String img) {
131            this(GuiUtils.getImageIcon(img).getImage());
132        }
133
134        public IconPanel(Image img) {
135            this.img = img;
136            Dimension size = new Dimension(img.getWidth(null), img.getHeight(null));
137            setPreferredSize(size);
138            setMinimumSize(size);
139            setMaximumSize(size);
140            setSize(size);
141            setLayout(null);
142        }
143
144        public void paintComponent(Graphics g) {
145            super.paintComponent(g);
146            g.drawImage(img, 0, 0, null);
147        }
148
149    }
150
151    /**
152     * Create a standard sized, right-justified label
153     *
154     * @param title Label text. Should not be {@code null}.
155     *
156     * @return A new label.
157     */
158    public static JLabel makeLabelRight(String title) {
159        return makeLabelRight(title, null);
160    }
161
162    public static JLabel makeLabelRight(String title, Width width) {
163        if (width == null) {
164            width = Width.SINGLE;
165        }
166        JLabel newLabel = new JLabel(title);
167        setComponentWidth(newLabel, width);
168        setLabelPosition(newLabel, Position.RIGHT);
169        return newLabel;
170    }
171
172    /**
173     * Create a standard sized, left-justified label.
174     *
175     * @param title Label text. Should not be {@code null}.
176     *
177     * @return A new label.
178     */
179    public static JLabel makeLabelLeft(String title) {
180        return makeLabelLeft(title, null);
181    }
182
183    public static JLabel makeLabelLeft(String title, Width width) {
184        if (width == null) {
185            width = Width.SINGLE;
186        }
187        JLabel newLabel = new JLabel(title);
188        setComponentWidth(newLabel, width);
189        setLabelPosition(newLabel, Position.LEFT);
190        return newLabel;
191    }
192
193    /**
194     * Create a sized, labeled component.
195     *
196     * @param label Label for {@code thing}. Should not be {@code null}.
197     * @param thing Component to label. Should not be {@code null}.
198     *
199     * @return A component with its label to the right.
200     */
201    public static JPanel makeLabeledComponent(String label, JComponent thing) {
202        return makeLabeledComponent(makeLabelRight(label), thing);
203    }
204
205    public static JPanel makeLabeledComponent(JLabel label, JComponent thing) {
206        return makeLabeledComponent(label, thing, Position.RIGHT);
207    }
208
209    public static JPanel makeLabeledComponent(String label, JComponent thing, Position position) {
210        return makeLabeledComponent(new JLabel(label), thing, position);
211    }
212
213    public static JPanel makeLabeledComponent(JLabel label, JComponent thing, Position position) {
214        JPanel newPanel = new JPanel();
215
216        if (position == Position.RIGHT) {
217            setComponentWidth(label);
218            setLabelPosition(label, Position.RIGHT);
219        }
220
221        GroupLayout layout = new GroupLayout(newPanel);
222        newPanel.setLayout(layout);
223        layout.setHorizontalGroup(
224            layout.createParallelGroup(LEADING)
225                .addGroup(layout.createSequentialGroup()
226                    .addComponent(label)
227                    .addGap(GAP_RELATED)
228                    .addComponent(thing))
229        );
230        layout.setVerticalGroup(
231            layout.createParallelGroup(LEADING)
232                .addGroup(layout.createParallelGroup(BASELINE)
233                    .addComponent(label)
234                    .addComponent(thing))
235        );
236        return newPanel;
237    }
238
239    /**
240     * Create a sized, labeled component.
241     *
242     * @param thing Component to label. Should not be {@code null}.
243     * @param label Label for {@code thing}. Should not be {@code null}.
244     *
245     * @return A labeled component.
246     */
247    public static JPanel makeComponentLabeled(JComponent thing, String label) {
248        return makeComponentLabeled(thing, new JLabel(label));
249    }
250
251    public static JPanel makeComponentLabeled(JComponent thing, String label, Position position) {
252        return makeComponentLabeled(thing, new JLabel(label), position);
253    }
254
255    public static JPanel makeComponentLabeled(JComponent thing, JLabel label) {
256        return makeComponentLabeled(thing, label, Position.LEFT);
257    }
258
259    public static JPanel makeComponentLabeled(JComponent thing, JLabel label, Position position) {
260        JPanel newPanel = new JPanel();
261
262        if (position == Position.RIGHT) {
263            setComponentWidth(label);
264            setLabelPosition(label, Position.RIGHT);
265        }
266
267        GroupLayout layout = new GroupLayout(newPanel);
268        newPanel.setLayout(layout);
269        layout.setHorizontalGroup(
270            layout.createParallelGroup(LEADING)
271                .addGroup(layout.createSequentialGroup()
272                    .addComponent(thing)
273                    .addGap(GAP_RELATED)
274                    .addComponent(label))
275        );
276        layout.setVerticalGroup(
277            layout.createParallelGroup(LEADING)
278                .addGroup(layout.createParallelGroup(BASELINE)
279                    .addComponent(thing)
280                    .addComponent(label))
281        );
282        return newPanel;
283    }
284
285    /**
286     * Set the width of an existing component.
287     *
288     * @param existingComponent Component that will have its width set.
289     */
290    public static void setComponentWidth(JComponent existingComponent) {
291        setComponentWidth(existingComponent, Width.SINGLE);
292    }
293
294    /**
295     * Set the width of an existing component using standard McIDAS-V component
296     * widths.
297     *
298     * @param existingComponent Component that will have its width set.
299     * @param width Width to use for {@code existingComponent}.
300     */
301    public static void setComponentWidth(JComponent existingComponent, Width width) {
302        if (width == null) {
303            width = Width.SINGLE;
304        }
305
306        int componentWidth;
307        switch (width) {
308            case HALF:
309                componentWidth = ELEMENT_HALF_WIDTH;
310                break;
311            case SINGLE:
312                componentWidth = ELEMENT_WIDTH;
313                break;
314            case ONEHALF:
315                componentWidth = ELEMENT_ONEHALF_WIDTH;
316                break;
317            case DOUBLE:
318                componentWidth = ELEMENT_DOUBLE_WIDTH;
319                break;
320            case TRIPLE:
321                componentWidth = ELEMENT_DOUBLE_WIDTH + ELEMENT_WIDTH;
322                break;
323            case QUADRUPLE:
324                componentWidth = ELEMENT_DOUBLE_WIDTH + ELEMENT_DOUBLE_WIDTH;
325                break;
326            case DOUBLEDOUBLE:
327                componentWidth = ELEMENT_DOUBLEDOUBLE_WIDTH;
328                break;
329            default:
330                componentWidth = ELEMENT_WIDTH;
331                break;
332        }
333        setComponentWidth(existingComponent, componentWidth);
334    }
335
336    /**
337     * Set the width of an existing component to a given integer width.
338     *
339     * @param existingComponent Component that will have its width set.
340     * @param width Width to use for {@code existingComponent}.
341     */
342    public static void setComponentWidth(JComponent existingComponent, int width) {
343        existingComponent.setMinimumSize(new Dimension(width, 24));
344        existingComponent.setMaximumSize(new Dimension(width, 24));
345        existingComponent.setPreferredSize(new Dimension(width, 24));
346    }
347
348    /**
349     * Set the component width to that of another component.
350     */
351    public static void setComponentWidth(JComponent setme, JComponent getme) {
352        setComponentWidth(setme, getme, 0);
353    }
354
355    public static void setComponentWidth(JComponent setme, JComponent getme, int padding) {
356        setme.setPreferredSize(new Dimension(getme.getPreferredSize().width + padding, getme.getPreferredSize().height));
357    }
358
359    /**
360     * Set the component height to that of another component.
361     */
362    public static void setComponentHeight(JComponent setme, JComponent getme) {
363        setComponentHeight(setme, getme, 0);
364    }
365
366    public static void setComponentHeight(JComponent setme, JComponent getme, int padding) {
367        setme.setPreferredSize(new Dimension(getme.getPreferredSize().width, getme.getPreferredSize().height + padding));
368    }
369
370    /**
371     * Set the label position of an existing label
372     * @param existingLabel
373     */
374    public static void setLabelPosition(JLabel existingLabel) {
375        setLabelPosition(existingLabel, Position.LEFT);
376    }
377
378    public static void setLabelPosition(JLabel existingLabel, Position position) {
379        switch (position) {
380            case LEFT:
381                existingLabel.setHorizontalTextPosition(SwingConstants.LEFT);
382                existingLabel.setHorizontalAlignment(SwingConstants.LEFT);
383                break;
384
385            case RIGHT:
386                existingLabel.setHorizontalTextPosition(SwingConstants.RIGHT);
387                existingLabel.setHorizontalAlignment(SwingConstants.RIGHT);
388                break;
389
390            case CENTER:
391                existingLabel.setHorizontalTextPosition(SwingConstants.CENTER);
392                existingLabel.setHorizontalAlignment(SwingConstants.CENTER);
393                break;
394
395            default:
396                existingLabel.setHorizontalTextPosition(SwingConstants.LEFT);
397                existingLabel.setHorizontalAlignment(SwingConstants.LEFT);
398                break;
399        }
400    }
401
402    /**
403     * Set the bold attribute of an existing label
404     * @param existingLabel
405     * @param bold
406     */
407    public static void setLabelBold(JLabel existingLabel, boolean bold) {
408        Font f = existingLabel.getFont();
409        if (bold) {
410            existingLabel.setFont(f.deriveFont(f.getStyle() ^ Font.BOLD));
411        } else {
412            existingLabel.setFont(f.deriveFont(f.getStyle() | Font.BOLD));
413        }
414    }
415
416    /**
417     * Set the foreground color of an existing component
418     * @param existingComponent
419     */
420    public static void setComponentColor(JComponent existingComponent) {
421        setComponentColor(existingComponent, TextColor.NORMAL);
422    }
423
424    public static void setComponentColor(JComponent existingComponent, TextColor color) {
425        switch (color) {
426            case NORMAL:
427                existingComponent.setForeground(new Color(0, 0, 0));
428                break;
429
430            case STATUS:
431                existingComponent.setForeground(MCV_BLUE_DARK);
432                break;
433
434            default:
435                existingComponent.setForeground(new Color(0, 0, 0));
436                break;
437        }
438    }
439
440    /**
441     * Custom makeImageButton to ensure proper sizing and mouseborder are set
442     */
443    public static JButton makeImageButton(String iconName, 
444            final Object object,
445            final String methodName,
446            final Object arg,
447            final String tooltip
448    ) {
449        final JButton btn = makeImageButton(iconName, tooltip);
450        return (JButton)GuiUtils.addActionListener(btn, object, methodName, arg);
451    }
452
453    /**
454     * Custom makeImageButton to ensure proper sizing and mouseborder are set
455     */
456    public static JButton makeImageButton(String iconName, String tooltip) {
457//        boolean addMouseOverBorder = true;
458
459        ImageIcon imageIcon = GuiUtils.getImageIcon(iconName);
460        if (imageIcon.getIconWidth() > 22 || imageIcon.getIconHeight() > 22) {
461            Image scaledImage  = imageIcon.getImage().getScaledInstance(22, 22, Image.SCALE_SMOOTH);
462            imageIcon = new ImageIcon(scaledImage);
463        }
464
465        final JButton btn = GuiUtils.getImageButton(imageIcon);
466        btn.setBackground(null);
467        btn.setContentAreaFilled(false);
468        btn.setSize(new Dimension(24, 24));
469        btn.setPreferredSize(new Dimension(24, 24));
470        btn.setMinimumSize(new Dimension(24, 24));
471//        if (addMouseOverBorder) {
472            GuiUtils.makeMouseOverBorder(btn);
473//        }
474        btn.setToolTipText(tooltip);
475        return btn;
476    }
477
478    /**
479     * Create a button with text and an icon
480     */
481    public static JButton makeImageTextButton(String iconName, String label) {
482        JButton newButton = new JButton(label);
483        setButtonImage(newButton, iconName);
484        return newButton;
485    }
486
487    /**
488     * Add an icon to a button... but only if the LookAndFeel supports it
489     */
490    public static void setButtonImage(JButton existingButton, String iconName) {
491        // TODO: see if this is fixed in some future Apple Java release?
492        // When using Aqua look and feel don't use icons in the buttons
493        // Messes with the button vertical sizing
494        if (existingButton.getBorder().toString().indexOf("Aqua") > 0) {
495            return;
496        }
497        ImageIcon imageIcon = GuiUtils.getImageIcon(iconName);
498        existingButton.setIcon(imageIcon);
499    }
500
501    /**
502     * Add an icon to a menu item
503     */
504    public static void setMenuImage(JMenuItem existingMenuItem, String iconName) {
505        ImageIcon imageIcon = GuiUtils.getImageIcon(iconName);
506        existingMenuItem.setIcon(imageIcon);
507    }
508
509    public static <E> JComboBox<E> makeComboBox(final E[] items, final E selected) {
510        return makeComboBox(CollectionHelpers.list(items), selected);
511    }
512
513    public static <E> JComboBox<E> makeComboBox(final E[] items, final E selected, final Width width) {
514        return makeComboBox(CollectionHelpers.list(items), selected, width);
515    }
516
517    public static <E> JComboBox<E> makeComboBox(final Collection<E> items, final E selected) {
518        return makeComboBox(items, selected, null);
519    }
520    
521    public static <E> JComboBox<E> makeComboBox(final Collection<E> items, final E selected, final Width width) {
522        JComboBox<E> newComboBox = getEditableBox(items, selected);
523        setComponentWidth(newComboBox, width);
524        return newComboBox;
525    }
526    
527    public static <E> void setListData(final JComboBox<E> box, final Collection<E> items, final E selected) {
528        box.removeAllItems();
529        if (items != null) {
530            for (E o : items) {
531                box.addItem(o);
532            }
533            if (selected != null && !items.contains(selected)) {
534                box.addItem(selected);
535            }
536        }
537    }
538    
539    public static <E> JComboBox<E> getEditableBox(final Collection<E> items, final E selected) {
540        JComboBox<E> fld = new JComboBox<>();
541        fld.setEditable(true);
542        setListData(fld, items, selected);
543        if (selected != null) {
544            fld.setSelectedItem(selected);
545        }
546        return fld;
547    }
548
549    /**
550     * Create a standard sized text field.
551     *
552     * @param value Text to place within the text field. Should not be {@code null}.
553     *
554     * @return {@link JTextField} with initial text taken from {@code value}.
555     */
556    public static JTextField makeTextField(String value) {
557        return makeTextField(value, null);
558    }
559
560    public static JTextField makeTextField(String value, Width width) {
561        JTextField newTextField = new McVTextField(value);
562        setComponentWidth(newTextField, width);
563        return newTextField;
564    }
565
566    /**
567     * Create some custom text entry widgets
568     */
569    public static McVTextField makeTextFieldLimit(String defaultString, int limit) {
570        return new McVTextField(defaultString, limit);
571    }
572
573    public static McVTextField makeTextFieldUpper(String defaultString, int limit) {
574        return new McVTextField(defaultString, limit, true);
575    }
576
577    public static McVTextField makeTextFieldAllow(String defaultString, int limit, boolean upper, String allow) {
578        McVTextField newField = new McVTextField(defaultString, limit, upper);
579        newField.setAllow(allow);
580        return newField;
581    }
582
583    public static McVTextField makeTextFieldDeny(String defaultString, int limit, boolean upper, String deny) {
584        McVTextField newField = new McVTextField(defaultString, limit, upper);
585        newField.setDeny(deny);
586        return newField;
587    }
588
589    public static McVTextField makeTextFieldAllow(String defaultString, int limit, boolean upper, char... allow) {
590        McVTextField newField = new McVTextField(defaultString, limit, upper);
591        newField.setAllow(allow);
592        return newField;
593    }
594
595    public static McVTextField makeTextFieldDeny(String defaultString, int limit, boolean upper, char... deny) {
596        McVTextField newField = new McVTextField(defaultString, limit, upper);
597        newField.setDeny(deny);
598        return newField;
599    }
600
601    public static McVTextField makeTextFieldAllow(String defaultString, int limit, boolean upper, Pattern allow) {
602        McVTextField newField = new McVTextField(defaultString, limit, upper);
603        newField.setAllow(allow);
604        return newField;
605    }
606
607    public static McVTextField makeTextFieldDeny(String defaultString, int limit, boolean upper, Pattern deny) {
608        McVTextField newField = new McVTextField(defaultString, limit, upper);
609        newField.setDeny(deny);
610        return newField;
611    }
612
613    /**
614     * Use GroupLayout for stacking components vertically.
615     * Set center to resize vertically.
616     *
617     * @param top Component to place at the top of the newly created panel. Should not be {@code null}.
618     * @param center Component to place in the center of the newly created panel. Should not be {@code null}.
619     * @param bottom Component to place at the bottom of the newly created panel. Should not be {@code null}.
620     *
621     * @return New {@link JPanel} with the given components in the top, center, and bottom positions.
622     */
623    public static JPanel topCenterBottom(JComponent top, JComponent center, JComponent bottom) {
624        JPanel newPanel = new JPanel();
625
626        GroupLayout layout = new GroupLayout(newPanel);
627        newPanel.setLayout(layout);
628        layout.setHorizontalGroup(
629            layout.createParallelGroup(LEADING)
630                .addComponent(top, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
631                .addComponent(center, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
632                .addComponent(bottom, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
633        );
634        layout.setVerticalGroup(
635            layout.createParallelGroup(LEADING)
636                .addGroup(layout.createSequentialGroup()
637                    .addComponent(top, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)
638                    .addPreferredGap(RELATED)
639                    .addComponent(center, PREFERRED_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
640                    .addPreferredGap(RELATED)
641                    .addComponent(bottom, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE))
642        );
643        return newPanel;
644    }
645
646    /**
647     * Use GroupLayout for stacking components vertically.
648     *
649     * @param top Component to place at the top of the newly created panel. Should not be {@code null}.
650     * @param bottom Component to place at the bottom of the newly created panel. Should not be {@code null}.
651     * @param which Which component's size to prefer. Should not be {@code null}.
652     *
653     * @return New {@link JPanel} with the given components.
654     */
655    public static JPanel topBottom(JComponent top, JComponent bottom, Prefer which) {
656        JPanel newPanel = new JPanel();
657
658        int topSize = PREFERRED_SIZE;
659
660        if (which == Prefer.TOP) {
661            topSize = Short.MAX_VALUE;
662        } else if (which == Prefer.BOTTOM) {
663            topSize = Short.MAX_VALUE;
664        }
665
666        GroupLayout layout = new GroupLayout(newPanel);
667        newPanel.setLayout(layout);
668        layout.setHorizontalGroup(
669            layout.createParallelGroup(LEADING)
670                .addComponent(top, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
671                .addComponent(bottom, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
672        );
673        layout.setVerticalGroup(
674            layout.createParallelGroup(LEADING)
675                .addGroup(layout.createSequentialGroup()
676                    .addComponent(top, PREFERRED_SIZE, DEFAULT_SIZE, topSize)
677                    .addPreferredGap(RELATED)
678                    .addComponent(bottom, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE))
679        );
680        return newPanel;
681    }
682
683    /**
684     * Use GroupLayout for wrapping components to stop vertical resizing.
685     *
686     * @param left Left component. Should not be {@code null}.
687     * @param right Right component. Should not be {@code null}.
688     *
689     * @return New {@link JPanel} with the given components side-by-side.
690     */
691    public static JPanel sideBySide(JComponent left, JComponent right) {
692        return sideBySide(left, right, GAP_RELATED);
693    }
694
695    /**
696     * Use GroupLayout for wrapping components to stop vertical resizing.
697     *
698     * @param left Left component. Should not be {@code null}.
699     * @param right Right component. Should not be {@code null}.
700     * @param gap Gap between {@code left} and {@code right}.
701     *
702     * @return New {@link JPanel} with the given components side-by-side,
703     * separated by value from {@code gap}.
704     */
705    public static JPanel sideBySide(JComponent left, JComponent right, int gap) {
706        JPanel newPanel = new JPanel();
707
708        GroupLayout layout = new GroupLayout(newPanel);
709        newPanel.setLayout(layout);
710        layout.setHorizontalGroup(
711            layout.createParallelGroup(LEADING)
712                .addGroup(layout.createSequentialGroup()
713                    .addComponent(left, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE)
714                    .addGap(gap)
715                    .addComponent(right, DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE))
716        );
717        layout.setVerticalGroup(
718            layout.createParallelGroup(LEADING)
719                .addGroup(layout.createSequentialGroup()
720                    .addGroup(layout.createParallelGroup(LEADING)
721                        .addComponent(left, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)
722                        .addComponent(right, PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE)))
723        );
724
725        return newPanel;
726    }
727
728    /**
729     * Use GroupLayout for wrapping a list of components horizontally.
730     *
731     * @param components Components to stack horizontally. Should not be {@code null}.
732     *
733     * @return {@link JPanel} with the given components.
734     */
735    public static JPanel horizontal(Component... components) {
736        JPanel newPanel = new JPanel();
737
738        GroupLayout layout = new GroupLayout(newPanel);
739        newPanel.setLayout(layout);
740
741        SequentialGroup hGroup = layout.createSequentialGroup();
742        for (int i = 0; i < components.length; i++) {
743            if (i > 0) {
744                hGroup.addGap(GAP_RELATED);
745            }
746            hGroup.addComponent(components[i], DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE);
747        }
748
749        SequentialGroup vGroup = layout.createSequentialGroup();
750        ParallelGroup vInner = layout.createParallelGroup(LEADING);
751        for (int i = 0; i < components.length; i++) {
752            vInner.addComponent(components[i], PREFERRED_SIZE, DEFAULT_SIZE, PREFERRED_SIZE);
753        }
754        vGroup.addGroup(vInner);
755
756        layout.setHorizontalGroup(layout.createParallelGroup(LEADING).addGroup(hGroup));
757        layout.setVerticalGroup(layout.createParallelGroup(LEADING).addGroup(vGroup));
758
759        return newPanel;
760    }
761
762    /**
763     * Use GroupLayout for wrapping a list of components vertically.
764     *
765     * @param components Components to stack vertically. Should not be {@code null}.
766     *
767     * @return {@link JPanel} with the given components.
768     */
769    public static JPanel vertical(Component... components) {
770        JPanel newPanel = new JPanel();
771
772        GroupLayout layout = new GroupLayout(newPanel);
773        newPanel.setLayout(layout);
774
775        ParallelGroup hGroup = layout.createParallelGroup(LEADING);
776        for (int i = 0; i < components.length; i++) {
777            hGroup.addComponent(components[i], DEFAULT_SIZE, DEFAULT_SIZE, Short.MAX_VALUE);
778        }
779
780        int vSize = PREFERRED_SIZE;
781
782        ParallelGroup vGroup = layout.createParallelGroup(LEADING);
783        SequentialGroup vInner = layout.createSequentialGroup();
784        for (int i = 0; i < components.length; i++) {
785            if (i > 0) {
786                vInner.addGap(GAP_RELATED);
787            }
788            if (i == components.length-1) {
789                vSize = Short.MAX_VALUE;
790            }
791            vInner.addComponent(components[i], PREFERRED_SIZE, DEFAULT_SIZE, vSize);
792        }
793        vGroup.addGroup(vInner);
794        layout.setHorizontalGroup(layout.createParallelGroup(LEADING).addGroup(hGroup));
795        layout.setVerticalGroup(layout.createParallelGroup(LEADING).addGroup(vGroup));
796        return newPanel;
797    }
798
799    /**
800     * Hack apart an IDV button panel and do a few things:
801     * - Reorder the buttons based on OS preference
802     *   Windows: OK on left
803     *   Mac: OK on right
804     * - Add icons when we understand the button name
805     *
806     * @param idvButtonPanel {@link JPanel} to scan for understood button names. Should not be {@code null}.
807     *
808     * @return The given {@code JPanel} with pretty buttons (where possible).
809     */
810    // TODO: Revisit this?  Could hamper GUI performance.  But it is niiice...
811    public static JPanel makePrettyButtons(JPanel idvButtonPanel) {
812        // These are the buttons we know about
813        JButton buttonOK = null;
814        JButton buttonApply = null;
815        JButton buttonCancel = null;
816        JButton buttonHelp = null;
817        JButton buttonNew = null;
818        JButton buttonReset = null;
819        JButton buttonYes = null;
820        JButton buttonNo = null;
821
822        // First pull apart the panel and see if it looks like we expect
823        Component[] comps = idvButtonPanel.getComponents();
824
825        // These are the buttons we don't know about
826        List<JButton> buttonList = new ArrayList<JButton>(comps.length);
827
828        for (int i = 0; i < comps.length; i++) {
829            if (!(comps[i] instanceof JButton)) {
830                continue;
831            }
832            JButton button = (JButton)comps[i];
833            if ("OK".equals(button.getText())) {
834                buttonOK = makePrettyButton(button);
835            } else if ("Apply".equals(button.getText())) {
836                buttonApply = makePrettyButton(button);
837            } else if ("Cancel".equals(button.getText())) {
838                buttonCancel = makePrettyButton(button);
839            } else if ("Help".equals(button.getText())) {
840                buttonHelp = makePrettyButton(button);
841            } else if ("New".equals(button.getText())) {
842                buttonNew = makePrettyButton(button);
843            } else if ("Reset".equals(button.getText())) {
844                buttonReset = makePrettyButton(button);
845            } else if ("Yes".equals(button.getText())) {
846                buttonYes = makePrettyButton(button);
847            } else if ("No".equals(button.getText())) {
848                buttonNo = makePrettyButton(button);
849            } else {
850                buttonList.add(button);
851            }
852        }
853
854        // If we are on a Mac, this is the order (right aligned)
855        // Help, New, Reset, No, Yes, Cancel, Apply, OK
856        if (System.getProperty("os.name").contains("Mac OS X")) {
857            JPanel newButtonPanel = new JPanel();
858            if (buttonHelp != null) {
859                newButtonPanel.add(buttonHelp);
860            }
861            if (buttonNew != null) {
862                newButtonPanel.add(buttonNew);
863            }
864            if (buttonReset != null) {
865                newButtonPanel.add(buttonReset);
866            }
867            if (buttonNo != null) {
868                newButtonPanel.add(buttonNo);
869            }
870            if (buttonYes != null) {
871                newButtonPanel.add(buttonYes);
872            }
873            if (buttonCancel != null) {
874                newButtonPanel.add(buttonCancel);
875            }
876            if (buttonApply != null) {
877                newButtonPanel.add(buttonApply);
878            }
879            if (buttonOK != null) {
880                newButtonPanel.add(buttonOK);
881            }
882            if (!buttonList.isEmpty()) {
883                return GuiUtils.right(GuiUtils.hbox(GuiUtils.hbox(buttonList), newButtonPanel));
884            } else {
885                return GuiUtils.right(newButtonPanel);
886            }
887        }
888
889        // If we are not on a Mac, this is the order (center aligned)
890        // OK, Apply, Cancel, Yes, No, Reset, New, Help
891        if (!System.getProperty("os.name").contains("Mac OS X")) {
892            JPanel newButtonPanel = new JPanel();
893            if (buttonOK != null) {
894                newButtonPanel.add(buttonOK);
895            }
896            if (buttonApply != null) {
897                newButtonPanel.add(buttonApply);
898            }
899            if (buttonCancel != null) {
900                newButtonPanel.add(buttonCancel);
901            }
902            if (buttonYes != null) {
903                newButtonPanel.add(buttonYes);
904            }
905            if (buttonNo != null) {
906                newButtonPanel.add(buttonNo);
907            }
908            if (buttonReset != null) {
909                newButtonPanel.add(buttonReset);
910            }
911            if (buttonNew != null) {
912                newButtonPanel.add(buttonNew);
913            }
914            if (buttonHelp != null) {
915                newButtonPanel.add(buttonHelp);
916            }
917            if (!buttonList.isEmpty()) {
918                return GuiUtils.center(GuiUtils.hbox(GuiUtils.hbox(buttonList), newButtonPanel));
919            } else {
920                return GuiUtils.center(newButtonPanel);
921            }
922        }
923
924        return idvButtonPanel;
925    }
926
927    /**
928     * Take a list of buttons and make them pretty.
929     * 
930     * @param buttonList List of buttons. Should not be {@code null}.
931     *
932     * @return {@link List} of pretty buttons.
933     */
934    public static List makePrettyButtons(List buttonList) {
935        int size = buttonList.size();
936        List newButtons = arrList(size);
937        for (int i = 0; i < size; i++) {
938            if (buttonList.get(i) instanceof JButton) {
939                newButtons.add(makePrettyButton((JButton)(buttonList.get(i))));
940            } else {
941                newButtons.add(buttonList.get(i));
942            }
943        }
944        return newButtons;
945    }
946
947    /**
948     * Convenience method to make a button based solely on its name.
949     *
950     * @param name Button text. Should not be {@code null}.
951     *
952     * @return A {@literal "pretty"} button.
953     */
954    public static JButton makePrettyButton(String name) {
955        return makePrettyButton(new JButton(name));
956    }
957
958    /**
959     * Add icons when we understand the button name.
960     * 
961     * @param button Button to make pretty. Should not be {@code null}.
962     *
963     * @return button Either the given {@code button} with an icon, or just the given
964     * {@code button} (if the name was not understood).
965     */
966    public static JButton makePrettyButton(JButton button) {
967        McVGuiUtils.setComponentWidth(button, Width.ONEHALF);
968        if ("OK".equals(button.getText())) {
969            McVGuiUtils.setButtonImage(button, ICON_ACCEPT_SMALL);
970        } else if ("Apply".equals(button.getText())) {
971            McVGuiUtils.setButtonImage(button, ICON_APPLY_SMALL);
972        } else if ("Cancel".equals(button.getText())) {
973            McVGuiUtils.setButtonImage(button, ICON_CANCEL_SMALL);
974        } else if ("Help".equals(button.getText())) {
975            McVGuiUtils.setButtonImage(button, ICON_HELP_SMALL);
976        } else if ("New".equals(button.getText())) {
977            McVGuiUtils.setButtonImage(button, ICON_ADD_SMALL);
978        } else if ("Reset".equals(button.getText())) {
979            McVGuiUtils.setButtonImage(button, ICON_UNDO_SMALL);
980        } else if ("Yes".equals(button.getText())) {
981            McVGuiUtils.setButtonImage(button, ICON_ACCEPT_SMALL);
982        } else if ("No".equals(button.getText())) {
983            McVGuiUtils.setButtonImage(button, ICON_CANCEL_SMALL);
984        } else if ("Close".equals(button.getText())) {
985            McVGuiUtils.setButtonImage(button, ICON_CANCEL_SMALL);
986        } else if ("Previous".equals(button.getText())) {
987            McVGuiUtils.setButtonImage(button, ICON_PREVIOUS_SMALL);
988        } else if ("Next".equals(button.getText())) {
989            McVGuiUtils.setButtonImage(button, ICON_NEXT_SMALL);
990        } else if ("Random".equals(button.getText())) {
991            McVGuiUtils.setButtonImage(button, ICON_RANDOM_SMALL);
992        } else if ("Support Form".equals(button.getText())) {
993            McVGuiUtils.setButtonImage(button, ICON_SUPPORT_SMALL);
994        }
995        return button;
996    }
997
998    /**
999     * Print the hierarchy of components.
1000     */
1001    public static void printUIComponents(JComponent parent) {
1002        printUIComponents(parent, 0, 0);
1003    }
1004
1005    public static void printUIComponents(JComponent parent, int index, int depth) {
1006        if (parent == null) {
1007            System.err.println("McVGuiUtils.printUIComponents: null parent");
1008            return;
1009        }
1010        Component[] children = parent.getComponents();
1011        int childcount = children.length;
1012
1013        String indent = "";
1014        for (int d=0; d<depth; d++) {
1015            indent += "  ";
1016        }
1017        System.out.println(indent + index + ": " + parent);
1018
1019        if (childcount > 0) {
1020            for (int c=0; c<childcount; c++) {
1021                if (children[c] instanceof JComponent) {
1022                    printUIComponents((JComponent)children[c], c, depth+1);
1023                }
1024            }
1025        }
1026    }
1027
1028    /**
1029     * Calls {@link SwingUtilities#invokeLater(Runnable)} if the current thread
1030     * is not the event dispatch thread. If this thread <b>is</b> the EDT,
1031     * then call {@link Runnable#run()} for {@code r}.
1032     * 
1033     * <p>Remember, you <i>do not</i> want to execute long-running tasks in the
1034     * event dispatch thread--it'll lock up the GUI.
1035     * 
1036     * @param r Code to run in the event dispatch thread. Cannot be {@code null}.
1037     */
1038    public static void runOnEDT(final Runnable r) {
1039        if (SwingUtilities.isEventDispatchThread()) {
1040            r.run();
1041        } else {
1042            SwingUtilities.invokeLater(r);
1043        }
1044    }
1045
1046    //    private static <E> List<E> sizedList() {
1047    //        McIDASV mcv = McIDASV.getStaticMcv();
1048    //        int viewManagerCount = ESTIMATED_VM_COUNT;
1049    //        if (mcv != null) {
1050    //            ViewManagerManager vmm = cast(mcv.getVMManager());
1051    //            viewManagerCount = vmm.getViewManagers().size();
1052    //        }
1053    //        return arrList(viewManagerCount);
1054    //    }
1055
1056
1057    private static int getVMCount() {
1058        McIDASV mcv = McIDASV.getStaticMcv();
1059        int viewManagerCount = ESTIMATED_VM_COUNT;
1060        if (mcv != null) {
1061            ViewManagerManager vmm = (ViewManagerManager)mcv.getVMManager();
1062            viewManagerCount = vmm.getViewManagerCount();
1063        }
1064        return viewManagerCount;
1065    }
1066
1067    private static int getHolderCount() {
1068        McIDASV mcv = McIDASV.getStaticMcv();
1069        int holderCount = ESTIMATED_VM_COUNT;
1070        if (mcv != null) {
1071            UIManager uiManager = (UIManager)mcv.getIdvUIManager();
1072            holderCount = uiManager.getComponentHolderCount();
1073        }
1074        return holderCount;
1075    }
1076
1077    private static int getGroupCount() {
1078        McIDASV mcv = McIDASV.getStaticMcv();
1079        int groupCount = ESTIMATED_VM_COUNT;
1080        if (mcv != null) {
1081            UIManager uiManager = (UIManager)mcv.getIdvUIManager();
1082            groupCount = uiManager.getComponentGroupCount();
1083        }
1084        return groupCount;
1085    }
1086
1087    public static List<ViewManager> getActiveViewManagers() {
1088        IdvWindow activeWindow = IdvWindow.getActiveWindow();
1089        List<ViewManager> vms;
1090        if (activeWindow != null) {
1091            vms = getViewManagers(activeWindow);
1092        } else {
1093            vms = Collections.emptyList();
1094        }
1095        return vms;
1096    }
1097
1098    public static List<ViewManager> getAllViewManagers() {
1099        McIDASV mcv = McIDASV.getStaticMcv();
1100        List<ViewManager> vms = Collections.emptyList();
1101        if (mcv != null) {
1102            ViewManagerManager vmm = (ViewManagerManager)mcv.getVMManager();
1103            vms = arrList(vmm.getViewManagers());
1104        }
1105        return vms;
1106    }
1107
1108    /**
1109     * Attempt to find the {@link IdvWindow} that contains the given
1110     * {@link ViewManager}.
1111     *
1112     * @param vm {@code ViewManager} whose {@code IdvWindow} is needed.
1113     * Cannot be {@code null}.
1114     *
1115     * @return Either the {@code IdvWindow} containing {@code vm}, or
1116     * {@code null}.
1117     */
1118    public static IdvWindow getWindowForViewManager(final ViewManager vm) {
1119        IdvWindow result = null;
1120        for (IdvWindow w : getAllDisplayWindows()) {
1121            List<ViewManager> viewManagers = getViewManagers(w);
1122            if (viewManagers.contains(vm)) {
1123                result = w;
1124                break;
1125            }
1126        }
1127        return result;
1128    }
1129
1130    public static List<Object> getShareGroupsInWindow(final IdvWindow window) {
1131        List<ViewManager> vms = arrList(getVMCount());
1132        vms.addAll(window.getViewManagers());
1133        for (IdvComponentHolder holder : getComponentHolders(window)) {
1134            List<ViewManager> holderVms = holder.getViewManagers();
1135            if (holderVms != null) {
1136                vms.addAll(holderVms);
1137            }
1138        }
1139        Set<Object> groupIds = newHashSet(vms.size());
1140        for (ViewManager vm : vms) {
1141            groupIds.add(vm.getShareGroup());
1142        }
1143        return arrList(groupIds);
1144    }
1145
1146    public static List<Object> getAllShareGroups() {
1147        List<ViewManager> vms = getAllViewManagers();
1148        Set<Object> groupIds = newHashSet(vms.size());
1149        for (ViewManager vm : vms) {
1150            groupIds.add(vm.getShareGroup());
1151        }
1152        return arrList(groupIds);
1153    }
1154
1155    public static List<ViewManager> getViewManagersInGroup(final Object sharedGroup) {
1156        List<ViewManager> allVMs = getAllViewManagers();
1157        List<ViewManager> filtered = arrList(allVMs.size());
1158        for (ViewManager vm : allVMs) {
1159            if (vm.getShareGroup().equals(sharedGroup)) {
1160                filtered.add(vm);
1161            }
1162        }
1163        return filtered;
1164    }
1165
1166    public static List<ViewManager> getViewManagers(final WindowInfo info) {
1167        List<ViewManager> vms = arrList(getVMCount());
1168        for (IdvComponentHolder holder : getComponentHolders(info)) {
1169            List<ViewManager> holderVms = holder.getViewManagers();
1170            if (holderVms != null) {
1171                vms.addAll(holderVms);
1172            }
1173        }
1174        return vms;
1175    }
1176
1177    public static List<ViewManager> getViewManagers(final IdvWindow window) {
1178        List<ViewManager> vms = arrList(getVMCount());
1179        vms.addAll(window.getViewManagers());
1180        for (IdvComponentHolder holder : getComponentHolders(window)) {
1181            List<ViewManager> holderVms = holder.getViewManagers();
1182            if (holderVms != null) {
1183                vms.addAll(holderVms);
1184            }
1185        }
1186        return vms;
1187    }
1188
1189    /**
1190     * @return Whether or not {@code h} contains some UI component like
1191     * the dashboard of field selector. Yes, it can happen!
1192     */
1193    public static boolean isUIHolder(final IdvComponentHolder h) {
1194        if (McvComponentHolder.TYPE_DYNAMIC_SKIN.equals(h.getType())) {
1195            return false;
1196        }
1197        return h.getViewManagers().isEmpty();
1198    }
1199
1200    /**
1201     * @return Whether or not {@code h} is a dynamic skin.
1202     */
1203    public static boolean isDynamicSkin(final IdvComponentHolder h) {
1204        return McvComponentHolder.TYPE_DYNAMIC_SKIN.equals(h.getType());
1205    }
1206
1207    /**
1208     * @return Whether or not {@code windows} has at least one dynamic
1209     * skin.
1210     */
1211    public static boolean hasDynamicSkins(final List<WindowInfo> windows) {
1212        for (WindowInfo window : windows) {
1213            for (IdvComponentHolder holder : getComponentHolders(window)) {
1214                if (isDynamicSkin(holder)) {
1215                    return true;
1216                }
1217            }
1218        }
1219        return false;
1220    }
1221
1222    /**
1223     * @return The component holders within {@code windowInfo}.
1224     * @see #getComponentHolders(IdvComponentGroup)
1225     */
1226    public static List<IdvComponentHolder> getComponentHolders(final WindowInfo windowInfo) {
1227        Collection<Object> comps =
1228            (Collection<Object>)windowInfo.getPersistentComponents().values();
1229        List<IdvComponentHolder> holders = arrList(getHolderCount());
1230        for (Object comp : comps) {
1231            if (!(comp instanceof IdvComponentGroup)) {
1232                continue;
1233            }
1234            holders.addAll(getComponentHolders((IdvComponentGroup)comp));
1235        }
1236        return holders;
1237    }
1238
1239    /**
1240     * @return The component holders within {@code idvWindow}.
1241     * @see #getComponentHolders(IdvComponentGroup)
1242     */
1243    public static List<IdvComponentHolder> getComponentHolders(final IdvWindow idvWindow) {
1244        List<IdvComponentHolder> holders = arrList(getHolderCount());
1245        for (IdvComponentGroup group : (List<IdvComponentGroup>)idvWindow.getComponentGroups()) {
1246            holders.addAll(getComponentHolders(group));
1247        }
1248        return holders;
1249    }
1250
1251    /**
1252     * @return <b>Recursively</b> searches {@code group} to find any 
1253     * component holders.
1254     */
1255    public static List<IdvComponentHolder> getComponentHolders(final IdvComponentGroup group) {
1256        List<IdvComponentHolder> holders = arrList(getHolderCount());
1257        List<ComponentHolder> comps = (List<ComponentHolder>)group.getDisplayComponents();
1258        if (comps.isEmpty()) {
1259            return holders;
1260        }
1261        for (ComponentHolder comp : comps) {
1262            if (comp instanceof IdvComponentGroup) {
1263                holders.addAll(getComponentHolders((IdvComponentGroup) comp));
1264            } else if (comp instanceof IdvComponentHolder) {
1265                holders.add((IdvComponentHolder)comp);
1266            }
1267        }
1268        return holders;
1269    }
1270
1271    /**
1272     * @return <b>Recursively</b> searches {@code group} for any nested
1273     * component groups.
1274     */
1275    public static List<IdvComponentGroup> getComponentGroups(final IdvComponentGroup group) {
1276        List<IdvComponentGroup> groups = arrList(getGroupCount());
1277        groups.add(group);
1278
1279        List<ComponentHolder> comps = (List<ComponentHolder>)group.getDisplayComponents();
1280        if (comps.isEmpty()) {
1281            return groups;
1282        }
1283        for (ComponentHolder comp : comps) {
1284            if (comp instanceof IdvComponentGroup) {
1285                groups.addAll(getComponentGroups((IdvComponentGroup)comp));
1286            }
1287        }
1288        return groups;
1289    }
1290
1291    /**
1292     * @return Component groups contained in {@code window}.
1293     * @see #getComponentGroups(IdvComponentGroup)
1294     */
1295    public static List<IdvComponentGroup> getComponentGroups(final WindowInfo window) {
1296        Collection<Object> comps = (Collection<Object>)window.getPersistentComponents().values();
1297        for (Object comp : comps) {
1298            if (comp instanceof IdvComponentGroup) {
1299                return getComponentGroups((IdvComponentGroup)comp);
1300            }
1301        }
1302        return Collections.emptyList();
1303    }
1304
1305    /**
1306     * @return Component groups contained in {@code windows}.
1307     * @see #getComponentGroups(IdvComponentGroup)
1308     */
1309    public static List<IdvComponentGroup> getComponentGroups(final List<WindowInfo> windows) {
1310        List<IdvComponentGroup> groups = arrList(getGroupCount());
1311        for (WindowInfo window : windows) {
1312            groups.addAll(getComponentGroups(window));
1313        }
1314        return groups;
1315    }
1316
1317    /**
1318     * @return The component group within {@code window}.
1319     */
1320    public static IdvComponentGroup getComponentGroup(final IdvWindow window) {
1321        List<IdvComponentGroup> groups = window.getComponentGroups();
1322        if (!groups.isEmpty()) {
1323            return groups.get(0);
1324        }
1325        return null;
1326    }
1327
1328    /**
1329     * @return Whether or not {@code group} contains any component
1330     *         groups.
1331     */
1332    public static boolean hasNestedGroups(final IdvComponentGroup group) {
1333        List<ComponentHolder> comps = (List<ComponentHolder>)group.getDisplayComponents();
1334        for (ComponentHolder comp : comps) {
1335            if (comp instanceof IdvComponentGroup) {
1336                return true;
1337            }
1338        }
1339        return false;
1340    }
1341
1342    /**
1343     * @return All active component holders in McIDAS-V.
1344     */
1345    // TODO: needs update for nested groups
1346    public static List<IdvComponentHolder> getAllComponentHolders() {
1347        List<IdvComponentHolder> holders = arrList(getHolderCount());
1348        for (IdvComponentGroup g : getAllComponentGroups()) {
1349            holders.addAll(g.getDisplayComponents());
1350        }
1351        return holders;
1352    }
1353
1354    /**
1355     * @return All active component groups in McIDAS-V.
1356     */
1357    // TODO: needs update for nested groups
1358    public static List<IdvComponentGroup> getAllComponentGroups() {
1359        List<IdvComponentGroup> groups = arrList(getGroupCount());
1360        for (IdvWindow w : getAllDisplayWindows()) {
1361            groups.addAll(w.getComponentGroups());
1362        }
1363        return groups;
1364    }
1365
1366    /**
1367     * @return All windows that contain at least one component group.
1368     */
1369    public static List<IdvWindow> getAllDisplayWindows() {
1370        List<IdvWindow> allWindows = (List<IdvWindow>)IdvWindow.getWindows();
1371        List<IdvWindow> windows = arrList(allWindows.size());
1372        for (IdvWindow w : allWindows) {
1373            if (!w.getComponentGroups().isEmpty()) {
1374                windows.add(w);
1375            }
1376        }
1377        return windows;
1378    }
1379
1380    /**
1381     * @return The component holder positioned after the active component holder.
1382     */
1383    public static IdvComponentHolder getAfterActiveHolder() {
1384        return getAfterHolder(getActiveComponentHolder());
1385    }
1386
1387    /**
1388     * @return The component holder positioned before the active component holder.
1389     */
1390    public static IdvComponentHolder getBeforeActiveHolder() {
1391        return getBeforeHolder(getActiveComponentHolder());
1392    }
1393
1394    /**
1395     * @return The active component holder in the active window.
1396     */
1397    public static IdvComponentHolder getActiveComponentHolder() {
1398        IdvWindow window = IdvWindow.getActiveWindow();
1399        McvComponentGroup group = (McvComponentGroup)getComponentGroup(window);
1400        return (IdvComponentHolder)group.getActiveComponentHolder();
1401    }
1402
1403    /**
1404     * @return The component holder positioned after {@code current}.
1405     */
1406    public static IdvComponentHolder getAfterHolder(final IdvComponentHolder current) {
1407        List<IdvComponentHolder> holders = getAllComponentHolders();
1408        int currentIndex = holders.indexOf(current);
1409        return holders.get((currentIndex + 1) % holders.size());
1410    }
1411
1412    /**
1413     * @return The component holder positioned before {@code current}.
1414     */
1415    public static IdvComponentHolder getBeforeHolder(final IdvComponentHolder current) {
1416        List<IdvComponentHolder> holders = getAllComponentHolders();
1417        int currentIndex = holders.indexOf(current);
1418        int newidx = (currentIndex - 1) % holders.size();
1419        if (newidx == -1) {
1420            newidx = holders.size() - 1;
1421        }
1422        return holders.get(newidx);
1423    }
1424
1425    /**
1426     * @param w {@link IdvWindow} whose component groups you want (as 
1427     * {@link McvComponentGroup}s).
1428     * 
1429     * @return A {@link List} of {@code McvComponentGroup}s or an empty list. 
1430     * If there were no {@code McvComponentGroup}s in {@code w}, 
1431     * <b>or</b> if {@code w} is {@code null}, an empty {@code List} is returned.
1432     */
1433    public static List<McvComponentGroup> idvGroupsToMcv(final IdvWindow w) {
1434        if (w == null) {
1435            return Collections.emptyList();
1436        }
1437        final List<IdvComponentGroup> idvLandGroups = w.getComponentGroups();
1438        final List<McvComponentGroup> groups = arrList(idvLandGroups.size());
1439        for (IdvComponentGroup group : idvLandGroups) {
1440            groups.add((McvComponentGroup)group);
1441        }
1442        return groups;
1443    }
1444
1445    public static void compGroup(final IdvComponentGroup g) {
1446        compGroup(g, 0);
1447    }
1448
1449    public static void compGroup(final IdvComponentGroup g, final int level) {
1450        p("Comp Group", level);
1451        p("  name=" + g.getName(), level);
1452        p("  id=" + g.getUniqueId(), level);
1453        p("  layout=" + g.getLayout(), level);
1454        p("  comp count=" + g.getDisplayComponents().size() + ": ", level);
1455        for (Object comp : g.getDisplayComponents()) {
1456            if (comp instanceof IdvComponentHolder) {
1457                compHolder((IdvComponentHolder)comp, level+1);
1458            } else if (comp instanceof IdvComponentGroup) {
1459                compGroup((IdvComponentGroup)comp, level+1);
1460            } else {
1461                p("    umm=" + comp.getClass().getName(), level);
1462            }
1463        }
1464    }
1465
1466    public static void compHolder(final IdvComponentHolder h, final int level) {
1467        p("Comp Holder", level);
1468        p("  cat=" + h.getCategory(), level);
1469        p("  name=" + h.getName(), level);
1470        p("  id=" + h.getUniqueId(), level);
1471        if (h.getViewManagers() == null) {
1472            System.err.println("  null vms!");
1473            return;
1474        }
1475        p("  vm count=" + h.getViewManagers().size() + ": ", level);
1476        for (ViewManager vm : (List<ViewManager>)h.getViewManagers()) {
1477            p("    " + vmType(vm) + "=" + vm.getViewDescriptor().getName(), level);
1478        }
1479    }
1480
1481    public static List<ViewManager> findvms(final List<WindowInfo> windows) {
1482        List<ViewManager> vms = new ArrayList<ViewManager>();
1483        for (WindowInfo window : windows) {
1484            for (IdvComponentHolder h : getComponentHolders(window)) {
1485                if (h.getViewManagers() != null) {
1486                    vms.addAll((List<ViewManager>)h.getViewManagers());
1487                } else {
1488                    System.err.println(h.getUniqueId() + " has no vms!");
1489                }
1490            }
1491        }
1492        for (ViewManager vm : vms) {
1493            System.err.println("vm=" + vm.getViewDescriptor().getName());
1494        }
1495        return vms;
1496    }
1497
1498    private static String vmType(final ViewManager vm) {
1499        if (vm instanceof MapViewManager) {
1500            if (((MapViewManager)vm).getUseGlobeDisplay()) {
1501                return "Globe";
1502            } else {
1503                return "Map";
1504            }
1505        }
1506        return "Other";
1507    }
1508
1509    private static String pad(final String str, final int pad) {
1510        char[] padding = new char[pad*2];
1511        for (int i = 0; i < pad*2; i++) {
1512            padding[i] = ' ';
1513        }
1514        return new String(padding).concat(str);
1515    }
1516
1517    private static void p(final String str, final int padding) {
1518        System.err.println(pad(str, padding));
1519    }
1520
1521    /**
1522     * Find the {@literal "bounds"} for the physical display at {@code index}.
1523     * 
1524     * @param index Zero-based index of the desired physical display.
1525     * 
1526     * @return Either a {@link java.awt.Rectangle} representing the display's
1527     * bounds, or {@code null} if {@code index} is invalid.
1528     */
1529    public static Rectangle getDisplayBoundsFor(final int index) {
1530        return SystemState.getDisplayBounds().get(index);
1531    }
1532
1533    /**
1534     * Tries to determine the physical display that contains the given
1535     * {@link java.awt.Rectangle}. <b>This method (currently) fails for 
1536     * {@code Rectangle}s that span multiple displays!</b>
1537     * 
1538     * @param rect {@code Rectangle} to test. Should not be {@code null}.
1539     * 
1540     * @return Either the (zero-based) index of the physical display, or 
1541     * {@code -1} if there was no match.
1542     */
1543    public static int findDisplayNumberForRectangle(final Rectangle rect) {
1544        Map<Integer, Rectangle> bounds = SystemState.getDisplayBounds();
1545        int index = -1;
1546        for (Entry<Integer, Rectangle> entry : bounds.entrySet()) {
1547            if (entry.getValue().contains(rect)) {
1548                index = entry.getKey();
1549                break;
1550            }
1551        }
1552        return index;
1553    }
1554
1555    /**
1556     * Tries to determine the physical display that contains the given
1557     * {@link java.awt.Component}. <b>This method (currently) fails for 
1558     * {@code Component}s that span multiple displays!</b>
1559     * 
1560     * @param comp {@code Component} to test. Should not be {@code null}.
1561     * 
1562     * @return Either the (zero-based) index of the physical display, or 
1563     * {@code -1} if there was no match.
1564     */
1565    public static int findDisplayNumberForComponent(final Component comp) {
1566        return findDisplayNumberForRectangle(
1567                new Rectangle(comp.getLocation(), comp.getSize()));
1568    }
1569
1570    /**
1571     * Tries to determine the physical display that contains the given
1572     * {@link ucar.unidata.ui.MultiFrame}. <b>This method (currently) fails 
1573     * for {@code MultiFrame}s that span multiple displays!</b>
1574     * 
1575     * @param mf {@code MultiFrame} to test. Should not be {@code null}.
1576     * 
1577     * @return Either the (zero-based) index of the physical display, or 
1578     * {@code -1} if there was no match.
1579     */
1580    public static int findDisplayNumberForMultiFrame(final MultiFrame mf) {
1581        return findDisplayNumberForRectangle(
1582                new Rectangle(mf.getLocation(), mf.getSize()));
1583    }
1584
1585    /**
1586     * Tries to determine the physical display that contains the rectangle 
1587     * defined by the specified coordinates. <b>This method (currently) fails 
1588     * for coordinates that span multiple displays!</b>
1589     * 
1590     * @param x X coordinate of the upper-left corner.
1591     * @param y Y coordinate of the upper-left corner.
1592     * @param width Width of the rectangle.
1593     * @param height Height of the rectangle.
1594     * 
1595     * @return Either the (zero-based) index of the physical display, or 
1596     * {@code -1} if there was no match.
1597     * 
1598     * @see java.awt.Rectangle#Rectangle(int, int, int, int)
1599     */
1600    public static int findDisplayNumberForCoords(final int x, final int y, 
1601            final int width, final int height) 
1602    {
1603        return findDisplayNumberForRectangle(
1604                new Rectangle(x, y, width, height));
1605    }
1606
1607    /**
1608     * Tries to determine which physical display contains the 
1609     * {@link java.awt.Component} or {@link ucar.unidata.ui.MultiFrame} that 
1610     * fired the given event. <b>This method (currently) fails for coordinates 
1611     * that span multiple displays!</b>
1612     * 
1613     * @param event {@code EventObject} to test. Should not be {@code null}.
1614     * 
1615     * @return Either the (zero-based) index of the physical display, or 
1616     * {@code -1} if there was no match.
1617     */
1618    public static int findDisplayNumberForEvent(final EventObject event) {
1619        int idx = -1;
1620        Object src = event.getSource();
1621        if (event instanceof HierarchyEvent) {
1622            src = ((HierarchyEvent)event).getChanged();
1623        }
1624        if (src != null) {
1625            if (src instanceof Component) {
1626                idx = findDisplayNumberForComponent((Component)src);
1627            } else if (src instanceof MultiFrame) {
1628                idx = findDisplayNumberForMultiFrame((MultiFrame)src);
1629            }
1630        }
1631        return idx;
1632    }
1633
1634    // thing below here are courtesy http://tips4java.wordpress.com/2008/11/13/swing-utils/
1635
1636    /**
1637     * Convenience method for searching below {@code container} in the
1638     * component hierarchy and return nested components that are instances of
1639     * class {@code clazz} it finds. Returns an empty list if no such
1640     * components exist in the container.
1641     * <P>
1642     * Invoking this method with a class parameter of JComponent.class
1643     * will return all nested components.
1644     * <P>
1645     * This method invokes
1646     * {@link #getDescendantsOfType(Class, java.awt.Container, boolean)}
1647     *
1648     * @param clazz the class of components whose instances are to be found.
1649     * @param container the container at which to begin the search
1650     * @return the List of components
1651     */
1652    public static <T extends JComponent> List<T> getDescendantsOfType(
1653        Class<T> clazz, Container container) {
1654        return getDescendantsOfType(clazz, container, true);
1655    }
1656
1657    /**
1658     * Convenience method for searching below {@code container} in the
1659     * component hierarchy and return nested components that are instances of
1660     * class {@code clazz} it finds. Returns an empty list if no such
1661     * components exist in the container.
1662     * <P>
1663     * Invoking this method with a class parameter of JComponent.class
1664     * will return all nested components.
1665     *
1666     * @param clazz the class of components whose instances are to be found.
1667     * @param container the container at which to begin the search
1668     * @param nested true to list components nested within another listed
1669     * component, false otherwise
1670     * @return the List of components
1671     */
1672    public static <T extends JComponent> List<T> getDescendantsOfType(
1673        Class<T> clazz, Container container, boolean nested) {
1674        List<T> tList = new ArrayList<>();
1675        for (Component component : container.getComponents()) {
1676            if (clazz.isAssignableFrom(component.getClass())) {
1677                tList.add(clazz.cast(component));
1678            }
1679            if (nested || !clazz.isAssignableFrom(component.getClass())) {
1680                tList.addAll(McVGuiUtils.getDescendantsOfType(clazz,
1681                    (Container)component, nested));
1682            }
1683        }
1684        return tList;
1685    }
1686
1687    /**
1688     * Convenience method that searches below {@code container} in the
1689     * component hierarchy and returns the first found component that is an
1690     * instance of class {@code clazz} having the bound property value.
1691     * Returns {@code null} if such component cannot be found.
1692     * <P>
1693     * This method invokes
1694     * {@link #getDescendantOfType(Class, java.awt.Container, String, Object, boolean)}
1695     *
1696     * @param clazz the class of component whose instance is to be found.
1697     * @param container the container at which to begin the search
1698     * @param property the className of the bound property, exactly as expressed in
1699     * the accessor e.g. {@literal "Text"} for getText(), {@literal "Value"}
1700     * for getValue().
1701     * @param value the value of the bound property
1702     * @return the component, or null if no such component exists in the
1703     * container
1704     * @throws java.lang.IllegalArgumentException if the bound property does
1705     * not exist for the class or cannot be accessed
1706     */
1707    public static <T extends JComponent> T getDescendantOfType(
1708        Class<T> clazz, Container container, String property, Object value)
1709        throws IllegalArgumentException
1710    {
1711        return getDescendantOfType(clazz, container, property, value, true);
1712    }
1713
1714    /**
1715     * Convenience method that searches below {@code container} in the
1716     * component hierarchy and returns the first found component that is an
1717     * instance of class {@code clazz} and has the bound property value.
1718     * Returns {@code null} if such component cannot be found.
1719     *
1720     * @param clazz the class of component whose instance to be found.
1721     * @param container the container at which to begin the search
1722     * @param property the className of the bound property, exactly as expressed in
1723     * the accessor e.g. {@literal "Text"} for getText(), {@literal "Value"}
1724     * for getValue().
1725     * @param value the value of the bound property
1726     * @param nested true to list components nested within another component
1727     * which is also an instance of {@code clazz}, false otherwise.
1728     *
1729     * @return the component, or null if no such component exists in the
1730     * container.
1731     *
1732     * @throws java.lang.IllegalArgumentException if the bound property does
1733     * not exist for the class or cannot be accessed.
1734     */
1735    public static <T extends JComponent> T getDescendantOfType(Class<T> clazz,
1736                                                               Container container,
1737                                                               String property,
1738                                                               Object value,
1739                                                               boolean nested)
1740        throws IllegalArgumentException
1741    {
1742        List<T> list = getDescendantsOfType(clazz, container, nested);
1743        return getComponentFromList(clazz, list, property, value);
1744    }
1745
1746    /**
1747     * Convenience method for searching below {@code container} in the
1748     * component hierarchy and return nested components of class
1749     * {@code clazz} it finds.  Returns an empty list if no such
1750     * components exist in the container.
1751     * <P>
1752     * This method invokes
1753     * {@link #getDescendantsOfClass(Class, java.awt.Container, boolean)}.
1754     *
1755     * @param clazz the class of components to be found.
1756     * @param container the container at which to begin the search
1757     * @return the List of components
1758     */
1759    public static <T extends JComponent> List<T> getDescendantsOfClass(
1760        Class<T> clazz, Container container)
1761    {
1762        return getDescendantsOfClass(clazz, container, true);
1763    }
1764
1765    /**
1766     * Convenience method for searching below {@code container} in the
1767     * component hierarchy and return nested components of class
1768     * {@code clazz} it finds.  Returns an empty list if no such
1769     * components exist in the container.
1770     *
1771     * @param clazz the class of components to be found.
1772     * @param container the container at which to begin the search
1773     * @param nested true to list components nested within another listed
1774     * component, false otherwise
1775     *
1776     * @return the List of components
1777     */
1778    public static <T extends JComponent> List<T> getDescendantsOfClass(
1779        Class<T> clazz, Container container, boolean nested)
1780    {
1781        List<T> tList = new ArrayList<>();
1782        for (Component component : container.getComponents()) {
1783            if (clazz.equals(component.getClass())) {
1784                tList.add(clazz.cast(component));
1785            }
1786            if (nested || !clazz.equals(component.getClass())) {
1787                tList.addAll(McVGuiUtils.getDescendantsOfClass(clazz,
1788                    (Container)component, nested));
1789            }
1790        }
1791        return tList;
1792    }
1793
1794    /**
1795     * Convenience method that searches below {@code container} in the
1796     * component hierarchy in a depth first manner and returns the first
1797     * found component of class {@code clazz} having the bound property
1798     * value.
1799     * <P>
1800     * Returns {@code null} if such component cannot be found.
1801     * <P>
1802     * This method invokes
1803     * {@link #getDescendantOfClass(Class, java.awt.Container, String, Object, boolean)}
1804     *
1805     * @param clazz the class of component to be found.
1806     * @param container the container at which to begin the search
1807     * @param property the className of the bound property, exactly as expressed in
1808     * the accessor e.g. {@literal "Text"} for getText(), {@literal "Value"}
1809     * for getValue(). This parameter is case sensitive.
1810     * @param value the value of the bound property
1811     *
1812     * @return the component, or null if no such component exists in the
1813     * container's hierarchy.
1814     *
1815     * @throws java.lang.IllegalArgumentException if the bound property does
1816     * not exist for the class or cannot be accessed
1817     */
1818    public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
1819                                                                Container container,
1820                                                                String property,
1821                                                                Object value)
1822        throws IllegalArgumentException
1823    {
1824        return getDescendantOfClass(clazz, container, property, value, true);
1825    }
1826
1827    /**
1828     * Convenience method that searches below {@code container} in the
1829     * component hierarchy in a depth first manner and returns the first
1830     * found component of class {@code clazz} having the bound property
1831     * value.
1832     * <P>
1833     * Returns {@code null} if such component cannot be found.
1834     *
1835     * @param clazz the class of component to be found.
1836     * @param container the container at which to begin the search
1837     * @param property the className of the bound property, exactly as expressed
1838     * in the accessor e.g. {@literal "Text"} for getText(), {@literal "Value"}
1839     * for getValue(). This parameter is case sensitive.
1840     * @param value the value of the bound property
1841     * @param nested true to include components nested within another listed
1842     * component, false otherwise.
1843     *
1844     * @return the component, or null if no such component exists in the
1845     * container's hierarchy.
1846     *
1847     * @throws java.lang.IllegalArgumentException if the bound property does
1848     * not exist for the class or cannot be accessed
1849     */
1850    public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
1851                                                                Container container,
1852                                                                String property,
1853                                                                Object value,
1854                                                                boolean nested)
1855        throws IllegalArgumentException
1856    {
1857        List<T> list = getDescendantsOfClass(clazz, container, nested);
1858        return getComponentFromList(clazz, list, property, value);
1859    }
1860
1861    private static <T extends JComponent> T getComponentFromList(Class<T> clazz,
1862                                                                 List<T> list,
1863                                                                 String property,
1864                                                                 Object value)
1865        throws IllegalArgumentException
1866    {
1867        T retVal = null;
1868        Method method = null;
1869        try {
1870            method = clazz.getMethod("get" + property);
1871        } catch (NoSuchMethodException ex) {
1872            try {
1873                method = clazz.getMethod("is" + property);
1874            } catch (NoSuchMethodException ex1) {
1875                throw new IllegalArgumentException("Property " + property +
1876                    " not found in class " + clazz.getName(), ex1);
1877            }
1878        }
1879        try {
1880            for (T t : list) {
1881                Object testVal = method.invoke(t);
1882                if (equals(value, testVal)) {
1883                    return t;
1884                }
1885            }
1886        } catch (InvocationTargetException ex) {
1887            throw new IllegalArgumentException(
1888                "Error accessing property " + property +
1889                    " in class " + clazz.getName(), ex);
1890        } catch (IllegalAccessException ex) {
1891            throw new IllegalArgumentException(
1892                "Property " + property +
1893                    " cannot be accessed in class " + clazz.getName(), ex);
1894        } catch (SecurityException ex) {
1895            throw new IllegalArgumentException(
1896                "Property " + property +
1897                    " cannot be accessed in class " + clazz.getName(), ex);
1898        }
1899        return retVal;
1900    }
1901
1902    /**
1903     * Convenience method for determining whether two objects are either
1904     * equal or both null.
1905     *
1906     * @param obj1 the first reference object to compare.
1907     * @param obj2 the second reference object to compare.
1908     * @return true if obj1 and obj2 are equal or if both are null,
1909     * false otherwise
1910     */
1911    public static boolean equals(Object obj1, Object obj2) {
1912        return (obj1 == null) ? (obj2 == null) : obj1.equals(obj2);
1913    }
1914
1915    /**
1916     * Convenience method for mapping a container in the hierarchy to its
1917     * contained components.  The keys are the containers, and the values
1918     * are lists of contained components.
1919     * <P>
1920     * Implementation note:  The returned value is a HashMap and the values
1921     * are of type ArrayList.  This is subject to change, so callers should
1922     * code against the interfaces Map and List.
1923     *
1924     * @param container The JComponent to be mapped
1925     * @param nested true to drill down to nested containers, false otherwise
1926     * @return the Map of the UI
1927     */
1928    public static Map<JComponent, List<JComponent>> getComponentMap(
1929        JComponent container, boolean nested)
1930    {
1931        List<JComponent> descendants =
1932            getDescendantsOfType(JComponent.class, container, false);
1933        Map<JComponent, List<JComponent>> retVal =
1934            new HashMap<>(descendants.size());
1935        for (JComponent component : descendants) {
1936            if (!retVal.containsKey(container)) {
1937                retVal.put(container,
1938                    new ArrayList<JComponent>());
1939            }
1940            retVal.get(container).add(component);
1941            if (nested) {
1942                retVal.putAll(getComponentMap(component, nested));
1943            }
1944        }
1945        return retVal;
1946    }
1947
1948    /**
1949     * Convenience method for retrieving a subset of the UIDefaults pertaining
1950     * to a particular class.
1951     *
1952     * @param clazz the class of interest
1953     * @return the UIDefaults of the class
1954     */
1955    public static UIDefaults getUIDefaultsOfClass(Class<?> clazz) {
1956        String name = clazz.getName();
1957        name = name.substring(name.lastIndexOf(".") + 2);
1958        return getUIDefaultsOfClass(name);
1959    }
1960
1961    /**
1962     * Convenience method for retrieving a subset of the UIDefaults pertaining
1963     * to a particular class.
1964     *
1965     * @param className fully qualified name of the class of interest
1966     * @return the UIDefaults of the class named
1967     */
1968    public static UIDefaults getUIDefaultsOfClass(String className) {
1969        UIDefaults retVal = new UIDefaults();
1970        UIDefaults defaults = javax.swing.UIManager.getLookAndFeelDefaults();
1971        List<?> listKeys = Collections.list(defaults.keys());
1972        for (Object key : listKeys) {
1973            if ((key instanceof String) && ((String) key).startsWith(className)) {
1974                String stringKey = (String) key;
1975                String property = stringKey;
1976                if (stringKey.contains(".")) {
1977                    property = stringKey.substring(stringKey.indexOf(".") + 1);
1978                }
1979                retVal.put(property, defaults.get(key));
1980            }
1981        }
1982        return retVal;
1983    }
1984
1985    /**
1986     * Convenience method for retrieving the UIDefault for a single property
1987     * of a particular class.
1988     *
1989     * @param clazz the class of interest
1990     * @param property the property to query
1991     * @return the UIDefault property, or null if not found
1992     */
1993    public static Object getUIDefaultOfClass(Class<?> clazz, String property) {
1994        Object retVal = null;
1995        UIDefaults defaults = getUIDefaultsOfClass(clazz);
1996        List<Object> listKeys = Collections.list(defaults.keys());
1997        for (Object key : listKeys) {
1998            if (key.equals(property)) {
1999                return defaults.get(key);
2000            }
2001            if (key.toString().equalsIgnoreCase(property)) {
2002                retVal = defaults.get(key);
2003            }
2004        }
2005        return retVal;
2006    }
2007
2008    /**
2009     * Exclude methods that return values that are meaningless to the user
2010     */
2011    static Set<String> setExclude = new HashSet<>(10);
2012    static {
2013        setExclude.add("getFocusCycleRootAncestor");
2014        setExclude.add("getAccessibleContext");
2015        setExclude.add("getColorModel");
2016        setExclude.add("getGraphics");
2017        setExclude.add("getGraphicsConfiguration");
2018    }
2019
2020    /**
2021     * Convenience method for obtaining most non-null human readable properties
2022     * of a JComponent.  Array properties are not included.
2023     * <P>
2024     * Implementation note:  The returned value is a HashMap.  This is subject
2025     * to change, so callers should code against the interface Map.
2026     *
2027     * @param component the component whose proerties are to be determined
2028     * @return the class and value of the properties
2029     */
2030    public static Map<Object, Object> getProperties(JComponent component) {
2031        Class<?> clazz = component.getClass();
2032        Method[] methods = clazz.getMethods();
2033        Map<Object, Object> retVal = new HashMap<>(methods.length);
2034        Object value = null;
2035        for (Method method : methods) {
2036            if (method.getName().matches("^(is|get).*") &&
2037                (method.getParameterTypes().length == 0)) {
2038                try {
2039                    Class<?> returnType = method.getReturnType();
2040                    if ((returnType != void.class) &&
2041                        !returnType.getName().startsWith("[") &&
2042                        !setExclude.contains(method.getName())) {
2043                        String key = method.getName();
2044                        value = method.invoke(component);
2045                        if ((value != null) && !(value instanceof Component)) {
2046                            retVal.put(key, value);
2047                        }
2048                    }
2049                    // ignore exceptions that arise if the property could not be accessed
2050                } catch (IllegalAccessException ex) {
2051                } catch (IllegalArgumentException ex) {
2052                } catch (InvocationTargetException ex) {
2053                }
2054            }
2055        }
2056        return retVal;
2057    }
2058
2059    /**
2060     * Convenience method to obtain the Swing class from which this
2061     * component was directly or indirectly derived.
2062     *
2063     * @param component The component whose Swing superclass is to be
2064     * determined
2065     * @return The nearest Swing class in the inheritance tree
2066     */
2067    public static <T extends JComponent> Class<?> getJClass(T component) {
2068        Class<?> clazz = component.getClass();
2069        while (!clazz.getName().matches("javax.swing.J[^.]*$")) {
2070            clazz = clazz.getSuperclass();
2071        }
2072        return clazz;
2073    }
2074
2075    /**
2076     * Gets the {@literal "text"} contents of a {@link JTextComponent} without
2077     * the possibility of a {@code NullPointerException}.
2078     *
2079     * @param textComponent {@code JTextComponent} whose contents should be
2080     * extracted. {@code null} is allowed.
2081     *
2082     * @return Either the results of {@link JTextComponent#getText()} or an
2083     * empty {@code String} if {@code textComponent} or {@code getText()} are
2084     * {@code null}.
2085     */
2086    public static String safeGetText(JTextComponent textComponent) {
2087        String value = "";
2088        if ((textComponent != null) && (textComponent.getText() != null)) {
2089            value = textComponent.getText();
2090        }
2091        return value;
2092    }
2093}