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 */ 021 022 //package org.jdesktop.swingx; 023 package org.openstreetmap.josm.gui; 024 025 import java.awt.Color; 026 import java.awt.Cursor; 027 import java.awt.Graphics; 028 import java.awt.Graphics2D; 029 import java.awt.Rectangle; 030 import java.awt.event.KeyEvent; 031 import java.awt.event.KeyListener; 032 import java.awt.event.MouseEvent; 033 import javax.accessibility.AccessibleContext; 034 import javax.accessibility.AccessibleRole; 035 import javax.swing.JPanel; 036 import javax.swing.event.MouseInputAdapter; 037 038 import org.openstreetmap.josm.gui.MultiSplitLayout.Divider; 039 import org.openstreetmap.josm.gui.MultiSplitLayout.Node; 040 041 /** 042 * 043 * <p> 044 * All properties in this class are bound: when a properties value 045 * is changed, all PropertyChangeListeners are fired. 046 * 047 * @author Hans Muller 048 */ 049 public class MultiSplitPane extends JPanel { 050 private AccessibleContext accessibleContext = null; 051 private boolean continuousLayout = true; 052 private DividerPainter dividerPainter = new DefaultDividerPainter(); 053 054 /** 055 * Creates a MultiSplitPane with it's LayoutManager set to 056 * to an empty MultiSplitLayout. 057 */ 058 public MultiSplitPane() { 059 super(new MultiSplitLayout()); 060 InputHandler inputHandler = new InputHandler(); 061 addMouseListener(inputHandler); 062 addMouseMotionListener(inputHandler); 063 addKeyListener(inputHandler); 064 setFocusable(true); 065 } 066 067 /** 068 * A convenience method that returns the layout manager cast 069 * to MutliSplitLayout. 070 * 071 * @return this MultiSplitPane'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 * 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 protected void paintChildren(Graphics g) { 205 super.paintChildren(g); 206 DividerPainter dp = getDividerPainter(); 207 Rectangle clipR = g.getClipBounds(); 208 if ((dp != null) && (clipR != null)) { 209 Graphics dpg = g.create(); 210 try { 211 MultiSplitLayout msl = getMultiSplitLayout(); 212 for(Divider divider : msl.dividersThatOverlap(clipR)) { 213 dp.paint(dpg, divider); 214 } 215 } 216 finally { 217 dpg.dispose(); 218 } 219 } 220 } 221 222 private boolean dragUnderway = false; 223 private MultiSplitLayout.Divider dragDivider = null; 224 private Rectangle initialDividerBounds = null; 225 private boolean oldFloatingDividers = true; 226 private int dragOffsetX = 0; 227 private int dragOffsetY = 0; 228 private int dragMin = -1; 229 private int dragMax = -1; 230 231 private void startDrag(int mx, int my) { 232 requestFocusInWindow(); 233 MultiSplitLayout msl = getMultiSplitLayout(); 234 MultiSplitLayout.Divider divider = msl.dividerAt(mx, my); 235 if (divider != null) { 236 MultiSplitLayout.Node prevNode = divider.previousSibling(); 237 MultiSplitLayout.Node nextNode = divider.nextSibling(); 238 if ((prevNode == null) || (nextNode == null)) { 239 dragUnderway = false; 240 } 241 else { 242 initialDividerBounds = divider.getBounds(); 243 dragOffsetX = mx - initialDividerBounds.x; 244 dragOffsetY = my - initialDividerBounds.y; 245 dragDivider = divider; 246 Rectangle prevNodeBounds = prevNode.getBounds(); 247 Rectangle nextNodeBounds = nextNode.getBounds(); 248 if (dragDivider.isVertical()) { 249 dragMin = prevNodeBounds.x; 250 dragMax = nextNodeBounds.x + nextNodeBounds.width; 251 dragMax -= dragDivider.getBounds().width; 252 } 253 else { 254 dragMin = prevNodeBounds.y; 255 dragMax = nextNodeBounds.y + nextNodeBounds.height; 256 dragMax -= dragDivider.getBounds().height; 257 } 258 oldFloatingDividers = getMultiSplitLayout().getFloatingDividers(); 259 getMultiSplitLayout().setFloatingDividers(false); 260 dragUnderway = true; 261 } 262 } 263 else { 264 dragUnderway = false; 265 } 266 } 267 268 private void repaintDragLimits() { 269 Rectangle damageR = dragDivider.getBounds(); 270 if (dragDivider.isVertical()) { 271 damageR.x = dragMin; 272 damageR.width = dragMax - dragMin; 273 } 274 else { 275 damageR.y = dragMin; 276 damageR.height = dragMax - dragMin; 277 } 278 repaint(damageR); 279 } 280 281 private void updateDrag(int mx, int my) { 282 if (!dragUnderway) { 283 return; 284 } 285 Rectangle oldBounds = dragDivider.getBounds(); 286 Rectangle bounds = new Rectangle(oldBounds); 287 if (dragDivider.isVertical()) { 288 bounds.x = mx - dragOffsetX; 289 bounds.x = Math.max(bounds.x, dragMin); 290 bounds.x = Math.min(bounds.x, dragMax); 291 } 292 else { 293 bounds.y = my - dragOffsetY; 294 bounds.y = Math.max(bounds.y, dragMin); 295 bounds.y = Math.min(bounds.y, dragMax); 296 } 297 dragDivider.setBounds(bounds); 298 if (isContinuousLayout()) { 299 revalidate(); 300 repaintDragLimits(); 301 } 302 else { 303 repaint(oldBounds.union(bounds)); 304 } 305 } 306 307 private void clearDragState() { 308 dragDivider = null; 309 initialDividerBounds = null; 310 oldFloatingDividers = true; 311 dragOffsetX = dragOffsetY = 0; 312 dragMin = dragMax = -1; 313 dragUnderway = false; 314 } 315 316 private void finishDrag(int x, int y) { 317 if (dragUnderway) { 318 clearDragState(); 319 if (!isContinuousLayout()) { 320 revalidate(); 321 repaint(); 322 } 323 } 324 } 325 326 private void cancelDrag() { 327 if (dragUnderway) { 328 dragDivider.setBounds(initialDividerBounds); 329 getMultiSplitLayout().setFloatingDividers(oldFloatingDividers); 330 setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); 331 repaint(); 332 revalidate(); 333 clearDragState(); 334 } 335 } 336 337 private void updateCursor(int x, int y, boolean show) { 338 if (dragUnderway) { 339 return; 340 } 341 int cursorID = Cursor.DEFAULT_CURSOR; 342 if (show) { 343 MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y); 344 if (divider != null) { 345 cursorID = (divider.isVertical()) ? 346 Cursor.E_RESIZE_CURSOR : 347 Cursor.N_RESIZE_CURSOR; 348 } 349 } 350 setCursor(Cursor.getPredefinedCursor(cursorID)); 351 } 352 353 private class InputHandler extends MouseInputAdapter implements KeyListener { 354 355 public void mouseEntered(MouseEvent e) { 356 updateCursor(e.getX(), e.getY(), true); 357 } 358 359 public void mouseMoved(MouseEvent e) { 360 updateCursor(e.getX(), e.getY(), true); 361 } 362 363 public void mouseExited(MouseEvent e) { 364 updateCursor(e.getX(), e.getY(), false); 365 } 366 367 public void mousePressed(MouseEvent e) { 368 startDrag(e.getX(), e.getY()); 369 } 370 public void mouseReleased(MouseEvent e) { 371 finishDrag(e.getX(), e.getY()); 372 } 373 public void mouseDragged(MouseEvent e) { 374 updateDrag(e.getX(), e.getY()); 375 } 376 public void keyPressed(KeyEvent e) { 377 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { 378 cancelDrag(); 379 } 380 } 381 public void keyReleased(KeyEvent e) { } 382 public void keyTyped(KeyEvent e) { } 383 } 384 385 public AccessibleContext getAccessibleContext() { 386 if( accessibleContext == null ) { 387 accessibleContext = new AccessibleMultiSplitPane(); 388 } 389 return accessibleContext; 390 } 391 392 protected class AccessibleMultiSplitPane extends AccessibleJPanel { 393 public AccessibleRole getAccessibleRole() { 394 return AccessibleRole.SPLIT_PANE; 395 } 396 } 397 }