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