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