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