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.Color;
032import java.awt.Component;
033import java.awt.Cursor;
034import java.awt.FontMetrics;
035import java.awt.Graphics;
036import java.awt.Image;
037import java.awt.Insets;
038import java.awt.Point;
039import java.awt.Rectangle;
040import java.awt.datatransfer.DataFlavor;
041import java.awt.datatransfer.Transferable;
042import java.awt.dnd.DnDConstants;
043import java.awt.dnd.DragGestureEvent;
044import java.awt.dnd.DragGestureListener;
045import java.awt.dnd.DragSource;
046import java.awt.dnd.DragSourceDragEvent;
047import java.awt.dnd.DragSourceDropEvent;
048import java.awt.dnd.DragSourceEvent;
049import java.awt.dnd.DragSourceListener;
050import java.awt.dnd.DropTarget;
051import java.awt.dnd.DropTargetDragEvent;
052import java.awt.dnd.DropTargetDropEvent;
053import java.awt.dnd.DropTargetEvent;
054import java.awt.dnd.DropTargetListener;
055import java.awt.event.InputEvent;
056import java.awt.event.MouseEvent;
057import java.awt.event.MouseListener;
058import java.awt.event.MouseMotionListener;
059
060import javax.swing.Icon;
061import javax.swing.ImageIcon;
062import javax.swing.JOptionPane;
063import javax.swing.JTabbedPane;
064import javax.swing.SwingConstants;
065import javax.swing.SwingUtilities;
066import javax.swing.plaf.basic.BasicTabbedPaneUI;
067import javax.swing.plaf.metal.MetalTabbedPaneUI;
068import javax.swing.text.View;
069
070import java.util.EnumMap;
071import java.util.List;
072
073import com.formdev.flatlaf.ui.FlatTabbedPaneUI;
074import org.w3c.dom.Element;
075
076import org.slf4j.Logger;
077import org.slf4j.LoggerFactory;
078
079import ucar.unidata.idv.IntegratedDataViewer;
080import ucar.unidata.idv.ViewManager;
081import ucar.unidata.idv.ui.IdvWindow;
082import ucar.unidata.ui.ComponentGroup;
083import ucar.unidata.ui.ComponentHolder;
084import ucar.unidata.util.GuiUtils;
085import ucar.unidata.xml.XmlUtil;
086
087import edu.wisc.ssec.mcidasv.Constants;
088
089/**
090 * This is a rather simplistic drag and drop enabled JTabbedPane. It allows
091 * users to use drag and drop to move tabs between windows and reorder tabs.
092 */
093public class DraggableTabbedPane extends JTabbedPane implements 
094    DragGestureListener, DragSourceListener, DropTargetListener, MouseListener,
095    MouseMotionListener
096{
097    private static final long serialVersionUID = -5710302260509445686L;
098
099    private static final Logger logger =
100        LoggerFactory.getLogger(DraggableTabbedPane.class);
101
102    /** Local shorthand for the actions we're accepting. */
103    private static final int VALID_ACTION = DnDConstants.ACTION_COPY_OR_MOVE;
104
105    /** Path to the icon we'll use as an index indicator. */
106    private static final String IDX_ICON = 
107        "/edu/wisc/ssec/mcidasv/resources/icons/tabmenu/go-down.png";
108
109    private static Color unselected = new Color(165, 165, 165);
110    private static Color selected = new Color(225, 225, 225);
111
112    private static final String INDEX_COLOR_METAL = "#AAAAAA";
113
114    private static final String INDEX_COLOR_UGLY_TABS = "#708090";
115
116    /** The actual image that we'll use to display the index indications. */
117    private final Image INDICATOR =
118        new ImageIcon(getClass().getResource(IDX_ICON)).getImage();
119
120    public enum ButtonState { DEFAULT, PRESSED, DISABLED, ROLLOVER };
121
122    /** Path to icon that represents the default button state. */
123    private static final String ICON_DEFAULT =
124        "/edu/wisc/ssec/mcidasv/resources/icons/closetab/metal_close_enabled.png";
125
126    /** Path to icon that represents the pressed button state. */
127    private static final String ICON_PRESSED =
128        "/edu/wisc/ssec/mcidasv/resources/icons/closetab/metal_close_pressed.png";
129
130    /** Path to icon that represents the rollover button state. */
131    private static final String ICON_ROLLOVER =
132        "/edu/wisc/ssec/mcidasv/resources/icons/closetab/metal_close_rollover.png";
133
134    /** 
135     * Used to signal across all DraggableTabbedPanes that the component 
136     * currently being dragged originated in another window. This'll let McV
137     * determine if it has to do a quiet ComponentHolder transfer.
138     */
139    protected static boolean outsideDrag = false;
140
141    /** The tab index where the drag started. */
142    private int sourceIndex = -1;
143
144    /** The tab index that the user is currently over. */
145    private int overIndex = -1;
146
147    private int draggedAtX;
148
149    private int draggedAtY;
150
151    /** Used for starting the dragging process. */
152    private DragSource dragSource;
153
154    /** Used for signaling that we'll accept drops (registers listeners). */
155    private DropTarget dropTarget;
156
157    /** The component group holding our components. */
158    private McvComponentGroup group;
159
160    /** The IDV window that contains this tabbed pane. */
161    private IdvWindow window;
162
163    /** Keep around this reference so that we can access the UI Manager. */
164    private IntegratedDataViewer idv;
165
166    /** RGB string for the color of the current tab. */
167    private String currentTabColor = INDEX_COLOR_METAL;
168
169    /**
170     * Mostly just registers that this component should listen for drag and
171     * drop operations.
172     * 
173     * @param win The IDV window containing this tabbed pane.
174     * @param idv The main IDV instance.
175     * @param group The {@link McvComponentGroup} that holds this component's tabs.
176     */
177    public DraggableTabbedPane(IdvWindow win, IntegratedDataViewer idv,
178        McvComponentGroup group)
179    {
180        dropTarget = new DropTarget(this, this);
181        dragSource = new DragSource();
182        dragSource.createDefaultDragGestureRecognizer(this, VALID_ACTION, this);
183
184        this.group = group;
185        this.idv = idv;
186        window = win;
187
188        addMouseListener(this);
189        addMouseMotionListener(this);
190
191        System.out.println("getUI returned: "+getUI().getClass().getCanonicalName());
192
193        if (getUI() instanceof MetalTabbedPaneUI) {
194            setUI(new CloseableMetalTabbedPaneUI(SwingConstants.LEFT));
195            currentTabColor = INDEX_COLOR_METAL;
196        } else if (!(getUI() instanceof FlatTabbedPaneUI)) {
197            setUI(new CloseableTabbedPaneUI(SwingConstants.LEFT));
198            currentTabColor = INDEX_COLOR_UGLY_TABS;
199        } else {
200            setUI(new FlatTabbedPaneUI());
201
202            try {
203                selected = javax.swing.UIManager.getColor("TabbedPane.focusColor");
204                unselected = javax.swing.UIManager.getColor("TabbedPane.contentAreaColor");
205                currentTabColor = "#" + Integer.toHexString(javax.swing.UIManager.getColor("TabbedPane.contentAreaColor").getRGB()).substring(2);
206            } catch (NullPointerException npe) {
207                logger.warn("Couldn't change currentTabColor, defaulting to Metal L&F");
208            }
209        }
210    }
211
212    /**
213     * Show a message explaining why drag and drop is temporarily disabled on macOS.
214     */
215    private void showMacDisabledMessage() {
216        JOptionPane.showMessageDialog(
217                null,
218                "Tab drag-and-drop has been disabled on macOS until the next release, apologies.",
219                "Tab Reorder on macOS",
220                JOptionPane.INFORMATION_MESSAGE
221        );
222    }
223
224    /**
225     * Triggered when the user does a (platform-dependent) drag initiating 
226     * gesture. Used to populate the things that the user is attempting to 
227     * drag. 
228     */
229    @Override public void dragGestureRecognized(DragGestureEvent e) {
230        if (System.getProperty("os.name").contains("Mac OS X")) {
231            // TJJ Apr 2023
232            // https://mcidas.ssec.wisc.edu/inquiry-v/?inquiry=3047
233            // Will resolve macOS tab reordering problem after 1.9 release
234            //
235            // sorry to be fraudulently creating TJJs, Tommy!
236            // just didn't want to lose track of these so that
237            // we can remove 'em after we've fixed the bug.
238            // - jon
239            showMacDisabledMessage();
240            return;
241        }
242
243        // currently we want to disable drag and drop for "chrome-less" windows
244        // one alternative is to have drag and drop simply *reposition*
245        // chrome-less windows.
246        if (showTabArea(group, this)) {
247            sourceIndex = getSelectedIndex();
248
249            // transferable allows us to store the current DraggableTabbedPane
250            // and the source index of the drag inside the various drag and
251            // drop event listeners.
252            Transferable transferable = new TransferableIndex(this, sourceIndex);
253
254            Cursor cursor = DragSource.DefaultMoveDrop;
255            if (e.getDragAction() != DnDConstants.ACTION_MOVE) {
256                cursor = DragSource.DefaultCopyDrop;
257            }
258            dragSource.startDrag(e, cursor, transferable, this);
259        }
260    }
261
262    /** 
263     * Triggered when the user drags into {@code dropTarget}.
264     */
265    @Override public void dragEnter(DropTargetDragEvent e) {
266        DataFlavor[] flave = e.getCurrentDataFlavors();
267        if ((flave.length == 0) || !(flave[0] instanceof DraggableTabFlavor)) {
268            return;
269        }
270
271//        logger.trace("entered window outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex);
272
273        // if the DraggableTabbedPane associated with this drag isn't the 
274        // "current" DraggableTabbedPane we're dealing with a drag from another
275        // window and we need to make this DraggableTabbedPane aware of that.
276        if (((DraggableTabFlavor)flave[0]).getDragTab() != this) {
277//            logger.trace("  coming from outside");
278            outsideDrag = true;
279        } else {
280//            logger.trace("  re-entered parent window");
281            outsideDrag = false;
282        }
283    }
284
285    /**
286     * Triggered when the user drags out of {@code dropTarget}.
287     */
288    @Override public void dragExit(DropTargetEvent e) {
289        if (showTabArea(group, this)) {
290//        logger.trace("drag left a window outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex);
291            overIndex = -1;
292            //outsideDrag = true;
293            repaint();
294        }
295    }
296
297    /**
298     * Triggered continually while the user is dragging over 
299     * {@code dropTarget}. McIDAS-V uses this to draw the index indicator.
300     * 
301     * @param e Information about the current state of the drag.
302     */
303    @Override public void dragOver(DropTargetDragEvent e) {
304//        logger.trace("dragOver outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex);
305        if (showTabArea(group, this)) {
306            if (!outsideDrag && (sourceIndex == -1)) {
307                return;
308            }
309
310            // This will disallow dropping a tab back into a window, while
311            // allowing the user to drag tabs out of display windows.
312            // if (System.getProperty("os.name").contains("Mac OS X")) {
313            //     e.rejectDrag();
314            //     return;
315            // }
316
317            Point dropPoint = e.getLocation();
318            overIndex = indexAtLocation(dropPoint.x, dropPoint.y);
319            repaint();
320        }
321    }
322
323    /**
324     * Triggered when a drop has happened over {@code dropTarget}.
325     * 
326     * @param e State that we'll need in order to handle the drop.
327     */
328    @Override public void drop(DropTargetDropEvent e) {
329        if (!showTabArea(group, this)) {
330            return;
331        }
332        // if the dragged ComponentHolder was dragged from another window we
333        // must do a behind-the-scenes transfer from its old ComponentGroup to 
334        // the end of the new ComponentGroup.
335        if (outsideDrag) {
336            DataFlavor[] flave = e.getCurrentDataFlavors();
337            DraggableTabbedPane other =
338                ((DraggableTabFlavor)flave[0]).getDragTab();
339
340            ComponentHolder target = other.removeDragged();
341            sourceIndex = group.quietAddComponent(target);
342            outsideDrag = false;
343    
344            McvComponentHolder draggedHolder = (McvComponentHolder)target;
345            
346            List<ViewManager> vms = draggedHolder.getViewManagers();
347            for (ViewManager vm : vms) {
348                vm.setWindow(window);
349            }
350        }
351
352        // check to see if we've actually dropped something McV understands.
353        if (sourceIndex >= 0) {
354            e.acceptDrop(VALID_ACTION);
355            Point dropPoint = e.getLocation();
356            int dropIndex = indexAtLocation(dropPoint.x, dropPoint.y);
357
358            // make sure the user chose to drop over a valid area/thing first
359            // then do the actual drop.
360            if ((dropIndex != -1) && (getComponentAt(dropIndex) != null)) {
361                // TJJ Apr 2023
362                // https://mcidas.ssec.wisc.edu/inquiry-v/?inquiry=3047
363                // Will resolve macOS tab reordering problem after 1.9 release
364                if (System.getProperty("os.name").contains("Mac OS X")) {
365                    showMacDisabledMessage();
366                } else {
367                    doDrop(sourceIndex, dropIndex);
368                }
369            }
370
371            // clean up anything associated with the current drag and drop
372            e.getDropTargetContext().dropComplete(true);
373            sourceIndex = -1;
374            overIndex = -1;
375
376            repaint();
377        }
378    }
379
380    /**
381     * {@literal "Quietly"} removes the dragged component from its group. If
382     * the last component in a group has been dragged out of the group, the
383     * associated window will be killed.
384     * 
385     * @return The removed component.
386     */
387    private ComponentHolder removeDragged() {
388        ComponentHolder removed = group.quietRemoveComponentAt(sourceIndex);
389
390        // no point in keeping an empty window around... but killing the 
391        // window here doesn't properly terminate the drag and drop (as this
392        // method is typically called from *another* window).
393        return removed;
394    }
395
396    /**
397     * Moves a component to its new index within the component group.
398     * 
399     * @param srcIdx The old index of the component.
400     * @param dstIdx The new index of the component.
401     */
402    public void doDrop(int srcIdx, int dstIdx) {
403        List<ComponentHolder> comps = group.getDisplayComponents();
404        ComponentHolder src = comps.get(srcIdx);
405        group.removeComponent(src);
406        group.addComponent(src, dstIdx);
407    }
408
409    /**
410     * Overridden so that McIDAS-V can draw an indicator of a dragged tab's 
411     * possible new position.
412     */
413    @Override public void paint(Graphics g) {
414        super.paint(g);
415        if (overIndex >= 0) {
416            Rectangle bounds = getBoundsAt(overIndex);
417            if (bounds != null) {
418                g.drawImage(INDICATOR, bounds.x-7, bounds.y, null);
419            }
420        }
421    }
422
423    /**
424     * Overriden so that McIDAS-V can change the window title upon changing
425     * tabs.
426     */
427    @Override public void setSelectedIndex(int index) {
428        super.setSelectedIndex(index);
429
430        // there are only ever component holders in the display comps.
431        @SuppressWarnings("unchecked")
432        List<ComponentHolder> comps = group.getDisplayComponents();
433
434        ComponentHolder h = comps.get(index);
435        String newTitle = 
436            UIManager.makeTitle(idv.getStateManager().getTitle(), h.getName());
437        if (window != null) {
438            window.setTitle(newTitle);
439        }
440    }
441
442    /**
443     * Used to simply provide a reference to the originating 
444     * DraggableTabbedPane while we're dragging and dropping.
445     */
446    private static class TransferableIndex implements Transferable {
447        private DraggableTabbedPane tabbedPane;
448
449        private int index;
450
451        public TransferableIndex(DraggableTabbedPane dt, int i) {
452            tabbedPane = dt;
453            index = i;
454        }
455
456        // whatever is returned here needs to be serializable. so we can't just
457        // return the tabbedPane. :(
458        @Override public Object getTransferData(DataFlavor flavor) {
459            return index;
460        }
461
462        @Override public DataFlavor[] getTransferDataFlavors() {
463            return new DataFlavor[] { new DraggableTabFlavor(tabbedPane) };
464        }
465
466        @Override public boolean isDataFlavorSupported(DataFlavor flavor) {
467            return true;
468        }
469    }
470
471    /**
472     * To be perfectly honest I'm still a bit fuzzy about DataFlavors. As far 
473     * as I can tell they're used like so: if a user dragged an image file on
474     * to a toolbar, the toolbar might be smart enough to add the image. If the
475     * user dragged the same image file into a text document, the text editor
476     * might be smart enough to insert the path to the image or something.
477     * 
478     * I'm thinking that would require two data flavors: some sort of toolbar
479     * flavor and then some sort of text flavor?
480     */
481    private static class DraggableTabFlavor extends DataFlavor {
482        private DraggableTabbedPane tabbedPane;
483
484        public DraggableTabFlavor(DraggableTabbedPane dt) {
485            super(DraggableTabbedPane.class, "DraggableTabbedPane");
486            tabbedPane = dt;
487        }
488
489        public DraggableTabbedPane getDragTab() {
490            return tabbedPane;
491        }
492    }
493
494    /**
495     * Handle the user dropping a tab outside of a McV window. This will create
496     * a new window and add the dragged tab to the ComponentGroup within the
497     * newly created window. The new window is the same size as the origin 
498     * window, with the top centered over the location where the user released
499     * the mouse.
500     * 
501     * @param dragged The ComponentHolder that's being dragged around.
502     * @param drop The x- and y-coordinates where the user dropped the tab.
503     */
504    private void newWindowDrag(ComponentHolder dragged, Point drop) {
505        if (dragged == null) {
506            return;
507        }
508
509        UIManager ui = (UIManager)idv.getIdvUIManager();
510
511        try {
512            Element skinRoot =
513                XmlUtil.getRoot(Constants.BLANK_COMP_GROUP, getClass());
514
515            // create the new window with visibility off, so we can position 
516            // the window in a sensible way before the user has to see it.
517            IdvWindow w = ui.createNewWindow(null, false, "McIDAS-V",
518                Constants.BLANK_COMP_GROUP, skinRoot, false, null);
519
520            // be sure to add the dragged component holder to the new window.
521            ComponentGroup newGroup = w.getComponentGroups().get(0);
522
523            newGroup.addComponent(dragged);
524            
525            McvComponentHolder draggedHolder = (McvComponentHolder)dragged;
526            List<ViewManager> vms = draggedHolder.getViewManagers();
527            for (ViewManager vm : vms) {
528                vm.setWindow(w);
529            }
530
531            // make the new window the same size as the old and center the
532            // *top* of the window over the drop point.
533            int height = window.getBounds().height;
534            int width = window.getBounds().width;
535            int startX = drop.x - (width / 2);
536
537            // let there be a window
538            SwingUtilities.invokeLater(() -> {
539                w.setBounds(new Rectangle(startX, drop.y, width, height));
540                w.pack();
541                w.setVisible(true);
542            });
543
544//            GuiUtils.toFront(w.getWindow());
545//            logger.trace("active window: {} new window: {}", Integer
546//                .toHexString
547//                (IdvWindow.getActiveWindow().hashCode()), Integer
548//                .toHexString(w.hashCode()));
549        } catch (Throwable e) {
550            logger.error("Error creating new window from dragged tab", e);
551        }
552    }
553
554    /**
555     * Handles what happens at the very end of a drag and drop. Since I could
556     * not find a better method for it, tabs that are dropped outside of a McV
557     * window are handled with this method.
558     */
559    public void dragDropEnd(DragSourceDropEvent e) {
560        if (!e.getDropSuccess() && (e.getDropAction() == 0)) {
561            newWindowDrag(removeDragged(), e.getLocation());
562        }
563
564        // this should probably be the last thing to happen in this method.
565        // checks to see if we've got a blank window after a drag and drop; 
566        // if so, dispose!
567        List<ComponentHolder> comps = group.getDisplayComponents();
568        if ((comps == null) || comps.isEmpty()) {
569            window.dispose();
570        }
571    }
572
573    // required methods that we don't need to implement yet.
574    @Override public void dragEnter(DragSourceDragEvent e) { }
575    @Override public void dragExit(DragSourceEvent e) { }
576    @Override public void dragOver(DragSourceDragEvent e) { }
577    @Override public void dropActionChanged(DragSourceDragEvent e) { }
578    @Override public void dropActionChanged(DropTargetDragEvent e) { }
579
580    @Override public void mouseClicked(final MouseEvent e) {
581        if (showTabArea(group, this)) {
582            processMouseEvents(e);
583        }
584    }
585
586    @Override public void mouseExited(final MouseEvent e) {
587        if (showTabArea(group, this)) {
588            processMouseEvents(e);
589        }
590    }
591
592    @Override public void mousePressed(final MouseEvent e) {
593        if (showTabArea(group, this)) {
594            processMouseEvents(e);
595        } else {
596            draggedAtX = e.getX();
597            draggedAtY = e.getY();
598        }
599    }
600
601    @Override public void mouseEntered(final MouseEvent e) {
602        if (showTabArea(group, this)) {
603            processMouseEvents(e);
604        }
605    }
606
607    @Override public void mouseMoved(final MouseEvent e) {
608        if (showTabArea(group, this)) {
609            processMouseEvents(e);
610        }
611    }
612
613    @Override public void mouseDragged(final MouseEvent e) {
614        // note: this method is called continously throughout the dragging
615        // process
616        if (showTabArea(group, this)) {
617            processMouseEvents(e);
618        } else {
619            window.setLocation(e.getX() - draggedAtX + window.getLocation().x,
620                               e.getY() - draggedAtY + window.getLocation().y);
621        }
622    }
623
624    @Override public void mouseReleased(final MouseEvent e) {
625        if (showTabArea(group, this)) {
626            processMouseEvents(e);
627        }
628    }
629
630    private void processMouseEvents(final MouseEvent e) {
631        int eventX = e.getX();
632        int eventY = e.getY();
633
634        int tabIndex = getUI().tabForCoordinate(this, eventX, eventY);
635        if (tabIndex < 0) {
636            return;
637        }
638
639        if (!showTabArea(group, this)) {
640            return;
641        }
642
643        TabButton icon = (TabButton)getIconAt(tabIndex);
644        if (icon == null) {
645            return;
646        }
647
648        int id = e.getID();
649        Rectangle iconBounds = icon.getBounds();
650        if (!iconBounds.contains(eventX, eventY) || (id == MouseEvent.MOUSE_EXITED)) {
651            ButtonState state = icon.getState();
652            if ((state == ButtonState.ROLLOVER) || (state == ButtonState.PRESSED)) {
653                icon.setState(ButtonState.DEFAULT);
654            }
655
656            if ((e.getClickCount() >= 2) && !e.isPopupTrigger() && (id == MouseEvent.MOUSE_CLICKED)) {
657                group.renameDisplay(tabIndex);
658            }
659
660            repaint(iconBounds);
661            return;
662        }
663
664        if ((id == MouseEvent.MOUSE_PRESSED) && ((e.getModifiersEx() & InputEvent.BUTTON1_DOWN_MASK) != 0)) {
665            icon.setState(ButtonState.PRESSED);
666        } else if (id == MouseEvent.MOUSE_CLICKED) {
667            icon.setState(ButtonState.DEFAULT);
668            group.destroyDisplay(tabIndex);
669        } else {
670            icon.setState(ButtonState.ROLLOVER);
671        }
672        repaint(iconBounds);
673    }
674
675    @Override public void addTab(String title, Component component) {
676        addTab(title, component, null);
677    }
678
679    public void addTab(String title, Component component, Icon extraIcon) {
680        int tabCount = getTabCount();
681        int displayNumber = 0;
682        if (tabCount < 9) {
683            displayNumber = tabCount + 1;
684        } else if (tabCount == 9) {
685            displayNumber = 0;
686        }
687        title = "<html><font color=\"" + currentTabColor+"\">"+displayNumber+"</font>"+title+"</html>";
688        if (showTabArea(group, this)) {
689            if (getUI() instanceof FlatTabbedPaneUI) {
690                super.addTab(title, component);
691            } else {
692                super.addTab(title, new TabButton(), component);
693            }
694        } else {
695            super.addTab("", component);
696        }
697    }
698
699    public static boolean showTabArea(McvComponentGroup mcvCompGroup,
700                                      JTabbedPane tabbedPane)
701    {
702        return !mcvCompGroup.getHideTabArea() || (tabbedPane.getTabCount() > 1);
703    }
704
705    class CloseableTabbedPaneUI extends BasicTabbedPaneUI {
706        private final Insets borderInsets = new Insets(0, 0, 0, 0);
707
708        private int horizontalTextPosition = SwingConstants.LEFT;
709
710        public CloseableTabbedPaneUI() { }
711
712        public CloseableTabbedPaneUI(int horizontalTextPosition) {
713            this.horizontalTextPosition = horizontalTextPosition;
714        }
715
716        @Override protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
717            if (showTabArea(group, tabPane)) {
718                super.paintContentBorder(g, tabPlacement, selectedIndex);
719            }
720        }
721
722        @Override protected Insets getContentBorderInsets(int tabPlacement) {
723            Insets insets = null;
724            if (showTabArea(group, tabPane)) {
725                insets = super.getContentBorderInsets(tabPlacement);
726            } else {
727                insets = borderInsets;
728            }
729            return insets;
730        }
731
732        @Override protected void layoutLabel(int tabPlacement, 
733            FontMetrics metrics, int tabIndex, String title, Icon icon, 
734            Rectangle tabRect, Rectangle iconRect, Rectangle textRect, 
735            boolean isSelected) 
736        {
737            if (tabPane.getTabCount() == 0) {
738                return;
739            }
740
741            if (!showTabArea(group, tabPane)) {
742                return;
743            }
744
745            textRect.x = textRect.y = iconRect.x = iconRect.y = 0;
746            View v = getTextViewForTab(tabIndex);
747            if (v != null) {
748                tabPane.putClientProperty("html", v);
749            }
750
751            SwingUtilities.layoutCompoundLabel(tabPane,
752                metrics,
753                title,
754                icon,
755                SwingConstants.CENTER,
756                SwingConstants.CENTER,
757                SwingConstants.CENTER,
758                horizontalTextPosition,
759                tabRect,
760                iconRect,
761                textRect,
762                textIconGap + 2);
763
764            int xNudge = getTabLabelShiftX(tabPlacement, tabIndex, isSelected);
765            int yNudge = getTabLabelShiftY(tabPlacement, tabIndex, isSelected);
766            iconRect.x += xNudge;
767            iconRect.y += yNudge;
768            textRect.x += xNudge;
769            textRect.y += yNudge;
770        }
771
772        @Override protected int calculateTabAreaHeight(int placement, int count, int height) {
773            return showTabArea(group, tabPane)
774                   ? super.calculateTabAreaHeight(placement, count, height)
775                   : 0;
776        }
777
778        @Override protected void paintTabBorder(Graphics g, int placement,
779                                                int idx,
780                                                int x, int y, int w, int h,
781                                                boolean isSel)
782        {
783            if (showTabArea(group, tabPane)) {
784                super.paintTabBorder(g, placement, idx, x, y, w, h, isSel);
785            }
786        }
787
788        @Override protected void paintTabBackground(Graphics g,
789            int placement, int idx, int x, int y, int w, int h,
790            boolean isSelected)
791        {
792            if (showTabArea(group, tabPane)) {
793                if (isSelected) {
794                    g.setColor(selected);
795                } else {
796                    g.setColor(unselected);
797                }
798                g.fillRect(x, y, w, h);
799                g.setColor(selected);
800                g.drawLine(x, y, x, y + h);
801            }
802        }
803    }
804
805    class CloseableMetalTabbedPaneUI extends MetalTabbedPaneUI {
806        private final Insets borderInsets = new Insets(0, 0, 0, 0);
807
808        private int horizontalTextPosition = SwingUtilities.LEFT;
809
810        public CloseableMetalTabbedPaneUI() { }
811
812        public CloseableMetalTabbedPaneUI(int newHorizontalTextPosition) {
813            this.horizontalTextPosition = newHorizontalTextPosition;
814        }
815
816        @Override protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
817            if (showTabArea(group, tabPane)) {
818                super.paintContentBorder(g, tabPlacement, selectedIndex);
819            }
820        }
821
822        @Override protected Insets getContentBorderInsets(int tabPlacement) {
823            Insets insets = null;
824            if (showTabArea(group, tabPane)) {
825                insets = super.getContentBorderInsets(tabPlacement);
826            } else {
827                insets = borderInsets;
828            }
829            return insets;
830        }
831
832        @Override protected void paintTabBorder(Graphics g, int placement,
833                                                int idx,
834                                                int x, int y, int w, int h,
835                                                boolean isSel)
836        {
837            if (showTabArea(group, tabPane)) {
838                super.paintTabBorder(g, placement, idx, x, y, w, h, isSel);
839            }
840        }
841
842        @Override protected void paintTabBackground(Graphics g, int placement,
843                                                    int idx,
844                                                    int x, int y, int w, int h,
845                                                    boolean isSel)
846        {
847            if (showTabArea(group, tabPane)) {
848                super.paintTabBackground(g, placement, idx, x, y, w, h, isSel);
849            }
850        }
851
852        @Override protected int calculateTabAreaHeight(int placement, int count, int height) {
853            return showTabArea(group, tabPane)
854                   ? super.calculateTabAreaHeight(placement, count, height)
855                   : 0;
856        }
857
858        @Override protected void layoutLabel(int placement,
859            FontMetrics metrics, int tabIndex, String title, Icon icon, 
860            Rectangle tabRect, Rectangle iconRect, Rectangle textRect, 
861            boolean isSelected) 
862        {
863            if (tabPane.getTabCount() != 0) {
864                textRect.x = 0;
865                textRect.y = 0;
866                iconRect.x = 0;
867                iconRect.y = 0;
868
869                View v = getTextViewForTab(tabIndex);
870                if (v != null) {
871                    tabPane.putClientProperty("html", v);
872                }
873
874                SwingUtilities.layoutCompoundLabel(tabPane,
875                    metrics,
876                    title,
877                    icon,
878                    SwingConstants.CENTER,
879                    SwingConstants.CENTER,
880                    SwingConstants.CENTER,
881                    horizontalTextPosition,
882                    tabRect,
883                    iconRect,
884                    textRect,
885                    textIconGap + 2);
886
887                int xNudge =
888                    getTabLabelShiftX(placement, tabIndex, isSelected);
889                int yNudge =
890                    getTabLabelShiftY(placement, tabIndex, isSelected);
891                iconRect.x += xNudge;
892                iconRect.y += yNudge;
893                textRect.x += xNudge;
894                textRect.y += yNudge;
895            }
896        }
897    }
898
899    public static class TabButton implements Icon {
900
901        private static final EnumMap<ButtonState, String> iconPaths =
902            new EnumMap<>(ButtonState.class);
903
904        private ButtonState currentState = ButtonState.DEFAULT;
905        private int iconWidth = 0;
906        private int iconHeight = 0;
907
908        private int posX = 0;
909        private int posY = 0;
910
911        public TabButton() {
912            setStateIcon(ButtonState.DEFAULT, ICON_DEFAULT);
913            setStateIcon(ButtonState.PRESSED, ICON_PRESSED);
914            setStateIcon(ButtonState.ROLLOVER, ICON_ROLLOVER);
915            setState(ButtonState.DEFAULT);
916        }
917
918        public static Icon getStateIcon(final ButtonState state) {
919            String path = iconPaths.get(state);
920            if (path == null) {
921                path = iconPaths.get(ButtonState.DEFAULT);
922            }
923            return GuiUtils.getImageIcon(path);
924        }
925
926        public static void setStateIcon(final ButtonState state,
927            final String path)
928        {
929            iconPaths.put(state, path);
930        }
931
932        public static String getStateIconPath(final ButtonState state) {
933            String path = iconPaths.get(ButtonState.DEFAULT);
934            if (iconPaths.containsKey(state)) {
935                path = iconPaths.get(state);
936            }
937            return path;
938        }
939
940        public void setState(final ButtonState state) {
941            currentState = state;
942            Icon currentIcon = getStateIcon(state);
943            if (currentIcon != null) {
944                iconWidth = currentIcon.getIconWidth();
945                iconHeight = currentIcon.getIconHeight();
946            }
947        }
948
949        public ButtonState getState() {
950            return currentState;
951        }
952
953        public Icon getIcon() {
954            return getStateIcon(currentState);
955        }
956
957        @Override public void paintIcon(Component c, Graphics g, int x, int y) {
958            Icon current = getIcon();
959            if (current != null) {
960                posX = x;
961                posY = y;
962                current.paintIcon(c, g, x, y);
963            }
964        }
965
966        @Override public int getIconWidth() {
967            return iconWidth;
968        }
969
970        @Override public int getIconHeight() {
971            return iconHeight;
972        }
973
974        public Rectangle getBounds() {
975            return new Rectangle(posX, posY, iconWidth, iconHeight);
976        }
977    }
978}