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