org.jdesktop.swingx.JXMultiSplitPane Maven / Gradle / Ivy
/*
* $Id: JXMultiSplitPane.java 4147 2012-02-01 17:13:24Z kschaefe $
*
* Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
* Santa Clara, California 95054, U.S.A. All rights reserved.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*/
package org.jdesktop.swingx;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.io.Serializable;
import javax.accessibility.AccessibleContext;
import javax.accessibility.AccessibleRole;
import javax.swing.JPanel;
import javax.swing.event.MouseInputAdapter;
import org.jdesktop.beans.JavaBean;
import org.jdesktop.swingx.MultiSplitLayout.Divider;
import org.jdesktop.swingx.MultiSplitLayout.Node;
import org.jdesktop.swingx.painter.AbstractPainter;
import org.jdesktop.swingx.painter.Painter;
/**
*
*
* All properties in this class are bound: when a properties value
* is changed, all PropertyChangeListeners are fired.
*
* @author Hans Muller
* @author Luan O'Carroll
*/
@JavaBean
public class JXMultiSplitPane extends JPanel implements BackgroundPaintable {
private AccessibleContext accessibleContext = null;
private boolean continuousLayout = true;
private DividerPainter dividerPainter = new DefaultDividerPainter();
private Painter backgroundPainter;
private boolean paintBorderInsets;
/**
* Creates a MultiSplitPane with it's LayoutManager set to
* to an empty MultiSplitLayout.
*/
public JXMultiSplitPane() {
this(new MultiSplitLayout());
}
/**
* Creates a MultiSplitPane.
* @param layout the new split pane's layout
*/
public JXMultiSplitPane( MultiSplitLayout layout ) {
super(layout);
InputHandler inputHandler = new InputHandler();
addMouseListener(inputHandler);
addMouseMotionListener(inputHandler);
addKeyListener(inputHandler);
setFocusable(true);
}
/**
* A convenience method that returns the layout manager cast
* to MutliSplitLayout.
*
* @return this MultiSplitPane's layout manager
* @see java.awt.Container#getLayout
* @see #setModel
*/
public final MultiSplitLayout getMultiSplitLayout() {
return (MultiSplitLayout)getLayout();
}
/**
* A convenience method that sets the MultiSplitLayout model.
* Equivalent to getMultiSplitLayout.setModel(model)
*
* @param model the root of the MultiSplitLayout model
* @see #getMultiSplitLayout
* @see MultiSplitLayout#setModel
*/
public final void setModel(Node model) {
getMultiSplitLayout().setModel(model);
}
/**
* A convenience method that sets the MultiSplitLayout dividerSize
* property. Equivalent to
* getMultiSplitLayout().setDividerSize(newDividerSize)
.
*
* @param dividerSize the value of the dividerSize property
* @see #getMultiSplitLayout
* @see MultiSplitLayout#setDividerSize
*/
public final void setDividerSize(int dividerSize) {
getMultiSplitLayout().setDividerSize(dividerSize);
}
/**
* A convenience method that returns the MultiSplitLayout dividerSize
* property. Equivalent to
* getMultiSplitLayout().getDividerSize()
.
*
* @see #getMultiSplitLayout
* @see MultiSplitLayout#getDividerSize
*/
public final int getDividerSize() {
return getMultiSplitLayout().getDividerSize();
}
/**
* Sets the value of the continuousLayout
property.
* If true, then the layout is revalidated continuously while
* a divider is being moved. The default value of this property
* is true.
*
* @param continuousLayout value of the continuousLayout property
* @see #isContinuousLayout
*/
public void setContinuousLayout(boolean continuousLayout) {
boolean oldContinuousLayout = isContinuousLayout();
this.continuousLayout = continuousLayout;
firePropertyChange("continuousLayout", oldContinuousLayout, isContinuousLayout());
}
/**
* Returns true if dragging a divider only updates
* the layout when the drag gesture ends (typically, when the
* mouse button is released).
*
* @return the value of the continuousLayout
property
* @see #setContinuousLayout
*/
public boolean isContinuousLayout() {
return continuousLayout;
}
/**
* Returns the Divider that's currently being moved, typically
* because the user is dragging it, or null.
*
* @return the Divider that's being moved or null.
*/
public Divider activeDivider() {
return dragDivider;
}
/**
* Draws a single Divider. Typically used to specialize the
* way the active Divider is painted.
*
* @see #getDividerPainter
* @see #setDividerPainter
*/
public static abstract class DividerPainter extends AbstractPainter {
}
private class DefaultDividerPainter extends DividerPainter implements Serializable {
@Override
protected void doPaint(Graphics2D g, Divider divider, int width, int height) {
if ((divider == activeDivider()) && !isContinuousLayout()) {
g.setColor(Color.black);
g.fillRect(0, 0, width, height);
}
}
}
/**
* The DividerPainter that's used to paint Dividers on this MultiSplitPane.
* This property may be null.
*
* @return the value of the dividerPainter Property
* @see #setDividerPainter
*/
public DividerPainter getDividerPainter() {
return dividerPainter;
}
/**
* Sets the DividerPainter that's used to paint Dividers on this
* MultiSplitPane. The default DividerPainter only draws
* the activeDivider (if there is one) and then, only if
* continuousLayout is false. The value of this property is
* used by the paintChildren method: Dividers are painted after
* the MultiSplitPane's children have been rendered so that
* the activeDivider can appear "on top of" the children.
*
* @param dividerPainter the value of the dividerPainter property, can be null
* @see #paintChildren
* @see #activeDivider
*/
public void setDividerPainter(DividerPainter dividerPainter) {
DividerPainter old = getDividerPainter();
this.dividerPainter = dividerPainter;
firePropertyChange("dividerPainter", old, getDividerPainter());
}
/**
* Calls the UI delegate's paint method, if the UI delegate
* is non-null
. We pass the delegate a copy of the
* Graphics
object to protect the rest of the
* paint code from irrevocable changes
* (for example, Graphics.translate
).
*
* If you override this in a subclass you should not make permanent
* changes to the passed in Graphics
. For example, you
* should not alter the clip Rectangle
or modify the
* transform. If you need to do these operations you may find it
* easier to create a new Graphics
from the passed in
* Graphics
and manipulate it. Further, if you do not
* invoker super's implementation you must honor the opaque property,
* that is
* if this component is opaque, you must completely fill in the background
* in a non-opaque color. If you do not honor the opaque property you
* will likely see visual artifacts.
*
* The passed in Graphics
object might
* have a transform other than the identify transform
* installed on it. In this case, you might get
* unexpected results if you cumulatively apply
* another transform.
*
* @param g the Graphics
object to protect
* @see #paint(Graphics)
* @see javax.swing.plaf.ComponentUI
*/
@Override
protected void paintComponent(Graphics g)
{
if (backgroundPainter == null) {
super.paintComponent(g);
} else {
if (isOpaque()) {
super.paintComponent(g);
}
Graphics2D g2 = (Graphics2D) g.create();
try {
SwingXUtilities.paintBackground(this, g2);
} finally {
g2.dispose();
}
getUI().paint(g, this);
}
}
/**
* Specifies a Painter to use to paint the background of this JXPanel.
* If p
is not null, then setOpaque(false) will be called
* as a side effect. A component should not be opaque if painters are
* being used, because Painters may paint transparent pixels or not
* paint certain pixels, such as around the border insets.
*/
@Override
public void setBackgroundPainter(Painter p)
{
Painter old = getBackgroundPainter();
this.backgroundPainter = p;
if (p != null) {
setOpaque(false);
}
firePropertyChange("backgroundPainter", old, getBackgroundPainter());
repaint();
}
@Override
public Painter getBackgroundPainter() {
return backgroundPainter;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isPaintBorderInsets() {
return paintBorderInsets;
}
/**
* {@inheritDoc}
*/
@Override
public void setPaintBorderInsets(boolean paintBorderInsets) {
boolean oldValue = isPaintBorderInsets();
this.paintBorderInsets = paintBorderInsets;
firePropertyChange("paintBorderInsets", oldValue, isPaintBorderInsets());
}
/**
* Uses the DividerPainter (if any) to paint each Divider that
* overlaps the clip Rectangle. This is done after the call to
* super.paintChildren()
so that Dividers can be
* rendered "on top of" the children.
*
* {@inheritDoc}
*/
@Override
protected void paintChildren(Graphics g) {
super.paintChildren(g);
DividerPainter dp = getDividerPainter();
Rectangle clipR = g.getClipBounds();
if ((dp != null) && (clipR != null)) {
MultiSplitLayout msl = getMultiSplitLayout();
if ( msl.hasModel()) {
for(Divider divider : msl.dividersThatOverlap(clipR)) {
Rectangle bounds = divider.getBounds();
Graphics cg = g.create( bounds.x, bounds.y, bounds.width, bounds.height );
try {
dp.paint((Graphics2D)cg, divider, bounds.width, bounds.height );
} finally {
cg.dispose();
}
}
}
}
}
private boolean dragUnderway = false;
private MultiSplitLayout.Divider dragDivider = null;
private Rectangle initialDividerBounds = null;
private boolean oldFloatingDividers = true;
private int dragOffsetX = 0;
private int dragOffsetY = 0;
private int dragMin = -1;
private int dragMax = -1;
private void startDrag(int mx, int my) {
requestFocusInWindow();
MultiSplitLayout msl = getMultiSplitLayout();
MultiSplitLayout.Divider divider = msl.dividerAt(mx, my);
if (divider != null) {
MultiSplitLayout.Node prevNode = divider.previousSibling();
MultiSplitLayout.Node nextNode = divider.nextSibling();
if ((prevNode == null) || (nextNode == null)) {
dragUnderway = false;
}
else {
initialDividerBounds = divider.getBounds();
dragOffsetX = mx - initialDividerBounds.x;
dragOffsetY = my - initialDividerBounds.y;
dragDivider = divider;
Rectangle prevNodeBounds = prevNode.getBounds();
Rectangle nextNodeBounds = nextNode.getBounds();
if (dragDivider.isVertical()) {
dragMin = prevNodeBounds.x;
dragMax = nextNodeBounds.x + nextNodeBounds.width;
dragMax -= dragDivider.getBounds().width;
if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT )
dragMax -= msl.getUserMinSize();
}
else {
dragMin = prevNodeBounds.y;
dragMax = nextNodeBounds.y + nextNodeBounds.height;
dragMax -= dragDivider.getBounds().height;
if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT )
dragMax -= msl.getUserMinSize();
}
if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT ) {
dragMin = dragMin + msl.getUserMinSize();
}
else {
if (dragDivider.isVertical()) {
dragMin = Math.max( dragMin, dragMin + getMinNodeSize(msl,prevNode).width );
dragMax = Math.min( dragMax, dragMax - getMinNodeSize(msl,nextNode).width );
Dimension maxDim = getMaxNodeSize(msl,prevNode);
if ( maxDim != null )
dragMax = Math.min( dragMax, prevNodeBounds.x + maxDim.width );
}
else {
dragMin = Math.max( dragMin, dragMin + getMinNodeSize(msl,prevNode).height );
dragMax = Math.min( dragMax, dragMax - getMinNodeSize(msl,nextNode).height );
Dimension maxDim = getMaxNodeSize(msl,prevNode);
if ( maxDim != null )
dragMax = Math.min( dragMax, prevNodeBounds.y + maxDim.height );
}
}
oldFloatingDividers = getMultiSplitLayout().getFloatingDividers();
getMultiSplitLayout().setFloatingDividers(false);
dragUnderway = true;
}
}
else {
dragUnderway = false;
}
}
/**
* Set the maximum node size. This method can be overridden to limit the
* size of a node during a drag operation on a divider. When implementing
* this method in a subclass the node instance should be checked, for
* example:
*
* class MyMultiSplitPane extends JXMultiSplitPane
* {
* protected Dimension getMaxNodeSize( MultiSplitLayout msl, Node n )
* {
* if (( n instanceof Leaf ) && ((Leaf)n).getName().equals( "top" ))
* return msl.maximumNodeSize( n );
* return null;
* }
* }
*
* @param msl the MultiSplitLayout used by this pane
* @param n the node being resized
* @return the maximum size or null (by default) to ignore the maximum size.
*/
protected Dimension getMaxNodeSize( MultiSplitLayout msl, Node n ) {
return null;
}
/**
* Set the minimum node size. This method can be overridden to limit the
* size of a node during a drag operation on a divider.
* @param msl the MultiSplitLayout used by this pane
* @param n the node being resized
* @return the maximum size or null (by default) to ignore the maximum size.
*/
protected Dimension getMinNodeSize( MultiSplitLayout msl, Node n ) {
return msl.minimumNodeSize(n);
}
private void repaintDragLimits() {
Rectangle damageR = dragDivider.getBounds();
if (dragDivider.isVertical()) {
damageR.x = dragMin;
damageR.width = dragMax - dragMin;
}
else {
damageR.y = dragMin;
damageR.height = dragMax - dragMin;
}
repaint(damageR);
}
private void updateDrag(int mx, int my) {
if (!dragUnderway) {
return;
}
Rectangle oldBounds = dragDivider.getBounds();
Rectangle bounds = new Rectangle(oldBounds);
if (dragDivider.isVertical()) {
bounds.x = mx - dragOffsetX;
bounds.x = Math.max(bounds.x, dragMin );
bounds.x = Math.min(bounds.x, dragMax);
}
else {
bounds.y = my - dragOffsetY;
bounds.y = Math.max(bounds.y, dragMin );
bounds.y = Math.min(bounds.y, dragMax);
}
dragDivider.setBounds(bounds);
if (isContinuousLayout()) {
revalidate();
repaintDragLimits();
}
else {
repaint(oldBounds.union(bounds));
}
}
private void clearDragState() {
dragDivider = null;
initialDividerBounds = null;
oldFloatingDividers = true;
dragOffsetX = dragOffsetY = 0;
dragMin = dragMax = -1;
dragUnderway = false;
}
private void finishDrag(int x, int y) {
if (dragUnderway) {
clearDragState();
if (!isContinuousLayout()) {
revalidate();
repaint();
}
}
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
private void cancelDrag() {
if (dragUnderway) {
dragDivider.setBounds(initialDividerBounds);
getMultiSplitLayout().setFloatingDividers(oldFloatingDividers);
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
repaint();
revalidate();
clearDragState();
}
}
private void updateCursor(int x, int y, boolean show) {
if (dragUnderway) {
return;
}
int cursorID = Cursor.DEFAULT_CURSOR;
if (show) {
MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y);
if (divider != null) {
cursorID = (divider.isVertical()) ?
Cursor.E_RESIZE_CURSOR :
Cursor.N_RESIZE_CURSOR;
}
}
setCursor(Cursor.getPredefinedCursor(cursorID));
}
private class InputHandler extends MouseInputAdapter implements KeyListener {
@Override
public void mouseEntered(MouseEvent e) {
updateCursor(e.getX(), e.getY(), true);
}
@Override
public void mouseMoved(MouseEvent e) {
updateCursor(e.getX(), e.getY(), true);
}
@Override
public void mouseExited(MouseEvent e) {
updateCursor(e.getX(), e.getY(), false);
}
@Override
public void mousePressed(MouseEvent e) {
startDrag(e.getX(), e.getY());
}
@Override
public void mouseReleased(MouseEvent e) {
finishDrag(e.getX(), e.getY());
}
@Override
public void mouseDragged(MouseEvent e) {
updateDrag(e.getX(), e.getY());
}
@Override
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
cancelDrag();
}
}
@Override
public void keyReleased(KeyEvent e) { }
@Override
public void keyTyped(KeyEvent e) { }
}
@Override
public AccessibleContext getAccessibleContext() {
if( accessibleContext == null ) {
accessibleContext = new AccessibleMultiSplitPane();
}
return accessibleContext;
}
protected class AccessibleMultiSplitPane extends AccessibleJPanel {
@Override
public AccessibleRole getAccessibleRole() {
return AccessibleRole.SPLIT_PANE;
}
}
}