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}