001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.core.windows.view.ui;
043:
044: import java.awt.Component;
045: import java.awt.Container;
046: import java.awt.Cursor;
047: import java.awt.Dimension;
048: import java.awt.Graphics;
049: import java.awt.LayoutManager;
050: import java.awt.Point;
051: import java.awt.event.MouseEvent;
052: import java.awt.event.MouseListener;
053: import java.awt.event.MouseMotionListener;
054: import java.util.ArrayList;
055: import java.util.Iterator;
056: import java.util.List;
057: import javax.accessibility.Accessible;
058: import javax.accessibility.AccessibleContext;
059: import javax.accessibility.AccessibleRole;
060: import javax.accessibility.AccessibleState;
061: import javax.accessibility.AccessibleStateSet;
062: import javax.swing.JPanel;
063: import javax.swing.JSplitPane;
064: import javax.swing.UIManager;
065: import org.netbeans.core.windows.view.ViewElement;
066:
067: /**
068: * A split pane that can display two or more resizeable components separated
069: * by draggable split bars. The child components can be arranged in a row
070: * (horizontal orientation) or in a column (vertical orientation).
071: *
072: * @author Stanislav Aubrecht
073: */
074: public class MultiSplitPane extends JPanel implements
075: MouseMotionListener, MouseListener {
076:
077: //the divider that user is currently dragging with a mouse
078: private MultiSplitDivider draggingDivider;
079: //a list of draggable split dividers
080: private ArrayList<MultiSplitDivider> dividers = new ArrayList<MultiSplitDivider>();
081: //true if the list of children has been updated
082: private boolean dirty = true;
083: //a list of children cells (component wrappers)
084: private ArrayList<MultiSplitCell> cells = new ArrayList<MultiSplitCell>();
085: //split orientation JSplitPane.HORIZONTAL_SPLIT or JSplitPane.VERTICAL_SPLIT
086: private int orientation;
087: //the width or height of the divider bar
088: private int dividerSize;
089:
090: private boolean userMovedSplit = false;
091:
092: public MultiSplitPane() {
093: setLayout(new MultiSplitLayout());
094: addMouseMotionListener(this );
095: addMouseListener(this );
096:
097: //get default divider size from SplitPane's UI
098: dividerSize = UIManager.getInt("SplitPane.dividerSize"); //NOI18N
099: if (0 == dividerSize)
100: dividerSize = 7;
101: }
102:
103: /**
104: * Set new list of components to be displayed in the split and also their
105: * resize weights and initial split weights.
106: *
107: * @param orientation Use JSplitPane.HORIZONTAL_SPLIT for horizontal orientation
108: * (children components are arranged in a single row) or JSplitPane.VERTICAL_SPLIT
109: * for vertical orientation (children components are arranged in a single column).
110: * @param childrenComponents ViewElements to be displayed in the split pane.
111: * @param splitWeights Initial split positions, i.e. what portion of the split window
112: * the child components initially require.
113: */
114: public void setChildren(int orientation,
115: ViewElement[] childrenViews, double[] splitWeights) {
116:
117: assert childrenViews.length == splitWeights.length;
118:
119: this .orientation = orientation;
120:
121: //list of components currently displayed in the split
122: List<Component> currentComponents = collectComponents();
123:
124: cells.clear();
125: for (int i = 0; i < childrenViews.length; i++) {
126: cells.add(new MultiSplitCell(childrenViews[i],
127: splitWeights[i], isHorizontalSplit()));
128: }
129: List<Component> updatedComponents = collectComponents();
130:
131: ArrayList<Component> removed = new ArrayList<Component>(
132: currentComponents);
133: removed.removeAll(updatedComponents); //componets that were removed from the split
134: ArrayList<Component> added = new ArrayList<Component>(
135: updatedComponents);
136: added.removeAll(currentComponents); //components that were added to the split
137:
138: for (Component c : removed) {
139: remove(c);
140: }
141:
142: for (Component c : added) {
143: add(c);
144: }
145:
146: dirty = true;
147: }
148:
149: int getCellCount() {
150: return cells.size();
151: }
152:
153: MultiSplitCell cellAt(int index) {
154: assert index >= 0;
155: assert index < cells.size();
156: return (MultiSplitCell) cells.get(index);
157: }
158:
159: /**
160: * Remove child component at the given position from the split.
161: */
162: public void removeViewElementAt(int index) {
163: if (index < 0 || index >= cells.size())
164: return;
165: MultiSplitCell cellToRemove = (MultiSplitCell) cells
166: .remove(index);
167: remove(cellToRemove.getComponent());
168: dirty = true;
169: }
170:
171: public int getOrientation() {
172: return orientation;
173: }
174:
175: public boolean isVerticalSplit() {
176: return orientation == JSplitPane.VERTICAL_SPLIT;
177: }
178:
179: public boolean isHorizontalSplit() {
180: return orientation == JSplitPane.HORIZONTAL_SPLIT;
181: }
182:
183: private List<Component> collectComponents() {
184: ArrayList<Component> res = new ArrayList<Component>(
185: getCellCount());
186: for (int i = 0; i < getCellCount(); i++) {
187: MultiSplitCell cell = cellAt(i);
188: Component c = cell.getComponent();
189: assert null != c;
190: res.add(c);
191: }
192: return res;
193: }
194:
195: /**
196: * Calculate split weights for all children components according to split's current dimensions.
197: */
198: public void calculateSplitWeights(List<ViewElement> visibleViews,
199: List<Double> splitWeights) {
200: double size = isHorizontalSplit() ? getSize().width
201: : getSize().height;
202: if (size <= 0.0)
203: return;
204: for (int i = 0; i < getCellCount(); i++) {
205: MultiSplitCell cell = cellAt(i);
206: double weight = cell.getSize() / size;
207: splitWeights.add(Double.valueOf(weight));
208: visibleViews.add(cell.getViewElement());
209: }
210: }
211:
212: public int getDividerSize() {
213: return dividerSize;
214: }
215:
216: public void setDividerSize(int newDividerSize) {
217: dirty |= newDividerSize != dividerSize;
218: this .dividerSize = newDividerSize;
219: }
220:
221: public Dimension getMinimumSize() {
222: //the minimum size is a sum of minimum sizes of all children components
223: Dimension d = new Dimension();
224: for (int i = 0; i < getCellCount(); i++) {
225: MultiSplitCell cell = cellAt(i);
226: int size = cell.getMinimumSize();
227: Dimension minDim = cell.getComponent().getMinimumSize();
228: if (isHorizontalSplit()) {
229: d.width += size;
230: if (minDim.height > d.height)
231: d.height = minDim.height;
232: } else {
233: d.height += size;
234: if (minDim.width > d.width)
235: d.width = minDim.width;
236: }
237: }
238: //the minimum size must hold at least the size of all split bars
239: if (isHorizontalSplit()) {
240: d.width += (getCellCount() - 1) * getDividerSize();
241: } else {
242: d.height += (getCellCount() - 1) * getDividerSize();
243: }
244: return d;
245: }
246:
247: public void mouseMoved(MouseEvent e) {
248: switchCursor(e);
249: e.consume();
250: }
251:
252: public void mouseDragged(MouseEvent e) {
253: if (null == draggingDivider)
254: return;
255:
256: draggingDivider.dragTo(e.getPoint());
257: e.consume();
258: }
259:
260: public void mouseReleased(MouseEvent e) {
261: if (null == draggingDivider)
262: return;
263:
264: final Point p = new Point(e.getPoint());
265: draggingDivider.finishDraggingTo(p);
266: draggingDivider = null;
267: setCursor(Cursor.getDefaultCursor());
268: e.consume();
269: }
270:
271: public void mousePressed(MouseEvent e) {
272: MultiSplitDivider divider = dividerAtPoint(e.getPoint());
273: if (null == divider)
274: return;
275:
276: draggingDivider = divider;
277: divider.startDragging(e.getPoint());
278: e.consume();
279: }
280:
281: public void mouseExited(MouseEvent e) {
282: if (null == draggingDivider) {
283: setCursor(Cursor.getDefaultCursor());
284: }
285: e.consume();
286: }
287:
288: public void mouseEntered(MouseEvent e) {
289: }
290:
291: public void mouseClicked(MouseEvent e) {
292: }
293:
294: private void switchCursor(MouseEvent e) {
295: MultiSplitDivider divider = dividerAtPoint(e.getPoint());
296: if (null == divider) {
297: setCursor(Cursor.getDefaultCursor());
298: } else {
299: if (divider.isHorizontal()) {
300: setCursor(Cursor
301: .getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
302: } else {
303: setCursor(Cursor
304: .getPredefinedCursor(Cursor.N_RESIZE_CURSOR));
305: }
306: }
307: }
308:
309: private MultiSplitDivider dividerAtPoint(Point p) {
310: for (MultiSplitDivider d : dividers) {
311: if (d.containsPoint(p))
312: return d;
313: }
314: return null;
315: }
316:
317: public void paint(Graphics g) {
318: super .paint(g);
319: //paint split bars
320: for (MultiSplitDivider divider : dividers) {
321: divider.paint(g);
322: }
323: }
324:
325: /**
326: * Shrink/grow children components.
327: *
328: * @param newSize Split pane's new widht/height depending on split orientation.
329: */
330: private void resize(int newSize) {
331: //find out what the delta is
332: int currentSize = 0;
333: for (int i = 0; i < getCellCount(); i++) {
334: currentSize += cellAt(i).getRequiredSize();
335: }
336: int totalDividerSize = getDividerSize() * (getCellCount() - 1);
337: int newNetSize = newSize - totalDividerSize;
338: int delta = newNetSize - currentSize;
339:
340: if (delta > 0) {
341: //the child cells will grow
342:
343: grow(delta);
344:
345: } else if (delta < 0) {
346:
347: delta = shrink(delta);
348:
349: if (delta > 0) {
350: //the complete delta couldn't be distributed because of minimum sizes constraints
351: newNetSize -= delta;
352: }
353: }
354:
355: //check for rounding errors and add 'missing' pixel(s) to the last cell
356: int totalSize = 0;
357: for (int i = 0; i < getCellCount(); i++) {
358: MultiSplitCell cell = cellAt(i);
359: totalSize += cell.getRequiredSize();
360: }
361: if (totalSize < newNetSize) {
362: MultiSplitCell lastCell = cellAt(getCellCount() - 1);
363: lastCell.setRequiredSize(lastCell.getRequiredSize()
364: + (newNetSize - totalSize));
365: }
366: }
367:
368: /**
369: * Grow children cell dimensions.
370: */
371: private void grow(int delta) {
372: //children with resize weight > 0 that are not collapsed
373: List<MultiSplitCell> hungryCells = getResizeHungryCells();
374:
375: //grow some/all child windows
376: if (!hungryCells.isEmpty()) {
377: //we have children with non-zero resize weight so let them consume the whole delta
378: normalizeResizeWeights(hungryCells);
379: distributeDelta(delta, hungryCells);
380: } else {
381: //resize all children proportionally
382: ArrayList<MultiSplitCell> resizeableCells = new ArrayList<MultiSplitCell>(
383: cells);
384: normalizeResizeWeights(resizeableCells);
385: distributeDelta(delta, resizeableCells);
386: }
387: }
388:
389: /**
390: * Shrink children cell dimensions.
391: * The children cells will not shrink below their minimum sizes.
392: *
393: * @return The remaining resize delta that has not been distributed among children cells.
394: */
395: private int shrink(int negativeDelta) {
396: int delta = -negativeDelta;
397:
398: //children with resize weight > 0 that are not collapsed
399: List<MultiSplitCell> hungryCells = getResizeHungryCells();
400:
401: //first find out how much cells with non-zero resize weight can shrink
402: int resizeArea = calculateShrinkableArea(hungryCells);
403: if (resizeArea >= delta) {
404: resizeArea = delta;
405: delta = 0;
406: } else {
407: delta -= resizeArea;
408: }
409: if (resizeArea > 0) {
410: //shrink cells with non-zero resize weight
411: distributeDelta(-resizeArea, hungryCells);
412: }
413:
414: if (delta > 0) {
415: //hungry cells did not consume the complete delta,
416: //distribute the remaining delta among other resizeable cells
417: ArrayList<MultiSplitCell> resizeableCells = new ArrayList<MultiSplitCell>(
418: cells);
419:
420: resizeArea = calculateShrinkableArea(resizeableCells);
421: if (resizeArea >= delta) {
422: resizeArea = delta;
423: delta = 0;
424: } else {
425: delta -= resizeArea;
426: }
427: if (resizeArea > 0) {
428: distributeDelta(-resizeArea, resizeableCells);
429: }
430: }
431: return delta;
432: }
433:
434: /**
435: * Sum up the available resize space of given cells. The resize space is the difference
436: * between child cell's current size and child cell's minimum size.
437: * Children cells that cannot be resized are removed from the given list and
438: * resize weights of remaining cells are normalized.
439: */
440: private int calculateShrinkableArea(List<MultiSplitCell> cells) {
441: int res = 0;
442: ArrayList<MultiSplitCell> nonShrinkable = new ArrayList<MultiSplitCell>(
443: cells.size());
444: for (int i = 0; i < cells.size(); i++) {
445: MultiSplitCell c = (MultiSplitCell) cells.get(i);
446: int currentSize = c.getRequiredSize();
447: int minSize = c.getMinimumSize();
448: if (currentSize - minSize > 0) {
449: res += currentSize - minSize;
450: } else {
451: nonShrinkable.add(c);
452: }
453: }
454:
455: cells.removeAll(nonShrinkable);
456: for (MultiSplitCell c : cells) {
457: int currentSize = c.getRequiredSize();
458: int minSize = c.getMinimumSize();
459: c.setNormalizedResizeWeight(1.0 * (currentSize - minSize)
460: / res);
461: }
462:
463: return res;
464: }
465:
466: /**
467: * Distribute the given delta among given cell dimensions using their normalized weights.
468: */
469: private void distributeDelta(int delta, List<MultiSplitCell> cells) {
470: int totalDistributed = 0;
471: for (int i = 0; i < cells.size(); i++) {
472: MultiSplitCell cell = cells.get(i);
473: int cellDelta = (int) (cell.getNormalizedResizeWeight() * delta);
474: totalDistributed += cellDelta;
475: if (i == cells.size() - 1) //fix rounding errors
476: cellDelta += delta - totalDistributed;
477: cell.setRequiredSize(cell.getRequiredSize() + cellDelta);
478: }
479: }
480:
481: /**
482: * Normalize resize weights so that their sum equals to 1.
483: */
484: private void normalizeResizeWeights(List cells) {
485: if (cells.isEmpty())
486: return;
487:
488: double totalWeight = 0.0;
489: for (Iterator i = cells.iterator(); i.hasNext();) {
490: MultiSplitCell c = (MultiSplitCell) i.next();
491: totalWeight += c.getResizeWeight();
492: }
493:
494: double deltaWeight = (1.0 - totalWeight) / cells.size();
495:
496: for (Iterator i = cells.iterator(); i.hasNext();) {
497: MultiSplitCell c = (MultiSplitCell) i.next();
498: c.setNormalizedResizeWeight(c.getResizeWeight()
499: + deltaWeight);
500: }
501: }
502:
503: /**
504: * @return List of children cells with non-zero resize weight.
505: */
506: List<MultiSplitCell> getResizeHungryCells() {
507: List<MultiSplitCell> res = new ArrayList<MultiSplitCell>(cells
508: .size());
509: for (int i = 0; i < getCellCount(); i++) {
510: MultiSplitCell cell = cellAt(i);
511: if (cell.getResizeWeight() <= 0.0)
512: continue;
513: res.add(cell);
514: }
515: return res;
516: }
517:
518: /**
519: * (Re)create wrapper classes for split divider rectangles.
520: */
521: void createDividers() {
522: dividers.clear();
523: for (int i = 0; i < getCellCount() - 1; i++) {
524: MultiSplitCell first = cellAt(i);
525: MultiSplitCell second = cellAt(i + 1);
526:
527: MultiSplitDivider divider = new MultiSplitDivider(
528: MultiSplitPane.this , first, second);
529: dividers.add(divider);
530: }
531: }
532:
533: void splitterMoved() {
534: userMovedSplit = true;
535: validate();
536: }
537:
538: // *************************************************************************
539: // Accessibility
540:
541: public AccessibleContext getAccessibleContext() {
542: if (accessibleContext == null) {
543: accessibleContext = new AccessibleMultiSplitPane();
544: }
545: return accessibleContext;
546: }
547:
548: int getDividerAccessibleIndex(MultiSplitDivider divider) {
549: int res = dividers.indexOf(divider);
550: res += getAccessibleContext().getAccessibleChildrenCount()
551: - dividers.size();
552: return res;
553: }
554:
555: protected class AccessibleMultiSplitPane extends
556: AccessibleJComponent {
557: public AccessibleStateSet getAccessibleStateSet() {
558: AccessibleStateSet states = super .getAccessibleStateSet();
559: if (isHorizontalSplit()) {
560: states.add(AccessibleState.HORIZONTAL);
561: } else {
562: states.add(AccessibleState.VERTICAL);
563: }
564: return states;
565: }
566:
567: public AccessibleRole getAccessibleRole() {
568: return AccessibleRole.SPLIT_PANE;
569: }
570:
571: public Accessible getAccessibleAt(Point p) {
572: MultiSplitDivider divider = dividerAtPoint(p);
573: if (null != divider) {
574: return divider;
575: }
576: return super .getAccessibleAt(p);
577: }
578:
579: public Accessible getAccessibleChild(int i) {
580:
581: int childrenCount = super .getAccessibleChildrenCount();
582: if (i < childrenCount) {
583: return super .getAccessibleChild(i);
584: }
585: if (i - childrenCount >= dividers.size()) {
586: return null;
587: }
588:
589: MultiSplitDivider divider = dividers.get(i - childrenCount);
590: return divider;
591: }
592:
593: public int getAccessibleChildrenCount() {
594: return super .getAccessibleChildrenCount() + dividers.size();
595: }
596:
597: } // inner class AccessibleMultiSplitPane
598:
599: // *************************************************************************
600:
601: protected class MultiSplitLayout implements LayoutManager {
602:
603: public void layoutContainer(Container c) {
604: if (c != MultiSplitPane.this )
605: return;
606:
607: int newSize = isHorizontalSplit() ? getSize().width
608: : getSize().height;
609: //if the list of children has been modified then let the cells calculate
610: //their initial sizes
611: for (int i = 0; i < getCellCount(); i++) {
612: MultiSplitCell cell = cellAt(i);
613: cell.maybeResetToInitialSize(newSize);
614: }
615:
616: //calculate new sizes for children cells
617: resize(newSize);
618:
619: //set children bounds
620: layoutCells();
621:
622: if (userMovedSplit) {
623: //user dragged splitbar to a new location -> fire a property change
624: userMovedSplit = false;
625: firePropertyChange("splitPositions", null, this );
626: }
627:
628: //update the rectangles of split divider bars
629: createDividers();
630: }
631:
632: private void layoutCells() {
633: int x = 0;
634: int y = 0;
635: int width = getWidth();
636: int height = getHeight();
637: for (int i = 0; i < getCellCount(); i++) {
638: MultiSplitCell cell = cellAt(i);
639:
640: //the child component may have been removed from this container
641: //(e.g. the view has been maximalized)
642: if (cell.getComponent().getParent() != MultiSplitPane.this ) {
643: add(cell.getComponent());
644: }
645:
646: if (isHorizontalSplit()) {
647: width = cell.getRequiredSize();
648: } else {
649: height = cell.getRequiredSize();
650: }
651: cell.layout(x, y, width, height);
652:
653: if (isHorizontalSplit()) {
654: x += width;
655: if (i < getCellCount()) {
656: x += getDividerSize();
657: }
658: } else {
659: y += height;
660: if (i < getCellCount() - 1) {
661: y += getDividerSize();
662: }
663: }
664: }
665: }
666:
667: public Dimension minimumLayoutSize(Container container) {
668: return container.getSize();
669: }
670:
671: public Dimension preferredLayoutSize(Container container) {
672: return container.getSize();
673: }
674:
675: public void removeLayoutComponent(Component c) {
676: }
677:
678: public void addLayoutComponent(String string, Component c) {
679: }
680: }
681: }
|