001 /*
002 * $Id: JXMultiSplitPane.java 3279 2009-03-01 12:01:11Z rah003 $
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.Dimension;
027 import java.awt.Graphics;
028 import java.awt.Graphics2D;
029 import java.awt.Insets;
030 import java.awt.Rectangle;
031 import java.awt.event.KeyEvent;
032 import java.awt.event.KeyListener;
033 import java.awt.event.MouseEvent;
034 import javax.accessibility.AccessibleContext;
035 import javax.accessibility.AccessibleRole;
036 import javax.swing.JPanel;
037 import javax.swing.event.MouseInputAdapter;
038 import org.jdesktop.swingx.MultiSplitLayout.Divider;
039 import org.jdesktop.swingx.MultiSplitLayout.Node;
040 import org.jdesktop.swingx.painter.AbstractPainter;
041 import org.jdesktop.swingx.painter.Painter;
042
043 /**
044 *
045 * <p>
046 * All properties in this class are bound: when a properties value
047 * is changed, all PropertyChangeListeners are fired.
048 *
049 * @author Hans Muller
050 * @author Luan O'Carroll
051 */
052 public class JXMultiSplitPane extends JPanel {
053 private AccessibleContext accessibleContext = null;
054 private boolean continuousLayout = true;
055 private DividerPainter dividerPainter = new DefaultDividerPainter();
056 private Painter backgroundPainter;
057
058 /**
059 * Creates a MultiSplitPane with it's LayoutManager set to
060 * to an empty MultiSplitLayout.
061 */
062 public JXMultiSplitPane() {
063 this(new MultiSplitLayout());
064 }
065
066 /**
067 * Creates a MultiSplitPane.
068 * @param layout the new split pane's layout
069 */
070 public JXMultiSplitPane( MultiSplitLayout layout ) {
071 super(layout);
072 InputHandler inputHandler = new InputHandler();
073 addMouseListener(inputHandler);
074 addMouseMotionListener(inputHandler);
075 addKeyListener(inputHandler);
076 setFocusable(true);
077 }
078
079 /**
080 * A convenience method that returns the layout manager cast
081 * to MutliSplitLayout.
082 *
083 * @return this MultiSplitPane's layout manager
084 * @see java.awt.Container#getLayout
085 * @see #setModel
086 */
087 public final MultiSplitLayout getMultiSplitLayout() {
088 return (MultiSplitLayout)getLayout();
089 }
090
091 /**
092 * A convenience method that sets the MultiSplitLayout model.
093 * Equivalent to <code>getMultiSplitLayout.setModel(model)</code>
094 *
095 * @param model the root of the MultiSplitLayout model
096 * @see #getMultiSplitLayout
097 * @see MultiSplitLayout#setModel
098 */
099 public final void setModel(Node model) {
100 getMultiSplitLayout().setModel(model);
101 }
102
103 /**
104 * A convenience method that sets the MultiSplitLayout dividerSize
105 * property. Equivalent to
106 * <code>getMultiSplitLayout().setDividerSize(newDividerSize)</code>.
107 *
108 * @param dividerSize the value of the dividerSize property
109 * @see #getMultiSplitLayout
110 * @see MultiSplitLayout#setDividerSize
111 */
112 public final void setDividerSize(int dividerSize) {
113 getMultiSplitLayout().setDividerSize(dividerSize);
114 }
115
116 /**
117 * A convenience method that returns the MultiSplitLayout dividerSize
118 * property. Equivalent to
119 * <code>getMultiSplitLayout().getDividerSize()</code>.
120 *
121 * @see #getMultiSplitLayout
122 * @see MultiSplitLayout#getDividerSize
123 */
124 public final int getDividerSize() {
125 return getMultiSplitLayout().getDividerSize();
126 }
127
128 /**
129 * Sets the value of the <code>continuousLayout</code> property.
130 * If true, then the layout is revalidated continuously while
131 * a divider is being moved. The default value of this property
132 * is true.
133 *
134 * @param continuousLayout value of the continuousLayout property
135 * @see #isContinuousLayout
136 */
137 public void setContinuousLayout(boolean continuousLayout) {
138 boolean oldContinuousLayout = isContinuousLayout();
139 this.continuousLayout = continuousLayout;
140 firePropertyChange("continuousLayout", oldContinuousLayout, isContinuousLayout());
141 }
142
143 /**
144 * Returns true if dragging a divider only updates
145 * the layout when the drag gesture ends (typically, when the
146 * mouse button is released).
147 *
148 * @return the value of the <code>continuousLayout</code> property
149 * @see #setContinuousLayout
150 */
151 public boolean isContinuousLayout() {
152 return continuousLayout;
153 }
154
155 /**
156 * Returns the Divider that's currently being moved, typically
157 * because the user is dragging it, or null.
158 *
159 * @return the Divider that's being moved or null.
160 */
161 public Divider activeDivider() {
162 return dragDivider;
163 }
164
165 /**
166 * Draws a single Divider. Typically used to specialize the
167 * way the active Divider is painted.
168 *
169 * @see #getDividerPainter
170 * @see #setDividerPainter
171 */
172 public static abstract class DividerPainter extends AbstractPainter<Divider> {
173 }
174
175 private class DefaultDividerPainter extends DividerPainter {
176 protected void doPaint(Graphics2D g, Divider divider, int width, int height) {
177 if ((divider == activeDivider()) && !isContinuousLayout()) {
178 g.setColor(Color.black);
179 g.fillRect(0, 0, width, height);
180 }
181 }
182 }
183
184 /**
185 * The DividerPainter that's used to paint Dividers on this MultiSplitPane.
186 * This property may be null.
187 *
188 * @return the value of the dividerPainter Property
189 * @see #setDividerPainter
190 */
191 public DividerPainter getDividerPainter() {
192 return dividerPainter;
193 }
194
195 /**
196 * Sets the DividerPainter that's used to paint Dividers on this
197 * MultiSplitPane. The default DividerPainter only draws
198 * the activeDivider (if there is one) and then, only if
199 * continuousLayout is false. The value of this property is
200 * used by the paintChildren method: Dividers are painted after
201 * the MultiSplitPane's children have been rendered so that
202 * the activeDivider can appear "on top of" the children.
203 *
204 * @param dividerPainter the value of the dividerPainter property, can be null
205 * @see #paintChildren
206 * @see #activeDivider
207 */
208 public void setDividerPainter(DividerPainter dividerPainter) {
209 DividerPainter old = getDividerPainter();
210 this.dividerPainter = dividerPainter;
211 firePropertyChange("dividerPainter", old, getDividerPainter());
212 }
213
214 /**
215 * Calls the UI delegate's paint method, if the UI delegate
216 * is non-<code>null</code>. We pass the delegate a copy of the
217 * <code>Graphics</code> object to protect the rest of the
218 * paint code from irrevocable changes
219 * (for example, <code>Graphics.translate</code>).
220 * <p>
221 * If you override this in a subclass you should not make permanent
222 * changes to the passed in <code>Graphics</code>. For example, you
223 * should not alter the clip <code>Rectangle</code> or modify the
224 * transform. If you need to do these operations you may find it
225 * easier to create a new <code>Graphics</code> from the passed in
226 * <code>Graphics</code> and manipulate it. Further, if you do not
227 * invoker super's implementation you must honor the opaque property,
228 * that is
229 * if this component is opaque, you must completely fill in the background
230 * in a non-opaque color. If you do not honor the opaque property you
231 * will likely see visual artifacts.
232 * <p>
233 * The passed in <code>Graphics</code> object might
234 * have a transform other than the identify transform
235 * installed on it. In this case, you might get
236 * unexpected results if you cumulatively apply
237 * another transform.
238 *
239 * @param g the <code>Graphics</code> object to protect
240 * @see #paint(Graphics)
241 * @see javax.swing.plaf.ComponentUI
242 */
243 protected void paintComponent(Graphics g)
244 {
245 if (backgroundPainter != null) {
246 Graphics2D g2 = (Graphics2D)g.create();
247
248 try {
249 Insets ins = this.getInsets();
250 g2.translate(ins.left, ins.top);
251 backgroundPainter.paint(g2, this, this.getWidth() - ins.left
252 - ins.right, this.getHeight() - ins.top - ins.bottom);
253 } finally {
254 g2.dispose();
255 }
256 } else {
257 super.paintComponent(g);
258 }
259 }
260
261 /**
262 * Specifies a Painter to use to paint the background of this JXPanel.
263 * If <code>p</code> is not null, then setOpaque(false) will be called
264 * as a side effect. A component should not be opaque if painters are
265 * being used, because Painters may paint transparent pixels or not
266 * paint certain pixels, such as around the border insets.
267 */
268 public void setBackgroundPainter(Painter p)
269 {
270 Painter old = getBackgroundPainter();
271 this.backgroundPainter = p;
272
273 if (p != null) {
274 setOpaque(false);
275 }
276
277 firePropertyChange("backgroundPainter", old, getBackgroundPainter());
278 repaint();
279 }
280
281 public Painter getBackgroundPainter() {
282 return backgroundPainter;
283 }
284 /**
285 * Uses the DividerPainter (if any) to paint each Divider that
286 * overlaps the clip Rectangle. This is done after the call to
287 * <code>super.paintChildren()</code> so that Dividers can be
288 * rendered "on top of" the children.
289 * <p>
290 * {@inheritDoc}
291 */
292 protected void paintChildren(Graphics g) {
293 super.paintChildren(g);
294 DividerPainter dp = getDividerPainter();
295 Rectangle clipR = g.getClipBounds();
296 if ((dp != null) && (clipR != null)) {
297 MultiSplitLayout msl = getMultiSplitLayout();
298 if ( msl.hasModel()) {
299 for(Divider divider : msl.dividersThatOverlap(clipR)) {
300 Rectangle bounds = divider.getBounds();
301 Graphics cg = g.create( bounds.x, bounds.y, bounds.width, bounds.height );
302 try {
303 dp.paint((Graphics2D)cg, divider, bounds.width, bounds.height );
304 } finally {
305 cg.dispose();
306 }
307 }
308 }
309 }
310 }
311
312 private boolean dragUnderway = false;
313 private MultiSplitLayout.Divider dragDivider = null;
314 private Rectangle initialDividerBounds = null;
315 private boolean oldFloatingDividers = true;
316 private int dragOffsetX = 0;
317 private int dragOffsetY = 0;
318 private int dragMin = -1;
319 private int dragMax = -1;
320
321 private void startDrag(int mx, int my) {
322 requestFocusInWindow();
323 MultiSplitLayout msl = getMultiSplitLayout();
324 MultiSplitLayout.Divider divider = msl.dividerAt(mx, my);
325 if (divider != null) {
326 MultiSplitLayout.Node prevNode = divider.previousSibling();
327 MultiSplitLayout.Node nextNode = divider.nextSibling();
328 if ((prevNode == null) || (nextNode == null)) {
329 dragUnderway = false;
330 }
331 else {
332 initialDividerBounds = divider.getBounds();
333 dragOffsetX = mx - initialDividerBounds.x;
334 dragOffsetY = my - initialDividerBounds.y;
335 dragDivider = divider;
336
337 Rectangle prevNodeBounds = prevNode.getBounds();
338 Rectangle nextNodeBounds = nextNode.getBounds();
339 if (dragDivider.isVertical()) {
340 dragMin = prevNodeBounds.x;
341 dragMax = nextNodeBounds.x + nextNodeBounds.width;
342 dragMax -= dragDivider.getBounds().width;
343 if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT )
344 dragMax -= msl.getUserMinSize();
345 }
346 else {
347 dragMin = prevNodeBounds.y;
348 dragMax = nextNodeBounds.y + nextNodeBounds.height;
349 dragMax -= dragDivider.getBounds().height;
350 if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT )
351 dragMax -= msl.getUserMinSize();
352 }
353
354 if ( msl.getLayoutMode() == MultiSplitLayout.USER_MIN_SIZE_LAYOUT ) {
355 dragMin = dragMin + msl.getUserMinSize();
356 }
357 else {
358 if (dragDivider.isVertical()) {
359 dragMin = Math.max( dragMin, dragMin + getMinNodeSize(msl,prevNode).width );
360 dragMax = Math.min( dragMax, dragMax - getMinNodeSize(msl,nextNode).width );
361
362 Dimension maxDim = getMaxNodeSize(msl,prevNode);
363 if ( maxDim != null )
364 dragMax = Math.min( dragMax, prevNodeBounds.x + maxDim.width );
365 }
366 else {
367 dragMin = Math.max( dragMin, dragMin + getMinNodeSize(msl,prevNode).height );
368 dragMax = Math.min( dragMax, dragMax - getMinNodeSize(msl,nextNode).height );
369
370 Dimension maxDim = getMaxNodeSize(msl,prevNode);
371 if ( maxDim != null )
372 dragMax = Math.min( dragMax, prevNodeBounds.y + maxDim.height );
373 }
374 }
375
376 oldFloatingDividers = getMultiSplitLayout().getFloatingDividers();
377 getMultiSplitLayout().setFloatingDividers(false);
378 dragUnderway = true;
379 }
380 }
381 else {
382 dragUnderway = false;
383 }
384 }
385
386 /**
387 * Set the maximum node size. This method can be overridden to limit the
388 * size of a node during a drag operation on a divider. When implementing
389 * this method in a subclass the node instance should be checked, for
390 * example:
391 * <code>
392 * class MyMultiSplitPane extends JXMultiSplitPane
393 * {
394 * protected Dimension getMaxNodeSize( MultiSplitLayout msl, Node n )
395 * {
396 * if (( n instanceof Leaf ) && ((Leaf)n).getName().equals( "top" ))
397 * return msl.maximumNodeSize( n );
398 * return null;
399 * }
400 * }
401 * </code>
402 * @param msl the MultiSplitLayout used by this pane
403 * @param n the node being resized
404 * @return the maximum size or null (by default) to ignore the maximum size.
405 */
406 protected Dimension getMaxNodeSize( MultiSplitLayout msl, Node n ) {
407 return null;
408 }
409
410 /**
411 * Set the minimum node size. This method can be overridden to limit the
412 * size of a node during a drag operation on a divider.
413 * @param msl the MultiSplitLayout used by this pane
414 * @param n the node being resized
415 * @return the maximum size or null (by default) to ignore the maximum size.
416 */
417 protected Dimension getMinNodeSize( MultiSplitLayout msl, Node n ) {
418 return msl.minimumNodeSize(n);
419 }
420
421 private void repaintDragLimits() {
422 Rectangle damageR = dragDivider.getBounds();
423 if (dragDivider.isVertical()) {
424 damageR.x = dragMin;
425 damageR.width = dragMax - dragMin;
426 }
427 else {
428 damageR.y = dragMin;
429 damageR.height = dragMax - dragMin;
430 }
431 repaint(damageR);
432 }
433
434 private void updateDrag(int mx, int my) {
435 if (!dragUnderway) {
436 return;
437 }
438 Rectangle oldBounds = dragDivider.getBounds();
439 Rectangle bounds = new Rectangle(oldBounds);
440 if (dragDivider.isVertical()) {
441 bounds.x = mx - dragOffsetX;
442 bounds.x = Math.max(bounds.x, dragMin );
443 bounds.x = Math.min(bounds.x, dragMax);
444 }
445 else {
446 bounds.y = my - dragOffsetY;
447 bounds.y = Math.max(bounds.y, dragMin );
448 bounds.y = Math.min(bounds.y, dragMax);
449 }
450 dragDivider.setBounds(bounds);
451 if (isContinuousLayout()) {
452 revalidate();
453 repaintDragLimits();
454 }
455 else {
456 repaint(oldBounds.union(bounds));
457 }
458 }
459
460 private void clearDragState() {
461 dragDivider = null;
462 initialDividerBounds = null;
463 oldFloatingDividers = true;
464 dragOffsetX = dragOffsetY = 0;
465 dragMin = dragMax = -1;
466 dragUnderway = false;
467 }
468
469 private void finishDrag(int x, int y) {
470 if (dragUnderway) {
471 clearDragState();
472 if (!isContinuousLayout()) {
473 revalidate();
474 repaint();
475 }
476 }
477 setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
478 }
479
480 private void cancelDrag() {
481 if (dragUnderway) {
482 dragDivider.setBounds(initialDividerBounds);
483 getMultiSplitLayout().setFloatingDividers(oldFloatingDividers);
484 setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
485 repaint();
486 revalidate();
487 clearDragState();
488 }
489 }
490
491 private void updateCursor(int x, int y, boolean show) {
492 if (dragUnderway) {
493 return;
494 }
495 int cursorID = Cursor.DEFAULT_CURSOR;
496 if (show) {
497 MultiSplitLayout.Divider divider = getMultiSplitLayout().dividerAt(x, y);
498 if (divider != null) {
499 cursorID = (divider.isVertical()) ?
500 Cursor.E_RESIZE_CURSOR :
501 Cursor.N_RESIZE_CURSOR;
502 }
503 }
504 setCursor(Cursor.getPredefinedCursor(cursorID));
505 }
506
507
508 private class InputHandler extends MouseInputAdapter implements KeyListener {
509
510 public void mouseEntered(MouseEvent e) {
511 updateCursor(e.getX(), e.getY(), true);
512 }
513
514 public void mouseMoved(MouseEvent e) {
515 updateCursor(e.getX(), e.getY(), true);
516 }
517
518 public void mouseExited(MouseEvent e) {
519 updateCursor(e.getX(), e.getY(), false);
520 }
521
522 public void mousePressed(MouseEvent e) {
523 startDrag(e.getX(), e.getY());
524 }
525 public void mouseReleased(MouseEvent e) {
526 finishDrag(e.getX(), e.getY());
527 }
528 public void mouseDragged(MouseEvent e) {
529 updateDrag(e.getX(), e.getY());
530 }
531 public void keyPressed(KeyEvent e) {
532 if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
533 cancelDrag();
534 }
535 }
536 public void keyReleased(KeyEvent e) { }
537
538 public void keyTyped(KeyEvent e) { }
539 }
540
541 public AccessibleContext getAccessibleContext() {
542 if( accessibleContext == null ) {
543 accessibleContext = new AccessibleMultiSplitPane();
544 }
545 return accessibleContext;
546 }
547
548 protected class AccessibleMultiSplitPane extends AccessibleJPanel {
549 public AccessibleRole getAccessibleRole() {
550 return AccessibleRole.SPLIT_PANE;
551 }
552 }
553 }