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