001    /*
002     * $Id: McvComponentGroup.java,v 1.36 2012/03/12 18:16:23 jbeavers Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
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    
031    package edu.wisc.ssec.mcidasv.ui;
032    
033    import java.awt.BorderLayout;
034    import java.awt.Component;
035    import java.awt.event.ActionEvent;
036    import java.awt.event.ActionListener;
037    import java.awt.event.MouseAdapter;
038    import java.awt.event.MouseEvent;
039    import java.lang.reflect.InvocationTargetException;
040    import java.net.URL;
041    import java.util.ArrayList;
042    import java.util.List;
043    
044    import javax.swing.ImageIcon;
045    import javax.swing.JComponent;
046    import javax.swing.JMenuItem;
047    import javax.swing.JOptionPane;
048    import javax.swing.JPanel;
049    import javax.swing.JPopupMenu;
050    import javax.swing.SwingUtilities;
051    import javax.swing.border.BevelBorder;
052    
053    import org.w3c.dom.Document;
054    import org.w3c.dom.Element;
055    
056    import ucar.unidata.idv.IdvResourceManager;
057    import ucar.unidata.idv.IntegratedDataViewer;
058    import ucar.unidata.idv.MapViewManager;
059    import ucar.unidata.idv.TransectViewManager;
060    import ucar.unidata.idv.ViewDescriptor;
061    import ucar.unidata.idv.ViewManager;
062    import ucar.unidata.idv.control.DisplayControlImpl;
063    import ucar.unidata.idv.ui.IdvComponentGroup;
064    import ucar.unidata.idv.ui.IdvComponentHolder;
065    import ucar.unidata.idv.ui.IdvUIManager;
066    import ucar.unidata.idv.ui.IdvWindow;
067    import ucar.unidata.ui.ComponentHolder;
068    import ucar.unidata.util.GuiUtils;
069    import ucar.unidata.util.LayoutUtil;
070    import ucar.unidata.util.LogUtil;
071    import ucar.unidata.util.Msg;
072    import ucar.unidata.xml.XmlResourceCollection;
073    import ucar.unidata.xml.XmlUtil;
074    
075    import 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     */
085    public 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            makeSkinAtIndex(index);
299        }
300        
301        public IdvComponentHolder makeSkinAtIndex(final int index) {
302            final XmlResourceCollection skins = idv.getResourceManager().getXmlResources(
303                            IdvResourceManager.RSC_SKIN);
304            String id = skins.getProperty("skinid", index);
305            if (id == null) {
306                id = skins.get(index).toString();
307            }
308            IdvComponentHolder comp = new McvComponentHolder(idv, id);
309            comp.setType(IdvComponentHolder.TYPE_SKIN);
310            comp.setName("untitled");
311    
312            addComponent(comp);
313            return comp;
314        }
315    
316        /**
317         * <p>
318         * Create a new component whose type will be determined by the contents of
319         * <code>what</code>.
320         * </p>
321         * 
322         * <p>
323         * Overridden so that McV can wrap up the components in
324         * McVComponentHolders, which allow McV to map ViewManagers to
325         * ComponentHolders.
326         * </p>
327         * 
328         * @param what String that determines what sort of component we create.
329         */
330        @Override public void makeNew(final String what) {
331            try {
332                ViewManager vm = null;
333                ComponentHolder comp = null;
334                String property = "showControlLegend=false";
335                ViewDescriptor desc = new ViewDescriptor();
336    
337                // we're only really interested in map, globe, or transect views.
338                if (what.equals(IdvUIManager.COMP_MAPVIEW)) {
339                    vm = new MapViewManager(idv, desc, property);
340                } else if (what.equals(IdvUIManager.COMP_TRANSECTVIEW)) {
341                    vm = new TransectViewManager(idv, desc, property);
342                } else if (what.equals(IdvUIManager.COMP_GLOBEVIEW)) {
343                    vm = new MapViewManager(idv, desc, property);
344                    ((MapViewManager)vm).setUseGlobeDisplay(true);
345                } else {
346                    // hand off uninteresting things to the IDV
347                    super.makeNew(what);
348                    return;
349                }
350    
351                // make sure we get the component into a mcv component holder,
352                // otherwise we won't be able to easily map ViewManagers to
353                // ComponentHolders for the hierarchical names in the ViewPanel.
354                idv.getVMManager().addViewManager(vm);
355                comp = new McvComponentHolder(idv, vm);
356    
357                if (comp != null) {
358                    addComponent(comp);
359    //                GuiUtils.showComponentInTabs(comp.getContents());
360                }
361    
362            } catch (Exception exc) {
363                LogUtil.logException("Error making new " + what, exc);
364            }
365        }
366    
367        /**
368         * <p>
369         * Forces this group to layout its components. Extended because the IDV was
370         * doing extra work that McIDAS-V doesn't need, such as dealing with
371         * layouts other than LAYOUT_TABS and needlessly reinitializing the group's
372         * container.
373         * </p>
374         * 
375         * @see ucar.unidata.ui.ComponentGroup#redoLayout()
376         */
377        @SuppressWarnings("unchecked")
378        @Override public void redoLayout() {
379            final List<ComponentHolder> currentHolders = getDisplayComponents();
380            if (!tabRenamed && knownHolders.equals(currentHolders)) {
381                return;
382            }
383    
384            if (tabbedPane == null) {
385                return;
386            }
387    
388            Runnable updateGui = new Runnable() {
389                public void run() {
390                    int selectedIndex = tabbedPane.getSelectedIndex();
391    
392                    tabbedPane.setVisible(false);
393                    tabbedPane.removeAll();
394    
395                    knownHolders = new ArrayList<ComponentHolder>(currentHolders);
396                    for (ComponentHolder holder : knownHolders) {
397                        tabbedPane.addTab(holder.getName(), holder.getContents());
398                    }
399    
400                    if (tabRenamed) {
401                        tabbedPane.setSelectedIndex(selectedIndex);
402                    }
403    
404                    tabbedPane.setVisible(true);
405                    tabRenamed = false;
406                }
407            };
408            
409            if (SwingUtilities.isEventDispatchThread()) {
410                SwingUtilities.invokeLater(updateGui);
411            } else {
412                try {
413                    SwingUtilities.invokeAndWait(updateGui);
414                } catch (InterruptedException e) {
415                    // TODO Auto-generated catch block
416                    e.printStackTrace();
417                } catch (InvocationTargetException e) {
418                    // TODO Auto-generated catch block
419                    e.printStackTrace();
420                }
421            }
422        }
423    
424        // TODO(jon): remove this method if Unidata implements your fix.
425        @Override public void getViewManagers(@SuppressWarnings("rawtypes") final List viewManagers) {
426            if ((viewManagers == null) || (getDisplayComponents() == null)) {
427    //            logger.debug("McvComponentGroup.getViewManagers(): bailing out early!");
428                return;
429            }
430    
431            super.getViewManagers(viewManagers);
432        }
433    
434        /**
435         * <p>
436         * Adds a component holder to this group. Extended so that the added holder
437         * becomes the active tab, and the component is explicitly set to visible
438         * in an effort to fix that heavyweight/lightweight component problem.
439         * </p>
440         * 
441         * @param holder
442         * @param index
443         * 
444         * @see ucar.unidata.ui.ComponentGroup#addComponent(ComponentHolder, int)
445         */
446        @Override public void addComponent(final ComponentHolder holder,
447            final int index) 
448        {
449            if (shouldGenerateName(holder, index)) {
450                holder.setName("untitled");
451            }
452    
453            if (holder.getName().trim().length() == 0) {
454                holder.setName("untitled");
455            }
456    
457            super.addComponent(holder, index);
458            setActiveComponentHolder(holder);
459            holder.getContents().setVisible(true);
460    
461            if (window != null) {
462                window.setTitle(makeWindowTitle(holder.getName()));
463            }
464        }
465    
466        private boolean shouldGenerateName(final ComponentHolder h, final int i) {
467            if (h.getName() != null && !h.getName().startsWith("untitled")) {
468                return false;
469            }
470    
471            boolean invalidIndex = (i >= 0);
472            boolean withoutName = (h.getName() == null || h.getName().length() == 0);
473            boolean loadingBundle = ((PersistenceManager)getIdv().getPersistenceManager()).isBundleLoading();
474    
475            return invalidIndex || withoutName || !loadingBundle;
476        }
477    
478        /**
479         * Used to set the tab associated with {@code holder} as the active tab 
480         * in our {@link JTabbedPane}.
481         * 
482         * @param holder The active component holder.
483         */
484        public void setActiveComponentHolder(final ComponentHolder holder) {
485            if (getDisplayComponentCount() > 1) {
486                final int newIdx = getDisplayComponents().indexOf(holder);
487                SwingUtilities.invokeLater(new Runnable() {
488                    public void run() {
489                        setActiveIndex(newIdx);
490                    }
491                });
492                
493            }
494    
495            // TODO: this doesn't work quite right...
496            if (window == null) {
497                window = IdvWindow.getActiveWindow();
498            }
499            if (window != null) {
500    //            SwingUtilities.invokeLater(new Runnable() {
501    //                public void run() {
502                        window.toFront();
503    //                  window.setTitle(holder.getName());
504                        window.setTitle(makeWindowTitle(holder.getName()));
505    //                }
506    //            });
507            }
508        }
509    
510        /**
511         * @return The index of the active component holder within this group.
512         */
513        public int getActiveIndex() {
514            if (tabbedPane == null) {
515                return -1;
516            } else {
517                return tabbedPane.getSelectedIndex();
518            }
519        }
520    
521        /**
522         * Make the component holder at {@code index} active.
523         * 
524         * @param index The index of the desired component holder.
525         * 
526         * @return True if the active component holder was set, false otherwise.
527         */
528        public boolean setActiveIndex(final int index) {
529            int size = getDisplayComponentCount();
530            if ((index < 0) || (index >= size)) {
531                return false;
532            }
533    
534    //        SwingUtilities.invokeLater(new Runnable() {
535    //            public void run() {
536                    tabbedPane.setSelectedIndex(index);
537                    if (window != null) {
538                        ComponentHolder h = (ComponentHolder)getDisplayComponents().get(index);
539                        if (h != null) {
540                            window.setTitle(makeWindowTitle(h.getName()));
541                        }
542                    }
543    //            }
544    //        });
545            return true;
546        }
547    
548        /**
549         * Returns the index of {@code holder} within this component group.
550         * 
551         * @return Either the index of {@code holder}, or {@code -1} 
552         * if {@link #getDisplayComponents()} returns a {@code null} {@link List}.
553         * 
554         * @see List#indexOf(Object)
555         */
556        @Override public int indexOf(final ComponentHolder holder) {
557            @SuppressWarnings("rawtypes")
558            List dispComps = getDisplayComponents();
559            if (dispComps == null) {
560                return -1;
561            } else {
562                return getDisplayComponents().indexOf(holder);
563            }
564        }
565    
566        /**
567         * Returns the {@link ComponentHolder} at the given position within this
568         * component group. 
569         * 
570         * @param index Index of the {@code ComponentHolder} to return.
571         * 
572         * @return {@code ComponentHolder} at {@code index}.
573         * 
574         * @see List#get(int)
575         */
576        protected ComponentHolder getHolderAt(final int index) {
577            @SuppressWarnings("unchecked")
578            List<ComponentHolder> dispComps = getDisplayComponents();
579            return dispComps.get(index);
580        }
581    
582        /**
583         * @return Component holder that corresponds to the selected tab.
584         */
585        public ComponentHolder getActiveComponentHolder() {
586            int idx = 0;
587    
588            if (getDisplayComponentCount() > 1) {
589    //            idx = tabbedPane.getSelectedIndex();
590                idx = getActiveIndex();
591            }
592    
593    //        return (ComponentHolder)getDisplayComponents().get(idx);
594            return getHolderAt(idx);
595        }
596    
597        /**
598         * Overridden so that McV can also update its copy of the IDV reference.
599         */
600        @Override public void setIdv(final IntegratedDataViewer newIdv) {
601            super.setIdv(newIdv);
602            idv = newIdv;
603        }
604    
605        /**
606         * Create a window title suitable for an application window.
607         * 
608         * @param title Window title
609         * 
610         * @return Application title plus the window title.
611         */
612        private String makeWindowTitle(final String title) {
613            String defaultApplicationName = "McIDAS-V";
614            if (idv != null) {
615                defaultApplicationName = idv.getStateManager().getTitle();
616            }
617            return UIManager.makeTitle(defaultApplicationName, title);
618        }
619    
620        /**
621         * Returns the number of display components {@literal "in"} this group.
622         * 
623         * @return Either the {@code size()} of the {@link List} returned by 
624         * {@link #getDisplayComponents()} or {@code -1} if 
625         * {@code getDisplayComponents()} returns a {@code null} {@code List}.
626         */
627        protected int getDisplayComponentCount() {
628            @SuppressWarnings("rawtypes")
629            List dispComps = getDisplayComponents();
630            if (dispComps == null) {
631                return -1;
632            } else {
633                return dispComps.size();
634            }
635        }
636        
637        /**
638         * Create the <tt>JPopupMenu</tt> that will be displayed for a tab.
639         * 
640         * @return Menu initialized with tab options
641         */
642        protected JPopupMenu doMakeTabMenu() {
643            ActionListener menuListener = new ActionListener() {
644                public void actionPerformed(ActionEvent evt) {
645                    final String cmd = evt.getActionCommand();
646                    if (CMD_DISPLAY_EJECT.equals(cmd)) {
647                        ejectDisplay(tabbedPane.getSelectedIndex());
648                    } else if (CMD_DISPLAY_RENAME.equals(cmd)) {
649                        renameDisplay(tabbedPane.getSelectedIndex());
650                    } else if (CMD_DISPLAY_DESTROY.equals(cmd)) {
651                        destroyDisplay(tabbedPane.getSelectedIndex());
652                    }
653                }
654            };
655    
656            final JPopupMenu popup = new JPopupMenu();
657            JMenuItem item;
658    
659            // URL img = getClass().getResource(ICO_UNDOCK);
660            // item = new JMenuItem("Undock", new ImageIcon(img));
661            // item.setActionCommand(CMD_DISPLAY_EJECT);
662            // item.addActionListener(menuListener);
663            // popup.add(item);
664    
665            URL img = getClass().getResource(ICO_RENAME);
666            item = new JMenuItem("Rename", new ImageIcon(img));
667            item.setActionCommand(CMD_DISPLAY_RENAME);
668            item.addActionListener(menuListener);
669            popup.add(item);
670    
671            // popup.addSeparator();
672    
673            img = getClass().getResource(ICO_CLOSE);
674            item = new JMenuItem("Close", new ImageIcon(img));
675            item.setActionCommand(CMD_DISPLAY_DESTROY);
676            item.addActionListener(menuListener);
677            popup.add(item);
678    
679            popup.setBorder(new BevelBorder(BevelBorder.RAISED));
680    
681            Msg.translateTree(popup);
682            return popup;
683        }
684    
685        /**
686         * Remove the component holder at index {@code idx}. This method does
687         * not destroy the component holder.
688         * 
689         * @param idx Index of the ejected component holder.
690         * 
691         * @return Component holder that was ejected.
692         */
693        private ComponentHolder ejectDisplay(final int idx) {
694            return null;
695        }
696    
697        /**
698         * Prompt the user to change the name of the component holder at index
699         * {@code idx}. Nothing happens if the user doesn't enter anything.
700         * 
701         * @param idx Index of the component holder.
702         */
703        protected void renameDisplay(final int idx) {
704            final String title =
705                JOptionPane.showInputDialog(
706                    IdvWindow.getActiveWindow().getFrame(), "Enter new name",
707                    makeWindowTitle("Rename Tab"), JOptionPane.PLAIN_MESSAGE);
708    
709            if (title == null) {
710                return;
711            }
712    
713    //        final List<ComponentHolder> comps = getDisplayComponents();
714    //        comps.get(idx).setName(title);
715            getHolderAt(idx).setName(title);
716            tabRenamed = true;
717            if (window != null) {
718                window.setTitle(makeWindowTitle(title));
719            }
720            redoLayout();
721        }
722    
723        /**
724         * Prompts the user to confirm removal of the component holder at index
725         * {@code idx}. Nothing happens if the user declines.
726         * 
727         * @param idx Index of the component holder.
728         * 
729         * @return Either {@code true} if the user elected to remove, 
730         * {@code false} otherwise.
731         */
732        protected boolean destroyDisplay(final int idx) {
733    //        final List<IdvComponentHolder> comps = getDisplayComponents();
734    //        IdvComponentHolder comp = comps.get(idx);
735            return ((IdvComponentHolder)getHolderAt(idx)).removeDisplayComponent();
736    //        return comp.removeDisplayComponent();
737        }
738    
739        /**
740         * Remove the component at {@code index} without forcing the IDV-land
741         * component group to redraw.
742         * 
743         * @param index The index of the component to be removed.
744         * 
745         * @return The removed component.
746         */
747        @SuppressWarnings("unchecked")
748        public ComponentHolder quietRemoveComponentAt(final int index) {
749            List<ComponentHolder> comps = getDisplayComponents();
750            if (comps == null || comps.size() == 0) {
751                return null;
752            }
753            ComponentHolder removed = comps.remove(index);
754            removed.setParent(null);
755            return removed;
756        }
757    
758        /**
759         * Adds a component to the end of the list of display components without
760         * forcing the IDV-land code to redraw.
761         * 
762         * @param component The component to add.
763         * 
764         * @return The index of the newly added component, or {@code -1} if 
765         * {@link #getDisplayComponents()} returned a null {@code List}.
766         */
767        @SuppressWarnings("unchecked")
768        public int quietAddComponent(final ComponentHolder component) {
769            List<ComponentHolder> comps = getDisplayComponents();
770            if (comps == null) {
771                return -1;
772            }
773            if (comps.contains(component)) {
774                comps.remove(component);
775            }
776            comps.add(component);
777            component.setParent(this);
778            return comps.indexOf(component);
779        }
780    
781        /**
782         * Handle pop-up events for tabs.
783         */
784        @SuppressWarnings("unused")
785        private class TabPopupListener extends MouseAdapter {
786    
787            @Override public void mouseClicked(final MouseEvent evt) {
788                checkPopup(evt);
789            }
790    
791            @Override public void mousePressed(final MouseEvent evt) {
792                checkPopup(evt);
793            }
794    
795            @Override public void mouseReleased(final MouseEvent evt) {
796                checkPopup(evt);
797            }
798    
799            /**
800             * <p>
801             * Determines whether or not the tab popup menu should be shown, and
802             * if so, which parts of it should be enabled or disabled.
803             * </p>
804             * 
805             * @param evt Allows us to determine the type of event.
806             */
807            private void checkPopup(final MouseEvent evt) {
808                if (evt.isPopupTrigger()) {
809                    // can't close or eject last tab
810                    // TODO: re-evaluate this
811                    Component[] comps = popup.getComponents();
812                    for (Component comp : comps) {
813                        if (comp instanceof JMenuItem) {
814                            String cmd = ((JMenuItem)comp).getActionCommand();
815                            if ((CMD_DISPLAY_DESTROY.equals(cmd) || CMD_DISPLAY_EJECT.equals(cmd))
816                                && tabbedPane.getTabCount() == 1) {
817                                comp.setEnabled(false);
818                            } else {
819                                comp.setEnabled(true);
820                            }
821                        }
822                    }
823                    popup.show(tabbedPane, evt.getX(), evt.getY());
824                }
825            }
826        }
827    }