001/*
002 * $Id: McvComponentGroup.java,v 1.32 2011/03/24 18:13:11 davep Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030
031package edu.wisc.ssec.mcidasv.ui;
032
033import java.awt.BorderLayout;
034import java.awt.Component;
035import java.awt.event.ActionEvent;
036import java.awt.event.ActionListener;
037import java.awt.event.MouseAdapter;
038import java.awt.event.MouseEvent;
039import java.lang.reflect.InvocationTargetException;
040import java.net.URL;
041import java.util.ArrayList;
042import java.util.List;
043
044import javax.swing.ImageIcon;
045import javax.swing.JComponent;
046import javax.swing.JMenuItem;
047import javax.swing.JOptionPane;
048import javax.swing.JPanel;
049import javax.swing.JPopupMenu;
050import javax.swing.SwingUtilities;
051import javax.swing.border.BevelBorder;
052
053import org.w3c.dom.Document;
054import org.w3c.dom.Element;
055
056import ucar.unidata.idv.IdvResourceManager;
057import ucar.unidata.idv.IntegratedDataViewer;
058import ucar.unidata.idv.MapViewManager;
059import ucar.unidata.idv.TransectViewManager;
060import ucar.unidata.idv.ViewDescriptor;
061import ucar.unidata.idv.ViewManager;
062import ucar.unidata.idv.control.DisplayControlImpl;
063import ucar.unidata.idv.ui.IdvComponentGroup;
064import ucar.unidata.idv.ui.IdvComponentHolder;
065import ucar.unidata.idv.ui.IdvUIManager;
066import ucar.unidata.idv.ui.IdvWindow;
067import ucar.unidata.ui.ComponentHolder;
068import ucar.unidata.util.GuiUtils;
069import ucar.unidata.util.LayoutUtil;
070import ucar.unidata.util.LogUtil;
071import ucar.unidata.util.Msg;
072import ucar.unidata.xml.XmlResourceCollection;
073import ucar.unidata.xml.XmlUtil;
074
075import edu.wisc.ssec.mcidasv.PersistenceManager;
076
077/**
078 * Extends the IDV component groups so that we can intercept clicks for Bruce's
079 * tab popup menu and handle drag and drop. It also intercepts ViewManager
080 * creation in order to wrap components in McIDASVComponentHolders rather than
081 * IdvComponentHolders. Doing this allows us to associate ViewManagers back to
082 * their ComponentHolders, and this functionality is taken advantage of to form
083 * the hierarchical names seen in the McIDASVViewPanel.
084 */
085public class McvComponentGroup extends IdvComponentGroup {
086
087    /** Path to the "close tab" icon in the popup menu. */
088    protected static final String ICO_CLOSE =
089        "/edu/wisc/ssec/mcidasv/resources/icons/tabmenu/stop-loads16.png";
090
091    /** Path to the "rename" icon in the popup menu. */
092    protected static final String ICO_RENAME =
093        "/edu/wisc/ssec/mcidasv/resources/icons/tabmenu/accessories-text-editor16.png";
094
095    /** Path to the eject icon in the popup menu. */
096    protected static final String ICO_UNDOCK =
097        "/edu/wisc/ssec/mcidasv/resources/icons/tabmenu/media-eject16.png";
098
099    /** Action command for destroying a display. */
100    private static final String CMD_DISPLAY_DESTROY = "DESTROY_DISPLAY_TAB";
101
102    /** Action command for ejecting a display from a tab. */
103    private static final String CMD_DISPLAY_EJECT = "EJECT_TAB";
104
105    /** Action command for renaming a display. */
106    private static final String CMD_DISPLAY_RENAME = "RENAME_DISPLAY";
107
108    /** The popup menu for the McV tabbed display interface. */
109    private final JPopupMenu popup = doMakeTabMenu();
110
111    /** Number of tabs that have been stored in this group. */
112    @SuppressWarnings("unused")
113    private int tabCount = 0;
114
115    /** Whether or not <code>init</code> has been called. */
116    private boolean initDone = false;
117
118    /**
119     * Holders that McV knows are held by this component group. Used to avoid
120     * any needless work in <code>redoLayout</code>.
121     */
122    private List<ComponentHolder> knownHolders =
123        new ArrayList<ComponentHolder>();
124
125    /** Keep a reference to avoid extraneous calls to <tt>getIdv().</tt> */
126    private IntegratedDataViewer idv;
127
128    /** Reference to the window associated with this group. */
129    private IdvWindow window = IdvWindow.getActiveWindow();
130
131    /** 
132     * Whether or not {@link #redoLayout()} needs to worry about a renamed 
133     * tab. 
134     */
135    private boolean tabRenamed = false;
136
137    /**
138     * Default constructor for serialization.
139     */
140    public McvComponentGroup() {}
141
142    /**
143     * A pretty typical constructor.
144     * 
145     * @param idv The main IDV instance.
146     * @param name Presumably the name of this component group?
147     */
148    public McvComponentGroup(final IntegratedDataViewer idv, 
149        final String name) 
150    {
151        super(idv, name);
152        this.idv = idv;
153        init();
154    }
155
156    /**
157     * This constructor catches the window that will be contain this group.
158     * 
159     * @param idv The main IDV instance.
160     * @param name Presumably the name of this component group?
161     * @param window The window holding this component group.
162     */
163    public McvComponentGroup(final IntegratedDataViewer idv,
164        final String name, final IdvWindow window) 
165    {
166        super(idv, name);
167        this.window = window;
168        this.idv = idv;
169        init();
170    }
171
172    /**
173     * Initializes the various UI components.
174     */
175    private void init() {
176        if (initDone) {
177            return;
178        }
179        tabbedPane = new DraggableTabbedPane(window, idv, this);
180//        tabbedPane.addMouseListener(new TabPopupListener());
181
182        container = new JPanel(new BorderLayout());
183        container.add(tabbedPane);
184//        container.addComponentListener(new ComponentListener() {
185//            @Override public void componentHidden(ComponentEvent e) {
186//                
187//            }
188//            @Override public void componentShown(ComponentEvent e) {
189//                
190//            }
191//            @Override public void componentMoved(ComponentEvent e) {}
192//            @Override public void componentResized(ComponentEvent e) {}
193//        });
194        GuiUtils.handleHeavyWeightComponentsInTabs(tabbedPane);
195        initDone = true;
196    }
197
198    /**
199     * Create and return the GUI contents. Overridden so that McV can implement
200     * the right click tab menu and draggable tabs.
201     * 
202     * @return GUI contents
203     */
204    @Override public JComponent doMakeContents() {
205        redoLayout();
206        outerContainer = LayoutUtil.center(container);
207        outerContainer.validate();
208        return outerContainer;
209    }
210
211    /**
212     * <p>
213     * Importing a display control entails adding the control to the component
214     * group and informing the UI that the control is no longer in its own
215     * window.
216     * </p>
217     * 
218     * <p>
219     * Overridden in McV so that the display control is wrapped in a
220     * McIDASVComponentHolder rather than a IdvComponentHolder.
221     * </p>
222     * 
223     * @param dc The display control to import.
224     */
225    @Override public void importDisplayControl(final DisplayControlImpl dc) {
226        if (dc.getComponentHolder() != null) {
227            dc.getComponentHolder().removeDisplayControl(dc);
228        }
229        idv.getIdvUIManager().getViewPanel().removeDisplayControl(dc);
230        dc.guiImported();
231        addComponent(new McvComponentHolder(idv, dc));
232    }
233
234    /**
235     * Basically just creates a McVCompHolder for holding a dynamic skin and
236     * sets the name of the component holder.
237     * 
238     * @param root The XML skin that we'll use.
239     */
240    public void makeDynamicSkin(final Element root) {
241        IdvComponentHolder comp =
242            new McvComponentHolder(idv, XmlUtil.toString(root));
243
244        comp.setType(McvComponentHolder.TYPE_DYNAMIC_SKIN);
245        comp.setName("Dynamic Skin Test");
246        addComponent(comp);
247        comp.doMakeContents();
248    }
249
250    /**
251     * Doesn't do anything for the time being...
252     * 
253     * @param doc
254     * 
255     * @return XML representation of the contents of this component group.
256     */
257    @Override public Element createXmlNode(final Document doc) {
258        // System.err.println("caught createXmlNode");
259        Element e = super.createXmlNode(doc);
260        // System.err.println(XmlUtil.toString(e));
261        // System.err.println("exit createXmlNode");
262        return e;
263    }
264
265    /**
266     * <p>
267     * Handles creation of the component represented by the XML skin at the
268     * given index.
269     * </p>
270     * 
271     * <p>
272     * Overridden so that McV can wrap the component in a
273     * McIDASVComponentHolder.
274     * </p>
275     * 
276     * @param index The index of the skin within the skin resource.
277     */
278    @Override public void makeSkin(final int index) {
279        final XmlResourceCollection skins = idv.getResourceManager().getXmlResources(
280            IdvResourceManager.RSC_SKIN);
281
282//        String id = skins.getProperty("skinid", index);
283//        if (id == null)
284//            id = skins.get(index).toString();
285
286//        SwingUtilities.invokeLater(new Runnable() {
287//            public void run() {
288                String id = skins.getProperty("skinid", index);
289                if (id == null)
290                    id = skins.get(index).toString();
291                IdvComponentHolder comp = new McvComponentHolder(idv, id);
292                comp.setType(IdvComponentHolder.TYPE_SKIN);
293                comp.setName("untitled");
294
295                addComponent(comp);
296//            }
297//        });
298    }
299
300    /**
301     * <p>
302     * Create a new component whose type will be determined by the contents of
303     * <code>what</code>.
304     * </p>
305     * 
306     * <p>
307     * Overridden so that McV can wrap up the components in
308     * McVComponentHolders, which allow McV to map ViewManagers to
309     * ComponentHolders.
310     * </p>
311     * 
312     * @param what String that determines what sort of component we create.
313     */
314    @Override public void makeNew(final String what) {
315        try {
316            ViewManager vm = null;
317            ComponentHolder comp = null;
318            String property = "showControlLegend=false";
319            ViewDescriptor desc = new ViewDescriptor();
320
321            // we're only really interested in map, globe, or transect views.
322            if (what.equals(IdvUIManager.COMP_MAPVIEW)) {
323                vm = new MapViewManager(idv, desc, property);
324            } else if (what.equals(IdvUIManager.COMP_TRANSECTVIEW)) {
325                vm = new TransectViewManager(idv, desc, property);
326            } else if (what.equals(IdvUIManager.COMP_GLOBEVIEW)) {
327                vm = new MapViewManager(idv, desc, property);
328                ((MapViewManager)vm).setUseGlobeDisplay(true);
329            } else {
330                // hand off uninteresting things to the IDV
331                super.makeNew(what);
332                return;
333            }
334
335            // make sure we get the component into a mcv component holder,
336            // otherwise we won't be able to easily map ViewManagers to
337            // ComponentHolders for the hierarchical names in the ViewPanel.
338            idv.getVMManager().addViewManager(vm);
339            comp = new McvComponentHolder(idv, vm);
340
341            if (comp != null) {
342                addComponent(comp);
343//                GuiUtils.showComponentInTabs(comp.getContents());
344            }
345
346        } catch (Exception exc) {
347            LogUtil.logException("Error making new " + what, exc);
348        }
349    }
350
351    /**
352     * <p>
353     * Forces this group to layout its components. Extended because the IDV was
354     * doing extra work that McIDAS-V doesn't need, such as dealing with
355     * layouts other than LAYOUT_TABS and needlessly reinitializing the group's
356     * container.
357     * </p>
358     * 
359     * @see ucar.unidata.ui.ComponentGroup#redoLayout()
360     */
361    @SuppressWarnings("unchecked")
362    @Override public void redoLayout() {
363        final List<ComponentHolder> currentHolders = getDisplayComponents();
364        if (!tabRenamed && knownHolders.equals(currentHolders)) {
365            return;
366        }
367
368        if (tabbedPane == null) {
369            return;
370        }
371
372        Runnable updateGui = new Runnable() {
373            public void run() {
374                int selectedIndex = tabbedPane.getSelectedIndex();
375
376                tabbedPane.setVisible(false);
377                tabbedPane.removeAll();
378
379                knownHolders = new ArrayList<ComponentHolder>(currentHolders);
380                for (ComponentHolder holder : knownHolders) {
381                    tabbedPane.addTab(holder.getName(), holder.getContents());
382                }
383
384                if (tabRenamed) {
385                    tabbedPane.setSelectedIndex(selectedIndex);
386                }
387
388                tabbedPane.setVisible(true);
389                tabRenamed = false;
390            }
391        };
392        
393        if (SwingUtilities.isEventDispatchThread()) {
394            SwingUtilities.invokeLater(updateGui);
395        } else {
396            try {
397                SwingUtilities.invokeAndWait(updateGui);
398            } catch (InterruptedException e) {
399                // TODO Auto-generated catch block
400                e.printStackTrace();
401            } catch (InvocationTargetException e) {
402                // TODO Auto-generated catch block
403                e.printStackTrace();
404            }
405        }
406    }
407
408    // TODO(jon): remove this method if Unidata implements your fix.
409    @Override public void getViewManagers(@SuppressWarnings("rawtypes") final List viewManagers) {
410        if ((viewManagers == null) || (getDisplayComponents() == null)) {
411//            logger.debug("McvComponentGroup.getViewManagers(): bailing out early!");
412            return;
413        }
414
415        super.getViewManagers(viewManagers);
416    }
417
418    /**
419     * <p>
420     * Adds a component holder to this group. Extended so that the added holder
421     * becomes the active tab, and the component is explicitly set to visible
422     * in an effort to fix that heavyweight/lightweight component problem.
423     * </p>
424     * 
425     * @param holder
426     * @param index
427     * 
428     * @see ucar.unidata.ui.ComponentGroup#addComponent(ComponentHolder, int)
429     */
430    @Override public void addComponent(final ComponentHolder holder,
431        final int index) 
432    {
433        if (shouldGenerateName(holder, index)) {
434            holder.setName("untitled");
435        }
436
437        if (holder.getName().trim().length() == 0) {
438            holder.setName("untitled");
439        }
440
441        super.addComponent(holder, index);
442        setActiveComponentHolder(holder);
443        holder.getContents().setVisible(true);
444
445        if (window != null) {
446            window.setTitle(makeWindowTitle(holder.getName()));
447        }
448    }
449
450    private boolean shouldGenerateName(final ComponentHolder h, final int i) {
451        if (h.getName() != null && !h.getName().startsWith("untitled")) {
452            return false;
453        }
454
455        boolean invalidIndex = (i >= 0);
456        boolean withoutName = (h.getName() == null || h.getName().length() == 0);
457        boolean loadingBundle = ((PersistenceManager)getIdv().getPersistenceManager()).isBundleLoading();
458
459        return invalidIndex || withoutName || !loadingBundle;
460    }
461
462    /**
463     * Used to set the tab associated with {@code holder} as the active tab 
464     * in our {@link JTabbedPane}.
465     * 
466     * @param holder The active component holder.
467     */
468    public void setActiveComponentHolder(final ComponentHolder holder) {
469        if (getDisplayComponentCount() > 1) {
470            final int newIdx = getDisplayComponents().indexOf(holder);
471            SwingUtilities.invokeLater(new Runnable() {
472                public void run() {
473                    setActiveIndex(newIdx);
474                }
475            });
476            
477        }
478
479        // TODO: this doesn't work quite right...
480        if (window == null) {
481            window = IdvWindow.getActiveWindow();
482        }
483        if (window != null) {
484//            SwingUtilities.invokeLater(new Runnable() {
485//                public void run() {
486                    window.toFront();
487//                  window.setTitle(holder.getName());
488                    window.setTitle(makeWindowTitle(holder.getName()));
489//                }
490//            });
491        }
492    }
493
494    /**
495     * @return The index of the active component holder within this group.
496     */
497    public int getActiveIndex() {
498        if (tabbedPane == null) {
499            return -1;
500        } else {
501            return tabbedPane.getSelectedIndex();
502        }
503    }
504
505    /**
506     * Make the component holder at {@code index} active.
507     * 
508     * @param index The index of the desired component holder.
509     * 
510     * @return True if the active component holder was set, false otherwise.
511     */
512    public boolean setActiveIndex(final int index) {
513        int size = getDisplayComponentCount();
514        if ((index < 0) || (index >= size)) {
515            return false;
516        }
517
518//        SwingUtilities.invokeLater(new Runnable() {
519//            public void run() {
520                tabbedPane.setSelectedIndex(index);
521                if (window != null) {
522                    ComponentHolder h = (ComponentHolder)getDisplayComponents().get(index);
523                    if (h != null) {
524                        window.setTitle(makeWindowTitle(h.getName()));
525                    }
526                }
527//            }
528//        });
529        return true;
530    }
531
532    /**
533     * Returns the index of {@code holder} within this component group.
534     * 
535     * @return Either the index of {@code holder}, or {@code -1} 
536     * if {@link #getDisplayComponents()} returns a {@code null} {@link List}.
537     * 
538     * @see List#indexOf(Object)
539     */
540    @Override public int indexOf(final ComponentHolder holder) {
541        @SuppressWarnings("rawtypes")
542        List dispComps = getDisplayComponents();
543        if (dispComps == null) {
544            return -1;
545        } else {
546            return getDisplayComponents().indexOf(holder);
547        }
548    }
549
550    /**
551     * Returns the {@link ComponentHolder} at the given position within this
552     * component group. 
553     * 
554     * @param index Index of the {@code ComponentHolder} to return.
555     * 
556     * @return {@code ComponentHolder} at {@code index}.
557     * 
558     * @see List#get(int)
559     */
560    protected ComponentHolder getHolderAt(final int index) {
561        @SuppressWarnings("unchecked")
562        List<ComponentHolder> dispComps = getDisplayComponents();
563        return dispComps.get(index);
564    }
565
566    /**
567     * @return Component holder that corresponds to the selected tab.
568     */
569    public ComponentHolder getActiveComponentHolder() {
570        int idx = 0;
571
572        if (getDisplayComponentCount() > 1) {
573//            idx = tabbedPane.getSelectedIndex();
574            idx = getActiveIndex();
575        }
576
577//        return (ComponentHolder)getDisplayComponents().get(idx);
578        return getHolderAt(idx);
579    }
580
581    /**
582     * Overridden so that McV can also update its copy of the IDV reference.
583     */
584    @Override public void setIdv(final IntegratedDataViewer newIdv) {
585        super.setIdv(newIdv);
586        idv = newIdv;
587    }
588
589    /**
590     * Create a window title suitable for an application window.
591     * 
592     * @param title Window title
593     * 
594     * @return Application title plus the window title.
595     */
596    private String makeWindowTitle(final String title) {
597        String defaultApplicationName = "McIDAS-V";
598        if (idv != null) {
599            defaultApplicationName = idv.getStateManager().getTitle();
600        }
601        return UIManager.makeTitle(defaultApplicationName, title);
602    }
603
604    /**
605     * Returns the number of display components {@literal "in"} this group.
606     * 
607     * @return Either the {@code size()} of the {@link List} returned by 
608     * {@link #getDisplayComponents()} or {@code -1} if 
609     * {@code getDisplayComponents()} returns a {@code null} {@code List}.
610     */
611    protected int getDisplayComponentCount() {
612        @SuppressWarnings("rawtypes")
613        List dispComps = getDisplayComponents();
614        if (dispComps == null) {
615            return -1;
616        } else {
617            return dispComps.size();
618        }
619    }
620    
621    /**
622     * Create the <tt>JPopupMenu</tt> that will be displayed for a tab.
623     * 
624     * @return Menu initialized with tab options
625     */
626    protected JPopupMenu doMakeTabMenu() {
627        ActionListener menuListener = new ActionListener() {
628            public void actionPerformed(ActionEvent evt) {
629                final String cmd = evt.getActionCommand();
630                if (CMD_DISPLAY_EJECT.equals(cmd)) {
631                    ejectDisplay(tabbedPane.getSelectedIndex());
632                } else if (CMD_DISPLAY_RENAME.equals(cmd)) {
633                    renameDisplay(tabbedPane.getSelectedIndex());
634                } else if (CMD_DISPLAY_DESTROY.equals(cmd)) {
635                    destroyDisplay(tabbedPane.getSelectedIndex());
636                }
637            }
638        };
639
640        final JPopupMenu popup = new JPopupMenu();
641        JMenuItem item;
642
643        // URL img = getClass().getResource(ICO_UNDOCK);
644        // item = new JMenuItem("Undock", new ImageIcon(img));
645        // item.setActionCommand(CMD_DISPLAY_EJECT);
646        // item.addActionListener(menuListener);
647        // popup.add(item);
648
649        URL img = getClass().getResource(ICO_RENAME);
650        item = new JMenuItem("Rename", new ImageIcon(img));
651        item.setActionCommand(CMD_DISPLAY_RENAME);
652        item.addActionListener(menuListener);
653        popup.add(item);
654
655        // popup.addSeparator();
656
657        img = getClass().getResource(ICO_CLOSE);
658        item = new JMenuItem("Close", new ImageIcon(img));
659        item.setActionCommand(CMD_DISPLAY_DESTROY);
660        item.addActionListener(menuListener);
661        popup.add(item);
662
663        popup.setBorder(new BevelBorder(BevelBorder.RAISED));
664
665        Msg.translateTree(popup);
666        return popup;
667    }
668
669    /**
670     * Remove the component holder at index {@code idx}. This method does
671     * not destroy the component holder.
672     * 
673     * @param idx Index of the ejected component holder.
674     * 
675     * @return Component holder that was ejected.
676     */
677    private ComponentHolder ejectDisplay(final int idx) {
678        return null;
679    }
680
681    /**
682     * Prompt the user to change the name of the component holder at index
683     * {@code idx}. Nothing happens if the user doesn't enter anything.
684     * 
685     * @param idx Index of the component holder.
686     */
687    protected void renameDisplay(final int idx) {
688        final String title =
689            JOptionPane.showInputDialog(
690                IdvWindow.getActiveWindow().getFrame(), "Enter new name",
691                makeWindowTitle("Rename Tab"), JOptionPane.PLAIN_MESSAGE);
692
693        if (title == null) {
694            return;
695        }
696
697//        final List<ComponentHolder> comps = getDisplayComponents();
698//        comps.get(idx).setName(title);
699        getHolderAt(idx).setName(title);
700        tabRenamed = true;
701        if (window != null) {
702            window.setTitle(makeWindowTitle(title));
703        }
704        redoLayout();
705    }
706
707    /**
708     * Prompts the user to confirm removal of the component holder at index
709     * {@code idx}. Nothing happens if the user declines.
710     * 
711     * @param idx Index of the component holder.
712     * 
713     * @return Either {@code true} if the user elected to remove, 
714     * {@code false} otherwise.
715     */
716    protected boolean destroyDisplay(final int idx) {
717//        final List<IdvComponentHolder> comps = getDisplayComponents();
718//        IdvComponentHolder comp = comps.get(idx);
719        return ((IdvComponentHolder)getHolderAt(idx)).removeDisplayComponent();
720//        return comp.removeDisplayComponent();
721    }
722
723    /**
724     * Remove the component at {@code index} without forcing the IDV-land
725     * component group to redraw.
726     * 
727     * @param index The index of the component to be removed.
728     * 
729     * @return The removed component.
730     */
731    @SuppressWarnings("unchecked")
732    public ComponentHolder quietRemoveComponentAt(final int index) {
733        List<ComponentHolder> comps = getDisplayComponents();
734        if (comps == null || comps.size() == 0) {
735            return null;
736        }
737        ComponentHolder removed = comps.remove(index);
738        removed.setParent(null);
739        return removed;
740    }
741
742    /**
743     * Adds a component to the end of the list of display components without
744     * forcing the IDV-land code to redraw.
745     * 
746     * @param component The component to add.
747     * 
748     * @return The index of the newly added component, or {@code -1} if 
749     * {@link #getDisplayComponents()} returned a null {@code List}.
750     */
751    @SuppressWarnings("unchecked")
752    public int quietAddComponent(final ComponentHolder component) {
753        List<ComponentHolder> comps = getDisplayComponents();
754        if (comps == null) {
755            return -1;
756        }
757        if (comps.contains(component)) {
758            comps.remove(component);
759        }
760        comps.add(component);
761        component.setParent(this);
762        return comps.indexOf(component);
763    }
764
765    /**
766     * Handle pop-up events for tabs.
767     */
768    @SuppressWarnings("unused")
769    private class TabPopupListener extends MouseAdapter {
770
771        @Override public void mouseClicked(final MouseEvent evt) {
772            checkPopup(evt);
773        }
774
775        @Override public void mousePressed(final MouseEvent evt) {
776            checkPopup(evt);
777        }
778
779        @Override public void mouseReleased(final MouseEvent evt) {
780            checkPopup(evt);
781        }
782
783        /**
784         * <p>
785         * Determines whether or not the tab popup menu should be shown, and
786         * if so, which parts of it should be enabled or disabled.
787         * </p>
788         * 
789         * @param evt Allows us to determine the type of event.
790         */
791        private void checkPopup(final MouseEvent evt) {
792            if (evt.isPopupTrigger()) {
793                // can't close or eject last tab
794                // TODO: re-evaluate this
795                Component[] comps = popup.getComponents();
796                for (Component comp : comps) {
797                    if (comp instanceof JMenuItem) {
798                        String cmd = ((JMenuItem)comp).getActionCommand();
799                        if ((CMD_DISPLAY_DESTROY.equals(cmd) || CMD_DISPLAY_EJECT.equals(cmd))
800                            && tabbedPane.getTabCount() == 1) {
801                            comp.setEnabled(false);
802                        } else {
803                            comp.setEnabled(true);
804                        }
805                    }
806                }
807                popup.show(tabbedPane, evt.getX(), evt.getY());
808            }
809        }
810    }
811}