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    // Removed showMacDisabledMessage() as it no longer appears to be a problem
213
214    /**
215     * Triggered when the user does a (platform-dependent) drag initiating 
216     * gesture. Used to populate the things that the user is attempting to 
217     * drag. 
218     */
219    @Override public void dragGestureRecognized(DragGestureEvent e) {
220        // currently we want to disable drag and drop for "chrome-less" windows
221        // one alternative is to have drag and drop simply *reposition*
222        // chrome-less windows.
223        // 3141 June 2024 -> it looks like the MacOS tab problem (McV Inquiry #3047) is no longer an issue?
224        if (showTabArea(group, this)) {
225            sourceIndex = getSelectedIndex();
226
227            // transferable allows us to store the current DraggableTabbedPane
228            // and the source index of the drag inside the various drag and
229            // drop event listeners.
230            Transferable transferable = new TransferableIndex(this, sourceIndex);
231
232            Cursor cursor = DragSource.DefaultMoveDrop;
233            if (e.getDragAction() != DnDConstants.ACTION_MOVE) {
234                cursor = DragSource.DefaultCopyDrop;
235            }
236            dragSource.startDrag(e, cursor, transferable, this);
237        }
238    }
239
240    /** 
241     * Triggered when the user drags into {@code dropTarget}.
242     */
243    @Override public void dragEnter(DropTargetDragEvent e) {
244        DataFlavor[] flave = e.getCurrentDataFlavors();
245        if ((flave.length == 0) || !(flave[0] instanceof DraggableTabFlavor)) {
246            return;
247        }
248
249//        logger.trace("entered window outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex);
250
251        // if the DraggableTabbedPane associated with this drag isn't the 
252        // "current" DraggableTabbedPane we're dealing with a drag from another
253        // window and we need to make this DraggableTabbedPane aware of that.
254        if (((DraggableTabFlavor)flave[0]).getDragTab() != this) {
255//            logger.trace("  coming from outside");
256            outsideDrag = true;
257        } else {
258//            logger.trace("  re-entered parent window");
259            outsideDrag = false;
260        }
261    }
262
263    /**
264     * Triggered when the user drags out of {@code dropTarget}.
265     */
266    @Override public void dragExit(DropTargetEvent e) {
267        if (showTabArea(group, this)) {
268//        logger.trace("drag left a window outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex);
269            overIndex = -1;
270            //outsideDrag = true;
271            repaint();
272        }
273    }
274
275    /**
276     * Triggered continually while the user is dragging over 
277     * {@code dropTarget}. McIDAS-V uses this to draw the index indicator.
278     * 
279     * @param e Information about the current state of the drag.
280     */
281    @Override public void dragOver(DropTargetDragEvent e) {
282//        logger.trace("dragOver outsideDrag={} sourceIndex={}", outsideDrag, sourceIndex);
283        if (showTabArea(group, this)) {
284            if (!outsideDrag && (sourceIndex == -1)) {
285                return;
286            }
287
288            // This will disallow dropping a tab back into a window, while
289            // allowing the user to drag tabs out of display windows.
290            // if (System.getProperty("os.name").contains("Mac OS X")) {
291            //     e.rejectDrag();
292            //     return;
293            // }
294
295            Point dropPoint = e.getLocation();
296            overIndex = indexAtLocation(dropPoint.x, dropPoint.y);
297            repaint();
298        }
299    }
300
301    /**
302     * Triggered when a drop has happened over {@code dropTarget}.
303     * 
304     * @param e State that we'll need in order to handle the drop.
305     */
306    @Override public void drop(DropTargetDropEvent e) {
307        if (!showTabArea(group, this)) {
308            return;
309        }
310        // if the dragged ComponentHolder was dragged from another window we
311        // must do a behind-the-scenes transfer from its old ComponentGroup to 
312        // the end of the new ComponentGroup.
313        if (outsideDrag) {
314            DataFlavor[] flave = e.getCurrentDataFlavors();
315            DraggableTabbedPane other =
316                ((DraggableTabFlavor)flave[0]).getDragTab();
317
318            ComponentHolder target = other.removeDragged();
319            sourceIndex = group.quietAddComponent(target);
320            outsideDrag = false;
321    
322            McvComponentHolder draggedHolder = (McvComponentHolder)target;
323            
324            List<ViewManager> vms = draggedHolder.getViewManagers();
325            for (ViewManager vm : vms) {
326                vm.setWindow(window);
327            }
328        }
329
330        // check to see if we've actually dropped something McV understands.
331        if (sourceIndex >= 0) {
332            e.acceptDrop(VALID_ACTION);
333            Point dropPoint = e.getLocation();
334            int dropIndex = indexAtLocation(dropPoint.x, dropPoint.y);
335
336            // make sure the user chose to drop over a valid area/thing first
337            // then do the actual drop.
338            if ((dropIndex != -1) && (getComponentAt(dropIndex) != null)) {
339                doDrop(sourceIndex, dropIndex);
340                // TJJ Apr 2023
341                // https://mcidas.ssec.wisc.edu/inquiry-v/?inquiry=3047
342                // Will resolve macOS tab reordering problem after 1.9 release
343                // 3141 June 2024 -> it looks like this is no longer an issue?
344            }
345
346            // clean up anything associated with the current drag and drop
347            e.getDropTargetContext().dropComplete(true);
348            sourceIndex = -1;
349            overIndex = -1;
350
351            repaint();
352        }
353    }
354
355    /**
356     * {@literal "Quietly"} removes the dragged component from its group. If
357     * the last component in a group has been dragged out of the group, the
358     * associated window will be killed.
359     * 
360     * @return The removed component.
361     */
362    private ComponentHolder removeDragged() {
363        ComponentHolder removed = group.quietRemoveComponentAt(sourceIndex);
364
365        // no point in keeping an empty window around... but killing the 
366        // window here doesn't properly terminate the drag and drop (as this
367        // method is typically called from *another* window).
368        return removed;
369    }
370
371    /**
372     * Moves a component to its new index within the component group.
373     * 
374     * @param srcIdx The old index of the component.
375     * @param dstIdx The new index of the component.
376     */
377    public void doDrop(int srcIdx, int dstIdx) {
378        List<ComponentHolder> comps = group.getDisplayComponents();
379        ComponentHolder src = comps.get(srcIdx);
380        group.removeComponent(src);
381        group.addComponent(src, dstIdx);
382    }
383
384    /**
385     * Overridden so that McIDAS-V can draw an indicator of a dragged tab's 
386     * possible new position.
387     */
388    @Override public void paint(Graphics g) {
389        super.paint(g);
390        if (overIndex >= 0) {
391            Rectangle bounds = getBoundsAt(overIndex);
392            if (bounds != null) {
393                g.drawImage(INDICATOR, bounds.x-7, bounds.y, null);
394            }
395        }
396    }
397
398    /**
399     * Overriden so that McIDAS-V can change the window title upon changing
400     * tabs.
401     */
402    @Override public void setSelectedIndex(int index) {
403        super.setSelectedIndex(index);
404
405        // there are only ever component holders in the display comps.
406        @SuppressWarnings("unchecked")
407        List<ComponentHolder> comps = group.getDisplayComponents();
408
409        ComponentHolder h = comps.get(index);
410        String newTitle = 
411            UIManager.makeTitle(idv.getStateManager().getTitle(), h.getName());
412        if (window != null) {
413            window.setTitle(newTitle);
414        }
415    }
416
417    /**
418     * Used to simply provide a reference to the originating 
419     * DraggableTabbedPane while we're dragging and dropping.
420     */
421    private static class TransferableIndex implements Transferable {
422        private DraggableTabbedPane tabbedPane;
423
424        private int index;
425
426        public TransferableIndex(DraggableTabbedPane dt, int i) {
427            tabbedPane = dt;
428            index = i;
429        }
430
431        // whatever is returned here needs to be serializable. so we can't just
432        // return the tabbedPane. :(
433        @Override public Object getTransferData(DataFlavor flavor) {
434            return index;
435        }
436
437        @Override public DataFlavor[] getTransferDataFlavors() {
438            return new DataFlavor[] { new DraggableTabFlavor(tabbedPane) };
439        }
440
441        @Override public boolean isDataFlavorSupported(DataFlavor flavor) {
442            return true;
443        }
444    }
445
446    /**
447     * To be perfectly honest I'm still a bit fuzzy about DataFlavors. As far 
448     * as I can tell they're used like so: if a user dragged an image file on
449     * to a toolbar, the toolbar might be smart enough to add the image. If the
450     * user dragged the same image file into a text document, the text editor
451     * might be smart enough to insert the path to the image or something.
452     * 
453     * I'm thinking that would require two data flavors: some sort of toolbar
454     * flavor and then some sort of text flavor?
455     */
456    private static class DraggableTabFlavor extends DataFlavor {
457        private DraggableTabbedPane tabbedPane;
458
459        public DraggableTabFlavor(DraggableTabbedPane dt) {
460            super(DraggableTabbedPane.class, "DraggableTabbedPane");
461            tabbedPane = dt;
462        }
463
464        public DraggableTabbedPane getDragTab() {
465            return tabbedPane;
466        }
467    }
468
469    /**
470     * Handle the user dropping a tab outside of a McV window. This will create
471     * a new window and add the dragged tab to the ComponentGroup within the
472     * newly created window. The new window is the same size as the origin 
473     * window, with the top centered over the location where the user released
474     * the mouse.
475     * 
476     * @param dragged The ComponentHolder that's being dragged around.
477     * @param drop The x- and y-coordinates where the user dropped the tab.
478     */
479    private void newWindowDrag(ComponentHolder dragged, Point drop) {
480        if (dragged == null) {
481            return;
482        }
483
484        UIManager ui = (UIManager)idv.getIdvUIManager();
485
486        try {
487            Element skinRoot =
488                XmlUtil.getRoot(Constants.BLANK_COMP_GROUP, getClass());
489
490            // create the new window with visibility off, so we can position 
491            // the window in a sensible way before the user has to see it.
492            IdvWindow w = ui.createNewWindow(null, false, "McIDAS-V",
493                Constants.BLANK_COMP_GROUP, skinRoot, false, null);
494
495            // be sure to add the dragged component holder to the new window.
496            ComponentGroup newGroup = w.getComponentGroups().get(0);
497
498            newGroup.addComponent(dragged);
499            
500            McvComponentHolder draggedHolder = (McvComponentHolder)dragged;
501            List<ViewManager> vms = draggedHolder.getViewManagers();
502            for (ViewManager vm : vms) {
503                vm.setWindow(w);
504            }
505
506            // make the new window the same size as the old and center the
507            // *top* of the window over the drop point.
508            int height = window.getBounds().height;
509            int width = window.getBounds().width;
510            int startX = drop.x - (width / 2);
511
512            // let there be a window
513            SwingUtilities.invokeLater(() -> {
514                w.setBounds(new Rectangle(startX, drop.y, width, height));
515                w.pack();
516                w.setVisible(true);
517            });
518
519//            GuiUtils.toFront(w.getWindow());
520//            logger.trace("active window: {} new window: {}", Integer
521//                .toHexString
522//                (IdvWindow.getActiveWindow().hashCode()), Integer
523//                .toHexString(w.hashCode()));
524        } catch (Throwable e) {
525            logger.error("Error creating new window from dragged tab", e);
526        }
527    }
528
529    /**
530     * Handles what happens at the very end of a drag and drop. Since I could
531     * not find a better method for it, tabs that are dropped outside of a McV
532     * window are handled with this method.
533     */
534    public void dragDropEnd(DragSourceDropEvent e) {
535        if (!e.getDropSuccess() && (e.getDropAction() == 0)) {
536            newWindowDrag(removeDragged(), e.getLocation());
537        }
538
539        // this should probably be the last thing to happen in this method.
540        // checks to see if we've got a blank window after a drag and drop; 
541        // if so, dispose!
542        List<ComponentHolder> comps = group.getDisplayComponents();
543        if ((comps == null) || comps.isEmpty()) {
544            window.dispose();
545        }
546    }
547
548    // required methods that we don't need to implement yet.
549    @Override public void dragEnter(DragSourceDragEvent e) { }
550    @Override public void dragExit(DragSourceEvent e) { }
551    @Override public void dragOver(DragSourceDragEvent e) { }
552    @Override public void dropActionChanged(DragSourceDragEvent e) { }
553    @Override public void dropActionChanged(DropTargetDragEvent e) { }
554
555    @Override public void mouseClicked(final MouseEvent e) {
556        if (showTabArea(group, this)) {
557            processMouseEvents(e);
558        }
559    }
560
561    @Override public void mouseExited(final MouseEvent e) {
562        if (showTabArea(group, this)) {
563            processMouseEvents(e);
564        }
565    }
566
567    @Override public void mousePressed(final MouseEvent e) {
568        if (showTabArea(group, this)) {
569            processMouseEvents(e);
570        } else {
571            draggedAtX = e.getX();
572            draggedAtY = e.getY();
573        }
574    }
575
576    @Override public void mouseEntered(final MouseEvent e) {
577        if (showTabArea(group, this)) {
578            processMouseEvents(e);
579        }
580    }
581
582    @Override public void mouseMoved(final MouseEvent e) {
583        if (showTabArea(group, this)) {
584            processMouseEvents(e);
585        }
586    }
587
588    @Override public void mouseDragged(final MouseEvent e) {
589        // note: this method is called continously throughout the dragging
590        // process
591        if (showTabArea(group, this)) {
592            processMouseEvents(e);
593        } else {
594            window.setLocation(e.getX() - draggedAtX + window.getLocation().x,
595                               e.getY() - draggedAtY + window.getLocation().y);
596        }
597    }
598
599    @Override public void mouseReleased(final MouseEvent e) {
600        if (showTabArea(group, this)) {
601            processMouseEvents(e);
602        }
603    }
604
605    private void processMouseEvents(final MouseEvent e) {
606        int eventX = e.getX();
607        int eventY = e.getY();
608
609        int tabIndex = getUI().tabForCoordinate(this, eventX, eventY);
610        if (tabIndex < 0) {
611            return;
612        }
613
614        if (!showTabArea(group, this)) {
615            return;
616        }
617
618        TabButton icon = (TabButton)getIconAt(tabIndex);
619        if (icon == null) {
620            return;
621        }
622
623        int id = e.getID();
624        Rectangle iconBounds = icon.getBounds();
625        if (!iconBounds.contains(eventX, eventY) || (id == MouseEvent.MOUSE_EXITED)) {
626            ButtonState state = icon.getState();
627            if ((state == ButtonState.ROLLOVER) || (state == ButtonState.PRESSED)) {
628                icon.setState(ButtonState.DEFAULT);
629            }
630
631            if ((e.getClickCount() >= 2) && !e.isPopupTrigger() && (id == MouseEvent.MOUSE_CLICKED)) {
632                group.renameDisplay(tabIndex);
633            }
634
635            repaint(iconBounds);
636            return;
637        }
638
639        if ((id == MouseEvent.MOUSE_PRESSED) && ((e.getModifiersEx() & InputEvent.BUTTON1_DOWN_MASK) != 0)) {
640            icon.setState(ButtonState.PRESSED);
641        } else if (id == MouseEvent.MOUSE_CLICKED) {
642            icon.setState(ButtonState.DEFAULT);
643            group.destroyDisplay(tabIndex);
644        } else {
645            icon.setState(ButtonState.ROLLOVER);
646        }
647        repaint(iconBounds);
648    }
649
650    @Override public void addTab(String title, Component component) {
651        addTab(title, component, null);
652    }
653
654    public void addTab(String title, Component component, Icon extraIcon) {
655        int tabCount = getTabCount();
656        int displayNumber = 0;
657        if (tabCount < 9) {
658            displayNumber = tabCount + 1;
659        } else if (tabCount == 9) {
660            displayNumber = 0;
661        }
662        title = "<html><font color=\"" + currentTabColor+"\">"+displayNumber+"</font>"+title+"</html>";
663        if (showTabArea(group, this)) {
664            if (getUI() instanceof FlatTabbedPaneUI) {
665                super.addTab(title, component);
666            } else {
667                super.addTab(title, new TabButton(), component);
668            }
669        } else {
670            super.addTab("", component);
671        }
672    }
673
674    public static boolean showTabArea(McvComponentGroup mcvCompGroup,
675                                      JTabbedPane tabbedPane)
676    {
677        return !mcvCompGroup.getHideTabArea() || (tabbedPane.getTabCount() > 1);
678    }
679
680    class CloseableTabbedPaneUI extends BasicTabbedPaneUI {
681        private final Insets borderInsets = new Insets(0, 0, 0, 0);
682
683        private int horizontalTextPosition = SwingConstants.LEFT;
684
685        public CloseableTabbedPaneUI() { }
686
687        public CloseableTabbedPaneUI(int horizontalTextPosition) {
688            this.horizontalTextPosition = horizontalTextPosition;
689        }
690
691        @Override protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
692            if (showTabArea(group, tabPane)) {
693                super.paintContentBorder(g, tabPlacement, selectedIndex);
694            }
695        }
696
697        @Override protected Insets getContentBorderInsets(int tabPlacement) {
698            Insets insets = null;
699            if (showTabArea(group, tabPane)) {
700                insets = super.getContentBorderInsets(tabPlacement);
701            } else {
702                insets = borderInsets;
703            }
704            return insets;
705        }
706
707        @Override protected void layoutLabel(int tabPlacement, 
708            FontMetrics metrics, int tabIndex, String title, Icon icon, 
709            Rectangle tabRect, Rectangle iconRect, Rectangle textRect, 
710            boolean isSelected) 
711        {
712            if (tabPane.getTabCount() == 0) {
713                return;
714            }
715
716            if (!showTabArea(group, tabPane)) {
717                return;
718            }
719
720            textRect.x = textRect.y = iconRect.x = iconRect.y = 0;
721            View v = getTextViewForTab(tabIndex);
722            if (v != null) {
723                tabPane.putClientProperty("html", v);
724            }
725
726            SwingUtilities.layoutCompoundLabel(tabPane,
727                metrics,
728                title,
729                icon,
730                SwingConstants.CENTER,
731                SwingConstants.CENTER,
732                SwingConstants.CENTER,
733                horizontalTextPosition,
734                tabRect,
735                iconRect,
736                textRect,
737                textIconGap + 2);
738
739            int xNudge = getTabLabelShiftX(tabPlacement, tabIndex, isSelected);
740            int yNudge = getTabLabelShiftY(tabPlacement, tabIndex, isSelected);
741            iconRect.x += xNudge;
742            iconRect.y += yNudge;
743            textRect.x += xNudge;
744            textRect.y += yNudge;
745        }
746
747        @Override protected int calculateTabAreaHeight(int placement, int count, int height) {
748            return showTabArea(group, tabPane)
749                   ? super.calculateTabAreaHeight(placement, count, height)
750                   : 0;
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,
764            int placement, int idx, int x, int y, int w, int h,
765            boolean isSelected)
766        {
767            if (showTabArea(group, tabPane)) {
768                if (isSelected) {
769                    g.setColor(selected);
770                } else {
771                    g.setColor(unselected);
772                }
773                g.fillRect(x, y, w, h);
774                g.setColor(selected);
775                g.drawLine(x, y, x, y + h);
776            }
777        }
778    }
779
780    class CloseableMetalTabbedPaneUI extends MetalTabbedPaneUI {
781        private final Insets borderInsets = new Insets(0, 0, 0, 0);
782
783        private int horizontalTextPosition = SwingUtilities.LEFT;
784
785        public CloseableMetalTabbedPaneUI() { }
786
787        public CloseableMetalTabbedPaneUI(int newHorizontalTextPosition) {
788            this.horizontalTextPosition = newHorizontalTextPosition;
789        }
790
791        @Override protected void paintContentBorder(Graphics g, int tabPlacement, int selectedIndex) {
792            if (showTabArea(group, tabPane)) {
793                super.paintContentBorder(g, tabPlacement, selectedIndex);
794            }
795        }
796
797        @Override protected Insets getContentBorderInsets(int tabPlacement) {
798            Insets insets = null;
799            if (showTabArea(group, tabPane)) {
800                insets = super.getContentBorderInsets(tabPlacement);
801            } else {
802                insets = borderInsets;
803            }
804            return insets;
805        }
806
807        @Override protected void paintTabBorder(Graphics g, int placement,
808                                                int idx,
809                                                int x, int y, int w, int h,
810                                                boolean isSel)
811        {
812            if (showTabArea(group, tabPane)) {
813                super.paintTabBorder(g, placement, idx, x, y, w, h, isSel);
814            }
815        }
816
817        @Override protected void paintTabBackground(Graphics g, int placement,
818                                                    int idx,
819                                                    int x, int y, int w, int h,
820                                                    boolean isSel)
821        {
822            if (showTabArea(group, tabPane)) {
823                super.paintTabBackground(g, placement, idx, x, y, w, h, isSel);
824            }
825        }
826
827        @Override protected int calculateTabAreaHeight(int placement, int count, int height) {
828            return showTabArea(group, tabPane)
829                   ? super.calculateTabAreaHeight(placement, count, height)
830                   : 0;
831        }
832
833        @Override protected void layoutLabel(int placement,
834            FontMetrics metrics, int tabIndex, String title, Icon icon, 
835            Rectangle tabRect, Rectangle iconRect, Rectangle textRect, 
836            boolean isSelected) 
837        {
838            if (tabPane.getTabCount() != 0) {
839                textRect.x = 0;
840                textRect.y = 0;
841                iconRect.x = 0;
842                iconRect.y = 0;
843
844                View v = getTextViewForTab(tabIndex);
845                if (v != null) {
846                    tabPane.putClientProperty("html", v);
847                }
848
849                SwingUtilities.layoutCompoundLabel(tabPane,
850                    metrics,
851                    title,
852                    icon,
853                    SwingConstants.CENTER,
854                    SwingConstants.CENTER,
855                    SwingConstants.CENTER,
856                    horizontalTextPosition,
857                    tabRect,
858                    iconRect,
859                    textRect,
860                    textIconGap + 2);
861
862                int xNudge =
863                    getTabLabelShiftX(placement, tabIndex, isSelected);
864                int yNudge =
865                    getTabLabelShiftY(placement, tabIndex, isSelected);
866                iconRect.x += xNudge;
867                iconRect.y += yNudge;
868                textRect.x += xNudge;
869                textRect.y += yNudge;
870            }
871        }
872    }
873
874    public static class TabButton implements Icon {
875
876        private static final EnumMap<ButtonState, String> iconPaths =
877            new EnumMap<>(ButtonState.class);
878
879        private ButtonState currentState = ButtonState.DEFAULT;
880        private int iconWidth = 0;
881        private int iconHeight = 0;
882
883        private int posX = 0;
884        private int posY = 0;
885
886        public TabButton() {
887            setStateIcon(ButtonState.DEFAULT, ICON_DEFAULT);
888            setStateIcon(ButtonState.PRESSED, ICON_PRESSED);
889            setStateIcon(ButtonState.ROLLOVER, ICON_ROLLOVER);
890            setState(ButtonState.DEFAULT);
891        }
892
893        public static Icon getStateIcon(final ButtonState state) {
894            String path = iconPaths.get(state);
895            if (path == null) {
896                path = iconPaths.get(ButtonState.DEFAULT);
897            }
898            return GuiUtils.getImageIcon(path);
899        }
900
901        public static void setStateIcon(final ButtonState state,
902            final String path)
903        {
904            iconPaths.put(state, path);
905        }
906
907        public static String getStateIconPath(final ButtonState state) {
908            String path = iconPaths.get(ButtonState.DEFAULT);
909            if (iconPaths.containsKey(state)) {
910                path = iconPaths.get(state);
911            }
912            return path;
913        }
914
915        public void setState(final ButtonState state) {
916            currentState = state;
917            Icon currentIcon = getStateIcon(state);
918            if (currentIcon != null) {
919                iconWidth = currentIcon.getIconWidth();
920                iconHeight = currentIcon.getIconHeight();
921            }
922        }
923
924        public ButtonState getState() {
925            return currentState;
926        }
927
928        public Icon getIcon() {
929            return getStateIcon(currentState);
930        }
931
932        @Override public void paintIcon(Component c, Graphics g, int x, int y) {
933            Icon current = getIcon();
934            if (current != null) {
935                posX = x;
936                posY = y;
937                current.paintIcon(c, g, x, y);
938            }
939        }
940
941        @Override public int getIconWidth() {
942            return iconWidth;
943        }
944
945        @Override public int getIconHeight() {
946            return iconHeight;
947        }
948
949        public Rectangle getBounds() {
950            return new Rectangle(posX, posY, iconWidth, iconHeight);
951        }
952    }
953}