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