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 @FunctionalInterface 147 public interface 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 void paint(Graphics g, Divider divider); 155 } 156 157 private class DefaultDividerPainter implements 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 } finally { 217 dpg.dispose(); 218 } 219 } 220 } 221 222 private boolean dragUnderway; 223 private transient MultiSplitLayout.Divider dragDivider; 224 private Rectangle initialDividerBounds; 225 private boolean oldFloatingDividers = true; 226 private int dragOffsetX; 227 private int dragOffsetY; 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 } else { 241 initialDividerBounds = divider.getBounds(); 242 dragOffsetX = mx - initialDividerBounds.x; 243 dragOffsetY = my - initialDividerBounds.y; 244 dragDivider = divider; 245 Rectangle prevNodeBounds = prevNode.getBounds(); 246 Rectangle nextNodeBounds = nextNode.getBounds(); 247 if (dragDivider.isVertical()) { 248 dragMin = prevNodeBounds.x; 249 dragMax = nextNodeBounds.x + nextNodeBounds.width; 250 dragMax -= dragDivider.getBounds().width; 251 } else { 252 dragMin = prevNodeBounds.y; 253 dragMax = nextNodeBounds.y + nextNodeBounds.height; 254 dragMax -= dragDivider.getBounds().height; 255 } 256 oldFloatingDividers = getMultiSplitLayout().getFloatingDividers(); 257 getMultiSplitLayout().setFloatingDividers(false); 258 dragUnderway = true; 259 } 260 } else { 261 dragUnderway = false; 262 } 263 } 264 265 private void repaintDragLimits() { 266 Rectangle damageR = dragDivider.getBounds(); 267 if (dragDivider.isVertical()) { 268 damageR.x = dragMin; 269 damageR.width = dragMax - dragMin; 270 } else { 271 damageR.y = dragMin; 272 damageR.height = dragMax - dragMin; 273 } 274 repaint(damageR); 275 } 276 277 private void updateDrag(int mx, int my) { 278 if (!dragUnderway) { 279 return; 280 } 281 Rectangle oldBounds = dragDivider.getBounds(); 282 Rectangle bounds = new Rectangle(oldBounds); 283 if (dragDivider.isVertical()) { 284 bounds.x = mx - dragOffsetX; 285 bounds.x = Math.max(bounds.x, dragMin); 286 bounds.x = Math.min(bounds.x, dragMax); 287 } else { 288 bounds.y = my - dragOffsetY; 289 bounds.y = Math.max(bounds.y, dragMin); 290 bounds.y = Math.min(bounds.y, dragMax); 291 } 292 dragDivider.setBounds(bounds); 293 if (isContinuousLayout()) { 294 revalidate(); 295 repaintDragLimits(); 296 } else { 297 repaint(oldBounds.union(bounds)); 298 } 299 } 300 301 private void clearDragState() { 302 dragDivider = null; 303 initialDividerBounds = null; 304 oldFloatingDividers = true; 305 dragOffsetX = dragOffsetY = 0; 306 dragMin = dragMax = -1; 307 dragUnderway = false; 308 } 309 310 private void finishDrag() { 311 if (dragUnderway) { 312 clearDragState(); 313 if (!isContinuousLayout()) { 314 revalidate(); 315 repaint(); 316 } 317 } 318 } 319 320 private void cancelDrag() { 321 if (dragUnderway) { 322 dragDivider.setBounds(initialDividerBounds); 323 getMultiSplitLayout().setFloatingDividers(oldFloatingDividers); 324 setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); 325 repaint(); 326 revalidate(); 327 clearDragState(); 328 } 329 } 330 331 private void updateCursor(int x, int y, boolean show) { 332 if (dragUnderway) { 333 return; 334 } 335 int cursorID = Cursor.DEFAULT_CURSOR; 336 if (show) { 337 MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y); 338 if (divider != null) { 339 cursorID = divider.isVertical() ? 340 Cursor.E_RESIZE_CURSOR : 341 Cursor.N_RESIZE_CURSOR; 342 } 343 } 344 setCursor(Cursor.getPredefinedCursor(cursorID)); 345 } 346 347 private class InputHandler extends MouseInputAdapter implements KeyListener { 348 349 @Override 350 public void mouseEntered(MouseEvent e) { 351 updateCursor(e.getX(), e.getY(), true); 352 } 353 354 @Override 355 public void mouseMoved(MouseEvent e) { 356 updateCursor(e.getX(), e.getY(), true); 357 } 358 359 @Override 360 public void mouseExited(MouseEvent e) { 361 updateCursor(e.getX(), e.getY(), false); 362 } 363 364 @Override 365 public void mousePressed(MouseEvent e) { 366 startDrag(e.getX(), e.getY()); 367 } 368 369 @Override 370 public void mouseReleased(MouseEvent e) { 371 finishDrag(); 372 } 373 374 @Override 375 public void mouseDragged(MouseEvent e) { 376 updateDrag(e.getX(), e.getY()); 377 } 378 379 @Override 380 public void keyPressed(KeyEvent e) { 381 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) { 382 cancelDrag(); 383 } 384 } 385 386 @Override 387 public void keyReleased(KeyEvent e) { 388 // Do nothing 389 } 390 391 @Override 392 public void keyTyped(KeyEvent e) { 393 // Do nothing 394 } 395 } 396 397 @Override 398 public AccessibleContext getAccessibleContext() { 399 if (accessibleContext == null) { 400 accessibleContext = new AccessibleMultiSplitPane(); 401 } 402 return accessibleContext; 403 } 404 405 protected class AccessibleMultiSplitPane extends AccessibleJPanel { 406 @Override 407 public AccessibleRole getAccessibleRole() { 408 return AccessibleRole.SPLIT_PANE; 409 } 410 } 411}