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 }