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