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