001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2015
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    public static List<Object> getShareGroupsInWindow(final IdvWindow window) {
1109        List<ViewManager> vms = arrList(getVMCount());
1110        vms.addAll(window.getViewManagers());
1111        for (IdvComponentHolder holder : getComponentHolders(window)) {
1112            vms.addAll(holder.getViewManagers());
1113        }
1114        Set<Object> groupIds = newHashSet(vms.size());
1115        for (ViewManager vm : vms) {
1116            groupIds.add(vm.getShareGroup());
1117        }
1118        return arrList(groupIds);
1119    }
1120
1121    public static List<Object> getAllShareGroups() {
1122        List<ViewManager> vms = getAllViewManagers();
1123        Set<Object> groupIds = newHashSet(vms.size());
1124        for (ViewManager vm : vms) {
1125            groupIds.add(vm.getShareGroup());
1126        }
1127        return arrList(groupIds);
1128    }
1129
1130    public static List<ViewManager> getViewManagersInGroup(final Object sharedGroup) {
1131        List<ViewManager> allVMs = getAllViewManagers();
1132        List<ViewManager> filtered = arrList(allVMs.size());
1133        for (ViewManager vm : allVMs) {
1134            if (vm.getShareGroup().equals(sharedGroup)) {
1135                filtered.add(vm);
1136            }
1137        }
1138        return filtered;
1139    }
1140
1141    public static List<ViewManager> getViewManagers(final WindowInfo info) {
1142        List<ViewManager> vms = arrList(getVMCount());
1143        for (IdvComponentHolder holder : getComponentHolders(info)) {
1144            vms.addAll(holder.getViewManagers());
1145        }
1146        return vms;
1147    }
1148
1149    public static List<ViewManager> getViewManagers(final IdvWindow window) {
1150        List<ViewManager> vms = arrList(getVMCount());
1151        vms.addAll(window.getViewManagers());
1152        for (IdvComponentHolder holder : getComponentHolders(window)) {
1153            vms.addAll(holder.getViewManagers());
1154        }
1155        return vms;
1156    }
1157
1158    /**
1159     * @return Whether or not {@code h} contains some UI component like
1160     * the dashboard of field selector. Yes, it can happen!
1161     */
1162    public static boolean isUIHolder(final IdvComponentHolder h) {
1163        if (McvComponentHolder.TYPE_DYNAMIC_SKIN.equals(h.getType())) {
1164            return false;
1165        }
1166        return h.getViewManagers().isEmpty();
1167    }
1168
1169    /**
1170     * @return Whether or not {@code h} is a dynamic skin.
1171     */
1172    public static boolean isDynamicSkin(final IdvComponentHolder h) {
1173        return McvComponentHolder.TYPE_DYNAMIC_SKIN.equals(h.getType());
1174    }
1175
1176    /**
1177     * @return Whether or not {@code windows} has at least one dynamic
1178     * skin.
1179     */
1180    public static boolean hasDynamicSkins(final List<WindowInfo> windows) {
1181        for (WindowInfo window : windows) {
1182            for (IdvComponentHolder holder : getComponentHolders(window)) {
1183                if (isDynamicSkin(holder)) {
1184                    return true;
1185                }
1186            }
1187        }
1188        return false;
1189    }
1190
1191    /**
1192     * @return The component holders within {@code windowInfo}.
1193     * @see #getComponentHolders(IdvComponentGroup)
1194     */
1195    public static List<IdvComponentHolder> getComponentHolders(final WindowInfo windowInfo) {
1196        Collection<Object> comps =
1197            (Collection<Object>)windowInfo.getPersistentComponents().values();
1198        List<IdvComponentHolder> holders = arrList(getHolderCount());
1199        for (Object comp : comps) {
1200            if (!(comp instanceof IdvComponentGroup)) {
1201                continue;
1202            }
1203            holders.addAll(getComponentHolders((IdvComponentGroup)comp));
1204        }
1205        return holders;
1206    }
1207
1208    /**
1209     * @return The component holders within {@code idvWindow}.
1210     * @see #getComponentHolders(IdvComponentGroup)
1211     */
1212    public static List<IdvComponentHolder> getComponentHolders(final IdvWindow idvWindow) {
1213        List<IdvComponentHolder> holders = arrList(getHolderCount());
1214        for (IdvComponentGroup group : (List<IdvComponentGroup>)idvWindow.getComponentGroups()) {
1215            holders.addAll(getComponentHolders(group));
1216        }
1217        return holders;
1218    }
1219
1220    /**
1221     * @return <b>Recursively</b> searches {@code group} to find any 
1222     * component holders.
1223     */
1224    public static List<IdvComponentHolder> getComponentHolders(final IdvComponentGroup group) {
1225        List<IdvComponentHolder> holders = arrList(getHolderCount());
1226        List<ComponentHolder> comps = (List<ComponentHolder>)group.getDisplayComponents();
1227        if (comps.isEmpty()) {
1228            return holders;
1229        }
1230        for (ComponentHolder comp : comps) {
1231            if (comp instanceof IdvComponentGroup) {
1232                holders.addAll(getComponentHolders((IdvComponentGroup) comp));
1233            } else if (comp instanceof IdvComponentHolder) {
1234                holders.add((IdvComponentHolder)comp);
1235            }
1236        }
1237        return holders;
1238    }
1239
1240    /**
1241     * @return <b>Recursively</b> searches {@code group} for any nested
1242     * component groups.
1243     */
1244    public static List<IdvComponentGroup> getComponentGroups(final IdvComponentGroup group) {
1245        List<IdvComponentGroup> groups = arrList(getGroupCount());
1246        groups.add(group);
1247
1248        List<ComponentHolder> comps = (List<ComponentHolder>)group.getDisplayComponents();
1249        if (comps.isEmpty()) {
1250            return groups;
1251        }
1252        for (ComponentHolder comp : comps) {
1253            if (comp instanceof IdvComponentGroup) {
1254                groups.addAll(getComponentGroups((IdvComponentGroup)comp));
1255            }
1256        }
1257        return groups;
1258    }
1259
1260    /**
1261     * @return Component groups contained in {@code window}.
1262     * @see #getComponentGroups(IdvComponentGroup)
1263     */
1264    public static List<IdvComponentGroup> getComponentGroups(final WindowInfo window) {
1265        Collection<Object> comps = (Collection<Object>)window.getPersistentComponents().values();
1266        for (Object comp : comps) {
1267            if (comp instanceof IdvComponentGroup) {
1268                return getComponentGroups((IdvComponentGroup)comp);
1269            }
1270        }
1271        return Collections.emptyList();
1272    }
1273
1274    /**
1275     * @return Component groups contained in {@code windows}.
1276     * @see #getComponentGroups(IdvComponentGroup)
1277     */
1278    public static List<IdvComponentGroup> getComponentGroups(final List<WindowInfo> windows) {
1279        List<IdvComponentGroup> groups = arrList(getGroupCount());
1280        for (WindowInfo window : windows) {
1281            groups.addAll(getComponentGroups(window));
1282        }
1283        return groups;
1284    }
1285
1286    /**
1287     * @return The component group within {@code window}.
1288     */
1289    public static IdvComponentGroup getComponentGroup(final IdvWindow window) {
1290        List<IdvComponentGroup> groups = window.getComponentGroups();
1291        if (!groups.isEmpty()) {
1292            return groups.get(0);
1293        }
1294        return null;
1295    }
1296
1297    /**
1298     * @return Whether or not {@code group} contains any component
1299     *         groups.
1300     */
1301    public static boolean hasNestedGroups(final IdvComponentGroup group) {
1302        List<ComponentHolder> comps = (List<ComponentHolder>)group.getDisplayComponents();
1303        for (ComponentHolder comp : comps) {
1304            if (comp instanceof IdvComponentGroup) {
1305                return true;
1306            }
1307        }
1308        return false;
1309    }
1310
1311    /**
1312     * @return All active component holders in McIDAS-V.
1313     */
1314    // TODO: needs update for nested groups
1315    public static List<IdvComponentHolder> getAllComponentHolders() {
1316        List<IdvComponentHolder> holders = arrList(getHolderCount());
1317        for (IdvComponentGroup g : getAllComponentGroups()) {
1318            holders.addAll(g.getDisplayComponents());
1319        }
1320        return holders;
1321    }
1322
1323    /**
1324     * @return All active component groups in McIDAS-V.
1325     */
1326    // TODO: needs update for nested groups
1327    public static List<IdvComponentGroup> getAllComponentGroups() {
1328        List<IdvComponentGroup> groups = arrList(getGroupCount());
1329        for (IdvWindow w : getAllDisplayWindows()) {
1330            groups.addAll(w.getComponentGroups());
1331        }
1332        return groups;
1333    }
1334
1335    /**
1336     * @return All windows that contain at least one component group.
1337     */
1338    public static List<IdvWindow> getAllDisplayWindows() {
1339        List<IdvWindow> allWindows = (List<IdvWindow>)IdvWindow.getWindows();
1340        List<IdvWindow> windows = arrList(allWindows.size());
1341        for (IdvWindow w : allWindows) {
1342            if (!w.getComponentGroups().isEmpty()) {
1343                windows.add(w);
1344            }
1345        }
1346        return windows;
1347    }
1348
1349    /**
1350     * @return The component holder positioned after the active component holder.
1351     */
1352    public static IdvComponentHolder getAfterActiveHolder() {
1353        return getAfterHolder(getActiveComponentHolder());
1354    }
1355
1356    /**
1357     * @return The component holder positioned before the active component holder.
1358     */
1359    public static IdvComponentHolder getBeforeActiveHolder() {
1360        return getBeforeHolder(getActiveComponentHolder());
1361    }
1362
1363    /**
1364     * @return The active component holder in the active window.
1365     */
1366    public static IdvComponentHolder getActiveComponentHolder() {
1367        IdvWindow window = IdvWindow.getActiveWindow();
1368        McvComponentGroup group = (McvComponentGroup)getComponentGroup(window);
1369        return (IdvComponentHolder)group.getActiveComponentHolder();
1370    }
1371
1372    /**
1373     * @return The component holder positioned after {@code current}.
1374     */
1375    public static IdvComponentHolder getAfterHolder(final IdvComponentHolder current) {
1376        List<IdvComponentHolder> holders = getAllComponentHolders();
1377        int currentIndex = holders.indexOf(current);
1378        return holders.get((currentIndex + 1) % holders.size());
1379    }
1380
1381    /**
1382     * @return The component holder positioned before {@code current}.
1383     */
1384    public static IdvComponentHolder getBeforeHolder(final IdvComponentHolder current) {
1385        List<IdvComponentHolder> holders = getAllComponentHolders();
1386        int currentIndex = holders.indexOf(current);
1387        int newidx = (currentIndex - 1) % holders.size();
1388        if (newidx == -1) {
1389            newidx = holders.size() - 1;
1390        }
1391        return holders.get(newidx);
1392    }
1393
1394    /**
1395     * @param w {@link IdvWindow} whose component groups you want (as 
1396     * {@link McvComponentGroup}s).
1397     * 
1398     * @return A {@link List} of {@code McvComponentGroup}s or an empty list. 
1399     * If there were no {@code McvComponentGroup}s in {@code w}, 
1400     * <b>or</b> if {@code w} is {@code null}, an empty {@code List} is returned.
1401     */
1402    public static List<McvComponentGroup> idvGroupsToMcv(final IdvWindow w) {
1403        if (w == null) {
1404            return Collections.emptyList();
1405        }
1406        final List<IdvComponentGroup> idvLandGroups = w.getComponentGroups();
1407        final List<McvComponentGroup> groups = arrList(idvLandGroups.size());
1408        for (IdvComponentGroup group : idvLandGroups) {
1409            groups.add((McvComponentGroup)group);
1410        }
1411        return groups;
1412    }
1413
1414    public static void compGroup(final IdvComponentGroup g) {
1415        compGroup(g, 0);
1416    }
1417
1418    public static void compGroup(final IdvComponentGroup g, final int level) {
1419        p("Comp Group", level);
1420        p("  name=" + g.getName(), level);
1421        p("  id=" + g.getUniqueId(), level);
1422        p("  layout=" + g.getLayout(), level);
1423        p("  comp count=" + g.getDisplayComponents().size() + ": ", level);
1424        for (Object comp : g.getDisplayComponents()) {
1425            if (comp instanceof IdvComponentHolder) {
1426                compHolder((IdvComponentHolder)comp, level+1);
1427            } else if (comp instanceof IdvComponentGroup) {
1428                compGroup((IdvComponentGroup)comp, level+1);
1429            } else {
1430                p("    umm=" + comp.getClass().getName(), level);
1431            }
1432        }
1433    }
1434
1435    public static void compHolder(final IdvComponentHolder h, final int level) {
1436        p("Comp Holder", level);
1437        p("  cat=" + h.getCategory(), level);
1438        p("  name=" + h.getName(), level);
1439        p("  id=" + h.getUniqueId(), level);
1440        if (h.getViewManagers() == null) {
1441            System.err.println("  null vms!");
1442            return;
1443        }
1444        p("  vm count=" + h.getViewManagers().size() + ": ", level);
1445        for (ViewManager vm : (List<ViewManager>)h.getViewManagers()) {
1446            p("    " + vmType(vm) + "=" + vm.getViewDescriptor().getName(), level);
1447        }
1448    }
1449
1450    public static List<ViewManager> findvms(final List<WindowInfo> windows) {
1451        List<ViewManager> vms = new ArrayList<ViewManager>();
1452        for (WindowInfo window : windows) {
1453            for (IdvComponentHolder h : getComponentHolders(window)) {
1454                if (h.getViewManagers() != null) {
1455                    vms.addAll((List<ViewManager>)h.getViewManagers());
1456                } else {
1457                    System.err.println(h.getUniqueId() + " has no vms!");
1458                }
1459            }
1460        }
1461        for (ViewManager vm : vms) {
1462            System.err.println("vm=" + vm.getViewDescriptor().getName());
1463        }
1464        return vms;
1465    }
1466
1467    private static String vmType(final ViewManager vm) {
1468        if (vm instanceof MapViewManager) {
1469            if (((MapViewManager)vm).getUseGlobeDisplay()) {
1470                return "Globe";
1471            } else {
1472                return "Map";
1473            }
1474        }
1475        return "Other";
1476    }
1477
1478    private static String pad(final String str, final int pad) {
1479        char[] padding = new char[pad*2];
1480        for (int i = 0; i < pad*2; i++) {
1481            padding[i] = ' ';
1482        }
1483        return new String(padding).concat(str);
1484    }
1485
1486    private static void p(final String str, final int padding) {
1487        System.err.println(pad(str, padding));
1488    }
1489
1490    /**
1491     * Find the {@literal "bounds"} for the physical display at {@code index}.
1492     * 
1493     * @param index Zero-based index of the desired physical display.
1494     * 
1495     * @return Either a {@link java.awt.Rectangle} representing the display's
1496     * bounds, or {@code null} if {@code index} is invalid.
1497     */
1498    public static Rectangle getDisplayBoundsFor(final int index) {
1499        return SystemState.getDisplayBounds().get(index);
1500    }
1501
1502    /**
1503     * Tries to determine the physical display that contains the given
1504     * {@link java.awt.Rectangle}. <b>This method (currently) fails for 
1505     * {@code Rectangle}s that span multiple displays!</b>
1506     * 
1507     * @param rect {@code Rectangle} to test. Should not be {@code null}.
1508     * 
1509     * @return Either the (zero-based) index of the physical display, or 
1510     * {@code -1} if there was no match.
1511     */
1512    public static int findDisplayNumberForRectangle(final Rectangle rect) {
1513        Map<Integer, Rectangle> bounds = SystemState.getDisplayBounds();
1514        int index = -1;
1515        for (Entry<Integer, Rectangle> entry : bounds.entrySet()) {
1516            if (entry.getValue().contains(rect)) {
1517                index = entry.getKey();
1518                break;
1519            }
1520        }
1521        return index;
1522    }
1523
1524    /**
1525     * Tries to determine the physical display that contains the given
1526     * {@link java.awt.Component}. <b>This method (currently) fails for 
1527     * {@code Component}s that span multiple displays!</b>
1528     * 
1529     * @param comp {@code Component} to test. Should not be {@code null}.
1530     * 
1531     * @return Either the (zero-based) index of the physical display, or 
1532     * {@code -1} if there was no match.
1533     */
1534    public static int findDisplayNumberForComponent(final Component comp) {
1535        return findDisplayNumberForRectangle(
1536                new Rectangle(comp.getLocation(), comp.getSize()));
1537    }
1538
1539    /**
1540     * Tries to determine the physical display that contains the given
1541     * {@link ucar.unidata.ui.MultiFrame}. <b>This method (currently) fails 
1542     * for {@code MultiFrame}s that span multiple displays!</b>
1543     * 
1544     * @param mf {@code MultiFrame} to test. Should not be {@code null}.
1545     * 
1546     * @return Either the (zero-based) index of the physical display, or 
1547     * {@code -1} if there was no match.
1548     */
1549    public static int findDisplayNumberForMultiFrame(final MultiFrame mf) {
1550        return findDisplayNumberForRectangle(
1551                new Rectangle(mf.getLocation(), mf.getSize()));
1552    }
1553
1554    /**
1555     * Tries to determine the physical display that contains the rectangle 
1556     * defined by the specified coordinates. <b>This method (currently) fails 
1557     * for coordinates that span multiple displays!</b>
1558     * 
1559     * @param x X coordinate of the upper-left corner.
1560     * @param y Y coordinate of the upper-left corner.
1561     * @param width Width of the rectangle.
1562     * @param height Height of the rectangle.
1563     * 
1564     * @return Either the (zero-based) index of the physical display, or 
1565     * {@code -1} if there was no match.
1566     * 
1567     * @see java.awt.Rectangle#Rectangle(int, int, int, int)
1568     */
1569    public static int findDisplayNumberForCoords(final int x, final int y, 
1570            final int width, final int height) 
1571    {
1572        return findDisplayNumberForRectangle(
1573                new Rectangle(x, y, width, height));
1574    }
1575
1576    /**
1577     * Tries to determine which physical display contains the 
1578     * {@link java.awt.Component} or {@link ucar.unidata.ui.MultiFrame} that 
1579     * fired the given event. <b>This method (currently) fails for coordinates 
1580     * that span multiple displays!</b>
1581     * 
1582     * @param event {@code EventObject} to test. Should not be {@code null}.
1583     * 
1584     * @return Either the (zero-based) index of the physical display, or 
1585     * {@code -1} if there was no match.
1586     */
1587    public static int findDisplayNumberForEvent(final EventObject event) {
1588        int idx = -1;
1589        Object src = event.getSource();
1590        if (event instanceof HierarchyEvent) {
1591            src = ((HierarchyEvent)event).getChanged();
1592        }
1593        if (src != null) {
1594            if (src instanceof Component) {
1595                idx = findDisplayNumberForComponent((Component)src);
1596            } else if (src instanceof MultiFrame) {
1597                idx = findDisplayNumberForMultiFrame((MultiFrame)src);
1598            }
1599        }
1600        return idx;
1601    }
1602
1603    // thing below here are courtesy http://tips4java.wordpress.com/2008/11/13/swing-utils/
1604
1605    /**
1606     * Convenience method for searching below {@code container} in the
1607     * component hierarchy and return nested components that are instances of
1608     * class {@code clazz} it finds. Returns an empty list if no such
1609     * components exist in the container.
1610     * <P>
1611     * Invoking this method with a class parameter of JComponent.class
1612     * will return all nested components.
1613     * <P>
1614     * This method invokes
1615     * {@link #getDescendantsOfType(Class, java.awt.Container, boolean)}
1616     *
1617     * @param clazz the class of components whose instances are to be found.
1618     * @param container the container at which to begin the search
1619     * @return the List of components
1620     */
1621    public static <T extends JComponent> List<T> getDescendantsOfType(
1622        Class<T> clazz, Container container) {
1623        return getDescendantsOfType(clazz, container, true);
1624    }
1625
1626    /**
1627     * Convenience method for searching below {@code container} in the
1628     * component hierarchy and return nested components that are instances of
1629     * class {@code clazz} it finds. Returns an empty list if no such
1630     * components exist in the container.
1631     * <P>
1632     * Invoking this method with a class parameter of JComponent.class
1633     * will return all nested components.
1634     *
1635     * @param clazz the class of components whose instances are to be found.
1636     * @param container the container at which to begin the search
1637     * @param nested true to list components nested within another listed
1638     * component, false otherwise
1639     * @return the List of components
1640     */
1641    public static <T extends JComponent> List<T> getDescendantsOfType(
1642        Class<T> clazz, Container container, boolean nested) {
1643        List<T> tList = new ArrayList<>();
1644        for (Component component : container.getComponents()) {
1645            if (clazz.isAssignableFrom(component.getClass())) {
1646                tList.add(clazz.cast(component));
1647            }
1648            if (nested || !clazz.isAssignableFrom(component.getClass())) {
1649                tList.addAll(McVGuiUtils.getDescendantsOfType(clazz,
1650                    (Container)component, nested));
1651            }
1652        }
1653        return tList;
1654    }
1655
1656    /**
1657     * Convenience method that searches below {@code container} in the
1658     * component hierarchy and returns the first found component that is an
1659     * instance of class {@code clazz} having the bound property value.
1660     * Returns {@code null} if such component cannot be found.
1661     * <P>
1662     * This method invokes
1663     * {@link #getDescendantOfType(Class, java.awt.Container, String, Object, boolean)}
1664     *
1665     * @param clazz the class of component whose instance is to be found.
1666     * @param container the container at which to begin the search
1667     * @param property the className of the bound property, exactly as expressed in
1668     * the accessor e.g. {@literal "Text"} for getText(), {@literal "Value"}
1669     * for getValue().
1670     * @param value the value of the bound property
1671     * @return the component, or null if no such component exists in the
1672     * container
1673     * @throws java.lang.IllegalArgumentException if the bound property does
1674     * not exist for the class or cannot be accessed
1675     */
1676    public static <T extends JComponent> T getDescendantOfType(
1677        Class<T> clazz, Container container, String property, Object value)
1678        throws IllegalArgumentException
1679    {
1680        return getDescendantOfType(clazz, container, property, value, true);
1681    }
1682
1683    /**
1684     * Convenience method that searches below {@code container} in the
1685     * component hierarchy and returns the first found component that is an
1686     * instance of class {@code clazz} and has the bound property value.
1687     * Returns {@code null} if such component cannot be found.
1688     *
1689     * @param clazz the class of component whose instance to be found.
1690     * @param container the container at which to begin the search
1691     * @param property the className of the bound property, exactly as expressed in
1692     * the accessor e.g. {@literal "Text"} for getText(), {@literal "Value"}
1693     * for getValue().
1694     * @param value the value of the bound property
1695     * @param nested true to list components nested within another component
1696     * which is also an instance of {@code clazz}, false otherwise.
1697     *
1698     * @return the component, or null if no such component exists in the
1699     * container.
1700     *
1701     * @throws java.lang.IllegalArgumentException if the bound property does
1702     * not exist for the class or cannot be accessed.
1703     */
1704    public static <T extends JComponent> T getDescendantOfType(Class<T> clazz,
1705                                                               Container container,
1706                                                               String property,
1707                                                               Object value,
1708                                                               boolean nested)
1709        throws IllegalArgumentException
1710    {
1711        List<T> list = getDescendantsOfType(clazz, container, nested);
1712        return getComponentFromList(clazz, list, property, value);
1713    }
1714
1715    /**
1716     * Convenience method for searching below {@code container} in the
1717     * component hierarchy and return nested components of class
1718     * {@code clazz} it finds.  Returns an empty list if no such
1719     * components exist in the container.
1720     * <P>
1721     * This method invokes
1722     * {@link #getDescendantsOfClass(Class, java.awt.Container, boolean)}.
1723     *
1724     * @param clazz the class of components to be found.
1725     * @param container the container at which to begin the search
1726     * @return the List of components
1727     */
1728    public static <T extends JComponent> List<T> getDescendantsOfClass(
1729        Class<T> clazz, Container container)
1730    {
1731        return getDescendantsOfClass(clazz, container, true);
1732    }
1733
1734    /**
1735     * Convenience method for searching below {@code container} in the
1736     * component hierarchy and return nested components of class
1737     * {@code clazz} it finds.  Returns an empty list if no such
1738     * components exist in the container.
1739     *
1740     * @param clazz the class of components to be found.
1741     * @param container the container at which to begin the search
1742     * @param nested true to list components nested within another listed
1743     * component, false otherwise
1744     *
1745     * @return the List of components
1746     */
1747    public static <T extends JComponent> List<T> getDescendantsOfClass(
1748        Class<T> clazz, Container container, boolean nested)
1749    {
1750        List<T> tList = new ArrayList<>();
1751        for (Component component : container.getComponents()) {
1752            if (clazz.equals(component.getClass())) {
1753                tList.add(clazz.cast(component));
1754            }
1755            if (nested || !clazz.equals(component.getClass())) {
1756                tList.addAll(McVGuiUtils.getDescendantsOfClass(clazz,
1757                    (Container)component, nested));
1758            }
1759        }
1760        return tList;
1761    }
1762
1763    /**
1764     * Convenience method that searches below {@code container} in the
1765     * component hierarchy in a depth first manner and returns the first
1766     * found component of class {@code clazz} having the bound property
1767     * value.
1768     * <P>
1769     * Returns {@code null} if such component cannot be found.
1770     * <P>
1771     * This method invokes
1772     * {@link #getDescendantOfClass(Class, java.awt.Container, String, Object, boolean)}
1773     *
1774     * @param clazz the class of component to be found.
1775     * @param container the container at which to begin the search
1776     * @param property the className of the bound property, exactly as expressed in
1777     * the accessor e.g. {@literal "Text"} for getText(), {@literal "Value"}
1778     * for getValue(). This parameter is case sensitive.
1779     * @param value the value of the bound property
1780     *
1781     * @return the component, or null if no such component exists in the
1782     * container's hierarchy.
1783     *
1784     * @throws java.lang.IllegalArgumentException if the bound property does
1785     * not exist for the class or cannot be accessed
1786     */
1787    public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
1788                                                                Container container,
1789                                                                String property,
1790                                                                Object value)
1791        throws IllegalArgumentException
1792    {
1793        return getDescendantOfClass(clazz, container, property, value, true);
1794    }
1795
1796    /**
1797     * Convenience method that searches below {@code container} in the
1798     * component hierarchy in a depth first manner and returns the first
1799     * found component of class {@code clazz} having the bound property
1800     * value.
1801     * <P>
1802     * Returns {@code null} if such component cannot be found.
1803     *
1804     * @param clazz the class of component to be found.
1805     * @param container the container at which to begin the search
1806     * @param property the className of the bound property, exactly as expressed
1807     * in the accessor e.g. {@literal "Text"} for getText(), {@literal "Value"}
1808     * for getValue(). This parameter is case sensitive.
1809     * @param value the value of the bound property
1810     * @param nested true to include components nested within another listed
1811     * component, false otherwise.
1812     *
1813     * @return the component, or null if no such component exists in the
1814     * container's hierarchy.
1815     *
1816     * @throws java.lang.IllegalArgumentException if the bound property does
1817     * not exist for the class or cannot be accessed
1818     */
1819    public static <T extends JComponent> T getDescendantOfClass(Class<T> clazz,
1820                                                                Container container,
1821                                                                String property,
1822                                                                Object value,
1823                                                                boolean nested)
1824        throws IllegalArgumentException
1825    {
1826        List<T> list = getDescendantsOfClass(clazz, container, nested);
1827        return getComponentFromList(clazz, list, property, value);
1828    }
1829
1830    private static <T extends JComponent> T getComponentFromList(Class<T> clazz,
1831                                                                 List<T> list,
1832                                                                 String property,
1833                                                                 Object value)
1834        throws IllegalArgumentException
1835    {
1836        T retVal = null;
1837        Method method = null;
1838        try {
1839            method = clazz.getMethod("get" + property);
1840        } catch (NoSuchMethodException ex) {
1841            try {
1842                method = clazz.getMethod("is" + property);
1843            } catch (NoSuchMethodException ex1) {
1844                throw new IllegalArgumentException("Property " + property +
1845                    " not found in class " + clazz.getName());
1846            }
1847        }
1848        try {
1849            for (T t : list) {
1850                Object testVal = method.invoke(t);
1851                if (equals(value, testVal)) {
1852                    return t;
1853                }
1854            }
1855        } catch (InvocationTargetException ex) {
1856            throw new IllegalArgumentException(
1857                "Error accessing property " + property +
1858                    " in class " + clazz.getName());
1859        } catch (IllegalAccessException ex) {
1860            throw new IllegalArgumentException(
1861                "Property " + property +
1862                    " cannot be accessed in class " + clazz.getName());
1863        } catch (SecurityException ex) {
1864            throw new IllegalArgumentException(
1865                "Property " + property +
1866                    " cannot be accessed in class " + clazz.getName());
1867        }
1868        return retVal;
1869    }
1870
1871    /**
1872     * Convenience method for determining whether two objects are either
1873     * equal or both null.
1874     *
1875     * @param obj1 the first reference object to compare.
1876     * @param obj2 the second reference object to compare.
1877     * @return true if obj1 and obj2 are equal or if both are null,
1878     * false otherwise
1879     */
1880    public static boolean equals(Object obj1, Object obj2) {
1881        return (obj1 == null) ? (obj2 == null) : obj1.equals(obj2);
1882    }
1883
1884    /**
1885     * Convenience method for mapping a container in the hierarchy to its
1886     * contained components.  The keys are the containers, and the values
1887     * are lists of contained components.
1888     * <P>
1889     * Implementation note:  The returned value is a HashMap and the values
1890     * are of type ArrayList.  This is subject to change, so callers should
1891     * code against the interfaces Map and List.
1892     *
1893     * @param container The JComponent to be mapped
1894     * @param nested true to drill down to nested containers, false otherwise
1895     * @return the Map of the UI
1896     */
1897    public static Map<JComponent, List<JComponent>> getComponentMap(
1898        JComponent container, boolean nested)
1899    {
1900        List<JComponent> descendants =
1901            getDescendantsOfType(JComponent.class, container, false);
1902        Map<JComponent, List<JComponent>> retVal =
1903            new HashMap<>(descendants.size());
1904        for (JComponent component : descendants) {
1905            if (!retVal.containsKey(container)) {
1906                retVal.put(container,
1907                    new ArrayList<JComponent>());
1908            }
1909            retVal.get(container).add(component);
1910            if (nested) {
1911                retVal.putAll(getComponentMap(component, nested));
1912            }
1913        }
1914        return retVal;
1915    }
1916
1917    /**
1918     * Convenience method for retrieving a subset of the UIDefaults pertaining
1919     * to a particular class.
1920     *
1921     * @param clazz the class of interest
1922     * @return the UIDefaults of the class
1923     */
1924    public static UIDefaults getUIDefaultsOfClass(Class<?> clazz) {
1925        String name = clazz.getName();
1926        name = name.substring(name.lastIndexOf(".") + 2);
1927        return getUIDefaultsOfClass(name);
1928    }
1929
1930    /**
1931     * Convenience method for retrieving a subset of the UIDefaults pertaining
1932     * to a particular class.
1933     *
1934     * @param className fully qualified name of the class of interest
1935     * @return the UIDefaults of the class named
1936     */
1937    public static UIDefaults getUIDefaultsOfClass(String className) {
1938        UIDefaults retVal = new UIDefaults();
1939        UIDefaults defaults = javax.swing.UIManager.getLookAndFeelDefaults();
1940        List<?> listKeys = Collections.list(defaults.keys());
1941        for (Object key : listKeys) {
1942            if ((key instanceof String) && ((String) key).startsWith(className)) {
1943                String stringKey = (String) key;
1944                String property = stringKey;
1945                if (stringKey.contains(".")) {
1946                    property = stringKey.substring(stringKey.indexOf(".") + 1);
1947                }
1948                retVal.put(property, defaults.get(key));
1949            }
1950        }
1951        return retVal;
1952    }
1953
1954    /**
1955     * Convenience method for retrieving the UIDefault for a single property
1956     * of a particular class.
1957     *
1958     * @param clazz the class of interest
1959     * @param property the property to query
1960     * @return the UIDefault property, or null if not found
1961     */
1962    public static Object getUIDefaultOfClass(Class<?> clazz, String property) {
1963        Object retVal = null;
1964        UIDefaults defaults = getUIDefaultsOfClass(clazz);
1965        List<Object> listKeys = Collections.list(defaults.keys());
1966        for (Object key : listKeys) {
1967            if (key.equals(property)) {
1968                return defaults.get(key);
1969            }
1970            if (key.toString().equalsIgnoreCase(property)) {
1971                retVal = defaults.get(key);
1972            }
1973        }
1974        return retVal;
1975    }
1976
1977    /**
1978     * Exclude methods that return values that are meaningless to the user
1979     */
1980    static Set<String> setExclude = new HashSet<>(10);
1981    static {
1982        setExclude.add("getFocusCycleRootAncestor");
1983        setExclude.add("getAccessibleContext");
1984        setExclude.add("getColorModel");
1985        setExclude.add("getGraphics");
1986        setExclude.add("getGraphicsConfiguration");
1987    }
1988
1989    /**
1990     * Convenience method for obtaining most non-null human readable properties
1991     * of a JComponent.  Array properties are not included.
1992     * <P>
1993     * Implementation note:  The returned value is a HashMap.  This is subject
1994     * to change, so callers should code against the interface Map.
1995     *
1996     * @param component the component whose proerties are to be determined
1997     * @return the class and value of the properties
1998     */
1999    public static Map<Object, Object> getProperties(JComponent component) {
2000        Class<?> clazz = component.getClass();
2001        Method[] methods = clazz.getMethods();
2002        Map<Object, Object> retVal = new HashMap<>(methods.length);
2003        Object value = null;
2004        for (Method method : methods) {
2005            if (method.getName().matches("^(is|get).*") &&
2006                (method.getParameterTypes().length == 0)) {
2007                try {
2008                    Class<?> returnType = method.getReturnType();
2009                    if ((returnType != void.class) &&
2010                        !returnType.getName().startsWith("[") &&
2011                        !setExclude.contains(method.getName())) {
2012                        String key = method.getName();
2013                        value = method.invoke(component);
2014                        if ((value != null) && !(value instanceof Component)) {
2015                            retVal.put(key, value);
2016                        }
2017                    }
2018                    // ignore exceptions that arise if the property could not be accessed
2019                } catch (IllegalAccessException ex) {
2020                } catch (IllegalArgumentException ex) {
2021                } catch (InvocationTargetException ex) {
2022                }
2023            }
2024        }
2025        return retVal;
2026    }
2027
2028    /**
2029     * Convenience method to obtain the Swing class from which this
2030     * component was directly or indirectly derived.
2031     *
2032     * @param component The component whose Swing superclass is to be
2033     * determined
2034     * @return The nearest Swing class in the inheritance tree
2035     */
2036    public static <T extends JComponent> Class<?> getJClass(T component) {
2037        Class<?> clazz = component.getClass();
2038        while (!clazz.getName().matches("javax.swing.J[^.]*$")) {
2039            clazz = clazz.getSuperclass();
2040        }
2041        return clazz;
2042    }
2043
2044    /**
2045     * Gets the {@literal "text"} contents of a {@link JTextComponent} without
2046     * the possibility of a {@code NullPointerException}.
2047     *
2048     * @param textComponent {@code JTextComponent} whose contents should be
2049     * extracted. {@code null} is allowed.
2050     *
2051     * @return Either the results of {@link JTextComponent#getText()} or an
2052     * empty {@code String} if {@code textComponent} or {@code getText()} are
2053     * {@code null}.
2054     */
2055    public static String safeGetText(JTextComponent textComponent) {
2056        String value = "";
2057        if ((textComponent != null) && (textComponent.getText() != null)) {
2058            value = textComponent.getText();
2059        }
2060        return value;
2061    }
2062}