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