001    /*
002     * $Id: JXMultiSplitPane.java,v 1.1 2006/03/23 21:52:43 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    
024    import java.awt.Color;
025    import java.awt.Cursor;
026    import java.awt.Graphics;
027    import java.awt.Graphics2D;
028    import java.awt.Rectangle;
029    import java.awt.event.KeyEvent;
030    import java.awt.event.KeyListener;
031    import java.awt.event.MouseEvent;
032    import javax.accessibility.AccessibleContext;
033    import javax.accessibility.AccessibleRole;
034    import javax.swing.JPanel;
035    import javax.swing.event.MouseInputAdapter;
036    import org.jdesktop.swingx.MultiSplitLayout.Divider;
037    import org.jdesktop.swingx.MultiSplitLayout.Node;
038    
039    /**
040     *
041     * <p>
042     * All properties in this class are bound: when a properties value
043     * is changed, all PropertyChangeListeners are fired.
044     * 
045     * @author Hans Muller
046     */
047    public class JXMultiSplitPane extends JPanel {
048        private AccessibleContext accessibleContext = null;
049        private boolean continuousLayout = true;
050        private DividerPainter dividerPainter = new DefaultDividerPainter();
051    
052        /**
053         * Creates a JXMultiSplitPane with it's LayoutManager set to 
054         * to an empty MultiSplitLayout.
055         */
056        public JXMultiSplitPane() {
057        super(new MultiSplitLayout());
058        InputHandler inputHandler = new InputHandler();
059        addMouseListener(inputHandler);
060        addMouseMotionListener(inputHandler);
061        addKeyListener(inputHandler);
062        setFocusable(true);
063        }
064    
065        /**
066         * 
067         * A convenience method that returns the layout manager cast 
068         * to MutliSplitLayout.
069         * 
070         * 
071         * @return this JXMultiSplitPane'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         * 
170         * The DividerPainter that's used to paint Dividers on this JXMultiSplitPane.
171         * This property may be null.
172         * 
173         * 
174         * @return the value of the dividerPainter Property
175         * @see #setDividerPainter
176         */
177        public DividerPainter getDividerPainter() {
178        return dividerPainter;
179        }
180    
181        /**
182         * 
183         * Sets the DividerPainter that's used to paint Dividers on this 
184         * JXMultiSplitPane.  The default DividerPainter only draws
185         * the activeDivider (if there is one) and then, only if 
186         * continuousLayout is false.  The value of this property is 
187         * used by the paintChildren method: Dividers are painted after
188         * the JXMultiSplitPane's children have been rendered so that 
189         * the activeDivider can appear "on top of" the children.
190         * 
191         * 
192         * @param dividerPainter the value of the dividerPainter property, can be null
193         * @see #paintChildren
194         * @see #activeDivider
195         */
196        public void setDividerPainter(DividerPainter dividerPainter) {
197        this.dividerPainter = dividerPainter;
198        }
199    
200        /**
201         * Uses the DividerPainter (if any) to paint each Divider that
202         * overlaps the clip Rectangle.  This is done after the call to
203         * <code>super.paintChildren()</code> so that Dividers can be 
204         * rendered "on top of" the children.
205         * <p>
206         * {@inheritDoc}
207         */
208        protected void paintChildren(Graphics g) {
209        super.paintChildren(g);
210        DividerPainter dp = getDividerPainter();
211        Rectangle clipR = g.getClipBounds();
212        if ((dp != null) && (clipR != null)) {
213                Graphics dpg = g.create();
214                try {
215            MultiSplitLayout msl = getMultiSplitLayout();
216            for(Divider divider : msl.dividersThatOverlap(clipR)) {
217                dp.paint(dpg, divider);
218            }
219                }
220                finally {
221            dpg.dispose();
222                }
223        }
224        }
225    
226        private boolean dragUnderway = false;
227        private MultiSplitLayout.Divider dragDivider = null;
228        private Rectangle initialDividerBounds = null;
229        private boolean oldFloatingDividers = true;
230        private int dragOffsetX = 0;
231        private int dragOffsetY = 0;
232        private int dragMin = -1;
233        private int dragMax = -1;
234        
235        private void startDrag(int mx, int my) {
236        requestFocusInWindow();
237        MultiSplitLayout msl = getMultiSplitLayout();
238        MultiSplitLayout.Divider divider = msl.dividerAt(mx, my);
239        if (divider != null) {
240            MultiSplitLayout.Node prevNode = divider.previousSibling();
241            MultiSplitLayout.Node nextNode = divider.nextSibling();
242            if ((prevNode == null) || (nextNode == null)) {
243            dragUnderway = false;
244            }
245            else {
246            initialDividerBounds = divider.getBounds();
247            dragOffsetX = mx - initialDividerBounds.x;
248            dragOffsetY = my - initialDividerBounds.y;
249            dragDivider  = divider;
250            Rectangle prevNodeBounds = prevNode.getBounds();
251            Rectangle nextNodeBounds = nextNode.getBounds();
252            if (dragDivider.isVertical()) {
253                dragMin = prevNodeBounds.x;
254                dragMax = nextNodeBounds.x + nextNodeBounds.width;
255                dragMax -= dragDivider.getBounds().width;
256            }
257            else {
258                dragMin = prevNodeBounds.y;
259                dragMax = nextNodeBounds.y + nextNodeBounds.height;
260                dragMax -= dragDivider.getBounds().height;
261            }
262            oldFloatingDividers = getMultiSplitLayout().getFloatingDividers();
263            getMultiSplitLayout().setFloatingDividers(false);
264            dragUnderway = true;
265            }
266        }
267        else {
268            dragUnderway = false;
269        }
270        }
271    
272        private void repaintDragLimits() {
273        Rectangle damageR = dragDivider.getBounds();
274        if (dragDivider.isVertical()) {
275            damageR.x = dragMin;
276            damageR.width = dragMax - dragMin;
277        }
278        else {
279            damageR.y = dragMin;
280            damageR.height = dragMax - dragMin;
281        }
282        repaint(damageR);
283        }
284    
285        private void updateDrag(int mx, int my) {
286        if (!dragUnderway) {
287            return;
288        }
289        Rectangle oldBounds = dragDivider.getBounds();
290        Rectangle bounds = new Rectangle(oldBounds);
291        if (dragDivider.isVertical()) {
292            bounds.x = mx - dragOffsetX;
293            bounds.x = Math.max(bounds.x, dragMin);
294            bounds.x = Math.min(bounds.x, dragMax);
295        }
296        else {
297            bounds.y = my - dragOffsetY;
298            bounds.y = Math.max(bounds.y, dragMin);
299            bounds.y = Math.min(bounds.y, dragMax);
300        }
301        dragDivider.setBounds(bounds);
302        if (isContinuousLayout()) {
303            revalidate();
304            repaintDragLimits();
305        }
306        else {
307            repaint(oldBounds.union(bounds));
308        }
309        }
310    
311        private void clearDragState() {
312        dragDivider = null;
313        initialDividerBounds = null;
314        oldFloatingDividers = true;
315        dragOffsetX = dragOffsetY = 0;
316        dragMin = dragMax = -1;
317        dragUnderway = false;
318        }
319    
320        private void finishDrag(int x, int y) {
321        if (dragUnderway) {
322            clearDragState();
323            if (!isContinuousLayout()) {
324            revalidate();
325            repaint();
326            }
327        }
328        }
329        
330        private void cancelDrag() {       
331        if (dragUnderway) {
332            dragDivider.setBounds(initialDividerBounds);
333            getMultiSplitLayout().setFloatingDividers(oldFloatingDividers);
334            setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
335            repaint();
336            revalidate();
337            clearDragState();
338        }
339        }
340    
341        private void updateCursor(int x, int y, boolean show) {
342        if (dragUnderway) {
343            return;
344        }
345        int cursorID = Cursor.DEFAULT_CURSOR;
346        if (show) {
347            MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y);
348            if (divider != null) {
349            cursorID  = (divider.isVertical()) ? 
350                Cursor.E_RESIZE_CURSOR : 
351                Cursor.N_RESIZE_CURSOR;
352            }
353        }
354        setCursor(Cursor.getPredefinedCursor(cursorID));
355        }
356    
357    
358        private class InputHandler extends MouseInputAdapter implements KeyListener {
359    
360        public void mouseEntered(MouseEvent e) {
361            updateCursor(e.getX(), e.getY(), true);
362        }
363    
364        public void mouseMoved(MouseEvent e) {
365            updateCursor(e.getX(), e.getY(), true);
366        }
367    
368        public void mouseExited(MouseEvent e) {
369            updateCursor(e.getX(), e.getY(), false);
370        }
371    
372        public void mousePressed(MouseEvent e) {
373            startDrag(e.getX(), e.getY());
374        }
375        public void mouseReleased(MouseEvent e) {
376            finishDrag(e.getX(), e.getY());
377        }
378        public void mouseDragged(MouseEvent e) {
379            updateDrag(e.getX(), e.getY());     
380        }
381            public void keyPressed(KeyEvent e) { 
382            if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
383            cancelDrag();
384            }
385        }
386            public void keyReleased(KeyEvent e) { }
387        public void keyTyped(KeyEvent e) { }
388        }
389    
390        public AccessibleContext getAccessibleContext() {
391            if( accessibleContext == null ) {
392                accessibleContext = new AccessibleMultiSplitPane();
393            }
394            return accessibleContext;
395        }
396        
397        protected class AccessibleMultiSplitPane extends AccessibleJPanel {
398            public AccessibleRole getAccessibleRole() {
399                return AccessibleRole.SPLIT_PANE;
400            }
401        }
402    }