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