001    /*
002     * $Id: MultiSplitPane.java,v 1.15 2005/10/26 14:29:54 hansmuller Exp $
003     *
004     * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005     * Santa Clara, California 95054, U.S.A. All rights reserved.
006     *
007     * This library is free software; you can redistribute it and/or
008     * modify it under the terms of the GNU Lesser General Public
009     * License as published by the Free Software Foundation; either
010     * version 2.1 of the License, or (at your option) any later version.
011     *
012     * This library is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this library; if not, write to the Free Software
019     * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
020     */
021    
022    //package org.jdesktop.swingx;
023    package org.openstreetmap.josm.gui;
024    
025    import java.awt.Color;
026    import java.awt.Cursor;
027    import java.awt.Graphics;
028    import java.awt.Graphics2D;
029    import java.awt.Rectangle;
030    import java.awt.event.KeyEvent;
031    import java.awt.event.KeyListener;
032    import java.awt.event.MouseEvent;
033    import javax.accessibility.AccessibleContext;
034    import javax.accessibility.AccessibleRole;
035    import javax.swing.JPanel;
036    import javax.swing.event.MouseInputAdapter;
037    
038    import org.openstreetmap.josm.gui.MultiSplitLayout.Divider;
039    import org.openstreetmap.josm.gui.MultiSplitLayout.Node;
040    
041    /**
042     *
043     * <p>
044     * All properties in this class are bound: when a properties value
045     * is changed, all PropertyChangeListeners are fired.
046     *
047     * @author Hans Muller
048     */
049    public class MultiSplitPane extends JPanel {
050        private AccessibleContext accessibleContext = null;
051        private boolean continuousLayout = true;
052        private DividerPainter dividerPainter = new DefaultDividerPainter();
053    
054        /**
055         * Creates a MultiSplitPane with it's LayoutManager set to
056         * to an empty MultiSplitLayout.
057         */
058        public MultiSplitPane() {
059            super(new MultiSplitLayout());
060            InputHandler inputHandler = new InputHandler();
061            addMouseListener(inputHandler);
062            addMouseMotionListener(inputHandler);
063            addKeyListener(inputHandler);
064            setFocusable(true);
065        }
066    
067        /**
068         * A convenience method that returns the layout manager cast
069         * to MutliSplitLayout.
070         *
071         * @return this MultiSplitPane's layout manager
072         * @see java.awt.Container#getLayout
073         * @see #setModel
074         */
075        public final MultiSplitLayout getMultiSplitLayout() {
076            return (MultiSplitLayout)getLayout();
077        }
078    
079        /**
080         * A convenience method that sets the MultiSplitLayout model.
081         * Equivalent to <code>getMultiSplitLayout.setModel(model)</code>
082         *
083         * @param model the root of the MultiSplitLayout model
084         * @see #getMultiSplitLayout
085         * @see MultiSplitLayout#setModel
086         */
087        public final void setModel(Node model) {
088            getMultiSplitLayout().setModel(model);
089        }
090    
091        /**
092         * A convenience method that sets the MultiSplitLayout dividerSize
093         * property. Equivalent to
094         * <code>getMultiSplitLayout().setDividerSize(newDividerSize)</code>.
095         *
096         * @param dividerSize the value of the dividerSize property
097         * @see #getMultiSplitLayout
098         * @see MultiSplitLayout#setDividerSize
099         */
100        public final void setDividerSize(int dividerSize) {
101            getMultiSplitLayout().setDividerSize(dividerSize);
102        }
103    
104        /**
105         * Sets the value of the <code>continuousLayout</code> property.
106         * If true, then the layout is revalidated continuously while
107         * a divider is being moved.  The default value of this property
108         * is true.
109         *
110         * @param continuousLayout value of the continuousLayout property
111         * @see #isContinuousLayout
112         */
113        public void setContinuousLayout(boolean continuousLayout) {
114            boolean oldContinuousLayout = continuousLayout;
115            this.continuousLayout = continuousLayout;
116            firePropertyChange("continuousLayout", oldContinuousLayout, continuousLayout);
117        }
118    
119        /**
120         * Returns true if dragging a divider only updates
121         * the layout when the drag gesture ends (typically, when the
122         * mouse button is released).
123         *
124         * @return the value of the <code>continuousLayout</code> property
125         * @see #setContinuousLayout
126         */
127        public boolean isContinuousLayout() {
128            return continuousLayout;
129        }
130    
131        /**
132         * Returns the Divider that's currently being moved, typically
133         * because the user is dragging it, or null.
134         *
135         * @return the Divider that's being moved or null.
136         */
137        public Divider activeDivider() {
138            return dragDivider;
139        }
140    
141        /**
142         * Draws a single Divider.  Typically used to specialize the
143         * way the active Divider is painted.
144         *
145         * @see #getDividerPainter
146         * @see #setDividerPainter
147         */
148        public static abstract class DividerPainter {
149            /**
150             * Paint a single Divider.
151             *
152             * @param g the Graphics object to paint with
153             * @param divider the Divider to paint
154             */
155            public abstract void paint(Graphics g, Divider divider);
156        }
157    
158        private class DefaultDividerPainter extends DividerPainter {
159            public void paint(Graphics g, Divider divider) {
160                if ((divider == activeDivider()) && !isContinuousLayout()) {
161                    Graphics2D g2d = (Graphics2D)g;
162                    g2d.setColor(Color.black);
163                    g2d.fill(divider.getBounds());
164                }
165            }
166        }
167    
168        /**
169         * The DividerPainter that's used to paint Dividers on this MultiSplitPane.
170         * This property may be null.
171         *
172         * @return the value of the dividerPainter Property
173         * @see #setDividerPainter
174         */
175        public DividerPainter getDividerPainter() {
176            return dividerPainter;
177        }
178    
179        /**
180         * Sets the DividerPainter that's used to paint Dividers on this
181         * MultiSplitPane.  The default DividerPainter only draws
182         * the activeDivider (if there is one) and then, only if
183         * continuousLayout is false.  The value of this property is
184         * used by the paintChildren method: Dividers are painted after
185         * the MultiSplitPane's children have been rendered so that
186         * the activeDivider can appear "on top of" the children.
187         *
188         * @param dividerPainter the value of the dividerPainter property, can be null
189         * @see #paintChildren
190         * @see #activeDivider
191         */
192        public void setDividerPainter(DividerPainter dividerPainter) {
193            this.dividerPainter = dividerPainter;
194        }
195    
196        /**
197         * Uses the DividerPainter (if any) to paint each Divider that
198         * overlaps the clip Rectangle.  This is done after the call to
199         * <code>super.paintChildren()</code> so that Dividers can be
200         * rendered "on top of" the children.
201         * <p>
202         * {@inheritDoc}
203         */
204        protected void paintChildren(Graphics g) {
205            super.paintChildren(g);
206            DividerPainter dp = getDividerPainter();
207            Rectangle clipR = g.getClipBounds();
208            if ((dp != null) && (clipR != null)) {
209                Graphics dpg = g.create();
210                try {
211                    MultiSplitLayout msl = getMultiSplitLayout();
212                    for(Divider divider : msl.dividersThatOverlap(clipR)) {
213                        dp.paint(dpg, divider);
214                    }
215                }
216                finally {
217                    dpg.dispose();
218                }
219            }
220        }
221    
222        private boolean dragUnderway = false;
223        private MultiSplitLayout.Divider dragDivider = null;
224        private Rectangle initialDividerBounds = null;
225        private boolean oldFloatingDividers = true;
226        private int dragOffsetX = 0;
227        private int dragOffsetY = 0;
228        private int dragMin = -1;
229        private int dragMax = -1;
230    
231        private void startDrag(int mx, int my) {
232            requestFocusInWindow();
233            MultiSplitLayout msl = getMultiSplitLayout();
234            MultiSplitLayout.Divider divider = msl.dividerAt(mx, my);
235            if (divider != null) {
236                MultiSplitLayout.Node prevNode = divider.previousSibling();
237                MultiSplitLayout.Node nextNode = divider.nextSibling();
238                if ((prevNode == null) || (nextNode == null)) {
239                    dragUnderway = false;
240                }
241                else {
242                    initialDividerBounds = divider.getBounds();
243                    dragOffsetX = mx - initialDividerBounds.x;
244                    dragOffsetY = my - initialDividerBounds.y;
245                    dragDivider  = divider;
246                    Rectangle prevNodeBounds = prevNode.getBounds();
247                    Rectangle nextNodeBounds = nextNode.getBounds();
248                    if (dragDivider.isVertical()) {
249                        dragMin = prevNodeBounds.x;
250                        dragMax = nextNodeBounds.x + nextNodeBounds.width;
251                        dragMax -= dragDivider.getBounds().width;
252                    }
253                    else {
254                        dragMin = prevNodeBounds.y;
255                        dragMax = nextNodeBounds.y + nextNodeBounds.height;
256                        dragMax -= dragDivider.getBounds().height;
257                    }
258                    oldFloatingDividers = getMultiSplitLayout().getFloatingDividers();
259                    getMultiSplitLayout().setFloatingDividers(false);
260                    dragUnderway = true;
261                }
262            }
263            else {
264                dragUnderway = false;
265            }
266        }
267    
268        private void repaintDragLimits() {
269            Rectangle damageR = dragDivider.getBounds();
270            if (dragDivider.isVertical()) {
271                damageR.x = dragMin;
272                damageR.width = dragMax - dragMin;
273            }
274            else {
275                damageR.y = dragMin;
276                damageR.height = dragMax - dragMin;
277            }
278            repaint(damageR);
279        }
280    
281        private void updateDrag(int mx, int my) {
282            if (!dragUnderway) {
283                return;
284            }
285            Rectangle oldBounds = dragDivider.getBounds();
286            Rectangle bounds = new Rectangle(oldBounds);
287            if (dragDivider.isVertical()) {
288                bounds.x = mx - dragOffsetX;
289                bounds.x = Math.max(bounds.x, dragMin);
290                bounds.x = Math.min(bounds.x, dragMax);
291            }
292            else {
293                bounds.y = my - dragOffsetY;
294                bounds.y = Math.max(bounds.y, dragMin);
295                bounds.y = Math.min(bounds.y, dragMax);
296            }
297            dragDivider.setBounds(bounds);
298            if (isContinuousLayout()) {
299                revalidate();
300                repaintDragLimits();
301            }
302            else {
303                repaint(oldBounds.union(bounds));
304            }
305        }
306    
307        private void clearDragState() {
308            dragDivider = null;
309            initialDividerBounds = null;
310            oldFloatingDividers = true;
311            dragOffsetX = dragOffsetY = 0;
312            dragMin = dragMax = -1;
313            dragUnderway = false;
314        }
315    
316        private void finishDrag(int x, int y) {
317            if (dragUnderway) {
318                clearDragState();
319                if (!isContinuousLayout()) {
320                    revalidate();
321                    repaint();
322                }
323            }
324        }
325    
326        private void cancelDrag() {
327            if (dragUnderway) {
328                dragDivider.setBounds(initialDividerBounds);
329                getMultiSplitLayout().setFloatingDividers(oldFloatingDividers);
330                setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
331                repaint();
332                revalidate();
333                clearDragState();
334            }
335        }
336    
337        private void updateCursor(int x, int y, boolean show) {
338            if (dragUnderway) {
339                return;
340            }
341            int cursorID = Cursor.DEFAULT_CURSOR;
342            if (show) {
343                MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y);
344                if (divider != null) {
345                    cursorID  = (divider.isVertical()) ?
346                        Cursor.E_RESIZE_CURSOR :
347                        Cursor.N_RESIZE_CURSOR;
348                }
349            }
350            setCursor(Cursor.getPredefinedCursor(cursorID));
351        }
352    
353        private class InputHandler extends MouseInputAdapter implements KeyListener {
354    
355            public void mouseEntered(MouseEvent e) {
356                updateCursor(e.getX(), e.getY(), true);
357            }
358    
359            public void mouseMoved(MouseEvent e) {
360                updateCursor(e.getX(), e.getY(), true);
361            }
362    
363            public void mouseExited(MouseEvent e) {
364                updateCursor(e.getX(), e.getY(), false);
365            }
366    
367            public void mousePressed(MouseEvent e) {
368                startDrag(e.getX(), e.getY());
369            }
370            public void mouseReleased(MouseEvent e) {
371                finishDrag(e.getX(), e.getY());
372            }
373            public void mouseDragged(MouseEvent e) {
374                updateDrag(e.getX(), e.getY());
375            }
376            public void keyPressed(KeyEvent e) {
377                if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
378                    cancelDrag();
379                }
380            }
381            public void keyReleased(KeyEvent e) { }
382            public void keyTyped(KeyEvent e) { }
383        }
384    
385        public AccessibleContext getAccessibleContext() {
386            if( accessibleContext == null ) {
387                accessibleContext = new AccessibleMultiSplitPane();
388            }
389            return accessibleContext;
390        }
391    
392        protected class AccessibleMultiSplitPane extends AccessibleJPanel {
393            public AccessibleRole getAccessibleRole() {
394                return AccessibleRole.SPLIT_PANE;
395            }
396        }
397    }