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.api.editor.fold;
043:
044: import javax.swing.event.DocumentEvent;
045: import javax.swing.text.BadLocationException;
046: import javax.swing.text.Document;
047: import javax.swing.text.Position;
048: import org.netbeans.modules.editor.fold.FoldOperationImpl;
049: import org.netbeans.modules.editor.fold.FoldChildren;
050: import org.netbeans.modules.editor.fold.FoldUtilitiesImpl;
051: import org.openide.ErrorManager;
052:
053: /**
054: * Fold is a building block of the code folding tree-based hierarchy.
055: * <br>
056: * Folds cannot overlap but they can be nested arbitrarily.
057: * <br>
058: * It's possible to determine the fold's type, description
059: * and whether it is collapsed or expanded
060: * and explore the nested (children) folds.
061: * <br>
062: * There are various useful utility methods for folds in the {@link FoldUtilities}.
063: *
064: * <p>
065: * There is one <i>root fold</i> at the top of the code folding hierarchy.
066: * <br>
067: * The root fold is special uncollapsable fold covering the whole document.
068: * <br>
069: * It can be obtained by using {@link FoldHierarchy#getRootFold()}.
070: * <br>
071: * The regular top-level folds are children of the root fold.
072: *
073: * @author Miloslav Metelka
074: * @version 1.00
075: */
076:
077: public final class Fold {
078:
079: private static final Fold[] EMPTY_FOLD_ARRAY = new Fold[0];
080:
081: private static final String DEFAULT_DESCRIPTION = "..."; // NOI18N
082:
083: private final FoldOperationImpl operation;
084:
085: private final FoldType type;
086:
087: private boolean collapsed;
088:
089: private String description;
090:
091: private Fold parent;
092:
093: private FoldChildren children;
094:
095: private int rawIndex;
096:
097: private int startGuardedLength;
098: private int endGuardedLength;
099:
100: private Position startPos;
101: private Position endPos;
102:
103: private Position guardedEndPos;
104: private Position guardedStartPos;
105:
106: private Object extraInfo;
107:
108: Fold(FoldOperationImpl operation, FoldType type,
109: String description, boolean collapsed, Document doc,
110: int startOffset, int endOffset, int startGuardedLength,
111: int endGuardedLength, Object extraInfo)
112: throws BadLocationException {
113:
114: if (startGuardedLength < 0) {
115: throw new IllegalArgumentException("startGuardedLength=" // NOI18N
116: + startGuardedLength + " < 0"); // NOI18N
117: }
118: if (endGuardedLength < 0) {
119: throw new IllegalArgumentException("endGuardedLength=" // NOI18N
120: + endGuardedLength + " < 0"); // NOI18N
121: }
122: if (startOffset == endOffset) {
123: throw new IllegalArgumentException(
124: "startOffset == endOffset == " // NOI18N
125: + startOffset);
126: }
127: if ((endOffset - startOffset) < (startGuardedLength + endGuardedLength)) {
128: throw new IllegalArgumentException("(endOffset="
129: + endOffset // NOI18N
130: + " - startOffset=" + startOffset + ") < " // NOI18N
131: + "(startGuardedLength=" + startGuardedLength // NOI18N
132: + " + endGuardedLength=" + endGuardedLength + ")" // NOI18N
133: ); // NOI18N
134: }
135:
136: this .operation = operation;
137: this .type = type;
138:
139: this .collapsed = collapsed;
140: this .description = description;
141:
142: this .startPos = doc.createPosition(startOffset);
143: this .endPos = doc.createPosition(endOffset);
144:
145: this .startGuardedLength = startGuardedLength;
146: this .endGuardedLength = endGuardedLength;
147:
148: this .extraInfo = extraInfo;
149:
150: // Must assign guarded areas positions
151: // Even if the particular guarded area is zero the particular inner area
152: // has at least 1 character to detect changes leading
153: // to automatic forced expanding of the fold.
154: updateGuardedStartPos(doc, startOffset);
155: updateGuardedEndPos(doc, endOffset);
156:
157: }
158:
159: /**
160: * Get type of this fold.
161: *
162: * @return non-null type identification of this fold.
163: */
164: public FoldType getType() {
165: return type;
166: }
167:
168: /**
169: * Get parent fold of this fold.
170: *
171: * @return parent fold of this fold or null if this is root fold or if this
172: * fold was removed from the code folding hierarchy.
173: * <br>
174: * {@link FoldUtilities#isRootFold(Fold)} can be used to check
175: * whether this is root fold.
176: */
177: public Fold getParent() {
178: return parent;
179: }
180:
181: void setParent(Fold parent) {
182: if (isRootFold()) {
183: throw new IllegalArgumentException(
184: "Cannot set parent on root"); // NOI18N
185: } else {
186: this .parent = parent;
187: }
188: }
189:
190: /**
191: * Get the code folding hierarchy for which this fold was created.
192: *
193: * @return non-null code folding hierarchy for which this fold was constructed.
194: */
195: public FoldHierarchy getHierarchy() {
196: return operation.getHierarchy();
197: }
198:
199: FoldOperationImpl getOperation() {
200: return operation;
201: }
202:
203: /**
204: * Check whether this fold is currently a part of the hierarchy.
205: * <br>
206: * The fold may be temporarily removed from the hierarchy because
207: * it became blocked by another fold. Once the blocking fold gets
208: * removed the original fold becomes a part of the hierarchy again.
209: *
210: * @return true if the fold is actively a part of the hierarchy.
211: */
212: /* public boolean isHierarchyPart() {
213: return (getParent() != null) || isRootFold();
214: }
215: */
216:
217: boolean isRootFold() {
218: return (operation.getManager() == null);
219: }
220:
221: /**
222: * Get an absolute starting offset of this fold in the associated document.
223: * <br>
224: * The starting offset is expected to track possible changes in the underlying
225: * document (i.e. it's maintained
226: * in {@link javax.swing.text.Position}-like form).
227: *
228: * @return >=0 absolute starting offset of this fold in the document.
229: */
230: public int getStartOffset() {
231: return (isRootFold()) ? 0 : startPos.getOffset();
232: }
233:
234: void setStartOffset(Document doc, int startOffset)
235: throws BadLocationException {
236: if (isRootFold()) {
237: throw new IllegalStateException(
238: "Cannot set endOffset of root fold"); // NOI18N
239: } else {
240: this .startPos = doc.createPosition(startOffset);
241: updateGuardedStartPos(doc, startOffset);
242: }
243: }
244:
245: /**
246: * Get an absolute ending offset of this fold in the associated document.
247: * <br>
248: * The ending offset is expected to track possible changes in the underlying
249: * document (i.e. it's maintained
250: * in {@link javax.swing.text.Position}-like form).
251: *
252: * @return <code>>=getStartOffset()</code>
253: * offset of the first character in the document that is not part
254: * of this fold.
255: */
256: public int getEndOffset() {
257: return isRootFold() ? operation.getHierarchy().getComponent()
258: .getDocument().getLength() : endPos.getOffset();
259: }
260:
261: void setEndOffset(Document doc, int endOffset)
262: throws BadLocationException {
263: if (isRootFold()) {
264: throw new IllegalStateException(
265: "Cannot set endOffset of root fold"); // NOI18N
266: } else {
267: this .endPos = doc.createPosition(endOffset);
268: updateGuardedEndPos(doc, endOffset);
269: }
270: }
271:
272: /**
273: * Return whether this fold is collapsed or expanded.
274: * <br>
275: * To collapse fold {@link FoldHierarchy#collapse(Fold)}
276: * can be used.
277: *
278: * @return true if this fold is collapsed or false if it's expanded.
279: */
280: public boolean isCollapsed() {
281: return collapsed;
282: }
283:
284: void setCollapsed(boolean collapsed) {
285: if (isRootFold()) {
286: throw new IllegalStateException(
287: "Cannot set collapsed flag on root fold."); // NOI18N
288: }
289: this .collapsed = collapsed;
290: }
291:
292: /**
293: * Get text description that should be displayed when the fold
294: * is collapsed instead of the text contained in the fold.
295: * <br>
296: * If there is no specific description the "..." is returned.
297: *
298: * @return non-null description of the fold.
299: */
300: public String getDescription() {
301: return (description != null) ? description
302: : DEFAULT_DESCRIPTION;
303: }
304:
305: void setDescription(String description) {
306: this .description = description;
307: }
308:
309: /**
310: * Get total count of child folds contained in this fold.
311: *
312: * @return count of child folds contained in this fold.
313: * Zero means there are no children folds under this fold.
314: */
315: public int getFoldCount() {
316: return (children != null) ? children.getFoldCount() : 0;
317: }
318:
319: /**
320: * Get child fold of this fold at the given index.
321: *
322: * @param index >=0 && <{@link #getFoldCount()}
323: * index of child of this fold.
324: */
325: public Fold getFold(int index) {
326: if (children != null) {
327: return children.getFold(index);
328: } else { // no children exist
329: throw new IndexOutOfBoundsException("index=" + index // NOI18N
330: + " but no children exist."); // NOI18N
331: }
332: }
333:
334: Fold[] foldsToArray(int index, int count) {
335: if (children != null) {
336: return children.foldsToArray(index, count);
337: } else { // no children
338: if (count == 0) {
339: return EMPTY_FOLD_ARRAY;
340: } else { // invalid count
341: throw new IndexOutOfBoundsException(
342: "No children but count=" // NOI18N
343: + count);
344: }
345: }
346: }
347:
348: /**
349: * Remove the given folds and insert them as children
350: * of the given fold which will be put to their place.
351: *
352: * @param index index at which the starts the area of child folds to wrap.
353: * @param length number of child folds to wrap.
354: * @param fold fold to insert at place of children. The removed children
355: * become children of the fold.
356: */
357: void extractToChildren(int index, int length, Fold fold) {
358: if (fold.getFoldCount() != 0 || fold.getParent() != null) {
359: throw new IllegalStateException();
360: }
361: if (length != 0) { // create FoldChildren instance for the extracted folds
362: fold.setChildren(children.extractToChildren(index, length,
363: fold));
364: } else { // no children to extract -> insert the single child
365: if (children == null) {
366: children = new FoldChildren(this );
367: }
368: children.insert(index, fold); // insert the single child fold
369: }
370: }
371:
372: /**
373: * Remove the fold at the given index
374: * and put its children at its place.
375: *
376: * @param index index at which the child should be removed
377: * @return the removed child at the index.
378: */
379: Fold replaceByChildren(int index) {
380: Fold fold = getFold(index);
381: FoldChildren foldChildren = fold.getChildren();
382: fold.setChildren(null);
383: children.replaceByChildren(index, foldChildren);
384: return fold;
385: }
386:
387: private FoldChildren getChildren() {
388: return children;
389: }
390:
391: private void setChildren(FoldChildren children) {
392: this .children = children;
393: }
394:
395: Object getExtraInfo() {
396: return extraInfo;
397: }
398:
399: /**
400: * Get index of the given child fold in this fold.
401: * <br>
402: * The method has constant-time performance.
403: *
404: * @param child non-null child fold of this fold but in general
405: * it can be any non-null fold (see return value).
406: * @return >=0 index of the child fold in this fold
407: * or -1 if the given child fold is not a child of this fold.
408: */
409: public int getFoldIndex(Fold child) {
410: return (children != null) ? children.getFoldIndex(child) : -1;
411: }
412:
413: private void updateGuardedStartPos(Document doc, int startOffset)
414: throws BadLocationException {
415: if (!isRootFold()) {
416: int guardedStartOffset = isZeroStartGuardedLength() ? startOffset + 1
417: : startOffset + startGuardedLength;
418: this .guardedStartPos = doc
419: .createPosition(guardedStartOffset);
420: }
421: }
422:
423: private void updateGuardedEndPos(Document doc, int endOffset)
424: throws BadLocationException {
425: if (!isRootFold()) {
426: int guardedEndOffset = isZeroEndGuardedLength() ? endOffset - 1
427: : endOffset - endGuardedLength;
428: this .guardedEndPos = doc.createPosition(guardedEndOffset);
429: }
430: }
431:
432: private boolean isZeroStartGuardedLength() {
433: return (startGuardedLength == 0);
434: }
435:
436: private boolean isZeroEndGuardedLength() {
437: return (endGuardedLength == 0);
438: }
439:
440: private int getGuardedStartOffset() {
441: return isRootFold() ? getStartOffset() : guardedStartPos
442: .getOffset();
443: }
444:
445: private int getGuardedEndOffset() {
446: return isRootFold() ? getEndOffset() : guardedEndPos
447: .getOffset();
448: }
449:
450: void insertUpdate(DocumentEvent evt) {
451: if (evt.getOffset() + evt.getLength() == getGuardedStartOffset()) {
452: // inserted right at the end of the guarded area
453: try {
454: updateGuardedStartPos(evt.getDocument(),
455: getStartOffset());
456: } catch (BadLocationException e) {
457: ErrorManager.getDefault().notify(e);
458: }
459: }
460: }
461:
462: void removeUpdate(DocumentEvent evt) {
463: try {
464: if (getStartOffset() == getGuardedStartOffset()) {
465: updateGuardedStartPos(evt.getDocument(),
466: getStartOffset());
467: }
468: if (getEndOffset() == getGuardedEndOffset()) {
469: updateGuardedEndPos(evt.getDocument(), getEndOffset());
470: }
471: } catch (BadLocationException e) {
472: ErrorManager.getDefault().notify(e);
473: }
474: }
475:
476: /**
477: * Return true if the starting guarded area is damaged by a document modification.
478: */
479: boolean isStartDamaged() {
480: return (!isZeroStartGuardedLength() // no additional check if zero guarded length
481: && (getInnerStartOffset() - getStartOffset() != startGuardedLength));
482: }
483:
484: /**
485: * Return true if the ending guarded area is damaged by a document modification.
486: */
487: boolean isEndDamaged() {
488: return (!isZeroEndGuardedLength() // no additional check if zero guarded length
489: && (getEndOffset() - getInnerEndOffset() != endGuardedLength));
490: }
491:
492: boolean isExpandNecessary() {
493: // Only operate in case when isZero*() methods return true
494: // because if not the fold would already be marked as damaged
495: // and removed (isDamaged*() gets asked prior to this one).
496: return (isZeroStartGuardedLength() && (getStartOffset() == getGuardedStartOffset()))
497: || (isZeroEndGuardedLength() && (getEndOffset() == getGuardedEndOffset()));
498: }
499:
500: /**
501: * Get the position where the inner part of the fold starts
502: * (and the initial guarded area ends).
503: */
504: private int getInnerStartOffset() {
505: return isZeroStartGuardedLength() ? getStartOffset()
506: : getGuardedStartOffset();
507: }
508:
509: /**
510: * Get the position where the inner part of the fold ends
511: * (and the ending guarded area starts).
512: */
513: private int getInnerEndOffset() {
514: return isZeroEndGuardedLength() ? getEndOffset()
515: : getGuardedEndOffset();
516: }
517:
518: /**
519: * Get the raw index of this fold in the parent.
520: * <br>
521: * The SPI clients should never call this method.
522: */
523: int getRawIndex() {
524: return rawIndex;
525: }
526:
527: /**
528: * Set the raw index of this fold in the parent.
529: * <br>
530: * The SPI clients should never call this method.
531: */
532: void setRawIndex(int rawIndex) {
533: this .rawIndex = rawIndex;
534: }
535:
536: /**
537: * Update the raw index of this fold in the parent by a given delta.
538: * <br>
539: * The SPI clients should never call this method.
540: */
541: void updateRawIndex(int rawIndexDelta) {
542: this .rawIndex += rawIndexDelta;
543: }
544:
545: public String toString() {
546: return FoldUtilitiesImpl.foldToString(this ) + ", ["
547: + getInnerStartOffset() // NOI18N
548: + ", " + getInnerEndOffset() + "] {" // NOI18N
549: + getGuardedStartOffset() + ", " // NOI18N
550: + getGuardedEndOffset() + '}'; // NOI18N
551: }
552:
553: }
|