001 /* 002 * $Id: JXMultiSplitPane.java,v 1.1 2006/03/23 21:52:43 hansmuller Exp $ 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.Graphics; 027 import java.awt.Graphics2D; 028 import java.awt.Rectangle; 029 import java.awt.event.KeyEvent; 030 import java.awt.event.KeyListener; 031 import java.awt.event.MouseEvent; 032 import javax.accessibility.AccessibleContext; 033 import javax.accessibility.AccessibleRole; 034 import javax.swing.JPanel; 035 import javax.swing.event.MouseInputAdapter; 036 import org.jdesktop.swingx.MultiSplitLayout.Divider; 037 import org.jdesktop.swingx.MultiSplitLayout.Node; 038 039 /** 040 * 041 * <p> 042 * All properties in this class are bound: when a properties value 043 * is changed, all PropertyChangeListeners are fired. 044 * 045 * @author Hans Muller 046 */ 047 public class JXMultiSplitPane extends JPanel { 048 private AccessibleContext accessibleContext = null; 049 private boolean continuousLayout = true; 050 private DividerPainter dividerPainter = new DefaultDividerPainter(); 051 052 /** 053 * Creates a JXMultiSplitPane with it's LayoutManager set to 054 * to an empty MultiSplitLayout. 055 */ 056 public JXMultiSplitPane() { 057 super(new MultiSplitLayout()); 058 InputHandler inputHandler = new InputHandler(); 059 addMouseListener(inputHandler); 060 addMouseMotionListener(inputHandler); 061 addKeyListener(inputHandler); 062 setFocusable(true); 063 } 064 065 /** 066 * 067 * A convenience method that returns the layout manager cast 068 * to MutliSplitLayout. 069 * 070 * 071 * @return this JXMultiSplitPane's layout manager 072 * @see java.awt.Container#getLayout 073 * @see #setModel 074 */ 075 public final MultiSplitLayout getMultiSplitLayout() { 076 return (MultiSplitLayout)getLayout(); 077 } 078 079 /** 080 * A convenience method that sets the MultiSplitLayout model. 081 * Equivalent to <code>getMultiSplitLayout.setModel(model)</code> 082 * 083 * @param model the root of the MultiSplitLayout model 084 * @see #getMultiSplitLayout 085 * @see MultiSplitLayout#setModel 086 */ 087 public final void setModel(Node model) { 088 getMultiSplitLayout().setModel(model); 089 } 090 091 /** 092 * A convenience method that sets the MultiSplitLayout dividerSize 093 * property. Equivalent to 094 * <code>getMultiSplitLayout().setDividerSize(newDividerSize)</code>. 095 * 096 * @param dividerSize the value of the dividerSize property 097 * @see #getMultiSplitLayout 098 * @see MultiSplitLayout#setDividerSize 099 */ 100 public final void setDividerSize(int dividerSize) { 101 getMultiSplitLayout().setDividerSize(dividerSize); 102 } 103 104 /** 105 * Sets the value of the <code>continuousLayout</code> property. 106 * If true, then the layout is revalidated continuously while 107 * a divider is being moved. The default value of this property 108 * is true. 109 * 110 * @param continuousLayout value of the continuousLayout property 111 * @see #isContinuousLayout 112 */ 113 public void setContinuousLayout(boolean continuousLayout) { 114 boolean oldContinuousLayout = continuousLayout; 115 this.continuousLayout = continuousLayout; 116 firePropertyChange("continuousLayout", oldContinuousLayout, continuousLayout); 117 } 118 119 /** 120 * Returns true if dragging a divider only updates 121 * the layout when the drag gesture ends (typically, when the 122 * mouse button is released). 123 * 124 * @return the value of the <code>continuousLayout</code> property 125 * @see #setContinuousLayout 126 */ 127 public boolean isContinuousLayout() { 128 return continuousLayout; 129 } 130 131 /** 132 * Returns the Divider that's currently being moved, typically 133 * because the user is dragging it, or null. 134 * 135 * @return the Divider that's being moved or null. 136 */ 137 public Divider activeDivider() { 138 return dragDivider; 139 } 140 141 /** 142 * Draws a single Divider. Typically used to specialize the 143 * way the active Divider is painted. 144 * 145 * @see #getDividerPainter 146 * @see #setDividerPainter 147 */ 148 public static abstract class DividerPainter { 149 /** 150 * Paint a single Divider. 151 * 152 * @param g the Graphics object to paint with 153 * @param divider the Divider to paint 154 */ 155 public abstract void paint(Graphics g, Divider divider); 156 } 157 158 private class DefaultDividerPainter extends DividerPainter { 159 public void paint(Graphics g, Divider divider) { 160 if ((divider == activeDivider()) && !isContinuousLayout()) { 161 Graphics2D g2d = (Graphics2D)g; 162 g2d.setColor(Color.black); 163 g2d.fill(divider.getBounds()); 164 } 165 } 166 } 167 168 /** 169 * 170 * The DividerPainter that's used to paint Dividers on this JXMultiSplitPane. 171 * This property may be null. 172 * 173 * 174 * @return the value of the dividerPainter Property 175 * @see #setDividerPainter 176 */ 177 public DividerPainter getDividerPainter() { 178 return dividerPainter; 179 } 180 181 /** 182 * 183 * Sets the DividerPainter that's used to paint Dividers on this 184 * JXMultiSplitPane. The default DividerPainter only draws 185 * the activeDivider (if there is one) and then, only if 186 * continuousLayout is false. The value of this property is 187 * used by the paintChildren method: Dividers are painted after 188 * the JXMultiSplitPane's children have been rendered so that 189 * the activeDivider can appear "on top of" the children. 190 * 191 * 192 * @param dividerPainter the value of the dividerPainter property, can be null 193 * @see #paintChildren 194 * @see #activeDivider 195 */ 196 public void setDividerPainter(DividerPainter dividerPainter) { 197 this.dividerPainter = dividerPainter; 198 } 199 200 /** 201 * Uses the DividerPainter (if any) to paint each Divider that 202 * overlaps the clip Rectangle. This is done after the call to 203 * <code>super.paintChildren()</code> so that Dividers can be 204 * rendered "on top of" the children. 205 * <p> 206 * {@inheritDoc} 207 */ 208 protected void paintChildren(Graphics g) { 209 super.paintChildren(g); 210 DividerPainter dp = getDividerPainter(); 211 Rectangle clipR = g.getClipBounds(); 212 if ((dp != null) && (clipR != null)) { 213 Graphics dpg = g.create(); 214 try { 215 MultiSplitLayout msl = getMultiSplitLayout(); 216 for(Divider divider : msl.dividersThatOverlap(clipR)) { 217 dp.paint(dpg, divider); 218 } 219 } 220 finally { 221 dpg.dispose(); 222 } 223 } 224 } 225 226 private boolean dragUnderway = false; 227 private MultiSplitLayout.Divider dragDivider = null; 228 private Rectangle initialDividerBounds = null; 229 private boolean oldFloatingDividers = true; 230 private int dragOffsetX = 0; 231 private int dragOffsetY = 0; 232 private int dragMin = -1; 233 private int dragMax = -1; 234 235 private void startDrag(int mx, int my) { 236 requestFocusInWindow(); 237 MultiSplitLayout msl = getMultiSplitLayout(); 238 MultiSplitLayout.Divider divider = msl.dividerAt(mx, my); 239 if (divider != null) { 240 MultiSplitLayout.Node prevNode = divider.previousSibling(); 241 MultiSplitLayout.Node nextNode = divider.nextSibling(); 242 if ((prevNode == null) || (nextNode == null)) { 243 dragUnderway = false; 244 } 245 else { 246 initialDividerBounds = divider.getBounds(); 247 dragOffsetX = mx - initialDividerBounds.x; 248 dragOffsetY = my - initialDividerBounds.y; 249 dragDivider = divider; 250 Rectangle prevNodeBounds = prevNode.getBounds(); 251 Rectangle nextNodeBounds = nextNode.getBounds(); 252 if (dragDivider.isVertical()) { 253 dragMin = prevNodeBounds.x; 254 dragMax = nextNodeBounds.x + nextNodeBounds.width; 255 dragMax -= dragDivider.getBounds().width; 256 } 257 else { 258 dragMin = prevNodeBounds.y; 259 dragMax = nextNodeBounds.y + nextNodeBounds.height; 260 dragMax -= dragDivider.getBounds().height; 261 } 262 oldFloatingDividers = getMultiSplitLayout().getFloatingDividers(); 263 getMultiSplitLayout().setFloatingDividers(false); 264 dragUnderway = true; 265 } 266 } 267 else { 268 dragUnderway = false; 269 } 270 } 271 272 private void repaintDragLimits() { 273 Rectangle damageR = dragDivider.getBounds(); 274 if (dragDivider.isVertical()) { 275 damageR.x = dragMin; 276 damageR.width = dragMax - dragMin; 277 } 278 else { 279 damageR.y = dragMin; 280 damageR.height = dragMax - dragMin; 281 } 282 repaint(damageR); 283 } 284 285 private void updateDrag(int mx, int my) { 286 if (!dragUnderway) { 287 return; 288 } 289 Rectangle oldBounds = dragDivider.getBounds(); 290 Rectangle bounds = new Rectangle(oldBounds); 291 if (dragDivider.isVertical()) { 292 bounds.x = mx - dragOffsetX; 293 bounds.x = Math.max(bounds.x, dragMin); 294 bounds.x = Math.min(bounds.x, dragMax); 295 } 296 else { 297 bounds.y = my - dragOffsetY; 298 bounds.y = Math.max(bounds.y, dragMin); 299 bounds.y = Math.min(bounds.y, dragMax); 300 } 301 dragDivider.setBounds(bounds); 302 if (isContinuousLayout()) { 303 revalidate(); 304 repaintDragLimits(); 305 } 306 else { 307 repaint(oldBounds.union(bounds)); 308 } 309 } 310 311 private void clearDragState() { 312 dragDivider = null; 313 initialDividerBounds = null; 314 oldFloatingDividers = true; 315 dragOffsetX = dragOffsetY = 0; 316 dragMin = dragMax = -1; 317 dragUnderway = false; 318 } 319 320 private void finishDrag(int x, int y) { 321 if (dragUnderway) { 322 clearDragState(); 323 if (!isContinuousLayout()) { 324 revalidate(); 325 repaint(); 326 } 327 } 328 } 329 330 private void cancelDrag() { 331 if (dragUnderway) { 332 dragDivider.setBounds(initialDividerBounds); 333 getMultiSplitLayout().setFloatingDividers(oldFloatingDividers); 334 setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); 335 repaint(); 336 revalidate(); 337 clearDragState(); 338 } 339 } 340 341 private void updateCursor(int x, int y, boolean show) { 342 if (dragUnderway) { 343 return; 344 } 345 int cursorID = Cursor.DEFAULT_CURSOR; 346 if (show) { 347 MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y); 348 if (divider != null) { 349 cursorID = (divider.isVertical()) ? 350 Cursor.E_RESIZE_CURSOR : 351 Cursor.N_RESIZE_CURSOR; 352 } 353 } 354 setCursor(Cursor.getPredefinedCursor(cursorID)); 355 } 356 357 358 private class InputHandler extends MouseInputAdapter implements KeyListener { 359 360 public void mouseEntered(MouseEvent e) { 361 updateCursor(e.getX(), e.getY(), true); 362 } 363 364 public void mouseMoved(MouseEvent e) { 365 updateCursor(e.getX(), e.getY(), true); 366 } 367 368 public void mouseExited(MouseEvent e) { 369 updateCursor(e.getX(), e.getY(), false); 370 } 371 372 public void mousePressed(MouseEvent e) { 373 startDrag(e.getX(), e.getY()); 374 } 375 public void mouseReleased(MouseEvent e) { 376 finishDrag(e.getX(), e.getY()); 377 } 378 public void mouseDragged(MouseEvent e) { 379 updateDrag(e.getX(), e.getY()); 380 } 381 public void keyPressed(KeyEvent e) { 382 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { 383 cancelDrag(); 384 } 385 } 386 public void keyReleased(KeyEvent e) { } 387 public void keyTyped(KeyEvent e) { } 388 } 389 390 public AccessibleContext getAccessibleContext() { 391 if( accessibleContext == null ) { 392 accessibleContext = new AccessibleMultiSplitPane(); 393 } 394 return accessibleContext; 395 } 396 397 protected class AccessibleMultiSplitPane extends AccessibleJPanel { 398 public AccessibleRole getAccessibleRole() { 399 return AccessibleRole.SPLIT_PANE; 400 } 401 } 402 }