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