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