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