001    /*
002     * $Id: JXMultiSplitPane.java 3279 2009-03-01 12:01:11Z rah003 $
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.Dimension;
027    import java.awt.Graphics;
028    import java.awt.Graphics2D;
029    import java.awt.Insets;
030    import java.awt.Rectangle;
031    import java.awt.event.KeyEvent;
032    import java.awt.event.KeyListener;
033    import java.awt.event.MouseEvent;
034    import javax.accessibility.AccessibleContext;
035    import javax.accessibility.AccessibleRole;
036    import javax.swing.JPanel;
037    import javax.swing.event.MouseInputAdapter;
038    import org.jdesktop.swingx.MultiSplitLayout.Divider;
039    import org.jdesktop.swingx.MultiSplitLayout.Node;
040    import org.jdesktop.swingx.painter.AbstractPainter;
041    import org.jdesktop.swingx.painter.Painter;
042    
043    /**
044     *
045     * <p>
046     * All properties in this class are bound: when a properties value
047     * is changed, all PropertyChangeListeners are fired.
048     * 
049     * @author Hans Muller
050     * @author Luan O'Carroll
051     */
052    public class JXMultiSplitPane extends JPanel {
053        private AccessibleContext accessibleContext = null;
054        private boolean continuousLayout = true;
055        private DividerPainter dividerPainter = new DefaultDividerPainter();
056        private Painter backgroundPainter;
057    
058        /**
059         * Creates a MultiSplitPane with it's LayoutManager set to 
060         * to an empty MultiSplitLayout.
061         */
062        public JXMultiSplitPane() {
063            this(new MultiSplitLayout());
064        }
065    
066        /**
067         * Creates a MultiSplitPane.
068         * @param layout the new split pane's layout
069         */
070        public JXMultiSplitPane( MultiSplitLayout layout ) {
071            super(layout);
072            InputHandler inputHandler = new InputHandler();
073            addMouseListener(inputHandler);
074            addMouseMotionListener(inputHandler);
075            addKeyListener(inputHandler);
076            setFocusable(true);
077        }
078        
079        /** 
080         * A convenience method that returns the layout manager cast 
081         * to MutliSplitLayout.
082         * 
083         * @return this MultiSplitPane's layout manager
084         * @see java.awt.Container#getLayout
085         * @see #setModel
086         */
087        public final MultiSplitLayout getMultiSplitLayout() {
088            return (MultiSplitLayout)getLayout();
089        }
090    
091        /** 
092         * A convenience method that sets the MultiSplitLayout model.
093         * Equivalent to <code>getMultiSplitLayout.setModel(model)</code>
094         * 
095         * @param model the root of the MultiSplitLayout model
096         * @see #getMultiSplitLayout
097         * @see MultiSplitLayout#setModel
098         */
099        public final void setModel(Node model) {
100            getMultiSplitLayout().setModel(model);
101        }
102    
103        /** 
104         * A convenience method that sets the MultiSplitLayout dividerSize
105         * property. Equivalent to 
106         * <code>getMultiSplitLayout().setDividerSize(newDividerSize)</code>.
107         * 
108         * @param dividerSize the value of the dividerSize property
109         * @see #getMultiSplitLayout
110         * @see MultiSplitLayout#setDividerSize
111         */
112        public final void setDividerSize(int dividerSize) {
113            getMultiSplitLayout().setDividerSize(dividerSize);
114        }
115    
116        /** 
117         * A convenience method that returns the MultiSplitLayout dividerSize
118         * property. Equivalent to 
119         * <code>getMultiSplitLayout().getDividerSize()</code>.
120         * 
121         * @see #getMultiSplitLayout
122         * @see MultiSplitLayout#getDividerSize
123         */
124        public final int getDividerSize() {
125            return getMultiSplitLayout().getDividerSize();
126        }
127    
128        /**
129         * Sets the value of the <code>continuousLayout</code> property.
130         * If true, then the layout is revalidated continuously while
131         * a divider is being moved.  The default value of this property
132         * is true.
133         *
134         * @param continuousLayout value of the continuousLayout property
135         * @see #isContinuousLayout
136         */
137        public void setContinuousLayout(boolean continuousLayout) {
138            boolean oldContinuousLayout = isContinuousLayout();
139            this.continuousLayout = continuousLayout;
140            firePropertyChange("continuousLayout", oldContinuousLayout, isContinuousLayout());
141        }
142    
143        /**
144         * Returns true if dragging a divider only updates
145         * the layout when the drag gesture ends (typically, when the 
146         * mouse button is released).
147         *
148         * @return the value of the <code>continuousLayout</code> property
149         * @see #setContinuousLayout
150         */
151        public boolean isContinuousLayout() {
152            return continuousLayout;
153        }
154    
155        /** 
156         * Returns the Divider that's currently being moved, typically
157         * because the user is dragging it, or null.
158         * 
159         * @return the Divider that's being moved or null.
160         */
161        public Divider activeDivider() {
162            return dragDivider;
163        }
164    
165        /**
166         * Draws a single Divider.  Typically used to specialize the
167         * way the active Divider is painted.  
168         * 
169         * @see #getDividerPainter
170         * @see #setDividerPainter
171         */
172        public static abstract class DividerPainter extends AbstractPainter<Divider> {
173        }
174    
175        private class DefaultDividerPainter extends DividerPainter {
176            protected void doPaint(Graphics2D g, Divider divider, int width, int height) {
177                if ((divider == activeDivider()) && !isContinuousLayout()) {
178                g.setColor(Color.black);
179                g.fillRect(0, 0, width, height);
180                }
181            }
182        }
183    
184        /** 
185         * The DividerPainter that's used to paint Dividers on this MultiSplitPane.
186         * This property may be null.
187         * 
188         * @return the value of the dividerPainter Property
189         * @see #setDividerPainter
190         */
191        public DividerPainter getDividerPainter() {
192        return dividerPainter;
193        }
194    
195        /** 
196         * Sets the DividerPainter that's used to paint Dividers on this 
197         * MultiSplitPane.  The default DividerPainter only draws
198         * the activeDivider (if there is one) and then, only if 
199         * continuousLayout is false.  The value of this property is 
200         * used by the paintChildren method: Dividers are painted after
201         * the MultiSplitPane's children have been rendered so that 
202         * the activeDivider can appear "on top of" the children.
203         * 
204         * @param dividerPainter the value of the dividerPainter property, can be null
205         * @see #paintChildren
206         * @see #activeDivider
207         */
208        public void setDividerPainter(DividerPainter dividerPainter) {
209            DividerPainter old = getDividerPainter();
210            this.dividerPainter = dividerPainter;
211            firePropertyChange("dividerPainter", old, getDividerPainter());
212        }
213    
214        /**
215         * Calls the UI delegate's paint method, if the UI delegate
216         * is non-<code>null</code>.  We pass the delegate a copy of the
217         * <code>Graphics</code> object to protect the rest of the
218         * paint code from irrevocable changes
219         * (for example, <code>Graphics.translate</code>).
220         * <p>
221         * If you override this in a subclass you should not make permanent
222         * changes to the passed in <code>Graphics</code>. For example, you
223         * should not alter the clip <code>Rectangle</code> or modify the
224         * transform. If you need to do these operations you may find it
225         * easier to create a new <code>Graphics</code> from the passed in
226         * <code>Graphics</code> and manipulate it. Further, if you do not
227         * invoker super's implementation you must honor the opaque property,
228         * that is
229         * if this component is opaque, you must completely fill in the background
230         * in a non-opaque color. If you do not honor the opaque property you
231         * will likely see visual artifacts.
232         * <p>
233         * The passed in <code>Graphics</code> object might
234         * have a transform other than the identify transform
235         * installed on it.  In this case, you might get
236         * unexpected results if you cumulatively apply
237         * another transform.
238         *
239         * @param g the <code>Graphics</code> object to protect
240         * @see #paint(Graphics)
241         * @see javax.swing.plaf.ComponentUI
242         */
243        protected void paintComponent(Graphics g)
244        {
245          if (backgroundPainter != null) {
246              Graphics2D g2 = (Graphics2D)g.create();
247              
248                try {
249              Insets ins = this.getInsets();
250              g2.translate(ins.left, ins.top);
251                    backgroundPainter.paint(g2, this, this.getWidth() - ins.left
252                            - ins.right, this.getHeight() - ins.top - ins.bottom);
253                } finally {
254              g2.dispose();
255                }
256          } else {
257              super.paintComponent(g);
258          }
259        }
260        
261        /**
262         * Specifies a Painter to use to paint the background of this JXPanel.
263         * If <code>p</code> is not null, then setOpaque(false) will be called
264         * as a side effect. A component should not be opaque if painters are
265         * being used, because Painters may paint transparent pixels or not
266         * paint certain pixels, such as around the border insets.
267         */
268        public void setBackgroundPainter(Painter p)
269        {
270            Painter old = getBackgroundPainter();
271            this.backgroundPainter = p;
272            
273            if (p != null) {
274                setOpaque(false);
275            }
276            
277            firePropertyChange("backgroundPainter", old, getBackgroundPainter());
278            repaint();
279        }
280        
281        public Painter getBackgroundPainter() {
282            return backgroundPainter;
283        }    
284        /**
285         * Uses the DividerPainter (if any) to paint each Divider that
286         * overlaps the clip Rectangle.  This is done after the call to
287         * <code>super.paintChildren()</code> so that Dividers can be 
288         * rendered "on top of" the children.
289         * <p>
290         * {@inheritDoc}
291         */
292        protected void paintChildren(Graphics g) {
293          super.paintChildren(g);
294          DividerPainter dp = getDividerPainter();
295          Rectangle clipR = g.getClipBounds();
296          if ((dp != null) && (clipR != null)) {
297            MultiSplitLayout msl = getMultiSplitLayout();
298            if ( msl.hasModel()) {
299              for(Divider divider : msl.dividersThatOverlap(clipR)) {
300                Rectangle bounds = divider.getBounds();
301                Graphics cg = g.create( bounds.x, bounds.y, bounds.width, bounds.height );
302                try {
303                  dp.paint((Graphics2D)cg, divider, bounds.width, bounds.height );
304                } finally {
305                  cg.dispose();
306                }
307              }
308            }
309          }
310        }
311    
312        private boolean dragUnderway = false;
313        private MultiSplitLayout.Divider dragDivider = null;
314        private Rectangle initialDividerBounds = null;
315        private boolean oldFloatingDividers = true;
316        private int dragOffsetX = 0;
317        private int dragOffsetY = 0;
318        private int dragMin = -1;
319        private int dragMax = -1;
320        
321        private void startDrag(int mx, int my) {
322            requestFocusInWindow();
323            MultiSplitLayout msl = getMultiSplitLayout();
324            MultiSplitLayout.Divider divider = msl.dividerAt(mx, my);
325            if (divider != null) {
326                MultiSplitLayout.Node prevNode = divider.previousSibling();
327                MultiSplitLayout.Node nextNode = divider.nextSibling();
328                if ((prevNode == null) || (nextNode == null)) {
329                dragUnderway = false;
330                }
331                else {
332                initialDividerBounds = divider.getBounds();
333                dragOffsetX = mx - initialDividerBounds.x;
334                dragOffsetY = my - initialDividerBounds.y;
335                dragDivider  = divider;
336            
337                Rectangle prevNodeBounds = prevNode.getBounds();
338                Rectangle nextNodeBounds = nextNode.getBounds();
339                if (dragDivider.isVertical()) {
340                    dragMin = prevNodeBounds.x;
341                    dragMax = nextNodeBounds.x + nextNodeBounds.width;
342                    dragMax -= dragDivider.getBounds().width;
343                    if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT ) 
344                      dragMax -= msl.getUserMinSize();
345                }
346                else {
347                    dragMin = prevNodeBounds.y;
348                    dragMax = nextNodeBounds.y + nextNodeBounds.height;
349                    dragMax -= dragDivider.getBounds().height;
350                    if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT ) 
351                      dragMax -= msl.getUserMinSize();
352                }
353                
354                if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT ) {
355                  dragMin = dragMin + msl.getUserMinSize();
356                }
357                else {
358                  if (dragDivider.isVertical()) {           
359                    dragMin = Math.max( dragMin, dragMin + getMinNodeSize(msl,prevNode).width );
360                    dragMax = Math.min( dragMax, dragMax - getMinNodeSize(msl,nextNode).width );
361        
362                    Dimension maxDim = getMaxNodeSize(msl,prevNode);
363                    if ( maxDim != null )
364                      dragMax = Math.min( dragMax, prevNodeBounds.x + maxDim.width );
365                  }
366                  else {
367                    dragMin = Math.max( dragMin, dragMin + getMinNodeSize(msl,prevNode).height );
368                    dragMax = Math.min( dragMax, dragMax - getMinNodeSize(msl,nextNode).height );
369        
370                    Dimension maxDim  = getMaxNodeSize(msl,prevNode);
371                    if ( maxDim != null )
372                      dragMax = Math.min( dragMax, prevNodeBounds.y + maxDim.height );
373                  }
374                }
375                        
376                oldFloatingDividers = getMultiSplitLayout().getFloatingDividers();
377                getMultiSplitLayout().setFloatingDividers(false);
378                dragUnderway = true;
379                }
380            }
381            else {
382                dragUnderway = false;
383            }
384        }
385    
386        /**
387         * Set the maximum node size. This method can be overridden to limit the 
388         * size of a node during a drag operation on a divider. When implementing 
389         * this method in a subclass the node instance should be checked, for 
390         * example:
391         * <code>
392         * class MyMultiSplitPane extends JXMultiSplitPane
393         * {
394         *   protected Dimension getMaxNodeSize( MultiSplitLayout msl, Node n )
395         *   {
396         *     if (( n instanceof Leaf ) && ((Leaf)n).getName().equals( "top" ))
397         *       return msl.maximumNodeSize( n );
398         *     return null;
399         *   }
400         * }
401         * </code>
402         * @param msl the MultiSplitLayout used by this pane
403         * @param n the node being resized
404         * @return the maximum size or null (by default) to ignore the maximum size.
405         */
406        protected Dimension getMaxNodeSize( MultiSplitLayout msl, Node n ) {
407          return null;
408        }
409    
410        /**
411         * Set the minimum node size. This method can be overridden to limit the 
412         * size of a node during a drag operation on a divider. 
413         * @param msl the MultiSplitLayout used by this pane
414         * @param n the node being resized
415         * @return the maximum size or null (by default) to ignore the maximum size.
416         */
417        protected Dimension getMinNodeSize( MultiSplitLayout msl, Node n ) {
418          return msl.minimumNodeSize(n);
419        }
420        
421        private void repaintDragLimits() {
422            Rectangle damageR = dragDivider.getBounds();
423            if (dragDivider.isVertical()) {
424                damageR.x = dragMin;
425                damageR.width = dragMax - dragMin;
426            }
427            else {
428                damageR.y = dragMin;
429                damageR.height = dragMax - dragMin;
430            }
431            repaint(damageR);
432        }
433    
434        private void updateDrag(int mx, int my) {
435            if (!dragUnderway) {
436                return;
437            }
438            Rectangle oldBounds = dragDivider.getBounds();
439            Rectangle bounds = new Rectangle(oldBounds);
440            if (dragDivider.isVertical()) {
441                bounds.x = mx - dragOffsetX;
442                bounds.x = Math.max(bounds.x, dragMin );
443                bounds.x = Math.min(bounds.x, dragMax);
444            }
445            else {
446                bounds.y = my - dragOffsetY;
447                bounds.y = Math.max(bounds.y, dragMin );
448                bounds.y = Math.min(bounds.y, dragMax);
449            }
450            dragDivider.setBounds(bounds);
451            if (isContinuousLayout()) {
452                revalidate();
453                repaintDragLimits();
454            }
455            else {
456                repaint(oldBounds.union(bounds));
457            }
458        }
459    
460        private void clearDragState() {
461            dragDivider = null;
462            initialDividerBounds = null;
463            oldFloatingDividers = true;
464            dragOffsetX = dragOffsetY = 0;
465            dragMin = dragMax = -1;
466            dragUnderway = false;
467        }
468    
469        private void finishDrag(int x, int y) {
470            if (dragUnderway) {
471                clearDragState();
472                if (!isContinuousLayout()) {
473                revalidate();
474                repaint();
475                }
476            }
477            setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
478        }
479        
480        private void cancelDrag() {       
481            if (dragUnderway) {
482                dragDivider.setBounds(initialDividerBounds);
483                getMultiSplitLayout().setFloatingDividers(oldFloatingDividers);
484                setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
485                repaint();
486                revalidate();
487                clearDragState();
488            }
489        }
490    
491        private void updateCursor(int x, int y, boolean show) {
492            if (dragUnderway) {
493                return;
494            }
495            int cursorID = Cursor.DEFAULT_CURSOR;
496            if (show) {
497                MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y);
498                if (divider != null) {
499                cursorID  = (divider.isVertical()) ? 
500                    Cursor.E_RESIZE_CURSOR : 
501                    Cursor.N_RESIZE_CURSOR;
502                }
503            }
504            setCursor(Cursor.getPredefinedCursor(cursorID));
505        }
506    
507    
508        private class InputHandler extends MouseInputAdapter implements KeyListener {
509    
510            public void mouseEntered(MouseEvent e) {
511                updateCursor(e.getX(), e.getY(), true);
512            }
513        
514            public void mouseMoved(MouseEvent e) {
515                updateCursor(e.getX(), e.getY(), true);
516            }
517        
518            public void mouseExited(MouseEvent e) {
519                updateCursor(e.getX(), e.getY(), false);
520            }
521        
522            public void mousePressed(MouseEvent e) {
523                startDrag(e.getX(), e.getY());
524            }
525            public void mouseReleased(MouseEvent e) {
526                finishDrag(e.getX(), e.getY());
527            }
528            public void mouseDragged(MouseEvent e) {
529                updateDrag(e.getX(), e.getY());        
530            }
531            public void keyPressed(KeyEvent e) { 
532                if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
533                cancelDrag();
534                }
535            }
536            public void keyReleased(KeyEvent e) { }
537            
538            public void keyTyped(KeyEvent e) { }
539        }
540    
541        public AccessibleContext getAccessibleContext() {
542            if( accessibleContext == null ) {
543                accessibleContext = new AccessibleMultiSplitPane();
544            }
545            return accessibleContext;
546        }
547        
548        protected class AccessibleMultiSplitPane extends AccessibleJPanel {
549            public AccessibleRole getAccessibleRole() {
550                return AccessibleRole.SPLIT_PANE;
551            }
552        }
553    }